");
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);
// Create the selectable option with the HTML preview text.
var optionElement = $(opt);
$(".action-desc", optionElement).html(desc);
ul.append(optionElement);
}
}
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"),
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, 0);
});
}
attachAutoLinkInjector(".js-add-link", findCommentElements, injectAutoLink, autoLinkAction);
attachAutoLinkInjector(".edit-post", findEditSummaryElements, injectAutoLinkEdit, autoLinkAction);
attachAutoLinkInjector(".js-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(".js-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").match(/posts\/(\d+)\/edit/)[1];
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 = $("#site-specific-comment .text-counter");
var placeCommentIn = $("#site-specific-comment textarea");
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).trigger('input').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();
}
});
});
Which review comment to insert?