@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,502 @@
|
|
|
1
|
+
import { BlockFormat } 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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Heading Format - Handles heading and paragraph formatting
|
|
8
|
+
* Now supports multiple editor instances with separate popup instances
|
|
9
|
+
*/
|
|
10
|
+
class Heading extends BlockFormat {
|
|
11
|
+
static formatName = 'heading';
|
|
12
|
+
static tagName = 'H1'; // Default tag, will be overridden
|
|
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 Heading format');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.editorId = currentEditor.instanceId;
|
|
25
|
+
|
|
26
|
+
// Check if this editor already has a heading select instance
|
|
27
|
+
let customSelect = currentEditor.getPopupInstance('heading');
|
|
28
|
+
|
|
29
|
+
if (!customSelect) {
|
|
30
|
+
// Create new custom select instance for this editor
|
|
31
|
+
const tagMap = Heading.getTagMap();
|
|
32
|
+
const items = Object.values(tagMap).map(tagData => ({
|
|
33
|
+
value: tagData.tag,
|
|
34
|
+
label: tagData.element,
|
|
35
|
+
title: tagData.title
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
customSelect = new CustomSelect({
|
|
39
|
+
items: items,
|
|
40
|
+
displayProperty: 'label',
|
|
41
|
+
valueProperty: 'value',
|
|
42
|
+
className: 'heading-select',
|
|
43
|
+
onItemSelect: (value, item) => {
|
|
44
|
+
Heading.applyTagToCurrentSelection(value, this.editorId);
|
|
45
|
+
},
|
|
46
|
+
editor: currentEditor,
|
|
47
|
+
editorId: this.editorId
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Store popup instance in editor
|
|
51
|
+
currentEditor.setPopupInstance('heading', customSelect);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.customSelect = customSelect;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a new Heading format instance for a specific editor
|
|
59
|
+
* @param {string} editorId - Editor instance ID
|
|
60
|
+
* @returns {Heading} Heading format instance
|
|
61
|
+
*/
|
|
62
|
+
static createForEditor(editorId) {
|
|
63
|
+
const editor = Editor.getInstanceById(editorId);
|
|
64
|
+
if (!editor) {
|
|
65
|
+
console.warn('No editor instance found for ID:', editorId);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Temporarily set as current instance
|
|
70
|
+
const originalCurrent = Editor.currentInstance;
|
|
71
|
+
Editor.currentInstance = editor;
|
|
72
|
+
|
|
73
|
+
// Create format instance
|
|
74
|
+
const format = new Heading();
|
|
75
|
+
|
|
76
|
+
// Restore original current instance
|
|
77
|
+
Editor.currentInstance = originalCurrent;
|
|
78
|
+
|
|
79
|
+
return format;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get display name for tag
|
|
84
|
+
* @param {string} tag - HTML tag name
|
|
85
|
+
* @returns {string} Display name
|
|
86
|
+
*/
|
|
87
|
+
static getTagMap() {
|
|
88
|
+
return {
|
|
89
|
+
'H1': { tag: 'H1', element: '<h1 style="margin:0">Heading 1</h1>', title: 'Heading 1' },
|
|
90
|
+
'H2': { tag: 'H2', element: '<h2 style="margin:0">Heading 2</h2>', title: 'Heading 2' },
|
|
91
|
+
'H3': { tag: 'H3', element: '<h3 style="margin:0">Heading 3</h3>', title: 'Heading 3' },
|
|
92
|
+
'H4': { tag: 'H4', element: '<h4 style="margin:0">Heading 4</h4>', title: 'Heading 4' },
|
|
93
|
+
'H5': { tag: 'H5', element: '<h5 style="margin:0">Heading 5</h5>', title: 'Heading 5' },
|
|
94
|
+
'H6': { tag: 'H6', element: '<h6 style="margin:0">Heading 6</h6>', title: 'Heading 6' },
|
|
95
|
+
'P': { tag: 'P', element: '<p style="margin:0">Paragraph</p>', title: 'Paragraph' },
|
|
96
|
+
'PRE': { tag: 'PRE', element: '<pre style="margin:0">Code</pre>', title: 'Preformatted' },
|
|
97
|
+
'BLOCKQUOTE': { tag: 'BLOCKQUOTE', element: '<blockquote style="margin:0">Quote</blockquote>', title: 'Quote' }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static getTagDisplayName(tag) {
|
|
102
|
+
const tagMap = this.getTagMap();
|
|
103
|
+
return tagMap[tag]?.title || 'Paragraph';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Update custom button text based on current tag
|
|
108
|
+
*/
|
|
109
|
+
updateButtonText() {
|
|
110
|
+
const currentTag = this.getCurrentTag();
|
|
111
|
+
const displayName = Heading.getTagDisplayName(currentTag || 'P');
|
|
112
|
+
|
|
113
|
+
// Find heading button in the current editor's toolbar
|
|
114
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
115
|
+
if (!editor) return;
|
|
116
|
+
|
|
117
|
+
const toolbar = editor.getModule('toolbar');
|
|
118
|
+
let headingButton = null;
|
|
119
|
+
|
|
120
|
+
if (toolbar) {
|
|
121
|
+
headingButton = toolbar.getButton('heading');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
125
|
+
if (!headingButton) {
|
|
126
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
127
|
+
if (toolbarContainer) {
|
|
128
|
+
headingButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.heading-btn');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Final fallback: find any heading button in the current editor's wrapper
|
|
133
|
+
if (!headingButton) {
|
|
134
|
+
headingButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.heading-btn');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (headingButton && headingButton.updateText) {
|
|
138
|
+
headingButton.updateText(displayName);
|
|
139
|
+
} else if (headingButton) {
|
|
140
|
+
headingButton.textContent = displayName;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create element with specific tag
|
|
146
|
+
* @param {string} tag - HTML tag name (H1, H2, P, etc.)
|
|
147
|
+
* @returns {HTMLElement}
|
|
148
|
+
*/
|
|
149
|
+
static create(tag = 'P') {
|
|
150
|
+
const node = document.createElement(tag.toUpperCase());
|
|
151
|
+
return node;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Static method to apply tag to current selection
|
|
156
|
+
* @param {string} tag - HTML tag name
|
|
157
|
+
* @param {string} editorId - Editor instance ID
|
|
158
|
+
*/
|
|
159
|
+
static applyTagToCurrentSelection(tag, editorId = null) {
|
|
160
|
+
// Get the correct editor instance
|
|
161
|
+
let editor = null;
|
|
162
|
+
if (editorId) {
|
|
163
|
+
editor = Editor.getInstanceById(editorId);
|
|
164
|
+
} else {
|
|
165
|
+
editor = Editor.getCurrentInstance();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!editor) {
|
|
169
|
+
console.warn('No editor instance found for heading application');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selection = window.getSelection();
|
|
174
|
+
if (!selection || !selection.rangeCount) return;
|
|
175
|
+
|
|
176
|
+
// Save state before applying format
|
|
177
|
+
saveBeforeFormat();
|
|
178
|
+
|
|
179
|
+
const range = selection.getRangeAt(0);
|
|
180
|
+
const headingFormat = Heading.createForEditor(editorId);
|
|
181
|
+
if (headingFormat) {
|
|
182
|
+
headingFormat.apply(tag);
|
|
183
|
+
|
|
184
|
+
// Update button text after applying
|
|
185
|
+
headingFormat.updateButtonText();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Trigger content change after applying format
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
191
|
+
editor.onContentChange();
|
|
192
|
+
}
|
|
193
|
+
}, 0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Apply heading format with specified tag
|
|
198
|
+
* @param {string} tag - HTML tag name (H1, H2, P, etc.)
|
|
199
|
+
*/
|
|
200
|
+
apply(tag = 'P') {
|
|
201
|
+
const selection = window.getSelection();
|
|
202
|
+
if (!selection || !selection.rangeCount) return;
|
|
203
|
+
|
|
204
|
+
// Lưu selection trước khi đổi
|
|
205
|
+
const range = selection.getRangeAt(0);
|
|
206
|
+
const isCollapsed = range.collapsed; // true = không bôi đen gì
|
|
207
|
+
|
|
208
|
+
const blocks = this.getBlockElements(range);
|
|
209
|
+
|
|
210
|
+
if (blocks.length === 0) {
|
|
211
|
+
// Không có block - tạo mới
|
|
212
|
+
const newBlock = this.createBlockAtCursor(range, tag);
|
|
213
|
+
|
|
214
|
+
// Sau khi tạo block mới → đặt con trỏ vào block
|
|
215
|
+
const newRange = document.createRange();
|
|
216
|
+
newRange.setStart(newBlock, 0);
|
|
217
|
+
newRange.collapse(true);
|
|
218
|
+
selection.removeAllRanges();
|
|
219
|
+
selection.addRange(newRange);
|
|
220
|
+
} else {
|
|
221
|
+
// selection hiện tại
|
|
222
|
+
const selection = window.getSelection();
|
|
223
|
+
if (!selection.rangeCount) return;
|
|
224
|
+
const range = selection.getRangeAt(0);
|
|
225
|
+
const isCollapsed = range.collapsed;
|
|
226
|
+
|
|
227
|
+
// đảm bảo blocks là mảng
|
|
228
|
+
const blocksArray = Array.from(blocks);
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
// tìm block chứa 1 node
|
|
232
|
+
function findBlockIndex(node, blocks) {
|
|
233
|
+
while (node && node.nodeType !== 9 /*document*/) {
|
|
234
|
+
const idx = blocks.indexOf(node);
|
|
235
|
+
if (idx !== -1) return idx;
|
|
236
|
+
node = node.parentNode;
|
|
237
|
+
}
|
|
238
|
+
return -1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// tính số ký tự từ đầu block tới vị trí (sử dụng Range.toString())
|
|
242
|
+
function charOffsetFromBlockStart(block, container, offset) {
|
|
243
|
+
const r = document.createRange();
|
|
244
|
+
r.setStart(block, 0);
|
|
245
|
+
r.setEnd(container, offset);
|
|
246
|
+
return r.toString().length;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const startBlockIndex = findBlockIndex(range.startContainer, blocksArray);
|
|
250
|
+
const endBlockIndex = findBlockIndex(range.endContainer, blocksArray);
|
|
251
|
+
|
|
252
|
+
let startCharOffset = 0, endCharOffset = 0;
|
|
253
|
+
if (startBlockIndex !== -1) {
|
|
254
|
+
startCharOffset = charOffsetFromBlockStart(blocksArray[startBlockIndex], range.startContainer, range.startOffset);
|
|
255
|
+
}
|
|
256
|
+
if (!isCollapsed && endBlockIndex !== -1) {
|
|
257
|
+
endCharOffset = charOffsetFromBlockStart(blocksArray[endBlockIndex], range.endContainer, range.endOffset);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Thực hiện convert và lấy lại node mới trả về (nếu convertBlock trả về node mới)
|
|
261
|
+
const newBlocks = blocksArray.map(b => {
|
|
262
|
+
const newNode = this.convertBlock(b, tag);
|
|
263
|
+
return newNode || b; // nếu convertBlock trả về undefined thì dùng lại b (convert in-place)
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// helper: từ charOffset tìm text node + offset bên trong nó; nếu không tìm thì trả về block để set ở cuối
|
|
267
|
+
function resolvePositionByCharOffset(block, charOffset) {
|
|
268
|
+
const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null, false);
|
|
269
|
+
let node;
|
|
270
|
+
let remaining = charOffset;
|
|
271
|
+
while ((node = walker.nextNode())) {
|
|
272
|
+
const len = node.nodeValue.length;
|
|
273
|
+
if (remaining <= len) return { node, offset: remaining };
|
|
274
|
+
remaining -= len;
|
|
275
|
+
}
|
|
276
|
+
// không tìm text node phù hợp => đặt ở cuối block
|
|
277
|
+
return { node: block, offset: block.childNodes.length };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// tái tạo range
|
|
281
|
+
const newRange = document.createRange();
|
|
282
|
+
|
|
283
|
+
if (isCollapsed) {
|
|
284
|
+
const idx = (startBlockIndex !== -1 ? startBlockIndex : 0);
|
|
285
|
+
const pos = resolvePositionByCharOffset(newBlocks[idx], startCharOffset);
|
|
286
|
+
if (pos.node.nodeType === Node.TEXT_NODE) newRange.setStart(pos.node, pos.offset);
|
|
287
|
+
else newRange.setStart(pos.node, Math.max(0, pos.offset));
|
|
288
|
+
newRange.collapse(true);
|
|
289
|
+
} else {
|
|
290
|
+
if (startBlockIndex === -1 || endBlockIndex === -1) {
|
|
291
|
+
// fallback: nếu không nằm trong blocks thì giữ range cũ (hoặc handle theo logic của bạn)
|
|
292
|
+
selection.removeAllRanges();
|
|
293
|
+
selection.addRange(range);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const s = resolvePositionByCharOffset(newBlocks[startBlockIndex], startCharOffset);
|
|
297
|
+
const e = resolvePositionByCharOffset(newBlocks[endBlockIndex], endCharOffset);
|
|
298
|
+
|
|
299
|
+
// setStart/setEnd chấp nhận text node + offset hoặc element + childIndex
|
|
300
|
+
if (s.node.nodeType === Node.TEXT_NODE) newRange.setStart(s.node, s.offset);
|
|
301
|
+
else newRange.setStart(s.node, Math.min(s.offset, s.node.childNodes.length));
|
|
302
|
+
|
|
303
|
+
if (e.node.nodeType === Node.TEXT_NODE) newRange.setEnd(e.node, e.offset);
|
|
304
|
+
else newRange.setEnd(e.node, Math.min(e.offset, e.node.childNodes.length));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
selection.removeAllRanges();
|
|
308
|
+
selection.addRange(newRange);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Create new block at cursor position
|
|
315
|
+
* @param {Range} range - Current range
|
|
316
|
+
* @param {string} tag - HTML tag name
|
|
317
|
+
*/
|
|
318
|
+
createBlockAtCursor(range, tag) {
|
|
319
|
+
const blockNode = this.constructor.create(tag);
|
|
320
|
+
|
|
321
|
+
// Try to preserve style from existing block if cursor is inside one
|
|
322
|
+
const existingBlock = this.getBlockElement(range.startContainer);
|
|
323
|
+
if (existingBlock && existingBlock.style && existingBlock.style.cssText) {
|
|
324
|
+
blockNode.style.cssText = existingBlock.style.cssText;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (range.collapsed) {
|
|
328
|
+
// No selection - create empty block
|
|
329
|
+
blockNode.appendChild(document.createTextNode(''));
|
|
330
|
+
range.insertNode(blockNode);
|
|
331
|
+
|
|
332
|
+
// Position cursor inside the block
|
|
333
|
+
const newRange = document.createRange();
|
|
334
|
+
newRange.setStart(blockNode, 0);
|
|
335
|
+
newRange.collapse(true);
|
|
336
|
+
const selection = window.getSelection();
|
|
337
|
+
selection.removeAllRanges();
|
|
338
|
+
selection.addRange(newRange);
|
|
339
|
+
} else {
|
|
340
|
+
// Has selection - wrap in block
|
|
341
|
+
const contents = range.extractContents();
|
|
342
|
+
blockNode.appendChild(contents);
|
|
343
|
+
range.insertNode(blockNode);
|
|
344
|
+
|
|
345
|
+
// Select the content in the block
|
|
346
|
+
const newRange = document.createRange();
|
|
347
|
+
newRange.selectNodeContents(blockNode);
|
|
348
|
+
const selection = window.getSelection();
|
|
349
|
+
selection.removeAllRanges();
|
|
350
|
+
selection.addRange(newRange);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Convert existing block to new format
|
|
356
|
+
* @param {Element} block - Block element to convert
|
|
357
|
+
* @param {string} tag - HTML tag name
|
|
358
|
+
* @returns {Element} - The new block element
|
|
359
|
+
*/
|
|
360
|
+
convertBlock(block, tag) {
|
|
361
|
+
const newBlock = this.constructor.create(tag);
|
|
362
|
+
|
|
363
|
+
// Copy all child nodes
|
|
364
|
+
while (block.firstChild) {
|
|
365
|
+
newBlock.appendChild(block.firstChild);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Copy relevant attributes
|
|
369
|
+
if (block.className && this.shouldPreserveClass(block.className)) {
|
|
370
|
+
newBlock.className = block.className;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Copy style attributes to preserve formatting like text-align
|
|
374
|
+
if (block.style && block.style.cssText) {
|
|
375
|
+
newBlock.style.cssText = block.style.cssText;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Replace the block
|
|
379
|
+
block.parentNode.replaceChild(newBlock, block);
|
|
380
|
+
|
|
381
|
+
return newBlock;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Set cursor at start of block (fallback method)
|
|
386
|
+
* @param {Element} block - Block element
|
|
387
|
+
*/
|
|
388
|
+
setCursorAtStartOfBlock(block) {
|
|
389
|
+
const selection = window.getSelection();
|
|
390
|
+
const range = document.createRange();
|
|
391
|
+
|
|
392
|
+
// Find first text node or position at start of block
|
|
393
|
+
const walker = document.createTreeWalker(
|
|
394
|
+
block,
|
|
395
|
+
NodeFilter.SHOW_TEXT,
|
|
396
|
+
null,
|
|
397
|
+
false
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const firstTextNode = walker.nextNode();
|
|
401
|
+
if (firstTextNode) {
|
|
402
|
+
range.setStart(firstTextNode, 0);
|
|
403
|
+
range.collapse(true);
|
|
404
|
+
} else {
|
|
405
|
+
range.setStart(block, 0);
|
|
406
|
+
range.collapse(true);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
selection.removeAllRanges();
|
|
410
|
+
selection.addRange(range);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Toggle heading format - shows/hides tag picker
|
|
415
|
+
*/
|
|
416
|
+
async toggle() {
|
|
417
|
+
if (this.customSelect.isVisible) {
|
|
418
|
+
this.customSelect.hide();
|
|
419
|
+
} else {
|
|
420
|
+
await this.showTagPicker();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Show custom select positioned relative to heading button on toolbar
|
|
426
|
+
*/
|
|
427
|
+
async showTagPicker() {
|
|
428
|
+
// Find heading button in the current editor's toolbar
|
|
429
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
430
|
+
if (!editor) return;
|
|
431
|
+
|
|
432
|
+
const toolbar = editor.getModule('toolbar');
|
|
433
|
+
let headingButton = null;
|
|
434
|
+
|
|
435
|
+
if (toolbar) {
|
|
436
|
+
headingButton = toolbar.getButton('heading');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
440
|
+
if (!headingButton) {
|
|
441
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
442
|
+
if (toolbarContainer) {
|
|
443
|
+
headingButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.heading-btn');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Final fallback: find any heading button in the current editor's wrapper
|
|
448
|
+
if (!headingButton) {
|
|
449
|
+
headingButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.heading-btn');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!headingButton) {
|
|
453
|
+
console.warn('Heading button not found for editor:', this.editorId);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Update current selection before showing
|
|
458
|
+
const currentTag = this.getCurrentTag();
|
|
459
|
+
if (currentTag) {
|
|
460
|
+
this.customSelect.setCurrentValue(currentTag);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
await this.customSelect.show(headingButton);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check if heading format is active - always return false (no active state)
|
|
468
|
+
* Only update button text to show current tag
|
|
469
|
+
* @param {string} tag - Optional specific tag to check
|
|
470
|
+
* @returns {boolean}
|
|
471
|
+
*/
|
|
472
|
+
isActive(tag = null) {
|
|
473
|
+
// Always update button text to show current tag
|
|
474
|
+
this.updateButtonText();
|
|
475
|
+
|
|
476
|
+
// Never show active state for heading button
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get current tag of the selection
|
|
482
|
+
* @returns {string|null} Current tag name or null
|
|
483
|
+
*/
|
|
484
|
+
getCurrentTag() {
|
|
485
|
+
const selection = window.getSelection();
|
|
486
|
+
if (!selection || !selection.rangeCount) return null;
|
|
487
|
+
|
|
488
|
+
const range = selection.getRangeAt(0);
|
|
489
|
+
const block = this.getBlockElement(range.startContainer);
|
|
490
|
+
|
|
491
|
+
if (!block) return null;
|
|
492
|
+
|
|
493
|
+
const headingTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P', 'PRE', 'BLOCKQUOTE'];
|
|
494
|
+
if (headingTags.includes(block.tagName)) {
|
|
495
|
+
return block.tagName;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default Heading;
|