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

worktile / slate-angular / 69fe0099-5d8b-4249-8c40-f33e6ce7f3d6

20 Nov 2023 07:25AM UTC coverage: 48.5% (+2.4%) from 46.148%
69fe0099-5d8b-4249-8c40-f33e6ce7f3d6

Pull #242

circleci

pubuzhixing8
chore: enter prerelease mode
Pull Request #242: List render

378 of 964 branches covered (0.0%)

Branch coverage included in aggregate %.

284 of 334 new or added lines in 12 files covered. (85.03%)

8 existing lines in 3 files now uncovered.

948 of 1770 relevant lines covered (53.56%)

39.84 hits per line

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

90.82
/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 { 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, renderView, updateContext } from './utils';
11

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

21
    constructor(
22
        private viewContext: SlateViewContext,
204✔
23
        private viewContainerRef: ViewContainerRef,
204✔
24
        private getOutletElement: () => HTMLElement
204✔
25
    ) {}
26

27
    public initialize(children: Descendant[], parent: Ancestor, parentPath: Path, childrenContext: SlateChildrenContext) {
28
        this.initialized = true;
204✔
29
        this.children = children;
204✔
30
        children.forEach((descendant, index) => {
204✔
31
            NODE_TO_INDEX.set(descendant, index);
319✔
32
            NODE_TO_PARENT.set(descendant, parent);
319✔
33
            const context = getContext(index, descendant, parentPath, childrenContext, this.viewContext);
319✔
34
            const viewType = getViewType(descendant, parent, this.viewContext);
319✔
35
            const view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
319✔
36
            const blockCard = createBlockCard(descendant, view, this.viewContainerRef, this.viewContext);
319✔
37
            this.views.push(view);
319✔
38
            this.contexts.push(context);
319✔
39
            this.viewTypes.push(viewType);
319✔
40
            this.blockCards.push(blockCard);
319✔
41
        });
42
        mount(this.views, this.blockCards, this.getOutletElement());
204✔
43
        const newDiffers = this.viewContainerRef.injector.get(IterableDiffers);
204✔
44
        this.differ = newDiffers.find(children).create(trackBy(this.viewContext));
204✔
45
        this.differ.diff(children);
204✔
46
    }
47

48
    public update(children: Descendant[], parent: Ancestor, parentPath: Path, childrenContext: SlateChildrenContext) {
49
        if (!this.initialized) {
40!
NEW
50
            this.initialize(children, parent, parentPath, childrenContext);
×
NEW
51
            return;
×
52
        }
53
        const outletElement = this.getOutletElement();
40✔
54
        const diffResult = this.differ.diff(children);
40✔
55
        if (diffResult) {
40✔
56
            const newContexts = [];
25✔
57
            const newViewTypes = [];
25✔
58
            const newViews = [];
25✔
59
            const newBlockCards: (ComponentRef<SlateBlockCard> | null)[] = [];
25✔
60
            diffResult.forEachItem(record => {
25✔
61
                NODE_TO_INDEX.set(record.item, record.currentIndex);
59✔
62
                NODE_TO_PARENT.set(record.item, parent);
59✔
63
                let context = getContext(record.currentIndex, record.item, parentPath, childrenContext, this.viewContext);
59✔
64
                const viewType = getViewType(record.item, parent, this.viewContext);
59✔
65
                newViewTypes.push(viewType);
59✔
66
                let view: EmbeddedViewRef<any> | ComponentRef<any>;
67
                let blockCard: ComponentRef<SlateBlockCard> | null;
68
                if (record.previousIndex === null) {
59✔
69
                    view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
6✔
70
                    blockCard = createBlockCard(record.item, view, this.viewContainerRef, this.viewContext);
6✔
71
                    newContexts.push(context);
6✔
72
                    newViews.push(view);
6✔
73
                    newBlockCards.push(blockCard);
6✔
74
                    mountOnItemChange(record.currentIndex, record.item, newViews, newBlockCards, outletElement, this.viewContext);
6✔
75
                } else {
76
                    const previousView = this.views[record.previousIndex];
53✔
77
                    const previousViewType = this.viewTypes[record.previousIndex];
53✔
78
                    const previousContext = this.contexts[record.previousIndex];
53✔
79
                    const previousBlockCard = this.blockCards[record.previousIndex];
53✔
80
                    if (previousViewType !== viewType) {
53!
NEW
81
                        view = createEmbeddedViewOrComponent(viewType, context, this.viewContext, this.viewContainerRef);
×
NEW
82
                        blockCard = createBlockCard(record.item, view, this.viewContainerRef, this.viewContext);
×
NEW
83
                        const firstRootNode = getRootNodes(previousView, previousBlockCard)[0];
×
NEW
84
                        const newRootNodes = getRootNodes(view, blockCard);
×
NEW
85
                        firstRootNode.replaceWith(...newRootNodes);
×
NEW
86
                        previousView.destroy();
×
NEW
87
                        previousBlockCard?.destroy();
×
88
                    } else {
89
                        view = previousView;
53✔
90
                        blockCard = previousBlockCard;
53✔
91
                        if (memoizedContext(this.viewContext, record.item, previousContext as any, context as any)) {
53✔
92
                            context = previousContext;
37✔
93
                        } else {
94
                            updateContext(previousView, context, this.viewContext);
16✔
95
                        }
96
                    }
97
                    newContexts.push(context);
53✔
98
                    newViews.push(view);
53✔
99
                    newBlockCards.push(blockCard);
53✔
100
                }
101
            });
102
            diffResult.forEachOperation((record) => {
25✔
103
                // removed
104
                if (record.currentIndex === null) {
17✔
105
                    const view = this.views[record.previousIndex];
5✔
106
                    const blockCard = this.blockCards[record.previousIndex];
5✔
107
                    view.destroy();
5✔
108
                    blockCard?.destroy();
5✔
109
                }
110
                // moved
111
                if (record.previousIndex !== null && record.currentIndex !== null) {
17✔
112
                    mountOnItemChange(record.currentIndex, record.item, newViews, newBlockCards, outletElement, this.viewContext);
6✔
113
                    // Solve the block-card DOMElement loss when moving nodes
114
                    newBlockCards[record.currentIndex]?.instance.append();
6✔
115
                }
116
            });
117
            this.viewTypes = newViewTypes;
25✔
118
            this.views = newViews;
25✔
119
            this.contexts = newContexts;
25✔
120
            this.children = children;
25✔
121
            this.blockCards = newBlockCards;
25✔
122
        } else {
123
            const newContexts = [];
15✔
124
            this.children.forEach((child, index) => {
15✔
125
                let context = getContext(index, child, parentPath, childrenContext, this.viewContext);
113✔
126
                const previousContext = this.contexts[index];
113✔
127
                if (memoizedContext(this.viewContext, child, previousContext as any, context as any)) {
113✔
128
                    context = previousContext;
102✔
129
                } else {
130
                    updateContext(this.views[index], context, this.viewContext);
11✔
131
                }
132
                newContexts.push(context);
113✔
133
            });
134
            this.contexts = newContexts;
15✔
135
        }
136
    }
137
}
138

139
export function getContext(
140
    index: number,
141
    item: Descendant,
142
    parentPath: Path,
143
    childrenContext: SlateChildrenContext,
144
    viewContext: SlateViewContext
145
): SlateElementContext | SlateTextContext {
146
    if (Element.isElement(item)) {
491✔
147
        const computedContext = getCommonContext(index, item, parentPath, viewContext, childrenContext);
339✔
148
        const key = AngularEditor.findKey(viewContext.editor, item);
339✔
149
        const isInline = viewContext.editor.isInline(item);
339✔
150
        const isVoid = viewContext.editor.isVoid(item);
339✔
151
        const elementContext: SlateElementContext = {
339✔
152
            element: item,
153
            path: parentPath.concat(index),
154
            ...computedContext,
155
            attributes: {
156
                'data-slate-node': 'element',
157
                'data-slate-key': key.id
158
            },
159
            decorate: childrenContext.decorate,
160
            readonly: childrenContext.readonly
161
        };
162
        if (isInline) {
339!
NEW
163
            elementContext.attributes['data-slate-inline'] = true;
×
164
        }
165
        if (isVoid) {
339✔
166
            elementContext.attributes['data-slate-void'] = true;
1✔
167
            elementContext.attributes.contenteditable = false;
1✔
168
        }
169
        return elementContext;
339✔
170
    } else {
171
        const computedContext = getCommonContext(index, item, parentPath, viewContext, childrenContext);
152✔
172
        const isLeafBlock = AngularEditor.isLeafBlock(viewContext.editor, childrenContext.parent);
152✔
173
        const textContext: SlateTextContext = {
152✔
174
            decorations: computedContext.decorations,
175
            isLast: isLeafBlock && index === childrenContext.parent.children.length - 1,
304✔
176
            parent: childrenContext.parent as Element,
177
            text: item
178
        };
179
        return textContext;
152✔
180
    }
181
}
182

183
export function getCommonContext(
184
    index: number,
185
    item: Descendant,
186
    parentPath: Path,
187
    viewContext: SlateViewContext,
188
    childrenContext: SlateChildrenContext
189
): { selection: Range; decorations: Range[] } {
190
    const p = parentPath.concat(index);
491✔
191
    try {
491✔
192
        const ds = childrenContext.decorate([item, p]);
491✔
193
        // [list-render] performance optimization: reduce the number of calls to the `Editor.range(viewContext.editor, p)` method
194
        if (childrenContext.selection || childrenContext.decorations.length > 0) {
491✔
195
            const range = Editor.range(viewContext.editor, p);
124✔
196
            const sel = childrenContext.selection && Range.intersection(range, childrenContext.selection);
124✔
197
            for (const dec of childrenContext.decorations) {
124✔
198
                const d = Range.intersection(dec, range);
6✔
199
                if (d) {
6✔
200
                    ds.push(d);
6✔
201
                }
202
            }
203
            return { selection: sel, decorations: ds };
124✔
204
        } else {
205
            return { selection: null, decorations: ds };
367✔
206
        }
207
    } catch (error) {
NEW
208
        this.options.viewContext.editor.onError({
×
209
            code: SlateErrorCode.GetStartPointError,
210
            nativeError: error
211
        });
NEW
212
        return { selection: null, decorations: [] };
×
213
    }
214
}
215

216
export function getViewType(item: Descendant, parent: Ancestor, viewContext: SlateViewContext) {
217
    if (Element.isElement(item)) {
378✔
218
        return (viewContext.renderElement && viewContext.renderElement(item)) || viewContext.defaultElement;
234!
219
    } else {
220
        const isVoid = viewContext.editor.isVoid(parent as Element);
144✔
221
        return isVoid ? viewContext.defaultVoidText : (viewContext.renderText && viewContext.renderText(item)) || viewContext.defaultText;
144!
222
    }
223
}
224

225
export function createBlockCard(item: Descendant, view: EmbeddedViewRef<any> | ComponentRef<any>, viewContainerRef: ViewContainerRef, viewContext: SlateViewContext) {
226
    const isBlockCard = viewContext.editor.isBlockCard(item);
325✔
227
    if (isBlockCard) {
325✔
228
        const rootNodes = getRootNodes(view);
1✔
229
        const blockCardComponentRef = viewContainerRef.createComponent<SlateBlockCard>(SlateBlockCard, {
1✔
230
            injector: viewContainerRef.injector
231
        });
232
        blockCardComponentRef.instance.initializeCenter(rootNodes);
1✔
233
        return blockCardComponentRef;
1✔
234
    } else {
235
        return null;
324✔
236
    }
237
}
238

239
export function trackBy(viewContext: SlateViewContext) {
240
    return (index, node) => {
204✔
241
        return viewContext.trackBy(node) || AngularEditor.findKey(viewContext.editor, node);
491✔
242
    };
243
}
244

245
export function memoizedContext(
246
    viewContext: SlateViewContext,
247
    descendant: Descendant,
248
    prev: SlateElementContext | SlateTextContext,
249
    next: SlateElementContext | SlateTextContext
250
): boolean {
251
    if (Element.isElement(descendant)) {
166✔
252
        return memoizedElementContext(viewContext, prev as SlateElementContext, next as SlateElementContext);
155✔
253
    } else {
254
        return memoizedTextContext(prev as SlateTextContext, next as SlateTextContext);
11✔
255
    }
256
}
257

258
export function memoizedElementContext(viewContext: SlateViewContext, prev: SlateElementContext, next: SlateElementContext) {
259
    return (
155✔
260
        prev.element === next.element &&
862!
261
        (!viewContext.isStrictDecorate || prev.decorate === next.decorate) &&
262
        prev.readonly === next.readonly &&
263
        isDecoratorRangeListEqual(prev.decorations, next.decorations) &&
264
        (prev.selection === next.selection || (!!prev.selection && !!next.selection && Range.equals(prev.selection, next.selection)))
265
    );
266
}
267

268
export function memoizedTextContext(prev: SlateTextContext, next: SlateTextContext) {
269
    return (
11✔
270
        next.parent === prev.parent &&
35✔
271
        next.isLast === prev.isLast &&
272
        next.text === prev.text &&
273
        isDecoratorRangeListEqual(next.decorations, prev.decorations)
274
    );
275
}
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

© 2026 Coveralls, Inc