tulih-editor 0.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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/tulih-editor.css +1 -0
  4. package/dist/tulih-editor.es.js +3051 -0
  5. package/dist/tulih-editor.es.js.map +1 -0
  6. package/dist/tulih-editor.umd.js +8 -0
  7. package/dist/tulih-editor.umd.js.map +1 -0
  8. package/dist/types/core/Editor.d.ts +20 -0
  9. package/dist/types/core/PluginManager.d.ts +22 -0
  10. package/dist/types/core/helpers.d.ts +22 -0
  11. package/dist/types/index.d.ts +5 -0
  12. package/dist/types/plugins/align.d.ts +3 -0
  13. package/dist/types/plugins/autoLinkify.d.ts +3 -0
  14. package/dist/types/plugins/autosave.d.ts +3 -0
  15. package/dist/types/plugins/block.d.ts +3 -0
  16. package/dist/types/plugins/caseTransform.d.ts +3 -0
  17. package/dist/types/plugins/codeBlock.d.ts +3 -0
  18. package/dist/types/plugins/colors.d.ts +3 -0
  19. package/dist/types/plugins/darkMode.d.ts +3 -0
  20. package/dist/types/plugins/direction.d.ts +3 -0
  21. package/dist/types/plugins/dragDrop.d.ts +3 -0
  22. package/dist/types/plugins/emoji.d.ts +3 -0
  23. package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
  24. package/dist/types/plugins/findReplace.d.ts +3 -0
  25. package/dist/types/plugins/floatingToolbar.d.ts +3 -0
  26. package/dist/types/plugins/fontFamily.d.ts +3 -0
  27. package/dist/types/plugins/fontSize.d.ts +3 -0
  28. package/dist/types/plugins/fullscreen.d.ts +3 -0
  29. package/dist/types/plugins/history.d.ts +3 -0
  30. package/dist/types/plugins/hr.d.ts +3 -0
  31. package/dist/types/plugins/iframe.d.ts +3 -0
  32. package/dist/types/plugins/image.d.ts +3 -0
  33. package/dist/types/plugins/imageProps.d.ts +3 -0
  34. package/dist/types/plugins/imageTools.d.ts +3 -0
  35. package/dist/types/plugins/indent.d.ts +3 -0
  36. package/dist/types/plugins/index.d.ts +2 -0
  37. package/dist/types/plugins/inline.d.ts +3 -0
  38. package/dist/types/plugins/inlineCode.d.ts +3 -0
  39. package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
  40. package/dist/types/plugins/lineHeight.d.ts +3 -0
  41. package/dist/types/plugins/link.d.ts +3 -0
  42. package/dist/types/plugins/linkTooltip.d.ts +3 -0
  43. package/dist/types/plugins/list.d.ts +3 -0
  44. package/dist/types/plugins/markdown.d.ts +3 -0
  45. package/dist/types/plugins/mediaEmbed.d.ts +3 -0
  46. package/dist/types/plugins/pasteImage.d.ts +3 -0
  47. package/dist/types/plugins/pastePlain.d.ts +3 -0
  48. package/dist/types/plugins/pre.d.ts +3 -0
  49. package/dist/types/plugins/readOnly.d.ts +3 -0
  50. package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
  51. package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
  52. package/dist/types/plugins/source.d.ts +3 -0
  53. package/dist/types/plugins/specialChars.d.ts +3 -0
  54. package/dist/types/plugins/statusBar.d.ts +3 -0
  55. package/dist/types/plugins/subSuper.d.ts +3 -0
  56. package/dist/types/plugins/table.d.ts +3 -0
  57. package/dist/types/plugins/tableBg.d.ts +3 -0
  58. package/dist/types/plugins/tableTools.d.ts +3 -0
  59. package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
  60. package/dist/types/plugins/unlink.d.ts +3 -0
  61. package/dist/types/plugins/wordCount.d.ts +3 -0
  62. package/dist/types/types.d.ts +226 -0
  63. package/package.json +66 -0
  64. package/src/core/Editor.ts +460 -0
  65. package/src/core/PluginManager.ts +140 -0
  66. package/src/core/helpers.ts +209 -0
  67. package/src/css.d.ts +2 -0
  68. package/src/index.ts +87 -0
  69. package/src/plugins/align.ts +72 -0
  70. package/src/plugins/autoLinkify.ts +34 -0
  71. package/src/plugins/autosave.ts +69 -0
  72. package/src/plugins/block.ts +32 -0
  73. package/src/plugins/caseTransform.ts +54 -0
  74. package/src/plugins/codeBlock.ts +93 -0
  75. package/src/plugins/colors.ts +68 -0
  76. package/src/plugins/darkMode.ts +123 -0
  77. package/src/plugins/direction.ts +30 -0
  78. package/src/plugins/dragDrop.ts +68 -0
  79. package/src/plugins/emoji.ts +188 -0
  80. package/src/plugins/emojiAutocomplete.ts +183 -0
  81. package/src/plugins/findReplace.ts +229 -0
  82. package/src/plugins/floatingToolbar.ts +258 -0
  83. package/src/plugins/fontFamily.ts +41 -0
  84. package/src/plugins/fontSize.ts +32 -0
  85. package/src/plugins/fullscreen.ts +36 -0
  86. package/src/plugins/history.ts +14 -0
  87. package/src/plugins/hr.ts +118 -0
  88. package/src/plugins/iframe.ts +88 -0
  89. package/src/plugins/image.ts +107 -0
  90. package/src/plugins/imageProps.ts +119 -0
  91. package/src/plugins/imageTools.ts +344 -0
  92. package/src/plugins/indent.ts +29 -0
  93. package/src/plugins/index.ts +101 -0
  94. package/src/plugins/inline.ts +17 -0
  95. package/src/plugins/inlineCode.ts +21 -0
  96. package/src/plugins/keyboardShortcuts.ts +92 -0
  97. package/src/plugins/lineHeight.ts +40 -0
  98. package/src/plugins/link.ts +344 -0
  99. package/src/plugins/linkTooltip.ts +63 -0
  100. package/src/plugins/list.ts +141 -0
  101. package/src/plugins/markdown.ts +61 -0
  102. package/src/plugins/mediaEmbed.ts +44 -0
  103. package/src/plugins/pasteImage.ts +61 -0
  104. package/src/plugins/pastePlain.ts +43 -0
  105. package/src/plugins/pre.ts +11 -0
  106. package/src/plugins/readOnly.ts +46 -0
  107. package/src/plugins/shortcutCustomizer.ts +125 -0
  108. package/src/plugins/shortcutsHelp.ts +51 -0
  109. package/src/plugins/source.ts +77 -0
  110. package/src/plugins/specialChars.ts +64 -0
  111. package/src/plugins/statusBar.ts +85 -0
  112. package/src/plugins/subSuper.ts +20 -0
  113. package/src/plugins/table.ts +166 -0
  114. package/src/plugins/tableBg.ts +11 -0
  115. package/src/plugins/tableTools.ts +475 -0
  116. package/src/plugins/toolbarCollapse.ts +14 -0
  117. package/src/plugins/unlink.ts +29 -0
  118. package/src/plugins/wordCount.ts +34 -0
  119. package/src/styles/base.css +258 -0
  120. package/src/styles/editor.css +309 -0
  121. package/src/styles/index.css +6 -0
  122. package/src/types.ts +278 -0
@@ -0,0 +1,344 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ const link: Plugin = {
4
+ name: 'link',
5
+ order: 50,
6
+ toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Link" data-cmd="createLink"><i class="ti ti-link"></i></button>',
7
+ css: '' +
8
+ '.te-link-tools {' +
9
+ ' position: fixed; display: none; z-index: 3000;' +
10
+ ' background: #fff; border: 1px solid #dee2e6;' +
11
+ ' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
12
+ ' padding: .25rem .35rem; gap: .2rem; align-items: center;' +
13
+ ' white-space: nowrap;' +
14
+ '}' +
15
+ '.te-link-tools .btn {' +
16
+ ' padding: .2rem .45rem !important; font-size: .75rem !important; line-height: 1 !important;' +
17
+ '}' +
18
+ '.te-link-tools .btn i,' +
19
+ '.te-link-tools .btn svg {' +
20
+ ' width: 12px !important; height: 12px !important;' +
21
+ ' pointer-events: none;' +
22
+ '}' +
23
+ '.te-link-tools .te-divider-v {' +
24
+ ' display: inline-block; width: 1px; height: 14px;' +
25
+ ' background: #dee2e6; margin: 0 .15rem; vertical-align: middle;' +
26
+ '}' +
27
+ '.te-link-edit-popup {' +
28
+ ' position: fixed; display: none; z-index: 3001;' +
29
+ ' background: #fff; border: 1px solid #dee2e6;' +
30
+ ' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
31
+ ' padding: .35rem; gap: .25rem; align-items: center;' +
32
+ ' white-space: nowrap;' +
33
+ '}' +
34
+ '.te-link-edit-popup input {' +
35
+ ' width: 220px; padding: .2rem .4rem; font-size: .8rem;' +
36
+ ' border: 1px solid #dee2e6; border-radius: .25rem; outline: none;' +
37
+ '}' +
38
+ '.te-link-edit-popup input:focus {' +
39
+ ' border-color: #86b7fe; box-shadow: 0 0 0 2px rgba(13,110,253,.15);' +
40
+ '}' +
41
+ '.te-link-edit-popup .btn {' +
42
+ ' padding: .2rem .5rem !important; font-size: .75rem !important; line-height: 1 !important;' +
43
+ '}',
44
+ modalHTML: '' +
45
+ '<div class="te-link-modal te-modal" aria-hidden="true">' +
46
+ ' <div class="te-modal-backdrop" data-te-close></div>' +
47
+ ' <div class="te-modal-dialog">' +
48
+ ' <div class="te-modal-header">' +
49
+ ' <h5 class="te-modal-title m-0">Insert Link</h5>' +
50
+ ' <button type="button" class="btn-close" data-te-close aria-label="Close"></button>' +
51
+ ' </div>' +
52
+ ' <form class="te-link-form">' +
53
+ ' <div class="te-modal-body">' +
54
+ ' <div class="mb-3">' +
55
+ ' <label class="form-label">URL</label>' +
56
+ ' <input type="url" class="te-link-url form-control" placeholder="https://example.com" required>' +
57
+ ' </div>' +
58
+ ' <div class="mb-3">' +
59
+ ' <label class="form-label">Text (optional)</label>' +
60
+ ' <input type="text" class="te-link-text form-control" placeholder="Link text">' +
61
+ ' </div>' +
62
+ ' <div class="mb-3">' +
63
+ ' <label class="form-label">Rel (optional)</label>' +
64
+ ' <input type="text" class="te-link-rel form-control" placeholder="noopener noreferrer, nofollow, ugc, sponsored">' +
65
+ ' </div>' +
66
+ ' <div class="form-check">' +
67
+ ' <input class="te-link-blank form-check-input" type="checkbox" checked>' +
68
+ ' <label class="form-check-label">Open in new tab</label>' +
69
+ ' </div>' +
70
+ ' </div>' +
71
+ ' <div class="te-modal-footer">' +
72
+ ' <button type="button" class="btn btn-outline-secondary" data-te-close>Cancel</button>' +
73
+ ' <button type="submit" class="btn btn-primary">Insert</button>' +
74
+ ' </div>' +
75
+ ' </form>' +
76
+ ' </div>' +
77
+ '</div>',
78
+ init(ctx: PluginContext): void {
79
+ if(!ctx.features.link) return;
80
+ var editingLink: HTMLAnchorElement | null = null;
81
+
82
+ var linkTools = document.createElement('div');
83
+ linkTools.className = 'te-link-tools';
84
+ linkTools.innerHTML = '' +
85
+ '<button type="button" class="btn btn-sm btn-light" data-te-link="edit" title="Edit link"><i class="ti ti-edit"></i> Edit</button>' +
86
+ '<button type="button" class="btn btn-sm btn-light" data-te-link="remove" title="Remove link"><i class="ti ti-link-off"></i> Remove</button>';
87
+ document.body.appendChild(linkTools);
88
+
89
+ var linkEditPopup = document.createElement('div');
90
+ linkEditPopup.className = 'te-link-edit-popup';
91
+ linkEditPopup.innerHTML = '' +
92
+ '<input type="url" class="te-link-edit-url" placeholder="https://example.com" />' +
93
+ '<button type="button" class="btn btn-sm btn-primary" data-te-link-edit="apply">Apply</button>' +
94
+ '<button type="button" class="btn btn-sm btn-light" data-te-link-edit="cancel">Cancel</button>';
95
+ document.body.appendChild(linkEditPopup);
96
+
97
+ function posLinkTools(a: HTMLAnchorElement): void {
98
+ var rect = a.getBoundingClientRect();
99
+ var er = ctx.editor.getBoundingClientRect();
100
+ linkTools.style.display = 'flex';
101
+ linkTools.style.visibility = 'hidden';
102
+ var tw = linkTools.offsetWidth;
103
+ var th = linkTools.offsetHeight;
104
+ var left = rect.left + rect.width / 2 - tw / 2;
105
+ if(left < er.left) left = er.left;
106
+ if(left + tw > er.right) left = er.right - tw;
107
+ linkTools.style.left = left + 'px';
108
+ var spaceBelow = window.innerHeight - rect.bottom;
109
+ var top = (spaceBelow >= th + 6) ? (rect.bottom + 6) : (rect.top - th - 6);
110
+ if(top < er.top) top = rect.bottom + 6;
111
+ linkTools.style.top = top + 'px';
112
+ linkTools.style.visibility = '';
113
+ }
114
+
115
+ function hideLinkTools(): void {
116
+ linkTools.style.display = 'none';
117
+ editingLink = null;
118
+ }
119
+
120
+ var _editPopupTime = 0;
121
+
122
+ function hideEditPopup(_from?: string): void {
123
+ if(Date.now() - _editPopupTime < 200) return;
124
+ linkEditPopup.style.display = 'none';
125
+ }
126
+
127
+ function showEditPopup(link: HTMLAnchorElement): void {
128
+ _editPopupTime = Date.now();
129
+ var rect = link.getBoundingClientRect();
130
+ var er = ctx.editor.getBoundingClientRect();
131
+ linkEditPopup.style.display = 'flex';
132
+ linkEditPopup.style.visibility = 'hidden';
133
+ var pw = linkEditPopup.offsetWidth || 350;
134
+ var ph = linkEditPopup.offsetHeight || 32;
135
+ var left = rect.left + rect.width / 2 - pw / 2;
136
+ if(left < er.left) left = er.left;
137
+ if(left + pw > er.right) left = er.right - pw;
138
+ linkEditPopup.style.left = left + 'px';
139
+ var spaceBelow = window.innerHeight - rect.bottom;
140
+ linkEditPopup.style.top = ((spaceBelow >= ph + 6) ? (rect.bottom + 6) : (rect.top - ph - 6)) + 'px';
141
+ linkEditPopup.style.visibility = '';
142
+ (linkEditPopup.querySelector('.te-link-edit-url') as HTMLInputElement).value = link.getAttribute('href') || '';
143
+ setTimeout(function(){ (linkEditPopup.querySelector('.te-link-edit-url') as HTMLInputElement).focus(); }, 0);
144
+ }
145
+
146
+ function openLinkModal(): void {
147
+ ctx.saveSel();
148
+ var m = ctx.wrapper.querySelector('.te-link-modal');
149
+ if(m) m.classList.add('is-open');
150
+ }
151
+
152
+ function closeLinkModal(): void {
153
+ var m = ctx.wrapper.querySelector('.te-link-modal');
154
+ if(m) m.classList.remove('is-open');
155
+ }
156
+
157
+ function fillModal(a: HTMLAnchorElement): void {
158
+ var u = ctx.wrapper.querySelector('.te-link-url') as HTMLInputElement | null;
159
+ var t = ctx.wrapper.querySelector('.te-link-text') as HTMLInputElement | null;
160
+ var r = ctx.wrapper.querySelector('.te-link-rel') as HTMLInputElement | null;
161
+ var b = ctx.wrapper.querySelector('.te-link-blank') as HTMLInputElement | null;
162
+ var sub = ctx.wrapper.querySelector('.te-link-form button[type="submit"]') as HTMLButtonElement | null;
163
+ if(u) u.value = a.getAttribute('href') || '';
164
+ if(t) t.value = a.textContent || '';
165
+ if(r) r.value = a.getAttribute('rel') || '';
166
+ if(b) b.checked = a.getAttribute('target') === '_blank';
167
+ if(sub) sub.textContent = 'Update';
168
+ }
169
+
170
+ function getSelectedText(): string {
171
+ var sel = window.getSelection();
172
+ return (sel && sel.toString().trim()) || '';
173
+ }
174
+
175
+ var btn = ctx.wrapper.querySelector('[data-cmd="createLink"]');
176
+ if(btn){
177
+ btn.addEventListener('click', function(){
178
+ editingLink = null;
179
+ hideLinkTools();
180
+ hideEditPopup();
181
+ var u = ctx.wrapper.querySelector('.te-link-url') as HTMLInputElement | null;
182
+ var t = ctx.wrapper.querySelector('.te-link-text') as HTMLInputElement | null;
183
+ var r = ctx.wrapper.querySelector('.te-link-rel') as HTMLInputElement | null;
184
+ var b = ctx.wrapper.querySelector('.te-link-blank') as HTMLInputElement | null;
185
+ var sub = ctx.wrapper.querySelector('.te-link-form button[type="submit"]') as HTMLButtonElement | null;
186
+ if(u) u.value = '';
187
+ if(t) t.value = getSelectedText() || '';
188
+ if(r) r.value = '';
189
+ if(b) b.checked = true;
190
+ if(sub) sub.textContent = 'Insert';
191
+ openLinkModal();
192
+ });
193
+ }
194
+
195
+ ctx.editor.addEventListener('click', function(e: MouseEvent){
196
+ var a = (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('a') as HTMLAnchorElement | null;
197
+ if(!a || !ctx.editor.contains(a)) return;
198
+ if(!ctx.wrapper.contains(a)) return;
199
+ if(e.ctrlKey || e.metaKey) e.preventDefault();
200
+ e.stopPropagation();
201
+ hideEditPopup('editor.click');
202
+ editingLink = a;
203
+ fillModal(a);
204
+ posLinkTools(a);
205
+ });
206
+
207
+ linkTools.addEventListener('mousedown', function(e: MouseEvent){
208
+ e.preventDefault();
209
+ e.stopPropagation();
210
+ var actionBtn = (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('[data-te-link]') as HTMLElement | null;
211
+ if(!actionBtn) return;
212
+ var action = actionBtn.getAttribute('data-te-link');
213
+ var link = editingLink;
214
+
215
+ if(action === 'edit'){
216
+ if(link && ctx.editor.contains(link)){
217
+ editingLink = link;
218
+ showEditPopup(link);
219
+ setTimeout(function(){ linkTools.style.display = 'none'; }, 0);
220
+ }
221
+ }
222
+ else if(action === 'remove'){
223
+ hideLinkTools();
224
+ if(link && ctx.editor.contains(link)){
225
+ var parent = link.parentNode!;
226
+ while(link.firstChild){
227
+ parent.insertBefore(link.firstChild, link);
228
+ }
229
+ parent.removeChild(link);
230
+ ctx.editor.focus();
231
+ }
232
+ }
233
+ });
234
+
235
+ function updateLink(url: string): void {
236
+ if(!editingLink || !ctx.editor.contains(editingLink)) return;
237
+ if(!/^https?:\/\//i.test(url) && !/^mailto:/i.test(url) && !/^tel:/i.test(url)) url = 'https://' + url;
238
+ editingLink.setAttribute('href', url);
239
+ hideEditPopup('updateLink');
240
+ editingLink = null;
241
+ ctx.editor.focus();
242
+ }
243
+
244
+ linkEditPopup.addEventListener('click', function(e: MouseEvent){ e.stopPropagation(); });
245
+
246
+ linkEditPopup.addEventListener('mousedown', function(e: MouseEvent){
247
+ e.preventDefault();
248
+ e.stopPropagation();
249
+ var actionBtn = (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('[data-te-link-edit]') as HTMLElement | null;
250
+ if(!actionBtn) return;
251
+ var action = actionBtn.getAttribute('data-te-link-edit');
252
+
253
+ if(action === 'cancel'){
254
+ hideEditPopup();
255
+ editingLink = null;
256
+ ctx.editor.focus();
257
+ return;
258
+ }
259
+
260
+ if(action === 'apply'){
261
+ var url = (linkEditPopup.querySelector('.te-link-edit-url') as HTMLInputElement).value.trim();
262
+ if(!url) return;
263
+ updateLink(url);
264
+ }
265
+ });
266
+
267
+ linkEditPopup.addEventListener('keydown', function(e: KeyboardEvent){
268
+ if(e.key === 'Enter'){
269
+ e.preventDefault();
270
+ var url = (linkEditPopup.querySelector('.te-link-edit-url') as HTMLInputElement).value.trim();
271
+ if(!url) return;
272
+ updateLink(url);
273
+ }
274
+ if(e.key === 'Escape'){
275
+ hideEditPopup();
276
+ editingLink = null;
277
+ ctx.editor.focus();
278
+ }
279
+ });
280
+
281
+ linkTools.addEventListener('click', function(e: MouseEvent){ e.stopPropagation(); });
282
+
283
+ document.addEventListener('click', function(e: MouseEvent){
284
+ if(linkTools.style.display !== 'none' && !linkTools.contains(e.target as Node)){
285
+ hideLinkTools();
286
+ }
287
+ if(linkEditPopup.style.display !== 'none' && !linkEditPopup.contains(e.target as Node)){
288
+ var fromTools = (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('.te-link-tools');
289
+ if(!fromTools){
290
+ if(Date.now() - _editPopupTime < 200){
291
+ // too soon after showing edit popup, don't close
292
+ } else {
293
+ hideEditPopup('doc.click');
294
+ editingLink = null;
295
+ }
296
+ }
297
+ }
298
+ });
299
+
300
+ var form = ctx.wrapper.querySelector('.te-link-form');
301
+ if(form){
302
+ form.addEventListener('submit', function(e: Event){
303
+ e.preventDefault();
304
+ var url = ((ctx.wrapper.querySelector('.te-link-url') as HTMLInputElement).value || '').trim();
305
+ var text = ((ctx.wrapper.querySelector('.te-link-text') as HTMLInputElement).value || '').trim();
306
+ var blank = (ctx.wrapper.querySelector('.te-link-blank') as HTMLInputElement).checked;
307
+ var rel = ((ctx.wrapper.querySelector('.te-link-rel') as HTMLInputElement).value || '').trim();
308
+ if(!url) return;
309
+ var isOk = ctx.utils.isSafeUrl ? ctx.utils.isSafeUrl(url, (ctx.options as any).urlSchemes || ['http','https']) : true;
310
+ if(!isOk) return;
311
+
312
+ if(editingLink && ctx.editor.contains(editingLink)){
313
+ editingLink.setAttribute('href', url);
314
+ if(blank) editingLink.setAttribute('target', '_blank');
315
+ else editingLink.removeAttribute('target');
316
+ if(rel) editingLink.setAttribute('rel', rel);
317
+ else editingLink.removeAttribute('rel');
318
+ if(text) editingLink.textContent = text;
319
+ closeLinkModal();
320
+ ctx.editor.focus();
321
+ return;
322
+ }
323
+
324
+ ctx.restoreSel();
325
+ var relFinal = rel || (blank ? 'noopener noreferrer' : '');
326
+ var displayText = (text || url).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
327
+ var a = '<a href="' + url.replace(/"/g,'&quot;') + '"' +
328
+ (blank ? ' target="_blank"' : '') +
329
+ (relFinal ? ' rel="' + relFinal.replace(/"/g,'&quot;') + '"' : '') +
330
+ '>' + displayText + '</a>';
331
+ document.execCommand('insertHTML', false, a);
332
+ closeLinkModal();
333
+ ctx.editor.focus();
334
+ });
335
+ }
336
+
337
+ var closeBtns = ctx.wrapper.querySelectorAll('[data-te-close]');
338
+ for(var i = 0; i < closeBtns.length; i++){
339
+ closeBtns[i].addEventListener('click', function(){ editingLink = null; });
340
+ }
341
+ }
342
+ };
343
+
344
+ export default link;
@@ -0,0 +1,63 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ const linkTooltip: Plugin = {
4
+ name: 'linkTooltip',
5
+ order: 4,
6
+ css: '' +
7
+ '.te-link-tooltip {' +
8
+ ' position: fixed; z-index: 9999;' +
9
+ ' background: #333; color: #fff;' +
10
+ ' padding: 4px 8px; border-radius: 4px;' +
11
+ ' font-size: .75rem; line-height: 1.4;' +
12
+ ' max-width: 300px; overflow: hidden;' +
13
+ ' text-overflow: ellipsis; white-space: nowrap;' +
14
+ ' pointer-events: none; opacity: 0;' +
15
+ ' transition: opacity .15s;' +
16
+ '}' +
17
+ '.te-link-tooltip.show { opacity: 1; }',
18
+ init(ctx: PluginContext): void {
19
+ if(ctx.features.linkTooltip === false) return;
20
+ var editor = ctx.editor;
21
+
22
+ var tooltip = document.createElement('div');
23
+ tooltip.className = 'te-link-tooltip';
24
+ document.body.appendChild(tooltip);
25
+ var hideTimer: ReturnType<typeof setTimeout> | null = null;
26
+
27
+ editor.addEventListener('mouseover', function(e: MouseEvent){
28
+ var a = (e.target as HTMLElement) && (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('a') as HTMLAnchorElement | null;
29
+ if(!a || !editor.contains(a)){ hide(); return; }
30
+ clearTimeout(hideTimer!);
31
+ var href = a.getAttribute('href') || '';
32
+ tooltip.textContent = href;
33
+ position(e);
34
+ tooltip.classList.add('show');
35
+ });
36
+
37
+ editor.addEventListener('mousemove', function(e: MouseEvent){
38
+ if(!tooltip.classList.contains('show')) return;
39
+ position(e);
40
+ });
41
+
42
+ editor.addEventListener('mouseout', function(e: MouseEvent){
43
+ var a = (e.target as HTMLElement) && (e.target as HTMLElement).closest && (e.target as HTMLElement).closest('a');
44
+ if(!a) return;
45
+ hideTimer = setTimeout(hide, 200);
46
+ });
47
+
48
+ function position(e: MouseEvent): void {
49
+ var x = e.clientX + 12;
50
+ var y = e.clientY + 12;
51
+ if(x + 310 > window.innerWidth) x = window.innerWidth - 310;
52
+ if(y + 30 > window.innerHeight) y = e.clientY - 30;
53
+ tooltip.style.left = x + 'px';
54
+ tooltip.style.top = y + 'px';
55
+ }
56
+
57
+ function hide(): void {
58
+ tooltip.classList.remove('show');
59
+ }
60
+ }
61
+ };
62
+
63
+ export default linkTooltip;
@@ -0,0 +1,141 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ var CLS_RE = /\bte-list-\S+/g;
4
+
5
+ var OL_STYLES = [
6
+ { val: 'decimal', label: '1.', cls: '' },
7
+ { val: 'upper-alpha', label: 'A.', cls: 'te-list-upper-alpha' },
8
+ { val: 'lower-alpha', label: 'a.', cls: 'te-list-lower-alpha' },
9
+ { val: 'upper-roman', label: 'I.', cls: 'te-list-upper-roman' },
10
+ { val: 'lower-roman', label: 'i.', cls: 'te-list-lower-roman' }
11
+ ];
12
+
13
+ var UL_STYLES = [
14
+ { val: 'disc', label: '•', cls: '' },
15
+ { val: 'circle', label: '○', cls: 'te-list-circle' },
16
+ { val: 'square', label: '■', cls: 'te-list-square' }
17
+ ];
18
+
19
+ function setListStyle(list: HTMLElement | null, val: string, cls: string){
20
+ if(!list) return;
21
+ list.className = (list.className || '').replace(CLS_RE, '').trim();
22
+ if(cls) list.classList.add(cls);
23
+ list.style.listStyleType = val || '';
24
+ }
25
+
26
+ function findAncestorList(node: Node | null): HTMLElement | null {
27
+ while(node && (node as Element).nodeType === 1){
28
+ if((node as Element).tagName === 'OL' || (node as Element).tagName === 'UL') return node as HTMLElement;
29
+ node = (node as Element).parentElement;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function buildOptions(styles: { val: string; label: string; cls: string }[]){
35
+ var html = '<option value="">—</option>';
36
+ for(var i = 0; i < styles.length; i++){
37
+ html += '<option value="' + styles[i].val + '">' + styles[i].label + '</option>';
38
+ }
39
+ return html;
40
+ }
41
+
42
+ function handleSelect(ctx: PluginContext, select: HTMLSelectElement, tag: string, styles: { val: string; label: string; cls: string }[]){
43
+ var val = select.value;
44
+ select.value = '';
45
+
46
+ var sel = window.getSelection();
47
+ if(!sel || !sel.anchorNode) return;
48
+ var node: Node | null = sel.anchorNode;
49
+ if(node.nodeType === 3) node = node.parentNode;
50
+
51
+ var list = findAncestorList(node);
52
+ var execCmd = tag === 'OL' ? 'insertOrderedList' : 'insertUnorderedList';
53
+
54
+ if(!val){
55
+ if(list && list.tagName === tag){
56
+ document.execCommand(execCmd, false, undefined);
57
+ }
58
+ ctx.editor.focus();
59
+ return;
60
+ }
61
+
62
+ var matched: { val: string; label: string; cls: string } | null = null;
63
+ for(var i = 0; i < styles.length; i++){
64
+ if(styles[i].val === val){ matched = styles[i]; break; }
65
+ }
66
+ if(!matched) return;
67
+
68
+ if(list){
69
+ if(list.tagName === tag){
70
+ setListStyle(list, val, matched.cls);
71
+ } else {
72
+ document.execCommand(execCmd, false, undefined);
73
+ var afterNode: Node | null = sel.focusNode;
74
+ if(afterNode && afterNode.nodeType === 3) afterNode = afterNode.parentNode;
75
+ var newList = findAncestorList(afterNode);
76
+ if(newList && newList.tagName === tag) setListStyle(newList, val, matched.cls);
77
+ }
78
+ } else {
79
+ document.execCommand(execCmd, false, undefined);
80
+ var afterNode: Node | null = sel.focusNode;
81
+ if(afterNode && afterNode.nodeType === 3) afterNode = afterNode.parentNode;
82
+ var newList = findAncestorList(afterNode);
83
+ if(newList) setListStyle(newList, val, matched.cls);
84
+ }
85
+ ctx.editor.focus();
86
+ }
87
+
88
+ const list: Plugin = {
89
+ name: 'list',
90
+ order: 25,
91
+ css: '' +
92
+ '.te-list-select { width: auto; }' +
93
+ '.te-editor ol.te-list-upper-alpha { list-style-type: upper-alpha; }' +
94
+ '.te-editor ol.te-list-lower-alpha { list-style-type: lower-alpha; }' +
95
+ '.te-editor ol.te-list-upper-roman { list-style-type: upper-roman; }' +
96
+ '.te-editor ol.te-list-lower-roman { list-style-type: lower-roman; }' +
97
+ '.te-editor ul.te-list-circle { list-style-type: circle; }' +
98
+ '.te-editor ul.te-list-square { list-style-type: square; }',
99
+ toolbarHTML: '' +
100
+ '<select class="te-list-ol form-select form-select-sm te-list-select" title="Ordered list style">' +
101
+ buildOptions(OL_STYLES) +
102
+ '</select>' +
103
+ '<select class="te-list-ul form-select form-select-sm te-list-select" title="Unordered list style">' +
104
+ buildOptions(UL_STYLES) +
105
+ '</select>',
106
+ init(ctx: PluginContext): void {
107
+ if(ctx.features.list === false) return;
108
+
109
+ var olSel = ctx.wrapper.querySelector('.te-list-ol') as HTMLSelectElement | null;
110
+ var ulSel = ctx.wrapper.querySelector('.te-list-ul') as HTMLSelectElement | null;
111
+
112
+ if(olSel){
113
+ olSel.addEventListener('change', function(){
114
+ handleSelect(ctx, olSel!, 'OL', OL_STYLES);
115
+ });
116
+ }
117
+
118
+ if(ulSel){
119
+ ulSel.addEventListener('change', function(){
120
+ handleSelect(ctx, ulSel!, 'UL', UL_STYLES);
121
+ });
122
+ }
123
+
124
+ ctx.editor.addEventListener('keydown', function(e: KeyboardEvent){
125
+ if(e.key === 'Tab'){
126
+ var sel = window.getSelection();
127
+ if(!sel || !sel.anchorNode) return;
128
+ var node: Node | null = sel.anchorNode;
129
+ if(node.nodeType === 3) node = node.parentNode;
130
+ var list = findAncestorList(node);
131
+ if(list){
132
+ e.preventDefault();
133
+ document.execCommand(e.shiftKey ? 'outdent' : 'indent', false, undefined);
134
+ ctx.editor.focus();
135
+ }
136
+ }
137
+ });
138
+ }
139
+ };
140
+
141
+ export default list;
@@ -0,0 +1,61 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ var RULES: Array<{ re: RegExp; cmd: string; val: string | null }> = [
4
+ { re: /^#{6}\s$/, cmd: 'formatBlock', val: 'h6' },
5
+ { re: /^#{5}\s$/, cmd: 'formatBlock', val: 'h5' },
6
+ { re: /^#{4}\s$/, cmd: 'formatBlock', val: 'h4' },
7
+ { re: /^#{3}\s$/, cmd: 'formatBlock', val: 'h3' },
8
+ { re: /^#{2}\s$/, cmd: 'formatBlock', val: 'h2' },
9
+ { re: /^#{1}\s$/, cmd: 'formatBlock', val: 'h1' },
10
+ { re: /^>\s$/, cmd: 'formatBlock', val: 'blockquote' },
11
+ { re: /^[-*]\s$/, cmd: 'insertUnorderedList', val: null },
12
+ { re: /^1[.)]\s$/, cmd: 'insertOrderedList', val: null },
13
+ ];
14
+
15
+ const markdown: Plugin = {
16
+ name: 'markdown',
17
+ order: 5,
18
+ init(ctx: PluginContext): void {
19
+ if(!ctx.features.markdown) return;
20
+
21
+ ctx.editor.addEventListener('keydown', function(e: KeyboardEvent){
22
+ if(e.key !== ' ' && e.key !== 'Enter') return;
23
+
24
+ var sel = window.getSelection();
25
+ if(!sel || sel.rangeCount === 0 || !sel.isCollapsed) return;
26
+ var node = sel.anchorNode;
27
+ if(!node || node.nodeType !== 3) return;
28
+ var text = node.textContent || '';
29
+
30
+ for(var i = 0; i < RULES.length; i++){
31
+ var rule = RULES[i];
32
+ if(!rule.re.test(text)) continue;
33
+
34
+ e.preventDefault();
35
+
36
+ if(e.key === 'Enter'){
37
+ node.textContent = text.slice(0, -1);
38
+ setTimeout(function(){
39
+ document.execCommand(rule.cmd, false, rule.val || undefined);
40
+ }, 0);
41
+ return;
42
+ }
43
+
44
+ node.textContent = '';
45
+ document.execCommand(rule.cmd, false, rule.val || undefined);
46
+ ctx.editor.focus();
47
+ return;
48
+ }
49
+
50
+ if(e.key === 'Enter' && text.trim() === ''){
51
+ var parent = node.parentNode as HTMLElement | null;
52
+ if(parent && (parent.tagName === 'BLOCKQUOTE' || parent.tagName === 'PRE')){
53
+ e.preventDefault();
54
+ document.execCommand('formatBlock', false, 'p');
55
+ }
56
+ }
57
+ });
58
+ }
59
+ };
60
+
61
+ export default markdown;
@@ -0,0 +1,44 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+ import * as H from '../core/helpers';
3
+
4
+ var EMBED_PROVIDERS: { match: RegExp; embed: (id: string) => string }[] = [
5
+ {
6
+ match: /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
7
+ embed: function(id){ return '<iframe src="https://www.youtube.com/embed/' + id + '" style="max-width:100%;border:0;aspect-ratio:16/9;" allowfullscreen></iframe>'; }
8
+ },
9
+ {
10
+ match: /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,
11
+ embed: function(id){ return '<iframe src="https://player.vimeo.com/video/' + id + '" style="max-width:100%;border:0;aspect-ratio:16/9;" allowfullscreen></iframe>'; }
12
+ }
13
+ ];
14
+
15
+ const mediaEmbed: Plugin = {
16
+ name: 'mediaEmbed',
17
+ order: 2,
18
+ init(ctx: PluginContext): void {
19
+ if(ctx.features.mediaEmbed === false) return;
20
+ var editor = ctx.editor;
21
+
22
+ editor.addEventListener('paste', function(e: ClipboardEvent){
23
+ var text = (e.clipboardData && e.clipboardData.getData('text/plain')) || '';
24
+ if(!text) return;
25
+ text = text.trim();
26
+
27
+ for(var i = 0; i < EMBED_PROVIDERS.length; i++){
28
+ var m = text.match(EMBED_PROVIDERS[i].match);
29
+ if(m && m[1]){
30
+ e.preventDefault();
31
+ e.stopImmediatePropagation();
32
+ var html = EMBED_PROVIDERS[i].embed(m[1]);
33
+ document.execCommand('insertHTML', false, html);
34
+ setTimeout(function(){
35
+ try { H.normalizeParagraphs(editor); H.ensureInitialParagraph(editor); } catch(_){ }
36
+ }, 0);
37
+ return;
38
+ }
39
+ }
40
+ }, true);
41
+ }
42
+ };
43
+
44
+ export default mediaEmbed;