@ebl-vue/editor-full 2.31.35 → 2.31.37

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 (97) hide show
  1. package/.postcssrc.yml +33 -0
  2. package/dist/index.d.ts +3 -7
  3. package/dist/index.mjs +184 -183
  4. package/dist/index.mjs.map +1 -0
  5. package/package.json +1 -1
  6. package/postcss.config.js +15 -0
  7. package/src/components/Editor/Editor.vue +293 -0
  8. package/src/components/index.ts +27 -0
  9. package/src/constants/index.ts +1 -0
  10. package/src/i18n/zh-cn.ts +160 -0
  11. package/src/icons/index.ts +93 -0
  12. package/src/index.ts +21 -0
  13. package/src/installer.ts +21 -0
  14. package/src/plugins/alert/index.ts +455 -0
  15. package/src/plugins/block-alignment/index.ts +117 -0
  16. package/src/plugins/block-alignment/readme.md +1 -0
  17. package/src/plugins/code/LICENSE +21 -0
  18. package/src/plugins/code/index.ts +619 -0
  19. package/src/plugins/code/utils/string.ts +34 -0
  20. package/src/plugins/color-picker/index.ts +132 -0
  21. package/src/plugins/delimiter/index.ts +121 -0
  22. package/src/plugins/drag-drop/index.css +19 -0
  23. package/src/plugins/drag-drop/index.ts +151 -0
  24. package/src/plugins/drag-drop/readme.md +1 -0
  25. package/src/plugins/header/H1.ts +404 -0
  26. package/src/plugins/header/H2.ts +403 -0
  27. package/src/plugins/header/H3.ts +404 -0
  28. package/src/plugins/header/H4.ts +404 -0
  29. package/src/plugins/header/H5.ts +403 -0
  30. package/src/plugins/header/H6.ts +404 -0
  31. package/src/plugins/header/index.ts +15 -0
  32. package/src/plugins/header/types.d.ts +46 -0
  33. package/src/plugins/imageResizeCrop/ImageTune.ts +635 -0
  34. package/src/plugins/imageResizeCrop/index.css +230 -0
  35. package/src/plugins/imageResizeCrop/index.ts +5 -0
  36. package/src/plugins/imageResizeCrop/types.d.ts +23 -0
  37. package/src/plugins/imageTool/index.ts +510 -0
  38. package/src/plugins/imageTool/types/codexteam__ajax.d.ts +89 -0
  39. package/src/plugins/imageTool/types/types.ts +236 -0
  40. package/src/plugins/imageTool/ui.ts +313 -0
  41. package/src/plugins/imageTool/uploader.ts +293 -0
  42. package/src/plugins/imageTool/utils/dom.ts +24 -0
  43. package/src/plugins/imageTool/utils/index.ts +73 -0
  44. package/src/plugins/imageTool/utils/isPromise.ts +10 -0
  45. package/src/plugins/indent/index.ts +695 -0
  46. package/src/plugins/inline-code/index.ts +203 -0
  47. package/src/plugins/list/ListRenderer/ChecklistRenderer.ts +208 -0
  48. package/src/plugins/list/ListRenderer/ListRenderer.ts +73 -0
  49. package/src/plugins/list/ListRenderer/OrderedListRenderer.ts +123 -0
  50. package/src/plugins/list/ListRenderer/UnorderedListRenderer.ts +123 -0
  51. package/src/plugins/list/ListRenderer/index.ts +6 -0
  52. package/src/plugins/list/ListTabulator/index.ts +1179 -0
  53. package/src/plugins/list/index.ts +488 -0
  54. package/src/plugins/list/styles/CssPrefix.ts +4 -0
  55. package/src/plugins/list/types/Elements.ts +14 -0
  56. package/src/plugins/list/types/ItemMeta.ts +40 -0
  57. package/src/plugins/list/types/ListParams.ts +102 -0
  58. package/src/plugins/list/types/ListRenderer.ts +6 -0
  59. package/src/plugins/list/types/OlCounterType.ts +63 -0
  60. package/src/plugins/list/types/index.ts +14 -0
  61. package/src/plugins/list/utils/focusItem.ts +18 -0
  62. package/src/plugins/list/utils/getChildItems.ts +40 -0
  63. package/src/plugins/list/utils/getItemChildWrapper.ts +10 -0
  64. package/src/plugins/list/utils/getItemContentElement.ts +10 -0
  65. package/src/plugins/list/utils/getSiblings.ts +52 -0
  66. package/src/plugins/list/utils/isLastItem.ts +9 -0
  67. package/src/plugins/list/utils/itemHasSublist.ts +10 -0
  68. package/src/plugins/list/utils/normalizeData.ts +83 -0
  69. package/src/plugins/list/utils/removeChildWrapperIfEmpty.ts +31 -0
  70. package/src/plugins/list/utils/renderToolboxInput.ts +113 -0
  71. package/src/plugins/list/utils/stripNumbers.ts +7 -0
  72. package/src/plugins/list/utils/type-guards.ts +8 -0
  73. package/src/plugins/marker/index.ts +199 -0
  74. package/src/plugins/outline/index.ts +62 -0
  75. package/src/plugins/outline/outline.css +52 -0
  76. package/src/plugins/paragraph/index.ts +384 -0
  77. package/src/plugins/paragraph/types/icons.d.ts +4 -0
  78. package/src/plugins/paragraph/utils/makeFragment.ts +17 -0
  79. package/src/plugins/quote/index.ts +203 -0
  80. package/src/plugins/table/index.ts +4 -0
  81. package/src/plugins/table/plugin.ts +255 -0
  82. package/src/plugins/table/table.ts +1202 -0
  83. package/src/plugins/table/toolbox.ts +166 -0
  84. package/src/plugins/table/utils/dom.ts +130 -0
  85. package/src/plugins/table/utils/popover.ts +185 -0
  86. package/src/plugins/table/utils/throttled.ts +22 -0
  87. package/src/plugins/underline/index.ts +214 -0
  88. package/src/plugins/undo/index.ts +526 -0
  89. package/src/plugins/undo/observer.ts +101 -0
  90. package/src/plugins/undo/vanilla-caret-js.ts +102 -0
  91. package/src/style.css +139 -0
  92. package/src/types.ts +3 -0
  93. package/src/utils/AxiosService.ts +87 -0
  94. package/src/utils/index.ts +15 -0
  95. package/src/utils/install.ts +19 -0
  96. package/tsconfig.json +37 -0
  97. package/vite.config.ts +81 -0
@@ -0,0 +1,526 @@
1
+ import VanillaCaret from "./vanilla-caret-js";
2
+ import Observer from "./observer";
3
+ import type EditorJS from '@ebl-vue/editorjs';
4
+
5
+ /**
6
+ * https://github.com/kommitters/editorjs-undo/tree/main
7
+ * Undo/Redo feature for Editor.js.
8
+ *
9
+ * @typedef {Object} Undo
10
+ * @description Feature's initialization class.
11
+ * @property {Object} editor — Editor.js instance object.
12
+ * @property {Number} maxLength - Max amount of changes recorded by the history stack.
13
+ * @property {Function} onUpdate - Callback called when the user performs an undo or redo action.
14
+ * @property {Boolean} shouldSaveHistory - Defines if the plugin should save the change in the stack
15
+ * @property {Object} initialItem - Initial data object.
16
+ */
17
+ export default class Undo {
18
+ editor: EditorJS | null;
19
+ holder: any;
20
+ defaultBlock: any;
21
+ blocks: any;
22
+ caret: any;
23
+ shouldSaveHistory: boolean;
24
+ readOnly: any;
25
+ maxLength: any;
26
+ onUpdate: any;
27
+ config: { debounceTimer: any; shortcuts: { undo: any; redo: any; }; };
28
+ initialItem: any;
29
+ stack: any;
30
+ position: number=0;
31
+ /**
32
+ * @param options — Plugin custom options.
33
+ */
34
+ constructor({ editor, config = {}, onUpdate, maxLength }:{ editor: EditorJS, config?: any, onUpdate?: any, maxLength?: any }) {
35
+ const defaultOptions = {
36
+ maxLength: 30,
37
+ onUpdate() {},
38
+ config: {
39
+ debounceTimer: 200,
40
+ shortcuts: {
41
+ undo: ["CMD+Z"],
42
+ redo: ["CMD+Y", "CMD+SHIFT+Z"],
43
+ },
44
+ },
45
+ };
46
+
47
+ const { blocks, caret } = editor;
48
+ const { configuration } = editor as any;
49
+ const { holder, defaultBlock } = configuration;
50
+ const defaultShortcuts = defaultOptions.config.shortcuts;
51
+ const { shortcuts: configShortcuts } = config;
52
+ const shortcuts = { ...defaultShortcuts, ...configShortcuts };
53
+ const undo = Array.isArray(shortcuts.undo) ? shortcuts.undo : [shortcuts.undo];
54
+ const redo = Array.isArray(shortcuts.redo) ? shortcuts.redo : [shortcuts.redo];
55
+ const defaultDebounceTimer = defaultOptions.config.debounceTimer;
56
+ const { debounceTimer = defaultDebounceTimer } = config;
57
+
58
+ this.holder =
59
+ typeof holder === "string" ? document.getElementById(holder) : holder;
60
+ this.editor = editor;
61
+ this.defaultBlock = defaultBlock;
62
+ this.blocks = blocks;
63
+ this.caret = caret;
64
+ this.shouldSaveHistory = true;
65
+ this.readOnly = configuration.readOnly;
66
+ this.maxLength = maxLength || defaultOptions.maxLength;
67
+ this.onUpdate = onUpdate || defaultOptions.onUpdate;
68
+ this.config = { debounceTimer, shortcuts: { undo, redo } };
69
+
70
+ const observer = new Observer(
71
+ () => this.registerChange(),
72
+ this.holder,
73
+ this.config.debounceTimer
74
+ );
75
+ observer.setMutationObserver();
76
+
77
+ this.setEventListeners();
78
+ this.initialItem = null;
79
+ this.clear();
80
+ }
81
+
82
+ /**
83
+ * Notify core that read-only mode is suppoorted
84
+ *
85
+ * @returns {boolean}
86
+ */
87
+ static get isReadOnlySupported() {
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Truncates the history stack when it excedes the limit of changes.
93
+ *
94
+ * @param {Object} stack Changes history stack.
95
+ * @param {Number} stack Limit of changes recorded by the history stack.
96
+ */
97
+ truncate(stack: any, limit: number) {
98
+ while (stack.length > limit) {
99
+ stack.shift();
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Initializes the stack when the user provides initial data.
105
+ *
106
+ * @param {Object} initialItem Initial data provided by the user.
107
+ */
108
+ initialize(initialItem: any) {
109
+ const initialData =
110
+ "blocks" in initialItem ? initialItem.blocks : initialItem;
111
+ const initialIndex = initialData.length - 1;
112
+ const firstElement = { index: initialIndex, state: initialData };
113
+ this.stack[0] = firstElement;
114
+ this.initialItem = firstElement;
115
+ }
116
+
117
+ /**
118
+ * Clears the history stack.
119
+ */
120
+ clear() {
121
+ this.stack = this.initialItem
122
+ ? [this.initialItem]
123
+ : [{ index: 0, state: [{ type: this.defaultBlock, data: {} }] }];
124
+ this.position = 0;
125
+ this.onUpdate();
126
+ }
127
+
128
+ /**
129
+ * Returns true if readOnly was toggled to true
130
+ * @returns {Node} Indirectly shows if readOnly was set to true or false
131
+ */
132
+ setReadOnly() {
133
+ const toolbox = this.holder.querySelector(".ce-toolbox");
134
+ this.readOnly = !toolbox;
135
+ }
136
+
137
+ /**
138
+ * Registers the data returned by API's save method into the history stack.
139
+ */
140
+ registerChange() {
141
+ this.setReadOnly();
142
+ if (!this.readOnly) {
143
+ if (this.editor && this.editor.save && this.shouldSaveHistory) {
144
+ this.editor.save().then((savedData) => {
145
+ if (this.editorDidUpdate(savedData.blocks))
146
+ this.save(savedData.blocks);
147
+ });
148
+ }
149
+ this.shouldSaveHistory = true;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Checks if the saved data has to be added to the history stack.
155
+ *
156
+ * @param {Object} newData New data to be saved in the history stack.
157
+ * @returns {Boolean}
158
+ */
159
+ editorDidUpdate(newData: any) {
160
+ const { state } = this.stack[this.position];
161
+ if (!newData.length) return false;
162
+ if (newData.length !== state.length) return true;
163
+
164
+ return JSON.stringify(state) !== JSON.stringify(newData);
165
+ }
166
+
167
+ /**
168
+ * Adds the saved data in the history stack and updates current position.
169
+ */
170
+ save(state: any) {
171
+ if (this.position >= this.maxLength) {
172
+ this.truncate(this.stack, this.maxLength);
173
+ }
174
+ this.position = Math.min(this.position, this.stack.length - 1);
175
+
176
+ this.stack = this.stack.slice(0, this.position + 1);
177
+
178
+ const index = this.blocks.getCurrentBlockIndex();
179
+ const blockCount = this.blocks.getBlocksCount();
180
+ let indexInState = index;
181
+
182
+ if (!state[index]) indexInState -= blockCount - state.length;
183
+ const caretIndex =
184
+ state[indexInState] && (
185
+ state[indexInState].type === "paragraph" ||
186
+ state[indexInState].type === "header")
187
+ ? this.getCaretIndex(index)
188
+ : null;
189
+ this.stack.push({ index: indexInState, state, caretIndex });
190
+ this.position += 1;
191
+ this.onUpdate();
192
+ }
193
+
194
+ /**
195
+ * Gets the caret position.
196
+ * @param {Number} index is the block index
197
+ * @returns The caret position
198
+ */
199
+ getCaretIndex(index: number) {
200
+ const blocks = this.holder.getElementsByClassName("ce-block__content");
201
+ const caretBlock = new VanillaCaret(blocks[index].firstChild);
202
+
203
+ return caretBlock.getPos();
204
+ }
205
+
206
+ /**
207
+ * Inserts a block deleted previously
208
+ * @param {Array} state is the current state according to this.position.
209
+ * @param {Array} compState is the state to compare and know the deleted block.
210
+ * @param {Number} index is the block index in state.
211
+ */
212
+ insertDeletedBlock(state: any, compState: any, index: number) {
213
+ for (let i = 0; i < state.length; i += 1) {
214
+ if (!compState[i] || state[i].id !== compState[i].id) {
215
+ this.blocks.insert(state[i].type, state[i].data, {}, i, true);
216
+ this.caret.setToBlock(index, "end");
217
+ break;
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Returns true if a block was dropped previously
224
+ * @param {Array} state is the current state according to this.position.
225
+ * @param {Array} compState is the state to compare and know the dropped block.
226
+ * @returns {Boolean} true if the block was dropped
227
+ */
228
+ blockWasDropped(state: any, compState: any) {
229
+ if (state.length === compState.length) {
230
+ return state.some((block: any, i: number) => block.id !== compState[i].id);
231
+ }
232
+ return false;
233
+ }
234
+
235
+ /**
236
+ * Returns true if the block has to be deleted because it was skipped previously.
237
+ * @param {Array} state is the current state according to this.position.
238
+ * @param {Array} compState is the state to compare if there was a deleted block.
239
+ * @returns {Boolean} true if a block was inserted previously.
240
+ */
241
+ blockWasSkipped(state: any, compState: any) {
242
+ return state.length !== compState.length;
243
+ }
244
+
245
+ /**
246
+ * Returns true if the content in a block without the focus was modified.
247
+ * @param {Number} index is the block index in state.
248
+ * @param {Number} compIndex is the index to compare and know if the block was inserted previously
249
+ * @returns true if the content in a block without the focus was modified.
250
+ */
251
+ contentChangedInNoFocusBlock(index: number, compIndex: number) {
252
+ return index !== compIndex;
253
+ }
254
+
255
+ /**
256
+ * Returns true if a block was deleted previously.
257
+ * @param {Array} state is the current state according to this.position.
258
+ * @param {Array} compState is the state to compare and know if a block was deleted.
259
+ * @returns {Boolean} true if a block was deleted previously.
260
+ */
261
+ blockWasDeleted(state: any, compState: any) {
262
+ return state.length > compState.length;
263
+ }
264
+
265
+ /**
266
+ * Returns true if the content was copied.
267
+ * @param {Array} state is the current state according to this.position.
268
+ * @param {Array} compState is the state to compare and know if the content was copied.
269
+ * @param {Number} index is the block index in state.
270
+ * @returns {Boolean} true if a block was deleted previously.
271
+ */
272
+ contentWasCopied(state: any, compState: any, index: number) {
273
+ return state[index]&&state[index].data&&Object.keys(state[index].data).length === 0
274
+ && JSON.stringify(compState[index + 1]) !== JSON.stringify(state[index + 1]);
275
+ }
276
+
277
+ /**
278
+ * Decreases the current position and update the respective block in the editor.
279
+ */
280
+ async undo() {
281
+ if (this.canUndo()) {
282
+ const { index: nextIndex, state: nextState } = this.stack[this.position];
283
+
284
+ this.position -= 1;
285
+ this.shouldSaveHistory = false;
286
+ let { index } = this.stack[this.position];
287
+ const { state, caretIndex } = this.stack[this.position];
288
+
289
+ this.onUpdate();
290
+ const blockCount = this.blocks.getBlocksCount();
291
+
292
+ if (!state[index]) {
293
+ index -= 1;
294
+ this.stack[this.position].index = index;
295
+ }
296
+
297
+ if (this.blockWasDeleted(state, nextState)) {
298
+ this.insertDeletedBlock(state, nextState, index);
299
+ } else if (this.contentWasCopied(state, nextState, index)) {
300
+ await this.blocks.render({ blocks: state });
301
+ this.caret.setToBlock(index, 'end');
302
+ } else if (index < nextIndex && this.blockWasSkipped(state, nextState)) {
303
+ await this.blocks.delete(nextIndex);
304
+ this.caret.setToBlock(index, "end");
305
+ } else if (blockCount > state.length) {
306
+ await this.blocks.render({ blocks: state });
307
+ this.setCaretIndex(index, caretIndex);
308
+ } else if (this.blockWasDropped(state, nextState)) {
309
+ await this.blocks.render({ blocks: state });
310
+ this.caret.setToBlock(index, 'end');
311
+ } else if (this.contentChangedInNoFocusBlock(index, nextIndex)) {
312
+ const byBlock = this.blocks.getBlockByIndex(nextIndex);
313
+ //const { id } = this.blocks.getBlockByIndex(nextIndex);
314
+ if (byBlock.id) {
315
+ await this.blocks.update(byBlock.id, state[nextIndex].data);
316
+ this.setCaretIndex(index, caretIndex);
317
+ }
318
+ }
319
+
320
+ const block = this.blocks.getBlockByIndex(index);
321
+ if (block) {
322
+ await this.blocks.update(block.id, state[index].data);
323
+ this.setCaretIndex(index, caretIndex);
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Sets the caret position.
330
+ * @param {Number} index is the block index
331
+ * @param {Number} caretIndex is the caret position
332
+ * @param {Array} state is the current state according to this.position.
333
+ */
334
+ setCaretIndex(index: number, caretIndex: number) {
335
+ if (caretIndex && caretIndex !== -1) {
336
+ const blocks = this.holder.getElementsByClassName("ce-block__content");
337
+ const caretBlock = new VanillaCaret(blocks[index].firstChild);
338
+ setTimeout(() => caretBlock.setPos(caretIndex), 50);
339
+ } else {
340
+ this.caret.setToBlock(index, "end");
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Inserts new block
346
+ * @param {Array} state is the current state according to this.position.
347
+ * @param {Number} index is the block index
348
+ */
349
+ async insertBlock(state: any, index: number) {
350
+ await this.blocks.insert(
351
+ state[index].type,
352
+ state[index].data,
353
+ {},
354
+ index,
355
+ true,
356
+ );
357
+ }
358
+
359
+ /**
360
+ * Inserts a block when is skipped and update the previous one if it changed.
361
+ * @param {Array} prevState is the previous state according to this.position.
362
+ * @param {Array} state is the current state according to this.position.
363
+ * @param {Number} index is the block index.
364
+ */
365
+ async insertSkippedBlocks(prevState: any, state: any, index: number) {
366
+ for (let i = prevState.length; i < state.length; i += 1) {
367
+ this.insertBlock(state, i);
368
+ }
369
+
370
+ if (JSON.stringify(prevState[index - 1]) !== JSON.stringify(state[index - 1])) {
371
+ await this.updateModifiedBlock(state, index);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Updates the passed block or render the state when the content was copied.
377
+ * @param {Array} state is the current state according to this.position.
378
+ * @param {Number} index is the block index.
379
+ */
380
+ async updateModifiedBlock(state: any, index: number) {
381
+ const block = state[index - 1];
382
+ if (this.editor!.blocks.getById(block.id)) return this.blocks.update(block.id, block.data);
383
+ return this.blocks.render({ blocks: state });
384
+ }
385
+
386
+ /**
387
+ * Increases the current position and update the respective block in the editor.
388
+ */
389
+ async redo() {
390
+ if (this.canRedo()) {
391
+ this.position += 1;
392
+ this.shouldSaveHistory = false;
393
+ const { index, state, caretIndex } = this.stack[(this.position)];
394
+ const { index:prevIndex, state: prevState } =
395
+ this.stack[this.position - 1];
396
+ console.log(prevIndex);
397
+ if (this.blockWasDeleted(prevState, state)) {
398
+ await this.blocks.delete();
399
+ this.caret.setToBlock(index, "end");
400
+ } else if (this.blockWasSkipped(state, prevState)) {
401
+ await this.insertSkippedBlocks(prevState, state, index);
402
+ this.caret.setToBlock(index, 'end');
403
+ } else if (this.blockWasDropped(state, prevState) && this.position !== 1) {
404
+ await this.blocks.render({ blocks: state });
405
+ this.caret.setToBlock(index, "end");
406
+ }
407
+
408
+ this.onUpdate();
409
+ const block = this.blocks.getBlockByIndex(index);
410
+ if (block) {
411
+ await this.blocks.update(block.id, state[index].data);
412
+ this.setCaretIndex(index, caretIndex);
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Checks if the history stack can perform an undo action.
419
+ *
420
+ * @returns {Boolean}
421
+ */
422
+ canUndo() {
423
+ return !this.readOnly && this.position > 0;
424
+ }
425
+
426
+ /**
427
+ * Checks if the history stack can perform a redo action.
428
+ *
429
+ * @returns {Boolean}
430
+ */
431
+ canRedo() {
432
+ return !this.readOnly && this.position < this.count();
433
+ }
434
+
435
+ /**
436
+ * Returns the number of changes recorded in the history stack.
437
+ *
438
+ * @returns {Number}
439
+ */
440
+ count() {
441
+ return this.stack.length - 1; // -1 because of initial item
442
+ }
443
+
444
+ /**
445
+ * Parses the keys passed in the shortcut property to accept CMD,ALT and SHIFT
446
+ *
447
+ * @param {Array} keys are the keys passed in shortcuts in config
448
+ * @returns {Array}
449
+ */
450
+
451
+ parseKeys(keys: string[]) {
452
+ const specialKeys: any = {
453
+ CMD: /(Mac)/i.test(navigator.platform) ? "metaKey" : "ctrlKey",
454
+ ALT: "altKey",
455
+ SHIFT: "shiftKey",
456
+ };
457
+ const parsedKeys = keys.slice(0, -1).map((key) => specialKeys[key]);
458
+
459
+ const letterKey =
460
+ parsedKeys.includes("shiftKey") && keys.length === 2
461
+ ? keys[keys.length - 1].toUpperCase()
462
+ : keys[keys.length - 1].toLowerCase();
463
+
464
+ parsedKeys.push(letterKey);
465
+ return parsedKeys;
466
+ }
467
+
468
+ /**
469
+ * Sets events listeners to allow keyboard actions support
470
+ */
471
+
472
+ setEventListeners() {
473
+ const { holder } = this;
474
+ const { shortcuts } = this.config;
475
+ const { undo, redo } = shortcuts;
476
+ const keysUndo = undo.map((undoShortcut: string) => undoShortcut.replace(/ /g, "").split("+"));
477
+ const keysRedo = redo.map((redoShortcut: string) => redoShortcut.replace(/ /g, "").split("+"));
478
+
479
+ const keysUndoParsed = keysUndo.map((keys: string[]) => this.parseKeys(keys));
480
+ const keysRedoParsed = keysRedo.map((keys: string[]) => this.parseKeys(keys));
481
+
482
+ const twoKeysPressed = (e: any, keys: any) => {
483
+ return keys.length === 2 && e[keys[0]] && (e.key.toLowerCase() === keys[1]);
484
+ }
485
+ const threeKeysPressed = (e: any, keys: any) => {
486
+ return keys.length === 3 && e[keys[0]] && e[keys[1]] && (e.key.toLowerCase() === keys[2]);
487
+ }
488
+ const verifyListTwoKeysPressed = (e: KeyboardEvent, keysList:string[]) =>
489
+ keysList.reduce((result, keys:string) => result || twoKeysPressed(e, keys), false);
490
+ const verifyListThreeKeysPressed = (e: KeyboardEvent, keysList:string[]) =>
491
+ keysList.reduce((result, keys) => result || threeKeysPressed(e, keys), false);
492
+
493
+ const pressedKeys = (e: KeyboardEvent, keys: string[], compKeys: string[]) => {
494
+ if (verifyListTwoKeysPressed(e, keys) && !verifyListThreeKeysPressed(e, compKeys)) {
495
+ return true;
496
+ }
497
+ if (verifyListThreeKeysPressed(e, keys)) {
498
+ return true;
499
+ }
500
+ return false;
501
+ };
502
+
503
+ const handleUndo = (e: KeyboardEvent) => {
504
+ if (pressedKeys(e, keysUndoParsed, keysRedoParsed)) {
505
+ e.preventDefault();
506
+ this.undo();
507
+ }
508
+ };
509
+
510
+ const handleRedo = (e: KeyboardEvent) => {
511
+ if (pressedKeys(e, keysRedoParsed, keysUndoParsed)) {
512
+ e.preventDefault();
513
+ this.redo();
514
+ }
515
+ };
516
+
517
+ const handleDestroy = () => {
518
+ holder.removeEventListener("keydown", handleUndo);
519
+ holder.removeEventListener("keydown", handleRedo);
520
+ };
521
+
522
+ holder.addEventListener("keydown", handleUndo);
523
+ holder.addEventListener("keydown", handleRedo);
524
+ holder.addEventListener("destroy", handleDestroy);
525
+ }
526
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @typedef {Object} Observer
3
+ * @description Custom MutationObserver to detect changes in the editor.
4
+ * @property {String} holder — Editor.js holder id.
5
+ * @property {Object} observer - MutationObserver object that detects changes in the editor.
6
+ * @property {Number} debounceTimer - Delay time for the debouncer.
7
+ * @property {Function} mutationDebouncer - Debouncer to delay the changes registration.
8
+ */
9
+ export default class Observer {
10
+ private observer: MutationObserver | null;
11
+ private debounceTimer: number;
12
+ private mutationDebouncer: Function;
13
+ private holder: HTMLElement;
14
+ /**
15
+ * Creates a new instance of the Observer object.
16
+ * @param {Function} registerChange - Function that register a change in the history stack.
17
+ * @param {String} holder - Editor.js holder id.
18
+ * @param {Number} debounceTimer Delay time for the debouncer.
19
+ */
20
+ constructor(registerChange: Function, holder: HTMLElement, debounceTimer: number) {
21
+
22
+ this.holder = holder;
23
+ this.observer = null;
24
+ this.debounceTimer = debounceTimer;
25
+ this.mutationDebouncer = this.debounce(() => {
26
+ registerChange();
27
+ }, this.debounceTimer);
28
+ }
29
+
30
+ /**
31
+ * Sets a mutation observer to catch every change in the editor.
32
+ */
33
+ setMutationObserver() {
34
+ const observerOptions = {
35
+ childList: true,
36
+ attributes: true,
37
+ subtree: true,
38
+ characterData: true,
39
+ characterDataOldValue: true,
40
+ };
41
+
42
+ const target = this.holder.querySelector('.codex-editor__redactor') as HTMLElement;
43
+
44
+ this.observer = new MutationObserver((mutationList) => {
45
+ this.mutationHandler(mutationList);
46
+ });
47
+ this.observer.observe(target, observerOptions);
48
+ }
49
+
50
+ /**
51
+ * Handles the mutations and checks if a new mutation has been produced.
52
+ * @param {Object} mutationList The registered mutations
53
+ */
54
+ mutationHandler(mutationList: any[]) {
55
+ let contentMutated = false;
56
+
57
+ mutationList.forEach((mutation:any) => {
58
+ switch (mutation.type) {
59
+ case 'childList':
60
+ if (mutation.target === this.holder) {
61
+ this.onDestroy();
62
+ } else {
63
+ contentMutated = true;
64
+ }
65
+ break;
66
+ case 'characterData':
67
+ contentMutated = true;
68
+ break;
69
+ case 'attributes':
70
+ if (!mutation.target.classList.contains('ce-block') && !mutation.target.classList.contains('tc-toolbox')) {
71
+ contentMutated = true;
72
+ }
73
+ break;
74
+ default:
75
+ break;
76
+ }
77
+ });
78
+
79
+ if (contentMutated) this.mutationDebouncer();
80
+ }
81
+
82
+ /**
83
+ * Delays invoking a function until after wait millis have elapsed.
84
+ * @param {Function} callback The function to be delayed.
85
+ * @param {Number} wait The deplay time in millis.
86
+ */
87
+ debounce(callback: Function, wait: number) {
88
+ let timeout: any;
89
+ return (...args:any) => {
90
+ const context = this;
91
+ clearTimeout(timeout);
92
+ timeout = setTimeout(() => callback.apply(context, args), wait);
93
+ };
94
+ }
95
+
96
+ onDestroy() {
97
+ const destroyEvent = new CustomEvent('destroy');
98
+ document.dispatchEvent(destroyEvent);
99
+ this.observer?.disconnect();
100
+ }
101
+ }