');
var commands = $('
save').click(function () { SaveEditable($(this).parent().parent()); UnborkFor(el); })
.add('
| ')
.add($('
cancel').click(function () { el.siblings(".quick-insert").show(); $(".popup-submit", popup).prop("disabled", false); CancelEditable($(this).parent().parent(), backup); UnborkFor(el); }));
actions.append(commands);
//set contents of element to textarea with links
el.html(txt.add(actions));
}
//This is to stop the input pinching focus when I click inside textarea
//Could have done something clever with contentEditable, but this is evil, and it annoys Yi :P
function BorkFor(el) {
var label = el.parent('label');
label.attr('for', 'borken');
}
function UnborkFor(el) {
var label = el.parent('label');
label.attr('for', label.prev().attr('id'));
}
//Save textarea contents, replace element html with new edited content
function SaveEditable(el) {
var html = markDownToHtml(el.find('textarea').val());
SetStorage(el.attr('id'), Tag(html));
el.html((showGreeting ? greeting : "") + UnTag(html));
}
function CancelEditable(el, backup) {
el.html(backup);
}
//Empty all custom comments from storage and rewrite to ui
function ResetComments() {
ClearStorage("name-"); ClearStorage("desc-");
$.each(defaultcomments, function (index, value) {
var targetsPrefix = "";
if( value.Target ) {
var targets = value.Target.join(",");
targetsPrefix = "[" + targets + "] ";
}
SetStorage('name-' + index, targetsPrefix + value["Name"]);
SetStorage('desc-' + index, value["Description"]);
});
SetStorage("commentcount", defaultcomments.length);
}
//rewrite all comments to ui (typically after import or reset)
function WriteComments(popup) {
if(!GetStorage("commentcount")) ResetComments();
var ul = popup.find('.action-list');
ul.empty();
for(var i = 0; i < GetStorage("commentcount"); i++) {
var commentName = GetStorage('name-' + i);
if( IsCommentValidForPostType( commentName, popup.posttype ) ) {
commentName = commentName.replace( Target.MATCH_ALL, "" );
var desc = GetStorage('desc-' + i).replace(/\$SITENAME\$/g, sitename).replace(/\$SITEURL\$/g, siteurl).replace(/\$MYUSERID\$/g, myuserid).replace(/\$/g, "$$$");
var opt = optionTemplate.replace(/\$ID\$/g, i)
.replace("$NAME$", commentName.replace(/\$/g, "$$$"))
.replace("$DESCRIPTION$", (showGreeting ? greeting : "") + desc);
ul.append(opt);
}
}
ShowHideDescriptions(popup);
AddOptionEventHandlers(popup);
AddSearchEventHandlers(popup);
}
/**
* Checks if a given comment could be used together with a given post type.
* @param {String} comment The comment itself.
* @param {Target} postType The type of post the comment could be placed on.
* @return {Boolean} true if the comment is valid for the type of post; false otherwise.
*/
function IsCommentValidForPostType( comment, postType ) {
var designator = comment.match( Target.MATCH_ALL );
if( !designator ) return true;
return ( -1 < designator.indexOf( postType ) );
}
function AddOptionEventHandlers(popup) {
popup.find('label > span').dblclick(function () { ToEditable(popup, $(this)); });
popup.find('label > .quick-insert').click(function () {
var parent = $(this).parent();
var li = parent.parent();
var radio = parent.siblings("input");
// Mark action as selected.
li.addClass('action-selected');
radio.prop('checked',true);
// Triger form submission.
popup.find('.popup-submit').trigger('click');
});
//add click handler to radio buttons
popup.find('input:radio').click(function () {
popup.find('.popup-submit').removeAttr("disabled"); //enable submit button
//unset/set selected class, hide others if necessary
$(this).parents('ul').find('.action-selected').removeClass('action-selected');
if(GetStorage('hide-desc') == "hide") {
$(this).parents('ul').find('.action-desc').hide();
}
$(this).parent().addClass('action-selected')
.find('.action-desc').show();
});
popup.find('input:radio').keyup(function (event) {
if(event.which == 13) {
event.preventDefault();
popup.find('.popup-submit').trigger('click');
}
});
}
function filterOn(popup, text) {
var words = text.toLowerCase().split(/\s+/).filter(
function(word) {
return word.length > 0;
}
);
popup.find('.action-list > li').each(
function (idx, item) {
var show = true,
li = $(item),
title = li.find('.action-name').text().toLowerCase(),
desc = li.find('.action-desc').text().toLowerCase();
words.forEach(
function(word) {
show = show && ((title.indexOf(word) >= 0) || (desc.indexOf(word) >= 0));
}
);
if (show) {
li.show();
}
else {
li.hide();
}
}
);
}
function AddSearchEventHandlers(popup) {
var sbox = popup.find('.searchbox'),
stext = sbox.find('.searchfilter'),
main = popup.find('#main'),
kicker = popup.find('.popup-actions-filter'),
storageKey = "showFilter",
shown = GetStorage(storageKey) == "show";
var showHideFilter = function() {
if (shown) {
sbox.show();
stext.focus();
SetStorage(storageKey, "show");
}
else {
sbox.hide();
stext.text('');
filterOn(popup, '');
SetStorage(storageKey, "hide");
}
};
var filterOnText = function() {
var text = stext.val();
filterOn(popup, text);
};
showHideFilter();
kicker.click( function() {
shown = ! shown;
showHideFilter();
return false;
});
stext.on("keydown change search cut paste",
function() {
setTimeout( filterOnText, 100 );
}
);
}
//Adjust the descriptions so they show or hide based on the user's preference.
function ShowHideDescriptions(popup) {
//get list of all descriptions except the currently selected one
var descriptions = popup.find("ul.action-list li:not(.action-selected) span[id*='desc-']");
if(GetStorage('hide-desc') == "hide") {
descriptions.hide();
}
else {
descriptions.show();
}
}
//Show a message (like notify.show) inside popup
function ShowMessage(popup, title, body, callback) {
var html = body.replace(/\n/g, '
');
var message = $(messageTemplate.replace("$TITLE$", title)
.replace('$BODY$', html));
message.find('.notify-close').click(function () {
$(this).parent().fadeOutAndRemove();
callback();
});
popup.find('h2').before(message);
}
//Get remote content via ajax, target url must contain valid json wrapped in callback() function
function GetRemote(url, callback, onerror) {
$.ajax({ type: "GET", url: url + '?jsonp=?', dataType: "jsonp", jsonpCallback: "callback", timeout: 2000, success: callback, error: onerror, async: false });
}
//customise welcome
//reverse compatible!
function LoadFromRemote(url, done, error) {
GetRemote(url, function (data) {
SetStorage("commentcount", data.length);
ClearStorage("name-"); ClearStorage("desc-");
$.each(data, function (index, value) {
SetStorage('name-' + index, value.name);
SetStorage('desc-' + index, markDownToHtml(value.description));
});
done();
}, error);
}
//Factored out from main popu creation, just because it's too long
function SetupRemoteBox(popup) {
var remote = popup.find('#remote-popup');
var remoteerror = remote.find('#remoteerror1');
var urlfield = remote.find('#remoteurl');
var autofield = remote.find('#remoteauto');
var throbber = remote.find("#throbber1");
popup.find('.popup-actions-remote').click(function () {
urlfield.val(GetStorage("RemoteUrl"));
autofield.prop('checked', GetStorage("AutoRemote") == 'true');
remote.show();
});
popup.find('.remote-cancel').click(function () {
throbber.hide();
remoteerror.text("");
remote.hide();
});
popup.find('.remote-save').click(function () {
SetStorage("RemoteUrl", urlfield.val());
SetStorage("AutoRemote", autofield.prop('checked'));
remote.hide();
});
popup.find('.remote-get').click(function () {
throbber.show();
LoadFromRemote(urlfield.val(), function () {
WriteComments(popup);
throbber.hide();
}, function (d, msg) {
remoteerror.text(msg);
});
});
}
function SetupWelcomeBox(popup) {
var welcome = popup.find('#welcome-popup');
var custom = welcome.find('#customwelcome');
popup.find('.popup-actions-welcome').click(function () {
custom.val(greeting);
welcome.show();
});
popup.find('.welcome-cancel').click(function () {
welcome.hide();
});
popup.find('.welcome-force').click(function () {
showGreeting = true;
WriteComments(popup);
welcome.hide();
});
popup.find('.welcome-save').click(function () {
var msg = custom.val() == "" ? "NONE" : custom.val();
SetStorage("WelcomeMessage", msg);
greeting = custom.val();
welcome.hide();
});
}
var cssElement = $(cssTemplate);
$("head").append(cssElement);
/**
* Attach an "auto" link somewhere in the DOM. This link is going to trigger the iconic ARC behavior.
* @param {String} triggerSelector A selector for a DOM element which, when clicked, will invoke the locator.
* @param {Function} locator A function that will search for both the DOM element, next to which the "auto" link
* will be placed and where the text selected from the popup will be inserted.
* This function will receive the triggerElement as the first argument when called and it
* should return an array with the two DOM elements in the expected order.
* @param {Function} injector A function that will be called to actually inject the "auto" link into the DOM.
* This function will receive the element that the locator found as the first argument when called.
* It will receive the action function as the second argument, so it know what to invoke when the "auto" link is clicked.
* @param {Function} action A function that will be called when the injected "auto" link is clicked.
*/
function attachAutoLinkInjector( triggerSelector, locator, injector, action ) {
/**
* The internal injector invokes the locator to find an element in relation to the trigger element and then invokes the injector on it.
* @param {jQuery} triggerElement The element that triggered the mechanism.
* @param {Number} [retryCount=0] How often this operation was already retried. 20 retries will be performed in 50ms intervals.
* @private
*/
var _internalInjector = function( triggerElement, retryCount ) {
// If we didn't find the element after 20 retries, give up.
if( 20 <= retryCount ) return;
// Try to locate the elements.
var targetElements = locator( triggerElement );
var injectNextTo = targetElements[ 0 ];
var placeCommentIn = targetElements[ 1 ]
// We didn't find it? Try again in 50ms.
if( !injectNextTo.length ) {
setTimeout( function() {
_internalInjector( triggerElement, retryCount + 1 );
}, 50 );
} else {
// Call our injector on the found element.
injector( injectNextTo, action, placeCommentIn );
}
};
// Maybe use this instead (if supported): $( "#content" ).on( "click", triggerSelector, function() {
$( "#content" ).delegate( triggerSelector, "click", function( event ) {
/** @type jQuery */
var triggerElement = $( event.target );
_internalInjector( triggerElement );
} );
}
attachAutoLinkInjector( ".js-add-link", findCommentElements, injectAutoLink, autoLinkAction );
attachAutoLinkInjector( ".edit-post", findEditSummaryElements, injectAutoLinkEdit, autoLinkAction );
attachAutoLinkInjector( ".close-question-link", findClosureElements, injectAutoLinkClosure, autoLinkAction );
attachAutoLinkInjector( ".review-actions input:first", findReviewQueueElements, injectAutoLinkReviewQueue, autoLinkAction );
/**
* A locator for the help link next to the comment box under a post and the textarea for the comment.
* @param {jQuery} where A DOM element, near which we're looking for the location where to inject our link.
* @returns {[jQuery]} The DOM element next to which the link should be inserted and the element into which the
* comment should be placed.
*/
function findCommentElements( where ) {
var divid = where.parent().attr('id').replace('-link', '');
var injectNextTo = $('#' + divid).find('.comment-help-link');
var placeCommentIn = $('#' + divid).find("textarea");
return [ injectNextTo, placeCommentIn ];
}
/**
* A locator for the edit summary input box under a post while it is being edited.
* @param {jQuery} where A DOM element, near which we're looking for the location where to inject our link.
* @returns {[jQuery]} The DOM element next to which the link should be inserted and the element into which the
* comment should be placed.
*/
function findEditSummaryElements( where ) {
var divid = where.attr('href').replace('/posts/', '').replace('/edit', '');
var injectNextTo = $('#post-editor-' + divid).next().find('.edit-comment');
var placeCommentIn = injectNextTo;
return [ injectNextTo, placeCommentIn ];
}
/**
* A locator for the text area in which to put a custom off-topic closure reason in the closure dialog.
* @param {jQuery} where A DOM element, near which we're looking for the location where to inject our link.
* @returns {[jQuery]} The DOM element next to which the link should be inserted and the element into which the
* comment should be placed.
*/
function findClosureElements( where ) {
var injectNextTo = $(".close-as-off-topic-pane textarea");
var placeCommentIn = injectNextTo;
return [ injectNextTo, placeCommentIn ];
}
/**
* A locator for the edit summary you get in the "Help and Improvement" review queue.
* @param {jQuery} where A DOM element, near which we're looking for the location where to inject our link.
* @returns {[jQuery]} The DOM element next to which the link should be inserted and the element into which the
* comment should be placed.
*/
function findReviewQueueElements( where ) {
var injectNextTo = $(".text-counter");
var placeCommentIn = $(".edit-comment");
return [ injectNextTo, placeCommentIn ];
}
/**
* Inject the auto link next to the given DOM element.
* @param {jQuery} where The DOM element next to which we'll place the link.
* @param {Function} what The function that will be called when the link is clicked.
* @param {jQuery} placeCommentIn The DOM element into which the comment should be placed.
*/
function injectAutoLink( where, what, placeCommentIn ) {
// Don't add auto links if one already exists
var existingAutoLinks = where.siblings( ".comment-auto-link" );
if( existingAutoLinks && existingAutoLinks.length ) {
return;
}
var posttype = where.parents(".question, .answer").attr("class").split(' ')[0]; //slightly fragile
if( "answer" == posttype ) posttype = Target.CommentAnswer;
if( "question" == posttype ) posttype = Target.CommentQuestion;
var _autoLinkAction = function(){
what( placeCommentIn, posttype );
};
var autoLink = $('
| ').add($('').click(_autoLinkAction));
autoLink.insertAfter( where );
}
/**
* Inject the auto link next to the edit summary input box.
* This will also slightly shrink the input box, so that the link will fit next to it.
* @param {jQuery} where The DOM element next to which we'll place the link.
* @param {Function} what The function that will be called when the link is clicked.
* @param {jQuery} placeCommentIn The DOM element into which the comment should be placed.
*/
function injectAutoLinkEdit( where, what, placeCommentIn ) {
// Don't add auto links if one already exists
var existingAutoLinks = where.siblings( ".comment-auto-link" );
if( existingAutoLinks && existingAutoLinks.length ) {
return;
}
where.css( "width", "510px" );
where.siblings( ".actual-edit-overlay" ).css( "width", "510px" );
var posttype = where.parents(".question, .answer").attr("class").split(' ')[0]; //slightly fragile
if( "answer" == posttype ) posttype = Target.EditSummaryAnswer;
if( "question" == posttype ) posttype = Target.EditSummaryQuestion;
var _autoLinkAction = function(){
what( placeCommentIn, posttype );
};
var autoLink = $('
| ').add($('').click(_autoLinkAction));
autoLink.insertAfter( where );
}
/**
* Inject the auto link next to the given DOM element.
* @param {jQuery} where The DOM element next to which we'll place the link.
* @param {Function} what The function that will be called when the link is clicked.
* @param {jQuery} placeCommentIn The DOM element into which the comment should be placed.
*/
function injectAutoLinkClosure( where, what, placeCommentIn ) {
// Don't add auto links if one already exists
var existingAutoLinks = where.siblings( ".comment-auto-link" );
if( existingAutoLinks && existingAutoLinks.length ) {
return;
}
var _autoLinkAction = function(){
what( placeCommentIn, Target.Closure );
};
var autoLink = $('
| ').add($('').click(_autoLinkAction));
autoLink.insertAfter( where );
}
/**
* Inject hte auto link next to the "characters left" counter below the edit summary in the review queue.
* @param {jQuery} where The DOM element next to which we'll place the link.
* @param {Function} what The function that will be called when the link is clicked.
* @param {jQuery} placeCommentIn The DOM element into which the comment should be placed.
*/
function injectAutoLinkReviewQueue( where, what, placeCommentIn ) {
// Don't add auto links if one already exists
var existingAutoLinks = where.siblings( ".comment-auto-link" );
if( existingAutoLinks && existingAutoLinks.length ) {
return;
}
var _autoLinkAction = function(){
what( placeCommentIn, Target.EditSummaryQuestion );
};
var autoLink = $('
| ').add($('').click(_autoLinkAction));
autoLink.insertAfter( where );
}
function autoLinkAction( targetObject, posttype ) {
//Create popup and wire-up the functionality
var popup = $(markupTemplate);
popup.find('.popup-close').click(function () { popup.fadeOutAndRemove(); });
popup.posttype = posttype;
//Reset this, otherwise we get the greeting twice...
showGreeting = false;
//create/add options
WriteComments(popup);
//Add handlers for command links
popup.find('.popup-actions-cancel').click(function () { popup.fadeOutAndRemove(); });
popup.find('.popup-actions-reset').click(function () { ResetComments(); WriteComments(popup); });
popup.find('.popup-actions-see').hover(function () {
popup.fadeTo('fast', '0.4').children().not('#close').fadeTo('fast', '0.0')
}, function () {
popup.fadeTo('fast', '1.0').children().not('#close').fadeTo('fast', '1.0')
});
popup.find('.popup-actions-impexp').click(function () { ImportExport(popup); });
popup.find('.popup-actions-toggledesc').click(function () {
var hideDesc = GetStorage('hide-desc') || "show";
SetStorage('hide-desc', hideDesc == "show" ? "hide" : "show");
ShowHideDescriptions(popup);
});
//Handle remote url & welcome
SetupRemoteBox(popup);
SetupWelcomeBox(popup);
//on submit, convert html to markdown and copy to comment textarea
popup.find('.popup-submit').click(function () {
var selected = popup.find('input:radio:checked');
var markdown = htmlToMarkDown(selected.parent().find('.action-desc').html()).replace(/\[username\]/g, username).replace(/\[OP\]/g, OP);
targetObject.val(markdown).focus(); //focus provokes character count test
var caret = markdown.indexOf('[type here]')
if(caret >= 0) targetObject[0].setSelectionRange(caret, caret + '[type here]'.length);
popup.fadeOutAndRemove();
});
//Auto-load from remote if required
if(!window.VersionChecked && GetStorage("AutoRemote") == 'true') {
var throbber = popup.find("#throbber2");
var remoteerror = popup.find('#remoteerror2');
throbber.show();
LoadFromRemote(GetStorage("RemoteUrl"),
function () { WriteComments(popup); throbber.hide(); },
function (d, msg) { remoteerror.text(msg); });
}
// Attach to #content, everything else is too fragile.
$("#content").append(popup);
popup.center();
StackExchange.helpers.bindMovablePopups();
//Get user info and inject
var userid = getUserId(targetObject);
getUserInfo(userid, popup);
OP = getOP();
//We only actually perform the updates check when someone clicks, this should make it less costly, and more timely
//also wrap it so that it only gets called the *FIRST* time we open this dialog on any given page (not much of an optimisation).
if(typeof CheckForNewVersion == "function" && !window.VersionChecked) { CheckForNewVersion(popup); window.VersionChecked = true; }
}
});
});
Which review comment to insert?