Created
March 30, 2018 20:45
-
-
Save gandalf3/468e3704132a18dab7875341cca68571 to your computer and use it in GitHub Desktop.
kbd formatting button for stackexchange
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name kbd formatting button for stackexchange | |
// @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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment