@@ -49,6 +49,124 @@ export default defineContentScript({
4949 console . error ( "Failed to watch settings:" , error ) ;
5050 }
5151
52+ // TextareaObserver for dynamic element detection
53+ class TextareaObserver {
54+ private observer : MutationObserver | null = null ;
55+ private processedElements = new WeakSet < HTMLElement > ( ) ;
56+ private pendingElements = new Set < HTMLElement > ( ) ;
57+ private rafId : number | null = null ;
58+
59+ start ( ) {
60+ if ( this . observer ) return ;
61+
62+ // Process existing textareas first
63+ this . scanExistingElements ( ) ;
64+
65+ this . observer = new MutationObserver ( ( mutations ) => {
66+ this . handleMutations ( mutations ) ;
67+ } ) ;
68+
69+ this . observer . observe ( document . body , {
70+ childList : true ,
71+ subtree : true ,
72+ attributes : true ,
73+ attributeFilter : [ "contenteditable" , "role" ] ,
74+ } ) ;
75+ }
76+
77+ stop ( ) {
78+ if ( this . rafId ) {
79+ cancelAnimationFrame ( this . rafId ) ;
80+ this . rafId = null ;
81+ }
82+ if ( this . observer ) {
83+ this . observer . disconnect ( ) ;
84+ this . observer = null ;
85+ }
86+ this . pendingElements . clear ( ) ;
87+ }
88+
89+ private scanExistingElements ( ) {
90+ const textareas = document . querySelectorAll (
91+ 'textarea, input, [contenteditable="true"], [role="textbox"]'
92+ ) ;
93+ textareas . forEach ( ( el ) => {
94+ if ( el instanceof HTMLElement && isEditableElement ( el ) ) {
95+ this . scheduleProcessing ( el ) ;
96+ }
97+ } ) ;
98+ }
99+
100+ private handleMutations ( mutations : MutationRecord [ ] ) {
101+ for ( const mutation of mutations ) {
102+ // Handle attribute changes (contenteditable or role added to existing element)
103+ if ( mutation . type === "attributes" && mutation . target instanceof HTMLElement ) {
104+ this . checkElement ( mutation . target ) ;
105+ }
106+
107+ // Handle new nodes
108+ for ( const node of mutation . addedNodes ) {
109+ if ( node instanceof HTMLElement ) {
110+ this . checkElement ( node ) ;
111+ }
112+ }
113+ }
114+ }
115+
116+ private checkElement ( element : HTMLElement ) {
117+ // Check the element itself
118+ if (
119+ isEditableElement ( element ) &&
120+ ! this . processedElements . has ( element )
121+ ) {
122+ this . scheduleProcessing ( element ) ;
123+ }
124+
125+ // Check children
126+ const editables = element . querySelectorAll (
127+ 'textarea, input, [contenteditable="true"], [role="textbox"]'
128+ ) ;
129+ editables . forEach ( ( el ) => {
130+ if (
131+ el instanceof HTMLElement &&
132+ isEditableElement ( el ) &&
133+ ! this . processedElements . has ( el )
134+ ) {
135+ this . scheduleProcessing ( el ) ;
136+ }
137+ } ) ;
138+ }
139+
140+ private scheduleProcessing ( element : HTMLElement ) {
141+ this . pendingElements . add ( element ) ;
142+
143+ if ( ! this . rafId ) {
144+ this . rafId = requestAnimationFrame ( ( ) => {
145+ this . processBatch ( ) ;
146+ } ) ;
147+ }
148+ }
149+
150+ private processBatch ( ) {
151+ this . rafId = null ;
152+
153+ this . pendingElements . forEach ( ( element ) => {
154+ if (
155+ ! this . processedElements . has ( element ) &&
156+ document . contains ( element )
157+ ) {
158+ this . processedElements . add ( element ) ;
159+ handleFocus ( element ) ;
160+ }
161+ } ) ;
162+
163+ this . pendingElements . clear ( ) ;
164+ }
165+ }
166+
167+ const textareaObserver = new TextareaObserver ( ) ;
168+ textareaObserver . start ( ) ;
169+
52170 // Styles for Shadow DOM
53171 const STYLES = `
54172 * {
@@ -1182,6 +1300,7 @@ export default defineContentScript({
11821300 ) ;
11831301
11841302 ctx . onInvalidated ( ( ) => {
1303+ textareaObserver . stop ( ) ;
11851304 cleanup ( ) ;
11861305 if ( unwatchSettings ) unwatchSettings ( ) ;
11871306 } ) ;
0 commit comments