// ==UserScript==
// @name kbd formatting button for stackexchange
// @description Adds the ability to quickly insert kbd formatting tags in the SE editor
// @namespace http://blender.org
// @include *.stackexchange.com/*
// @include http://stackoverflow.com/*
// @include http://askubuntu.com/*
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @version 7
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
//Credits to CoDEmanX and iKlsR
//see https://blender.meta.stackexchange.com/q/388/599 for discussion
//calls to GM functions must be outside of injected code, so put them here
function toggle_extra_markdown() {
console.log("checkbox click, was", GM_getValue("extra_markdown", 1))
if (GM_getValue("extra_markdown", 1) == 1) {
GM_setValue("extra_markdown", 0);
}
else {
GM_setValue("extra_markdown", 1);
}
}
function get_prefs() {
return GM_getValue("extra_markdown", 1);
}
//stuff which will be injected with jquery goes in main:
function main() {
var pref_extra_markdown = 0
console.log("running main!");
function startInjection() {
//add kbd button when any of these elements are clicked:
$(document).on('click', 'a.edit-post', waitForButtonRow); //inline editing
$(document).on('click', 'input#answer-from-ask', waitForButtonRow); //answering own question in ask questions page
$(document).on('click', 'input[value="Add Another Answer"]', waitForButtonRow); //adding multiple answers
//review editing:
$(document).on('click', 'input[value="Improve"]', waitForButtonRow); //improving suggested edits
$(document).on('click', 'input[value="Edit"]', waitForButtonRow); //editing close voted questions
//define keyboard shortcut even handler (Ctrl+Y)
$(document).on('keydown', "textarea.wmd-input", function(e) {
if (e.ctrlKey && (e.which === 89)) {
// turns out SE silently binds Ctrl+Y to redo in addition to Ctrl+Shift+Z; needless to say, us both messing with the content at the same time causes havoc, so we stop SE.
// TODO: this doesn't always seem to work, possibly a race condition? May be best to bind to a different key.
e.stopImmediatePropagation();
insertKbdTag(this);
}
});
waitForButtonRow();
}
function waitForButtonRow() {
console.log("waiting for button row..")
function testForButtonRow() { /*test for a .wmd-button-row every half a second until one is found*/
if (counter < 60) {
if ($(".wmd-button-row").length > 0) { //if button row(s) exist, test each one to see if it already has a kbd button
console.log("found .wmd-button-row");
$(".wmd-button-row").each(function() {console.log("does it have a kbd button? ", $(this).has(".wmd-kbd-button").length);console.log("id", $(this).attr("id"))});
$(".wmd-button-row").each(function() {
if ($(this).has(".wmd-kbd-button").length == 0) { //if no kbd button exists, inject one
console.log("does not contain kbd button, inserting one");
injectButton($(this));
}
});
}
else {
setTimeout(testForButtonRow, 500);
counter++;
}
}
else {
console.log("did not find a place to put kbd button within 30 seconds. giving up.");
return;
}
}
var counter = 0;
setTimeout(testForButtonRow, 500); //bit of spacer time to allow SE js to execute and add button rows.
//TODO: This causes a potential race condition (if SE js takes longer than 500ms), a better workaround would be nice..
}
function injectButton(buttonRow) {
//abandonded attempt to make it work on unity answers:
//console.log("host: " + window.location.hostname);
//if (window.location.hostname != "answers.unity3d.com") {
console.log("id-number:" + buttonRow.attr("id").replace(/[^0-9]+/g, ""))
var kbdButtonId = 'wmd-kbd-button' + buttonRow.attr("id").replace(/[^0-9]+/g, "");
/*}
else {
kbdButtonId = "";
}*/
var li = $("<li/>");
li.attr('id', kbdButtonId);
li.attr('title', 'Keyboard Shortcut <kbd> Ctrl+Y');
li.addClass('wmd-button wmd-kbd-button');
li.click(function() {
insertKbdTag($(this).parents("div[class='wmd-container']").find("textarea").first()[0]);
});
//shuffle existing buttons around so kbd button is the one after image button
var imgButton = $(buttonRow).children("[id^=wmd-image]");
li.insertAfter(imgButton);
li.css("left", parseInt(imgButton.css("left")) + 25 + "px"); //put kbd button 25 px after img button
li.nextAll().each(function() {
$(this).css("left", parseInt($(this).css("left")) + 25 + "px"); //move buttons after kbd button farther over
});
//Add image element with embedded png icon
var img = $("<img/>").appendTo(li); // Look at that slope :P.. ============> \
img.attr('src', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAMAAACeyVWk\
AAAAUVBMVEUAAADMzMz6%2BvrQ0NDS0tL39%2Ff%2F%2F%2F8AAAAAAAAAAADZ2dlGRkYzMzPc3Nz09PSHh4\
eOjo7R0dF5eXmYmJhOTk7t7e06OjrAwMC7u7tiYmLHx8fiGhLAAAAACnRSTlMd%2F%2F%2F%2Fcv%2F%2FAiQ\
FjE%2F%2BXQAAAHdJREFUGNOd0LsawyAIQGHQGi0EL0l6ff8HLV83MFPO4PDLApAhom2UDEBhsSUCGGnxhQion\
yadwmsqaz%2FR%2FcN11gOP76SRU2%2BTtp6Qq9PKq%2FZ2%2BmJdvG1Ot10fej6shvAfu7JxDGeXFErT1QVuMt\
AWpUAG8lruP7kVCZBoOBuAAAAAAElFTkSuQmCC');
//define RMB preferences menu
$(li).on("contextmenu", function(e) {
e.preventDefault();
console.log("started creating context menu. pref_extra_markdown =", pref_extra_markdown)
/*check if a preference menu already exists*/
console.log("contextmenu.length: " + $("#kbd-context-menu").length)
if ($("#kbd-context-menu").length < 1) { //ensure context menu doesn't already exist
//console.log("contextmenu")
var div = $("<div>").appendTo($(li).parent());
div.attr("id", "kbd-context-menu")
var pOffset = $(li).parent().offset();
div.css({"position": "absolute", "left": (e.pageX-pOffset.left)+5 + "px", "top": (e.pageY-pOffset.top) + "px",
"background-color": "rgba(0,0,0,.7)",
"color": "#f8f8f8",
"padding": "5px",
"padding-top": "1px",
"border-radius": "5px",
"box-shadow": "5px 5px 10px rgba(0,0,0,.7)"});
var ul = $("<ul>").appendTo(div);
ul.css({"list-style": "none",
"margin": "3px",
"cursor": "default"});
//styling for headings, links
ul.append("<li id='kbd_info_links'>");
$("#kbd_info_links").html("<a href='https://blender.meta.stackexchange.com/a/391/599' title='Go to meta post for discussion and feedback'>About</a>").css({"font-size": "6pt"});
ul.append("<li id='kbd_context_title'>");
$("#kbd_context_title").html("Preferences:<br><hr>").css({"font-weight": "bold"});
$("#kbd_context_title hr").css({"margin": "0", "background-color": "rgba(200,200,200,.2)"});
//TODO stylize checkbox
ul.append("<li id='entry1'>");
$("#entry1").html("Extra markdown <input type='checkbox' />");
$("#entry1").attr("title", "Insert mouse and modifier key icons");
$("#entry1 > input").css({"margin": "0"});
//console.log("div height: " + div.css("height"));
div.css({"top": (e.pageY-pOffset.top) - parseInt(div.css("height")) });
//bind mouse sensors to the menu so it goes away on mouse off:
var vanish_delay = setTimeout(function() {$("#kbd-context-menu").fadeOut(500,function() {$(this).remove()})}, 1500);
div.mouseleave(function() {
vanish_delay = setTimeout(function() {$("#kbd-context-menu").fadeOut(500,function() {$(this).remove()})}, 500);
})
div.mouseenter(function() {
console.log("on context menu");
clearTimeout(vanish_delay);
})
/*store preferences*/
if (typeof get_prefs === "function") { //for normal chrome extensions get_prefs will be outside of scope
console.log("toggle_markdown:", get_prefs());
if (get_prefs() == 1) {
$("#entry1 > input").prop("checked", 1);
}
}
else { //if being run as chrome extension, use normal variable instead
console.log("get_prefs not found, probably running as chrome extension.", "WARNING: preferences won't be saved accross page loads")
if (pref_extra_markdown == 1) {
$("#entry1 > input").prop("checked", 1);
}
}
//bind mouse click sensor to the checkbox:
if (typeof toggle_extra_markdown === "function") {
$("#entry1 > input").click(toggle_extra_markdown)
}
else {
$("#entry1 > input").click(function(){pref_extra_markdown ^= 1}) //toggle non persistent var with xor operator
}
}
else {
$("#kbd-context-menu").remove() //right clicking on the icon when there is an existing context menu will remove it
}
console.log("finished creating context menu. pref_extra_markdown =", pref_extra_markdown)
});
}
function insertKbdTag(txta) {
if (txta.selectionStart == null) return;
var start = txta.selectionStart;
var end = txta.selectionEnd;
var added = 0;
var chars = txta.value;
console.log("chars: " + chars);
/*function to insert mousebutton icon references as needed*/
function insertIcon(txta, mb) {
function addRef(ref) { //function to test if image references exists, and add it if it doesn't
if (txta.value.indexOf(ref) < 0) {
post = post + "\n\n " + ref; //insert image reference at end of post
}
}
console.log("mb", mb);
switch (mb.toUpperCase()) {
case "MW":
addRef("[MW]: http://i.stack.imgur.com/v1vyT.png (Mouse Wheel)");
break;
case "LMB":
addRef("[LMB]: http://i.stack.imgur.com/FwrAW.png (Left Mouse Button)");
break;
case "RMB":
addRef("[RMB]: http://i.stack.imgur.com/LPwD4.png (Right Mouse Button)");
break;
case "MMB":
addRef("[MMB]: http://i.stack.imgur.com/OASpJ.png (Middle Mouse Button)");
break;
case "WIN":
addRef("[WIN]: http://i.imgur.com/AAjIi.png (Windows key)"); //use http://i.stack.imgur.com/DHxcg.png for windows 9x logo
break;
case "LINUX":
addRef("[LINUX]: http://i.stack.imgur.com/X9TZA.png (LINUX5EVAH -CharlesL)");
break;
}
}
//separate selection from rest of body
var pre = chars.slice(0, start);
var post = chars.slice(end);
if (start != end) {
var sel = chars.slice(start, end);
console.log("sel: " + sel);
sel = sel.match(/(?:\S+|\s)/g); //split string around whitespace without deleting whitespace, thanks to this SO post: http://stackoverflow.com/a/24504047/2730823
console.log("sel: " + sel);
//remove extra spaces and replace them with kbd markdown
//var lastElement = ""; //holds previous element
var wasSpace = 0; //tracks if last element was a space
var endSpaces = 0; //needed for special end cases
var endSpace = 0;
var refined_markdown = "";
for (var char = 0; char < sel.length; char++) {
console.log("element " + char + ": " + "'" + sel[char] + "'")
//if current this element is a space, check to see if it should be replaced with a kbd
if (sel[char] == " ") {
//if previous element was not a space, replace space with kbd
if (wasSpace != 1 && char != 0) {
sel.splice(char, 1, '</kbd><kbd>');
//added += 10;
wasSpace = 1;
endSpace = char;
}
else {
//console.log("asdf42")
//console.log(sel.join(""))
sel.splice(char, 1); //remove extra space
//console.log(sel.join(""))
wasSpace = 1;
char--; //go back one element
}
}
else {
wasSpace = 0;
}
if (wasSpace == 1) {
endSpaces ++;
}
else {
endSpaces = 0;
}
//test if get_prefs is defined, and if it is test if GM_value "extra markdown" is 1. If get_prefs is not defined, use the non-persistent variable:
if (((typeof get_prefs === "function") ? get_prefs() : pref_extra_markdown) == 1 ) {
//console.log("element: " + sel[char])
switch(sel[char].toLowerCase()) {
case "control":
case "ctrl":
refined_markdown = "⎈ Ctrl";
break;
case "alternate":
case "alt":
refined_markdown = "⎇ Alt";
break;
case "shift":
refined_markdown = "⇧ Shift";
break;
case "tab":
refined_markdown = "↹ Tab";
break;
case "delete":
case "del":
refined_markdown = "⌦ Delete";
break;
case "enter":
case "return":
refined_markdown = "⏎ Enter";
break;
case "backspace":
refined_markdown = "⟵ Backspace";
break;
case "pageup":
case "pgup":
refined_markdown = "⇞ Page up";
break;
case "pagedown":
case "pgdn":
refined_markdown = "⇟ Page down";
break;
case "printscreen":
refined_markdown = "⎙ Print Screen";
break;
case "up":
refined_markdown = "↑ Up arrow";
break;
case "left":
refined_markdown = "← Left arrow";
break;
case "right":
refined_markdown = "→ Right arrow";
break;
case "down":
refined_markdown = "↓ Down arrow";
break;
case "caps":
case "capslock":
refined_markdown = "⇪ Caps Lock"; //maybe use ⇬ instead?
break;
case "win":
case "windows":
case "windowskey":
case "winkey":
insertIcon(txta, "WIN");
refined_markdown = "![Windows key][WIN]";
break;
case "super":
case "linux":
case "linuxkey":
case "tuxkey":
insertIcon(txta, "LINUX");
refined_markdown = "![Linux key][LINUX]";
break;
case "meta":
refined_markdown = "◆ Meta";
break;
//mac thingies
case "command":
case "cmd":
refined_markdown = "⌘ Cmd";
break;
case "option":
case "opt":
refined_markdown = "⌥ Opt";
break;
//mouse things
case "wheel":
case "scrollwheel":
case "mousewheel":
case "mw":
insertIcon(txta, "MW");
refined_markdown = "![MW][MW] MW";
break;
case "mmb":
insertIcon(txta, "MMB");
refined_markdown = "![MMB][MMB] MMB";
break;
case "lmb":
insertIcon(txta, "LMB");
refined_markdown = "![LMB][LMB] LMB";
break;
case "rmb":
refined_markdown = "![RMB][RMB] RMB";
insertIcon(txta, "RMB");
break;
}
console.log("refined_markdown: " + refined_markdown)
console.log("refined_markdown.length: " + refined_markdown.length)
if (refined_markdown.length > 0) {
//added += refined_markdown.length;
sel.splice(char, 1, refined_markdown);
refined_markdown = "";
}
}
}
//handle end case separatly; if there is more than 1 space at the end, the last array item is '</kbd><kbd>'
//that will result in an extra <kbd> pair, so remove it.
if (endSpaces > 0) {
sel.splice(endSpace, 1);
}
}
else { /*if there is no selection, assign sel to an array so that sel.join returns ""*/
var sel = ["",];
}
//put everything back together again
txta.value = pre + "<kbd>" + sel.join("") + "</kbd>" + post;
added = sel.join("").length + 11
//TODO, this is broken. Need to update cursor position calculation
txta.selectionStart = txta.selectionEnd = pre.length + ((start == end) ? 5 : added); //remove the selection and move
$(txta).focus();
updateMarkdownPreview(txta);
/*
// jQuery-way doesn't work :(
var evt = $.Event('keydown');
evt.which = 17;
evt.keyCode = 17; // Ctrl
$(txta).trigger(e);
// another failing attempt
$(txta).trigger({
type: "keydown",
which : 17
});
*/
}
//function to force update the live markdown render
function updateMarkdownPreview(element) {
var keyboardEvent = document.createEvent("KeyboardEvent");
var initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ? "initKeyboardEvent" : "initKeyEvent";
/*keyboardEvent[initMethod](
"keydown", // event type : keydown, keyup, keypress
true, // bubbles
true, // cancelable
window, // viewArg: should be window
false, // ctrlKeyArg
false, // altKeyArg
false, // shiftKeyArg
false, // metaKeyArg
17, // keyCodeArg : unsigned long the virtual key code, else 0
0 // charCodeArgs : unsigned long the Unicode character associated with the depressed key, else 0
);
element.dispatchEvent(keyboardEvent);*/
//horrible hack so undo after inserting kbd tags only removes kbd tags
//TODO not sure why this works, need to investigate at some point..
keyboardEvent[initMethod](
"keydown", // event type : keydown, keyup, keypress
true, // bubbles
true, // cancelable
document.defaultView, // viewArg: should be window
false, // ctrlKeyArg
false, // altKeyArg
false, // shiftKeyArg
false, // metaKeyArg
66, // keyCodeArg : unsigned long the virtual key code, else 0
0 // charCodeArgs : unsigned long the Unicode character associated with the depressed key, else 0
);
element.dispatchEvent(keyboardEvent);
keyboardEvent[initMethod](
"keydown", // event type : keydown, keyup, keypress
true, // bubbles
true, // cancelable
document.defaultView, // viewArg: should be window
false, // ctrlKeyArg
false, // altKeyArg
false, // shiftKeyArg
false, // metaKeyArg
8, // keyCodeArg : unsigned long the virtual key code, else 0
0 // charCodeArgs : unsigned long the Unicode character associated with the depressed key, else 0
);
element.dispatchEvent(keyboardEvent);
}
startInjection() //call initial startup function (bind keyboard shortcuts, etc.)
}
//get jquery on chrome, thanks to this SO post: http://stackoverflow.com/a/12751531/2730823
if (typeof jQuery === "function") {
console.log ("Running with local copy of jQuery!");
main (jQuery);
}
else {
console.log ("fetching jQuery from some 3rd-party server.");
add_jQuery (main, "1.7.2");
}
function add_jQuery (callbackFn, jqVersion) {
var jqVersion = jqVersion || "1.7.2";
var D = document;
var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
var scriptNode = D.createElement ('script');
scriptNode.src = 'http://ajax.googleapis.com/ajax/libs/jquery/'
+ jqVersion
+ '/jquery.min.js'
;
scriptNode.addEventListener ("load", function () {
var scriptNode = D.createElement ("script");
scriptNode.textContent =
'var gm_jQuery = jQuery.noConflict (true);\n'
+ '(' + callbackFn.toString () + ')(gm_jQuery);'
;
targ.appendChild (scriptNode);
}, false);
targ.appendChild (scriptNode);
}