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

worktile / slate-angular / bbaac697-0225-46fd-ad72-9e4ed6d090b9

01 Mar 2024 06:34AM UTC coverage: 28.702% (+0.1%) from 28.587%
bbaac697-0225-46fd-ad72-9e4ed6d090b9

push

circleci

pubuzhixing8
build: release 16.1.0-next.11

186 of 1012 branches covered (18.38%)

Branch coverage included in aggregate %.

630 of 1831 relevant lines covered (34.41%)

3.02 hits per line

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

9.38
/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);
3✔
66
        if (!window) {
3!
67
            throw new Error('Unable to find a host window element for this editor');
×
68
        }
69
        return window;
3✔
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);
50✔
77

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

83
        return key;
50✔
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 = [];
17✔
102
        let child = node;
17✔
103

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

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

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

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

121
            path.unshift(i);
15✔
122
            child = parent;
15✔
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;
×
129

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

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

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

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

147
            child = parent;
×
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);
2✔
159
        const root = el.getRootNode();
2✔
160
        if ((root instanceof Document || root instanceof ShadowRoot) && (root as Document).getSelection != null) {
2!
161
            return root;
2✔
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);
2✔
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);
×
222
        IS_FOCUSED.set(editor, true);
×
223

224
        const window = AngularEditor.getWindow(editor);
×
225
        if (window.document.activeElement !== el) {
×
226
            el.focus({ preventScroll: true });
×
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 {
×
253
        const { editable = false } = options;
×
254
        const editorEl = AngularEditor.toDOMNode(editor, editor);
×
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 {
×
262
            targetEl = (isDOMElement(target) ? target : target.parentElement) as HTMLElement;
×
263
        } catch (err) {
264
            if (!err.message.includes('Permission denied to access property "nodeType"')) {
×
265
                throw err;
×
266
            }
267
        }
268

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

273
        return (
×
274
            targetEl.closest(`[data-slate-editor]`) === editorEl &&
×
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);
5✔
335

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

340
        return domNode;
5✔
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);
×
349
        const el = AngularEditor.toDOMNode(editor, node);
×
350
        let domPoint: DOMPoint | undefined;
351

352
        // block card
353
        const cardTargetAttr = getCardTargetAttribute(el);
×
354
        if (cardTargetAttr) {
×
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 })) {
×
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]`;
×
374
        const texts = Array.from(el.querySelectorAll(selector));
×
375
        let start = 0;
×
376

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

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

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

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

399
            start = end;
×
400
        }
401

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

406
        return domPoint;
×
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;
×
415
        const isBackward = Range.isBackward(range);
×
416
        const domAnchor = AngularEditor.toDOMPoint(editor, anchor);
×
417
        const domFocus = Range.isCollapsed(range) ? domAnchor : AngularEditor.toDOMPoint(editor, focus);
×
418

419
        const window = AngularEditor.getWindow(editor);
×
420
        const domRange = window.document.createRange();
×
421
        const [startNode, startOffset] = isBackward ? domFocus : domAnchor;
×
422
        const [endNode, endOffset] = isBackward ? domAnchor : domFocus;
×
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;
×
428
        const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
×
429
        const endEl = (isDOMElement(endNode) ? endNode : endNode.parentElement) as HTMLElement;
×
430
        const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');
×
431

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

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

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

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

448
        const node = domEl ? ELEMENT_TO_NODE.get(domEl as HTMLElement) : null;
×
449

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

454
        return node;
×
455
    },
456

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

674
        // COMPAT: Triple-clicking a word in chrome will sometimes place the focus
675
        // inside a `contenteditable="false"` DOM node following the word, which
676
        // will cause `toSlatePoint` to throw an error. (2023/03/07)
677
        if (
×
678
            'getAttribute' in focusNode &&
×
679
            (focusNode as HTMLElement).getAttribute('contenteditable') === 'false' &&
680
            (focusNode as HTMLElement).getAttribute('data-slate-void') !== 'true'
681
        ) {
682
            focusNode = anchorNode;
×
683
            focusOffset = anchorNode.textContent?.length || 0;
×
684
        }
685

686
        const anchor = AngularEditor.toSlatePoint(editor, [anchorNode, anchorOffset]);
×
687
        const focus = isCollapsed ? anchor : AngularEditor.toSlatePoint(editor, [focusNode, focusOffset]);
×
688

689
        let range: Range = { anchor: anchor as Point, focus: focus as Point };
×
690
        // if the selection is a hanging range that ends in a void
691
        // and the DOM focus is an Element
692
        // (meaning that the selection ends before the element)
693
        // unhang the range to avoid mistakenly including the void
694
        if (
×
695
            Range.isExpanded(range) &&
×
696
            Range.isForward(range) &&
697
            isDOMElement(focusNode) &&
698
            Editor.void(editor, { at: range.focus, mode: 'highest' })
699
        ) {
700
            range = Editor.unhangRange(editor, range, { voids: true });
×
701
        }
702

703
        return range;
×
704
    },
705

706
    isLeafBlock(editor: AngularEditor, node: Node): boolean {
707
        return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
9✔
708
    },
709

710
    isBlockCardLeftCursor(editor: AngularEditor) {
711
        return (
×
712
            editor.selection.anchor.offset === FAKE_LEFT_BLOCK_CARD_OFFSET && editor.selection.focus.offset === FAKE_LEFT_BLOCK_CARD_OFFSET
×
713
        );
714
    },
715

716
    isBlockCardRightCursor(editor: AngularEditor) {
717
        return (
×
718
            editor.selection.anchor.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET &&
×
719
            editor.selection.focus.offset === FAKE_RIGHT_BLOCK_CARD_OFFSET
720
        );
721
    },
722

723
    getCardCursorNode(
724
        editor: AngularEditor,
725
        blockCardNode: Node,
726
        options: {
727
            direction: 'left' | 'right' | 'center';
728
        }
729
    ) {
730
        const blockCardElement = AngularEditor.toDOMNode(editor, blockCardNode);
×
731
        const cardCenter = blockCardElement.parentElement;
×
732
        return options.direction === 'left' ? cardCenter.previousElementSibling.firstChild : cardCenter.nextElementSibling.firstChild;
×
733
    },
734

735
    toSlateCardEntry(editor: AngularEditor, node: DOMNode): NodeEntry {
736
        const element = node.parentElement.closest('.slate-block-card')?.querySelector('[card-target="card-center"]').firstElementChild;
×
737
        const slateNode = AngularEditor.toSlateNode(editor, element);
×
738
        const path = AngularEditor.findPath(editor, slateNode);
×
739
        return [slateNode, path];
×
740
    },
741

742
    /**
743
     * move native selection to card-left or card-right
744
     * @param editor
745
     * @param blockCardNode
746
     * @param options
747
     */
748
    moveBlockCard(
749
        editor: AngularEditor,
750
        blockCardNode: Node,
751
        options: {
752
            direction: 'left' | 'right';
753
        }
754
    ) {
755
        const cursorNode = AngularEditor.getCardCursorNode(editor, blockCardNode, options);
×
756
        const window = AngularEditor.getWindow(editor);
×
757
        const domSelection = window.getSelection();
×
758
        domSelection.setBaseAndExtent(cursorNode, 1, cursorNode, 1);
×
759
    },
760

761
    /**
762
     * move slate selection to card-left or card-right
763
     * @param editor
764
     * @param path
765
     * @param options
766
     */
767
    moveBlockCardCursor(
768
        editor: AngularEditor,
769
        path: Path,
770
        options: {
771
            direction: 'left' | 'right';
772
        }
773
    ) {
774
        const cursor = {
×
775
            path,
776
            offset: options.direction === 'left' ? FAKE_LEFT_BLOCK_CARD_OFFSET : FAKE_RIGHT_BLOCK_CARD_OFFSET
×
777
        };
778
        Transforms.select(editor, { anchor: cursor, focus: cursor });
×
779
    },
780

781
    hasRange(editor: AngularEditor, range: Range): boolean {
782
        const { anchor, focus } = range;
×
783
        return Editor.hasPath(editor, anchor.path) && Editor.hasPath(editor, focus.path);
×
784
    }
785
};
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