From 836be5e4f460030e8eb3670450977871d4ad22fe Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 15 Sep 2022 23:53:11 +0500 Subject: [PATCH] web: take `keymap` plugin from `prosemirror-keymap` we'll be using this later on for everything keyboard related. it is simple implementation that normalizes key codes across platforms --- apps/web/package-lock.json | 135 +++++++++++++++++++---------- apps/web/package.json | 7 +- apps/web/src/common/key-handler.ts | 113 ++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 50 deletions(-) create mode 100644 apps/web/src/common/key-handler.ts diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 075bbc9ef..4e6ad5cc1 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -50,6 +50,7 @@ "react-virtuoso": "2.4.0", "showdown": "github:thecodrr/showdown", "timeago.js": "^4.0.2", + "w3c-keyname": "^2.2.6", "web-streams-polyfill": "^3.1.1", "wouter": "^2.7.3", "zustand": "^3.3.1" @@ -14298,7 +14299,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -14406,7 +14406,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -15238,7 +15237,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -16720,7 +16718,6 @@ }, "node_modules/typescript": { "version": "4.8.2", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17004,6 +17001,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", "license": "MIT", @@ -19042,10 +19044,12 @@ } }, "@csstools/postcss-unset-value": { - "version": "1.0.2" + "version": "1.0.2", + "requires": {} }, "@csstools/selector-specificity": { - "version": "2.0.2" + "version": "2.0.2", + "requires": {} }, "@develar/schema-utils": { "version": "2.6.5", @@ -19067,7 +19071,8 @@ }, "ajv-keywords": { "version": "3.5.2", - "dev": true + "dev": true, + "requires": {} }, "json-schema-traverse": { "version": "0.4.1", @@ -20678,10 +20683,12 @@ } }, "acorn-import-assertions": { - "version": "1.8.0" + "version": "1.8.0", + "requires": {} }, "acorn-jsx": { - "version": "5.3.2" + "version": "5.3.2", + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -20997,7 +21004,8 @@ } }, "ajv-keywords": { - "version": "3.5.2" + "version": "3.5.2", + "requires": {} }, "json-schema-traverse": { "version": "0.4.1" @@ -21046,7 +21054,8 @@ } }, "babel-plugin-named-asset-import": { - "version": "0.3.8" + "version": "0.3.8", + "requires": {} }, "babel-plugin-polyfill-corejs2": { "version": "0.3.2", @@ -21722,7 +21731,8 @@ } }, "css-declaration-sorter": { - "version": "6.3.0" + "version": "6.3.0", + "requires": {} }, "css-has-pseudo": { "version": "3.0.4", @@ -21783,7 +21793,8 @@ } }, "css-prefers-color-scheme": { - "version": "6.0.3" + "version": "6.0.3", + "requires": {} }, "css-select": { "version": "4.3.0", @@ -21867,7 +21878,8 @@ } }, "cssnano-utils": { - "version": "3.1.0" + "version": "3.1.0", + "requires": {} }, "csso": { "version": "4.2.0", @@ -22809,7 +22821,8 @@ } }, "eslint-plugin-react-hooks": { - "version": "4.6.0" + "version": "4.6.0", + "requires": {} }, "eslint-plugin-testing-library": { "version": "5.6.0", @@ -23275,7 +23288,8 @@ } }, "ajv-keywords": { - "version": "3.5.2" + "version": "3.5.2", + "requires": {} }, "cosmiconfig": { "version": "6.0.0", @@ -23578,7 +23592,8 @@ } }, "goober": { - "version": "2.1.11" + "version": "2.1.11", + "requires": {} }, "got": { "version": "9.6.0", @@ -23799,7 +23814,8 @@ } }, "icss-utils": { - "version": "5.1.0" + "version": "5.1.0", + "requires": {} }, "idb": { "version": "7.0.2" @@ -24368,7 +24384,8 @@ } }, "jest-pnp-resolver": { - "version": "1.2.2" + "version": "1.2.2", + "requires": {} }, "jest-regex-util": { "version": "27.5.1" @@ -24883,7 +24900,8 @@ } }, "localforage-driver-commons": { - "version": "1.0.3" + "version": "1.0.3", + "requires": {} }, "localforage-driver-memory": { "version": "1.0.5", @@ -25727,7 +25745,8 @@ } }, "postcss-browser-comments": { - "version": "4.0.0" + "version": "4.0.0", + "requires": {} }, "postcss-calc": { "version": "8.2.4", @@ -25801,16 +25820,20 @@ } }, "postcss-discard-comments": { - "version": "5.1.2" + "version": "5.1.2", + "requires": {} }, "postcss-discard-duplicates": { - "version": "5.1.0" + "version": "5.1.0", + "requires": {} }, "postcss-discard-empty": { - "version": "5.1.1" + "version": "5.1.1", + "requires": {} }, "postcss-discard-overridden": { - "version": "5.1.0" + "version": "5.1.0", + "requires": {} }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -25826,7 +25849,8 @@ } }, "postcss-flexbugs-fixes": { - "version": "5.0.2" + "version": "5.0.2", + "requires": {} }, "postcss-focus-visible": { "version": "6.0.4", @@ -25841,10 +25865,12 @@ } }, "postcss-font-variant": { - "version": "5.0.0" + "version": "5.0.0", + "requires": {} }, "postcss-gap-properties": { - "version": "3.0.5" + "version": "3.0.5", + "requires": {} }, "postcss-image-set-function": { "version": "4.0.7", @@ -25861,7 +25887,8 @@ } }, "postcss-initial": { - "version": "4.0.1" + "version": "4.0.1", + "requires": {} }, "postcss-js": { "version": "4.0.0", @@ -25905,10 +25932,12 @@ } }, "postcss-logical": { - "version": "5.0.4" + "version": "5.0.4", + "requires": {} }, "postcss-media-minmax": { - "version": "5.0.0" + "version": "5.0.0", + "requires": {} }, "postcss-merge-longhand": { "version": "5.1.6", @@ -25955,7 +25984,8 @@ } }, "postcss-modules-extract-imports": { - "version": "3.0.0" + "version": "3.0.0", + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -25999,7 +26029,8 @@ } }, "postcss-normalize-charset": { - "version": "5.1.0" + "version": "5.1.0", + "requires": {} }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -26073,7 +26104,8 @@ } }, "postcss-page-break": { - "version": "3.0.4" + "version": "3.0.4", + "requires": {} }, "postcss-place": { "version": "7.0.5", @@ -26155,7 +26187,8 @@ } }, "postcss-replace-overflow-wrap": { - "version": "4.0.0" + "version": "4.0.0", + "requires": {} }, "postcss-selector-not": { "version": "6.0.1", @@ -26441,7 +26474,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -26518,7 +26550,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -26549,7 +26580,8 @@ "version": "3.0.4" }, "react-loading-skeleton": { - "version": "3.1.0" + "version": "3.1.0", + "requires": {} }, "react-modal": { "version": "3.15.1", @@ -27029,7 +27061,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -27053,7 +27084,8 @@ } }, "ajv-keywords": { - "version": "3.5.2" + "version": "3.5.2", + "requires": {} }, "json-schema-traverse": { "version": "0.4.1" @@ -27501,7 +27533,8 @@ "version": "3.1.1" }, "style-loader": { - "version": "3.3.1" + "version": "3.3.1", + "requires": {} }, "style-value-types": { "version": "4.1.4", @@ -28007,8 +28040,7 @@ } }, "typescript": { - "version": "4.8.2", - "dev": true + "version": "4.8.2" }, "unbox-primitive": { "version": "1.0.2", @@ -28176,6 +28208,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + }, "w3c-xmlserializer": { "version": "2.0.0", "requires": { @@ -28373,7 +28410,8 @@ } }, "ws": { - "version": "8.8.1" + "version": "8.8.1", + "requires": {} } } }, @@ -28669,7 +28707,8 @@ } }, "wouter": { - "version": "2.7.5" + "version": "2.7.5", + "requires": {} }, "wrap-ansi": { "version": "7.0.0", @@ -28692,7 +28731,8 @@ } }, "ws": { - "version": "7.5.9" + "version": "7.5.9", + "requires": {} }, "xdg-basedir": { "version": "4.0.0", @@ -28752,7 +28792,8 @@ "version": "0.1.0" }, "zustand": { - "version": "3.7.2" + "version": "3.7.2", + "requires": {} }, "zx": { "version": "7.0.8", diff --git a/apps/web/package.json b/apps/web/package.json index f412707de..cb406e789 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -56,6 +56,7 @@ "react-virtuoso": "2.4.0", "showdown": "github:thecodrr/showdown", "timeago.js": "^4.0.2", + "w3c-keyname": "^2.2.6", "web-streams-polyfill": "^3.1.1", "wouter": "^2.7.3", "zustand": "^3.3.1" @@ -80,12 +81,12 @@ "lorem-ipsum": "^2.0.4", "patch-package": "^6.4.7", "progress-bar-webpack-plugin": "^2.1.0", + "react": "17.0.2", + "react-dom": "17.0.2", "source-map-explorer": "^2.5.2", "typescript": "^4.8.2", "webpack-bundle-analyzer": "^4.5.0", - "worker-loader": "^3.0.8", - "react": "17.0.2", - "react-dom": "17.0.2" + "worker-loader": "^3.0.8" }, "overrides": { "react@>17": "17.0.2", diff --git a/apps/web/src/common/key-handler.ts b/apps/web/src/common/key-handler.ts new file mode 100644 index 000000000..db7d95cd3 --- /dev/null +++ b/apps/web/src/common/key-handler.ts @@ -0,0 +1,113 @@ +/* eslint-disable header/header */ +/* +Copyright (C) 2015-2017 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +import { base, keyName } from "w3c-keyname"; + +export type Command = () => boolean; +const mac = + typeof navigator != "undefined" + ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) + : false; + +function normalizeKeyName(name: 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 (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 normalize(map: { [key: string]: Command }) { + const copy: { [key: string]: Command } = Object.create(null); + for (const prop in map) copy[normalizeKeyName(prop)] = map[prop]; + return copy; +} + +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; +} + +// Given a set of bindings (using the same format as +// [`keymap`](#keymap.keymap)), return a [keydown +// handler](#view.EditorProps.handleKeyDown) that handles them. +export function keydownHandler( + bindings: Record +): (event: KeyboardEvent) => boolean { + const map = normalize(bindings); + return function (event) { + const name = keyName(event), + isChar = name.length == 1 && name != " "; + let baseName: string; + const direct = map[modifiers(name, event, !isChar)]; + if (direct && direct()) { + event.preventDefault(); + return true; + } + if ( + isChar && + (event.shiftKey || + event.altKey || + event.metaKey || + name.charCodeAt(0) > 127) && + (baseName = base[event.keyCode]) && + baseName != name + ) { + // Try falling back to the keyCode when there's a modifier + // active or the character produced isn't ASCII, and our table + // produces a different name from the the keyCode. See #668, + // #1060 + const fromCode = map[modifiers(baseName, event, true)]; + if (fromCode && fromCode()) { + event.preventDefault(); + return true; + } + } else if (isChar && event.shiftKey) { + // Otherwise, if shift is active, also try the binding with the + // Shift- prefix enabled. See #997 + const withShift = map[modifiers(name, event, true)]; + if (withShift && withShift()) { + event.preventDefault(); + return true; + } + } + return false; + }; +}