Created
July 29, 2018 19:31
-
-
Save AdamWagner/3ae260fffe7108e0a741b2f98eee1962 to your computer and use it in GitHub Desktop.
Userscript: Google Docs Shortcuts / Vim mode
This file contains hidden or 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 Google Docs Shortcuts | |
// @include http*://docs.google.com/* | |
// @require http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
var editor = document.getElementsByClassName("docs-texteventtarget-iframe")[0].contentWindow.document.querySelector("[contenteditable=\"true\"]"); | |
var $editor = $(".docs-texteventtarget-iframe").contents().find("[contenteditable=\"true\"]"); | |
var utils = { | |
triggerMouseEvent: function(node, eventType) { | |
var clickEvent = document.createEvent('MouseEvents'); | |
clickEvent.initEvent(eventType, true, true); | |
node.dispatchEvent(clickEvent); | |
}, | |
click: function(target) { | |
this.triggerMouseEvent(target, "mouseover"); | |
this.triggerMouseEvent(target, "mousedown"); | |
this.triggerMouseEvent(target, "mouseup"); | |
}, | |
createKeyboardEvent: function(type, keyConfig) { | |
var defaultConfig = { | |
keyCode: null, | |
bubbles: true, | |
cancelable: true, | |
composed: true, // important for event to get through | |
shiftKey: vim.mode === "visual" ? true : false, | |
metaKey: false, | |
altKey: false, | |
} | |
var mergedConfig = Object.assign(defaultConfig, keyConfig); // 2nd object takes priority | |
return new KeyboardEvent(type, mergedConfig); | |
}, | |
sendKey: function(type, keyConfig) { | |
var e = this.createKeyboardEvent(type, keyConfig); | |
editor.dispatchEvent(e); | |
}, | |
commandKeyCombo: function (keyConfig) { | |
var cmd = { | |
keyCode: 91, | |
metaKey: true | |
}; | |
var cmdDown = this.createKeyboardEvent("keydown", cmd); | |
var keyDown = this.createKeyboardEvent("keydown", keyConfig); | |
var keyUp = this.createKeyboardEvent("keyup", keyConfig); | |
var keyPress = this.createKeyboardEvent("keypress", keyConfig); | |
var cmdUp = this.createKeyboardEvent("keydown", cmd); | |
var cmdPress = this.createKeyboardEvent("keypress", cmd); | |
editor.dispatchEvent(cmdDown); | |
editor.dispatchEvent(keyDown); | |
editor.dispatchEvent(keyUp); | |
editor.dispatchEvent(keyPress); | |
editor.dispatchEvent(cmdUp); | |
editor.dispatchEvent(cmdPress); | |
}, | |
}; | |
function setCursorWidth (width) { | |
$("head").append("<style>.kix-cursor-caret { border-width: " + width + "; opacity:0.5; }</style>"); | |
} | |
function pasteClipboard(){ | |
var pasteMenuItem = $("span.goog-menuitem-accel:contains('⌘V')")[0]; | |
utils.click(pasteMenuItem); | |
} | |
function cutSelection(){ | |
var cutMenuItem = $("span.goog-menuitem-accel:contains('⌘X')")[0]; | |
utils.click(cutMenuItem); | |
} | |
function stopProp(e){ | |
e.preventDefault(); | |
e.stopImmediatePropagation(); | |
} | |
var History = { | |
history: [], | |
add: function(key){ | |
this.history.push(key); | |
this.history = this.history.slice(-5); | |
}, | |
get: function(){ | |
return this.history; | |
}, | |
getPrevKey: function(){ | |
return this.history[this.history.length - 2]; | |
}, | |
reset: function(){ | |
this.history = []; | |
}, | |
getNums: function(){ | |
var numCodes = Object.keys(codeToNum).map(parseFloat); | |
var nums = this.history.filter(item => numCodes.indexOf(item) > -1); | |
var number = ""; | |
for (var i = 0; i < nums.length; i++) { | |
number = number + codeToNum[nums[i]].toString(); | |
} | |
this.reset(); | |
return parseFloat(number) || 1; | |
}, | |
} | |
var keyCodes = { | |
left: 37, | |
down: 40, | |
up: 38, | |
right: 39, | |
end: 35, | |
home: 36, | |
delete: 46, | |
backspace: 8, | |
e: 69, // move to end of next word | |
c: 67, // copy | |
g: 71, | |
v: 86, // paste | |
b: 66, // bold | |
d: 68, // delete | |
enter: 13, | |
} | |
var moveKeys = { | |
KeyH: keyCodes.left, | |
KeyJ: keyCodes.down, | |
KeyK: keyCodes.up, | |
KeyL: keyCodes.right | |
} | |
var codeToNum = { | |
48:0, | |
49:1, | |
50:2, | |
51:3, | |
52:4, | |
53:5, | |
54:6, | |
55:7, | |
56:8, | |
57:9, | |
} | |
var vim = { | |
mode: "insert" | |
}; | |
vim.switchToNormalMode = function () { | |
vim.mode = "normal"; | |
setCursorWidth("9px"); | |
$editor.off("keydown", vim.handleInsertMode); | |
$editor.on("keydown", vim.handleNormalMode); | |
}; | |
vim.switchToInsertMode = function () { | |
vim.mode = "insert"; | |
setCursorWidth("2px"); | |
$editor.on("keydown", vim.handleInsertMode); | |
$editor.off("keydown", vim.handleNormalMode); | |
}; | |
vim.handleInsertMode = function (e) { | |
if (e.key == "Escape") { | |
console.log('esc pressed') | |
vim.switchToNormalMode(); | |
} | |
}; | |
vim.handleNormalMode = function (e) { | |
var oe = e.originalEvent; | |
console.log(oe); | |
// only track keys the user types, not generated events | |
if (oe.isTrusted && !oe.metaKey && !oe.shiftKey) { | |
History.add(oe.keyCode); | |
} | |
// block number keys | |
var currentKey = e.keyCode; | |
var numCodes = Object.keys(codeToNum).map(parseFloat); | |
if (numCodes.indexOf(currentKey) > -1 && !oe.metaKey && !oe.shiftKey) { | |
stopProp(e); | |
} | |
// `i` enter insert mode | |
if (e.key == "i") { | |
stopProp(e); | |
vim.switchToInsertMode(); | |
return true; | |
} | |
// `gg` go to top of document | |
if (oe.code === "KeyG") { | |
if (History.getPrevKey() === keyCodes.g ) { | |
utils.sendKey("keydown", {keyCode: keyCodes.up, metaKey: true}) | |
} | |
stopProp(e); | |
} | |
// change word | |
if(oe.code === "KeyC") {e.preventDefault()} | |
if (oe.code === "KeyW") { | |
if (History.getPrevKey() === keyCodes.c) { | |
utils.sendKey("keydown", {keyCode: keyCodes.right, altKey: true, shiftKey: true}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
vim.switchToInsertMode(); | |
} | |
stopProp(e); | |
} | |
// `shift + g` go to bottom of document | |
if(oe.code === "KeyG" && oe.shiftKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.down, metaKey: true}) | |
} | |
// listen for ctrl + hjkl, send left down up right | |
for (var key in moveKeys) { | |
if(oe.code === key && !oe.metaKey && !oe.shiftKey) { | |
stopProp(e); | |
var repeat = History.getNums(); | |
for (var i = 0; i < repeat; i++) { | |
utils.sendKey("keydown", {keyCode: moveKeys[key]}); | |
} | |
} | |
} | |
// `e` go to end of next word | |
if(oe.code === "KeyE") { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.right, altKey: true}) | |
} | |
// `b` go to beginning of previous word | |
if(oe.code === "KeyB" && !oe.shiftKey && !oe.metaKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode:keyCodes.left, altKey: true, }) | |
} | |
// `shift + 4` go to end of line | |
if(oe.shiftKey && oe.code === "Digit4") { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.end}) | |
} | |
// `0` goes to beginning of line | |
if(oe.code === "Digit0" && !oe.shiftKey && !oe.altKey && !oe.metaKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.home}) | |
} | |
// `shift + j` deletes space below current line | |
if( oe.shiftKey && oe.code === "KeyJ") { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.end}); | |
utils.sendKey("keydown", {keyCode: keyCodes.delete}); | |
} | |
// `o` enter insert mode on line below | |
if(oe.code === "KeyO" && !oe.shiftKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.end}); | |
utils.sendKey("keydown", {keyCode: keyCodes.enter}); | |
vim.switchToInsertMode(); | |
} | |
// `shift + o` enter insert mode on line above | |
if( oe.shiftKey && oe.code === "KeyO") { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.home}); | |
utils.sendKey("keydown", {keyCode: keyCodes.enter}); | |
utils.sendKey("keydown", {keyCode: keyCodes.up}); | |
vim.switchToInsertMode(); | |
} | |
// `shift + d` deletes until end of line | |
if(oe.shiftKey && oe.code === "KeyD" && !oe.metaKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.right, metaKey:true, shiftKey:true}); | |
cutSelection() | |
} | |
// `shift + a` enter insert mode at end of line | |
if(oe.shiftKey && oe.code === "KeyA" && !oe.metaKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.right, metaKey:true}); | |
vim.switchToInsertMode(); | |
} | |
// `v` enters "visual" mode. | |
// Simulated by applying the shift key to all keys pressed | |
// while in this mode | |
if(oe.code === "KeyV" && !oe.metaKey) { | |
stopProp(e); | |
vim.mode === "normal" ? vim.mode = "visual" : vim.mode = "normal"; | |
} | |
// `y` to copy | |
if(oe.code === "KeyY") { | |
stopProp(e); | |
vim.mode = "normal"; | |
var copyMenuItem = $("span.goog-menuitem-accel:contains('⌘C')")[0]; | |
utils.click(copyMenuItem) | |
} | |
// TODO: update to detect selection. If selection, cut. If not, backspace. | |
// `x` to cut | |
if(oe.code === "KeyX") { | |
stopProp(e); | |
vim.mode = "normal"; | |
cutSelection() | |
} | |
// `p` to paste | |
if(oe.code === "KeyP") { | |
stopProp(e); | |
pasteClipboard() | |
} | |
// `dd` to delete a line | |
if(oe.code === "KeyD") { | |
if (vim.mode === "visual") {cutSelection();} | |
if (History.getPrevKey() === keyCodes.d) { | |
utils.sendKey("keydown", {keyCode: keyCodes.left, metaKey: true}) | |
utils.sendKey("keydown", {keyCode: keyCodes.right, metaKey: true, shiftKey: true}) | |
cutSelection(); | |
// TODO: Determine if we're in a list. If so, these kestrokes are required to | |
// undo the list. If not, they'll delete parts of other words unintentionally. | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
} | |
stopProp(e); | |
} | |
// `u` to undo | |
if(oe.code === "KeyU") { | |
stopProp(e); | |
var undoMenuItem = $("span.goog-menuitem-accel:contains('⌘Z')")[0]; | |
utils.click(undoMenuItem); | |
} | |
// ctrl + cmd + arrows move lines up / down | |
// Caution, doesn't work if triggered multiple times in a row, quickly. | |
if(oe.ctrlKey && oe.metaKey && (oe.key === 'ArrowUp' || oe.key === 'ArrowDown')) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.home}) | |
utils.sendKey("keydown", {keyCode: keyCodes.end, shiftKey: true}) | |
cutSelection(); | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
utils.sendKey("keydown", {keyCode: keyCodes.backspace}) | |
if (oe.key === 'ArrowDown') { | |
utils.sendKey("keydown", {keyCode: keyCodes.down}) | |
utils.sendKey("keydown", {keyCode: keyCodes.down}) | |
} | |
utils.sendKey("keydown", {keyCode: keyCodes.home}) | |
utils.sendKey("keydown", {keyCode: keyCodes.enter}) | |
utils.sendKey("keydown", {keyCode: keyCodes.up}) | |
pasteClipboard(); | |
} | |
// `shift` + b to bold line | |
if(oe.shiftKey && oe.code === "KeyB" && !oe.metaKey) { | |
stopProp(e); | |
utils.sendKey("keydown", {keyCode: keyCodes.home}) | |
utils.sendKey("keydown", {keyCode: keyCodes.end, shiftKey: true}) | |
utils.sendKey("keydown", {keyCode: keyCodes.b, metaKey: true}) | |
utils.sendKey("keydown", {keyCode: keyCodes.left, metaKey: true}) | |
} | |
// `escape` to exit visual mode | |
if (oe.key == "Escape") { | |
vim.mode = "normal"; | |
utils.sendKey("keyDown", {keyCode: keyCodes.end}) | |
} | |
}; | |
function handleGlobalShortcuts(e) { | |
var oe = e.originalEvent; | |
var toggleSpacingButtons = { | |
above: $("span.goog-menuitem-label:contains('space before')")[0], | |
below: $("span.goog-menuitem-label:contains('space after')")[0], | |
} | |
function toggleSpace(direction) { | |
utils.click(toggleSpacingButtons[direction]); | |
} | |
if(oe.metaKey && oe.shiftKey && oe.code === "KeyJ") { | |
toggleSpace('above'); | |
oe.preventDefault(); | |
} | |
if(oe.metaKey && !oe.shiftKey && oe.code === "KeyJ") { | |
toggleSpace('below'); | |
oe.preventDefault(); | |
} | |
} | |
$editor.on("keydown", vim.handleInsertMode); // start in insert mode | |
$editor.on("keydown", handleGlobalShortcuts); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment