@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,200 @@
|
|
|
1
|
+
import Module from '../core/module.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @mention module — trigger-based autocomplete that inserts a token carrying an
|
|
5
|
+
* id, so the serialized HTML/Markdown can tell the server who was tagged.
|
|
6
|
+
*
|
|
7
|
+
* new Editor(el, {
|
|
8
|
+
* mention: {
|
|
9
|
+
* trigger: '@',
|
|
10
|
+
* source: async (query) => [{ id, name, avatar_url }],
|
|
11
|
+
* renderItem: (item) => `<img src="${item.avatar_url}"> ${item.name}`,
|
|
12
|
+
* // optional extra triggers, e.g. '#' for task refs:
|
|
13
|
+
* triggers: [{ char: '#', source: async (q) => [...] }]
|
|
14
|
+
* }
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* Token HTML: <span class="mention" data-id="ID" data-trigger="@"
|
|
18
|
+
* contenteditable="false">@Name</span>
|
|
19
|
+
* → getMarkdown() emits `@[Name](id)`. Fires editor.on('mention:select', item).
|
|
20
|
+
*/
|
|
21
|
+
export default class Mention extends Module {
|
|
22
|
+
constructor(editor, options = {}) {
|
|
23
|
+
super(editor, options);
|
|
24
|
+
this.isOpen = false;
|
|
25
|
+
this.activeIndex = 0;
|
|
26
|
+
this.items = [];
|
|
27
|
+
this.sources = this._buildSources();
|
|
28
|
+
this.buildMenu();
|
|
29
|
+
this.bindEvents();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_buildSources() {
|
|
33
|
+
const cfg = this.editor.options.mention || this.options || {};
|
|
34
|
+
const map = {};
|
|
35
|
+
const renderItem = cfg.renderItem;
|
|
36
|
+
if (typeof cfg.source === 'function') {
|
|
37
|
+
map[cfg.trigger || '@'] = { source: cfg.source, renderItem: cfg.renderItem || renderItem };
|
|
38
|
+
}
|
|
39
|
+
(cfg.triggers || []).forEach((t) => {
|
|
40
|
+
if (t && t.char && typeof t.source === 'function') {
|
|
41
|
+
map[t.char] = { source: t.source, renderItem: t.renderItem || renderItem };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return map;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get enabled() { return Object.keys(this.sources).length > 0; }
|
|
48
|
+
|
|
49
|
+
buildMenu() {
|
|
50
|
+
const menu = document.createElement('div');
|
|
51
|
+
menu.className = 'yjd-mention-menu';
|
|
52
|
+
menu.setAttribute('role', 'listbox');
|
|
53
|
+
menu.style.display = 'none';
|
|
54
|
+
this.menu = menu;
|
|
55
|
+
document.body.appendChild(menu);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
bindEvents() {
|
|
59
|
+
if (!this.enabled) return;
|
|
60
|
+
this._onInput = () => this.handleInput();
|
|
61
|
+
this.editor.editor.addEventListener('input', this._onInput);
|
|
62
|
+
|
|
63
|
+
this._onKeydown = (e) => {
|
|
64
|
+
if (!this.isOpen) return;
|
|
65
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); this.move(1); }
|
|
66
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); this.move(-1); }
|
|
67
|
+
else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); this.choose(this.activeIndex); }
|
|
68
|
+
else if (e.key === 'Escape') { e.preventDefault(); this.close(); }
|
|
69
|
+
};
|
|
70
|
+
this.editor.editor.addEventListener('keydown', this._onKeydown, true);
|
|
71
|
+
|
|
72
|
+
this._onDocPointer = (e) => { if (this.isOpen && !this.menu.contains(e.target)) this.close(); };
|
|
73
|
+
document.addEventListener('pointerdown', this._onDocPointer, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
handleInput() {
|
|
77
|
+
const sel = window.getSelection();
|
|
78
|
+
if (!sel || !sel.isCollapsed || !sel.rangeCount) return this.close();
|
|
79
|
+
const range = sel.getRangeAt(0);
|
|
80
|
+
const node = range.startContainer;
|
|
81
|
+
if (node.nodeType !== Node.TEXT_NODE) return this.close();
|
|
82
|
+
|
|
83
|
+
const triggers = Object.keys(this.sources).map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('');
|
|
84
|
+
const before = node.textContent.slice(0, range.startOffset);
|
|
85
|
+
const m = before.match(new RegExp(`(?:^|\\s)([${triggers}])([^\\s${triggers}]*)$`));
|
|
86
|
+
if (!m) return this.close();
|
|
87
|
+
|
|
88
|
+
this.char = m[1];
|
|
89
|
+
this.query = m[2];
|
|
90
|
+
this.node = node;
|
|
91
|
+
this.start = range.startOffset - this.query.length - 1; // index of trigger char
|
|
92
|
+
this._loadFor(range);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_loadFor(range) {
|
|
96
|
+
const src = this.sources[this.char];
|
|
97
|
+
if (!src) return this.close();
|
|
98
|
+
clearTimeout(this._t);
|
|
99
|
+
const q = this.query, char = this.char;
|
|
100
|
+
this._t = setTimeout(() => {
|
|
101
|
+
Promise.resolve(src.source(q)).then((items) => {
|
|
102
|
+
// Ignore stale responses (user kept typing / switched trigger).
|
|
103
|
+
if (this.char !== char || this.query !== q) return;
|
|
104
|
+
this.items = Array.isArray(items) ? items : [];
|
|
105
|
+
if (!this.items.length) return this.close();
|
|
106
|
+
this.activeIndex = 0;
|
|
107
|
+
this.render(src.renderItem);
|
|
108
|
+
this.open(range);
|
|
109
|
+
}).catch(() => this.close());
|
|
110
|
+
}, 120);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
open(range) {
|
|
114
|
+
this.isOpen = true;
|
|
115
|
+
this.menu.style.display = 'block';
|
|
116
|
+
const rect = range.getBoundingClientRect();
|
|
117
|
+
const x = rect.left || (range.startContainer.parentElement || this.editor.editor).getBoundingClientRect().left;
|
|
118
|
+
const y = rect.bottom || rect.top;
|
|
119
|
+
this.menu.style.left = `${Math.round(x + window.scrollX)}px`;
|
|
120
|
+
this.menu.style.top = `${Math.round(y + window.scrollY + 6)}px`;
|
|
121
|
+
const mh = this.menu.offsetHeight;
|
|
122
|
+
if (rect.bottom + mh + 8 > window.innerHeight) {
|
|
123
|
+
this.menu.style.top = `${Math.round(rect.top + window.scrollY - mh - 6)}px`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
close() {
|
|
128
|
+
if (!this.isOpen) return;
|
|
129
|
+
this.isOpen = false;
|
|
130
|
+
this.menu.style.display = 'none';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
move(d) {
|
|
134
|
+
this.activeIndex = (this.activeIndex + d + this.items.length) % this.items.length;
|
|
135
|
+
[...this.menu.children].forEach((el, i) => {
|
|
136
|
+
el.classList.toggle('active', i === this.activeIndex);
|
|
137
|
+
el.setAttribute('aria-selected', i === this.activeIndex ? 'true' : 'false');
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render(renderItem) {
|
|
142
|
+
this.menu.innerHTML = '';
|
|
143
|
+
this.items.forEach((item, i) => {
|
|
144
|
+
const el = document.createElement('button');
|
|
145
|
+
el.type = 'button';
|
|
146
|
+
el.className = 'yjd-mention-item' + (i === this.activeIndex ? ' active' : '');
|
|
147
|
+
el.setAttribute('role', 'option');
|
|
148
|
+
el.setAttribute('aria-selected', i === this.activeIndex ? 'true' : 'false');
|
|
149
|
+
const label = item.name || item.label || item.id || '';
|
|
150
|
+
el.innerHTML = typeof renderItem === 'function'
|
|
151
|
+
? renderItem(item)
|
|
152
|
+
: `${item.avatar_url ? `<img class="yjd-mention-avatar" src="${item.avatar_url}" alt="">` : ''}<span class="yjd-mention-name">${this.char}${label}</span>`;
|
|
153
|
+
el.addEventListener('pointerdown', (e) => { e.preventDefault(); this.choose(i); });
|
|
154
|
+
this.menu.appendChild(el);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
choose(index) {
|
|
159
|
+
const item = this.items[index];
|
|
160
|
+
if (!item) return this.close();
|
|
161
|
+
const name = item.name || item.label || item.id || '';
|
|
162
|
+
try {
|
|
163
|
+
const node = this.node;
|
|
164
|
+
const sel = window.getSelection();
|
|
165
|
+
const del = document.createRange();
|
|
166
|
+
del.setStart(node, this.start);
|
|
167
|
+
del.setEnd(node, this.start + this.query.length + 1);
|
|
168
|
+
del.deleteContents();
|
|
169
|
+
|
|
170
|
+
const span = document.createElement('span');
|
|
171
|
+
span.className = 'mention';
|
|
172
|
+
span.setAttribute('data-id', String(item.id != null ? item.id : ''));
|
|
173
|
+
span.setAttribute('data-trigger', this.char);
|
|
174
|
+
span.setAttribute('contenteditable', 'false');
|
|
175
|
+
span.textContent = this.char + name;
|
|
176
|
+
del.insertNode(span);
|
|
177
|
+
|
|
178
|
+
const space = document.createTextNode(' ');
|
|
179
|
+
span.after(space);
|
|
180
|
+
const caret = document.createRange();
|
|
181
|
+
caret.setStart(space, 1);
|
|
182
|
+
caret.collapse(true);
|
|
183
|
+
sel.removeAllRanges();
|
|
184
|
+
sel.addRange(caret);
|
|
185
|
+
} catch (e) { /* node moved */ }
|
|
186
|
+
|
|
187
|
+
this.close();
|
|
188
|
+
this.editor.focus();
|
|
189
|
+
if (typeof this.editor.onContentChange === 'function') this.editor.onContentChange();
|
|
190
|
+
this.editor.emit('mention:select', item);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
destroy() {
|
|
194
|
+
if (this._onInput) this.editor.editor.removeEventListener('input', this._onInput);
|
|
195
|
+
if (this._onKeydown) this.editor.editor.removeEventListener('keydown', this._onKeydown, true);
|
|
196
|
+
if (this._onDocPointer) document.removeEventListener('pointerdown', this._onDocPointer, true);
|
|
197
|
+
if (this.menu && this.menu.parentNode) this.menu.parentNode.removeChild(this.menu);
|
|
198
|
+
super.destroy();
|
|
199
|
+
}
|
|
200
|
+
}
|