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

worktile / slate-angular / 48a2f992-08ad-4616-af80-105cd607cb1d

09 Aug 2024 05:55AM UTC coverage: 46.828% (-0.2%) from 46.993%
48a2f992-08ad-4616-af80-105cd607cb1d

push

circleci

web-flow
fix(fragment): complete the table element structure obtained through the selection (#277)

409 of 1075 branches covered (38.05%)

Branch coverage included in aggregate %.

2 of 14 new or added lines in 2 files covered. (14.29%)

1 existing line in 1 file now uncovered.

1023 of 1983 relevant lines covered (51.59%)

44.31 hits per line

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

32.2
/packages/src/plugins/with-angular.ts
1
import { Editor, Element, Node, Operation, Path, PathRef, Range, Transforms } from 'slate';
2
import { OriginEvent } from '../types/clipboard';
3
import { SlateError } from '../types/error';
4
import { completeTable, EDITOR_TO_ON_CHANGE, getPlainText, isDOMText, isInvalidTable, Key, NODE_TO_KEY } from '../utils';
5
import { getClipboardData, setClipboardData } from '../utils/clipboard/clipboard';
6
import { findCurrentLineRange } from '../utils/lines';
7
import { AngularEditor } from './angular-editor';
8

9
export const withAngular = <T extends Editor>(editor: T, clipboardFormatKey = 'x-slate-fragment') => {
1✔
10
    const e = editor as T & AngularEditor;
28✔
11
    const { apply, onChange, deleteBackward } = e;
28✔
12

13
    e.deleteBackward = unit => {
28✔
14
        if (unit !== 'line') {
×
15
            return deleteBackward(unit);
×
16
        }
17

18
        if (editor.selection && Range.isCollapsed(editor.selection)) {
×
19
            const parentBlockEntry = Editor.above(editor, {
×
20
                match: n => Element.isElement(n) && Editor.isBlock(editor, n),
×
21
                at: editor.selection
22
            });
23

24
            if (parentBlockEntry) {
×
25
                const [, parentBlockPath] = parentBlockEntry;
×
26
                const parentElementRange = Editor.range(editor, parentBlockPath, editor.selection.anchor);
×
27

28
                const currentLineRange = findCurrentLineRange(e, parentElementRange);
×
29

30
                if (!Range.isCollapsed(currentLineRange)) {
×
31
                    Transforms.delete(editor, { at: currentLineRange });
×
32
                }
33
            }
34
        }
35
    };
36

37
    e.apply = (op: Operation) => {
28✔
38
        const matches: [Path | PathRef, Key][] = [];
24✔
39

40
        switch (op.type) {
24✔
41
            case 'insert_text':
42
            case 'remove_text':
43
            case 'set_node': {
44
                for (const [node, path] of Editor.levels(e, { at: op.path })) {
3✔
45
                    const key = AngularEditor.findKey(e, node);
9✔
46
                    matches.push([path, key]);
9✔
47
                }
48

49
                break;
3✔
50
            }
51

52
            case 'insert_node':
53
            case 'remove_node':
54
            case 'merge_node':
55
            case 'split_node': {
56
                for (const [node, path] of Editor.levels(e, {
5✔
57
                    at: Path.parent(op.path)
58
                })) {
59
                    const key = AngularEditor.findKey(e, node);
7✔
60
                    matches.push([path, key]);
7✔
61
                }
62

63
                break;
5✔
64
            }
65

66
            case 'move_node': {
67
                const commonPath = Path.common(Path.parent(op.path), Path.parent(op.newPath));
8✔
68
                for (const [node, path] of Editor.levels(e, {
8✔
69
                    at: Path.parent(op.path)
70
                })) {
71
                    const key = AngularEditor.findKey(e, node);
14✔
72
                    matches.push([Editor.pathRef(editor, path), key]);
14✔
73
                }
74
                for (const [node, path] of Editor.levels(e, {
8✔
75
                    at: Path.parent(op.newPath)
76
                })) {
77
                    if (path.length > commonPath.length) {
16✔
78
                        const key = AngularEditor.findKey(e, node);
5✔
79
                        matches.push([Editor.pathRef(editor, path), key]);
5✔
80
                    }
81
                }
82
                break;
8✔
83
            }
84
        }
85

86
        apply(op);
24✔
87

88
        for (const [source, key] of matches) {
24✔
89
            const [node] = Editor.node(e, Path.isPath(source) ? source : source.current);
35✔
90
            NODE_TO_KEY.set(node, key);
35✔
91
        }
92
    };
93

94
    e.onChange = () => {
28✔
95
        const onContextChange = EDITOR_TO_ON_CHANGE.get(e);
16✔
96

97
        if (onContextChange) {
16✔
98
            onContextChange();
14✔
99
        }
100

101
        onChange();
16✔
102
    };
103

104
    e.setFragmentData = (dataTransfer?: Pick<DataTransfer, 'getData' | 'setData'>, originEvent?: OriginEvent) => {
28✔
105
        const { selection } = e;
×
106

107
        if (!selection) {
×
108
            return;
×
109
        }
110

111
        const [start, end] = Range.edges(selection);
×
112
        const startVoid = Editor.void(e, { at: start.path });
×
113
        const endVoid = Editor.void(e, { at: end.path });
×
114

115
        if (Range.isCollapsed(selection) && !startVoid) {
×
116
            return;
×
117
        }
118

119
        // Create a fake selection so that we can add a Base64-encoded copy of the
120
        // fragment to the HTML, to decode on future pastes.
121
        const domRange = AngularEditor.toDOMRange(e, selection);
×
122
        let contents = domRange.cloneContents();
×
123
        let attach = contents.childNodes[0] as HTMLElement;
×
124

125
        // Make sure attach is non-empty, since empty nodes will not get copied.
126
        const contentsArray = Array.from(contents.children);
×
127
        contentsArray.forEach(node => {
×
128
            if (node.textContent && node.textContent.trim() !== '') {
×
129
                attach = node as HTMLElement;
×
130
            }
131
        });
132

133
        // COMPAT: If the end node is a void node, we need to move the end of the
134
        // range from the void node's spacer span, to the end of the void node's
135
        // content, since the spacer is before void's content in the DOM.
136
        if (endVoid) {
×
137
            const [voidNode] = endVoid;
×
138
            const r = domRange.cloneRange();
×
139
            const domNode = AngularEditor.toDOMNode(e, voidNode);
×
140
            r.setEndAfter(domNode);
×
141
            contents = r.cloneContents();
×
142
        }
143

144
        // COMPAT: If the start node is a void node, we need to attach the encoded
145
        // fragment to the void node's content node instead of the spacer, because
146
        // attaching it to empty `<div>/<span>` nodes will end up having it erased by
147
        // most browsers. (2018/04/27)
148
        if (startVoid) {
×
149
            attach = contents.querySelector('[data-slate-spacer]')! as HTMLElement;
×
150
        }
151

152
        // Remove any zero-width space spans from the cloned DOM so that they don't
153
        // show up elsewhere when pasted.
154
        Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach(zw => {
×
155
            const isNewline = zw.getAttribute('data-slate-zero-width') === 'n';
×
156
            zw.textContent = isNewline ? '\n' : '';
×
157
        });
158

159
        // Set a `data-slate-fragment` attribute on a non-empty node, so it shows up
160
        // in the HTML, and can be used for intra-Slate pasting. If it's a text
161
        // node, wrap it in a `<span>` so we have something to set an attribute on.
162
        if (isDOMText(attach)) {
×
163
            const span = attach.ownerDocument.createElement('span');
×
164
            // COMPAT: In Chrome and Safari, if we don't add the `white-space` style
165
            // then leading and trailing spaces will be ignored. (2017/09/21)
166
            span.style.whiteSpace = 'pre';
×
167
            span.appendChild(attach);
×
168
            contents.appendChild(span);
×
169
            attach = span;
×
170
        }
171

172
        const fragment = e.getFragment();
×
173

174
        // Add the content to a <div> so that we can get its inner HTML.
175
        const div = contents.ownerDocument.createElement('div');
×
NEW
176
        const attachWrapper = document.createElement('div');
×
NEW
177
        const elements = Array.from(contents.children);
×
NEW
178
        if (isInvalidTable(elements)) {
×
NEW
179
            contents = completeTable(contents.cloneNode(true) as DocumentFragment);
×
180
        }
181

NEW
182
        attachWrapper.appendChild(attach);
×
183
        div.appendChild(contents);
×
184
        div.setAttribute('hidden', 'true');
×
185
        contents.ownerDocument.body.appendChild(div);
×
NEW
186
        setClipboardData({ text: getPlainText(div), elements: fragment as Element[] }, div, attachWrapper, dataTransfer);
×
187
        contents.ownerDocument.body.removeChild(div);
×
188
    };
189

190
    e.deleteCutData = () => {
28✔
191
        const { selection } = editor;
×
192
        if (selection) {
×
193
            if (Range.isExpanded(selection)) {
×
194
                Editor.deleteFragment(editor);
×
195
            } else {
196
                const node = Node.parent(editor, selection.anchor.path);
×
197
                if (Element.isElement(node) && Editor.isVoid(editor, node)) {
×
198
                    Transforms.delete(editor);
×
199
                }
200
            }
201
        }
202
    };
203

204
    e.insertData = async (data: DataTransfer) => {
28✔
205
        if (!(await e.insertFragmentData(data))) {
×
206
            e.insertTextData(data);
×
207
        }
208
    };
209

210
    e.insertFragmentData = async (data: DataTransfer): Promise<boolean> => {
28✔
211
        /**
212
         * Checking copied fragment from application/x-slate-fragment or data-slate-fragment
213
         */
214
        const clipboardData = await getClipboardData(data);
×
215
        if (clipboardData && clipboardData.elements) {
×
216
            e.insertFragment(clipboardData.elements);
×
217
            return true;
×
218
        }
219
        return false;
×
220
    };
221

222
    e.insertTextData = async (data: DataTransfer): Promise<boolean> => {
28✔
223
        const clipboardData = await getClipboardData(data);
×
224

225
        if (clipboardData && clipboardData.text) {
×
226
            const lines = clipboardData.text.split(/\r\n|\r|\n/);
×
227
            let split = false;
×
228

229
            for (const line of lines) {
×
230
                if (split) {
×
231
                    Transforms.splitNodes(e, { always: true });
×
232
                }
233

234
                e.insertText(line);
×
235
                split = true;
×
236
            }
237
            return true;
×
238
        }
239
        return false;
×
240
    };
241

242
    e.onKeydown = () => {};
28✔
243

244
    e.onClick = () => {};
28✔
245

246
    e.isBlockCard = element => false;
364✔
247

248
    e.isExpanded = element => true;
215✔
249

250
    e.onError = (errorData: SlateError) => {
28✔
251
        if (errorData.nativeError) {
×
252
            console.error(errorData.nativeError);
×
253
        } else {
254
            console.error(errorData);
×
255
        }
256
    };
257

258
    return e;
28✔
259
};
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