edytor 0.0.7 → 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.
Files changed (107) hide show
  1. package/README.md +225 -5
  2. package/dist/block/block.svelte.d.ts +123 -0
  3. package/dist/block/block.svelte.js +430 -0
  4. package/dist/block/block.utils.d.ts +82 -0
  5. package/dist/block/block.utils.js +499 -0
  6. package/dist/block/content.svelte.d.ts +14 -0
  7. package/dist/block/content.svelte.js +40 -0
  8. package/dist/block/inlineBlock.svelte.d.ts +30 -0
  9. package/dist/block/inlineBlock.svelte.js +53 -0
  10. package/dist/components/Block.svelte +27 -0
  11. package/dist/components/Block.svelte.d.ts +7 -0
  12. package/dist/components/Content.svelte +44 -0
  13. package/dist/components/Content.svelte.d.ts +7 -0
  14. package/dist/components/Edytor.svelte +165 -0
  15. package/dist/components/Edytor.svelte.d.ts +33 -0
  16. package/dist/components/InlineBlock.svelte +17 -0
  17. package/dist/components/InlineBlock.svelte.d.ts +7 -0
  18. package/dist/components/Mark.svelte +38 -0
  19. package/dist/components/Mark.svelte.d.ts +11 -0
  20. package/dist/components/ReadonlyEditor.svelte +46 -0
  21. package/dist/components/ReadonlyEditor.svelte.d.ts +4 -0
  22. package/dist/components/Text.svelte +37 -0
  23. package/dist/components/Text.svelte.d.ts +7 -0
  24. package/dist/components/readonlyElements.svelte.d.ts +82 -0
  25. package/dist/components/readonlyElements.svelte.js +144 -0
  26. package/dist/constants.d.ts +4 -0
  27. package/dist/constants.js +4 -0
  28. package/dist/dnd.svelte.d.ts +17 -0
  29. package/dist/dnd.svelte.js +125 -0
  30. package/dist/edytor.svelte.d.ts +94 -0
  31. package/dist/edytor.svelte.js +262 -0
  32. package/dist/edytor.utils.d.ts +3 -0
  33. package/dist/edytor.utils.js +78 -0
  34. package/dist/events/onBeforeInput.d.ts +2 -2
  35. package/dist/events/onBeforeInput.js +338 -0
  36. package/dist/events/onKeyDown.d.ts +2 -69
  37. package/dist/events/onKeyDown.js +18 -0
  38. package/dist/hotkeys.d.ts +27 -0
  39. package/dist/hotkeys.js +282 -0
  40. package/dist/index.d.ts +1 -5
  41. package/dist/index.js +2 -8
  42. package/dist/localProvider.d.ts +82 -0
  43. package/dist/localProvider.js +267 -0
  44. package/dist/plugins/arrowMove/arrowMove.d.ts +2 -0
  45. package/dist/plugins/arrowMove/arrowMove.js +66 -0
  46. package/dist/plugins/code/CodePlugin.svelte +218 -0
  47. package/dist/plugins/code/CodePlugin.svelte.d.ts +23 -0
  48. package/dist/plugins/code/prism.css +141 -0
  49. package/dist/plugins/image/ImagePlugin.svelte +22 -0
  50. package/dist/plugins/image/ImagePlugin.svelte.d.ts +20 -0
  51. package/dist/plugins/mention/MentionPlugin.svelte +40 -0
  52. package/dist/plugins/mention/MentionPlugin.svelte.d.ts +20 -0
  53. package/dist/plugins/richtext/RichTextPlugin.svelte +241 -0
  54. package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +26 -0
  55. package/dist/plugins.d.ts +202 -0
  56. package/dist/plugins.js +19 -0
  57. package/dist/selection/selection.svelte.d.ts +68 -0
  58. package/dist/selection/selection.svelte.js +391 -0
  59. package/dist/selection/selection.utils.d.ts +14 -0
  60. package/dist/selection/selection.utils.js +153 -0
  61. package/dist/text/deltas.d.ts +11 -0
  62. package/dist/text/deltas.js +109 -0
  63. package/dist/text/text.svelte.d.ts +70 -0
  64. package/dist/text/text.svelte.js +119 -0
  65. package/dist/text/text.utils.d.ts +40 -0
  66. package/dist/text/text.utils.js +174 -0
  67. package/dist/utils/json.d.ts +32 -0
  68. package/dist/utils/json.js +2 -0
  69. package/dist/utils/jsx.d.ts +25 -0
  70. package/dist/utils/jsx.js +66 -0
  71. package/dist/utils/serialize.d.ts +1 -0
  72. package/dist/utils/serialize.js +1 -0
  73. package/dist/utils.d.ts +6 -0
  74. package/dist/utils.js +19 -0
  75. package/package.json +81 -33
  76. package/dist/edytor.cjs.development.js +0 -1762
  77. package/dist/edytor.cjs.development.js.map +0 -1
  78. package/dist/edytor.cjs.production.min.js +0 -2
  79. package/dist/edytor.cjs.production.min.js.map +0 -1
  80. package/dist/edytor.esm.js +0 -1691
  81. package/dist/edytor.esm.js.map +0 -1
  82. package/dist/events/index.d.ts +0 -3
  83. package/dist/events/onDrop.d.ts +0 -3
  84. package/dist/operations/deleteText.d.ts +0 -7
  85. package/dist/operations/formatText.d.ts +0 -20
  86. package/dist/operations/index.d.ts +0 -9
  87. package/dist/operations/insertText.d.ts +0 -5
  88. package/dist/operations/merge.d.ts +0 -3
  89. package/dist/operations/moveNode.d.ts +0 -12
  90. package/dist/operations/nestNode.d.ts +0 -2
  91. package/dist/operations/removeEmptyText.d.ts +0 -2
  92. package/dist/operations/splitLeaf.d.ts +0 -2
  93. package/dist/operations/splitNode.d.ts +0 -9
  94. package/dist/types.d.ts +0 -140
  95. package/dist/utils/common.d.ts +0 -17
  96. package/dist/utils/dropper.d.ts +0 -26
  97. package/dist/utils/index.d.ts +0 -6
  98. package/dist/utils/leaves.d.ts +0 -34
  99. package/dist/utils/nodes.d.ts +0 -14
  100. package/dist/utils/selection.d.ts +0 -51
  101. package/dist/utils/yClasses/EdytorDoc.d.ts +0 -16
  102. package/dist/utils/yClasses/index.d.ts +0 -3
  103. package/dist/utils/yClasses/yLeaf.d.ts +0 -7
  104. package/dist/utils/yClasses/yNode.d.ts +0 -12
  105. package/dist/yjs/index.d.ts +0 -1
  106. package/dist/yjs/wsProvider.d.ts +0 -1
  107. 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
+ };