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.
@@ -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 mathformula template charmap emoji | 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';
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
- '#BFEDD2',
422
- '#FBEEB8',
423
- '#F8CAC6',
424
- '#ECCAFA',
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
- '#FFFFFF'
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
- mathic: S('<text x="2" y="18" font-size="17" stroke="none" fill="#222f3e" font-family="Georgia, \'Times New Roman\', serif" font-style="italic">&#8721;</text>'),
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.positionMathCtx();
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 === '$' && !e.ctrlKey && !e.metaKey && !e.altKey)
1860
- this.tryMathTyping(e);
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.positionMathCtx();
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.positionMathCtx();
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.positionMathCtx();
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.mathCtx && this.mathCtx.contains(e.target)) ||
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.clearMathSel();
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.clearMathSel();
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.clearMathSel();
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
- colorMenu(anchor, kind) {
2536
- this.popup(anchor, (el) => {
2537
- const grid = document.createElement('div');
2538
- grid.className = 'cpal';
2539
- COLORS.forEach((c) => {
2540
- const b = document.createElement('button');
2541
- b.style.background = c;
2542
- b.title = c;
2543
- b.addEventListener('click', () => {
2544
- this.closePop();
2545
- this.applyColor(kind, c);
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
- this.applyColor(kind, picker.value);
2723
+ apply(c);
2577
2724
  });
2578
- wheel.appendChild(picker);
2579
- foot.append(cur, nocolor, wheel);
2580
- el.appendChild(foot);
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(prop === 'lineHeight' ? 'line-height' : 'word-spacing');
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: [{ label: this.t('cellprops'), icon: IC.tableic, action: () => this.cellPropsDlg() }]
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
- /* ---------------------------------------------------------- math / LaTeX formulas */
4395
- renderMathHTML(latex, block) {
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, '&amp;').replace(/"/g, '&quot;');
4402
4725
  }
4403
- /** Shared by the math dialog, math paste, and math typing: renders `src` with
4404
- * KaTeX and inserts it at the caret as the standard math element (inline span /
4405
- * block div carrying its LaTeX source in data-latex). Block formulas get the
4406
- * same \begin{aligned} wrapping the dialog applies, so pasted derivations align
4407
- * their steps and re-open correctly in the edit dialog. */
4408
- insertMathElement(src, block) {
4409
- const latex = block ? `\\begin{aligned}${src}\\end{aligned}` : src;
4410
- const rendered = this.renderMathHTML(latex, block);
4411
- const tag = block ? 'div' : 'span';
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 block = t.match(/^\$\$([\s\S]+)\$\$$/) || t.match(/^\\\[([\s\S]+)\\\]$/);
4425
- if (block) {
4426
- const src = block[1].trim();
4427
- if (!src || src.includes('$'))
4428
- return null;
4429
- return { latex: this.joinDerivationLines(src), block: true };
4430
- }
4431
- if (/^\\begin\{[a-zA-Z*]+\}[\s\S]+\\end\{[a-zA-Z*]+\}$/.test(t)) {
4432
- return { latex: this.joinDerivationLines(t), block: true };
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
- const inline = t.match(/^\\\(([\s\S]+)\\\)$/) || t.match(/^\$([^$\n]+)\$$/);
4435
- if (inline) {
4436
- const src = inline[1].trim();
4437
- if (!src || src.includes('$'))
4438
- return null;
4439
- if (t.startsWith('$') && !/[\\^_{}=]/.test(src))
4440
- return null;
4441
- return { latex: src, block: false };
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
- return null;
4444
- }
4445
- /** A derivation pasted as separate lines has no explicit \\ row breaks — add them.
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 "$…$" converts to an inline formula the moment the closing "$" is typed,
4460
- * so math can be written in place without opening the dialog. Only kicks in when
4461
- * the run since the opening "$" parses as valid LaTeX and contains LaTeX-ish
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('.math-fable, pre, code'))
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) || !/[\\^_{}=]/.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(range);
4488
- this.insertMathElement(src, false);
4814
+ sel.addRange(after);
4815
+ this.saveSel();
4489
4816
  this.onChange();
4490
4817
  }
4491
- mathDlg(existing) {
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
- const isBlock = existing?.classList.contains('math-fable-block') ?? false;
4494
- const initialLatex = existing?.dataset.latex ? this.unescapeAttr(existing.dataset.latex) : '';
4874
+ let langDd;
4495
4875
  let ta;
4496
- let derivChk;
4497
- let preview;
4498
- const updatePreview = () => {
4499
- const src = ta.value || '';
4500
- const block = derivChk.checked;
4501
- const latex = block ? `\\begin{aligned}${src}\\end{aligned}` : src;
4502
- preview.innerHTML = this.renderMathHTML(latex, block);
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.value = initialLatex;
4507
- ta.placeholder = 'e.g. x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}';
4508
- ta.style.cssText = 'width:360px;max-width:100%;min-height:80px;font-family:monospace;font-size:13px';
4509
- const chkRow = document.createElement('label');
4510
- chkRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin:8px 0;font-size:13.5px;color:#445';
4511
- derivChk = document.createElement('input');
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.trim();
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
- const latex = block ? `\\begin{aligned}${src}\\end{aligned}` : src;
4538
- existing.className = block ? 'math-fable math-fable-block' : 'math-fable';
4539
- existing.dataset.latex = this.escapeAttr(src);
4540
- existing.innerHTML = this.renderMathHTML(latex, block);
4541
- this.clearMathSel();
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
- this.insertMathElement(src, block);
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
- escapeAttr(s) {
4553
- return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
4554
- }
4555
- unescapeAttr(s) {
4556
- return s.replace(/&quot;/g, '"').replace(/&amp;/g, '&');
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
- selectMathEl(el) {
4565
- if (this.mathActive === el)
4925
+ selectCodeEl(el) {
4926
+ if (this.codeActive === el)
4566
4927
  return;
4567
- this.clearMathSel();
4568
- this.mathActive = el;
4569
- el.classList.add('math-selected');
4570
- this.mathCtx = this.buildMathCtxToolbar(el);
4571
- this.positionMathCtx();
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
- buildMathCtxToolbar(el) {
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('editmath'), () => this.mathDlg(el)), this.ctxSep(), this.ctxBtn(IC.trash, this.t('deletemath'), () => {
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.clearMathSel();
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
- positionMathCtx() {
4587
- if (!this.mathActive || !document.body.contains(this.mathActive)) {
4588
- this.clearMathSel();
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 r = this.mathActive.getBoundingClientRect();
4592
- if (!this.mathCtx)
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
- const cw = this.mathCtx.offsetWidth;
4595
- const ch = this.mathCtx.offsetHeight;
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.mathCtx.style.left = cx + 'px';
4602
- this.mathCtx.style.top = cy + 'px';
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
- 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.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.coldelete, this.t('delcol'), () => this.tableOp('delcol')), 'col'), this.ctxSep(), this.ctxBtn(IC.trash, this.t('deltable'), () => this.tableOp('deltable')), this.ctxBtn(IC.tableic, this.t('tablepropsttl'), () => this.tablePropsDlg()));
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
- /* plain-text pastes get two additive pre-checks before the normal text
5325
- pipeline runs (the paste engine itself is untouched): a clipboard that
5326
- is entirely one LaTeX formula/derivation becomes a rendered math
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.clearMathSel();
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: [{