From 1a6721516b376641cad7923a70673b5d5c6738f1 Mon Sep 17 00:00:00 2001 From: "LB (Ben Johnston)" Date: Mon, 21 Aug 2023 08:39:41 +1000 Subject: [PATCH] Add support for `mod` key to be dynamic based on Mac/Windows - Adds `getModKey` to the `defaultSchema` which will resolve to `Meta` on a MacOS like device and `Control` on Windows like - Uses `mod` to be used as a key filter modifier for either `metaKey` or `ctrlKey` based on the resolved value - Add unit tests and documentation for the `mod` key, and an example to the slideshow page - Closes #654 --- docs/reference/actions.md | 13 ++--- examples/views/slideshow.ejs | 6 ++- src/core/action.ts | 13 +++-- src/core/schema.ts | 5 ++ .../core/action_keyboard_filter_tests.ts | 52 ++++++++++++++++++- 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 6aa43db3..5826a4b7 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -120,12 +120,13 @@ If you want to subscribe to a compound filter using a modifier key, you can writ The list of supported modifier keys is shown below. -| Modifier | Notes | -| -------- | ------------------ | -| `alt` | `option` on MacOS | -| `ctrl` | | -| `meta` | Command key on MacOS | -| `shift` | | +| Modifier | Notes | +| -------- | ------------------------------------------------------ | +| `alt` | `option` on MacOS | +| `ctrl` | | +| `meta` | `⌘ command` key on MacOS | +| `shift` | | +| `mod` | `⌘ command` (Meta) key on MacOS, `ctrl` key on Windows | ### Global Events diff --git a/examples/views/slideshow.ejs b/examples/views/slideshow.ejs index c404a9a8..23a0c8be 100644 --- a/examples/views/slideshow.ejs +++ b/examples/views/slideshow.ejs @@ -1,6 +1,6 @@ <%- include("layout/head") %> -
+
@@ -8,6 +8,10 @@
🙈
🙉
🙊
+ +

+ Hint: Use keyboard shortcuts to navigate the slideshow: mod + j & mod + k +

<%- include("layout/tail") %> diff --git a/src/core/action.ts b/src/core/action.ts index e009399b..dd47678c 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -4,7 +4,7 @@ import { Schema } from "./schema" import { camelize } from "./string_helpers" import { hasProperty } from "./utils" -const allModifiers = ["meta", "ctrl", "alt", "shift"] +const allModifiers = ["meta", "ctrl", "alt", "shift", "mod"] export class Action { readonly element: Element @@ -98,9 +98,14 @@ export class Action { } private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array): boolean { - const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier)) - - return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift + const [meta, ctrl, alt, shift, mod] = allModifiers.map((modifier) => filters.includes(modifier)) + const modKey = mod && this.schema.getModKey() + return ( + event.metaKey !== (meta || modKey === "Meta") || + event.ctrlKey !== (ctrl || modKey === "Control") || + event.altKey !== alt || + event.shiftKey !== shift + ) } } diff --git a/src/core/schema.ts b/src/core/schema.ts index bbc5b24a..fc6ca1f0 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -5,6 +5,7 @@ export interface Schema { targetAttributeForScope(identifier: string): string outletAttributeForScope(identifier: string, outlet: string): string keyMappings: { [key: string]: string } + getModKey(): string } export const defaultSchema: Schema = { @@ -31,6 +32,10 @@ export const defaultSchema: Schema = { // [0-9] ...objectFromEntries("0123456789".split("").map((n) => [n, n])), }, + getModKey: ((_: { key?: string }) => () => { + if (!_.key) _.key = /Mac|iPod|iPhone|iPad/.test(window?.navigator?.platform || "") ? "Meta" : "Control" + return _.key // memoize the modifier key on first call to avoid platform sniffing on every call + })({ key: "" }), } function objectFromEntries(array: [string, any][]): object { diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index 64a9303c..904773d3 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -22,6 +22,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase {
` @@ -177,7 +178,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { this.assertActions({ name: "log2", identifier: "a", eventType: "keydown", currentTarget: button }) } - async "test ignore event handlers associated with modifiers other than ctrol+shift+a"() { + async "test ignore event handlers associated with modifiers other than ctrl+shift+a"() { const button = this.findElement("#button9") await this.nextFrame await this.triggerKeyboardEvent(button, "keydown", { key: "A", ctrlKey: true, shiftKey: true }) @@ -197,4 +198,53 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { await this.triggerEvent(button, "jquery.a") this.assertActions({ name: "log2", identifier: "a", eventType: "jquery.a", currentTarget: button }) } + + async "test that the default schema getModKey resolved value is based on the platform"() { + const expectedKeyMapping = navigator.platform?.match(/Mac|iPod|iPhone|iPad/) ? "Meta" : "Control" + this.assert.equal(defaultSchema.getModKey(), expectedKeyMapping) + } + + async "test ignore event handlers associated with modifiers mod+ (dynamic based on platform)"() { + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + // We should only see one event using `mod` (which is dynamic based on platform) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + customSchema.getModKey = () => "Control" // set up for next test + } + + async "test ignore event handlers associated with modifiers mod+ (set to 'Control')"() { + // see .mod setting in previous test (mocking Windows) + this.schema = { + ...this.application.schema, + keyMappings: { ...this.application.schema.keyMappings, mod: "Control" }, + } + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + this.assertNoActions() + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + customSchema.getModKey = () => "Meta" // set up for next test + } + + async "test ignore event handlers associated with modifiers mod+ (set to 'Meta')"() { + // see .mod setting in previous test (mocking Windows) + this.schema = { + ...this.application.schema, + keyMappings: { ...this.application.schema.keyMappings, mod: "Meta" }, + } + const button = this.findElement("#button11") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", { key: "s", ctrlKey: true }) + this.assertNoActions() + await this.triggerKeyboardEvent(button, "keydown", { key: "s", metaKey: true }) + this.assertActions({ name: "log", identifier: "a", eventType: "keydown", currentTarget: button }) + + // Reset to default for any subsequent tests + this.schema = customSchema + } }