@oix1987/yjd 1.0.0 → 1.0.2
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 +73 -22
- package/dist/rich-editor.esm.js +2 -0
- package/dist/rich-editor.esm.js.map +1 -0
- package/dist/rich-editor.min.js +2 -0
- package/dist/rich-editor.min.js.map +1 -0
- package/package.json +12 -7
- package/index.js +0 -221
- package/lib/core/editor.js +0 -1175
- package/lib/core/format.js +0 -542
- package/lib/core/module.js +0 -81
- package/lib/core/registry.js +0 -152
- package/lib/formats/background.js +0 -212
- package/lib/formats/bold.js +0 -67
- package/lib/formats/capitalization.js +0 -563
- package/lib/formats/color.js +0 -165
- package/lib/formats/emoji.js +0 -282
- package/lib/formats/font-family.js +0 -547
- package/lib/formats/heading.js +0 -502
- package/lib/formats/image.js +0 -344
- package/lib/formats/import.js +0 -385
- package/lib/formats/indent.js +0 -297
- package/lib/formats/italic.js +0 -27
- package/lib/formats/line-height.js +0 -558
- package/lib/formats/link.js +0 -251
- package/lib/formats/list.js +0 -635
- package/lib/formats/strike.js +0 -31
- package/lib/formats/subscript.js +0 -36
- package/lib/formats/superscript.js +0 -35
- package/lib/formats/table.js +0 -288
- package/lib/formats/tag.js +0 -304
- package/lib/formats/text-align.js +0 -421
- package/lib/formats/text-size.js +0 -497
- package/lib/formats/underline.js +0 -30
- package/lib/formats/video.js +0 -372
- package/lib/modules/block-toolbar.js +0 -628
- package/lib/modules/code-view.js +0 -434
- package/lib/modules/history.js +0 -410
- package/lib/modules/resize-handles.js +0 -677
- package/lib/modules/table-toolbar.js +0 -618
- package/lib/modules/toolbar.js +0 -424
- package/lib/styles-loader.js +0 -144
- package/lib/styles.css +0 -2123
- package/lib/ui/color-picker.js +0 -296
- package/lib/ui/customselect.js +0 -319
- package/lib/ui/emoji-picker.js +0 -196
- package/lib/ui/icons.js +0 -413
- package/lib/ui/image-popup.js +0 -444
- package/lib/ui/import-popup.js +0 -288
- package/lib/ui/link-popup.js +0 -191
- package/lib/ui/list-picker.js +0 -307
- package/lib/ui/select-button.js +0 -61
- package/lib/ui/table-popup.js +0 -171
- package/lib/ui/tag-popup.js +0 -249
- package/lib/ui/text-align-picker.js +0 -281
- package/lib/ui/video-popup.js +0 -422
- package/lib/utils/history-helper.js +0 -50
- package/lib/utils/popup-helper.js +0 -219
- package/lib/utils/popup-positioning.js +0 -231
package/lib/modules/history.js
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import Module from '../core/module.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* History Module - Handles undo/redo functionality
|
|
5
|
-
* Extracted from FormatManager.js and ToolbarManager.js logic
|
|
6
|
-
*/
|
|
7
|
-
class History extends Module {
|
|
8
|
-
static DEFAULTS = {
|
|
9
|
-
delay: 1000, // Delay between history saves
|
|
10
|
-
maxStack: 100, // Maximum number of undo states
|
|
11
|
-
userOnly: false // Only save user-initiated changes
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
constructor(editor, options = {}) {
|
|
15
|
-
super(editor, options);
|
|
16
|
-
this.stack = [];
|
|
17
|
-
this.index = -1;
|
|
18
|
-
this.lastSave = 0;
|
|
19
|
-
this.savedSelection = null;
|
|
20
|
-
|
|
21
|
-
this.init();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
init() {
|
|
25
|
-
this.setupEventListeners();
|
|
26
|
-
this.saveState(); // Save initial state
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Setup event listeners for automatic history saving
|
|
31
|
-
*/
|
|
32
|
-
setupEventListeners() {
|
|
33
|
-
// Save state on input with debouncing
|
|
34
|
-
this.editor.editor.addEventListener('input', () => {
|
|
35
|
-
this.handleInput();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Save state on specific commands
|
|
39
|
-
this.editor.editor.addEventListener('keydown', (e) => {
|
|
40
|
-
// Save state before destructive operations
|
|
41
|
-
if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Delete') {
|
|
42
|
-
this.saveState();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Listen for DOM changes to catch all formatting operations
|
|
47
|
-
this.setupMutationObserver();
|
|
48
|
-
|
|
49
|
-
// Listen for toolbar clicks to save state before formatting
|
|
50
|
-
this.editor.wrapper.addEventListener('click', (e) => {
|
|
51
|
-
if (e.target.closest('.rich-editor-toolbar-btn')) {
|
|
52
|
-
// Save state before applying format
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
this.saveState();
|
|
55
|
-
}, 0);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Handle undo/redo shortcuts - only when editor is focused
|
|
60
|
-
this.editor.editor.addEventListener('keydown', (e) => {
|
|
61
|
-
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
|
|
62
|
-
e.preventDefault();
|
|
63
|
-
this.undo();
|
|
64
|
-
} else if (((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') ||
|
|
65
|
-
((e.ctrlKey || e.metaKey) && e.key === 'y')) {
|
|
66
|
-
e.preventDefault();
|
|
67
|
-
this.redo();
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Setup mutation observer to watch for DOM changes
|
|
74
|
-
*/
|
|
75
|
-
setupMutationObserver() {
|
|
76
|
-
this.mutationObserver = new MutationObserver((mutations) => {
|
|
77
|
-
let shouldSave = false;
|
|
78
|
-
|
|
79
|
-
for (const mutation of mutations) {
|
|
80
|
-
// Check if the mutation is relevant (not just attribute changes on non-content elements)
|
|
81
|
-
if (mutation.type === 'childList' ||
|
|
82
|
-
(mutation.type === 'attributes' &&
|
|
83
|
-
(mutation.target.nodeType === Node.TEXT_NODE ||
|
|
84
|
-
['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'PRE', 'UL', 'OL', 'LI', 'SPAN', 'STRONG', 'EM', 'U', 'S', 'SUB', 'SUP', 'A', 'IMG', 'VIDEO', 'TABLE', 'TR', 'TD', 'TH'].includes(mutation.target.tagName)))) {
|
|
85
|
-
shouldSave = true;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (shouldSave) {
|
|
91
|
-
// Debounce the save to avoid too many saves
|
|
92
|
-
clearTimeout(this.mutationTimeout);
|
|
93
|
-
this.mutationTimeout = setTimeout(() => {
|
|
94
|
-
this.saveState();
|
|
95
|
-
}, 100);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Start observing
|
|
100
|
-
this.mutationObserver.observe(this.editor.editor, {
|
|
101
|
-
childList: true,
|
|
102
|
-
subtree: true,
|
|
103
|
-
attributes: true,
|
|
104
|
-
attributeFilter: ['style', 'class', 'href', 'src', 'alt', 'title']
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Handle input event with debouncing
|
|
110
|
-
*/
|
|
111
|
-
handleInput() {
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
if (now - this.lastSave > this.options.delay) {
|
|
114
|
-
this.saveState();
|
|
115
|
-
this.lastSave = now;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Save current editor state
|
|
121
|
-
*/
|
|
122
|
-
saveState() {
|
|
123
|
-
const content = this.editor.getContent();
|
|
124
|
-
const selection = this.saveSelection();
|
|
125
|
-
|
|
126
|
-
// Don't save if content hasn't changed
|
|
127
|
-
if (this.stack.length > 0 && this.stack[this.index]?.content === content) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Don't save if it's too soon after last save (debouncing)
|
|
132
|
-
const now = Date.now();
|
|
133
|
-
if (this.lastSave && now - this.lastSave < 50) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Remove any redo states if we're not at the end
|
|
138
|
-
if (this.index < this.stack.length - 1) {
|
|
139
|
-
this.stack.splice(this.index + 1);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Add new state
|
|
143
|
-
this.stack.push({
|
|
144
|
-
content,
|
|
145
|
-
selection,
|
|
146
|
-
timestamp: now
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Limit stack size
|
|
150
|
-
if (this.stack.length > this.options.maxStack) {
|
|
151
|
-
this.stack.shift();
|
|
152
|
-
} else {
|
|
153
|
-
this.index++;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.lastSave = now;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Undo last change
|
|
161
|
-
*/
|
|
162
|
-
undo() {
|
|
163
|
-
if (!this.canUndo()) return false;
|
|
164
|
-
|
|
165
|
-
this.index--;
|
|
166
|
-
const state = this.stack[this.index];
|
|
167
|
-
|
|
168
|
-
this.restoreState(state);
|
|
169
|
-
this.onHistoryChange('undo');
|
|
170
|
-
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Redo last undone change
|
|
176
|
-
*/
|
|
177
|
-
redo() {
|
|
178
|
-
if (!this.canRedo()) return false;
|
|
179
|
-
|
|
180
|
-
this.index++;
|
|
181
|
-
const state = this.stack[this.index];
|
|
182
|
-
|
|
183
|
-
this.restoreState(state);
|
|
184
|
-
this.onHistoryChange('redo');
|
|
185
|
-
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Check if undo is possible
|
|
191
|
-
*/
|
|
192
|
-
canUndo() {
|
|
193
|
-
return this.index > 0;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Check if redo is possible
|
|
198
|
-
*/
|
|
199
|
-
canRedo() {
|
|
200
|
-
return this.index < this.stack.length - 1;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Restore editor state
|
|
205
|
-
* @param {object} state - State to restore
|
|
206
|
-
*/
|
|
207
|
-
restoreState(state) {
|
|
208
|
-
if (!state) return;
|
|
209
|
-
|
|
210
|
-
// Restore content
|
|
211
|
-
this.editor.setContent(state.content);
|
|
212
|
-
|
|
213
|
-
// Restore selection
|
|
214
|
-
if (state.selection) {
|
|
215
|
-
setTimeout(() => {
|
|
216
|
-
this.restoreSelection(state.selection);
|
|
217
|
-
}, 10);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Save current selection
|
|
223
|
-
*/
|
|
224
|
-
saveSelection() {
|
|
225
|
-
const selection = window.getSelection();
|
|
226
|
-
if (!selection || !selection.rangeCount) return null;
|
|
227
|
-
|
|
228
|
-
const range = selection.getRangeAt(0);
|
|
229
|
-
const editorEl = this.editor.editor;
|
|
230
|
-
|
|
231
|
-
// Calculate offset relative to editor
|
|
232
|
-
const startOffset = this.getOffsetInEditor(range.startContainer, range.startOffset, editorEl);
|
|
233
|
-
const endOffset = this.getOffsetInEditor(range.endContainer, range.endOffset, editorEl);
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
startOffset,
|
|
237
|
-
endOffset,
|
|
238
|
-
collapsed: range.collapsed
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Restore selection
|
|
244
|
-
* @param {object} selectionState - Selection state to restore
|
|
245
|
-
*/
|
|
246
|
-
restoreSelection(selectionState) {
|
|
247
|
-
if (!selectionState) return;
|
|
248
|
-
|
|
249
|
-
const editorEl = this.editor.editor;
|
|
250
|
-
const range = document.createRange();
|
|
251
|
-
const selection = window.getSelection();
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
const startNode = this.getNodeAtOffset(editorEl, selectionState.startOffset);
|
|
255
|
-
const endNode = this.getNodeAtOffset(editorEl, selectionState.endOffset);
|
|
256
|
-
|
|
257
|
-
if (startNode && endNode) {
|
|
258
|
-
range.setStart(startNode.node, startNode.offset);
|
|
259
|
-
range.setEnd(endNode.node, endNode.offset);
|
|
260
|
-
|
|
261
|
-
selection.removeAllRanges();
|
|
262
|
-
selection.addRange(range);
|
|
263
|
-
}
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.warn('Could not restore selection:', error);
|
|
266
|
-
// Fallback: focus editor
|
|
267
|
-
this.editor.focus();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Get offset of a position within editor
|
|
273
|
-
* @param {Node} node - DOM node
|
|
274
|
-
* @param {number} offset - Offset within node
|
|
275
|
-
* @param {Element} root - Root element (editor)
|
|
276
|
-
*/
|
|
277
|
-
getOffsetInEditor(node, offset, root) {
|
|
278
|
-
let totalOffset = 0;
|
|
279
|
-
const walker = document.createTreeWalker(
|
|
280
|
-
root,
|
|
281
|
-
NodeFilter.SHOW_TEXT,
|
|
282
|
-
null,
|
|
283
|
-
false
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
let currentNode;
|
|
287
|
-
while (currentNode = walker.nextNode()) {
|
|
288
|
-
if (currentNode === node) {
|
|
289
|
-
return totalOffset + offset;
|
|
290
|
-
}
|
|
291
|
-
totalOffset += currentNode.textContent.length;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return totalOffset;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get node at specific offset within editor
|
|
299
|
-
* @param {Element} root - Root element (editor)
|
|
300
|
-
* @param {number} targetOffset - Target offset
|
|
301
|
-
*/
|
|
302
|
-
getNodeAtOffset(root, targetOffset) {
|
|
303
|
-
let currentOffset = 0;
|
|
304
|
-
const walker = document.createTreeWalker(
|
|
305
|
-
root,
|
|
306
|
-
NodeFilter.SHOW_TEXT,
|
|
307
|
-
null,
|
|
308
|
-
false
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
let currentNode;
|
|
312
|
-
while (currentNode = walker.nextNode()) {
|
|
313
|
-
const nodeLength = currentNode.textContent.length;
|
|
314
|
-
if (currentOffset + nodeLength >= targetOffset) {
|
|
315
|
-
return {
|
|
316
|
-
node: currentNode,
|
|
317
|
-
offset: targetOffset - currentOffset
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
currentOffset += nodeLength;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Fallback: return last node
|
|
324
|
-
return {
|
|
325
|
-
node: root.lastChild || root,
|
|
326
|
-
offset: 0
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Clear history
|
|
332
|
-
*/
|
|
333
|
-
clear() {
|
|
334
|
-
this.stack = [];
|
|
335
|
-
this.index = -1;
|
|
336
|
-
this.saveState(); // Save current state as first entry
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Get current history state info
|
|
341
|
-
*/
|
|
342
|
-
getState() {
|
|
343
|
-
return {
|
|
344
|
-
canUndo: this.canUndo(),
|
|
345
|
-
canRedo: this.canRedo(),
|
|
346
|
-
stackLength: this.stack.length,
|
|
347
|
-
currentIndex: this.index
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Called when history changes (undo/redo)
|
|
353
|
-
* @param {string} action - 'undo' or 'redo'
|
|
354
|
-
*/
|
|
355
|
-
onHistoryChange(action) {
|
|
356
|
-
// Notify other modules about history change
|
|
357
|
-
this.editor.modules.forEach(module => {
|
|
358
|
-
if (module !== this && typeof module.onHistoryChange === 'function') {
|
|
359
|
-
module.onHistoryChange(action, this.getState());
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// Trigger custom event
|
|
364
|
-
const event = new CustomEvent('historychange', {
|
|
365
|
-
detail: { action, state: this.getState() }
|
|
366
|
-
});
|
|
367
|
-
this.editor.editor.dispatchEvent(event);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Force save current state (useful before major operations)
|
|
372
|
-
*/
|
|
373
|
-
forceSave() {
|
|
374
|
-
// Temporarily disable debouncing for force save
|
|
375
|
-
const originalLastSave = this.lastSave;
|
|
376
|
-
this.lastSave = 0;
|
|
377
|
-
this.saveState();
|
|
378
|
-
this.lastSave = originalLastSave;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Save state before applying format (called by editor)
|
|
383
|
-
*/
|
|
384
|
-
saveBeforeFormat() {
|
|
385
|
-
this.forceSave();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Destroy module
|
|
390
|
-
*/
|
|
391
|
-
destroy() {
|
|
392
|
-
// Disconnect mutation observer
|
|
393
|
-
if (this.mutationObserver) {
|
|
394
|
-
this.mutationObserver.disconnect();
|
|
395
|
-
this.mutationObserver = null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Clear timeout
|
|
399
|
-
if (this.mutationTimeout) {
|
|
400
|
-
clearTimeout(this.mutationTimeout);
|
|
401
|
-
this.mutationTimeout = null;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
this.stack = [];
|
|
405
|
-
this.index = -1;
|
|
406
|
-
this.savedSelection = null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
export default History;
|