@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,183 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import ColorPicker from '../ui/color-picker.js';
|
|
3
|
+
import { saveBeforeFormat } from '../utils/history-helper.js';
|
|
4
|
+
import Editor from '../core/editor.js';
|
|
5
|
+
import { execFormat, setStyleWithCSS } from '../utils/exec-command.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Color Format - Handles text color formatting
|
|
9
|
+
*/
|
|
10
|
+
class Color extends InlineFormat {
|
|
11
|
+
static formatName = 'color';
|
|
12
|
+
static tagName = 'SPAN';
|
|
13
|
+
static attribute = 'color';
|
|
14
|
+
|
|
15
|
+
// Selection saved when the picker opens, restored before applying — so the
|
|
16
|
+
// colour still lands on the right text after a tap clears the live selection
|
|
17
|
+
// (mobile/touch) or focus moves to the picker.
|
|
18
|
+
static savedRanges = new Map();
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
|
|
23
|
+
// Get current editor instance
|
|
24
|
+
const currentEditor = Editor.getCurrentInstance();
|
|
25
|
+
if (!currentEditor) {
|
|
26
|
+
console.warn('No editor instance found for Color format');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.editorId = currentEditor.instanceId;
|
|
31
|
+
|
|
32
|
+
// Check if this editor already has a color picker instance
|
|
33
|
+
let colorPicker = currentEditor.getPopupInstance('color');
|
|
34
|
+
|
|
35
|
+
if (!colorPicker) {
|
|
36
|
+
// Create new color picker instance for this editor
|
|
37
|
+
const editorId = this.editorId;
|
|
38
|
+
colorPicker = new ColorPicker({
|
|
39
|
+
onColorSelect: (color) => {
|
|
40
|
+
Color.applyColorToCurrentSelection(color, editorId);
|
|
41
|
+
},
|
|
42
|
+
editor: Editor.getCurrentInstance()
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Store popup instance in editor
|
|
46
|
+
currentEditor.setPopupInstance('color', colorPicker);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.colorPicker = colorPicker;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Static method to apply color to current selection
|
|
54
|
+
*/
|
|
55
|
+
static applyColorToCurrentSelection(color, editorId = null) {
|
|
56
|
+
const selection = window.getSelection();
|
|
57
|
+
// Restore the selection captured when the picker opened (a tap on the
|
|
58
|
+
// picker may have collapsed the live selection, especially on mobile).
|
|
59
|
+
const saved = editorId != null ? Color.savedRanges.get(editorId) : null;
|
|
60
|
+
if (saved) {
|
|
61
|
+
selection.removeAllRanges();
|
|
62
|
+
selection.addRange(saved);
|
|
63
|
+
}
|
|
64
|
+
if (editorId != null) Color.savedRanges.delete(editorId);
|
|
65
|
+
|
|
66
|
+
if (!selection || !selection.rangeCount || selection.isCollapsed) return;
|
|
67
|
+
|
|
68
|
+
// Save state before applying format
|
|
69
|
+
saveBeforeFormat();
|
|
70
|
+
|
|
71
|
+
setStyleWithCSS(true);
|
|
72
|
+
// 'transparent' is the picker's "reset to default" entry — clear the colour
|
|
73
|
+
// back to the editor default rather than painting an explicit colour.
|
|
74
|
+
execFormat('foreColor', color === 'transparent' ? 'inherit' : color);
|
|
75
|
+
|
|
76
|
+
// Refresh toolbar state (active highlight + swatch) and notify listeners.
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
const currentEditor = Editor.getCurrentInstance();
|
|
79
|
+
if (currentEditor) {
|
|
80
|
+
if (typeof currentEditor.updateToolbarButtonStates === 'function') {
|
|
81
|
+
currentEditor.updateToolbarButtonStates();
|
|
82
|
+
}
|
|
83
|
+
if (typeof currentEditor.onContentChange === 'function') {
|
|
84
|
+
currentEditor.onContentChange();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Toggle color formatting - shows/hides color picker
|
|
92
|
+
*/
|
|
93
|
+
toggle() {
|
|
94
|
+
if (this.colorPicker.isVisible) {
|
|
95
|
+
this.colorPicker.hide();
|
|
96
|
+
} else {
|
|
97
|
+
this.showColorPicker();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Show color picker positioned relative to color button on toolbar
|
|
103
|
+
*/
|
|
104
|
+
showColorPicker() {
|
|
105
|
+
// Find color button in the current editor's toolbar
|
|
106
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
107
|
+
if (!editor) return;
|
|
108
|
+
|
|
109
|
+
// Capture the current selection so we can apply the colour to it even if a
|
|
110
|
+
// tap on the picker clears the live selection. Fall back to the editor's
|
|
111
|
+
// last non-collapsed range (mobile clears the selection on touchstart).
|
|
112
|
+
const sel = window.getSelection();
|
|
113
|
+
if (sel && sel.rangeCount && !sel.isCollapsed) {
|
|
114
|
+
Color.savedRanges.set(this.editorId, sel.getRangeAt(0).cloneRange());
|
|
115
|
+
} else if (editor._lastRange) {
|
|
116
|
+
Color.savedRanges.set(this.editorId, editor._lastRange.cloneRange());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const toolbar = editor.getModule('toolbar');
|
|
120
|
+
let colorButton = null;
|
|
121
|
+
|
|
122
|
+
if (toolbar) {
|
|
123
|
+
colorButton = toolbar.getButton('color');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
127
|
+
if (!colorButton) {
|
|
128
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
129
|
+
if (toolbarContainer) {
|
|
130
|
+
colorButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.color-btn');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Final fallback: find any color button in the current editor's wrapper
|
|
135
|
+
if (!colorButton) {
|
|
136
|
+
colorButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.color-btn');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!colorButton) {
|
|
140
|
+
console.warn('Color button not found for editor:', this.editorId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.colorPicker.show(colorButton);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if color formatting is active in current selection
|
|
149
|
+
*/
|
|
150
|
+
isActive() {
|
|
151
|
+
return !!Color.getCurrentColor();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Return the explicit text colour applied at the current selection, or null
|
|
156
|
+
* when the text uses the editor's default colour. We look for an EXPLICIT
|
|
157
|
+
* inline colour (the `<span style="color:…">` / `<font color>` that the
|
|
158
|
+
* editor inserts) rather than comparing the computed colour to a hardcoded
|
|
159
|
+
* default — the default depends on the active theme, so a hardcoded compare
|
|
160
|
+
* made the button look permanently "active".
|
|
161
|
+
*/
|
|
162
|
+
static getCurrentColor() {
|
|
163
|
+
const selection = window.getSelection();
|
|
164
|
+
if (!selection || !selection.rangeCount) return null;
|
|
165
|
+
|
|
166
|
+
let node = selection.getRangeAt(0).startContainer;
|
|
167
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
|
|
168
|
+
|
|
169
|
+
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
170
|
+
if (node.classList && node.classList.contains('rich-editor-area')) break;
|
|
171
|
+
if (node.style && node.style.color && node.style.color !== 'inherit') {
|
|
172
|
+
return node.style.color;
|
|
173
|
+
}
|
|
174
|
+
if (node.tagName === 'FONT' && node.getAttribute('color')) {
|
|
175
|
+
return node.getAttribute('color');
|
|
176
|
+
}
|
|
177
|
+
node = node.parentNode;
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default Color;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import EmojiPicker from '../ui/emoji-picker.js';
|
|
3
|
+
import Editor from '../core/editor.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Emoji Format - Handles emoji insertion
|
|
7
|
+
* Now supports multiple editor instances with separate popup instances
|
|
8
|
+
*/
|
|
9
|
+
class Emoji extends InlineFormat {
|
|
10
|
+
static formatName = 'emoji';
|
|
11
|
+
static tagName = 'SPAN';
|
|
12
|
+
static className = 'emoji';
|
|
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 Emoji format');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.editorId = currentEditor.instanceId;
|
|
25
|
+
|
|
26
|
+
// Check if this editor already has an emoji picker instance
|
|
27
|
+
let emojiPicker = currentEditor.getPopupInstance('emoji');
|
|
28
|
+
|
|
29
|
+
if (!emojiPicker) {
|
|
30
|
+
// Create new emoji picker instance for this editor
|
|
31
|
+
emojiPicker = new EmojiPicker({
|
|
32
|
+
onEmojiSelect: (emoji) => {
|
|
33
|
+
Emoji.insertEmojiAtCurrentPosition(emoji, this.editorId);
|
|
34
|
+
},
|
|
35
|
+
editor: currentEditor,
|
|
36
|
+
editorId: this.editorId
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Store popup instance in editor
|
|
40
|
+
currentEditor.setPopupInstance('emoji', emojiPicker);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.emojiPicker = emojiPicker;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a new Emoji format instance for a specific editor
|
|
48
|
+
* @param {string} editorId - Editor instance ID
|
|
49
|
+
* @returns {Emoji} Emoji 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 Emoji();
|
|
64
|
+
|
|
65
|
+
// Restore original current instance
|
|
66
|
+
Editor.currentInstance = originalCurrent;
|
|
67
|
+
|
|
68
|
+
return format;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create emoji element
|
|
73
|
+
* @param {string} value - Emoji character
|
|
74
|
+
* @returns {HTMLElement}
|
|
75
|
+
*/
|
|
76
|
+
static create(value) {
|
|
77
|
+
const span = document.createElement('SPAN');
|
|
78
|
+
span.className = 'emoji';
|
|
79
|
+
span.textContent = value;
|
|
80
|
+
span.setAttribute('data-emoji', value);
|
|
81
|
+
return span;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Insert emoji at current cursor position
|
|
86
|
+
* @param {string} emoji - Emoji character to insert
|
|
87
|
+
* @param {string} editorId - Editor instance ID
|
|
88
|
+
*/
|
|
89
|
+
static insertEmojiAtCurrentPosition(emoji, editorId = null) {
|
|
90
|
+
// Get the correct editor instance
|
|
91
|
+
let editor = null;
|
|
92
|
+
if (editorId) {
|
|
93
|
+
editor = Editor.getInstanceById(editorId);
|
|
94
|
+
} else {
|
|
95
|
+
editor = Editor.getCurrentInstance();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!editor) {
|
|
99
|
+
console.warn('No editor instance found for emoji insertion');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const selection = window.getSelection();
|
|
104
|
+
if (!selection || !selection.rangeCount) return;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const range = selection.getRangeAt(0);
|
|
108
|
+
|
|
109
|
+
// Check if cursor is inside an existing emoji span
|
|
110
|
+
let currentNode = range.startContainer;
|
|
111
|
+
let emojiParent = null;
|
|
112
|
+
|
|
113
|
+
// If cursor is in a text node, check its parent
|
|
114
|
+
if (currentNode.nodeType === Node.TEXT_NODE) {
|
|
115
|
+
currentNode = currentNode.parentNode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find if we're inside an emoji span
|
|
119
|
+
while (currentNode && currentNode !== editor.element) {
|
|
120
|
+
if (currentNode.classList && currentNode.classList.contains('emoji')) {
|
|
121
|
+
emojiParent = currentNode;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
currentNode = currentNode.parentNode;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If cursor is inside an emoji span, move it outside
|
|
128
|
+
if (emojiParent) {
|
|
129
|
+
// Move cursor after the emoji span
|
|
130
|
+
range.setStartAfter(emojiParent);
|
|
131
|
+
range.collapse(true);
|
|
132
|
+
selection.removeAllRanges();
|
|
133
|
+
selection.addRange(range);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create emoji element
|
|
137
|
+
const emojiElement = Emoji.create(emoji);
|
|
138
|
+
|
|
139
|
+
// Insert emoji at cursor position
|
|
140
|
+
range.deleteContents();
|
|
141
|
+
range.insertNode(emojiElement);
|
|
142
|
+
|
|
143
|
+
// Create a zero-width space character after the emoji
|
|
144
|
+
const zeroWidthSpace = document.createTextNode('\u200B'); // Zero-width space
|
|
145
|
+
|
|
146
|
+
// Insert zero-width space after emoji
|
|
147
|
+
range.setStartAfter(emojiElement);
|
|
148
|
+
range.insertNode(zeroWidthSpace);
|
|
149
|
+
|
|
150
|
+
// Position cursor after the zero-width space
|
|
151
|
+
range.setStartAfter(zeroWidthSpace);
|
|
152
|
+
range.collapse(true);
|
|
153
|
+
selection.removeAllRanges();
|
|
154
|
+
selection.addRange(range);
|
|
155
|
+
|
|
156
|
+
// Trigger content change event
|
|
157
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
158
|
+
editor.onContentChange();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error inserting emoji:', error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Apply emoji formatting - shows emoji picker
|
|
168
|
+
*/
|
|
169
|
+
apply(value) {
|
|
170
|
+
if (value) {
|
|
171
|
+
Emoji.insertEmojiAtCurrentPosition(value, this.editorId);
|
|
172
|
+
} else {
|
|
173
|
+
this.showEmojiPicker();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove emoji formatting
|
|
179
|
+
*/
|
|
180
|
+
remove() {
|
|
181
|
+
const selection = window.getSelection();
|
|
182
|
+
if (!selection || !selection.rangeCount) return;
|
|
183
|
+
|
|
184
|
+
const range = selection.getRangeAt(0);
|
|
185
|
+
const emojiElement = this.getEmojiElement(range);
|
|
186
|
+
|
|
187
|
+
if (emojiElement) {
|
|
188
|
+
// Replace emoji element with its text content
|
|
189
|
+
const textNode = document.createTextNode(emojiElement.textContent);
|
|
190
|
+
emojiElement.parentNode.replaceChild(textNode, emojiElement);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Toggle emoji formatting - shows emoji picker
|
|
196
|
+
*/
|
|
197
|
+
toggle() {
|
|
198
|
+
if (this.emojiPicker.isVisible) {
|
|
199
|
+
this.emojiPicker.hide();
|
|
200
|
+
} else {
|
|
201
|
+
this.showEmojiPicker();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Show emoji picker popup
|
|
207
|
+
*/
|
|
208
|
+
showEmojiPicker() {
|
|
209
|
+
// Find emoji button in the current editor's toolbar
|
|
210
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
211
|
+
if (!editor) return;
|
|
212
|
+
|
|
213
|
+
const toolbar = editor.getModule('toolbar');
|
|
214
|
+
let emojiButton = null;
|
|
215
|
+
|
|
216
|
+
if (toolbar) {
|
|
217
|
+
emojiButton = toolbar.getButton('emoji');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fallback: find button by class in the current editor's toolbar
|
|
221
|
+
if (!emojiButton) {
|
|
222
|
+
const toolbarContainer = toolbar?.getContainer();
|
|
223
|
+
if (toolbarContainer) {
|
|
224
|
+
emojiButton = toolbarContainer.querySelector('.rich-editor-toolbar-btn.emoji-btn');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Final fallback: find any emoji button in the current editor's wrapper
|
|
229
|
+
if (!emojiButton) {
|
|
230
|
+
emojiButton = editor.wrapper.querySelector('.rich-editor-toolbar-btn.emoji-btn');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!emojiButton) {
|
|
234
|
+
console.warn('Emoji button not found for editor:', this.editorId);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.emojiPicker.show(emojiButton);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if emoji formatting is active
|
|
243
|
+
*/
|
|
244
|
+
isActive() {
|
|
245
|
+
const selection = window.getSelection();
|
|
246
|
+
if (!selection || !selection.rangeCount) return false;
|
|
247
|
+
|
|
248
|
+
const range = selection.getRangeAt(0);
|
|
249
|
+
const emojiElement = this.getEmojiElement(range);
|
|
250
|
+
|
|
251
|
+
return emojiElement !== null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get emoji element from selection
|
|
256
|
+
* @param {Range} range - Selection range
|
|
257
|
+
* @returns {HTMLElement|null}
|
|
258
|
+
*/
|
|
259
|
+
getEmojiElement(range) {
|
|
260
|
+
let node = range.commonAncestorContainer;
|
|
261
|
+
|
|
262
|
+
// If it's a text node, get its parent
|
|
263
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
264
|
+
node = node.parentNode;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if current node is an emoji
|
|
268
|
+
if (node.classList && node.classList.contains('emoji')) {
|
|
269
|
+
return node;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check if selection contains an emoji
|
|
273
|
+
const emojiInSelection = range.cloneContents().querySelector('.emoji');
|
|
274
|
+
if (emojiInSelection) {
|
|
275
|
+
return emojiInSelection;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export default Emoji;
|