More Airtable hotkeys / shortcuts!

I put together a bunch of hotkeys to make my own life easier, and figured I might as well clean it up and post it in case any one else found them useful!

This all works via a Tampermonkey script, and installation instructions can be found below

Demo

Hotkey Demo

Shortcut List

Keyboard Shortcut Action Gif
/Left Alt + ⇧ Left Shift + ' Add a new field Add a field
/Left Alt + ⇧ Right Shift Customize Field modal - Click “Save” Save field
/Left Alt + ' Next field header Next header
/Left Alt + ; Previous field header Previous header
s Next field menu option Next field menu option
w Previous field menu option Previous field menu option
/Left Alt + ↵ Enter “Click” selected field menu option Select field menu option
/Left Alt + ↵ Enter Open field menu for selected field Open field menu
Escape Deselect Deselect
/Left Alt + ⇧ Left Shift + 1 Select first cell Select first cell

Want another hotkey? Tell me about it and I’ll see what I can do!

How to use

  1. Install Tampermonkey
  2. Open the extension, click “Create a new script”, and paste the code found at the end of the post into it and save
  3. You’re done!
    Script installation

Notes

I’ve tested this mostly on Firefox on a Mac, but have also tested it a bit on:

  1. Chrome - Mac
  2. Firefox - Windows
  3. Chrome - Windows

And so it should be fine, but do let me know if you hit any issues and I’ll do my best to fix it

You can also customize the keys used as well, and instructions on how can be found in the code itself!

// ==UserScript==
// @name         Airtable Hotkeys
// @description  Adds custom hotkeys to Airtable
// @author       Adam - The Time Saving Company
// @author       adam@thetimesaving.company
// @match        https://airtable.com/*
// @grant        GM_addStyle

// ==/UserScript==
let osLeftKey

if(navigator.userAgent.includes("Windows")){
    osLeftKey = "AltLeft"
}
else if(navigator.userAgent.includes("Firefox")){
    osLeftKey = "OSLeft"
}
else{
    osLeftKey = "MetaLeft"
}

// CUSTOMIZATION START
// Changes made here will overwrite the default hotkeys and may cause some weirdness
// Feel free to make changes, but do check against the default hotkey list
// We can use this site to find out what the keys' name is: https://www.toptal.com/developers/keycode/for/enter
// Specifically, look for the value of the "event.code" section

// Set the two keys to press in conjunction that will then hit the "Save" button so that we don't have to use the mouse to click it anymore
// IMPORTANT THING TO NOTE: The key "Enter" cannot be used here as it's in use by Airtable
let saveKey1 = osLeftKey
let saveKey2 = "ShiftRight"

// Set the keys to press in conjunction that will then hit the "Add New Field" button
let addFieldKey1 = osLeftKey
let addFieldKey2 = "ShiftLeft"
let addFieldKey3 = "Quote"

// Set the keys to press in conjunction that will select the field header to the left
let fieldHeaderPrev1 = osLeftKey
let fieldHeaderPrev2 = "Semicolon"

// Set the keys to press in conjunction that will select the field header to the right
let fieldHeaderNext1 = osLeftKey
let fieldHeaderNext2 = "Quote"

// Set the keys to press in conjunction that will open the menu of the selected field header i.e. right clicking it
let fieldMenuOpenKey1 = osLeftKey
let fieldMenuOpenKey2 = "Enter"

// Set the keys to press to move up and down the field's menu
let fieldMenuNavigateUp = "KeyW"
let fieldMenuNavigateDown = "KeyS"

// Set the keys to press in conjunction that will click the selected menu item (i.e. "Customize type", "Rename" etc)
let fieldMenuOptionSelectKey1 = osLeftKey
let fieldMenuOptionSelectKey2 = "Enter"

// Set the key to press that will unselect everything
let escapeKey = "Escape"

let selectFirstCellKey1 = osLeftKey // CMD / Windows button
let selectFirstCellKey2 = "ShiftLeft"
let selectFirstCellKey3 = "Digit1"
// CUSTOMIZATION END

GM_addStyle('.column-selected-by-hotkey  { border: blue 1px solid }');
GM_addStyle('.columnMenuItem-selected-by-hotkey  { background-color: grey !important}');

let keysPressed = {};
let selectedColumnIndex = 0;

document.addEventListener('keydown', (event) => {

    keysPressed[event.code] = true;
    switch (true) {
        case keysPressed[selectFirstCellKey1] && keysPressed[selectFirstCellKey2] && event.code == selectFirstCellKey3:
            selectFirstCell()
            break;
        case keysPressed[addFieldKey1] && keysPressed[addFieldKey2] && event.code == addFieldKey3:
            addField()
            break;
        case document.getElementsByClassName('column-selected-by-hotkey')[0] && event.code == escapeKey:
            deselect()
            break;
        case keysPressed[saveKey1] && event.code == saveKey2:
            saveFieldChanges()
            break;
        case document.querySelector("[data-tutorial-selector-id='columnMenuItem-delete']") && keysPressed[fieldMenuNavigateUp]:
            navigateFieldMenuOptionsUp()
            break;
        case document.querySelector("[data-tutorial-selector-id='columnMenuItem-delete']") && keysPressed[fieldMenuNavigateDown]:
            navigateFieldMenuOptionsDown()
            break;
        case keysPressed[fieldHeaderPrev1] && event.code == fieldHeaderPrev2:
            navigateFieldHeadersLeft()
            break;
        case keysPressed[fieldHeaderNext1] && event.code == fieldHeaderNext2:
            navigateFieldHeadersRight()
            break;
        case document.querySelector('.column-selected-by-hotkey') && keysPressed[fieldMenuOpenKey1] && event.code == fieldMenuOpenKey2:
            rightClickSelectedFieldHeader()
            break;
        case document.getElementsByClassName('columnMenuItem-selected-by-hotkey')[0] && keysPressed[fieldMenuOptionSelectKey1] && event.code == fieldMenuOptionSelectKey2:
            clickSelectedFieldMenuItem()
            break;
    }
});

document.addEventListener('keyup', (event) => {
    if(navigator.userAgent.includes("Windows")){
        delete keysPressed[event.code]
    }
    else{
        keysPressed = {}
    }
});

function selectFirstCell() {
    simulateMouseClick(document.querySelector(".primary.cell.read[data-rowindex='0']"))
    clearClass('column-selected-by-hotkey')
}
function addField() {
    document.getElementsByClassName('gridHeaderRowAddFieldButton')[0].click()
    clearClass('column-selected-by-hotkey')
}

function deselect() {
    document.getElementsByClassName('column-selected-by-hotkey')[0].classList.remove('column-selected-by-hotkey')
    selectedColumnIndex = 0
}

function saveFieldChanges() {
    if (document.querySelector('[aria-label="Field customization"]')) {
        document.querySelector('[aria-label="Field customization"]').getElementsByClassName("text-white flex-none flex-inline items-center justify-center rounded blue blueDark1-hover blueDark1-focus strong pointer focus-visible")[0].click()
    }
    else if (document.querySelector('[aria-label="Create field"]')) {
        document.querySelector('[aria-label="Create field"]').querySelector('[data-tutorial-selector-id="createColumnDialogCreateButton"]').click()
    }
}

function navigateFieldMenuOptionsUp() {
    if (document.getElementsByClassName('columnMenuItem-selected-by-hotkey')[0]) {
        setNextMenuItem("up")
    }
    else {
        document.querySelector("[data-tutorial-selector-id='columnMenuItem-delete']").classList.add('columnMenuItem-selected-by-hotkey')
    }
}

function navigateFieldMenuOptionsDown() {
    if (document.getElementsByClassName('columnMenuItem-selected-by-hotkey')[0]) {
        setNextMenuItem("down")
    }
    else {
        document.querySelector("[data-tutorial-selector-id='columnMenuItem-rename']").classList.add('columnMenuItem-selected-by-hotkey')
    }
}

function navigateFieldHeadersLeft() {
    // Select Field Left
    if (document.getElementsByClassName('column-selected-by-hotkey').length === 0) {
        document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
    }
    else {
        selectedColumnIndex--
        if (document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]')) {
            document.querySelector('[data-columnindex="' + (selectedColumnIndex + 1) + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.remove('column-selected-by-hotkey')
            document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
        }
        else {
            document.querySelector('[data-columnindex="' + (selectedColumnIndex + 1) + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.remove('column-selected-by-hotkey')
            let columnIndex = document.querySelectorAll('[data-tutorial-selector-id="gridHeaderCell"]').length

            selectedColumnIndex = columnIndex - 1
            document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
        }
    }
}

function navigateFieldHeadersRight() {
    // Select Field Right
    if (document.getElementsByClassName('column-selected-by-hotkey').length === 0) {
        document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
    }
    else {
        selectedColumnIndex++
        if (document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]')) {
            document.querySelector('[data-columnindex="' + (selectedColumnIndex - 1) + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.remove('column-selected-by-hotkey')
            document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
        }
        else {
            document.querySelector('[data-columnindex="' + (selectedColumnIndex - 1) + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.remove('column-selected-by-hotkey')
            selectedColumnIndex = 0
            document.querySelector('[data-columnindex="' + selectedColumnIndex + '"][data-tutorial-selector-id="gridHeaderCell"]').firstElementChild.classList.add('column-selected-by-hotkey')
        }
    }
}

function rightClickSelectedFieldHeader() {
    // Right click on selected field to bring up the field menu
    let element = document.querySelector('.column-selected-by-hotkey')
    element.classList.remove('column-selected-by-hotkey')

    let ev2 = new MouseEvent("contextmenu", {
        bubbles: true,
        cancelable: false,
        button: 2,
        buttons: 0,
        clientX: element.getBoundingClientRect().x,
        clientY: element.getBoundingClientRect().y
    });
    element.dispatchEvent(ev2);

    document.querySelector("[data-tutorial-selector-id='columnMenuItem-changeType']").classList.add('columnMenuItem-selected-by-hotkey')
}

function clickSelectedFieldMenuItem() {
    document.getElementsByClassName('columnMenuItem-selected-by-hotkey')[0].click()
}

function clearClass(currentClass) {
    if (document.getElementsByClassName(currentClass)[0]) {
        document.getElementsByClassName(currentClass)[0].classList.remove(currentClass)
    }
}

function setNextMenuItem(direction) {
    let currentMenuItem = document.getElementsByClassName('columnMenuItem-selected-by-hotkey')[0]
    let currentSelectorId = currentMenuItem.dataset.tutorialSelectorId
    let nextSelectorId

    if (direction === "up") {
        switch (currentSelectorId) {
            case "columnMenuItem-delete":
                if (document.querySelector("[data-tutorial-selector-id='columnMenuItem-showDependencies']")) {
                    nextSelectorId = "columnMenuItem-showDependencies"
                }
                else {
                    nextSelectorId = "columnMenuItem-hide"
                }
                break;
            case "columnMenuItem-showDependencies":
                nextSelectorId = "columnMenuItem-hide"
                break;
            case "columnMenuItem-hide":
                nextSelectorId = "columnMenuItem-groupBy"
                break;
            case "columnMenuItem-groupBy":
                nextSelectorId = "columnMenuItem-addFilter"
                break;
            case "columnMenuItem-addFilter":
                nextSelectorId = "columnMenuItem-sortDescending"
                break;
            case "columnMenuItem-sortDescending":
                nextSelectorId = "columnMenuItem-sortAscending"
                break;
            case "columnMenuItem-sortAscending":
                nextSelectorId = "columnMenuItem-insertRight"
                break;
            case "columnMenuItem-insertRight":
                nextSelectorId = "columnMenuItem-insertLeft"
                break;
            case "columnMenuItem-insertLeft":
                nextSelectorId = "columnMenuItem-duplicate"
                break;
            case "columnMenuItem-duplicate":
                nextSelectorId = "columnMenuItem-editPermissions"
                break;
            case "columnMenuItem-editPermissions":
                nextSelectorId = "columnMenuItem-editDescription"
                break;
            case "columnMenuItem-editDescription":
                nextSelectorId = "columnMenuItem-rename"
                break;
            case "columnMenuItem-rename":
                nextSelectorId = "columnMenuItem-changeType"
                break;
            case "columnMenuItem-changeType":
                nextSelectorId = "columnMenuItem-delete"
        }
    }
    else {
        switch (currentSelectorId) {
            case "columnMenuItem-delete":
                nextSelectorId = "columnMenuItem-changeType"
                break;
            case "columnMenuItem-showDependencies":
                nextSelectorId = "columnMenuItem-delete"
                break;
            case "columnMenuItem-hide":
                if (document.querySelector("[data-tutorial-selector-id='columnMenuItem-showDependencies']")) {
                    nextSelectorId = "columnMenuItem-showDependencies"
                }
                else {
                    nextSelectorId = "columnMenuItem-delete"
                }
                nextSelectorId = "columnMenuItem-showDependencies"
                break;
            case "columnMenuItem-groupBy":
                nextSelectorId = "columnMenuItem-hide"
                break;
            case "columnMenuItem-addFilter":
                nextSelectorId = "columnMenuItem-groupBy"
                break;
            case "columnMenuItem-sortDescending":
                nextSelectorId = "columnMenuItem-addFilter"
                break;
            case "columnMenuItem-sortAscending":
                nextSelectorId = "columnMenuItem-sortDescending"
                break;
            case "columnMenuItem-insertRight":
                nextSelectorId = "columnMenuItem-sortAscending"
                break;
            case "columnMenuItem-insertLeft":
                nextSelectorId = "columnMenuItem-insertRight"
                break;
            case "columnMenuItem-duplicate":
                nextSelectorId = "columnMenuItem-insertLeft"
                break;
            case "columnMenuItem-editPermissions":
                nextSelectorId = "columnMenuItem-duplicate"
                break;
            case "columnMenuItem-editDescription":
                nextSelectorId = "columnMenuItem-editPermissions"
                break;
            case "columnMenuItem-rename":
                nextSelectorId = "columnMenuItem-editDescription"
                break;
            case "columnMenuItem-changeType":
                nextSelectorId = "columnMenuItem-rename"
                break;
        }
    }

    currentMenuItem.classList.remove('columnMenuItem-selected-by-hotkey')
    document.querySelector("[data-tutorial-selector-id='" + nextSelectorId + "']").classList.add('columnMenuItem-selected-by-hotkey')
}

function simulateMouseClick(targetNode) {
    function triggerMouseEvent(targetNode, eventType) {
        var clickEvent = document.createEvent('MouseEvents');
        clickEvent.initEvent(eventType, true, true);
        targetNode.dispatchEvent(clickEvent);
    }
    ["mouseover", "mousedown", "mouseup", "click"].forEach(function (eventType) {
        triggerMouseEvent(targetNode, eventType);
    });
}
3 Likes