From ae920f06711cee625e204032a40ce737415dcdfe Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 13:00:58 +0200 Subject: [PATCH 1/4] Panel event module --- panel/src/panel/events.js | 345 ++++++++++++++++++++++++ panel/src/panel/events.keychain.test.js | 116 ++++++++ 2 files changed, 461 insertions(+) create mode 100644 panel/src/panel/events.js create mode 100644 panel/src/panel/events.keychain.test.js diff --git a/panel/src/panel/events.js b/panel/src/panel/events.js new file mode 100644 index 0000000000..9b19779fee --- /dev/null +++ b/panel/src/panel/events.js @@ -0,0 +1,345 @@ +import { lcfirst } from "@/helpers/string"; +import mitt from "mitt"; + +/** + * Global event delegation and event bus + * which can be used by any component in the app + * to start and stop listening to events + */ +export default () => { + const emitter = mitt(); + + /** + * Config for globally delegated events. + * Some events need to be fired on the document + * and some on window. The boolean value determins if + * they capture events on children or not. + */ + const events = { + document: { + blur: true, + click: false, + copy: true, + focus: true, + paste: true + }, + window: { + dragenter: false, + dragexit: false, + dragleave: false, + dragover: false, + drop: false, + keydown: false, + keyup: false, + offline: false, + online: false, + popstate: false + } + }; + + /** + * Each globally delegated event + * has its own method. This helps + * to add and remove the event listeners + * properly and some event listeners also + * have a bit of extra treatment in those + * methods (i.e. drag events) + */ + return { + /** + * Global blur event + * + * @param {Event} e + */ + blur(e) { + this.emit("blur", e); + }, + + /** + * Global click event + * + * @param {Event} e + */ + click(e) { + this.emit("click", e); + }, + + /** + * Global clipboard copy event + * + * @param {Event} e + */ + copy(e) { + this.emit("copy", e); + }, + + /** + * Global dragenter event, which + * prevents the default and keeps + * track of the entered element. + * + * @param {Event} e + */ + dragenter(e) { + this.entered = e.target; + this.prevent(e); + this.emit("dragenter", e); + }, + + /** + * Global dragexit event, which + * prevents the default + * + * @param {Event} e + */ + dragexit(e) { + this.prevent(e); + this.emit("dragexit", e); + }, + + /** + * Global dragleave event, which + * prevents the default and also + * is only fired when the entered + * element matches with the left element + * + * @param {Event} e + */ + dragleave(e) { + this.prevent(e); + + if (this.entered === e.target) { + this.emit("dragleave", e); + } + }, + + /** + * Global dragover event, which + * prevents the default + * + * @param {Event} e + */ + dragover(e) { + this.prevent(e); + this.emit("dragover", e); + }, + + /** + * Global drop event, which + * prevents the default and + * enables dropping elements + * on any Panel component + * + * @param {Event} e + */ + drop(e) { + this.prevent(e); + this.emit("drop", e); + }, + + /** + * Proxy for mitt's emit method + */ + emit: emitter.emit, + + /** + * Keeps track of the entered element + * on drag events + */ + entered: null, + + /** + * Global focus event + * + * @param {Event} e + */ + focus(e) { + this.emit("focus", e); + }, + + /** + * The keychain helper function creates + * a key modifier string which is used in + * global keyup and keydown events to send a + * more specific global event. This is super + * useful if you only want to listen to a + * particular keyboard shortcut. + * + * @example + * window.panel.events.on("keydown.esc", () => {}) + * window.panel.events.on("keydown.cmd.s", () => {}) + */ + keychain(type, event) { + let parts = [type]; + + // with meta or control key + if (event.metaKey || event.ctrlKey) { + parts.push("cmd"); + } + + if (event.altKey === true) { + parts.push("alt"); + } + + if (event.shiftKey === true) { + parts.push("shift"); + } + + let key = event.key ? lcfirst(event.key) : null; + + // key replacements + const keys = { + escape: "esc", + arrowUp: "up", + arrowDown: "down", + arrowLeft: "left", + arrowRight: "right" + }; + + if (keys[key]) { + key = keys[key]; + } + + if (key && ["alt", "control", "shift", "meta"].includes(key) === false) { + parts.push(key); + } + + return parts.join("."); + }, + + /** + * Global keydown event which also + * fires a more useful event with + * key modifiers. I.e. keydown.esc + * + * @param {Event} e + */ + keydown(e) { + this.emit(this.keychain("keydown", e), e); + this.emit("keydown", e); + }, + + /** + * Global keyup event which also + * fires a more useful event with + * key modifiers. I.e. keyup.esc + * + * @param {Event} e + */ + keyup(e) { + this.emit(keychain("keyup", e), e); + this.emit("keyup", e); + }, + + /** + * Proxy for mitt's off method + */ + off: emitter.off, + + /** + * The Panel just went offline + * + * @param {Event} e + */ + offline(e) { + this.emit("offline", e); + }, + + /** + * Proxy for mitt's on method + */ + on: emitter.on, + + /** + * The Panel is online again after + * being offline + * + * @param {Event} e + */ + online(e) { + this.emit("online", e); + }, + + /** + * Global clipboard paste event + * + * @param {Event} e + */ + paste(e) { + this.emit("paste", e); + }, + + /** + * Browser back button event + * + * @param {Event} e + */ + popstate(e) { + this.emit("popstate", e); + }, + + /** + * Prevents the event from bubbling + * and stops the default behaviour. + * + * @param {Event} e + */ + prevent(e) { + e.stopPropagation(); + e.preventDefault(); + }, + + /** + * Registers all global event listeners + * from the events config. This is used + * in the created hook of the app. + */ + subscribe() { + for (const event in events.document) { + document.addEventListener( + event, + this[event].bind(this), + events.document[event] + ); + } + + for (const event in events.window) { + window.addEventListener( + event, + this[event].bind(this), + events.window[event] + ); + } + }, + + /** + * Removes all global event listeners + * from the events config. This is + * used in the destroyed hook of the app + */ + unsubscribe() { + for (const event in events.document) { + document.removeEventListener(event, this[event]); + } + + for (const event in events.window) { + window.removeEventListener(event, this[event]); + } + }, + + /** + * @deprecated use this.on instead + */ + $on: emitter.on, + + /** + * @deprecated use this.emit instead + */ + $emit: emitter.emit, + + /** + * @deprecated use this.off instead + */ + $off: emitter.off + }; +}; diff --git a/panel/src/panel/events.keychain.test.js b/panel/src/panel/events.keychain.test.js new file mode 100644 index 0000000000..329020d639 --- /dev/null +++ b/panel/src/panel/events.keychain.test.js @@ -0,0 +1,116 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Events from "./events.js"; + +describe.concurrent("panel.events.keychain", () => { + const events = Events(); + + it("should only add the type", async () => { + const result = events.keychain("keydown", {}); + expect(result).toStrictEqual("keydown"); + }); + + it("should add shift", async () => { + const result = events.keychain("keydown", { + shiftKey: true + }); + expect(result).toStrictEqual("keydown.shift"); + }); + + it("should add shift & key", async () => { + const result = events.keychain("keydown", { + shiftKey: true, + key: "s" + }); + + expect(result).toStrictEqual("keydown.shift.s"); + }); + + it("should add cmd", async () => { + // Cmd + const cmd = events.keychain("keydown", { + metaKey: true + }); + + expect(cmd).toStrictEqual("keydown.cmd"); + + // Ctrl + const ctrl = events.keychain("keydown", { + ctrlKey: true + }); + + expect(ctrl).toStrictEqual("keydown.cmd"); + }); + + it("should add cmd & key", async () => { + const result = events.keychain("keydown", { + metaKey: true, + key: "s" + }); + + expect(result).toStrictEqual("keydown.cmd.s"); + }); + + it("should add alt", async () => { + const result = events.keychain("keydown", { + altKey: true + }); + + expect(result).toStrictEqual("keydown.alt"); + }); + + it("should add alt & key", async () => { + const result = events.keychain("keydown", { + altKey: true, + key: "v" + }); + + expect(result).toStrictEqual("keydown.alt.v"); + }); + + it("should add key replacement", async () => { + const esc = events.keychain("keydown", { + key: "escape" + }); + + expect(esc).toStrictEqual("keydown.esc"); + + const up = events.keychain("keydown", { + key: "arrowUp" + }); + + expect(up).toStrictEqual("keydown.up"); + + const down = events.keychain("keydown", { + key: "arrowDown" + }); + + expect(down).toStrictEqual("keydown.down"); + + const left = events.keychain("keydown", { + key: "arrowLeft" + }); + + expect(left).toStrictEqual("keydown.left"); + + const right = events.keychain("keydown", { + key: "arrowRight" + }); + + expect(right).toStrictEqual("keydown.right"); + }); + + it("should add combo", async () => { + const result = events.keychain("keydown", { + metaKey: true, + shiftKey: true, + altKey: true, + key: "escape" + }); + + expect(result).toStrictEqual("keydown.cmd.alt.shift.esc"); + }); +}); From 41a74c2a9c965732d7f53a225d1377dea130918b Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 13:12:13 +0200 Subject: [PATCH 2/4] Unit tests for the event module --- panel/src/panel/events.drag.test.js | 40 ++++++++++++++++++++++++++ panel/src/panel/events.js | 2 +- panel/src/panel/events.keydown.test.js | 25 ++++++++++++++++ panel/src/panel/events.keyup.test.js | 25 ++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 panel/src/panel/events.drag.test.js create mode 100644 panel/src/panel/events.keydown.test.js create mode 100644 panel/src/panel/events.keyup.test.js diff --git a/panel/src/panel/events.drag.test.js b/panel/src/panel/events.drag.test.js new file mode 100644 index 0000000000..f063142472 --- /dev/null +++ b/panel/src/panel/events.drag.test.js @@ -0,0 +1,40 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Events from "./events.js"; + +describe.concurrent("panel.events drag & drop", () => { + const events = Events(); + + it("should keep track of target", async () => { + let fired = false; + + const eventA = { + target: "targetA", + preventDefault: () => {}, + stopPropagation: () => {} + }; + + const eventB = { + ...eventA, + target: "targetB" + }; + + events.on("dragleave", () => { + fired = true; + }); + + events.dragenter(eventA); + expect(events.entered).toStrictEqual(eventA.target); + + // should not fire because it's a different target + events.dragleave(eventB); + expect(fired).toStrictEqual(false); + + // should fire because it's the same target + events.dragleave(eventA); + expect(fired).toStrictEqual(true); + }); +}); diff --git a/panel/src/panel/events.js b/panel/src/panel/events.js index 9b19779fee..b2a21adedf 100644 --- a/panel/src/panel/events.js +++ b/panel/src/panel/events.js @@ -227,7 +227,7 @@ export default () => { * @param {Event} e */ keyup(e) { - this.emit(keychain("keyup", e), e); + this.emit(this.keychain("keyup", e), e); this.emit("keyup", e); }, diff --git a/panel/src/panel/events.keydown.test.js b/panel/src/panel/events.keydown.test.js new file mode 100644 index 0000000000..78dcb381b0 --- /dev/null +++ b/panel/src/panel/events.keydown.test.js @@ -0,0 +1,25 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Events from "./events.js"; + +describe.concurrent("panel.events.keydown", () => { + const events = Events(); + + it("should fire keydown event with modifiers", async () => { + let fired = false; + + events.on("keydown.cmd.s", () => { + fired = true; + }); + + events.keydown({ + metaKey: true, + key: "s" + }); + + expect(fired).toStrictEqual(true); + }); +}); diff --git a/panel/src/panel/events.keyup.test.js b/panel/src/panel/events.keyup.test.js new file mode 100644 index 0000000000..3eca4ef3cf --- /dev/null +++ b/panel/src/panel/events.keyup.test.js @@ -0,0 +1,25 @@ +/** + * @vitest-environment node + */ + +import { describe, expect, it } from "vitest"; +import Events from "./events.js"; + +describe.concurrent("panel.events.keyup", () => { + const events = Events(); + + it("should fire keyup event with modifiers", async () => { + let fired = false; + + events.on("keyup.cmd.s", () => { + fired = true; + }); + + events.keyup({ + metaKey: true, + key: "s" + }); + + expect(fired).toStrictEqual(true); + }); +}); From 7e9e428509c31a3304bed89438412190d0ff9f3c Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 13:15:05 +0200 Subject: [PATCH 3/4] Replace old event bus --- panel/src/config/events.js | 116 +++---------------------------------- 1 file changed, 9 insertions(+), 107 deletions(-) diff --git a/panel/src/config/events.js b/panel/src/config/events.js index 76fdbc1edf..b52a2348a6 100644 --- a/panel/src/config/events.js +++ b/panel/src/config/events.js @@ -1,114 +1,16 @@ -import mitt from "mitt"; +import Events from "@/panel/events.js"; +/** + * This is just a temporary + * implementation to keep the old event bus + * until window.panel is fully implemented + */ export default { install(app) { - const emitter = mitt(); + const events = Events(); - const bus = { - $on: emitter.on, - $off: emitter.off, - $emit: emitter.emit, - blur(e) { - bus.$emit("blur", e); - }, - click(e) { - bus.$emit("click", e); - }, - copy(e) { - bus.$emit("copy", e); - }, - dragenter(e) { - bus.entered = e.target; - bus.prevent(e); - bus.$emit("dragenter", e); - }, - dragleave(e) { - bus.prevent(e); + events.subscribe(); - if (bus.entered === e.target) { - bus.$emit("dragleave", e); - } - }, - drop(e) { - bus.prevent(e); - bus.$emit("drop", e); - }, - entered: null, - focus(e) { - bus.$emit("focus", e); - }, - keydown(e) { - let parts = ["keydown"]; - - // with meta or control key - if (e.metaKey || e.ctrlKey) { - parts.push("cmd"); - } - - if (e.altKey === true) { - parts.push("alt"); - } - - if (e.shiftKey === true) { - parts.push("shift"); - } - - let key = app.prototype.$helper.string.lcfirst(e.key); - - // key replacements - const keys = { - escape: "esc", - arrowUp: "up", - arrowDown: "down", - arrowLeft: "left", - arrowRight: "right" - }; - - if (keys[key]) { - key = keys[key]; - } - - if (["alt", "control", "shift", "meta"].includes(key) === false) { - parts.push(key); - } - - bus.$emit(parts.join("."), e); - bus.$emit("keydown", e); - }, - keyup(e) { - bus.$emit("keyup", e); - }, - online(e) { - bus.$emit("online", e); - }, - offline(e) { - bus.$emit("offline", e); - }, - paste(e) { - bus.$emit("paste", e); - }, - prevent(e) { - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener("click", bus.click, false); - document.addEventListener("copy", bus.copy, true); - document.addEventListener("focus", bus.focus, true); - document.addEventListener("paste", bus.paste, true); - - window.addEventListener("blur", bus.blur, false); - window.addEventListener("dragenter", bus.dragenter, false); - window.addEventListener("dragexit", bus.prevent, false); - window.addEventListener("dragleave", bus.dragleave, false); - window.addEventListener("drop", bus.drop, false); - window.addEventListener("dragover", bus.prevent, false); - window.addEventListener("keydown", bus.keydown, false); - window.addEventListener("keyup", bus.keyup, false); - window.addEventListener("offline", bus.offline); - window.addEventListener("online", bus.online); - - app.prototype.$events = bus; + app.prototype.$events = events; } }; From 6bc486a28156e5d3da7dae3998dac8201147db70 Mon Sep 17 00:00:00 2001 From: Bastian Allgeier Date: Wed, 5 Apr 2023 18:19:01 +0200 Subject: [PATCH 4/4] Reseting entered --- panel/src/panel/events.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/panel/src/panel/events.js b/panel/src/panel/events.js index b2a21adedf..2c589482d9 100644 --- a/panel/src/panel/events.js +++ b/panel/src/panel/events.js @@ -94,6 +94,7 @@ export default () => { */ dragexit(e) { this.prevent(e); + this.entered = null; this.emit("dragexit", e); }, @@ -109,6 +110,7 @@ export default () => { this.prevent(e); if (this.entered === e.target) { + this.entered = null; this.emit("dragleave", e); } }, @@ -134,6 +136,7 @@ export default () => { */ drop(e) { this.prevent(e); + this.entered = null; this.emit("drop", e); },