X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=js%2Futil.js;h=eace1778e203362cc84980a6cc1b925abf06eaa0;hb=26baad63f2e7d5cd0f783aacdb9eb2a65a638656;hp=74eef4df170dac457a7e45f18fb47cc212fe4de1;hpb=62dfdb34a613f61e8f546b60468e1a73ee18d7e0;p=quix0rs-gnu-social.git diff --git a/js/util.js b/js/util.js index 74eef4df17..eace1778e2 100644 --- a/js/util.js +++ b/js/util.js @@ -19,7 +19,8 @@ * @package StatusNet * @author Sarven Capadisli * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @author Brion Vibber + * @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 + * 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(''); + + // 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( + $('

') + .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('

Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.

'); + // @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('

(Sorry! We had trouble sending your notice ('+xhr.status+' '+xhr.statusText+'). Please report the problem to the site administrator if this happens again.

'); + // @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('

'+result+'

'); + 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('

'+result+'

'); + 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('

'+result_title+'

'); + 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 + * 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 = $('') + .attr('title', tooltip) + .attr('alt', tooltip) + .attr('src', url) + .attr('style', 'height: 120px'); + $('#'+SN.C.S.NoticeDataAttachSelected).append(img); + }); + } else { + var img = $('
').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(); - -})(); -}