Skip to content

Commit

Permalink
refactor: optimize table operation shortcuts (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing authored Oct 22, 2023
1 parent 2ec8572 commit 0da7488
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 137 deletions.
4 changes: 3 additions & 1 deletion console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@codemirror/commands": "^6.3.0",
"@codemirror/language": "^6.9.1",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.19.0",
Expand All @@ -29,7 +30,8 @@
"remark-parse": "^11.0.0",
"remark-rehype": "^11.0.0",
"unified": "^11.0.3",
"vue": "^3.3.4"
"vue": "^3.3.4",
"w3c-keyname": "^2.2.8"
},
"devDependencies": {
"@iconify-json/mdi": "^1.1.54",
Expand Down
6 changes: 6 additions & 0 deletions console/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions console/src/lib/codemirror-kit/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// code referenced from https://github.com/codemirror/view/blob/main/src/browser.ts

let nav: any =
typeof navigator != "undefined"
? navigator
: { userAgent: "", vendor: "", platform: "" };
const ie_edge = /Edge\/(\d+)/.exec(nav.userAgent);
const ie_upto10 = /MSIE \d/.test(nav.userAgent);
const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(nav.userAgent);
const ie = !!(ie_upto10 || ie_11up || ie_edge);
const safari = !ie && /Apple Computer/.test(nav.vendor);
const ios =
safari && (/Mobile\/\w+/.test(nav.userAgent) || nav.maxTouchPoints > 2);
const windows = /Win/.test(nav.platform);

export default {
mac: ios || /Mac/.test(nav.platform),
windows: /Win/.test(nav.platform),
linux: /Linux|X11/.test(nav.platform),
};
260 changes: 260 additions & 0 deletions console/src/lib/codemirror-kit/keymap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { base, shift, keyName } from "w3c-keyname";
import { codePointSize, codePointAt } from "@codemirror/state";
import { keymap, type Command, type EditorView, type KeyBinding } from "@codemirror/view";
import browser from "./browser";

export function priorRunHandlers(bindings: readonly KeyBinding[], view: EditorView, event: KeyboardEvent) {
const keyMap = getKeymap(bindings, view);
console.log(keyMap)
return runHandlers(keyMap, event, view, "editor")
}

function getKeymap(bindings: readonly KeyBinding[], view: EditorView) {
const facetKeymap = view.state.facet(keymap);
const existingBindings = facetKeymap.reduce((a, b) => a.concat(b), [])
const merged = concatArray(existingBindings, bindings)
return buildKeymap(merged, currentPlatform)
}

function concatArray(array: readonly KeyBinding[], append: readonly KeyBinding[]) {
const map = new Map();
array?.forEach(item => {
map.set(item.key, item);
})
append?.forEach(item => {
map.set(item.key, item);
})
return Array.from(map.values());
}

// code referenced from https://github.com/codemirror/view/blob/main/src/keymap.ts below:

// Key codes for modifier keys
export const modifierCodes = [16, 17, 18, 20, 91, 92, 224, 225];

type PlatformName = "mac" | "win" | "linux" | "key";

export function normalizeKeyName(name: string, platform: PlatformName): string {
const parts = name.split(/-(?!$)/);
let result = parts[parts.length - 1];
if (result == "Space") result = " ";
let alt, ctrl, shift, meta;
for (let i = 0; i < parts.length - 1; ++i) {
const mod = parts[i];
if (/^(cmd|meta|m)$/i.test(mod)) meta = true;
else if (/^a(lt)?$/i.test(mod)) alt = true;
else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true;
else if (/^s(hift)?$/i.test(mod)) shift = true;
else if (/^mod$/i.test(mod)) {
if (platform == "mac") meta = true;
else ctrl = true;
} else throw new Error("Unrecognized modifier name: " + mod);
}
if (alt) result = "Alt-" + result;
if (ctrl) result = "Ctrl-" + result;
if (meta) result = "Meta-" + result;
if (shift) result = "Shift-" + result;
return result;
}

function modifiers(name: string, event: KeyboardEvent, shift: boolean) {
if (event.altKey) name = "Alt-" + name;
if (event.ctrlKey) name = "Ctrl-" + name;
if (event.metaKey) name = "Meta-" + name;
if (shift !== false && event.shiftKey) name = "Shift-" + name;
return name;
}

type Binding = {
preventDefault: boolean;
stopPropagation: boolean;
run: ((view: EditorView, event: KeyboardEvent) => boolean)[];
};

// In each scope, the `_all` property is used for bindings that apply
// to all keys.
type Keymap = { [scope: string]: { [key: string]: Binding } };

let storedPrefix: { view: EditorView; prefix: string; scope: string } | null =
null;
const currentPlatform: PlatformName = browser.mac
? "mac"
: browser.windows
? "win"
: browser.linux
? "linux"
: "key";

const PrefixTimeout = 4000;

export function buildKeymap(
bindings: readonly KeyBinding[],
platform = currentPlatform
) {
let bound: Keymap = Object.create(null);
let isPrefix: { [prefix: string]: boolean } = Object.create(null);

let checkPrefix = (name: string, is: boolean) => {
let current = isPrefix[name];
if (current == null) isPrefix[name] = is;
else if (current != is)
throw new Error(
"Key binding " +
name +
" is used both as a regular binding and as a multi-stroke prefix"
);
};

let add = (
scope: string,
key: string,
command: Command | undefined,
preventDefault?: boolean,
stopPropagation?: boolean
) => {
let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
let parts = key.split(/ (?!$)/).map((k) => normalizeKeyName(k, platform));
for (let i = 1; i < parts.length; i++) {
let prefix = parts.slice(0, i).join(" ");
checkPrefix(prefix, true);
if (!scopeObj[prefix])
scopeObj[prefix] = {
preventDefault: true,
stopPropagation: false,
run: [
(view: EditorView) => {
let ourObj = (storedPrefix = { view, prefix, scope });
setTimeout(() => {
if (storedPrefix == ourObj) storedPrefix = null;
}, PrefixTimeout);
return true;
},
],
};
}
let full = parts.join(" ");
checkPrefix(full, false);
let binding =
scopeObj[full] ||
(scopeObj[full] = {
preventDefault: false,
stopPropagation: false,
run: scopeObj._any?.run?.slice() || [],
});
if (command) binding.run.push(command);
if (preventDefault) binding.preventDefault = true;
if (stopPropagation) binding.stopPropagation = true;
};

for (let b of bindings) {
let scopes = b.scope ? b.scope.split(" ") : ["editor"];
if (b.any)
for (let scope of scopes) {
let scopeObj = bound[scope] || (bound[scope] = Object.create(null));
if (!scopeObj._any)
scopeObj._any = {
preventDefault: false,
stopPropagation: false,
run: [],
};
for (let key in scopeObj) scopeObj[key].run.push(b.any);
}
let name = b[platform] || b.key;
if (!name) continue;
for (let scope of scopes) {
add(scope, name, b.run, b.preventDefault, b.stopPropagation);
if (b.shift)
add(
scope,
"Shift-" + name,
b.shift,
b.preventDefault,
b.stopPropagation
);
}
}
return bound;
}

export function runHandlers(
map: Keymap,
event: KeyboardEvent,
view: EditorView,
scope: string
): boolean {
let name = keyName(event);
let charCode = codePointAt(name, 0),
isChar = codePointSize(charCode) == name.length && name != " ";
let prefix = "",
handled = false,
prevented = false,
stopPropagation = false;
if (
storedPrefix &&
storedPrefix.view == view &&
storedPrefix.scope == scope
) {
prefix = storedPrefix.prefix + " ";
if (modifierCodes.indexOf(event.keyCode) < 0) {
prevented = true;
storedPrefix = null;
}
}

let ran: Set<(view: EditorView, event: KeyboardEvent) => boolean> = new Set();
let runFor = (binding: Binding | undefined) => {
if (binding) {
for (let cmd of binding.run)
if (!ran.has(cmd)) {
ran.add(cmd);
if (cmd(view, event)) {
if (binding.stopPropagation) stopPropagation = true;
return true;
}
}
if (binding.preventDefault) {
if (binding.stopPropagation) stopPropagation = true;
prevented = true;
}
}
return false;
};

let scopeObj = map[scope],
baseName,
shiftName;
if (scopeObj) {
if (runFor(scopeObj[prefix + modifiers(name, event, !isChar)])) {
handled = true;
} else if (
isChar &&
(event.altKey || event.metaKey || event.ctrlKey) &&
// Ctrl-Alt may be used for AltGr on Windows
!(browser.windows && event.ctrlKey && event.altKey) &&
(baseName = base[event.keyCode]) &&
baseName != name
) {
if (runFor(scopeObj[prefix + modifiers(baseName, event, true)])) {
handled = true;
} else if (
event.shiftKey &&
(shiftName = shift[event.keyCode]) != name &&
shiftName != baseName &&
runFor(scopeObj[prefix + modifiers(shiftName, event, false)])
) {
handled = true;
}
} else if (
isChar &&
event.shiftKey &&
runFor(scopeObj[prefix + modifiers(name, event, true)])
) {
handled = true;
}
if (!handled && runFor(scopeObj._any)) handled = true;
}

if (prevented) handled = true;
if (handled && stopPropagation) event.stopPropagation();
return handled;
}
Loading

0 comments on commit 0da7488

Please sign in to comment.