- // ==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);
- }