]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - js/util.js
Merge branch '0.9.x' into 1.0.x
[quix0rs-gnu-social.git] / js / util.js
index 74eef4df170dac457a7e45f18fb47cc212fe4de1..eace1778e203362cc84980a6cc1b925abf06eaa0 100644 (file)
@@ -19,7 +19,8 @@
  * @package   StatusNet
  * @author    Sarven Capadisli <csarven@status.net>
  * @author    Evan Prodromou <evan@status.net>
- * @copyright 2009 StatusNet, Inc.
+ * @author    Brion Vibber <brion@status.net>
+ * @copyright 2009,2010 StatusNet, Inc.
  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  * @link      http://status.net/
  */
@@ -33,6 +34,14 @@ var SN = { // StatusNet
             HTTP20x30x: [200, 201, 202, 203, 204, 205, 206, 300, 301, 302, 303, 304, 305, 306, 307]
         },
 
+        /**
+         * @fixme are these worth the trouble? They seem to mostly just duplicate
+         * themselves while slightly obscuring the actual selector, so it's hard
+         * to pop over to the HTML and find something.
+         *
+         * In theory, minification could reduce them to shorter variable names,
+         * but at present that doesn't happen with yui-compressor.
+         */
         S: { // Selector
             Disabled: 'disabled',
             Warning: 'warning',
@@ -59,7 +68,25 @@ var SN = { // StatusNet
         }
     },
 
+    /**
+     * Map of localized message strings exported to script from the PHP
+     * side via Action::getScriptMessages().
+     *
+     * Retrieve them via SN.msg(); this array is an implementation detail.
+     *
+     * @access private
+     */
     messages: {},
+
+    /**
+     * Grabs a localized string that's been previously exported to us
+     * from server-side code via Action::getScriptMessages().
+     *
+     * @example alert(SN.msg('coolplugin-failed'));
+     *
+     * @param {String} key: string key name to pull from message index
+     * @return matching localized message string
+     */
     msg: function(key) {
         if (typeof SN.messages[key] == "undefined") {
             return '[' + key + ']';
@@ -69,6 +96,14 @@ var SN = { // StatusNet
     },
 
     U: { // Utils
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers on the new notice form.
+         *
+         * @param {jQuery} form: jQuery object whose first matching element is the form
+         * @access private
+         */
         FormNoticeEnhancements: function(form) {
             if (jQuery.data(form[0], 'ElementData') === undefined) {
                 MaxLength = form.find('#'+SN.C.S.NoticeTextCount).text();
@@ -85,6 +120,19 @@ var SN = { // StatusNet
                     SN.U.Counter(form);
                 });
 
+                var delayedUpdate= function(e) {
+                    // Cut and paste events fire *before* the operation,
+                    // so we need to trigger an update in a little bit.
+                    // This would be so much easier if the 'change' event
+                    // actually fired every time the value changed. :P
+                    window.setTimeout(function() {
+                        SN.U.Counter(form);
+                    }, 50);
+                };
+                // Note there's still no event for mouse-triggered 'delete'.
+                NDT.bind('cut', delayedUpdate)
+                   .bind('paste', delayedUpdate);
+
                 NDT.bind('keydown', function(e) {
                     SN.U.SubmitOnReturn(e, form);
                 });
@@ -98,6 +146,17 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * To be called from keydown event handler on the notice import form.
+         * Checks if return or enter key was pressed, and if so attempts to
+         * submit the form and cancel standard processing of the enter key.
+         *
+         * @param {Event} event
+         * @param {jQuery} el: jQuery object whose first element is the notice posting form
+         *
+         * @return {boolean} whether to cancel the event? Does this actually pass through?
+         * @access private
+         */
         SubmitOnReturn: function(event, el) {
             if (event.keyCode == 13 || event.keyCode == 10) {
                 el.submit();
@@ -110,6 +169,20 @@ var SN = { // StatusNet
             return true;
         },
 
+        /**
+         * To be called from event handlers on the notice import form.
+         * Triggers an update of the remaining-characters counter.
+         *
+         * Additional counter updates will be suppressed during the
+         * next half-second to avoid flooding the layout engine with
+         * updates, followed by another automatic check.
+         *
+         * The maximum length is pulled from data established by
+         * FormNoticeEnhancements.
+         *
+         * @param {jQuery} form: jQuery object whose first element is the notice posting form
+         * @access private
+         */
         Counter: function(form) {
             SN.C.I.FormNoticeCurrent = form;
 
@@ -143,10 +216,24 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Pull the count of characters in the current edit field.
+         * Plugins replacing the edit control may need to override this.
+         *
+         * @param {jQuery} form: jQuery object whose first element is the notice posting form
+         * @return number of chars
+         */
         CharacterCount: function(form) {
             return form.find('#'+SN.C.S.NoticeDataText).val().length;
         },
 
+        /**
+         * Called internally after the counter update blackout period expires;
+         * runs another update to make sure we didn't miss anything.
+         *
+         * @param {jQuery} form: jQuery object whose first element is the notice posting form
+         * @access private
+         */
         ClearCounterBlackout: function(form) {
             // Allow keyup events to poke the counter again
             SN.C.I.CounterBlackout = false;
@@ -154,11 +241,47 @@ var SN = { // StatusNet
             SN.U.Counter(form);
         },
 
+        /**
+         * Helper function to rewrite default HTTP form action URLs to HTTPS
+         * so we can actually fetch them when on an SSL page in ssl=sometimes
+         * mode.
+         *
+         * It would be better to output URLs that didn't hardcode protocol
+         * and hostname in the first place...
+         *
+         * @param {String} url
+         * @return string
+         */
+        RewriteAjaxAction: function(url) {
+            // Quick hack: rewrite AJAX submits to HTTPS if they'd fail otherwise.
+            if (document.location.protocol == 'https:' && url.substr(0, 5) == 'http:') {
+                return url.replace(/^http:\/\/[^:\/]+/, 'https://' + document.location.host);
+            } else {
+                return url;
+            }
+        },
+
+        /**
+         * Grabs form data and submits it asynchronously, with 'ajax=1'
+         * parameter added to the rest.
+         *
+         * If a successful response includes another form, that form
+         * will be extracted and copied in, replacing the original form.
+         * If there's no form, the first paragraph will be used.
+         *
+         * @fixme can sometimes explode confusingly if returnd data is bogus
+         * @fixme error handling is pretty vague
+         * @fixme can't submit file uploads
+         *
+         * @param {jQuery} form: jQuery object whose first element is a form
+         *
+         * @access public
+         */
         FormXHR: function(form) {
             $.ajax({
                 type: 'POST',
                 dataType: 'xml',
-                url: form.attr('action'),
+                url: SN.U.RewriteAjaxAction(form.attr('action')),
                 data: form.serialize() + '&ajax=1',
                 beforeSend: function(xhr) {
                     form
@@ -182,9 +305,62 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers for special-cased async submission of the
+         * notice-posting form, including some pre-post validation.
+         *
+         * Unlike FormXHR() this does NOT submit the form immediately!
+         * It sets up event handlers so that any method of submitting the
+         * form (click on submit button, enter, submit() etc) will trigger
+         * it properly.
+         *
+         * Also unlike FormXHR(), this system will use a hidden iframe
+         * automatically to handle file uploads via <input type="file">
+         * controls.
+         *
+         * @fixme tl;dr
+         * @fixme vast swaths of duplicate code and really long variable names clutter this function up real bad
+         * @fixme error handling is unreliable
+         * @fixme cookieValue is a global variable, but probably shouldn't be
+         * @fixme saving the location cache cookies should be split out
+         * @fixme some error messages are hardcoded english: needs i18n
+         * @fixme special-case for bookmarklet is confusing and uses a global var "self". Is this ok?
+         *
+         * @param {jQuery} form: jQuery object whose first element is a form
+         *
+         * @access public
+         */
         FormNoticeXHR: function(form) {
             SN.C.I.NoticeDataGeo = {};
             form.append('<input type="hidden" name="ajax" value="1"/>');
+
+            // Make sure we don't have a mixed HTTP/HTTPS submission...
+            form.attr('action', SN.U.RewriteAjaxAction(form.attr('action')));
+
+            /**
+             * Show a response feedback bit under the new-notice dialog.
+             *
+             * @param {String} cls: CSS class name to use ('error' or 'success')
+             * @param {String} text
+             * @access private
+             */
+            var showFeedback = function(cls, text) {
+                form.append(
+                    $('<p class="form_response"></p>')
+                        .addClass(cls)
+                        .text(text)
+                );
+            };
+
+            /**
+             * Hide the previous response feedback, if any.
+             */
+            var removeFeedback = function() {
+                form.find('.form_response').remove();
+            };
+
             form.ajaxForm({
                 dataType: 'xml',
                 timeout: '60000',
@@ -231,13 +407,15 @@ var SN = { // StatusNet
                         .find('#'+SN.C.S.NoticeActionSubmit)
                             .removeClass(SN.C.S.Disabled)
                             .removeAttr(SN.C.S.Disabled, SN.C.S.Disabled);
-                    form.find('.form_response').remove();
+                    removeFeedback();
                     if (textStatus == 'timeout') {
-                        form.append('<p class="form_response error">Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.</p>');
+                        // @fixme i18n
+                        showFeedback('error', 'Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.');
                     }
                     else {
-                        if ($('.'+SN.C.S.Error, xhr.responseXML).length > 0) {
-                            form.append(document._importNode($('.'+SN.C.S.Error, xhr.responseXML)[0], true));
+                        var response = SN.U.GetResponseXML(xhr);
+                        if ($('.'+SN.C.S.Error, response).length > 0) {
+                            form.append(document._importNode($('.'+SN.C.S.Error, response)[0], true));
                         }
                         else {
                             if (parseInt(xhr.status) === 0 || jQuery.inArray(parseInt(xhr.status), SN.C.I.HTTP20x30x) >= 0) {
@@ -247,28 +425,27 @@ var SN = { // StatusNet
                                 SN.U.FormNoticeEnhancements(form);
                             }
                             else {
-                                form.append('<p class="form_response error">(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.</p>');
+                                // @fixme i18n
+                                showFeedback('error', '(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.');
                             }
                         }
                     }
                 },
                 success: function(data, textStatus) {
-                    form.find('.form_response').remove();
-                    var result;
-                    if ($('#'+SN.C.S.Error, data).length > 0) {
-                        result = document._importNode($('p', data)[0], true);
-                        result = result.textContent || result.innerHTML;
-                        form.append('<p class="form_response error">'+result+'</p>');
+                    removeFeedback();
+                    var errorResult = $('#'+SN.C.S.Error, data);
+                    if (errorResult.length > 0) {
+                        showFeedback('error', errorResult.text());
                     }
                     else {
                         if($('body')[0].id == 'bookmarklet') {
+                            // @fixme self is not referenced anywhere?
                             self.close();
                         }
 
-                        if ($('#'+SN.C.S.CommandResult, data).length > 0) {
-                            result = document._importNode($('p', data)[0], true);
-                            result = result.textContent || result.innerHTML;
-                            form.append('<p class="form_response success">'+result+'</p>');
+                        var commandResult = $('#'+SN.C.S.CommandResult, data);
+                        if (commandResult.length > 0) {
+                            showFeedback('success', commandResult.text());
                         }
                         else {
                             // New notice post was successful. If on our timeline, show it!
@@ -297,9 +474,7 @@ var SN = { // StatusNet
                             else {
                                 // Not on a timeline that this belongs on?
                                 // Just show a success message.
-                                result = document._importNode($('title', data)[0], true);
-                                result_title = result.textContent || result.innerHTML;
-                                form.append('<p class="form_response success">'+result_title+'</p>');
+                                showFeedback('success', $('title', data).text());
                             }
                         }
                         form.resetForm();
@@ -326,12 +501,57 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Fetch an XML DOM from an XHR's response data.
+         *
+         * Works around unavailable responseXML when document.domain
+         * has been modified by Meteor or other tools, in some but not
+         * all browsers.
+         *
+         * @param {XMLHTTPRequest} xhr
+         * @return DOMDocument
+         */
+        GetResponseXML: function(xhr) {
+            try {
+                return xhr.responseXML;
+            } catch (e) {
+                return (new DOMParser()).parseFromString(xhr.responseText, "text/xml");
+            }
+        },
+
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers on all visible notice's reply buttons to
+         * tweak the new-notice form with needed variables and focus it
+         * when pushed.
+         *
+         * (This replaces the default reply button behavior to submit
+         * directly to a form which comes back with a specialized page
+         * with the form data prefilled.)
+         *
+         * @access private
+         */
         NoticeReply: function() {
             if ($('#'+SN.C.S.NoticeDataText).length > 0 && $('#content .notice_reply').length > 0) {
                 $('#content .notice').each(function() { SN.U.NoticeReplyTo($(this)); });
             }
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers on the given notice's reply button to
+         * tweak the new-notice form with needed variables and focus it
+         * when pushed.
+         *
+         * (This replaces the default reply button behavior to submit
+         * directly to a form which comes back with a specialized page
+         * with the form data prefilled.)
+         *
+         * @param {jQuery} notice: jQuery object containing one or more notices
+         * @access private
+         */
         NoticeReplyTo: function(notice) {
             notice.find('.notice_reply').live('click', function() {
                 var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname.uid');
@@ -340,6 +560,16 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Updates the new notice posting form with bits for replying to the
+         * given user. Adds replyto parameter to the form, and a "@foo" to the
+         * text area.
+         *
+         * @fixme replyto is a global variable, but probably shouldn't be
+         *
+         * @param {String} nick
+         * @param {String} id
+         */
         NoticeReplySet: function(nick,id) {
             if (nick.match(SN.C.I.PatternUsername)) {
                 var text = $('#'+SN.C.S.NoticeDataText);
@@ -357,11 +587,25 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Setup function -- DOES NOT apply immediately.
+         *
+         * Sets up event handlers for favor/disfavor forms to submit via XHR.
+         * Uses 'live' rather than 'bind', so applies to future as well as present items.
+         */
         NoticeFavor: function() {
             $('.form_favor').live('click', function() { SN.U.FormXHR($(this)); return false; });
             $('.form_disfavor').live('click', function() { SN.U.FormXHR($(this)); return false; });
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers for repeat forms to toss up a confirmation
+         * popout before submitting.
+         *
+         * Uses 'live' rather than 'bind', so applies to future as well as present items.
+         */
         NoticeRepeat: function() {
             $('.form_repeat').live('click', function(e) {
                 e.preventDefault();
@@ -371,6 +615,22 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Shows a confirmation dialog box variant of the repeat button form.
+         * This seems to use a technique where the repeat form contains
+         * _both_ a standalone button _and_ text and buttons for a dialog.
+         * The dialog will close after its copy of the form is submitted,
+         * or if you click its 'close' button.
+         *
+         * The dialog is created by duplicating the original form and changing
+         * its style; while clever, this is hard to generalize and probably
+         * duplicates a lot of unnecessary HTML output.
+         *
+         * @fixme create confirmation dialogs through a generalized interface
+         * that can be reused instead of hardcoded text and styles.
+         *
+         * @param {jQuery} form
+         */
         NoticeRepeatConfirmation: function(form) {
             var submit_i = form.find('.submit');
 
@@ -404,12 +664,28 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Goes through all notices currently displayed and sets up attachment
+         * handling if needed.
+         */
         NoticeAttachments: function() {
             $('.notice a.attachment').each(function() {
                 SN.U.NoticeWithAttachment($(this).closest('.notice'));
             });
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up special attachment link handling if needed. Currently this
+         * consists only of making the "more" button used for OStatus message
+         * cropping turn into an auto-expansion button that loads the full
+         * text from an attachment file.
+         *
+         * @param {jQuery} notice
+         */
         NoticeWithAttachment: function(notice) {
             if (notice.find('.attachment').length === 0) {
                 return;
@@ -429,6 +705,17 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Sets up event handlers for the file-attachment widget in the
+         * new notice form. When a file is selected, a box will be added
+         * below the text input showing the filename and, if supported
+         * by the browser, a thumbnail preview.
+         *
+         * This preview box will also allow removing the attachment
+         * prior to posting.
+         */
         NoticeDataAttach: function() {
             NDA = $('#'+SN.C.S.NoticeDataAttach);
             NDA.change(function() {
@@ -446,9 +733,115 @@ var SN = { // StatusNet
 
                     return false;
                 });
+                if (typeof this.files == "object") {
+                    // Some newer browsers will let us fetch the files for preview.
+                    for (var i = 0; i < this.files.length; i++) {
+                        SN.U.PreviewAttach(this.files[i]);
+                    }
+                }
             });
         },
 
+        /**
+         * For browsers with FileAPI support: make a thumbnail if possible,
+         * and append it into the attachment display widget.
+         *
+         * Known good:
+         * - Firefox 3.6.6, 4.0b7
+         * - Chrome 8.0.552.210
+         *
+         * Known ok metadata, can't get contents:
+         * - Safari 5.0.2
+         *
+         * Known fail:
+         * - Opera 10.63, 11 beta (no input.files interface)
+         *
+         * @param {File} file
+         *
+         * @todo use configured thumbnail size
+         * @todo detect pixel size?
+         * @todo should we render a thumbnail to a canvas and then use the smaller image?
+         */
+        PreviewAttach: function(file) {
+            var tooltip = file.type + ' ' + Math.round(file.size / 1024) + 'KB';
+            var preview = true;
+
+            var blobAsDataURL;
+            if (typeof window.createObjectURL != "undefined") {
+                /**
+                 * createObjectURL lets us reference the file directly from an <img>
+                 * This produces a compact URL with an opaque reference to the file,
+                 * which we can reference immediately.
+                 *
+                 * - Firefox 3.6.6: no
+                 * - Firefox 4.0b7: no
+                 * - Safari 5.0.2: no
+                 * - Chrome 8.0.552.210: works!
+                 */
+                blobAsDataURL = function(blob, callback) {
+                    callback(window.createObjectURL(blob));
+                }
+            } else if (typeof window.FileReader != "undefined") {
+                /**
+                 * FileAPI's FileReader can build a data URL from a blob's contents,
+                 * but it must read the file and build it asynchronously. This means
+                 * we'll be passing a giant data URL around, which may be inefficient.
+                 *
+                 * - Firefox 3.6.6: works!
+                 * - Firefox 4.0b7: works!
+                 * - Safari 5.0.2: no
+                 * - Chrome 8.0.552.210: works!
+                 */
+                blobAsDataURL = function(blob, callback) {
+                    var reader = new FileReader();
+                    reader.onload = function(event) {
+                        callback(reader.result);
+                    }
+                    reader.readAsDataURL(blob);
+                }
+            } else {
+                preview = false;
+            }
+
+            var imageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/svg+xml'];
+            if ($.inArray(file.type, imageTypes) == -1) {
+                // We probably don't know how to show the file.
+                preview = false;
+            }
+
+            var maxSize = 8 * 1024 * 1024;
+            if (file.size > maxSize) {
+                // Don't kill the browser trying to load some giant image.
+                preview = false;
+            }
+
+            if (preview) {
+                blobAsDataURL(file, function(url) {
+                    var img = $('<img>')
+                        .attr('title', tooltip)
+                        .attr('alt', tooltip)
+                        .attr('src', url)
+                        .attr('style', 'height: 120px');
+                    $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
+                });
+            } else {
+                var img = $('<div></div>').text(tooltip);
+                $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
+            }
+        },
+
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Initializes state for the location-lookup features in the
+         * new-notice form. Seems to set up some event handlers for
+         * triggering lookups and using the new values.
+         *
+         * @fixme tl;dr
+         * @fixme there's not good visual state update here, so users have a
+         *        hard time figuring out if it's working or fixing if it's wrong.
+         *
+         */
         NoticeLocationAttach: function() {
             var NLat = $('#'+SN.C.S.NoticeLat).val();
             var NLon = $('#'+SN.C.S.NoticeLon).val();
@@ -606,6 +999,18 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Setup function -- DOES NOT trigger actions immediately.
+         *
+         * Initializes event handlers for the "Send direct message" link on
+         * profile pages, setting it up to display a dialog box when clicked.
+         *
+         * Unlike the repeat confirmation form, this appears to fetch
+         * the form _from the original link target_, so the form itself
+         * doesn't need to be in the current document.
+         *
+         * @fixme breaks ability to open link in new window?
+         */
         NewDirectMessage: function() {
             NDM = $('.entity_send-a-message a');
             NDM.attr({'href':NDM.attr('href')+'&ajax=1'});
@@ -634,6 +1039,15 @@ var SN = { // StatusNet
             });
         },
 
+        /**
+         * Return a date object with the current local time on the
+         * given year, month, and day.
+         *
+         * @param {number} year: 4-digit year
+         * @param {number} month: 0 == January
+         * @param {number} day: 1 == 1
+         * @return {Date}
+         */
         GetFullYear: function(year, month, day) {
             var date = new Date();
             date.setFullYear(year, month, day);
@@ -641,7 +1055,22 @@ var SN = { // StatusNet
             return date;
         },
 
+        /**
+         * Some sort of object interface for storing some structured
+         * information in a cookie.
+         *
+         * Appears to be used to save the last-used login nickname?
+         * That's something that browsers usually take care of for us
+         * these days, do we really need to do it? Does anything else
+         * use this interface?
+         *
+         * @fixme what is this?
+         * @fixme should this use non-cookie local storage when available?
+         */
         StatusNetInstance: {
+            /**
+             * @fixme what is this?
+             */
             Set: function(value) {
                 var SNI = SN.U.StatusNetInstance.Get();
                 if (SNI !== null) {
@@ -657,6 +1086,9 @@ var SN = { // StatusNet
                     });
             },
 
+            /**
+             * @fixme what is this?
+             */
             Get: function() {
                 var cookieValue = $.cookie(SN.C.S.StatusNetInstance);
                 if (cookieValue !== null) {
@@ -665,6 +1097,9 @@ var SN = { // StatusNet
                 return null;
             },
 
+            /**
+             * @fixme what is this?
+             */
             Delete: function() {
                 $.cookie(SN.C.S.StatusNetInstance, null);
             }
@@ -676,6 +1111,9 @@ var SN = { // StatusNet
          *
          * @fixme this should be done in a saner way, with machine-readable
          * info about what page we're looking at.
+         *
+         * @param {DOMElement} notice: HTML chunk with formatted notice
+         * @return boolean
          */
         belongsOnTimeline: function(notice) {
             var action = $("body").attr('id');
@@ -704,6 +1142,14 @@ var SN = { // StatusNet
     },
 
     Init: {
+        /**
+         * If user is logged in, run setup code for the new notice form:
+         *
+         *  - char counter
+         *  - AJAX submission
+         *  - location events
+         *  - file upload events
+         */
         NoticeForm: function() {
             if ($('body.user_in').length > 0) {
                 SN.U.NoticeLocationAttach();
@@ -717,6 +1163,12 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Run setup code for notice timeline views items:
+         *
+         * - AJAX submission for fave/repeat/reply (if logged in)
+         * - Attachment link extras ('more' links)
+         */
         Notices: function() {
             if ($('body.user_in').length > 0) {
                 SN.U.NoticeFavor();
@@ -727,6 +1179,12 @@ var SN = { // StatusNet
             SN.U.NoticeAttachments();
         },
 
+        /**
+         * Run setup code for user & group profile page header area if logged in:
+         *
+         * - AJAX submission for sub/unsub/join/leave/nudge
+         * - AJAX form popup for direct-message
+         */
         EntityActions: function() {
             if ($('body.user_in').length > 0) {
                 $('.form_user_subscribe').live('click', function() { SN.U.FormXHR($(this)); return false; });
@@ -739,6 +1197,14 @@ var SN = { // StatusNet
             }
         },
 
+        /**
+         * Run setup code for login form:
+         *
+         * - loads saved last-used-nickname from cookie
+         * - sets event handler to save nickname to cookie on submit
+         *
+         * @fixme is this necessary? Browsers do their own form saving these days.
+         */
         Login: function() {
             if (SN.U.StatusNetInstance.Get() !== null) {
                 var nickname = SN.U.StatusNetInstance.Get().Nickname;
@@ -755,6 +1221,13 @@ var SN = { // StatusNet
     }
 };
 
+/**
+ * Run initialization functions on DOM-ready.
+ *
+ * Note that if we're waiting on other scripts to load, this won't happen
+ * until that's done. To load scripts asynchronously without delaying setup,
+ * don't start them loading until after DOM-ready time!
+ */
 $(document).ready(function(){
     if ($('.'+SN.C.S.FormNotice).length > 0) {
         SN.Init.NoticeForm();
@@ -769,270 +1242,3 @@ $(document).ready(function(){
         SN.Init.Login();
     }
 });
-
-// Formerly in xbImportNode.js
-
-/* is this stuff defined? */
-if (!document.ELEMENT_NODE) {
-       document.ELEMENT_NODE = 1;
-       document.ATTRIBUTE_NODE = 2;
-       document.TEXT_NODE = 3;
-       document.CDATA_SECTION_NODE = 4;
-       document.ENTITY_REFERENCE_NODE = 5;
-       document.ENTITY_NODE = 6;
-       document.PROCESSING_INSTRUCTION_NODE = 7;
-       document.COMMENT_NODE = 8;
-       document.DOCUMENT_NODE = 9;
-       document.DOCUMENT_TYPE_NODE = 10;
-       document.DOCUMENT_FRAGMENT_NODE = 11;
-       document.NOTATION_NODE = 12;
-}
-
-document._importNode = function(node, allChildren) {
-       /* find the node type to import */
-       switch (node.nodeType) {
-               case document.ELEMENT_NODE:
-                       /* create a new element */
-                       var newNode = document.createElement(node.nodeName);
-                       /* does the node have any attributes to add? */
-                       if (node.attributes && node.attributes.length > 0)
-                               /* add all of the attributes */
-                               for (var i = 0, il = node.attributes.length; i < il;) {
-                                       if (node.attributes[i].nodeName == 'class') {
-                                               newNode.className = node.getAttribute(node.attributes[i++].nodeName);
-                                       } else {
-                                               newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i++].nodeName));
-                                       }
-                               }
-                       /* are we going after children too, and does the node have any? */
-                       if (allChildren && node.childNodes && node.childNodes.length > 0)
-                               /* recursively get all of the child nodes */
-                               for (var i = 0, il = node.childNodes.length; i < il;)
-                                       newNode.appendChild(document._importNode(node.childNodes[i++], allChildren));
-                       return newNode;
-                       break;
-               case document.TEXT_NODE:
-               case document.CDATA_SECTION_NODE:
-               case document.COMMENT_NODE:
-                       return document.createTextNode(node.nodeValue);
-                       break;
-       }
-};
-
-// A shim to implement the W3C Geolocation API Specification using Gears or the Ajax API
-if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) { (function(){
-
-// -- BEGIN GEARS_INIT
-(function() {
-  // We are already defined. Hooray!
-  if (window.google && google.gears) {
-    return;
-  }
-
-  var factory = null;
-
-  // Firefox
-  if (typeof GearsFactory != 'undefined') {
-    factory = new GearsFactory();
-  } else {
-    // IE
-    try {
-      factory = new ActiveXObject('Gears.Factory');
-      // privateSetGlobalObject is only required and supported on WinCE.
-      if (factory.getBuildInfo().indexOf('ie_mobile') != -1) {
-        factory.privateSetGlobalObject(this);
-      }
-    } catch (e) {
-      // Safari
-      if ((typeof navigator.mimeTypes != 'undefined') && navigator.mimeTypes["application/x-googlegears"]) {
-        factory = document.createElement("object");
-        factory.style.display = "none";
-        factory.width = 0;
-        factory.height = 0;
-        factory.type = "application/x-googlegears";
-        document.documentElement.appendChild(factory);
-      }
-    }
-  }
-
-  // *Do not* define any objects if Gears is not installed. This mimics the
-  // behavior of Gears defining the objects in the future.
-  if (!factory) {
-    return;
-  }
-
-  // Now set up the objects, being careful not to overwrite anything.
-  //
-  // Note: In Internet Explorer for Windows Mobile, you can't add properties to
-  // the window object. However, global objects are automatically added as
-  // properties of the window object in all browsers.
-  if (!window.google) {
-    google = {};
-  }
-
-  if (!google.gears) {
-    google.gears = {factory: factory};
-  }
-})();
-// -- END GEARS_INIT
-
-var GearsGeoLocation = (function() {
-    // -- PRIVATE
-    var geo = google.gears.factory.create('beta.geolocation');
-
-    var wrapSuccess = function(callback, self) { // wrap it for lastPosition love
-        return function(position) {
-            callback(position);
-            self.lastPosition = position;
-        };
-    };
-
-    // -- PUBLIC
-    return {
-        shim: true,
-
-        type: "Gears",
-
-        lastPosition: null,
-
-        getCurrentPosition: function(successCallback, errorCallback, options) {
-            var self = this;
-            var sc = wrapSuccess(successCallback, self);
-            geo.getCurrentPosition(sc, errorCallback, options);
-        },
-
-        watchPosition: function(successCallback, errorCallback, options) {
-            geo.watchPosition(successCallback, errorCallback, options);
-        },
-
-        clearWatch: function(watchId) {
-            geo.clearWatch(watchId);
-        },
-
-        getPermission: function(siteName, imageUrl, extraMessage) {
-            geo.getPermission(siteName, imageUrl, extraMessage);
-        }
-
-    };
-});
-
-var AjaxGeoLocation = (function() {
-    // -- PRIVATE
-    var loading = false;
-    var loadGoogleLoader = function() {
-        if (!hasGoogleLoader() && !loading) {
-            loading = true;
-            var s = document.createElement('script');
-            s.src = (document.location.protocol == "https:"?"https://":"http://") + 'www.google.com/jsapi?callback=_google_loader_apiLoaded';
-            s.type = "text/javascript";
-            document.getElementsByTagName('body')[0].appendChild(s);
-        }
-    };
-
-    var queue = [];
-    var addLocationQueue = function(callback) {
-        queue.push(callback);
-    };
-
-    var runLocationQueue = function() {
-        if (hasGoogleLoader()) {
-            while (queue.length > 0) {
-                var call = queue.pop();
-                call();
-            }
-        }
-    };
-
-    window['_google_loader_apiLoaded'] = function() {
-        runLocationQueue();
-    };
-
-    var hasGoogleLoader = function() {
-        return (window['google'] && google['loader']);
-    };
-
-    var checkGoogleLoader = function(callback) {
-        if (hasGoogleLoader()) { return true; }
-
-        addLocationQueue(callback);
-
-        loadGoogleLoader();
-
-        return false;
-    };
-
-    loadGoogleLoader(); // start to load as soon as possible just in case
-
-    // -- PUBLIC
-    return {
-        shim: true,
-
-        type: "ClientLocation",
-
-        lastPosition: null,
-
-        getCurrentPosition: function(successCallback, errorCallback, options) {
-            var self = this;
-            if (!checkGoogleLoader(function() {
-                self.getCurrentPosition(successCallback, errorCallback, options);
-            })) { return; }
-
-            if (google.loader.ClientLocation) {
-                var cl = google.loader.ClientLocation;
-
-                var position = {
-                    coords: {
-                        latitude: cl.latitude,
-                        longitude: cl.longitude,
-                        altitude: null,
-                        accuracy: 43000, // same as Gears accuracy over wifi?
-                        altitudeAccuracy: null,
-                        heading: null,
-                        speed: null
-                    },
-                    // extra info that is outside of the bounds of the core API
-                    address: {
-                        city: cl.address.city,
-                        country: cl.address.country,
-                        country_code: cl.address.country_code,
-                        region: cl.address.region
-                    },
-                    timestamp: new Date()
-                };
-
-                successCallback(position);
-
-                this.lastPosition = position;
-            } else if (errorCallback === "function")  {
-                errorCallback({ code: 3, message: "Using the Google ClientLocation API and it is not able to calculate a location."});
-            }
-        },
-
-        watchPosition: function(successCallback, errorCallback, options) {
-            this.getCurrentPosition(successCallback, errorCallback, options);
-
-            var self = this;
-            var watchId = setInterval(function() {
-                self.getCurrentPosition(successCallback, errorCallback, options);
-            }, 10000);
-
-            return watchId;
-        },
-
-        clearWatch: function(watchId) {
-            clearInterval(watchId);
-        },
-
-        getPermission: function(siteName, imageUrl, extraMessage) {
-            // for now just say yes :)
-            return true;
-        }
-
-    };
-});
-
-// If you have Gears installed use that, else use Ajax ClientLocation
-navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation();
-
-})();
-}