Last active
October 25, 2025 07:24
-
-
Save ncjones/acc6bae44148ee0baa30e8c93827cee7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript:(function(){ | |
| /** | |
| * Bookmarklet to workaround MacOS Voice Control not being able to dictate | |
| * into rich text editors that use <div contenteditable> such as ChatGPT. | |
| * | |
| * ChatGPT uses a nonstandard contenteditable input that does not expose | |
| * itself as a real text input through Apple accessibility APIs. As a result, | |
| * Voice Control shows "dictation not available here" and cannot type into it. | |
| * | |
| * This bookmarklet injects a plain <textarea> above the contenteditable input that | |
| * fully supports macOS Voice Control, then transfers the composed message | |
| * into the original editor on enter. | |
| * Usage | |
| * | |
| * 1) Create a browser bookmark with the URL set to the entire contents of this file. | |
| * 2) Click the bookmarklet to inject a functional text editor above a "contenteditable" element. | |
| * 3) Press the "return" key to replicate content into the original element. | |
| */ | |
| const VC_INPUT_ID = 'vc-textarea'; | |
| function createInput() { | |
| const el = document.createElement('textarea'); | |
| el.id = VC_INPUT_ID; | |
| el.placeholder = 'Dictate here with Voice Control...'; | |
| Object.assign(el.style, { | |
| width: '99%', | |
| height: '80px', | |
| margin: '10px 1px', | |
| borderRadius: '6px', | |
| background: 'inherit', | |
| color: 'inherit', | |
| }); | |
| return el; | |
| } | |
| function sendEnterTo(el) { | |
| const opts = { | |
| key: 'Enter', | |
| code: 'Enter', | |
| keyCode: 13, | |
| which: 13, | |
| bubbles: true, | |
| cancelable: true | |
| }; | |
| el.dispatchEvent(new KeyboardEvent('keydown', opts)); | |
| el.dispatchEvent(new KeyboardEvent('keypress', opts)); | |
| el.dispatchEvent(new KeyboardEvent('keyup', opts)); | |
| } | |
| function populateEditableEl(editable, content) { | |
| editable.innerHTML = content | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .split('\n') | |
| .map(s => `<p>${s}</p>`) | |
| .join('\n'); | |
| editable.dispatchEvent(new InputEvent('input', {bubbles:true})); | |
| } | |
| function populateAndFocusInput(input, content) { | |
| input.value = content; | |
| input.focus(); | |
| document.execCommand('selectAll'); | |
| document.getSelection().collapseToEnd(); | |
| } | |
| function findOrCreateShim(original) { | |
| let input = document.getElementById(VC_INPUT_ID); | |
| if (input) { | |
| return input; | |
| } | |
| input = createInput(); | |
| original?.parentNode.insertBefore(input, original); | |
| suppressMouse(input); | |
| input.addEventListener('keydown', e => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| populateEditableEl(original, input.value); | |
| input.value = ''; | |
| setTimeout(() => sendEnterTo(original), 50); | |
| } | |
| }); | |
| return input; | |
| } | |
| function suppressMouse(target) { | |
| [ | |
| 'click','dblclick','mousedown','mouseup','pointerdown','pointerup','contextmenu' | |
| ].forEach(t => { | |
| target.addEventListener(t, e => e.stopPropagation(), true); | |
| }); | |
| } | |
| function safeHide(el) { | |
| Object.assign(el.style, { | |
| visibility: 'hidden', | |
| position: 'absolute', | |
| height: '0', | |
| minHeight: '0', | |
| overflow: 'hidden', | |
| pointerEvents: 'none', | |
| opacity: '0' | |
| }); | |
| } | |
| const editableArea = document.querySelector('[contenteditable="true"]'); | |
| if (!editableArea) { | |
| return; | |
| } | |
| const inputShim = findOrCreateShim(editableArea); | |
| populateAndFocusInput(inputShim, editableArea.innerText.trim()); | |
| safeHide(editableArea); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment