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

worktile / slate-angular / 4fec1c69-d3d6-4ad1-bb9b-dd4b0f13f2f2

03 Dec 2025 08:15AM UTC coverage: 45.723% (-1.1%) from 46.809%
4fec1c69-d3d6-4ad1-bb9b-dd4b0f13f2f2

Pull #304

circleci

pubuzhixing8
chore: enter next
Pull Request #304: feat: Support virtual scrolling

380 of 1047 branches covered (36.29%)

Branch coverage included in aggregate %.

46 of 128 new or added lines in 3 files covered. (35.94%)

203 existing lines in 3 files now uncovered.

1042 of 2063 relevant lines covered (50.51%)

31.35 hits per line

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

36.75
/packages/src/view/base.ts
1
import {
2
    ChangeDetectorRef,
3
    Directive,
4
    ElementRef,
5
    HostBinding,
6
    Input,
7
    OnDestroy,
8
    OnInit,
9
    ViewChild,
10
    ViewContainerRef,
11
    inject
12
} from '@angular/core';
13
import { Descendant, Element, Range, Text } from 'slate';
14
import { SlateChildrenOutlet } from '../components/children/children-outlet.component';
15
import { AngularEditor } from '../plugins/angular-editor';
16
import { ELEMENT_TO_COMPONENT } from '../utils/weak-maps';
17
import { SlateChildrenContext, SlateElementContext, SlateLeafContext, SlateTextContext, SlateViewContext } from './context';
18
import { hasAfterContextChange, hasBeforeContextChange } from './context-change';
19
import { LeavesRender } from './render/leaves-render';
20
import { ListRender, addAfterViewInitQueue } from './render/list-render';
21
import { ELEMENT_TO_NODE, NODE_TO_ELEMENT } from 'slate-dom';
22
import { getContentHeight } from '../utils/dom';
23
import { SlateStringRender } from '../components/string/string-render';
24

25
/**
26
 * base class for template
27
 */
28
export interface BaseEmbeddedView<T, K extends AngularEditor = AngularEditor> {
29
    context: T;
30
    viewContext: SlateViewContext<K>;
31
}
32

33
/**
34
 * base class for custom element component or text component
35
 */
36
@Directive()
37
export abstract class BaseComponent<
1✔
38
    T = SlateTextContext | SlateLeafContext | SlateElementContext,
39
    K extends AngularEditor = AngularEditor
40
> {
41
    initialized = false;
2✔
42

43
    protected _context: T;
44

45
    @Input()
46
    set context(value: T) {
47
        if (hasBeforeContextChange<T>(this)) {
3!
48
            this.beforeContextChange(value);
×
49
        }
50
        this._context = value;
3✔
51
        this.onContextChange();
3✔
52
        if (this.initialized) {
3✔
53
            this.cdr.detectChanges();
1✔
54
        }
55
        if (hasAfterContextChange<T>(this)) {
3!
56
            this.afterContextChange();
×
57
        }
58
    }
59

60
    get context() {
61
        return this._context;
×
62
    }
63

64
    @Input() viewContext: SlateViewContext<K>;
65

66
    get editor() {
67
        return this.viewContext && this.viewContext.editor;
5✔
68
    }
69

70
    get nativeElement(): HTMLElement {
71
        return this.elementRef.nativeElement;
17✔
72
    }
73

74
    public elementRef = inject(ElementRef);
2✔
75

76
    public cdr = inject(ChangeDetectorRef);
2✔
77

78
    abstract onContextChange();
79
}
80

81
/**
82
 * base class for custom element component
83
 */
84
@Directive()
85
export class BaseElementComponent<T extends Element = Element, K extends AngularEditor = AngularEditor>
1✔
86
    extends BaseComponent<SlateElementContext<T>, K>
87
    implements OnInit, OnDestroy
88
{
89
    viewContainerRef = inject(ViewContainerRef);
2✔
90

91
    childrenContext: SlateChildrenContext;
92

93
    @ViewChild(SlateChildrenOutlet, { static: true })
94
    childrenOutletInstance?: SlateChildrenOutlet;
95

96
    get element(): T {
97
        return this._context && this._context.element;
23✔
98
    }
99

100
    get selection(): Range {
101
        return this._context && this._context.selection;
×
102
    }
103

104
    get decorations(): Range[] {
105
        return this._context && this._context.decorations;
×
106
    }
107

108
    get children(): Descendant[] {
109
        return this._context && this._context.element.children;
3✔
110
    }
111

112
    get isCollapsed() {
113
        return this.selection && Range.isCollapsed(this.selection);
×
114
    }
115

116
    get isCollapsedAndNonReadonly() {
117
        return this.selection && Range.isCollapsed(this.selection) && !this.readonly;
×
118
    }
119

120
    get readonly() {
121
        return this._context && this._context.readonly;
×
122
    }
123

124
    getOutletParent = () => {
2✔
125
        return this.elementRef.nativeElement;
3✔
126
    };
127

128
    getOutletElement = () => {
2✔
129
        if (this.childrenOutletInstance) {
2✔
130
            return this.childrenOutletInstance.getNativeElement();
2✔
131
        }
132
        return null;
×
133
    };
134

135
    listRender: ListRender;
136

137
    ngOnInit() {
138
        for (const key in this._context.attributes) {
2✔
139
            this.nativeElement.setAttribute(key, this._context.attributes[key]);
4✔
140
        }
141
        this.initialized = true;
2✔
142
        this.listRender = new ListRender(this.viewContext, this.viewContainerRef, this.getOutletParent, this.getOutletElement);
2✔
143
        if (this.editor.isExpanded(this.element)) {
2✔
144
            this.listRender.initialize(this.children, this.element, this.childrenContext);
2✔
145
        }
146
        addAfterViewInitQueue(this.editor, () => {
2✔
147
            this.afterViewInit();
2✔
148
        });
149
    }
150

151
    afterViewInit() {
152
        if (this._context.contentEditable !== undefined) {
2!
153
            this.nativeElement.setAttribute('contenteditable', this._context.contentEditable + '');
×
154
        }
155
    }
156

157
    updateWeakMap() {
158
        NODE_TO_ELEMENT.set(this.element, this.nativeElement);
3✔
159
        ELEMENT_TO_NODE.set(this.nativeElement, this.element);
3✔
160
        ELEMENT_TO_COMPONENT.set(this.element, this);
3✔
161
    }
162

163
    ngOnDestroy() {
164
        if (NODE_TO_ELEMENT.get(this.element) === this.nativeElement) {
2✔
165
            NODE_TO_ELEMENT.delete(this.element);
2✔
166
        }
167
        ELEMENT_TO_NODE.delete(this.nativeElement);
2✔
168
        if (ELEMENT_TO_COMPONENT.get(this.element) === this) {
2✔
169
            ELEMENT_TO_COMPONENT.delete(this.element);
2✔
170
        }
171
    }
172

173
    onContextChange() {
174
        this.childrenContext = this.getChildrenContext();
3✔
175
        this.updateWeakMap();
3✔
176
        if (!this.initialized) {
3✔
177
            return;
2✔
178
        }
179
        this.updateChildrenView();
1✔
180
    }
181

182
    updateChildrenView() {
183
        if (this.editor.isExpanded(this.element)) {
1!
184
            this.listRender.update(this.children, this.element, this.childrenContext);
1✔
185
        } else {
186
            if (this.listRender.initialized) {
×
187
                this.listRender.destroy();
×
188
            }
189
        }
190
    }
191

192
    getChildrenContext(): SlateChildrenContext {
193
        return {
3✔
194
            parent: this._context.element,
195
            selection: this._context.selection,
196
            decorations: this._context.decorations,
197
            decorate: this._context.decorate,
198
            readonly: this._context.readonly
199
        };
200
    }
201
}
202

203
/**
204
 * base class for custom text component
205
 */
206
@Directive()
207
export class BaseTextComponent<T extends Text = Text> extends BaseComponent<SlateTextContext<T>> implements OnInit, OnDestroy {
1✔
UNCOV
208
    viewContainerRef = inject(ViewContainerRef);
×
209

210
    get text(): T {
UNCOV
211
        return this._context && this._context.text;
×
212
    }
213

214
    leavesRender: LeavesRender;
215

216
    @ViewChild(SlateChildrenOutlet, { static: true })
217
    childrenOutletInstance?: SlateChildrenOutlet;
218

UNCOV
219
    getOutletParent = () => {
×
UNCOV
220
        return this.elementRef.nativeElement;
×
221
    };
222

223
    getOutletElement = () => {
×
224
        if (this.childrenOutletInstance) {
×
UNCOV
225
            return this.childrenOutletInstance.getNativeElement();
×
226
        }
227
        return null;
×
228
    };
229

230
    ngOnInit() {
231
        this.initialized = true;
×
UNCOV
232
        this.leavesRender = new LeavesRender(this.viewContext, this.viewContainerRef, this.getOutletParent, this.getOutletElement);
×
UNCOV
233
        this.leavesRender.initialize(this.context);
×
234
    }
235

236
    updateWeakMap() {
237
        ELEMENT_TO_NODE.set(this.nativeElement, this.text);
×
UNCOV
238
        NODE_TO_ELEMENT.set(this.text, this.nativeElement);
×
239
    }
240

241
    ngOnDestroy() {
242
        if (NODE_TO_ELEMENT.get(this.text) === this.nativeElement) {
×
UNCOV
243
            NODE_TO_ELEMENT.delete(this.text);
×
244
        }
UNCOV
245
        ELEMENT_TO_NODE.delete(this.nativeElement);
×
246
    }
247

248
    onContextChange() {
249
        this.updateWeakMap();
×
UNCOV
250
        if (!this.initialized) {
×
UNCOV
251
            return;
×
252
        }
253
        this.leavesRender.update(this.context);
×
254
    }
255
}
256

257
/**
258
 * base class for custom leaf component
259
 */
260
@Directive()
261
export class BaseLeafComponent extends BaseComponent<SlateLeafContext> implements OnInit {
1✔
262
    placeholderElement: HTMLSpanElement;
263

UNCOV
264
    stringRender: SlateStringRender | null = null;
×
265

UNCOV
266
    @HostBinding('attr.data-slate-leaf') isSlateLeaf = true;
×
267

268
    get text(): Text {
UNCOV
269
        return this.context && this.context.text;
×
270
    }
271

272
    get leaf(): Text {
273
        return this.context && this.context.leaf;
×
274
    }
275

276
    ngOnInit() {
277
        this.initialized = true;
×
278
    }
279

280
    onContextChange() {
281
        if (!this.initialized) {
×
UNCOV
282
            this.stringRender = new SlateStringRender(this.context, this.viewContext);
×
UNCOV
283
            const stringNode = this.stringRender.render();
×
UNCOV
284
            this.nativeElement.appendChild(stringNode);
×
285
        } else {
286
            this.stringRender?.update(this.context, this.viewContext);
×
287
        }
288
        if (!this.initialized) {
×
UNCOV
289
            return;
×
290
        }
291
    }
292

293
    renderPlaceholder() {
294
        // issue-1: IME input was interrupted
295
        // issue-2: IME input focus jumping
296
        // Issue occurs when the span node of the placeholder is before the slateString span node
UNCOV
297
        if (this.context.leaf['placeholder']) {
×
UNCOV
298
            if (!this.placeholderElement) {
×
UNCOV
299
                this.createPlaceholder();
×
300
            }
301
            this.updatePlaceholder();
×
302
        } else {
303
            this.destroyPlaceholder();
×
304
        }
305
    }
306

307
    createPlaceholder() {
UNCOV
308
        const placeholderElement = document.createElement('span');
×
UNCOV
309
        placeholderElement.innerText = this.context.leaf['placeholder'];
×
UNCOV
310
        placeholderElement.contentEditable = 'false';
×
UNCOV
311
        placeholderElement.setAttribute('data-slate-placeholder', 'true');
×
312
        this.placeholderElement = placeholderElement;
×
313
        this.nativeElement.classList.add('leaf-with-placeholder');
×
314
        this.nativeElement.appendChild(placeholderElement);
×
315

316
        setTimeout(() => {
×
317
            const editorElement = this.nativeElement.closest('.the-editor-typo');
×
318
            const editorContentHeight = getContentHeight(editorElement);
×
UNCOV
319
            if (editorContentHeight > 0) {
×
320
                // Not supported webkitLineClamp exceeds height hiding
321
                placeholderElement.style.maxHeight = `${editorContentHeight}px`;
×
322
            }
323
            const lineClamp = Math.floor(editorContentHeight / this.nativeElement.offsetHeight) || 0;
×
UNCOV
324
            placeholderElement.style.webkitLineClamp = `${Math.max(lineClamp, 1)}`;
×
325
        });
326
    }
327

328
    updatePlaceholder() {
UNCOV
329
        if (this.placeholderElement.innerText !== this.context.leaf['placeholder']) {
×
UNCOV
330
            this.placeholderElement.innerText = this.context.leaf['placeholder'];
×
331
        }
332
    }
333

334
    destroyPlaceholder() {
UNCOV
335
        if (this.placeholderElement) {
×
UNCOV
336
            this.placeholderElement.remove();
×
UNCOV
337
            this.placeholderElement = null;
×
UNCOV
338
            this.nativeElement.classList.remove('leaf-with-placeholder');
×
339
        }
340
    }
341
}
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