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,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">&uarr;</button>' +
31
+ '<button type="button" class="btn btn-sm btn-primary te-fr-next" title="Next">&darr;</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">&times;</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;