@@ -12,8 +12,14 @@ import {
1212 TextAddedEvent ,
1313 TextRemovedEvent
1414} from '@editorjs/model' ;
15- import type { EventBus } from '@editorjs/sdk' ;
16- import { type BlockToolAdapter as BlockToolAdapterInterface , type CoreConfig } from '@editorjs/sdk' ;
15+ import type {
16+ EventBus ,
17+ BlockToolAdapter as BlockToolAdapterInterface ,
18+ CoreConfig ,
19+ BeforeInputUIEvent ,
20+ BeforeInputUIEventPayload
21+ } from '@editorjs/sdk' ;
22+ import { BeforeInputUIEventName } from '@editorjs/sdk' ;
1723import type { CaretAdapter } from '../CaretAdapter/index.js' ;
1824import type { FormattingAdapter } from '../FormattingAdapter/index.js' ;
1925import {
@@ -63,6 +69,13 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
6369 */
6470 #config: Required < CoreConfig > ;
6571
72+ /**
73+ * Inputs that bound to the model
74+ *
75+ * @todo handle inputs deletion — remove inputs from the map when they are removed from the DOM
76+ */
77+ #attachedInputs = new Map < DataKey , HTMLElement > ( ) ;
78+
6679 /**
6780 * BlockToolAdapter constructor
6881 *
@@ -90,9 +103,9 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
90103 this . #formattingAdapter = formattingAdapter ;
91104 this . #toolName = toolName ;
92105
93- // eventBus.addEventListener(BeforeInputUIEventName, (event: BeforeInputUIEvent) => {
94- // console.log('BeforeInputUIEventName', event);
95- // });
106+ eventBus . addEventListener ( `ui: ${ BeforeInputUIEventName } ` , ( event : BeforeInputUIEvent ) => {
107+ this . #processDelegatedBeforeInput ( event ) ;
108+ } ) ;
96109 }
97110
98111 /**
@@ -109,7 +122,7 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
109122
110123 const key = createDataKey ( keyRaw ) ;
111124
112- input . addEventListener ( 'beforeinput' , event => this . #handleBeforeInputEvent ( event , input , key ) ) ;
125+ this . #attachedInputs . set ( key , input ) ;
113126
114127 this . #model. addEventListener ( EventType . Changed , ( event : ModelEvents ) => this . #handleModelUpdate( event , input , key ) ) ;
115128
@@ -133,16 +146,64 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
133146 }
134147 }
135148
149+ /**
150+ * Check current selection and find it across all attached inputs
151+ *
152+ * @returns tuple of data key and input element or null if no focused input is found
153+ */
154+ #findFocusedInput( ) : [ DataKey , HTMLElement ] | null {
155+ const currentInput = Array . from ( this . #attachedInputs. entries ( ) ) . find ( ( [ _ , input ] ) => {
156+ /**
157+ * Case 1: Input is a native input — check if it has selection
158+ */
159+ if ( input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement ) {
160+ return input . selectionStart !== null && input . selectionEnd !== null ;
161+ }
162+
163+ /**
164+ * Case 2: Input is a contenteditable element — check if it has range start container
165+ */
166+ if ( input . isContentEditable ) {
167+ const selection = window . getSelection ( ) ;
168+
169+ if ( selection !== null && selection . rangeCount > 0 ) {
170+ const range = selection . getRangeAt ( 0 ) ;
171+
172+ return input . contains ( range . startContainer ) ;
173+ }
174+ }
175+
176+ return false ;
177+ } ) ;
178+
179+ return currentInput !== undefined ? currentInput : null ;
180+ }
181+
182+ /**
183+ * Handles 'beforeinput' event delegated from the blocks host element
184+ *
185+ * @param event - event containig necessary data
186+ */
187+ #processDelegatedBeforeInput( event : BeforeInputUIEvent ) : void {
188+ const [ dataKey , currentInput ] = this . #findFocusedInput( ) ?? [ ] ;
189+
190+ if ( currentInput === undefined || dataKey === undefined ) {
191+ return ;
192+ }
193+
194+ this . #handleBeforeInputEvent( event . detail , currentInput , dataKey ) ;
195+ }
196+
136197 /**
137198 * Handles delete events in native input
138199 *
139- * @param event - beforeinput event
200+ * @param payload - beforeinput event payload
140201 * @param input - input element
141202 * @param key - data key input is attached to
142203 * @private
143204 */
144- #handleDeleteInNativeInput( event : InputEvent , input : HTMLInputElement | HTMLTextAreaElement , key : DataKey ) : void {
145- const inputType = event . inputType as InputType ;
205+ #handleDeleteInNativeInput( payload : BeforeInputUIEventPayload , input : HTMLInputElement | HTMLTextAreaElement , key : DataKey ) : void {
206+ const inputType = payload . inputType ;
146207
147208 /**
148209 * Check that selection exists in current input
@@ -226,12 +287,12 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
226287 /**
227288 * Handles delete events in contenteditable element
228289 *
229- * @param event - beforeinput event
290+ * @param payload - beforeinput event payload
230291 * @param input - input element
231292 * @param key - data key input is attached to
232293 */
233- #handleDeleteInContentEditable( event : InputEvent , input : HTMLElement , key : DataKey ) : void {
234- const targetRanges = event . getTargetRanges ( ) ;
294+ #handleDeleteInContentEditable( payload : BeforeInputUIEventPayload , input : HTMLElement , key : DataKey ) : void {
295+ const { targetRanges } = payload ;
235296 const range = targetRanges [ 0 ] ;
236297
237298 const start : number = getAbsoluteRangeOffset ( input , range . startContainer , range . startOffset ) ;
@@ -245,23 +306,18 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
245306 *
246307 * We prevent beforeinput event of any type to handle it manually via model update
247308 *
248- * @param event - beforeinput event
309+ * @param payload - payload of input event
249310 * @param input - input element
250311 * @param key - data key input is attached to
251312 */
252- #handleBeforeInputEvent( event : InputEvent , input : HTMLElement , key : DataKey ) : void {
253- /**
254- * We prevent all events to handle them manually via model update
255- */
256- event . preventDefault ( ) ;
313+ #handleBeforeInputEvent( payload : BeforeInputUIEventPayload , input : HTMLElement , key : DataKey ) : void {
314+ const { data, inputType, targetRanges } = payload ;
257315
258316 const isInputNative = isNativeInput ( input ) ;
259- const inputType = event . inputType as InputType ;
260317 let start : number ;
261318 let end : number ;
262319
263320 if ( isInputNative === false ) {
264- const targetRanges = event . getTargetRanges ( ) ;
265321 const range = targetRanges [ 0 ] ;
266322
267323 start = getAbsoluteRangeOffset ( input , range . startContainer , range . startOffset ) ;
@@ -281,20 +337,6 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
281337 this . #model. removeText ( this . #config. userId , this . #blockIndex, key , start , end ) ;
282338 }
283339
284- let data : string ;
285-
286- /**
287- * For native inputs data for those events comes from event.data property
288- * while for contenteditable elements it's stored in event.dataTransfer
289- *
290- * @see https://www.w3.org/TR/input-events-2/#overview
291- */
292- if ( isInputNative ) {
293- data = event . data ?? '' ;
294- } else {
295- data = event . dataTransfer ! . getData ( 'text/plain' ) ;
296- }
297-
298340 this . #model. insertText ( this . #config. userId , this . #blockIndex, key , data , start ) ;
299341
300342 break ;
@@ -312,8 +354,6 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
312354 this . #model. removeText ( this . #config. userId , this . #blockIndex, key , start , end ) ;
313355 }
314356
315- const data = event . data as string ;
316-
317357 this . #model. insertText ( this . #config. userId , this . #blockIndex, key , data , start ) ;
318358 break ;
319359 }
@@ -331,9 +371,9 @@ export class BlockToolAdapter implements BlockToolAdapterInterface {
331371 case InputType . DeleteWordBackward :
332372 case InputType . DeleteWordForward : {
333373 if ( isInputNative === true ) {
334- this . #handleDeleteInNativeInput( event , input as HTMLInputElement | HTMLTextAreaElement , key ) ;
374+ this . #handleDeleteInNativeInput( payload , input as HTMLInputElement | HTMLTextAreaElement , key ) ;
335375 } else {
336- this . #handleDeleteInContentEditable( event , input , key ) ;
376+ this . #handleDeleteInContentEditable( payload , input , key ) ;
337377 }
338378 break ;
339379 }
0 commit comments