kbd formatting button for stackexchange

Adds the ability to quickly insert kbd formatting tags in the SE editor

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 = "&#9096; Ctrl";
                            break;
                        case "alternate":
                        case "alt":
                            refined_markdown = "&#9095; Alt";
                            break;
                        case "shift":
                            refined_markdown = "&#8679; Shift";
                            break;
                        case "tab":
                            refined_markdown = "&#8633; Tab";
                            break;
                        case "delete":
                        case "del":
                            refined_markdown = "&#8998; Delete";
                            break;
                        case "enter":
                        case "return":
                            refined_markdown = "&#9166; Enter";
                            break;
                        case "backspace":
                            refined_markdown = "&#10229; Backspace";
                            break;
                        case "pageup":
                        case "pgup":
                            refined_markdown = "&#8670; Page up";
                            break;
                        case "pagedown":
                        case "pgdn":
                            refined_markdown = "&#8671; Page down";
                            break;
                        case "printscreen":
                            refined_markdown = "&#9113; Print Screen";
                            break;
                        case "up":
                            refined_markdown = "&#8593; Up arrow";
                            break;
                        case "left":
                            refined_markdown = "&#8592; Left arrow";
                            break;
                        case "right":
                            refined_markdown = "&#8594; Right arrow";
                            break;
                        case "down":
                            refined_markdown = "&#8595; Down arrow";
                            break;
                        case "caps":
                        case "capslock":
                            refined_markdown = "&#8682; Caps Lock"; //maybe use &#8684; 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 = "&#9670; Meta";
                            break;


                            //mac thingies
                        case "command":
                        case "cmd":
                            refined_markdown = "&#8984; Cmd";
                            break;
                        case "option":
                        case "opt":
                            refined_markdown = "&#8997; 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);
}