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,229 @@
|
|
|
1
|
+
import type { Plugin, PluginContext, TeEditorElement } from '../types';
|
|
2
|
+
|
|
3
|
+
const findReplace: Plugin = {
|
|
4
|
+
name: 'findReplace',
|
|
5
|
+
order: 31,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-fr-popup {' +
|
|
8
|
+
' position: absolute; z-index: 3000;' +
|
|
9
|
+
' background: #fff; border: 1px solid #dee2e6;' +
|
|
10
|
+
' border-radius: .5rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
|
|
11
|
+
' padding: .75rem; width: 320px;' +
|
|
12
|
+
'}' +
|
|
13
|
+
'.te-fr-popup .te-fr-row { display: flex; gap: .5rem; margin-bottom: .5rem; }' +
|
|
14
|
+
'.te-fr-popup .te-fr-row:last-child { margin-bottom: 0; }' +
|
|
15
|
+
'.te-fr-popup input { flex: 1; }' +
|
|
16
|
+
'.te-fr-popup .btn { white-space: nowrap; }' +
|
|
17
|
+
'.te-fr-count { font-size: .8rem; color: #6c757d; padding: .25rem .5rem; }' +
|
|
18
|
+
'.te-fr-match { background: #fff3cd; outline: 2px solid #ffc107; }' +
|
|
19
|
+
'.te-fr-active { background: #f0ad4e; outline: 2px solid #e68600; }',
|
|
20
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Find & Replace" data-cmd="findReplace"><i class="ti ti-search"></i></button>',
|
|
21
|
+
init(ctx: PluginContext): void {
|
|
22
|
+
if(!ctx.features.findReplace) return;
|
|
23
|
+
var editor = ctx.editor as TeEditorElement;
|
|
24
|
+
var popup = document.createElement('div');
|
|
25
|
+
popup.className = 'te-fr-popup';
|
|
26
|
+
popup.style.display = 'none';
|
|
27
|
+
popup.innerHTML = '' +
|
|
28
|
+
'<div class="te-fr-row">' +
|
|
29
|
+
'<input type="text" class="form-control form-control-sm te-fr-find" placeholder="Find..." />' +
|
|
30
|
+
'<button type="button" class="btn btn-sm btn-outline-primary te-fr-prev" title="Previous">↑</button>' +
|
|
31
|
+
'<button type="button" class="btn btn-sm btn-primary te-fr-next" title="Next">↓</button>' +
|
|
32
|
+
'</div>' +
|
|
33
|
+
'<div class="te-fr-row">' +
|
|
34
|
+
'<input type="text" class="form-control form-control-sm te-fr-replace" placeholder="Replace..." />' +
|
|
35
|
+
'<button type="button" class="btn btn-sm btn-outline-secondary te-fr-replace-btn">Replace</button>' +
|
|
36
|
+
'<button type="button" class="btn btn-sm btn-outline-secondary te-fr-replace-all">All</button>' +
|
|
37
|
+
'</div>' +
|
|
38
|
+
'<div class="te-fr-row">' +
|
|
39
|
+
'<span class="te-fr-count"></span>' +
|
|
40
|
+
'<button type="button" class="btn btn-sm btn-outline-secondary te-fr-close ms-auto" title="Close">×</button>' +
|
|
41
|
+
'</div>';
|
|
42
|
+
document.body.appendChild(popup);
|
|
43
|
+
|
|
44
|
+
var findInp = popup.querySelector('.te-fr-find') as HTMLInputElement;
|
|
45
|
+
var replaceInp = popup.querySelector('.te-fr-replace') as HTMLInputElement;
|
|
46
|
+
var countEl = popup.querySelector('.te-fr-count') as HTMLElement;
|
|
47
|
+
|
|
48
|
+
function escapeRe(s: string){
|
|
49
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function clearHighlights(){
|
|
53
|
+
editor.querySelectorAll('.te-fr-match, .te-fr-active').forEach(function(el){
|
|
54
|
+
var parent = el.parentNode;
|
|
55
|
+
if(!parent) return;
|
|
56
|
+
while(el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
57
|
+
parent.removeChild(el);
|
|
58
|
+
});
|
|
59
|
+
editor.normalize();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function selectMatch(el: Element | null){
|
|
63
|
+
editor.querySelectorAll('.te-fr-active').forEach(function(a){ a.classList.remove('te-fr-active'); a.classList.add('te-fr-match'); });
|
|
64
|
+
if(!el) return;
|
|
65
|
+
el.classList.remove('te-fr-match');
|
|
66
|
+
el.classList.add('te-fr-active');
|
|
67
|
+
(el as HTMLElement).scrollIntoView({ block: 'nearest' });
|
|
68
|
+
var range = document.createRange();
|
|
69
|
+
range.selectNodeContents(el);
|
|
70
|
+
var sel = window.getSelection()!;
|
|
71
|
+
sel.removeAllRanges();
|
|
72
|
+
sel.addRange(range);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var highlights: Element[] = [];
|
|
76
|
+
var currentIdx = -1;
|
|
77
|
+
|
|
78
|
+
function doFind(){
|
|
79
|
+
clearHighlights();
|
|
80
|
+
var text = findInp.value.trim();
|
|
81
|
+
if(!text) return;
|
|
82
|
+
var nodes: Node[] = [];
|
|
83
|
+
var walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null);
|
|
84
|
+
while(walker.nextNode()) nodes.push(walker.currentNode);
|
|
85
|
+
highlights = [];
|
|
86
|
+
nodes.forEach(function(node){
|
|
87
|
+
var nodeText = node.textContent!;
|
|
88
|
+
var re2 = new RegExp(escapeRe(text), 'gi');
|
|
89
|
+
var matches: { idx: number; len: number }[] = [];
|
|
90
|
+
var match: RegExpExecArray | null;
|
|
91
|
+
while((match = re2.exec(nodeText)) !== null){
|
|
92
|
+
matches.push({ idx: match.index, len: match[0].length });
|
|
93
|
+
}
|
|
94
|
+
// Process matches in reverse order to avoid index drift
|
|
95
|
+
for(var i = matches.length - 1; i >= 0; i--){
|
|
96
|
+
var m = matches[i];
|
|
97
|
+
var span = document.createElement('span');
|
|
98
|
+
span.className = 'te-fr-match';
|
|
99
|
+
span.textContent = nodeText.substring(m.idx, m.idx + m.len);
|
|
100
|
+
var after = (node as Text).splitText(m.idx + m.len);
|
|
101
|
+
var mid = (node as Text).splitText(m.idx);
|
|
102
|
+
mid.parentNode!.replaceChild(span, mid);
|
|
103
|
+
highlights.push(span);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
countEl.textContent = highlights.length + ' matches';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function goNext(){
|
|
110
|
+
if(!highlights.length){ doFind(); if(!highlights.length) return; currentIdx = 0; }
|
|
111
|
+
else currentIdx = (currentIdx + 1) % highlights.length;
|
|
112
|
+
selectMatch(highlights[currentIdx]);
|
|
113
|
+
}
|
|
114
|
+
function goPrev(){
|
|
115
|
+
if(!highlights.length){ doFind(); if(!highlights.length) return; currentIdx = highlights.length - 1; }
|
|
116
|
+
else currentIdx = (currentIdx - 1 + highlights.length) % highlights.length;
|
|
117
|
+
selectMatch(highlights[currentIdx]);
|
|
118
|
+
}
|
|
119
|
+
function doFindAndSelect(dir: number){
|
|
120
|
+
doFind();
|
|
121
|
+
if(!highlights.length) return;
|
|
122
|
+
currentIdx = dir === 1 ? 0 : highlights.length - 1;
|
|
123
|
+
selectMatch(highlights[currentIdx]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
popup.querySelector('.te-fr-next')!.addEventListener('click', goNext);
|
|
127
|
+
popup.querySelector('.te-fr-prev')!.addEventListener('click', goPrev);
|
|
128
|
+
function closePopup(){
|
|
129
|
+
popup.style.display = 'none';
|
|
130
|
+
clearHighlights();
|
|
131
|
+
editor.focus();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
findInp.addEventListener('input', function(){ doFind(); });
|
|
135
|
+
findInp.addEventListener('keydown', function(e: KeyboardEvent){
|
|
136
|
+
if(e.key === 'Enter') { e.preventDefault(); doFindAndSelect(1); }
|
|
137
|
+
if(e.key === 'Escape') { e.preventDefault(); closePopup(); }
|
|
138
|
+
});
|
|
139
|
+
replaceInp.addEventListener('keydown', function(e: KeyboardEvent){
|
|
140
|
+
if(e.key === 'Enter') { e.preventDefault(); (popup.querySelector('.te-fr-replace-btn') as HTMLButtonElement).click(); }
|
|
141
|
+
if(e.key === 'Escape') { e.preventDefault(); closePopup(); }
|
|
142
|
+
});
|
|
143
|
+
function saveUndoState(){
|
|
144
|
+
clearHighlights();
|
|
145
|
+
if(!editor._tableHist) editor._tableHist = {undo:[], redo:[]};
|
|
146
|
+
editor._tableHist.undo.push(editor.innerHTML);
|
|
147
|
+
if(editor._tableHist.undo.length > 50) editor._tableHist.undo.shift();
|
|
148
|
+
editor._tableHist.redo = [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateUndoRedoBtns(){
|
|
152
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="undo"]') as HTMLButtonElement | null;
|
|
153
|
+
var rbtn = ctx.wrapper.querySelector('[data-cmd="redo"]') as HTMLButtonElement | null;
|
|
154
|
+
var h = editor._tableHist;
|
|
155
|
+
if(btn) btn.toggleAttribute('disabled', !h || !h.undo.length);
|
|
156
|
+
if(rbtn) rbtn.toggleAttribute('disabled', !h || !h.redo.length);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
popup.querySelector('.te-fr-replace-btn')!.addEventListener('click', function(){
|
|
160
|
+
if(!highlights.length) return;
|
|
161
|
+
var active = editor.querySelector('.te-fr-active');
|
|
162
|
+
if(!active) return;
|
|
163
|
+
saveUndoState();
|
|
164
|
+
var text = replaceInp.value;
|
|
165
|
+
var parent = active.parentNode;
|
|
166
|
+
if(parent) parent.replaceChild(document.createTextNode(text), active);
|
|
167
|
+
// Update highlights
|
|
168
|
+
highlights.splice(currentIdx, 1);
|
|
169
|
+
countEl.textContent = highlights.length + ' matches';
|
|
170
|
+
if(highlights.length){
|
|
171
|
+
currentIdx = Math.min(currentIdx, highlights.length - 1);
|
|
172
|
+
selectMatch(highlights[currentIdx]);
|
|
173
|
+
} else {
|
|
174
|
+
countEl.textContent = '0 matches';
|
|
175
|
+
}
|
|
176
|
+
updateUndoRedoBtns();
|
|
177
|
+
});
|
|
178
|
+
popup.querySelector('.te-fr-replace-all')!.addEventListener('click', function(){
|
|
179
|
+
var findText = findInp.value.trim();
|
|
180
|
+
var replaceText = replaceInp.value;
|
|
181
|
+
if(!findText) return;
|
|
182
|
+
saveUndoState();
|
|
183
|
+
clearHighlights();
|
|
184
|
+
var re = new RegExp(escapeRe(findText), 'gi');
|
|
185
|
+
var walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null);
|
|
186
|
+
var nodes: Node[] = [];
|
|
187
|
+
while(walker.nextNode()) nodes.push(walker.currentNode);
|
|
188
|
+
var count = 0;
|
|
189
|
+
nodes.forEach(function(node){
|
|
190
|
+
var text = node.textContent!;
|
|
191
|
+
if(!re.test(text)) return;
|
|
192
|
+
re.lastIndex = 0;
|
|
193
|
+
node.textContent = text.replace(re, function(){ count++; return replaceText; });
|
|
194
|
+
});
|
|
195
|
+
countEl.textContent = count + ' replaced';
|
|
196
|
+
highlights = [];
|
|
197
|
+
currentIdx = -1;
|
|
198
|
+
updateUndoRedoBtns();
|
|
199
|
+
});
|
|
200
|
+
popup.querySelector('.te-fr-close')!.addEventListener('click', closePopup);
|
|
201
|
+
|
|
202
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="findReplace"]') as HTMLButtonElement | null;
|
|
203
|
+
if(!btn) return;
|
|
204
|
+
btn.addEventListener('click', function(e: MouseEvent){
|
|
205
|
+
var rect = (btn as HTMLButtonElement).getBoundingClientRect();
|
|
206
|
+
popup.style.left = (window.scrollX + rect.left) + 'px';
|
|
207
|
+
popup.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
208
|
+
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
|
|
209
|
+
if(popup.style.display !== 'none'){
|
|
210
|
+
findInp.value = '';
|
|
211
|
+
replaceInp.value = '';
|
|
212
|
+
countEl.textContent = '';
|
|
213
|
+
clearHighlights();
|
|
214
|
+
highlights = [];
|
|
215
|
+
currentIdx = -1;
|
|
216
|
+
findInp.focus();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
document.addEventListener('click', function(e: MouseEvent){
|
|
220
|
+
if(popup.style.display === 'none') return;
|
|
221
|
+
if(!popup.contains(e.target as Node) && e.target !== btn){
|
|
222
|
+
popup.style.display = 'none';
|
|
223
|
+
clearHighlights();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export default findReplace;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const floatingToolbar: Plugin = {
|
|
4
|
+
name: 'floatingToolbar',
|
|
5
|
+
order: 58,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-float-toolbar {' +
|
|
8
|
+
' position: fixed; display: none; z-index: 3100;' +
|
|
9
|
+
' background: #fff; border: 1px solid #dee2e6;' +
|
|
10
|
+
' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
|
|
11
|
+
' padding: .2rem; gap: 2px; align-items: center;' +
|
|
12
|
+
' white-space: nowrap;' +
|
|
13
|
+
'}' +
|
|
14
|
+
'.te-float-toolbar .btn {' +
|
|
15
|
+
' padding: .2rem .4rem !important; font-size: .75rem !important; line-height: 1 !important;' +
|
|
16
|
+
'}' +
|
|
17
|
+
'.te-float-toolbar .btn i,' +
|
|
18
|
+
'.te-float-toolbar .btn svg {' +
|
|
19
|
+
' width: 13px !important; height: 13px !important;' +
|
|
20
|
+
' pointer-events: none;' +
|
|
21
|
+
'}' +
|
|
22
|
+
'.te-float-link-input {' +
|
|
23
|
+
' position: fixed; display: none; z-index: 3101;' +
|
|
24
|
+
' background: #fff; border: 1px solid #dee2e6;' +
|
|
25
|
+
' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
|
|
26
|
+
' padding: .35rem; gap: .25rem; align-items: center;' +
|
|
27
|
+
' white-space: nowrap;' +
|
|
28
|
+
'}' +
|
|
29
|
+
'.te-float-link-input input {' +
|
|
30
|
+
' width: 220px; padding: .2rem .4rem; font-size: .8rem;' +
|
|
31
|
+
' border: 1px solid #dee2e6; border-radius: .25rem; outline: none;' +
|
|
32
|
+
'}' +
|
|
33
|
+
'.te-float-link-input input:focus {' +
|
|
34
|
+
' border-color: #86b7fe; box-shadow: 0 0 0 2px rgba(13,110,253,.15);' +
|
|
35
|
+
'}' +
|
|
36
|
+
'.te-float-link-input .btn {' +
|
|
37
|
+
' padding: .2rem .5rem !important; font-size: .75rem !important; line-height: 1 !important;' +
|
|
38
|
+
'}',
|
|
39
|
+
init(ctx: PluginContext): void {
|
|
40
|
+
if(!ctx.features.floatingToolbar) return;
|
|
41
|
+
|
|
42
|
+
var editor = ctx.editor;
|
|
43
|
+
var tools = document.createElement('div');
|
|
44
|
+
tools.className = 'te-float-toolbar';
|
|
45
|
+
tools.innerHTML = '' +
|
|
46
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float="bold" title="Bold"><i class="ti ti-bold"></i></button>' +
|
|
47
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float="italic" title="Italic"><i class="ti ti-italic"></i></button>' +
|
|
48
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float="underline" title="Underline"><i class="ti ti-underline"></i></button>' +
|
|
49
|
+
'<span class="te-divider-v" style="display:inline-block;width:1px;height:14px;background:#dee2e6;margin:0 2px;vertical-align:middle"></span>' +
|
|
50
|
+
'<button type="button" class="btn btn-sm btn-light te-float-fore" title="Text color" style="position:relative;padding:.15rem .35rem!important;min-width:22px;text-align:center"><span class="te-float-cp" style="display:block;font-size:.8rem;line-height:1">A<span style="display:block;height:2px;border-radius:1px;margin-top:1px;background:#000"></span></span><input type="color" class="te-float-fore-input" value="#000000" style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;padding:0"></button>' +
|
|
51
|
+
'<button type="button" class="btn btn-sm btn-light te-float-back" title="Background color" style="position:relative;padding:.15rem .35rem!important;min-width:22px;text-align:center"><span class="te-float-cp" style="display:block;font-size:.8rem;line-height:1">A<span style="display:block;height:2px;border-radius:1px;margin-top:1px;background:#ffff00"></span></span><input type="color" class="te-float-back-input" value="#ffff00" style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;padding:0"></button>' +
|
|
52
|
+
'<span class="te-divider-v" style="display:inline-block;width:1px;height:14px;background:#dee2e6;margin:0 2px;vertical-align:middle"></span>' +
|
|
53
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float="link" title="Insert link"><i class="ti ti-link"></i></button>' +
|
|
54
|
+
'<span class="te-divider-v" style="display:inline-block;width:1px;height:14px;background:#dee2e6;margin:0 2px;vertical-align:middle"></span>' +
|
|
55
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float="removeFormat" title="Clear formatting"><i class="ti ti-clear-formatting"></i></button>';
|
|
56
|
+
document.body.appendChild(tools);
|
|
57
|
+
|
|
58
|
+
var linkInput = document.createElement('div');
|
|
59
|
+
linkInput.className = 'te-float-link-input';
|
|
60
|
+
linkInput.innerHTML = '' +
|
|
61
|
+
'<input type="url" class="te-float-link-url" placeholder="https://example.com" />' +
|
|
62
|
+
'<button type="button" class="btn btn-sm btn-primary" data-te-float-link="apply">Apply</button>' +
|
|
63
|
+
'<button type="button" class="btn btn-sm btn-light" data-te-float-link="cancel">Cancel</button>';
|
|
64
|
+
document.body.appendChild(linkInput);
|
|
65
|
+
|
|
66
|
+
var foreInput = tools.querySelector('.te-float-fore-input') as HTMLInputElement | null;
|
|
67
|
+
var backInput = tools.querySelector('.te-float-back-input') as HTMLInputElement | null;
|
|
68
|
+
|
|
69
|
+
if(foreInput){
|
|
70
|
+
foreInput.addEventListener('change', function(){
|
|
71
|
+
editor.focus();
|
|
72
|
+
restoreRange();
|
|
73
|
+
document.execCommand('foreColor', false, (foreInput as HTMLInputElement).value);
|
|
74
|
+
var preview = tools.querySelector('.te-float-fore .te-float-cp') as HTMLElement | null;
|
|
75
|
+
if(preview) preview.style.color = (foreInput as HTMLInputElement).value;
|
|
76
|
+
hideTools();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if(backInput){
|
|
81
|
+
backInput.addEventListener('change', function(){
|
|
82
|
+
editor.focus();
|
|
83
|
+
restoreRange();
|
|
84
|
+
try{ document.execCommand('hiliteColor', false, (backInput as HTMLInputElement).value); }
|
|
85
|
+
catch(e){ document.execCommand('backColor', false, (backInput as HTMLInputElement).value); }
|
|
86
|
+
var bar = tools.querySelector('.te-float-back .te-float-cp span:last-child') as HTMLElement | null;
|
|
87
|
+
if(bar) bar.style.background = (backInput as HTMLInputElement).value;
|
|
88
|
+
hideTools();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var hideTimer: ReturnType<typeof setTimeout> | null = null;
|
|
93
|
+
var isVisible = false;
|
|
94
|
+
var savedRange: Range | null = null;
|
|
95
|
+
|
|
96
|
+
function saveRange(){
|
|
97
|
+
var sel = window.getSelection();
|
|
98
|
+
if(sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
|
|
99
|
+
else savedRange = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function restoreRange(){
|
|
103
|
+
if(!savedRange) return;
|
|
104
|
+
var sel = window.getSelection()!;
|
|
105
|
+
sel.removeAllRanges();
|
|
106
|
+
sel.addRange(savedRange);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function posTools(x: number, y: number, rect: DOMRect | null){
|
|
110
|
+
var tw = tools.offsetWidth || 100;
|
|
111
|
+
var th = tools.offsetHeight || 30;
|
|
112
|
+
var er = editor.getBoundingClientRect();
|
|
113
|
+
var left = x - tw / 2;
|
|
114
|
+
if(left < er.left) left = er.left;
|
|
115
|
+
if(left + tw > er.right) left = er.right - tw;
|
|
116
|
+
var spaceAbove = rect ? rect.top - er.top : y - er.top;
|
|
117
|
+
var spaceBelow = rect ? er.bottom - rect.bottom : er.bottom - y;
|
|
118
|
+
var top;
|
|
119
|
+
if(spaceAbove >= th + 8){
|
|
120
|
+
top = (rect ? rect.top : y) - th - 8;
|
|
121
|
+
} else if(spaceBelow >= th + 8){
|
|
122
|
+
top = (rect ? rect.bottom : y) + 8;
|
|
123
|
+
} else {
|
|
124
|
+
top = spaceAbove > spaceBelow ? er.top + 2 : er.bottom - th - 2;
|
|
125
|
+
}
|
|
126
|
+
tools.style.left = left + 'px';
|
|
127
|
+
tools.style.top = top + 'px';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hideTools(){
|
|
131
|
+
isVisible = false;
|
|
132
|
+
linkInput.style.display = 'none';
|
|
133
|
+
tools.style.display = 'none';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function showTools(x: number, y: number, rect: DOMRect | null){
|
|
137
|
+
isVisible = true;
|
|
138
|
+
tools.style.display = 'flex';
|
|
139
|
+
posTools(x, y, rect);
|
|
140
|
+
saveRange();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function showLinkInput(){
|
|
144
|
+
saveRange();
|
|
145
|
+
var er = editor.getBoundingClientRect();
|
|
146
|
+
var toolsRect = tools.getBoundingClientRect();
|
|
147
|
+
linkInput.style.display = 'flex';
|
|
148
|
+
linkInput.style.visibility = 'hidden';
|
|
149
|
+
var liw = linkInput.offsetWidth || 320;
|
|
150
|
+
var lih = linkInput.offsetHeight || 32;
|
|
151
|
+
var left = toolsRect.left + toolsRect.width / 2 - liw / 2;
|
|
152
|
+
if(left < er.left) left = er.left;
|
|
153
|
+
if(left + liw > er.right) left = er.right - liw;
|
|
154
|
+
linkInput.style.left = left + 'px';
|
|
155
|
+
var spaceBelow = window.innerHeight - toolsRect.bottom;
|
|
156
|
+
linkInput.style.top = ((spaceBelow >= lih + 4) ? (toolsRect.bottom + 4) : (toolsRect.top - lih - 4)) + 'px';
|
|
157
|
+
linkInput.style.visibility = '';
|
|
158
|
+
(linkInput.querySelector('.te-float-link-url') as HTMLInputElement).value = '';
|
|
159
|
+
(linkInput.querySelector('.te-float-link-url') as HTMLInputElement).focus();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
editor.addEventListener('mouseup', function(e: MouseEvent){
|
|
163
|
+
if(editor.contentEditable !== 'true') return;
|
|
164
|
+
if(hideTimer) clearTimeout(hideTimer);
|
|
165
|
+
var sel = window.getSelection();
|
|
166
|
+
if(!sel || sel.isCollapsed || !sel.toString().trim()){
|
|
167
|
+
hideTools();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if(!editor.contains(sel.anchorNode) || !editor.contains(sel.focusNode)){
|
|
171
|
+
hideTools();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
var range = sel.getRangeAt(0);
|
|
175
|
+
var rect = range.getBoundingClientRect();
|
|
176
|
+
var x = rect.left + rect.width / 2;
|
|
177
|
+
showTools(x, 0, rect);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
editor.addEventListener('mousedown', function(){
|
|
181
|
+
if(hideTimer) clearTimeout(hideTimer);
|
|
182
|
+
hideTimer = setTimeout(function(){ hideTools(); }, 200);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
editor.addEventListener('keydown', function(){
|
|
186
|
+
hideTools();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
editor.addEventListener('scroll', function(){
|
|
190
|
+
if(isVisible) hideTools();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
tools.addEventListener('mousedown', function(e: MouseEvent){
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
var actionBtn = (e.target as HTMLElement).closest('[data-te-float]');
|
|
196
|
+
if(!actionBtn) return;
|
|
197
|
+
var action = actionBtn.getAttribute('data-te-float');
|
|
198
|
+
if(!action) return;
|
|
199
|
+
|
|
200
|
+
if(action === 'link'){
|
|
201
|
+
showLinkInput();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
editor.focus();
|
|
206
|
+
var sel = window.getSelection();
|
|
207
|
+
if(!sel || sel.rangeCount === 0) return;
|
|
208
|
+
document.execCommand(action, false, undefined);
|
|
209
|
+
hideTools();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
linkInput.addEventListener('mousedown', function(e: MouseEvent){
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
var actionBtn = (e.target as HTMLElement).closest('[data-te-float-link]');
|
|
215
|
+
if(!actionBtn) return;
|
|
216
|
+
var action = actionBtn.getAttribute('data-te-float-link');
|
|
217
|
+
|
|
218
|
+
if(action === 'cancel'){
|
|
219
|
+
hideTools();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if(action === 'apply'){
|
|
224
|
+
var url = (linkInput.querySelector('.te-float-link-url') as HTMLInputElement).value.trim();
|
|
225
|
+
if(!url) return;
|
|
226
|
+
editor.focus();
|
|
227
|
+
restoreRange();
|
|
228
|
+
if (!/^https?:\/\//i.test(url) && !/^mailto:/i.test(url) && !/^tel:/i.test(url)) url = 'https://' + url;
|
|
229
|
+
document.execCommand('createLink', false, url);
|
|
230
|
+
hideTools();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
linkInput.addEventListener('keydown', function(e: KeyboardEvent){
|
|
235
|
+
if(e.key === 'Enter'){
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
var url = (linkInput.querySelector('.te-float-link-url') as HTMLInputElement).value.trim();
|
|
238
|
+
if(!url) return;
|
|
239
|
+
editor.focus();
|
|
240
|
+
restoreRange();
|
|
241
|
+
if (!/^https?:\/\//i.test(url) && !/^mailto:/i.test(url) && !/^tel:/i.test(url)) url = 'https://' + url;
|
|
242
|
+
document.execCommand('createLink', false, url);
|
|
243
|
+
hideTools();
|
|
244
|
+
}
|
|
245
|
+
if(e.key === 'Escape'){
|
|
246
|
+
hideTools();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
document.addEventListener('mousedown', function(e: MouseEvent){
|
|
251
|
+
if(isVisible && !tools.contains(e.target as Node) && !linkInput.contains(e.target as Node) && e.target !== editor && !editor.contains(e.target as Node)){
|
|
252
|
+
hideTools();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export default floatingToolbar;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var FONTS = [
|
|
4
|
+
'Arial',
|
|
5
|
+
'Arial Black',
|
|
6
|
+
'Comic Sans MS',
|
|
7
|
+
'Courier New',
|
|
8
|
+
'Georgia',
|
|
9
|
+
'Impact',
|
|
10
|
+
'Lucida Console',
|
|
11
|
+
'Tahoma',
|
|
12
|
+
'Times New Roman',
|
|
13
|
+
'Trebuchet MS',
|
|
14
|
+
'Verdana'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const fontFamily: Plugin = {
|
|
18
|
+
name: 'fontFamily',
|
|
19
|
+
order: 28,
|
|
20
|
+
css: '' +
|
|
21
|
+
'.te-fontfamily-select { width: auto; }',
|
|
22
|
+
toolbarHTML: '' +
|
|
23
|
+
'<span class="te-dropdown-icon"><i class="ti ti-typography"></i></span>' +
|
|
24
|
+
'<select class="te-fontfamily form-select form-select-sm te-fontfamily-select" title="Font family">' +
|
|
25
|
+
' <option value="">Normal</option>' +
|
|
26
|
+
FONTS.map(function(f){ return '<option value="' + f + '">' + f + '</option>'; }).join('') +
|
|
27
|
+
'</select>',
|
|
28
|
+
init(ctx: PluginContext): void {
|
|
29
|
+
if(!ctx.features.fontFamily) return;
|
|
30
|
+
var sel = ctx.wrapper.querySelector('.te-fontfamily') as HTMLSelectElement | null;
|
|
31
|
+
if(!sel) return;
|
|
32
|
+
sel.addEventListener('change', function(){
|
|
33
|
+
var v = sel!.value;
|
|
34
|
+
if(!v) return;
|
|
35
|
+
document.execCommand('fontName', false, v);
|
|
36
|
+
ctx.editor.focus();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default fontFamily;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
import * as H from '../core/helpers';
|
|
3
|
+
|
|
4
|
+
var FONT_SIZES = ['8', '9', '10', '11', '12', '13', '14', '16', '18', '20', '22', '24', '26', '28', '36', '48', '72'];
|
|
5
|
+
|
|
6
|
+
const fontSize: Plugin = {
|
|
7
|
+
name: 'fontSize',
|
|
8
|
+
order: 27,
|
|
9
|
+
css: '' +
|
|
10
|
+
'.te-fontsize-select { width: auto; }',
|
|
11
|
+
toolbarHTML: '' +
|
|
12
|
+
'<span class="te-dropdown-icon te-dropdown-icon-text">Aa</span>' +
|
|
13
|
+
'<select class="te-fontsize form-select form-select-sm te-fontsize-select" title="Font size">' +
|
|
14
|
+
' <option value="">Normal</option>' +
|
|
15
|
+
FONT_SIZES.map(function (s) { return '<option value="' + s + '">' + s + '</option>'; }).join('') +
|
|
16
|
+
'</select>',
|
|
17
|
+
init(ctx: PluginContext): void {
|
|
18
|
+
if (!ctx.features.fontSize) return;
|
|
19
|
+
var sel = ctx.wrapper.querySelector('.te-fontsize') as HTMLSelectElement | null;
|
|
20
|
+
if (!sel) return;
|
|
21
|
+
sel.addEventListener('change', function () {
|
|
22
|
+
var v = sel!.value;
|
|
23
|
+
if (!v) return;
|
|
24
|
+
var txt = H.escapeHtml(window.getSelection()!.toString() || 'text');
|
|
25
|
+
var html = '<span style="font-size:' + v + 'px">' + txt + '</span>';
|
|
26
|
+
document.execCommand('insertHTML', false, html);
|
|
27
|
+
ctx.editor.focus();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default fontSize;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const fullscreen: Plugin = {
|
|
4
|
+
name: 'fullscreen',
|
|
5
|
+
order: 75,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-fullscreen {' +
|
|
8
|
+
' position: fixed; inset: 0; z-index: 9999;' +
|
|
9
|
+
'}' +
|
|
10
|
+
'.te-fullscreen .te-container {' +
|
|
11
|
+
' max-width: none; display: flex; flex-direction: column; height: 100%;' +
|
|
12
|
+
'}' +
|
|
13
|
+
'.te-fullscreen .te-container .te-editor {' +
|
|
14
|
+
' flex: 1; height: auto !important; min-height: 300px;' +
|
|
15
|
+
'}' +
|
|
16
|
+
'.te-fullscreen .te-container .te-toolbar {' +
|
|
17
|
+
' border-radius: 0;' +
|
|
18
|
+
'}' +
|
|
19
|
+
'.te-fullscreen .te-btn-fullscreen {' +
|
|
20
|
+
' background: #e7f1ff; border-color: #b6d4fe;' +
|
|
21
|
+
'}',
|
|
22
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light te-btn-fullscreen" title="Fullscreen" data-cmd="fullscreen"><i class="ti ti-maximize"></i></button>',
|
|
23
|
+
init(ctx: PluginContext): void {
|
|
24
|
+
if (!ctx.features.fullscreen) return;
|
|
25
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="fullscreen"]') as HTMLElement | null;
|
|
26
|
+
if (!btn) return;
|
|
27
|
+
btn.addEventListener('click', function () {
|
|
28
|
+
var isFull = ctx.wrapper.classList.toggle('te-fullscreen');
|
|
29
|
+
btn!.innerHTML = isFull ? '<i class="ti ti-minimize"></i>' : '<i class="ti ti-maximize"></i>';
|
|
30
|
+
|
|
31
|
+
ctx.editor.focus();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default fullscreen;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const history: Plugin = {
|
|
4
|
+
name: 'history',
|
|
5
|
+
order: 70,
|
|
6
|
+
toolbarHTML: '' +
|
|
7
|
+
'<button type="button" class="btn btn-sm btn-light" title="Undo" data-cmd="undo"><i class="ti ti-arrow-back-up"></i></button>' +
|
|
8
|
+
'<button type="button" class="btn btn-sm btn-light" title="Redo" data-cmd="redo"><i class="ti ti-arrow-forward-up"></i></button>' +
|
|
9
|
+
'<button type="button" class="btn btn-sm btn-light" title="Clear formatting" data-cmd="removeFormat"><i class="ti ti-trash"></i></button>',
|
|
10
|
+
init(ctx: PluginContext): void {
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default history;
|