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

worktile / slate-angular / 03df4984-e68d-4684-8b25-503f7b9d7d4c

17 Jun 2024 07:12AM UTC coverage: 46.879% (+0.1%) from 46.754%
03df4984-e68d-4684-8b25-503f7b9d7d4c

push

circleci

web-flow
feat(core): improve block-card cursor (#273)

407 of 1075 branches covered (37.86%)

Branch coverage included in aggregate %.

4 of 19 new or added lines in 1 file covered. (21.05%)

1 existing line in 1 file now uncovered.

1020 of 1969 relevant lines covered (51.8%)

44.44 hits per line

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

47.78
/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
import { ClipboardData, OriginEvent } from '../types/clipboard';
40

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

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

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

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

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

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

84
        return key;
808✔
85
    },
86

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

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

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

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

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

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

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

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

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

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

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

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

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

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

148
            child = parent;
6✔
149
        }
150

151
        return false;
×
152
    },
153

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

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

165
        return el.ownerDocument;
×
166
    },
167

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

292
    insertFragmentData(editor: AngularEditor, data: DataTransfer): Promise<boolean> {
293
        return editor.insertFragmentData(data);
×
294
    },
295

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

300
    insertTextData(editor: AngularEditor, data: DataTransfer): Promise<boolean> {
301
        return editor.insertTextData(data);
×
302
    },
303

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

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

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

322
    setFragmentData(editor: AngularEditor, data: DataTransfer, originEvent?: OriginEvent): void {
323
        editor.setFragmentData(data, originEvent);
×
324
    },
325

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

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

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

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

341
        return domNode;
39✔
342
    },
343

344
    /**
345
     * Find a native DOM selection point from a Slate point.
346
     */
347
    toDOMPoint(editor: AngularEditor, point: Point, options: { range: Range }): DOMPoint {
348
        const [node] = Editor.node(editor, point.path);
3✔
349
        const el = AngularEditor.toDOMNode(editor, node);
3✔
350
        let domPoint: DOMPoint | undefined;
351
        // block card
352
        const [parentNode] = Editor.parent(editor, point.path);
3✔
353
        if (editor.isBlockCard(parentNode) || editor.isBlockCard(node)) {
3!
NEW
354
            if (point.offset < 0) {
×
NEW
355
                if (point.offset === FAKE_LEFT_BLOCK_CARD_OFFSET) {
×
NEW
356
                    const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'left' });
×
NEW
357
                    return [cursorNode, 1];
×
358
                } else {
NEW
359
                    const cursorNode = AngularEditor.getCardCursorNode(editor, node, { direction: 'right' });
×
NEW
360
                    return [cursorNode, 1];
×
361
                }
362
            }
NEW
363
            if (Range.isExpanded(options.range)) {
×
NEW
364
                const [start, end] = Range.edges(options.range);
×
NEW
365
                if (start === point) {
×
NEW
366
                    const cursorNode = AngularEditor.getCardCursorNode(editor, parentNode, { direction: 'left' });
×
NEW
367
                    return [cursorNode, 1];
×
368
                } else {
NEW
369
                    const cursorNode = AngularEditor.getCardCursorNode(editor, parentNode, { direction: 'right' });
×
NEW
370
                    return [cursorNode, 1];
×
371
                }
372
            }
373
        }
374

375
        // If we're inside a void node, force the offset to 0, otherwise the zero
376
        // width spacing character will result in an incorrect offset of 1
377
        if (Editor.void(editor, { at: point })) {
3!
378
            point = { path: point.path, offset: 0 };
×
379
        }
380

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

388
        for (const text of texts) {
3✔
389
            const domNode = text.childNodes[0] as HTMLElement;
3✔
390

391
            if (domNode == null || domNode.textContent == null) {
3!
392
                continue;
×
393
            }
394

395
            const { length } = domNode.textContent;
3✔
396
            const attr = text.getAttribute('data-slate-length');
3✔
397
            const trueLength = attr == null ? length : parseInt(attr, 10);
3✔
398
            const end = start + trueLength;
3✔
399

400
            if (point.offset <= end) {
3✔
401
                const offset = Math.min(length, Math.max(0, point.offset - start));
3✔
402
                domPoint = [domNode, offset];
3✔
403
                // fixed cursor position after zero width char
404
                if (offset === 0 && length === 1 && domNode.textContent === '\uFEFF') {
3✔
405
                    domPoint = [domNode, offset + 1];
1✔
406
                }
407
                break;
3✔
408
            }
409

410
            start = end;
×
411
        }
412

413
        if (!domPoint) {
3!
414
            throw new Error(`Cannot resolve a DOM point from Slate point: ${JSON.stringify(point)}`);
×
415
        }
416

417
        return domPoint;
3✔
418
    },
419

420
    /**
421
     * Find a native DOM range from a Slate `range`.
422
     */
423

424
    toDOMRange(editor: AngularEditor, range: Range): DOMRange {
425
        const { anchor, focus } = range;
3✔
426
        const isBackward = Range.isBackward(range);
3✔
427
        const domAnchor = AngularEditor.toDOMPoint(editor, anchor, { range });
3✔
428
        const domFocus = Range.isCollapsed(range) ? domAnchor : AngularEditor.toDOMPoint(editor, focus, { range });
3!
429

430
        const window = AngularEditor.getWindow(editor);
3✔
431
        const domRange = window.document.createRange();
3✔
432
        const [startNode, startOffset] = isBackward ? domFocus : domAnchor;
3!
433
        const [endNode, endOffset] = isBackward ? domAnchor : domFocus;
3!
434

435
        // A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
436
        // zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
437
        // adjust the offset accordingly.
438
        const startEl = (isDOMElement(startNode) ? startNode : startNode.parentElement) as HTMLElement;
3!
439
        const isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
3✔
440
        const endEl = (isDOMElement(endNode) ? endNode : endNode.parentElement) as HTMLElement;
3!
441
        const isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');
3✔
442

443
        domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset);
3✔
444
        domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset);
3✔
445
        return domRange;
3✔
446
    },
447

448
    /**
449
     * Find a Slate node from a native DOM `element`.
450
     */
451

452
    toSlateNode<T extends boolean>(
453
        editor: AngularEditor,
454
        domNode: DOMNode,
455
        options?: {
456
            suppressThrow: T;
457
        }
458
    ): T extends true ? Node | null : Node {
459
        const { suppressThrow } = options || { suppressThrow: false };
7!
460
        let domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;
7!
461

462
        if (domEl && !domEl.hasAttribute('data-slate-node')) {
7!
463
            domEl = domEl.closest(`[data-slate-node]`);
×
464
        }
465

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

468
        if (!node) {
7!
469
            if (suppressThrow) {
×
470
                return null as T extends true ? Node | null : Node;
×
471
            }
472
            throw new Error(`Cannot resolve a Slate node from DOM node: ${domEl}`);
×
473
        }
474

475
        return node;
7✔
476
    },
477

478
    /**
479
     * Get the target range from a DOM `event`.
480
     */
481

482
    findEventRange(editor: AngularEditor, event: any): Range {
483
        if ('nativeEvent' in event) {
×
484
            event = event.nativeEvent;
×
485
        }
486

487
        const { clientX: x, clientY: y, target } = event;
×
488

489
        if (x == null || y == null) {
×
490
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
491
        }
492

493
        const node = AngularEditor.toSlateNode(editor, event.target, { suppressThrow: false });
×
494
        const path = AngularEditor.findPath(editor, node);
×
495

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

503
            const edge = Editor.point(editor, path, {
×
504
                edge: isPrev ? 'start' : 'end'
×
505
            });
506
            const point = isPrev ? Editor.before(editor, edge) : Editor.after(editor, edge);
×
507

508
            if (point) {
×
509
                return Editor.range(editor, point);
×
510
            }
511
        }
512

513
        // Else resolve a range from the caret position where the drop occured.
514
        let domRange: DOMRange;
515
        const window = AngularEditor.getWindow(editor);
×
516
        const { document } = window;
×
517

518
        // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
519
        if (document.caretRangeFromPoint) {
×
520
            domRange = document.caretRangeFromPoint(x, y);
×
521
        } else {
522
            const position = (document as SafeAny).caretPositionFromPoint(x, y);
×
523

524
            if (position) {
×
525
                domRange = document.createRange();
×
526
                domRange.setStart(position.offsetNode, position.offset);
×
527
                domRange.setEnd(position.offsetNode, position.offset);
×
528
            }
529
        }
530

531
        if (!domRange) {
×
532
            throw new Error(`Cannot resolve a Slate range from a DOM event: ${event}`);
×
533
        }
534

535
        // Resolve a Slate range from the DOM range.
536
        const range = AngularEditor.toSlateRange(editor, domRange, { suppressThrow: false });
×
537
        return range;
×
538
    },
539

540
    isLeafInEditor(
541
        editor: AngularEditor,
542
        leafNode: DOMElement,
543
        options: {
544
            suppressThrow: boolean;
545
        }
546
    ): boolean {
547
        const { suppressThrow } = options;
3✔
548
        const textNode = leafNode.closest('[data-slate-node="text"]')!;
3✔
549
        const node = AngularEditor.toSlateNode(editor, textNode, { suppressThrow });
3✔
550
        if (node && AngularEditor.isNodeInEditor(editor, node)) {
3!
551
            return true;
3✔
552
        } else {
553
            return false;
×
554
        }
555
    },
556

557
    /**
558
     * Find a Slate point from a DOM selection's `domNode` and `domOffset`.
559
     */
560

561
    toSlatePoint<T extends boolean>(
562
        editor: AngularEditor,
563
        domPoint: DOMPoint,
564
        options: {
565
            exactMatch?: boolean;
566
            suppressThrow: T;
567
        }
568
    ): T extends true ? Point | null : Point {
569
        const { exactMatch, suppressThrow } = options;
3✔
570
        const [domNode] = domPoint;
3✔
571
        const [nearestNode, nearestOffset] = normalizeDOMPoint(domPoint);
3✔
572
        let parentNode = nearestNode.parentNode as DOMElement;
3✔
573
        let textNode: DOMElement | null = null;
3✔
574
        let offset = 0;
3✔
575

576
        // block card
577
        const cardTargetAttr = getCardTargetAttribute(domNode);
3✔
578
        if (cardTargetAttr) {
3!
579
            const domSelection = window.getSelection();
×
580
            const isBackward = editor.selection && Range.isBackward(editor.selection);
×
581
            const blockCardEntry = AngularEditor.toSlateCardEntry(editor, domNode) || AngularEditor.toSlateCardEntry(editor, nearestNode);
×
582
            const [, blockPath] = blockCardEntry;
×
583
            if (domSelection.isCollapsed) {
×
584
                if (isCardLeftByTargetAttr(cardTargetAttr)) {
×
585
                    return { path: blockPath, offset: -1 };
×
586
                } else {
587
                    return { path: blockPath, offset: -2 };
×
588
                }
589
            }
NEW
590
            if (isCardLeftByTargetAttr(cardTargetAttr)) {
×
UNCOV
591
                return Editor.start(editor, blockPath);
×
592
            } else {
NEW
593
                return Editor.end(editor, blockPath);
×
594
            }
595
        }
596

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

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

616
                removals.forEach(el => {
3✔
617
                    el!.parentNode!.removeChild(el);
×
618
                });
619

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

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

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

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

666
    /**
667
     * Find a Slate range from a DOM range or selection.
668
     */
669

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

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

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

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

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

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

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

750
        return range;
3✔
751
    },
752

753
    isLeafBlock(editor: AngularEditor, node: Node): boolean {
754
        return Element.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
159✔
755
    },
756

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

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

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

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

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

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

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