Processing: 'processing',
CommandResult: 'command_result',
FormNotice: 'form_notice',
- NoticeDataText: 'notice_data-text',
- NoticeTextCount: 'notice_text-count',
NoticeInReplyTo: 'notice_in-reply-to',
- NoticeDataAttach: 'notice_data-attach',
- NoticeDataAttachSelected: 'notice_data-attach_selected',
NoticeActionSubmit: 'notice_action-submit',
NoticeLat: 'notice_data-lat',
NoticeLon: 'notice_data-lon',
*/
FormNoticeEnhancements: function(form) {
if (jQuery.data(form[0], 'ElementData') === undefined) {
- MaxLength = form.find('#'+SN.C.S.NoticeTextCount).text();
+ MaxLength = form.find('.count').text();
if (typeof(MaxLength) == 'undefined') {
MaxLength = SN.C.I.MaxLength;
}
SN.U.Counter(form);
- NDT = form.find('#'+SN.C.S.NoticeDataText);
+ NDT = form.find('[name=status_textarea]');
NDT.bind('keyup', function(e) {
SN.U.Counter(form);
// 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);
- });
}
else {
- form.find('#'+SN.C.S.NoticeTextCount).text(jQuery.data(form[0], 'ElementData').MaxLength);
- }
-
- if ($('body')[0].id != 'conversation' && window.location.hash.length === 0 && $(window).scrollTop() == 0) {
- form.find('textarea').focus();
+ form.find('.count').text(jQuery.data(form[0], 'ElementData').MaxLength);
}
},
- /**
- * 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();
- event.preventDefault();
- event.stopPropagation();
- $('#'+el[0].id+' #'+SN.C.S.NoticeDataText).blur();
- $('body').focus();
- return false;
- }
- return true;
- },
-
/**
* To be called from event handlers on the notice import form.
* Triggers an update of the remaining-characters counter.
}
var remaining = MaxLength - SN.U.CharacterCount(form);
- var counter = form.find('#'+SN.C.S.NoticeTextCount);
+ var counter = form.find('.count');
if (remaining.toString() != counter.text()) {
if (!SN.C.I.CounterBlackout || remaining === 0) {
* @return number of chars
*/
CharacterCount: function(form) {
- return form.find('#'+SN.C.S.NoticeDataText).val().length;
+ return form.find('[name=status_textarea]').val().length;
},
/**
dataType: 'xml',
timeout: '60000',
beforeSend: function(formData) {
- if (form.find('#'+SN.C.S.NoticeDataText)[0].value.length === 0) {
+ if (form.find('[name=status_textarea]').val() == '') {
form.addClass(SN.C.S.Warning);
return false;
}
.addClass(SN.C.S.Disabled)
.attr(SN.C.S.Disabled, SN.C.S.Disabled);
- SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val();
- SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val();
- SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
- SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val();
- SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked');
-
- cookieValue = $.cookie(SN.C.S.NoticeDataGeoCookie);
-
- if (cookieValue !== null && cookieValue != 'disabled') {
- cookieValue = JSON.parse(cookieValue);
- SN.C.I.NoticeDataGeo.NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val();
- SN.C.I.NoticeDataGeo.NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val();
- if ($('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS)) {
- SN.C.I.NoticeDataGeo.NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val();
- SN.C.I.NoticeDataGeo.NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val();
- }
- }
- if (cookieValue == 'disabled') {
- SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
- }
- else {
- SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
- }
+ SN.U.normalizeGeoData(form);
return true;
},
if (parseInt(xhr.status) === 0 || jQuery.inArray(parseInt(xhr.status), SN.C.I.HTTP20x30x) >= 0) {
form
.resetForm()
- .find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ .find('.attach-status').remove();
SN.U.FormNoticeEnhancements(form);
}
else {
else {
// New notice post was successful. If on our timeline, show it!
var notice = document._importNode($('li', data)[0], true);
- var notices = $('#notices_primary .notices');
+ var notices = $('#notices_primary .notices:first');
if (notices.length > 0 && SN.U.belongsOnTimeline(notice)) {
if ($('#'+notice.id).length === 0) {
var notice_irt_value = $('#'+SN.C.S.NoticeInReplyTo).val();
}
}
form.resetForm();
- form.find('#'+SN.C.S.NoticeInReplyTo).val('');
- form.find('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ form.find('[name=inreplyto]').val('');
+ form.find('.attach-status').remove();
SN.U.FormNoticeEnhancements(form);
}
},
});
},
+ normalizeGeoData: function(form) {
+ SN.C.I.NoticeDataGeo.NLat = form.find('[name=lat]').val();
+ SN.C.I.NoticeDataGeo.NLon = form.find('[name=lon]').val();
+ SN.C.I.NoticeDataGeo.NLNS = form.find('[name=location_ns]').val();
+ SN.C.I.NoticeDataGeo.NLID = form.find('[name=location_id]').val();
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked'); // @fixme
+
+ var cookieValue = $.cookie(SN.C.S.NoticeDataGeoCookie);
+
+ if (cookieValue !== null && cookieValue != 'disabled') {
+ cookieValue = JSON.parse(cookieValue);
+ SN.C.I.NoticeDataGeo.NLat = form.find('[name=lat]').val(cookieValue.NLat).val();
+ SN.C.I.NoticeDataGeo.NLon = form.find('[name=lon]').val(cookieValue.NLon).val();
+ if (cookieValue.NLNS) {
+ SN.C.I.NoticeDataGeo.NLNS = form.find('[name=location_ns]').val(cookieValue.NLNS).val();
+ SN.C.I.NoticeDataGeo.NLID = form.find('[name=location_id]').val(cookieValue.NLID).val();
+ } else {
+ form.find('[name=location_ns]').val('');
+ form.find('[name=location_id]').val('');
+ }
+ }
+ if (cookieValue == 'disabled') {
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked');
+ }
+ else {
+ SN.C.I.NoticeDataGeo.NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', true).attr('checked');
+ }
+
+ },
/**
* Fetch an XML DOM from an XHR's response data.
*
* @access private
*/
NoticeReply: function() {
- if ($('#'+SN.C.S.NoticeDataText).length > 0 && $('#content .notice_reply').length > 0) {
+ if ($('#content .notice_reply').length > 0) {
$('#content .notice').each(function() { SN.U.NoticeReplyTo($(this)); });
}
},
* @access private
*/
NoticeReplyTo: function(notice) {
- notice.find('.notice_reply').live('click', function() {
+ notice.find('.notice_reply').live('click', function(e) {
+ e.preventDefault();
var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname.uid');
- SN.U.NoticeReplySet(nickname.text(), $($('.notice_id', notice)[0]).text());
+ SN.U.NoticeInlineReplyTrigger(notice, '@' + nickname.text());
return false;
});
},
/**
- * 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.
+ * Open up a notice's inline reply box.
*
- * @fixme replyto is a global variable, but probably shouldn't be
- *
- * @param {String} nick
- * @param {String} id
+ * @param {jQuery} notice: jQuery object containing one notice
+ * @param {String} initialText
*/
- NoticeReplySet: function(nick,id) {
- if (nick.match(SN.C.I.PatternUsername)) {
- var text = $('#'+SN.C.S.NoticeDataText);
- if (text.length > 0) {
- replyto = '@' + nick + ' ';
- text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
- $('#'+SN.C.S.FormNotice+' #'+SN.C.S.NoticeInReplyTo).val(id);
-
- text[0].focus();
- if (text[0].setSelectionRange) {
- var len = text.val().length;
- text[0].setSelectionRange(len,len);
- }
+ NoticeInlineReplyTrigger: function(notice, initialText) {
+ // Find the notice we're replying to...
+ var id = $($('.notice_id', notice)[0]).text();
+ var parentNotice = notice;
+
+ // Find the threaded replies view we'll be adding to...
+ var list = notice.closest('.notices');
+ if (list.hasClass('threaded-replies')) {
+ // We're replying to a reply; use reply form on the end of this list.
+ // We'll add our form at the end of this; grab the root notice.
+ parentNotice = list.closest('.notice');
+ } else {
+ // We're replying to a parent notice; pull its threaded list
+ // and we'll add on the end of it. Will add if needed.
+ list = $('ul.threaded-replies', notice);
+ if (list.length == 0) {
+ list = $('<ul class="notices threaded-replies xoxo"></ul>');
+ notice.append(list);
+ }
+ }
+
+ // See if the form's already open...
+ var replyForm = $('.notice-reply-form', list);
+ if (replyForm.length == 0) {
+ // Remove placeholder if any
+ $('li.notice-reply-placeholder').remove();
+
+ // Create the reply form entry at the end
+ var replyItem = $('li.notice-reply', list);
+ if (replyItem.length == 0) {
+ replyItem = $('<li class="notice-reply">' +
+ '<form class="notice-reply-form" method="post">' +
+ '<textarea name="status_textarea"></textarea>' +
+ '<div class="controls">' +
+ '<input type="hidden" name="token">' +
+ '<input type="hidden" name="inreplyto">' +
+ '<input type="submit" class="submit">' +
+ '</div>' +
+ '</form>' +
+ '</li>');
+
+ var baseForm = $('#form_notice');
+ replyForm = replyItem.find('form');
+ replyForm.attr('action', baseForm.attr('action'));
+ replyForm.find('input[name="token"]').val(baseForm.find('input[name=token]').val());
+ replyForm.find('input[type="submit"]').val(SN.msg('reply_submit'));
+ list.append(replyItem);
+
+ replyForm.find('textarea').blur(function() {
+ var textarea = $(this);
+ var txt = $.trim(textarea.val());
+ if (txt == '' || txt == textarea.data('initialText')) {
+ // Nothing to say? Begone!
+ replyItem.remove();
+ if (list.find('li').length > 0) {
+ SN.U.NoticeInlineReplyPlaceholder(parentNotice);
+ } else {
+ list.remove();
+ }
+ }
+ });
+ replyForm.submit(function(event) {
+ var form = replyForm;
+ $.ajax({
+ type: 'POST',
+ dataType: 'xml',
+ url: SN.U.RewriteAjaxAction(form.attr('action')),
+ data: form.serialize() + '&ajax=1',
+ beforeSend: function(xhr) {
+ form
+ .addClass(SN.C.S.Processing)
+ .find('.submit')
+ .addClass(SN.C.S.Disabled)
+ .attr(SN.C.S.Disabled, SN.C.S.Disabled)
+ .end()
+ .find('textarea')
+ .addClass(SN.C.S.Disabled)
+ .attr(SN.C.S.Disabled, SN.C.S.Disabled);
+ },
+ error: function (xhr, textStatus, errorThrown) {
+ alert(errorThrown || textStatus);
+ },
+ success: function(data, textStatus) {
+ var orig_li = $('li', data)[0];
+ if (orig_li) {
+ var li = document._importNode(orig_li, true);
+ var id = $(li).attr('id');
+ if ($("#"+id).length == 0) {
+ replyItem.replaceWith(li);
+ SN.U.NoticeInlineReplyPlaceholder(parentNotice);
+ } else {
+ // Realtime came through before us...
+ replyItem.remove();
+ }
+ }
+ }
+ });
+ event.preventDefault();
+ return false;
+ });
}
}
+
+ // Override...?
+ replyForm.find('input[name=inreplyto]').val(id);
+
+ // Set focus...
+ var text = replyForm.find('textarea');
+ if (text.length == 0) {
+ throw "No textarea";
+ }
+ var replyto = '';
+ if (initialText) {
+ replyto = initialText + ' ';
+ }
+ text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
+ text.data('initialText', $.trim(initialText + ''));
+ text.focus();
+ if (text[0].setSelectionRange) {
+ var len = text.val().length;
+ text[0].setSelectionRange(len,len);
+ }
},
/**
$('.form_disfavor').live('click', function() { SN.U.FormXHR($(this)); return false; });
},
+ NoticeInlineReplyPlaceholder: function(notice) {
+ var list = notice.find('ul.threaded-replies');
+ var placeholder = $('<li class="notice-reply-placeholder">' +
+ '<input class="placeholder">' +
+ '</li>');
+ placeholder.click(function() {
+ SN.U.NoticeInlineReplyTrigger(notice);
+ });
+ placeholder.find('input').val(SN.msg('reply_placeholder'));
+ list.append(placeholder);
+ },
+
+ /**
+ * 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.
+ */
+ NoticeInlineReplySetup: function() {
+ $('.threaded-replies').each(function() {
+ var list = $(this);
+ var notice = list.closest('.notice');
+ SN.U.NoticeInlineReplyPlaceholder(notice);
+ });
+ },
+
/**
* Setup function -- DOES NOT trigger actions immediately.
*
*
* This preview box will also allow removing the attachment
* prior to posting.
+ *
+ * @param {jQuery} form
*/
- NoticeDataAttach: function() {
- NDA = $('#'+SN.C.S.NoticeDataAttach);
+ NoticeDataAttach: function(form) {
+ var NDA = form.find('input[type=file]');
NDA.change(function(event) {
+ form.find('.attach-status').remove();
+
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);
- }
- else {
- $('#'+SN.C.S.FormNotice).append(S);
- }
- $('#'+SN.C.S.NoticeDataAttachSelected+' button').click(function(){
- $('#'+SN.C.S.NoticeDataAttachSelected).remove();
+ var attachStatus = $('<div class="attach-status '+SN.C.S.Success+'"><code></code> <button class="close">×</button></div>');
+ attachStatus.find('code').text(filename);
+ attachStatus.find('button').click(function(){
+ attachStatus.remove();
NDA.val('');
return false;
});
+ form.append(attachStatus);
+
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]);
+ SN.U.PreviewAttach(form, this.files[i]);
}
}
});
* Known fail:
* - Opera 10.63, 11 beta (no input.files interface)
*
+ * @param {jQuery} form
* @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) {
+ PreviewAttach: function(form, file) {
var tooltip = file.type + ' ' + Math.round(file.size / 1024) + 'KB';
var preview = true;
.attr('alt', tooltip)
.attr('src', url)
.attr('style', 'height: 120px');
- $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
+ form.find('.attach-status').append(img);
});
} else {
var img = $('<div></div>').text(tooltip);
- $('#'+SN.C.S.NoticeDataAttachSelected).append(img);
+ form.find('.attach-status').append(img);
}
},
*
*/
NoticeLocationAttach: function() {
+ // @fixme this should not be tied to the main notice form, as there may be multiple notice forms...
var NLat = $('#'+SN.C.S.NoticeLat).val();
var NLon = $('#'+SN.C.S.NoticeLon).val();
var NLNS = $('#'+SN.C.S.NoticeLocationNs).val();
var NLN = $('#'+SN.C.S.NoticeGeoName).text();
var NDGe = $('#'+SN.C.S.NoticeDataGeo);
- function removeNoticeDataGeo() {
+ function removeNoticeDataGeo(error) {
$('label[for='+SN.C.S.NoticeDataGeo+']')
.attr('title', jQuery.trim($('label[for='+SN.C.S.NoticeDataGeo+']').text()))
.removeClass('checked');
$('#'+SN.C.S.NoticeDataGeo).attr('checked', false);
$.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/' });
+
+ if (error) {
+ $('.geo_status_wrapper').removeClass('success').addClass('error');
+ $('.geo_status_wrapper .geo_status').text(error);
+ } else {
+ $('.geo_status_wrapper').remove();
+ }
}
function getJSONgeocodeURL(geocodeURL, data) {
+ SN.U.NoticeGeoStatus('Looking up place name...');
$.getJSON(geocodeURL, data, function(location) {
var lns, lid;
NLN_text = location.name;
}
+ SN.U.NoticeGeoStatus(NLN_text, data.lat, data.lon, location.url);
$('label[for='+SN.C.S.NoticeDataGeo+']')
.attr('title', NoticeDataGeo_text.ShareDisable + ' (' + NLN_text + ')');
if ($.cookie(SN.C.S.NoticeDataGeoCookie) === null || $.cookie(SN.C.S.NoticeDataGeoCookie) == 'disabled') {
if (navigator.geolocation) {
+ SN.U.NoticeGeoStatus('Requesting location from browser...');
navigator.geolocation.getCurrentPosition(
function(position) {
$('#'+SN.C.S.NoticeLat).val(position.coords.latitude);
function(error) {
switch(error.code) {
case error.PERMISSION_DENIED:
- removeNoticeDataGeo();
+ removeNoticeDataGeo('Location permission denied.');
break;
case error.TIMEOUT:
- $('#'+SN.C.S.NoticeDataGeo).attr('checked', false);
+ //$('#'+SN.C.S.NoticeDataGeo).attr('checked', false);
+ removeNoticeDataGeo('Location lookup timeout.');
break;
}
},
$('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID);
$('#'+SN.C.S.NoticeDataGeo).attr('checked', cookieValue.NDG);
+ SN.U.NoticeGeoStatus(cookieValue.NLN, cookieValue.NLat, cookieValue.NLon, cookieValue.NLNU);
$('label[for='+SN.C.S.NoticeDataGeo+']')
.attr('title', NoticeDataGeo_text.ShareDisable + ' (' + cookieValue.NLN + ')')
.addClass('checked');
}
},
+ /**
+ * Create or update a geolocation status widget in this notice posting form.
+ *
+ * @param {String} status
+ * @param {String} lat (optional)
+ * @param {String} lon (optional)
+ * @param {String} url (optional)
+ */
+ NoticeGeoStatus: function(status, lat, lon, url)
+ {
+ var form = $('#form_notice');
+ var wrapper = form.find('.geo_status_wrapper');
+ if (wrapper.length == 0) {
+ wrapper = $('<div class="'+SN.C.S.Success+' geo_status_wrapper"><button class="close" style="float:right">×</button><div class="geo_status"></div></div>');
+ wrapper.find('button.close').click(function() {
+ $('#'+SN.C.S.NoticeDataGeo).removeAttr('checked').change();
+ });
+ form.append(wrapper);
+ }
+ var label;
+ if (url) {
+ label = $('<a></a>').attr('href', url);
+ } else {
+ label = $('<span></span>');
+ }
+ label.text(status);
+ if (lat || lon) {
+ var latlon = lat + ';' + lon;
+ label.attr('title', latlon);
+ if (!status) {
+ label.text(latlon)
+ }
+ }
+ wrapper.find('.geo_status').empty().append(label);
+ },
+
/**
* Setup function -- DOES NOT trigger actions immediately.
*
$('.'+SN.C.S.FormNotice).each(function() {
SN.U.FormNoticeXHR($(this));
SN.U.FormNoticeEnhancements($(this));
+ SN.U.NoticeDataAttach($(this));
});
-
- SN.U.NoticeDataAttach();
}
},
SN.U.NoticeFavor();
SN.U.NoticeRepeat();
SN.U.NoticeReply();
+ SN.U.NoticeInlineReplySetup();
}
SN.U.NoticeAttachments();