@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,249 @@
1
+ /**
2
+ * Tag Popup Component - Popup for inserting custom tags
3
+ */
4
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
5
+
6
+ class TagPopup {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ onTagInsert: null,
10
+ editor: null,
11
+ ...options
12
+ };
13
+
14
+ this.popup = null;
15
+ this.isVisible = false;
16
+ this.clickOutsideHandler = null;
17
+ this.selectedTagType = 'mention';
18
+
19
+ this.createTagPopup();
20
+ }
21
+
22
+ createTagPopup() {
23
+ this.popup = document.createElement('div');
24
+ this.popup.className = 'tag-popup';
25
+
26
+ const content = document.createElement('div');
27
+ content.className = 'tag-popup-content';
28
+
29
+ // Title
30
+ const title = document.createElement('h3');
31
+ title.textContent = 'Insert tags';
32
+ title.className = 'yjd-input-title';
33
+ content.appendChild(title);
34
+
35
+ // Tag type selector
36
+ const group1 = document.createElement('div');
37
+ group1.className = 'yjd-input-group';
38
+
39
+ const typeLabel = document.createElement('label');
40
+ typeLabel.textContent = 'Type';
41
+ typeLabel.className = 'yjd-input-label';
42
+
43
+ this.typeSelect = document.createElement('select');
44
+ this.typeSelect.className = 'yjd-select-input';
45
+ this.typeSelect.innerHTML = `
46
+ <option value="mention">Mention</option>
47
+ <option value="hashtag">Hashtag</option>
48
+ <option value="custom">Custom</option>
49
+ `;
50
+ this.typeSelect.addEventListener('change', () => this.updateSuggestions());
51
+
52
+ group1.appendChild(typeLabel);
53
+ group1.appendChild(this.typeSelect);
54
+ content.appendChild(group1);
55
+
56
+ // Content input
57
+ const group2 = document.createElement('div');
58
+ group2.className = 'yjd-input-group';
59
+
60
+ const contentLabel = document.createElement('label');
61
+ contentLabel.textContent = 'Content';
62
+ contentLabel.className = 'yjd-input-label';
63
+
64
+ this.contentInput = document.createElement('input');
65
+ this.contentInput.type = 'text';
66
+ this.contentInput.className = 'yjd-input';
67
+ this.contentInput.placeholder = 'Please enter tag content';
68
+ this.contentInput.addEventListener('input', () => this.updateInsertButton());
69
+ this.contentInput.addEventListener('keydown', (e) => {
70
+ if (e.key === 'Enter') {
71
+ e.preventDefault();
72
+ this.insertTag();
73
+ }
74
+ });
75
+
76
+ group2.appendChild(contentLabel);
77
+ group2.appendChild(this.contentInput);
78
+ content.appendChild(group2);
79
+
80
+ // Suggestions
81
+ const group3 = document.createElement('div');
82
+ group3.className = 'yjd-input-group';
83
+ this.suggestionsContainer = document.createElement('div');
84
+ this.suggestionsContainer.className = 'tag-suggestions-container';
85
+
86
+ const suggestionsLabel = document.createElement('label');
87
+ suggestionsLabel.textContent = 'Suggestions';
88
+ suggestionsLabel.className = 'yjd-input-label';
89
+
90
+ this.suggestionsList = document.createElement('div');
91
+ this.suggestionsList.className = 'yjd-suggestions-list';
92
+
93
+ this.suggestionsContainer.appendChild(this.suggestionsList);
94
+ group3.appendChild(suggestionsLabel);
95
+ group3.appendChild(this.suggestionsContainer);
96
+ content.appendChild(group3);
97
+
98
+ // Buttons
99
+ const buttonContainer = document.createElement('div');
100
+ buttonContainer.className = 'yjd-button-container';
101
+
102
+ const cancelButton = document.createElement('button');
103
+ cancelButton.type = 'button';
104
+ cancelButton.className = 'yjd-button-cancel';
105
+ cancelButton.textContent = 'Cancel';
106
+ cancelButton.addEventListener('click', () => this.hide());
107
+
108
+ this.insertButton = document.createElement('button');
109
+ this.insertButton.type = 'button';
110
+ this.insertButton.className = 'yjd-button-confirm';
111
+ this.insertButton.textContent = 'Insert Tag';
112
+ this.insertButton.disabled = true;
113
+ this.insertButton.addEventListener('click', () => this.insertTag());
114
+
115
+ buttonContainer.appendChild(cancelButton);
116
+ buttonContainer.appendChild(this.insertButton);
117
+ content.appendChild(buttonContainer);
118
+
119
+ this.popup.appendChild(content);
120
+ appendPopup(this.popup);
121
+
122
+ // Prevent focus loss when clicking on popup
123
+ if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
124
+ this.options.editor.preventFocusLoss(this.popup);
125
+ }
126
+
127
+ this.updateSuggestions();
128
+ }
129
+
130
+ updateSuggestions() {
131
+ this.selectedTagType = this.typeSelect.value;
132
+ this.suggestionsList.innerHTML = '';
133
+
134
+ const suggestions = this.getSuggestions(this.selectedTagType);
135
+
136
+ suggestions.forEach(suggestion => {
137
+ const suggestionButton = document.createElement('button');
138
+ suggestionButton.type = 'button';
139
+ suggestionButton.className = 'yjd-suggestion-button';
140
+ suggestionButton.textContent = suggestion;
141
+
142
+ suggestionButton.addEventListener('click', () => {
143
+ this.contentInput.value = suggestion;
144
+ this.updateInsertButton();
145
+ this.contentInput.focus();
146
+ });
147
+
148
+ this.suggestionsList.appendChild(suggestionButton);
149
+ });
150
+ }
151
+
152
+ getSuggestions(tagType) {
153
+ const suggestions = {
154
+ mention: ['john', 'admin', 'team', 'support'],
155
+ hashtag: ['urgent', 'done', 'important'],
156
+ custom: ['warning', 'info', 'success']
157
+ };
158
+
159
+ return suggestions[tagType] || [];
160
+ }
161
+
162
+ updateInsertButton() {
163
+ const hasContent = this.contentInput.value.trim();
164
+ this.insertButton.disabled = !hasContent;
165
+ }
166
+
167
+ insertTag() {
168
+ const content = this.contentInput.value.trim();
169
+
170
+ if (!content) return;
171
+
172
+ if (this.options.onTagInsert) {
173
+ this.options.onTagInsert(this.selectedTagType, content);
174
+ }
175
+
176
+ this.hide();
177
+ this.reset();
178
+ }
179
+
180
+ reset() {
181
+ this.contentInput.value = '';
182
+ this.typeSelect.value = 'mention';
183
+ this.selectedTagType = 'mention';
184
+ this.updateInsertButton();
185
+ this.updateSuggestions();
186
+ }
187
+
188
+ setupClickOutside() {
189
+ if (this.clickOutsideHandler) {
190
+ document.removeEventListener('click', this.clickOutsideHandler);
191
+ }
192
+
193
+ this.clickOutsideHandler = (e) => {
194
+ if (!this.popup.contains(e.target)) {
195
+ this.hide();
196
+ }
197
+ };
198
+
199
+ setTimeout(() => {
200
+ document.addEventListener('click', this.clickOutsideHandler);
201
+ }, 100);
202
+ }
203
+
204
+ removeClickOutside() {
205
+ if (this.clickOutsideHandler) {
206
+ document.removeEventListener('click', this.clickOutsideHandler);
207
+ this.clickOutsideHandler = null;
208
+ }
209
+ }
210
+
211
+ show(anchor) {
212
+ if (!anchor) return;
213
+
214
+ // Calculate and set popup position
215
+ const position = calculatePopupPosition(anchor, this.popup, {
216
+ offsetY: 5,
217
+ offsetX: 0
218
+ });
219
+ setPopupPosition(this.popup, position);
220
+
221
+ this.popup.classList.add('visible');
222
+ this.isVisible = true;
223
+
224
+ this.setupClickOutside();
225
+
226
+ setTimeout(() => {
227
+ this.contentInput.focus();
228
+ }, 100);
229
+ }
230
+
231
+ hide() {
232
+ this.popup.classList.remove('visible');
233
+ this.isVisible = false;
234
+ this.removeClickOutside();
235
+ }
236
+
237
+ destroy() {
238
+ this.removeClickOutside();
239
+
240
+ if (this.popup && this.popup.parentNode) {
241
+ this.popup.parentNode.removeChild(this.popup);
242
+ }
243
+
244
+ this.popup = null;
245
+ this.isVisible = false;
246
+ }
247
+ }
248
+
249
+ export default TagPopup;
@@ -0,0 +1,278 @@
1
+ import IconUtils from './icons.js';
2
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
3
+
4
+ /**
5
+ * Text Align Picker Component - Popup for selecting text alignment
6
+ */
7
+ class TextAlignPicker {
8
+ constructor(options = {}) {
9
+ this.options = {
10
+ alignments: [
11
+ { value: 'left', label: 'Align Left', icon: 'align-left' },
12
+ { value: 'center', label: 'Align Center', icon: 'align-center' },
13
+ { value: 'right', label: 'Align Right', icon: 'align-right' },
14
+ { value: 'justify', label: 'Justify', icon: 'align-justify' }
15
+ ],
16
+ onAlignSelect: null,
17
+ ...options
18
+ };
19
+
20
+ this.popup = null;
21
+ this.isVisible = false;
22
+ this.currentAlignment = 'left';
23
+ this.clickOutsideHandler = null;
24
+
25
+ this.createAlignPicker();
26
+ }
27
+
28
+ /**
29
+ * Create text align picker popup
30
+ */
31
+ createAlignPicker() {
32
+ // Create popup
33
+ this.popup = document.createElement('div');
34
+ this.popup.className = 'text-align-picker-popup';
35
+
36
+ // Create alignment buttons
37
+ this.createAlignmentButtons();
38
+
39
+ // Add popup to container
40
+ appendPopup(this.popup);
41
+ }
42
+
43
+ /**
44
+ * Create alignment buttons
45
+ */
46
+ async createAlignmentButtons() {
47
+ const buttonContainer = document.createElement('div');
48
+ buttonContainer.className = 'align-button-container';
49
+
50
+ // Icons are now inline, no need to preload
51
+
52
+ // Create buttons
53
+ for (const alignment of this.options.alignments) {
54
+ const alignButton = document.createElement('button');
55
+ alignButton.type = 'button';
56
+ alignButton.className = 'align-button';
57
+ alignButton.dataset.alignment = alignment.value;
58
+ alignButton.title = alignment.label;
59
+
60
+ // Icon only — the label lives in the tooltip (title) above.
61
+ const iconSvg = IconUtils.getIcon(alignment.icon);
62
+ if (iconSvg) {
63
+ alignButton.innerHTML = `<span class="icon-wrapper">${iconSvg}</span>`;
64
+ } else {
65
+ alignButton.textContent = alignment.label.charAt(0);
66
+ }
67
+
68
+ alignButton.addEventListener('click', (e) => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ this.selectAlignment(alignment.value);
72
+ });
73
+
74
+ buttonContainer.appendChild(alignButton);
75
+ }
76
+
77
+ this.popup.appendChild(buttonContainer);
78
+ }
79
+
80
+ /**
81
+ * Setup click outside handler
82
+ */
83
+ setupClickOutside() {
84
+ if (this.clickOutsideHandler) {
85
+ document.removeEventListener('click', this.clickOutsideHandler);
86
+ }
87
+
88
+ this.clickOutsideHandler = (e) => {
89
+ if (!this.popup.contains(e.target)) {
90
+ this.hide();
91
+ }
92
+ };
93
+
94
+ // Add slight delay to avoid immediate close
95
+ setTimeout(() => {
96
+ document.addEventListener('click', this.clickOutsideHandler);
97
+ }, 100);
98
+ }
99
+
100
+ /**
101
+ * Remove click outside handler
102
+ */
103
+ removeClickOutside() {
104
+ if (this.clickOutsideHandler) {
105
+ document.removeEventListener('click', this.clickOutsideHandler);
106
+ this.clickOutsideHandler = null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Show text align picker popup
112
+ * @param {HTMLElement} anchor - Element to position popup relative to
113
+ */
114
+ show(anchor) {
115
+ if (!anchor) return;
116
+
117
+ // Ensure popup is in DOM
118
+ if (!document.body.contains(this.popup)) {
119
+ appendPopup(this.popup);
120
+ }
121
+
122
+ // Update current alignment state
123
+ this.updateCurrentAlignment();
124
+
125
+ // Calculate and set popup position
126
+ const position = calculatePopupPosition(anchor, this.popup, {
127
+ offsetY: 5,
128
+ offsetX: 0
129
+ });
130
+ setPopupPosition(this.popup, position);
131
+
132
+ // Show popup by adding visible class
133
+ this.popup.classList.add('visible');
134
+ this.isVisible = true;
135
+
136
+ // Setup click outside handler
137
+ this.setupClickOutside();
138
+ }
139
+
140
+ /**
141
+ * Hide text align picker popup
142
+ */
143
+ hide() {
144
+ this.popup.classList.remove('visible');
145
+ this.isVisible = false;
146
+ this.removeClickOutside();
147
+ }
148
+
149
+ /**
150
+ * Select alignment and trigger callback
151
+ * @param {string} alignment - Selected alignment
152
+ */
153
+ selectAlignment(alignment) {
154
+ this.currentAlignment = alignment;
155
+
156
+ if (this.options.onAlignSelect) {
157
+ this.options.onAlignSelect(alignment);
158
+ }
159
+
160
+ this.hide();
161
+ }
162
+
163
+ /**
164
+ * Update current alignment state based on selection
165
+ */
166
+ updateCurrentAlignment() {
167
+ const selection = window.getSelection();
168
+ if (!selection || !selection.rangeCount) return;
169
+
170
+ try {
171
+ const range = selection.getRangeAt(0);
172
+
173
+ // Lấy tất cả block trong vùng chọn
174
+ const blocks = this.getSelectedBlockElements(range);
175
+
176
+ // Nếu có block → lấy block đầu tiên, nếu không thì fallback về block bao quanh
177
+ const firstBlock = blocks.length > 0
178
+ ? blocks[0]
179
+ : this.getBlockElement(range.commonAncestorContainer);
180
+
181
+ if (firstBlock) {
182
+ const textAlign = window.getComputedStyle(firstBlock).textAlign;
183
+ this.currentAlignment =
184
+ textAlign === 'left' || textAlign === 'start' || !textAlign
185
+ ? 'left'
186
+ : textAlign;
187
+ } else {
188
+ this.currentAlignment = 'left';
189
+ }
190
+
191
+ // Cập nhật trạng thái nút trong popup
192
+ const buttons = this.popup.querySelectorAll('.align-button');
193
+ buttons.forEach(button => {
194
+ if (button.dataset.alignment === this.currentAlignment) {
195
+ button.classList.add('active');
196
+ } else {
197
+ button.classList.remove('active');
198
+ }
199
+ });
200
+
201
+ // Cập nhật icon trên toolbar
202
+ this.updateToolbarButtonIcon(this.currentAlignment);
203
+ } catch (error) {
204
+ console.error('Error updating current alignment:', error);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Lấy tất cả block element trong vùng chọn
210
+ */
211
+ getSelectedBlockElements(range) {
212
+ const blocks = [];
213
+ const startBlock = this.getBlockElement(range.startContainer);
214
+ const endBlock = this.getBlockElement(range.endContainer);
215
+
216
+ if (startBlock) blocks.push(startBlock);
217
+
218
+ if (startBlock && endBlock && startBlock !== endBlock) {
219
+ let current = startBlock;
220
+ while (current && current !== endBlock) {
221
+ current = current.nextElementSibling;
222
+ if (current && this.getBlockElement(current) && !blocks.includes(current)) {
223
+ blocks.push(current);
224
+ }
225
+ }
226
+ if (endBlock && !blocks.includes(endBlock)) {
227
+ blocks.push(endBlock);
228
+ }
229
+ }
230
+
231
+ return blocks;
232
+ }
233
+
234
+ /**
235
+ * Update toolbar button icon based on alignment
236
+ * @param {string} alignment - Current alignment
237
+ */
238
+ updateToolbarButtonIcon(alignment) {
239
+ // Import TextAlign class to use its static method
240
+ import('../formats/text-align.js').then(module => {
241
+ const TextAlign = module.default;
242
+ TextAlign.updateToolbarButtonIcon(alignment);
243
+ }).catch(error => {
244
+ console.warn('Could not import TextAlign class:', error);
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Get the block element containing the given node
250
+ */
251
+ getBlockElement(node) {
252
+ if (!node) return null;
253
+
254
+ let currentNode = node;
255
+ while (currentNode && currentNode !== document.body) {
256
+ if (currentNode.nodeType === Node.ELEMENT_NODE) {
257
+ const tagName = currentNode.tagName;
258
+ if (['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI'].includes(tagName)) {
259
+ return currentNode;
260
+ }
261
+ }
262
+ currentNode = currentNode.parentNode;
263
+ }
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * Destroy text align picker
269
+ */
270
+ destroy() {
271
+ this.removeClickOutside();
272
+ if (this.popup && this.popup.parentNode) {
273
+ this.popup.parentNode.removeChild(this.popup);
274
+ }
275
+ }
276
+ }
277
+
278
+ export default TextAlignPicker;