@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,39 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import { saveBeforeFormat } from '../utils/history-helper.js';
|
|
3
|
+
import registry from '../core/registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Superscript Format - Handles superscript text formatting
|
|
6
|
+
* Creates <sup> elements for superscript text
|
|
7
|
+
*/
|
|
8
|
+
class Superscript extends InlineFormat {
|
|
9
|
+
static formatName = 'superscript';
|
|
10
|
+
static tagName = 'SUP';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Toggle superscript formatting
|
|
14
|
+
*/
|
|
15
|
+
removeSubscriptBeforeApply() {
|
|
16
|
+
// Resolved via registry (not a static import) to avoid a circular
|
|
17
|
+
// dependency between superscript.js and subscript.js.
|
|
18
|
+
const Subscript = registry.get('formats/subscript');
|
|
19
|
+
if (!Subscript) return;
|
|
20
|
+
const subscript = new Subscript();
|
|
21
|
+
if (subscript.isActive()) {
|
|
22
|
+
subscript.remove();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
toggle() {
|
|
26
|
+
// Save state before applying format
|
|
27
|
+
saveBeforeFormat();
|
|
28
|
+
|
|
29
|
+
if (this.isActive()) {
|
|
30
|
+
this.remove();
|
|
31
|
+
} else {
|
|
32
|
+
// Ensure mutual exclusivity: remove subscript before applying superscript
|
|
33
|
+
this.removeSubscriptBeforeApply();
|
|
34
|
+
this.apply();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default Superscript;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { BlockFormat } from '../core/format.js';
|
|
2
|
+
import TablePopup from '../ui/table-popup.js';
|
|
3
|
+
import Editor from '../core/editor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Table Format - HTML table insertion
|
|
7
|
+
* Now supports multiple editor instances with separate popup instances
|
|
8
|
+
*/
|
|
9
|
+
class Table extends BlockFormat {
|
|
10
|
+
static formatName = 'table';
|
|
11
|
+
static tagName = 'TABLE';
|
|
12
|
+
static savedRanges = new Map(); // Map to store saved ranges for each editor
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
// Get current editor instance
|
|
18
|
+
const currentEditor = Editor.getCurrentInstance();
|
|
19
|
+
if (!currentEditor) {
|
|
20
|
+
console.warn('No editor instance found for Table format');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.editorId = currentEditor.instanceId;
|
|
25
|
+
|
|
26
|
+
// Check if this editor already has a table popup instance
|
|
27
|
+
let tablePopup = currentEditor.getPopupInstance('table');
|
|
28
|
+
|
|
29
|
+
if (!tablePopup) {
|
|
30
|
+
// Create new table popup instance for this editor
|
|
31
|
+
tablePopup = new TablePopup({
|
|
32
|
+
onTableSelect: (tableData) => {
|
|
33
|
+
Table.insertTable(tableData, this.editorId);
|
|
34
|
+
},
|
|
35
|
+
editor: currentEditor,
|
|
36
|
+
editorId: this.editorId
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Store popup instance in editor
|
|
40
|
+
currentEditor.setPopupInstance('table', tablePopup);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.tablePopup = tablePopup;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a new Table format instance for a specific editor
|
|
48
|
+
* @param {string} editorId - Editor instance ID
|
|
49
|
+
* @returns {Table} Table format instance
|
|
50
|
+
*/
|
|
51
|
+
static createForEditor(editorId) {
|
|
52
|
+
const editor = Editor.getInstanceById(editorId);
|
|
53
|
+
if (!editor) {
|
|
54
|
+
console.warn('No editor instance found for ID:', editorId);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Temporarily set as current instance
|
|
59
|
+
const originalCurrent = Editor.currentInstance;
|
|
60
|
+
Editor.currentInstance = editor;
|
|
61
|
+
|
|
62
|
+
// Create format instance
|
|
63
|
+
const format = new Table();
|
|
64
|
+
|
|
65
|
+
// Restore original current instance
|
|
66
|
+
Editor.currentInstance = originalCurrent;
|
|
67
|
+
|
|
68
|
+
return format;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Insert table at saved cursor position
|
|
73
|
+
* @param {Object} tableData - Table data with rows and cols
|
|
74
|
+
* @param {string} editorId - Editor instance ID
|
|
75
|
+
*/
|
|
76
|
+
static insertTable(tableData, editorId = null) {
|
|
77
|
+
// Get the correct editor instance
|
|
78
|
+
let editor = null;
|
|
79
|
+
if (editorId) {
|
|
80
|
+
editor = Editor.getInstanceById(editorId);
|
|
81
|
+
} else {
|
|
82
|
+
editor = Editor.getCurrentInstance();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!editor) {
|
|
86
|
+
console.warn('No editor instance found for table insertion');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Get saved range for this editor
|
|
91
|
+
const savedRange = Table.savedRanges.get(editorId);
|
|
92
|
+
if (!savedRange) return;
|
|
93
|
+
|
|
94
|
+
const selection = window.getSelection();
|
|
95
|
+
selection.removeAllRanges();
|
|
96
|
+
selection.addRange(savedRange);
|
|
97
|
+
|
|
98
|
+
const range = selection.getRangeAt(0);
|
|
99
|
+
|
|
100
|
+
// Create table HTML
|
|
101
|
+
const tableElement = Table.createTableElement(tableData.rows, tableData.cols);
|
|
102
|
+
|
|
103
|
+
// Clear any selected content
|
|
104
|
+
if (!range.collapsed) {
|
|
105
|
+
range.deleteContents();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Insert the table as a top-level block (not nested inside a heading or
|
|
109
|
+
// inline formatting tags, which would produce invalid HTML).
|
|
110
|
+
if (typeof editor.insertBlock === 'function') {
|
|
111
|
+
editor.insertBlock(tableElement);
|
|
112
|
+
} else {
|
|
113
|
+
range.insertNode(tableElement);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Position cursor in first cell
|
|
117
|
+
const firstCell = tableElement.querySelector('td');
|
|
118
|
+
if (firstCell) {
|
|
119
|
+
const newRange = document.createRange();
|
|
120
|
+
newRange.setStart(firstCell, 0);
|
|
121
|
+
newRange.collapse(true);
|
|
122
|
+
selection.removeAllRanges();
|
|
123
|
+
selection.addRange(newRange);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Clear saved range for this editor
|
|
127
|
+
Table.savedRanges.delete(editorId);
|
|
128
|
+
|
|
129
|
+
// Trigger content change event
|
|
130
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
131
|
+
editor.onContentChange();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create table element
|
|
137
|
+
*/
|
|
138
|
+
static createTableElement(rows, cols) {
|
|
139
|
+
const table = document.createElement('table');
|
|
140
|
+
table.className = 'rich-editor-table';
|
|
141
|
+
table.cellSpacing = '0';
|
|
142
|
+
table.cellPadding = '0';
|
|
143
|
+
table.border = '1';
|
|
144
|
+
|
|
145
|
+
const tbody = document.createElement('tbody');
|
|
146
|
+
|
|
147
|
+
for (let r = 0; r < rows; r++) {
|
|
148
|
+
const row = document.createElement('tr');
|
|
149
|
+
|
|
150
|
+
for (let c = 0; c < cols; c++) {
|
|
151
|
+
const cell = document.createElement('td');
|
|
152
|
+
cell.innerHTML = '<br>'; // empty placeholder (keeps height, not counted as content)
|
|
153
|
+
cell.style.minWidth = '50px';
|
|
154
|
+
cell.style.minHeight = '24px';
|
|
155
|
+
cell.style.padding = '4px 8px';
|
|
156
|
+
cell.style.border = '1px solid #ddd';
|
|
157
|
+
cell.style.verticalAlign = 'top';
|
|
158
|
+
|
|
159
|
+
// Make cells editable
|
|
160
|
+
cell.contentEditable = 'true';
|
|
161
|
+
|
|
162
|
+
row.appendChild(cell);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
tbody.appendChild(row);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
table.appendChild(tbody);
|
|
169
|
+
|
|
170
|
+
// Add table styles
|
|
171
|
+
table.style.borderCollapse = 'collapse';
|
|
172
|
+
table.style.width = '100%';
|
|
173
|
+
table.style.margin = '10px 0';
|
|
174
|
+
|
|
175
|
+
return table;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Toggle table popup
|
|
180
|
+
*/
|
|
181
|
+
toggle() {
|
|
182
|
+
if (this.tablePopup.isVisible) {
|
|
183
|
+
this.tablePopup.hide();
|
|
184
|
+
} else {
|
|
185
|
+
this.showPopup();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Show table popup
|
|
191
|
+
*/
|
|
192
|
+
showPopup() {
|
|
193
|
+
// Lưu vị trí con trỏ hiện tại cho editor này
|
|
194
|
+
const selection = window.getSelection();
|
|
195
|
+
if (selection && selection.rangeCount > 0) {
|
|
196
|
+
Table.savedRanges.set(this.editorId, selection.getRangeAt(0).cloneRange());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Find table button in the current editor's toolbar
|
|
200
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
201
|
+
if (!editor) return;
|
|
202
|
+
|
|
203
|
+
const toolbar = editor.getModule('toolbar');
|
|
204
|
+
let tableButton = null;
|
|
205
|
+
|
|
206
|
+
if (toolbar) {
|
|
207
|
+
tableButton = toolbar.getButton('table');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
211
|
+
if (!tableButton) {
|
|
212
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
213
|
+
if (toolbarContainer) {
|
|
214
|
+
tableButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.table-btn');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Final fallback: find any table button in the current editor's wrapper
|
|
219
|
+
if (!tableButton) {
|
|
220
|
+
tableButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.table-btn');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!tableButton) {
|
|
224
|
+
console.warn('Table button not found for editor:', this.editorId);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.tablePopup.show(tableButton);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if cursor is in a table
|
|
233
|
+
*/
|
|
234
|
+
isActive() {
|
|
235
|
+
const selection = window.getSelection();
|
|
236
|
+
if (!selection || !selection.rangeCount) return false;
|
|
237
|
+
|
|
238
|
+
let node = selection.getRangeAt(0).startContainer;
|
|
239
|
+
|
|
240
|
+
// Find parent table element
|
|
241
|
+
while (node && node !== document.body) {
|
|
242
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'TABLE') {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
node = node.parentNode;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get current table if cursor is in one
|
|
253
|
+
*/
|
|
254
|
+
getCurrentTable() {
|
|
255
|
+
const selection = window.getSelection();
|
|
256
|
+
if (!selection || !selection.rangeCount) return null;
|
|
257
|
+
|
|
258
|
+
let node = selection.getRangeAt(0).startContainer;
|
|
259
|
+
|
|
260
|
+
// Find parent table element
|
|
261
|
+
while (node && node !== document.body) {
|
|
262
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'TABLE') {
|
|
263
|
+
return node;
|
|
264
|
+
}
|
|
265
|
+
node = node.parentNode;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Apply table formatting (not applicable for this format)
|
|
273
|
+
*/
|
|
274
|
+
apply() {
|
|
275
|
+
this.showPopup();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Remove table formatting
|
|
280
|
+
*/
|
|
281
|
+
remove() {
|
|
282
|
+
const table = this.getCurrentTable();
|
|
283
|
+
if (table) {
|
|
284
|
+
// Hide resize handles before removing table
|
|
285
|
+
if (window.richEditor && window.richEditor.resizeHandles) {
|
|
286
|
+
window.richEditor.resizeHandles.hideHandles();
|
|
287
|
+
}
|
|
288
|
+
table.parentNode.removeChild(table);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default Table;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import TagPopup from '../ui/tag-popup.js';
|
|
3
|
+
import Editor from '../core/editor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tag Format - Handles custom tag insertion
|
|
7
|
+
* Now supports multiple editor instances with separate popup instances
|
|
8
|
+
*/
|
|
9
|
+
class Tag extends InlineFormat {
|
|
10
|
+
static formatName = 'tag';
|
|
11
|
+
static tagName = 'SPAN';
|
|
12
|
+
static className = 'custom-tag';
|
|
13
|
+
static savedRanges = new Map(); // Map to store saved ranges for each editor
|
|
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 Tag format');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.editorId = currentEditor.instanceId;
|
|
26
|
+
|
|
27
|
+
// Check if this editor already has a tag popup instance
|
|
28
|
+
let tagPopup = currentEditor.getPopupInstance('tag');
|
|
29
|
+
|
|
30
|
+
if (!tagPopup) {
|
|
31
|
+
// Create new tag popup instance for this editor
|
|
32
|
+
tagPopup = new TagPopup({
|
|
33
|
+
onTagInsert: (tagType, tagContent) => {
|
|
34
|
+
Tag.insertTagAtCurrentPosition(tagType, tagContent, this.editorId);
|
|
35
|
+
},
|
|
36
|
+
editor: currentEditor,
|
|
37
|
+
editorId: this.editorId
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Store popup instance in editor
|
|
41
|
+
currentEditor.setPopupInstance('tag', tagPopup);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.tagPopup = tagPopup;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new Tag format instance for a specific editor
|
|
49
|
+
* @param {string} editorId - Editor instance ID
|
|
50
|
+
* @returns {Tag} Tag format instance
|
|
51
|
+
*/
|
|
52
|
+
static createForEditor(editorId) {
|
|
53
|
+
const editor = Editor.getInstanceById(editorId);
|
|
54
|
+
if (!editor) {
|
|
55
|
+
console.warn('No editor instance found for ID:', editorId);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Temporarily set as current instance
|
|
60
|
+
const originalCurrent = Editor.currentInstance;
|
|
61
|
+
Editor.currentInstance = editor;
|
|
62
|
+
|
|
63
|
+
// Create format instance
|
|
64
|
+
const format = new Tag();
|
|
65
|
+
|
|
66
|
+
// Restore original current instance
|
|
67
|
+
Editor.currentInstance = originalCurrent;
|
|
68
|
+
|
|
69
|
+
return format;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create tag element
|
|
74
|
+
* @param {string} tagType - Type of tag (@, #, custom)
|
|
75
|
+
* @param {string} content - Tag content
|
|
76
|
+
* @returns {HTMLElement}
|
|
77
|
+
*/
|
|
78
|
+
static create(tagType, content) {
|
|
79
|
+
const span = document.createElement('SPAN');
|
|
80
|
+
span.className = `custom-tag tag-${tagType}`;
|
|
81
|
+
|
|
82
|
+
let displayText = content;
|
|
83
|
+
if (tagType === 'mention') {
|
|
84
|
+
displayText = `@${content}`;
|
|
85
|
+
} else if (tagType === 'hashtag') {
|
|
86
|
+
displayText = `#${content}`;
|
|
87
|
+
} else if (tagType === 'custom') {
|
|
88
|
+
displayText = `<${content}>`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
span.textContent = displayText;
|
|
92
|
+
span.setAttribute('data-tag-type', tagType);
|
|
93
|
+
span.setAttribute('data-tag-content', content);
|
|
94
|
+
span.setAttribute('contenteditable', 'false');
|
|
95
|
+
|
|
96
|
+
return span;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Insert tag at current cursor position
|
|
101
|
+
* @param {string} tagType - Type of tag
|
|
102
|
+
* @param {string} content - Tag content
|
|
103
|
+
* @param {string} editorId - Editor instance ID
|
|
104
|
+
*/
|
|
105
|
+
static insertTagAtCurrentPosition(tagType, content, editorId = null) {
|
|
106
|
+
// Get the correct editor instance
|
|
107
|
+
let editor = null;
|
|
108
|
+
if (editorId) {
|
|
109
|
+
editor = Editor.getInstanceById(editorId);
|
|
110
|
+
} else {
|
|
111
|
+
editor = Editor.getCurrentInstance();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!editor) {
|
|
115
|
+
console.warn('No editor instance found for tag insertion');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use saved range if available, otherwise get current selection
|
|
120
|
+
const selection = window.getSelection();
|
|
121
|
+
if (!selection) return;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// Restore saved range if exists for this editor
|
|
125
|
+
const savedRange = Tag.savedRanges.get(editorId);
|
|
126
|
+
if (savedRange) {
|
|
127
|
+
selection.removeAllRanges();
|
|
128
|
+
selection.addRange(savedRange);
|
|
129
|
+
Tag.savedRanges.delete(editorId);
|
|
130
|
+
} else if (!selection.rangeCount) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const range = selection.getRangeAt(0);
|
|
135
|
+
|
|
136
|
+
// Create tag element
|
|
137
|
+
const tagElement = Tag.create(tagType, content);
|
|
138
|
+
|
|
139
|
+
// Insert tag at cursor position
|
|
140
|
+
range.deleteContents();
|
|
141
|
+
range.insertNode(tagElement);
|
|
142
|
+
|
|
143
|
+
// Add a space after the tag for easier editing
|
|
144
|
+
const spaceNode = document.createTextNode(' ');
|
|
145
|
+
range.setStartAfter(tagElement);
|
|
146
|
+
range.insertNode(spaceNode);
|
|
147
|
+
|
|
148
|
+
// Position cursor after the space
|
|
149
|
+
range.setStartAfter(spaceNode);
|
|
150
|
+
range.collapse(true);
|
|
151
|
+
selection.removeAllRanges();
|
|
152
|
+
selection.addRange(range);
|
|
153
|
+
|
|
154
|
+
// Focus back on editor
|
|
155
|
+
if (editor && editor.element) {
|
|
156
|
+
editor.element.focus();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Trigger content change event
|
|
160
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
161
|
+
editor.onContentChange();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('Error inserting tag:', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Apply tag formatting - shows tag popup
|
|
171
|
+
*/
|
|
172
|
+
apply(tagType, content) {
|
|
173
|
+
if (tagType && content) {
|
|
174
|
+
Tag.insertTagAtCurrentPosition(tagType, content, this.editorId);
|
|
175
|
+
} else {
|
|
176
|
+
// Save current selection before showing popup
|
|
177
|
+
const selection = window.getSelection();
|
|
178
|
+
if (selection && selection.rangeCount > 0) {
|
|
179
|
+
Tag.savedRanges.set(this.editorId, selection.getRangeAt(0).cloneRange());
|
|
180
|
+
}
|
|
181
|
+
this.showTagPopup();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Remove tag formatting
|
|
187
|
+
*/
|
|
188
|
+
remove() {
|
|
189
|
+
const selection = window.getSelection();
|
|
190
|
+
if (!selection || !selection.rangeCount) return;
|
|
191
|
+
|
|
192
|
+
const range = selection.getRangeAt(0);
|
|
193
|
+
const tagElement = this.getTagElement(range);
|
|
194
|
+
|
|
195
|
+
if (tagElement) {
|
|
196
|
+
// Replace tag element with its text content
|
|
197
|
+
const textNode = document.createTextNode(tagElement.textContent);
|
|
198
|
+
tagElement.parentNode.replaceChild(textNode, tagElement);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Toggle tag formatting - shows tag popup
|
|
204
|
+
*/
|
|
205
|
+
toggle() {
|
|
206
|
+
if (this.tagPopup.isVisible) {
|
|
207
|
+
this.tagPopup.hide();
|
|
208
|
+
} else {
|
|
209
|
+
// Save current selection before showing popup
|
|
210
|
+
const selection = window.getSelection();
|
|
211
|
+
if (selection && selection.rangeCount > 0) {
|
|
212
|
+
Tag.savedRanges.set(this.editorId, selection.getRangeAt(0).cloneRange());
|
|
213
|
+
}
|
|
214
|
+
this.showTagPopup();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Show tag popup
|
|
220
|
+
*/
|
|
221
|
+
showTagPopup() {
|
|
222
|
+
// Find tag button in the current editor's toolbar
|
|
223
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
224
|
+
if (!editor) return;
|
|
225
|
+
|
|
226
|
+
const toolbar = editor.getModule('toolbar');
|
|
227
|
+
let tagButton = null;
|
|
228
|
+
|
|
229
|
+
if (toolbar) {
|
|
230
|
+
tagButton = toolbar.getButton('tag');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
234
|
+
if (!tagButton) {
|
|
235
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
236
|
+
if (toolbarContainer) {
|
|
237
|
+
tagButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.tag-btn');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Final fallback: find any tag button in the current editor's wrapper
|
|
242
|
+
if (!tagButton) {
|
|
243
|
+
tagButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.tag-btn');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!tagButton) {
|
|
247
|
+
console.warn('Tag button not found for editor:', this.editorId);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.tagPopup.show(tagButton);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if tag formatting is active
|
|
256
|
+
*/
|
|
257
|
+
isActive() {
|
|
258
|
+
const selection = window.getSelection();
|
|
259
|
+
if (!selection || !selection.rangeCount) return false;
|
|
260
|
+
|
|
261
|
+
const range = selection.getRangeAt(0);
|
|
262
|
+
return this.getTagElement(range) !== null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get tag element from selection
|
|
267
|
+
* @param {Range} range - Selection range
|
|
268
|
+
* @returns {HTMLElement|null}
|
|
269
|
+
*/
|
|
270
|
+
getTagElement(range) {
|
|
271
|
+
let node = range.commonAncestorContainer;
|
|
272
|
+
|
|
273
|
+
// If it's a text node, get its parent
|
|
274
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
275
|
+
node = node.parentNode;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check if current node is a tag
|
|
279
|
+
if (node.classList && node.classList.contains('custom-tag')) {
|
|
280
|
+
return node;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check if selection contains a tag
|
|
284
|
+
const tagInSelection = range.cloneContents().querySelector('.custom-tag');
|
|
285
|
+
return tagInSelection || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get predefined tag suggestions
|
|
290
|
+
* @param {string} tagType - Type of tag
|
|
291
|
+
* @returns {Array} - Array of suggestions
|
|
292
|
+
*/
|
|
293
|
+
static getSuggestions(tagType) {
|
|
294
|
+
const suggestions = {
|
|
295
|
+
mention: ['john', 'sarah', 'admin', 'team', 'support'],
|
|
296
|
+
hashtag: ['urgent', 'todo', 'done', 'review', 'important'],
|
|
297
|
+
custom: ['note', 'warning', 'tip', 'info', 'success']
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return suggestions[tagType] || [];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default Tag;
|