@onetype/framework 2.0.52 → 2.0.54

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 (154) hide show
  1. package/addons/core/database/back/items/commands/create.js +1 -1
  2. package/addons/core/database/back/items/commands/delete.js +3 -3
  3. package/addons/core/database/back/items/commands/update.js +1 -1
  4. package/addons/float/popup/css/popup.css +2 -2
  5. package/addons/render/directives/front/functions/process.js +3 -1
  6. package/addons/render/directives/front/items/self/160-slot.js +9 -1
  7. package/addons/render/directives/front/items/self/660-form.js +19 -13
  8. package/addons/render/editor/addon.js +13 -0
  9. package/addons/render/editor/functions/block/add.js +57 -0
  10. package/addons/render/editor/functions/block/delete.js +32 -0
  11. package/addons/render/editor/functions/block/find.js +30 -0
  12. package/addons/render/editor/functions/render/block.js +114 -0
  13. package/addons/render/editor/functions/render/blocks.js +31 -0
  14. package/addons/render/editor/items/elements/editor.js +69 -0
  15. package/addons/render/editor/items/self/paragraph.js +21 -0
  16. package/addons/render/editor/styles/editor.css +252 -0
  17. package/addons/render/elements/front/functions/types/colors/badge.js +19 -0
  18. package/addons/render/elements/front/functions/types/colors/status.js +24 -0
  19. package/addons/render/elements/front/functions/types/escape.js +8 -0
  20. package/addons/render/elements/front/functions/types/format/currency.js +16 -0
  21. package/addons/render/elements/front/functions/types/format/date.js +16 -0
  22. package/addons/render/elements/front/functions/types/format/number.js +24 -0
  23. package/addons/render/elements/front/functions/types/format/timeago.js +43 -0
  24. package/addons/render/elements/front/functions/types/render/avatar.js +10 -0
  25. package/addons/render/elements/front/functions/types/render/badge.js +17 -0
  26. package/addons/render/elements/front/functions/types/render/boolean.js +8 -0
  27. package/addons/render/elements/front/functions/types/render/chip.js +6 -0
  28. package/addons/render/elements/front/functions/types/render/currency.js +7 -0
  29. package/addons/render/elements/front/functions/types/render/date.js +6 -0
  30. package/addons/render/elements/front/functions/types/render/description.js +12 -0
  31. package/addons/render/elements/front/functions/types/render/group.js +11 -0
  32. package/addons/render/elements/front/functions/types/render/icon.js +8 -0
  33. package/addons/render/elements/front/functions/types/render/image.js +9 -0
  34. package/addons/render/elements/front/functions/types/render/link.js +8 -0
  35. package/addons/render/elements/front/functions/types/render/media.js +16 -0
  36. package/addons/render/elements/front/functions/types/render/metric.js +13 -0
  37. package/addons/render/elements/front/functions/types/render/number.js +6 -0
  38. package/addons/render/elements/front/functions/types/render/progress.js +13 -0
  39. package/addons/render/elements/front/functions/types/render/status.js +12 -0
  40. package/addons/render/elements/front/functions/types/render/tag.js +6 -0
  41. package/addons/render/elements/front/functions/types/render/tags.js +11 -0
  42. package/addons/render/elements/front/functions/types/render/text.js +8 -0
  43. package/addons/render/elements/front/functions/types/render/timeago.js +6 -0
  44. package/addons/render/elements/front/functions/types/render.js +16 -0
  45. package/addons/render/elements/front/items/directives/element.js +8 -3
  46. package/addons/render/elements/front/items/self/cards/info/info.css +499 -0
  47. package/addons/render/elements/front/items/self/cards/info/info.js +224 -0
  48. package/addons/render/elements/front/items/self/cards/item/item.css +614 -0
  49. package/addons/render/elements/front/items/self/cards/item/item.js +200 -0
  50. package/addons/render/elements/front/items/self/cards/pricing/pricing.css +318 -46
  51. package/addons/render/elements/front/items/self/cards/pricing/pricing.js +81 -30
  52. package/addons/render/elements/front/items/self/cards/profile/profile.css +446 -0
  53. package/addons/render/elements/front/items/self/cards/profile/profile.js +186 -0
  54. package/addons/render/elements/front/items/self/cards/share/share.css +445 -0
  55. package/addons/render/elements/front/items/self/cards/share/share.js +316 -0
  56. package/addons/render/elements/front/items/self/cards/stat/stat.css +356 -0
  57. package/addons/render/elements/front/items/self/cards/stat/stat.js +146 -0
  58. package/addons/render/elements/front/items/self/charts/bar/bar.css +263 -0
  59. package/addons/render/elements/front/items/self/charts/bar/bar.js +156 -0
  60. package/addons/render/elements/front/items/self/charts/donut/donut.css +222 -0
  61. package/addons/render/elements/front/items/self/charts/donut/donut.js +164 -0
  62. package/addons/render/elements/front/items/self/charts/line/line.css +229 -0
  63. package/addons/render/elements/front/items/self/charts/line/line.js +249 -0
  64. package/addons/render/elements/front/items/self/charts/sparkline/sparkline.css +102 -0
  65. package/addons/render/elements/front/items/self/charts/sparkline/sparkline.js +164 -0
  66. package/addons/render/elements/front/items/self/core/builder/builder.css +71 -116
  67. package/addons/render/elements/front/items/self/core/builder/builder.js +212 -127
  68. package/addons/render/elements/front/items/self/core/repeater/repeater.css +338 -71
  69. package/addons/render/elements/front/items/self/core/repeater/repeater.js +191 -44
  70. package/addons/render/elements/front/items/self/data/filters/filters.css +541 -0
  71. package/addons/render/elements/front/items/self/data/filters/filters.js +504 -0
  72. package/addons/render/elements/front/items/self/data/table/table.css +588 -0
  73. package/addons/render/elements/front/items/self/data/table/table.js +589 -0
  74. package/addons/render/elements/front/items/self/form/button/button.css +430 -103
  75. package/addons/render/elements/front/items/self/form/button/button.js +109 -101
  76. package/addons/render/elements/front/items/self/form/checkbox/checkbox.css +356 -39
  77. package/addons/render/elements/front/items/self/form/checkbox/checkbox.js +109 -75
  78. package/addons/render/elements/front/items/self/form/color/color.css +255 -61
  79. package/addons/render/elements/front/items/self/form/color/color.js +135 -41
  80. package/addons/render/elements/front/items/self/form/date/date.css +289 -38
  81. package/addons/render/elements/front/items/self/form/date/date.js +108 -24
  82. package/addons/render/elements/front/items/self/form/editor/editor.css +447 -0
  83. package/addons/render/elements/front/items/self/form/editor/editor.js +794 -0
  84. package/addons/render/elements/front/items/self/form/field/field.css +160 -29
  85. package/addons/render/elements/front/items/self/form/field/field.js +36 -16
  86. package/addons/render/elements/front/items/self/form/input/input.css +272 -32
  87. package/addons/render/elements/front/items/self/form/input/input.js +324 -124
  88. package/addons/render/elements/front/items/self/form/radio/radio.css +310 -45
  89. package/addons/render/elements/front/items/self/form/radio/radio.js +99 -80
  90. package/addons/render/elements/front/items/self/form/rating/rating.css +234 -57
  91. package/addons/render/elements/front/items/self/form/rating/rating.js +216 -86
  92. package/addons/render/elements/front/items/self/form/section/section.css +247 -32
  93. package/addons/render/elements/front/items/self/form/section/section.js +53 -16
  94. package/addons/render/elements/front/items/self/form/select/select.css +362 -64
  95. package/addons/render/elements/front/items/self/form/select/select.js +156 -30
  96. package/addons/render/elements/front/items/self/form/slider/slider.css +331 -123
  97. package/addons/render/elements/front/items/self/form/slider/slider.js +124 -26
  98. package/addons/render/elements/front/items/self/form/tags/tags.css +328 -53
  99. package/addons/render/elements/front/items/self/form/tags/tags.js +155 -28
  100. package/addons/render/elements/front/items/self/form/textarea/textarea.css +128 -27
  101. package/addons/render/elements/front/items/self/form/textarea/textarea.js +172 -113
  102. package/addons/render/elements/front/items/self/form/toggle/toggle.css +239 -39
  103. package/addons/render/elements/front/items/self/form/toggle/toggle.js +32 -17
  104. package/addons/render/elements/front/items/self/form/transfer/transfer.css +377 -0
  105. package/addons/render/elements/front/items/self/form/transfer/transfer.js +453 -0
  106. package/addons/render/elements/front/items/self/form/upload/upload.css +408 -0
  107. package/addons/render/elements/front/items/self/form/upload/upload.js +469 -0
  108. package/addons/render/elements/front/items/self/global/accordion/accordion.css +377 -0
  109. package/addons/render/elements/front/items/self/global/accordion/accordion.js +135 -0
  110. package/addons/render/elements/front/items/self/global/code/code.css +207 -44
  111. package/addons/render/elements/front/items/self/global/code/code.js +327 -19
  112. package/addons/render/elements/front/items/self/global/gallery/gallery.css +521 -0
  113. package/addons/render/elements/front/items/self/global/gallery/gallery.js +291 -0
  114. package/addons/render/elements/front/items/self/global/heading/heading.css +151 -49
  115. package/addons/render/elements/front/items/self/global/heading/heading.js +30 -15
  116. package/addons/render/elements/front/items/self/global/markdown/markdown.css +284 -135
  117. package/addons/render/elements/front/items/self/global/markdown/markdown.js +35 -5
  118. package/addons/render/elements/front/items/self/global/menu/menu.css +311 -56
  119. package/addons/render/elements/front/items/self/global/menu/menu.js +95 -47
  120. package/addons/render/elements/front/items/self/global/notice/notice.css +263 -23
  121. package/addons/render/elements/front/items/self/global/notice/notice.js +51 -11
  122. package/addons/render/elements/front/items/self/global/parameters/parameters.css +276 -33
  123. package/addons/render/elements/front/items/self/global/parameters/parameters.js +86 -16
  124. package/addons/render/elements/front/items/self/global/tags/tags.css +215 -29
  125. package/addons/render/elements/front/items/self/global/tags/tags.js +91 -17
  126. package/addons/render/elements/front/items/self/navigation/dock/dock.css +221 -0
  127. package/addons/render/elements/front/items/self/navigation/dock/dock.js +134 -0
  128. package/addons/render/elements/front/items/self/navigation/footer/footer.css +356 -0
  129. package/addons/render/elements/front/items/self/navigation/footer/footer.js +219 -0
  130. package/addons/render/elements/front/items/self/navigation/navbar/navbar.css +736 -76
  131. package/addons/render/elements/front/items/self/navigation/navbar/navbar.js +437 -29
  132. package/addons/render/elements/front/items/self/navigation/sidebar/sidebar.css +327 -196
  133. package/addons/render/elements/front/items/self/navigation/sidebar/sidebar.js +115 -62
  134. package/addons/render/elements/front/items/self/navigation/steps/steps.css +345 -0
  135. package/addons/render/elements/front/items/self/navigation/steps/steps.js +113 -0
  136. package/addons/render/elements/front/items/self/navigation/tabs/tabs.css +507 -33
  137. package/addons/render/elements/front/items/self/navigation/tabs/tabs.js +62 -19
  138. package/addons/render/elements/front/items/self/status/code/code.css +83 -12
  139. package/addons/render/elements/front/items/self/status/code/code.js +15 -4
  140. package/addons/render/elements/front/items/self/status/empty/empty.css +95 -15
  141. package/addons/render/elements/front/items/self/status/empty/empty.js +17 -12
  142. package/addons/render/elements/front/items/self/status/error/error.css +99 -14
  143. package/addons/render/elements/front/items/self/status/error/error.js +21 -11
  144. package/addons/render/elements/front/items/self/status/loading/loading.css +85 -14
  145. package/addons/render/elements/front/items/self/status/loading/loading.js +5 -6
  146. package/addons/render/elements/front/styles/types.css +363 -0
  147. package/instructions.txt +28 -0
  148. package/lib/load.js +1 -0
  149. package/lib/styles/reset.css +89 -76
  150. package/package.json +1 -1
  151. package/addons/render/elements/front/items/self/form/editor-markdown/editor-markdown.css +0 -410
  152. package/addons/render/elements/front/items/self/form/editor-markdown/editor-markdown.js +0 -191
  153. package/addons/render/elements/front/items/self/global/faq/faq.css +0 -98
  154. package/addons/render/elements/front/items/self/global/faq/faq.js +0 -56
@@ -0,0 +1,794 @@
1
+ onetype.AddonReady('elements', (elements) =>
2
+ {
3
+ elements.ItemAdd({
4
+ id: 'form-editor',
5
+ icon: 'edit_note',
6
+ name: 'Editor',
7
+ description: 'Premium WYSIWYG editor with toolbar, floating selection bar, slash menu, paste cleanup and clean HTML output.',
8
+ category: 'Form',
9
+ author: 'OneType',
10
+ config: {
11
+ value: {
12
+ type: 'string',
13
+ value: ''
14
+ },
15
+ placeholder: {
16
+ type: 'string',
17
+ value: 'Start writing…'
18
+ },
19
+ name: {
20
+ type: 'string',
21
+ value: ''
22
+ },
23
+ variant: {
24
+ type: 'array',
25
+ value: ['bg-1', 'border', 'size-m', 'floating', 'slash'],
26
+ options: ['bg-1', 'bg-2', 'bg-3', 'bg-4', 'border', 'compact', 'no-toolbar', 'floating', 'slash', 'size-s', 'size-m', 'size-l']
27
+ },
28
+ _change: {
29
+ type: 'function'
30
+ }
31
+ },
32
+ render: function()
33
+ {
34
+ this.hasToolbar = !this.variant.includes('no-toolbar');
35
+ this.hasFloating = this.variant.includes('floating');
36
+ this.hasSlash = this.variant.includes('slash');
37
+
38
+ this.tools = [
39
+ { cmd: 'bold', icon: 'format_bold', label: 'Bold', shortcut: '⌘B' },
40
+ { cmd: 'italic', icon: 'format_italic', label: 'Italic', shortcut: '⌘I' },
41
+ { cmd: 'underline', icon: 'format_underlined', label: 'Underline', shortcut: '⌘U' },
42
+ { cmd: 'strikeThrough', icon: 'strikethrough_s', label: 'Strikethrough' },
43
+ { sep: true },
44
+ { cmd: 'heading1', icon: 'title', label: 'Heading 1' },
45
+ { cmd: 'heading2', icon: 'text_fields', label: 'Heading 2' },
46
+ { cmd: 'heading3', icon: 'format_size', label: 'Heading 3' },
47
+ { cmd: 'blockquote', icon: 'format_quote', label: 'Quote' },
48
+ { sep: true },
49
+ { cmd: 'insertUnorderedList', icon: 'format_list_bulleted', label: 'Bullet list' },
50
+ { cmd: 'insertOrderedList', icon: 'format_list_numbered', label: 'Numbered list' },
51
+ { sep: true },
52
+ { cmd: 'link', icon: 'link', label: 'Link', shortcut: '⌘K' },
53
+ { cmd: 'image', icon: 'image', label: 'Image' },
54
+ { cmd: 'code', icon: 'code', label: 'Inline code' },
55
+ { cmd: 'divider', icon: 'horizontal_rule', label: 'Divider' },
56
+ { sep: true },
57
+ { cmd: 'clear', icon: 'format_clear', label: 'Clear formatting' },
58
+ { cmd: 'undo', icon: 'undo', label: 'Undo' },
59
+ { cmd: 'redo', icon: 'redo', label: 'Redo' }
60
+ ];
61
+
62
+ this.floatingTools = [
63
+ { cmd: 'bold', icon: 'format_bold' },
64
+ { cmd: 'italic', icon: 'format_italic' },
65
+ { cmd: 'underline', icon: 'format_underlined' },
66
+ { cmd: 'link', icon: 'link' },
67
+ { cmd: 'heading2', icon: 'title' },
68
+ { cmd: 'blockquote', icon: 'format_quote' }
69
+ ];
70
+
71
+ this.slashItems = [
72
+ { cmd: 'heading1', icon: 'title', label: 'Heading 1', description: 'Big section heading.' },
73
+ { cmd: 'heading2', icon: 'text_fields', label: 'Heading 2', description: 'Medium section heading.' },
74
+ { cmd: 'heading3', icon: 'format_size', label: 'Heading 3', description: 'Small section heading.' },
75
+ { cmd: 'insertUnorderedList', icon: 'format_list_bulleted', label: 'Bullet list', description: 'Simple bullet list.' },
76
+ { cmd: 'insertOrderedList', icon: 'format_list_numbered', label: 'Numbered list', description: 'Ordered list with numbers.' },
77
+ { cmd: 'blockquote', icon: 'format_quote', label: 'Quote', description: 'Highlighted quote block.' },
78
+ { cmd: 'divider', icon: 'horizontal_rule', label: 'Divider', description: 'Horizontal line separator.' },
79
+ { cmd: 'image', icon: 'image', label: 'Image', description: 'Insert image from URL.' }
80
+ ];
81
+
82
+ // ---- HTML cleanup (used on paste and on output) ----
83
+
84
+ this.cleanHtml = (html) =>
85
+ {
86
+ const allowed = {
87
+ P: [], H2: [], H3: [], H4: [],
88
+ STRONG: [], B: [], EM: [], I: [], U: [], S: [], STRIKE: [], DEL: [],
89
+ A: ['href'], UL: [], OL: [], LI: [],
90
+ BLOCKQUOTE: [], PRE: [], CODE: [],
91
+ BR: [], HR: [], IMG: ['src', 'alt']
92
+ };
93
+
94
+ const build = (node, output) =>
95
+ {
96
+ Array.from(node.childNodes).forEach((child) =>
97
+ {
98
+ if(child.nodeType === 3)
99
+ {
100
+ output.appendChild(document.createTextNode(child.textContent));
101
+ return;
102
+ }
103
+
104
+ if(child.nodeType !== 1)
105
+ {
106
+ return;
107
+ }
108
+
109
+ let tag = child.tagName;
110
+
111
+ if(tag === 'H1')
112
+ {
113
+ tag = 'H2';
114
+ }
115
+
116
+ if(tag === 'B' || tag === 'STRONG')
117
+ {
118
+ const node = document.createElement('strong');
119
+ build(child, node);
120
+ output.appendChild(node);
121
+ return;
122
+ }
123
+
124
+ if(tag === 'I' || tag === 'EM')
125
+ {
126
+ const node = document.createElement('em');
127
+ build(child, node);
128
+ output.appendChild(node);
129
+ return;
130
+ }
131
+
132
+ if(tag === 'STRIKE' || tag === 'DEL')
133
+ {
134
+ const node = document.createElement('s');
135
+ build(child, node);
136
+ output.appendChild(node);
137
+ return;
138
+ }
139
+
140
+ if(tag in allowed)
141
+ {
142
+ const node = document.createElement(tag);
143
+ const attrs = allowed[tag];
144
+
145
+ attrs.forEach((attr) =>
146
+ {
147
+ const value = child.getAttribute(attr);
148
+
149
+ if(value)
150
+ {
151
+ node.setAttribute(attr, value);
152
+ }
153
+ });
154
+
155
+ build(child, node);
156
+ output.appendChild(node);
157
+ return;
158
+ }
159
+
160
+ build(child, output);
161
+ });
162
+ };
163
+
164
+ const source = document.createElement('div');
165
+ source.innerHTML = html;
166
+
167
+ const target = document.createElement('div');
168
+ build(source, target);
169
+
170
+ let result = target.innerHTML.trim();
171
+
172
+ // Strip empty paragraphs
173
+ result = result.replace(/<p>(\s|&nbsp;|<br>)*<\/p>/g, '');
174
+
175
+ return result;
176
+ };
177
+
178
+ // ---- Command execution ----
179
+
180
+ this.exec = async (cmd) =>
181
+ {
182
+ if(!this.body)
183
+ {
184
+ return;
185
+ }
186
+
187
+ this.body.focus();
188
+
189
+ const block = document.queryCommandValue('formatBlock');
190
+
191
+ if(cmd === 'heading1')
192
+ {
193
+ document.execCommand('formatBlock', false, block === 'h2' ? 'p' : 'h2');
194
+ }
195
+ else if(cmd === 'heading2')
196
+ {
197
+ document.execCommand('formatBlock', false, block === 'h3' ? 'p' : 'h3');
198
+ }
199
+ else if(cmd === 'heading3')
200
+ {
201
+ document.execCommand('formatBlock', false, block === 'h4' ? 'p' : 'h4');
202
+ }
203
+ else if(cmd === 'blockquote')
204
+ {
205
+ document.execCommand('formatBlock', false, block === 'blockquote' ? 'p' : 'blockquote');
206
+ }
207
+ else if(cmd === 'code')
208
+ {
209
+ const selection = window.getSelection();
210
+
211
+ if(selection && selection.toString())
212
+ {
213
+ const range = selection.getRangeAt(0);
214
+ const code = document.createElement('code');
215
+ code.textContent = selection.toString();
216
+ range.deleteContents();
217
+ range.insertNode(code);
218
+ range.setStartAfter(code);
219
+ range.setEndAfter(code);
220
+ selection.removeAllRanges();
221
+ selection.addRange(range);
222
+ }
223
+ }
224
+ else if(cmd === 'divider')
225
+ {
226
+ document.execCommand('insertHTML', false, '<hr>');
227
+ }
228
+ else if(cmd === 'clear')
229
+ {
230
+ document.execCommand('removeFormat');
231
+ document.execCommand('formatBlock', false, 'p');
232
+ }
233
+ else if(cmd === 'link')
234
+ {
235
+ const selection = window.getSelection();
236
+ const text = selection ? selection.toString() : '';
237
+
238
+ const url = await $ot.confirm({
239
+ type: 'default',
240
+ icon: 'link',
241
+ title: 'Insert link',
242
+ description: 'Enter the destination URL.',
243
+ input: { placeholder: 'https://', value: '' },
244
+ confirm: 'Insert',
245
+ cancel: 'Cancel'
246
+ });
247
+
248
+ if(url)
249
+ {
250
+ if(text)
251
+ {
252
+ document.execCommand('createLink', false, url);
253
+ }
254
+ else
255
+ {
256
+ document.execCommand('insertHTML', false, `<a href="${url}">${url}</a>`);
257
+ }
258
+ }
259
+ }
260
+ else if(cmd === 'image')
261
+ {
262
+ const url = await $ot.confirm({
263
+ type: 'default',
264
+ icon: 'image',
265
+ title: 'Insert image',
266
+ description: 'Enter the image URL.',
267
+ input: { placeholder: 'https://…', value: '' },
268
+ confirm: 'Insert',
269
+ cancel: 'Cancel'
270
+ });
271
+
272
+ if(url)
273
+ {
274
+ document.execCommand('insertImage', false, url);
275
+ }
276
+ }
277
+ else
278
+ {
279
+ document.execCommand(cmd, false, null);
280
+ }
281
+
282
+ this.syncButtons();
283
+ this.emit();
284
+ };
285
+
286
+ // ---- State tracking ----
287
+
288
+ this.getState = () =>
289
+ {
290
+ const state = {
291
+ bold: document.queryCommandState('bold'),
292
+ italic: document.queryCommandState('italic'),
293
+ underline: document.queryCommandState('underline'),
294
+ strikeThrough: document.queryCommandState('strikeThrough'),
295
+ insertUnorderedList: document.queryCommandState('insertUnorderedList'),
296
+ insertOrderedList: document.queryCommandState('insertOrderedList')
297
+ };
298
+
299
+ const block = document.queryCommandValue('formatBlock');
300
+ state.heading1 = block === 'h2';
301
+ state.heading2 = block === 'h3';
302
+ state.heading3 = block === 'h4';
303
+ state.blockquote = block === 'blockquote';
304
+
305
+ return state;
306
+ };
307
+
308
+ this.syncButtons = () =>
309
+ {
310
+ if(!this.Element)
311
+ {
312
+ return;
313
+ }
314
+
315
+ const state = this.getState();
316
+ const buttons = this.Element.querySelectorAll('[data-cmd]');
317
+
318
+ buttons.forEach((btn) =>
319
+ {
320
+ const cmd = btn.getAttribute('data-cmd');
321
+ btn.classList.toggle('active', !!(cmd && state[cmd]));
322
+ });
323
+ };
324
+
325
+ // ---- Emit change ----
326
+
327
+ this.emit = () =>
328
+ {
329
+ if(!this.body || !this._change)
330
+ {
331
+ return;
332
+ }
333
+
334
+ const clean = this.cleanHtml(this.body.innerHTML);
335
+
336
+ if(this.hidden)
337
+ {
338
+ this.hidden.value = clean;
339
+ }
340
+
341
+ this._change({ value: clean });
342
+ };
343
+
344
+ // ---- Floating toolbar ----
345
+
346
+ this.floatingId = null;
347
+
348
+ this.showFloating = () =>
349
+ {
350
+ if(!this.hasFloating)
351
+ {
352
+ return;
353
+ }
354
+
355
+ const selection = window.getSelection();
356
+
357
+ if(!selection || selection.isCollapsed || !selection.toString().trim())
358
+ {
359
+ this.hideFloating();
360
+ return;
361
+ }
362
+
363
+ if(!this.body || !this.body.contains(selection.anchorNode))
364
+ {
365
+ return;
366
+ }
367
+
368
+ const range = selection.getRangeAt(0);
369
+ const rect = range.getBoundingClientRect();
370
+
371
+ if(rect.width === 0 && rect.height === 0)
372
+ {
373
+ return;
374
+ }
375
+
376
+ this.hideFloating();
377
+
378
+ const anchor = document.createElement('div');
379
+ anchor.style.position = 'fixed';
380
+ anchor.style.left = rect.left + rect.width / 2 + 'px';
381
+ anchor.style.top = rect.top + 'px';
382
+ anchor.style.width = '1px';
383
+ anchor.style.height = '1px';
384
+ anchor.style.pointerEvents = 'none';
385
+ document.body.appendChild(anchor);
386
+
387
+ this.floatingAnchor = anchor;
388
+
389
+ const tools = this.floatingTools;
390
+
391
+ const id = $ot.popup.open(anchor, function()
392
+ {
393
+ this.click = (cmd) =>
394
+ {
395
+ this.Destroy();
396
+ };
397
+
398
+ return /* html */ `
399
+ <div class="e-45c96e98-float">
400
+ ${tools.map((tool) => `
401
+ <button type="button" class="btn" data-cmd="${tool.cmd}">
402
+ <i>${tool.icon}</i>
403
+ </button>
404
+ `).join('')}
405
+ </div>
406
+ `;
407
+ }, {
408
+ position: { x: 'center', y: 'top' },
409
+ offset: { x: 0, y: -8 },
410
+ closeable: false
411
+ });
412
+
413
+ this.floatingId = id;
414
+
415
+ // Delegate button clicks
416
+ setTimeout(() =>
417
+ {
418
+ const overlay = document.querySelector('[data-overlay-id="' + id + '"]');
419
+
420
+ if(overlay)
421
+ {
422
+ overlay.addEventListener('mousedown', (event) =>
423
+ {
424
+ const btn = event.target.closest('[data-cmd]');
425
+
426
+ if(btn)
427
+ {
428
+ event.preventDefault();
429
+ const cmd = btn.getAttribute('data-cmd');
430
+ this.exec(cmd);
431
+ }
432
+ });
433
+ }
434
+ }, 0);
435
+ };
436
+
437
+ this.hideFloating = () =>
438
+ {
439
+ if(this.floatingId)
440
+ {
441
+ $ot.popup.close(this.floatingId);
442
+ this.floatingId = null;
443
+ }
444
+
445
+ if(this.floatingAnchor)
446
+ {
447
+ this.floatingAnchor.remove();
448
+ this.floatingAnchor = null;
449
+ }
450
+ };
451
+
452
+ // ---- Slash menu ----
453
+
454
+ this.slashId = null;
455
+
456
+ this.showSlash = () =>
457
+ {
458
+ if(!this.hasSlash)
459
+ {
460
+ return;
461
+ }
462
+
463
+ const selection = window.getSelection();
464
+
465
+ if(!selection || !selection.rangeCount)
466
+ {
467
+ return;
468
+ }
469
+
470
+ const range = selection.getRangeAt(0);
471
+ const rect = range.getBoundingClientRect();
472
+
473
+ this.hideSlash();
474
+
475
+ const anchor = document.createElement('div');
476
+ anchor.style.position = 'fixed';
477
+ anchor.style.left = rect.left + 'px';
478
+ anchor.style.top = rect.bottom + 'px';
479
+ anchor.style.width = '1px';
480
+ anchor.style.height = '1px';
481
+ anchor.style.pointerEvents = 'none';
482
+ document.body.appendChild(anchor);
483
+
484
+ this.slashAnchor = anchor;
485
+
486
+ const items = this.slashItems;
487
+ const self = this;
488
+
489
+ const id = $ot.popup.open(anchor, function()
490
+ {
491
+ return /* html */ `
492
+ <div class="e-45c96e98-slash">
493
+ ${items.map((item) => `
494
+ <button type="button" class="item" data-cmd="${item.cmd}">
495
+ <div class="icon"><i>${item.icon}</i></div>
496
+ <div class="text">
497
+ <div class="label">${item.label}</div>
498
+ <div class="description">${item.description}</div>
499
+ </div>
500
+ </button>
501
+ `).join('')}
502
+ </div>
503
+ `;
504
+ }, {
505
+ position: { x: 'left-in', y: 'bottom' },
506
+ offset: { x: 0, y: 8 }
507
+ });
508
+
509
+ this.slashId = id;
510
+
511
+ setTimeout(() =>
512
+ {
513
+ const overlay = document.querySelector('[data-overlay-id="' + id + '"]');
514
+
515
+ if(overlay)
516
+ {
517
+ overlay.addEventListener('mousedown', (event) =>
518
+ {
519
+ const btn = event.target.closest('[data-cmd]');
520
+
521
+ if(btn)
522
+ {
523
+ event.preventDefault();
524
+ const cmd = btn.getAttribute('data-cmd');
525
+ self.removeSlashChar();
526
+ self.hideSlash();
527
+ self.exec(cmd);
528
+ }
529
+ });
530
+ }
531
+ }, 0);
532
+ };
533
+
534
+ this.hideSlash = () =>
535
+ {
536
+ if(this.slashId)
537
+ {
538
+ $ot.popup.close(this.slashId);
539
+ this.slashId = null;
540
+ }
541
+
542
+ if(this.slashAnchor)
543
+ {
544
+ this.slashAnchor.remove();
545
+ this.slashAnchor = null;
546
+ }
547
+ };
548
+
549
+ this.removeSlashChar = () =>
550
+ {
551
+ const selection = window.getSelection();
552
+
553
+ if(!selection || !selection.rangeCount)
554
+ {
555
+ return;
556
+ }
557
+
558
+ const range = selection.getRangeAt(0);
559
+ const node = range.startContainer;
560
+
561
+ if(node.nodeType === 3 && range.startOffset > 0)
562
+ {
563
+ const text = node.textContent;
564
+ const before = text.substring(0, range.startOffset - 1);
565
+ const after = text.substring(range.startOffset);
566
+ node.textContent = before + after;
567
+
568
+ const newRange = document.createRange();
569
+ newRange.setStart(node, before.length);
570
+ newRange.collapse(true);
571
+ selection.removeAllRanges();
572
+ selection.addRange(newRange);
573
+ }
574
+ };
575
+
576
+ // ---- Lifecycle ----
577
+
578
+ this.OnReady(() =>
579
+ {
580
+ this.body = this.Element.querySelector('.body');
581
+ this.hidden = this.Element.querySelector('input.hidden');
582
+
583
+ if(!this.body)
584
+ {
585
+ return;
586
+ }
587
+
588
+ if(this.value)
589
+ {
590
+ this.body.innerHTML = this.value;
591
+ }
592
+
593
+ // Toolbar clicks (fixed toolbar)
594
+ this.Element.querySelectorAll('.toolbar [data-cmd]').forEach((btn) =>
595
+ {
596
+ btn.addEventListener('mousedown', (event) =>
597
+ {
598
+ event.preventDefault();
599
+ const cmd = btn.getAttribute('data-cmd');
600
+ this.exec(cmd);
601
+ });
602
+ });
603
+
604
+ // Body input
605
+ this.body.addEventListener('input', () =>
606
+ {
607
+ // Slash menu trigger
608
+ if(this.hasSlash)
609
+ {
610
+ const selection = window.getSelection();
611
+
612
+ if(selection && selection.rangeCount)
613
+ {
614
+ const range = selection.getRangeAt(0);
615
+ const node = range.startContainer;
616
+
617
+ if(node.nodeType === 3)
618
+ {
619
+ const text = node.textContent.substring(0, range.startOffset);
620
+
621
+ if(text.endsWith('/'))
622
+ {
623
+ // Only trigger if slash is at start of block or after space/newline
624
+ const char = text.length >= 2 ? text.charAt(text.length - 2) : '';
625
+
626
+ if(!char || char === ' ' || char === '\n')
627
+ {
628
+ this.showSlash();
629
+ }
630
+ }
631
+ else if(this.slashId)
632
+ {
633
+ this.hideSlash();
634
+ }
635
+ }
636
+ }
637
+ }
638
+
639
+ this.emit();
640
+ });
641
+
642
+ // Paste cleanup
643
+ this.body.addEventListener('paste', (event) =>
644
+ {
645
+ event.preventDefault();
646
+
647
+ const html = event.clipboardData.getData('text/html');
648
+ const text = event.clipboardData.getData('text/plain');
649
+
650
+ let content;
651
+
652
+ if(html)
653
+ {
654
+ content = this.cleanHtml(html);
655
+ }
656
+ else
657
+ {
658
+ const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
659
+ content = '<p>' + escaped.replace(/\n\n/g, '</p><p>').replace(/\n/g, '<br>') + '</p>';
660
+ }
661
+
662
+ document.execCommand('insertHTML', false, content);
663
+ });
664
+
665
+ // Keyboard shortcuts + special keys
666
+ this.body.addEventListener('keydown', (event) =>
667
+ {
668
+ const meta = event.metaKey || event.ctrlKey;
669
+
670
+ if(meta && event.key === 'b')
671
+ {
672
+ event.preventDefault();
673
+ this.exec('bold');
674
+ return;
675
+ }
676
+
677
+ if(meta && event.key === 'i')
678
+ {
679
+ event.preventDefault();
680
+ this.exec('italic');
681
+ return;
682
+ }
683
+
684
+ if(meta && event.key === 'u')
685
+ {
686
+ event.preventDefault();
687
+ this.exec('underline');
688
+ return;
689
+ }
690
+
691
+ if(meta && event.key === 'k')
692
+ {
693
+ event.preventDefault();
694
+ this.exec('link');
695
+ return;
696
+ }
697
+
698
+ // Escape closes slash menu
699
+ if(event.key === 'Escape' && this.slashId)
700
+ {
701
+ event.preventDefault();
702
+ this.hideSlash();
703
+ return;
704
+ }
705
+
706
+ // Enter inside blockquote/heading exits to paragraph on double enter
707
+ if(event.key === 'Enter' && !event.shiftKey)
708
+ {
709
+ const block = document.queryCommandValue('formatBlock');
710
+
711
+ if(block === 'blockquote')
712
+ {
713
+ const selection = window.getSelection();
714
+
715
+ if(selection && selection.anchorNode)
716
+ {
717
+ const text = selection.anchorNode.textContent || '';
718
+
719
+ if(!text.trim())
720
+ {
721
+ event.preventDefault();
722
+ document.execCommand('formatBlock', false, 'p');
723
+ }
724
+ }
725
+ }
726
+ }
727
+ });
728
+
729
+ // Selection tracking for active buttons + floating toolbar
730
+ const onSelection = () =>
731
+ {
732
+ if(!this.body)
733
+ {
734
+ return;
735
+ }
736
+
737
+ const selection = window.getSelection();
738
+
739
+ if(!selection || !selection.anchorNode || !this.body.contains(selection.anchorNode))
740
+ {
741
+ return;
742
+ }
743
+
744
+ this.syncButtons();
745
+
746
+ if(this.hasFloating)
747
+ {
748
+ if(!selection.isCollapsed && selection.toString().trim())
749
+ {
750
+ this.showFloating();
751
+ }
752
+ else
753
+ {
754
+ this.hideFloating();
755
+ }
756
+ }
757
+ };
758
+
759
+ document.addEventListener('selectionchange', onSelection);
760
+
761
+ this.OnDestroy(() =>
762
+ {
763
+ document.removeEventListener('selectionchange', onSelection);
764
+ this.hideFloating();
765
+ this.hideSlash();
766
+ });
767
+
768
+ this.body.addEventListener('blur', () =>
769
+ {
770
+ // Keep floating until click resolves
771
+ setTimeout(() => this.hideFloating(), 150);
772
+ });
773
+ });
774
+
775
+ return /* html */ `
776
+ <div :class="'holder ' + variant.join(' ')">
777
+ <input ot-if="name" class="hidden" type="hidden" :name="name" :value="value" />
778
+ <div ot-if="hasToolbar" class="toolbar">
779
+ <div ot-for="tool in tools">
780
+ <div ot-if="tool.sep" class="sep"></div>
781
+ <button ot-if="!tool.sep" type="button" class="btn" :data-cmd="tool.cmd" :title="tool.label + (tool.shortcut ? ' (' + tool.shortcut + ')' : '')">
782
+ <i>{{ tool.icon }}</i>
783
+ </button>
784
+ </div>
785
+ </div>
786
+
787
+ <div class="content">
788
+ <div class="body" contenteditable="true" :data-placeholder="placeholder"></div>
789
+ </div>
790
+ </div>
791
+ `;
792
+ }
793
+ });
794
+ });