From aad454a720635253ef2bd51db8b24a11c0723c80 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Mon, 6 Sep 2021 17:39:27 +0900 Subject: [PATCH 01/29] Add modifier to filter keyboard events. --- src/core/action.ts | 23 ++++++++++++++++++++++- src/core/action_descriptor.ts | 12 +++++++----- src/core/binding.ts | 5 +++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 1f70e758..99f516fe 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -2,6 +2,19 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } f import { Token } from "../mutation-observers" import { camelize } from "./string_helpers" +const keyMappings: { [key: string]: string } = { + "enter": "Enter", + "tab": "Tab", + "esc": "Escape", + "space": " ", + "up": "ArrowUp", + "down": "ArrowDown", + "left": "ArrowLeft", + "right": "ArrowRight", + "home": "Home", + "end": "End" +} + export class Action { readonly element: Element readonly index: number @@ -10,6 +23,7 @@ export class Action { readonly eventOptions: AddEventListenerOptions readonly identifier: string readonly methodName: string + readonly keyFilter: string static forToken(token: Token) { return new this(token.element, token.index, parseActionDescriptorString(token.content)) @@ -23,11 +37,18 @@ export class Action { this.eventOptions = descriptor.eventOptions || {} this.identifier = descriptor.identifier || error("missing identifier") this.methodName = descriptor.methodName || error("missing method name") + this.keyFilter = descriptor.keyFilter || '' } toString() { const eventNameSuffix = this.eventTargetName ? `@${this.eventTargetName}` : "" - return `${this.eventName}${eventNameSuffix}->${this.identifier}#${this.methodName}` + const eventFilter = this.keyFilter ? `.${this.keyFilter}` : "" + return `${this.eventName}${eventNameSuffix}${eventFilter}->${this.identifier}#${this.methodName}` + } + + isFilterTarget(key: string): boolean { + if (!this.keyFilter) { return false; } + return keyMappings[this.keyFilter] !== key } get params(): object { diff --git a/src/core/action_descriptor.ts b/src/core/action_descriptor.ts index da6a32d1..a1b90cd0 100644 --- a/src/core/action_descriptor.ts +++ b/src/core/action_descriptor.ts @@ -4,10 +4,11 @@ export interface ActionDescriptor { eventName: string identifier: string methodName: string + keyFilter: string } -// capture nos.: 12 23 4 43 1 5 56 7 768 9 98 -const descriptorPattern = /^((.+?)(@(window|document))?->)?(.+?)(#([^:]+?))(:(.+))?$/ +// capture nos.: 12 23 4 43 5 6 65 1 7 78 9 981011 1110 +const descriptorPattern = /^((.+?)(@(window|document))?(\.(up|down|left|right))?->)?(.+?)(#([^:]+?))(:(.+))?$/ export function parseActionDescriptorString(descriptorString: string): Partial { const source = descriptorString.trim() @@ -15,9 +16,10 @@ export function parseActionDescriptorString(descriptorString: string): Partial Date: Mon, 6 Sep 2021 18:23:01 +0900 Subject: [PATCH 02/29] Fixed that some keys were not working. --- src/core/action.ts | 5 ++++- src/core/action_descriptor.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 99f516fe..531c934c 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -47,7 +47,10 @@ export class Action { } isFilterTarget(key: string): boolean { - if (!this.keyFilter) { return false; } + if (!(this.keyFilter && keyMappings[this.keyFilter])) { + return false; + } + return keyMappings[this.keyFilter] !== key } diff --git a/src/core/action_descriptor.ts b/src/core/action_descriptor.ts index a1b90cd0..8ef1aba5 100644 --- a/src/core/action_descriptor.ts +++ b/src/core/action_descriptor.ts @@ -7,8 +7,8 @@ export interface ActionDescriptor { keyFilter: string } -// capture nos.: 12 23 4 43 5 6 65 1 7 78 9 981011 1110 -const descriptorPattern = /^((.+?)(@(window|document))?(\.(up|down|left|right))?->)?(.+?)(#([^:]+?))(:(.+))?$/ +// capture nos.: 12 23 4 43 5 6 65 1 7 78 9 981011 1110 +const descriptorPattern = /^((.+?)(@(window|document))?(\.(.+?))?->)?(.+?)(#([^:]+?))(:(.+))?$/ export function parseActionDescriptorString(descriptorString: string): Partial { const source = descriptorString.trim() From c2e30a42678221ccb932587e44dd062e1f9d2ba7 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Mon, 13 Sep 2021 17:51:44 +0900 Subject: [PATCH 03/29] add ie support --- src/core/action.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 531c934c..80e2b50f 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -2,17 +2,17 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } f import { Token } from "../mutation-observers" import { camelize } from "./string_helpers" -const keyMappings: { [key: string]: string } = { - "enter": "Enter", - "tab": "Tab", - "esc": "Escape", - "space": " ", - "up": "ArrowUp", - "down": "ArrowDown", - "left": "ArrowLeft", - "right": "ArrowRight", - "home": "Home", - "end": "End" +const keyMappings: { [key: string]: string[] } = { + "enter": ["Enter"], + "tab": ["Tab"], + "esc": ["Escape", "Esc"], + "space": [" ", "Spacebar"], + "up": ["ArrowUp", "Up"], + "down": ["ArrowDown", "Down"], + "left": ["ArrowLeft", "Left"], + "right": ["ArrowRight", "Right"], + "home": ["Home"], + "end": ["End"] } export class Action { @@ -51,7 +51,7 @@ export class Action { return false; } - return keyMappings[this.keyFilter] !== key + return !keyMappings[this.keyFilter].includes(key) } get params(): object { From 49503b76ec3864e0f86da18b69fbb1f9e702fbbe Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Mon, 13 Sep 2021 17:58:37 +0900 Subject: [PATCH 04/29] added tests for filtering keyboard events --- src/tests/cases/dom_test_case.ts | 9 + .../core/action_keyboard_filter_tests.ts | 185 ++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/tests/modules/core/action_keyboard_filter_tests.ts diff --git a/src/tests/cases/dom_test_case.ts b/src/tests/cases/dom_test_case.ts index f132e6f4..061eae36 100644 --- a/src/tests/cases/dom_test_case.ts +++ b/src/tests/cases/dom_test_case.ts @@ -51,6 +51,15 @@ export class DOMTestCase extends TestCase { return event } + async triggerKeyboardEvent(selectorOrTarget: string | EventTarget, type: string, options: KeyboardEventInit = {}) { + const eventTarget = typeof selectorOrTarget == "string" ? this.findElement(selectorOrTarget) : selectorOrTarget + const event = new KeyboardEvent(type, options); + + eventTarget.dispatchEvent(event) + await this.nextFrame + return event + } + findElement(selector: string) { const element = this.fixtureElement.querySelector(selector) if (element) { diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts new file mode 100644 index 00000000..d70a04ab --- /dev/null +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -0,0 +1,185 @@ +import { LogControllerTestCase } from "../../cases/log_controller_test_case" + +export default class ActionKeyboardFilterTests extends LogControllerTestCase { + identifier = ["a"] + fixtureHTML = ` +
+ + + + + + +
+ ` + + async "test ignore event handlers associated with modifiers other than Enter"() { + const button = this.findElement("#button1") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Enter'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Space"() { + const button = this.findElement("#button1") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: ' '}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Space on ie"() { + const button = this.findElement("#button1") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Spacebar'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Tab"() { + const button = this.findElement("#button2") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Tab'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Escape"() { + const button = this.findElement("#button2") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Escape'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Escape on ie"() { + const button = this.findElement("#button2") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Esc'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowUp"() { + const button = this.findElement("#button3") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'ArrowUp'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowDown"() { + const button = this.findElement("#button3") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'ArrowDown'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowUp on ie"() { + const button = this.findElement("#button3") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Up'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowDown on ie"() { + const button = this.findElement("#button3") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Down'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowLeft"() { + const button = this.findElement("#button4") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'ArrowLeft'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowRight"() { + const button = this.findElement("#button4") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'ArrowRight'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowLeft on ie"() { + const button = this.findElement("#button4") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Left'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than ArrowRight on ie"() { + const button = this.findElement("#button4") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Right'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than Home"() { + const button = this.findElement("#button5") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Home'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test ignore event handlers associated with modifiers other than End"() { + const button = this.findElement("#button5") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'End'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} + ) + } + + async "test keyup"() { + const button = this.findElement("#button6") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keyup", {key: 'End'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keyup', currentTarget: button}, + {name: "log3", identifier: "a", eventType: 'keyup', currentTarget: button} + ) + } +} From 18d6fa2002b7e374834751c7f67ea311b7fc9ba6 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Mon, 13 Sep 2021 19:29:40 +0900 Subject: [PATCH 05/29] fix modifier position The modifier was in the wrong position. It should be before the target. * before: evt@global.modifier->... * after: evt.modifier@global->... --- src/core/action_descriptor.ts | 16 ++++++++-------- .../modules/core/action_keyboard_filter_tests.ts | 12 +++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/core/action_descriptor.ts b/src/core/action_descriptor.ts index 8ef1aba5..ee51fe86 100644 --- a/src/core/action_descriptor.ts +++ b/src/core/action_descriptor.ts @@ -7,19 +7,19 @@ export interface ActionDescriptor { keyFilter: string } -// capture nos.: 12 23 4 43 5 6 65 1 7 78 9 981011 1110 -const descriptorPattern = /^((.+?)(@(window|document))?(\.(.+?))?->)?(.+?)(#([^:]+?))(:(.+))?$/ +// capture nos.: 1 1 2 2 3 3 4 4 5 5 6 6 +const descriptorPattern = /^(?:(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/ export function parseActionDescriptorString(descriptorString: string): Partial { const source = descriptorString.trim() const matches = source.match(descriptorPattern) || [] return { - eventTarget: parseEventTarget(matches[4]), - eventName: matches[2], - eventOptions: matches[11] ? parseEventOptions(matches[11]) : {}, - identifier: matches[7], - methodName: matches[9], - keyFilter: matches[6] + eventTarget: parseEventTarget(matches[3]), + eventName: matches[1], + eventOptions: matches[6] ? parseEventOptions(matches[6]) : {}, + identifier: matches[4], + methodName: matches[5], + keyFilter: matches[2] } } diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index d70a04ab..47d5ce17 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -3,13 +3,14 @@ import { LogControllerTestCase } from "../../cases/log_controller_test_case" export default class ActionKeyboardFilterTests extends LogControllerTestCase { identifier = ["a"] fixtureHTML = ` -
+
+
` @@ -182,4 +183,13 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { {name: "log3", identifier: "a", eventType: 'keyup', currentTarget: button} ) } + + async "test global event"() { + const button = this.findElement("#button7") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'Escape', bubbles: true}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: document}, + ) + } } From 10ee3de2196dcc3d7cf282300a07f3d0315f53b5 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Mon, 13 Sep 2021 22:51:22 +0900 Subject: [PATCH 06/29] Remove support for ie11 --- src/core/action.ts | 24 ++++---- .../core/action_keyboard_filter_tests.ts | 60 ------------------- 2 files changed, 12 insertions(+), 72 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 80e2b50f..cd4f471b 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -2,17 +2,17 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } f import { Token } from "../mutation-observers" import { camelize } from "./string_helpers" -const keyMappings: { [key: string]: string[] } = { - "enter": ["Enter"], - "tab": ["Tab"], - "esc": ["Escape", "Esc"], - "space": [" ", "Spacebar"], - "up": ["ArrowUp", "Up"], - "down": ["ArrowDown", "Down"], - "left": ["ArrowLeft", "Left"], - "right": ["ArrowRight", "Right"], - "home": ["Home"], - "end": ["End"] +const keyMappings: { [key: string]: string } = { + "enter": "Enter", + "tab": "Tab", + "esc": "Escape", + "space": " ", + "up": "ArrowUp", + "down": "ArrowDown", + "left": "ArrowLeft", + "right": "ArrowRight", + "home": "Home", + "end": "End" } export class Action { @@ -51,7 +51,7 @@ export class Action { return false; } - return !keyMappings[this.keyFilter].includes(key) + return keyMappings[this.keyFilter] !== key } get params(): object { diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index 47d5ce17..9031ed32 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -34,16 +34,6 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { ) } - async "test ignore event handlers associated with modifiers other than Space on ie"() { - const button = this.findElement("#button1") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Spacebar'}) - this.assertActions( - {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - async "test ignore event handlers associated with modifiers other than Tab"() { const button = this.findElement("#button2") await this.nextFrame @@ -64,16 +54,6 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { ) } - async "test ignore event handlers associated with modifiers other than Escape on ie"() { - const button = this.findElement("#button2") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Esc'}) - this.assertActions( - {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - async "test ignore event handlers associated with modifiers other than ArrowUp"() { const button = this.findElement("#button3") await this.nextFrame @@ -94,26 +74,6 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { ) } - async "test ignore event handlers associated with modifiers other than ArrowUp on ie"() { - const button = this.findElement("#button3") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Up'}) - this.assertActions( - {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - - async "test ignore event handlers associated with modifiers other than ArrowDown on ie"() { - const button = this.findElement("#button3") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Down'}) - this.assertActions( - {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - async "test ignore event handlers associated with modifiers other than ArrowLeft"() { const button = this.findElement("#button4") await this.nextFrame @@ -134,26 +94,6 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { ) } - async "test ignore event handlers associated with modifiers other than ArrowLeft on ie"() { - const button = this.findElement("#button4") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Left'}) - this.assertActions( - {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - - async "test ignore event handlers associated with modifiers other than ArrowRight on ie"() { - const button = this.findElement("#button4") - await this.nextFrame - await this.triggerKeyboardEvent(button, "keydown", {key: 'Right'}) - this.assertActions( - {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, - {name: "log3", identifier: "a", eventType: 'keydown', currentTarget: button} - ) - } - async "test ignore event handlers associated with modifiers other than Home"() { const button = this.findElement("#button5") await this.nextFrame From ff8cf8baef2064178773fe75b1b85c09abeca6e8 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Tue, 14 Sep 2021 13:06:00 +0900 Subject: [PATCH 07/29] fix action.toString before:`${event}@${target}.${filter}->${identifier}#${methodName}` after: `${event}.${filter}@${target}->${identifier}#${methodName}` --- src/core/action.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index cd4f471b..ae813c0e 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -41,9 +41,9 @@ export class Action { } toString() { - const eventNameSuffix = this.eventTargetName ? `@${this.eventTargetName}` : "" const eventFilter = this.keyFilter ? `.${this.keyFilter}` : "" - return `${this.eventName}${eventNameSuffix}${eventFilter}->${this.identifier}#${this.methodName}` + const eventTarget = this.eventTargetName ? `@${this.eventTargetName}` : "" + return `${this.eventName}${eventFilter}${eventTarget}->${this.identifier}#${this.methodName}` } isFilterTarget(key: string): boolean { From 2fe88524a4dacea8d858f15a73ef6e78341cd268 Mon Sep 17 00:00:00 2001 From: "Takuya Nakajima [LIFULL]" Date: Tue, 14 Sep 2021 18:23:07 +0900 Subject: [PATCH 08/29] Document added about keyboard event filters --- docs/reference/actions.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index fccc6f93..36b37214 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -64,11 +64,40 @@ input type=submit | click select | change textarea | input +## KeyboardEvent Filter + +You may have Actions that you wish to run only when certain keystrokes are received. + +You can install an event listener that responds only to the `Escape` key by adding `.esc` to the event name of the action descriptor, as in the following example. + +```html +
+
+``` + +This will only work if the event being fired is a keyboard event. + +The correspondence between these filter modifier and keys is shown below. + +Modifier | Key Name +-------- | -------- +enter | Enter +tab | Tab +esc | Escape +space: | " " +up | ArrowUp +down | ArrowDown +left | ArrowLeft +right | ArrowRight +home | Home +end | End + ### Global Events Sometimes a controller needs to listen for events dispatched on the global `window` or `document` objects. -You can append `@window` or `@document` to the event name in an action descriptor to install the event listener on `window` or `document`, respectively, as in the following example: +You can append `@window` or `@document` to the event name (contains filter modifer) in an action descriptor to install the event listener on `window` or `document`, respectively, as in the following example: From 5693106fe50e083c222256f79a8acf139f4757bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Wed, 30 Mar 2022 21:10:38 +0900 Subject: [PATCH 09/29] support: Modifiable keymapping --- src/core/action.ts | 30 +++++++--------- src/core/binding_observer.ts | 4 +-- src/core/schema.ts | 15 +++++++- src/tests/cases/application_test_case.ts | 2 +- .../core/action_keyboard_filter_tests.ts | 34 +++++++++++++++++++ 5 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index ae813c0e..66ccd349 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -1,20 +1,8 @@ import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor" import { Token } from "../mutation-observers" +import { Schema } from "./schema" import { camelize } from "./string_helpers" -const keyMappings: { [key: string]: string } = { - "enter": "Enter", - "tab": "Tab", - "esc": "Escape", - "space": " ", - "up": "ArrowUp", - "down": "ArrowDown", - "left": "ArrowLeft", - "right": "ArrowRight", - "home": "Home", - "end": "End" -} - export class Action { readonly element: Element readonly index: number @@ -24,12 +12,13 @@ export class Action { readonly identifier: string readonly methodName: string readonly keyFilter: string + readonly schema: Schema - static forToken(token: Token) { - return new this(token.element, token.index, parseActionDescriptorString(token.content)) + static forToken(token: Token, schema: Schema) { + return new this(token.element, token.index, parseActionDescriptorString(token.content), schema) } - constructor(element: Element, index: number, descriptor: Partial) { + constructor(element: Element, index: number, descriptor: Partial, schema: Schema) { this.element = element this.index = index this.eventTarget = descriptor.eventTarget || element @@ -38,6 +27,7 @@ export class Action { this.identifier = descriptor.identifier || error("missing identifier") this.methodName = descriptor.methodName || error("missing method name") this.keyFilter = descriptor.keyFilter || '' + this.schema = schema } toString() { @@ -47,11 +37,11 @@ export class Action { } isFilterTarget(key: string): boolean { - if (!(this.keyFilter && keyMappings[this.keyFilter])) { + if (!(this.keyFilter && this.keyMappings[this.keyFilter])) { return false; } - return keyMappings[this.keyFilter] !== key + return this.keyMappings[this.keyFilter] !== key } get params(): object { @@ -80,6 +70,10 @@ export class Action { private get eventTargetName() { return stringifyEventTarget(this.eventTarget) } + + private get keyMappings() { + return this.schema.keyMappings; + } } const defaultEventNames: { [tagName: string]: (element: Element) => string } = { diff --git a/src/core/binding_observer.ts b/src/core/binding_observer.ts index 33a2c095..be2d66dd 100644 --- a/src/core/binding_observer.ts +++ b/src/core/binding_observer.ts @@ -89,7 +89,7 @@ export class BindingObserver implements ValueListObserverDelegate { // Value observer delegate parseValueForToken(token: Token): Action | undefined { - const action = Action.forToken(token) + const action = Action.forToken(token, this.schema) if (action.identifier == this.identifier) { return action } @@ -104,7 +104,7 @@ export class BindingObserver implements ValueListObserverDelegate { } elementMatchedNoValue(token: Token) { - const action = Action.forToken(token) + const action = Action.forToken(token, this.schema) this.context.handleWarning( `Action "${token.content}" references undefined controller "${action.identifier}"`, `connecting action "${token.content}"` diff --git a/src/core/schema.ts b/src/core/schema.ts index 8ed55abc..5ffa6832 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -3,11 +3,24 @@ export interface Schema { actionAttribute: string targetAttribute: string targetAttributeForScope(identifier: string): string + keyMappings: {[key: string]: string} } export const defaultSchema: Schema = { controllerAttribute: "data-controller", actionAttribute: "data-action", targetAttribute: "data-target", - targetAttributeForScope: identifier => `data-${identifier}-target` + targetAttributeForScope: identifier => `data-${identifier}-target`, + keyMappings: { + "enter": "Enter", + "tab": "Tab", + "esc": "Escape", + "space": " ", + "up": "ArrowUp", + "down": "ArrowDown", + "left": "ArrowLeft", + "right": "ArrowRight", + "home": "Home", + "end": "End" + } } diff --git a/src/tests/cases/application_test_case.ts b/src/tests/cases/application_test_case.ts index 9b046b57..440798a8 100644 --- a/src/tests/cases/application_test_case.ts +++ b/src/tests/cases/application_test_case.ts @@ -2,7 +2,7 @@ import { Application } from "../../core/application"; import { DOMTestCase } from "./dom_test_case"; import { Schema, defaultSchema } from "../../core/schema"; -class TestApplication extends Application { +export class TestApplication extends Application { handleError(error: Error, message: string, detail: object) { throw error } diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index 9031ed32..b2b6c6b5 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -1,6 +1,14 @@ +import { TestApplication } from "../../cases/application_test_case" import { LogControllerTestCase } from "../../cases/log_controller_test_case" +import { Schema, defaultSchema } from "../../../core/schema"; +import { Application } from "../../../core/application"; + +const customSchema = {...defaultSchema, keyMappings: {...defaultSchema.keyMappings, a: "a", b: "b"}}; export default class ActionKeyboardFilterTests extends LogControllerTestCase { + schema: Schema = customSchema + application: Application = new TestApplication(this.fixtureElement, this.schema) + identifier = ["a"] fixtureHTML = `
@@ -11,6 +19,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { +
` @@ -132,4 +141,29 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { {name: "log", identifier: "a", eventType: 'keydown', currentTarget: document}, ) } + + async "test custom keymapping: a"() { + const button = this.findElement("#button8") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'a'}) + this.assertActions( + {name: "log", identifier: "a", eventType: 'keydown', currentTarget: button}, + ) + } + + async "test custom keymapping: b"() { + const button = this.findElement("#button8") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'b'}) + this.assertActions( + {name: "log2", identifier: "a", eventType: 'keydown', currentTarget: button}, + ) + } + + async "test custom keymapping: unknown c"() { + const button = this.findElement("#button8") + await this.nextFrame + await this.triggerKeyboardEvent(button, "keydown", {key: 'c'}) + this.assertActions() + } } From cf5473c39a748b1ad6b9c404bdf206ce9d470a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Wed, 30 Mar 2022 21:15:40 +0900 Subject: [PATCH 10/29] add example: tabs(for modifier) --- examples/controllers/tabs_controller.js | 44 +++++++++++++++++++++++++ examples/public/examples.css | 10 ++++++ examples/server.js | 1 + examples/views/tabs.ejs | 42 +++++++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 examples/controllers/tabs_controller.js create mode 100644 examples/views/tabs.ejs diff --git a/examples/controllers/tabs_controller.js b/examples/controllers/tabs_controller.js new file mode 100644 index 00000000..f78ac5eb --- /dev/null +++ b/examples/controllers/tabs_controller.js @@ -0,0 +1,44 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = [ "tab", "tabpanel" ] + static classes = [ "current" ] + static values = { index: { default: 0, type: Number } } + + next() { + if (this.indexValue < this.lastIndex) { + this.indexValue++ + return + } + this.indexValue = 0 + } + + previous() { + if (this.indexValue > 0) { + this.indexValue-- + return + } + this.indexValue = this.lastIndex + } + + open(evt) { + this.indexValue = this.tabTargets.indexOf(evt.currentTarget) + } + + get lastIndex() { + return this.tabTargets.length - 1 + } + + indexValueChanged(current, old) { + let panels = this.tabpanelTargets + let tabs = this.tabTargets + + if (old != null) { + panels[old].classList.remove(...this.currentClasses) + tabs[old].tabIndex = -1 + } + panels[current].classList.add(...this.currentClasses) + tabs[current].tabIndex = 0 + tabs[current].focus() + } +} diff --git a/examples/public/examples.css b/examples/public/examples.css index 5f74abc0..bc436cce 100644 --- a/examples/public/examples.css +++ b/examples/public/examples.css @@ -94,3 +94,13 @@ main { min-width: 16em; } +.tabpanel { + border: 1px solid #dedede; + display: none; + margin-top: .4rem; + padding: 0.8rem; + font-size: 6rem; +} +.tabpanel--current { + display: block; +} diff --git a/examples/server.js b/examples/server.js index 85084218..a5573f33 100644 --- a/examples/server.js +++ b/examples/server.js @@ -22,6 +22,7 @@ const pages = [ { path: "/clipboard", title: "Clipboard" }, { path: "/slideshow", title: "Slideshow" }, { path: "/content-loader", title: "Content Loader" }, + { path: "/tabs", title: "Tabs" }, ] app.get("/", (req, res) => { diff --git a/examples/views/tabs.ejs b/examples/views/tabs.ejs new file mode 100644 index 00000000..a6682594 --- /dev/null +++ b/examples/views/tabs.ejs @@ -0,0 +1,42 @@ +<% include layout/head %> + +
+

This tabbed interface is operated by focusing on a button and pressing the left and right keys.

+
+ + +
+ +
🐵
+
🙈
+
+ +<% include layout/tail %> From 4e13612a712997a4125de04ff1b8fa78bd5c8589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Wed, 30 Mar 2022 21:21:54 +0900 Subject: [PATCH 11/29] add docs of custom keymapping --- docs/reference/actions.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/reference/actions.md b/docs/reference/actions.md index 36b37214..d295fd40 100644 --- a/docs/reference/actions.md +++ b/docs/reference/actions.md @@ -64,6 +64,8 @@ input type=submit | click select | change textarea | input + + ## KeyboardEvent Filter You may have Actions that you wish to run only when certain keystrokes are received. @@ -85,7 +87,7 @@ Modifier | Key Name enter | Enter tab | Tab esc | Escape -space: | " " +space | " " up | ArrowUp down | ArrowDown left | ArrowLeft @@ -93,6 +95,20 @@ right | ArrowRight home | Home end | End +If you need to support other keys, you can customize the modifier using custom schema. + +```javascript +import { Application, defaultSchema } from "@hotwired/stimulus" + +const customSchema = { + ...defaultSchema, + keyMappings: {...defaultSchema.keyMappings, a: "a", w: "w", s: "s", d: "d" }, +} + +const app = Application.start(document.documentElement, customSchema); +``` + + ### Global Events Sometimes a controller needs to listen for events dispatched on the global `window` or `document` objects. From 4e55fead5af04485fcfd6543a42a44f37fe67e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Thu, 31 Mar 2022 11:14:39 +0900 Subject: [PATCH 12/29] fixed merge miss --- examples/index.js | 3 +++ src/core/binding_observer.ts | 8 -------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/index.js b/examples/index.js index baa80321..f40397a5 100644 --- a/examples/index.js +++ b/examples/index.js @@ -16,3 +16,6 @@ application.register("hello", HelloController) import SlideshowController from "./controllers/slideshow_controller" application.register("slideshow", SlideshowController) + +import TabsController from "./controllers/tabs_controller" +application.register("tabs", TabsController) diff --git a/src/core/binding_observer.ts b/src/core/binding_observer.ts index 24a456b3..c84895ca 100644 --- a/src/core/binding_observer.ts +++ b/src/core/binding_observer.ts @@ -92,12 +92,4 @@ export class BindingObserver implements ValueListObserverDelegate { elementUnmatchedValue(element: Element, action: Action) { this.disconnectAction(action) } - - elementMatchedNoValue(token: Token) { - const action = Action.forToken(token, this.schema) - this.context.handleWarning( - `Action "${token.content}" references undefined controller "${action.identifier}"`, - `connecting action "${token.content}"` - ) - } } From c8e4a41dabc59e9798662587ca4acdbd591c5d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Sat, 19 Nov 2022 01:41:47 +0900 Subject: [PATCH 13/29] fixed example --- examples/views/tabs.ejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/views/tabs.ejs b/examples/views/tabs.ejs index a6682594..5a1b8254 100644 --- a/examples/views/tabs.ejs +++ b/examples/views/tabs.ejs @@ -1,4 +1,4 @@ -<% include layout/head %> +<%- include("layout/head") %>

This tabbed interface is operated by focusing on a button and pressing the left and right keys.

@@ -39,4 +39,4 @@ >🙈
-<% include layout/tail %> +<%- include("layout/tail") %> From 53e1cdf6f385a352a428bb4834f04cfea5da6373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=AD=E5=B3=B6=20=E6=8B=93=E5=93=89?= Date: Sat, 19 Nov 2022 10:38:28 +0900 Subject: [PATCH 14/29] add modifier key(meta, ctrl, alt, shift) --- src/core/action.ts | 25 +++++++++++++---- src/core/binding.ts | 2 +- .../core/action_keyboard_filter_tests.ts | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/core/action.ts b/src/core/action.ts index 467a2ed1..3ac4771e 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -35,12 +35,27 @@ export class Action { return `${this.eventName}${eventFilter}${eventTarget}->${this.identifier}#${this.methodName}` } - isFilterTarget(key: string): boolean { - if (!(this.keyFilter && this.keyMappings[this.keyFilter])) { - return false; + isFilterTarget(event: KeyboardEvent): boolean { + if (!this.keyFilter) { return false } + + const filteres = this.keyFilter.split("+") + const modifiers = ["meta", "ctrl", "alt", "shift"] + const [meta, ctrl, alt, shift] = modifiers.map(modifier => filteres.includes(modifier)) + + if (event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift) { + return true + } + + const standardFilter = filteres.filter(key => !modifiers.includes(key))[0] + if (!standardFilter) { // missing non modifier key + return false + } + + if (!this.keyMappings.hasOwnProperty(standardFilter)) { + error(`contains unkown key filter: ${this.keyFilter}`) } - return this.keyMappings[this.keyFilter] !== key + return this.keyMappings[standardFilter].toLowerCase() !== event.key.toLowerCase() } get params() { @@ -62,7 +77,7 @@ export class Action { } private get keyMappings() { - return this.schema.keyMappings; + return this.schema.keyMappings } } diff --git a/src/core/binding.ts b/src/core/binding.ts index 73147519..c05c34a6 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -82,7 +82,7 @@ export class Binding { private willBeInvokedByEvent(event: Event): boolean { const eventTarget = event.target - if (event instanceof KeyboardEvent && this.action.isFilterTarget(event.key)) { + if (event instanceof KeyboardEvent && this.action.isFilterTarget(event)) { return false; } diff --git a/src/tests/modules/core/action_keyboard_filter_tests.ts b/src/tests/modules/core/action_keyboard_filter_tests.ts index b2b6c6b5..85d70ceb 100644 --- a/src/tests/modules/core/action_keyboard_filter_tests.ts +++ b/src/tests/modules/core/action_keyboard_filter_tests.ts @@ -20,6 +20,7 @@ export default class ActionKeyboardFilterTests extends LogControllerTestCase { +