@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,251 @@
|
|
|
1
|
+
import { InlineFormat } from '../core/format.js';
|
|
2
|
+
import LinkPopup from '../ui/link-popup.js';
|
|
3
|
+
import Editor from '../core/editor.js';
|
|
4
|
+
import { isSafeUrl } from '../utils/sanitize.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Link Format - Simple link insertion
|
|
8
|
+
* Now supports multiple editor instances with separate popup instances
|
|
9
|
+
*/
|
|
10
|
+
class Link extends InlineFormat {
|
|
11
|
+
static formatName = 'link';
|
|
12
|
+
static tagName = 'A';
|
|
13
|
+
|
|
14
|
+
// Map to store saved ranges for each editor instance
|
|
15
|
+
static savedRanges = new Map();
|
|
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 Link format');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.editorId = currentEditor.instanceId;
|
|
28
|
+
|
|
29
|
+
// Check if this editor already has a link popup instance
|
|
30
|
+
let linkPopup = currentEditor.getPopupInstance('link');
|
|
31
|
+
|
|
32
|
+
if (!linkPopup) {
|
|
33
|
+
// Create new popup instance for this editor
|
|
34
|
+
linkPopup = new LinkPopup({
|
|
35
|
+
onLinkSelect: (linkData) => {
|
|
36
|
+
Link.insertLink(linkData, this.editorId);
|
|
37
|
+
},
|
|
38
|
+
editor: currentEditor,
|
|
39
|
+
editorId: this.editorId
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Store popup instance in editor
|
|
43
|
+
currentEditor.setPopupInstance('link', linkPopup);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.linkPopup = linkPopup;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a new Link format instance for a specific editor
|
|
51
|
+
* @param {string} editorId - Editor instance ID
|
|
52
|
+
* @returns {Link} Link 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 Link();
|
|
67
|
+
|
|
68
|
+
// Restore original current instance
|
|
69
|
+
Editor.currentInstance = originalCurrent;
|
|
70
|
+
|
|
71
|
+
return format;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Insert link at saved cursor position
|
|
76
|
+
* @param {Object} linkData - Link data with url and text
|
|
77
|
+
* @param {string} editorId - Editor instance ID
|
|
78
|
+
*/
|
|
79
|
+
static insertLink(linkData, editorId = null) {
|
|
80
|
+
// Get the correct editor instance
|
|
81
|
+
let editor = null;
|
|
82
|
+
if (editorId) {
|
|
83
|
+
editor = Editor.getInstanceById(editorId);
|
|
84
|
+
} else {
|
|
85
|
+
editor = Editor.getCurrentInstance();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!editor) {
|
|
89
|
+
console.warn('No editor instance found for link insertion');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Block unsafe URL schemes (javascript:, data:text/html, vbscript:, ...)
|
|
94
|
+
if (!isSafeUrl(linkData.url)) {
|
|
95
|
+
console.warn('Blocked unsafe link URL:', linkData.url);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get saved range for this specific editor
|
|
100
|
+
const savedRange = Link.savedRanges.get(editorId);
|
|
101
|
+
if (!savedRange) {
|
|
102
|
+
console.warn('No saved range found for editor:', editorId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const selection = window.getSelection();
|
|
107
|
+
selection.removeAllRanges();
|
|
108
|
+
selection.addRange(savedRange);
|
|
109
|
+
|
|
110
|
+
const range = selection.getRangeAt(0);
|
|
111
|
+
|
|
112
|
+
if (range.collapsed) {
|
|
113
|
+
// No selection - insert link at cursor
|
|
114
|
+
const linkElement = document.createElement('A');
|
|
115
|
+
linkElement.href = linkData.url;
|
|
116
|
+
linkElement.target = '_blank';
|
|
117
|
+
linkElement.rel = 'noopener noreferrer';
|
|
118
|
+
linkElement.textContent = linkData.text || linkData.url;
|
|
119
|
+
range.insertNode(linkElement);
|
|
120
|
+
} else {
|
|
121
|
+
// Has selection - wrap existing content with link while preserving styles
|
|
122
|
+
const fragment = range.extractContents();
|
|
123
|
+
const linkElement = document.createElement('A');
|
|
124
|
+
linkElement.href = linkData.url;
|
|
125
|
+
linkElement.target = '_blank';
|
|
126
|
+
linkElement.rel = 'noopener noreferrer';
|
|
127
|
+
|
|
128
|
+
// Move all nodes from fragment to link element
|
|
129
|
+
while (fragment.firstChild) {
|
|
130
|
+
linkElement.appendChild(fragment.firstChild);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
range.insertNode(linkElement);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Position cursor after link
|
|
137
|
+
const newRange = document.createRange();
|
|
138
|
+
newRange.setStartAfter(range.endContainer);
|
|
139
|
+
newRange.collapse(true);
|
|
140
|
+
selection.removeAllRanges();
|
|
141
|
+
selection.addRange(newRange);
|
|
142
|
+
|
|
143
|
+
// Trigger content change after applying format
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
146
|
+
editor.onContentChange();
|
|
147
|
+
}
|
|
148
|
+
}, 0);
|
|
149
|
+
|
|
150
|
+
// Clear saved range for this editor
|
|
151
|
+
Link.savedRanges.delete(editorId);
|
|
152
|
+
|
|
153
|
+
// Trigger content change event
|
|
154
|
+
if (editor && typeof editor.onContentChange === 'function') {
|
|
155
|
+
editor.onContentChange();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Toggle link popup
|
|
161
|
+
*/
|
|
162
|
+
toggle() {
|
|
163
|
+
if (this.linkPopup.isVisible) {
|
|
164
|
+
this.linkPopup.hide();
|
|
165
|
+
} else {
|
|
166
|
+
this.showPopup();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Show link popup
|
|
172
|
+
*/
|
|
173
|
+
showPopup() {
|
|
174
|
+
const editor = Editor.getInstanceById(this.editorId);
|
|
175
|
+
if (!editor) {
|
|
176
|
+
console.warn('No editor found for ID:', this.editorId);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Save the current range and anchor the popup right at the selected text.
|
|
181
|
+
const selection = window.getSelection();
|
|
182
|
+
let rect = null;
|
|
183
|
+
if (selection && selection.rangeCount > 0) {
|
|
184
|
+
const range = selection.getRangeAt(0).cloneRange();
|
|
185
|
+
Link.savedRanges.set(this.editorId, range);
|
|
186
|
+
rect = range.getBoundingClientRect();
|
|
187
|
+
// A collapsed caret can report an empty rect — fall back to its element.
|
|
188
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) {
|
|
189
|
+
const node = range.startContainer.nodeType === Node.TEXT_NODE
|
|
190
|
+
? range.startContainer.parentElement
|
|
191
|
+
: range.startContainer;
|
|
192
|
+
if (node && node.getBoundingClientRect) rect = node.getBoundingClientRect();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const existingLink = this.getCurrentLink();
|
|
197
|
+
let selectedText = '';
|
|
198
|
+
if (selection && !selection.isCollapsed) {
|
|
199
|
+
selectedText = selection.toString().trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Anchor at the selection rect (virtual anchor); fall back to the toolbar
|
|
203
|
+
// button when there's no usable rect.
|
|
204
|
+
let anchor = null;
|
|
205
|
+
if (rect && (rect.width || rect.height)) {
|
|
206
|
+
anchor = { getBoundingClientRect: () => rect };
|
|
207
|
+
} else {
|
|
208
|
+
const toolbar = editor.getModule('toolbar');
|
|
209
|
+
anchor = toolbar && (toolbar.getButton('link')
|
|
210
|
+
|| editor.wrapper.querySelector('.rich-editor-toolbar-btn[data-command="link"]'));
|
|
211
|
+
}
|
|
212
|
+
if (!anchor) {
|
|
213
|
+
console.warn('No anchor for link popup, editor:', this.editorId);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.linkPopup.show(anchor, existingLink, selectedText);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get current link if cursor is in one
|
|
222
|
+
*/
|
|
223
|
+
getCurrentLink() {
|
|
224
|
+
const selection = window.getSelection();
|
|
225
|
+
if (!selection || !selection.rangeCount) return null;
|
|
226
|
+
|
|
227
|
+
let node = selection.getRangeAt(0).startContainer;
|
|
228
|
+
|
|
229
|
+
// Find parent link element
|
|
230
|
+
while (node && node !== document.body) {
|
|
231
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'A') {
|
|
232
|
+
return {
|
|
233
|
+
url: node.href || '',
|
|
234
|
+
text: node.textContent || ''
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
node = node.parentNode;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Check if cursor is in a link
|
|
245
|
+
*/
|
|
246
|
+
isActive() {
|
|
247
|
+
return this.getCurrentLink() !== null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export default Link;
|