@oix1987/yjd 1.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 (55) hide show
  1. package/README.md +91 -0
  2. package/index.d.ts +103 -0
  3. package/index.js +221 -0
  4. package/lib/core/editor.js +1175 -0
  5. package/lib/core/format.js +542 -0
  6. package/lib/core/module.js +81 -0
  7. package/lib/core/registry.js +152 -0
  8. package/lib/formats/background.js +212 -0
  9. package/lib/formats/bold.js +67 -0
  10. package/lib/formats/capitalization.js +563 -0
  11. package/lib/formats/color.js +165 -0
  12. package/lib/formats/emoji.js +282 -0
  13. package/lib/formats/font-family.js +547 -0
  14. package/lib/formats/heading.js +502 -0
  15. package/lib/formats/image.js +344 -0
  16. package/lib/formats/import.js +385 -0
  17. package/lib/formats/indent.js +297 -0
  18. package/lib/formats/italic.js +27 -0
  19. package/lib/formats/line-height.js +558 -0
  20. package/lib/formats/link.js +251 -0
  21. package/lib/formats/list.js +635 -0
  22. package/lib/formats/strike.js +31 -0
  23. package/lib/formats/subscript.js +36 -0
  24. package/lib/formats/superscript.js +35 -0
  25. package/lib/formats/table.js +288 -0
  26. package/lib/formats/tag.js +304 -0
  27. package/lib/formats/text-align.js +421 -0
  28. package/lib/formats/text-size.js +497 -0
  29. package/lib/formats/underline.js +30 -0
  30. package/lib/formats/video.js +372 -0
  31. package/lib/modules/block-toolbar.js +628 -0
  32. package/lib/modules/code-view.js +434 -0
  33. package/lib/modules/history.js +410 -0
  34. package/lib/modules/resize-handles.js +677 -0
  35. package/lib/modules/table-toolbar.js +618 -0
  36. package/lib/modules/toolbar.js +424 -0
  37. package/lib/styles-loader.js +144 -0
  38. package/lib/styles.css +2123 -0
  39. package/lib/ui/color-picker.js +296 -0
  40. package/lib/ui/customselect.js +319 -0
  41. package/lib/ui/emoji-picker.js +196 -0
  42. package/lib/ui/icons.js +413 -0
  43. package/lib/ui/image-popup.js +444 -0
  44. package/lib/ui/import-popup.js +288 -0
  45. package/lib/ui/link-popup.js +191 -0
  46. package/lib/ui/list-picker.js +307 -0
  47. package/lib/ui/select-button.js +61 -0
  48. package/lib/ui/table-popup.js +171 -0
  49. package/lib/ui/tag-popup.js +249 -0
  50. package/lib/ui/text-align-picker.js +281 -0
  51. package/lib/ui/video-popup.js +422 -0
  52. package/lib/utils/history-helper.js +50 -0
  53. package/lib/utils/popup-helper.js +219 -0
  54. package/lib/utils/popup-positioning.js +231 -0
  55. package/package.json +26 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Image Popup Component - Popup for inserting images
3
+ */
4
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
5
+
6
+ class ImagePopup {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ onImageInsert: null,
10
+ editor: null,
11
+ ...options
12
+ };
13
+
14
+ this.popup = null;
15
+ this.isVisible = false;
16
+ this.clickOutsideHandler = null;
17
+ this.selectedImageSrc = null;
18
+ this.savedSelection = null; // Save editor selection
19
+ this.resizeHandler = null;
20
+
21
+ this.createImagePopup();
22
+ }
23
+
24
+ /**
25
+ * Create image popup
26
+ */
27
+ createImagePopup() {
28
+ this.popup = document.createElement('div');
29
+ this.popup.className = 'image-popup';
30
+
31
+ const content = document.createElement('div');
32
+ content.className = 'image-popup-content';
33
+
34
+ // Title
35
+ const title = document.createElement('h3');
36
+ title.textContent = 'Upload image';
37
+ title.className = 'yjd-input-title';
38
+ content.appendChild(title);
39
+
40
+ // Container
41
+ const uploadContainer = document.createElement('div');
42
+ uploadContainer.className = 'image-input-container';
43
+
44
+ const textLabel = document.createElement('p');
45
+ textLabel.textContent = 'Your image url';
46
+ textLabel.className = 'yjd-input-label';
47
+
48
+ const inputgroup1 = document.createElement('div');
49
+ inputgroup1.className = 'yjd-input-upload-group';
50
+ this.inputGroup = inputgroup1; // Store reference
51
+
52
+
53
+ // input url
54
+ this.urlInput = document.createElement('input');
55
+ this.urlInput.type = 'url';
56
+ this.urlInput.className = 'yjd-input';
57
+ this.urlInput.placeholder = 'Please enter your image URL';
58
+
59
+
60
+ // Hidden file input
61
+ this.fileInput = document.createElement('input');
62
+ this.fileInput.type = 'file';
63
+ this.fileInput.accept = 'image/*';
64
+ this.fileInput.className = 'image-input-hidden'; // ẩn bằng CSS
65
+ this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
66
+
67
+ // Custom button
68
+ const customButton = document.createElement('button');
69
+ customButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 17 17" fill="none">
70
+ <g clip-path="url(#clip0_243_650)">
71
+ <path d="M9.45721 4.06101V11.4287C9.45721 11.966 9.02311 12.4001 8.48578 12.4001C7.94846 12.4001 7.51436 11.966 7.51436 11.4287V4.06101L5.28614 6.28923C4.90668 6.66869 4.29043 6.66869 3.91096 6.28923C3.5315 5.90976 3.5315 5.29351 3.91096 4.91405L7.79668 1.02833C8.17614 0.64887 8.79239 0.64887 9.17186 1.02833L13.0576 4.91405C13.437 5.29351 13.437 5.90976 13.0576 6.28923C12.6781 6.66869 12.0619 6.66869 11.6824 6.28923L9.45721 4.06101ZM2.65721 11.4287H6.54293C6.54293 12.5003 7.41418 13.3715 8.48578 13.3715C9.55739 13.3715 10.4286 12.5003 10.4286 11.4287H14.3144C15.386 11.4287 16.2572 12.2999 16.2572 13.3715V14.343C16.2572 15.4146 15.386 16.2858 14.3144 16.2858H2.65721C1.58561 16.2858 0.714355 15.4146 0.714355 14.343V13.3715C0.714355 12.2999 1.58561 11.4287 2.65721 11.4287ZM13.8286 14.5858C14.0219 14.5858 14.2072 14.5091 14.3438 14.3724C14.4805 14.2358 14.5572 14.0505 14.5572 13.8573C14.5572 13.664 14.4805 13.4787 14.3438 13.3421C14.2072 13.2055 14.0219 13.1287 13.8286 13.1287C13.6354 13.1287 13.4501 13.2055 13.3135 13.3421C13.1768 13.4787 13.1001 13.664 13.1001 13.8573C13.1001 14.0505 13.1768 14.2358 13.3135 14.3724C13.4501 14.5091 13.6354 14.5858 13.8286 14.5858Z" fill="#252424"/>
72
+ </g>
73
+ <defs>
74
+ <clipPath id="clip0_243_650">
75
+ <rect width="15.5429" height="15.5429" fill="white" transform="translate(0.714355 0.742859)"/>
76
+ </clipPath>
77
+ </defs>
78
+ </svg>`;
79
+ customButton.className = 'yjd-custom-upload-button';
80
+ this.customButton = customButton;
81
+ customButton.addEventListener('click', () => this.fileInput.click());
82
+
83
+ // Create preview container
84
+ this.createPreviewContainer();
85
+
86
+ // Append elements
87
+ inputgroup1.appendChild(this.urlInput);
88
+ inputgroup1.appendChild(this.fileInput);
89
+ inputgroup1.appendChild(customButton);
90
+ uploadContainer.appendChild(textLabel);
91
+ uploadContainer.appendChild(inputgroup1);
92
+ uploadContainer.appendChild(this.previewContainer);
93
+ content.appendChild(uploadContainer);
94
+ this.urlInput.addEventListener('input', () => {
95
+ this.updateInsertButton();
96
+ // Show preview if URL is valid
97
+ const url = this.urlInput.value.trim();
98
+ if (url && this.isValidImageUrl(url)) {
99
+ this.showPreview(url);
100
+ } else {
101
+ this.removePreview();
102
+ }
103
+ if(this.urlInput.value.trim()){
104
+ this.customButton.style.display = 'none';
105
+ }else{
106
+ this.customButton.style.display = 'flex';
107
+ }
108
+ });
109
+ // Buttons
110
+ const buttonContainer = document.createElement('div');
111
+ buttonContainer.className = 'yjd-button-container';
112
+
113
+ const cancelButton = document.createElement('button');
114
+ cancelButton.type = 'button';
115
+ cancelButton.className = 'image-button yjd-button-cancel';
116
+ cancelButton.textContent = 'Cancel';
117
+ cancelButton.addEventListener('click', () => {
118
+ this.hide();
119
+ // Maintain editor focus after popup close
120
+ if (this.options.editor) {
121
+ setTimeout(() => this.options.editor.focus(), 0);
122
+ }
123
+ });
124
+
125
+ this.insertButton = document.createElement('button');
126
+ this.insertButton.type = 'button';
127
+ this.insertButton.className = 'image-button yjd-button-confirm button-disable';
128
+ this.insertButton.textContent = 'Add image';
129
+ this.insertButton.disabled = true;
130
+ this.insertButton.addEventListener('click', () => {
131
+ this.insertImage();
132
+ // Maintain editor focus after insert
133
+ if (this.options.editor) {
134
+ setTimeout(() => this.options.editor.focus(), 0);
135
+ }
136
+ });
137
+
138
+ buttonContainer.appendChild(cancelButton);
139
+ buttonContainer.appendChild(this.insertButton);
140
+ content.appendChild(buttonContainer);
141
+
142
+ this.popup.appendChild(content);
143
+ appendPopup(this.popup);
144
+
145
+ // Prevent focus loss when clicking on popup
146
+ if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
147
+ this.options.editor.preventFocusLoss(this.popup);
148
+ }
149
+ }
150
+
151
+ async handleFileSelect(e) {
152
+ const file = e.target.files[0];
153
+ if (!file) return;
154
+
155
+ try {
156
+ const { default: Image } = await import('../formats/image.js');
157
+ this.selectedImageSrc = await Image.handleFileUpload(file);
158
+ this.urlInput.value = '';
159
+ this.showPreview(this.selectedImageSrc);
160
+ this.updateInsertButton();
161
+ } catch (error) {
162
+ alert(error.message);
163
+ }
164
+ }
165
+
166
+ updateInsertButton() {
167
+ const hasImage = this.selectedImageSrc || this.urlInput.value.trim();
168
+ this.insertButton.disabled = !hasImage;
169
+ this.insertButton.classList.toggle('button-disable', !hasImage);
170
+ }
171
+
172
+ /**
173
+ * Show image preview
174
+ */
175
+ showPreview(imageSrc) {
176
+ if (!imageSrc) return;
177
+
178
+ this.imagePreview.src = imageSrc;
179
+ this.previewContainer.style.display = 'block';
180
+ this.selectedImageSrc = imageSrc;
181
+
182
+ // Hide input group
183
+ this.toggleInputGroup(false);
184
+
185
+ // Recalculate position after preview is shown to ensure buttons remain visible
186
+ this.recalculatePosition();
187
+ }
188
+
189
+ /**
190
+ * Remove image preview and show input again
191
+ */
192
+ removePreview() {
193
+ this.selectedImageSrc = null;
194
+ this.previewContainer.style.display = 'none';
195
+ this.imagePreview.src = '';
196
+
197
+ // Show input group and reset file input
198
+ this.toggleInputGroup(true);
199
+ if (this.fileInput) {
200
+ this.fileInput.value = '';
201
+ }
202
+
203
+ this.updateInsertButton();
204
+
205
+ // Recalculate position after preview is removed
206
+ this.recalculatePosition();
207
+ }
208
+
209
+ /**
210
+ * Toggle input group visibility
211
+ */
212
+ toggleInputGroup(show) {
213
+ if (!this.inputGroup) return;
214
+
215
+ if (show) {
216
+ this.inputGroup.style.display = 'flex';
217
+ this.inputGroup.style.visibility = 'visible';
218
+ if (this.customButton) {
219
+ this.customButton.style.pointerEvents = 'auto';
220
+ }
221
+ } else {
222
+ this.inputGroup.style.display = 'none';
223
+ this.inputGroup.style.visibility = 'hidden';
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Create preview container with image and remove button
229
+ */
230
+ createPreviewContainer() {
231
+ this.previewContainer = document.createElement('div');
232
+ this.previewContainer.className = 'image-preview-container';
233
+ this.previewContainer.style.cssText = 'display: none; position: relative;';
234
+
235
+ // Image preview
236
+ this.imagePreview = document.createElement('img');
237
+ this.imagePreview.className = 'image-preview';
238
+ this.imagePreview.style.cssText = 'max-width: 100%; max-height: 200px; border-radius: 8px; object-fit: contain;';
239
+
240
+ // Remove button
241
+ this.removeButton = document.createElement('button');
242
+ this.removeButton.className = 'image-remove-button';
243
+ this.removeButton.innerHTML = '×';
244
+ this.removeButton.style.cssText = `
245
+ position: absolute; top: 5px; right: 5px; background: rgba(0,0,0,0.7);
246
+ color: white; border: none; border-radius: 50%; width: 24px; height: 24px;
247
+ cursor: pointer; font-size: 16px; font-weight: bold;
248
+ `;
249
+ this.removeButton.addEventListener('click', () => this.removePreview());
250
+
251
+ this.previewContainer.appendChild(this.imagePreview);
252
+ this.previewContainer.appendChild(this.removeButton);
253
+ }
254
+
255
+ /**
256
+ * Check if URL is a valid image URL
257
+ */
258
+ isValidImageUrl(url) {
259
+ try {
260
+ const urlObj = new URL(url);
261
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'];
262
+ const imageHosts = ['imgur.com', 'images.unsplash.com', 'picsum.photos', 'via.placeholder.com'];
263
+
264
+ const pathname = urlObj.pathname.toLowerCase();
265
+ const hasImageExtension = imageExtensions.some(ext => pathname.endsWith(ext));
266
+ const isFromImageHost = imageHosts.some(host => urlObj.hostname.includes(host));
267
+
268
+ return hasImageExtension || isFromImageHost;
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ async insertImage() {
275
+ let src = this.selectedImageSrc || this.urlInput.value.trim();
276
+ const alt = '';
277
+
278
+ if (!src) return;
279
+
280
+ // Always validate URL (both file upload and URL input)
281
+ try {
282
+ const { default: Image } = await import('../formats/image.js');
283
+ const isValid = await Image.validateImageUrl(src);
284
+ if (!isValid) {
285
+ alert('Invalid image URL. Please check the URL and try again.');
286
+ return;
287
+ }
288
+ } catch (error) {
289
+ alert('Error validating image URL.');
290
+ return;
291
+ }
292
+
293
+ // Restore editor selection before inserting
294
+ this.restoreSelection();
295
+
296
+ if (this.options.onImageInsert) {
297
+ this.options.onImageInsert(src, alt);
298
+ }
299
+
300
+ this.hide();
301
+ this.reset();
302
+ }
303
+
304
+ reset() {
305
+ this.fileInput.value = '';
306
+ this.urlInput.value = '';
307
+ this.selectedImageSrc = null;
308
+
309
+ // Hide preview and show input
310
+ this.previewContainer.style.display = 'none';
311
+ this.imagePreview.src = '';
312
+ this.toggleInputGroup(true);
313
+
314
+ this.updateInsertButton();
315
+ this.customButton.style.display = 'block';
316
+ }
317
+
318
+ /**
319
+ * Save current editor selection
320
+ */
321
+ saveSelection() {
322
+ const selection = window.getSelection();
323
+ if (selection && selection.rangeCount > 0) {
324
+ this.savedSelection = selection.getRangeAt(0).cloneRange();
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Restore editor selection
330
+ */
331
+ restoreSelection() {
332
+ if (this.savedSelection) {
333
+ const selection = window.getSelection();
334
+ selection.removeAllRanges();
335
+ selection.addRange(this.savedSelection);
336
+ }
337
+ }
338
+
339
+ setupClickOutside() {
340
+ if (this.clickOutsideHandler) {
341
+ document.removeEventListener('click', this.clickOutsideHandler);
342
+ }
343
+
344
+ this.clickOutsideHandler = (e) => {
345
+ if (!this.popup.contains(e.target)) {
346
+ this.hide();
347
+ }
348
+ };
349
+
350
+ setTimeout(() => {
351
+ document.addEventListener('click', this.clickOutsideHandler);
352
+ }, 100);
353
+ }
354
+
355
+ setupResizeHandler() {
356
+ if (this.resizeHandler) {
357
+ window.removeEventListener('resize', this.resizeHandler);
358
+ }
359
+
360
+ this.resizeHandler = () => {
361
+ if (this.isVisible) {
362
+ this.recalculatePosition();
363
+ }
364
+ };
365
+
366
+ window.addEventListener('resize', this.resizeHandler);
367
+ }
368
+
369
+ removeResizeHandler() {
370
+ if (this.resizeHandler) {
371
+ window.removeEventListener('resize', this.resizeHandler);
372
+ this.resizeHandler = null;
373
+ }
374
+ }
375
+
376
+ removeClickOutside() {
377
+ if (this.clickOutsideHandler) {
378
+ document.removeEventListener('click', this.clickOutsideHandler);
379
+ this.clickOutsideHandler = null;
380
+ }
381
+ }
382
+
383
+ show(anchor) {
384
+ if (!anchor) return;
385
+
386
+ // Save current editor selection before showing popup
387
+ this.saveSelection();
388
+
389
+ // Reset state when showing popup
390
+ this.reset();
391
+
392
+ // Store anchor for recalculation
393
+ this.currentAnchor = anchor;
394
+
395
+ // Calculate and set popup position
396
+ const position = calculatePopupPosition(anchor, this.popup, {
397
+ offsetY: 5,
398
+ offsetX: 0
399
+ });
400
+ setPopupPosition(this.popup, position);
401
+
402
+ this.popup.classList.add('visible');
403
+ this.isVisible = true;
404
+
405
+ this.setupClickOutside();
406
+ }
407
+
408
+ /**
409
+ * Recalculate popup position to ensure it stays within viewport
410
+ */
411
+ recalculatePosition() {
412
+ if (!this.currentAnchor || !this.isVisible) return;
413
+
414
+ // Small delay to ensure DOM updates are complete
415
+ setTimeout(() => {
416
+ const position = calculatePopupPosition(this.currentAnchor, this.popup, {
417
+ offsetY: 5,
418
+ offsetX: 0
419
+ });
420
+ setPopupPosition(this.popup, position);
421
+ }, 10);
422
+ }
423
+
424
+ hide() {
425
+ this.popup.classList.remove('visible');
426
+ this.isVisible = false;
427
+ this.removeClickOutside();
428
+ // Clear saved selection to avoid memory leaks
429
+ this.savedSelection = null;
430
+ }
431
+
432
+ destroy() {
433
+ this.removeClickOutside();
434
+
435
+ if (this.popup && this.popup.parentNode) {
436
+ this.popup.parentNode.removeChild(this.popup);
437
+ }
438
+
439
+ this.popup = null;
440
+ this.isVisible = false;
441
+ }
442
+ }
443
+
444
+ export default ImagePopup;