@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
|
@@ -1,628 +0,0 @@
|
|
|
1
|
-
import Module from '../core/module.js';
|
|
2
|
-
import IconUtils from '../ui/icons.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Block Toolbar Module - Floating toolbar hiện lên khi select text hoặc ấn Enter
|
|
6
|
-
*/
|
|
7
|
-
class BlockToolbar extends Module {
|
|
8
|
-
static DEFAULTS = {
|
|
9
|
-
showOnSelection: true,
|
|
10
|
-
showOnEnter: true,
|
|
11
|
-
buttons: ['bold', 'italic', 'underline', 'strike', 'code']
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
constructor(editor, options = {}) {
|
|
15
|
-
super(editor, options);
|
|
16
|
-
this.blockToolbar = null;
|
|
17
|
-
this.isVisible = false;
|
|
18
|
-
this.currentSelection = null; // Store current selection for scroll updates
|
|
19
|
-
this.currentCursorPosition = null; // Store current cursor position for scroll updates
|
|
20
|
-
this.originalTags = new Map(); // Store original tags before converting to code
|
|
21
|
-
this.init();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
init() {
|
|
25
|
-
this.preloadIcons();
|
|
26
|
-
this.createBlockToolbar();
|
|
27
|
-
this.setupEventListeners();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async preloadIcons() {
|
|
31
|
-
// Icons are now inline, no need to preload
|
|
32
|
-
// This method is kept for backward compatibility
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
createBlockToolbar() {
|
|
36
|
-
this.blockToolbar = document.createElement('div');
|
|
37
|
-
this.blockToolbar.className = 'block-toolbar';
|
|
38
|
-
|
|
39
|
-
// Create toolbar container
|
|
40
|
-
const toolbarContainer = document.createElement('div');
|
|
41
|
-
toolbarContainer.className = 'block-toolbar-container';
|
|
42
|
-
|
|
43
|
-
const buttons = [
|
|
44
|
-
{ cmd: 'bold', icon: 'bold', title: 'Bold (Ctrl+B)' },
|
|
45
|
-
{ cmd: 'italic', icon: 'italic', title: 'Italic (Ctrl+I)' },
|
|
46
|
-
{ cmd: 'underline', icon: 'underline', title: 'Underline (Ctrl+U)' },
|
|
47
|
-
{ cmd: 'strike', icon: 'strike', title: 'Strikethrough' },
|
|
48
|
-
{ cmd: 'code', icon: 'code', title: 'Code' },
|
|
49
|
-
{ cmd: 'font-family', icon: 'heading', title: 'Font Family' }
|
|
50
|
-
];
|
|
51
|
-
buttons.forEach(({ cmd, icon, title }) => {
|
|
52
|
-
const button = document.createElement('button');
|
|
53
|
-
button.className = 'block-toolbar-btn';
|
|
54
|
-
button.title = title;
|
|
55
|
-
button.dataset.command = cmd;
|
|
56
|
-
const iconElement = IconUtils.createIconElement(icon, { width: '16px', height: '16px' });
|
|
57
|
-
button.appendChild(iconElement);
|
|
58
|
-
button.addEventListener('click', (e) => {
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
e.stopPropagation();
|
|
61
|
-
this.handleCommand(cmd, button);
|
|
62
|
-
});
|
|
63
|
-
toolbarContainer.appendChild(button);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Create arrow element
|
|
67
|
-
const arrow = document.createElement('div');
|
|
68
|
-
arrow.className = 'block-toolbar-arrow';
|
|
69
|
-
|
|
70
|
-
// Add container and arrow to toolbar
|
|
71
|
-
this.blockToolbar.appendChild(toolbarContainer);
|
|
72
|
-
this.blockToolbar.appendChild(arrow);
|
|
73
|
-
|
|
74
|
-
this.editor.wrapper.appendChild(this.blockToolbar);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setupEventListeners() {
|
|
78
|
-
if (this.options.showOnSelection) {
|
|
79
|
-
this.editor.editor.addEventListener('mouseup', () => {
|
|
80
|
-
setTimeout(() => this.handleSelectionChange(), 0);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
if (this.options.showOnEnter) {
|
|
84
|
-
this.editor.editor.addEventListener('keydown', (e) => {
|
|
85
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
86
|
-
requestAnimationFrame(() => {
|
|
87
|
-
setTimeout(() => this.showAtCursorAfterEnter(), 10);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
else{
|
|
91
|
-
this.hide();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
document.addEventListener('mousedown', (e) => {
|
|
97
|
-
// Don't hide if clicking on font-family popup or its items
|
|
98
|
-
if (e.target.closest('.font-family-select-popup') || e.target.closest('.custom-select-popup')) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!e.target.closest('.block-toolbar') && !e.target.closest('.rich-editor-area')) {
|
|
103
|
-
this.hide();
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Update scroll event listeners to track position instead of hiding
|
|
108
|
-
window.addEventListener('scroll', () => {
|
|
109
|
-
if (this.isVisible) {
|
|
110
|
-
this.updateToolbarPosition();
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Add editor scroll listener
|
|
115
|
-
this.editor.editor.addEventListener('scroll', () => {
|
|
116
|
-
if (this.isVisible) {
|
|
117
|
-
this.updateToolbarPosition();
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
this.editor.editor.addEventListener('keyup', (e) => {
|
|
122
|
-
// Nếu là Shift + Enter thì ẩn toolbar
|
|
123
|
-
if (e.key === 'Enter' && e.shiftKey) {
|
|
124
|
-
this.hide();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (this.isVisible) {
|
|
129
|
-
this.updateButtonStates();
|
|
130
|
-
} else {
|
|
131
|
-
this.handleSelectionChange();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
handleSelectionChange() {
|
|
137
|
-
const selection = window.getSelection();
|
|
138
|
-
if (!selection || selection.rangeCount === 0) return this.hide();
|
|
139
|
-
const range = selection.getRangeAt(0);
|
|
140
|
-
const isInEditableArea = this.editor.isSelectionInEditableArea ?
|
|
141
|
-
this.editor.isSelectionInEditableArea(selection) :
|
|
142
|
-
this.editor.editor.contains(range.commonAncestorContainer);
|
|
143
|
-
if (!isInEditableArea) return this.hide();
|
|
144
|
-
if (!range.collapsed && selection.toString().trim().length > 0) {
|
|
145
|
-
this.showAtSelection(selection);
|
|
146
|
-
} else {
|
|
147
|
-
this.hide();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
showAtSelection(selection) {
|
|
152
|
-
if (!selection || selection.rangeCount === 0) return;
|
|
153
|
-
|
|
154
|
-
// Store current selection for scroll updates
|
|
155
|
-
this.currentSelection = selection;
|
|
156
|
-
this.currentCursorPosition = null;
|
|
157
|
-
|
|
158
|
-
const range = selection.getRangeAt(0);
|
|
159
|
-
const rect = range.getBoundingClientRect();
|
|
160
|
-
const editorRect = this.editor.wrapper.getBoundingClientRect();
|
|
161
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
162
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
163
|
-
this.showAt(
|
|
164
|
-
rect.left + rect.width / 2 - editorRect.left + scrollLeft,
|
|
165
|
-
rect.top - editorRect.top + scrollTop - 10
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
showAtCursorAfterEnter() {
|
|
170
|
-
this.editor.focus();
|
|
171
|
-
const selection = window.getSelection();
|
|
172
|
-
if (!selection || selection.rangeCount === 0) return;
|
|
173
|
-
const range = selection.getRangeAt(0);
|
|
174
|
-
const isInEditableArea = this.editor.isSelectionInEditableArea ?
|
|
175
|
-
this.editor.isSelectionInEditableArea(selection) :
|
|
176
|
-
this.editor.editor.contains(range.commonAncestorContainer);
|
|
177
|
-
if (!isInEditableArea) return;
|
|
178
|
-
|
|
179
|
-
this.ensureCursorAtEndOfLine(range);
|
|
180
|
-
|
|
181
|
-
// Store current cursor position for scroll updates
|
|
182
|
-
this.currentSelection = selection;
|
|
183
|
-
this.currentCursorPosition = this.getCursorPositionAfterEnter();
|
|
184
|
-
|
|
185
|
-
const rect = this.currentCursorPosition;
|
|
186
|
-
if (!rect) return;
|
|
187
|
-
const editorRect = this.editor.wrapper.getBoundingClientRect();
|
|
188
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
189
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
190
|
-
this.showAt(
|
|
191
|
-
rect.left - editorRect.left + scrollLeft,
|
|
192
|
-
rect.top - editorRect.top + scrollTop - 10
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
ensureCursorAtEndOfLine(range) {
|
|
197
|
-
if (!range.collapsed) return;
|
|
198
|
-
const selection = window.getSelection();
|
|
199
|
-
const currentNode = range.startContainer;
|
|
200
|
-
if (currentNode.nodeType === Node.TEXT_NODE) {
|
|
201
|
-
const textLength = currentNode.textContent.length;
|
|
202
|
-
if (range.startOffset < textLength) {
|
|
203
|
-
range.setStart(currentNode, textLength);
|
|
204
|
-
range.setEnd(currentNode, textLength);
|
|
205
|
-
selection.removeAllRanges();
|
|
206
|
-
selection.addRange(range);
|
|
207
|
-
}
|
|
208
|
-
} else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
209
|
-
const walker = document.createTreeWalker(currentNode, NodeFilter.SHOW_TEXT, null, false);
|
|
210
|
-
let lastTextNode = null, node;
|
|
211
|
-
while (node = walker.nextNode()) lastTextNode = node;
|
|
212
|
-
if (lastTextNode) {
|
|
213
|
-
const textLength = lastTextNode.textContent.length;
|
|
214
|
-
range.setStart(lastTextNode, textLength);
|
|
215
|
-
range.setEnd(lastTextNode, textLength);
|
|
216
|
-
selection.removeAllRanges();
|
|
217
|
-
selection.addRange(range);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
getCursorPositionAfterEnter() {
|
|
223
|
-
const selection = window.getSelection();
|
|
224
|
-
if (!selection || selection.rangeCount === 0) return null;
|
|
225
|
-
const range = selection.getRangeAt(0);
|
|
226
|
-
const marker = document.createElement('span');
|
|
227
|
-
marker.innerHTML = '​';
|
|
228
|
-
marker.style.position = 'absolute';
|
|
229
|
-
marker.style.visibility = 'hidden';
|
|
230
|
-
marker.style.pointerEvents = 'none';
|
|
231
|
-
range.insertNode(marker);
|
|
232
|
-
const rect = marker.getBoundingClientRect();
|
|
233
|
-
if (marker.parentNode) marker.parentNode.removeChild(marker);
|
|
234
|
-
const newRange = document.createRange();
|
|
235
|
-
newRange.setStart(range.startContainer, range.startOffset);
|
|
236
|
-
newRange.collapse(true);
|
|
237
|
-
selection.removeAllRanges();
|
|
238
|
-
selection.addRange(newRange);
|
|
239
|
-
return rect;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
showAtCursor() {
|
|
243
|
-
const selection = window.getSelection();
|
|
244
|
-
if (!selection || selection.rangeCount === 0) return;
|
|
245
|
-
const range = selection.getRangeAt(0);
|
|
246
|
-
const isInEditableArea = this.editor.isSelectionInEditableArea ?
|
|
247
|
-
this.editor.isSelectionInEditableArea(selection) :
|
|
248
|
-
this.editor.editor.contains(range.commonAncestorContainer);
|
|
249
|
-
if (!isInEditableArea) return;
|
|
250
|
-
let rect;
|
|
251
|
-
if (range.collapsed) {
|
|
252
|
-
const span = document.createElement('span');
|
|
253
|
-
span.innerHTML = '​';
|
|
254
|
-
span.style.position = 'absolute';
|
|
255
|
-
span.style.visibility = 'hidden';
|
|
256
|
-
span.style.pointerEvents = 'none';
|
|
257
|
-
range.insertNode(span);
|
|
258
|
-
rect = span.getBoundingClientRect();
|
|
259
|
-
if (span.parentNode) span.parentNode.removeChild(span);
|
|
260
|
-
const newRange = document.createRange();
|
|
261
|
-
newRange.setStart(range.startContainer, range.startOffset);
|
|
262
|
-
newRange.collapse(true);
|
|
263
|
-
selection.removeAllRanges();
|
|
264
|
-
selection.addRange(newRange);
|
|
265
|
-
} else {
|
|
266
|
-
rect = range.getBoundingClientRect();
|
|
267
|
-
}
|
|
268
|
-
const editorRect = this.editor.wrapper.getBoundingClientRect();
|
|
269
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
270
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
271
|
-
this.showAt(
|
|
272
|
-
rect.left - editorRect.left + scrollLeft,
|
|
273
|
-
rect.top - editorRect.top + scrollTop - 10
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
showAt(x, y) {
|
|
278
|
-
if (!this.blockToolbar) return;
|
|
279
|
-
this.blockToolbar.classList.add('visible');
|
|
280
|
-
this.isVisible = true;
|
|
281
|
-
this.ensureToolbarInViewport(x,y);
|
|
282
|
-
this.updateButtonStates();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
ensureToolbarInViewport(x,y) {
|
|
286
|
-
if (!this.blockToolbar) return;
|
|
287
|
-
|
|
288
|
-
// Lấy thông tin về editor-area
|
|
289
|
-
const editorArea = this.editor.editor;
|
|
290
|
-
const editorRect = editorArea.getBoundingClientRect();
|
|
291
|
-
const toolbarRect = this.blockToolbar.getBoundingClientRect();
|
|
292
|
-
const toolbarContainer = this.editor.wrapper.querySelector('.rich-editor-toolbar-container');
|
|
293
|
-
const toolbarRect2 = toolbarContainer ? toolbarContainer.getBoundingClientRect() : null;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let left = x - this.blockToolbar.offsetWidth/2;
|
|
297
|
-
let top = editorRect.y + y -(toolbarRect2.height) - editorArea.scrollTop - (editorRect.y + window.scrollY) +toolbarContainer.offsetHeight-49;
|
|
298
|
-
console.log('toolbarContainer.offsetHeight', toolbarContainer.offsetHeight);
|
|
299
|
-
console.log('editorRect.y', editorRect.y);
|
|
300
|
-
console.log('y', y);
|
|
301
|
-
console.log('toolbarRect2.height', toolbarRect2.height);
|
|
302
|
-
console.log('editorArea.scrollTop', editorArea.scrollTop);
|
|
303
|
-
console.log('top', top);
|
|
304
|
-
console.log('window.scrollY', window.scrollY);
|
|
305
|
-
|
|
306
|
-
let arrowLeft = '50%';
|
|
307
|
-
let arrowDirection = 'down'; // mũi tên hướng xuống
|
|
308
|
-
|
|
309
|
-
// Trường hợp 1: Vượt quá lề trái của editor
|
|
310
|
-
if (left < 0) {
|
|
311
|
-
left =(x - (this.blockToolbar.offsetWidth * (10/100)));
|
|
312
|
-
if(left < 0) left = 0;
|
|
313
|
-
arrowLeft = '10%'; // Mũi tên ở 10%
|
|
314
|
-
}
|
|
315
|
-
// Trường hợp 2: Vượt quá lề phải của editor
|
|
316
|
-
if (left + this.blockToolbar.offsetWidth > (this.editor.wrapper.offsetWidth - 2)) {
|
|
317
|
-
left = x - this.blockToolbar.offsetWidth*0.9;
|
|
318
|
-
arrowLeft = '90%'; // Mũi tên ở 90%
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Trường hợp 3: Vượt quá lề trên của editor
|
|
322
|
-
|
|
323
|
-
if (top < toolbarRect2.height) {
|
|
324
|
-
top = editorRect.y + y -(toolbarRect2.height) - editorArea.scrollTop +100 - (editorRect.y + window.scrollY)+toolbarContainer.offsetHeight-49;
|
|
325
|
-
arrowDirection = 'up'; // Mũi tên hướng lên
|
|
326
|
-
if(top < toolbarRect2.height ){
|
|
327
|
-
this.hide();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if(top > editorRect.height){
|
|
332
|
-
this.hide();
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
// Cập nhật vị trí mũi tên
|
|
336
|
-
const arrow = this.blockToolbar.querySelector('.block-toolbar-arrow');
|
|
337
|
-
if (arrow) {
|
|
338
|
-
arrow.style.left = arrowLeft;
|
|
339
|
-
|
|
340
|
-
if (arrowDirection === 'up') {
|
|
341
|
-
// Mũi tên hướng lên
|
|
342
|
-
arrow.style.bottom = 'auto';
|
|
343
|
-
arrow.style.top = '-8px';
|
|
344
|
-
arrow.style.borderTop = 'none';
|
|
345
|
-
arrow.style.borderBottom = '8px solid #fff';
|
|
346
|
-
arrow.style.borderLeft = '6px solid transparent';
|
|
347
|
-
arrow.style.borderRight = '6px solid transparent';
|
|
348
|
-
} else {
|
|
349
|
-
// Mũi tên hướng xuống (mặc định)
|
|
350
|
-
arrow.style.top = 'auto';
|
|
351
|
-
arrow.style.bottom = '-8px';
|
|
352
|
-
arrow.style.borderBottom = 'none';
|
|
353
|
-
arrow.style.borderTop = '8px solid #fff';
|
|
354
|
-
arrow.style.borderLeft = '6px solid transparent';
|
|
355
|
-
arrow.style.borderRight = '6px solid transparent';
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// Áp dụng vị trí cuối cùng
|
|
359
|
-
this.blockToolbar.style.left = left + 'px';
|
|
360
|
-
this.blockToolbar.style.top = top + 'px';
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Update toolbar position based on current selection or cursor position
|
|
365
|
-
*/
|
|
366
|
-
updateToolbarPosition() {
|
|
367
|
-
if (!this.isVisible) return;
|
|
368
|
-
|
|
369
|
-
const selection = window.getSelection();
|
|
370
|
-
if (!selection || selection.rangeCount === 0) {
|
|
371
|
-
this.hide();
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const range = selection.getRangeAt(0);
|
|
376
|
-
const isInEditableArea = this.editor.isSelectionInEditableArea ?
|
|
377
|
-
this.editor.isSelectionInEditableArea(selection) :
|
|
378
|
-
this.editor.editor.contains(range.commonAncestorContainer);
|
|
379
|
-
|
|
380
|
-
if (!isInEditableArea) {
|
|
381
|
-
this.hide();
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
let rect;
|
|
386
|
-
|
|
387
|
-
if (range.collapsed) {
|
|
388
|
-
// For cursor position, get current cursor rect
|
|
389
|
-
const span = document.createElement('span');
|
|
390
|
-
span.innerHTML = '​';
|
|
391
|
-
span.style.position = 'absolute';
|
|
392
|
-
span.style.visibility = 'hidden';
|
|
393
|
-
span.style.pointerEvents = 'none';
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
range.insertNode(span);
|
|
397
|
-
rect = span.getBoundingClientRect();
|
|
398
|
-
if (span.parentNode) span.parentNode.removeChild(span);
|
|
399
|
-
|
|
400
|
-
// Restore range
|
|
401
|
-
const newRange = document.createRange();
|
|
402
|
-
newRange.setStart(range.startContainer, range.startOffset);
|
|
403
|
-
newRange.collapse(true);
|
|
404
|
-
selection.removeAllRanges();
|
|
405
|
-
selection.addRange(newRange);
|
|
406
|
-
} catch (e) {
|
|
407
|
-
// If insertion fails, hide toolbar
|
|
408
|
-
this.hide();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
} else {
|
|
412
|
-
// For selection, use selection rect
|
|
413
|
-
rect = range.getBoundingClientRect();
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const editorRect = this.editor.wrapper.getBoundingClientRect();
|
|
417
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
418
|
-
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
|
419
|
-
|
|
420
|
-
let x, y;
|
|
421
|
-
if (range.collapsed) {
|
|
422
|
-
x = rect.left - editorRect.left + scrollLeft;
|
|
423
|
-
y = rect.top - editorRect.top + scrollTop - 10;
|
|
424
|
-
} else {
|
|
425
|
-
x = rect.left + rect.width / 2 - editorRect.left + scrollLeft;
|
|
426
|
-
y = rect.top - editorRect.top + scrollTop - 10;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
this.updateToolbarAt(x, y);
|
|
430
|
-
|
|
431
|
-
// Update font-family popup position if it's visible
|
|
432
|
-
const fontFamilyFormat = this.editor.registry.get('formats/font-family');
|
|
433
|
-
if (fontFamilyFormat && fontFamilyFormat.selectInstance && fontFamilyFormat.selectInstance.isVisible) {
|
|
434
|
-
fontFamilyFormat.selectInstance.updatePosition();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Update toolbar position at specific coordinates
|
|
440
|
-
*/
|
|
441
|
-
updateToolbarAt(x, y) {
|
|
442
|
-
if (!this.blockToolbar) return;
|
|
443
|
-
|
|
444
|
-
this.ensureToolbarInViewport(x, y);
|
|
445
|
-
this.updateButtonStates();
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
hide() {
|
|
449
|
-
if (!this.blockToolbar || !this.isVisible) return;
|
|
450
|
-
this.blockToolbar.classList.remove('visible');
|
|
451
|
-
this.isVisible = false;
|
|
452
|
-
// Clear stored positions
|
|
453
|
-
this.currentSelection = null;
|
|
454
|
-
this.currentCursorPosition = null;
|
|
455
|
-
|
|
456
|
-
// Hide any open font-family popup when block toolbar is hidden
|
|
457
|
-
const fontFamilyFormat = this.editor.registry.get('formats/font-family');
|
|
458
|
-
if (fontFamilyFormat && fontFamilyFormat.selectInstance) {
|
|
459
|
-
fontFamilyFormat.selectInstance.hide();
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
handleCommand(command, button) {
|
|
464
|
-
const selection = window.getSelection();
|
|
465
|
-
const isInEditableArea = this.editor.isSelectionInEditableArea ?
|
|
466
|
-
this.editor.isSelectionInEditableArea(selection) : true;
|
|
467
|
-
if (!isInEditableArea) {
|
|
468
|
-
this.hide();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Special handling for font-family command
|
|
473
|
-
if (command === 'font-family') {
|
|
474
|
-
const fontFamilyFormat = this.editor.registry.get('formats/font-family');
|
|
475
|
-
if (fontFamilyFormat) {
|
|
476
|
-
const format = new fontFamilyFormat();
|
|
477
|
-
format.toggle(button); // Pass the button as anchor
|
|
478
|
-
this.updateButtonState(command, button);
|
|
479
|
-
this.editor.focus();
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Special handling for code command to use PRE tag from heading format
|
|
485
|
-
if (command === 'code') {
|
|
486
|
-
const headingFormat = this.editor.registry.get('formats/heading');
|
|
487
|
-
if (headingFormat) {
|
|
488
|
-
const heading = new headingFormat();
|
|
489
|
-
const currentTag = heading.getCurrentTag();
|
|
490
|
-
|
|
491
|
-
// If current tag is PRE, convert back to original tag or P
|
|
492
|
-
// If current tag is not PRE, convert to PRE (code format)
|
|
493
|
-
if (currentTag === 'PRE') {
|
|
494
|
-
// Get the selection to find the block element
|
|
495
|
-
const selection = window.getSelection();
|
|
496
|
-
if (selection && selection.rangeCount) {
|
|
497
|
-
const range = selection.getRangeAt(0);
|
|
498
|
-
const block = this.getBlockElement(range.startContainer);
|
|
499
|
-
|
|
500
|
-
if (block) {
|
|
501
|
-
// Get original tag for this block, default to P
|
|
502
|
-
const originalTag = this.originalTags.get(block) || 'P';
|
|
503
|
-
heading.apply(originalTag);
|
|
504
|
-
this.originalTags.delete(block); // Clean up
|
|
505
|
-
} else {
|
|
506
|
-
heading.apply('P');
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
heading.apply('P');
|
|
510
|
-
}
|
|
511
|
-
} else {
|
|
512
|
-
// Store original tag before converting to PRE
|
|
513
|
-
const selection = window.getSelection();
|
|
514
|
-
if (selection && selection.rangeCount) {
|
|
515
|
-
const range = selection.getRangeAt(0);
|
|
516
|
-
const block = this.getBlockElement(range.startContainer);
|
|
517
|
-
|
|
518
|
-
if (block) {
|
|
519
|
-
this.originalTags.set(block, currentTag || 'P');
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
heading.apply('PRE');
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
this.updateButtonState(command, button);
|
|
527
|
-
this.editor.focus();
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const formatClass = this.editor.registry.get(`formats/${command}`);
|
|
533
|
-
if (formatClass) {
|
|
534
|
-
const format = new formatClass();
|
|
535
|
-
if (typeof format.toggle === 'function') format.toggle();
|
|
536
|
-
else if (typeof format.apply === 'function') format.apply();
|
|
537
|
-
} else {
|
|
538
|
-
document.execCommand(command, false, null);
|
|
539
|
-
}
|
|
540
|
-
this.updateButtonState(command, button);
|
|
541
|
-
this.editor.focus();
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
updateButtonStates() {
|
|
545
|
-
if (!this.blockToolbar) return;
|
|
546
|
-
const buttons = this.blockToolbar.querySelectorAll('.block-toolbar-btn');
|
|
547
|
-
buttons.forEach(button => {
|
|
548
|
-
const command = button.dataset.command;
|
|
549
|
-
this.updateButtonState(command, button);
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
updateButtonState(command, button) {
|
|
554
|
-
if (!button) return;
|
|
555
|
-
let isActive = false;
|
|
556
|
-
if (command === 'font-family') {
|
|
557
|
-
const fontFamilyFormat = this.editor.registry.get('formats/font-family');
|
|
558
|
-
if (fontFamilyFormat) {
|
|
559
|
-
const format = new fontFamilyFormat();
|
|
560
|
-
isActive = format.isActive();
|
|
561
|
-
}
|
|
562
|
-
} else if (command === 'code') {
|
|
563
|
-
// Check if current block is PRE tag
|
|
564
|
-
const headingFormat = this.editor.registry.get('formats/heading');
|
|
565
|
-
if (headingFormat) {
|
|
566
|
-
const heading = new headingFormat();
|
|
567
|
-
const currentTag = heading.getCurrentTag();
|
|
568
|
-
isActive = currentTag === 'PRE';
|
|
569
|
-
}
|
|
570
|
-
} else if (command === 'strike') {
|
|
571
|
-
const formatClass = this.editor.registry.get(`formats/${command}`);
|
|
572
|
-
if (formatClass) {
|
|
573
|
-
const format = new formatClass();
|
|
574
|
-
isActive = format.isActive();
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
try {
|
|
578
|
-
isActive = document.queryCommandState(command);
|
|
579
|
-
} catch (e) {
|
|
580
|
-
isActive = false;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
if (isActive) button.classList.add('active');
|
|
584
|
-
else button.classList.remove('active');
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Get block element from a node
|
|
589
|
-
* @param {Node} node - Node to find block element for
|
|
590
|
-
* @returns {Element|null} Block element or null
|
|
591
|
-
*/
|
|
592
|
-
getBlockElement(node) {
|
|
593
|
-
if (!node) return null;
|
|
594
|
-
|
|
595
|
-
// If node is an element and is a block, return it
|
|
596
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
597
|
-
const blockTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'BLOCKQUOTE', 'DIV'];
|
|
598
|
-
if (blockTags.includes(node.tagName)) {
|
|
599
|
-
return node;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Walk up the DOM tree to find block element
|
|
604
|
-
let current = node;
|
|
605
|
-
while (current && current !== this.editor.editor) {
|
|
606
|
-
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
607
|
-
const blockTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'BLOCKQUOTE', 'DIV'];
|
|
608
|
-
if (blockTags.includes(current.tagName)) {
|
|
609
|
-
return current;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
current = current.parentNode;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
destroy() {
|
|
619
|
-
if (this.blockToolbar && this.blockToolbar.parentNode) {
|
|
620
|
-
this.blockToolbar.parentNode.removeChild(this.blockToolbar);
|
|
621
|
-
}
|
|
622
|
-
this.blockToolbar = null;
|
|
623
|
-
this.isVisible = false;
|
|
624
|
-
this.originalTags.clear(); // Clean up stored tags
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
export default BlockToolbar;
|