edytor 0.0.7 → 0.0.9
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 +7 -5
- package/dist/index.js +7 -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/index.d.ts +3 -0
- package/dist/plugins/index.js +3 -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 -1762
- 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 -1691
- 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
package/dist/hotkeys.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { Text } from './text/text.svelte.js';
|
|
2
|
+
import { prevent, PreventionError } from './utils.js';
|
|
3
|
+
import { Block } from './block/block.svelte.js';
|
|
4
|
+
import { tick } from 'svelte';
|
|
5
|
+
const letter = new Set([
|
|
6
|
+
'a',
|
|
7
|
+
'b',
|
|
8
|
+
'c',
|
|
9
|
+
'd',
|
|
10
|
+
'e',
|
|
11
|
+
'f',
|
|
12
|
+
'g',
|
|
13
|
+
'h',
|
|
14
|
+
'i',
|
|
15
|
+
'j',
|
|
16
|
+
'k',
|
|
17
|
+
'l',
|
|
18
|
+
'm',
|
|
19
|
+
'n',
|
|
20
|
+
'o',
|
|
21
|
+
'p',
|
|
22
|
+
'q',
|
|
23
|
+
'r',
|
|
24
|
+
's',
|
|
25
|
+
't',
|
|
26
|
+
'u',
|
|
27
|
+
'v',
|
|
28
|
+
'w',
|
|
29
|
+
'x',
|
|
30
|
+
'y',
|
|
31
|
+
'z'
|
|
32
|
+
]);
|
|
33
|
+
const escapedKeys = new Set(['shift']);
|
|
34
|
+
const defaultHotKeys = {
|
|
35
|
+
'mod+z': ({ edytor }) => {
|
|
36
|
+
prevent(() => {
|
|
37
|
+
edytor.undoManager.undo();
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
'mod+shift+z': ({ edytor, prevent }) => {
|
|
41
|
+
prevent(() => {
|
|
42
|
+
edytor.undoManager.redo();
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
'mod+enter': ({ edytor, prevent }) => {
|
|
46
|
+
prevent(() => {
|
|
47
|
+
const newBlock = edytor.selection.state.startText?.parent.splitBlock({
|
|
48
|
+
index: edytor.selection.state.startText?.yText.length,
|
|
49
|
+
text: edytor.selection.state.startText
|
|
50
|
+
});
|
|
51
|
+
if (newBlock && newBlock.content[0] instanceof Text) {
|
|
52
|
+
newBlock && edytor.selection.setAtTextOffset(newBlock.content[0], newBlock.content.length);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
'mod+a': ({ edytor, prevent }) => {
|
|
57
|
+
prevent(() => {
|
|
58
|
+
const { startText, startBlock, islandRoot, isVoid, isIsland, voidRoot } = edytor.selection.state;
|
|
59
|
+
if (startText) {
|
|
60
|
+
if (edytor.selection.selectedBlocks.size) {
|
|
61
|
+
edytor.selection.selectBlocks(...edytor.root.children);
|
|
62
|
+
}
|
|
63
|
+
else if (edytor.selection.state.isAtStartOfBlock &&
|
|
64
|
+
edytor.selection.state.isAtEndOfBlock) {
|
|
65
|
+
window.getSelection()?.removeAllRanges();
|
|
66
|
+
edytor.selection.selectBlocks(edytor.selection.state.isIsland ? islandRoot : startText.parent);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
edytor.selection.setAtBlockRange(startBlock);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
arrowup: ({ edytor, prevent }) => {
|
|
75
|
+
const selectedBlocks = edytor.selection.selectedBlocks;
|
|
76
|
+
if (selectedBlocks.size === 1) {
|
|
77
|
+
prevent(() => {
|
|
78
|
+
const selectedBlock = selectedBlocks.values().next().value;
|
|
79
|
+
let prevBlock = selectedBlock.closestPreviousBlock;
|
|
80
|
+
// If the block is inside an island, we will select the island root
|
|
81
|
+
while (prevBlock?.insideIsland) {
|
|
82
|
+
if (prevBlock.parent instanceof Block) {
|
|
83
|
+
prevBlock = prevBlock.parent;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (prevBlock && prevBlock instanceof Block) {
|
|
87
|
+
edytor.selection.selectBlocks(prevBlock);
|
|
88
|
+
edytor.selection.focusBlocks();
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
arrowdown: ({ edytor, prevent }) => {
|
|
94
|
+
const selectedBlocks = edytor.selection.selectedBlocks;
|
|
95
|
+
if (selectedBlocks.size === 1) {
|
|
96
|
+
prevent(() => {
|
|
97
|
+
const selectedBlock = selectedBlocks.values().next().value;
|
|
98
|
+
let nextBlock = selectedBlock.definition.island
|
|
99
|
+
? selectedBlock.nextBlock
|
|
100
|
+
: selectedBlock.closestNextBlock;
|
|
101
|
+
if (nextBlock && nextBlock instanceof Block) {
|
|
102
|
+
edytor.selection.selectBlocks(nextBlock);
|
|
103
|
+
edytor.selection.focusBlocks();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
tab: ({ edytor, prevent }) => {
|
|
109
|
+
prevent(() => {
|
|
110
|
+
const selectedBlocks = edytor.selection.selectedBlocks;
|
|
111
|
+
if (selectedBlocks.size <= 1) {
|
|
112
|
+
const selectedBlock = selectedBlocks.values().next().value;
|
|
113
|
+
const { yStart, startText, startBlock } = edytor.selection.state;
|
|
114
|
+
const blockToNest = selectedBlock || startBlock;
|
|
115
|
+
const newBlock = blockToNest?.nestBlock();
|
|
116
|
+
const index = startText?.index;
|
|
117
|
+
if (selectedBlock) {
|
|
118
|
+
tick().then(() => {
|
|
119
|
+
newBlock && edytor.selection.selectBlocks(newBlock);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (newBlock && index !== undefined) {
|
|
124
|
+
const textToFocus = newBlock.content[index];
|
|
125
|
+
edytor.selection.setAtTextOffset(textToFocus, yStart);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
escape: ({ edytor, prevent }) => {
|
|
132
|
+
if (edytor.selection.selectedBlocks.size > 0) {
|
|
133
|
+
prevent(() => {
|
|
134
|
+
const firstSelectedBlock = edytor.selection.selectedBlocks.values().next().value;
|
|
135
|
+
edytor.selection.selectBlocks();
|
|
136
|
+
if (firstSelectedBlock.firstEditableText) {
|
|
137
|
+
edytor.selection.setAtTextOffset(firstSelectedBlock.firstEditableText, firstSelectedBlock.firstEditableText.yText.length);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
backspace: ({ edytor, prevent }) => {
|
|
143
|
+
if (edytor.selection.selectedBlocks.size) {
|
|
144
|
+
const selectedBlock = edytor.selection.selectedBlocks.values().next().value;
|
|
145
|
+
const isNested = selectedBlock.parent !== edytor.root;
|
|
146
|
+
const isLastChild = selectedBlock.parent?.children.at(-1) === selectedBlock;
|
|
147
|
+
if (isNested && isLastChild) {
|
|
148
|
+
const newBlock = selectedBlock.unNestBlock();
|
|
149
|
+
if (newBlock) {
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
edytor.selection.selectBlocks(newBlock);
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const selectedBlocks = Array.from(edytor.selection.selectedBlocks.values());
|
|
157
|
+
edytor.edytor.plugins.forEach((plugin) => {
|
|
158
|
+
plugin.onDeleteSelectedBlocks?.({ prevent, selectedBlocks });
|
|
159
|
+
});
|
|
160
|
+
const firstSelectedBlock = selectedBlocks.at(0);
|
|
161
|
+
const lastSelectedBlock = selectedBlocks.at(-1);
|
|
162
|
+
const blockToFocus = firstSelectedBlock.closestPreviousBlock || lastSelectedBlock.closestNextBlock;
|
|
163
|
+
edytor.edytor.transact(() => {
|
|
164
|
+
edytor.selection.selectedBlocks.forEach((block) => {
|
|
165
|
+
block.removeBlock();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
if (blockToFocus) {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
edytor.edytor.node?.focus();
|
|
171
|
+
edytor.selection.setAtTextOffset(blockToFocus.firstEditableText, blockToFocus.firstEditableText?.yText.length);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
// Features:
|
|
178
|
+
// Case insensitive hotkeys
|
|
179
|
+
// Modifier keys in any order
|
|
180
|
+
// Hotkeys can prevent execution of subsequent hotkeys and therefore allow for overriding default hotkeys
|
|
181
|
+
// Mod to match modifier cmd on mac or ctrl on windows
|
|
182
|
+
export class HotKeys {
|
|
183
|
+
edytor;
|
|
184
|
+
userHotKeys;
|
|
185
|
+
plugins;
|
|
186
|
+
hotkeys = new Map();
|
|
187
|
+
get isMac() {
|
|
188
|
+
return typeof window != 'undefined' && /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
|
|
189
|
+
}
|
|
190
|
+
constructor(edytor, userHotKeys = {}, plugins = []) {
|
|
191
|
+
this.edytor = edytor;
|
|
192
|
+
this.userHotKeys = userHotKeys;
|
|
193
|
+
this.plugins = plugins;
|
|
194
|
+
}
|
|
195
|
+
init = () => {
|
|
196
|
+
if (typeof window === 'undefined')
|
|
197
|
+
return;
|
|
198
|
+
// User hotkeys > plugins hotkeys > default hotkeys
|
|
199
|
+
const entries = [
|
|
200
|
+
this.userHotKeys || {},
|
|
201
|
+
...this.plugins.map((plugin) => plugin.hotkeys || {}),
|
|
202
|
+
defaultHotKeys
|
|
203
|
+
];
|
|
204
|
+
entries.forEach((hotKeys) => {
|
|
205
|
+
Object.entries(hotKeys).forEach(([key, func]) => {
|
|
206
|
+
const normalizedKey = this.normalizeKey(key);
|
|
207
|
+
if (this.hotkeys.has(normalizedKey)) {
|
|
208
|
+
this.hotkeys.get(normalizedKey).push(func);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.hotkeys.set(this.normalizeKey(key), [func]);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
normalizeKey = (key) => {
|
|
217
|
+
const parts = key.toLowerCase().split('+');
|
|
218
|
+
const orderedParts = [];
|
|
219
|
+
// Add modifiers in the correct order
|
|
220
|
+
if (parts.includes('mod'))
|
|
221
|
+
orderedParts.push('mod');
|
|
222
|
+
if (parts.includes('alt'))
|
|
223
|
+
orderedParts.push('alt');
|
|
224
|
+
if (parts.includes('ctrl'))
|
|
225
|
+
orderedParts.push('ctrl');
|
|
226
|
+
if (parts.includes('shift'))
|
|
227
|
+
orderedParts.push('shift');
|
|
228
|
+
// Add remaining keys that aren't modifiers
|
|
229
|
+
parts.forEach((part) => {
|
|
230
|
+
if (!['mod', 'alt', 'ctrl', 'shift'].includes(part)) {
|
|
231
|
+
orderedParts.push(part);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
return orderedParts.join('+');
|
|
235
|
+
};
|
|
236
|
+
combination = (e) => {
|
|
237
|
+
const key = e.key.toLowerCase();
|
|
238
|
+
let combination = '';
|
|
239
|
+
// Is modifier
|
|
240
|
+
if (e.ctrlKey || e.metaKey) {
|
|
241
|
+
combination += 'mod+';
|
|
242
|
+
}
|
|
243
|
+
// Is alt
|
|
244
|
+
if (e.altKey) {
|
|
245
|
+
combination += 'alt+';
|
|
246
|
+
}
|
|
247
|
+
// Is ctrl
|
|
248
|
+
if (e.ctrlKey && !e.metaKey) {
|
|
249
|
+
combination += 'ctrl+';
|
|
250
|
+
}
|
|
251
|
+
// Is shift
|
|
252
|
+
if (e.shiftKey) {
|
|
253
|
+
combination += 'shift+';
|
|
254
|
+
}
|
|
255
|
+
if (!escapedKeys.has(key)) {
|
|
256
|
+
combination += key;
|
|
257
|
+
}
|
|
258
|
+
// Remove trailing plus if it exists
|
|
259
|
+
combination = combination.replace(/\+$/, '');
|
|
260
|
+
return combination;
|
|
261
|
+
};
|
|
262
|
+
isHotkey = (e) => {
|
|
263
|
+
const combination = this.combination(e);
|
|
264
|
+
const hotKeys = this.hotkeys.get(combination);
|
|
265
|
+
if (!hotKeys?.length)
|
|
266
|
+
return false;
|
|
267
|
+
try {
|
|
268
|
+
hotKeys.forEach((hotKey) => {
|
|
269
|
+
hotKey({ event: e, edytor: this.edytor, prevent });
|
|
270
|
+
});
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error instanceof PreventionError) {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
error.cb?.();
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
1
|
+
export { default as Edytor } from './components/Edytor.svelte';
|
|
2
|
+
export { useEdytor } from './edytor.svelte.js';
|
|
3
|
+
export { Block } from './block/block.svelte.js';
|
|
4
|
+
export { InlineBlock } from './block/inlineBlock.svelte.js';
|
|
5
|
+
export { Text } from './text/text.svelte.js';
|
|
6
|
+
export { type Plugin } from './plugins.js';
|
|
7
|
+
export * from './plugins/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
}
|
|
1
|
+
export { default as Edytor } from './components/Edytor.svelte';
|
|
2
|
+
export { useEdytor } from './edytor.svelte.js';
|
|
3
|
+
export { Block } from './block/block.svelte.js';
|
|
4
|
+
export { InlineBlock } from './block/inlineBlock.svelte.js';
|
|
5
|
+
export { Text } from './text/text.svelte.js';
|
|
6
|
+
export {} from './plugins.js';
|
|
7
|
+
export * from './plugins/index.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { ObservableV2 } from 'lib0/observable';
|
|
3
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
4
|
+
export declare const PREFERRED_TRIM_SIZE = 500;
|
|
5
|
+
type IdbPersistence = {
|
|
6
|
+
db: IDBDatabase | null;
|
|
7
|
+
doc: Y.Doc;
|
|
8
|
+
_dbref: number;
|
|
9
|
+
_dbsize: number;
|
|
10
|
+
_destroyed: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* @param {IndexeddbPersistence} idbPersistence
|
|
14
|
+
* @param {function(IDBObjectStore):void} [beforeApplyUpdatesCallback]
|
|
15
|
+
* @param {function(IDBObjectStore):void} [afterApplyUpdatesCallback]
|
|
16
|
+
*/
|
|
17
|
+
export declare const fetchUpdates: (idbPersistence: IdbPersistence, beforeApplyUpdatesCallback?: (store: IDBObjectStore) => void, afterApplyUpdatesCallback?: (store: IDBObjectStore) => void) => Promise<IDBObjectStore>;
|
|
18
|
+
/**
|
|
19
|
+
* @param {IndexeddbPersistence} idbPersistence
|
|
20
|
+
* @param {boolean} forceStore
|
|
21
|
+
*/
|
|
22
|
+
export declare const storeState: (idbPersistence: IdbPersistence, forceStore?: boolean) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} name
|
|
25
|
+
*/
|
|
26
|
+
export declare const clearDocument: (name: string) => Promise<any>;
|
|
27
|
+
/**
|
|
28
|
+
* @extends Observable<string>
|
|
29
|
+
*/
|
|
30
|
+
export declare class IndexeddbPersistence extends ObservableV2<{
|
|
31
|
+
synced: (provider: IndexeddbPersistence) => void;
|
|
32
|
+
}> {
|
|
33
|
+
doc: Y.Doc;
|
|
34
|
+
name: string;
|
|
35
|
+
_dbref: number;
|
|
36
|
+
_dbsize: number;
|
|
37
|
+
_destroyed: boolean;
|
|
38
|
+
db: IDBDatabase | null;
|
|
39
|
+
synced: boolean;
|
|
40
|
+
_db: Promise<IDBDatabase>;
|
|
41
|
+
whenSynced: Promise<IndexeddbPersistence>;
|
|
42
|
+
_storeTimeout: number;
|
|
43
|
+
_storeTimeoutId: number | null;
|
|
44
|
+
_storeUpdate: (update: Uint8Array, origin: any) => void;
|
|
45
|
+
awareness: awarenessProtocol.Awareness;
|
|
46
|
+
bcconnected: boolean;
|
|
47
|
+
private _bcSubscriber;
|
|
48
|
+
private _awarenessUpdateHandler;
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} name
|
|
51
|
+
* @param {Y.Doc} doc
|
|
52
|
+
*/
|
|
53
|
+
constructor(name: string, doc: Y.Doc);
|
|
54
|
+
private messageHandlers;
|
|
55
|
+
private readMessage;
|
|
56
|
+
private broadcastMessage;
|
|
57
|
+
private connectBc;
|
|
58
|
+
private disconnectBc;
|
|
59
|
+
/**
|
|
60
|
+
* Destroys this instance and removes all data from indexeddb.
|
|
61
|
+
*
|
|
62
|
+
* @return {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
destroy(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
67
|
+
* @return {Promise<String | number | ArrayBuffer | Date | any>}
|
|
68
|
+
*/
|
|
69
|
+
get(key: string | number | ArrayBuffer | Date): Promise<string | number | any[] | ArrayBuffer | Date>;
|
|
70
|
+
/**
|
|
71
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
72
|
+
* @param {String | number | ArrayBuffer | Date} value
|
|
73
|
+
* @return {Promise<String | number | ArrayBuffer | Date>}
|
|
74
|
+
*/
|
|
75
|
+
set(key: string | number | ArrayBuffer | Date, value: string | number | ArrayBuffer | Date): Promise<any>;
|
|
76
|
+
/**
|
|
77
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
78
|
+
* @return {Promise<undefined>}
|
|
79
|
+
*/
|
|
80
|
+
del(key: string | number | ArrayBuffer | Date): Promise<any>;
|
|
81
|
+
}
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import * as idb from 'lib0/indexeddb';
|
|
3
|
+
import * as promise from 'lib0/promise';
|
|
4
|
+
import { ObservableV2 } from 'lib0/observable';
|
|
5
|
+
import * as bc from 'lib0/broadcastchannel';
|
|
6
|
+
import * as encoding from 'lib0/encoding';
|
|
7
|
+
import * as decoding from 'lib0/decoding';
|
|
8
|
+
import * as syncProtocol from 'y-protocols/sync';
|
|
9
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
10
|
+
const customStoreName = 'custom';
|
|
11
|
+
const updatesStoreName = 'updates';
|
|
12
|
+
const messageSync = 0;
|
|
13
|
+
const messageQueryAwareness = 3;
|
|
14
|
+
const messageAwareness = 1;
|
|
15
|
+
export const PREFERRED_TRIM_SIZE = 500;
|
|
16
|
+
/**
|
|
17
|
+
* @param {IndexeddbPersistence} idbPersistence
|
|
18
|
+
* @param {function(IDBObjectStore):void} [beforeApplyUpdatesCallback]
|
|
19
|
+
* @param {function(IDBObjectStore):void} [afterApplyUpdatesCallback]
|
|
20
|
+
*/
|
|
21
|
+
export const fetchUpdates = (idbPersistence, beforeApplyUpdatesCallback = () => { }, afterApplyUpdatesCallback = () => { }) => {
|
|
22
|
+
const [updatesStore] = idb.transact(idbPersistence.db, [updatesStoreName]);
|
|
23
|
+
return idb
|
|
24
|
+
.getAll(updatesStore, idb.createIDBKeyRangeLowerBound(idbPersistence._dbref, false))
|
|
25
|
+
.then((updates) => {
|
|
26
|
+
if (!idbPersistence._destroyed) {
|
|
27
|
+
beforeApplyUpdatesCallback(updatesStore);
|
|
28
|
+
Y.transact(idbPersistence.doc, () => {
|
|
29
|
+
updates.forEach((val) => Y.applyUpdate(idbPersistence.doc, val));
|
|
30
|
+
}, idbPersistence, false);
|
|
31
|
+
afterApplyUpdatesCallback(updatesStore);
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.then(() => idb.getLastKey(updatesStore).then((lastKey) => {
|
|
35
|
+
idbPersistence._dbref = lastKey + 1;
|
|
36
|
+
}))
|
|
37
|
+
.then(() => idb.count(updatesStore).then((cnt) => {
|
|
38
|
+
idbPersistence._dbsize = cnt;
|
|
39
|
+
}))
|
|
40
|
+
.then(() => updatesStore);
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* @param {IndexeddbPersistence} idbPersistence
|
|
44
|
+
* @param {boolean} forceStore
|
|
45
|
+
*/
|
|
46
|
+
export const storeState = (idbPersistence, forceStore = true) => fetchUpdates(idbPersistence).then((updatesStore) => {
|
|
47
|
+
if (forceStore || idbPersistence._dbsize >= PREFERRED_TRIM_SIZE) {
|
|
48
|
+
idb
|
|
49
|
+
.addAutoKey(updatesStore, Y.encodeStateAsUpdate(idbPersistence.doc))
|
|
50
|
+
.then(() => idb.del(updatesStore, idb.createIDBKeyRangeUpperBound(idbPersistence._dbref, true)))
|
|
51
|
+
.then(() => idb.count(updatesStore).then((cnt) => {
|
|
52
|
+
idbPersistence._dbsize = cnt;
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} name
|
|
58
|
+
*/
|
|
59
|
+
export const clearDocument = (name) => idb.deleteDB(name);
|
|
60
|
+
/**
|
|
61
|
+
* @extends Observable<string>
|
|
62
|
+
*/
|
|
63
|
+
export class IndexeddbPersistence extends ObservableV2 {
|
|
64
|
+
doc;
|
|
65
|
+
name;
|
|
66
|
+
_dbref;
|
|
67
|
+
_dbsize;
|
|
68
|
+
_destroyed;
|
|
69
|
+
db;
|
|
70
|
+
synced;
|
|
71
|
+
_db;
|
|
72
|
+
whenSynced;
|
|
73
|
+
_storeTimeout;
|
|
74
|
+
_storeTimeoutId;
|
|
75
|
+
_storeUpdate;
|
|
76
|
+
awareness;
|
|
77
|
+
bcconnected = false;
|
|
78
|
+
_bcSubscriber;
|
|
79
|
+
_awarenessUpdateHandler;
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} name
|
|
82
|
+
* @param {Y.Doc} doc
|
|
83
|
+
*/
|
|
84
|
+
constructor(name, doc) {
|
|
85
|
+
super();
|
|
86
|
+
this.doc = doc;
|
|
87
|
+
this.name = name;
|
|
88
|
+
this._dbref = 0;
|
|
89
|
+
this._dbsize = 0;
|
|
90
|
+
this._destroyed = false;
|
|
91
|
+
this.db = null;
|
|
92
|
+
this.synced = false;
|
|
93
|
+
this.awareness = new awarenessProtocol.Awareness(doc);
|
|
94
|
+
this._db = idb.openDB(name, (db) => idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']]));
|
|
95
|
+
this.whenSynced = promise.create((resolve) => this.on('synced', () => resolve(this)));
|
|
96
|
+
this._bcSubscriber = (data, origin) => {
|
|
97
|
+
if (origin !== this) {
|
|
98
|
+
const encoder = this.readMessage(new Uint8Array(data), false);
|
|
99
|
+
if (encoding.length(encoder) > 1) {
|
|
100
|
+
bc.publish(this.name, encoding.toUint8Array(encoder), this);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
|
|
105
|
+
const changedClients = added.concat(updated).concat(removed);
|
|
106
|
+
const encoder = encoding.createEncoder();
|
|
107
|
+
encoding.writeVarUint(encoder, messageAwareness);
|
|
108
|
+
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients));
|
|
109
|
+
this.broadcastMessage(encoding.toUint8Array(encoder));
|
|
110
|
+
};
|
|
111
|
+
this._db.then((db) => {
|
|
112
|
+
this.db = db;
|
|
113
|
+
const beforeApplyUpdatesCallback = (updatesStore) => idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(doc));
|
|
114
|
+
const afterApplyUpdatesCallback = () => {
|
|
115
|
+
if (this._destroyed)
|
|
116
|
+
return this;
|
|
117
|
+
this.connectBc();
|
|
118
|
+
};
|
|
119
|
+
fetchUpdates(this, beforeApplyUpdatesCallback, afterApplyUpdatesCallback);
|
|
120
|
+
});
|
|
121
|
+
this._storeTimeout = 1000;
|
|
122
|
+
this._storeTimeoutId = null;
|
|
123
|
+
this._storeUpdate = (update, origin) => {
|
|
124
|
+
if (this.db && origin !== this) {
|
|
125
|
+
const [updatesStore] = idb.transact(this.db, [updatesStoreName]);
|
|
126
|
+
idb.addAutoKey(updatesStore, update);
|
|
127
|
+
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
|
|
128
|
+
if (this._storeTimeoutId !== null) {
|
|
129
|
+
clearTimeout(this._storeTimeoutId);
|
|
130
|
+
}
|
|
131
|
+
this._storeTimeoutId = setTimeout(() => {
|
|
132
|
+
storeState(this, false);
|
|
133
|
+
this._storeTimeoutId = null;
|
|
134
|
+
}, this._storeTimeout);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Broadcast the update to other tabs
|
|
138
|
+
if (origin !== this) {
|
|
139
|
+
const encoder = encoding.createEncoder();
|
|
140
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
141
|
+
syncProtocol.writeUpdate(encoder, update);
|
|
142
|
+
this.broadcastMessage(encoding.toUint8Array(encoder));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
doc.on('update', this._storeUpdate);
|
|
146
|
+
this.awareness.on('update', this._awarenessUpdateHandler);
|
|
147
|
+
this.destroy = this.destroy.bind(this);
|
|
148
|
+
doc.on('destroy', this.destroy);
|
|
149
|
+
globalThis.addEventListener('beforeunload', () => this.destroy());
|
|
150
|
+
}
|
|
151
|
+
messageHandlers = {
|
|
152
|
+
[messageSync]: (encoder, decoder, provider, _emitSynced) => {
|
|
153
|
+
encoding.writeVarUint(encoder, messageSync);
|
|
154
|
+
const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider);
|
|
155
|
+
if (syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) {
|
|
156
|
+
//
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[messageQueryAwareness]: (encoder, _decoder, provider) => {
|
|
160
|
+
encoding.writeVarUint(encoder, messageAwareness);
|
|
161
|
+
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys())));
|
|
162
|
+
},
|
|
163
|
+
[messageAwareness]: (_encoder, decoder, provider) => {
|
|
164
|
+
awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
readMessage = (buf, emitSynced) => {
|
|
168
|
+
const decoder = decoding.createDecoder(buf);
|
|
169
|
+
const encoder = encoding.createEncoder();
|
|
170
|
+
const messageType = decoding.readVarUint(decoder);
|
|
171
|
+
const messageHandler = this.messageHandlers[messageType];
|
|
172
|
+
if (messageHandler) {
|
|
173
|
+
messageHandler(encoder, decoder, this, emitSynced);
|
|
174
|
+
}
|
|
175
|
+
return encoder;
|
|
176
|
+
};
|
|
177
|
+
broadcastMessage(buf) {
|
|
178
|
+
if (this.bcconnected) {
|
|
179
|
+
bc.publish(this.name, buf, this);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
connectBc() {
|
|
183
|
+
if (!this.bcconnected) {
|
|
184
|
+
bc.subscribe(this.name, this._bcSubscriber);
|
|
185
|
+
this.bcconnected = true;
|
|
186
|
+
}
|
|
187
|
+
// Sync initial state
|
|
188
|
+
const encoderSync = encoding.createEncoder();
|
|
189
|
+
encoding.writeVarUint(encoderSync, messageSync);
|
|
190
|
+
syncProtocol.writeSyncStep1(encoderSync, this.doc);
|
|
191
|
+
bc.publish(this.name, encoding.toUint8Array(encoderSync), this);
|
|
192
|
+
const encoderState = encoding.createEncoder();
|
|
193
|
+
encoding.writeVarUint(encoderState, messageSync);
|
|
194
|
+
syncProtocol.writeSyncStep2(encoderState, this.doc);
|
|
195
|
+
bc.publish(this.name, encoding.toUint8Array(encoderState), this);
|
|
196
|
+
// Sync awareness state
|
|
197
|
+
const encoderAwarenessQuery = encoding.createEncoder();
|
|
198
|
+
encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
|
|
199
|
+
bc.publish(this.name, encoding.toUint8Array(encoderAwarenessQuery), this);
|
|
200
|
+
const encoderAwarenessState = encoding.createEncoder();
|
|
201
|
+
encoding.writeVarUint(encoderAwarenessState, messageAwareness);
|
|
202
|
+
encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID]));
|
|
203
|
+
bc.publish(this.name, encoding.toUint8Array(encoderAwarenessState), this);
|
|
204
|
+
this.synced = true;
|
|
205
|
+
this.emit('synced', [this]);
|
|
206
|
+
}
|
|
207
|
+
disconnectBc() {
|
|
208
|
+
// Notify other clients about disconnection
|
|
209
|
+
const encoder = encoding.createEncoder();
|
|
210
|
+
encoding.writeVarUint(encoder, messageAwareness);
|
|
211
|
+
encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map()));
|
|
212
|
+
this.broadcastMessage(encoding.toUint8Array(encoder));
|
|
213
|
+
if (this.bcconnected) {
|
|
214
|
+
bc.unsubscribe(this.name, this._bcSubscriber);
|
|
215
|
+
this.bcconnected = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Destroys this instance and removes all data from indexeddb.
|
|
220
|
+
*
|
|
221
|
+
* @return {Promise<void>}
|
|
222
|
+
*/
|
|
223
|
+
destroy() {
|
|
224
|
+
if (this._storeTimeoutId) {
|
|
225
|
+
clearTimeout(this._storeTimeoutId);
|
|
226
|
+
}
|
|
227
|
+
this.doc.off('update', this._storeUpdate);
|
|
228
|
+
this.doc.off('destroy', this.destroy);
|
|
229
|
+
this.awareness.off('update', this._awarenessUpdateHandler);
|
|
230
|
+
this._destroyed = true;
|
|
231
|
+
this.disconnectBc();
|
|
232
|
+
return this._db.then((db) => {
|
|
233
|
+
db.close();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
238
|
+
* @return {Promise<String | number | ArrayBuffer | Date | any>}
|
|
239
|
+
*/
|
|
240
|
+
get(key) {
|
|
241
|
+
return this._db.then((db) => {
|
|
242
|
+
const [custom] = idb.transact(db, [customStoreName], 'readonly');
|
|
243
|
+
return idb.get(custom, key);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
248
|
+
* @param {String | number | ArrayBuffer | Date} value
|
|
249
|
+
* @return {Promise<String | number | ArrayBuffer | Date>}
|
|
250
|
+
*/
|
|
251
|
+
set(key, value) {
|
|
252
|
+
return this._db.then((db) => {
|
|
253
|
+
const [custom] = idb.transact(db, [customStoreName]);
|
|
254
|
+
return idb.put(custom, value, key);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* @param {String | number | ArrayBuffer | Date} key
|
|
259
|
+
* @return {Promise<undefined>}
|
|
260
|
+
*/
|
|
261
|
+
del(key) {
|
|
262
|
+
return this._db.then((db) => {
|
|
263
|
+
const [custom] = idb.transact(db, [customStoreName]);
|
|
264
|
+
return idb.del(custom, key);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|