diff --git a/ambient.d.ts b/ambient.d.ts index c5c7f58..b0545e2 100644 --- a/ambient.d.ts +++ b/ambient.d.ts @@ -349,7 +349,7 @@ interface Utils { complete_do_target(actions: {self: unknown, others: unknown}): Set complete_do(this: Optional): undefined replace_me(_match: string, _offset: number, whole: string): string - //pad_chat(chat: HTMLDivElement): undefined + scroll(): true } interface Complete { @@ -445,15 +445,25 @@ interface InputHistory { */ ids: Set | undefined + /** + * Specific key handlers. + */ + key: Record true> + + /** + * Set or unset the readonly mode on the input. + */ + icro(textarea: HTMLTextAreaElement, readonly: boolean): true + /** * Enter the history mode. */ enter(textarea: HTMLTextAreaElement, input: string, bottom: boolean, ids: Set): true /** - * Exit the history mode and optionally restore original input. + * Exit the history mode and proc the key event. */ - exit(textarea: HTMLTextAreaElement, restore_input: boolean): true + exit(textarea: HTMLTextAreaElement, key: KeyboardEvent['key']): true } // FIXME spread around readonly where appropriate diff --git a/mbchc.mjs b/mbchc.mjs index a87ebe0..2ea0126 100644 --- a/mbchc.mjs +++ b/mbchc.mjs @@ -158,7 +158,7 @@ const/**@type {Utils}*/U = { remove_loader_hook: $, RGB: {Polly: '#81b1e7', Mute return $Ss })}, replace_me: (_, __, whole) => cur(whole.slice(1), t => `${U.ACT}<${U.cid(W.Player)}:>SourceCharacter${t.startsWith('\'') || t.startsWith(' ') ? $S : ' '}`), - //pad_chat: c => loo(100, _ => c.scrollHeight <= c.clientHeight, _ => yes(void c.prepend(U.mkdiv('\u061C')))) && void W.ElementScrollToEnd('TextAreaChatLog') + scroll: () => yes(void W.ElementScrollToEnd('TextAreaChatLog')), } const/**@type {SUBCOMMANDS}*/SUBCOMMANDS_MBCHC = { @@ -178,7 +178,7 @@ const/**@type {Complete}*/C = { S_OPTS: {behavior: 'instant'}, hint: cs => m_t(cs) || yes(_ => { yes(C.e.style.display = 'block') && C.div?.replaceChildren(...[...cs].sort().reverse().map(U.mkdiv)) W.ElementSetDataAttribute(C.e.id, 'colortheme', W.Player.ChatSettings?.ColorTheme ?? 'Light') - cur(W.ElementIsScrolledToEnd('TextAreaChatLog'), r => yes(void W.ChatRoomResize(false)) && r && void W.ElementScrollToEnd('TextAreaChatLog')) + cur(W.ElementIsScrolledToEnd('TextAreaChatLog'), r => yes(void W.ChatRoomResize(false)) && r && U.scroll()) C.div?.lastElementChild?.scrollIntoView(C.S_OPTS) }), get hidden() {return C.e.parentElement === null || C.e.style.display === 'none'}, @@ -203,8 +203,13 @@ const/**@type {Complete}*/C = { S_OPTS: {behavior: 'instant'}, } const/**@type {InputHistory}*/H = { input: undefined, ids: undefined, bottom: undefined, // FIXME ids don't need to be a set, but I'm too tired right now - enter: (ic, i, b, is) => yes(H.input = i, H.bottom = b, H.ids = is, ic.readOnly = true, val(ic.parentElement?.parentElement?.dataset, d => d['mbchcMode'] = 'h'), b && void W.ElementScrollToEnd('TextAreaChatLog')), - exit: (ic, r) => yes(ic.readOnly = false, val(ic.parentElement?.parentElement?.dataset, d => del(d, 'mbchcMode')), r && val(H.input, i => ic.value = i), val(H.bottom, b => b && void W.ElementScrollToEnd('TextAreaChatLog')), W.ChatRoomLastMessageIndex = W.ChatRoomLastMessage.length) + key: { + Escape: ic => yes(val(H.input, i => ic.value = i)), + ArrowLeft: ic => yes(void ic.setSelectionRange(0, 0)), + }, + icro: (ic, ro) => yes(ic.readOnly = ro, val(ic.parentElement?.parentElement?.dataset, d => ro ? d['mbchcMode'] = 'h' : del(d, 'mbchcMode'))), + enter: (ic, i, b, is) => yes(H.input = i, H.bottom = b, H.ids = is, H.icro(ic, true), b && U.scroll()), + exit: (ic, k) => yes(H.icro(ic, false), H.key[k]?.(ic), val(H.bottom, b => b && U.scroll()), W.ChatRoomLastMessageIndex = W.ChatRoomLastMessage.length) } ass('MBCHC found, aborting loading', W.MBCHC === $) @@ -475,7 +480,7 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n) // W.ElementSetDataAttribute(this.COMP_HINT.id, 'colortheme', (W.Player.ChatSettings?.ColorTheme || 'Light')) // const rescroll = W.ElementIsScrolledToEnd('TextAreaChatLog') // W.ChatRoomResize(false) - // if (rescroll) W.ElementScrollToEnd('TextAreaChatLog') + // if (rescroll) U.scroll() // this.COMP_HINT.firstChild?.lastChild?.scrollIntoView({behaviour: 'instant'}) //}, @@ -585,7 +590,7 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n) #${C.e.id}[data-colortheme^="dark"] > div { background-color: ${U.RGB.Mute}; color: white; } #${C.e.id} > div div { margin: 0.25ex 0; } #chat-room-div #TextAreaChatLog::before { content: ''; display: block; height: 100%; } - #chat-room-div[data-mbchc-mode="h"] #TextAreaChatLog::after { content: 'π—΅π—Άπ˜€π˜π—Όπ—Ώπ˜† βŸ¨π˜—π˜¨π˜œπ˜±/π˜‹π˜―βŸ© π—Œπ–Όπ—‹π—ˆπ—…π—… β‡… ⟨𝘌𝘯𝘡𝘦𝘳⟩ π—Œπ–Ύπ—‡π–½ ↡ βŸ¨π˜›π˜’π˜£βŸ© 𝖾𝖽𝗂𝗍 ⌨ ⟨𝘌𝘴𝘀⟩ π–Ίπ–»π—ˆπ—‹π— ⟲'; display: block; position: sticky; bottom: 0; background: black; color: orange; padding: 0 0.2ex; animation: 0.2s cubic-bezier(0.19, 1, 0.22, 1) mbchc_hist_hint_show; } + #chat-room-div[data-mbchc-mode="h"] #TextAreaChatLog::after { content: 'π—΅π—Άπ˜€π˜π—Όπ—Ώπ˜† βŸ¨π˜—π˜¨π˜œπ˜±/π˜‹π˜―/β†•βŸ© π—Œπ–Όπ—‹π—ˆπ—…π—… β‡… ⟨𝘌𝘯𝘡𝘦𝘳⟩ π—Œπ–Ύπ—‡π–½ ↡ βŸ¨π˜›π˜’π˜£/β†”βŸ© 𝖾𝖽𝗂𝗍 ⌨ ⟨𝘌𝘴𝘀⟩ π–Ίπ–»π—ˆπ—‹π— ⟲'; display: block; position: sticky; bottom: 0; background: black; color: orange; padding: 0 0.2ex; animation: 0.2s cubic-bezier(0.19, 1, 0.22, 1) mbchc_hist_hint_show; } #InputChat:read-only { background: black; color: orange; } @keyframes mbchc_hist_hint_show { from {transform: translateX(-100%);} to {transform: translateX(0);} } `) @@ -630,11 +635,11 @@ const/**@type {SDK.Hook}*/after = (name, f) => mod.hookFunction(name, 0, (na, n) C.hide() const ic = this // eslint-disable-line unicorn/no-this-assignment,@typescript-eslint/no-this-alias if (ic.readOnly) switch (e.key) { - case 'Tab': case 'Escape': + case 'ArrowUp': case 'ArrowDown': W.ChatRoomScrollHistory(e.key === 'ArrowUp'); break + case 'Escape': case 'Tab': case 'ArrowRight': case 'ArrowLeft': e.stopImmediatePropagation() e.preventDefault() // falls through - case 'Enter': - H.exit(ic, e.key === 'Escape') // no default + case 'Enter': H.exit(ic, e.key) // no default } } after('ChatRoomCreateElement', () => { // This thing runs on every frame actually. diff --git a/server.js b/server.js index 803dffb..6df9882 100644 --- a/server.js +++ b/server.js @@ -24,7 +24,8 @@ const h_all = { } const server = createServer((rq, rx) => { - rq.method !== undefined && resp[rq.method] !== undefined && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? resp[rq.method](rx) : rx.writeHead(400) + const handler = resp[rq.method ?? ''] + handler !== undefined && (new URL(`http://${config.host}:${config.port}${rq.url}`)).pathname === '/' ? handler(rx) : rx.writeHead(400) rx.end(() => void console.log('%s %d %s %s', (new Date()).toISOString(), rx.statusCode, rq.method, rq.url)) }) server.listen(config.port, config.host, () => void console.log(`Server started at http://${config.host}:${config.port} for ${config.filename}`))