diff --git a/plugins/unsloth-chat/index.jsx b/plugins/unsloth-chat/index.jsx index a739ae4..945d8a4 100644 --- a/plugins/unsloth-chat/index.jsx +++ b/plugins/unsloth-chat/index.jsx @@ -83,62 +83,90 @@ function setMessageInput(text) { '[class*="chat"] [class*="channelTextArea"] [role="textbox"]', ]; - let textarea = null; + let editorEl = null; for (const sel of selectors) { - textarea = document.querySelector(sel); - if (textarea) break; + editorEl = document.querySelector(sel); + if (editorEl) break; } - if (!textarea) return false; + if (!editorEl) return false; // Determine the native input element let nativeInput; const isContentEditable = - textarea.tagName !== "TEXTAREA" && - (textarea.isContentEditable || textarea.getAttribute("contenteditable") === "true"); + editorEl.tagName !== "TEXTAREA" && + (editorEl.isContentEditable || editorEl.getAttribute("contenteditable") === "true"); - if (textarea.tagName === "TEXTAREA") { + if (editorEl.tagName === "TEXTAREA") { // Classic Discord: direct textarea element - nativeInput = textarea; + nativeInput = editorEl; } else if (isContentEditable) { // Modern Discord: Slate/contenteditable div - use it directly - nativeInput = textarea; + nativeInput = editorEl; } else { // Look for a nested textarea (older layout) - nativeInput = textarea.querySelector("textarea"); + nativeInput = editorEl.querySelector("textarea"); } if (!nativeInput) return false; - // Set the value + // ── Set the value ─────────────────────────────────────────── nativeInput.focus(); if (nativeInput.tagName === "TEXTAREA") { // Classic textarea input nativeInput.value = text; + nativeInput.dispatchEvent(new Event("input", { bubbles: true, cancelable: true })); } else { - // ContentEditable div (Slate editor / modern Discord) - // Clear existing content and insert new text - nativeInput.textContent = ""; - nativeInput.focus(); + // ContentEditable div — modern Discord Slate editor + // Slate listens for "beforeinput" events, not plain DOM mutations. + // We select all existing content, then dispatch beforeinput so + // Slate handles the replacement through its own React pipeline. - // Use execCommand as fallback for Slate-based editors - try { - document.execCommand("insertText", false, text); - } catch (_) { - // Fallback: set textContent directly - nativeInput.textContent = text; + // 1. Select all existing content so the insert replaces everything + const sel = window.getSelection(); + if (sel) { + try { + const range = document.createRange(); + range.selectNodeContents(nativeInput); + sel.removeAllRanges(); + sel.addRange(range); + } catch (_) { /* selection API may fail on some elements */ } } - } - // Dispatch an input event so React picks up the change - const nativeInputEv = new Event("input", { bubbles: true, cancelable: true }); - nativeInput.dispatchEvent(nativeInputEv); + // 2. Dispatch beforeinput — this is what Slate's withReact + // plugin hooks into to update the editor's React state. + // dispatchEvent returns FALSE when the event was cancelled + // (Slate calls preventDefault when it handles the event). + // TRUE means nobody cancelled -> we need the fallback. + let needsFallback = true; + try { + const notCancelled = nativeInput.dispatchEvent(new InputEvent("beforeinput", { + inputType: "insertText", + data: text, + bubbles: true, + cancelable: true, + })); + needsFallback = notCancelled; + } catch (_) { + // InputEvent constructor may not be available (old Electron) + } - // Also dispatch on the outer element if it differs and is contenteditable - if (textarea !== nativeInput && textarea.isContentEditable) { - textarea.textContent = text; - textarea.dispatchEvent(new Event("input", { bubbles: true })); + // 3. Fallback: if beforeinput wasn't cancelled by any handler + // use execCommand which works on older Discord layouts. + if (needsFallback) { + try { + document.execCommand("selectAll", false, null); + document.execCommand("insertText", false, text); + } catch (_) { + // Last resort: set textContent directly + nativeInput.textContent = text; + } + } + + // 4. Always dispatch input — React-controlled components often + // listen for this in addition to beforeinput. + nativeInput.dispatchEvent(new Event("input", { bubbles: true })); } return true;