@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,351 @@
1
+ import IconUtils from './icons.js';
2
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
3
+ import Editor from '../core/editor.js';
4
+
5
+ /**
6
+ * Custom Select Component - Reusable dropdown/popup select component
7
+ */
8
+ class CustomSelect {
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ items: [], // Array of items to display
12
+ onItemSelect: null, // Callback when item is selected
13
+ displayProperty: 'label', // Property to display as text
14
+ valueProperty: 'value', // Property to use as value
15
+ className: 'custom-select', // CSS class for the popup
16
+ title: '', // Optional header label shown at the top of the popup
17
+ width: 200, // Popup width
18
+ height: 280, // Popup height
19
+ ...options
20
+ };
21
+
22
+ this.popup = null;
23
+ this.isVisible = false;
24
+ this.currentValue = null;
25
+ this.clickOutsideHandler = null;
26
+ this.initialized = false;
27
+
28
+ this.createSelect();
29
+ }
30
+
31
+ /**
32
+ * Create select popup
33
+ */
34
+ createSelect() {
35
+ // Create popup
36
+ this.popup = document.createElement('div');
37
+ this.popup.className = `${this.options.className}-popup`;
38
+
39
+ // Optional header so it's clear what the dropdown controls.
40
+ if (this.options.title) {
41
+ const header = document.createElement('div');
42
+ header.className = 'custom-select-header';
43
+ header.textContent = this.options.title;
44
+ this.popup.appendChild(header);
45
+ }
46
+
47
+ // Add popup to container
48
+ appendPopup(this.popup);
49
+
50
+ // Initialize async
51
+ this.init();
52
+ }
53
+
54
+ /**
55
+ * Initialize component with async operations
56
+ */
57
+ async init() {
58
+ // Create item list
59
+ await this.createItemList();
60
+ this.initialized = true;
61
+ }
62
+
63
+ /**
64
+ * Create item list
65
+ */
66
+ async createItemList() {
67
+ const list = document.createElement('div');
68
+ list.className = 'item-list';
69
+
70
+ // Get check icon
71
+ const checkIconSvg = IconUtils.getIcon('check');
72
+
73
+ this.options.items.forEach(item => {
74
+ const itemButton = document.createElement('button');
75
+ itemButton.type = 'button';
76
+ itemButton.className = 'custom-select-item-button';
77
+ itemButton.dataset.value = this.getItemValue(item);
78
+
79
+ // Create item content with text and checkmark
80
+ const itemText = document.createElement('div');
81
+ itemText.className = 'item-text';
82
+ itemText.innerHTML = this.getItemDisplay(item);
83
+
84
+ const checkmark = document.createElement('span');
85
+ checkmark.className = 'item-checkmark';
86
+ checkmark.innerHTML = checkIconSvg || '';
87
+
88
+ itemButton.appendChild(itemText);
89
+ itemButton.appendChild(checkmark);
90
+
91
+ itemButton.addEventListener('click', (e) => {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ this.selectItem(item);
95
+ });
96
+
97
+ list.appendChild(itemButton);
98
+ });
99
+
100
+ this.popup.appendChild(list);
101
+ }
102
+
103
+ /**
104
+ * Get display text for item
105
+ */
106
+ getItemDisplay(item) {
107
+ return item[this.options.displayProperty] || item.toString();
108
+ }
109
+
110
+ /**
111
+ * Get value for item
112
+ */
113
+ getItemValue(item) {
114
+ return item[this.options.valueProperty] || item[this.options.displayProperty] || item;
115
+ }
116
+
117
+ /**
118
+ * Update items in the select
119
+ */
120
+ async updateItems(items) {
121
+ this.options.items = items;
122
+
123
+ // Remove existing list
124
+ const existingList = this.popup.querySelector('.item-list');
125
+ if (existingList) {
126
+ existingList.remove();
127
+ }
128
+
129
+ // Create new list
130
+ await this.createItemList();
131
+ }
132
+
133
+ /**
134
+ * Setup click outside handler
135
+ */
136
+ setupClickOutside() {
137
+ if (this.clickOutsideHandler) {
138
+ document.removeEventListener('click', this.clickOutsideHandler);
139
+ }
140
+
141
+ this.clickOutsideHandler = (e) => {
142
+ // Don't hide if clicking on block toolbar or its buttons
143
+ if (e.target.closest('.block-toolbar')) {
144
+ return;
145
+ }
146
+
147
+ if (!this.popup.contains(e.target)) {
148
+ this.hide();
149
+ }
150
+ };
151
+
152
+ // Add slight delay to avoid immediate close
153
+ setTimeout(() => {
154
+ document.addEventListener('click', this.clickOutsideHandler);
155
+ }, 100);
156
+ }
157
+
158
+ /**
159
+ * Remove click outside handler
160
+ */
161
+ removeClickOutside() {
162
+ if (this.clickOutsideHandler) {
163
+ document.removeEventListener('click', this.clickOutsideHandler);
164
+ this.clickOutsideHandler = null;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Setup scroll handler to update popup position
170
+ */
171
+ setupScrollHandler() {
172
+ if (this.scrollHandler) {
173
+ window.removeEventListener('scroll', this.scrollHandler);
174
+ }
175
+
176
+ this.scrollHandler = () => {
177
+ if (this.isVisible) {
178
+ this.updatePosition();
179
+ }
180
+ };
181
+
182
+ window.addEventListener('scroll', this.scrollHandler);
183
+ }
184
+
185
+ /**
186
+ * Remove scroll handler
187
+ */
188
+ removeScrollHandler() {
189
+ if (this.scrollHandler) {
190
+ window.removeEventListener('scroll', this.scrollHandler);
191
+ this.scrollHandler = null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Show select popup
197
+ */
198
+ async show(anchor) {
199
+ if (!anchor) return;
200
+
201
+ // Capture the editor selection NOW so the format applies to the right place
202
+ // even if a tap on the popup clears the live selection (mobile/touch).
203
+ // Prefer the LIVE selection when it's inside the editor — including a
204
+ // collapsed caret (needed for "apply then keep typing"). Only fall back to
205
+ // the last real range when the selection is genuinely gone/outside.
206
+ const sel = window.getSelection();
207
+ const ed = Editor.getCurrentInstance && Editor.getCurrentInstance();
208
+ const editorEl = ed && ed.editor;
209
+ if (sel && sel.rangeCount && editorEl && editorEl.contains(sel.anchorNode)) {
210
+ this._savedRange = sel.getRangeAt(0).cloneRange();
211
+ } else {
212
+ this._savedRange = ed && ed._lastRange ? ed._lastRange.cloneRange() : null;
213
+ }
214
+
215
+ // Wait for initialization if not ready
216
+ if (!this.initialized) {
217
+ await new Promise(resolve => {
218
+ const checkInit = () => {
219
+ if (this.initialized) {
220
+ resolve();
221
+ } else {
222
+ setTimeout(checkInit, 10);
223
+ }
224
+ };
225
+ checkInit();
226
+ });
227
+ }
228
+
229
+ // Ensure popup is in DOM
230
+ if (!document.body.contains(this.popup)) {
231
+ appendPopup(this.popup);
232
+ }
233
+
234
+ // Update current selection highlight
235
+ this.highlightCurrentItem(this.currentValue);
236
+
237
+ // Calculate and set popup position
238
+ const position = calculatePopupPosition(anchor, this.popup, {
239
+ offsetY: 5,
240
+ offsetX: 0
241
+ });
242
+ setPopupPosition(this.popup, position);
243
+
244
+ // Show popup by adding visible class
245
+ this.popup.classList.add('visible');
246
+ this.isVisible = true;
247
+
248
+ // Setup click outside handler
249
+ this.setupClickOutside();
250
+
251
+ // Setup scroll handler to update position
252
+ this.setupScrollHandler();
253
+
254
+ // Store reference to anchor for potential repositioning
255
+ this.currentAnchor = anchor;
256
+ }
257
+
258
+ /**
259
+ * Hide select popup
260
+ */
261
+ hide() {
262
+ this.popup.classList.remove('visible');
263
+ this.isVisible = false;
264
+ this.removeClickOutside();
265
+ this.currentAnchor = null;
266
+ }
267
+
268
+ /**
269
+ * Update popup position based on current anchor
270
+ */
271
+ updatePosition() {
272
+ if (this.isVisible && this.currentAnchor) {
273
+ // Calculate and set popup position
274
+ const position = calculatePopupPosition(this.currentAnchor, this.popup, {
275
+ offsetY: 5,
276
+ offsetX: 0
277
+ });
278
+ setPopupPosition(this.popup, position);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Set current value
284
+ */
285
+ setCurrentValue(value) {
286
+ this.currentValue = value;
287
+ this.highlightCurrentItem(value);
288
+ }
289
+
290
+ /**
291
+ * Highlight current item in the list
292
+ */
293
+ highlightCurrentItem(value) {
294
+ // Remove previous highlights
295
+ this.popup.querySelectorAll('.custom-select-item-button.current').forEach(btn => {
296
+ btn.classList.remove('current');
297
+ });
298
+
299
+ // Highlight current item - find by comparing dataset.value directly
300
+ if (value != null) {
301
+ const buttons = this.popup.querySelectorAll('.custom-select-item-button');
302
+ for (const button of buttons) {
303
+ if (button.dataset.value === value.toString()) {
304
+ button.classList.add('current');
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Select item and trigger callback
313
+ */
314
+ selectItem(item) {
315
+ const value = this.getItemValue(item);
316
+ this.currentValue = value;
317
+
318
+ // Restore the selection captured when the popup opened, so the format
319
+ // applies even if the tap cleared the live selection (mobile/touch).
320
+ if (this._savedRange) {
321
+ const s = window.getSelection();
322
+ s.removeAllRanges();
323
+ s.addRange(this._savedRange);
324
+ }
325
+
326
+ if (this.options.onItemSelect) {
327
+ this.options.onItemSelect(value, item);
328
+ }
329
+
330
+ this.hide();
331
+ }
332
+
333
+ /**
334
+ * Get current selected value
335
+ */
336
+ getCurrentValue() {
337
+ return this.currentValue;
338
+ }
339
+
340
+ /**
341
+ * Destroy select component
342
+ */
343
+ destroy() {
344
+ this.removeClickOutside();
345
+ if (this.popup && this.popup.parentNode) {
346
+ this.popup.parentNode.removeChild(this.popup);
347
+ }
348
+ }
349
+ }
350
+
351
+ export default CustomSelect;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Emoji Picker Component - Popup for selecting emojis
3
+ */
4
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
5
+
6
+ class EmojiPicker {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ emojis: [
10
+ // Smileys & People
11
+ '😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',
12
+ '😋', '😎', '😍', '🥰', '😘', '😗', '😙', '😚', '🙂', '🤗',
13
+ '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱',
14
+ '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻',
15
+ ],
16
+ onEmojiSelect: null,
17
+ ...options
18
+ };
19
+
20
+ this.popup = null;
21
+ this.isVisible = false;
22
+ this.clickOutsideHandler = null;
23
+
24
+ this.createEmojiPicker();
25
+ }
26
+
27
+ /**
28
+ * Detect operating system
29
+ * @returns {string} 'mac' or 'windows'
30
+ */
31
+ detectOS() {
32
+ const platform = navigator.platform.toLowerCase();
33
+ if (platform.includes('mac')) {
34
+ return 'mac';
35
+ } else if (platform.includes('win')) {
36
+ return 'windows';
37
+ }
38
+ // Default to windows for other platforms
39
+ return 'windows';
40
+ }
41
+
42
+ /**
43
+ * Get emoji shortcut message based on OS
44
+ * @returns {string} HTML string for the shortcut message
45
+ */
46
+ getEmojiShortcutMessage() {
47
+ const os = this.detectOS();
48
+
49
+ if (os === 'mac') {
50
+ return `<div style="color: rgb(113, 120, 124); font-style: normal; font-weight: 400; line-height: normal; text-align: center;">Get more emojis with <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">⌘</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">CTRL</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">SPACE</span></div>`;
51
+ } else {
52
+ return `<div style="color: rgb(113, 120, 124); font-style: normal; font-weight: 400; line-height: normal; text-align: center;">Get more emojis with <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">WIN</span> <span style="color: #000;">+</span> <span style="border-radius: 2.2px; background: #EEE; padding: 2px 4px;">.</span></div>`;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Create emoji picker popup
58
+ */
59
+ createEmojiPicker() {
60
+ // Create popup
61
+ this.popup = document.createElement('div');
62
+ this.popup.className = 'emoji-picker-popup';
63
+
64
+ // Create emoji grid
65
+ this.createEmojiGrid();
66
+ const emojiTextMessage = document.createElement('div');
67
+ emojiTextMessage.className = 'emoji-text-message';
68
+
69
+ emojiTextMessage.innerHTML = this.getEmojiShortcutMessage();
70
+ this.popup.appendChild(emojiTextMessage);
71
+
72
+ // Add popup to container
73
+ appendPopup(this.popup);
74
+ }
75
+
76
+ /**
77
+ * Create emoji grid
78
+ */
79
+ createEmojiGrid() {
80
+ const emojiGrid = document.createElement('div');
81
+ emojiGrid.className = 'emoji-grid';
82
+
83
+ // Create emoji buttons
84
+ this.options.emojis.forEach(emoji => {
85
+ const emojiButton = document.createElement('button');
86
+ emojiButton.type = 'button';
87
+ emojiButton.className = 'emoji-button';
88
+ emojiButton.textContent = emoji;
89
+ emojiButton.title = emoji;
90
+
91
+ emojiButton.addEventListener('click', (e) => {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ this.selectEmoji(emoji);
95
+ });
96
+
97
+ emojiGrid.appendChild(emojiButton);
98
+ });
99
+
100
+ this.popup.appendChild(emojiGrid);
101
+ }
102
+
103
+ /**
104
+ * Setup click outside handler
105
+ */
106
+ setupClickOutside() {
107
+ if (this.clickOutsideHandler) {
108
+ document.removeEventListener('click', this.clickOutsideHandler);
109
+ }
110
+
111
+ this.clickOutsideHandler = (e) => {
112
+ if (!this.popup.contains(e.target)) {
113
+ this.hide();
114
+ }
115
+ };
116
+
117
+ // Add slight delay to avoid immediate close
118
+ setTimeout(() => {
119
+ document.addEventListener('click', this.clickOutsideHandler);
120
+ }, 100);
121
+ }
122
+
123
+ /**
124
+ * Remove click outside handler
125
+ */
126
+ removeClickOutside() {
127
+ if (this.clickOutsideHandler) {
128
+ document.removeEventListener('click', this.clickOutsideHandler);
129
+ this.clickOutsideHandler = null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Show emoji picker popup
135
+ * @param {HTMLElement} anchor - Element to position popup relative to
136
+ */
137
+ show(anchor) {
138
+ if (!anchor) return;
139
+
140
+ // Ensure popup is in DOM
141
+ if (!document.body.contains(this.popup)) {
142
+ appendPopup(this.popup);
143
+ }
144
+
145
+ // Calculate and set popup position
146
+ const position = calculatePopupPosition(anchor, this.popup, {
147
+ offsetY: 5,
148
+ offsetX: 0
149
+ });
150
+ setPopupPosition(this.popup, position);
151
+
152
+ // Show popup by adding visible class
153
+ this.popup.classList.add('visible');
154
+ this.isVisible = true;
155
+
156
+ // Setup click outside handler
157
+ this.setupClickOutside();
158
+ }
159
+
160
+ /**
161
+ * Hide emoji picker popup
162
+ */
163
+ hide() {
164
+ this.popup.classList.remove('visible');
165
+ this.isVisible = false;
166
+ this.removeClickOutside();
167
+ }
168
+
169
+ /**
170
+ * Select emoji and trigger callback
171
+ * @param {string} emoji - Selected emoji
172
+ */
173
+ selectEmoji(emoji) {
174
+ if (this.options.onEmojiSelect) {
175
+ this.options.onEmojiSelect(emoji);
176
+ }
177
+
178
+ this.hide();
179
+ }
180
+
181
+ /**
182
+ * Destroy the emoji picker
183
+ */
184
+ destroy() {
185
+ this.removeClickOutside();
186
+
187
+ if (this.popup && this.popup.parentNode) {
188
+ this.popup.parentNode.removeChild(this.popup);
189
+ }
190
+
191
+ this.popup = null;
192
+ this.isVisible = false;
193
+ }
194
+ }
195
+
196
+ export default EmojiPicker;