From e182800c8980911dd351a67f32fc2a0737b5e68f Mon Sep 17 00:00:00 2001 From: Melonify <13710061+Melonify@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:21:25 -0400 Subject: [PATCH 1/4] Better tab-complete * Allow the ability to tab complete words other than the last one * Reset tab complete cursor when input has been changed * Finish tab complete when the user unfocuses the chat box. --- src/Content/Runtime/TabCompleteDetection.tsx | 78 +++++++++++++------- src/Page/Page.tsx | 5 +- src/Page/Runtime/TabCompletion.tsx | 5 ++ 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/Content/Runtime/TabCompleteDetection.tsx b/src/Content/Runtime/TabCompleteDetection.tsx index dbfcd6bac..6867cbdc0 100644 --- a/src/Content/Runtime/TabCompleteDetection.tsx +++ b/src/Content/Runtime/TabCompleteDetection.tsx @@ -6,10 +6,13 @@ import { Twitch } from 'src/Page/Util/Twitch'; export class TabCompleteDetection { tab = { index: 0, - cursor: '' + cursor: '', + expectedValue: '', + expectedCursorLocation: 0 }; - private currentListener: ((this: HTMLInputElement, ev: KeyboardEvent) => any) | undefined; + private keyListener: ((this: HTMLInputElement, ev: KeyboardEvent) => any) | undefined; + private finalizeListener: ((this: HTMLInputElement, ev: Event) => any) | undefined; private emotes = [] as EmoteStore.Emote[]; constructor(public app: App) {} @@ -29,35 +32,43 @@ export class TabCompleteDetection { Logger.Get().info('Handling tab completion'); const input = this.getInput(); - const listener = this.currentListener = (ev) => { + this.keyListener = (ev) => { if (ev.key === 'Tab') { const foundEmotes = this.emotes.map(e => e.name); if (foundEmotes.length === 0) { return undefined; } - this.handleTabPress(ev, foundEmotes); - } else if (resetKeycodes.includes(ev.key)) { // Reset the cursor when the user is done typing an emote - // Remove the last character - // Unless the user is selecting many characters - if (this.tab.cursor.length > 0 && input.selectionStart === input.selectionEnd) { - this.app.sendMessageDown('SetChatInput', input.value.slice(0, input.value.length - 1)); - } + const input = ev.target as HTMLInputElement; + + if (input.value != this.tab.expectedValue) this.resetCursor(); + else if (input.selectionStart != this.tab.expectedCursorLocation) this.resetCursor(); - this.resetCursor(); + this.handleTabPress(ev, foundEmotes); } }; - input.addEventListener('keydown', listener, { + input.addEventListener('keydown', this.keyListener, { + capture: false + }); + + this.finalizeListener = () => this.resetCursor(); + input.addEventListener('change', this.finalizeListener, { capture: false }); + } stop(): void { const input = this.getInput(); - if (typeof this.currentListener === 'function') { - input.removeEventListener('keydown', this.currentListener); + if (typeof this.keyListener === 'function') { + input.removeEventListener('keydown', this.keyListener); + } + + if (typeof this.finalizeListener === 'function') { + input.removeEventListener('change', this.finalizeListener); } + this.emotes = []; } @@ -67,6 +78,8 @@ export class TabCompleteDetection { resetCursor(): void { this.tab.cursor = ''; this.tab.index = 0; + this.tab.expectedValue = ''; + this.tab.expectedCursorLocation = 0; } /** @@ -76,9 +89,22 @@ export class TabCompleteDetection { * @param emotes an array of emote name strings */ private handleTabPress(ev: KeyboardEvent, emotes: string[]): void { - const input = this.getInput(); - const cursorValue = (this.tab.cursor || input.value).match(/\b(\w+)\W*$/)?.[0]; // The current value of the cursor, or the input if no cursor is set - const currentWord = input.value.match(/\b(\w+)\W*$/)?.[0]; // The latest word typed by the user + const input = ev.target as HTMLInputElement; + const inputText = input.value; + const cursorPosition = input.selectionStart || 0; + + let startIndex = 0; + for (let i = cursorPosition - 1; i >= 0; i--) { + const currentChar = inputText.charAt(i); + if (currentChar == " ") { + startIndex = i + 1; + break; + } + } + + let currentWord = inputText.substring(startIndex, cursorPosition); + + const cursorValue = this.tab.cursor || currentWord; // The current value of the cursor, or the input if no cursor is set if (!cursorValue) return undefined; // Find emotes matching the cursor @@ -104,16 +130,16 @@ export class TabCompleteDetection { if (!next) return undefined; // Request the pagescript to modify the input - const final = currentWord ?? ''; - const lastOccurence = input.value.lastIndexOf(final); - this.app.sendMessageDown('SetChatInput', (input.value.slice(0, lastOccurence) + input.value.slice(lastOccurence).replace(final, next + ' ')).slice(0, 500)); + const firstMessageHalf = inputText.substring(0, startIndex) + next; + const newMessage = (firstMessageHalf + inputText.substring(cursorPosition)).slice(0, 500); + const newCursorPosition = firstMessageHalf.length; + + this.tab.expectedValue = newMessage; + this.tab.expectedCursorLocation = newCursorPosition; + + this.app.sendMessageDown('SetChatInput', {message: newMessage, cursorPosition: newCursorPosition}); } } const startsWith = (prefix: string, emoteName: string): boolean => - emoteName.toLowerCase().startsWith(prefix.toLowerCase()); - -const resetKeycodes = [ - ' ', 'Backspace', 'Enter', - 'Delete' -]; + emoteName.toLowerCase().startsWith(prefix.toLowerCase()); \ No newline at end of file diff --git a/src/Page/Page.tsx b/src/Page/Page.tsx index ff4e7968e..9717787aa 100644 --- a/src/Page/Page.tsx +++ b/src/Page/Page.tsx @@ -129,8 +129,9 @@ export class PageScript { } @PageScriptListener('SetChatInput') - whenUserTabCompletesAndTheChatInputBoxShouldBeChanged(value: string): void { - tabCompletion.setInputValue(value); + whenUserTabCompletesAndTheChatInputBoxShouldBeChanged(value: { message: string, cursorPosition: number }): void { + tabCompletion.setInputValue(value.message); + tabCompletion.setInputCursorPosition(value.cursorPosition); } @PageScriptListener('SendSystemMessage') diff --git a/src/Page/Runtime/TabCompletion.tsx b/src/Page/Runtime/TabCompletion.tsx index 15efc2bb8..071bc36e1 100644 --- a/src/Page/Runtime/TabCompletion.tsx +++ b/src/Page/Runtime/TabCompletion.tsx @@ -54,4 +54,9 @@ export class TabCompletion { } } } + + setInputCursorPosition(position: number) { + const el = document.querySelector(Twitch.Selectors.ChatInput) as HTMLInputElement; + el.setSelectionRange(position, position); + } } From 2f68e2293e1d6e80e495e1961db7b654440a17fd Mon Sep 17 00:00:00 2001 From: Melonify <13710061+Melonify@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:25:04 -0400 Subject: [PATCH 2/4] Conform with linting requirements. --- src/Content/Runtime/TabCompleteDetection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Content/Runtime/TabCompleteDetection.tsx b/src/Content/Runtime/TabCompleteDetection.tsx index 6867cbdc0..9c13ccc37 100644 --- a/src/Content/Runtime/TabCompleteDetection.tsx +++ b/src/Content/Runtime/TabCompleteDetection.tsx @@ -96,7 +96,7 @@ export class TabCompleteDetection { let startIndex = 0; for (let i = cursorPosition - 1; i >= 0; i--) { const currentChar = inputText.charAt(i); - if (currentChar == " ") { + if (currentChar == ' ') { startIndex = i + 1; break; } @@ -142,4 +142,4 @@ export class TabCompleteDetection { } const startsWith = (prefix: string, emoteName: string): boolean => - emoteName.toLowerCase().startsWith(prefix.toLowerCase()); \ No newline at end of file + emoteName.toLowerCase().startsWith(prefix.toLowerCase()); From 176afd9f9f086a637e9ee40b50ffad9aa435276b Mon Sep 17 00:00:00 2001 From: Melonify <13710061+Melonify@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:45:29 -0400 Subject: [PATCH 3/4] Ignore space immediately after word when finding words. --- src/Content/Runtime/TabCompleteDetection.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Content/Runtime/TabCompleteDetection.tsx b/src/Content/Runtime/TabCompleteDetection.tsx index 9c13ccc37..44ae7b978 100644 --- a/src/Content/Runtime/TabCompleteDetection.tsx +++ b/src/Content/Runtime/TabCompleteDetection.tsx @@ -93,10 +93,11 @@ export class TabCompleteDetection { const inputText = input.value; const cursorPosition = input.selectionStart || 0; + let searchStart = cursorPosition - 1; let startIndex = 0; - for (let i = cursorPosition - 1; i >= 0; i--) { + for (let i = searchStart; i >= 0; i--) { // Search backwards until we find a space const currentChar = inputText.charAt(i); - if (currentChar == ' ') { + if (currentChar == ' ' && i != searchStart) { // If the first character we hit is a space, skip it startIndex = i + 1; break; } @@ -130,7 +131,7 @@ export class TabCompleteDetection { if (!next) return undefined; // Request the pagescript to modify the input - const firstMessageHalf = inputText.substring(0, startIndex) + next; + const firstMessageHalf = inputText.substring(0, startIndex) + next + " "; const newMessage = (firstMessageHalf + inputText.substring(cursorPosition)).slice(0, 500); const newCursorPosition = firstMessageHalf.length; From 0262256fae95c5412103c4db6a06e2ce5e15fa1a Mon Sep 17 00:00:00 2001 From: Melonify <13710061+Melonify@users.noreply.github.com> Date: Mon, 9 Aug 2021 17:47:30 -0400 Subject: [PATCH 4/4] Fix linting (again) --- src/Content/Runtime/TabCompleteDetection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/Runtime/TabCompleteDetection.tsx b/src/Content/Runtime/TabCompleteDetection.tsx index 44ae7b978..67251ade5 100644 --- a/src/Content/Runtime/TabCompleteDetection.tsx +++ b/src/Content/Runtime/TabCompleteDetection.tsx @@ -131,7 +131,7 @@ export class TabCompleteDetection { if (!next) return undefined; // Request the pagescript to modify the input - const firstMessageHalf = inputText.substring(0, startIndex) + next + " "; + const firstMessageHalf = inputText.substring(0, startIndex) + next + ' '; const newMessage = (firstMessageHalf + inputText.substring(cursorPosition)).slice(0, 500); const newCursorPosition = firstMessageHalf.length;