fable-editor 1.1.0 → 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 +22 -12
- package/dist/angular/README.md +22 -12
- package/dist/angular/angular/fable-editor.component.d.ts +4 -1
- package/dist/angular/core/config.d.ts +5 -0
- package/dist/angular/core/editor.d.ts +58 -31
- package/dist/angular/core/i18n.d.ts +16 -6
- package/dist/angular/core/types.d.ts +11 -0
- package/dist/angular/esm2022/angular/fable-editor.component.mjs +14 -2
- package/dist/angular/esm2022/core/config.mjs +85 -35
- package/dist/angular/esm2022/core/editor.mjs +525 -231
- package/dist/angular/esm2022/core/i18n.mjs +33 -13
- package/dist/angular/esm2022/core/icons.mjs +9 -3
- package/dist/angular/esm2022/core/types.mjs +1 -1
- package/dist/angular/fesm2022/fable-editor.mjs +659 -277
- package/dist/angular/fesm2022/fable-editor.mjs.map +1 -1
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/editor.css +287 -75
- package/dist/core/editor.d.ts +58 -31
- package/dist/core/editor.d.ts.map +1 -1
- package/dist/core/i18n.d.ts +16 -6
- package/dist/core/i18n.d.ts.map +1 -1
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/core/index.cjs +6 -262
- package/dist/core/index.mjs +2360 -13471
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/index.cjs +15 -271
- package/dist/react/index.mjs +2787 -13896
- package/package.json +1 -5
- package/style.css +287 -75
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { EventEmitter, Output, Input, ViewChild, Component, NgModule } from '@angular/core';
|
|
3
|
-
import katex from 'katex';
|
|
4
3
|
|
|
5
4
|
const I18N = {
|
|
6
5
|
en: {
|
|
@@ -42,9 +41,14 @@ const I18N = {
|
|
|
42
41
|
rowabove: 'Insert row before',
|
|
43
42
|
rowbelow: 'Insert row after',
|
|
44
43
|
delrow: 'Delete row',
|
|
44
|
+
moverowup: 'Move row up',
|
|
45
|
+
moverowdown: 'Move row down',
|
|
45
46
|
colbefore: 'Insert column before',
|
|
46
47
|
colafter: 'Insert column after',
|
|
47
48
|
delcol: 'Delete column',
|
|
49
|
+
movecolleft: 'Move column left',
|
|
50
|
+
movecolright: 'Move column right',
|
|
51
|
+
cellbg: 'Cell background',
|
|
48
52
|
deltable: 'Delete table',
|
|
49
53
|
ltr: 'Left to right',
|
|
50
54
|
rtl: 'Right to left',
|
|
@@ -91,6 +95,9 @@ const I18N = {
|
|
|
91
95
|
tblcellpadding: 'Cell padding',
|
|
92
96
|
tblborder: 'Border width',
|
|
93
97
|
tblalign: 'Alignment',
|
|
98
|
+
tblalignleft: 'Align table left',
|
|
99
|
+
tblaligncenter: 'Center table',
|
|
100
|
+
tblalignright: 'Align table right',
|
|
94
101
|
alignnone: 'None',
|
|
95
102
|
quote: 'Blockquote',
|
|
96
103
|
link: 'Link…',
|
|
@@ -99,6 +106,7 @@ const I18N = {
|
|
|
99
106
|
unlink: 'Remove link',
|
|
100
107
|
lineheight: 'Line height',
|
|
101
108
|
wordspacing: 'Word spacing',
|
|
109
|
+
letterspacing: 'Letter spacing',
|
|
102
110
|
changecase: 'Change case',
|
|
103
111
|
lowercase: 'Lowercase',
|
|
104
112
|
uppercase: 'Uppercase',
|
|
@@ -152,12 +160,6 @@ const I18N = {
|
|
|
152
160
|
videourlph: 'Paste video URL…',
|
|
153
161
|
insertvideo: 'Insert',
|
|
154
162
|
dropvideo: 'Drop video here',
|
|
155
|
-
mathformula: 'Insert math formula',
|
|
156
|
-
mathdlgttl: 'Insert Math Formula',
|
|
157
|
-
mathderivation: 'Multi-line derivation',
|
|
158
|
-
mathlatex: 'LaTeX',
|
|
159
|
-
editmath: 'Edit formula',
|
|
160
|
-
deletemath: 'Delete formula',
|
|
161
163
|
videodlgttl: 'Insert/edit video',
|
|
162
164
|
vidgeneral: 'General',
|
|
163
165
|
vidembed: 'Embed',
|
|
@@ -167,6 +169,13 @@ const I18N = {
|
|
|
167
169
|
vidaltsource: 'Alternative source URL',
|
|
168
170
|
vidposter: 'Media poster (Image URL)',
|
|
169
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',
|
|
170
179
|
chcat_currency: 'Currency',
|
|
171
180
|
chcat_text: 'Text & punctuation',
|
|
172
181
|
chcat_math: 'Mathematical',
|
|
@@ -223,9 +232,14 @@ const I18N = {
|
|
|
223
232
|
rowabove: 'إدراج صف قبل',
|
|
224
233
|
rowbelow: 'إدراج صف بعد',
|
|
225
234
|
delrow: 'حذف الصف',
|
|
235
|
+
moverowup: 'نقل الصف لأعلى',
|
|
236
|
+
moverowdown: 'نقل الصف لأسفل',
|
|
226
237
|
colbefore: 'إدراج عمود قبل',
|
|
227
238
|
colafter: 'إدراج عمود بعد',
|
|
228
239
|
delcol: 'حذف العمود',
|
|
240
|
+
movecolleft: 'نقل العمود لليسار',
|
|
241
|
+
movecolright: 'نقل العمود لليمين',
|
|
242
|
+
cellbg: 'خلفية الخلية',
|
|
229
243
|
deltable: 'حذف الجدول',
|
|
230
244
|
ltr: 'من اليسار إلى اليمين',
|
|
231
245
|
rtl: 'من اليمين إلى اليسار',
|
|
@@ -272,6 +286,9 @@ const I18N = {
|
|
|
272
286
|
tblcellpadding: 'حشو الخلايا',
|
|
273
287
|
tblborder: 'عرض الحدود',
|
|
274
288
|
tblalign: 'المحاذاة',
|
|
289
|
+
tblalignleft: 'محاذاة الجدول لليسار',
|
|
290
|
+
tblaligncenter: 'توسيط الجدول',
|
|
291
|
+
tblalignright: 'محاذاة الجدول لليمين',
|
|
275
292
|
alignnone: 'بدون',
|
|
276
293
|
quote: 'اقتباس',
|
|
277
294
|
link: 'رابط…',
|
|
@@ -280,6 +297,7 @@ const I18N = {
|
|
|
280
297
|
unlink: 'إزالة الرابط',
|
|
281
298
|
lineheight: 'تباعد الأسطر',
|
|
282
299
|
wordspacing: 'تباعد الكلمات',
|
|
300
|
+
letterspacing: 'تباعد الأحرف',
|
|
283
301
|
changecase: 'تغيير حالة الأحرف',
|
|
284
302
|
lowercase: 'أحرف صغيرة',
|
|
285
303
|
uppercase: 'أحرف كبيرة',
|
|
@@ -333,12 +351,6 @@ const I18N = {
|
|
|
333
351
|
videourlph: 'الصق رابط الفيديو…',
|
|
334
352
|
insertvideo: 'إدراج',
|
|
335
353
|
dropvideo: 'أسقط الفيديو هنا',
|
|
336
|
-
mathformula: 'إدراج معادلة رياضية',
|
|
337
|
-
mathdlgttl: 'إدراج معادلة رياضية',
|
|
338
|
-
mathderivation: 'اشتقاق متعدد الأسطر',
|
|
339
|
-
mathlatex: 'LaTeX',
|
|
340
|
-
editmath: 'تعديل المعادلة',
|
|
341
|
-
deletemath: 'حذف المعادلة',
|
|
342
354
|
videodlgttl: 'إدراج/تعديل فيديو',
|
|
343
355
|
vidgeneral: 'عام',
|
|
344
356
|
vidembed: 'تضمين',
|
|
@@ -348,6 +360,13 @@ const I18N = {
|
|
|
348
360
|
vidaltsource: 'رابط مصدر بديل',
|
|
349
361
|
vidposter: 'صورة الغلاف (رابط الصورة)',
|
|
350
362
|
emoji: 'رموز تعبيرية…',
|
|
363
|
+
code: 'كود مضمّن',
|
|
364
|
+
codesample: 'عينة كود…',
|
|
365
|
+
codedlgttl: 'إدراج/تعديل عينة كود',
|
|
366
|
+
codelang: 'اللغة',
|
|
367
|
+
editcode: 'تعديل عينة الكود',
|
|
368
|
+
deletecode: 'حذف عينة الكود',
|
|
369
|
+
copycode: 'نسخ الكود',
|
|
351
370
|
chcat_currency: 'عملات',
|
|
352
371
|
chcat_text: 'نص وعلامات ترقيم',
|
|
353
372
|
chcat_math: 'رموز رياضية',
|
|
@@ -374,8 +393,8 @@ function getStrings(lang) {
|
|
|
374
393
|
* `toolbar: true` (the default) renders identically to the hard-coded original. */
|
|
375
394
|
const DEFAULT_TOOLBAR = 'undo redo | preview print | importword revhistory | fontfamily fontsize | fontsizeincrease fontsizedecrease | ' +
|
|
376
395
|
'bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | ' +
|
|
377
|
-
'bullist numlist | outdent indent | link blockquote changecase lineheight wordspacing | removeformat | blocks | ' +
|
|
378
|
-
'ltr rtl | quickimage quickvideo quicktable
|
|
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';
|
|
379
398
|
/** Transcribed from the editor's built-in menu keys so that `menubar: true` (the
|
|
380
399
|
* default) renders identically to the hard-coded original. */
|
|
381
400
|
const DEFAULT_MENUBAR = 'file edit view insert format tools table help';
|
|
@@ -405,6 +424,26 @@ const MIN_FONT_PX = 1;
|
|
|
405
424
|
const MAX_FONT_PX = 100;
|
|
406
425
|
const LINE_HEIGHTS = ['1', '1.15', '1.4', '1.5', '1.75', '2', '2.5', '3'];
|
|
407
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
|
+
];
|
|
408
447
|
const BLOCKS = [
|
|
409
448
|
['p', 'para'],
|
|
410
449
|
['h1', 'heading1'],
|
|
@@ -417,75 +456,105 @@ const BLOCKS = [
|
|
|
417
456
|
];
|
|
418
457
|
/** Quick-access text color chips shown directly in the toolbar. */
|
|
419
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. */
|
|
420
461
|
const COLORS = [
|
|
421
|
-
'#
|
|
422
|
-
'#
|
|
423
|
-
'#
|
|
424
|
-
'#
|
|
425
|
-
'#C2E0F4',
|
|
426
|
-
'#2DC26B',
|
|
427
|
-
'#F1C40F',
|
|
428
|
-
'#E03E2D',
|
|
429
|
-
'#B96AD9',
|
|
430
|
-
'#3598DB',
|
|
431
|
-
'#169179',
|
|
432
|
-
'#E67E23',
|
|
433
|
-
'#BA372A',
|
|
434
|
-
'#843FA1',
|
|
435
|
-
'#236FA1',
|
|
436
|
-
'#ECF0F1',
|
|
437
|
-
'#CED4D9',
|
|
438
|
-
'#95A5A6',
|
|
439
|
-
'#7E8C8D',
|
|
440
|
-
'#34495E',
|
|
462
|
+
'#FFFFFF',
|
|
463
|
+
'#F4F4F4',
|
|
464
|
+
'#E8EAED',
|
|
465
|
+
'#DADCE0',
|
|
441
466
|
'#000000',
|
|
442
|
-
'#
|
|
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'
|
|
443
487
|
];
|
|
444
488
|
/** Special-character map, grouped for the two-pane picker (tabs left, glyphs right).
|
|
445
489
|
* `CHARS` below is the legacy flat list and is kept exported for back-compat. */
|
|
446
490
|
const CHAR_CATEGORIES = [
|
|
447
491
|
{
|
|
448
492
|
key: 'chcat_currency',
|
|
449
|
-
chars: [
|
|
493
|
+
chars: [
|
|
494
|
+
'€', '£', '¥', '¢', '$', '₹', '₽', '₩', '₺', '₴', '₦', '฿', '₫', '₪', '﷼', '¤',
|
|
495
|
+
'₡', '₱', '₲', '₵', '₭', '₮', '₸', '₼', '₾', '₿'
|
|
496
|
+
]
|
|
450
497
|
},
|
|
451
498
|
{
|
|
452
499
|
key: 'chcat_text',
|
|
453
500
|
chars: [
|
|
454
|
-
'§', '¶', '†', '‡', '•', '…', '′', '″', '‰', '–', '—', '¡', '¿', '·',
|
|
455
|
-
'‹', '›', '«', '»', '“', '”', '‘', '’', '„', '‚', '¦', 'ª', 'º'
|
|
501
|
+
'§', '¶', '†', '‡', '•', '…', '′', '″', '‰', '‱', '–', '—', '―', '¡', '¿', '·',
|
|
502
|
+
'‹', '›', '«', '»', '“', '”', '‘', '’', '„', '‚', '¦', 'ª', 'º',
|
|
503
|
+
'‽', '※', '❝', '❞', '❮', '❯'
|
|
456
504
|
]
|
|
457
505
|
},
|
|
458
506
|
{
|
|
459
507
|
key: 'chcat_math',
|
|
460
508
|
chars: [
|
|
461
509
|
'°', '±', '×', '÷', '≈', '≠', '≤', '≥', '∞', 'µ', '√', '∑', '∫', '∂', '∆', '∏',
|
|
462
|
-
'π', 'Ω', 'α', 'β', 'γ', '
|
|
463
|
-
'
|
|
510
|
+
'π', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'θ', 'λ', 'σ', 'φ', 'ψ', 'ω',
|
|
511
|
+
'∈', '∉', '∪', '∩', '⊂', '⊃', '⊆', '⊇', '∀', '∃', '∅', '∝', '∴', '∵', '¬', '∧', '∨',
|
|
512
|
+
'½', '⅓', '¼', '¾', '⅔', '⅛', '⅜', '⅝', '⅞',
|
|
513
|
+
'¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹', '⁰',
|
|
514
|
+
'₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉', '₀'
|
|
464
515
|
]
|
|
465
516
|
},
|
|
466
517
|
{
|
|
467
518
|
key: 'chcat_arrows',
|
|
468
|
-
chars: [
|
|
519
|
+
chars: [
|
|
520
|
+
'←', '→', '↑', '↓', '↔', '↕', '⇐', '⇒', '⇑', '⇓', '⇔',
|
|
521
|
+
'↖', '↗', '↘', '↙', '↩', '↪', '↺', '↻',
|
|
522
|
+
'⤴', '⤵', '➜', '➝', '➞', '➟', '⇦', '⇨', '⇧', '⇩'
|
|
523
|
+
]
|
|
469
524
|
},
|
|
470
525
|
{
|
|
471
526
|
key: 'chcat_symbols',
|
|
472
527
|
chars: [
|
|
473
|
-
'©', '®', '™', '℠', '№', '℮',
|
|
474
|
-
'
|
|
528
|
+
'©', '®', '™', '℠', '№', '℮',
|
|
529
|
+
'✓', '✔', '✗', '✘',
|
|
530
|
+
'★', '☆', '✦', '✪',
|
|
531
|
+
'♠', '♣', '♥', '♦',
|
|
532
|
+
'♪', '♫', '♬',
|
|
533
|
+
'☀', '☁', '☂', '☃', '☾',
|
|
534
|
+
'☹', '☺', '☻',
|
|
535
|
+
'✉', '✂', '✎', '✏',
|
|
536
|
+
'☎', '☏', '⌛', '⌚',
|
|
537
|
+
'☞', '☜', '☝', '☟'
|
|
475
538
|
]
|
|
476
539
|
},
|
|
477
540
|
{
|
|
478
541
|
key: 'chcat_latin',
|
|
479
542
|
chars: [
|
|
480
543
|
'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï',
|
|
481
|
-
'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'ß',
|
|
544
|
+
'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß',
|
|
482
545
|
'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï',
|
|
483
|
-
'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ'
|
|
546
|
+
'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ',
|
|
547
|
+
'Œ', 'œ', 'Š', 'š', 'Ž', 'ž', 'Ł', 'ł'
|
|
484
548
|
]
|
|
485
549
|
},
|
|
486
550
|
{
|
|
487
551
|
key: 'chcat_arabic',
|
|
488
|
-
chars: [
|
|
552
|
+
chars: [
|
|
553
|
+
'؟', '،', '؛', '٪', '٭', 'ـ',
|
|
554
|
+
'٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩',
|
|
555
|
+
'﴾', '﴿', '۞', '؎',
|
|
556
|
+
'ء', 'آ', 'أ', 'ؤ', 'إ', 'ئ', 'ى', 'ة'
|
|
557
|
+
]
|
|
489
558
|
}
|
|
490
559
|
];
|
|
491
560
|
/** Emoji map for the emoji picker — same two-pane dialog as the character map. */
|
|
@@ -726,8 +795,14 @@ const IC = {
|
|
|
726
795
|
ol_romanu: LIST(['I', 'II', 'III'], 6.5),
|
|
727
796
|
video: S('<rect x="3" y="6" width="13" height="12" rx="1.5"/><path d="M16 10l5-3v10l-5-3z"/>'),
|
|
728
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"/>'),
|
|
729
|
-
|
|
730
|
-
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"/>')
|
|
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"/>')
|
|
731
806
|
};
|
|
732
807
|
const TXT = (html) => `<span class="txt">${html}</span>`;
|
|
733
808
|
|
|
@@ -1618,6 +1693,82 @@ const TPL_ICON = {
|
|
|
1618
1693
|
'img-top': IC.tpltop,
|
|
1619
1694
|
'img-center': IC.tplcenter
|
|
1620
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
|
+
}
|
|
1621
1772
|
class FableEditor {
|
|
1622
1773
|
static instanceCounter = 0;
|
|
1623
1774
|
options;
|
|
@@ -1667,9 +1818,11 @@ class FableEditor {
|
|
|
1667
1818
|
vphUploadTarget = null;
|
|
1668
1819
|
videoUploadHandler;
|
|
1669
1820
|
onVideoUploadError;
|
|
1670
|
-
mathActive = null;
|
|
1671
|
-
mathCtx = null;
|
|
1672
1821
|
selCtx = null;
|
|
1822
|
+
tbrMore = null;
|
|
1823
|
+
tbrExpanded = false;
|
|
1824
|
+
codeActive = null;
|
|
1825
|
+
codeCtx = null;
|
|
1673
1826
|
tplActive = null;
|
|
1674
1827
|
tplCtx = null;
|
|
1675
1828
|
docInput;
|
|
@@ -1701,6 +1854,7 @@ class FableEditor {
|
|
|
1701
1854
|
this.videoUploadHandler = options.videoUploadHandler;
|
|
1702
1855
|
this.onVideoUploadError = options.onVideoUploadError;
|
|
1703
1856
|
this.contentStyle = options.contentStyle;
|
|
1857
|
+
this.applyTheme(options);
|
|
1704
1858
|
this.lang = this.options.language;
|
|
1705
1859
|
this.draftStorageKey = 'tmclone-draft:' + this.options.draftKey;
|
|
1706
1860
|
this.initDOM();
|
|
@@ -1759,6 +1913,7 @@ class FableEditor {
|
|
|
1759
1913
|
this.shell.appendChild(this.contentStyleEl);
|
|
1760
1914
|
}
|
|
1761
1915
|
target.appendChild(this.shell);
|
|
1916
|
+
installTooltips();
|
|
1762
1917
|
if (!this.options.statusbar)
|
|
1763
1918
|
this.statusbar.style.display = 'none';
|
|
1764
1919
|
this.imgInput = document.createElement('input');
|
|
@@ -1777,6 +1932,18 @@ class FableEditor {
|
|
|
1777
1932
|
this.docInput.style.display = 'none';
|
|
1778
1933
|
target.appendChild(this.docInput);
|
|
1779
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
|
+
}
|
|
1780
1947
|
/** Rewrites the literal word `body` in a user-supplied `contentStyle` string to a
|
|
1781
1948
|
* selector scoped to this instance's content div. Everything else passes through
|
|
1782
1949
|
* verbatim, since `.ed` is a plain div in the host page (not an iframe) and an
|
|
@@ -1841,7 +2008,7 @@ class FableEditor {
|
|
|
1841
2008
|
this.positionTplCtx();
|
|
1842
2009
|
this.positionVphCtx();
|
|
1843
2010
|
this.positionVidCtx();
|
|
1844
|
-
this.
|
|
2011
|
+
this.positionCodeCtx();
|
|
1845
2012
|
});
|
|
1846
2013
|
this.on(this.ed, 'keydown', (e) => {
|
|
1847
2014
|
if (e.altKey && e.key === '0') {
|
|
@@ -1856,8 +2023,8 @@ class FableEditor {
|
|
|
1856
2023
|
e.preventDefault();
|
|
1857
2024
|
this.removeVphPlaceholder();
|
|
1858
2025
|
}
|
|
1859
|
-
if (e.key === '
|
|
1860
|
-
this.
|
|
2026
|
+
if (e.key === '`' && !e.ctrlKey && !e.metaKey && !e.altKey)
|
|
2027
|
+
this.tryCodeTyping(e);
|
|
1861
2028
|
});
|
|
1862
2029
|
this.on(this.ed, 'paste', (e) => this.handlePaste(e));
|
|
1863
2030
|
this.on(this.ed, 'drop', (e) => {
|
|
@@ -1910,6 +2077,14 @@ class FableEditor {
|
|
|
1910
2077
|
this.selectTemplate(tpl);
|
|
1911
2078
|
else
|
|
1912
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
|
+
}
|
|
1913
2088
|
const img = target.closest?.('img');
|
|
1914
2089
|
if (img && this.ed.contains(img) && !this.options.readonly) {
|
|
1915
2090
|
e.preventDefault();
|
|
@@ -1926,14 +2101,6 @@ class FableEditor {
|
|
|
1926
2101
|
else {
|
|
1927
2102
|
this.clearVidHandles();
|
|
1928
2103
|
}
|
|
1929
|
-
const mathEl = target.closest?.('.math-fable');
|
|
1930
|
-
if (mathEl && this.ed.contains(mathEl) && !this.options.readonly) {
|
|
1931
|
-
e.preventDefault();
|
|
1932
|
-
this.selectMathEl(mathEl);
|
|
1933
|
-
}
|
|
1934
|
-
else {
|
|
1935
|
-
this.clearMathSel();
|
|
1936
|
-
}
|
|
1937
2104
|
});
|
|
1938
2105
|
this.on(this.ed, 'mouseover', (e) => {
|
|
1939
2106
|
if (this.options.readonly)
|
|
@@ -1963,7 +2130,7 @@ class FableEditor {
|
|
|
1963
2130
|
this.positionTplCtx();
|
|
1964
2131
|
this.positionVphCtx();
|
|
1965
2132
|
this.positionVidCtx();
|
|
1966
|
-
this.
|
|
2133
|
+
this.positionCodeCtx();
|
|
1967
2134
|
});
|
|
1968
2135
|
this.onWin('scroll', () => {
|
|
1969
2136
|
this.positionTableHandles();
|
|
@@ -1972,7 +2139,7 @@ class FableEditor {
|
|
|
1972
2139
|
this.positionTplCtx();
|
|
1973
2140
|
this.positionVphCtx();
|
|
1974
2141
|
this.positionVidCtx();
|
|
1975
|
-
this.
|
|
2142
|
+
this.positionCodeCtx();
|
|
1976
2143
|
this.repositionSelToolbar();
|
|
1977
2144
|
}, true);
|
|
1978
2145
|
this.onWin('resize', () => {
|
|
@@ -1982,9 +2149,17 @@ class FableEditor {
|
|
|
1982
2149
|
this.positionTplCtx();
|
|
1983
2150
|
this.positionVphCtx();
|
|
1984
2151
|
this.positionVidCtx();
|
|
1985
|
-
this.
|
|
2152
|
+
this.positionCodeCtx();
|
|
1986
2153
|
this.repositionSelToolbar();
|
|
2154
|
+
this.updateToolbarOverflow();
|
|
1987
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
|
+
}
|
|
1988
2163
|
this.on(this.imgInput, 'change', () => {
|
|
1989
2164
|
const file = this.imgInput.files?.[0];
|
|
1990
2165
|
if (!file)
|
|
@@ -2039,7 +2214,7 @@ class FableEditor {
|
|
|
2039
2214
|
(this.selCtx && this.selCtx.contains(e.target)) ||
|
|
2040
2215
|
(this.vphCtx && this.vphCtx.contains(e.target)) ||
|
|
2041
2216
|
(this.vidCtx && this.vidCtx.contains(e.target)) ||
|
|
2042
|
-
(this.
|
|
2217
|
+
(this.codeCtx && this.codeCtx.contains(e.target)) ||
|
|
2043
2218
|
(this.openPop && this.openPop.contains(e.target)) ||
|
|
2044
2219
|
(this.openSubEl && this.openSubEl.contains(e.target)))
|
|
2045
2220
|
return;
|
|
@@ -2049,7 +2224,7 @@ class FableEditor {
|
|
|
2049
2224
|
this.clearTplSel();
|
|
2050
2225
|
this.clearVphPlaceholderSel();
|
|
2051
2226
|
this.clearVidHandles();
|
|
2052
|
-
this.
|
|
2227
|
+
this.clearCodeSel();
|
|
2053
2228
|
this.clearSelToolbar();
|
|
2054
2229
|
});
|
|
2055
2230
|
}
|
|
@@ -2078,7 +2253,7 @@ class FableEditor {
|
|
|
2078
2253
|
this.clearTplSel();
|
|
2079
2254
|
this.clearVphPlaceholderSel();
|
|
2080
2255
|
this.clearVidHandles();
|
|
2081
|
-
this.
|
|
2256
|
+
this.clearCodeSel();
|
|
2082
2257
|
this.clearSelToolbar();
|
|
2083
2258
|
this.onChange();
|
|
2084
2259
|
}
|
|
@@ -2143,7 +2318,7 @@ class FableEditor {
|
|
|
2143
2318
|
this.clearTplSel();
|
|
2144
2319
|
this.clearVphPlaceholderSel();
|
|
2145
2320
|
this.clearVidHandles();
|
|
2146
|
-
this.
|
|
2321
|
+
this.clearCodeSel();
|
|
2147
2322
|
this.clearSelToolbar();
|
|
2148
2323
|
this.listeners.forEach((fn) => fn());
|
|
2149
2324
|
this.listeners = [];
|
|
@@ -2343,14 +2518,14 @@ class FableEditor {
|
|
|
2343
2518
|
sub.style.top = y + 'px';
|
|
2344
2519
|
this.openSubEl = sub;
|
|
2345
2520
|
}
|
|
2346
|
-
popup(anchor, build) {
|
|
2521
|
+
popup(anchor, build, cls) {
|
|
2347
2522
|
if (this.openPop && this.popAnchor === anchor) {
|
|
2348
2523
|
this.closePop();
|
|
2349
2524
|
return;
|
|
2350
2525
|
}
|
|
2351
2526
|
this.closePop();
|
|
2352
2527
|
const el = document.createElement('div');
|
|
2353
|
-
el.className = 'pop';
|
|
2528
|
+
el.className = 'pop' + (cls ? ' ' + cls : '');
|
|
2354
2529
|
el.dir = this.dir();
|
|
2355
2530
|
build(el);
|
|
2356
2531
|
document.body.appendChild(el);
|
|
@@ -2532,53 +2707,61 @@ class FableEditor {
|
|
|
2532
2707
|
el.appendChild(grid);
|
|
2533
2708
|
});
|
|
2534
2709
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
grid.appendChild(b);
|
|
2548
|
-
});
|
|
2549
|
-
el.appendChild(grid);
|
|
2550
|
-
el.insertAdjacentHTML('beforeend', '<div class="sep"></div>');
|
|
2551
|
-
const foot = document.createElement('div');
|
|
2552
|
-
foot.className = 'cfoot';
|
|
2553
|
-
const cur = document.createElement('div');
|
|
2554
|
-
cur.className = 'cswatch cur';
|
|
2555
|
-
cur.title = this.t('currentcolor');
|
|
2556
|
-
const curColor = kind === 'fore' ? this.foreColor : this.backColor;
|
|
2557
|
-
cur.style.background = /^#[0-9a-f]{6}$/i.test(curColor) ? curColor : '#000000';
|
|
2558
|
-
cur.innerHTML = IC.check;
|
|
2559
|
-
const nocolor = document.createElement('button');
|
|
2560
|
-
nocolor.type = 'button';
|
|
2561
|
-
nocolor.className = 'cswatch nocolor';
|
|
2562
|
-
nocolor.title = this.t('removecolor');
|
|
2563
|
-
nocolor.addEventListener('click', () => {
|
|
2564
|
-
this.closePop();
|
|
2565
|
-
this.applyColor(kind, null);
|
|
2566
|
-
});
|
|
2567
|
-
const wheel = document.createElement('label');
|
|
2568
|
-
wheel.className = 'cswatch cwheel';
|
|
2569
|
-
wheel.title = this.t('customcolor');
|
|
2570
|
-
wheel.innerHTML = IC.palette;
|
|
2571
|
-
const picker = document.createElement('input');
|
|
2572
|
-
picker.type = 'color';
|
|
2573
|
-
picker.value = /^#[0-9a-f]{6}$/i.test(curColor) ? curColor : '#000000';
|
|
2574
|
-
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', () => {
|
|
2575
2722
|
this.closePop();
|
|
2576
|
-
|
|
2723
|
+
apply(c);
|
|
2577
2724
|
});
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2725
|
+
grid.appendChild(b);
|
|
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);
|
|
2581
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');
|
|
2582
2765
|
}
|
|
2583
2766
|
applyColor(kind, c) {
|
|
2584
2767
|
if (kind === 'fore') {
|
|
@@ -2741,11 +2924,12 @@ class FableEditor {
|
|
|
2741
2924
|
applyBlockStyle(prop, value) {
|
|
2742
2925
|
this.restoreSel();
|
|
2743
2926
|
const blocks = this.selectedBlocks();
|
|
2927
|
+
const cssProp = prop === 'lineHeight' ? 'line-height' : prop === 'wordSpacing' ? 'word-spacing' : 'letter-spacing';
|
|
2744
2928
|
blocks.forEach((b) => {
|
|
2745
2929
|
if (value)
|
|
2746
2930
|
b.style[prop] = value;
|
|
2747
2931
|
else
|
|
2748
|
-
b.style.removeProperty(
|
|
2932
|
+
b.style.removeProperty(cssProp);
|
|
2749
2933
|
});
|
|
2750
2934
|
this.saveSel();
|
|
2751
2935
|
this.onChange();
|
|
@@ -2781,6 +2965,19 @@ class FableEditor {
|
|
|
2781
2965
|
]);
|
|
2782
2966
|
});
|
|
2783
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
|
+
}
|
|
2784
2981
|
closestAnchor() {
|
|
2785
2982
|
const s = window.getSelection();
|
|
2786
2983
|
let node = s && this.ed.contains(s.anchorNode) ? s.anchorNode : null;
|
|
@@ -2981,16 +3178,18 @@ class FableEditor {
|
|
|
2981
3178
|
indent: () => this.tbtn(IC.indent, this.t('indent'), () => this.exec('indent')),
|
|
2982
3179
|
link: () => this.tbtn(IC.link, this.t('linkttl'), () => this.linkDlg()),
|
|
2983
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()),
|
|
2984
3183
|
changecase: () => this.menuTbtn(IC.caseic, this.t('changecase'), (b) => this.caseMenu(b)),
|
|
2985
3184
|
lineheight: () => this.menuTbtn(IC.lineheight, this.t('lineheight'), (b) => this.lineHeightMenu(b)),
|
|
2986
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)),
|
|
2987
3187
|
removeformat: () => this.tbtn(IC.clearformatic, this.t('removeformat'), () => this.exec('removeFormat')),
|
|
2988
3188
|
blocks: () => this.tsel('w-block', 'blocksel', this.t('para'), (b) => this.blocksMenu(b)),
|
|
2989
3189
|
ltr: () => this.tbtn(IC.ltr, this.t('ltr'), () => this.setDir('ltr'), 'ltr'),
|
|
2990
3190
|
rtl: () => this.tbtn(IC.rtl, this.t('rtl'), () => this.setDir('rtl'), 'rtl'),
|
|
2991
3191
|
quickimage: () => this.tbtn(IC.image, this.t('quickimage'), () => this.insertImagePlaceholder()),
|
|
2992
3192
|
quickvideo: () => this.tbtn(IC.video, this.t('quickvideo'), () => this.videoDlg()),
|
|
2993
|
-
mathformula: () => this.tbtn(IC.mathic, this.t('mathformula'), () => this.mathDlg()),
|
|
2994
3193
|
quicktable: () => tableBtn,
|
|
2995
3194
|
template: () => this.menuTbtn(IC.templateic, this.t('template'), (b) => this.templateMenu(b)),
|
|
2996
3195
|
charmap: () => this.tbtn(IC.charic, this.t('charmap'), () => this.charMap()),
|
|
@@ -3027,7 +3226,40 @@ class FableEditor {
|
|
|
3027
3226
|
if (elements.length)
|
|
3028
3227
|
this.toolbar.appendChild(this.group(...elements));
|
|
3029
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);
|
|
3030
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);
|
|
3031
3263
|
}
|
|
3032
3264
|
/* ---------------------------------------------------------- menubar */
|
|
3033
3265
|
mItem(label, icon, action, shortcut) {
|
|
@@ -3072,8 +3304,8 @@ class FableEditor {
|
|
|
3072
3304
|
this.mItem('link', IC.link, () => this.linkDlg()),
|
|
3073
3305
|
this.mItem('image', IC.image, () => this.insertImagePlaceholder()),
|
|
3074
3306
|
this.mItem('video', IC.video, () => this.videoDlg()),
|
|
3075
|
-
this.mItem('mathformula', IC.mathic, () => this.mathDlg()),
|
|
3076
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()),
|
|
3077
3309
|
{ label: this.t('template'), icon: IC.templateic, subBuild: (el) => this.buildTemplatePickInto(el) },
|
|
3078
3310
|
this.mItem('hr', IC.hric, () => this.exec('insertHorizontalRule')),
|
|
3079
3311
|
'|',
|
|
@@ -3099,6 +3331,7 @@ class FableEditor {
|
|
|
3099
3331
|
this.mItem('strikethrough', IC.strikethrough, () => this.exec('strikeThrough')),
|
|
3100
3332
|
this.mItem('superscript', IC.superscriptic, () => this.exec('superscript')),
|
|
3101
3333
|
this.mItem('subscript', IC.subscriptic, () => this.exec('subscript')),
|
|
3334
|
+
this.mItem('code', IC.inlinecodeic, () => this.toggleInlineCode()),
|
|
3102
3335
|
'|',
|
|
3103
3336
|
this.mItem('lowercase', IC.lowercaseic, () => this.transformCase('lower')),
|
|
3104
3337
|
this.mItem('uppercase', IC.uppercaseic, () => this.transformCase('upper')),
|
|
@@ -3112,7 +3345,10 @@ class FableEditor {
|
|
|
3112
3345
|
'|',
|
|
3113
3346
|
{
|
|
3114
3347
|
label: this.t('cell'),
|
|
3115
|
-
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
|
+
]
|
|
3116
3352
|
},
|
|
3117
3353
|
{
|
|
3118
3354
|
label: this.t('row'),
|
|
@@ -3121,6 +3357,9 @@ class FableEditor {
|
|
|
3121
3357
|
{ label: this.t('rowbelow'), icon: IC.rowafter, action: () => this.tableOp('rowbelow'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
3122
3358
|
{ label: this.t('delrow'), icon: IC.rowdelete, action: () => this.tableOp('delrow'), hover: (on) => this.setOpTarget(on ? 'row' : null) },
|
|
3123
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
|
+
'|',
|
|
3124
3363
|
{ label: this.t('rowprops'), icon: IC.tableic, action: () => this.rowPropsDlg(), hover: (on) => this.setOpTarget(on ? 'row' : null) }
|
|
3125
3364
|
]
|
|
3126
3365
|
},
|
|
@@ -3131,6 +3370,9 @@ class FableEditor {
|
|
|
3131
3370
|
{ label: this.t('colafter'), icon: IC.colafter, action: () => this.tableOp('colafter'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
3132
3371
|
{ label: this.t('delcol'), icon: IC.coldelete, action: () => this.tableOp('delcol'), hover: (on) => this.setOpTarget(on ? 'col' : null) },
|
|
3133
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
|
+
'|',
|
|
3134
3376
|
{ label: this.t('colprops'), icon: IC.tableic, action: () => this.colPropsDlg(), hover: (on) => this.setOpTarget(on ? 'col' : null) }
|
|
3135
3377
|
]
|
|
3136
3378
|
},
|
|
@@ -3241,6 +3483,93 @@ class FableEditor {
|
|
|
3241
3483
|
this.onChange();
|
|
3242
3484
|
this.positionTableHandles();
|
|
3243
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
|
+
}
|
|
3244
3573
|
applyTableProps(table, vals) {
|
|
3245
3574
|
if (vals.width)
|
|
3246
3575
|
table.style.width = /^\d+$/.test(vals.width) ? vals.width + 'px' : vals.width;
|
|
@@ -4391,215 +4720,260 @@ class FableEditor {
|
|
|
4391
4720
|
}
|
|
4392
4721
|
]);
|
|
4393
4722
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
try {
|
|
4397
|
-
return katex.renderToString(latex, { throwOnError: false, displayMode: block, strict: 'ignore' });
|
|
4398
|
-
}
|
|
4399
|
-
catch {
|
|
4400
|
-
return '';
|
|
4401
|
-
}
|
|
4723
|
+
escapeAttr(s) {
|
|
4724
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
4402
4725
|
}
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
const
|
|
4410
|
-
const
|
|
4411
|
-
|
|
4412
|
-
document.execCommand('insertHTML', false, `<${tag} class="math-fable${block ? ' math-fable-block' : ''}" contenteditable="false" data-latex="${this.escapeAttr(src)}">${rendered}</${tag}>`);
|
|
4413
|
-
}
|
|
4414
|
-
/** Recognizes a plain-text clipboard payload that is entirely one math formula:
|
|
4415
|
-
* $$…$$ / \[…\] (block), \(…\) (inline), a \begin{…}…\end{…} environment (block),
|
|
4416
|
-
* or $…$ (inline — only when the inside looks like LaTeX, so "$50" stays text).
|
|
4417
|
-
* Multi-line block content (a derivation pasted as separate lines) gets its lines
|
|
4418
|
-
* joined with \\ so each step lands on its own line. Returns null for anything
|
|
4419
|
-
* else, letting the normal paste pipeline run unchanged. */
|
|
4420
|
-
mathFromClipboard(text) {
|
|
4421
|
-
const t = text.trim();
|
|
4422
|
-
if (!t)
|
|
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)
|
|
4423
4735
|
return null;
|
|
4424
|
-
const
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
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
|
+
}
|
|
4433
4762
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
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);
|
|
4442
4776
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
* Content that already breaks rows itself (\\ present, or a \begin environment
|
|
4447
|
-
* managing its own layout) only has its newlines collapsed to spaces. */
|
|
4448
|
-
joinDerivationLines(src) {
|
|
4449
|
-
if (!/\n/.test(src))
|
|
4450
|
-
return src;
|
|
4451
|
-
if (/\\\\/.test(src) || /\\begin\{/.test(src))
|
|
4452
|
-
return src.replace(/\s*\n\s*/g, ' ');
|
|
4453
|
-
return src
|
|
4454
|
-
.split(/\n+/)
|
|
4455
|
-
.map((l) => l.trim())
|
|
4456
|
-
.filter(Boolean)
|
|
4457
|
-
.join(' \\\\ ');
|
|
4777
|
+
this.saveSel();
|
|
4778
|
+
this.refreshState();
|
|
4779
|
+
this.onChange();
|
|
4458
4780
|
}
|
|
4459
|
-
/** Typing
|
|
4460
|
-
* so
|
|
4461
|
-
|
|
4462
|
-
* syntax (\ ^ _ { } =) — a literal "$5 and 3$" stays plain text. */
|
|
4463
|
-
tryMathTyping(e) {
|
|
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) {
|
|
4464
4784
|
const sel = window.getSelection();
|
|
4465
4785
|
if (!sel || !sel.isCollapsed || !sel.anchorNode)
|
|
4466
4786
|
return;
|
|
4467
4787
|
const node = sel.anchorNode;
|
|
4468
4788
|
if (node.nodeType !== Node.TEXT_NODE || !this.ed.contains(node))
|
|
4469
4789
|
return;
|
|
4470
|
-
if (node.parentElement?.closest('
|
|
4790
|
+
if (node.parentElement?.closest('code, pre'))
|
|
4471
4791
|
return;
|
|
4472
4792
|
const before = (node.textContent || '').slice(0, sel.anchorOffset);
|
|
4473
|
-
const m = before.match(/(?:^|[
|
|
4793
|
+
const m = before.match(/(?:^|[^`])`([^`]+)$/);
|
|
4474
4794
|
if (!m)
|
|
4475
4795
|
return;
|
|
4476
4796
|
const src = m[1];
|
|
4477
|
-
if (/^\s|\s$/.test(src)
|
|
4478
|
-
return;
|
|
4479
|
-
const rendered = this.renderMathHTML(src, false);
|
|
4480
|
-
if (!rendered || rendered.includes('katex-error'))
|
|
4797
|
+
if (/^\s|\s$/.test(src))
|
|
4481
4798
|
return;
|
|
4482
4799
|
e.preventDefault();
|
|
4483
4800
|
const range = document.createRange();
|
|
4484
4801
|
range.setStart(node, sel.anchorOffset - src.length - 1);
|
|
4485
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);
|
|
4486
4813
|
sel.removeAllRanges();
|
|
4487
|
-
sel.addRange(
|
|
4488
|
-
this.
|
|
4814
|
+
sel.addRange(after);
|
|
4815
|
+
this.saveSel();
|
|
4489
4816
|
this.onChange();
|
|
4490
4817
|
}
|
|
4491
|
-
|
|
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) {
|
|
4492
4873
|
this.saveSel();
|
|
4493
|
-
|
|
4494
|
-
const initialLatex = existing?.dataset.latex ? this.unescapeAttr(existing.dataset.latex) : '';
|
|
4874
|
+
let langDd;
|
|
4495
4875
|
let ta;
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
const
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
};
|
|
4504
|
-
this.dialog(this.t('mathdlgttl'), (body) => {
|
|
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);
|
|
4505
4883
|
ta = document.createElement('textarea');
|
|
4506
|
-
ta.
|
|
4507
|
-
ta.
|
|
4508
|
-
ta.
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
derivChk.type = 'checkbox';
|
|
4513
|
-
derivChk.checked = isBlock;
|
|
4514
|
-
chkRow.append(derivChk, document.createTextNode(this.t('mathderivation')));
|
|
4515
|
-
preview = document.createElement('div');
|
|
4516
|
-
preview.className = 'math-preview';
|
|
4517
|
-
preview.style.cssText = 'min-height:40px;margin-top:8px;padding:10px;border:1px solid #e1e5ea;border-radius:6px;overflow:auto';
|
|
4518
|
-
ta.addEventListener('input', updatePreview);
|
|
4519
|
-
derivChk.addEventListener('change', updatePreview);
|
|
4520
|
-
body.append(ta, chkRow, preview);
|
|
4521
|
-
setTimeout(() => {
|
|
4522
|
-
ta.focus();
|
|
4523
|
-
updatePreview();
|
|
4524
|
-
}, 0);
|
|
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);
|
|
4525
4890
|
}, [
|
|
4526
4891
|
{ label: this.t('cancel'), action: () => this.closeDlg() },
|
|
4527
4892
|
{
|
|
4528
4893
|
label: this.t('save'),
|
|
4529
4894
|
primary: true,
|
|
4530
4895
|
action: () => {
|
|
4531
|
-
const src = ta.value.
|
|
4896
|
+
const src = ta.value.replace(/\s+$/, '');
|
|
4897
|
+
const lang = langDd.value();
|
|
4898
|
+
const label = CODE_LANGS.find(([v]) => v === lang)?.[1] || lang;
|
|
4532
4899
|
this.closeDlg();
|
|
4533
|
-
if (!src)
|
|
4900
|
+
if (!src.trim())
|
|
4534
4901
|
return;
|
|
4535
|
-
const block = derivChk.checked;
|
|
4536
4902
|
if (existing) {
|
|
4537
|
-
|
|
4538
|
-
existing.
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
this.
|
|
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();
|
|
4542
4908
|
this.onChange();
|
|
4543
4909
|
return;
|
|
4544
4910
|
}
|
|
4545
4911
|
this.restoreSel();
|
|
4546
|
-
|
|
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>`);
|
|
4547
4914
|
this.onChange();
|
|
4548
4915
|
}
|
|
4549
4916
|
}
|
|
4550
4917
|
]);
|
|
4551
4918
|
}
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
}
|
|
4558
|
-
clearMathSel() {
|
|
4559
|
-
this.mathCtx?.remove();
|
|
4560
|
-
this.mathCtx = null;
|
|
4561
|
-
this.mathActive?.classList.remove('math-selected');
|
|
4562
|
-
this.mathActive = null;
|
|
4919
|
+
clearCodeSel() {
|
|
4920
|
+
this.codeCtx?.remove();
|
|
4921
|
+
this.codeCtx = null;
|
|
4922
|
+
this.codeActive?.classList.remove('code-selected');
|
|
4923
|
+
this.codeActive = null;
|
|
4563
4924
|
}
|
|
4564
|
-
|
|
4565
|
-
if (this.
|
|
4925
|
+
selectCodeEl(el) {
|
|
4926
|
+
if (this.codeActive === el)
|
|
4566
4927
|
return;
|
|
4567
|
-
this.
|
|
4568
|
-
this.
|
|
4569
|
-
el.classList.add('
|
|
4570
|
-
this.
|
|
4571
|
-
this.
|
|
4928
|
+
this.clearCodeSel();
|
|
4929
|
+
this.codeActive = el;
|
|
4930
|
+
el.classList.add('code-selected');
|
|
4931
|
+
this.codeCtx = this.buildCodeCtxToolbar(el);
|
|
4932
|
+
this.positionCodeCtx();
|
|
4572
4933
|
}
|
|
4573
|
-
|
|
4934
|
+
buildCodeCtxToolbar(el) {
|
|
4574
4935
|
const ctx = document.createElement('div');
|
|
4575
4936
|
ctx.className = 'imgctx';
|
|
4576
4937
|
ctx.dir = this.dir();
|
|
4577
|
-
ctx.append(this.ctxBtn(IC.editic, this.t('
|
|
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'), () => {
|
|
4578
4939
|
el.remove();
|
|
4579
|
-
this.
|
|
4940
|
+
this.clearCodeSel();
|
|
4580
4941
|
this.onChange();
|
|
4581
4942
|
}));
|
|
4582
4943
|
ctx.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4583
4944
|
document.body.appendChild(ctx);
|
|
4584
4945
|
return ctx;
|
|
4585
4946
|
}
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4947
|
+
copyCodeBlock(el) {
|
|
4948
|
+
const text = el.querySelector('code')?.textContent ?? el.textContent ?? '';
|
|
4949
|
+
if (navigator.clipboard?.writeText) {
|
|
4950
|
+
navigator.clipboard.writeText(text).catch(() => { });
|
|
4589
4951
|
return;
|
|
4590
4952
|
}
|
|
4591
|
-
const
|
|
4592
|
-
|
|
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();
|
|
4593
4963
|
return;
|
|
4594
|
-
|
|
4595
|
-
const
|
|
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;
|
|
4596
4970
|
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4597
4971
|
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4598
4972
|
let cy = r.top + scrollY - ch - 8;
|
|
4599
4973
|
if (cy < this.ctxMinTop())
|
|
4600
4974
|
cy = r.bottom + scrollY + 8;
|
|
4601
|
-
this.
|
|
4602
|
-
this.
|
|
4975
|
+
this.codeCtx.style.left = cx + 'px';
|
|
4976
|
+
this.codeCtx.style.top = cy + 'px';
|
|
4603
4977
|
}
|
|
4604
4978
|
/* ---------------------------------------------------------- templates */
|
|
4605
4979
|
templateMenu(anchor) {
|
|
@@ -4840,7 +5214,8 @@ class FableEditor {
|
|
|
4840
5214
|
b.addEventListener('mouseleave', () => this.setOpTarget(null));
|
|
4841
5215
|
return b;
|
|
4842
5216
|
};
|
|
4843
|
-
|
|
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()));
|
|
4844
5219
|
document.body.appendChild(el);
|
|
4845
5220
|
return el;
|
|
4846
5221
|
}
|
|
@@ -5197,7 +5572,7 @@ class FableEditor {
|
|
|
5197
5572
|
const caseBtn = this.ctxBtn(IC.caseic, this.t('changecase'), () => this.caseMenu(caseBtn));
|
|
5198
5573
|
const lhBtn = this.ctxBtn(IC.lineheight, this.t('lineheight'), () => this.lineHeightMenu(lhBtn));
|
|
5199
5574
|
const wsBtn = this.ctxBtn(IC.wordspacing, this.t('wordspacing'), () => this.wordSpacingMenu(wsBtn));
|
|
5200
|
-
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);
|
|
5201
5576
|
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
5202
5577
|
document.body.appendChild(el);
|
|
5203
5578
|
this.syncColorSwatches(el);
|
|
@@ -5280,6 +5655,9 @@ class FableEditor {
|
|
|
5280
5655
|
if (b)
|
|
5281
5656
|
b.classList.toggle('on', inEd && blkTag === tag);
|
|
5282
5657
|
});
|
|
5658
|
+
const codeB = container.querySelector('[data-id=code]');
|
|
5659
|
+
if (codeB)
|
|
5660
|
+
codeB.classList.toggle('on', inEd && !!this.closestInlineCode());
|
|
5283
5661
|
});
|
|
5284
5662
|
const blk = s ? this.closestBlock(s.anchorNode) : null;
|
|
5285
5663
|
const dir = blk?.closest('[dir]')?.getAttribute('dir') || this.ed.getAttribute('dir') || this.dir();
|
|
@@ -5321,18 +5699,10 @@ class FableEditor {
|
|
|
5321
5699
|
return;
|
|
5322
5700
|
}
|
|
5323
5701
|
if (!html) {
|
|
5324
|
-
/*
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
element, and a bare video page URL (YouTube & co.) becomes an
|
|
5328
|
-
embedded player. Anything else falls through unchanged. */
|
|
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. */
|
|
5329
5705
|
const text = cd.getData('text/plain');
|
|
5330
|
-
const math = this.mathFromClipboard(text);
|
|
5331
|
-
if (math) {
|
|
5332
|
-
this.insertMathElement(math.latex, math.block);
|
|
5333
|
-
this.onChange();
|
|
5334
|
-
return;
|
|
5335
|
-
}
|
|
5336
5706
|
const embed = /^\S+$/.test(text.trim()) ? this.videoEmbedUrl(text.trim()) : null;
|
|
5337
5707
|
if (embed) {
|
|
5338
5708
|
document.execCommand('insertHTML', false, this.videoEmbedHTML(embed));
|
|
@@ -5356,7 +5726,7 @@ class FableEditor {
|
|
|
5356
5726
|
this.clearImgPlaceholderSel();
|
|
5357
5727
|
this.clearVphPlaceholderSel();
|
|
5358
5728
|
this.clearVidHandles();
|
|
5359
|
-
this.
|
|
5729
|
+
this.clearCodeSel();
|
|
5360
5730
|
this.shell.dir = this.dir();
|
|
5361
5731
|
this.ed.dir = this.dir();
|
|
5362
5732
|
if (this.options.menubar)
|
|
@@ -5380,6 +5750,9 @@ class FableEditorComponent {
|
|
|
5380
5750
|
toolbar = true;
|
|
5381
5751
|
statusbar = true;
|
|
5382
5752
|
readonly = false;
|
|
5753
|
+
primaryColor;
|
|
5754
|
+
toolbarGroupBackground;
|
|
5755
|
+
uiFontFamily;
|
|
5383
5756
|
init = {};
|
|
5384
5757
|
value = '';
|
|
5385
5758
|
editorChange = new EventEmitter();
|
|
@@ -5396,6 +5769,9 @@ class FableEditorComponent {
|
|
|
5396
5769
|
toolbar: this.toolbar,
|
|
5397
5770
|
statusbar: this.statusbar,
|
|
5398
5771
|
readonly: this.readonly,
|
|
5772
|
+
primaryColor: this.primaryColor,
|
|
5773
|
+
toolbarGroupBackground: this.toolbarGroupBackground,
|
|
5774
|
+
uiFontFamily: this.uiFontFamily,
|
|
5399
5775
|
initialContent: this.value,
|
|
5400
5776
|
...this.init,
|
|
5401
5777
|
onChange: (content) => {
|
|
@@ -5440,7 +5816,7 @@ class FableEditorComponent {
|
|
|
5440
5816
|
this.editor?.setContent(value ?? '');
|
|
5441
5817
|
}
|
|
5442
5818
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FableEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5443
|
-
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"] });
|
|
5444
5820
|
}
|
|
5445
5821
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FableEditorComponent, decorators: [{
|
|
5446
5822
|
type: Component,
|
|
@@ -5460,6 +5836,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
5460
5836
|
type: Input
|
|
5461
5837
|
}], readonly: [{
|
|
5462
5838
|
type: Input
|
|
5839
|
+
}], primaryColor: [{
|
|
5840
|
+
type: Input
|
|
5841
|
+
}], toolbarGroupBackground: [{
|
|
5842
|
+
type: Input
|
|
5843
|
+
}], uiFontFamily: [{
|
|
5844
|
+
type: Input
|
|
5463
5845
|
}], init: [{
|
|
5464
5846
|
type: Input
|
|
5465
5847
|
}], value: [{
|