7 of 16 Updated button cosmetics

<kbd> markup shortcuts

Inspired by the question, "Markdown syntax for <kbd>foo</kbd>", this script adds both a button and a keyboard shortcut to each post's <textarea> (when editing posts).

Press the button or press AltK on the keyboard and <kbd></kbd> will be inserted at the cursor.
If text is selected, it will be wrapped by the <kbd> tags instead.

Tested the script on both Firefox (needs the Greasemonkey add-on) and Chrome (might work for Opera too).

Screenshot:
This script in action


To install, click here (Add_kbd_shortcut.user.js install at Github).

To preview the source, See the copy below, or click here (Add_kbd_shortcut.user.js preview at Github).


Known limitations, possible future enhancements:

  1. When pressing the button, the preview does not immediately update. Make any other changes to the text (normal editing), and the preview catches up.
    The keyboard shortcut always updates the preview instantaneously.

    Fixed!

  2. If text with trailing space(s) is selected, the spaces get wrapped, with <kbd> too. This is different from the other markup shortcuts.
    EG: If foo  is selected, the wrapped product will be <kbd>foo </kbd>, not <kbd>foo</kbd> .

    Fixed! This shortcut now handles leading and trailing whitespace the same way that other SE shortcuts (Bold, Italic, Code) do.

  3. This shortcut does not reverse itself. For example, if foo is selected, pressing AltK once yields <kbd>foo</kbd>. Pressing it again yields <kbd><kbd>foo</kbd></kbd>.

    Fixed! This shortcut now wraps and unwraps the same way that other SE shortcuts (Bold, Italic, Code) do.

  4. For very low rep users, the newbie AJAX notifications can erase the shortcut button. The keyboard shortcut continues to work though.

  5. Improved button placement? See Manishearth's answer.




Copy of the code for convenience:

// ==UserScript==
// @name            StackExchange, Add kbd shortcut
// @namespace       StackExchange
// @description     Adds a button and a keyboard shortcut (Alt-K) to add <kbd> tags.
// @version         1.1
// @match           http://*.askubuntu.com/*
// @match           http://*.onstartups.com/*
// @match           http://*.serverfault.com/*
// @match           http://*.stackapps.com/*
// @match           http://*.stackexchange.com/*
// @match           http://*.stackoverflow.com/*
// @match           http://*.superuser.com/*
// ==/UserScript==

function AddKbdShortcuts ($) {
    $("textarea.wmd-input").each (AddKbdButtonAsNeeded);

    //.on() not working right!!
    $("textarea.wmd-input").live ("focus",   AddKbdButtonAsNeeded);
    $("textarea.wmd-input").live ("keydown", InsertKbdTagByKeypress);
    $("li.wmd-kbd-button") .live ("click",   InsertKbdTagByClick);

    /*------------*/
    function AddKbdButtonAsNeeded () {
        var jThis   = $(this);
        if ( ! jThis.data ("hasKbdBttn") ) {
            //--- Find the button bar and add our button.
            var btnBar  = jThis.prevAll ("div.wmd-button-bar");
            if (btnBar.length) {
                //--- The button bar takes a while to AJAX-in.
                var bbListTimer = setInterval ( function() {
                        var bbList  = btnBar.find ("ul.wmd-button-row");
                        if (bbList.length) {
                            clearInterval (bbListTimer);
                            bbList.append (
                                  '<li class="wmd-button wmd-kbd-button" '
                                + 'title="Keyboard tag &lt;kbd&gt; Alt+K" '
                                + 'style="left: 380px;">'
                                + '<span><kbd style="border: 0px;">kbd</kbd></span></li>'
                            );
                            jThis.data ("hasKbdBttn", true);
                        }
                    },
                    100
                );
            }
        }
    }

    function InsertKbdTagByKeypress (zEvent) {
        //--- On Alt-K, insert the <kbd> set. Ignore all other keys.
        if (zEvent.altKey  &&  zEvent.which == 75) {
            InsertKbdTag (this);
            return false;
        }
        return true;
    }

    function InsertKbdTagByClick (zEvent) {
        //--- From the clicked button, find the matching textarea.
        var targArea    = $(this).parents ("div.wmd-button-bar")
                        .nextAll ("textarea.wmd-input");

        InsertKbdTag (targArea[0]);
        targArea.focus ();
        try {
            //--- This is a utility function that SE currently provides on its pages.
            StackExchange.MarkdownEditor.refreshAllPreviews ();
        }
        catch (e) {
            console.warn ("***Userscript error: refreshAllPreviews() is no longer defined!");
        }
    }

    function InsertKbdTag (node) {
        //--- Wrap selected text or insert at curser.
        var tagLength       = 5;    // Tag is: "<kbd>"
        var oldText         = node.value || node.textContent;
        var newText;
        var iTargetStart    = node.selectionStart;
        var iTargetEnd      = node.selectionEnd;
        var selectedText    = oldText.slice (iTargetStart, iTargetEnd);
        var possWrappedTxt;
        try {
            //--- Lazyman's overrun checking...
            possWrappedTxt  = oldText.slice (
                                iTargetStart - tagLength,
                                iTargetEnd   + tagLength + 1
                            );
        }
        catch (e) {
            possWrappedTxt  = "Text can't be wrapped, cause we overran the string.";
        }

        /*--- Is the current selection wrapped?  If so, just unwrap it.
            This works the same way as SE's bold, italic, code, etc...
            "]text["                --> "<kbd>]text[</kbd>"
            "<kbd>]text[</kbd>"     --> "]text["
            "]<kbd>text</kbd>["     --> "<kbd>]<kbd>text</kbd>[</kbd>"

            Except that:
            "]["                    --> "<kbd>][</kbd>"
            "<kbd>][</kbd>"         --> "]["
            with no placeholder text.
            Note that `]` and `[` denote the selected text here.
        */
        if (possWrappedTxt  &&
            selectedText    == possWrappedTxt.replace (/^<kbd>((?:.|\n|\r)*)<\/kbd>$/, "$1")
        ) {
            iTargetStart   -= tagLength;
            iTargetEnd     += tagLength + 1;
            newText         = oldText.slice (0, iTargetStart)
                            + selectedText + oldText.slice (iTargetEnd)
                            ;
            iTargetEnd      = iTargetStart + selectedText.length;
        }
        else {
            /*--- Here we will wrap the selection in our tags, but there is one extra
                condition.  We don't want to wrap leading or trailing whitespace.
            */
            var trimSelctd  = selectedText.match (/^(\s*)(\S?(?:.|\n|\r)*\S)(\s*)$/)
                            || ["", "", "", ""]
                            ;
            if (trimSelctd.length != 4) {
                console.warn ("***Userscript error: unexpected failure of whitespace RE.");
            }
            else {
                newText         = trimSelctd[1]     //-- Leading whitespace, if any.
                                + '<kbd>' + trimSelctd[2] + '</kbd>'
                                + trimSelctd[3]     //-- Trailing whitespace, if any.
                                ;
                newText         = oldText.slice (0, iTargetStart)
                                + newText + oldText.slice (iTargetEnd)
                                ;
                iTargetStart   += tagLength + trimSelctd[1].length;
                iTargetEnd     += tagLength - trimSelctd[3].length;
            }
        }

        //console.log (newText);
        node.value          = newText;
        //--- After using spelling corrector, this gets buggered, hence the multiple sets.
        node.textContent    = newText;

        //--- Have to reset the selection, since we overwrote the text.
        node.selectionStart = iTargetStart;
        node.selectionEnd   = iTargetEnd;
    }
}


withPages_jQuery (AddKbdShortcuts);


function withPages_jQuery (NAMED_FunctionToRun) {
    //--- Use named functions for clarity and debugging...
    var funcText        = NAMED_FunctionToRun.toString ();
    var funcName        = funcText.replace (/^function\s+(\w+)\s*\((.|\n|\r)+$/, "$1");
    var script          = document.createElement ("script");
    script.textContent  = funcText + "\n\n";
    script.textContent += 'jQuery(document).ready( function () {' + funcName + '(jQuery);} );';
    document.body.appendChild (script);
};