@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.
Files changed (73) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +223 -142
  3. package/core.js +82 -0
  4. package/dist/core.esm.js +2 -0
  5. package/dist/core.esm.js.map +1 -0
  6. package/dist/rich-editor.esm.js +1 -1
  7. package/dist/rich-editor.esm.js.map +1 -1
  8. package/dist/rich-editor.min.js +1 -1
  9. package/dist/rich-editor.min.js.map +1 -1
  10. package/index.d.ts +230 -103
  11. package/index.js +297 -0
  12. package/lib/core/editor.js +1885 -0
  13. package/lib/core/format.js +540 -0
  14. package/lib/core/module.js +81 -0
  15. package/lib/core/registry.js +158 -0
  16. package/lib/formats/background.js +213 -0
  17. package/lib/formats/bold.js +49 -0
  18. package/lib/formats/capitalization.js +579 -0
  19. package/lib/formats/color.js +183 -0
  20. package/lib/formats/emoji.js +282 -0
  21. package/lib/formats/font-family.js +548 -0
  22. package/lib/formats/heading.js +502 -0
  23. package/lib/formats/image.js +341 -0
  24. package/lib/formats/import.js +385 -0
  25. package/lib/formats/indent.js +297 -0
  26. package/lib/formats/italic.js +27 -0
  27. package/lib/formats/line-height.js +562 -0
  28. package/lib/formats/link.js +251 -0
  29. package/lib/formats/list.js +635 -0
  30. package/lib/formats/strike.js +31 -0
  31. package/lib/formats/subscript.js +40 -0
  32. package/lib/formats/superscript.js +39 -0
  33. package/lib/formats/table.js +293 -0
  34. package/lib/formats/tag.js +304 -0
  35. package/lib/formats/text-align.js +422 -0
  36. package/lib/formats/text-size.js +498 -0
  37. package/lib/formats/underline.js +30 -0
  38. package/lib/formats/video.js +381 -0
  39. package/lib/modules/block-toolbar.js +639 -0
  40. package/lib/modules/code-view.js +447 -0
  41. package/lib/modules/find-replace.js +273 -0
  42. package/lib/modules/history.js +425 -0
  43. package/lib/modules/mention.js +200 -0
  44. package/lib/modules/resize-handles.js +701 -0
  45. package/lib/modules/slash-menu.js +183 -0
  46. package/lib/modules/table-toolbar.js +635 -0
  47. package/lib/modules/toolbar.js +607 -0
  48. package/lib/serialize.js +241 -0
  49. package/lib/static.js +28 -0
  50. package/lib/styles-loader.js +142 -0
  51. package/{dist → lib}/styles.css +1392 -35
  52. package/lib/styles.css.js +2 -0
  53. package/lib/styles.min.css +1 -0
  54. package/lib/ui/color-picker.js +296 -0
  55. package/lib/ui/customselect.js +351 -0
  56. package/lib/ui/emoji-picker.js +196 -0
  57. package/lib/ui/icons.js +145 -0
  58. package/lib/ui/image-popup.js +435 -0
  59. package/lib/ui/import-popup.js +288 -0
  60. package/lib/ui/link-popup.js +139 -0
  61. package/lib/ui/list-picker.js +307 -0
  62. package/lib/ui/select-button.js +68 -0
  63. package/lib/ui/table-popup.js +171 -0
  64. package/lib/ui/tag-popup.js +249 -0
  65. package/lib/ui/text-align-picker.js +278 -0
  66. package/lib/ui/video-popup.js +413 -0
  67. package/lib/utils/exec-command.js +72 -0
  68. package/lib/utils/history-helper.js +50 -0
  69. package/lib/utils/popup-helper.js +219 -0
  70. package/lib/utils/popup-positioning.js +234 -0
  71. package/lib/utils/sanitize.js +164 -0
  72. package/package.json +51 -32
  73. 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
+ }