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

worktile / slate-angular / 53a13102-d226-41e7-81aa-acf941488ffb

11 Apr 2024 06:13AM UTC coverage: 48.503% (+0.04%) from 48.468%
53a13102-d226-41e7-81aa-acf941488ffb

push

circleci

pubuzhixing8
fix(list-render): fix the issue that inline void element can not bind contentEditable attribute correctly

406 of 1040 branches covered (39.04%)

Branch coverage included in aggregate %.

13 of 13 new or added lines in 3 files covered. (100.0%)

1 existing line in 1 file now uncovered.

1003 of 1865 relevant lines covered (53.78%)

46.94 hits per line

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

86.13
/packages/src/view/render/list-render.ts
1
import { Ancestor, Descendant, Range, Editor, Element, Path } from 'slate';
2
import { ComponentRef, EmbeddedViewRef, IterableDiffer, IterableDiffers, ViewContainerRef } from '@angular/core';
3
import { ViewType } from '../../types/view';
4
import { SlateChildrenContext, SlateElementContext, SlateTextContext, SlateViewContext } from '../context';
5
import { AngularEditor } from '../../plugins/angular-editor';
6
import { SlateErrorCode } from '../../types/error';
7
import { EDITOR_TO_AFTER_VIEW_INIT_QUEUE, NODE_TO_INDEX, NODE_TO_PARENT } from '../../utils/weak-maps';
8
import { isDecoratorRangeListEqual } from '../../utils/range-list';
9
import { SlateBlockCard } from '../../components/block-card/block-card.component';
10
import { createEmbeddedViewOrComponent, getRootNodes, mount, mountOnItemChange, updateContext } from './utils';
11

12
export class ListRender {
13
    private children: Descendant[];
14
    private views: (EmbeddedViewRef<any> | ComponentRef<any>)[] = [];
218✔
15
    // private addedViews: (EmbeddedViewRef<any> | ComponentRef<any>)[] = [];
16
    private blockCards: (ComponentRef<SlateBlockCard> | null)[] = [];
218✔
17
    private contexts: (SlateTextContext | SlateElementContext)[] = [];
218✔
18
    private viewTypes: ViewType[] = [];
218✔
19
    private differ: IterableDiffer<any> | null = null;
218✔
20
    public initialized = false;
218✔
21

22
    constructor(
23
        private viewContext: SlateViewContext,
218✔
24
        private viewContainerRef: ViewContainerRef,
218✔
25
        private getOutletParent: () => HTMLElement,
218✔
26
        private getOutletElement: () => HTMLElement | null
218✔
27
    ) {}
28

29
    public initialize(children: Descendant[], parent: Ancestor, childrenContext: SlateChildrenContext) {
30
        this.initialized = true;
218✔
31
        this.children = children;
218✔
32
        const parentPath = AngularEditor.findPath(this.viewContext.editor, parent);
218✔
33
        children.forEach((descendant, index) => {
218✔
34
            NODE_TO_INDEX.set(descendant, index);
338✔
35
            NODE_TO_PARENT.set(descendant, parent);
338✔
36
            const context = getContext(index, descendant, parentPath, childrenContext, this.viewContext);
338✔
37
            const viewType = getViewType(descendant, parent, this.viewContext);
338✔
38
            const view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
338✔
39
            const blockCard = createBlockCard(descendant, view, this.viewContainerRef, this.viewContext);
338✔
40
            this.views.push(view);
338✔
41
            this.contexts.push(context);
338✔
42
            this.viewTypes.push(viewType);
338✔
43
            this.blockCards.push(blockCard);
338✔
44
        });
45
        mount(this.views, this.blockCards, this.getOutletParent(), this.getOutletElement());
218✔
46
        const newDiffers = this.viewContainerRef.injector.get(IterableDiffers);
218✔
47
        this.differ = newDiffers.find(children).create(trackBy(this.viewContext));
218✔
48
        this.differ.diff(children);
218✔
49
        if (parent === this.viewContext.editor) {
218✔
50
            executeAfterViewInit(this.viewContext.editor);
23✔
51
        }
52
    }
53

54
    public update(children: Descendant[], parent: Ancestor, childrenContext: SlateChildrenContext) {
55
        if (!this.initialized || this.children.length === 0) {
40!
56
            this.initialize(children, parent, childrenContext);
×
57
            return;
×
58
        }
59
        if (!this.differ) {
40!
60
            throw new Error('Exception: Can not find differ ');
×
61
        }
62
        const outletParent = this.getOutletParent();
40✔
63
        const diffResult = this.differ.diff(children);
40✔
64
        const parentPath = AngularEditor.findPath(this.viewContext.editor, parent);
40✔
65
        if (diffResult) {
40✔
66
            let firstRootNode = getRootNodes(this.views[0], this.blockCards[0])[0];
28✔
67
            const newContexts = [];
28✔
68
            const newViewTypes = [];
28✔
69
            const newViews = [];
28✔
70
            const newBlockCards: (ComponentRef<SlateBlockCard> | null)[] = [];
28✔
71
            diffResult.forEachItem(record => {
28✔
72
                NODE_TO_INDEX.set(record.item, record.currentIndex);
68✔
73
                NODE_TO_PARENT.set(record.item, parent);
68✔
74
                let context = getContext(record.currentIndex, record.item, parentPath, childrenContext, this.viewContext);
68✔
75
                const viewType = getViewType(record.item, parent, this.viewContext);
68✔
76
                newViewTypes.push(viewType);
68✔
77
                let view: EmbeddedViewRef<any> | ComponentRef<any>;
78
                let blockCard: ComponentRef<SlateBlockCard> | null;
79
                if (record.previousIndex === null) {
68✔
80
                    view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
7✔
81
                    blockCard = createBlockCard(record.item, view, this.viewContainerRef, this.viewContext);
7✔
82
                    newContexts.push(context);
7✔
83
                    newViews.push(view);
7✔
84
                    newBlockCards.push(blockCard);
7✔
85
                    mountOnItemChange(
7✔
86
                        record.currentIndex,
87
                        record.item,
88
                        newViews,
89
                        newBlockCards,
90
                        outletParent,
91
                        firstRootNode,
92
                        this.viewContext
93
                    );
94
                } else {
95
                    const previousView = this.views[record.previousIndex];
61✔
96
                    const previousViewType = this.viewTypes[record.previousIndex];
61✔
97
                    const previousContext = this.contexts[record.previousIndex];
61✔
98
                    const previousBlockCard = this.blockCards[record.previousIndex];
61✔
99
                    if (previousViewType !== viewType) {
61!
100
                        view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
×
101
                        blockCard = createBlockCard(record.item, view, this.viewContainerRef, this.viewContext);
×
102
                        const firstRootNode = getRootNodes(previousView, previousBlockCard)[0];
×
103
                        const newRootNodes = getRootNodes(view, blockCard);
×
104
                        firstRootNode.replaceWith(...newRootNodes);
×
105
                        previousView.destroy();
×
106
                        previousBlockCard?.destroy();
×
107
                    } else {
108
                        view = previousView;
61✔
109
                        blockCard = previousBlockCard;
61✔
110
                        if (memoizedContext(this.viewContext, record.item, previousContext as any, context as any)) {
61✔
111
                            context = previousContext;
44✔
112
                        } else {
113
                            updateContext(previousView, context, this.viewContext);
17✔
114
                        }
115
                    }
116
                    newContexts.push(context);
61✔
117
                    newViews.push(view);
61✔
118
                    newBlockCards.push(blockCard);
61✔
119
                }
120
            });
121
            diffResult.forEachOperation(record => {
28✔
122
                // removed
123
                if (record.currentIndex === null) {
19✔
124
                    const view = this.views[record.previousIndex];
5✔
125
                    const blockCard = this.blockCards[record.previousIndex];
5✔
126
                    view.destroy();
5✔
127
                    blockCard?.destroy();
5✔
128
                }
129
                // moved
130
                if (record.previousIndex !== null && record.currentIndex !== null) {
19✔
131
                    mountOnItemChange(
7✔
132
                        record.currentIndex,
133
                        record.item,
134
                        newViews,
135
                        newBlockCards,
136
                        outletParent,
137
                        firstRootNode,
138
                        this.viewContext
139
                    );
140
                    // Solve the block-card DOMElement loss when moving nodes
141
                    newBlockCards[record.currentIndex]?.instance.append();
7✔
142
                }
143
            });
144
            this.viewTypes = newViewTypes;
28✔
145
            this.views = newViews;
28✔
146
            this.contexts = newContexts;
28✔
147
            this.children = children;
28✔
148
            this.blockCards = newBlockCards;
28✔
149
            if (parent === this.viewContext.editor) {
28✔
150
                executeAfterViewInit(this.viewContext.editor);
14✔
151
            }
152
        } else {
153
            const newContexts = [];
12✔
154
            this.children.forEach((child, index) => {
12✔
155
                NODE_TO_INDEX.set(child, index);
61✔
156
                NODE_TO_PARENT.set(child, parent);
61✔
157
                let context = getContext(index, child, parentPath, childrenContext, this.viewContext);
61✔
158
                const previousContext = this.contexts[index];
61✔
159
                if (memoizedContext(this.viewContext, child, previousContext as any, context as any)) {
61✔
160
                    context = previousContext;
52✔
161
                } else {
162
                    updateContext(this.views[index], context, this.viewContext);
9✔
163
                }
164
                newContexts.push(context);
61✔
165
            });
166
            this.contexts = newContexts;
12✔
167
        }
168
    }
169

170
    public destroy() {
UNCOV
171
        this.children.forEach((element: Element, index: number) => {
×
172
            if (this.views[index]) {
×
173
                this.views[index].destroy();
×
174
            }
175
            if (this.blockCards[index]) {
×
176
                this.blockCards[index].destroy();
×
177
            }
178
        });
179
        this.views = [];
×
180
        this.blockCards = [];
×
181
        this.contexts = [];
×
182
        this.viewTypes = [];
×
183
        this.initialized = false;
×
184
        this.differ = null;
×
185
    }
186
}
187

188
export function getContext(
189
    index: number,
190
    item: Descendant,
191
    parentPath: Path,
192
    childrenContext: SlateChildrenContext,
193
    viewContext: SlateViewContext
194
): SlateElementContext | SlateTextContext {
195
    if (Element.isElement(item)) {
467✔
196
        const computedContext = getCommonContext(index, item, parentPath, viewContext, childrenContext);
308✔
197
        const key = AngularEditor.findKey(viewContext.editor, item);
308✔
198
        const isInline = viewContext.editor.isInline(item);
308✔
199
        const isVoid = viewContext.editor.isVoid(item);
308✔
200
        const elementContext: SlateElementContext = {
308✔
201
            element: item,
202
            ...computedContext,
203
            attributes: {
204
                'data-slate-node': 'element',
205
                'data-slate-key': key.id
206
            },
207
            decorate: childrenContext.decorate,
208
            readonly: childrenContext.readonly
209
        };
210
        if (isInline) {
308!
211
            elementContext.attributes['data-slate-inline'] = true;
×
212
        }
213
        if (isVoid) {
308✔
214
            elementContext.attributes['data-slate-void'] = true;
1✔
215
            elementContext.contentEditable = false;
1✔
216
        }
217
        return elementContext;
308✔
218
    } else {
219
        const computedContext = getCommonContext(index, item, parentPath, viewContext, childrenContext);
159✔
220
        const isLeafBlock = AngularEditor.isLeafBlock(viewContext.editor, childrenContext.parent);
159✔
221
        const textContext: SlateTextContext = {
159✔
222
            decorations: computedContext.decorations,
223
            isLast: isLeafBlock && index === childrenContext.parent.children.length - 1,
318✔
224
            parent: childrenContext.parent as Element,
225
            text: item
226
        };
227
        return textContext;
159✔
228
    }
229
}
230

231
export function getCommonContext(
232
    index: number,
233
    item: Descendant,
234
    parentPath: Path,
235
    viewContext: SlateViewContext,
236
    childrenContext: SlateChildrenContext
237
): { selection: Range; decorations: Range[] } {
238
    const p = parentPath.concat(index);
467✔
239
    try {
467✔
240
        const ds = childrenContext.decorate([item, p]);
467✔
241
        // [list-render] performance optimization: reduce the number of calls to the `Editor.range(viewContext.editor, p)` method
242
        if (childrenContext.selection || childrenContext.decorations.length > 0) {
467✔
243
            const range = Editor.range(viewContext.editor, p);
73✔
244
            const sel = childrenContext.selection && Range.intersection(range, childrenContext.selection);
73✔
245
            for (const dec of childrenContext.decorations) {
73✔
246
                const d = Range.intersection(dec, range);
6✔
247
                if (d) {
6✔
248
                    ds.push(d);
6✔
249
                }
250
            }
251
            return { selection: sel, decorations: ds };
73✔
252
        } else {
253
            return { selection: null, decorations: ds };
394✔
254
        }
255
    } catch (error) {
256
        viewContext.editor.onError({
×
257
            code: SlateErrorCode.GetStartPointError,
258
            nativeError: error
259
        });
260
        return { selection: null, decorations: [] };
×
261
    }
262
}
263

264
export function getViewType(item: Descendant, parent: Ancestor, viewContext: SlateViewContext) {
265
    if (Element.isElement(item)) {
406✔
266
        return (viewContext.renderElement && viewContext.renderElement(item)) || viewContext.defaultElement;
253✔
267
    } else {
268
        const isVoid = viewContext.editor.isVoid(parent as Element);
153✔
269
        return isVoid ? viewContext.defaultVoidText : (viewContext.renderText && viewContext.renderText(item)) || viewContext.defaultText;
153!
270
    }
271
}
272

273
export function createBlockCard(
274
    item: Descendant,
275
    view: EmbeddedViewRef<any> | ComponentRef<any>,
276
    viewContainerRef: ViewContainerRef,
277
    viewContext: SlateViewContext
278
) {
279
    const isBlockCard = viewContext.editor.isBlockCard(item);
345✔
280
    if (isBlockCard) {
345✔
281
        const rootNodes = getRootNodes(view);
1✔
282
        const blockCardComponentRef = viewContainerRef.createComponent<SlateBlockCard>(SlateBlockCard, {
1✔
283
            injector: viewContainerRef.injector
284
        });
285
        blockCardComponentRef.instance.initializeCenter(rootNodes);
1✔
286
        blockCardComponentRef.changeDetectorRef.detectChanges();
1✔
287
        return blockCardComponentRef;
1✔
288
    } else {
289
        return null;
344✔
290
    }
291
}
292

293
export function trackBy(viewContext: SlateViewContext) {
294
    return (index, node) => {
218✔
295
        return viewContext.trackBy(node) || AngularEditor.findKey(viewContext.editor, node);
467✔
296
    };
297
}
298

299
export function memoizedContext(
300
    viewContext: SlateViewContext,
301
    descendant: Descendant,
302
    prev: SlateElementContext | SlateTextContext,
303
    next: SlateElementContext | SlateTextContext
304
): boolean {
305
    if (Element.isElement(descendant)) {
122✔
306
        return memoizedElementContext(viewContext, prev as SlateElementContext, next as SlateElementContext);
113✔
307
    } else {
308
        return memoizedTextContext(prev as SlateTextContext, next as SlateTextContext);
9✔
309
    }
310
}
311

312
export function memoizedElementContext(viewContext: SlateViewContext, prev: SlateElementContext, next: SlateElementContext) {
313
    return (
113✔
314
        prev.element === next.element &&
602!
315
        (!viewContext.isStrictDecorate || prev.decorate === next.decorate) &&
316
        prev.readonly === next.readonly &&
317
        isDecoratorRangeListEqual(prev.decorations, next.decorations) &&
318
        (prev.selection === next.selection || (!!prev.selection && !!next.selection && Range.equals(prev.selection, next.selection)))
319
    );
320
}
321

322
export function memoizedTextContext(prev: SlateTextContext, next: SlateTextContext) {
323
    return (
9✔
324
        next.parent === prev.parent &&
27✔
325
        next.isLast === prev.isLast &&
326
        next.text === prev.text &&
327
        isDecoratorRangeListEqual(next.decorations, prev.decorations)
328
    );
329
}
330

331
export function addAfterViewInitQueue(editor: Editor, afterViewInitCallback: () => void) {
332
    const queue = getAfterViewInitQueue(editor);
195✔
333
    queue.push(afterViewInitCallback);
195✔
334
    EDITOR_TO_AFTER_VIEW_INIT_QUEUE.set(editor, queue);
195✔
335
}
336

337
export function getAfterViewInitQueue(editor: Editor) {
338
    return EDITOR_TO_AFTER_VIEW_INIT_QUEUE.get(editor) || [];
232✔
339
}
340

341
export function clearAfterViewInitQueue(editor: Editor) {
342
    EDITOR_TO_AFTER_VIEW_INIT_QUEUE.set(editor, []);
37✔
343
}
344

345
export function executeAfterViewInit(editor: Editor) {
346
    const queue = getAfterViewInitQueue(editor);
37✔
347
    queue.forEach(callback => callback());
195✔
348
    clearAfterViewInitQueue(editor);
37✔
349
}
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