@oix1987/yjd 1.0.3 → 2.0.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 +146 -142
- package/core.js +77 -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 +134 -103
- package/index.js +227 -0
- package/lib/core/editor.js +1806 -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 +347 -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/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/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1285 -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 +18 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { BlockFormat } from '../core/format.js';
|
|
2
|
+
import TextAlignPicker from '../ui/text-align-picker.js';
|
|
3
|
+
import IconUtils from '../ui/icons.js';
|
|
4
|
+
import { saveBeforeFormat } from '../utils/history-helper.js';
|
|
5
|
+
import Editor from '../core/editor.js';
|
|
6
|
+
import { execFormat } from '../utils/exec-command.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Text Align Format - Handles text alignment formatting
|
|
10
|
+
* Now supports multiple editor instances with separate popup instances
|
|
11
|
+
*/
|
|
12
|
+
class TextAlign extends BlockFormat {
|
|
13
|
+
static formatName = 'text-align';
|
|
14
|
+
static tagName = 'P';
|
|
15
|
+
static attribute = 'style';
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
|
|
20
|
+
// Get current editor instance
|
|
21
|
+
const currentEditor = Editor.getCurrentInstance();
|
|
22
|
+
if (!currentEditor) {
|
|
23
|
+
console.warn('No editor instance found for TextAlign format');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.editorId = currentEditor.instanceId;
|
|
28
|
+
|
|
29
|
+
// Check if this editor already has a text align picker instance
|
|
30
|
+
let alignPicker = currentEditor.getPopupInstance('text-align');
|
|
31
|
+
|
|
32
|
+
if (!alignPicker) {
|
|
33
|
+
// Create new text align picker instance for this editor
|
|
34
|
+
alignPicker = new TextAlignPicker({
|
|
35
|
+
onAlignSelect: (alignment) => {
|
|
36
|
+
TextAlign.applyAlignToCurrentSelection(alignment, this.editorId);
|
|
37
|
+
},
|
|
38
|
+
editor: currentEditor,
|
|
39
|
+
editorId: this.editorId
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Store popup instance in editor
|
|
43
|
+
currentEditor.setPopupInstance('text-align', alignPicker);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.alignPicker = alignPicker;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a new TextAlign format instance for a specific editor
|
|
51
|
+
* @param {string} editorId - Editor instance ID
|
|
52
|
+
* @returns {TextAlign} TextAlign format instance
|
|
53
|
+
*/
|
|
54
|
+
static createForEditor(editorId) {
|
|
55
|
+
const editor = Editor.getInstanceById(editorId);
|
|
56
|
+
if (!editor) {
|
|
57
|
+
console.warn('No editor instance found for ID:', editorId);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Temporarily set as current instance
|
|
62
|
+
const originalCurrent = Editor.currentInstance;
|
|
63
|
+
Editor.currentInstance = editor;
|
|
64
|
+
|
|
65
|
+
// Create format instance
|
|
66
|
+
const format = new TextAlign();
|
|
67
|
+
|
|
68
|
+
// Restore original current instance
|
|
69
|
+
Editor.currentInstance = originalCurrent;
|
|
70
|
+
|
|
71
|
+
return format;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create block element with text alignment
|
|
76
|
+
* @param {string} value - Alignment value (left, center, right, justify)
|
|
77
|
+
* @returns {HTMLElement}
|
|
78
|
+
*/
|
|
79
|
+
static create(value) {
|
|
80
|
+
const node = document.createElement(this.tagName);
|
|
81
|
+
if (value && value !== 'left') {
|
|
82
|
+
node.style.textAlign = value;
|
|
83
|
+
}
|
|
84
|
+
return node;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Static method to apply alignment to current selection or cursor position
|
|
89
|
+
* @param {string} alignment - Alignment value
|
|
90
|
+
* @param {string} editorId - Editor instance ID
|
|
91
|
+
*/
|
|
92
|
+
static applyAlignToCurrentSelection(alignment, editorId = null) {
|
|
93
|
+
// Get the correct editor instance
|
|
94
|
+
let editor = null;
|
|
95
|
+
if (editorId) {
|
|
96
|
+
editor = Editor.getInstanceById(editorId);
|
|
97
|
+
} else {
|
|
98
|
+
editor = Editor.getCurrentInstance();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!editor) {
|
|
102
|
+
console.warn('No editor instance found for text alignment application');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const selection = window.getSelection();
|
|
107
|
+
if (!selection || !selection.rangeCount) return;
|
|
108
|
+
|
|
109
|
+
// Save state before applying format
|
|
110
|
+
saveBeforeFormat();
|
|
111
|
+
// Lưu vị trí caret trước khi thay đổi
|
|
112
|
+
const originalRange = selection.getRangeAt(0);
|
|
113
|
+
const caretContainer = originalRange.endContainer;
|
|
114
|
+
const caretOffset = originalRange.endOffset;
|
|
115
|
+
try {
|
|
116
|
+
const range = selection.getRangeAt(0);
|
|
117
|
+
const blockElements = TextAlign.getSelectedBlockElements(range);
|
|
118
|
+
|
|
119
|
+
if (blockElements.length === 0) {
|
|
120
|
+
// If no block elements found, create one
|
|
121
|
+
execFormat('formatBlock', 'p');
|
|
122
|
+
const newRange = selection.getRangeAt(0);
|
|
123
|
+
const newBlocks = TextAlign.getSelectedBlockElements(newRange);
|
|
124
|
+
newBlocks.forEach(block => {
|
|
125
|
+
TextAlign.applyAlignmentToBlock(block, alignment);
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
// Apply alignment to existing blocks
|
|
129
|
+
blockElements.forEach(block => {
|
|
130
|
+
TextAlign.applyAlignmentToBlock(block, alignment);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Update toolbar button icon after applying alignment
|
|
135
|
+
TextAlign.updateToolbarButtonIcon(alignment, editorId);
|
|
136
|
+
// Khôi phục caret
|
|
137
|
+
selection.removeAllRanges();
|
|
138
|
+
const newCaretRange = document.createRange();
|
|
139
|
+
newCaretRange.setStart(caretContainer, caretOffset);
|
|
140
|
+
newCaretRange.collapse(true);
|
|
141
|
+
selection.addRange(newCaretRange);
|
|
142
|
+
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Error applying text alignment:', error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Trigger content change after applying format
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
150
|
+
editor.onContentChange();
|
|
151
|
+
}
|
|
152
|
+
}, 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Apply alignment to a specific block element
|
|
157
|
+
*/
|
|
158
|
+
static applyAlignmentToBlock(block, alignment) {
|
|
159
|
+
if (alignment === 'left') {
|
|
160
|
+
block.style.textAlign = '';
|
|
161
|
+
} else {
|
|
162
|
+
block.style.textAlign = alignment;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get icon name for alignment value
|
|
168
|
+
* @param {string} alignment - Alignment value
|
|
169
|
+
* @returns {string} Icon name
|
|
170
|
+
*/
|
|
171
|
+
static getIconNameForAlignment(alignment) {
|
|
172
|
+
const iconMap = {
|
|
173
|
+
'left': 'align-left',
|
|
174
|
+
'center': 'align-center',
|
|
175
|
+
'right': 'align-right',
|
|
176
|
+
'justify': 'align-justify'
|
|
177
|
+
};
|
|
178
|
+
return iconMap[alignment] || 'align-center';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Update toolbar button icon based on alignment
|
|
183
|
+
* @param {string} alignment - Current alignment
|
|
184
|
+
* @param {string} editorId - Editor instance ID
|
|
185
|
+
*/
|
|
186
|
+
static updateToolbarButtonIcon(alignment, editorId = null) {
|
|
187
|
+
// Get the correct editor instance
|
|
188
|
+
let editor = null;
|
|
189
|
+
if (editorId) {
|
|
190
|
+
editor = Editor.getInstanceById(editorId);
|
|
191
|
+
} else {
|
|
192
|
+
editor = Editor.getCurrentInstance();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!editor) return;
|
|
196
|
+
|
|
197
|
+
const toolbar = editor.getModule('toolbar');
|
|
198
|
+
let button = null;
|
|
199
|
+
|
|
200
|
+
if (toolbar) {
|
|
201
|
+
button = toolbar.getButton('text-align');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
205
|
+
if (!button) {
|
|
206
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
207
|
+
if (toolbarContainer) {
|
|
208
|
+
button = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-align-btn');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Final fallback: find any text-align button in the current editor's wrapper
|
|
213
|
+
if (!button) {
|
|
214
|
+
button = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-align-btn');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!button) return;
|
|
218
|
+
|
|
219
|
+
const iconName = TextAlign.getIconNameForAlignment(alignment);
|
|
220
|
+
const titleMap = {
|
|
221
|
+
'left': 'Align Left',
|
|
222
|
+
'center': 'Align Center',
|
|
223
|
+
'right': 'Align Right',
|
|
224
|
+
'justify': 'Justify'
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Update button title
|
|
228
|
+
button.title = titleMap[alignment] || 'Text Alignment';
|
|
229
|
+
|
|
230
|
+
// Update icon
|
|
231
|
+
const svgContent = IconUtils.getIcon(iconName);
|
|
232
|
+
if (svgContent) {
|
|
233
|
+
const iconSpan = button.querySelector('.icon');
|
|
234
|
+
if (iconSpan) {
|
|
235
|
+
iconSpan.innerHTML = svgContent;
|
|
236
|
+
} else {
|
|
237
|
+
button.innerHTML = `<span class="icon">${svgContent}</span>`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all selected block elements
|
|
244
|
+
*/
|
|
245
|
+
static getSelectedBlockElements(range) {
|
|
246
|
+
const blocks = [];
|
|
247
|
+
|
|
248
|
+
// Xác định block chứa điểm bắt đầu và kết thúc
|
|
249
|
+
const startBlock = TextAlign.getBlockElement(range.startContainer);
|
|
250
|
+
const endBlock = TextAlign.getBlockElement(range.endContainer);
|
|
251
|
+
|
|
252
|
+
if (!startBlock || !endBlock) return blocks;
|
|
253
|
+
|
|
254
|
+
// Nếu chỉ trong 1 block
|
|
255
|
+
if (startBlock === endBlock) {
|
|
256
|
+
blocks.push(startBlock);
|
|
257
|
+
return blocks;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Duyệt từ startBlock tới endBlock
|
|
261
|
+
let currentBlock = startBlock;
|
|
262
|
+
while (currentBlock) {
|
|
263
|
+
// Chỉ thêm block nếu nó giao với range
|
|
264
|
+
const blockRange = document.createRange();
|
|
265
|
+
blockRange.selectNodeContents(currentBlock);
|
|
266
|
+
|
|
267
|
+
if (range.compareBoundaryPoints(Range.END_TO_START, blockRange) < 0 &&
|
|
268
|
+
range.compareBoundaryPoints(Range.START_TO_END, blockRange) > 0) {
|
|
269
|
+
blocks.push(currentBlock);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (currentBlock === endBlock) break;
|
|
273
|
+
currentBlock = TextAlign.getNextBlockElement(currentBlock);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return blocks;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the block element containing the given node
|
|
281
|
+
*/
|
|
282
|
+
static getBlockElement(node) {
|
|
283
|
+
if (!node) return null;
|
|
284
|
+
|
|
285
|
+
let currentNode = node;
|
|
286
|
+
while (currentNode && currentNode !== document.body) {
|
|
287
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
288
|
+
const tagName = currentNode.tagName;
|
|
289
|
+
if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI'].includes(tagName)) {
|
|
290
|
+
return currentNode;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
currentNode = currentNode.parentNode;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get next block element in document order
|
|
300
|
+
*/
|
|
301
|
+
static getNextBlockElement(element) {
|
|
302
|
+
let currentNode = element.nextSibling;
|
|
303
|
+
|
|
304
|
+
while (currentNode) {
|
|
305
|
+
if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
306
|
+
const tagName = currentNode.tagName;
|
|
307
|
+
if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI'].includes(tagName)) {
|
|
308
|
+
return currentNode;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
currentNode = currentNode.nextSibling;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Apply alignment formatting with specified value
|
|
319
|
+
* @param {string} value - Alignment value (left, center, right, justify)
|
|
320
|
+
*/
|
|
321
|
+
apply(value = 'left') {
|
|
322
|
+
TextAlign.applyAlignToCurrentSelection(value, this.editorId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove alignment formatting (reset to left)
|
|
327
|
+
*/
|
|
328
|
+
remove() {
|
|
329
|
+
TextAlign.applyAlignToCurrentSelection('left', this.editorId);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Toggle alignment formatting - shows/hides alignment picker
|
|
334
|
+
*/
|
|
335
|
+
toggle() {
|
|
336
|
+
if (this.alignPicker.isVisible) {
|
|
337
|
+
this.alignPicker.hide();
|
|
338
|
+
} else {
|
|
339
|
+
this.showAlignPicker();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Show alignment picker positioned relative to align button on toolbar
|
|
345
|
+
*/
|
|
346
|
+
showAlignPicker() {
|
|
347
|
+
// Find text-align button in the current editor's toolbar
|
|
348
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
349
|
+
if (!editor) return;
|
|
350
|
+
|
|
351
|
+
const toolbar = editor.getModule('toolbar');
|
|
352
|
+
let alignButton = null;
|
|
353
|
+
|
|
354
|
+
if (toolbar) {
|
|
355
|
+
alignButton = toolbar.getButton('text-align');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
359
|
+
if (!alignButton) {
|
|
360
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
361
|
+
if (toolbarContainer) {
|
|
362
|
+
alignButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.text-align-btn');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Final fallback: find any text-align button in the current editor's wrapper
|
|
367
|
+
if (!alignButton) {
|
|
368
|
+
alignButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.text-align-btn');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!alignButton) {
|
|
372
|
+
console.warn('Text-align button not found for editor:', this.editorId);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.alignPicker.show(alignButton);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check if specific alignment is active in current selection
|
|
381
|
+
* Always returns false because text-align button should not have active state
|
|
382
|
+
* Instead, the button icon changes to reflect current alignment
|
|
383
|
+
*/
|
|
384
|
+
isActive(alignment = null) {
|
|
385
|
+
// Update button icon based on current alignment
|
|
386
|
+
const currentAlignment = TextAlign.getCurrentAlignment();
|
|
387
|
+
TextAlign.updateToolbarButtonIcon(currentAlignment, this.editorId);
|
|
388
|
+
|
|
389
|
+
// Highlight when a non-default alignment (center / right / justify) is set.
|
|
390
|
+
return !!currentAlignment && currentAlignment !== 'left' && currentAlignment !== 'start';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get current alignment of selection
|
|
395
|
+
*/
|
|
396
|
+
static getCurrentAlignment() {
|
|
397
|
+
const selection = window.getSelection();
|
|
398
|
+
if (!selection || !selection.rangeCount) return 'left';
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const range = selection.getRangeAt(0);
|
|
402
|
+
// Lấy tất cả block trong vùng chọn
|
|
403
|
+
const blocks = TextAlign.getSelectedBlockElements(range);
|
|
404
|
+
|
|
405
|
+
// Nếu có nhiều block -> lấy block đầu tiên
|
|
406
|
+
const firstBlock = blocks.length > 0
|
|
407
|
+
? blocks[0]
|
|
408
|
+
: TextAlign.getBlockElement(range.commonAncestorContainer);
|
|
409
|
+
|
|
410
|
+
if (!firstBlock) return 'left';
|
|
411
|
+
|
|
412
|
+
const textAlign = window.getComputedStyle(firstBlock).textAlign;
|
|
413
|
+
return textAlign === 'left' || textAlign === 'start' || !textAlign ? 'left' : textAlign;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error('Error getting current alignment:', error);
|
|
416
|
+
return 'left';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default TextAlign;
|