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
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
package/dist/index.js
CHANGED
|
@@ -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
|
+
}
|