• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

worktile / slate-angular / 33a610af-2214-4a70-85dd-0f19af504322

20 Nov 2023 08:44AM UTC coverage: 48.455% (-0.05%) from 48.5%
33a610af-2214-4a70-85dd-0f19af504322

push

circleci

pubuzhixing8
fix(core): cancel throttleRAF for setBaseAndExtent

379 of 963 branches covered (0.0%)

Branch coverage included in aggregate %.

1 of 2 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

938 of 1755 relevant lines covered (53.45%)

39.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.27
/packages/src/components/editable/editable.component.ts
1
import {
2
    Component,
3
    OnInit,
4
    Input,
5
    ViewChild,
6
    HostBinding,
7
    Renderer2,
8
    ElementRef,
9
    ChangeDetectionStrategy,
10
    OnDestroy,
11
    ChangeDetectorRef,
12
    NgZone,
13
    Injector,
14
    forwardRef,
15
    OnChanges,
16
    SimpleChanges,
17
    AfterViewChecked,
18
    DoCheck,
19
    Inject,
20
    inject,
21
    ViewContainerRef
22
} from '@angular/core';
23
import {
24
    NODE_TO_ELEMENT,
25
    IS_FOCUSED,
26
    EDITOR_TO_ELEMENT,
27
    ELEMENT_TO_NODE,
28
    IS_READONLY,
29
    EDITOR_TO_ON_CHANGE,
30
    EDITOR_TO_WINDOW
31
} from '../../utils/weak-maps';
32
import { Text as SlateText, Element, Transforms, Editor, Range, Path, NodeEntry, Node } from 'slate';
33
import { direction } from 'direction';
34
import scrollIntoView from 'scroll-into-view-if-needed';
35
import { AngularEditor } from '../../plugins/angular-editor';
36
import {
37
    DOMElement,
38
    DOMNode,
39
    isDOMNode,
40
    DOMStaticRange,
41
    DOMRange,
42
    isDOMElement,
43
    isPlainTextOnlyPaste,
44
    DOMSelection,
45
    getDefaultView
46
} from '../../utils/dom';
47
import { Subject } from 'rxjs';
48
import { IS_FIREFOX, IS_SAFARI, IS_CHROME, HAS_BEFORE_INPUT_SUPPORT, IS_ANDROID } from '../../utils/environment';
49
import Hotkeys from '../../utils/hotkeys';
50
import { BeforeInputEvent, extractBeforeInputEvent } from '../../custom-event/BeforeInputEventPlugin';
51
import { BEFORE_INPUT_EVENTS } from '../../custom-event/before-input-polyfill';
52
import { SlateErrorCode } from '../../types/error';
53
import { SlateStringTemplate } from '../string/template.component';
54
import { NG_VALUE_ACCESSOR } from '@angular/forms';
55
import { SlateChildrenContext, SlateViewContext } from '../../view/context';
56
import { ComponentType, ViewType } from '../../types/view';
57
import { HistoryEditor } from 'slate-history';
58
import { isDecoratorRangeListEqual, check, normalize } from '../../utils';
59
import { SlatePlaceholder } from '../../types/feature';
60
import { restoreDom } from '../../utils/restore-dom';
61
import { SlateChildren } from '../children/children.component';
62
import { SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN } from '../element/default-element.component.token';
63
import { SLATE_DEFAULT_TEXT_COMPONENT_TOKEN, SLATE_DEFAULT_VOID_TEXT_COMPONENT_TOKEN } from '../text/token';
64
import { SlateVoidText } from '../text/void-text.component';
65
import { SlateDefaultText } from '../text/default-text.component';
66
import { SlateDefaultElement } from '../element/default-element.component';
67
import { SlateDefaultLeaf } from '../leaf/default-leaf.component';
68
import { SLATE_DEFAULT_LEAF_COMPONENT_TOKEN } from '../leaf/token';
69
import { BaseElementComponent, BaseLeafComponent, BaseTextComponent } from '../../view/base';
70
import { ListRender } from '../../view/render/list-render';
71
import { ThrottleRAF, createThrottleRAF } from '../../utils/throttle';
72

73
// not correctly clipboardData on beforeinput
74
const forceOnDOMPaste = IS_SAFARI;
1✔
75

76
@Component({
77
    selector: 'slate-editable',
78
    host: {
79
        class: 'slate-editable-container',
80
        '[attr.contenteditable]': 'readonly ? undefined : true',
81
        '[attr.role]': `readonly ? undefined : 'textbox'`,
82
        '[attr.spellCheck]': `!hasBeforeInputSupport ? false : spellCheck`,
83
        '[attr.autoCorrect]': `!hasBeforeInputSupport ? 'false' : autoCorrect`,
84
        '[attr.autoCapitalize]': `!hasBeforeInputSupport ? 'false' : autoCapitalize`
85
    },
86
    templateUrl: 'editable.component.html',
87
    changeDetection: ChangeDetectionStrategy.OnPush,
88
    providers: [
89
        {
90
            provide: NG_VALUE_ACCESSOR,
91
            useExisting: forwardRef(() => SlateEditable),
20✔
92
            multi: true
93
        },
94
        {
95
            provide: SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN,
96
            useValue: SlateDefaultElement
97
        },
98
        {
99
            provide: SLATE_DEFAULT_TEXT_COMPONENT_TOKEN,
100
            useValue: SlateDefaultText
101
        },
102
        {
103
            provide: SLATE_DEFAULT_VOID_TEXT_COMPONENT_TOKEN,
104
            useValue: SlateVoidText
105
        },
106
        {
107
            provide: SLATE_DEFAULT_LEAF_COMPONENT_TOKEN,
108
            useValue: SlateDefaultLeaf
109
        }
110
    ],
111
    standalone: true,
112
    imports: [SlateChildren, SlateStringTemplate]
113
})
114
export class SlateEditable implements OnInit, OnChanges, OnDestroy, AfterViewChecked, DoCheck {
1✔
115
    viewContext: SlateViewContext;
116
    context: SlateChildrenContext;
117

118
    private destroy$ = new Subject();
20✔
119

120
    isComposing = false;
20✔
121
    isDraggingInternally = false;
20✔
122
    isUpdatingSelection = false;
20✔
123
    latestElement = null as DOMElement | null;
20✔
124

125
    protected manualListeners: (() => void)[] = [];
20✔
126

127
    private initialized: boolean;
128

129
    private onTouchedCallback: () => void = () => {};
20✔
130

131
    private onChangeCallback: (_: any) => void = () => {};
20✔
132

133
    @Input() editor: AngularEditor;
134

135
    @Input() renderElement: (element: Element) => ViewType | null;
136

137
    @Input() renderLeaf: (text: SlateText) => ViewType | null;
138

139
    @Input() renderText: (text: SlateText) => ViewType | null;
140

141
    @Input() decorate: (entry: NodeEntry) => Range[] = () => [];
213✔
142

143
    @Input() placeholderDecorate: (editor: Editor) => SlatePlaceholder[];
144

145
    @Input() scrollSelectionIntoView: (editor: AngularEditor, domRange: DOMRange) => void = defaultScrollSelectionIntoView;
20✔
146

147
    @Input() isStrictDecorate: boolean = true;
20✔
148

149
    @Input() trackBy: (node: Element) => any = () => null;
196✔
150

151
    @Input() readonly = false;
20✔
152

153
    @Input() placeholder: string;
154

155
    //#region input event handler
156
    @Input() beforeInput: (event: Event) => void;
157
    @Input() blur: (event: Event) => void;
158
    @Input() click: (event: MouseEvent) => void;
159
    @Input() compositionEnd: (event: CompositionEvent) => void;
160
    @Input() compositionUpdate: (event: CompositionEvent) => void;
161
    @Input() compositionStart: (event: CompositionEvent) => void;
162
    @Input() copy: (event: ClipboardEvent) => void;
163
    @Input() cut: (event: ClipboardEvent) => void;
164
    @Input() dragOver: (event: DragEvent) => void;
165
    @Input() dragStart: (event: DragEvent) => void;
166
    @Input() dragEnd: (event: DragEvent) => void;
167
    @Input() drop: (event: DragEvent) => void;
168
    @Input() focus: (event: Event) => void;
169
    @Input() keydown: (event: KeyboardEvent) => void;
170
    @Input() paste: (event: ClipboardEvent) => void;
171
    //#endregion
172

173
    //#region DOM attr
174
    @Input() spellCheck = false;
20✔
175
    @Input() autoCorrect = false;
20✔
176
    @Input() autoCapitalize = false;
20✔
177

178
    @HostBinding('attr.data-slate-editor') dataSlateEditor = true;
20✔
179
    @HostBinding('attr.data-slate-node') dataSlateNode = 'value';
20✔
180
    @HostBinding('attr.data-gramm') dataGramm = false;
20✔
181

182
    get hasBeforeInputSupport() {
183
        return HAS_BEFORE_INPUT_SUPPORT;
396✔
184
    }
185
    //#endregion
186

187
    @ViewChild('templateComponent', { static: true })
188
    templateComponent: SlateStringTemplate;
189

190
    @ViewChild('templateComponent', { static: true, read: ElementRef })
191
    templateElementRef: ElementRef<any>;
192

193
    viewContainerRef = inject(ViewContainerRef);
20✔
194

195
    getOutletElement = () => {
20✔
196
        return this.elementRef.nativeElement;
38✔
197
    };
198

199
    listRender: ListRender;
200

201
    constructor(
202
        public elementRef: ElementRef,
20✔
203
        public renderer2: Renderer2,
20✔
204
        public cdr: ChangeDetectorRef,
20✔
205
        private ngZone: NgZone,
20✔
206
        private injector: Injector,
20✔
207
        @Inject(SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN)
208
        public defaultElement: ComponentType<BaseElementComponent>,
20✔
209
        @Inject(SLATE_DEFAULT_TEXT_COMPONENT_TOKEN)
210
        public defaultText: ComponentType<BaseTextComponent>,
20✔
211
        @Inject(SLATE_DEFAULT_VOID_TEXT_COMPONENT_TOKEN)
212
        public defaultVoidText: ComponentType<BaseTextComponent>,
20✔
213
        @Inject(SLATE_DEFAULT_LEAF_COMPONENT_TOKEN)
214
        public defaultLeaf: ComponentType<BaseLeafComponent>
20✔
215
    ) {
216
    }
217

218
    ngOnInit() {
219
        this.editor.injector = this.injector;
20✔
220
        this.editor.children = [];
20✔
221
        let window = getDefaultView(this.elementRef.nativeElement);
20✔
222
        EDITOR_TO_WINDOW.set(this.editor, window);
20✔
223
        EDITOR_TO_ELEMENT.set(this.editor, this.elementRef.nativeElement);
20✔
224
        NODE_TO_ELEMENT.set(this.editor, this.elementRef.nativeElement);
20✔
225
        ELEMENT_TO_NODE.set(this.elementRef.nativeElement, this.editor);
20✔
226
        IS_READONLY.set(this.editor, this.readonly);
20✔
227
        EDITOR_TO_ON_CHANGE.set(this.editor, () => {
20✔
228
            this.ngZone.run(() => {
11✔
229
                this.onChange();
11✔
230
            });
231
        });
232
        this.ngZone.runOutsideAngular(() => {
20✔
233
            this.initialize();
20✔
234
        });
235
        this.initializeViewContext();
20✔
236
        this.initializeContext();
20✔
237

238
        // remove unused DOM, just keep templateComponent instance
239
        this.templateElementRef.nativeElement.remove();
20✔
240

241
        // add browser class
242
        let browserClass = IS_FIREFOX ? 'firefox' : IS_SAFARI ? 'safari' : '';
20!
243
        browserClass && this.elementRef.nativeElement.classList.add(browserClass);
20!
244
        this.listRender = new ListRender(this.viewContext, this.viewContainerRef, this.getOutletElement);
20✔
245
    }
246

247
    ngOnChanges(simpleChanges: SimpleChanges) {
248
        if (!this.initialized) {
23✔
249
            return;
20✔
250
        }
251
        const decorateChange = simpleChanges['decorate'];
3✔
252
        if (decorateChange) {
3✔
253
            this.forceRender();
2✔
254
        }
255
        const placeholderChange = simpleChanges['placeholder'];
3✔
256
        if (placeholderChange) {
3✔
257
            this.render();
1✔
258
        }
259
        const readonlyChange = simpleChanges['readonly'];
3✔
260
        if (readonlyChange) {
3!
261
            IS_READONLY.set(this.editor, this.readonly);
×
262
            this.render();
×
263
            this.toSlateSelection();
×
264
        }
265
    }
266

267
    registerOnChange(fn: any) {
268
        this.onChangeCallback = fn;
20✔
269
    }
270
    registerOnTouched(fn: any) {
271
        this.onTouchedCallback = fn;
20✔
272
    }
273

274
    writeValue(value: Element[]) {
275
        if (value && value.length) {
43✔
276
            if (check(value)) {
23!
277
                this.editor.children = value;
23✔
278
            } else {
279
                this.editor.onError({
×
280
                    code: SlateErrorCode.InvalidValueError,
281
                    name: 'initialize invalid data',
282
                    data: value
283
                });
284
                this.editor.children = normalize(value);
×
285
            }
286
            this.initializeContext();
23✔
287
            if (!this.listRender.initialized) {
23✔
288
                this.listRender.initialize(this.editor.children, this.editor, [], this.context);
20✔
289
            } else {
290
                this.listRender.update(this.editor.children, this.editor, [], this.context);
3✔
291
            }
292
            this.cdr.markForCheck();
23✔
293
        }
294
    }
295

296
    initialize() {
297
        this.initialized = true;
20✔
298
        const window = AngularEditor.getWindow(this.editor);
20✔
299
        this.addEventListener(
20✔
300
            'selectionchange',
301
            event => {
302
                this.toSlateSelection();
3✔
303
            },
304
            window.document
305
        );
306
        if (HAS_BEFORE_INPUT_SUPPORT) {
20✔
307
            this.addEventListener('beforeinput', this.onDOMBeforeInput.bind(this));
20✔
308
        }
309
        this.addEventListener('blur', this.onDOMBlur.bind(this));
20✔
310
        this.addEventListener('click', this.onDOMClick.bind(this));
20✔
311
        this.addEventListener('compositionend', this.onDOMCompositionEnd.bind(this));
20✔
312
        this.addEventListener('compositionupdate', this.onDOMCompositionUpdate.bind(this));
20✔
313
        this.addEventListener('compositionstart', this.onDOMCompositionStart.bind(this));
20✔
314
        this.addEventListener('copy', this.onDOMCopy.bind(this));
20✔
315
        this.addEventListener('cut', this.onDOMCut.bind(this));
20✔
316
        this.addEventListener('dragover', this.onDOMDragOver.bind(this));
20✔
317
        this.addEventListener('dragstart', this.onDOMDragStart.bind(this));
20✔
318
        this.addEventListener('dragend', this.onDOMDragEnd.bind(this));
20✔
319
        this.addEventListener('drop', this.onDOMDrop.bind(this));
20✔
320
        this.addEventListener('focus', this.onDOMFocus.bind(this));
20✔
321
        this.addEventListener('keydown', this.onDOMKeydown.bind(this));
20✔
322
        this.addEventListener('paste', this.onDOMPaste.bind(this));
20✔
323
        BEFORE_INPUT_EVENTS.forEach(event => {
20✔
324
            this.addEventListener(event.name, () => {});
100✔
325
        });
326
    }
327

328
    toNativeSelection() {
329
        try {
13✔
330
            const { selection } = this.editor;
13✔
331
            const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
13✔
332
            const { activeElement } = root;
13✔
333
            const domSelection = (root as Document).getSelection();
13✔
334

335
            if ((this.isComposing && !IS_ANDROID) || !domSelection || !AngularEditor.isFocused(this.editor)) {
13!
336
                return;
12✔
337
            }
338

339
            const hasDomSelection = domSelection.type !== 'None';
1✔
340

341
            // If the DOM selection is properly unset, we're done.
342
            if (!selection && !hasDomSelection) {
1!
343
                return;
×
344
            }
345

346
            // If the DOM selection is already correct, we're done.
347
            // verify that the dom selection is in the editor
348
            const editorElement = EDITOR_TO_ELEMENT.get(this.editor)!;
1✔
349
            let hasDomSelectionInEditor = false;
1✔
350
            if (editorElement.contains(domSelection.anchorNode) && editorElement.contains(domSelection.focusNode)) {
1✔
351
                hasDomSelectionInEditor = true;
1✔
352
            }
353

354
            // If the DOM selection is in the editor and the editor selection is already correct, we're done.
355
            if (
1!
356
                hasDomSelection &&
5✔
357
                hasDomSelectionInEditor &&
358
                selection &&
359
                hasStringTarget(domSelection) &&
360
                Range.equals(AngularEditor.toSlateRange(this.editor, domSelection), selection)
361
            ) {
UNCOV
362
                return;
×
363
            }
364

365
            // prevent updating native selection when active element is void element
366
            if (isTargetInsideVoid(this.editor, activeElement)) {
1!
367
                return;
×
368
            }
369

370
            // when <Editable/> is being controlled through external value
371
            // then its children might just change - DOM responds to it on its own
372
            // but Slate's value is not being updated through any operation
373
            // and thus it doesn't transform selection on its own
374
            if (selection && !AngularEditor.hasRange(this.editor, selection)) {
1!
375
                this.editor.selection = AngularEditor.toSlateRange(this.editor, domSelection);
×
376
                return;
×
377
            }
378

379
            // Otherwise the DOM selection is out of sync, so update it.
380
            const el = AngularEditor.toDOMNode(this.editor, this.editor);
1✔
381
            this.isUpdatingSelection = true;
1✔
382

383
            const newDomRange = selection && AngularEditor.toDOMRange(this.editor, selection);
1✔
384

385
            if (newDomRange) {
1!
386
                // COMPAT: Since the DOM range has no concept of backwards/forwards
387
                // we need to check and do the right thing here.
388
                if (Range.isBackward(selection)) {
1!
389
                    // eslint-disable-next-line max-len
NEW
390
                    domSelection.setBaseAndExtent(
×
391
                        newDomRange.endContainer,
392
                        newDomRange.endOffset,
393
                        newDomRange.startContainer,
394
                        newDomRange.startOffset
395
                    );
396
                } else {
397
                    // eslint-disable-next-line max-len
398
                    domSelection.setBaseAndExtent(
1✔
399
                        newDomRange.startContainer,
400
                        newDomRange.startOffset,
401
                        newDomRange.endContainer,
402
                        newDomRange.endOffset
403
                    );
404
                }
405
            } else {
406
                domSelection.removeAllRanges();
×
407
            }
408

409
            newDomRange && this.scrollSelectionIntoView(this.editor, newDomRange);
1✔
410

411
            setTimeout(() => {
1✔
412
                // COMPAT: In Firefox, it's not enough to create a range, you also need
413
                // to focus the contenteditable element too. (2016/11/16)
414
                if (newDomRange && IS_FIREFOX) {
1!
415
                    el.focus();
×
416
                }
417

418
                this.isUpdatingSelection = false;
1✔
419
            });
420
        } catch (error) {
421
            this.editor.onError({
×
422
                code: SlateErrorCode.ToNativeSelectionError,
423
                nativeError: error
424
            });
425
            this.isUpdatingSelection = false;
×
426
        }
427
    }
428

429
    onChange() {
430
        this.forceRender();
11✔
431
        this.onChangeCallback(this.editor.children);
11✔
432
    }
433

434
    ngAfterViewChecked() {}
435

436
    ngDoCheck() {}
437

438
    forceRender() {
439
        this.updateContext();
13✔
440
        this.listRender.update(this.editor.children, this.editor, [], this.context);
13✔
441
        // repair collaborative editing when Chinese input is interrupted by other users' cursors
442
        // when the DOMElement where the selection is located is removed
443
        // the compositionupdate and compositionend events will no longer be fired
444
        // so isComposing needs to be corrected
445
        // need exec after this.cdr.detectChanges() to render HTML
446
        // need exec before this.toNativeSelection() to correct native selection
447
        if (this.isComposing) {
13!
448
            // Composition input text be not rendered when user composition input with selection is expanded
449
            // At this time, the following matching conditions are met, assign isComposing to false, and the status is wrong
450
            // this time condition is true and isComposiing is assigned false
451
            // Therefore, need to wait for the composition input text to be rendered before performing condition matching
452
            setTimeout(() => {
×
453
                const textNode = Node.get(this.editor, this.editor.selection.anchor.path);
×
454
                const textDOMNode = AngularEditor.toDOMNode(this.editor, textNode);
×
455
                let textContent = '';
×
456
                // skip decorate text
457
                textDOMNode.querySelectorAll('[editable-text]').forEach(stringDOMNode => {
×
458
                    let text = stringDOMNode.textContent;
×
459
                    const zeroChar = '\uFEFF';
×
460
                    // remove zero with char
461
                    if (text.startsWith(zeroChar)) {
×
462
                        text = text.slice(1);
×
463
                    }
464
                    if (text.endsWith(zeroChar)) {
×
465
                        text = text.slice(0, text.length - 1);
×
466
                    }
467
                    textContent += text;
×
468
                });
469
                if (Node.string(textNode).endsWith(textContent)) {
×
470
                    this.isComposing = false;
×
471
                }
472
            }, 0);
473
        }
474
        this.toNativeSelection();
13✔
475
    }
476

477
    render() {
478
        const changed = this.updateContext();
2✔
479
        if (changed) {
2✔
480
            this.listRender.update(this.editor.children, this.editor, [], this.context);
2✔
481
        }
482
    }
483

484
    updateContext() {
485
        const decorations = this.generateDecorations();
15✔
486
        if (
15✔
487
            this.context.selection !== this.editor.selection ||
38✔
488
            this.context.decorate !== this.decorate ||
489
            this.context.readonly !== this.readonly ||
490
            !isDecoratorRangeListEqual(this.context.decorations, decorations)
491
        ) {
492
            this.context = {
10✔
493
                parent: this.editor,
494
                selection: this.editor.selection,
495
                decorations: decorations,
496
                decorate: this.decorate,
497
                readonly: this.readonly
498
            };
499
            return true;
10✔
500
        }
501
        return false;
5✔
502
    }
503

504
    initializeContext() {
505
        this.context = {
43✔
506
            parent: this.editor,
507
            selection: this.editor.selection,
508
            decorations: this.generateDecorations(),
509
            decorate: this.decorate,
510
            readonly: this.readonly
511
        };
512
    }
513

514
    initializeViewContext() {
515
        this.viewContext = {
20✔
516
            editor: this.editor,
517
            renderElement: this.renderElement,
518
            renderLeaf: this.renderLeaf,
519
            renderText: this.renderText,
520
            trackBy: this.trackBy,
521
            isStrictDecorate: this.isStrictDecorate,
522
            templateComponent: this.templateComponent,
523
            defaultElement: this.defaultElement,
524
            defaultText: this.defaultText,
525
            defaultVoidText: this.defaultVoidText,
526
            defaultLeaf: this.defaultLeaf
527
        };
528
    }
529

530
    composePlaceholderDecorate(editor: Editor) {
531
        if (this.placeholderDecorate) {
56!
532
            return this.placeholderDecorate(editor) || [];
×
533
        }
534

535
        if (this.placeholder && editor.children.length === 1 && Array.from(Node.texts(editor)).length === 1 && Node.string(editor) === '') {
56✔
536
            const start = Editor.start(editor, []);
3✔
537
            return [
3✔
538
                {
539
                    placeholder: this.placeholder,
540
                    anchor: start,
541
                    focus: start
542
                }
543
            ];
544
        } else {
545
            return [];
53✔
546
        }
547
    }
548

549
    generateDecorations() {
550
        const decorations = this.decorate([this.editor, []]);
58✔
551
        const placeholderDecorations = this.isComposing ? [] : this.composePlaceholderDecorate(this.editor);
58✔
552
        decorations.push(...placeholderDecorations);
58✔
553
        return decorations;
58✔
554
    }
555

556
    //#region event proxy
557
    private addEventListener(eventName: string, listener: EventListener, target: HTMLElement | Document = this.elementRef.nativeElement) {
400✔
558
        this.manualListeners.push(
420✔
559
            this.renderer2.listen(target, eventName, (event: Event) => {
560
                const beforeInputEvent = extractBeforeInputEvent(event.type, null, event, event.target);
6✔
561
                if (beforeInputEvent) {
6!
562
                    this.onFallbackBeforeInput(beforeInputEvent);
×
563
                }
564
                listener(event);
6✔
565
            })
566
        );
567
    }
568

569
    private toSlateSelection() {
570
        if ((!this.isComposing || IS_ANDROID) && !this.isUpdatingSelection && !this.isDraggingInternally) {
3✔
571
            try {
2✔
572
                const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
2✔
573
                const { activeElement } = root;
2✔
574
                const el = AngularEditor.toDOMNode(this.editor, this.editor);
2✔
575
                const domSelection = (root as Document).getSelection();
2✔
576

577
                if (activeElement === el) {
2!
578
                    this.latestElement = activeElement;
2✔
579
                    IS_FOCUSED.set(this.editor, true);
2✔
580
                } else {
581
                    IS_FOCUSED.delete(this.editor);
×
582
                }
583

584
                if (!domSelection) {
2!
585
                    return Transforms.deselect(this.editor);
×
586
                }
587

588
                const editorElement = EDITOR_TO_ELEMENT.get(this.editor);
2✔
589
                const hasDomSelectionInEditor =
590
                    editorElement.contains(domSelection.anchorNode) && editorElement.contains(domSelection.focusNode);
2✔
591
                if (!hasDomSelectionInEditor) {
2!
592
                    Transforms.deselect(this.editor);
×
593
                    return;
×
594
                }
595

596
                // try to get the selection directly, because some terrible case can be normalize for normalizeDOMPoint
597
                // for example, double-click the last cell of the table to select a non-editable DOM
598
                const range = AngularEditor.toSlateRange(this.editor, domSelection);
2✔
599
                if (this.editor.selection && Range.equals(range, this.editor.selection) && !hasStringTarget(domSelection)) {
2!
600
                    if (!isTargetInsideVoid(this.editor, activeElement)) {
×
601
                        // force adjust DOMSelection
602
                        this.toNativeSelection();
×
603
                    }
604
                } else {
605
                    Transforms.select(this.editor, range);
2✔
606
                }
607
            } catch (error) {
608
                this.editor.onError({
×
609
                    code: SlateErrorCode.ToSlateSelectionError,
610
                    nativeError: error
611
                });
612
            }
613
        }
614
    }
615

616
    private onDOMBeforeInput(
617
        event: Event & {
618
            inputType: string;
619
            isComposing: boolean;
620
            data: string | null;
621
            dataTransfer: DataTransfer | null;
622
            getTargetRanges(): DOMStaticRange[];
623
        }
624
    ) {
625
        const editor = this.editor;
×
626
        const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
×
627
        const { activeElement } = root;
×
628
        const { selection } = editor;
×
629
        const { inputType: type } = event;
×
630
        const data = event.dataTransfer || event.data || undefined;
×
631
        if (IS_ANDROID) {
×
632
            let targetRange: Range | null = null;
×
633
            let [nativeTargetRange] = event.getTargetRanges();
×
634
            if (nativeTargetRange) {
×
635
                targetRange = AngularEditor.toSlateRange(editor, nativeTargetRange);
×
636
            }
637
            // COMPAT: SelectionChange event is fired after the action is performed, so we
638
            // have to manually get the selection here to ensure it's up-to-date.
639
            const window = AngularEditor.getWindow(editor);
×
640
            const domSelection = window.getSelection();
×
641
            if (!targetRange && domSelection) {
×
642
                targetRange = AngularEditor.toSlateRange(editor, domSelection);
×
643
            }
644
            targetRange = targetRange ?? editor.selection;
×
645
            if (type === 'insertCompositionText') {
×
646
                if (data && data.toString().includes('\n')) {
×
647
                    restoreDom(editor, () => {
×
648
                        Editor.insertBreak(editor);
×
649
                    });
650
                } else {
651
                    if (targetRange) {
×
652
                        if (data) {
×
653
                            restoreDom(editor, () => {
×
654
                                Transforms.insertText(editor, data.toString(), { at: targetRange });
×
655
                            });
656
                        } else {
657
                            restoreDom(editor, () => {
×
658
                                Transforms.delete(editor, { at: targetRange });
×
659
                            });
660
                        }
661
                    }
662
                }
663
                return;
×
664
            }
665
            if (type === 'deleteContentBackward') {
×
666
                // gboard can not prevent default action, so must use restoreDom,
667
                // sougou Keyboard can prevent default action(only in Chinese input mode).
668
                // In order to avoid weird action in Sougou Keyboard, use resotreDom only range's isCollapsed is false (recognize gboard)
669
                if (!Range.isCollapsed(targetRange)) {
×
670
                    restoreDom(editor, () => {
×
671
                        Transforms.delete(editor, { at: targetRange });
×
672
                    });
673
                    return;
×
674
                }
675
            }
676
            if (type === 'insertText') {
×
677
                restoreDom(editor, () => {
×
678
                    if (typeof data === 'string') {
×
679
                        Editor.insertText(editor, data);
×
680
                    }
681
                });
682
                return;
×
683
            }
684
        }
685
        if (
×
686
            !this.readonly &&
×
687
            hasEditableTarget(editor, event.target) &&
688
            !isTargetInsideVoid(editor, activeElement) &&
689
            !this.isDOMEventHandled(event, this.beforeInput)
690
        ) {
691
            try {
×
692
                event.preventDefault();
×
693

694
                // COMPAT: If the selection is expanded, even if the command seems like
695
                // a delete forward/backward command it should delete the selection.
696
                if (selection && Range.isExpanded(selection) && type.startsWith('delete')) {
×
697
                    const direction = type.endsWith('Backward') ? 'backward' : 'forward';
×
698
                    Editor.deleteFragment(editor, { direction });
×
699
                    return;
×
700
                }
701

702
                switch (type) {
×
703
                    case 'deleteByComposition':
704
                    case 'deleteByCut':
705
                    case 'deleteByDrag': {
706
                        Editor.deleteFragment(editor);
×
707
                        break;
×
708
                    }
709

710
                    case 'deleteContent':
711
                    case 'deleteContentForward': {
712
                        Editor.deleteForward(editor);
×
713
                        break;
×
714
                    }
715

716
                    case 'deleteContentBackward': {
717
                        Editor.deleteBackward(editor);
×
718
                        break;
×
719
                    }
720

721
                    case 'deleteEntireSoftLine': {
722
                        Editor.deleteBackward(editor, { unit: 'line' });
×
723
                        Editor.deleteForward(editor, { unit: 'line' });
×
724
                        break;
×
725
                    }
726

727
                    case 'deleteHardLineBackward': {
728
                        Editor.deleteBackward(editor, { unit: 'block' });
×
729
                        break;
×
730
                    }
731

732
                    case 'deleteSoftLineBackward': {
733
                        Editor.deleteBackward(editor, { unit: 'line' });
×
734
                        break;
×
735
                    }
736

737
                    case 'deleteHardLineForward': {
738
                        Editor.deleteForward(editor, { unit: 'block' });
×
739
                        break;
×
740
                    }
741

742
                    case 'deleteSoftLineForward': {
743
                        Editor.deleteForward(editor, { unit: 'line' });
×
744
                        break;
×
745
                    }
746

747
                    case 'deleteWordBackward': {
748
                        Editor.deleteBackward(editor, { unit: 'word' });
×
749
                        break;
×
750
                    }
751

752
                    case 'deleteWordForward': {
753
                        Editor.deleteForward(editor, { unit: 'word' });
×
754
                        break;
×
755
                    }
756

757
                    case 'insertLineBreak':
758
                    case 'insertParagraph': {
759
                        Editor.insertBreak(editor);
×
760
                        break;
×
761
                    }
762

763
                    case 'insertFromComposition': {
764
                        // COMPAT: in safari, `compositionend` event is dispatched after
765
                        // the beforeinput event with the inputType "insertFromComposition" has been dispatched.
766
                        // https://www.w3.org/TR/input-events-2/
767
                        // so the following code is the right logic
768
                        // because DOM selection in sync will be exec before `compositionend` event
769
                        // isComposing is true will prevent DOM selection being update correctly.
770
                        this.isComposing = false;
×
771
                        preventInsertFromComposition(event, this.editor);
×
772
                    }
773
                    case 'insertFromDrop':
774
                    case 'insertFromPaste':
775
                    case 'insertFromYank':
776
                    case 'insertReplacementText':
777
                    case 'insertText': {
778
                        // use a weak comparison instead of 'instanceof' to allow
779
                        // programmatic access of paste events coming from external windows
780
                        // like cypress where cy.window does not work realibly
781
                        if (data?.constructor.name === 'DataTransfer') {
×
782
                            AngularEditor.insertData(editor, data as DataTransfer);
×
783
                        } else if (typeof data === 'string') {
×
784
                            Editor.insertText(editor, data);
×
785
                        }
786
                        break;
×
787
                    }
788
                }
789
            } catch (error) {
790
                this.editor.onError({
×
791
                    code: SlateErrorCode.OnDOMBeforeInputError,
792
                    nativeError: error
793
                });
794
            }
795
        }
796
    }
797

798
    private onDOMBlur(event: FocusEvent) {
799
        if (
×
800
            this.readonly ||
×
801
            this.isUpdatingSelection ||
802
            !hasEditableTarget(this.editor, event.target) ||
803
            this.isDOMEventHandled(event, this.blur)
804
        ) {
805
            return;
×
806
        }
807

808
        const window = AngularEditor.getWindow(this.editor);
×
809

810
        // COMPAT: If the current `activeElement` is still the previous
811
        // one, this is due to the window being blurred when the tab
812
        // itself becomes unfocused, so we want to abort early to allow to
813
        // editor to stay focused when the tab becomes focused again.
814
        const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
×
815
        if (this.latestElement === root.activeElement) {
×
816
            return;
×
817
        }
818

819
        const { relatedTarget } = event;
×
820
        const el = AngularEditor.toDOMNode(this.editor, this.editor);
×
821

822
        // COMPAT: The event should be ignored if the focus is returning
823
        // to the editor from an embedded editable element (eg. an <input>
824
        // element inside a void node).
825
        if (relatedTarget === el) {
×
826
            return;
×
827
        }
828

829
        // COMPAT: The event should be ignored if the focus is moving from
830
        // the editor to inside a void node's spacer element.
831
        if (isDOMElement(relatedTarget) && relatedTarget.hasAttribute('data-slate-spacer')) {
×
832
            return;
×
833
        }
834

835
        // COMPAT: The event should be ignored if the focus is moving to a
836
        // non- editable section of an element that isn't a void node (eg.
837
        // a list item of the check list example).
838
        if (relatedTarget != null && isDOMNode(relatedTarget) && AngularEditor.hasDOMNode(this.editor, relatedTarget)) {
×
839
            const node = AngularEditor.toSlateNode(this.editor, relatedTarget);
×
840

841
            if (Element.isElement(node) && !this.editor.isVoid(node)) {
×
842
                return;
×
843
            }
844
        }
845

846
        IS_FOCUSED.delete(this.editor);
×
847
    }
848

849
    private onDOMClick(event: MouseEvent) {
850
        if (
×
851
            !this.readonly &&
×
852
            hasTarget(this.editor, event.target) &&
853
            !this.isDOMEventHandled(event, this.click) &&
854
            isDOMNode(event.target)
855
        ) {
856
            const node = AngularEditor.toSlateNode(this.editor, event.target);
×
857
            const path = AngularEditor.findPath(this.editor, node);
×
858
            const start = Editor.start(this.editor, path);
×
859
            const end = Editor.end(this.editor, path);
×
860

861
            const startVoid = Editor.void(this.editor, { at: start });
×
862
            const endVoid = Editor.void(this.editor, { at: end });
×
863

864
            if (startVoid && endVoid && Path.equals(startVoid[1], endVoid[1])) {
×
865
                const range = Editor.range(this.editor, start);
×
866
                Transforms.select(this.editor, range);
×
867
            }
868
        }
869
    }
870

871
    private onDOMCompositionStart(event: CompositionEvent) {
872
        const { selection } = this.editor;
1✔
873
        if (selection) {
1!
874
            // solve the problem of cross node Chinese input
875
            if (Range.isExpanded(selection)) {
×
876
                Editor.deleteFragment(this.editor);
×
877
                this.forceRender();
×
878
            }
879
        }
880
        if (hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.compositionStart)) {
1✔
881
            this.isComposing = true;
1✔
882
        }
883
        this.render();
1✔
884
    }
885

886
    private onDOMCompositionUpdate(event: CompositionEvent) {
887
        this.isDOMEventHandled(event, this.compositionUpdate);
×
888
    }
889

890
    private onDOMCompositionEnd(event: CompositionEvent) {
891
        if (!event.data && !Range.isCollapsed(this.editor.selection)) {
×
892
            Transforms.delete(this.editor);
×
893
        }
894
        if (hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.compositionEnd)) {
×
895
            // COMPAT: In Chrome/Firefox, `beforeinput` events for compositions
896
            // aren't correct and never fire the "insertFromComposition"
897
            // type that we need. So instead, insert whenever a composition
898
            // ends since it will already have been committed to the DOM.
899
            if (this.isComposing === true && !IS_SAFARI && !IS_ANDROID && event.data) {
×
900
                preventInsertFromComposition(event, this.editor);
×
901
                Editor.insertText(this.editor, event.data);
×
902
            }
903

904
            // COMPAT: In Firefox 87.0 CompositionEnd fire twice
905
            // so we need avoid repeat isnertText by isComposing === true,
906
            this.isComposing = false;
×
907
        }
908
        this.render();
×
909
    }
910

911
    private onDOMCopy(event: ClipboardEvent) {
912
        const window = AngularEditor.getWindow(this.editor);
×
913
        const isOutsideSlate = !hasStringTarget(window.getSelection()) && isTargetInsideVoid(this.editor, event.target);
×
914
        if (!isOutsideSlate && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.copy)) {
×
915
            event.preventDefault();
×
916
            AngularEditor.setFragmentData(this.editor, event.clipboardData, 'copy');
×
917
        }
918
    }
919

920
    private onDOMCut(event: ClipboardEvent) {
921
        if (!this.readonly && hasEditableTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.cut)) {
×
922
            event.preventDefault();
×
923
            AngularEditor.setFragmentData(this.editor, event.clipboardData, 'cut');
×
924
            const { selection } = this.editor;
×
925

926
            if (selection) {
×
927
                AngularEditor.deleteCutData(this.editor);
×
928
            }
929
        }
930
    }
931

932
    private onDOMDragOver(event: DragEvent) {
933
        if (hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.dragOver)) {
×
934
            // Only when the target is void, call `preventDefault` to signal
935
            // that drops are allowed. Editable content is droppable by
936
            // default, and calling `preventDefault` hides the cursor.
937
            const node = AngularEditor.toSlateNode(this.editor, event.target);
×
938

939
            if (Editor.isVoid(this.editor, node)) {
×
940
                event.preventDefault();
×
941
            }
942
        }
943
    }
944

945
    private onDOMDragStart(event: DragEvent) {
946
        if (!this.readonly && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.dragStart)) {
×
947
            const node = AngularEditor.toSlateNode(this.editor, event.target);
×
948
            const path = AngularEditor.findPath(this.editor, node);
×
949
            const voidMatch = Editor.isVoid(this.editor, node) || Editor.void(this.editor, { at: path, voids: true });
×
950

951
            // If starting a drag on a void node, make sure it is selected
952
            // so that it shows up in the selection's fragment.
953
            if (voidMatch) {
×
954
                const range = Editor.range(this.editor, path);
×
955
                Transforms.select(this.editor, range);
×
956
            }
957

958
            this.isDraggingInternally = true;
×
959

960
            AngularEditor.setFragmentData(this.editor, event.dataTransfer, 'drag');
×
961
        }
962
    }
963

964
    private onDOMDrop(event: DragEvent) {
965
        const editor = this.editor;
×
966
        if (!this.readonly && hasTarget(this.editor, event.target) && !this.isDOMEventHandled(event, this.drop)) {
×
967
            event.preventDefault();
×
968
            // Keep a reference to the dragged range before updating selection
969
            const draggedRange = editor.selection;
×
970

971
            // Find the range where the drop happened
972
            const range = AngularEditor.findEventRange(editor, event);
×
973
            const data = event.dataTransfer;
×
974

975
            Transforms.select(editor, range);
×
976

977
            if (this.isDraggingInternally) {
×
978
                if (draggedRange) {
×
979
                    Transforms.delete(editor, {
×
980
                        at: draggedRange
981
                    });
982
                }
983

984
                this.isDraggingInternally = false;
×
985
            }
986

987
            AngularEditor.insertData(editor, data);
×
988

989
            // When dragging from another source into the editor, it's possible
990
            // that the current editor does not have focus.
991
            if (!AngularEditor.isFocused(editor)) {
×
992
                AngularEditor.focus(editor);
×
993
            }
994
        }
995
    }
996

997
    private onDOMDragEnd(event: DragEvent) {
998
        if (
×
999
            !this.readonly &&
×
1000
            this.isDraggingInternally &&
1001
            hasTarget(this.editor, event.target) &&
1002
            !this.isDOMEventHandled(event, this.dragEnd)
1003
        ) {
1004
            this.isDraggingInternally = false;
×
1005
        }
1006
    }
1007

1008
    private onDOMFocus(event: Event) {
1009
        if (
2✔
1010
            !this.readonly &&
8✔
1011
            !this.isUpdatingSelection &&
1012
            hasEditableTarget(this.editor, event.target) &&
1013
            !this.isDOMEventHandled(event, this.focus)
1014
        ) {
1015
            const el = AngularEditor.toDOMNode(this.editor, this.editor);
2✔
1016
            const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
2✔
1017
            this.latestElement = root.activeElement;
2✔
1018

1019
            // COMPAT: If the editor has nested editable elements, the focus
1020
            // can go to them. In Firefox, this must be prevented because it
1021
            // results in issues with keyboard navigation. (2017/03/30)
1022
            if (IS_FIREFOX && event.target !== el) {
2!
1023
                el.focus();
×
1024
                return;
×
1025
            }
1026

1027
            IS_FOCUSED.set(this.editor, true);
2✔
1028
        }
1029
    }
1030

1031
    private onDOMKeydown(event: KeyboardEvent) {
1032
        const editor = this.editor;
×
1033
        const root = AngularEditor.findDocumentOrShadowRoot(this.editor);
×
1034
        const { activeElement } = root;
×
1035
        if (
×
1036
            !this.readonly &&
×
1037
            hasEditableTarget(editor, event.target) &&
1038
            !isTargetInsideVoid(editor, activeElement) && // stop fire keydown handle when focus void node
1039
            !this.isComposing &&
1040
            !this.isDOMEventHandled(event, this.keydown)
1041
        ) {
1042
            const nativeEvent = event;
×
1043
            const { selection } = editor;
×
1044

1045
            const element = editor.children[selection !== null ? selection.focus.path[0] : 0];
×
1046
            const isRTL = direction(Node.string(element)) === 'rtl';
×
1047

1048
            try {
×
1049
                // COMPAT: Since we prevent the default behavior on
1050
                // `beforeinput` events, the browser doesn't think there's ever
1051
                // any history stack to undo or redo, so we have to manage these
1052
                // hotkeys ourselves. (2019/11/06)
1053
                if (Hotkeys.isRedo(nativeEvent)) {
×
1054
                    event.preventDefault();
×
1055

1056
                    if (HistoryEditor.isHistoryEditor(editor)) {
×
1057
                        editor.redo();
×
1058
                    }
1059

1060
                    return;
×
1061
                }
1062

1063
                if (Hotkeys.isUndo(nativeEvent)) {
×
1064
                    event.preventDefault();
×
1065

1066
                    if (HistoryEditor.isHistoryEditor(editor)) {
×
1067
                        editor.undo();
×
1068
                    }
1069

1070
                    return;
×
1071
                }
1072

1073
                // COMPAT: Certain browsers don't handle the selection updates
1074
                // properly. In Chrome, the selection isn't properly extended.
1075
                // And in Firefox, the selection isn't properly collapsed.
1076
                // (2017/10/17)
1077
                if (Hotkeys.isMoveLineBackward(nativeEvent)) {
×
1078
                    event.preventDefault();
×
1079
                    Transforms.move(editor, { unit: 'line', reverse: true });
×
1080
                    return;
×
1081
                }
1082

1083
                if (Hotkeys.isMoveLineForward(nativeEvent)) {
×
1084
                    event.preventDefault();
×
1085
                    Transforms.move(editor, { unit: 'line' });
×
1086
                    return;
×
1087
                }
1088

1089
                if (Hotkeys.isExtendLineBackward(nativeEvent)) {
×
1090
                    event.preventDefault();
×
1091
                    Transforms.move(editor, {
×
1092
                        unit: 'line',
1093
                        edge: 'focus',
1094
                        reverse: true
1095
                    });
1096
                    return;
×
1097
                }
1098

1099
                if (Hotkeys.isExtendLineForward(nativeEvent)) {
×
1100
                    event.preventDefault();
×
1101
                    Transforms.move(editor, { unit: 'line', edge: 'focus' });
×
1102
                    return;
×
1103
                }
1104

1105
                // COMPAT: If a void node is selected, or a zero-width text node
1106
                // adjacent to an inline is selected, we need to handle these
1107
                // hotkeys manually because browsers won't be able to skip over
1108
                // the void node with the zero-width space not being an empty
1109
                // string.
1110
                if (Hotkeys.isMoveBackward(nativeEvent)) {
×
1111
                    event.preventDefault();
×
1112

1113
                    if (selection && Range.isCollapsed(selection)) {
×
1114
                        Transforms.move(editor, { reverse: !isRTL });
×
1115
                    } else {
1116
                        Transforms.collapse(editor, { edge: 'start' });
×
1117
                    }
1118

1119
                    return;
×
1120
                }
1121

1122
                if (Hotkeys.isMoveForward(nativeEvent)) {
×
1123
                    event.preventDefault();
×
1124

1125
                    if (selection && Range.isCollapsed(selection)) {
×
1126
                        Transforms.move(editor, { reverse: isRTL });
×
1127
                    } else {
1128
                        Transforms.collapse(editor, { edge: 'end' });
×
1129
                    }
1130

1131
                    return;
×
1132
                }
1133

1134
                if (Hotkeys.isMoveWordBackward(nativeEvent)) {
×
1135
                    event.preventDefault();
×
1136

1137
                    if (selection && Range.isExpanded(selection)) {
×
1138
                        Transforms.collapse(editor, { edge: 'focus' });
×
1139
                    }
1140

1141
                    Transforms.move(editor, { unit: 'word', reverse: !isRTL });
×
1142
                    return;
×
1143
                }
1144

1145
                if (Hotkeys.isMoveWordForward(nativeEvent)) {
×
1146
                    event.preventDefault();
×
1147

1148
                    if (selection && Range.isExpanded(selection)) {
×
1149
                        Transforms.collapse(editor, { edge: 'focus' });
×
1150
                    }
1151

1152
                    Transforms.move(editor, { unit: 'word', reverse: isRTL });
×
1153
                    return;
×
1154
                }
1155

1156
                // COMPAT: Certain browsers don't support the `beforeinput` event, so we
1157
                // fall back to guessing at the input intention for hotkeys.
1158
                // COMPAT: In iOS, some of these hotkeys are handled in the
1159
                if (!HAS_BEFORE_INPUT_SUPPORT) {
×
1160
                    // We don't have a core behavior for these, but they change the
1161
                    // DOM if we don't prevent them, so we have to.
1162
                    if (Hotkeys.isBold(nativeEvent) || Hotkeys.isItalic(nativeEvent) || Hotkeys.isTransposeCharacter(nativeEvent)) {
×
1163
                        event.preventDefault();
×
1164
                        return;
×
1165
                    }
1166

1167
                    if (Hotkeys.isSplitBlock(nativeEvent)) {
×
1168
                        event.preventDefault();
×
1169
                        Editor.insertBreak(editor);
×
1170
                        return;
×
1171
                    }
1172

1173
                    if (Hotkeys.isDeleteBackward(nativeEvent)) {
×
1174
                        event.preventDefault();
×
1175

1176
                        if (selection && Range.isExpanded(selection)) {
×
1177
                            Editor.deleteFragment(editor, {
×
1178
                                direction: 'backward'
1179
                            });
1180
                        } else {
1181
                            Editor.deleteBackward(editor);
×
1182
                        }
1183

1184
                        return;
×
1185
                    }
1186

1187
                    if (Hotkeys.isDeleteForward(nativeEvent)) {
×
1188
                        event.preventDefault();
×
1189

1190
                        if (selection && Range.isExpanded(selection)) {
×
1191
                            Editor.deleteFragment(editor, {
×
1192
                                direction: 'forward'
1193
                            });
1194
                        } else {
1195
                            Editor.deleteForward(editor);
×
1196
                        }
1197

1198
                        return;
×
1199
                    }
1200

1201
                    if (Hotkeys.isDeleteLineBackward(nativeEvent)) {
×
1202
                        event.preventDefault();
×
1203

1204
                        if (selection && Range.isExpanded(selection)) {
×
1205
                            Editor.deleteFragment(editor, {
×
1206
                                direction: 'backward'
1207
                            });
1208
                        } else {
1209
                            Editor.deleteBackward(editor, { unit: 'line' });
×
1210
                        }
1211

1212
                        return;
×
1213
                    }
1214

1215
                    if (Hotkeys.isDeleteLineForward(nativeEvent)) {
×
1216
                        event.preventDefault();
×
1217

1218
                        if (selection && Range.isExpanded(selection)) {
×
1219
                            Editor.deleteFragment(editor, {
×
1220
                                direction: 'forward'
1221
                            });
1222
                        } else {
1223
                            Editor.deleteForward(editor, { unit: 'line' });
×
1224
                        }
1225

1226
                        return;
×
1227
                    }
1228

1229
                    if (Hotkeys.isDeleteWordBackward(nativeEvent)) {
×
1230
                        event.preventDefault();
×
1231

1232
                        if (selection && Range.isExpanded(selection)) {
×
1233
                            Editor.deleteFragment(editor, {
×
1234
                                direction: 'backward'
1235
                            });
1236
                        } else {
1237
                            Editor.deleteBackward(editor, { unit: 'word' });
×
1238
                        }
1239

1240
                        return;
×
1241
                    }
1242

1243
                    if (Hotkeys.isDeleteWordForward(nativeEvent)) {
×
1244
                        event.preventDefault();
×
1245

1246
                        if (selection && Range.isExpanded(selection)) {
×
1247
                            Editor.deleteFragment(editor, {
×
1248
                                direction: 'forward'
1249
                            });
1250
                        } else {
1251
                            Editor.deleteForward(editor, { unit: 'word' });
×
1252
                        }
1253

1254
                        return;
×
1255
                    }
1256
                } else {
1257
                    if (IS_CHROME || IS_SAFARI) {
×
1258
                        // COMPAT: Chrome and Safari support `beforeinput` event but do not fire
1259
                        // an event when deleting backwards in a selected void inline node
1260
                        if (
×
1261
                            selection &&
×
1262
                            (Hotkeys.isDeleteBackward(nativeEvent) || Hotkeys.isDeleteForward(nativeEvent)) &&
1263
                            Range.isCollapsed(selection)
1264
                        ) {
1265
                            const currentNode = Node.parent(editor, selection.anchor.path);
×
1266
                            if (
×
1267
                                Element.isElement(currentNode) &&
×
1268
                                Editor.isVoid(editor, currentNode) &&
1269
                                Editor.isInline(editor, currentNode)
1270
                            ) {
1271
                                event.preventDefault();
×
1272
                                Editor.deleteBackward(editor, {
×
1273
                                    unit: 'block'
1274
                                });
1275
                                return;
×
1276
                            }
1277
                        }
1278
                    }
1279
                }
1280
            } catch (error) {
1281
                this.editor.onError({
×
1282
                    code: SlateErrorCode.OnDOMKeydownError,
1283
                    nativeError: error
1284
                });
1285
            }
1286
        }
1287
    }
1288

1289
    private onDOMPaste(event: ClipboardEvent) {
1290
        // COMPAT: Certain browsers don't support the `beforeinput` event, so we
1291
        // fall back to React's `onPaste` here instead.
1292
        // COMPAT: Firefox, Chrome and Safari are not emitting `beforeinput` events
1293
        // when "paste without formatting" option is used.
1294
        // This unfortunately needs to be handled with paste events instead.
1295
        if (
×
1296
            !this.isDOMEventHandled(event, this.paste) &&
×
1297
            (!HAS_BEFORE_INPUT_SUPPORT || isPlainTextOnlyPaste(event) || forceOnDOMPaste) &&
1298
            !this.readonly &&
1299
            hasEditableTarget(this.editor, event.target)
1300
        ) {
1301
            event.preventDefault();
×
1302
            AngularEditor.insertData(this.editor, event.clipboardData);
×
1303
        }
1304
    }
1305

1306
    private onFallbackBeforeInput(event: BeforeInputEvent) {
1307
        // COMPAT: Certain browsers don't support the `beforeinput` event, so we
1308
        // fall back to React's leaky polyfill instead just for it. It
1309
        // only works for the `insertText` input type.
1310
        if (
×
1311
            !HAS_BEFORE_INPUT_SUPPORT &&
×
1312
            !this.readonly &&
1313
            !this.isDOMEventHandled(event.nativeEvent, this.beforeInput) &&
1314
            hasEditableTarget(this.editor, event.nativeEvent.target)
1315
        ) {
1316
            event.nativeEvent.preventDefault();
×
1317
            try {
×
1318
                const text = event.data;
×
1319
                if (!Range.isCollapsed(this.editor.selection)) {
×
1320
                    Editor.deleteFragment(this.editor);
×
1321
                }
1322
                // just handle Non-IME input
1323
                if (!this.isComposing) {
×
1324
                    Editor.insertText(this.editor, text);
×
1325
                }
1326
            } catch (error) {
1327
                this.editor.onError({
×
1328
                    code: SlateErrorCode.ToNativeSelectionError,
1329
                    nativeError: error
1330
                });
1331
            }
1332
        }
1333
    }
1334

1335
    private isDOMEventHandled(event: Event, handler?: (event: Event) => void) {
1336
        if (!handler) {
3✔
1337
            return false;
3✔
1338
        }
1339
        handler(event);
×
1340
        return event.defaultPrevented;
×
1341
    }
1342
    //#endregion
1343

1344
    ngOnDestroy() {
1345
        NODE_TO_ELEMENT.delete(this.editor);
19✔
1346
        this.manualListeners.forEach(manualListener => {
19✔
1347
            manualListener();
399✔
1348
        });
1349
        this.destroy$.complete();
19✔
1350
        EDITOR_TO_ON_CHANGE.delete(this.editor);
19✔
1351
    }
1352
}
1353

1354
export const defaultScrollSelectionIntoView = (editor: AngularEditor, domRange: DOMRange) => {
1✔
1355
    // This was affecting the selection of multiple blocks and dragging behavior,
1356
    // so enabled only if the selection has been collapsed.
1357
    if (domRange.getBoundingClientRect && (!editor.selection || (editor.selection && Range.isCollapsed(editor.selection)))) {
×
1358
        const leafEl = domRange.startContainer.parentElement!;
×
1359
        leafEl.getBoundingClientRect = domRange.getBoundingClientRect.bind(domRange);
×
1360
        scrollIntoView(leafEl, {
×
1361
            scrollMode: 'if-needed'
1362
        });
1363
        delete leafEl.getBoundingClientRect;
×
1364
    }
1365
};
1366

1367
/**
1368
 * Check if the target is editable and in the editor.
1369
 */
1370

1371
export const hasEditableTarget = (editor: AngularEditor, target: EventTarget | null): target is DOMNode => {
1✔
1372
    return isDOMNode(target) && AngularEditor.hasDOMNode(editor, target, { editable: true });
3✔
1373
};
1374

1375
/**
1376
 * Check if two DOM range objects are equal.
1377
 */
1378
const isRangeEqual = (a: DOMRange, b: DOMRange) => {
1✔
1379
    return (
×
1380
        (a.startContainer === b.startContainer &&
×
1381
            a.startOffset === b.startOffset &&
1382
            a.endContainer === b.endContainer &&
1383
            a.endOffset === b.endOffset) ||
1384
        (a.startContainer === b.endContainer &&
1385
            a.startOffset === b.endOffset &&
1386
            a.endContainer === b.startContainer &&
1387
            a.endOffset === b.startOffset)
1388
    );
1389
};
1390

1391
/**
1392
 * Check if the target is in the editor.
1393
 */
1394

1395
const hasTarget = (editor: AngularEditor, target: EventTarget | null): target is DOMNode => {
1✔
1396
    return isDOMNode(target) && AngularEditor.hasDOMNode(editor, target);
1✔
1397
};
1398

1399
/**
1400
 * Check if the target is inside void and in the editor.
1401
 */
1402

1403
const isTargetInsideVoid = (editor: AngularEditor, target: EventTarget | null): boolean => {
1✔
1404
    const slateNode = hasTarget(editor, target) && AngularEditor.toSlateNode(editor, target);
1✔
1405
    return Editor.isVoid(editor, slateNode);
1✔
1406
};
1407

1408
const hasStringTarget = (domSelection: DOMSelection) => {
1✔
1409
    return (
3✔
1410
        (domSelection.anchorNode.parentElement.hasAttribute('data-slate-string') ||
6!
1411
            domSelection.anchorNode.parentElement.hasAttribute('data-slate-zero-width')) &&
1412
        (domSelection.focusNode.parentElement.hasAttribute('data-slate-string') ||
1413
            domSelection.focusNode.parentElement.hasAttribute('data-slate-zero-width'))
1414
    );
1415
};
1416

1417
/**
1418
 * remove default insert from composition
1419
 * @param text
1420
 */
1421
const preventInsertFromComposition = (event: Event, editor: AngularEditor) => {
1✔
1422
    const types = ['compositionend', 'insertFromComposition'];
×
1423
    if (!types.includes(event.type)) {
×
1424
        return;
×
1425
    }
1426
    const insertText = (event as CompositionEvent).data;
×
1427
    const window = AngularEditor.getWindow(editor);
×
1428
    const domSelection = window.getSelection();
×
1429
    // ensure text node insert composition input text
1430
    if (insertText && domSelection.anchorNode instanceof Text && domSelection.anchorNode.textContent.endsWith(insertText)) {
×
1431
        const textNode = domSelection.anchorNode;
×
1432
        textNode.splitText(textNode.length - insertText.length).remove();
×
1433
    }
1434
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc