Skip to content

Instantly share code, notes, and snippets.

@masterchop
Forked from AdamWagner/script.js
Created November 1, 2020 21:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masterchop/4ffb24b564b7430346c0ad2c07d7713f to your computer and use it in GitHub Desktop.
Save masterchop/4ffb24b564b7430346c0ad2c07d7713f to your computer and use it in GitHub Desktop.
Userscript: Google Docs Shortcuts / Vim mode
// ==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