@oix1987/yjd 1.0.3 → 2.0.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 (70) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +146 -142
  3. package/core.js +77 -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 +134 -103
  11. package/index.js +227 -0
  12. package/lib/core/editor.js +1806 -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 +347 -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/resize-handles.js +701 -0
  44. package/lib/modules/slash-menu.js +183 -0
  45. package/lib/modules/table-toolbar.js +635 -0
  46. package/lib/modules/toolbar.js +607 -0
  47. package/lib/styles-loader.js +142 -0
  48. package/{dist → lib}/styles.css +1285 -35
  49. package/lib/styles.css.js +2 -0
  50. package/lib/styles.min.css +1 -0
  51. package/lib/ui/color-picker.js +296 -0
  52. package/lib/ui/customselect.js +351 -0
  53. package/lib/ui/emoji-picker.js +196 -0
  54. package/lib/ui/icons.js +145 -0
  55. package/lib/ui/image-popup.js +435 -0
  56. package/lib/ui/import-popup.js +288 -0
  57. package/lib/ui/link-popup.js +139 -0
  58. package/lib/ui/list-picker.js +307 -0
  59. package/lib/ui/select-button.js +68 -0
  60. package/lib/ui/table-popup.js +171 -0
  61. package/lib/ui/tag-popup.js +249 -0
  62. package/lib/ui/text-align-picker.js +278 -0
  63. package/lib/ui/video-popup.js +413 -0
  64. package/lib/utils/exec-command.js +72 -0
  65. package/lib/utils/history-helper.js +50 -0
  66. package/lib/utils/popup-helper.js +219 -0
  67. package/lib/utils/popup-positioning.js +234 -0
  68. package/lib/utils/sanitize.js +164 -0
  69. package/package.json +51 -32
  70. package/umd-entry.js +18 -0
@@ -0,0 +1,183 @@
1
+ import Module from '../core/module.js';
2
+ import IconUtils from '../ui/icons.js';
3
+
4
+ /**
5
+ * Slash command menu.
6
+ *
7
+ * Type "/" at the start of a block (or after whitespace) to open a quick menu
8
+ * of block commands. Filter by typing, navigate with ↑/↓, choose with Enter,
9
+ * dismiss with Esc. Selecting a command removes the typed "/query" and applies
10
+ * the block transform.
11
+ */
12
+ export default class SlashMenu extends Module {
13
+ constructor(editor, options = {}) {
14
+ super(editor, options);
15
+ this.isOpen = false;
16
+ this.activeIndex = 0;
17
+ this.query = '';
18
+ this.filtered = [];
19
+ this.commands = this.buildCommands();
20
+ this.buildMenu();
21
+ this.bindEvents();
22
+ }
23
+
24
+ buildCommands() {
25
+ const ed = this.editor;
26
+ return [
27
+ { id: 'h1', label: 'Heading 1', hint: 'Big section heading', icon: 'heading', run: () => ed.setBlockType('h1') },
28
+ { id: 'h2', label: 'Heading 2', hint: 'Medium heading', icon: 'heading', run: () => ed.setBlockType('h2') },
29
+ { id: 'h3', label: 'Heading 3', hint: 'Small heading', icon: 'heading', run: () => ed.setBlockType('h3') },
30
+ { id: 'ul', label: 'Bullet list', hint: 'Unordered list', icon: 'list-bullet', run: () => ed.setBlockType('ul') },
31
+ { id: 'ol', label: 'Numbered list', hint: 'Ordered list', icon: 'list-ordered', run: () => ed.setBlockType('ol') },
32
+ { id: 'quote', label: 'Quote', hint: 'Blockquote', icon: 'code', run: () => ed.setBlockType('blockquote') },
33
+ { id: 'code', label: 'Code block', hint: 'Preformatted code', icon: 'code-view', run: () => ed.setBlockType('pre') },
34
+ { id: 'hr', label: 'Divider', hint: 'Horizontal rule', icon: 'horizontal-rule', run: () => ed.insertHorizontalRule() },
35
+ { id: 'table', label: 'Table', hint: '3×3 table', icon: 'table', run: () => this.insertTable() },
36
+ { id: 'p', label: 'Text', hint: 'Plain paragraph', icon: 'font-family', run: () => ed.setBlockType('p') }
37
+ ];
38
+ }
39
+
40
+ insertTable() {
41
+ const Table = this.editor.registry.get('formats/table');
42
+ if (Table && typeof Table.createTableElement === 'function' && typeof this.editor.insertBlock === 'function') {
43
+ this.editor.insertBlock(Table.createTableElement(3, 3));
44
+ }
45
+ }
46
+
47
+ buildMenu() {
48
+ const menu = document.createElement('div');
49
+ menu.className = 'yjd-slash-menu';
50
+ menu.setAttribute('role', 'listbox');
51
+ menu.style.display = 'none';
52
+ this.menu = menu;
53
+ document.body.appendChild(menu);
54
+ }
55
+
56
+ bindEvents() {
57
+ this._onInput = () => this.handleInput();
58
+ this.editor.editor.addEventListener('input', this._onInput);
59
+
60
+ // Keyboard interaction while open (capture so we beat other handlers).
61
+ this._onKeydown = (e) => {
62
+ if (!this.isOpen) return;
63
+ if (e.key === 'ArrowDown') { e.preventDefault(); this.move(1); }
64
+ else if (e.key === 'ArrowUp') { e.preventDefault(); this.move(-1); }
65
+ else if (e.key === 'Enter') { e.preventDefault(); this.choose(this.activeIndex); }
66
+ else if (e.key === 'Escape') { e.preventDefault(); this.close(); }
67
+ };
68
+ this.editor.editor.addEventListener('keydown', this._onKeydown, true);
69
+
70
+ this._onDocPointer = (e) => {
71
+ if (this.isOpen && !this.menu.contains(e.target)) this.close();
72
+ };
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 before = node.textContent.slice(0, range.startOffset);
84
+ const m = before.match(/(?:^|\s)\/([^\s/]*)$/);
85
+ if (!m) return this.close();
86
+
87
+ this.query = m[1];
88
+ this.slashNode = node;
89
+ this.slashStart = range.startOffset - this.query.length - 1; // index of "/"
90
+ const q = this.query.toLowerCase();
91
+ this.filtered = this.commands.filter(c =>
92
+ c.label.toLowerCase().includes(q) ||
93
+ c.id.toLowerCase().includes(q) ||
94
+ (c.hint || '').toLowerCase().includes(q));
95
+ if (!this.filtered.length) return this.close();
96
+ this.activeIndex = 0;
97
+ this.render();
98
+ this.open(range);
99
+ }
100
+
101
+ open(range) {
102
+ this.isOpen = true;
103
+ this.menu.style.display = 'block';
104
+ // Position below the caret.
105
+ const rect = range.getBoundingClientRect();
106
+ const x = rect.left || (range.startContainer.parentElement || this.editor.editor).getBoundingClientRect().left;
107
+ const y = rect.bottom || rect.top;
108
+ this.menu.style.left = `${Math.round(x + window.scrollX)}px`;
109
+ this.menu.style.top = `${Math.round(y + window.scrollY + 6)}px`;
110
+ // Flip up if off the bottom.
111
+ const mh = this.menu.offsetHeight;
112
+ if (rect.bottom + mh + 8 > window.innerHeight) {
113
+ this.menu.style.top = `${Math.round(rect.top + window.scrollY - mh - 6)}px`;
114
+ }
115
+ }
116
+
117
+ close() {
118
+ if (!this.isOpen) return;
119
+ this.isOpen = false;
120
+ this.menu.style.display = 'none';
121
+ }
122
+
123
+ move(delta) {
124
+ this.activeIndex = (this.activeIndex + delta + this.filtered.length) % this.filtered.length;
125
+ this.render();
126
+ }
127
+
128
+ render() {
129
+ this.menu.innerHTML = '';
130
+ this.filtered.forEach((cmd, i) => {
131
+ const item = document.createElement('button');
132
+ item.type = 'button';
133
+ item.className = 'yjd-slash-item' + (i === this.activeIndex ? ' active' : '');
134
+ item.setAttribute('role', 'option');
135
+ item.setAttribute('aria-selected', i === this.activeIndex ? 'true' : 'false');
136
+
137
+ const icon = document.createElement('span');
138
+ icon.className = 'yjd-slash-icon';
139
+ icon.innerHTML = IconUtils.getIcon(cmd.icon) || '';
140
+
141
+ const text = document.createElement('span');
142
+ text.className = 'yjd-slash-text';
143
+ text.innerHTML = `<span class="yjd-slash-label">${cmd.label}</span><span class="yjd-slash-hint">${cmd.hint}</span>`;
144
+
145
+ item.append(icon, text);
146
+ // pointerdown (not click) so the editor selection isn't lost first.
147
+ item.addEventListener('pointerdown', (e) => { e.preventDefault(); this.choose(i); });
148
+ this.menu.appendChild(item);
149
+ });
150
+ }
151
+
152
+ choose(index) {
153
+ const cmd = this.filtered[index];
154
+ if (!cmd) return this.close();
155
+
156
+ // Remove the typed "/query" then run the command.
157
+ try {
158
+ const node = this.slashNode;
159
+ const sel = window.getSelection();
160
+ const del = document.createRange();
161
+ del.setStart(node, this.slashStart);
162
+ del.setEnd(node, this.slashStart + this.query.length + 1);
163
+ del.deleteContents();
164
+ const caret = document.createRange();
165
+ caret.setStart(node, this.slashStart);
166
+ caret.collapse(true);
167
+ sel.removeAllRanges();
168
+ sel.addRange(caret);
169
+ } catch (e) { /* node moved; run anyway */ }
170
+
171
+ this.close();
172
+ this.editor.focus();
173
+ cmd.run(this.editor);
174
+ }
175
+
176
+ destroy() {
177
+ this.editor.editor.removeEventListener('input', this._onInput);
178
+ this.editor.editor.removeEventListener('keydown', this._onKeydown, true);
179
+ document.removeEventListener('pointerdown', this._onDocPointer, true);
180
+ if (this.menu && this.menu.parentNode) this.menu.parentNode.removeChild(this.menu);
181
+ super.destroy();
182
+ }
183
+ }