@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,288 @@
1
+ /**
2
+ * Import Popup Component - Popup for importing various file types
3
+ */
4
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
5
+
6
+ class ImportPopup {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ onImport: null,
10
+ ...options
11
+ };
12
+
13
+ this.popup = null;
14
+ this.isVisible = false;
15
+ this.clickOutsideHandler = null;
16
+ this.selectedFile = null;
17
+ this.fileType = null;
18
+
19
+ this.createImportPopup();
20
+ }
21
+
22
+ createImportPopup() {
23
+ this.popup = document.createElement('div');
24
+ this.popup.className = 'import-popup';
25
+
26
+ const content = document.createElement('div');
27
+ content.className = 'import-popup-content';
28
+
29
+ // Title
30
+ const title = document.createElement('h3');
31
+ title.textContent = 'Import File';
32
+ title.className = 'import-popup-title';
33
+ content.appendChild(title);
34
+
35
+ // File type selector
36
+ const typeContainer = document.createElement('div');
37
+ typeContainer.className = 'import-type-container';
38
+
39
+ const typeLabel = document.createElement('label');
40
+ typeLabel.textContent = 'File Type:';
41
+ typeLabel.className = 'import-input-label';
42
+
43
+ this.typeSelect = document.createElement('select');
44
+ this.typeSelect.className = 'import-type-select';
45
+ this.typeSelect.innerHTML = `
46
+ <option value="">Select file type...</option>
47
+ <option value="html">HTML (.html, .htm)</option>
48
+ <option value="excel">Excel/CSV (.csv, .xlsx, .xls)</option>
49
+ <option value="pdf">PDF (.pdf)</option>
50
+ <option value="word">Word (.doc, .docx)</option>
51
+ `;
52
+ this.typeSelect.addEventListener('change', () => this.updateFileInput());
53
+
54
+ typeContainer.appendChild(typeLabel);
55
+ typeContainer.appendChild(this.typeSelect);
56
+ content.appendChild(typeContainer);
57
+
58
+ // File input
59
+ this.fileInput = document.createElement('input');
60
+ this.fileInput.type = 'file';
61
+ this.fileInput.className = 'import-file-input';
62
+ this.fileInput.disabled = true;
63
+ this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
64
+
65
+ content.appendChild(this.fileInput);
66
+
67
+ // File info
68
+ this.fileInfo = document.createElement('div');
69
+ this.fileInfo.className = 'import-file-info';
70
+ this.fileInfo.style.display = 'none';
71
+ content.appendChild(this.fileInfo);
72
+
73
+ // Buttons
74
+ const buttonContainer = document.createElement('div');
75
+ buttonContainer.className = 'import-button-container';
76
+
77
+ const cancelButton = document.createElement('button');
78
+ cancelButton.type = 'button';
79
+ cancelButton.className = 'import-button cancel-button';
80
+ cancelButton.textContent = 'Cancel';
81
+ cancelButton.addEventListener('click', () => this.hide());
82
+
83
+ this.importButton = document.createElement('button');
84
+ this.importButton.type = 'button';
85
+ this.importButton.className = 'import-button import-button-main';
86
+ this.importButton.textContent = 'Import';
87
+ this.importButton.disabled = true;
88
+ this.importButton.addEventListener('click', () => this.processImport());
89
+
90
+ buttonContainer.appendChild(cancelButton);
91
+ buttonContainer.appendChild(this.importButton);
92
+ content.appendChild(buttonContainer);
93
+
94
+ this.popup.appendChild(content);
95
+ appendPopup(this.popup);
96
+ }
97
+
98
+ updateFileInput() {
99
+ const selectedType = this.typeSelect.value;
100
+
101
+ if (selectedType) {
102
+ this.fileType = selectedType;
103
+ this.fileInput.disabled = false;
104
+
105
+ const acceptTypes = this.getAcceptTypes(selectedType);
106
+ this.fileInput.accept = acceptTypes;
107
+ } else {
108
+ this.fileType = null;
109
+ this.fileInput.disabled = true;
110
+ this.fileInput.accept = '';
111
+ }
112
+
113
+ this.updateImportButton();
114
+ }
115
+
116
+ getAcceptTypes(fileType) {
117
+ const types = {
118
+ html: '.html,.htm,text/html',
119
+ excel: '.csv,.xlsx,.xls,text/csv',
120
+ pdf: '.pdf,application/pdf',
121
+ word: '.doc,.docx'
122
+ };
123
+
124
+ return types[fileType] || '';
125
+ }
126
+
127
+ handleFileSelect(e) {
128
+ const file = e.target.files[0];
129
+ if (file) {
130
+ this.setSelectedFile(file);
131
+ }
132
+ }
133
+
134
+ setSelectedFile(file) {
135
+ this.selectedFile = file;
136
+
137
+ this.fileInfo.style.display = 'block';
138
+ this.fileInfo.innerHTML = `
139
+ <div><strong>Name:</strong> ${file.name}</div>
140
+ <div><strong>Size:</strong> ${this.formatFileSize(file.size)}</div>
141
+ <div><strong>Type:</strong> ${file.type || 'Unknown'}</div>
142
+ `;
143
+
144
+ this.updateImportButton();
145
+ }
146
+
147
+ formatFileSize(bytes) {
148
+ if (bytes === 0) return '0 Bytes';
149
+
150
+ const k = 1024;
151
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
152
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
153
+
154
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
155
+ }
156
+
157
+ updateImportButton() {
158
+ this.importButton.disabled = !this.selectedFile || !this.fileType;
159
+ }
160
+
161
+ async processImport() {
162
+ if (!this.selectedFile || !this.fileType) return;
163
+
164
+ try {
165
+ let content;
166
+
167
+ if (this.fileType === 'html') {
168
+ content = await this.readAsText(this.selectedFile);
169
+ } else if (this.fileType === 'excel') {
170
+ if (this.selectedFile.name.toLowerCase().endsWith('.csv')) {
171
+ const csvContent = await this.readAsText(this.selectedFile);
172
+ content = this.parseCSV(csvContent);
173
+ } else {
174
+ alert('Excel files (.xlsx/.xls) require additional libraries. Please use CSV format.');
175
+ return;
176
+ }
177
+ } else if (this.fileType === 'pdf') {
178
+ alert('PDF import requires additional libraries. Feature coming soon.');
179
+ return;
180
+ } else if (this.fileType === 'word') {
181
+ alert('Word document import requires additional libraries. Feature coming soon.');
182
+ return;
183
+ }
184
+
185
+ if (this.options.onImport) {
186
+ this.options.onImport(content, this.fileType);
187
+ }
188
+
189
+ this.hide();
190
+ this.reset();
191
+
192
+ } catch (error) {
193
+ console.error('Import error:', error);
194
+ alert('Error importing file: ' + error.message);
195
+ }
196
+ }
197
+
198
+ parseCSV(csvContent) {
199
+ const lines = csvContent.split('\n');
200
+ const result = [];
201
+
202
+ lines.forEach(line => {
203
+ if (line.trim()) {
204
+ const cells = line.split(',').map(cell => cell.trim().replace(/^["']|["']$/g, ''));
205
+ result.push(cells);
206
+ }
207
+ });
208
+
209
+ return result;
210
+ }
211
+
212
+ readAsText(file) {
213
+ return new Promise((resolve, reject) => {
214
+ const reader = new FileReader();
215
+ reader.onload = (e) => resolve(e.target.result);
216
+ reader.onerror = () => reject(new Error('Failed to read file'));
217
+ reader.readAsText(file);
218
+ });
219
+ }
220
+
221
+ reset() {
222
+ this.selectedFile = null;
223
+ this.fileType = null;
224
+ this.typeSelect.value = '';
225
+ this.fileInput.value = '';
226
+ this.fileInput.disabled = true;
227
+ this.fileInfo.style.display = 'none';
228
+ this.updateImportButton();
229
+ }
230
+
231
+ setupClickOutside() {
232
+ if (this.clickOutsideHandler) {
233
+ document.removeEventListener('click', this.clickOutsideHandler);
234
+ }
235
+
236
+ this.clickOutsideHandler = (e) => {
237
+ if (!this.popup.contains(e.target)) {
238
+ this.hide();
239
+ }
240
+ };
241
+
242
+ setTimeout(() => {
243
+ document.addEventListener('click', this.clickOutsideHandler);
244
+ }, 100);
245
+ }
246
+
247
+ removeClickOutside() {
248
+ if (this.clickOutsideHandler) {
249
+ document.removeEventListener('click', this.clickOutsideHandler);
250
+ this.clickOutsideHandler = null;
251
+ }
252
+ }
253
+
254
+ show(anchor) {
255
+ if (!anchor) return;
256
+
257
+ // Calculate and set popup position
258
+ const position = calculatePopupPosition(anchor, this.popup, {
259
+ offsetY: 5,
260
+ offsetX: 0
261
+ });
262
+ setPopupPosition(this.popup, position);
263
+
264
+ this.popup.classList.add('visible');
265
+ this.isVisible = true;
266
+
267
+ this.setupClickOutside();
268
+ }
269
+
270
+ hide() {
271
+ this.popup.classList.remove('visible');
272
+ this.isVisible = false;
273
+ this.removeClickOutside();
274
+ }
275
+
276
+ destroy() {
277
+ this.removeClickOutside();
278
+
279
+ if (this.popup && this.popup.parentNode) {
280
+ this.popup.parentNode.removeChild(this.popup);
281
+ }
282
+
283
+ this.popup = null;
284
+ this.isVisible = false;
285
+ }
286
+ }
287
+
288
+ export default ImportPopup;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Link Popup Component — a compact, inline link input that appears right at the
3
+ * selected text (Notion/Medium style). Shows just a URL field + Apply; the
4
+ * display-text field only appears when no text is selected.
5
+ */
6
+ import { appendPopup, calculatePopupPosition, setPopupPosition } from '../utils/popup-helper.js';
7
+
8
+ class LinkPopup {
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ onLinkSelect: null,
12
+ editor: null,
13
+ ...options
14
+ };
15
+
16
+ this.popup = null;
17
+ this.isVisible = false;
18
+ this.urlInput = null;
19
+ this.textInput = null;
20
+
21
+ this.createPopup();
22
+ }
23
+
24
+ createPopup() {
25
+ this.popup = document.createElement('div');
26
+ this.popup.className = 'link-popup link-popup--inline';
27
+
28
+ const content = document.createElement('div');
29
+ content.className = 'link-popup-content';
30
+
31
+ // Display-text field — only shown when there's no selected text to link.
32
+ this.textGroup = document.createElement('div');
33
+ this.textGroup.className = 'link-popup-row';
34
+ this.textInput = document.createElement('input');
35
+ this.textInput.type = 'text';
36
+ this.textInput.className = 'yjd-input';
37
+ this.textInput.placeholder = 'Text to display';
38
+ this.textGroup.appendChild(this.textInput);
39
+
40
+ // URL row: input + Apply.
41
+ const row = document.createElement('div');
42
+ row.className = 'link-popup-row';
43
+
44
+ this.urlInput = document.createElement('input');
45
+ this.urlInput.type = 'text';
46
+ this.urlInput.className = 'yjd-input';
47
+ this.urlInput.placeholder = 'Paste or type a link…';
48
+
49
+ this.applyBtn = document.createElement('button');
50
+ this.applyBtn.type = 'button';
51
+ this.applyBtn.className = 'yjd-button-confirm link-popup-apply';
52
+ this.applyBtn.textContent = 'Apply';
53
+ this.applyBtn.onclick = () => { this.handleOk(); this._refocusEditor(); };
54
+
55
+ row.appendChild(this.urlInput);
56
+ row.appendChild(this.applyBtn);
57
+
58
+ content.appendChild(this.textGroup);
59
+ content.appendChild(row);
60
+ this.popup.appendChild(content);
61
+
62
+ const onKey = (e) => {
63
+ if (e.key === 'Enter') { e.preventDefault(); this.handleOk(); this._refocusEditor(); }
64
+ if (e.key === 'Escape') { this.hide(); this._refocusEditor(); }
65
+ };
66
+ this.urlInput.onkeydown = onKey;
67
+ this.textInput.onkeydown = onKey;
68
+
69
+ appendPopup(this.popup);
70
+
71
+ // Prevent focus loss when clicking on popup
72
+ if (this.options.editor && typeof this.options.editor.preventFocusLoss === 'function') {
73
+ this.options.editor.preventFocusLoss(this.popup);
74
+ }
75
+ }
76
+
77
+ _refocusEditor() {
78
+ if (this.options.editor) setTimeout(() => this.options.editor.focus(), 0);
79
+ }
80
+
81
+ handleOk() {
82
+ const raw = this.urlInput.value.trim();
83
+ if (!raw) { this.urlInput.focus(); return; }
84
+
85
+ // Friendly normalisation: bare domains get https://; keep anchors,
86
+ // root-relative paths and explicit schemes (mailto:, tel:, …) as-is.
87
+ let url = raw;
88
+ const hasScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url);
89
+ if (!hasScheme && !url.startsWith('/') && !url.startsWith('#')) {
90
+ url = 'https://' + url;
91
+ }
92
+
93
+ const text = this.textInput.value.trim();
94
+ if (this.options.onLinkSelect) this.options.onLinkSelect({ url, text });
95
+ this.hide();
96
+ }
97
+
98
+ show(anchor, existingLink = null, selectedText = '') {
99
+ if (!anchor) return;
100
+
101
+ const hasSelection = !!selectedText;
102
+ this.urlInput.value = existingLink ? existingLink.url : '';
103
+ this.textInput.value = selectedText || (existingLink ? existingLink.text : '');
104
+ // No need to ask for display text when text is already selected.
105
+ this.textGroup.style.display = hasSelection ? 'none' : '';
106
+
107
+ const position = calculatePopupPosition(anchor, this.popup, { offsetY: 8, offsetX: 0 });
108
+ setPopupPosition(this.popup, position);
109
+
110
+ this.popup.classList.add('visible');
111
+ this.isVisible = true;
112
+
113
+ setTimeout(() => this.urlInput.focus(), 60);
114
+ setTimeout(() => {
115
+ document.addEventListener('click', this.closeOnClickOutside);
116
+ }, 100);
117
+ }
118
+
119
+ hide() {
120
+ this.popup.classList.remove('visible');
121
+ this.isVisible = false;
122
+ document.removeEventListener('click', this.closeOnClickOutside);
123
+ }
124
+
125
+ closeOnClickOutside = (e) => {
126
+ if (!this.popup.contains(e.target)) {
127
+ this.hide();
128
+ }
129
+ }
130
+
131
+ destroy() {
132
+ document.removeEventListener('click', this.closeOnClickOutside);
133
+ if (this.popup && this.popup.parentNode) {
134
+ this.popup.parentNode.removeChild(this.popup);
135
+ }
136
+ }
137
+ }
138
+
139
+ export default LinkPopup;