fable-editor 1.0.2 → 1.2.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/README.md +55 -6
- package/dist/angular/README.md +55 -6
- package/dist/angular/angular/fable-editor.component.d.ts +4 -1
- package/dist/angular/core/config.d.ts +15 -0
- package/dist/angular/core/editor.d.ts +107 -0
- package/dist/angular/core/i18n.d.ts +49 -0
- package/dist/angular/core/types.d.ts +21 -0
- package/dist/angular/esm2022/angular/fable-editor.component.mjs +14 -2
- package/dist/angular/esm2022/core/config.mjs +215 -24
- package/dist/angular/esm2022/core/editor.mjs +1255 -74
- package/dist/angular/esm2022/core/i18n.mjs +101 -3
- package/dist/angular/esm2022/core/icons.mjs +12 -2
- package/dist/angular/esm2022/core/types.mjs +1 -1
- package/dist/angular/fesm2022/fable-editor.mjs +1591 -99
- package/dist/angular/fesm2022/fable-editor.mjs.map +1 -1
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/editor.css +452 -57
- package/dist/core/editor.d.ts +107 -0
- package/dist/core/editor.d.ts.map +1 -1
- package/dist/core/i18n.d.ts +49 -0
- package/dist/core/i18n.d.ts.map +1 -1
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/core/index.cjs +4 -4
- package/dist/core/index.mjs +2580 -703
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/FableEditor.d.ts.map +1 -1
- package/dist/react/index.cjs +13 -13
- package/dist/react/index.mjs +2982 -1136
- package/package.json +1 -1
- package/style.css +452 -57
|
@@ -41,9 +41,14 @@ const I18N = {
|
|
|
41
41
|
rowabove: 'Insert row before',
|
|
42
42
|
rowbelow: 'Insert row after',
|
|
43
43
|
delrow: 'Delete row',
|
|
44
|
+
moverowup: 'Move row up',
|
|
45
|
+
moverowdown: 'Move row down',
|
|
44
46
|
colbefore: 'Insert column before',
|
|
45
47
|
colafter: 'Insert column after',
|
|
46
48
|
delcol: 'Delete column',
|
|
49
|
+
movecolleft: 'Move column left',
|
|
50
|
+
movecolright: 'Move column right',
|
|
51
|
+
cellbg: 'Cell background',
|
|
47
52
|
deltable: 'Delete table',
|
|
48
53
|
ltr: 'Left to right',
|
|
49
54
|
rtl: 'Right to left',
|
|
@@ -90,6 +95,9 @@ const I18N = {
|
|
|
90
95
|
tblcellpadding: 'Cell padding',
|
|
91
96
|
tblborder: 'Border width',
|
|
92
97
|
tblalign: 'Alignment',
|
|
98
|
+
tblalignleft: 'Align table left',
|
|
99
|
+
tblaligncenter: 'Center table',
|
|
100
|
+
tblalignright: 'Align table right',
|
|
93
101
|
alignnone: 'None',
|
|
94
102
|
quote: 'Blockquote',
|
|
95
103
|
link: 'Link…',
|
|
@@ -98,6 +106,7 @@ const I18N = {
|
|
|
98
106
|
unlink: 'Remove link',
|
|
99
107
|
lineheight: 'Line height',
|
|
100
108
|
wordspacing: 'Word spacing',
|
|
109
|
+
letterspacing: 'Letter spacing',
|
|
101
110
|
changecase: 'Change case',
|
|
102
111
|
lowercase: 'Lowercase',
|
|
103
112
|
uppercase: 'Uppercase',
|
|
@@ -142,7 +151,47 @@ const I18N = {
|
|
|
142
151
|
tplimgright: 'Image right, text left',
|
|
143
152
|
tplimgtop: 'Image top, text below',
|
|
144
153
|
tplimgcenter: 'Text top, image center',
|
|
145
|
-
deltemplate: 'Delete template'
|
|
154
|
+
deltemplate: 'Delete template',
|
|
155
|
+
video: 'Video',
|
|
156
|
+
quickvideo: 'Insert video',
|
|
157
|
+
deletevideo: 'Delete video',
|
|
158
|
+
uploadvideo: 'Upload from computer',
|
|
159
|
+
videolink: 'Insert from URL',
|
|
160
|
+
videourlph: 'Paste video URL…',
|
|
161
|
+
insertvideo: 'Insert',
|
|
162
|
+
dropvideo: 'Drop video here',
|
|
163
|
+
videodlgttl: 'Insert/edit video',
|
|
164
|
+
vidgeneral: 'General',
|
|
165
|
+
vidembed: 'Embed',
|
|
166
|
+
vidadvanced: 'Advanced',
|
|
167
|
+
vidsource: 'Source',
|
|
168
|
+
vidembedhint: 'Paste your embed code below:',
|
|
169
|
+
vidaltsource: 'Alternative source URL',
|
|
170
|
+
vidposter: 'Media poster (Image URL)',
|
|
171
|
+
emoji: 'Emojis…',
|
|
172
|
+
code: 'Inline code',
|
|
173
|
+
codesample: 'Code sample…',
|
|
174
|
+
codedlgttl: 'Insert/edit code sample',
|
|
175
|
+
codelang: 'Language',
|
|
176
|
+
editcode: 'Edit code sample',
|
|
177
|
+
deletecode: 'Delete code sample',
|
|
178
|
+
copycode: 'Copy code',
|
|
179
|
+
chcat_currency: 'Currency',
|
|
180
|
+
chcat_text: 'Text & punctuation',
|
|
181
|
+
chcat_math: 'Mathematical',
|
|
182
|
+
chcat_arrows: 'Arrows',
|
|
183
|
+
chcat_symbols: 'Symbols',
|
|
184
|
+
chcat_latin: 'Extended Latin',
|
|
185
|
+
chcat_arabic: 'Arabic',
|
|
186
|
+
emcat_smileys: 'Smileys & emotion',
|
|
187
|
+
emcat_people: 'People & body',
|
|
188
|
+
emcat_animals: 'Animals & nature',
|
|
189
|
+
emcat_food: 'Food & drink',
|
|
190
|
+
emcat_travel: 'Travel & places',
|
|
191
|
+
emcat_activities: 'Activities',
|
|
192
|
+
emcat_objects: 'Objects',
|
|
193
|
+
emcat_symbols: 'Symbols',
|
|
194
|
+
emcat_flags: 'Flags'
|
|
146
195
|
},
|
|
147
196
|
ar: {
|
|
148
197
|
dir: 'rtl',
|
|
@@ -183,9 +232,14 @@ const I18N = {
|
|
|
183
232
|
rowabove: 'إدراج صف قبل',
|
|
184
233
|
rowbelow: 'إدراج صف بعد',
|
|
185
234
|
delrow: 'حذف الصف',
|
|
235
|
+
moverowup: 'نقل الصف لأعلى',
|
|
236
|
+
moverowdown: 'نقل الصف لأسفل',
|
|
186
237
|
colbefore: 'إدراج عمود قبل',
|
|
187
238
|
colafter: 'إدراج عمود بعد',
|
|
188
239
|
delcol: 'حذف العمود',
|
|
240
|
+
movecolleft: 'نقل العمود لليسار',
|
|
241
|
+
movecolright: 'نقل العمود لليمين',
|
|
242
|
+
cellbg: 'خلفية الخلية',
|
|
189
243
|
deltable: 'حذف الجدول',
|
|
190
244
|
ltr: 'من اليسار إلى اليمين',
|
|
191
245
|
rtl: 'من اليمين إلى اليسار',
|
|
@@ -232,6 +286,9 @@ const I18N = {
|
|
|
232
286
|
tblcellpadding: 'حشو الخلايا',
|
|
233
287
|
tblborder: 'عرض الحدود',
|
|
234
288
|
tblalign: 'المحاذاة',
|
|
289
|
+
tblalignleft: 'محاذاة الجدول لليسار',
|
|
290
|
+
tblaligncenter: 'توسيط الجدول',
|
|
291
|
+
tblalignright: 'محاذاة الجدول لليمين',
|
|
235
292
|
alignnone: 'بدون',
|
|
236
293
|
quote: 'اقتباس',
|
|
237
294
|
link: 'رابط…',
|
|
@@ -240,6 +297,7 @@ const I18N = {
|
|
|
240
297
|
unlink: 'إزالة الرابط',
|
|
241
298
|
lineheight: 'تباعد الأسطر',
|
|
242
299
|
wordspacing: 'تباعد الكلمات',
|
|
300
|
+
letterspacing: 'تباعد الأحرف',
|
|
243
301
|
changecase: 'تغيير حالة الأحرف',
|
|
244
302
|
lowercase: 'أحرف صغيرة',
|
|
245
303
|
uppercase: 'أحرف كبيرة',
|
|
@@ -284,7 +342,47 @@ const I18N = {
|
|
|
284
342
|
tplimgright: 'صورة يمين، نص يسار',
|
|
285
343
|
tplimgtop: 'صورة أعلى، نص أسفل',
|
|
286
344
|
tplimgcenter: 'نص أعلى، صورة وسط',
|
|
287
|
-
deltemplate: 'حذف القالب'
|
|
345
|
+
deltemplate: 'حذف القالب',
|
|
346
|
+
video: 'فيديو',
|
|
347
|
+
quickvideo: 'إدراج فيديو',
|
|
348
|
+
deletevideo: 'حذف الفيديو',
|
|
349
|
+
uploadvideo: 'رفع من الجهاز',
|
|
350
|
+
videolink: 'إدراج من رابط',
|
|
351
|
+
videourlph: 'الصق رابط الفيديو…',
|
|
352
|
+
insertvideo: 'إدراج',
|
|
353
|
+
dropvideo: 'أسقط الفيديو هنا',
|
|
354
|
+
videodlgttl: 'إدراج/تعديل فيديو',
|
|
355
|
+
vidgeneral: 'عام',
|
|
356
|
+
vidembed: 'تضمين',
|
|
357
|
+
vidadvanced: 'متقدم',
|
|
358
|
+
vidsource: 'المصدر',
|
|
359
|
+
vidembedhint: 'الصق كود التضمين أدناه:',
|
|
360
|
+
vidaltsource: 'رابط مصدر بديل',
|
|
361
|
+
vidposter: 'صورة الغلاف (رابط الصورة)',
|
|
362
|
+
emoji: 'رموز تعبيرية…',
|
|
363
|
+
code: 'كود مضمّن',
|
|
364
|
+
codesample: 'عينة كود…',
|
|
365
|
+
codedlgttl: 'إدراج/تعديل عينة كود',
|
|
366
|
+
codelang: 'اللغة',
|
|
367
|
+
editcode: 'تعديل عينة الكود',
|
|
368
|
+
deletecode: 'حذف عينة الكود',
|
|
369
|
+
copycode: 'نسخ الكود',
|
|
370
|
+
chcat_currency: 'عملات',
|
|
371
|
+
chcat_text: 'نص وعلامات ترقيم',
|
|
372
|
+
chcat_math: 'رموز رياضية',
|
|
373
|
+
chcat_arrows: 'أسهم',
|
|
374
|
+
chcat_symbols: 'رموز',
|
|
375
|
+
chcat_latin: 'لاتينية موسعة',
|
|
376
|
+
chcat_arabic: 'عربية',
|
|
377
|
+
emcat_smileys: 'وجوه ومشاعر',
|
|
378
|
+
emcat_people: 'أشخاص وإيماءات',
|
|
379
|
+
emcat_animals: 'حيوانات وطبيعة',
|
|
380
|
+
emcat_food: 'طعام وشراب',
|
|
381
|
+
emcat_travel: 'سفر وأماكن',
|
|
382
|
+
emcat_activities: 'أنشطة',
|
|
383
|
+
emcat_objects: 'أشياء',
|
|
384
|
+
emcat_symbols: 'رموز',
|
|
385
|
+
emcat_flags: 'أعلام'
|
|
288
386
|
}
|
|
289
387
|
};
|
|
290
388
|
function getStrings(lang) {
|
|
@@ -295,8 +393,8 @@ function getStrings(lang) {
|
|
|
295
393
|
* `toolbar: true` (the default) renders identically to the hard-coded original. */
|
|
296
394
|
const DEFAULT_TOOLBAR = 'undo redo | preview print | importword revhistory | fontfamily fontsize | fontsizeincrease fontsizedecrease | ' +
|
|
297
395
|
'bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | ' +
|
|
298
|
-
'bullist numlist | outdent indent | link blockquote changecase lineheight wordspacing | removeformat | blocks | ' +
|
|
299
|
-
'ltr rtl | quickimage quicktable template charmap | fullscreen sourcecode';
|
|
396
|
+
'bullist numlist | outdent indent | link blockquote code changecase lineheight wordspacing letterspacing | removeformat | blocks | ' +
|
|
397
|
+
'ltr rtl | quickimage quickvideo quicktable codesample template charmap emoji | fullscreen sourcecode';
|
|
300
398
|
/** Transcribed from the editor's built-in menu keys so that `menubar: true` (the
|
|
301
399
|
* default) renders identically to the hard-coded original. */
|
|
302
400
|
const DEFAULT_MENUBAR = 'file edit view insert format tools table help';
|
|
@@ -326,6 +424,26 @@ const MIN_FONT_PX = 1;
|
|
|
326
424
|
const MAX_FONT_PX = 100;
|
|
327
425
|
const LINE_HEIGHTS = ['1', '1.15', '1.4', '1.5', '1.75', '2', '2.5', '3'];
|
|
328
426
|
const WORD_SPACINGS = ['1px', '2px', '4px', '6px', '8px', '12px'];
|
|
427
|
+
const LETTER_SPACINGS = ['0.5px', '1px', '1.5px', '2px', '3px', '4px'];
|
|
428
|
+
/** Languages offered by the code-sample dialog: [value stored in data-lang, label]. */
|
|
429
|
+
const CODE_LANGS = [
|
|
430
|
+
['plain', 'Plain text'],
|
|
431
|
+
['html', 'HTML/XML'],
|
|
432
|
+
['css', 'CSS'],
|
|
433
|
+
['javascript', 'JavaScript'],
|
|
434
|
+
['typescript', 'TypeScript'],
|
|
435
|
+
['json', 'JSON'],
|
|
436
|
+
['python', 'Python'],
|
|
437
|
+
['java', 'Java'],
|
|
438
|
+
['csharp', 'C#'],
|
|
439
|
+
['cpp', 'C/C++'],
|
|
440
|
+
['php', 'PHP'],
|
|
441
|
+
['ruby', 'Ruby'],
|
|
442
|
+
['go', 'Go'],
|
|
443
|
+
['rust', 'Rust'],
|
|
444
|
+
['sql', 'SQL'],
|
|
445
|
+
['bash', 'Bash/Shell']
|
|
446
|
+
];
|
|
329
447
|
const BLOCKS = [
|
|
330
448
|
['p', 'para'],
|
|
331
449
|
['h1', 'heading1'],
|
|
@@ -338,29 +456,200 @@ const BLOCKS = [
|
|
|
338
456
|
];
|
|
339
457
|
/** Quick-access text color chips shown directly in the toolbar. */
|
|
340
458
|
const QUICK_COLORS = ['#F76707', '#4263EB', '#CC5DE8'];
|
|
459
|
+
/** 25 swatches — a 5×5 grid in the picker (pastels, brights, darks, grays,
|
|
460
|
+
* then pink/brown/cyan + black/white). Shared by fore/back color and cell fill. */
|
|
341
461
|
const COLORS = [
|
|
342
|
-
'#
|
|
343
|
-
'#
|
|
344
|
-
'#
|
|
345
|
-
'#
|
|
346
|
-
'#C2E0F4',
|
|
347
|
-
'#2DC26B',
|
|
348
|
-
'#F1C40F',
|
|
349
|
-
'#E03E2D',
|
|
350
|
-
'#B96AD9',
|
|
351
|
-
'#3598DB',
|
|
352
|
-
'#169179',
|
|
353
|
-
'#E67E23',
|
|
354
|
-
'#BA372A',
|
|
355
|
-
'#843FA1',
|
|
356
|
-
'#236FA1',
|
|
357
|
-
'#ECF0F1',
|
|
358
|
-
'#CED4D9',
|
|
359
|
-
'#95A5A6',
|
|
360
|
-
'#7E8C8D',
|
|
361
|
-
'#34495E',
|
|
462
|
+
'#FFFFFF',
|
|
463
|
+
'#F4F4F4',
|
|
464
|
+
'#E8EAED',
|
|
465
|
+
'#DADCE0',
|
|
362
466
|
'#000000',
|
|
363
|
-
'#
|
|
467
|
+
'#F28B82',
|
|
468
|
+
'#FBBC04',
|
|
469
|
+
'#FFF475',
|
|
470
|
+
'#CCFF90',
|
|
471
|
+
'#A7FFEB',
|
|
472
|
+
'#CBF0F8',
|
|
473
|
+
'#AECBFA',
|
|
474
|
+
'#D7AEFB',
|
|
475
|
+
'#FDCFE8',
|
|
476
|
+
'#E6C9A8',
|
|
477
|
+
'#EA4335',
|
|
478
|
+
'#FB8C00',
|
|
479
|
+
'#FDD835',
|
|
480
|
+
'#34A853',
|
|
481
|
+
'#00ACC1',
|
|
482
|
+
'#4285F4',
|
|
483
|
+
'#5E35B1',
|
|
484
|
+
'#8E24AA',
|
|
485
|
+
'#C2185B',
|
|
486
|
+
'#6D4C41'
|
|
487
|
+
];
|
|
488
|
+
/** Special-character map, grouped for the two-pane picker (tabs left, glyphs right).
|
|
489
|
+
* `CHARS` below is the legacy flat list and is kept exported for back-compat. */
|
|
490
|
+
const CHAR_CATEGORIES = [
|
|
491
|
+
{
|
|
492
|
+
key: 'chcat_currency',
|
|
493
|
+
chars: [
|
|
494
|
+
'€', '£', '¥', '¢', '$', '₹', '₽', '₩', '₺', '₴', '₦', '฿', '₫', '₪', '﷼', '¤',
|
|
495
|
+
'₡', '₱', '₲', '₵', '₭', '₮', '₸', '₼', '₾', '₿'
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
key: 'chcat_text',
|
|
500
|
+
chars: [
|
|
501
|
+
'§', '¶', '†', '‡', '•', '…', '′', '″', '‰', '‱', '–', '—', '―', '¡', '¿', '·',
|
|
502
|
+
'‹', '›', '«', '»', '“', '”', '‘', '’', '„', '‚', '¦', 'ª', 'º',
|
|
503
|
+
'‽', '※', '❝', '❞', '❮', '❯'
|
|
504
|
+
]
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
key: 'chcat_math',
|
|
508
|
+
chars: [
|
|
509
|
+
'°', '±', '×', '÷', '≈', '≠', '≤', '≥', '∞', 'µ', '√', '∑', '∫', '∂', '∆', '∏',
|
|
510
|
+
'π', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'θ', 'λ', 'σ', 'φ', 'ψ', 'ω',
|
|
511
|
+
'∈', '∉', '∪', '∩', '⊂', '⊃', '⊆', '⊇', '∀', '∃', '∅', '∝', '∴', '∵', '¬', '∧', '∨',
|
|
512
|
+
'½', '⅓', '¼', '¾', '⅔', '⅛', '⅜', '⅝', '⅞',
|
|
513
|
+
'¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹', '⁰',
|
|
514
|
+
'₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉', '₀'
|
|
515
|
+
]
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
key: 'chcat_arrows',
|
|
519
|
+
chars: [
|
|
520
|
+
'←', '→', '↑', '↓', '↔', '↕', '⇐', '⇒', '⇑', '⇓', '⇔',
|
|
521
|
+
'↖', '↗', '↘', '↙', '↩', '↪', '↺', '↻',
|
|
522
|
+
'⤴', '⤵', '➜', '➝', '➞', '➟', '⇦', '⇨', '⇧', '⇩'
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
key: 'chcat_symbols',
|
|
527
|
+
chars: [
|
|
528
|
+
'©', '®', '™', '℠', '№', '℮',
|
|
529
|
+
'✓', '✔', '✗', '✘',
|
|
530
|
+
'★', '☆', '✦', '✪',
|
|
531
|
+
'♠', '♣', '♥', '♦',
|
|
532
|
+
'♪', '♫', '♬',
|
|
533
|
+
'☀', '☁', '☂', '☃', '☾',
|
|
534
|
+
'☹', '☺', '☻',
|
|
535
|
+
'✉', '✂', '✎', '✏',
|
|
536
|
+
'☎', '☏', '⌛', '⌚',
|
|
537
|
+
'☞', '☜', '☝', '☟'
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
key: 'chcat_latin',
|
|
542
|
+
chars: [
|
|
543
|
+
'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï',
|
|
544
|
+
'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß',
|
|
545
|
+
'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï',
|
|
546
|
+
'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ',
|
|
547
|
+
'Œ', 'œ', 'Š', 'š', 'Ž', 'ž', 'Ł', 'ł'
|
|
548
|
+
]
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
key: 'chcat_arabic',
|
|
552
|
+
chars: [
|
|
553
|
+
'؟', '،', '؛', '٪', '٭', 'ـ',
|
|
554
|
+
'٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩',
|
|
555
|
+
'﴾', '﴿', '۞', '؎',
|
|
556
|
+
'ء', 'آ', 'أ', 'ؤ', 'إ', 'ئ', 'ى', 'ة'
|
|
557
|
+
]
|
|
558
|
+
}
|
|
559
|
+
];
|
|
560
|
+
/** Emoji map for the emoji picker — same two-pane dialog as the character map. */
|
|
561
|
+
const EMOJI_CATEGORIES = [
|
|
562
|
+
{
|
|
563
|
+
key: 'emcat_smileys',
|
|
564
|
+
chars: [
|
|
565
|
+
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰',
|
|
566
|
+
'😘', '😋', '😛', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥳', '😏', '😒', '😞', '😔', '😟', '😕',
|
|
567
|
+
'🙁', '😣', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤯', '😳', '🥵', '🥶', '😱', '😨',
|
|
568
|
+
'😥', '🤗', '🤔', '🤭', '🤫', '😐', '😑', '😬', '🙄', '😯', '😴', '😪', '😷', '🤒', '🤕', '🤢',
|
|
569
|
+
'🤧', '😈', '💀', '👻', '👽', '🤖', '💩', '😺', '😸', '😹', '😻', '🙀'
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
key: 'emcat_people',
|
|
574
|
+
chars: [
|
|
575
|
+
'👋', '🤚', '✋', '🖖', '👌', '🤏', '✌', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '👇', '☝',
|
|
576
|
+
'👍', '👎', '✊', '👊', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '💪', '✍', '💅', '🤳', '👂', '👃',
|
|
577
|
+
'👀', '👁', '🧠', '👶', '🧒', '👦', '👧', '🧑', '👨', '👩', '🧓', '👴', '👵', '👮', '👷', '💂',
|
|
578
|
+
'🕵', '👸', '🤴', '👰', '🤵', '🤰', '👼', '🎅', '🦸', '🦹', '🧙', '🧚', '🧛', '🧟', '💃', '🕺',
|
|
579
|
+
'🚶', '🏃', '🧘'
|
|
580
|
+
]
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
key: 'emcat_animals',
|
|
584
|
+
chars: [
|
|
585
|
+
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
|
|
586
|
+
'🐧', '🐦', '🐤', '🦆', '🦅', '🦉', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐢',
|
|
587
|
+
'🐍', '🦎', '🦖', '🐙', '🦑', '🦀', '🐠', '🐟', '🐬', '🐳', '🦈', '🐊', '🐅', '🦓', '🦍', '🐘',
|
|
588
|
+
'🦏', '🐪', '🦒', '🦘', '🐄', '🐎', '🐖', '🐑', '🐐', '🦌', '🐕', '🐈', '🐓', '🦃', '🦚', '🦜',
|
|
589
|
+
'🦢', '🕊', '🐇', '🦔', '🌵', '🌲', '🌴', '🌱', '🌿', '☘', '🍀', '🍁', '🌸', '🌺', '🌻', '🌹',
|
|
590
|
+
'🌷', '💐'
|
|
591
|
+
]
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
key: 'emcat_food',
|
|
595
|
+
chars: [
|
|
596
|
+
'🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝',
|
|
597
|
+
'🍅', '🍆', '🥑', '🥦', '🥒', '🌶', '🌽', '🥕', '🥔', '🥐', '🍞', '🥖', '🥨', '🧀', '🥚', '🍳',
|
|
598
|
+
'🥞', '🥓', '🥩', '🍗', '🍖', '🌭', '🍔', '🍟', '🍕', '🥪', '🌮', '🌯', '🥗', '🍝', '🍜', '🍲',
|
|
599
|
+
'🍛', '🍣', '🍱', '🥟', '🍤', '🍚', '🍧', '🍨', '🍦', '🥧', '🧁', '🍰', '🎂', '🍮', '🍭', '🍬',
|
|
600
|
+
'🍫', '🍿', '🍩', '🍪', '🥜', '🍯', '🥛', '☕', '🍵', '🥤', '🍺', '🥂', '🍷', '🍸', '🍹'
|
|
601
|
+
]
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
key: 'emcat_travel',
|
|
605
|
+
chars: [
|
|
606
|
+
'🚗', '🚕', '🚙', '🚌', '🏎', '🚓', '🚑', '🚒', '🚚', '🚜', '🛵', '🏍', '🚲', '🛴', '🚔', '🚃',
|
|
607
|
+
'🚄', '🚅', '🚂', '🚆', '🚇', '🚉', '✈', '🛫', '🛬', '💺', '🚁', '🚀', '🛸', '🚢', '⛵', '🚤',
|
|
608
|
+
'⚓', '⛽', '🚧', '🗺', '🗿', '🗽', '🗼', '🏰', '🏯', '🎡', '🎢', '🎠', '⛲', '🏖', '🏝', '🌋',
|
|
609
|
+
'⛰', '🏔', '🗻', '🏕', '⛺', '🏠', '🏡', '🏢', '🏬', '🏥', '🏦', '🏨', '🏪', '🏫', '💒', '⛪',
|
|
610
|
+
'🕌', '🕍', '🕋', '⛩', '🌅', '🌄', '🌇', '🌆', '🌃', '🌉', '🌌', '🌠'
|
|
611
|
+
]
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
key: 'emcat_activities',
|
|
615
|
+
chars: [
|
|
616
|
+
'⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🏒', '🏑', '🏏', '⛳', '🏹', '🎣',
|
|
617
|
+
'🥊', '🥋', '⛸', '🎿', '⛷', '🏂', '🏋', '🤸', '⛹', '🤺', '🏇', '🧗', '🏄', '🏊', '🚣', '🚵',
|
|
618
|
+
'🚴', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖', '🎫', '🎪', '🤹', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼',
|
|
619
|
+
'🎹', '🥁', '🎷', '🎺', '🎸', '🎻', '🎲', '🎯', '🎳', '🎮', '🎰', '🧩'
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
key: 'emcat_objects',
|
|
624
|
+
chars: [
|
|
625
|
+
'⌚', '📱', '💻', '⌨', '🖥', '🖨', '🖱', '🕹', '💽', '💾', '💿', '📀', '📷', '📸', '📹', '🎥',
|
|
626
|
+
'📞', '☎', '📺', '📻', '🎙', '⏱', '⏰', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯', '💸',
|
|
627
|
+
'💵', '💰', '💳', '💎', '⚖', '🔧', '🔨', '⚒', '🛠', '⛏', '🔩', '⚙', '🧲', '🔫', '💣', '🔪',
|
|
628
|
+
'🛡', '🔮', '🧿', '🔭', '🔬', '💊', '💉', '🌡', '🧬', '🧪', '🧹', '🧺', '🛁', '🔑', '🗝', '🚪',
|
|
629
|
+
'🛋', '🛏', '🧸', '🖼', '🛍', '🛒', '🎁', '🎈', '🎀', '🎊', '🎉', '✉', '📩', '📦', '📜', '📄',
|
|
630
|
+
'📊', '📈', '📉', '📅', '📋', '📁', '📰', '📓', '📚', '📖', '🔖', '🔗', '📎', '📐', '📏', '📌',
|
|
631
|
+
'📍', '✂', '🖊', '✒', '🖌', '🖍', '📝', '✏', '🔍', '🔒', '🔓'
|
|
632
|
+
]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
key: 'emcat_symbols',
|
|
636
|
+
chars: [
|
|
637
|
+
'❤', '🧡', '💛', '💚', '💙', '💜', '🖤', '💔', '❣', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
|
|
638
|
+
'☮', '✝', '☪', '🕉', '☸', '✡', '☯', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐',
|
|
639
|
+
'♑', '♒', '♓', '⚛', '❌', '⭕', '🛑', '⛔', '🚫', '💯', '💢', '⚠', '🚸', '🔱', '⚜', '🔰',
|
|
640
|
+
'♻', '✅', '❇', '✳', '❎', '🌐', '💠', '🌀', '💤', '🏧', '♿', '🚭', '❗', '❕', '❓', '❔',
|
|
641
|
+
'‼', '⁉', '💲', '🔅', '🔆', '〽', 'ℹ', '🆗', '🆕', '🆓', '🆒', '🆙', '🈯', '🈚'
|
|
642
|
+
]
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
key: 'emcat_flags',
|
|
646
|
+
chars: [
|
|
647
|
+
'🏁', '🚩', '🎌', '🏴', '🏳', '🇦🇪', '🇸🇦', '🇶🇦', '🇰🇼', '🇧🇭', '🇴🇲', '🇪🇬', '🇯🇴', '🇱🇧', '🇮🇳', '🇵🇰',
|
|
648
|
+
'🇺🇸', '🇬🇧', '🇨🇦', '🇦🇺', '🇫🇷', '🇩🇪', '🇮🇹', '🇪🇸', '🇵🇹', '🇳🇱', '🇧🇪', '🇨🇭', '🇸🇪', '🇳🇴', '🇩🇰', '🇫🇮',
|
|
649
|
+
'🇮🇪', '🇬🇷', '🇹🇷', '🇷🇺', '🇺🇦', '🇵🇱', '🇯🇵', '🇰🇷', '🇨🇳', '🇸🇬', '🇲🇾', '🇮🇩', '🇹🇭', '🇵🇭', '🇻🇳', '🇧🇷',
|
|
650
|
+
'🇦🇷', '🇲🇽', '🇿🇦', '🇳🇬', '🇰🇪', '🇲🇦'
|
|
651
|
+
]
|
|
652
|
+
}
|
|
364
653
|
];
|
|
365
654
|
const CHARS = [
|
|
366
655
|
'©',
|
|
@@ -503,7 +792,17 @@ const IC = {
|
|
|
503
792
|
ol_alpha: LIST(['a', 'b', 'c']),
|
|
504
793
|
ol_roman: LIST(['i', 'ii', 'iii'], 6.5),
|
|
505
794
|
ol_alphau: LIST(['A', 'B', 'C']),
|
|
506
|
-
ol_romanu: LIST(['I', 'II', 'III'], 6.5)
|
|
795
|
+
ol_romanu: LIST(['I', 'II', 'III'], 6.5),
|
|
796
|
+
video: S('<rect x="3" y="6" width="13" height="12" rx="1.5"/><path d="M16 10l5-3v10l-5-3z"/>'),
|
|
797
|
+
editic: S('<path d="M4 20l1-4.5L15.5 5 19 8.5 8.5 19 4 20z"/><path d="M13.5 6.5l4 4"/>'),
|
|
798
|
+
letterspacing: S('<text x="2.5" y="13" font-size="12" stroke="none" fill="#222f3e" font-family="Arial" font-weight="bold">A</text><text x="13" y="13" font-size="12" stroke="none" fill="#222f3e" font-family="Arial" font-weight="bold">B</text><path d="M7 18.5h10"/><path class="f" d="M7.5 18.5 4.5 16.6v3.8z"/><path class="f" d="M16.5 18.5l3-1.9v3.8z"/>'),
|
|
799
|
+
emojiic: S('<circle cx="12" cy="12" r="8.5"/><path d="M8.6 14.3a4.6 4.6 0 0 0 6.8 0"/><path d="M9.2 9.6v.7M14.8 9.6v.7"/>'),
|
|
800
|
+
inlinecodeic: S('<path d="M8.5 8 4.5 12l4 4M15.5 8l4 4-4 4"/><path d="M13 6l-2 12"/>'),
|
|
801
|
+
moverowup: S('<rect x="4" y="15.5" width="16" height="5" rx="1"/><path d="M12 12.5V4M8.5 7.5 12 4l3.5 3.5"/>'),
|
|
802
|
+
moverowdown: S('<rect x="4" y="3.5" width="16" height="5" rx="1"/><path d="M12 11.5V20M8.5 16.5 12 20l3.5-3.5"/>'),
|
|
803
|
+
movecolleft: S('<rect x="15.5" y="4" width="5" height="16" rx="1"/><path d="M12.5 12H4M7.5 8.5 4 12l3.5 3.5"/>'),
|
|
804
|
+
movecolright: S('<rect x="3.5" y="4" width="5" height="16" rx="1"/><path d="M11.5 12H20M16.5 8.5 20 12l-3.5 3.5"/>'),
|
|
805
|
+
codesampleic: S('<rect x="3.5" y="4" width="17" height="16" rx="2"/><path d="M3.5 8.5h17"/><path d="M9.5 12 7 14.25l2.5 2.25M14.5 12l2.5 2.25-2.5 2.25"/>')
|
|
507
806
|
};
|
|
508
807
|
const TXT = (html) => `<span class="txt">${html}</span>`;
|
|
509
808
|
|
|
@@ -1394,6 +1693,82 @@ const TPL_ICON = {
|
|
|
1394
1693
|
'img-top': IC.tpltop,
|
|
1395
1694
|
'img-center': IC.tplcenter
|
|
1396
1695
|
};
|
|
1696
|
+
/* ---------------------------------------------------------- tooltips
|
|
1697
|
+
One shared bubble + one delegated document listener restyles every native
|
|
1698
|
+
`title` tooltip in the editor UI (toolbar, menus, popups, context toolbars,
|
|
1699
|
+
dialogs) without extra wrapper markup per control. Titles inside the
|
|
1700
|
+
editable area are user content and keep the browser default. */
|
|
1701
|
+
const TIP_SCOPE = '.tox,.pop,.tblctx,.imgctx,.selctx,.dlg';
|
|
1702
|
+
let tipEl = null;
|
|
1703
|
+
let tipTarget = null;
|
|
1704
|
+
let tipTimer = 0;
|
|
1705
|
+
let tipsInstalled = false;
|
|
1706
|
+
function hideTip() {
|
|
1707
|
+
clearTimeout(tipTimer);
|
|
1708
|
+
tipTarget = null;
|
|
1709
|
+
if (tipEl)
|
|
1710
|
+
tipEl.classList.remove('show');
|
|
1711
|
+
}
|
|
1712
|
+
function showTip(el) {
|
|
1713
|
+
const text = el.dataset.tip;
|
|
1714
|
+
if (!text)
|
|
1715
|
+
return;
|
|
1716
|
+
tipTarget = el;
|
|
1717
|
+
clearTimeout(tipTimer);
|
|
1718
|
+
// moving along a toolbar re-anchors an already-visible tip almost instantly
|
|
1719
|
+
const delay = tipEl && tipEl.classList.contains('show') ? 60 : 350;
|
|
1720
|
+
tipTimer = window.setTimeout(() => {
|
|
1721
|
+
if (tipTarget !== el || !document.contains(el))
|
|
1722
|
+
return;
|
|
1723
|
+
if (!tipEl) {
|
|
1724
|
+
tipEl = document.createElement('div');
|
|
1725
|
+
tipEl.className = 'etip';
|
|
1726
|
+
document.body.appendChild(tipEl);
|
|
1727
|
+
}
|
|
1728
|
+
tipEl.textContent = text;
|
|
1729
|
+
tipEl.classList.add('show');
|
|
1730
|
+
const r = el.getBoundingClientRect();
|
|
1731
|
+
const w = tipEl.offsetWidth;
|
|
1732
|
+
const h = tipEl.offsetHeight;
|
|
1733
|
+
let x = r.left + r.width / 2 - w / 2;
|
|
1734
|
+
x = Math.max(6, Math.min(x, innerWidth - w - 6));
|
|
1735
|
+
let y = r.bottom + 7;
|
|
1736
|
+
if (y + h > innerHeight - 6)
|
|
1737
|
+
y = r.top - h - 7;
|
|
1738
|
+
tipEl.style.left = x + 'px';
|
|
1739
|
+
tipEl.style.top = y + 'px';
|
|
1740
|
+
}, delay);
|
|
1741
|
+
}
|
|
1742
|
+
function installTooltips() {
|
|
1743
|
+
if (tipsInstalled)
|
|
1744
|
+
return;
|
|
1745
|
+
tipsInstalled = true;
|
|
1746
|
+
document.addEventListener('mouseover', (e) => {
|
|
1747
|
+
const raw = e.target;
|
|
1748
|
+
const el = raw.closest ? raw.closest('[title],[data-tip]') : null;
|
|
1749
|
+
if (!el || !el.closest(TIP_SCOPE) || el.closest('.earea')) {
|
|
1750
|
+
if (el !== tipTarget)
|
|
1751
|
+
hideTip();
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
const title = el.getAttribute('title');
|
|
1755
|
+
if (title) {
|
|
1756
|
+
// hand the text to the custom bubble and disarm the native one
|
|
1757
|
+
el.dataset.tip = title;
|
|
1758
|
+
el.removeAttribute('title');
|
|
1759
|
+
if (!el.getAttribute('aria-label'))
|
|
1760
|
+
el.setAttribute('aria-label', title);
|
|
1761
|
+
}
|
|
1762
|
+
if (el !== tipTarget)
|
|
1763
|
+
showTip(el);
|
|
1764
|
+
});
|
|
1765
|
+
document.addEventListener('mouseout', (e) => {
|
|
1766
|
+
if (tipTarget && !tipTarget.contains(e.relatedTarget))
|
|
1767
|
+
hideTip();
|
|
1768
|
+
});
|
|
1769
|
+
document.addEventListener('mousedown', hideTip, true);
|
|
1770
|
+
addEventListener('scroll', hideTip, true);
|
|
1771
|
+
}
|
|
1397
1772
|
class FableEditor {
|
|
1398
1773
|
static instanceCounter = 0;
|
|
1399
1774
|
options;
|
|
@@ -1432,7 +1807,22 @@ class FableEditor {
|
|
|
1432
1807
|
imageUploadHandler;
|
|
1433
1808
|
onImageUploadError;
|
|
1434
1809
|
contentStyle;
|
|
1810
|
+
vidInput;
|
|
1811
|
+
/* a <video> element or a .video-embed iframe wrapper — both share the
|
|
1812
|
+
same selection outline + align/delete context toolbar */
|
|
1813
|
+
vidActive = null;
|
|
1814
|
+
vidCtx = null;
|
|
1815
|
+
vidHideTimer = null;
|
|
1816
|
+
vphActive = null;
|
|
1817
|
+
vphCtx = null;
|
|
1818
|
+
vphUploadTarget = null;
|
|
1819
|
+
videoUploadHandler;
|
|
1820
|
+
onVideoUploadError;
|
|
1435
1821
|
selCtx = null;
|
|
1822
|
+
tbrMore = null;
|
|
1823
|
+
tbrExpanded = false;
|
|
1824
|
+
codeActive = null;
|
|
1825
|
+
codeCtx = null;
|
|
1436
1826
|
tplActive = null;
|
|
1437
1827
|
tplCtx = null;
|
|
1438
1828
|
docInput;
|
|
@@ -1456,11 +1846,15 @@ class FableEditor {
|
|
|
1456
1846
|
readonly: options.readonly ?? false,
|
|
1457
1847
|
draftKey: options.draftKey ?? (typeof location !== 'undefined' ? location.pathname : 'default'),
|
|
1458
1848
|
fontFamilyFormats: options.fontFamilyFormats ?? FONTS,
|
|
1459
|
-
imageFileTypes: options.imageFileTypes ?? ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp']
|
|
1849
|
+
imageFileTypes: options.imageFileTypes ?? ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'],
|
|
1850
|
+
videoFileTypes: options.videoFileTypes ?? ['video/mp4', 'video/webm', 'video/ogg']
|
|
1460
1851
|
};
|
|
1461
1852
|
this.imageUploadHandler = options.imageUploadHandler;
|
|
1462
1853
|
this.onImageUploadError = options.onImageUploadError;
|
|
1854
|
+
this.videoUploadHandler = options.videoUploadHandler;
|
|
1855
|
+
this.onVideoUploadError = options.onVideoUploadError;
|
|
1463
1856
|
this.contentStyle = options.contentStyle;
|
|
1857
|
+
this.applyTheme(options);
|
|
1464
1858
|
this.lang = this.options.language;
|
|
1465
1859
|
this.draftStorageKey = 'tmclone-draft:' + this.options.draftKey;
|
|
1466
1860
|
this.initDOM();
|
|
@@ -1519,6 +1913,7 @@ class FableEditor {
|
|
|
1519
1913
|
this.shell.appendChild(this.contentStyleEl);
|
|
1520
1914
|
}
|
|
1521
1915
|
target.appendChild(this.shell);
|
|
1916
|
+
installTooltips();
|
|
1522
1917
|
if (!this.options.statusbar)
|
|
1523
1918
|
this.statusbar.style.display = 'none';
|
|
1524
1919
|
this.imgInput = document.createElement('input');
|
|
@@ -1526,12 +1921,29 @@ class FableEditor {
|
|
|
1526
1921
|
this.imgInput.accept = this.options.imageFileTypes.join(',');
|
|
1527
1922
|
this.imgInput.style.display = 'none';
|
|
1528
1923
|
target.appendChild(this.imgInput);
|
|
1924
|
+
this.vidInput = document.createElement('input');
|
|
1925
|
+
this.vidInput.type = 'file';
|
|
1926
|
+
this.vidInput.accept = this.options.videoFileTypes.join(',');
|
|
1927
|
+
this.vidInput.style.display = 'none';
|
|
1928
|
+
target.appendChild(this.vidInput);
|
|
1529
1929
|
this.docInput = document.createElement('input');
|
|
1530
1930
|
this.docInput.type = 'file';
|
|
1531
1931
|
this.docInput.accept = '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
1532
1932
|
this.docInput.style.display = 'none';
|
|
1533
1933
|
target.appendChild(this.docInput);
|
|
1534
1934
|
}
|
|
1935
|
+
/** Theme options land as CSS variables on the document root rather than the editor
|
|
1936
|
+
* shell, because dialogs, popups and floating context toolbars are appended to
|
|
1937
|
+
* document.body and would not inherit variables scoped to the shell. */
|
|
1938
|
+
applyTheme(options) {
|
|
1939
|
+
const root = document.documentElement;
|
|
1940
|
+
if (options.primaryColor)
|
|
1941
|
+
root.style.setProperty('--fable-primary', options.primaryColor);
|
|
1942
|
+
if (options.toolbarGroupBackground)
|
|
1943
|
+
root.style.setProperty('--fable-tgrp-bg', options.toolbarGroupBackground);
|
|
1944
|
+
if (options.uiFontFamily)
|
|
1945
|
+
root.style.setProperty('--fable-ui-font', options.uiFontFamily);
|
|
1946
|
+
}
|
|
1535
1947
|
/** Rewrites the literal word `body` in a user-supplied `contentStyle` string to a
|
|
1536
1948
|
* selector scoped to this instance's content div. Everything else passes through
|
|
1537
1949
|
* verbatim, since `.ed` is a plain div in the host page (not an iframe) and an
|
|
@@ -1594,6 +2006,9 @@ class FableEditor {
|
|
|
1594
2006
|
this.positionImageHandles();
|
|
1595
2007
|
this.positionImgPhCtx();
|
|
1596
2008
|
this.positionTplCtx();
|
|
2009
|
+
this.positionVphCtx();
|
|
2010
|
+
this.positionVidCtx();
|
|
2011
|
+
this.positionCodeCtx();
|
|
1597
2012
|
});
|
|
1598
2013
|
this.on(this.ed, 'keydown', (e) => {
|
|
1599
2014
|
if (e.altKey && e.key === '0') {
|
|
@@ -1604,18 +2019,32 @@ class FableEditor {
|
|
|
1604
2019
|
e.preventDefault();
|
|
1605
2020
|
this.removeImgPlaceholder();
|
|
1606
2021
|
}
|
|
2022
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && this.vphActive) {
|
|
2023
|
+
e.preventDefault();
|
|
2024
|
+
this.removeVphPlaceholder();
|
|
2025
|
+
}
|
|
2026
|
+
if (e.key === '`' && !e.ctrlKey && !e.metaKey && !e.altKey)
|
|
2027
|
+
this.tryCodeTyping(e);
|
|
1607
2028
|
});
|
|
1608
2029
|
this.on(this.ed, 'paste', (e) => this.handlePaste(e));
|
|
1609
2030
|
this.on(this.ed, 'drop', (e) => {
|
|
1610
2031
|
/* content drops stay blocked (powerpaste_block_drop) — only an
|
|
1611
|
-
image file dropped onto
|
|
2032
|
+
image/video file dropped onto its matching upload placeholder is consumed */
|
|
1612
2033
|
e.preventDefault();
|
|
1613
|
-
const
|
|
1614
|
-
|
|
2034
|
+
const target = e.target;
|
|
2035
|
+
const ph = target.closest?.('.img-ph:not(.vid-ph)');
|
|
2036
|
+
if (ph && this.ed.contains(ph) && !this.options.readonly) {
|
|
2037
|
+
const file = Array.from(e.dataTransfer?.files || []).find((f) => f.type.startsWith('image/'));
|
|
2038
|
+
if (file)
|
|
2039
|
+
this.readImageFileInto(file, ph);
|
|
1615
2040
|
return;
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2041
|
+
}
|
|
2042
|
+
const vph = target.closest?.('.vid-ph');
|
|
2043
|
+
if (vph && this.ed.contains(vph) && !this.options.readonly) {
|
|
2044
|
+
const file = Array.from(e.dataTransfer?.files || []).find((f) => f.type.startsWith('video/'));
|
|
2045
|
+
if (file)
|
|
2046
|
+
this.readVideoFileInto(file, vph);
|
|
2047
|
+
}
|
|
1619
2048
|
});
|
|
1620
2049
|
this.on(this.ed, 'dragover', (e) => e.preventDefault());
|
|
1621
2050
|
this.on(this.ed, 'mousedown', (e) => {
|
|
@@ -1625,7 +2054,7 @@ class FableEditor {
|
|
|
1625
2054
|
this.selectTableForResize(table);
|
|
1626
2055
|
else
|
|
1627
2056
|
this.clearTableHandles();
|
|
1628
|
-
const ph = target.closest?.('.img-ph');
|
|
2057
|
+
const ph = target.closest?.('.img-ph:not(.vid-ph)');
|
|
1629
2058
|
if (ph && this.ed.contains(ph) && !this.options.readonly) {
|
|
1630
2059
|
e.preventDefault();
|
|
1631
2060
|
this.ed.focus();
|
|
@@ -1634,11 +2063,28 @@ class FableEditor {
|
|
|
1634
2063
|
else {
|
|
1635
2064
|
this.clearImgPlaceholderSel();
|
|
1636
2065
|
}
|
|
2066
|
+
const vph = target.closest?.('.vid-ph');
|
|
2067
|
+
if (vph && this.ed.contains(vph) && !this.options.readonly) {
|
|
2068
|
+
e.preventDefault();
|
|
2069
|
+
this.ed.focus();
|
|
2070
|
+
this.selectVphPlaceholder(vph);
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
this.clearVphPlaceholderSel();
|
|
2074
|
+
}
|
|
1637
2075
|
const tpl = target.closest?.('.tpl');
|
|
1638
2076
|
if (tpl && this.ed.contains(tpl) && !this.options.readonly)
|
|
1639
2077
|
this.selectTemplate(tpl);
|
|
1640
2078
|
else
|
|
1641
2079
|
this.clearTplSel();
|
|
2080
|
+
const codeBlk = target.closest?.('pre.code-block');
|
|
2081
|
+
if (codeBlk && this.ed.contains(codeBlk) && !this.options.readonly) {
|
|
2082
|
+
e.preventDefault();
|
|
2083
|
+
this.selectCodeEl(codeBlk);
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
this.clearCodeSel();
|
|
2087
|
+
}
|
|
1642
2088
|
const img = target.closest?.('img');
|
|
1643
2089
|
if (img && this.ed.contains(img) && !this.options.readonly) {
|
|
1644
2090
|
e.preventDefault();
|
|
@@ -1647,6 +2093,14 @@ class FableEditor {
|
|
|
1647
2093
|
else {
|
|
1648
2094
|
this.clearImageHandles();
|
|
1649
2095
|
}
|
|
2096
|
+
const vid = target.closest?.('video, .video-embed');
|
|
2097
|
+
if (vid && this.ed.contains(vid) && !this.options.readonly) {
|
|
2098
|
+
e.preventDefault();
|
|
2099
|
+
this.selectVideo(vid);
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
this.clearVidHandles();
|
|
2103
|
+
}
|
|
1650
2104
|
});
|
|
1651
2105
|
this.on(this.ed, 'mouseover', (e) => {
|
|
1652
2106
|
if (this.options.readonly)
|
|
@@ -1674,12 +2128,18 @@ class FableEditor {
|
|
|
1674
2128
|
this.positionImageHandles();
|
|
1675
2129
|
this.positionImgPhCtx();
|
|
1676
2130
|
this.positionTplCtx();
|
|
2131
|
+
this.positionVphCtx();
|
|
2132
|
+
this.positionVidCtx();
|
|
2133
|
+
this.positionCodeCtx();
|
|
1677
2134
|
});
|
|
1678
2135
|
this.onWin('scroll', () => {
|
|
1679
2136
|
this.positionTableHandles();
|
|
1680
2137
|
this.positionImageHandles();
|
|
1681
2138
|
this.positionImgPhCtx();
|
|
1682
2139
|
this.positionTplCtx();
|
|
2140
|
+
this.positionVphCtx();
|
|
2141
|
+
this.positionVidCtx();
|
|
2142
|
+
this.positionCodeCtx();
|
|
1683
2143
|
this.repositionSelToolbar();
|
|
1684
2144
|
}, true);
|
|
1685
2145
|
this.onWin('resize', () => {
|
|
@@ -1687,8 +2147,19 @@ class FableEditor {
|
|
|
1687
2147
|
this.positionImageHandles();
|
|
1688
2148
|
this.positionImgPhCtx();
|
|
1689
2149
|
this.positionTplCtx();
|
|
2150
|
+
this.positionVphCtx();
|
|
2151
|
+
this.positionVidCtx();
|
|
2152
|
+
this.positionCodeCtx();
|
|
1690
2153
|
this.repositionSelToolbar();
|
|
2154
|
+
this.updateToolbarOverflow();
|
|
1691
2155
|
});
|
|
2156
|
+
/* container-driven, not just viewport-driven: an editor inside a resizable
|
|
2157
|
+
panel collapses/expands its toolbar as its own width changes */
|
|
2158
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2159
|
+
const ro = new ResizeObserver(() => this.updateToolbarOverflow());
|
|
2160
|
+
ro.observe(this.shell);
|
|
2161
|
+
this.listeners.push(() => ro.disconnect());
|
|
2162
|
+
}
|
|
1692
2163
|
this.on(this.imgInput, 'change', () => {
|
|
1693
2164
|
const file = this.imgInput.files?.[0];
|
|
1694
2165
|
if (!file)
|
|
@@ -1707,6 +2178,24 @@ class FableEditor {
|
|
|
1707
2178
|
};
|
|
1708
2179
|
fr.readAsDataURL(file);
|
|
1709
2180
|
});
|
|
2181
|
+
this.on(this.vidInput, 'change', () => {
|
|
2182
|
+
const file = this.vidInput.files?.[0];
|
|
2183
|
+
if (!file)
|
|
2184
|
+
return;
|
|
2185
|
+
const vph = this.vphUploadTarget;
|
|
2186
|
+
this.vphUploadTarget = null;
|
|
2187
|
+
if (vph && this.ed.contains(vph)) {
|
|
2188
|
+
this.readVideoFileInto(file, vph);
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
const fr = new FileReader();
|
|
2192
|
+
fr.onload = () => {
|
|
2193
|
+
this.restoreSel();
|
|
2194
|
+
document.execCommand('insertHTML', false, `<video controls src="${fr.result}" title="${file.name.replace(/"/g, '')}"></video>`);
|
|
2195
|
+
this.onChange();
|
|
2196
|
+
};
|
|
2197
|
+
fr.readAsDataURL(file);
|
|
2198
|
+
});
|
|
1710
2199
|
this.on(this.docInput, 'change', () => {
|
|
1711
2200
|
const file = this.docInput.files?.[0];
|
|
1712
2201
|
if (!file)
|
|
@@ -1723,6 +2212,9 @@ class FableEditor {
|
|
|
1723
2212
|
(this.phCtx && this.phCtx.contains(e.target)) ||
|
|
1724
2213
|
(this.tplCtx && this.tplCtx.contains(e.target)) ||
|
|
1725
2214
|
(this.selCtx && this.selCtx.contains(e.target)) ||
|
|
2215
|
+
(this.vphCtx && this.vphCtx.contains(e.target)) ||
|
|
2216
|
+
(this.vidCtx && this.vidCtx.contains(e.target)) ||
|
|
2217
|
+
(this.codeCtx && this.codeCtx.contains(e.target)) ||
|
|
1726
2218
|
(this.openPop && this.openPop.contains(e.target)) ||
|
|
1727
2219
|
(this.openSubEl && this.openSubEl.contains(e.target)))
|
|
1728
2220
|
return;
|
|
@@ -1730,6 +2222,9 @@ class FableEditor {
|
|
|
1730
2222
|
this.clearImageHandles();
|
|
1731
2223
|
this.clearImgPlaceholderSel();
|
|
1732
2224
|
this.clearTplSel();
|
|
2225
|
+
this.clearVphPlaceholderSel();
|
|
2226
|
+
this.clearVidHandles();
|
|
2227
|
+
this.clearCodeSel();
|
|
1733
2228
|
this.clearSelToolbar();
|
|
1734
2229
|
});
|
|
1735
2230
|
}
|
|
@@ -1756,6 +2251,9 @@ class FableEditor {
|
|
|
1756
2251
|
this.clearImageHandles();
|
|
1757
2252
|
this.clearImgPlaceholderSel();
|
|
1758
2253
|
this.clearTplSel();
|
|
2254
|
+
this.clearVphPlaceholderSel();
|
|
2255
|
+
this.clearVidHandles();
|
|
2256
|
+
this.clearCodeSel();
|
|
1759
2257
|
this.clearSelToolbar();
|
|
1760
2258
|
this.onChange();
|
|
1761
2259
|
}
|
|
@@ -1818,6 +2316,9 @@ class FableEditor {
|
|
|
1818
2316
|
this.clearImageHandles();
|
|
1819
2317
|
this.clearImgPlaceholderSel();
|
|
1820
2318
|
this.clearTplSel();
|
|
2319
|
+
this.clearVphPlaceholderSel();
|
|
2320
|
+
this.clearVidHandles();
|
|
2321
|
+
this.clearCodeSel();
|
|
1821
2322
|
this.clearSelToolbar();
|
|
1822
2323
|
this.listeners.forEach((fn) => fn());
|
|
1823
2324
|
this.listeners = [];
|
|
@@ -1825,6 +2326,8 @@ class FableEditor {
|
|
|
1825
2326
|
this.shell.parentNode.removeChild(this.shell);
|
|
1826
2327
|
if (this.imgInput && this.imgInput.parentNode)
|
|
1827
2328
|
this.imgInput.parentNode.removeChild(this.imgInput);
|
|
2329
|
+
if (this.vidInput && this.vidInput.parentNode)
|
|
2330
|
+
this.vidInput.parentNode.removeChild(this.vidInput);
|
|
1828
2331
|
if (this.docInput && this.docInput.parentNode)
|
|
1829
2332
|
this.docInput.parentNode.removeChild(this.docInput);
|
|
1830
2333
|
this.contentStyleEl = null;
|
|
@@ -2015,14 +2518,14 @@ class FableEditor {
|
|
|
2015
2518
|
sub.style.top = y + 'px';
|
|
2016
2519
|
this.openSubEl = sub;
|
|
2017
2520
|
}
|
|
2018
|
-
popup(anchor, build) {
|
|
2521
|
+
popup(anchor, build, cls) {
|
|
2019
2522
|
if (this.openPop && this.popAnchor === anchor) {
|
|
2020
2523
|
this.closePop();
|
|
2021
2524
|
return;
|
|
2022
2525
|
}
|
|
2023
2526
|
this.closePop();
|
|
2024
2527
|
const el = document.createElement('div');
|
|
2025
|
-
el.className = 'pop';
|
|
2528
|
+
el.className = 'pop' + (cls ? ' ' + cls : '');
|
|
2026
2529
|
el.dir = this.dir();
|
|
2027
2530
|
build(el);
|
|
2028
2531
|
document.body.appendChild(el);
|
|
@@ -2204,53 +2707,61 @@ class FableEditor {
|
|
|
2204
2707
|
el.appendChild(grid);
|
|
2205
2708
|
});
|
|
2206
2709
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
grid.appendChild(b);
|
|
2220
|
-
});
|
|
2221
|
-
el.appendChild(grid);
|
|
2222
|
-
el.insertAdjacentHTML('beforeend', '<div class="sep"></div>');
|
|
2223
|
-
const foot = document.createElement('div');
|
|
2224
|
-
foot.className = 'cfoot';
|
|
2225
|
-
const cur = document.createElement('div');
|
|
2226
|
-
cur.className = 'cswatch cur';
|
|
2227
|
-
cur.title = this.t('currentcolor');
|
|
2228
|
-
const curColor = kind === 'fore' ? this.foreColor : this.backColor;
|
|
2229
|
-
cur.style.background = /^#[0-9a-f]{6}$/i.test(curColor) ? curColor : '#000000';
|
|
2230
|
-
cur.innerHTML = IC.check;
|
|
2231
|
-
const nocolor = document.createElement('button');
|
|
2232
|
-
nocolor.type = 'button';
|
|
2233
|
-
nocolor.className = 'cswatch nocolor';
|
|
2234
|
-
nocolor.title = this.t('removecolor');
|
|
2235
|
-
nocolor.addEventListener('click', () => {
|
|
2236
|
-
this.closePop();
|
|
2237
|
-
this.applyColor(kind, null);
|
|
2238
|
-
});
|
|
2239
|
-
const wheel = document.createElement('label');
|
|
2240
|
-
wheel.className = 'cswatch cwheel';
|
|
2241
|
-
wheel.title = this.t('customcolor');
|
|
2242
|
-
wheel.innerHTML = IC.palette;
|
|
2243
|
-
const picker = document.createElement('input');
|
|
2244
|
-
picker.type = 'color';
|
|
2245
|
-
picker.value = /^#[0-9a-f]{6}$/i.test(curColor) ? curColor : '#000000';
|
|
2246
|
-
picker.addEventListener('change', () => {
|
|
2710
|
+
/** Shared color-picker popup body — swatch grid plus the current-color /
|
|
2711
|
+
* no-color / custom-picker footer — so the fore/back toolbar menus and the
|
|
2712
|
+
* table cell-background menu all show the identical popup. */
|
|
2713
|
+
buildPaletteInto(el, curColor, apply) {
|
|
2714
|
+
const grid = document.createElement('div');
|
|
2715
|
+
grid.className = 'cpal';
|
|
2716
|
+
COLORS.forEach((c) => {
|
|
2717
|
+
const b = document.createElement('button');
|
|
2718
|
+
b.type = 'button';
|
|
2719
|
+
b.style.background = c;
|
|
2720
|
+
b.title = c;
|
|
2721
|
+
b.addEventListener('click', () => {
|
|
2247
2722
|
this.closePop();
|
|
2248
|
-
|
|
2723
|
+
apply(c);
|
|
2249
2724
|
});
|
|
2250
|
-
|
|
2251
|
-
foot.append(cur, nocolor, wheel);
|
|
2252
|
-
el.appendChild(foot);
|
|
2725
|
+
grid.appendChild(b);
|
|
2253
2726
|
});
|
|
2727
|
+
el.appendChild(grid);
|
|
2728
|
+
el.insertAdjacentHTML('beforeend', '<div class="sep"></div>');
|
|
2729
|
+
const foot = document.createElement('div');
|
|
2730
|
+
foot.className = 'cfoot';
|
|
2731
|
+
const hex = /^#[0-9a-f]{6}$/i.test(curColor) ? curColor : '#000000';
|
|
2732
|
+
const cur = document.createElement('div');
|
|
2733
|
+
cur.className = 'cswatch cur';
|
|
2734
|
+
cur.title = this.t('currentcolor');
|
|
2735
|
+
cur.style.background = hex;
|
|
2736
|
+
cur.innerHTML = IC.check;
|
|
2737
|
+
const nocolor = document.createElement('button');
|
|
2738
|
+
nocolor.type = 'button';
|
|
2739
|
+
nocolor.className = 'cswatch nocolor';
|
|
2740
|
+
nocolor.title = this.t('removecolor');
|
|
2741
|
+
nocolor.addEventListener('click', () => {
|
|
2742
|
+
this.closePop();
|
|
2743
|
+
apply(null);
|
|
2744
|
+
});
|
|
2745
|
+
const wheel = document.createElement('label');
|
|
2746
|
+
wheel.className = 'cswatch cwheel';
|
|
2747
|
+
wheel.title = this.t('customcolor');
|
|
2748
|
+
wheel.innerHTML = IC.palette;
|
|
2749
|
+
const picker = document.createElement('input');
|
|
2750
|
+
picker.type = 'color';
|
|
2751
|
+
picker.value = hex;
|
|
2752
|
+
picker.addEventListener('change', () => {
|
|
2753
|
+
this.closePop();
|
|
2754
|
+
apply(picker.value);
|
|
2755
|
+
});
|
|
2756
|
+
wheel.appendChild(picker);
|
|
2757
|
+
foot.append(cur, nocolor, wheel);
|
|
2758
|
+
el.appendChild(foot);
|
|
2759
|
+
}
|
|
2760
|
+
colorMenu(anchor, kind) {
|
|
2761
|
+
this.popup(anchor, (el) => {
|
|
2762
|
+
const curColor = kind === 'fore' ? this.foreColor : this.backColor;
|
|
2763
|
+
this.buildPaletteInto(el, curColor, (c) => this.applyColor(kind, c));
|
|
2764
|
+
}, kind === 'fore' ? 'pop-forecolor' : 'pop-backcolor');
|
|
2254
2765
|
}
|
|
2255
2766
|
applyColor(kind, c) {
|
|
2256
2767
|
if (kind === 'fore') {
|
|
@@ -2413,11 +2924,12 @@ class FableEditor {
|
|
|
2413
2924
|
applyBlockStyle(prop, value) {
|
|
2414
2925
|
this.restoreSel();
|
|
2415
2926
|
const blocks = this.selectedBlocks();
|
|
2927
|
+
const cssProp = prop === 'lineHeight' ? 'line-height' : prop === 'wordSpacing' ? 'word-spacing' : 'letter-spacing';
|
|
2416
2928
|
blocks.forEach((b) => {
|
|
2417
2929
|
if (value)
|
|
2418
2930
|
b.style[prop] = value;
|
|
2419
2931
|
else
|
|
2420
|
-
b.style.removeProperty(
|
|
2932
|
+
b.style.removeProperty(cssProp);
|
|
2421
2933
|
});
|
|
2422
2934
|
this.saveSel();
|
|
2423
2935
|
this.onChange();
|
|
@@ -2453,6 +2965,19 @@ class FableEditor {
|
|
|
2453
2965
|
]);
|
|
2454
2966
|
});
|
|
2455
2967
|
}
|
|
2968
|
+
letterSpacingMenu(anchor) {
|
|
2969
|
+
const cur = this.currentBlockStyle('letterSpacing');
|
|
2970
|
+
this.popup(anchor, (el) => {
|
|
2971
|
+
this.menuItems(el, [
|
|
2972
|
+
{ label: this.t('normal'), on: !cur, action: () => this.applyBlockStyle('letterSpacing', '') },
|
|
2973
|
+
...LETTER_SPACINGS.map((v) => ({
|
|
2974
|
+
label: v,
|
|
2975
|
+
on: cur === v,
|
|
2976
|
+
action: () => this.applyBlockStyle('letterSpacing', v)
|
|
2977
|
+
}))
|
|
2978
|
+
]);
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2456
2981
|
closestAnchor() {
|
|
2457
2982
|
const s = window.getSelection();
|
|
2458
2983
|
let node = s && this.ed.contains(s.anchorNode) ? s.anchorNode : null;
|
|
@@ -2626,7 +3151,7 @@ class FableEditor {
|
|
|
2626
3151
|
* (the quick color chips); everything else yields one. */
|
|
2627
3152
|
buildToolbarRegistry() {
|
|
2628
3153
|
const tableBtn = this.tbtn(IC.tableic, this.t('quicktable'), () => this.tableGrid(tableBtn));
|
|
2629
|
-
|
|
3154
|
+
const registry = {
|
|
2630
3155
|
undo: () => this.tbtn(IC.undo, this.t('undo'), () => this.exec('undo')),
|
|
2631
3156
|
redo: () => this.tbtn(IC.redo, this.t('redo'), () => this.exec('redo')),
|
|
2632
3157
|
preview: () => this.tbtn(IC.prevw, this.t('preview'), () => this.previewDlg()),
|
|
@@ -2653,20 +3178,32 @@ class FableEditor {
|
|
|
2653
3178
|
indent: () => this.tbtn(IC.indent, this.t('indent'), () => this.exec('indent')),
|
|
2654
3179
|
link: () => this.tbtn(IC.link, this.t('linkttl'), () => this.linkDlg()),
|
|
2655
3180
|
blockquote: () => this.tbtn(IC.quote, this.t('quote'), () => this.toggleBlock('blockquote'), 'blockquote'),
|
|
3181
|
+
code: () => this.tbtn(IC.inlinecodeic, this.t('code'), () => this.toggleInlineCode(), 'code'),
|
|
3182
|
+
codesample: () => this.tbtn(IC.codesampleic, this.t('codesample'), () => this.codeDlg()),
|
|
2656
3183
|
changecase: () => this.menuTbtn(IC.caseic, this.t('changecase'), (b) => this.caseMenu(b)),
|
|
2657
3184
|
lineheight: () => this.menuTbtn(IC.lineheight, this.t('lineheight'), (b) => this.lineHeightMenu(b)),
|
|
2658
3185
|
wordspacing: () => this.menuTbtn(IC.wordspacing, this.t('wordspacing'), (b) => this.wordSpacingMenu(b)),
|
|
3186
|
+
letterspacing: () => this.menuTbtn(IC.letterspacing, this.t('letterspacing'), (b) => this.letterSpacingMenu(b)),
|
|
2659
3187
|
removeformat: () => this.tbtn(IC.clearformatic, this.t('removeformat'), () => this.exec('removeFormat')),
|
|
2660
3188
|
blocks: () => this.tsel('w-block', 'blocksel', this.t('para'), (b) => this.blocksMenu(b)),
|
|
2661
3189
|
ltr: () => this.tbtn(IC.ltr, this.t('ltr'), () => this.setDir('ltr'), 'ltr'),
|
|
2662
3190
|
rtl: () => this.tbtn(IC.rtl, this.t('rtl'), () => this.setDir('rtl'), 'rtl'),
|
|
2663
3191
|
quickimage: () => this.tbtn(IC.image, this.t('quickimage'), () => this.insertImagePlaceholder()),
|
|
3192
|
+
quickvideo: () => this.tbtn(IC.video, this.t('quickvideo'), () => this.videoDlg()),
|
|
2664
3193
|
quicktable: () => tableBtn,
|
|
2665
3194
|
template: () => this.menuTbtn(IC.templateic, this.t('template'), (b) => this.templateMenu(b)),
|
|
2666
3195
|
charmap: () => this.tbtn(IC.charic, this.t('charmap'), () => this.charMap()),
|
|
3196
|
+
emoji: () => this.tbtn(IC.emojiic, this.t('emoji'), () => this.emojiMap()),
|
|
2667
3197
|
fullscreen: () => this.tbtn(IC.fullscreen, this.t('fullscreen'), () => this.toggleFullscreen(), 'fullscreen'),
|
|
2668
3198
|
sourcecode: () => this.tbtn(IC.srcic, this.t('sourcecode'), () => this.sourceDlg())
|
|
2669
3199
|
};
|
|
3200
|
+
/* TinyMCE-compatible aliases so common TinyMCE toolbar strings
|
|
3201
|
+
(e.g. "undo redo | styles | ... | link image media") work as-is */
|
|
3202
|
+
registry.styles = registry.blocks;
|
|
3203
|
+
registry.image = registry.quickimage;
|
|
3204
|
+
registry.media = registry.quickvideo;
|
|
3205
|
+
registry.table = registry.quicktable;
|
|
3206
|
+
return registry;
|
|
2670
3207
|
}
|
|
2671
3208
|
buildToolbar() {
|
|
2672
3209
|
this.toolbar.innerHTML = '';
|
|
@@ -2689,7 +3226,40 @@ class FableEditor {
|
|
|
2689
3226
|
if (elements.length)
|
|
2690
3227
|
this.toolbar.appendChild(this.group(...elements));
|
|
2691
3228
|
});
|
|
3229
|
+
this.tbrMore = this.tbtn(IC.more, this.t('more'), () => {
|
|
3230
|
+
this.tbrExpanded = !this.tbrExpanded;
|
|
3231
|
+
this.updateToolbarOverflow();
|
|
3232
|
+
});
|
|
3233
|
+
this.tbrMore.classList.add('tbr-more');
|
|
3234
|
+
this.toolbar.appendChild(this.tbrMore);
|
|
2692
3235
|
this.syncColorSwatches(this.toolbar);
|
|
3236
|
+
this.updateToolbarOverflow();
|
|
3237
|
+
}
|
|
3238
|
+
/** xs/sm/md editors (< 992px wide) collapse an overflowing toolbar to one row +
|
|
3239
|
+
* "…" expander; at lg widths and up the toolbar wraps to multiple rows as before. */
|
|
3240
|
+
static TBR_COLLAPSE_MAX = 992;
|
|
3241
|
+
/** Responsive one-line toolbar: on mobile/tablet widths (or a narrow host
|
|
3242
|
+
* container) a toolbar that can't fit on a single row collapses to one
|
|
3243
|
+
* clipped row plus a trailing "…" button that expands/collapses the full
|
|
3244
|
+
* set. Driven by the editor's own width, so it also applies to narrow
|
|
3245
|
+
* panels on desktop. */
|
|
3246
|
+
updateToolbarOverflow() {
|
|
3247
|
+
const tb = this.toolbar;
|
|
3248
|
+
if (!this.tbrMore)
|
|
3249
|
+
return;
|
|
3250
|
+
tb.classList.add('tbr-measure');
|
|
3251
|
+
const overflows = tb.scrollWidth > tb.clientWidth + 1;
|
|
3252
|
+
tb.classList.remove('tbr-measure');
|
|
3253
|
+
const narrow = this.shell.offsetWidth < FableEditor.TBR_COLLAPSE_MAX;
|
|
3254
|
+
if (!overflows || !narrow) {
|
|
3255
|
+
this.tbrExpanded = false;
|
|
3256
|
+
tb.classList.remove('collapsible', 'expanded');
|
|
3257
|
+
this.tbrMore.classList.remove('on');
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
tb.classList.add('collapsible');
|
|
3261
|
+
tb.classList.toggle('expanded', this.tbrExpanded);
|
|
3262
|
+
this.tbrMore.classList.toggle('on', this.tbrExpanded);
|
|
2693
3263
|
}
|
|
2694
3264
|
/* ---------------------------------------------------------- menubar */
|
|
2695
3265
|
mItem(label, icon, action, shortcut) {
|
|
@@ -2733,11 +3303,14 @@ class FableEditor {
|
|
|
2733
3303
|
insert: [
|
|
2734
3304
|
this.mItem('link', IC.link, () => this.linkDlg()),
|
|
2735
3305
|
this.mItem('image', IC.image, () => this.insertImagePlaceholder()),
|
|
3306
|
+
this.mItem('video', IC.video, () => this.videoDlg()),
|
|
2736
3307
|
{ label: this.t('inserttable'), icon: IC.tableic, action: () => this.tableGrid(this.menubar.querySelector('[data-menu-key="table"]') || this.menubar) },
|
|
3308
|
+
this.mItem('codesample', IC.codesampleic, () => this.codeDlg()),
|
|
2737
3309
|
{ label: this.t('template'), icon: IC.templateic, subBuild: (el) => this.buildTemplatePickInto(el) },
|
|
2738
3310
|
this.mItem('hr', IC.hric, () => this.exec('insertHorizontalRule')),
|
|
2739
3311
|
'|',
|
|
2740
3312
|
this.mItem('charmap', IC.charic, () => this.charMap()),
|
|
3313
|
+
this.mItem('emoji', IC.emojiic, () => this.emojiMap()),
|
|
2741
3314
|
this.mItem('datetime', IC.dateic, () => this.exec('insertText', new Date().toLocaleString(this.lang === 'ar' ? 'ar-AE' : 'en-GB'))),
|
|
2742
3315
|
'|',
|
|
2743
3316
|
{
|
|
@@ -2758,6 +3331,7 @@ class FableEditor {
|
|
|
2758
3331
|
this.mItem('strikethrough', IC.strikethrough, () => this.exec('strikeThrough')),
|
|
2759
3332
|
this.mItem('superscript', IC.superscriptic, () => this.exec('superscript')),
|
|
2760
3333
|
this.mItem('subscript', IC.subscriptic, () => this.exec('subscript')),
|
|
3334
|
+
this.mItem('code', IC.inlinecodeic, () => this.toggleInlineCode()),
|
|
2761
3335
|
'|',
|
|
2762
3336
|
this.mItem('lowercase', IC.lowercaseic, () => this.transformCase('lower')),
|
|
2763
3337
|
this.mItem('uppercase', IC.uppercaseic, () => this.transformCase('upper')),
|
|
@@ -2771,7 +3345,10 @@ class FableEditor {
|
|
|
2771
3345
|
'|',
|
|
2772
3346
|
{
|
|
2773
3347
|
label: this.t('cell'),
|
|
2774
|
-
sub: [
|
|
3348
|
+
sub: [
|
|
3349
|
+
{ label: this.t('cellprops'), icon: IC.tableic, action: () => this.cellPropsDlg() },
|
|
3350
|
+
{ label: this.t('cellbg'), icon: IC.palette, subBuild: (el) => this.buildCellFillInto(el) }
|
|
3351
|
+
]
|
|
2775
3352
|
},
|
|
2776
3353
|
{
|
|
2777
3354
|
label: this.t('row'),
|
|
@@ -2780,6 +3357,9 @@ class FableEditor {
|
|
|
2780
3357
|
{ label: this.t('rowbelow'), icon: IC.rowafter, action: () => this.tableOp('rowbelow'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
2781
3358
|
{ label: this.t('delrow'), icon: IC.rowdelete, action: () => this.tableOp('delrow'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
2782
3359
|
'|',
|
|
3360
|
+
{ label: this.t('moverowup'), icon: IC.moverowup, action: () => this.tableMove('rowup'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
3361
|
+
{ label: this.t('moverowdown'), icon: IC.moverowdown, action: () => this.tableMove('rowdown'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
3362
|
+
'|',
|
|
2783
3363
|
{ label: this.t('rowprops'), icon: IC.tableic, action: () => this.rowPropsDlg(), hover: (on) => this.setOpTarget(on ? 'row' : null) }
|
|
2784
3364
|
]
|
|
2785
3365
|
},
|
|
@@ -2790,6 +3370,9 @@ class FableEditor {
|
|
|
2790
3370
|
{ label: this.t('colafter'), icon: IC.colafter, action: () => this.tableOp('colafter'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
2791
3371
|
{ label: this.t('delcol'), icon: IC.coldelete, action: () => this.tableOp('delcol'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
2792
3372
|
'|',
|
|
3373
|
+
{ label: this.t('movecolleft'), icon: IC.movecolleft, action: () => this.tableMove('colleft'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
3374
|
+
{ label: this.t('movecolright'), icon: IC.movecolright, action: () => this.tableMove('colright'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
3375
|
+
'|',
|
|
2793
3376
|
{ label: this.t('colprops'), icon: IC.tableic, action: () => this.colPropsDlg(), hover: (on) => this.setOpTarget(on ? 'col' : null) }
|
|
2794
3377
|
]
|
|
2795
3378
|
},
|
|
@@ -2900,6 +3483,93 @@ class FableEditor {
|
|
|
2900
3483
|
this.onChange();
|
|
2901
3484
|
this.positionTableHandles();
|
|
2902
3485
|
}
|
|
3486
|
+
/** Reorders the current row/column: 'rowup'/'rowdown' swap the caret's row with
|
|
3487
|
+
* its neighbour, 'colleft'/'colright' move the caret's column one position in
|
|
3488
|
+
* DOM order across every row. The caret travels with the moved cell. */
|
|
3489
|
+
tableMove(op) {
|
|
3490
|
+
this.restoreSel();
|
|
3491
|
+
const cell = this.currentCell();
|
|
3492
|
+
if (!cell)
|
|
3493
|
+
return;
|
|
3494
|
+
const row = cell.parentElement;
|
|
3495
|
+
const table = cell.closest('table');
|
|
3496
|
+
const idx = Array.from(row.children).indexOf(cell);
|
|
3497
|
+
if (op === 'rowup') {
|
|
3498
|
+
const prev = row.previousElementSibling;
|
|
3499
|
+
if (prev)
|
|
3500
|
+
prev.before(row);
|
|
3501
|
+
}
|
|
3502
|
+
if (op === 'rowdown') {
|
|
3503
|
+
const next = row.nextElementSibling;
|
|
3504
|
+
if (next)
|
|
3505
|
+
next.after(row);
|
|
3506
|
+
}
|
|
3507
|
+
if (op === 'colleft' && idx > 0) {
|
|
3508
|
+
table.querySelectorAll('tr').forEach((tr) => {
|
|
3509
|
+
const c = tr.children[idx];
|
|
3510
|
+
const prev = tr.children[idx - 1];
|
|
3511
|
+
if (c && prev)
|
|
3512
|
+
prev.before(c);
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
if (op === 'colright') {
|
|
3516
|
+
table.querySelectorAll('tr').forEach((tr) => {
|
|
3517
|
+
const c = tr.children[idx];
|
|
3518
|
+
const next = tr.children[idx + 1];
|
|
3519
|
+
if (c && next)
|
|
3520
|
+
next.after(c);
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
/* moving a node resets live ranges inside it (they collapse to the old
|
|
3524
|
+
parent), so put the caret back into the moved cell explicitly — this is
|
|
3525
|
+
what lets the user click "move" repeatedly to walk a row/column along */
|
|
3526
|
+
const range = document.createRange();
|
|
3527
|
+
range.selectNodeContents(cell);
|
|
3528
|
+
range.collapse(true);
|
|
3529
|
+
const s = window.getSelection();
|
|
3530
|
+
if (s) {
|
|
3531
|
+
s.removeAllRanges();
|
|
3532
|
+
s.addRange(range);
|
|
3533
|
+
}
|
|
3534
|
+
this.saveSel();
|
|
3535
|
+
this.onChange();
|
|
3536
|
+
this.positionTableHandles();
|
|
3537
|
+
this.positionCellMarker();
|
|
3538
|
+
}
|
|
3539
|
+
/** Align the whole table via auto margins — same semantics as the align
|
|
3540
|
+
* field in the table-properties dialog. */
|
|
3541
|
+
alignTable(align) {
|
|
3542
|
+
const table = this.tblActive;
|
|
3543
|
+
if (!table)
|
|
3544
|
+
return;
|
|
3545
|
+
table.style.marginLeft = align === 'left' ? '0' : 'auto';
|
|
3546
|
+
table.style.marginRight = align === 'right' ? '0' : 'auto';
|
|
3547
|
+
this.onChange();
|
|
3548
|
+
this.positionTableHandles();
|
|
3549
|
+
}
|
|
3550
|
+
/** Color grid applying a background to the caret's cell — the direct way to
|
|
3551
|
+
* build header rows (the toolbar's backcolor highlights text, not cells). */
|
|
3552
|
+
buildCellFillInto(el) {
|
|
3553
|
+
const apply = (c) => {
|
|
3554
|
+
const cell = this.currentCell();
|
|
3555
|
+
if (!cell) {
|
|
3556
|
+
alert(this.t('nocell'));
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
this.setOrClear(cell, 'background-color', c || '');
|
|
3560
|
+
this.onChange();
|
|
3561
|
+
};
|
|
3562
|
+
const cell = this.currentCell();
|
|
3563
|
+
this.buildPaletteInto(el, this.cssToHex(cell ? cell.style.backgroundColor : ''), apply);
|
|
3564
|
+
}
|
|
3565
|
+
/** style.backgroundColor reads back as rgb(...) — normalise to #rrggbb for the picker. */
|
|
3566
|
+
cssToHex(c) {
|
|
3567
|
+
const m = c.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
|
|
3568
|
+
return m ? '#' + m.slice(1, 4).map((n) => (+n).toString(16).padStart(2, '0')).join('') : c;
|
|
3569
|
+
}
|
|
3570
|
+
cellFillMenu(anchor) {
|
|
3571
|
+
this.popup(anchor, (el) => this.buildCellFillInto(el), 'pop-cellbg');
|
|
3572
|
+
}
|
|
2903
3573
|
applyTableProps(table, vals) {
|
|
2904
3574
|
if (vals.width)
|
|
2905
3575
|
table.style.width = /^\d+$/.test(vals.width) ? vals.width + 'px' : vals.width;
|
|
@@ -3308,22 +3978,53 @@ class FableEditor {
|
|
|
3308
3978
|
body.appendChild(box);
|
|
3309
3979
|
});
|
|
3310
3980
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3981
|
+
/** Two-pane picker shared by the special-character map and the emoji map:
|
|
3982
|
+
* fixed category tabs on the left, the active category's glyphs on the right. */
|
|
3983
|
+
glyphPickerDlg(title, cats, emoji) {
|
|
3984
|
+
this.saveSel();
|
|
3985
|
+
this.dialog(title, (body) => {
|
|
3986
|
+
const wrap = document.createElement('div');
|
|
3987
|
+
wrap.className = 'chmap';
|
|
3988
|
+
const tabs = document.createElement('div');
|
|
3989
|
+
tabs.className = 'chtabs';
|
|
3990
|
+
const panel = document.createElement('div');
|
|
3991
|
+
panel.className = 'chpanel';
|
|
3992
|
+
const grid = document.createElement('div');
|
|
3993
|
+
grid.className = 'chgrid' + (emoji ? ' emgrid' : '');
|
|
3994
|
+
panel.appendChild(grid);
|
|
3995
|
+
const show = (i) => {
|
|
3996
|
+
tabs.querySelectorAll('button').forEach((b, bi) => b.classList.toggle('on', bi === i));
|
|
3997
|
+
grid.innerHTML = '';
|
|
3998
|
+
cats[i].chars.forEach((ch) => {
|
|
3999
|
+
const b = document.createElement('button');
|
|
4000
|
+
b.type = 'button';
|
|
4001
|
+
b.textContent = ch;
|
|
4002
|
+
b.onclick = () => {
|
|
4003
|
+
this.closeDlg();
|
|
4004
|
+
this.exec('insertText', ch);
|
|
4005
|
+
};
|
|
4006
|
+
grid.appendChild(b);
|
|
4007
|
+
});
|
|
4008
|
+
panel.scrollTop = 0;
|
|
4009
|
+
};
|
|
4010
|
+
cats.forEach((c, i) => {
|
|
3316
4011
|
const b = document.createElement('button');
|
|
3317
|
-
b.
|
|
3318
|
-
b.
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
};
|
|
3322
|
-
g.appendChild(b);
|
|
4012
|
+
b.type = 'button';
|
|
4013
|
+
b.textContent = this.t(c.key);
|
|
4014
|
+
b.addEventListener('click', () => show(i));
|
|
4015
|
+
tabs.appendChild(b);
|
|
3323
4016
|
});
|
|
3324
|
-
|
|
4017
|
+
wrap.append(tabs, panel);
|
|
4018
|
+
body.appendChild(wrap);
|
|
4019
|
+
show(0);
|
|
3325
4020
|
});
|
|
3326
4021
|
}
|
|
4022
|
+
charMap() {
|
|
4023
|
+
this.glyphPickerDlg(this.t('charmap').replace('…', ''), CHAR_CATEGORIES, false);
|
|
4024
|
+
}
|
|
4025
|
+
emojiMap() {
|
|
4026
|
+
this.glyphPickerDlg(this.t('emoji').replace('…', ''), EMOJI_CATEGORIES, true);
|
|
4027
|
+
}
|
|
3327
4028
|
printDoc() {
|
|
3328
4029
|
const w = window.open('', '_blank');
|
|
3329
4030
|
if (!w)
|
|
@@ -3389,9 +4090,34 @@ class FableEditor {
|
|
|
3389
4090
|
el.append(this.ctxBtn(IC.uploadic, this.t('uploadimg'), () => {
|
|
3390
4091
|
this.phUploadTarget = this.phActive;
|
|
3391
4092
|
this.pickImage();
|
|
3392
|
-
}), this.ctxBtn(IC.link, this.t('imagelink'), () => this.renderPhCtxUrlInput())
|
|
4093
|
+
}), this.ctxBtn(IC.link, this.t('imagelink'), () => this.renderPhCtxUrlInput()));
|
|
4094
|
+
/* a template's media slot is image-first, but can host a video instead */
|
|
4095
|
+
if (this.phActive?.closest('.tpl-media')) {
|
|
4096
|
+
el.append(this.ctxBtn(IC.video, this.t('quickvideo'), () => this.swapPlaceholderKind('video')));
|
|
4097
|
+
}
|
|
4098
|
+
el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deleteimg'), () => this.removeImgPlaceholder()));
|
|
3393
4099
|
this.positionImgPhCtx();
|
|
3394
4100
|
}
|
|
4101
|
+
/** Swaps a template slot's placeholder between image and video kinds, so the
|
|
4102
|
+
* same media slot can be filled with either. */
|
|
4103
|
+
swapPlaceholderKind(to) {
|
|
4104
|
+
const ph = to === 'video' ? this.phActive : this.vphActive;
|
|
4105
|
+
if (!ph)
|
|
4106
|
+
return;
|
|
4107
|
+
const tmp = document.createElement('div');
|
|
4108
|
+
tmp.innerHTML = to === 'video' ? this.vidPhHTML() : this.imgPhHTML();
|
|
4109
|
+
const next = tmp.firstElementChild;
|
|
4110
|
+
if (to === 'video')
|
|
4111
|
+
this.clearImgPlaceholderSel();
|
|
4112
|
+
else
|
|
4113
|
+
this.clearVphPlaceholderSel();
|
|
4114
|
+
ph.replaceWith(next);
|
|
4115
|
+
if (to === 'video')
|
|
4116
|
+
this.selectVphPlaceholder(next);
|
|
4117
|
+
else
|
|
4118
|
+
this.selectImgPlaceholder(next);
|
|
4119
|
+
this.onChange();
|
|
4120
|
+
}
|
|
3395
4121
|
renderPhCtxUrlInput() {
|
|
3396
4122
|
const el = this.phCtx;
|
|
3397
4123
|
if (!el)
|
|
@@ -3514,6 +4240,741 @@ class FableEditor {
|
|
|
3514
4240
|
this.phCtx.style.left = cx + 'px';
|
|
3515
4241
|
this.phCtx.style.top = cy + 'px';
|
|
3516
4242
|
}
|
|
4243
|
+
/* ---------------------------------------------------------- video upload placeholder */
|
|
4244
|
+
pickVideo() {
|
|
4245
|
+
this.vidInput.value = '';
|
|
4246
|
+
this.vidInput.click();
|
|
4247
|
+
}
|
|
4248
|
+
vidPhHTML(id) {
|
|
4249
|
+
return `<div class="img-ph vid-ph"${id ? ` id="${id}"` : ''} contenteditable="false">${IC.video}<span>${this.t('dropvideo')}</span></div>`;
|
|
4250
|
+
}
|
|
4251
|
+
insertVideoPlaceholder() {
|
|
4252
|
+
this.restoreSel();
|
|
4253
|
+
const id = 'vid-ph-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
4254
|
+
document.execCommand('insertHTML', false, this.vidPhHTML(id));
|
|
4255
|
+
const ph = this.ed.querySelector('#' + id);
|
|
4256
|
+
if (ph) {
|
|
4257
|
+
ph.removeAttribute('id');
|
|
4258
|
+
this.selectVphPlaceholder(ph);
|
|
4259
|
+
}
|
|
4260
|
+
this.onChange();
|
|
4261
|
+
}
|
|
4262
|
+
clearVphPlaceholderSel() {
|
|
4263
|
+
this.vphCtx?.remove();
|
|
4264
|
+
this.vphCtx = null;
|
|
4265
|
+
this.vphActive?.classList.remove('active');
|
|
4266
|
+
this.vphActive = null;
|
|
4267
|
+
}
|
|
4268
|
+
removeVphPlaceholder() {
|
|
4269
|
+
const ph = this.vphActive;
|
|
4270
|
+
if (!ph)
|
|
4271
|
+
return;
|
|
4272
|
+
this.clearVphPlaceholderSel();
|
|
4273
|
+
/* like the image variant: a placeholder inside a template block IS the
|
|
4274
|
+
media slot — deleting it drops the slot, leaving a text-only block */
|
|
4275
|
+
(ph.closest('.tpl-media') || ph).remove();
|
|
4276
|
+
this.refreshState();
|
|
4277
|
+
this.onChange();
|
|
4278
|
+
}
|
|
4279
|
+
selectVphPlaceholder(ph) {
|
|
4280
|
+
if (this.vphActive === ph)
|
|
4281
|
+
return;
|
|
4282
|
+
this.clearVphPlaceholderSel();
|
|
4283
|
+
this.vphActive = ph;
|
|
4284
|
+
ph.classList.add('active');
|
|
4285
|
+
this.vphCtx = document.createElement('div');
|
|
4286
|
+
this.vphCtx.className = 'imgctx img-ph-ctx';
|
|
4287
|
+
this.vphCtx.dir = this.dir();
|
|
4288
|
+
document.body.appendChild(this.vphCtx);
|
|
4289
|
+
this.renderVphCtxButtons();
|
|
4290
|
+
}
|
|
4291
|
+
renderVphCtxButtons() {
|
|
4292
|
+
const el = this.vphCtx;
|
|
4293
|
+
if (!el)
|
|
4294
|
+
return;
|
|
4295
|
+
el.innerHTML = '';
|
|
4296
|
+
el.append(this.ctxBtn(IC.uploadic, this.t('uploadvideo'), () => {
|
|
4297
|
+
this.vphUploadTarget = this.vphActive;
|
|
4298
|
+
this.pickVideo();
|
|
4299
|
+
}), this.ctxBtn(IC.link, this.t('videolink'), () => this.renderVphCtxUrlInput()));
|
|
4300
|
+
/* inside a template slot, offer switching the slot back to an image */
|
|
4301
|
+
if (this.vphActive?.closest('.tpl-media')) {
|
|
4302
|
+
el.append(this.ctxBtn(IC.image, this.t('quickimage'), () => this.swapPlaceholderKind('image')));
|
|
4303
|
+
}
|
|
4304
|
+
el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deletevideo'), () => this.removeVphPlaceholder()));
|
|
4305
|
+
this.positionVphCtx();
|
|
4306
|
+
}
|
|
4307
|
+
renderVphCtxUrlInput() {
|
|
4308
|
+
const el = this.vphCtx;
|
|
4309
|
+
if (!el)
|
|
4310
|
+
return;
|
|
4311
|
+
el.innerHTML = '';
|
|
4312
|
+
const inp = document.createElement('input');
|
|
4313
|
+
inp.type = 'url';
|
|
4314
|
+
inp.className = 'urlinp';
|
|
4315
|
+
inp.placeholder = this.t('videourlph');
|
|
4316
|
+
const ok = document.createElement('button');
|
|
4317
|
+
ok.type = 'button';
|
|
4318
|
+
ok.className = 'txtbtn';
|
|
4319
|
+
ok.textContent = this.t('insertvideo');
|
|
4320
|
+
const submit = () => {
|
|
4321
|
+
const url = inp.value.trim();
|
|
4322
|
+
const ph = this.vphActive;
|
|
4323
|
+
if (!ph)
|
|
4324
|
+
return;
|
|
4325
|
+
if (!/^(https?:|data:video\/|blob:)/i.test(url)) {
|
|
4326
|
+
inp.classList.add('err');
|
|
4327
|
+
inp.focus();
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
/* a page URL from a known video host can't play in a <video> tag —
|
|
4331
|
+
swap in the host's iframe player instead of probing it */
|
|
4332
|
+
const embed = this.videoEmbedUrl(url);
|
|
4333
|
+
if (embed) {
|
|
4334
|
+
this.replacePlaceholderWithEmbed(ph, embed);
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
ok.disabled = true;
|
|
4338
|
+
inp.disabled = true;
|
|
4339
|
+
/* only place the video once its metadata actually loads, so the user
|
|
4340
|
+
gets a working preview instead of a broken player */
|
|
4341
|
+
const probe = document.createElement('video');
|
|
4342
|
+
probe.preload = 'metadata';
|
|
4343
|
+
probe.onloadedmetadata = () => this.replacePlaceholderWithVideo(ph, url, '');
|
|
4344
|
+
probe.onerror = () => {
|
|
4345
|
+
ok.disabled = false;
|
|
4346
|
+
inp.disabled = false;
|
|
4347
|
+
inp.classList.add('err');
|
|
4348
|
+
inp.focus();
|
|
4349
|
+
};
|
|
4350
|
+
probe.src = url;
|
|
4351
|
+
};
|
|
4352
|
+
ok.addEventListener('click', submit);
|
|
4353
|
+
inp.addEventListener('input', () => inp.classList.remove('err'));
|
|
4354
|
+
inp.addEventListener('keydown', (e) => {
|
|
4355
|
+
if (e.key === 'Enter') {
|
|
4356
|
+
e.preventDefault();
|
|
4357
|
+
submit();
|
|
4358
|
+
}
|
|
4359
|
+
else if (e.key === 'Escape') {
|
|
4360
|
+
e.stopPropagation();
|
|
4361
|
+
this.renderVphCtxButtons();
|
|
4362
|
+
}
|
|
4363
|
+
});
|
|
4364
|
+
el.append(inp, ok);
|
|
4365
|
+
this.positionVphCtx();
|
|
4366
|
+
inp.focus();
|
|
4367
|
+
}
|
|
4368
|
+
readVideoFileInto(file, ph) {
|
|
4369
|
+
if (ph.classList.contains('uploading'))
|
|
4370
|
+
return;
|
|
4371
|
+
if (this.videoUploadHandler) {
|
|
4372
|
+
this.uploadVideoFile(file, ph);
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4375
|
+
const fr = new FileReader();
|
|
4376
|
+
fr.onload = () => {
|
|
4377
|
+
if (this.ed.contains(ph))
|
|
4378
|
+
this.replacePlaceholderWithVideo(ph, fr.result, file.name.replace(/"/g, ''));
|
|
4379
|
+
};
|
|
4380
|
+
fr.readAsDataURL(file);
|
|
4381
|
+
}
|
|
4382
|
+
/** Runs the host-supplied videoUploadHandler instead of inlining base64,
|
|
4383
|
+
* so apps can route the file to S3 / Azure Blob / their own server. */
|
|
4384
|
+
uploadVideoFile(file, ph) {
|
|
4385
|
+
const title = file.name.replace(/"/g, '');
|
|
4386
|
+
ph.classList.remove('upload-error');
|
|
4387
|
+
ph.classList.add('uploading');
|
|
4388
|
+
const span = ph.querySelector('span');
|
|
4389
|
+
if (span)
|
|
4390
|
+
span.textContent = this.t('uploading');
|
|
4391
|
+
this.videoUploadHandler(file)
|
|
4392
|
+
.then((url) => {
|
|
4393
|
+
if (!this.ed.contains(ph))
|
|
4394
|
+
return;
|
|
4395
|
+
this.replacePlaceholderWithVideo(ph, url, title);
|
|
4396
|
+
})
|
|
4397
|
+
.catch((err) => {
|
|
4398
|
+
if (!this.ed.contains(ph))
|
|
4399
|
+
return;
|
|
4400
|
+
ph.classList.remove('uploading');
|
|
4401
|
+
ph.classList.add('upload-error');
|
|
4402
|
+
if (span)
|
|
4403
|
+
span.textContent = this.t('uploadfailed');
|
|
4404
|
+
this.onVideoUploadError?.(err, file);
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
replacePlaceholderWithVideo(ph, src, title) {
|
|
4408
|
+
const vid = document.createElement('video');
|
|
4409
|
+
vid.controls = true;
|
|
4410
|
+
vid.src = src;
|
|
4411
|
+
if (title)
|
|
4412
|
+
vid.title = title;
|
|
4413
|
+
ph.replaceWith(vid);
|
|
4414
|
+
if (this.vphActive === ph)
|
|
4415
|
+
this.clearVphPlaceholderSel();
|
|
4416
|
+
this.refreshState();
|
|
4417
|
+
this.onChange();
|
|
4418
|
+
}
|
|
4419
|
+
/** Swaps a video placeholder for a hosted player iframe (YouTube & co.). */
|
|
4420
|
+
replacePlaceholderWithEmbed(ph, embedUrl) {
|
|
4421
|
+
const tmp = document.createElement('div');
|
|
4422
|
+
tmp.innerHTML = this.videoEmbedHTML(embedUrl);
|
|
4423
|
+
ph.replaceWith(tmp.firstElementChild);
|
|
4424
|
+
if (this.vphActive === ph)
|
|
4425
|
+
this.clearVphPlaceholderSel();
|
|
4426
|
+
this.refreshState();
|
|
4427
|
+
this.onChange();
|
|
4428
|
+
}
|
|
4429
|
+
positionVphCtx() {
|
|
4430
|
+
if (this.vphActive && !document.body.contains(this.vphActive)) {
|
|
4431
|
+
this.clearVphPlaceholderSel();
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
if (!this.vphActive || !this.vphCtx)
|
|
4435
|
+
return;
|
|
4436
|
+
const r = this.vphActive.getBoundingClientRect();
|
|
4437
|
+
const cw = this.vphCtx.offsetWidth;
|
|
4438
|
+
const ch = this.vphCtx.offsetHeight;
|
|
4439
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4440
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4441
|
+
let cy = r.bottom + scrollY + 8;
|
|
4442
|
+
if (r.bottom + 8 + ch > innerHeight - 4)
|
|
4443
|
+
cy = Math.max(this.ctxMinTop(), r.top + scrollY - ch - 8);
|
|
4444
|
+
this.vphCtx.style.left = cx + 'px';
|
|
4445
|
+
this.vphCtx.style.top = cy + 'px';
|
|
4446
|
+
}
|
|
4447
|
+
/* ---------------------------------------------------------- video toolbar (post-insert) */
|
|
4448
|
+
cancelVidHide() {
|
|
4449
|
+
if (this.vidHideTimer != null) {
|
|
4450
|
+
window.clearTimeout(this.vidHideTimer);
|
|
4451
|
+
this.vidHideTimer = null;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
clearVidHandles() {
|
|
4455
|
+
this.cancelVidHide();
|
|
4456
|
+
this.vidCtx?.remove();
|
|
4457
|
+
this.vidCtx = null;
|
|
4458
|
+
this.vidActive?.classList.remove('vid-selected');
|
|
4459
|
+
this.vidActive = null;
|
|
4460
|
+
}
|
|
4461
|
+
selectVideo(vid) {
|
|
4462
|
+
if (this.vidActive === vid)
|
|
4463
|
+
return;
|
|
4464
|
+
this.clearVidHandles();
|
|
4465
|
+
this.vidActive = vid;
|
|
4466
|
+
vid.classList.add('vid-selected');
|
|
4467
|
+
this.vidCtx = this.buildVideoCtxToolbar(vid);
|
|
4468
|
+
this.positionVidCtx();
|
|
4469
|
+
}
|
|
4470
|
+
buildVideoCtxToolbar(vid) {
|
|
4471
|
+
const el = document.createElement('div');
|
|
4472
|
+
el.className = 'imgctx';
|
|
4473
|
+
el.dir = this.dir();
|
|
4474
|
+
const rebuild = () => {
|
|
4475
|
+
const rebuilt = this.buildVideoCtxToolbar(vid);
|
|
4476
|
+
this.vidCtx?.replaceWith(rebuilt);
|
|
4477
|
+
this.vidCtx = rebuilt;
|
|
4478
|
+
this.positionVidCtx();
|
|
4479
|
+
};
|
|
4480
|
+
const align = (a) => {
|
|
4481
|
+
this.setVideoAlign(vid, a);
|
|
4482
|
+
rebuild();
|
|
4483
|
+
};
|
|
4484
|
+
const isLeft = vid.style.float === 'left';
|
|
4485
|
+
const isRight = vid.style.float === 'right';
|
|
4486
|
+
const isCenter = !isLeft && !isRight && vid.style.marginLeft === 'auto' && vid.style.marginRight === 'auto';
|
|
4487
|
+
const mkBtn = (icon, tip, on, fn) => {
|
|
4488
|
+
const b = this.ctxBtn(icon, tip, fn);
|
|
4489
|
+
b.classList.toggle('on', on);
|
|
4490
|
+
return b;
|
|
4491
|
+
};
|
|
4492
|
+
el.append(mkBtn(IC.alignleft, this.t('alignleft'), isLeft, () => align('left')), mkBtn(IC.aligncenter, this.t('aligncenter'), isCenter, () => align('center')), mkBtn(IC.alignright, this.t('alignright'), isRight, () => align('right')), this.ctxSep(), mkBtn(IC.trash, this.t('deletevideo'), false, () => {
|
|
4493
|
+
vid.remove();
|
|
4494
|
+
this.clearVidHandles();
|
|
4495
|
+
this.onChange();
|
|
4496
|
+
}));
|
|
4497
|
+
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4498
|
+
el.addEventListener('mouseenter', () => this.cancelVidHide());
|
|
4499
|
+
el.addEventListener('mouseleave', () => {
|
|
4500
|
+
this.cancelVidHide();
|
|
4501
|
+
this.vidHideTimer = window.setTimeout(() => this.clearVidHandles(), 250);
|
|
4502
|
+
});
|
|
4503
|
+
document.body.appendChild(el);
|
|
4504
|
+
return el;
|
|
4505
|
+
}
|
|
4506
|
+
setVideoAlign(vid, align) {
|
|
4507
|
+
vid.style.float = '';
|
|
4508
|
+
vid.style.display = '';
|
|
4509
|
+
vid.style.marginLeft = '';
|
|
4510
|
+
vid.style.marginRight = '';
|
|
4511
|
+
vid.style.marginBottom = '';
|
|
4512
|
+
if (align === 'left') {
|
|
4513
|
+
vid.style.float = 'left';
|
|
4514
|
+
vid.style.marginInlineEnd = '1em';
|
|
4515
|
+
vid.style.marginBottom = '0.5em';
|
|
4516
|
+
}
|
|
4517
|
+
else if (align === 'right') {
|
|
4518
|
+
vid.style.float = 'right';
|
|
4519
|
+
vid.style.marginInlineStart = '1em';
|
|
4520
|
+
vid.style.marginBottom = '0.5em';
|
|
4521
|
+
}
|
|
4522
|
+
else {
|
|
4523
|
+
vid.style.display = 'block';
|
|
4524
|
+
vid.style.marginLeft = 'auto';
|
|
4525
|
+
vid.style.marginRight = 'auto';
|
|
4526
|
+
}
|
|
4527
|
+
this.onChange();
|
|
4528
|
+
}
|
|
4529
|
+
positionVidCtx() {
|
|
4530
|
+
if (!this.vidActive || !document.body.contains(this.vidActive)) {
|
|
4531
|
+
this.clearVidHandles();
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
const r = this.vidActive.getBoundingClientRect();
|
|
4535
|
+
if (!this.vidCtx)
|
|
4536
|
+
return;
|
|
4537
|
+
const cw = this.vidCtx.offsetWidth;
|
|
4538
|
+
const ch = this.vidCtx.offsetHeight;
|
|
4539
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4540
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4541
|
+
let cy = r.top + scrollY - ch - 8;
|
|
4542
|
+
if (cy < this.ctxMinTop())
|
|
4543
|
+
cy = r.bottom + scrollY + 8;
|
|
4544
|
+
this.vidCtx.style.left = cx + 'px';
|
|
4545
|
+
this.vidCtx.style.top = cy + 'px';
|
|
4546
|
+
}
|
|
4547
|
+
/* ---------------------------------------------------------- video dialog / embeds */
|
|
4548
|
+
/** Maps a page URL from a known video host to its iframe player URL. Returns
|
|
4549
|
+
* null for anything else (e.g. direct .mp4 file URLs). */
|
|
4550
|
+
videoEmbedUrl(url) {
|
|
4551
|
+
let m = url.match(/^https?:\/\/(?:www\.|m\.)?youtube\.com\/watch\?(?:[^\s#]*[?&])?v=([\w-]{5,})/i);
|
|
4552
|
+
if (!m)
|
|
4553
|
+
m = url.match(/^https?:\/\/(?:www\.)?(?:youtu\.be|youtube\.com\/(?:embed|shorts|live))\/([\w-]{5,})/i);
|
|
4554
|
+
if (m)
|
|
4555
|
+
return 'https://www.youtube.com/embed/' + m[1];
|
|
4556
|
+
m = url.match(/^https?:\/\/(?:www\.)?vimeo\.com\/(\d+)/i);
|
|
4557
|
+
if (m)
|
|
4558
|
+
return 'https://player.vimeo.com/video/' + m[1];
|
|
4559
|
+
m = url.match(/^https?:\/\/(?:www\.)?dailymotion\.com\/video\/(\w+)/i);
|
|
4560
|
+
if (m)
|
|
4561
|
+
return 'https://www.dailymotion.com/embed/video/' + m[1];
|
|
4562
|
+
return null;
|
|
4563
|
+
}
|
|
4564
|
+
videoEmbedHTML(src, w = 560, h = 314) {
|
|
4565
|
+
return `<span class="video-embed" contenteditable="false"><iframe src="${src}" width="${w}" height="${h}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></span>`;
|
|
4566
|
+
}
|
|
4567
|
+
/** Insert/edit-video dialog with General (source/width/height), Embed (embed
|
|
4568
|
+
* code) and Advanced (alternative source, poster image) tabs. Hosted page
|
|
4569
|
+
* URLs (YouTube & co.) become iframe players; anything else becomes a
|
|
4570
|
+
* <video> tag. The General tab also offers the same upload path as before
|
|
4571
|
+
* (videoUploadHandler when configured, else inline base64). */
|
|
4572
|
+
videoDlg() {
|
|
4573
|
+
this.saveSel();
|
|
4574
|
+
let srcInp;
|
|
4575
|
+
let wInp;
|
|
4576
|
+
let hInp;
|
|
4577
|
+
let embedTa;
|
|
4578
|
+
let altInp;
|
|
4579
|
+
let posterInp;
|
|
4580
|
+
const field = (labelKey, input) => {
|
|
4581
|
+
const f = document.createElement('div');
|
|
4582
|
+
f.className = 'dfield';
|
|
4583
|
+
const l = document.createElement('label');
|
|
4584
|
+
l.textContent = this.t(labelKey);
|
|
4585
|
+
f.append(l, input);
|
|
4586
|
+
return f;
|
|
4587
|
+
};
|
|
4588
|
+
const urlInput = () => {
|
|
4589
|
+
const i = document.createElement('input');
|
|
4590
|
+
i.type = 'url';
|
|
4591
|
+
return i;
|
|
4592
|
+
};
|
|
4593
|
+
this.dialog(this.t('videodlgttl'), (body) => {
|
|
4594
|
+
const tabs = document.createElement('div');
|
|
4595
|
+
tabs.className = 'dtabs';
|
|
4596
|
+
const panels = [];
|
|
4597
|
+
const mkTab = (labelKey, build) => {
|
|
4598
|
+
const b = document.createElement('button');
|
|
4599
|
+
b.type = 'button';
|
|
4600
|
+
b.textContent = this.t(labelKey);
|
|
4601
|
+
const p = document.createElement('div');
|
|
4602
|
+
p.className = 'dtabpanel';
|
|
4603
|
+
build(p);
|
|
4604
|
+
const i = panels.length;
|
|
4605
|
+
panels.push(p);
|
|
4606
|
+
b.addEventListener('click', () => show(i));
|
|
4607
|
+
tabs.appendChild(b);
|
|
4608
|
+
};
|
|
4609
|
+
const show = (i) => {
|
|
4610
|
+
tabs.querySelectorAll('button').forEach((x, xi) => x.classList.toggle('on', xi === i));
|
|
4611
|
+
panels.forEach((x, xi) => (x.style.display = xi === i ? '' : 'none'));
|
|
4612
|
+
};
|
|
4613
|
+
mkTab('vidgeneral', (p) => {
|
|
4614
|
+
srcInp = urlInput();
|
|
4615
|
+
srcInp.placeholder = this.t('videourlph');
|
|
4616
|
+
const srcRow = document.createElement('div');
|
|
4617
|
+
srcRow.className = 'drow';
|
|
4618
|
+
const upBtn = document.createElement('button');
|
|
4619
|
+
upBtn.type = 'button';
|
|
4620
|
+
upBtn.className = 'txtbtn';
|
|
4621
|
+
upBtn.textContent = this.t('uploadvideo');
|
|
4622
|
+
const file = document.createElement('input');
|
|
4623
|
+
file.type = 'file';
|
|
4624
|
+
file.accept = this.options.videoFileTypes.join(',');
|
|
4625
|
+
file.style.display = 'none';
|
|
4626
|
+
upBtn.addEventListener('click', () => {
|
|
4627
|
+
file.value = '';
|
|
4628
|
+
file.click();
|
|
4629
|
+
});
|
|
4630
|
+
file.addEventListener('change', () => {
|
|
4631
|
+
const f = file.files?.[0];
|
|
4632
|
+
if (!f)
|
|
4633
|
+
return;
|
|
4634
|
+
upBtn.disabled = true;
|
|
4635
|
+
upBtn.textContent = this.t('uploading');
|
|
4636
|
+
const done = (url) => {
|
|
4637
|
+
srcInp.value = url;
|
|
4638
|
+
upBtn.disabled = false;
|
|
4639
|
+
upBtn.textContent = this.t('uploadvideo');
|
|
4640
|
+
};
|
|
4641
|
+
if (this.videoUploadHandler) {
|
|
4642
|
+
this.videoUploadHandler(f)
|
|
4643
|
+
.then(done)
|
|
4644
|
+
.catch((err) => {
|
|
4645
|
+
upBtn.disabled = false;
|
|
4646
|
+
upBtn.textContent = this.t('uploadfailed');
|
|
4647
|
+
this.onVideoUploadError?.(err, f);
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
else {
|
|
4651
|
+
const fr = new FileReader();
|
|
4652
|
+
fr.onload = () => done(fr.result);
|
|
4653
|
+
fr.readAsDataURL(f);
|
|
4654
|
+
}
|
|
4655
|
+
});
|
|
4656
|
+
srcRow.append(srcInp, upBtn, file);
|
|
4657
|
+
srcInp.style.flex = '1';
|
|
4658
|
+
p.appendChild(field('vidsource', srcRow));
|
|
4659
|
+
const dims = document.createElement('div');
|
|
4660
|
+
dims.className = 'drow';
|
|
4661
|
+
wInp = document.createElement('input');
|
|
4662
|
+
wInp.type = 'number';
|
|
4663
|
+
wInp.value = '560';
|
|
4664
|
+
hInp = document.createElement('input');
|
|
4665
|
+
hInp.type = 'number';
|
|
4666
|
+
hInp.value = '314';
|
|
4667
|
+
dims.append(field('tblwidth', wInp), field('tblheight', hInp));
|
|
4668
|
+
p.appendChild(dims);
|
|
4669
|
+
});
|
|
4670
|
+
mkTab('vidembed', (p) => {
|
|
4671
|
+
const hint = document.createElement('p');
|
|
4672
|
+
hint.textContent = this.t('vidembedhint');
|
|
4673
|
+
hint.style.cssText = 'margin:0 0 8px;color:#556;font-size:13.5px';
|
|
4674
|
+
embedTa = document.createElement('textarea');
|
|
4675
|
+
embedTa.style.cssText = 'width:100%;box-sizing:border-box;height:160px';
|
|
4676
|
+
p.append(hint, embedTa);
|
|
4677
|
+
});
|
|
4678
|
+
mkTab('vidadvanced', (p) => {
|
|
4679
|
+
altInp = urlInput();
|
|
4680
|
+
posterInp = urlInput();
|
|
4681
|
+
p.append(field('vidaltsource', altInp), field('vidposter', posterInp));
|
|
4682
|
+
});
|
|
4683
|
+
const wrap = document.createElement('div');
|
|
4684
|
+
wrap.className = 'dtabbody';
|
|
4685
|
+
panels.forEach((p) => wrap.appendChild(p));
|
|
4686
|
+
body.append(tabs, wrap);
|
|
4687
|
+
show(0);
|
|
4688
|
+
setTimeout(() => srcInp.focus(), 0);
|
|
4689
|
+
}, [
|
|
4690
|
+
{ label: this.t('cancel'), action: () => this.closeDlg() },
|
|
4691
|
+
{
|
|
4692
|
+
label: this.t('save'),
|
|
4693
|
+
primary: true,
|
|
4694
|
+
action: () => {
|
|
4695
|
+
const embedCode = embedTa.value.trim();
|
|
4696
|
+
const src = srcInp.value.trim();
|
|
4697
|
+
const w = parseInt(wInp.value, 10) || 560;
|
|
4698
|
+
const h = parseInt(hInp.value, 10) || 314;
|
|
4699
|
+
this.closeDlg();
|
|
4700
|
+
if (!embedCode && !src)
|
|
4701
|
+
return;
|
|
4702
|
+
this.restoreSel();
|
|
4703
|
+
if (embedCode) {
|
|
4704
|
+
document.execCommand('insertHTML', false, `<span class="video-embed" contenteditable="false">${embedCode}</span>`);
|
|
4705
|
+
this.onChange();
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
const embed = this.videoEmbedUrl(src);
|
|
4709
|
+
if (embed) {
|
|
4710
|
+
document.execCommand('insertHTML', false, this.videoEmbedHTML(embed, w, h));
|
|
4711
|
+
}
|
|
4712
|
+
else {
|
|
4713
|
+
const alt = altInp.value.trim();
|
|
4714
|
+
const poster = posterInp.value.trim();
|
|
4715
|
+
document.execCommand('insertHTML', false, `<video controls width="${w}" height="${h}"${poster ? ` poster="${this.escapeAttr(poster)}"` : ''}>` +
|
|
4716
|
+
`<source src="${this.escapeAttr(src)}">${alt ? `<source src="${this.escapeAttr(alt)}">` : ''}</video>`);
|
|
4717
|
+
}
|
|
4718
|
+
this.onChange();
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
]);
|
|
4722
|
+
}
|
|
4723
|
+
escapeAttr(s) {
|
|
4724
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
4725
|
+
}
|
|
4726
|
+
escHtml(s) {
|
|
4727
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
4728
|
+
}
|
|
4729
|
+
/* ---------------------------------------------------------- inline code & code samples */
|
|
4730
|
+
/** The inline <code> run the caret sits in, if any (code blocks don't count). */
|
|
4731
|
+
closestInlineCode() {
|
|
4732
|
+
const s = window.getSelection();
|
|
4733
|
+
const node = s && this.ed.contains(s.anchorNode) ? s.anchorNode : null;
|
|
4734
|
+
if (!node)
|
|
4735
|
+
return null;
|
|
4736
|
+
const el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
4737
|
+
const code = el?.closest('code');
|
|
4738
|
+
return code && this.ed.contains(code) && !code.closest('pre') ? code : null;
|
|
4739
|
+
}
|
|
4740
|
+
/** Wraps the selection in an inline <code> run, or unwraps the run the caret is
|
|
4741
|
+
* in. Selections spanning multiple blocks are left alone (a <code> can't wrap
|
|
4742
|
+
* across paragraphs without producing invalid markup). */
|
|
4743
|
+
toggleInlineCode() {
|
|
4744
|
+
this.restoreSel();
|
|
4745
|
+
const sel = window.getSelection();
|
|
4746
|
+
if (!sel || !sel.rangeCount)
|
|
4747
|
+
return;
|
|
4748
|
+
const existing = this.closestInlineCode();
|
|
4749
|
+
if (existing) {
|
|
4750
|
+
const first = existing.firstChild;
|
|
4751
|
+
const last = existing.lastChild;
|
|
4752
|
+
while (existing.firstChild)
|
|
4753
|
+
existing.parentNode.insertBefore(existing.firstChild, existing);
|
|
4754
|
+
existing.remove();
|
|
4755
|
+
if (first && last) {
|
|
4756
|
+
const r = document.createRange();
|
|
4757
|
+
r.setStartBefore(first);
|
|
4758
|
+
r.setEndAfter(last);
|
|
4759
|
+
sel.removeAllRanges();
|
|
4760
|
+
sel.addRange(r);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
else {
|
|
4764
|
+
const range = sel.getRangeAt(0);
|
|
4765
|
+
if (range.collapsed)
|
|
4766
|
+
return;
|
|
4767
|
+
if (this.closestBlock(range.startContainer) !== this.closestBlock(range.endContainer))
|
|
4768
|
+
return;
|
|
4769
|
+
const code = document.createElement('code');
|
|
4770
|
+
code.appendChild(range.extractContents());
|
|
4771
|
+
range.insertNode(code);
|
|
4772
|
+
const r = document.createRange();
|
|
4773
|
+
r.selectNodeContents(code);
|
|
4774
|
+
sel.removeAllRanges();
|
|
4775
|
+
sel.addRange(r);
|
|
4776
|
+
}
|
|
4777
|
+
this.saveSel();
|
|
4778
|
+
this.refreshState();
|
|
4779
|
+
this.onChange();
|
|
4780
|
+
}
|
|
4781
|
+
/** Typing `snippet` converts to an inline <code> run the moment the closing
|
|
4782
|
+
* backtick is typed, so code can be marked up without reaching for the toolbar. */
|
|
4783
|
+
tryCodeTyping(e) {
|
|
4784
|
+
const sel = window.getSelection();
|
|
4785
|
+
if (!sel || !sel.isCollapsed || !sel.anchorNode)
|
|
4786
|
+
return;
|
|
4787
|
+
const node = sel.anchorNode;
|
|
4788
|
+
if (node.nodeType !== Node.TEXT_NODE || !this.ed.contains(node))
|
|
4789
|
+
return;
|
|
4790
|
+
if (node.parentElement?.closest('code, pre'))
|
|
4791
|
+
return;
|
|
4792
|
+
const before = (node.textContent || '').slice(0, sel.anchorOffset);
|
|
4793
|
+
const m = before.match(/(?:^|[^`])`([^`]+)$/);
|
|
4794
|
+
if (!m)
|
|
4795
|
+
return;
|
|
4796
|
+
const src = m[1];
|
|
4797
|
+
if (/^\s|\s$/.test(src))
|
|
4798
|
+
return;
|
|
4799
|
+
e.preventDefault();
|
|
4800
|
+
const range = document.createRange();
|
|
4801
|
+
range.setStart(node, sel.anchorOffset - src.length - 1);
|
|
4802
|
+
range.setEnd(node, sel.anchorOffset);
|
|
4803
|
+
/* direct DOM instead of execCommand('insertHTML') — with styleWithCSS
|
|
4804
|
+
active the browser rewrites a pasted <code> into a styled <span>,
|
|
4805
|
+
losing the semantic tag */
|
|
4806
|
+
range.deleteContents();
|
|
4807
|
+
const code = document.createElement('code');
|
|
4808
|
+
code.textContent = src;
|
|
4809
|
+
range.insertNode(code);
|
|
4810
|
+
const after = document.createRange();
|
|
4811
|
+
after.setStartAfter(code);
|
|
4812
|
+
after.collapse(true);
|
|
4813
|
+
sel.removeAllRanges();
|
|
4814
|
+
sel.addRange(after);
|
|
4815
|
+
this.saveSel();
|
|
4816
|
+
this.onChange();
|
|
4817
|
+
}
|
|
4818
|
+
/** Custom dropdown for dialog fields (a native <select> popup can't be styled):
|
|
4819
|
+
* a field-styled button showing the current choice that opens a floating,
|
|
4820
|
+
* scrollable option list. Read the current choice via `.value()`. */
|
|
4821
|
+
dlgSelect(options, selected) {
|
|
4822
|
+
let value = selected;
|
|
4823
|
+
const el = document.createElement('div');
|
|
4824
|
+
el.className = 'dsel';
|
|
4825
|
+
const btn = document.createElement('button');
|
|
4826
|
+
btn.type = 'button';
|
|
4827
|
+
btn.className = 'dsel-btn';
|
|
4828
|
+
const lbl = document.createElement('span');
|
|
4829
|
+
lbl.className = 'lbl';
|
|
4830
|
+
lbl.textContent = options.find(([v]) => v === selected)?.[1] || '';
|
|
4831
|
+
btn.append(lbl);
|
|
4832
|
+
btn.insertAdjacentHTML('beforeend', IC.chev);
|
|
4833
|
+
const list = document.createElement('div');
|
|
4834
|
+
list.className = 'dsel-list';
|
|
4835
|
+
options.forEach(([v, label]) => {
|
|
4836
|
+
const o = document.createElement('button');
|
|
4837
|
+
o.type = 'button';
|
|
4838
|
+
o.textContent = label;
|
|
4839
|
+
o.classList.toggle('on', v === value);
|
|
4840
|
+
o.addEventListener('click', () => {
|
|
4841
|
+
value = v;
|
|
4842
|
+
lbl.textContent = label;
|
|
4843
|
+
list.querySelectorAll('button').forEach((b) => b.classList.toggle('on', b === o));
|
|
4844
|
+
el.classList.remove('open');
|
|
4845
|
+
btn.focus();
|
|
4846
|
+
});
|
|
4847
|
+
list.appendChild(o);
|
|
4848
|
+
});
|
|
4849
|
+
btn.addEventListener('click', () => {
|
|
4850
|
+
el.classList.toggle('open');
|
|
4851
|
+
if (el.classList.contains('open'))
|
|
4852
|
+
list.querySelector('.on')?.scrollIntoView({ block: 'nearest' });
|
|
4853
|
+
});
|
|
4854
|
+
el.addEventListener('focusout', (e) => {
|
|
4855
|
+
if (!el.contains(e.relatedTarget))
|
|
4856
|
+
el.classList.remove('open');
|
|
4857
|
+
});
|
|
4858
|
+
el.addEventListener('keydown', (e) => {
|
|
4859
|
+
/* Escape closes the open list without closing the whole dialog */
|
|
4860
|
+
if (e.key === 'Escape' && el.classList.contains('open')) {
|
|
4861
|
+
e.stopPropagation();
|
|
4862
|
+
el.classList.remove('open');
|
|
4863
|
+
btn.focus();
|
|
4864
|
+
}
|
|
4865
|
+
});
|
|
4866
|
+
el.append(btn, list);
|
|
4867
|
+
return { el, value: () => value };
|
|
4868
|
+
}
|
|
4869
|
+
/** Insert/edit-code-sample dialog: language dropdown + monospace textarea to
|
|
4870
|
+
* paste or type code into. Inserts a non-editable <pre class="code-block">
|
|
4871
|
+
* whose header shows the language; click the block to edit / copy / delete. */
|
|
4872
|
+
codeDlg(existing) {
|
|
4873
|
+
this.saveSel();
|
|
4874
|
+
let langDd;
|
|
4875
|
+
let ta;
|
|
4876
|
+
this.dialog(this.t('codedlgttl'), (body) => {
|
|
4877
|
+
const f = document.createElement('div');
|
|
4878
|
+
f.className = 'dfield';
|
|
4879
|
+
const l = document.createElement('label');
|
|
4880
|
+
l.textContent = this.t('codelang');
|
|
4881
|
+
langDd = this.dlgSelect(CODE_LANGS, existing?.dataset.lang || 'plain');
|
|
4882
|
+
f.append(l, langDd.el);
|
|
4883
|
+
ta = document.createElement('textarea');
|
|
4884
|
+
ta.className = 'code-input';
|
|
4885
|
+
ta.spellcheck = false;
|
|
4886
|
+
ta.value = existing?.querySelector('code')?.textContent || '';
|
|
4887
|
+
ta.placeholder = 'const answer = 42;';
|
|
4888
|
+
body.append(f, ta);
|
|
4889
|
+
setTimeout(() => ta.focus(), 0);
|
|
4890
|
+
}, [
|
|
4891
|
+
{ label: this.t('cancel'), action: () => this.closeDlg() },
|
|
4892
|
+
{
|
|
4893
|
+
label: this.t('save'),
|
|
4894
|
+
primary: true,
|
|
4895
|
+
action: () => {
|
|
4896
|
+
const src = ta.value.replace(/\s+$/, '');
|
|
4897
|
+
const lang = langDd.value();
|
|
4898
|
+
const label = CODE_LANGS.find(([v]) => v === lang)?.[1] || lang;
|
|
4899
|
+
this.closeDlg();
|
|
4900
|
+
if (!src.trim())
|
|
4901
|
+
return;
|
|
4902
|
+
if (existing) {
|
|
4903
|
+
existing.dataset.lang = lang;
|
|
4904
|
+
existing.setAttribute('data-lang-label', label);
|
|
4905
|
+
const codeEl = existing.querySelector('code') || existing;
|
|
4906
|
+
codeEl.textContent = src;
|
|
4907
|
+
this.clearCodeSel();
|
|
4908
|
+
this.onChange();
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
this.restoreSel();
|
|
4912
|
+
document.execCommand('insertHTML', false, `<pre class="code-block" contenteditable="false" data-lang="${lang}" data-lang-label="${this.escapeAttr(label)}">` +
|
|
4913
|
+
`<code>${this.escHtml(src)}</code></pre><p><br></p>`);
|
|
4914
|
+
this.onChange();
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
]);
|
|
4918
|
+
}
|
|
4919
|
+
clearCodeSel() {
|
|
4920
|
+
this.codeCtx?.remove();
|
|
4921
|
+
this.codeCtx = null;
|
|
4922
|
+
this.codeActive?.classList.remove('code-selected');
|
|
4923
|
+
this.codeActive = null;
|
|
4924
|
+
}
|
|
4925
|
+
selectCodeEl(el) {
|
|
4926
|
+
if (this.codeActive === el)
|
|
4927
|
+
return;
|
|
4928
|
+
this.clearCodeSel();
|
|
4929
|
+
this.codeActive = el;
|
|
4930
|
+
el.classList.add('code-selected');
|
|
4931
|
+
this.codeCtx = this.buildCodeCtxToolbar(el);
|
|
4932
|
+
this.positionCodeCtx();
|
|
4933
|
+
}
|
|
4934
|
+
buildCodeCtxToolbar(el) {
|
|
4935
|
+
const ctx = document.createElement('div');
|
|
4936
|
+
ctx.className = 'imgctx';
|
|
4937
|
+
ctx.dir = this.dir();
|
|
4938
|
+
ctx.append(this.ctxBtn(IC.editic, this.t('editcode'), () => this.codeDlg(el)), this.ctxBtn(IC.copyic, this.t('copycode'), () => this.copyCodeBlock(el)), this.ctxSep(), this.ctxBtn(IC.trash, this.t('deletecode'), () => {
|
|
4939
|
+
el.remove();
|
|
4940
|
+
this.clearCodeSel();
|
|
4941
|
+
this.onChange();
|
|
4942
|
+
}));
|
|
4943
|
+
ctx.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4944
|
+
document.body.appendChild(ctx);
|
|
4945
|
+
return ctx;
|
|
4946
|
+
}
|
|
4947
|
+
copyCodeBlock(el) {
|
|
4948
|
+
const text = el.querySelector('code')?.textContent ?? el.textContent ?? '';
|
|
4949
|
+
if (navigator.clipboard?.writeText) {
|
|
4950
|
+
navigator.clipboard.writeText(text).catch(() => { });
|
|
4951
|
+
return;
|
|
4952
|
+
}
|
|
4953
|
+
const ta = document.createElement('textarea');
|
|
4954
|
+
ta.value = text;
|
|
4955
|
+
document.body.appendChild(ta);
|
|
4956
|
+
ta.select();
|
|
4957
|
+
document.execCommand('copy');
|
|
4958
|
+
ta.remove();
|
|
4959
|
+
}
|
|
4960
|
+
positionCodeCtx() {
|
|
4961
|
+
if (!this.codeActive || !document.body.contains(this.codeActive)) {
|
|
4962
|
+
this.clearCodeSel();
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
const r = this.codeActive.getBoundingClientRect();
|
|
4966
|
+
if (!this.codeCtx)
|
|
4967
|
+
return;
|
|
4968
|
+
const cw = this.codeCtx.offsetWidth;
|
|
4969
|
+
const ch = this.codeCtx.offsetHeight;
|
|
4970
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4971
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4972
|
+
let cy = r.top + scrollY - ch - 8;
|
|
4973
|
+
if (cy < this.ctxMinTop())
|
|
4974
|
+
cy = r.bottom + scrollY + 8;
|
|
4975
|
+
this.codeCtx.style.left = cx + 'px';
|
|
4976
|
+
this.codeCtx.style.top = cy + 'px';
|
|
4977
|
+
}
|
|
3517
4978
|
/* ---------------------------------------------------------- templates */
|
|
3518
4979
|
templateMenu(anchor) {
|
|
3519
4980
|
this.popup(anchor, (el) => this.buildTemplatePickInto(el));
|
|
@@ -3753,7 +5214,8 @@ class FableEditor {
|
|
|
3753
5214
|
b.addEventListener('mouseleave', () => this.setOpTarget(null));
|
|
3754
5215
|
return b;
|
|
3755
5216
|
};
|
|
3756
|
-
|
|
5217
|
+
const fillBtn = this.ctxBtn(IC.palette, this.t('cellbg'), () => this.cellFillMenu(fillBtn));
|
|
5218
|
+
el.append(hl(this.ctxBtn(IC.rowbefore, this.t('rowabove'), () => this.tableOp('rowabove')), 'row'), hl(this.ctxBtn(IC.rowafter, this.t('rowbelow'), () => this.tableOp('rowbelow')), 'row'), hl(this.ctxBtn(IC.moverowup, this.t('moverowup'), () => this.tableMove('rowup')), 'row'), hl(this.ctxBtn(IC.moverowdown, this.t('moverowdown'), () => this.tableMove('rowdown')), 'row'), hl(this.ctxBtn(IC.rowdelete, this.t('delrow'), () => this.tableOp('delrow')), 'row'), this.ctxSep(), hl(this.ctxBtn(IC.colbefore, this.t('colbefore'), () => this.tableOp('colbefore')), 'col'), hl(this.ctxBtn(IC.colafter, this.t('colafter'), () => this.tableOp('colafter')), 'col'), hl(this.ctxBtn(IC.movecolleft, this.t('movecolleft'), () => this.tableMove('colleft')), 'col'), hl(this.ctxBtn(IC.movecolright, this.t('movecolright'), () => this.tableMove('colright')), 'col'), hl(this.ctxBtn(IC.coldelete, this.t('delcol'), () => this.tableOp('delcol')), 'col'), this.ctxSep(), this.ctxBtn(IC.alignleft, this.t('tblalignleft'), () => this.alignTable('left')), this.ctxBtn(IC.aligncenter, this.t('tblaligncenter'), () => this.alignTable('center')), this.ctxBtn(IC.alignright, this.t('tblalignright'), () => this.alignTable('right')), this.ctxSep(), fillBtn, this.ctxBtn(IC.trash, this.t('deltable'), () => this.tableOp('deltable')), this.ctxBtn(IC.tableic, this.t('tablepropsttl'), () => this.tablePropsDlg()));
|
|
3757
5219
|
document.body.appendChild(el);
|
|
3758
5220
|
return el;
|
|
3759
5221
|
}
|
|
@@ -4110,7 +5572,7 @@ class FableEditor {
|
|
|
4110
5572
|
const caseBtn = this.ctxBtn(IC.caseic, this.t('changecase'), () => this.caseMenu(caseBtn));
|
|
4111
5573
|
const lhBtn = this.ctxBtn(IC.lineheight, this.t('lineheight'), () => this.lineHeightMenu(lhBtn));
|
|
4112
5574
|
const wsBtn = this.ctxBtn(IC.wordspacing, this.t('wordspacing'), () => this.wordSpacingMenu(wsBtn));
|
|
4113
|
-
el.append(this.ctxBtn(IC.bold, this.t('bold'), () => this.exec('bold'), 'bold'), this.ctxBtn(IC.italic, this.t('italic'), () => this.exec('italic'), 'italic'), foreBtn, backBtn, this.ctxSep(), this.ctxBtn(IC.quote, this.t('quote'), () => this.toggleBlock('blockquote'), 'blockquote'), this.ctxBtn('<b style="font:700 12px/1 Arial,sans-serif">H2</b>', this.blockLabel('h2'), () => this.toggleBlock('h2'), 'h2'), this.ctxBtn('<b style="font:700 12px/1 Arial,sans-serif">H3</b>', this.blockLabel('h3'), () => this.toggleBlock('h3'), 'h3'), this.ctxSep(), this.ctxBtn(IC.link, this.t('linkttl'), () => this.linkDlg()), caseBtn, lhBtn, wsBtn);
|
|
5575
|
+
el.append(this.ctxBtn(IC.bold, this.t('bold'), () => this.exec('bold'), 'bold'), this.ctxBtn(IC.italic, this.t('italic'), () => this.exec('italic'), 'italic'), foreBtn, backBtn, this.ctxSep(), this.ctxBtn(IC.quote, this.t('quote'), () => this.toggleBlock('blockquote'), 'blockquote'), this.ctxBtn(IC.inlinecodeic, this.t('code'), () => this.toggleInlineCode(), 'code'), this.ctxBtn('<b style="font:700 12px/1 Arial,sans-serif">H2</b>', this.blockLabel('h2'), () => this.toggleBlock('h2'), 'h2'), this.ctxBtn('<b style="font:700 12px/1 Arial,sans-serif">H3</b>', this.blockLabel('h3'), () => this.toggleBlock('h3'), 'h3'), this.ctxSep(), this.ctxBtn(IC.link, this.t('linkttl'), () => this.linkDlg()), caseBtn, lhBtn, wsBtn);
|
|
4114
5576
|
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4115
5577
|
document.body.appendChild(el);
|
|
4116
5578
|
this.syncColorSwatches(el);
|
|
@@ -4193,6 +5655,9 @@ class FableEditor {
|
|
|
4193
5655
|
if (b)
|
|
4194
5656
|
b.classList.toggle('on', inEd && blkTag === tag);
|
|
4195
5657
|
});
|
|
5658
|
+
const codeB = container.querySelector('[data-id=code]');
|
|
5659
|
+
if (codeB)
|
|
5660
|
+
codeB.classList.toggle('on', inEd && !!this.closestInlineCode());
|
|
4196
5661
|
});
|
|
4197
5662
|
const blk = s ? this.closestBlock(s.anchorNode) : null;
|
|
4198
5663
|
const dir = blk?.closest('[dir]')?.getAttribute('dir') || this.ed.getAttribute('dir') || this.dir();
|
|
@@ -4233,6 +5698,18 @@ class FableEditor {
|
|
|
4233
5698
|
fr.readAsDataURL(imgItem.getAsFile());
|
|
4234
5699
|
return;
|
|
4235
5700
|
}
|
|
5701
|
+
if (!html) {
|
|
5702
|
+
/* additive pre-check before the normal text pipeline runs (the paste
|
|
5703
|
+
engine itself is untouched): a bare video page URL (YouTube & co.)
|
|
5704
|
+
becomes an embedded player. Anything else falls through unchanged. */
|
|
5705
|
+
const text = cd.getData('text/plain');
|
|
5706
|
+
const embed = /^\S+$/.test(text.trim()) ? this.videoEmbedUrl(text.trim()) : null;
|
|
5707
|
+
if (embed) {
|
|
5708
|
+
document.execCommand('insertHTML', false, this.videoEmbedHTML(embed));
|
|
5709
|
+
this.onChange();
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
4236
5713
|
if (html) {
|
|
4237
5714
|
document.execCommand('insertHTML', false, cleanPastedHTML(html, this.dir()));
|
|
4238
5715
|
}
|
|
@@ -4247,6 +5724,9 @@ class FableEditor {
|
|
|
4247
5724
|
this.closeDlg();
|
|
4248
5725
|
this.clearTableHandles();
|
|
4249
5726
|
this.clearImgPlaceholderSel();
|
|
5727
|
+
this.clearVphPlaceholderSel();
|
|
5728
|
+
this.clearVidHandles();
|
|
5729
|
+
this.clearCodeSel();
|
|
4250
5730
|
this.shell.dir = this.dir();
|
|
4251
5731
|
this.ed.dir = this.dir();
|
|
4252
5732
|
if (this.options.menubar)
|
|
@@ -4270,6 +5750,9 @@ class FableEditorComponent {
|
|
|
4270
5750
|
toolbar = true;
|
|
4271
5751
|
statusbar = true;
|
|
4272
5752
|
readonly = false;
|
|
5753
|
+
primaryColor;
|
|
5754
|
+
toolbarGroupBackground;
|
|
5755
|
+
uiFontFamily;
|
|
4273
5756
|
init = {};
|
|
4274
5757
|
value = '';
|
|
4275
5758
|
editorChange = new EventEmitter();
|
|
@@ -4286,6 +5769,9 @@ class FableEditorComponent {
|
|
|
4286
5769
|
toolbar: this.toolbar,
|
|
4287
5770
|
statusbar: this.statusbar,
|
|
4288
5771
|
readonly: this.readonly,
|
|
5772
|
+
primaryColor: this.primaryColor,
|
|
5773
|
+
toolbarGroupBackground: this.toolbarGroupBackground,
|
|
5774
|
+
uiFontFamily: this.uiFontFamily,
|
|
4289
5775
|
initialContent: this.value,
|
|
4290
5776
|
...this.init,
|
|
4291
5777
|
onChange: (content) => {
|
|
@@ -4330,7 +5816,7 @@ class FableEditorComponent {
|
|
|
4330
5816
|
this.editor?.setContent(value ?? '');
|
|
4331
5817
|
}
|
|
4332
5818
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FableEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4333
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FableEditorComponent, isStandalone: true, selector: "fable-editor", inputs: { language: "language", height: "height", menubar: "menubar", toolbar: "toolbar", statusbar: "statusbar", readonly: "readonly", init: "init", value: "value" }, outputs: { editorChange: "editorChange", editorReady: "editorReady" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `<div #container></div>`, isInline: true, styles: [":host{display:block}\n"] });
|
|
5819
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FableEditorComponent, isStandalone: true, selector: "fable-editor", inputs: { language: "language", height: "height", menubar: "menubar", toolbar: "toolbar", statusbar: "statusbar", readonly: "readonly", primaryColor: "primaryColor", toolbarGroupBackground: "toolbarGroupBackground", uiFontFamily: "uiFontFamily", init: "init", value: "value" }, outputs: { editorChange: "editorChange", editorReady: "editorReady" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `<div #container></div>`, isInline: true, styles: [":host{display:block}\n"] });
|
|
4334
5820
|
}
|
|
4335
5821
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FableEditorComponent, decorators: [{
|
|
4336
5822
|
type: Component,
|
|
@@ -4350,6 +5836,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
4350
5836
|
type: Input
|
|
4351
5837
|
}], readonly: [{
|
|
4352
5838
|
type: Input
|
|
5839
|
+
}], primaryColor: [{
|
|
5840
|
+
type: Input
|
|
5841
|
+
}], toolbarGroupBackground: [{
|
|
5842
|
+
type: Input
|
|
5843
|
+
}], uiFontFamily: [{
|
|
5844
|
+
type: Input
|
|
4353
5845
|
}], init: [{
|
|
4354
5846
|
type: Input
|
|
4355
5847
|
}], value: [{
|