|
| 1 | +import { Scrapbox } from "../../deps/scrapbox.ts"; |
| 2 | +import { textInput } from "./dom.ts"; |
| 3 | +import { decode, encode } from "./_internal.ts"; |
| 4 | +declare const scrapbox: Scrapbox; |
| 5 | + |
| 6 | +/** - first key: event name |
| 7 | + * - second key: listener |
| 8 | + * - value: encoded options |
| 9 | + */ |
| 10 | +const listenerMap = new Map< |
| 11 | + keyof HTMLElementEventMap, |
| 12 | + Map<EventListener, Set<number>> |
| 13 | +>(); |
| 14 | +const onceListenerMap = new Map<EventListener, Map<number, EventListener>>(); |
| 15 | + |
| 16 | +/** `#text-input`に対してイベントリスナーを追加する |
| 17 | + * |
| 18 | + * `#text-input`はページレイアウトが変わると削除されるため、登録したイベントリスナーの記憶と再登録をこの関数で行っている |
| 19 | + * |
| 20 | + * @param name event name |
| 21 | + * @param listener event listener |
| 22 | + * @param options event listener options |
| 23 | + * @returns |
| 24 | + */ |
| 25 | +export const addTextInputEventListener = <K extends keyof HTMLElementEventMap>( |
| 26 | + name: K, |
| 27 | + listener: ( |
| 28 | + this: HTMLTextAreaElement, |
| 29 | + event: HTMLElementEventMap[K], |
| 30 | + ) => unknown, |
| 31 | + options?: boolean | AddEventListenerOptions, |
| 32 | +): void => { |
| 33 | + const argMap = listenerMap.get(name) ?? new Map<EventListener, Set<number>>(); |
| 34 | + const encodedOptions = argMap.get(listener as EventListener) ?? new Set(); |
| 35 | + if (encodedOptions.has(encode(options))) return; |
| 36 | + encodedOptions.add(encode(options)); |
| 37 | + argMap.set(listener as EventListener, encodedOptions); |
| 38 | + listenerMap.set(name, argMap); |
| 39 | + if (typeof options === "object" && options?.once) { |
| 40 | + const onceMap = onceListenerMap.get(listener as EventListener) ?? |
| 41 | + new Map<number, EventListener>(); |
| 42 | + const encoded = encode(options); |
| 43 | + |
| 44 | + /** 呼び出し時に、`listenerMap`からの登録も解除するwrapper listener */ |
| 45 | + const onceListener = function ( |
| 46 | + this: HTMLTextAreaElement, |
| 47 | + event: HTMLElementEventMap[K], |
| 48 | + ) { |
| 49 | + removeTextInputEventListener(name, listener, options); |
| 50 | + onceMap.delete(encoded); |
| 51 | + return listener.call(this, event); |
| 52 | + }; |
| 53 | + onceMap.set(encoded, onceListener as EventListener); |
| 54 | + onceListenerMap.set(listener as EventListener, onceMap); |
| 55 | + |
| 56 | + const textinput = textInput(); |
| 57 | + if (!textinput) return; |
| 58 | + textinput.addEventListener<K>(name, onceListener, options); |
| 59 | + } |
| 60 | + const textinput = textInput(); |
| 61 | + if (!textinput) return; |
| 62 | + textinput.addEventListener<K>(name, listener, options); |
| 63 | +}; |
| 64 | + |
| 65 | +// re-register event listeners when the layout changes |
| 66 | +scrapbox.on("layout:changed", () => { |
| 67 | + const textinput = textInput(); |
| 68 | + if (!textinput) return; |
| 69 | + for (const [name, argMap] of listenerMap) { |
| 70 | + for (const [listener, encodedOptions] of argMap) { |
| 71 | + for (const encoded of encodedOptions) { |
| 72 | + textinput.addEventListener( |
| 73 | + name, |
| 74 | + listener as EventListener, |
| 75 | + decode(encoded), |
| 76 | + ); |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | +}); |
| 81 | + |
| 82 | +export const removeTextInputEventListener = < |
| 83 | + K extends keyof HTMLElementEventMap, |
| 84 | +>( |
| 85 | + name: K, |
| 86 | + listener: (event: HTMLElementEventMap[K]) => unknown, |
| 87 | + options?: boolean | AddEventListenerOptions, |
| 88 | +): void => { |
| 89 | + const argMap = listenerMap.get(name); |
| 90 | + if (!argMap) return; |
| 91 | + const encodedOptions = argMap.get(listener as EventListener); |
| 92 | + if (!encodedOptions) return; |
| 93 | + const encoded = encode(options); |
| 94 | + encodedOptions.delete(encoded); |
| 95 | + if (typeof options === "object" && options?.once) { |
| 96 | + const onceMap = onceListenerMap.get(listener as EventListener); |
| 97 | + if (!onceMap) return; |
| 98 | + const onceListener = onceMap.get(encoded); |
| 99 | + if (!onceListener) return; |
| 100 | + |
| 101 | + const textinput = textInput(); |
| 102 | + if (!textinput) return; |
| 103 | + textinput.removeEventListener(name, onceListener, options); |
| 104 | + onceMap.delete(encoded); |
| 105 | + return; |
| 106 | + } |
| 107 | + const textinput = textInput(); |
| 108 | + if (!textinput) return; |
| 109 | + textinput.removeEventListener(name, listener, options); |
| 110 | +}; |
0 commit comments