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

worktile / slate-angular / bbef38c2-b9dd-46f4-8180-1ef56bbede3e

08 Mar 2024 09:19AM UTC coverage: 48.043% (+19.3%) from 28.702%
bbef38c2-b9dd-46f4-8180-1ef56bbede3e

push

circleci

pubuzhixing8
feat(core): support suppressThrow in toSlateRange/toSlatePoint/isLeafInEditor to check domSelection is valid

402 of 1034 branches covered (38.88%)

Branch coverage included in aggregate %.

19 of 32 new or added lines in 3 files covered. (59.38%)

2 existing lines in 1 file now uncovered.

985 of 1853 relevant lines covered (53.16%)

46.22 hits per line

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

46.97
/packages/src/plugins/angular-editor.ts
1
import { Editor, Node, Path, Point, Range, Transforms, Element, BaseEditor } from 'slate';
2
import {
3
    EDITOR_TO_ELEMENT,
4
    ELEMENT_TO_NODE,
5
    IS_FOCUSED,
6
    IS_READONLY,
7
    NODE_TO_INDEX,
8
    NODE_TO_PARENT,
9
    NODE_TO_ELEMENT,
10
    NODE_TO_KEY,
11
    EDITOR_TO_WINDOW
12
} from '../utils/weak-maps';
13
import {
14
    DOMElement,
15
    DOMNode,
16
    DOMPoint,
17
    DOMRange,
18
    DOMSelection,
19
    DOMStaticRange,
20
    hasShadowRoot,
21
    isDOMElement,
22
    isDOMSelection,
23
    normalizeDOMPoint
24
} from '../utils/dom';
25
import { Injector } from '@angular/core';
26
import { NodeEntry } from 'slate';
27
import { SlateError } from '../types/error';
28
import { Key } from '../utils/key';
29
import { IS_CHROME, IS_FIREFOX } from '../utils/environment';
30
import {
31
    FAKE_LEFT_BLOCK_CARD_OFFSET,
32
    FAKE_RIGHT_BLOCK_CARD_OFFSET,
33
    getCardTargetAttribute,
34
    isCardCenterByTargetAttr,
35
    isCardLeftByTargetAttr,
36
    isCardRightByTargetAttr
37
} from '../utils/block-card';
38
import { SafeAny } from '../types';
39

40
/**
41
 * An Angular and DOM-specific version of the `Editor` interface.
42
 */
43

44
export interface AngularEditor extends BaseEditor {
45
    insertData: (data: DataTransfer) => void;
46
    insertFragmentData: (data: DataTransfer) => boolean;
47
    insertTextData: (data: DataTransfer) => boolean;
48
    setFragmentData: (data: DataTransfer, originEvent?: 'drag' | 'copy' | 'cut') => void;
49
    deleteCutData: () => void;
50
    onKeydown: (event: KeyboardEvent) => void;
51
    onClick: (event: MouseEvent) => void;
52
    injector: Injector;
53
    isBlockCard: (node: Node) => boolean;
54
    isExpanded: (node: Element) => boolean;
55
    onError: (errorData: SlateError) => void;
56
    hasRange: (editor: AngularEditor, range: Range) => boolean;
57
}
58

59
export const AngularEditor = {
1✔
60
    /**
61
     * Return the host window of the current editor.
62
     */
63

64
    getWindow(editor: AngularEditor): Window {
65
        const window = EDITOR_TO_WINDOW.get(editor);
31✔
66
        if (!window) {
31!
67
            throw new Error('Unable to find a host window element for this editor');
×
68
        }
69
        return window;
31✔
70
    },
71
    /**
72
     * Find a key for a Slate node.
73
     */
74

75
    findKey(editor: AngularEditor, node: Node): Key {
76
        let key = NODE_TO_KEY.get(node);
808✔
77

78
        if (!key) {
808✔
79
            key = new Key();
354✔
80
            NODE_TO_KEY.set(node, key);
354✔
81
        }
82

83
        return key;
808✔
84
    },
85

86
    /**
87
     * handle editor error.
88
     */
89

90
    onError(errorData: SlateError) {
91
        if (errorData.nativeError) {
×
92
            throw errorData.nativeError;
×
93
        }
94
    },
95

96
    /**
97
     * Find the path of Slate node.
98
     */
99

100
    findPath(editor: AngularEditor, node: Node): Path {
101
        const path: Path = [];
261✔
102
        let child = node;
261✔
103

104
        while (true) {
261✔
105
            const parent = NODE_TO_PARENT.get(child);
704✔
106

107
            if (parent == null) {
704✔
108
                if (Editor.isEditor(child) && child === editor) {
261!
109
                    return path;
261✔
110
                } else {
111
                    break;
×
112
                }
113
            }
114

115
            const i = NODE_TO_INDEX.get(child);
443✔
116

117
            if (i == null) {
443!
118
                break;
×
119
            }
120

121
            path.unshift(i);
443✔
122
            child = parent;
443✔
123
        }
124
        throw new Error(`Unable to find the path for Slate node: ${JSON.stringify(node)}`);
×
125
    },
126

127
    isNodeInEditor(editor: AngularEditor, node: Node): boolean {
128
        let child = node;
3✔
129

130
        while (true) {
3✔
131
            const parent = NODE_TO_PARENT.get(child);
9✔
132

133
            if (parent == null) {
9✔
134
                if (Editor.isEditor(child) && child === editor) {
3!
135
                    return true;
3✔
136
                } else {
137
                    break;
×
138
                }
139
            }
140

141
            const i = NODE_TO_INDEX.get(child);
6✔
142

143
            if (i == null) {
6!
144
                break;
×
145
            }
146

147
            child = parent;
6✔
148
        }
149

150
        return false;
×
151
    },
152

153
    /**
154
     * Find the DOM node that implements DocumentOrShadowRoot for the editor.
155
     */
156

157
    findDocumentOrShadowRoot(editor: AngularEditor): Document | ShadowRoot {
158
        const el = AngularEditor.toDOMNode(editor, editor);
19✔
159
        const root = el.getRootNode();
19✔
160
        if ((root instanceof Document || root instanceof ShadowRoot) && (root as Document).getSelection != null) {
19!
161
            return root;
19✔
162
        }
163

164
        return el.ownerDocument;
×
165
    },
166

167
    /**
168
     * Check if the editor is focused.
169
     */
170

171
    isFocused(editor: AngularEditor): boolean {
172
        return !!IS_FOCUSED.get(editor);
15✔
173
    },
174

175
    /**
176
     * Check if the editor is in read-only mode.
177
     */
178

179
    isReadonly(editor: AngularEditor): boolean {
180
        return !!IS_READONLY.get(editor);
×
181
    },
182

183
    /**
184
     * Check if the editor is hanging right.
185
     */
186
    isBlockHangingRight(editor: AngularEditor): boolean {
187
        const { selection } = editor;
×
188
        if (!selection) {
×
189
            return false;
×
190
        }
191
        if (Range.isCollapsed(selection)) {
×
192
            return false;
×
193
        }
194
        const [start, end] = Range.edges(selection);
×
195
        const endBlock = Editor.above(editor, {
×
196
            at: end,
197
            match: node => Element.isElement(node) && Editor.isBlock(editor, node)
×
198
        });
199
        return Editor.isStart(editor, end, endBlock[1]);
×
200
    },
201

202
    /**
203
     * Blur the editor.
204
     */
205

206
    blur(editor: AngularEditor): void {
207
        const el = AngularEditor.toDOMNode(editor, editor);
×
208
        const root = AngularEditor.findDocumentOrShadowRoot(editor);
×
209
        IS_FOCUSED.set(editor, false);
×
210

211
        if (root.activeElement === el) {
×
212
            el.blur();
×
213
        }
214
    },
215

216
    /**
217
     * Focus the editor.
218
     */
219

220
    focus(editor: AngularEditor): void {
221
        const el = AngularEditor.toDOMNode(editor, editor);
2✔
222
        IS_FOCUSED.set(editor, true);
2✔
223

224
        const window = AngularEditor.getWindow(editor);
2✔
225
        if (window.document.activeElement !== el) {
2✔
226
            el.focus({ preventScroll: true });
2✔
227
        }
228
    },
229

230
    /**
231
     * Deselect the editor.
232
     */
233

234
    deselect(editor: AngularEditor): void {
235
        const { selection } = editor;
×
236
        const root = AngularEditor.findDocumentOrShadowRoot(editor);
×
237
        const domSelection = (root as Document).getSelection();
×
238

239
        if (domSelection && domSelection.rangeCount > 0) {
×
240
            domSelection.removeAllRanges();
×
241
        }
242

243
        if (selection) {
×
244
            Transforms.deselect(editor);
×
245
        }
246
    },
247

248
    /**
249
     * Check if a DOM node is within the editor.
250
     */
251

252
    hasDOMNode(editor: AngularEditor, target: DOMNode, options: { editable?: boolean } = {}): boolean {
1✔
253
        const { editable = false } = options;
4✔
254
        const editorEl = AngularEditor.toDOMNode(editor, editor);
4✔
255
        let targetEl;
256

257
        // COMPAT: In Firefox, reading `target.nodeType` will throw an error if
258
        // target is originating from an internal "restricted" element (e.g. a
259
        // stepper arrow on a number input). (2018/05/04)
260
        // https://github.com/ianstormtaylor/slate/issues/1819
261
        try {
4✔
262
            targetEl = (isDOMElement(target) ? target : target.parentElement) as HTMLElement;
4!
263
        } catch (err) {
264
            if (!err.message.includes('Permission denied to access property "nodeType"')) {
×
265
                throw err;
×
266
            }
267
        }
268

269
        if (!targetEl) {
4!
270
            return false;
×
271
        }
272

273
        return (
4✔
274
            targetEl.closest(`[data-slate-editor]`) === editorEl &&
11!
275
            (!editable || targetEl.isContentEditable || !!targetEl.getAttribute('data-slate-zero-width'))
276
        );
277
    },
278

279
    /**
280
     * Insert data from a `DataTransfer` into the editor.
281
     */
282

283
    insertData(editor: AngularEditor, data: DataTransfer): void {
284
        editor.insertData(data);
×
285
    },
286

287
    /**
288
     * Insert fragment data from a `DataTransfer` into the editor.
289
     */
290

291
    insertFragmentData(editor: AngularEditor, data: DataTransfer): boolean {
292
        return editor.insertFragmentData(data);
×
293
    },
294

295
    /**
296
     * Insert text data from a `DataTransfer` into the editor.
297
     */
298

299
    insertTextData(editor: AngularEditor, data: DataTransfer): boolean {
300
        return editor.insertTextData(data);
×
301
    },
302

303
    /**
304
     * onKeydown hook.
305
     */
306
    onKeydown(editor: AngularEditor, data: KeyboardEvent): void {
307
        editor.onKeydown(data);
×
308
    },
309

310
    /**
311
     * onClick hook.
312
     */
313
    onClick(editor: AngularEditor, data: MouseEvent): void {
314
        editor.onClick(data);
×
315
    },
316

317
    /**
318
     * Sets data from the currently selected fragment on a `DataTransfer`.
319
     */
320

321
    setFragmentData(editor: AngularEditor, data: DataTransfer, originEvent?: 'drag' | 'copy' | 'cut'): void {
322
        editor.setFragmentData(data, originEvent);
×
323
    },
324

325
    deleteCutData(editor: AngularEditor): void {
326
        editor.deleteCutData();
×
327
    },
328

329
    /**
330
     * Find the native DOM element from a Slate node.
331
     */
332

333
    toDOMNode(editor: AngularEditor, node: Node): HTMLElement {
334
        const domNode = Editor.isEditor(node) ? EDITOR_TO_ELEMENT.get(editor) : NODE_TO_ELEMENT.get(node);
39✔
335

336
        if (!domNode) {
39!
337
            throw new Error(`Cannot resolve a DOM node from Slate node: ${JSON.stringify(node)}`);
×
338
        }
339

340
        return domNode;
39✔
341
    },
342

343
    /**
344
     * Find a native DOM selection point from a Slate point.
345
     */
346

347
    toDOMPoint(editor: AngularEditor, point: Point): DOMPoint {
348
        const [node] = Editor.node(editor, point.path);
3✔
349
        const el = AngularEditor.toDOMNode(editor, node);
3✔
350
        let domPoint: DOMPoint | undefined;
351

352
        // block card
353
        const cardTargetAttr = getCardTargetAttribute(el);
3✔
354
        if (cardTargetAttr) {
3!
355
            if (point.offset === FAKE_LEFT_BLOCK_CARD_OFFSET) {
×
356
                const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'left' });
×
357
                return [cursorNode, 1];
×
358
            } else {
359
                const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'right' });
×
360
                return [cursorNode, 1];
×
361
            }
362
        }
363

364
        // If we're inside a void node, force the offset to 0, otherwise the zero
365
        // width spacing character will result in an incorrect offset of 1
366
        if (Editor.void(editor, { at: point })) {
3!
367
            point = { path: point.path, offset: 0 };
×
368
        }
369

370
        // For each leaf, we need to isolate its content, which means filtering
371
        // to its direct text and zero-width spans. (We have to filter out any
372
        // other siblings that may have been rendered alongside them.)
373
        const selector = `[data-slate-string], [data-slate-zero-width]`;
3✔
374
        const texts = Array.from(el.querySelectorAll(selector));
3✔
375
        let start = 0;
3✔
376

377
        for (const text of texts) {
3✔
378
            const domNode = text.childNodes[0] as HTMLElement;
3✔
379

380
            if (domNode == null || domNode.textContent == null) {
3!
381
                continue;
×
382
            }
383

384
            const { length } = domNode.textContent;
3✔
385
            const attr = text.getAttribute('data-slate-length');
3✔
386
            const trueLength = attr == null ? length : parseInt(attr, 10);
3✔
387
            const end = start + trueLength;
3✔
388

389
            if (point.offset <= end) {
3✔
390
                const offset = Math.min(length, Math.max(0, point.offset - start));
3✔
391
                domPoint = [domNode, offset];
3✔
392
                // fixed cursor position after zero width char
393
                if (offset === 0 && length === 1 && domNode.textContent === '\uFEFF') {
3✔
394
                    domPoint = [domNode, offset + 1];
1✔
395
                }
396
                break;
3✔
397
            }
398

399
            start = end;
×
400
        }
401

402
        if (!domPoint) {
3!
403
            throw new Error(`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`);
×
404
        }
405

406
        return domPoint;
3✔
407
    },
408

409
    /**
410
     * Find a native DOM range from a Slate `range`.
411
     */
412

413
    toDOMRange(editor: AngularEditor, range: Range): DOMRange {
414
        const { anchor, focus } = range;
3✔
415
        const isBackward = Range.isBackward(range);
3✔
416
        const domAnchor = AngularEditor.toDOMPoint(editor, anchor);
3✔
417
        const domFocus = Range.isCollapsed(range) ? domAnchor : AngularEditor.toDOMPoint(editor, focus);
3!
418

419
        const window = AngularEditor.getWindow(editor);
3✔
420
        const domRange = window.document.createRange();
3✔
421
        const [startNode, startOffset] = isBackward ? domFocus : domAnchor;
3!
422
        const [endNode, endOffset] = isBackward ? domAnchor : domFocus;
3!
423

424
        // A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
425
        // zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
426
        // adjust the offset accordingly.
427
        const startEl = (isDOMElement(startNode) ? startNode : startNode.parentElement) as HTMLElement;
3!
428
        const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
3✔
429
        const endEl = (isDOMElement(endNode) ? endNode : endNode.parentElement) as HTMLElement;
3!
430
        const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');
3✔
431

432
        domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset);
3✔
433
        domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset);
3✔
434
        return domRange;
3✔
435
    },
436

437
    /**
438
     * Find a Slate node from a native DOM `element`.
439
     */
440

441
    toSlateNode<T extends boolean>(
442
        editor: AngularEditor,
443
        domNode: DOMNode,
444
        options?: {
445
            suppressThrow: T;
446
        }
447
    ): T extends true ? Node | null : Node {
448
        const { suppressThrow } = options || { suppressThrow: false };
7✔
449
        let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;
7!
450

451
        if (domEl && !domEl.hasAttribute('data-slate-node')) {
7!
452
            domEl = domEl.closest(`[data-slate-node]`);
×
453
        }
454

455
        const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null;
7!
456

457
        if (!node) {
7!
NEW
458
            if (suppressThrow) {
×
NEW
459
                return null as T extends true ? Node | null : Node;
×
460
            }
UNCOV
461
            throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`);
×
462
        }
463

464
        return node;
7✔
465
    },
466

467
    /**
468
     * Get the target range from a DOM `event`.
469
     */
470

471
    findEventRange(editor: AngularEditor, event: any): Range {
472
        if ('nativeEvent' in event) {
×
473
            event = event.nativeEvent;
×
474
        }
475

476
        const { clientX: x, clientY: y, target } = event;
×
477

478
        if (x == null || y == null) {
×
479
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
480
        }
481

NEW
482
        const node = AngularEditor.toSlateNode(editor, event.target, { suppressThrow: false });
×
483
        const path = AngularEditor.findPath(editor, node);
×
484

485
        // If the drop target is inside a void node, move it into either the
486
        // next or previous node, depending on which side the `x` and `y`
487
        // coordinates are closest to.
488
        if (Element.isElement(node) && Editor.isVoid(editor, node)) {
×
489
            const rect = target.getBoundingClientRect();
×
490
            const isPrev = editor.isInline(node) ? x - rect.left < rect.left + rect.width - x : y - rect.top < rect.top + rect.height - y;
×
491

492
            const edge = Editor.point(editor, path, {
×
493
                edge: isPrev ? 'start' : 'end'
×
494
            });
495
            const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge);
×
496

497
            if (point) {
×
498
                return Editor.range(editor, point);
×
499
            }
500
        }
501

502
        // Else resolve a range from the caret position where the drop occured.
503
        let domRange: DOMRange;
504
        const window = AngularEditor.getWindow(editor);
×
505
        const { document } = window;
×
506

507
        // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
508
        if (document.caretRangeFromPoint) {
×
509
            domRange = document.caretRangeFromPoint(x, y);
×
510
        } else {
511
            const position = (document as SafeAny).caretPositionFromPoint(x, y);
×
512

513
            if (position) {
×
514
                domRange = document.createRange();
×
515
                domRange.setStart(position.offsetNode, position.offset);
×
516
                domRange.setEnd(position.offsetNode, position.offset);
×
517
            }
518
        }
519

520
        if (!domRange) {
×
521
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
522
        }
523

524
        // Resolve a Slate range from the DOM range.
NEW
525
        const range = AngularEditor.toSlateRange(editor, domRange, { suppressThrow: false });
×
526
        return range;
×
527
    },
528

529
    isLeafInEditor(
530
        editor: AngularEditor,
531
        leafNode: DOMElement,
532
        options: {
533
            suppressThrow: boolean;
534
        }
535
    ): boolean {
536
        const { suppressThrow } = options;
3✔
537
        const textNode = leafNode.closest('[data-slate-node="text"]')!;
3✔
538
        const node = AngularEditor.toSlateNode(editor, textNode, { suppressThrow });
3✔
539
        if (node && AngularEditor.isNodeInEditor(editor, node)) {
3!
540
            return true;
3✔
541
        } else {
NEW
542
            return false;
×
543
        }
544
    },
545

546
    /**
547
     * Find a Slate point from a DOM selection's `domNode` and `domOffset`.
548
     */
549

550
    toSlatePoint<T extends boolean>(
551
        editor: AngularEditor,
552
        domPoint: DOMPoint,
553
        options: {
554
            exactMatch?: boolean;
555
            suppressThrow: T;
556
        }
557
    ): T extends true ? Point | null : Point {
558
        const { exactMatch, suppressThrow } = options;
3✔
559
        const [domNode] = domPoint;
3✔
560
        const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint);
3✔
561
        let parentNode = nearestNode.parentNode as DOMElement;
3✔
562
        let textNode: DOMElement | null = null;
3✔
563
        let offset = 0;
3✔
564

565
        // block card
566
        const cardTargetAttr = getCardTargetAttribute(domNode);
3✔
567
        if (cardTargetAttr) {
3!
568
            const domSelection = window.getSelection();
×
569
            const isBackward = editor.selection && Range.isBackward(editor.selection);
×
570
            const blockCardEntry = AngularEditor.toSlateCardEntry(editor, domNode) || AngularEditor.toSlateCardEntry(editor, nearestNode);
×
571
            const [, blockPath] = blockCardEntry;
×
572
            if (domSelection.isCollapsed) {
×
573
                if (isCardLeftByTargetAttr(cardTargetAttr)) {
×
574
                    return { path: blockPath, offset: -1 };
×
575
                } else {
576
                    return { path: blockPath, offset: -2 };
×
577
                }
578
            }
579
            // forward
580
            // and to the end of previous node
581
            if (isCardLeftByTargetAttr(cardTargetAttr) && !isBackward) {
×
582
                const endPath = blockPath[blockPath.length - 1] <= 0 ? blockPath : Path.previous(blockPath);
×
583
                return Editor.end(editor, endPath);
×
584
            }
585
            // to the of current node
586
            if ((isCardCenterByTargetAttr(cardTargetAttr) || isCardRightByTargetAttr(cardTargetAttr)) && !isBackward) {
×
587
                return Editor.end(editor, blockPath);
×
588
            }
589
            // backward
590
            // and to the start of next node
591
            if (isCardRightByTargetAttr(cardTargetAttr) && isBackward) {
×
592
                return Editor.start(editor, Path.next(blockPath));
×
593
            }
594
            // and to the start of current node
595
            if ((isCardCenterByTargetAttr(cardTargetAttr) || isCardLeftByTargetAttr(cardTargetAttr)) && isBackward) {
×
596
                return Editor.start(editor, blockPath);
×
597
            }
598
        }
599

600
        if (parentNode) {
3✔
601
            const voidNode = parentNode.closest('[data-slate-void="true"]');
3✔
602
            let leafNode = parentNode.closest('[data-slate-leaf]');
3✔
603
            let domNode: DOMElement | null = null;
3✔
604

605
            // Calculate how far into the text node the `nearestNode` is, so that we
606
            // can determine what the offset relative to the text node is.
607
            if (leafNode && AngularEditor.isLeafInEditor(editor, leafNode, { suppressThrow: true })) {
3!
608
                textNode = leafNode.closest('[data-slate-node="text"]')!;
3✔
609
                const window = AngularEditor.getWindow(editor);
3✔
610
                const range = window.document.createRange();
3✔
611
                range.setStart(textNode, 0);
3✔
612
                range.setEnd(nearestNode, nearestOffset);
3✔
613
                const contents = range.cloneContents();
3✔
614
                const removals = [
3✔
615
                    ...Array.prototype.slice.call(contents.querySelectorAll('[data-slate-zero-width]')),
616
                    ...Array.prototype.slice.call(contents.querySelectorAll('[contenteditable=false]'))
617
                ];
618

619
                removals.forEach(el => {
3✔
620
                    el!.parentNode!.removeChild(el);
×
621
                });
622

623
                // COMPAT: Edge has a bug where Range.prototype.toString() will
624
                // convert \n into \r\n. The bug causes a loop when slate-react
625
                // attempts to reposition its cursor to match the native position. Use
626
                // textContent.length instead.
627
                // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
628
                offset = contents.textContent!.length;
3✔
629
                domNode = textNode;
3✔
630
            } else if (voidNode) {
×
631
                // For void nodes, the element with the offset key will be a cousin, not an
632
                // ancestor, so find it by going down from the nearest void parent.
633
                const spacer = voidNode.querySelector('[data-slate-spacer="true"]')!;
×
634
                leafNode = spacer.firstElementChild;
×
635
                parentNode = leafNode.firstElementChild;
×
636
                textNode = spacer;
×
637
                domNode = leafNode;
×
638
                offset = domNode.textContent!.length;
×
639
            }
640

641
            // COMPAT: If the parent node is a Slate zero-width space, editor is
642
            // because the text node should have no characters. However, during IME
643
            // composition the ASCII characters will be prepended to the zero-width
644
            // space, so subtract 1 from the offset to account for the zero-width
645
            // space character.
646
            if (domNode && offset === domNode.textContent!.length && parentNode && parentNode.hasAttribute('data-slate-zero-width')) {
3!
647
                offset--;
×
648
            }
649
        }
650

651
        if (!textNode) {
3!
NEW
652
            if (suppressThrow) {
×
NEW
653
                return null as T extends true ? Point | null : Point;
×
654
            }
UNCOV
655
            throw new Error(`Cannot resolve a Slate point from DOM point: ${domPoint}`);
×
656
        }
657

658
        // COMPAT: If someone is clicking from one Slate editor into another,
659
        // the select event fires twice, once for the old editor's `element`
660
        // first, and then afterwards for the correct `element`. (2017/03/03)
661
        const slateNode = AngularEditor.toSlateNode(editor, textNode, { suppressThrow });
3✔
662
        if (!slateNode && suppressThrow) {
3!
NEW
663
            return null as T extends true ? Point | null : Point;
×
664
        }
665
        const path = AngularEditor.findPath(editor, slateNode);
3✔
666
        return { path, offset };
3✔
667
    },
668

669
    /**
670
     * Find a Slate range from a DOM range or selection.
671
     */
672

673
    toSlateRange<T extends boolean>(
674
        editor: AngularEditor,
675
        domRange: DOMRange | DOMStaticRange | DOMSelection,
676
        options?: {
677
            exactMatch?: boolean;
678
            suppressThrow: T;
679
        }
680
    ): T extends true ? Range | null : Range {
681
        const { exactMatch, suppressThrow } = options || {};
3✔
682
        const el = isDOMSelection(domRange) ? domRange.anchorNode : domRange.startContainer;
3!
683
        let anchorNode;
684
        let anchorOffset;
685
        let focusNode;
686
        let focusOffset;
687
        let isCollapsed;
688

689
        if (el) {
3✔
690
            if (isDOMSelection(domRange)) {
3!
691
                anchorNode = domRange.anchorNode;
3✔
692
                anchorOffset = domRange.anchorOffset;
3✔
693
                focusNode = domRange.focusNode;
3✔
694
                focusOffset = domRange.focusOffset;
3✔
695
                // COMPAT: There's a bug in chrome that always returns `true` for
696
                // `isCollapsed` for a Selection that comes from a ShadowRoot.
697
                // (2020/08/08)
698
                // https://bugs.chromium.org/p/chromium/issues/detail?id=447523
699
                if (IS_CHROME && hasShadowRoot()) {
3!
700
                    isCollapsed = domRange.anchorNode === domRange.focusNode && domRange.anchorOffset === domRange.focusOffset;
×
701
                } else {
702
                    isCollapsed = domRange.isCollapsed;
3✔
703
                }
704
            } else {
705
                anchorNode = domRange.startContainer;
×
706
                anchorOffset = domRange.startOffset;
×
707
                focusNode = domRange.endContainer;
×
708
                focusOffset = domRange.endOffset;
×
709
                isCollapsed = domRange.collapsed;
×
710
            }
711
        }
712

713
        if (anchorNode == null || focusNode == null || anchorOffset == null || focusOffset == null) {
3!
714
            throw new Error(`Cannot resolve a Slate range from DOM range: ${domRange}`);
×
715
        }
716

717
        // COMPAT: Triple-clicking a word in chrome will sometimes place the focus
718
        // inside a `contenteditable="false"` DOM node following the word, which
719
        // will cause `toSlatePoint` to throw an error. (2023/03/07)
720
        if (
3!
721
            'getAttribute' in focusNode &&
3!
722
            (focusNode as HTMLElement).getAttribute('contenteditable') === 'false' &&
723
            (focusNode as HTMLElement).getAttribute('data-slate-void') !== 'true'
724
        ) {
725
            focusNode = anchorNode;
×
726
            focusOffset = anchorNode.textContent?.length || 0;
×
727
        }
728

729
        const anchor = AngularEditor.toSlatePoint(editor, [anchorNode, anchorOffset], { suppressThrow, exactMatch });
3✔
730
        if (!anchor) {
3!
NEW
731
            return null as T extends true ? Range | null : Range;
×
732
        }
733

734
        const focus = isCollapsed ? anchor : AngularEditor.toSlatePoint(editor, [focusNode, focusOffset], { suppressThrow, exactMatch });
3!
735
        if (!focus) {
3!
NEW
736
            return null as T extends true ? Range | null : Range;
×
737
        }
738

739
        let range: Range = { anchor: anchor as Point, focus: focus as Point };
3✔
740
        // if the selection is a hanging range that ends in a void
741
        // and the DOM focus is an Element
742
        // (meaning that the selection ends before the element)
743
        // unhang the range to avoid mistakenly including the void
744
        if (
3!
745
            Range.isExpanded(range) &&
3!
746
            Range.isForward(range) &&
747
            isDOMElement(focusNode) &&
748
            Editor.void(editor, { at: range.focus, mode: 'highest' })
749
        ) {
750
            range = Editor.unhangRange(editor, range, { voids: true });
×
751
        }
752

753
        return range;
3✔
754
    },
755

756
    isLeafBlock(editor: AngularEditor, node: Node): boolean {
757
        return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
160✔
758
    },
759

760
    isBlockCardLeftCursor(editor: AngularEditor) {
761
        return (
×
762
            editor.selection.anchor.offset === FAKE_LEFT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_LEFT_BLOCK_CARD_OFFSET
×
763
        );
764
    },
765

766
    isBlockCardRightCursor(editor: AngularEditor) {
767
        return (
×
768
            editor.selection.anchor.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET &&
×
769
            editor.selection.focus.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET
770
        );
771
    },
772

773
    getCardCursorNode(
774
        editor: AngularEditor,
775
        blockCardNode: Node,
776
        options: {
777
            direction: 'left' | 'right' | 'center';
778
        }
779
    ) {
780
        const blockCardElement = AngularEditor.toDOMNode(editor, blockCardNode);
×
781
        const cardCenter = blockCardElement.parentElement;
×
782
        return options.direction === 'left' ? cardCenter.previousElementSibling.firstChild : cardCenter.nextElementSibling.firstChild;
×
783
    },
784

785
    toSlateCardEntry(editor: AngularEditor, node: DOMNode): NodeEntry {
786
        const element = node.parentElement.closest('.slate-block-card')?.querySelector('[card-target="card-center"]').firstElementChild;
×
NEW
787
        const slateNode = AngularEditor.toSlateNode(editor, element, { suppressThrow: false });
×
788
        const path = AngularEditor.findPath(editor, slateNode);
×
789
        return [slateNode, path];
×
790
    },
791

792
    /**
793
     * move native selection to card-left or card-right
794
     * @param editor
795
     * @param blockCardNode
796
     * @param options
797
     */
798
    moveBlockCard(
799
        editor: AngularEditor,
800
        blockCardNode: Node,
801
        options: {
802
            direction: 'left' | 'right';
803
        }
804
    ) {
805
        const cursorNode = AngularEditor.getCardCursorNode(editor, blockCardNode, options);
×
806
        const window = AngularEditor.getWindow(editor);
×
807
        const domSelection = window.getSelection();
×
808
        domSelection.setBaseAndExtent(cursorNode, 1, cursorNode, 1);
×
809
    },
810

811
    /**
812
     * move slate selection to card-left or card-right
813
     * @param editor
814
     * @param path
815
     * @param options
816
     */
817
    moveBlockCardCursor(
818
        editor: AngularEditor,
819
        path: Path,
820
        options: {
821
            direction: 'left' | 'right';
822
        }
823
    ) {
824
        const cursor = {
×
825
            path,
826
            offset: options.direction === 'left' ? FAKE_LEFT_BLOCK_CARD_OFFSET : FAKE_RIGHT_BLOCK_CARD_OFFSET
×
827
        };
828
        Transforms.select(editor, { anchor: cursor, focus: cursor });
×
829
    },
830

831
    hasRange(editor: AngularEditor, range: Range): boolean {
832
        const { anchor, focus } = range;
1✔
833
        return Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path);
1✔
834
    }
835
};
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

© 2025 Coveralls, Inc