edytor 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -5
- package/dist/block/block.svelte.d.ts +123 -0
- package/dist/block/block.svelte.js +430 -0
- package/dist/block/block.utils.d.ts +82 -0
- package/dist/block/block.utils.js +499 -0
- package/dist/block/content.svelte.d.ts +14 -0
- package/dist/block/content.svelte.js +40 -0
- package/dist/block/inlineBlock.svelte.d.ts +30 -0
- package/dist/block/inlineBlock.svelte.js +53 -0
- package/dist/components/Block.svelte +27 -0
- package/dist/components/Block.svelte.d.ts +7 -0
- package/dist/components/Content.svelte +44 -0
- package/dist/components/Content.svelte.d.ts +7 -0
- package/dist/components/Edytor.svelte +165 -0
- package/dist/components/Edytor.svelte.d.ts +33 -0
- package/dist/components/InlineBlock.svelte +17 -0
- package/dist/components/InlineBlock.svelte.d.ts +7 -0
- package/dist/components/Mark.svelte +38 -0
- package/dist/components/Mark.svelte.d.ts +11 -0
- package/dist/components/ReadonlyEditor.svelte +46 -0
- package/dist/components/ReadonlyEditor.svelte.d.ts +4 -0
- package/dist/components/Text.svelte +37 -0
- package/dist/components/Text.svelte.d.ts +7 -0
- package/dist/components/readonlyElements.svelte.d.ts +82 -0
- package/dist/components/readonlyElements.svelte.js +144 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/dnd.svelte.d.ts +17 -0
- package/dist/dnd.svelte.js +125 -0
- package/dist/edytor.svelte.d.ts +94 -0
- package/dist/edytor.svelte.js +262 -0
- package/dist/edytor.utils.d.ts +3 -0
- package/dist/edytor.utils.js +78 -0
- package/dist/events/onBeforeInput.d.ts +2 -2
- package/dist/events/onBeforeInput.js +338 -0
- package/dist/events/onKeyDown.d.ts +2 -69
- package/dist/events/onKeyDown.js +18 -0
- package/dist/hotkeys.d.ts +27 -0
- package/dist/hotkeys.js +282 -0
- package/dist/index.d.ts +1 -5
- package/dist/index.js +2 -8
- package/dist/localProvider.d.ts +82 -0
- package/dist/localProvider.js +267 -0
- package/dist/plugins/arrowMove/arrowMove.d.ts +2 -0
- package/dist/plugins/arrowMove/arrowMove.js +66 -0
- package/dist/plugins/code/CodePlugin.svelte +218 -0
- package/dist/plugins/code/CodePlugin.svelte.d.ts +23 -0
- package/dist/plugins/code/prism.css +141 -0
- package/dist/plugins/image/ImagePlugin.svelte +22 -0
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +20 -0
- package/dist/plugins/mention/MentionPlugin.svelte +40 -0
- package/dist/plugins/mention/MentionPlugin.svelte.d.ts +20 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte +241 -0
- package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +26 -0
- package/dist/plugins.d.ts +202 -0
- package/dist/plugins.js +19 -0
- package/dist/selection/selection.svelte.d.ts +68 -0
- package/dist/selection/selection.svelte.js +391 -0
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +153 -0
- package/dist/text/deltas.d.ts +11 -0
- package/dist/text/deltas.js +109 -0
- package/dist/text/text.svelte.d.ts +70 -0
- package/dist/text/text.svelte.js +119 -0
- package/dist/text/text.utils.d.ts +40 -0
- package/dist/text/text.utils.js +174 -0
- package/dist/utils/json.d.ts +32 -0
- package/dist/utils/json.js +2 -0
- package/dist/utils/jsx.d.ts +25 -0
- package/dist/utils/jsx.js +66 -0
- package/dist/utils/serialize.d.ts +1 -0
- package/dist/utils/serialize.js +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +19 -0
- package/package.json +81 -33
- package/dist/edytor.cjs.development.js +0 -1763
- package/dist/edytor.cjs.development.js.map +0 -1
- package/dist/edytor.cjs.production.min.js +0 -2
- package/dist/edytor.cjs.production.min.js.map +0 -1
- package/dist/edytor.esm.js +0 -1692
- package/dist/edytor.esm.js.map +0 -1
- package/dist/events/index.d.ts +0 -3
- package/dist/events/onDrop.d.ts +0 -3
- package/dist/operations/deleteText.d.ts +0 -7
- package/dist/operations/formatText.d.ts +0 -20
- package/dist/operations/index.d.ts +0 -9
- package/dist/operations/insertText.d.ts +0 -5
- package/dist/operations/merge.d.ts +0 -3
- package/dist/operations/moveNode.d.ts +0 -12
- package/dist/operations/nestNode.d.ts +0 -2
- package/dist/operations/removeEmptyText.d.ts +0 -2
- package/dist/operations/splitLeaf.d.ts +0 -2
- package/dist/operations/splitNode.d.ts +0 -9
- package/dist/types.d.ts +0 -140
- package/dist/utils/common.d.ts +0 -17
- package/dist/utils/dropper.d.ts +0 -26
- package/dist/utils/index.d.ts +0 -6
- package/dist/utils/leaves.d.ts +0 -34
- package/dist/utils/nodes.d.ts +0 -14
- package/dist/utils/selection.d.ts +0 -51
- package/dist/utils/yClasses/EdytorDoc.d.ts +0 -16
- package/dist/utils/yClasses/index.d.ts +0 -3
- package/dist/utils/yClasses/yLeaf.d.ts +0 -7
- package/dist/utils/yClasses/yNode.d.ts +0 -12
- package/dist/yjs/index.d.ts +0 -1
- package/dist/yjs/wsProvider.d.ts +0 -1
- package/index.ts +0 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import {} from 'yjs';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
import { Text } from '../text/text.svelte.js';
|
|
4
|
+
import { climb, climbDom, getRangesFromSelection, getTextOfNode, getTextsInSelection, getYIndex } from './selection.utils.js';
|
|
5
|
+
import { Block } from '../block/block.svelte.js';
|
|
6
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
7
|
+
import { tick } from 'svelte';
|
|
8
|
+
import { InlineBlock } from '../block/inlineBlock.svelte.js';
|
|
9
|
+
export class EdytorSelection {
|
|
10
|
+
edytorOnSelectionChange;
|
|
11
|
+
edytor;
|
|
12
|
+
focusedBlocks = new SvelteSet();
|
|
13
|
+
selectedBlocks = new SvelteSet();
|
|
14
|
+
hasSelectedAll = $state(false);
|
|
15
|
+
state = $state({
|
|
16
|
+
selection: null,
|
|
17
|
+
start: 0,
|
|
18
|
+
end: 0,
|
|
19
|
+
yStart: 0,
|
|
20
|
+
yEnd: 0,
|
|
21
|
+
length: 0,
|
|
22
|
+
content: '',
|
|
23
|
+
isCollapsed: true,
|
|
24
|
+
ranges: [],
|
|
25
|
+
texts: [],
|
|
26
|
+
blocks: [],
|
|
27
|
+
contentParts: [],
|
|
28
|
+
isAtStartOfBlock: false,
|
|
29
|
+
isAtEndOfBlock: false,
|
|
30
|
+
isAtStartOfText: false,
|
|
31
|
+
isAtEndOfText: false,
|
|
32
|
+
startNode: null,
|
|
33
|
+
endNode: null,
|
|
34
|
+
startText: null,
|
|
35
|
+
endText: null,
|
|
36
|
+
startBlock: null,
|
|
37
|
+
endBlock: null,
|
|
38
|
+
isTextSpanning: false,
|
|
39
|
+
isBlockSpanning: false,
|
|
40
|
+
isVoid: false,
|
|
41
|
+
isIsland: false,
|
|
42
|
+
islandRoot: null,
|
|
43
|
+
voidRoot: null,
|
|
44
|
+
relativePosition: null,
|
|
45
|
+
// TOREMOVE
|
|
46
|
+
yTextContent: ''
|
|
47
|
+
});
|
|
48
|
+
constructor(edytor, edytorOnSelectionChange) {
|
|
49
|
+
this.edytorOnSelectionChange = edytorOnSelectionChange;
|
|
50
|
+
this.edytor = edytor;
|
|
51
|
+
}
|
|
52
|
+
destroy = () => {
|
|
53
|
+
document?.removeEventListener('selectionchange', this.onSelectionChange);
|
|
54
|
+
};
|
|
55
|
+
getTextOfNode = getTextOfNode.bind(this);
|
|
56
|
+
getTextsInSelection = getTextsInSelection.bind(this);
|
|
57
|
+
init = () => {
|
|
58
|
+
this.edytor.undoManager.on('stack-item-added', (event) => {
|
|
59
|
+
event.stackItem.meta.set('cursor-location', {
|
|
60
|
+
id: this.state.startText?.id,
|
|
61
|
+
offset: this.state.yEnd
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
this.edytor.undoManager.on('stack-item-popped', (event) => {
|
|
65
|
+
const cursorLocation = event.stackItem.meta.get('cursor-location');
|
|
66
|
+
const text = this.edytor.getTextById(cursorLocation.id);
|
|
67
|
+
text && this.setAtTextOffset(text, cursorLocation.offset);
|
|
68
|
+
});
|
|
69
|
+
document?.addEventListener('selectionchange', this.onSelectionChange);
|
|
70
|
+
};
|
|
71
|
+
handleTripleClick = async (e) => {
|
|
72
|
+
if (e.detail < 3)
|
|
73
|
+
return;
|
|
74
|
+
await tick();
|
|
75
|
+
const { startText } = this.state;
|
|
76
|
+
startText && this.setAtTextRange(startText, 0, startText.yText.length);
|
|
77
|
+
};
|
|
78
|
+
shift = async (length) => {
|
|
79
|
+
this.state.yStart += length;
|
|
80
|
+
this.state.yEnd += length;
|
|
81
|
+
this.state.start += length;
|
|
82
|
+
this.state.end += length;
|
|
83
|
+
this.state.yTextContent = this.state.startText?.yText.toJSON();
|
|
84
|
+
};
|
|
85
|
+
onSelectionChange = () => {
|
|
86
|
+
const selection = window.getSelection();
|
|
87
|
+
if (!selection?.anchorNode || !this.edytor.container?.contains(selection.anchorNode)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const { anchorNode, focusNode, anchorOffset, focusOffset, isCollapsed, direction, type } = selection;
|
|
91
|
+
const ranges = getRangesFromSelection(selection);
|
|
92
|
+
const isReversed = focusNode === anchorNode
|
|
93
|
+
? focusOffset < anchorOffset
|
|
94
|
+
: focusNode
|
|
95
|
+
? anchorNode.compareDocumentPosition(focusNode) === Node.DOCUMENT_POSITION_PRECEDING
|
|
96
|
+
: false;
|
|
97
|
+
const content = selection?.toString() || '';
|
|
98
|
+
const startNode = isReversed ? focusNode : anchorNode;
|
|
99
|
+
const endNode = isReversed ? anchorNode : focusNode;
|
|
100
|
+
const start = isReversed ? focusOffset : anchorOffset;
|
|
101
|
+
const end = isReversed ? anchorOffset : focusOffset;
|
|
102
|
+
const { startText, endText, texts } = this.getTextsInSelection(startNode, endNode, ranges);
|
|
103
|
+
this.selectBlocks();
|
|
104
|
+
this.focusBlocks(...texts.map((text) => text.parent));
|
|
105
|
+
let yStart = getYIndex(startText, startNode, start);
|
|
106
|
+
let yEnd = isCollapsed ? yStart : getYIndex(endText, endNode, end);
|
|
107
|
+
if (startText?.parent.isEmpty && yStart > startText.length) {
|
|
108
|
+
// In case the placeholder or suggestion is not absolutely positioned, we need to set the selection to the start of the text because caret may be placed after the placeholder which is deceptive
|
|
109
|
+
return this.setAtTextOffset(startText, startText.length);
|
|
110
|
+
}
|
|
111
|
+
let isIsland = false;
|
|
112
|
+
let islandRoot = null;
|
|
113
|
+
climb(startText?.parent, (block) => {
|
|
114
|
+
if (block instanceof Block && block.definition.island) {
|
|
115
|
+
isIsland = true;
|
|
116
|
+
islandRoot = block;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
let isVoid = false;
|
|
121
|
+
let voidRoot = null;
|
|
122
|
+
climb(startText?.parent, (block) => {
|
|
123
|
+
if (block instanceof Block && block.definition.void) {
|
|
124
|
+
isVoid = true;
|
|
125
|
+
voidRoot = block;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (!isVoid) {
|
|
130
|
+
climbDom(startNode, (node) => {
|
|
131
|
+
if (node instanceof HTMLElement && node.dataset.edytorBlock && node.dataset.edytorVoid) {
|
|
132
|
+
const id = node.dataset.edytorId;
|
|
133
|
+
const block = id && this.edytor.idToBlock.get(id);
|
|
134
|
+
if (block) {
|
|
135
|
+
isVoid = true;
|
|
136
|
+
voidRoot = block;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const isAtStartOfText = yStart === 0;
|
|
143
|
+
const isAtEndOfText = yEnd === endText?.yText.length;
|
|
144
|
+
const isAtStartOfBlock = (startText && startText === startText.parent.firstText && isAtStartOfText) || false;
|
|
145
|
+
const isAtEndOfBlock = (endText && endText === endText.parent.lastText && isAtEndOfText) || false;
|
|
146
|
+
const startBlock = startText?.parent || null;
|
|
147
|
+
const endBlock = endText?.parent || null;
|
|
148
|
+
const { startBlock: previousStartBlock, endBlock: previousEndBlock } = this.state;
|
|
149
|
+
if ((previousStartBlock !== startBlock || previousEndBlock !== endBlock) &&
|
|
150
|
+
[previousStartBlock, previousEndBlock].some((block) => block?.suggestions)) {
|
|
151
|
+
[previousStartBlock, previousEndBlock].forEach((block) => {
|
|
152
|
+
if (block?.suggestions) {
|
|
153
|
+
block.suggestions = null;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const isTextSpanning = startText !== endText;
|
|
158
|
+
const isBlockSpanning = startBlock !== endBlock;
|
|
159
|
+
const blocks = startBlock ? [startBlock] : [];
|
|
160
|
+
let currentBlock = startBlock;
|
|
161
|
+
// Collect all blocks between the start and end blocks
|
|
162
|
+
while (currentBlock && currentBlock !== endBlock) {
|
|
163
|
+
const nextBlock = currentBlock.closestNextBlock;
|
|
164
|
+
currentBlock = nextBlock;
|
|
165
|
+
if (nextBlock) {
|
|
166
|
+
blocks.push(nextBlock);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Flatten the blocks into a single array of content parts
|
|
170
|
+
let allContentParts = blocks.flatMap((block) => block.content);
|
|
171
|
+
let contentParts = [];
|
|
172
|
+
let currentPart = startText;
|
|
173
|
+
let i = 0;
|
|
174
|
+
while (currentPart && currentPart !== endText) {
|
|
175
|
+
contentParts.push(currentPart);
|
|
176
|
+
currentPart = allContentParts[i + 1] || null;
|
|
177
|
+
i++;
|
|
178
|
+
}
|
|
179
|
+
if (endText) {
|
|
180
|
+
contentParts.push(endText);
|
|
181
|
+
}
|
|
182
|
+
this.state = {
|
|
183
|
+
selection,
|
|
184
|
+
start,
|
|
185
|
+
end,
|
|
186
|
+
yStart,
|
|
187
|
+
yEnd,
|
|
188
|
+
length: content.length,
|
|
189
|
+
content,
|
|
190
|
+
isCollapsed,
|
|
191
|
+
ranges,
|
|
192
|
+
texts: contentParts.filter((part) => part instanceof Text),
|
|
193
|
+
contentParts,
|
|
194
|
+
blocks,
|
|
195
|
+
isBlockSpanning,
|
|
196
|
+
startText,
|
|
197
|
+
endText,
|
|
198
|
+
isAtStartOfText,
|
|
199
|
+
isAtEndOfText,
|
|
200
|
+
isAtEndOfBlock,
|
|
201
|
+
isAtStartOfBlock,
|
|
202
|
+
isTextSpanning,
|
|
203
|
+
startNode,
|
|
204
|
+
endNode,
|
|
205
|
+
startBlock,
|
|
206
|
+
endBlock,
|
|
207
|
+
isVoid,
|
|
208
|
+
isIsland,
|
|
209
|
+
islandRoot,
|
|
210
|
+
voidRoot,
|
|
211
|
+
relativePosition: startText
|
|
212
|
+
? Y.createRelativePositionFromTypeIndex(startText.yText, yStart, -1)
|
|
213
|
+
: null,
|
|
214
|
+
// TOREMOVE
|
|
215
|
+
yTextContent: startText?.yText.toJSON()
|
|
216
|
+
};
|
|
217
|
+
this.edytorOnSelectionChange?.(this);
|
|
218
|
+
this.edytor.plugins.forEach((plugin) => {
|
|
219
|
+
plugin.onSelectionChange?.(this);
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
restoreRelativePosition = (text) => {
|
|
223
|
+
if (text !== this.state.startText || !this.state.relativePosition) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const absolutePosition = Y.createAbsolutePositionFromRelativePosition(this.state.relativePosition, this.edytor.doc);
|
|
227
|
+
if (!absolutePosition) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.setAtTextOffset(text, absolutePosition?.index);
|
|
231
|
+
};
|
|
232
|
+
selectBlocks = (...blocks) => {
|
|
233
|
+
const difference = this.selectedBlocks.difference(new Set(blocks));
|
|
234
|
+
difference.forEach((block) => {
|
|
235
|
+
this.selectedBlocks.delete(block);
|
|
236
|
+
block.definition.onBlur?.({ block });
|
|
237
|
+
block.node?.removeAttribute('data-edytor-selected');
|
|
238
|
+
});
|
|
239
|
+
blocks.forEach((block) => {
|
|
240
|
+
this.selectedBlocks.add(block);
|
|
241
|
+
block.definition.onSelect?.({ block });
|
|
242
|
+
block.node?.setAttribute('data-edytor-selected', 'true');
|
|
243
|
+
});
|
|
244
|
+
if (blocks.length === 1) {
|
|
245
|
+
this.focusBlocks();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
focusBlocks = (...blocks) => {
|
|
249
|
+
const difference = this.focusedBlocks.difference(new Set(blocks));
|
|
250
|
+
difference.forEach((block) => {
|
|
251
|
+
this.focusedBlocks.delete(block);
|
|
252
|
+
block.definition.onBlur?.({ block });
|
|
253
|
+
block.node?.removeAttribute('data-edytor-focused');
|
|
254
|
+
});
|
|
255
|
+
blocks.forEach((block) => {
|
|
256
|
+
this.focusedBlocks.add(block);
|
|
257
|
+
block.definition.onFocus?.({ block });
|
|
258
|
+
block.node?.setAttribute('data-edytor-focused', 'true');
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
findTextNode = (node, offset = 0) => {
|
|
262
|
+
let nodeOffset = 0;
|
|
263
|
+
const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
|
|
264
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
265
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
266
|
+
}
|
|
267
|
+
return NodeFilter.FILTER_SKIP;
|
|
268
|
+
});
|
|
269
|
+
let currentNode = treeWalker.nextNode();
|
|
270
|
+
let textNode = null;
|
|
271
|
+
let currentOffset = 0;
|
|
272
|
+
while (currentNode) {
|
|
273
|
+
const endOffset = currentOffset + currentNode.length;
|
|
274
|
+
if (offset >= currentOffset && offset <= endOffset) {
|
|
275
|
+
textNode = currentNode;
|
|
276
|
+
nodeOffset = offset - currentOffset;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
currentOffset = endOffset;
|
|
280
|
+
currentNode = treeWalker.nextNode();
|
|
281
|
+
}
|
|
282
|
+
return [textNode, nodeOffset];
|
|
283
|
+
};
|
|
284
|
+
setAtTextOffset = async (textOrId, textOffset = this.state.yStart) => {
|
|
285
|
+
if (!textOrId || typeof textOffset !== 'number') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const node = await this.edytor.getTextNode(textOrId);
|
|
289
|
+
const [textNode, nodeOffset] = this.findTextNode(node, textOffset);
|
|
290
|
+
if (textNode) {
|
|
291
|
+
this.setAtNodeOffset(textNode, nodeOffset);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
setAtTextsRange = async (startText, endText) => {
|
|
295
|
+
if (!startText.node || !endText.node) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const [startTextNode, startNodeOffset] = this.findTextNode(startText.node, 0);
|
|
299
|
+
const [endTextNode, endNodeOffset] = this.findTextNode(endText.node, endText.yText.length);
|
|
300
|
+
if (startTextNode && endTextNode) {
|
|
301
|
+
const selection = window.getSelection();
|
|
302
|
+
const range = document.createRange();
|
|
303
|
+
range.setStart(startTextNode, startNodeOffset);
|
|
304
|
+
range.setEnd(endTextNode, endNodeOffset);
|
|
305
|
+
selection?.removeAllRanges();
|
|
306
|
+
selection?.addRange(range);
|
|
307
|
+
startTextNode.parentElement?.focus();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
setAtBlockRange = async (block, startOffset, endOffset) => {
|
|
311
|
+
if (!block) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!startOffset) {
|
|
315
|
+
startOffset = 0;
|
|
316
|
+
}
|
|
317
|
+
if (!endOffset) {
|
|
318
|
+
endOffset = block.lastText.length;
|
|
319
|
+
}
|
|
320
|
+
const startNode = await this.edytor.getTextNode(block.firstText);
|
|
321
|
+
const [startTextNode, startNodeOffset] = this.findTextNode(startNode, startOffset);
|
|
322
|
+
const endNode = await this.edytor.getTextNode(block.lastText);
|
|
323
|
+
const [endTextNode, endNodeOffset] = this.findTextNode(endNode, endOffset);
|
|
324
|
+
if (startTextNode && endTextNode) {
|
|
325
|
+
const selection = window.getSelection();
|
|
326
|
+
const range = document.createRange();
|
|
327
|
+
range.setStart(startTextNode, startNodeOffset);
|
|
328
|
+
range.setEnd(endTextNode, endNodeOffset);
|
|
329
|
+
selection?.removeAllRanges();
|
|
330
|
+
selection?.addRange(range);
|
|
331
|
+
// this.edytor.node!.focus();
|
|
332
|
+
startTextNode.parentElement?.focus();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
setAtTextRange = async (text, start, end) => {
|
|
336
|
+
if (!text || typeof start !== 'number' || typeof end !== 'number') {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const node = await this.edytor.getTextNode(text);
|
|
340
|
+
let startNode = null;
|
|
341
|
+
let startOffset = 0;
|
|
342
|
+
let endNode = null;
|
|
343
|
+
let endOffset = 0;
|
|
344
|
+
const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
|
|
345
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
346
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
347
|
+
}
|
|
348
|
+
return NodeFilter.FILTER_SKIP;
|
|
349
|
+
});
|
|
350
|
+
let currentNode = treeWalker.nextNode();
|
|
351
|
+
let currentOffset = 0;
|
|
352
|
+
while (currentNode) {
|
|
353
|
+
const nodeLength = currentNode.textContent?.length || 0;
|
|
354
|
+
const endPosition = currentOffset + nodeLength;
|
|
355
|
+
// Find start position
|
|
356
|
+
if (!startNode && start >= currentOffset && start <= endPosition) {
|
|
357
|
+
startNode = currentNode;
|
|
358
|
+
startOffset = start - currentOffset;
|
|
359
|
+
}
|
|
360
|
+
// Find end position
|
|
361
|
+
if (!endNode && end >= currentOffset && end <= endPosition) {
|
|
362
|
+
endNode = currentNode;
|
|
363
|
+
endOffset = end - currentOffset;
|
|
364
|
+
}
|
|
365
|
+
if (startNode && endNode) {
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
currentOffset = endPosition;
|
|
369
|
+
currentNode = treeWalker.nextNode();
|
|
370
|
+
}
|
|
371
|
+
if (startNode && endNode) {
|
|
372
|
+
const selection = window.getSelection();
|
|
373
|
+
const range = document.createRange();
|
|
374
|
+
range.setStart(startNode, startOffset);
|
|
375
|
+
range.setEnd(endNode, endOffset);
|
|
376
|
+
selection?.removeAllRanges();
|
|
377
|
+
selection?.addRange(range);
|
|
378
|
+
// this.edytor.node!.focus();
|
|
379
|
+
startNode.parentElement?.focus();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
setAtNodeOffset = (node, offset) => {
|
|
383
|
+
const selection = window.getSelection();
|
|
384
|
+
const range = document.createRange();
|
|
385
|
+
range.setStart(node, offset);
|
|
386
|
+
range.collapse(true);
|
|
387
|
+
selection?.removeAllRanges();
|
|
388
|
+
selection?.addRange(range);
|
|
389
|
+
this.onSelectionChange();
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EdytorSelection } from './selection.svelte.js';
|
|
2
|
+
import { Text } from '../text/text.svelte.js';
|
|
3
|
+
import { Block } from '../block/block.svelte.js';
|
|
4
|
+
import { Edytor } from '../edytor.svelte.js';
|
|
5
|
+
export declare function getTextOfNode(this: EdytorSelection, node: Node | null): Text | null;
|
|
6
|
+
export declare function getTextsInSelection(this: EdytorSelection, startNode: Node | null, endNode: Node | null, ranges: Range[]): {
|
|
7
|
+
startText: Text | null;
|
|
8
|
+
endText: Text | null;
|
|
9
|
+
texts: Text[];
|
|
10
|
+
};
|
|
11
|
+
export declare const getYIndex: (text: Text | null, node: Node | null, _start: number) => number;
|
|
12
|
+
export declare const getRangesFromSelection: (selection: Selection) => Range[];
|
|
13
|
+
export declare const climb: (block: Block | Edytor | undefined, cb: (block: Block) => void | true) => void;
|
|
14
|
+
export declare const climbDom: (node: Node | undefined | null, cb: (node: Node) => void | true) => void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { EdytorSelection } from './selection.svelte.js';
|
|
2
|
+
import { Text } from '../text/text.svelte.js';
|
|
3
|
+
import { Block } from '../block/block.svelte.js';
|
|
4
|
+
import { Edytor } from '../edytor.svelte.js';
|
|
5
|
+
export function getTextOfNode(node) {
|
|
6
|
+
if (!node)
|
|
7
|
+
return null;
|
|
8
|
+
// If the node is not a text node, we need to find the closest text node
|
|
9
|
+
let text = null;
|
|
10
|
+
let currentNode = node;
|
|
11
|
+
if (node.nodeType !== Node.TEXT_NODE) {
|
|
12
|
+
const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT, (node) => {
|
|
13
|
+
if (node instanceof HTMLSpanElement && node.hasAttribute('data-edytor-text')) {
|
|
14
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
15
|
+
}
|
|
16
|
+
return NodeFilter.FILTER_SKIP;
|
|
17
|
+
});
|
|
18
|
+
currentNode = treeWalker.nextNode();
|
|
19
|
+
text = this.edytor.nodeToText.get(currentNode) || null;
|
|
20
|
+
while (currentNode && !text) {
|
|
21
|
+
currentNode = treeWalker.nextNode();
|
|
22
|
+
text = this.edytor.nodeToText.get(currentNode) || null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
while (currentNode.parentElement && !text) {
|
|
27
|
+
text = this.edytor.nodeToText.get(currentNode) || null;
|
|
28
|
+
currentNode = currentNode.parentElement;
|
|
29
|
+
}
|
|
30
|
+
// Maybe a weird behavior in firefox that force us to do that.
|
|
31
|
+
// Didn't explore much as it seems to work.
|
|
32
|
+
if (!text) {
|
|
33
|
+
currentNode = node;
|
|
34
|
+
// try with previousSibling
|
|
35
|
+
while (currentNode.previousSibling && !text) {
|
|
36
|
+
text = this.edytor.nodeToText.get(currentNode) || null;
|
|
37
|
+
currentNode = currentNode.previousSibling;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
export function getTextsInSelection(startNode, endNode, ranges) {
|
|
44
|
+
const startText = this.getTextOfNode(startNode);
|
|
45
|
+
const endText = this.getTextOfNode(endNode);
|
|
46
|
+
if (!startText) {
|
|
47
|
+
return {
|
|
48
|
+
startText: null,
|
|
49
|
+
endText: null,
|
|
50
|
+
texts: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const edytor = this.edytor;
|
|
54
|
+
const isAfterFirstText = (node) => {
|
|
55
|
+
return node.compareDocumentPosition(startText.node) === Node.DOCUMENT_POSITION_FOLLOWING;
|
|
56
|
+
};
|
|
57
|
+
const isBeforeLastText = (node) => {
|
|
58
|
+
return (node.compareDocumentPosition(endText?.node || startText.node) ===
|
|
59
|
+
Node.DOCUMENT_POSITION_PRECEDING);
|
|
60
|
+
};
|
|
61
|
+
const texts = new Set();
|
|
62
|
+
// Create a TreeWalker to traverse nodes within the range
|
|
63
|
+
const walker = document.createTreeWalker(ranges[0].commonAncestorContainer, NodeFilter.SHOW_ELEMENT, // Only consider element nodes
|
|
64
|
+
(node) => {
|
|
65
|
+
return node === startText.node ||
|
|
66
|
+
node === endText?.node ||
|
|
67
|
+
(node instanceof HTMLSpanElement &&
|
|
68
|
+
node.hasAttribute('data-edytor-text') &&
|
|
69
|
+
isAfterFirstText(node) &&
|
|
70
|
+
isBeforeLastText(node))
|
|
71
|
+
? NodeFilter.FILTER_ACCEPT
|
|
72
|
+
: NodeFilter.FILTER_SKIP;
|
|
73
|
+
});
|
|
74
|
+
ranges.forEach((range) => {
|
|
75
|
+
const isNodeInRange = (node) => {
|
|
76
|
+
const nodeRange = document.createRange();
|
|
77
|
+
try {
|
|
78
|
+
nodeRange.selectNode(node);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// If the node cannot be selected, it's not a valid target
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
// Check if the start or end of the node range intersects with the original range
|
|
85
|
+
return (range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0 &&
|
|
86
|
+
range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0);
|
|
87
|
+
};
|
|
88
|
+
// Start traversing the nodes within the range
|
|
89
|
+
while (walker.nextNode()) {
|
|
90
|
+
// If the current node is within the range, add it to the spans array
|
|
91
|
+
if (isNodeInRange(walker.currentNode)) {
|
|
92
|
+
texts.add(edytor.nodeToText.get(walker.currentNode));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (!texts.size) {
|
|
97
|
+
texts.add(startText);
|
|
98
|
+
if (endText && endText.node !== startText.node) {
|
|
99
|
+
texts.add(endText);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
startText,
|
|
104
|
+
endText,
|
|
105
|
+
texts: Array.from(texts)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export const getYIndex = (text, node, _start) => {
|
|
109
|
+
if (!text || !node)
|
|
110
|
+
return _start;
|
|
111
|
+
const parent = text.node;
|
|
112
|
+
const { isEmpty } = text;
|
|
113
|
+
const treeWalker = document.createTreeWalker(parent, NodeFilter.SHOW_TEXT, (child) => {
|
|
114
|
+
return child.textContent &&
|
|
115
|
+
child.compareDocumentPosition(node) === Node.DOCUMENT_POSITION_FOLLOWING
|
|
116
|
+
? NodeFilter.FILTER_ACCEPT
|
|
117
|
+
: NodeFilter.FILTER_SKIP;
|
|
118
|
+
});
|
|
119
|
+
let start = _start;
|
|
120
|
+
while (treeWalker.nextNode()) {
|
|
121
|
+
const child = treeWalker.currentNode;
|
|
122
|
+
if (child.textContent) {
|
|
123
|
+
start += child.textContent.length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if ((start === 0 || start === 1) && isEmpty) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
return start;
|
|
130
|
+
};
|
|
131
|
+
export const getRangesFromSelection = (selection) => {
|
|
132
|
+
return Array.from({ length: selection.rangeCount }, (_, i) => selection.getRangeAt(i));
|
|
133
|
+
};
|
|
134
|
+
export const climb = (block, cb) => {
|
|
135
|
+
if (!block)
|
|
136
|
+
return;
|
|
137
|
+
let parent = block;
|
|
138
|
+
while (parent && parent instanceof Block) {
|
|
139
|
+
if (cb(parent))
|
|
140
|
+
break;
|
|
141
|
+
parent = parent.parent;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
export const climbDom = (node, cb) => {
|
|
145
|
+
if (!node)
|
|
146
|
+
return;
|
|
147
|
+
let parent = node;
|
|
148
|
+
while (parent) {
|
|
149
|
+
if (cb(parent))
|
|
150
|
+
break;
|
|
151
|
+
parent = parent.parentElement;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import type { JSONText, SerializableContent } from '../utils/json.ts';
|
|
3
|
+
export type Mark = [string, SerializableContent];
|
|
4
|
+
export type JSONDelta = {
|
|
5
|
+
text: string;
|
|
6
|
+
marks: Mark[];
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const jsonToDelta: (text: JSONText[]) => JSONDelta[];
|
|
10
|
+
export declare const deltaToJson: (delta: JSONDelta[]) => JSONText[];
|
|
11
|
+
export declare const toDeltas: (text: Y.Text) => readonly [JSONDelta[], true];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// This is an adaptation of the toDeltas function of the yjs library that better suits our needs
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
export const jsonToDelta = (text) => {
|
|
4
|
+
return text.map((t) => ({
|
|
5
|
+
text: t.text,
|
|
6
|
+
marks: Object.entries(t.marks || {}),
|
|
7
|
+
id: crypto.randomUUID()
|
|
8
|
+
}));
|
|
9
|
+
};
|
|
10
|
+
export const deltaToJson = (delta) => {
|
|
11
|
+
return delta.map((d) => {
|
|
12
|
+
const result = {
|
|
13
|
+
text: d.text,
|
|
14
|
+
marks: Object.fromEntries(d.marks)
|
|
15
|
+
};
|
|
16
|
+
if (d.marks.length === 0) {
|
|
17
|
+
delete result.marks;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const createDelta = (text, attributes) => {
|
|
23
|
+
const size = attributes.size;
|
|
24
|
+
if (size === 0) {
|
|
25
|
+
return { text, marks: [], id: crypto.randomUUID() };
|
|
26
|
+
}
|
|
27
|
+
const marks = new Array(size);
|
|
28
|
+
let i = 0;
|
|
29
|
+
attributes.forEach((value, key) => {
|
|
30
|
+
marks[i++] = [key, value];
|
|
31
|
+
});
|
|
32
|
+
return { text, marks, id: crypto.randomUUID() };
|
|
33
|
+
};
|
|
34
|
+
const areMarksEqual = (marks1, marks2) => {
|
|
35
|
+
if (marks1.size !== marks2.size)
|
|
36
|
+
return false;
|
|
37
|
+
for (const [key, value] of marks1) {
|
|
38
|
+
if (!marks2.has(key) || marks2.get(key) !== value)
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
export const toDeltas = (text) => {
|
|
44
|
+
const result = [];
|
|
45
|
+
if (!text) {
|
|
46
|
+
return [result, true];
|
|
47
|
+
}
|
|
48
|
+
const currentAttributes = new Map();
|
|
49
|
+
let currentString = '';
|
|
50
|
+
let n = text._start;
|
|
51
|
+
let isEmpty = true;
|
|
52
|
+
const flushString = () => {
|
|
53
|
+
if (currentString) {
|
|
54
|
+
const newDelta = createDelta(currentString, currentAttributes);
|
|
55
|
+
// Check if we can merge with the previous delta
|
|
56
|
+
const lastDelta = result[result.length - 1];
|
|
57
|
+
if (lastDelta && lastDelta.marks.length === newDelta.marks.length) {
|
|
58
|
+
const lastDeltaMarks = new Map(lastDelta.marks);
|
|
59
|
+
const newDeltaMarks = new Map(newDelta.marks);
|
|
60
|
+
if (areMarksEqual(lastDeltaMarks, newDeltaMarks)) {
|
|
61
|
+
// Merge the text with the previous delta
|
|
62
|
+
lastDelta.text += currentString;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
result.push(newDelta);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result.push(newDelta);
|
|
70
|
+
}
|
|
71
|
+
currentString = '';
|
|
72
|
+
isEmpty = false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
while (n) {
|
|
76
|
+
if (!n.deleted) {
|
|
77
|
+
const content = n.content;
|
|
78
|
+
const contentType = content.constructor;
|
|
79
|
+
if (contentType === Y.ContentString) {
|
|
80
|
+
// Check ychange first to minimize map lookups
|
|
81
|
+
if (currentAttributes.has('ychange')) {
|
|
82
|
+
flushString();
|
|
83
|
+
currentAttributes.delete('ychange');
|
|
84
|
+
}
|
|
85
|
+
currentString += content.str;
|
|
86
|
+
}
|
|
87
|
+
else if (contentType === Y.ContentType || contentType === Y.ContentEmbed) {
|
|
88
|
+
flushString();
|
|
89
|
+
result.push(createDelta(String(content.getContent()[0]), currentAttributes));
|
|
90
|
+
}
|
|
91
|
+
else if (contentType === Y.ContentFormat) {
|
|
92
|
+
if (!n.deleted) {
|
|
93
|
+
flushString();
|
|
94
|
+
const format = content;
|
|
95
|
+
const { key, value } = format;
|
|
96
|
+
if (value === null) {
|
|
97
|
+
currentAttributes.delete(key);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
currentAttributes.set(key, value);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
n = n.right;
|
|
106
|
+
}
|
|
107
|
+
flushString();
|
|
108
|
+
return [result, isEmpty];
|
|
109
|
+
};
|