@oix1987/yjd 1.0.3 → 2.1.0
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/LICENSE +15 -0
- package/README.md +223 -142
- package/core.js +82 -0
- package/dist/core.esm.js +2 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +230 -103
- package/index.js +297 -0
- package/lib/core/editor.js +1885 -0
- package/lib/core/format.js +540 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +158 -0
- package/lib/formats/background.js +213 -0
- package/lib/formats/bold.js +49 -0
- package/lib/formats/capitalization.js +579 -0
- package/lib/formats/color.js +183 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +548 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +341 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +562 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +40 -0
- package/lib/formats/superscript.js +39 -0
- package/lib/formats/table.js +293 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +422 -0
- package/lib/formats/text-size.js +498 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +381 -0
- package/lib/modules/block-toolbar.js +639 -0
- package/lib/modules/code-view.js +447 -0
- package/lib/modules/find-replace.js +273 -0
- package/lib/modules/history.js +425 -0
- package/lib/modules/mention.js +200 -0
- package/lib/modules/resize-handles.js +701 -0
- package/lib/modules/slash-menu.js +183 -0
- package/lib/modules/table-toolbar.js +635 -0
- package/lib/modules/toolbar.js +607 -0
- package/lib/serialize.js +241 -0
- package/lib/static.js +28 -0
- package/lib/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1392 -35
- package/lib/styles.css.js +2 -0
- package/lib/styles.min.css +1 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +351 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +145 -0
- package/lib/ui/image-popup.js +435 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +139 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +68 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +278 -0
- package/lib/ui/video-popup.js +413 -0
- package/lib/utils/exec-command.js +72 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +234 -0
- package/lib/utils/sanitize.js +164 -0
- package/package.json +51 -32
- package/umd-entry.js +19 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import CustomSelect from '../ui/customselect.js';
|
|
3
|
+
import { saveBeforeFormat } from '../utils/history-helper.js';
|
|
4
|
+
import Editor from '../core/editor.js';
|
|
5
|
+
import { execFormat, queryFormatValue } from '../utils/exec-command.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Text Size Format - Handles font size formatting with 7 levels via execCommand
|
|
9
|
+
* Now supports multiple editor instances with separate popup instances
|
|
10
|
+
*/
|
|
11
|
+
class TextSize extends InlineFormat {
|
|
12
|
+
static formatName = 'textSize';
|
|
13
|
+
static tagName = 'SPAN';
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
|
|
18
|
+
// Get current editor instance
|
|
19
|
+
const currentEditor = Editor.getCurrentInstance();
|
|
20
|
+
if (!currentEditor) {
|
|
21
|
+
console.warn('No editor instance found for TextSize format');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.editorId = currentEditor.instanceId;
|
|
26
|
+
|
|
27
|
+
// Check if this editor already has a text size select instance
|
|
28
|
+
let customSelect = currentEditor.getPopupInstance('text-size');
|
|
29
|
+
|
|
30
|
+
if (!customSelect) {
|
|
31
|
+
// Create new custom select instance for this editor
|
|
32
|
+
const sizeMap = TextSize.getSizeMap();
|
|
33
|
+
const items = Object.values(sizeMap).map(sizeData => ({
|
|
34
|
+
value: sizeData.size,
|
|
35
|
+
label: sizeData.element,
|
|
36
|
+
title: sizeData.title
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
customSelect = new CustomSelect({
|
|
40
|
+
items: items,
|
|
41
|
+
displayProperty: 'label',
|
|
42
|
+
valueProperty: 'value',
|
|
43
|
+
className: 'text-size-select',
|
|
44
|
+
onItemSelect: (value) => {
|
|
45
|
+
TextSize.applyTextSizeToCurrentSelection(value, this.editorId);
|
|
46
|
+
},
|
|
47
|
+
editor: currentEditor,
|
|
48
|
+
editorId: this.editorId
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Store popup instance in editor
|
|
52
|
+
currentEditor.setPopupInstance('text-size', customSelect);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.customSelect = customSelect;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a new TextSize format instance for a specific editor
|
|
60
|
+
* @param {string} editorId - Editor instance ID
|
|
61
|
+
* @returns {TextSize} TextSize format instance
|
|
62
|
+
*/
|
|
63
|
+
static createForEditor(editorId) {
|
|
64
|
+
const editor = Editor.getInstanceById(editorId);
|
|
65
|
+
if (!editor) {
|
|
66
|
+
console.warn('No editor instance found for ID:', editorId);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Temporarily set as current instance
|
|
71
|
+
const originalCurrent = Editor.currentInstance;
|
|
72
|
+
Editor.currentInstance = editor;
|
|
73
|
+
|
|
74
|
+
// Create format instance
|
|
75
|
+
const format = new TextSize();
|
|
76
|
+
|
|
77
|
+
// Restore original current instance
|
|
78
|
+
Editor.currentInstance = originalCurrent;
|
|
79
|
+
|
|
80
|
+
return format;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 7-level text size map aligned with execCommand('fontSize', 1..7)
|
|
85
|
+
*/
|
|
86
|
+
static getSizeMap() {
|
|
87
|
+
return {
|
|
88
|
+
'1': { size: '1', element: '<span >XX-Small</span>', title: 'XX-Small' },
|
|
89
|
+
'2': { size: '2', element: '<span >X-Small</span>', title: 'X-Small' },
|
|
90
|
+
'3': { size: '3', element: '<span >Small</span>', title: 'Small' },
|
|
91
|
+
'4': { size: '4', element: '<span >Medium</span>', title: 'Medium' },
|
|
92
|
+
'5': { size: '5', element: '<span >Large</span>', title: 'Large' },
|
|
93
|
+
'6': { size: '6', element: '<span >X-Large</span>', title: 'X-Large' },
|
|
94
|
+
'7': { size: '7', element: '<span >XX-Large</span>', title: 'XX-Large' },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static getSizeDisplayName(size) {
|
|
99
|
+
const sizeMap = this.getSizeMap();
|
|
100
|
+
return sizeMap[size]?.title || 'Medium';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update button text based on current text size
|
|
105
|
+
*/
|
|
106
|
+
updateButtonText() {
|
|
107
|
+
const currentSize = this.getCurrentSize();
|
|
108
|
+
const displayName = TextSize.getSizeDisplayName(currentSize || '4');
|
|
109
|
+
|
|
110
|
+
// Find text-size button in the current editor's toolbar
|
|
111
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
112
|
+
if (!editor) return;
|
|
113
|
+
|
|
114
|
+
const toolbar = editor.getModule('toolbar');
|
|
115
|
+
let textSizeButton = null;
|
|
116
|
+
|
|
117
|
+
if (toolbar) {
|
|
118
|
+
textSizeButton = toolbar.getButton('text-size');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
122
|
+
if (!textSizeButton) {
|
|
123
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
124
|
+
if (toolbarContainer) {
|
|
125
|
+
textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Final fallback: find any text-size button in the current editor's wrapper
|
|
130
|
+
if (!textSizeButton) {
|
|
131
|
+
textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (textSizeButton && textSizeButton.updateText) {
|
|
135
|
+
textSizeButton.updateText(displayName);
|
|
136
|
+
} else if (textSizeButton) {
|
|
137
|
+
textSizeButton.textContent = displayName;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Static method to update button text for any editor
|
|
143
|
+
* @param {string} editorId - Editor instance ID
|
|
144
|
+
*/
|
|
145
|
+
static updateButtonTextStatic(editorId = null) {
|
|
146
|
+
// Get the correct editor instance
|
|
147
|
+
let editor = null;
|
|
148
|
+
if (editorId) {
|
|
149
|
+
editor = Editor.getInstanceById(editorId);
|
|
150
|
+
} else {
|
|
151
|
+
editor = Editor.getCurrentInstance();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!editor) return;
|
|
155
|
+
|
|
156
|
+
const currentSize = TextSize.getCurrentSizeStatic();
|
|
157
|
+
const displayName = TextSize.getSizeDisplayName(currentSize || '4');
|
|
158
|
+
|
|
159
|
+
const toolbar = editor.getModule('toolbar');
|
|
160
|
+
let textSizeButton = null;
|
|
161
|
+
|
|
162
|
+
if (toolbar) {
|
|
163
|
+
textSizeButton = toolbar.getButton('text-size');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
167
|
+
if (!textSizeButton) {
|
|
168
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
169
|
+
if (toolbarContainer) {
|
|
170
|
+
textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Final fallback: find any text-size button in the current editor's wrapper
|
|
175
|
+
if (!textSizeButton) {
|
|
176
|
+
textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (textSizeButton && textSizeButton.updateText) {
|
|
180
|
+
textSizeButton.updateText(displayName);
|
|
181
|
+
} else if (textSizeButton) {
|
|
182
|
+
textSizeButton.textContent = displayName;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static create(size = '4') {
|
|
187
|
+
const node = document.createElement('span');
|
|
188
|
+
// Fallback creation with an approximate CSS size
|
|
189
|
+
node.style.fontSize = TextSize.sizeToCss(size);
|
|
190
|
+
return node;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Apply text size to current selection
|
|
195
|
+
* @param {string} size - Text size value
|
|
196
|
+
* @param {string} editorId - Editor instance ID
|
|
197
|
+
*/
|
|
198
|
+
static applyTextSizeToCurrentSelection(size, editorId = null) {
|
|
199
|
+
// Get the correct editor instance
|
|
200
|
+
let editor = null;
|
|
201
|
+
if (editorId) {
|
|
202
|
+
editor = Editor.getInstanceById(editorId);
|
|
203
|
+
} else {
|
|
204
|
+
editor = Editor.getCurrentInstance();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!editor) {
|
|
208
|
+
console.warn('No editor instance found for text size application');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const selection = window.getSelection();
|
|
213
|
+
if (!selection || !selection.rangeCount) return;
|
|
214
|
+
|
|
215
|
+
// Save state before applying format
|
|
216
|
+
saveBeforeFormat();
|
|
217
|
+
|
|
218
|
+
const sizeFormat = TextSize.createForEditor(editorId);
|
|
219
|
+
if (sizeFormat) {
|
|
220
|
+
sizeFormat.apply(size);
|
|
221
|
+
sizeFormat.updateButtonText();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Trigger content change after applying format
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
227
|
+
editor.onContentChange();
|
|
228
|
+
}
|
|
229
|
+
}, 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Map execCommand size (1..7) to CSS font-size for fallback/labels
|
|
234
|
+
*/
|
|
235
|
+
static sizeToCss(size) {
|
|
236
|
+
const map = {
|
|
237
|
+
'1': '10px',
|
|
238
|
+
'2': '12px',
|
|
239
|
+
'3': '14px',
|
|
240
|
+
'4': '16px',
|
|
241
|
+
'5': '20px',
|
|
242
|
+
'6': '28px',
|
|
243
|
+
'7': '36px',
|
|
244
|
+
};
|
|
245
|
+
return map[String(size)] || '16px';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Apply text size using execCommand; works with selection and collapsed caret
|
|
250
|
+
*/
|
|
251
|
+
apply(size = '4') {
|
|
252
|
+
const selection = window.getSelection();
|
|
253
|
+
if (!selection || !selection.rangeCount) return;
|
|
254
|
+
|
|
255
|
+
saveBeforeFormat();
|
|
256
|
+
|
|
257
|
+
const range = selection.getRangeAt(0);
|
|
258
|
+
|
|
259
|
+
if (!range.collapsed) {
|
|
260
|
+
// Bạn chưa nói đến xử lý khi bôi đen, nên mình bỏ qua
|
|
261
|
+
execFormat('fontSize', String(size));
|
|
262
|
+
|
|
263
|
+
// Lấy node bao quanh selection hiện tại
|
|
264
|
+
const sel = window.getSelection();
|
|
265
|
+
if (sel.rangeCount > 0) {
|
|
266
|
+
const container = sel.getRangeAt(0).commonAncestorContainer;
|
|
267
|
+
// Nếu container là text node → normalize ở parent
|
|
268
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
269
|
+
container.parentNode.normalize();
|
|
270
|
+
} else {
|
|
271
|
+
container.normalize();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let node = range.startContainer;
|
|
279
|
+
let offset = range.startOffset;
|
|
280
|
+
|
|
281
|
+
// Nếu caret đang trong text node → lấy cha
|
|
282
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
283
|
+
node = node.parentNode;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Kiểm tra nếu đang ở trong một <font>
|
|
287
|
+
const currentFont = node.closest && node.closest('font');
|
|
288
|
+
|
|
289
|
+
// ========================
|
|
290
|
+
// Trường hợp 1: caret trong <font> rỗng (chỉ có \u200B)
|
|
291
|
+
// ========================
|
|
292
|
+
if (currentFont && currentFont.textContent === "\u200B") {
|
|
293
|
+
currentFont.setAttribute('size', String(size));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ========================
|
|
298
|
+
// Trường hợp 2: caret trong <font> có ký tự thực
|
|
299
|
+
// ========================
|
|
300
|
+
if (currentFont && currentFont.firstChild && currentFont.firstChild.nodeType === Node.TEXT_NODE) {
|
|
301
|
+
const textNode = currentFont.firstChild;
|
|
302
|
+
const caretPos = range.startOffset; // vị trí caret trong text node
|
|
303
|
+
|
|
304
|
+
// Loại bỏ ký tự ẩn trong tính toán
|
|
305
|
+
|
|
306
|
+
const textBefore = textNode.data.slice(0, caretPos);
|
|
307
|
+
const textAfter = textNode.data.slice(caretPos);
|
|
308
|
+
|
|
309
|
+
const parent = currentFont.parentNode;
|
|
310
|
+
|
|
311
|
+
if (caretPos === 0) {
|
|
312
|
+
// Đang ở ĐẦU thẻ font → chèn font mới trước
|
|
313
|
+
const newFont = document.createElement('font');
|
|
314
|
+
newFont.setAttribute('size', String(size));
|
|
315
|
+
newFont.appendChild(document.createTextNode("\u200B"));
|
|
316
|
+
parent.insertBefore(newFont, currentFont);
|
|
317
|
+
|
|
318
|
+
moveCaretInside(newFont);
|
|
319
|
+
|
|
320
|
+
} else if (caretPos === textNode.data.length) {
|
|
321
|
+
// Đang ở CUỐI thẻ font → chèn font mới sau
|
|
322
|
+
const newFont = document.createElement('font');
|
|
323
|
+
newFont.setAttribute('size', String(size));
|
|
324
|
+
newFont.appendChild(document.createTextNode("\u200B"));
|
|
325
|
+
parent.insertBefore(newFont, currentFont.nextSibling);
|
|
326
|
+
|
|
327
|
+
moveCaretInside(newFont);
|
|
328
|
+
|
|
329
|
+
} else {
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
const font1 = document.createElement('font');
|
|
333
|
+
font1.setAttribute('size', currentFont.getAttribute('size'));
|
|
334
|
+
font1.appendChild(document.createTextNode(textBefore));
|
|
335
|
+
|
|
336
|
+
const font2 = document.createElement('font');
|
|
337
|
+
font2.setAttribute('size', String(size));
|
|
338
|
+
font2.appendChild(document.createTextNode("\u200B"));
|
|
339
|
+
|
|
340
|
+
const font3 = document.createElement('font');
|
|
341
|
+
font3.setAttribute('size', currentFont.getAttribute('size'));
|
|
342
|
+
font3.appendChild(document.createTextNode(textAfter));
|
|
343
|
+
|
|
344
|
+
parent.insertBefore(font1, currentFont);
|
|
345
|
+
parent.insertBefore(font2, currentFont);
|
|
346
|
+
parent.insertBefore(font3, currentFont);
|
|
347
|
+
|
|
348
|
+
parent.removeChild(currentFont);
|
|
349
|
+
|
|
350
|
+
moveCaretInside(font2);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ========================
|
|
356
|
+
// Trường hợp 3: không ở trong <font> nào → tạo mới
|
|
357
|
+
// ========================
|
|
358
|
+
const newFont = document.createElement('font');
|
|
359
|
+
newFont.setAttribute('size', String(size));
|
|
360
|
+
const zwsp = document.createTextNode("\u200B");
|
|
361
|
+
newFont.appendChild(zwsp);
|
|
362
|
+
|
|
363
|
+
range.insertNode(newFont);
|
|
364
|
+
moveCaretInside(newFont);
|
|
365
|
+
|
|
366
|
+
// Hàm phụ để đưa caret vào sau ký tự ẩn
|
|
367
|
+
function moveCaretInside(fontEl) {
|
|
368
|
+
const sel = window.getSelection();
|
|
369
|
+
const range = document.createRange();
|
|
370
|
+
const textNode = fontEl.firstChild;
|
|
371
|
+
range.setStart(textNode, textNode.length);
|
|
372
|
+
range.collapse(true);
|
|
373
|
+
sel.removeAllRanges();
|
|
374
|
+
sel.addRange(range);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async toggle() {
|
|
381
|
+
if (this.customSelect.isVisible) {
|
|
382
|
+
this.customSelect.hide();
|
|
383
|
+
} else {
|
|
384
|
+
await this.showSizePicker();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async showSizePicker() {
|
|
389
|
+
// Find text-size button in the current editor's toolbar
|
|
390
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
391
|
+
if (!editor) return;
|
|
392
|
+
|
|
393
|
+
const toolbar = editor.getModule('toolbar');
|
|
394
|
+
let textSizeButton = null;
|
|
395
|
+
|
|
396
|
+
if (toolbar) {
|
|
397
|
+
textSizeButton = toolbar.getButton('text-size');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
401
|
+
if (!textSizeButton) {
|
|
402
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
403
|
+
if (toolbarContainer) {
|
|
404
|
+
textSizeButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Final fallback: find any text-size button in the current editor's wrapper
|
|
409
|
+
if (!textSizeButton) {
|
|
410
|
+
textSizeButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-size-btn');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!textSizeButton) {
|
|
414
|
+
console.warn('Text-size button not found for editor:', this.editorId);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const currentSize = this.getCurrentSize();
|
|
419
|
+
if (currentSize) {
|
|
420
|
+
this.customSelect.setCurrentValue(currentSize);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
await this.customSelect.show(textSizeButton);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
isActive(size = null) {
|
|
427
|
+
this.updateButtonText();
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Get current text size near caret/selection, return one of '1'..'7'
|
|
433
|
+
*/
|
|
434
|
+
getCurrentSize() {
|
|
435
|
+
return TextSize.getCurrentSizeStatic();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Static method to get current text size
|
|
440
|
+
* @returns {string} Current text size
|
|
441
|
+
*/
|
|
442
|
+
static getCurrentSizeStatic() {
|
|
443
|
+
const selection = window.getSelection();
|
|
444
|
+
if (!selection || !selection.rangeCount) return '4';
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
// Try to use queryCommandValue when available (returns 1..7 in many browsers)
|
|
448
|
+
const val = queryFormatValue('fontSize');
|
|
449
|
+
const num = parseInt(val, 10);
|
|
450
|
+
if (!isNaN(num) && num >= 1 && num <= 7) {
|
|
451
|
+
return String(num);
|
|
452
|
+
}
|
|
453
|
+
} catch (_) {}
|
|
454
|
+
|
|
455
|
+
const range = selection.getRangeAt(0);
|
|
456
|
+
let currentNode = range.startContainer;
|
|
457
|
+
if (currentNode.nodeType === Node.TEXT_NODE) {
|
|
458
|
+
currentNode = currentNode.parentElement;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
while (currentNode && currentNode !== document.body) {
|
|
462
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
463
|
+
const element = currentNode;
|
|
464
|
+
const inline = element.style?.fontSize;
|
|
465
|
+
if (inline) return this.normalizeCssSizeToExecSize(inline);
|
|
466
|
+
|
|
467
|
+
const computed = window.getComputedStyle(element).fontSize;
|
|
468
|
+
if (computed) return this.normalizeCssSizeToExecSize(computed);
|
|
469
|
+
}
|
|
470
|
+
currentNode = currentNode.parentElement;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return '4';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Normalize CSS px value to closest execCommand size 1..7
|
|
478
|
+
*/
|
|
479
|
+
normalizeCssSizeToExecSize(cssSize) {
|
|
480
|
+
const px = parseFloat(cssSize);
|
|
481
|
+
if (isNaN(px)) return '4';
|
|
482
|
+
const steps = [10, 12, 14, 16, 20, 28, 36];
|
|
483
|
+
let closestIndex = 3; // default to '4' (16px)
|
|
484
|
+
let minDiff = Infinity;
|
|
485
|
+
for (let i = 0; i < steps.length; i++) {
|
|
486
|
+
const diff = Math.abs(px - steps[i]);
|
|
487
|
+
if (diff < minDiff) {
|
|
488
|
+
minDiff = diff;
|
|
489
|
+
closestIndex = i;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return String(closestIndex + 1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export default TextSize;
|
|
497
|
+
|
|
498
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import { saveBeforeFormat } from '../utils/history-helper.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Underline Format - Handles underline text formatting
|
|
6
|
+
* Extracted from FormatManager.js logic
|
|
7
|
+
*/
|
|
8
|
+
class Underline extends InlineFormat {
|
|
9
|
+
static formatName = 'underline';
|
|
10
|
+
static tagName = 'U';
|
|
11
|
+
static alternativeTagNames = ['SPAN'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Toggle underline formatting
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
toggle() {
|
|
18
|
+
// Save state before applying format
|
|
19
|
+
saveBeforeFormat();
|
|
20
|
+
|
|
21
|
+
if (this.isActive()) {
|
|
22
|
+
this.remove();
|
|
23
|
+
} else {
|
|
24
|
+
this.apply();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default Underline;
|