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.
- package/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/tulih-editor.css +1 -0
- package/dist/tulih-editor.es.js +3051 -0
- package/dist/tulih-editor.es.js.map +1 -0
- package/dist/tulih-editor.umd.js +8 -0
- package/dist/tulih-editor.umd.js.map +1 -0
- package/dist/types/core/Editor.d.ts +20 -0
- package/dist/types/core/PluginManager.d.ts +22 -0
- package/dist/types/core/helpers.d.ts +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/plugins/align.d.ts +3 -0
- package/dist/types/plugins/autoLinkify.d.ts +3 -0
- package/dist/types/plugins/autosave.d.ts +3 -0
- package/dist/types/plugins/block.d.ts +3 -0
- package/dist/types/plugins/caseTransform.d.ts +3 -0
- package/dist/types/plugins/codeBlock.d.ts +3 -0
- package/dist/types/plugins/colors.d.ts +3 -0
- package/dist/types/plugins/darkMode.d.ts +3 -0
- package/dist/types/plugins/direction.d.ts +3 -0
- package/dist/types/plugins/dragDrop.d.ts +3 -0
- package/dist/types/plugins/emoji.d.ts +3 -0
- package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
- package/dist/types/plugins/findReplace.d.ts +3 -0
- package/dist/types/plugins/floatingToolbar.d.ts +3 -0
- package/dist/types/plugins/fontFamily.d.ts +3 -0
- package/dist/types/plugins/fontSize.d.ts +3 -0
- package/dist/types/plugins/fullscreen.d.ts +3 -0
- package/dist/types/plugins/history.d.ts +3 -0
- package/dist/types/plugins/hr.d.ts +3 -0
- package/dist/types/plugins/iframe.d.ts +3 -0
- package/dist/types/plugins/image.d.ts +3 -0
- package/dist/types/plugins/imageProps.d.ts +3 -0
- package/dist/types/plugins/imageTools.d.ts +3 -0
- package/dist/types/plugins/indent.d.ts +3 -0
- package/dist/types/plugins/index.d.ts +2 -0
- package/dist/types/plugins/inline.d.ts +3 -0
- package/dist/types/plugins/inlineCode.d.ts +3 -0
- package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
- package/dist/types/plugins/lineHeight.d.ts +3 -0
- package/dist/types/plugins/link.d.ts +3 -0
- package/dist/types/plugins/linkTooltip.d.ts +3 -0
- package/dist/types/plugins/list.d.ts +3 -0
- package/dist/types/plugins/markdown.d.ts +3 -0
- package/dist/types/plugins/mediaEmbed.d.ts +3 -0
- package/dist/types/plugins/pasteImage.d.ts +3 -0
- package/dist/types/plugins/pastePlain.d.ts +3 -0
- package/dist/types/plugins/pre.d.ts +3 -0
- package/dist/types/plugins/readOnly.d.ts +3 -0
- package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
- package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
- package/dist/types/plugins/source.d.ts +3 -0
- package/dist/types/plugins/specialChars.d.ts +3 -0
- package/dist/types/plugins/statusBar.d.ts +3 -0
- package/dist/types/plugins/subSuper.d.ts +3 -0
- package/dist/types/plugins/table.d.ts +3 -0
- package/dist/types/plugins/tableBg.d.ts +3 -0
- package/dist/types/plugins/tableTools.d.ts +3 -0
- package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
- package/dist/types/plugins/unlink.d.ts +3 -0
- package/dist/types/plugins/wordCount.d.ts +3 -0
- package/dist/types/types.d.ts +226 -0
- package/package.json +66 -0
- package/src/core/Editor.ts +460 -0
- package/src/core/PluginManager.ts +140 -0
- package/src/core/helpers.ts +209 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +87 -0
- package/src/plugins/align.ts +72 -0
- package/src/plugins/autoLinkify.ts +34 -0
- package/src/plugins/autosave.ts +69 -0
- package/src/plugins/block.ts +32 -0
- package/src/plugins/caseTransform.ts +54 -0
- package/src/plugins/codeBlock.ts +93 -0
- package/src/plugins/colors.ts +68 -0
- package/src/plugins/darkMode.ts +123 -0
- package/src/plugins/direction.ts +30 -0
- package/src/plugins/dragDrop.ts +68 -0
- package/src/plugins/emoji.ts +188 -0
- package/src/plugins/emojiAutocomplete.ts +183 -0
- package/src/plugins/findReplace.ts +229 -0
- package/src/plugins/floatingToolbar.ts +258 -0
- package/src/plugins/fontFamily.ts +41 -0
- package/src/plugins/fontSize.ts +32 -0
- package/src/plugins/fullscreen.ts +36 -0
- package/src/plugins/history.ts +14 -0
- package/src/plugins/hr.ts +118 -0
- package/src/plugins/iframe.ts +88 -0
- package/src/plugins/image.ts +107 -0
- package/src/plugins/imageProps.ts +119 -0
- package/src/plugins/imageTools.ts +344 -0
- package/src/plugins/indent.ts +29 -0
- package/src/plugins/index.ts +101 -0
- package/src/plugins/inline.ts +17 -0
- package/src/plugins/inlineCode.ts +21 -0
- package/src/plugins/keyboardShortcuts.ts +92 -0
- package/src/plugins/lineHeight.ts +40 -0
- package/src/plugins/link.ts +344 -0
- package/src/plugins/linkTooltip.ts +63 -0
- package/src/plugins/list.ts +141 -0
- package/src/plugins/markdown.ts +61 -0
- package/src/plugins/mediaEmbed.ts +44 -0
- package/src/plugins/pasteImage.ts +61 -0
- package/src/plugins/pastePlain.ts +43 -0
- package/src/plugins/pre.ts +11 -0
- package/src/plugins/readOnly.ts +46 -0
- package/src/plugins/shortcutCustomizer.ts +125 -0
- package/src/plugins/shortcutsHelp.ts +51 -0
- package/src/plugins/source.ts +77 -0
- package/src/plugins/specialChars.ts +64 -0
- package/src/plugins/statusBar.ts +85 -0
- package/src/plugins/subSuper.ts +20 -0
- package/src/plugins/table.ts +166 -0
- package/src/plugins/tableBg.ts +11 -0
- package/src/plugins/tableTools.ts +475 -0
- package/src/plugins/toolbarCollapse.ts +14 -0
- package/src/plugins/unlink.ts +29 -0
- package/src/plugins/wordCount.ts +34 -0
- package/src/styles/base.css +258 -0
- package/src/styles/editor.css +309 -0
- package/src/styles/index.css +6 -0
- 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
327
|
+
var a = '<a href="' + url.replace(/"/g,'"') + '"' +
|
|
328
|
+
(blank ? ' target="_blank"' : '') +
|
|
329
|
+
(relFinal ? ' rel="' + relFinal.replace(/"/g,'"') + '"' : '') +
|
|
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;
|