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.
$.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
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',
.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 {
var response = SN.U.GetResponseXML(xhr);
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!
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();
/**
* 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() {
});
},
+ /**
+ * 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;
}
},
+ /**
+ * 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() {
- S = '<div id="'+SN.C.S.NoticeDataAttachSelected+'" class="'+SN.C.S.Success+'"><code>'+$(this).val()+'</code> <button class="close">×</button></div>';
+ NDA.change(function(event) {
+ var filename = $(this).val();
+ if (!filename) {
+ // No file -- we've been tricked!
+ $('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ return false;
+ }
+
+ // @fixme appending filename straight in is potentially unsafe
+ S = '<div id="'+SN.C.S.NoticeDataAttachSelected+'" class="'+SN.C.S.Success+'"><code>'+filename+'</code> <button class="close">×</button></div>';
NDAS = $('#'+SN.C.S.NoticeDataAttachSelected);
if (NDAS.length > 0) {
NDAS.replaceWith(S);
});
},
+ /**
+ * Get PHP's MAX_FILE_SIZE setting for this form;
+ * used to apply client-side file size limit checks.
+ *
+ * @param {jQuery} form
+ * @return int max size in bytes; 0 or negative means no limit
+ */
+ maxFileSize: function(form) {
+ var max = $(form).find('input[name=MAX_FILE_SIZE]').attr('value');
+ if (max) {
+ return parseInt(max);
+ } else {
+ return 0;
+ }
+ },
+
/**
* For browsers with FileAPI support: make a thumbnail if possible,
* and append it into the attachment display widget.
}
},
+ /**
+ * 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();
}
},
+ /**
+ * 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'});
});
},
+ /**
+ * 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);
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) {
});
},
+ /**
+ * @fixme what is this?
+ */
Get: function() {
var cookieValue = $.cookie(SN.C.S.StatusNetInstance);
if (cookieValue !== null) {
return null;
},
+ /**
+ * @fixme what is this?
+ */
Delete: function() {
$.cookie(SN.C.S.StatusNetInstance, null);
}
*
* @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');
},
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();
}
},
+ /**
+ * 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();
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; });
}
},
+ /**
+ * 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;
SN.U.StatusNetInstance.Set({Nickname: $('#form_login #nickname').val()});
return true;
});
+ },
+
+ /**
+ * Add logic to any file upload forms to handle file size limits,
+ * on browsers that support basic FileAPI.
+ */
+ UploadForms: function () {
+ $('input[type=file]').change(function(event) {
+ if (typeof this.files == "object" && this.files.length > 0) {
+ var size = 0;
+ for (var i = 0; i < this.files.length; i++) {
+ size += this.files[i].size;
+ }
+
+ var max = SN.U.maxFileSize($(this.form));
+ if (max > 0 && size > max) {
+ var msg = 'File too large: maximum upload size is %d bytes.';
+ alert(msg.replace('%d', max));
+
+ // Clear the files.
+ $(this).val('');
+ event.preventDefault();
+ return false;
+ }
+ }
+ });
}
}
};
+/**
+ * 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(){
+ SN.Init.UploadForms();
if ($('.'+SN.C.S.FormNotice).length > 0) {
SN.Init.NoticeForm();
}
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();
-
-})();
-}