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

worktile / slate-angular / 2aa1a6e9-0e99-48d9-b5aa-09e3a518da3c

pending completion
2aa1a6e9-0e99-48d9-b5aa-09e3a518da3c

push

circleci

pubuzhixing8
fix(core): correct selection when focus in void element include slate-angular component

352 of 928 branches covered (37.93%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 1 file covered. (100.0%)

815 of 1603 relevant lines covered (50.84%)

49.66 hits per line

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

47.89
/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
    onError: (errorData: SlateError) => void;
55
    hasRange: (editor: AngularEditor, range: Range) => boolean;
56
}
57

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

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

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

77
        if (!key) {
745✔
78
            key = new Key();
332✔
79
            NODE_TO_KEY.set(node, key);
332✔
80
        }
81

82
        return key;
745✔
83
    },
84

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

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

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

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

103
        while (true) {
726✔
104
            const parent = NODE_TO_PARENT.get(child);
2,166✔
105

106
            if (parent == null) {
2,166✔
107
                if (Editor.isEditor(child) && child === editor) {
726!
108
                    return path;
726✔
109
                } else {
110
                    break;
×
111
                }
112
            }
113

114
            const i = NODE_TO_INDEX.get(child);
1,440✔
115

116
            if (i == null) {
1,440!
117
                break;
×
118
            }
119

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

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

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

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

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

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

146
            child = parent;
6✔
147
        }
148

149
        return false;
×
150
    },
151

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

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

163
        return el.ownerDocument;
×
164
    },
165

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

339
        return domNode;
34✔
340
    },
341

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

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

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

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

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

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

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

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

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

398
            start = end;
×
399
        }
400

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

405
        return domPoint;
3✔
406
    },
407

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

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

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

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

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

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

440
    toSlateNode(editor: AngularEditor, domNode: DOMNode): Node {
441
        let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;
7!
442

443
        if (domEl && !domEl.hasAttribute('data-slate-node')) {
7!
444
            domEl = domEl.closest(`[data-slate-node]`);
×
445
        }
446

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

449
        if (!node) {
7!
450
            throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`);
×
451
        }
452

453
        return node;
7✔
454
    },
455

456
    /**
457
     * Get the target range from a DOM `event`.
458
     */
459

460
    findEventRange(editor: AngularEditor, event: any): Range {
461
        if ('nativeEvent' in event) {
×
462
            event = event.nativeEvent;
×
463
        }
464

465
        const { clientX: x, clientY: y, target } = event;
×
466

467
        if (x == null || y == null) {
×
468
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
469
        }
470

471
        const node = AngularEditor.toSlateNode(editor, event.target);
×
472
        const path = AngularEditor.findPath(editor, node);
×
473

474
        // If the drop target is inside a void node, move it into either the
475
        // next or previous node, depending on which side the `x` and `y`
476
        // coordinates are closest to.
477
        if (Editor.isVoid(editor, node)) {
×
478
            const rect = target.getBoundingClientRect();
×
479
            const isPrev = editor.isInline(node) ? x - rect.left < rect.left + rect.width - x : y - rect.top < rect.top + rect.height - y;
×
480

481
            const edge = Editor.point(editor, path, {
×
482
                edge: isPrev ? 'start' : 'end'
×
483
            });
484
            const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge);
×
485

486
            if (point) {
×
487
                return Editor.range(editor, point);
×
488
            }
489
        }
490

491
        // Else resolve a range from the caret position where the drop occured.
492
        let domRange: DOMRange;
493
        const window = AngularEditor.getWindow(editor);
×
494
        const { document } = window;
×
495

496
        // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
497
        if (document.caretRangeFromPoint) {
×
498
            domRange = document.caretRangeFromPoint(x, y);
×
499
        } else {
500
            const position = (document as SafeAny).caretPositionFromPoint(x, y);
×
501

502
            if (position) {
×
503
                domRange = document.createRange();
×
504
                domRange.setStart(position.offsetNode, position.offset);
×
505
                domRange.setEnd(position.offsetNode, position.offset);
×
506
            }
507
        }
508

509
        if (!domRange) {
×
510
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
511
        }
512

513
        // Resolve a Slate range from the DOM range.
514
        const range = AngularEditor.toSlateRange(editor, domRange);
×
515
        return range;
×
516
    },
517

518
    isLeafInEditor(editor: AngularEditor, leafNode: DOMElement): boolean {
519
        const textNode = leafNode.closest('[data-slate-node="text"]')!;
3✔
520
        const node = AngularEditor.toSlateNode(editor, textNode);
3✔
521
        return AngularEditor.isNodeInEditor(editor, node);
3✔
522
    },
523

524
    /**
525
     * Find a Slate point from a DOM selection's `domNode` and `domOffset`.
526
     */
527

528
    toSlatePoint(editor: AngularEditor, domPoint: DOMPoint): Point {
529
        const [domNode] = domPoint;
3✔
530
        const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint);
3✔
531
        let parentNode = nearestNode.parentNode as DOMElement;
3✔
532
        let textNode: DOMElement | null = null;
3✔
533
        let offset = 0;
3✔
534

535
        // block card
536
        const cardTargetAttr = getCardTargetAttribute(domNode);
3✔
537
        if (cardTargetAttr) {
3!
538
            const domSelection = window.getSelection();
×
539
            const isBackward = editor.selection && Range.isBackward(editor.selection);
×
540
            const blockCardEntry = AngularEditor.toSlateCardEntry(editor, domNode) || AngularEditor.toSlateCardEntry(editor, nearestNode);
×
541
            const [, blockPath] = blockCardEntry;
×
542
            if (domSelection.isCollapsed) {
×
543
                if (isCardLeftByTargetAttr(cardTargetAttr)) {
×
544
                    return { path: blockPath, offset: -1 };
×
545
                } else {
546
                    return { path: blockPath, offset: -2 };
×
547
                }
548
            }
549
            // forward
550
            // and to the end of previous node
551
            if (isCardLeftByTargetAttr(cardTargetAttr) && !isBackward) {
×
552
                const endPath = blockPath[blockPath.length - 1] <= 0 ? blockPath : Path.previous(blockPath);
×
553
                return Editor.end(editor, endPath);
×
554
            }
555
            // to the of current node
556
            if ((isCardCenterByTargetAttr(cardTargetAttr) || isCardRightByTargetAttr(cardTargetAttr)) && !isBackward) {
×
557
                return Editor.end(editor, blockPath);
×
558
            }
559
            // backward
560
            // and to the start of next node
561
            if (isCardRightByTargetAttr(cardTargetAttr) && isBackward) {
×
562
                return Editor.start(editor, Path.next(blockPath));
×
563
            }
564
            // and to the start of current node
565
            if ((isCardCenterByTargetAttr(cardTargetAttr) || isCardLeftByTargetAttr(cardTargetAttr)) && isBackward) {
×
566
                return Editor.start(editor, blockPath);
×
567
            }
568
        }
569

570
        if (parentNode) {
3✔
571
            const voidNode = parentNode.closest('[data-slate-void="true"]');
3✔
572
            let leafNode = parentNode.closest('[data-slate-leaf]');
3✔
573
            let domNode: DOMElement | null = null;
3✔
574

575
            // Calculate how far into the text node the `nearestNode` is, so that we
576
            // can determine what the offset relative to the text node is.
577
            if (leafNode && AngularEditor.isLeafInEditor(editor, leafNode)) {
3!
578
                textNode = leafNode.closest('[data-slate-node="text"]')!;
3✔
579
                const window = AngularEditor.getWindow(editor);
3✔
580
                const range = window.document.createRange();
3✔
581
                range.setStart(textNode, 0);
3✔
582
                range.setEnd(nearestNode, nearestOffset);
3✔
583
                const contents = range.cloneContents();
3✔
584
                const removals = [
3✔
585
                    ...Array.prototype.slice.call(contents.querySelectorAll('[data-slate-zero-width]')),
586
                    ...Array.prototype.slice.call(contents.querySelectorAll('[contenteditable=false]'))
587
                ];
588

589
                removals.forEach(el => {
3✔
590
                    el!.parentNode!.removeChild(el);
×
591
                });
592

593
                // COMPAT: Edge has a bug where Range.prototype.toString() will
594
                // convert \n into \r\n. The bug causes a loop when slate-react
595
                // attempts to reposition its cursor to match the native position. Use
596
                // textContent.length instead.
597
                // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
598
                offset = contents.textContent!.length;
3✔
599
                domNode = textNode;
3✔
600
            } else if (voidNode) {
×
601
                // For void nodes, the element with the offset key will be a cousin, not an
602
                // ancestor, so find it by going down from the nearest void parent.
603

604
                leafNode = voidNode.querySelector('[data-slate-leaf]')!;
×
605
                parentNode = voidNode.querySelector('[data-slate-length="0"]');
×
606
                textNode = leafNode.closest('[data-slate-node="text"]')!;
×
607
                domNode = leafNode;
×
608
                offset = domNode.textContent!.length;
×
609
            }
610

611
            // COMPAT: If the parent node is a Slate zero-width space, editor is
612
            // because the text node should have no characters. However, during IME
613
            // composition the ASCII characters will be prepended to the zero-width
614
            // space, so subtract 1 from the offset to account for the zero-width
615
            // space character.
616
            if (domNode && offset === domNode.textContent!.length && parentNode && parentNode.hasAttribute('data-slate-zero-width')) {
3!
617
                offset--;
×
618
            }
619
        }
620

621
        if (!textNode) {
3!
622
            throw new Error(`Cannot resolve a Slate point from DOM point: ${domPoint}`);
×
623
        }
624

625
        // COMPAT: If someone is clicking from one Slate editor into another,
626
        // the select event fires twice, once for the old editor's `element`
627
        // first, and then afterwards for the correct `element`. (2017/03/03)
628
        const slateNode = AngularEditor.toSlateNode(editor, textNode!);
3✔
629
        const path = AngularEditor.findPath(editor, slateNode);
3✔
630
        return { path, offset };
3✔
631
    },
632

633
    /**
634
     * Find a Slate range from a DOM range or selection.
635
     */
636

637
    toSlateRange(editor: AngularEditor, domRange: DOMRange | DOMStaticRange | DOMSelection): Range {
638
        const el = isDOMSelection(domRange) ? domRange.anchorNode : domRange.startContainer;
3!
639
        let anchorNode;
640
        let anchorOffset;
641
        let focusNode;
642
        let focusOffset;
643
        let isCollapsed;
644

645
        if (el) {
3✔
646
            if (isDOMSelection(domRange)) {
3!
647
                anchorNode = domRange.anchorNode;
3✔
648
                anchorOffset = domRange.anchorOffset;
3✔
649
                focusNode = domRange.focusNode;
3✔
650
                focusOffset = domRange.focusOffset;
3✔
651
                // COMPAT: There's a bug in chrome that always returns `true` for
652
                // `isCollapsed` for a Selection that comes from a ShadowRoot.
653
                // (2020/08/08)
654
                // https://bugs.chromium.org/p/chromium/issues/detail?id=447523
655
                if (IS_CHROME && hasShadowRoot()) {
3!
656
                    isCollapsed = domRange.anchorNode === domRange.focusNode && domRange.anchorOffset === domRange.focusOffset;
×
657
                } else {
658
                    isCollapsed = domRange.isCollapsed;
3✔
659
                }
660
            } else {
661
                anchorNode = domRange.startContainer;
×
662
                anchorOffset = domRange.startOffset;
×
663
                focusNode = domRange.endContainer;
×
664
                focusOffset = domRange.endOffset;
×
665
                isCollapsed = domRange.collapsed;
×
666
            }
667
        }
668

669
        if (anchorNode == null || focusNode == null || anchorOffset == null || focusOffset == null) {
3!
670
            throw new Error(`Cannot resolve a Slate range from DOM range: ${domRange}`);
×
671
        }
672

673
        const anchor = AngularEditor.toSlatePoint(editor, [anchorNode, anchorOffset]);
3✔
674
        const focus = isCollapsed ? anchor : AngularEditor.toSlatePoint(editor, [focusNode, focusOffset]);
3!
675

676
        return { anchor, focus };
3✔
677
    },
678

679
    isLeafBlock(editor: AngularEditor, node: Node): boolean {
680
        return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
151✔
681
    },
682

683
    isBlockCardLeftCursor(editor: AngularEditor) {
684
        return (
×
685
            editor.selection.anchor.offset === FAKE_LEFT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_LEFT_BLOCK_CARD_OFFSET
×
686
        );
687
    },
688

689
    isBlockCardRightCursor(editor: AngularEditor) {
690
        return (
×
691
            editor.selection.anchor.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET &&
×
692
            editor.selection.focus.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET
693
        );
694
    },
695

696
    getCardCursorNode(
697
        editor: AngularEditor,
698
        blockCardNode: Node,
699
        options: {
700
            direction: 'left' | 'right' | 'center';
701
        }
702
    ) {
703
        const blockCardElement = AngularEditor.toDOMNode(editor, blockCardNode);
×
704
        const cardCenter = blockCardElement.parentElement;
×
705
        return options.direction === 'left' ? cardCenter.previousElementSibling.firstChild : cardCenter.nextElementSibling.firstChild;
×
706
    },
707

708
    toSlateCardEntry(editor: AngularEditor, node: DOMNode): NodeEntry {
709
        const element = node.parentElement.closest('.slate-block-card')?.querySelector('[card-target="card-center"]').firstElementChild;
×
710
        const slateNode = AngularEditor.toSlateNode(editor, element);
×
711
        const path = AngularEditor.findPath(editor, slateNode);
×
712
        return [slateNode, path];
×
713
    },
714

715
    /**
716
     * move native selection to card-left or card-right
717
     * @param editor
718
     * @param blockCardNode
719
     * @param options
720
     */
721
    moveBlockCard(
722
        editor: AngularEditor,
723
        blockCardNode: Node,
724
        options: {
725
            direction: 'left' | 'right';
726
        }
727
    ) {
728
        const cursorNode = AngularEditor.getCardCursorNode(editor, blockCardNode, options);
×
729
        const window = AngularEditor.getWindow(editor);
×
730
        const domSelection = window.getSelection();
×
731
        domSelection.setBaseAndExtent(cursorNode, 1, cursorNode, 1);
×
732
    },
733

734
    /**
735
     * move slate selection to card-left or card-right
736
     * @param editor
737
     * @param path
738
     * @param options
739
     */
740
    moveBlockCardCursor(
741
        editor: AngularEditor,
742
        path: Path,
743
        options: {
744
            direction: 'left' | 'right';
745
        }
746
    ) {
747
        const cursor = {
×
748
            path,
749
            offset: options.direction === 'left' ? FAKE_LEFT_BLOCK_CARD_OFFSET : FAKE_RIGHT_BLOCK_CARD_OFFSET
×
750
        };
751
        Transforms.select(editor, { anchor: cursor, focus: cursor });
×
752
    },
753

754
    hasRange(editor: AngularEditor, range: Range): boolean {
755
        const { anchor, focus } = range;
1✔
756
        return Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path);
1✔
757
    }
758
};
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