fable-editor 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -4
- package/dist/angular/LICENSE +1 -1
- package/dist/angular/README.md +44 -5
- package/dist/angular/core/config.d.ts +10 -0
- package/dist/angular/core/editor.d.ts +96 -0
- package/dist/angular/core/i18n.d.ts +45 -0
- package/dist/angular/core/types.d.ts +10 -0
- package/dist/angular/esm2022/core/config.mjs +143 -2
- package/dist/angular/esm2022/core/editor.mjs +1102 -27
- package/dist/angular/esm2022/core/i18n.mjs +93 -3
- package/dist/angular/esm2022/core/icons.mjs +11 -2
- package/dist/angular/esm2022/core/types.mjs +1 -1
- package/dist/angular/fesm2022/fable-editor.mjs +1344 -29
- package/dist/angular/fesm2022/fable-editor.mjs.map +1 -1
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/editor.css +328 -3
- package/dist/core/editor.d.ts +96 -0
- package/dist/core/editor.d.ts.map +1 -1
- package/dist/core/i18n.d.ts +45 -0
- package/dist/core/i18n.d.ts.map +1 -1
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/core/index.cjs +262 -6
- package/dist/core/index.mjs +14547 -1451
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/react/FableEditor.d.ts.map +1 -1
- package/dist/react/index.cjs +271 -15
- package/dist/react/index.mjs +15064 -2004
- package/package.json +5 -1
- package/style.css +328 -3
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
3
4
|
|
|
4
5
|
const I18N = {
|
|
5
6
|
en: {
|
|
@@ -136,7 +137,52 @@ const I18N = {
|
|
|
136
137
|
insertimg: 'Insert',
|
|
137
138
|
dropimage: 'Drop image here',
|
|
138
139
|
uploading: 'Uploading…',
|
|
139
|
-
uploadfailed: 'Upload failed — click to retry'
|
|
140
|
+
uploadfailed: 'Upload failed — click to retry',
|
|
141
|
+
template: 'Templates',
|
|
142
|
+
tplimgleft: 'Image left, text right',
|
|
143
|
+
tplimgright: 'Image right, text left',
|
|
144
|
+
tplimgtop: 'Image top, text below',
|
|
145
|
+
tplimgcenter: 'Text top, image center',
|
|
146
|
+
deltemplate: 'Delete template',
|
|
147
|
+
video: 'Video',
|
|
148
|
+
quickvideo: 'Insert video',
|
|
149
|
+
deletevideo: 'Delete video',
|
|
150
|
+
uploadvideo: 'Upload from computer',
|
|
151
|
+
videolink: 'Insert from URL',
|
|
152
|
+
videourlph: 'Paste video URL…',
|
|
153
|
+
insertvideo: 'Insert',
|
|
154
|
+
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
|
+
videodlgttl: 'Insert/edit video',
|
|
162
|
+
vidgeneral: 'General',
|
|
163
|
+
vidembed: 'Embed',
|
|
164
|
+
vidadvanced: 'Advanced',
|
|
165
|
+
vidsource: 'Source',
|
|
166
|
+
vidembedhint: 'Paste your embed code below:',
|
|
167
|
+
vidaltsource: 'Alternative source URL',
|
|
168
|
+
vidposter: 'Media poster (Image URL)',
|
|
169
|
+
emoji: 'Emojis…',
|
|
170
|
+
chcat_currency: 'Currency',
|
|
171
|
+
chcat_text: 'Text & punctuation',
|
|
172
|
+
chcat_math: 'Mathematical',
|
|
173
|
+
chcat_arrows: 'Arrows',
|
|
174
|
+
chcat_symbols: 'Symbols',
|
|
175
|
+
chcat_latin: 'Extended Latin',
|
|
176
|
+
chcat_arabic: 'Arabic',
|
|
177
|
+
emcat_smileys: 'Smileys & emotion',
|
|
178
|
+
emcat_people: 'People & body',
|
|
179
|
+
emcat_animals: 'Animals & nature',
|
|
180
|
+
emcat_food: 'Food & drink',
|
|
181
|
+
emcat_travel: 'Travel & places',
|
|
182
|
+
emcat_activities: 'Activities',
|
|
183
|
+
emcat_objects: 'Objects',
|
|
184
|
+
emcat_symbols: 'Symbols',
|
|
185
|
+
emcat_flags: 'Flags'
|
|
140
186
|
},
|
|
141
187
|
ar: {
|
|
142
188
|
dir: 'rtl',
|
|
@@ -272,7 +318,52 @@ const I18N = {
|
|
|
272
318
|
insertimg: 'إدراج',
|
|
273
319
|
dropimage: 'أسقط الصورة هنا',
|
|
274
320
|
uploading: 'جارٍ الرفع…',
|
|
275
|
-
uploadfailed: 'فشل الرفع — انقر لإعادة المحاولة'
|
|
321
|
+
uploadfailed: 'فشل الرفع — انقر لإعادة المحاولة',
|
|
322
|
+
template: 'قوالب',
|
|
323
|
+
tplimgleft: 'صورة يسار، نص يمين',
|
|
324
|
+
tplimgright: 'صورة يمين، نص يسار',
|
|
325
|
+
tplimgtop: 'صورة أعلى، نص أسفل',
|
|
326
|
+
tplimgcenter: 'نص أعلى، صورة وسط',
|
|
327
|
+
deltemplate: 'حذف القالب',
|
|
328
|
+
video: 'فيديو',
|
|
329
|
+
quickvideo: 'إدراج فيديو',
|
|
330
|
+
deletevideo: 'حذف الفيديو',
|
|
331
|
+
uploadvideo: 'رفع من الجهاز',
|
|
332
|
+
videolink: 'إدراج من رابط',
|
|
333
|
+
videourlph: 'الصق رابط الفيديو…',
|
|
334
|
+
insertvideo: 'إدراج',
|
|
335
|
+
dropvideo: 'أسقط الفيديو هنا',
|
|
336
|
+
mathformula: 'إدراج معادلة رياضية',
|
|
337
|
+
mathdlgttl: 'إدراج معادلة رياضية',
|
|
338
|
+
mathderivation: 'اشتقاق متعدد الأسطر',
|
|
339
|
+
mathlatex: 'LaTeX',
|
|
340
|
+
editmath: 'تعديل المعادلة',
|
|
341
|
+
deletemath: 'حذف المعادلة',
|
|
342
|
+
videodlgttl: 'إدراج/تعديل فيديو',
|
|
343
|
+
vidgeneral: 'عام',
|
|
344
|
+
vidembed: 'تضمين',
|
|
345
|
+
vidadvanced: 'متقدم',
|
|
346
|
+
vidsource: 'المصدر',
|
|
347
|
+
vidembedhint: 'الصق كود التضمين أدناه:',
|
|
348
|
+
vidaltsource: 'رابط مصدر بديل',
|
|
349
|
+
vidposter: 'صورة الغلاف (رابط الصورة)',
|
|
350
|
+
emoji: 'رموز تعبيرية…',
|
|
351
|
+
chcat_currency: 'عملات',
|
|
352
|
+
chcat_text: 'نص وعلامات ترقيم',
|
|
353
|
+
chcat_math: 'رموز رياضية',
|
|
354
|
+
chcat_arrows: 'أسهم',
|
|
355
|
+
chcat_symbols: 'رموز',
|
|
356
|
+
chcat_latin: 'لاتينية موسعة',
|
|
357
|
+
chcat_arabic: 'عربية',
|
|
358
|
+
emcat_smileys: 'وجوه ومشاعر',
|
|
359
|
+
emcat_people: 'أشخاص وإيماءات',
|
|
360
|
+
emcat_animals: 'حيوانات وطبيعة',
|
|
361
|
+
emcat_food: 'طعام وشراب',
|
|
362
|
+
emcat_travel: 'سفر وأماكن',
|
|
363
|
+
emcat_activities: 'أنشطة',
|
|
364
|
+
emcat_objects: 'أشياء',
|
|
365
|
+
emcat_symbols: 'رموز',
|
|
366
|
+
emcat_flags: 'أعلام'
|
|
276
367
|
}
|
|
277
368
|
};
|
|
278
369
|
function getStrings(lang) {
|
|
@@ -284,7 +375,7 @@ function getStrings(lang) {
|
|
|
284
375
|
const DEFAULT_TOOLBAR = 'undo redo | preview print | importword revhistory | fontfamily fontsize | fontsizeincrease fontsizedecrease | ' +
|
|
285
376
|
'bold italic underline strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | ' +
|
|
286
377
|
'bullist numlist | outdent indent | link blockquote changecase lineheight wordspacing | removeformat | blocks | ' +
|
|
287
|
-
'ltr rtl | quickimage quicktable charmap | fullscreen sourcecode';
|
|
378
|
+
'ltr rtl | quickimage quickvideo quicktable mathformula template charmap emoji | fullscreen sourcecode';
|
|
288
379
|
/** Transcribed from the editor's built-in menu keys so that `menubar: true` (the
|
|
289
380
|
* default) renders identically to the hard-coded original. */
|
|
290
381
|
const DEFAULT_MENUBAR = 'file edit view insert format tools table help';
|
|
@@ -350,6 +441,147 @@ const COLORS = [
|
|
|
350
441
|
'#000000',
|
|
351
442
|
'#FFFFFF'
|
|
352
443
|
];
|
|
444
|
+
/** Special-character map, grouped for the two-pane picker (tabs left, glyphs right).
|
|
445
|
+
* `CHARS` below is the legacy flat list and is kept exported for back-compat. */
|
|
446
|
+
const CHAR_CATEGORIES = [
|
|
447
|
+
{
|
|
448
|
+
key: 'chcat_currency',
|
|
449
|
+
chars: ['€', '£', '¥', '¢', '$', '₹', '₽', '₩', '₺', '₴', '₦', '฿', '₫', '₪', '﷼', '¤']
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
key: 'chcat_text',
|
|
453
|
+
chars: [
|
|
454
|
+
'§', '¶', '†', '‡', '•', '…', '′', '″', '‰', '–', '—', '¡', '¿', '·',
|
|
455
|
+
'‹', '›', '«', '»', '“', '”', '‘', '’', '„', '‚', '¦', 'ª', 'º'
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
key: 'chcat_math',
|
|
460
|
+
chars: [
|
|
461
|
+
'°', '±', '×', '÷', '≈', '≠', '≤', '≥', '∞', 'µ', '√', '∑', '∫', '∂', '∆', '∏',
|
|
462
|
+
'π', 'Ω', 'α', 'β', 'γ', 'θ', 'λ', 'σ', 'φ', 'ω', '∈', '∉', '∪', '∩', '⊂', '⊃',
|
|
463
|
+
'∀', '∃', '∅', '∝', '∴', '¬', '½', '⅓', '¼', '¾', '⅔', '⅛', '⅜', '⅝', '⅞', '¹', '²', '³'
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
key: 'chcat_arrows',
|
|
468
|
+
chars: ['←', '→', '↑', '↓', '↔', '↕', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↗', '↘', '↙', '↩', '↪', '⤴', '⤵']
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
key: 'chcat_symbols',
|
|
472
|
+
chars: [
|
|
473
|
+
'©', '®', '™', '℠', '№', '℮', '✓', '✗', '★', '☆', '✦', '♠', '♣', '♥', '♦',
|
|
474
|
+
'♪', '♫', '☀', '☁', '☂', '☾', '☹', '☺', '✉', '✂', '✎', '☞', '☜'
|
|
475
|
+
]
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
key: 'chcat_latin',
|
|
479
|
+
chars: [
|
|
480
|
+
'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï',
|
|
481
|
+
'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'ß',
|
|
482
|
+
'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï',
|
|
483
|
+
'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'ÿ'
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
key: 'chcat_arabic',
|
|
488
|
+
chars: ['؟', '،', '؛', '٪', '٭', 'ـ', '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩', '﴾', '﴿', '۞', '؎']
|
|
489
|
+
}
|
|
490
|
+
];
|
|
491
|
+
/** Emoji map for the emoji picker — same two-pane dialog as the character map. */
|
|
492
|
+
const EMOJI_CATEGORIES = [
|
|
493
|
+
{
|
|
494
|
+
key: 'emcat_smileys',
|
|
495
|
+
chars: [
|
|
496
|
+
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰',
|
|
497
|
+
'😘', '😋', '😛', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥳', '😏', '😒', '😞', '😔', '😟', '😕',
|
|
498
|
+
'🙁', '😣', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤯', '😳', '🥵', '🥶', '😱', '😨',
|
|
499
|
+
'😥', '🤗', '🤔', '🤭', '🤫', '😐', '😑', '😬', '🙄', '😯', '😴', '😪', '😷', '🤒', '🤕', '🤢',
|
|
500
|
+
'🤧', '😈', '💀', '👻', '👽', '🤖', '💩', '😺', '😸', '😹', '😻', '🙀'
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
key: 'emcat_people',
|
|
505
|
+
chars: [
|
|
506
|
+
'👋', '🤚', '✋', '🖖', '👌', '🤏', '✌', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '👇', '☝',
|
|
507
|
+
'👍', '👎', '✊', '👊', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '💪', '✍', '💅', '🤳', '👂', '👃',
|
|
508
|
+
'👀', '👁', '🧠', '👶', '🧒', '👦', '👧', '🧑', '👨', '👩', '🧓', '👴', '👵', '👮', '👷', '💂',
|
|
509
|
+
'🕵', '👸', '🤴', '👰', '🤵', '🤰', '👼', '🎅', '🦸', '🦹', '🧙', '🧚', '🧛', '🧟', '💃', '🕺',
|
|
510
|
+
'🚶', '🏃', '🧘'
|
|
511
|
+
]
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
key: 'emcat_animals',
|
|
515
|
+
chars: [
|
|
516
|
+
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
|
|
517
|
+
'🐧', '🐦', '🐤', '🦆', '🦅', '🦉', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐢',
|
|
518
|
+
'🐍', '🦎', '🦖', '🐙', '🦑', '🦀', '🐠', '🐟', '🐬', '🐳', '🦈', '🐊', '🐅', '🦓', '🦍', '🐘',
|
|
519
|
+
'🦏', '🐪', '🦒', '🦘', '🐄', '🐎', '🐖', '🐑', '🐐', '🦌', '🐕', '🐈', '🐓', '🦃', '🦚', '🦜',
|
|
520
|
+
'🦢', '🕊', '🐇', '🦔', '🌵', '🌲', '🌴', '🌱', '🌿', '☘', '🍀', '🍁', '🌸', '🌺', '🌻', '🌹',
|
|
521
|
+
'🌷', '💐'
|
|
522
|
+
]
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
key: 'emcat_food',
|
|
526
|
+
chars: [
|
|
527
|
+
'🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝',
|
|
528
|
+
'🍅', '🍆', '🥑', '🥦', '🥒', '🌶', '🌽', '🥕', '🥔', '🥐', '🍞', '🥖', '🥨', '🧀', '🥚', '🍳',
|
|
529
|
+
'🥞', '🥓', '🥩', '🍗', '🍖', '🌭', '🍔', '🍟', '🍕', '🥪', '🌮', '🌯', '🥗', '🍝', '🍜', '🍲',
|
|
530
|
+
'🍛', '🍣', '🍱', '🥟', '🍤', '🍚', '🍧', '🍨', '🍦', '🥧', '🧁', '🍰', '🎂', '🍮', '🍭', '🍬',
|
|
531
|
+
'🍫', '🍿', '🍩', '🍪', '🥜', '🍯', '🥛', '☕', '🍵', '🥤', '🍺', '🥂', '🍷', '🍸', '🍹'
|
|
532
|
+
]
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
key: 'emcat_travel',
|
|
536
|
+
chars: [
|
|
537
|
+
'🚗', '🚕', '🚙', '🚌', '🏎', '🚓', '🚑', '🚒', '🚚', '🚜', '🛵', '🏍', '🚲', '🛴', '🚔', '🚃',
|
|
538
|
+
'🚄', '🚅', '🚂', '🚆', '🚇', '🚉', '✈', '🛫', '🛬', '💺', '🚁', '🚀', '🛸', '🚢', '⛵', '🚤',
|
|
539
|
+
'⚓', '⛽', '🚧', '🗺', '🗿', '🗽', '🗼', '🏰', '🏯', '🎡', '🎢', '🎠', '⛲', '🏖', '🏝', '🌋',
|
|
540
|
+
'⛰', '🏔', '🗻', '🏕', '⛺', '🏠', '🏡', '🏢', '🏬', '🏥', '🏦', '🏨', '🏪', '🏫', '💒', '⛪',
|
|
541
|
+
'🕌', '🕍', '🕋', '⛩', '🌅', '🌄', '🌇', '🌆', '🌃', '🌉', '🌌', '🌠'
|
|
542
|
+
]
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
key: 'emcat_activities',
|
|
546
|
+
chars: [
|
|
547
|
+
'⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🏒', '🏑', '🏏', '⛳', '🏹', '🎣',
|
|
548
|
+
'🥊', '🥋', '⛸', '🎿', '⛷', '🏂', '🏋', '🤸', '⛹', '🤺', '🏇', '🧗', '🏄', '🏊', '🚣', '🚵',
|
|
549
|
+
'🚴', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖', '🎫', '🎪', '🤹', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼',
|
|
550
|
+
'🎹', '🥁', '🎷', '🎺', '🎸', '🎻', '🎲', '🎯', '🎳', '🎮', '🎰', '🧩'
|
|
551
|
+
]
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
key: 'emcat_objects',
|
|
555
|
+
chars: [
|
|
556
|
+
'⌚', '📱', '💻', '⌨', '🖥', '🖨', '🖱', '🕹', '💽', '💾', '💿', '📀', '📷', '📸', '📹', '🎥',
|
|
557
|
+
'📞', '☎', '📺', '📻', '🎙', '⏱', '⏰', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯', '💸',
|
|
558
|
+
'💵', '💰', '💳', '💎', '⚖', '🔧', '🔨', '⚒', '🛠', '⛏', '🔩', '⚙', '🧲', '🔫', '💣', '🔪',
|
|
559
|
+
'🛡', '🔮', '🧿', '🔭', '🔬', '💊', '💉', '🌡', '🧬', '🧪', '🧹', '🧺', '🛁', '🔑', '🗝', '🚪',
|
|
560
|
+
'🛋', '🛏', '🧸', '🖼', '🛍', '🛒', '🎁', '🎈', '🎀', '🎊', '🎉', '✉', '📩', '📦', '📜', '📄',
|
|
561
|
+
'📊', '📈', '📉', '📅', '📋', '📁', '📰', '📓', '📚', '📖', '🔖', '🔗', '📎', '📐', '📏', '📌',
|
|
562
|
+
'📍', '✂', '🖊', '✒', '🖌', '🖍', '📝', '✏', '🔍', '🔒', '🔓'
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
key: 'emcat_symbols',
|
|
567
|
+
chars: [
|
|
568
|
+
'❤', '🧡', '💛', '💚', '💙', '💜', '🖤', '💔', '❣', '💕', '💞', '💓', '💗', '💖', '💘', '💝',
|
|
569
|
+
'☮', '✝', '☪', '🕉', '☸', '✡', '☯', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐',
|
|
570
|
+
'♑', '♒', '♓', '⚛', '❌', '⭕', '🛑', '⛔', '🚫', '💯', '💢', '⚠', '🚸', '🔱', '⚜', '🔰',
|
|
571
|
+
'♻', '✅', '❇', '✳', '❎', '🌐', '💠', '🌀', '💤', '🏧', '♿', '🚭', '❗', '❕', '❓', '❔',
|
|
572
|
+
'‼', '⁉', '💲', '🔅', '🔆', '〽', 'ℹ', '🆗', '🆕', '🆓', '🆒', '🆙', '🈯', '🈚'
|
|
573
|
+
]
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
key: 'emcat_flags',
|
|
577
|
+
chars: [
|
|
578
|
+
'🏁', '🚩', '🎌', '🏴', '🏳', '🇦🇪', '🇸🇦', '🇶🇦', '🇰🇼', '🇧🇭', '🇴🇲', '🇪🇬', '🇯🇴', '🇱🇧', '🇮🇳', '🇵🇰',
|
|
579
|
+
'🇺🇸', '🇬🇧', '🇨🇦', '🇦🇺', '🇫🇷', '🇩🇪', '🇮🇹', '🇪🇸', '🇵🇹', '🇳🇱', '🇧🇪', '🇨🇭', '🇸🇪', '🇳🇴', '🇩🇰', '🇫🇮',
|
|
580
|
+
'🇮🇪', '🇬🇷', '🇹🇷', '🇷🇺', '🇺🇦', '🇵🇱', '🇯🇵', '🇰🇷', '🇨🇳', '🇸🇬', '🇲🇾', '🇮🇩', '🇹🇭', '🇵🇭', '🇻🇳', '🇧🇷',
|
|
581
|
+
'🇦🇷', '🇲🇽', '🇿🇦', '🇳🇬', '🇰🇪', '🇲🇦'
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
];
|
|
353
585
|
const CHARS = [
|
|
354
586
|
'©',
|
|
355
587
|
'®',
|
|
@@ -475,6 +707,11 @@ const IC = {
|
|
|
475
707
|
wordic: S('<path d="M6 3h8l5 5v13H6z"/><path d="M14 3v5h5"/><text x="8" y="17" font-size="8.5" stroke="none" fill="#222f3e" font-family="Arial" font-weight="bold">W</text>'),
|
|
476
708
|
draftic: G('M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z'),
|
|
477
709
|
trash: S('<path d="M5 7h14M10 7V5h4v2M7.5 7l1 13h7l1-13"/>'),
|
|
710
|
+
templateic: S('<rect x="4" y="4" width="16" height="16" rx="1.5"/><rect x="6.5" y="6.5" width="5" height="5" rx=".5"/><path d="M14 7.5h3.5M14 10h3.5M6.5 14h11M6.5 17h11"/>'),
|
|
711
|
+
tplleft: S('<rect x="3" y="5" width="8" height="8" rx=".8"/><path d="M13.5 6.5H21M13.5 9.5H21M13.5 12.5H21M3 15.5h18M3 18.5h18"/>'),
|
|
712
|
+
tplright: S('<rect x="13" y="5" width="8" height="8" rx=".8"/><path d="M3 6.5h7.5M3 9.5h7.5M3 12.5h7.5M3 15.5h18M3 18.5h18"/>'),
|
|
713
|
+
tpltop: S('<rect x="5" y="3" width="14" height="9" rx=".8"/><path d="M3 15h18M3 18h18M3 21h12"/>'),
|
|
714
|
+
tplcenter: S('<path d="M3 4h18M3 7h18M3 10h12"/><rect x="5" y="12.5" width="14" height="9" rx=".8"/>'),
|
|
478
715
|
rotateleft: S('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>'),
|
|
479
716
|
rotateright: S('<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>'),
|
|
480
717
|
fliphorizontal: S('<path d="M12 3v18"/><path d="M7 8l-4 4 4 4"/><path d="M17 8l4 4-4 4"/>'),
|
|
@@ -486,7 +723,11 @@ const IC = {
|
|
|
486
723
|
ol_alpha: LIST(['a', 'b', 'c']),
|
|
487
724
|
ol_roman: LIST(['i', 'ii', 'iii'], 6.5),
|
|
488
725
|
ol_alphau: LIST(['A', 'B', 'C']),
|
|
489
|
-
ol_romanu: LIST(['I', 'II', 'III'], 6.5)
|
|
726
|
+
ol_romanu: LIST(['I', 'II', 'III'], 6.5),
|
|
727
|
+
video: S('<rect x="3" y="6" width="13" height="12" rx="1.5"/><path d="M16 10l5-3v10l-5-3z"/>'),
|
|
728
|
+
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">∑</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"/>')
|
|
490
731
|
};
|
|
491
732
|
const TXT = (html) => `<span class="txt">${html}</span>`;
|
|
492
733
|
|
|
@@ -1364,6 +1605,19 @@ async function importDocxToHtml(input) {
|
|
|
1364
1605
|
}
|
|
1365
1606
|
|
|
1366
1607
|
const IMG_CORNERS = ['nw', 'ne', 'sw', 'se'];
|
|
1608
|
+
const TPL_LAYOUTS = ['img-left', 'img-right', 'img-top', 'img-center'];
|
|
1609
|
+
const TPL_LABEL = {
|
|
1610
|
+
'img-left': 'tplimgleft',
|
|
1611
|
+
'img-right': 'tplimgright',
|
|
1612
|
+
'img-top': 'tplimgtop',
|
|
1613
|
+
'img-center': 'tplimgcenter'
|
|
1614
|
+
};
|
|
1615
|
+
const TPL_ICON = {
|
|
1616
|
+
'img-left': IC.tplleft,
|
|
1617
|
+
'img-right': IC.tplright,
|
|
1618
|
+
'img-top': IC.tpltop,
|
|
1619
|
+
'img-center': IC.tplcenter
|
|
1620
|
+
};
|
|
1367
1621
|
class FableEditor {
|
|
1368
1622
|
static instanceCounter = 0;
|
|
1369
1623
|
options;
|
|
@@ -1402,7 +1656,22 @@ class FableEditor {
|
|
|
1402
1656
|
imageUploadHandler;
|
|
1403
1657
|
onImageUploadError;
|
|
1404
1658
|
contentStyle;
|
|
1659
|
+
vidInput;
|
|
1660
|
+
/* a <video> element or a .video-embed iframe wrapper — both share the
|
|
1661
|
+
same selection outline + align/delete context toolbar */
|
|
1662
|
+
vidActive = null;
|
|
1663
|
+
vidCtx = null;
|
|
1664
|
+
vidHideTimer = null;
|
|
1665
|
+
vphActive = null;
|
|
1666
|
+
vphCtx = null;
|
|
1667
|
+
vphUploadTarget = null;
|
|
1668
|
+
videoUploadHandler;
|
|
1669
|
+
onVideoUploadError;
|
|
1670
|
+
mathActive = null;
|
|
1671
|
+
mathCtx = null;
|
|
1405
1672
|
selCtx = null;
|
|
1673
|
+
tplActive = null;
|
|
1674
|
+
tplCtx = null;
|
|
1406
1675
|
docInput;
|
|
1407
1676
|
revisions = [];
|
|
1408
1677
|
revTimer = null;
|
|
@@ -1424,10 +1693,13 @@ class FableEditor {
|
|
|
1424
1693
|
readonly: options.readonly ?? false,
|
|
1425
1694
|
draftKey: options.draftKey ?? (typeof location !== 'undefined' ? location.pathname : 'default'),
|
|
1426
1695
|
fontFamilyFormats: options.fontFamilyFormats ?? FONTS,
|
|
1427
|
-
imageFileTypes: options.imageFileTypes ?? ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp']
|
|
1696
|
+
imageFileTypes: options.imageFileTypes ?? ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'],
|
|
1697
|
+
videoFileTypes: options.videoFileTypes ?? ['video/mp4', 'video/webm', 'video/ogg']
|
|
1428
1698
|
};
|
|
1429
1699
|
this.imageUploadHandler = options.imageUploadHandler;
|
|
1430
1700
|
this.onImageUploadError = options.onImageUploadError;
|
|
1701
|
+
this.videoUploadHandler = options.videoUploadHandler;
|
|
1702
|
+
this.onVideoUploadError = options.onVideoUploadError;
|
|
1431
1703
|
this.contentStyle = options.contentStyle;
|
|
1432
1704
|
this.lang = this.options.language;
|
|
1433
1705
|
this.draftStorageKey = 'tmclone-draft:' + this.options.draftKey;
|
|
@@ -1494,6 +1766,11 @@ class FableEditor {
|
|
|
1494
1766
|
this.imgInput.accept = this.options.imageFileTypes.join(',');
|
|
1495
1767
|
this.imgInput.style.display = 'none';
|
|
1496
1768
|
target.appendChild(this.imgInput);
|
|
1769
|
+
this.vidInput = document.createElement('input');
|
|
1770
|
+
this.vidInput.type = 'file';
|
|
1771
|
+
this.vidInput.accept = this.options.videoFileTypes.join(',');
|
|
1772
|
+
this.vidInput.style.display = 'none';
|
|
1773
|
+
target.appendChild(this.vidInput);
|
|
1497
1774
|
this.docInput = document.createElement('input');
|
|
1498
1775
|
this.docInput.type = 'file';
|
|
1499
1776
|
this.docInput.accept = '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
@@ -1561,24 +1838,46 @@ class FableEditor {
|
|
|
1561
1838
|
this.positionTableHandles();
|
|
1562
1839
|
this.positionImageHandles();
|
|
1563
1840
|
this.positionImgPhCtx();
|
|
1841
|
+
this.positionTplCtx();
|
|
1842
|
+
this.positionVphCtx();
|
|
1843
|
+
this.positionVidCtx();
|
|
1844
|
+
this.positionMathCtx();
|
|
1564
1845
|
});
|
|
1565
1846
|
this.on(this.ed, 'keydown', (e) => {
|
|
1566
1847
|
if (e.altKey && e.key === '0') {
|
|
1567
1848
|
e.preventDefault();
|
|
1568
1849
|
this.helpDlg();
|
|
1569
1850
|
}
|
|
1851
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && this.phActive) {
|
|
1852
|
+
e.preventDefault();
|
|
1853
|
+
this.removeImgPlaceholder();
|
|
1854
|
+
}
|
|
1855
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && this.vphActive) {
|
|
1856
|
+
e.preventDefault();
|
|
1857
|
+
this.removeVphPlaceholder();
|
|
1858
|
+
}
|
|
1859
|
+
if (e.key === '$' && !e.ctrlKey && !e.metaKey && !e.altKey)
|
|
1860
|
+
this.tryMathTyping(e);
|
|
1570
1861
|
});
|
|
1571
1862
|
this.on(this.ed, 'paste', (e) => this.handlePaste(e));
|
|
1572
1863
|
this.on(this.ed, 'drop', (e) => {
|
|
1573
1864
|
/* content drops stay blocked (powerpaste_block_drop) — only an
|
|
1574
|
-
image file dropped onto
|
|
1865
|
+
image/video file dropped onto its matching upload placeholder is consumed */
|
|
1575
1866
|
e.preventDefault();
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1867
|
+
const target = e.target;
|
|
1868
|
+
const ph = target.closest?.('.img-ph:not(.vid-ph)');
|
|
1869
|
+
if (ph && this.ed.contains(ph) && !this.options.readonly) {
|
|
1870
|
+
const file = Array.from(e.dataTransfer?.files || []).find((f) => f.type.startsWith('image/'));
|
|
1871
|
+
if (file)
|
|
1872
|
+
this.readImageFileInto(file, ph);
|
|
1578
1873
|
return;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1874
|
+
}
|
|
1875
|
+
const vph = target.closest?.('.vid-ph');
|
|
1876
|
+
if (vph && this.ed.contains(vph) && !this.options.readonly) {
|
|
1877
|
+
const file = Array.from(e.dataTransfer?.files || []).find((f) => f.type.startsWith('video/'));
|
|
1878
|
+
if (file)
|
|
1879
|
+
this.readVideoFileInto(file, vph);
|
|
1880
|
+
}
|
|
1582
1881
|
});
|
|
1583
1882
|
this.on(this.ed, 'dragover', (e) => e.preventDefault());
|
|
1584
1883
|
this.on(this.ed, 'mousedown', (e) => {
|
|
@@ -1588,14 +1887,29 @@ class FableEditor {
|
|
|
1588
1887
|
this.selectTableForResize(table);
|
|
1589
1888
|
else
|
|
1590
1889
|
this.clearTableHandles();
|
|
1591
|
-
const ph = target.closest?.('.img-ph');
|
|
1890
|
+
const ph = target.closest?.('.img-ph:not(.vid-ph)');
|
|
1592
1891
|
if (ph && this.ed.contains(ph) && !this.options.readonly) {
|
|
1593
1892
|
e.preventDefault();
|
|
1893
|
+
this.ed.focus();
|
|
1594
1894
|
this.selectImgPlaceholder(ph);
|
|
1595
1895
|
}
|
|
1596
1896
|
else {
|
|
1597
1897
|
this.clearImgPlaceholderSel();
|
|
1598
1898
|
}
|
|
1899
|
+
const vph = target.closest?.('.vid-ph');
|
|
1900
|
+
if (vph && this.ed.contains(vph) && !this.options.readonly) {
|
|
1901
|
+
e.preventDefault();
|
|
1902
|
+
this.ed.focus();
|
|
1903
|
+
this.selectVphPlaceholder(vph);
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
this.clearVphPlaceholderSel();
|
|
1907
|
+
}
|
|
1908
|
+
const tpl = target.closest?.('.tpl');
|
|
1909
|
+
if (tpl && this.ed.contains(tpl) && !this.options.readonly)
|
|
1910
|
+
this.selectTemplate(tpl);
|
|
1911
|
+
else
|
|
1912
|
+
this.clearTplSel();
|
|
1599
1913
|
const img = target.closest?.('img');
|
|
1600
1914
|
if (img && this.ed.contains(img) && !this.options.readonly) {
|
|
1601
1915
|
e.preventDefault();
|
|
@@ -1604,6 +1918,22 @@ class FableEditor {
|
|
|
1604
1918
|
else {
|
|
1605
1919
|
this.clearImageHandles();
|
|
1606
1920
|
}
|
|
1921
|
+
const vid = target.closest?.('video, .video-embed');
|
|
1922
|
+
if (vid && this.ed.contains(vid) && !this.options.readonly) {
|
|
1923
|
+
e.preventDefault();
|
|
1924
|
+
this.selectVideo(vid);
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
this.clearVidHandles();
|
|
1928
|
+
}
|
|
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
|
+
}
|
|
1607
1937
|
});
|
|
1608
1938
|
this.on(this.ed, 'mouseover', (e) => {
|
|
1609
1939
|
if (this.options.readonly)
|
|
@@ -1630,17 +1960,29 @@ class FableEditor {
|
|
|
1630
1960
|
this.positionTableHandles();
|
|
1631
1961
|
this.positionImageHandles();
|
|
1632
1962
|
this.positionImgPhCtx();
|
|
1963
|
+
this.positionTplCtx();
|
|
1964
|
+
this.positionVphCtx();
|
|
1965
|
+
this.positionVidCtx();
|
|
1966
|
+
this.positionMathCtx();
|
|
1633
1967
|
});
|
|
1634
1968
|
this.onWin('scroll', () => {
|
|
1635
1969
|
this.positionTableHandles();
|
|
1636
1970
|
this.positionImageHandles();
|
|
1637
1971
|
this.positionImgPhCtx();
|
|
1972
|
+
this.positionTplCtx();
|
|
1973
|
+
this.positionVphCtx();
|
|
1974
|
+
this.positionVidCtx();
|
|
1975
|
+
this.positionMathCtx();
|
|
1638
1976
|
this.repositionSelToolbar();
|
|
1639
1977
|
}, true);
|
|
1640
1978
|
this.onWin('resize', () => {
|
|
1641
1979
|
this.positionTableHandles();
|
|
1642
1980
|
this.positionImageHandles();
|
|
1643
1981
|
this.positionImgPhCtx();
|
|
1982
|
+
this.positionTplCtx();
|
|
1983
|
+
this.positionVphCtx();
|
|
1984
|
+
this.positionVidCtx();
|
|
1985
|
+
this.positionMathCtx();
|
|
1644
1986
|
this.repositionSelToolbar();
|
|
1645
1987
|
});
|
|
1646
1988
|
this.on(this.imgInput, 'change', () => {
|
|
@@ -1661,6 +2003,24 @@ class FableEditor {
|
|
|
1661
2003
|
};
|
|
1662
2004
|
fr.readAsDataURL(file);
|
|
1663
2005
|
});
|
|
2006
|
+
this.on(this.vidInput, 'change', () => {
|
|
2007
|
+
const file = this.vidInput.files?.[0];
|
|
2008
|
+
if (!file)
|
|
2009
|
+
return;
|
|
2010
|
+
const vph = this.vphUploadTarget;
|
|
2011
|
+
this.vphUploadTarget = null;
|
|
2012
|
+
if (vph && this.ed.contains(vph)) {
|
|
2013
|
+
this.readVideoFileInto(file, vph);
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
const fr = new FileReader();
|
|
2017
|
+
fr.onload = () => {
|
|
2018
|
+
this.restoreSel();
|
|
2019
|
+
document.execCommand('insertHTML', false, `<video controls src="${fr.result}" title="${file.name.replace(/"/g, '')}"></video>`);
|
|
2020
|
+
this.onChange();
|
|
2021
|
+
};
|
|
2022
|
+
fr.readAsDataURL(file);
|
|
2023
|
+
});
|
|
1664
2024
|
this.on(this.docInput, 'change', () => {
|
|
1665
2025
|
const file = this.docInput.files?.[0];
|
|
1666
2026
|
if (!file)
|
|
@@ -1675,13 +2035,21 @@ class FableEditor {
|
|
|
1675
2035
|
this.imgHandles.includes(e.target) ||
|
|
1676
2036
|
(this.imgCtx && this.imgCtx.contains(e.target)) ||
|
|
1677
2037
|
(this.phCtx && this.phCtx.contains(e.target)) ||
|
|
2038
|
+
(this.tplCtx && this.tplCtx.contains(e.target)) ||
|
|
1678
2039
|
(this.selCtx && this.selCtx.contains(e.target)) ||
|
|
2040
|
+
(this.vphCtx && this.vphCtx.contains(e.target)) ||
|
|
2041
|
+
(this.vidCtx && this.vidCtx.contains(e.target)) ||
|
|
2042
|
+
(this.mathCtx && this.mathCtx.contains(e.target)) ||
|
|
1679
2043
|
(this.openPop && this.openPop.contains(e.target)) ||
|
|
1680
2044
|
(this.openSubEl && this.openSubEl.contains(e.target)))
|
|
1681
2045
|
return;
|
|
1682
2046
|
this.clearTableHandles();
|
|
1683
2047
|
this.clearImageHandles();
|
|
1684
2048
|
this.clearImgPlaceholderSel();
|
|
2049
|
+
this.clearTplSel();
|
|
2050
|
+
this.clearVphPlaceholderSel();
|
|
2051
|
+
this.clearVidHandles();
|
|
2052
|
+
this.clearMathSel();
|
|
1685
2053
|
this.clearSelToolbar();
|
|
1686
2054
|
});
|
|
1687
2055
|
}
|
|
@@ -1707,6 +2075,10 @@ class FableEditor {
|
|
|
1707
2075
|
this.clearTableHandles();
|
|
1708
2076
|
this.clearImageHandles();
|
|
1709
2077
|
this.clearImgPlaceholderSel();
|
|
2078
|
+
this.clearTplSel();
|
|
2079
|
+
this.clearVphPlaceholderSel();
|
|
2080
|
+
this.clearVidHandles();
|
|
2081
|
+
this.clearMathSel();
|
|
1710
2082
|
this.clearSelToolbar();
|
|
1711
2083
|
this.onChange();
|
|
1712
2084
|
}
|
|
@@ -1768,6 +2140,10 @@ class FableEditor {
|
|
|
1768
2140
|
this.clearTableHandles();
|
|
1769
2141
|
this.clearImageHandles();
|
|
1770
2142
|
this.clearImgPlaceholderSel();
|
|
2143
|
+
this.clearTplSel();
|
|
2144
|
+
this.clearVphPlaceholderSel();
|
|
2145
|
+
this.clearVidHandles();
|
|
2146
|
+
this.clearMathSel();
|
|
1771
2147
|
this.clearSelToolbar();
|
|
1772
2148
|
this.listeners.forEach((fn) => fn());
|
|
1773
2149
|
this.listeners = [];
|
|
@@ -1775,6 +2151,8 @@ class FableEditor {
|
|
|
1775
2151
|
this.shell.parentNode.removeChild(this.shell);
|
|
1776
2152
|
if (this.imgInput && this.imgInput.parentNode)
|
|
1777
2153
|
this.imgInput.parentNode.removeChild(this.imgInput);
|
|
2154
|
+
if (this.vidInput && this.vidInput.parentNode)
|
|
2155
|
+
this.vidInput.parentNode.removeChild(this.vidInput);
|
|
1778
2156
|
if (this.docInput && this.docInput.parentNode)
|
|
1779
2157
|
this.docInput.parentNode.removeChild(this.docInput);
|
|
1780
2158
|
this.contentStyleEl = null;
|
|
@@ -2576,7 +2954,7 @@ class FableEditor {
|
|
|
2576
2954
|
* (the quick color chips); everything else yields one. */
|
|
2577
2955
|
buildToolbarRegistry() {
|
|
2578
2956
|
const tableBtn = this.tbtn(IC.tableic, this.t('quicktable'), () => this.tableGrid(tableBtn));
|
|
2579
|
-
|
|
2957
|
+
const registry = {
|
|
2580
2958
|
undo: () => this.tbtn(IC.undo, this.t('undo'), () => this.exec('undo')),
|
|
2581
2959
|
redo: () => this.tbtn(IC.redo, this.t('redo'), () => this.exec('redo')),
|
|
2582
2960
|
preview: () => this.tbtn(IC.prevw, this.t('preview'), () => this.previewDlg()),
|
|
@@ -2611,11 +2989,22 @@ class FableEditor {
|
|
|
2611
2989
|
ltr: () => this.tbtn(IC.ltr, this.t('ltr'), () => this.setDir('ltr'), 'ltr'),
|
|
2612
2990
|
rtl: () => this.tbtn(IC.rtl, this.t('rtl'), () => this.setDir('rtl'), 'rtl'),
|
|
2613
2991
|
quickimage: () => this.tbtn(IC.image, this.t('quickimage'), () => this.insertImagePlaceholder()),
|
|
2992
|
+
quickvideo: () => this.tbtn(IC.video, this.t('quickvideo'), () => this.videoDlg()),
|
|
2993
|
+
mathformula: () => this.tbtn(IC.mathic, this.t('mathformula'), () => this.mathDlg()),
|
|
2614
2994
|
quicktable: () => tableBtn,
|
|
2995
|
+
template: () => this.menuTbtn(IC.templateic, this.t('template'), (b) => this.templateMenu(b)),
|
|
2615
2996
|
charmap: () => this.tbtn(IC.charic, this.t('charmap'), () => this.charMap()),
|
|
2997
|
+
emoji: () => this.tbtn(IC.emojiic, this.t('emoji'), () => this.emojiMap()),
|
|
2616
2998
|
fullscreen: () => this.tbtn(IC.fullscreen, this.t('fullscreen'), () => this.toggleFullscreen(), 'fullscreen'),
|
|
2617
2999
|
sourcecode: () => this.tbtn(IC.srcic, this.t('sourcecode'), () => this.sourceDlg())
|
|
2618
3000
|
};
|
|
3001
|
+
/* TinyMCE-compatible aliases so common TinyMCE toolbar strings
|
|
3002
|
+
(e.g. "undo redo | styles | ... | link image media") work as-is */
|
|
3003
|
+
registry.styles = registry.blocks;
|
|
3004
|
+
registry.image = registry.quickimage;
|
|
3005
|
+
registry.media = registry.quickvideo;
|
|
3006
|
+
registry.table = registry.quicktable;
|
|
3007
|
+
return registry;
|
|
2619
3008
|
}
|
|
2620
3009
|
buildToolbar() {
|
|
2621
3010
|
this.toolbar.innerHTML = '';
|
|
@@ -2682,10 +3071,14 @@ class FableEditor {
|
|
|
2682
3071
|
insert: [
|
|
2683
3072
|
this.mItem('link', IC.link, () => this.linkDlg()),
|
|
2684
3073
|
this.mItem('image', IC.image, () => this.insertImagePlaceholder()),
|
|
3074
|
+
this.mItem('video', IC.video, () => this.videoDlg()),
|
|
3075
|
+
this.mItem('mathformula', IC.mathic, () => this.mathDlg()),
|
|
2685
3076
|
{ label: this.t('inserttable'), icon: IC.tableic, action: () => this.tableGrid(this.menubar.querySelector('[data-menu-key="table"]') || this.menubar) },
|
|
3077
|
+
{ label: this.t('template'), icon: IC.templateic, subBuild: (el) => this.buildTemplatePickInto(el) },
|
|
2686
3078
|
this.mItem('hr', IC.hric, () => this.exec('insertHorizontalRule')),
|
|
2687
3079
|
'|',
|
|
2688
3080
|
this.mItem('charmap', IC.charic, () => this.charMap()),
|
|
3081
|
+
this.mItem('emoji', IC.emojiic, () => this.emojiMap()),
|
|
2689
3082
|
this.mItem('datetime', IC.dateic, () => this.exec('insertText', new Date().toLocaleString(this.lang === 'ar' ? 'ar-AE' : 'en-GB'))),
|
|
2690
3083
|
'|',
|
|
2691
3084
|
{
|
|
@@ -2755,7 +3148,7 @@ class FableEditor {
|
|
|
2755
3148
|
this.tablePropsDlg();
|
|
2756
3149
|
}
|
|
2757
3150
|
},
|
|
2758
|
-
{ label: this.t('deltable'), icon: IC.
|
|
3151
|
+
{ label: this.t('deltable'), icon: IC.trash, action: () => this.tableOp('deltable') }
|
|
2759
3152
|
],
|
|
2760
3153
|
help: [this.mItem('helpttl', IC.helpic, () => this.helpDlg(), 'Alt+0')]
|
|
2761
3154
|
};
|
|
@@ -3256,22 +3649,53 @@ class FableEditor {
|
|
|
3256
3649
|
body.appendChild(box);
|
|
3257
3650
|
});
|
|
3258
3651
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3652
|
+
/** Two-pane picker shared by the special-character map and the emoji map:
|
|
3653
|
+
* fixed category tabs on the left, the active category's glyphs on the right. */
|
|
3654
|
+
glyphPickerDlg(title, cats, emoji) {
|
|
3655
|
+
this.saveSel();
|
|
3656
|
+
this.dialog(title, (body) => {
|
|
3657
|
+
const wrap = document.createElement('div');
|
|
3658
|
+
wrap.className = 'chmap';
|
|
3659
|
+
const tabs = document.createElement('div');
|
|
3660
|
+
tabs.className = 'chtabs';
|
|
3661
|
+
const panel = document.createElement('div');
|
|
3662
|
+
panel.className = 'chpanel';
|
|
3663
|
+
const grid = document.createElement('div');
|
|
3664
|
+
grid.className = 'chgrid' + (emoji ? ' emgrid' : '');
|
|
3665
|
+
panel.appendChild(grid);
|
|
3666
|
+
const show = (i) => {
|
|
3667
|
+
tabs.querySelectorAll('button').forEach((b, bi) => b.classList.toggle('on', bi === i));
|
|
3668
|
+
grid.innerHTML = '';
|
|
3669
|
+
cats[i].chars.forEach((ch) => {
|
|
3670
|
+
const b = document.createElement('button');
|
|
3671
|
+
b.type = 'button';
|
|
3672
|
+
b.textContent = ch;
|
|
3673
|
+
b.onclick = () => {
|
|
3674
|
+
this.closeDlg();
|
|
3675
|
+
this.exec('insertText', ch);
|
|
3676
|
+
};
|
|
3677
|
+
grid.appendChild(b);
|
|
3678
|
+
});
|
|
3679
|
+
panel.scrollTop = 0;
|
|
3680
|
+
};
|
|
3681
|
+
cats.forEach((c, i) => {
|
|
3264
3682
|
const b = document.createElement('button');
|
|
3265
|
-
b.
|
|
3266
|
-
b.
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
};
|
|
3270
|
-
g.appendChild(b);
|
|
3683
|
+
b.type = 'button';
|
|
3684
|
+
b.textContent = this.t(c.key);
|
|
3685
|
+
b.addEventListener('click', () => show(i));
|
|
3686
|
+
tabs.appendChild(b);
|
|
3271
3687
|
});
|
|
3272
|
-
|
|
3688
|
+
wrap.append(tabs, panel);
|
|
3689
|
+
body.appendChild(wrap);
|
|
3690
|
+
show(0);
|
|
3273
3691
|
});
|
|
3274
3692
|
}
|
|
3693
|
+
charMap() {
|
|
3694
|
+
this.glyphPickerDlg(this.t('charmap').replace('…', ''), CHAR_CATEGORIES, false);
|
|
3695
|
+
}
|
|
3696
|
+
emojiMap() {
|
|
3697
|
+
this.glyphPickerDlg(this.t('emoji').replace('…', ''), EMOJI_CATEGORIES, true);
|
|
3698
|
+
}
|
|
3275
3699
|
printDoc() {
|
|
3276
3700
|
const w = window.open('', '_blank');
|
|
3277
3701
|
if (!w)
|
|
@@ -3286,10 +3710,13 @@ class FableEditor {
|
|
|
3286
3710
|
this.imgInput.click();
|
|
3287
3711
|
}
|
|
3288
3712
|
/* ---------------------------------------------- image upload placeholder */
|
|
3713
|
+
imgPhHTML(id) {
|
|
3714
|
+
return `<div class="img-ph"${id ? ` id="${id}"` : ''} contenteditable="false">${IC.image}<span>${this.t('dropimage')}</span></div>`;
|
|
3715
|
+
}
|
|
3289
3716
|
insertImagePlaceholder() {
|
|
3290
3717
|
this.restoreSel();
|
|
3291
3718
|
const id = 'img-ph-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
3292
|
-
document.execCommand('insertHTML', false,
|
|
3719
|
+
document.execCommand('insertHTML', false, this.imgPhHTML(id));
|
|
3293
3720
|
const ph = this.ed.querySelector('#' + id);
|
|
3294
3721
|
if (ph) {
|
|
3295
3722
|
ph.removeAttribute('id');
|
|
@@ -3303,6 +3730,17 @@ class FableEditor {
|
|
|
3303
3730
|
this.phActive?.classList.remove('active');
|
|
3304
3731
|
this.phActive = null;
|
|
3305
3732
|
}
|
|
3733
|
+
removeImgPlaceholder() {
|
|
3734
|
+
const ph = this.phActive;
|
|
3735
|
+
if (!ph)
|
|
3736
|
+
return;
|
|
3737
|
+
this.clearImgPlaceholderSel();
|
|
3738
|
+
/* the placeholder inside a template block IS the media slot — deleting
|
|
3739
|
+
it drops the slot entirely, leaving a text-only block */
|
|
3740
|
+
(ph.closest('.tpl-media') || ph).remove();
|
|
3741
|
+
this.refreshState();
|
|
3742
|
+
this.onChange();
|
|
3743
|
+
}
|
|
3306
3744
|
selectImgPlaceholder(ph) {
|
|
3307
3745
|
if (this.phActive === ph)
|
|
3308
3746
|
return;
|
|
@@ -3324,8 +3762,33 @@ class FableEditor {
|
|
|
3324
3762
|
this.phUploadTarget = this.phActive;
|
|
3325
3763
|
this.pickImage();
|
|
3326
3764
|
}), this.ctxBtn(IC.link, this.t('imagelink'), () => this.renderPhCtxUrlInput()));
|
|
3765
|
+
/* a template's media slot is image-first, but can host a video instead */
|
|
3766
|
+
if (this.phActive?.closest('.tpl-media')) {
|
|
3767
|
+
el.append(this.ctxBtn(IC.video, this.t('quickvideo'), () => this.swapPlaceholderKind('video')));
|
|
3768
|
+
}
|
|
3769
|
+
el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deleteimg'), () => this.removeImgPlaceholder()));
|
|
3327
3770
|
this.positionImgPhCtx();
|
|
3328
3771
|
}
|
|
3772
|
+
/** Swaps a template slot's placeholder between image and video kinds, so the
|
|
3773
|
+
* same media slot can be filled with either. */
|
|
3774
|
+
swapPlaceholderKind(to) {
|
|
3775
|
+
const ph = to === 'video' ? this.phActive : this.vphActive;
|
|
3776
|
+
if (!ph)
|
|
3777
|
+
return;
|
|
3778
|
+
const tmp = document.createElement('div');
|
|
3779
|
+
tmp.innerHTML = to === 'video' ? this.vidPhHTML() : this.imgPhHTML();
|
|
3780
|
+
const next = tmp.firstElementChild;
|
|
3781
|
+
if (to === 'video')
|
|
3782
|
+
this.clearImgPlaceholderSel();
|
|
3783
|
+
else
|
|
3784
|
+
this.clearVphPlaceholderSel();
|
|
3785
|
+
ph.replaceWith(next);
|
|
3786
|
+
if (to === 'video')
|
|
3787
|
+
this.selectVphPlaceholder(next);
|
|
3788
|
+
else
|
|
3789
|
+
this.selectImgPlaceholder(next);
|
|
3790
|
+
this.onChange();
|
|
3791
|
+
}
|
|
3329
3792
|
renderPhCtxUrlInput() {
|
|
3330
3793
|
const el = this.phCtx;
|
|
3331
3794
|
if (!el)
|
|
@@ -3448,6 +3911,829 @@ class FableEditor {
|
|
|
3448
3911
|
this.phCtx.style.left = cx + 'px';
|
|
3449
3912
|
this.phCtx.style.top = cy + 'px';
|
|
3450
3913
|
}
|
|
3914
|
+
/* ---------------------------------------------------------- video upload placeholder */
|
|
3915
|
+
pickVideo() {
|
|
3916
|
+
this.vidInput.value = '';
|
|
3917
|
+
this.vidInput.click();
|
|
3918
|
+
}
|
|
3919
|
+
vidPhHTML(id) {
|
|
3920
|
+
return `<div class="img-ph vid-ph"${id ? ` id="${id}"` : ''} contenteditable="false">${IC.video}<span>${this.t('dropvideo')}</span></div>`;
|
|
3921
|
+
}
|
|
3922
|
+
insertVideoPlaceholder() {
|
|
3923
|
+
this.restoreSel();
|
|
3924
|
+
const id = 'vid-ph-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
3925
|
+
document.execCommand('insertHTML', false, this.vidPhHTML(id));
|
|
3926
|
+
const ph = this.ed.querySelector('#' + id);
|
|
3927
|
+
if (ph) {
|
|
3928
|
+
ph.removeAttribute('id');
|
|
3929
|
+
this.selectVphPlaceholder(ph);
|
|
3930
|
+
}
|
|
3931
|
+
this.onChange();
|
|
3932
|
+
}
|
|
3933
|
+
clearVphPlaceholderSel() {
|
|
3934
|
+
this.vphCtx?.remove();
|
|
3935
|
+
this.vphCtx = null;
|
|
3936
|
+
this.vphActive?.classList.remove('active');
|
|
3937
|
+
this.vphActive = null;
|
|
3938
|
+
}
|
|
3939
|
+
removeVphPlaceholder() {
|
|
3940
|
+
const ph = this.vphActive;
|
|
3941
|
+
if (!ph)
|
|
3942
|
+
return;
|
|
3943
|
+
this.clearVphPlaceholderSel();
|
|
3944
|
+
/* like the image variant: a placeholder inside a template block IS the
|
|
3945
|
+
media slot — deleting it drops the slot, leaving a text-only block */
|
|
3946
|
+
(ph.closest('.tpl-media') || ph).remove();
|
|
3947
|
+
this.refreshState();
|
|
3948
|
+
this.onChange();
|
|
3949
|
+
}
|
|
3950
|
+
selectVphPlaceholder(ph) {
|
|
3951
|
+
if (this.vphActive === ph)
|
|
3952
|
+
return;
|
|
3953
|
+
this.clearVphPlaceholderSel();
|
|
3954
|
+
this.vphActive = ph;
|
|
3955
|
+
ph.classList.add('active');
|
|
3956
|
+
this.vphCtx = document.createElement('div');
|
|
3957
|
+
this.vphCtx.className = 'imgctx img-ph-ctx';
|
|
3958
|
+
this.vphCtx.dir = this.dir();
|
|
3959
|
+
document.body.appendChild(this.vphCtx);
|
|
3960
|
+
this.renderVphCtxButtons();
|
|
3961
|
+
}
|
|
3962
|
+
renderVphCtxButtons() {
|
|
3963
|
+
const el = this.vphCtx;
|
|
3964
|
+
if (!el)
|
|
3965
|
+
return;
|
|
3966
|
+
el.innerHTML = '';
|
|
3967
|
+
el.append(this.ctxBtn(IC.uploadic, this.t('uploadvideo'), () => {
|
|
3968
|
+
this.vphUploadTarget = this.vphActive;
|
|
3969
|
+
this.pickVideo();
|
|
3970
|
+
}), this.ctxBtn(IC.link, this.t('videolink'), () => this.renderVphCtxUrlInput()));
|
|
3971
|
+
/* inside a template slot, offer switching the slot back to an image */
|
|
3972
|
+
if (this.vphActive?.closest('.tpl-media')) {
|
|
3973
|
+
el.append(this.ctxBtn(IC.image, this.t('quickimage'), () => this.swapPlaceholderKind('image')));
|
|
3974
|
+
}
|
|
3975
|
+
el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deletevideo'), () => this.removeVphPlaceholder()));
|
|
3976
|
+
this.positionVphCtx();
|
|
3977
|
+
}
|
|
3978
|
+
renderVphCtxUrlInput() {
|
|
3979
|
+
const el = this.vphCtx;
|
|
3980
|
+
if (!el)
|
|
3981
|
+
return;
|
|
3982
|
+
el.innerHTML = '';
|
|
3983
|
+
const inp = document.createElement('input');
|
|
3984
|
+
inp.type = 'url';
|
|
3985
|
+
inp.className = 'urlinp';
|
|
3986
|
+
inp.placeholder = this.t('videourlph');
|
|
3987
|
+
const ok = document.createElement('button');
|
|
3988
|
+
ok.type = 'button';
|
|
3989
|
+
ok.className = 'txtbtn';
|
|
3990
|
+
ok.textContent = this.t('insertvideo');
|
|
3991
|
+
const submit = () => {
|
|
3992
|
+
const url = inp.value.trim();
|
|
3993
|
+
const ph = this.vphActive;
|
|
3994
|
+
if (!ph)
|
|
3995
|
+
return;
|
|
3996
|
+
if (!/^(https?:|data:video\/|blob:)/i.test(url)) {
|
|
3997
|
+
inp.classList.add('err');
|
|
3998
|
+
inp.focus();
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
/* a page URL from a known video host can't play in a <video> tag —
|
|
4002
|
+
swap in the host's iframe player instead of probing it */
|
|
4003
|
+
const embed = this.videoEmbedUrl(url);
|
|
4004
|
+
if (embed) {
|
|
4005
|
+
this.replacePlaceholderWithEmbed(ph, embed);
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
ok.disabled = true;
|
|
4009
|
+
inp.disabled = true;
|
|
4010
|
+
/* only place the video once its metadata actually loads, so the user
|
|
4011
|
+
gets a working preview instead of a broken player */
|
|
4012
|
+
const probe = document.createElement('video');
|
|
4013
|
+
probe.preload = 'metadata';
|
|
4014
|
+
probe.onloadedmetadata = () => this.replacePlaceholderWithVideo(ph, url, '');
|
|
4015
|
+
probe.onerror = () => {
|
|
4016
|
+
ok.disabled = false;
|
|
4017
|
+
inp.disabled = false;
|
|
4018
|
+
inp.classList.add('err');
|
|
4019
|
+
inp.focus();
|
|
4020
|
+
};
|
|
4021
|
+
probe.src = url;
|
|
4022
|
+
};
|
|
4023
|
+
ok.addEventListener('click', submit);
|
|
4024
|
+
inp.addEventListener('input', () => inp.classList.remove('err'));
|
|
4025
|
+
inp.addEventListener('keydown', (e) => {
|
|
4026
|
+
if (e.key === 'Enter') {
|
|
4027
|
+
e.preventDefault();
|
|
4028
|
+
submit();
|
|
4029
|
+
}
|
|
4030
|
+
else if (e.key === 'Escape') {
|
|
4031
|
+
e.stopPropagation();
|
|
4032
|
+
this.renderVphCtxButtons();
|
|
4033
|
+
}
|
|
4034
|
+
});
|
|
4035
|
+
el.append(inp, ok);
|
|
4036
|
+
this.positionVphCtx();
|
|
4037
|
+
inp.focus();
|
|
4038
|
+
}
|
|
4039
|
+
readVideoFileInto(file, ph) {
|
|
4040
|
+
if (ph.classList.contains('uploading'))
|
|
4041
|
+
return;
|
|
4042
|
+
if (this.videoUploadHandler) {
|
|
4043
|
+
this.uploadVideoFile(file, ph);
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
const fr = new FileReader();
|
|
4047
|
+
fr.onload = () => {
|
|
4048
|
+
if (this.ed.contains(ph))
|
|
4049
|
+
this.replacePlaceholderWithVideo(ph, fr.result, file.name.replace(/"/g, ''));
|
|
4050
|
+
};
|
|
4051
|
+
fr.readAsDataURL(file);
|
|
4052
|
+
}
|
|
4053
|
+
/** Runs the host-supplied videoUploadHandler instead of inlining base64,
|
|
4054
|
+
* so apps can route the file to S3 / Azure Blob / their own server. */
|
|
4055
|
+
uploadVideoFile(file, ph) {
|
|
4056
|
+
const title = file.name.replace(/"/g, '');
|
|
4057
|
+
ph.classList.remove('upload-error');
|
|
4058
|
+
ph.classList.add('uploading');
|
|
4059
|
+
const span = ph.querySelector('span');
|
|
4060
|
+
if (span)
|
|
4061
|
+
span.textContent = this.t('uploading');
|
|
4062
|
+
this.videoUploadHandler(file)
|
|
4063
|
+
.then((url) => {
|
|
4064
|
+
if (!this.ed.contains(ph))
|
|
4065
|
+
return;
|
|
4066
|
+
this.replacePlaceholderWithVideo(ph, url, title);
|
|
4067
|
+
})
|
|
4068
|
+
.catch((err) => {
|
|
4069
|
+
if (!this.ed.contains(ph))
|
|
4070
|
+
return;
|
|
4071
|
+
ph.classList.remove('uploading');
|
|
4072
|
+
ph.classList.add('upload-error');
|
|
4073
|
+
if (span)
|
|
4074
|
+
span.textContent = this.t('uploadfailed');
|
|
4075
|
+
this.onVideoUploadError?.(err, file);
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
replacePlaceholderWithVideo(ph, src, title) {
|
|
4079
|
+
const vid = document.createElement('video');
|
|
4080
|
+
vid.controls = true;
|
|
4081
|
+
vid.src = src;
|
|
4082
|
+
if (title)
|
|
4083
|
+
vid.title = title;
|
|
4084
|
+
ph.replaceWith(vid);
|
|
4085
|
+
if (this.vphActive === ph)
|
|
4086
|
+
this.clearVphPlaceholderSel();
|
|
4087
|
+
this.refreshState();
|
|
4088
|
+
this.onChange();
|
|
4089
|
+
}
|
|
4090
|
+
/** Swaps a video placeholder for a hosted player iframe (YouTube & co.). */
|
|
4091
|
+
replacePlaceholderWithEmbed(ph, embedUrl) {
|
|
4092
|
+
const tmp = document.createElement('div');
|
|
4093
|
+
tmp.innerHTML = this.videoEmbedHTML(embedUrl);
|
|
4094
|
+
ph.replaceWith(tmp.firstElementChild);
|
|
4095
|
+
if (this.vphActive === ph)
|
|
4096
|
+
this.clearVphPlaceholderSel();
|
|
4097
|
+
this.refreshState();
|
|
4098
|
+
this.onChange();
|
|
4099
|
+
}
|
|
4100
|
+
positionVphCtx() {
|
|
4101
|
+
if (this.vphActive && !document.body.contains(this.vphActive)) {
|
|
4102
|
+
this.clearVphPlaceholderSel();
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
if (!this.vphActive || !this.vphCtx)
|
|
4106
|
+
return;
|
|
4107
|
+
const r = this.vphActive.getBoundingClientRect();
|
|
4108
|
+
const cw = this.vphCtx.offsetWidth;
|
|
4109
|
+
const ch = this.vphCtx.offsetHeight;
|
|
4110
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4111
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4112
|
+
let cy = r.bottom + scrollY + 8;
|
|
4113
|
+
if (r.bottom + 8 + ch > innerHeight - 4)
|
|
4114
|
+
cy = Math.max(this.ctxMinTop(), r.top + scrollY - ch - 8);
|
|
4115
|
+
this.vphCtx.style.left = cx + 'px';
|
|
4116
|
+
this.vphCtx.style.top = cy + 'px';
|
|
4117
|
+
}
|
|
4118
|
+
/* ---------------------------------------------------------- video toolbar (post-insert) */
|
|
4119
|
+
cancelVidHide() {
|
|
4120
|
+
if (this.vidHideTimer != null) {
|
|
4121
|
+
window.clearTimeout(this.vidHideTimer);
|
|
4122
|
+
this.vidHideTimer = null;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
clearVidHandles() {
|
|
4126
|
+
this.cancelVidHide();
|
|
4127
|
+
this.vidCtx?.remove();
|
|
4128
|
+
this.vidCtx = null;
|
|
4129
|
+
this.vidActive?.classList.remove('vid-selected');
|
|
4130
|
+
this.vidActive = null;
|
|
4131
|
+
}
|
|
4132
|
+
selectVideo(vid) {
|
|
4133
|
+
if (this.vidActive === vid)
|
|
4134
|
+
return;
|
|
4135
|
+
this.clearVidHandles();
|
|
4136
|
+
this.vidActive = vid;
|
|
4137
|
+
vid.classList.add('vid-selected');
|
|
4138
|
+
this.vidCtx = this.buildVideoCtxToolbar(vid);
|
|
4139
|
+
this.positionVidCtx();
|
|
4140
|
+
}
|
|
4141
|
+
buildVideoCtxToolbar(vid) {
|
|
4142
|
+
const el = document.createElement('div');
|
|
4143
|
+
el.className = 'imgctx';
|
|
4144
|
+
el.dir = this.dir();
|
|
4145
|
+
const rebuild = () => {
|
|
4146
|
+
const rebuilt = this.buildVideoCtxToolbar(vid);
|
|
4147
|
+
this.vidCtx?.replaceWith(rebuilt);
|
|
4148
|
+
this.vidCtx = rebuilt;
|
|
4149
|
+
this.positionVidCtx();
|
|
4150
|
+
};
|
|
4151
|
+
const align = (a) => {
|
|
4152
|
+
this.setVideoAlign(vid, a);
|
|
4153
|
+
rebuild();
|
|
4154
|
+
};
|
|
4155
|
+
const isLeft = vid.style.float === 'left';
|
|
4156
|
+
const isRight = vid.style.float === 'right';
|
|
4157
|
+
const isCenter = !isLeft && !isRight && vid.style.marginLeft === 'auto' && vid.style.marginRight === 'auto';
|
|
4158
|
+
const mkBtn = (icon, tip, on, fn) => {
|
|
4159
|
+
const b = this.ctxBtn(icon, tip, fn);
|
|
4160
|
+
b.classList.toggle('on', on);
|
|
4161
|
+
return b;
|
|
4162
|
+
};
|
|
4163
|
+
el.append(mkBtn(IC.alignleft, this.t('alignleft'), isLeft, () => align('left')), mkBtn(IC.aligncenter, this.t('aligncenter'), isCenter, () => align('center')), mkBtn(IC.alignright, this.t('alignright'), isRight, () => align('right')), this.ctxSep(), mkBtn(IC.trash, this.t('deletevideo'), false, () => {
|
|
4164
|
+
vid.remove();
|
|
4165
|
+
this.clearVidHandles();
|
|
4166
|
+
this.onChange();
|
|
4167
|
+
}));
|
|
4168
|
+
el.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4169
|
+
el.addEventListener('mouseenter', () => this.cancelVidHide());
|
|
4170
|
+
el.addEventListener('mouseleave', () => {
|
|
4171
|
+
this.cancelVidHide();
|
|
4172
|
+
this.vidHideTimer = window.setTimeout(() => this.clearVidHandles(), 250);
|
|
4173
|
+
});
|
|
4174
|
+
document.body.appendChild(el);
|
|
4175
|
+
return el;
|
|
4176
|
+
}
|
|
4177
|
+
setVideoAlign(vid, align) {
|
|
4178
|
+
vid.style.float = '';
|
|
4179
|
+
vid.style.display = '';
|
|
4180
|
+
vid.style.marginLeft = '';
|
|
4181
|
+
vid.style.marginRight = '';
|
|
4182
|
+
vid.style.marginBottom = '';
|
|
4183
|
+
if (align === 'left') {
|
|
4184
|
+
vid.style.float = 'left';
|
|
4185
|
+
vid.style.marginInlineEnd = '1em';
|
|
4186
|
+
vid.style.marginBottom = '0.5em';
|
|
4187
|
+
}
|
|
4188
|
+
else if (align === 'right') {
|
|
4189
|
+
vid.style.float = 'right';
|
|
4190
|
+
vid.style.marginInlineStart = '1em';
|
|
4191
|
+
vid.style.marginBottom = '0.5em';
|
|
4192
|
+
}
|
|
4193
|
+
else {
|
|
4194
|
+
vid.style.display = 'block';
|
|
4195
|
+
vid.style.marginLeft = 'auto';
|
|
4196
|
+
vid.style.marginRight = 'auto';
|
|
4197
|
+
}
|
|
4198
|
+
this.onChange();
|
|
4199
|
+
}
|
|
4200
|
+
positionVidCtx() {
|
|
4201
|
+
if (!this.vidActive || !document.body.contains(this.vidActive)) {
|
|
4202
|
+
this.clearVidHandles();
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
const r = this.vidActive.getBoundingClientRect();
|
|
4206
|
+
if (!this.vidCtx)
|
|
4207
|
+
return;
|
|
4208
|
+
const cw = this.vidCtx.offsetWidth;
|
|
4209
|
+
const ch = this.vidCtx.offsetHeight;
|
|
4210
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4211
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4212
|
+
let cy = r.top + scrollY - ch - 8;
|
|
4213
|
+
if (cy < this.ctxMinTop())
|
|
4214
|
+
cy = r.bottom + scrollY + 8;
|
|
4215
|
+
this.vidCtx.style.left = cx + 'px';
|
|
4216
|
+
this.vidCtx.style.top = cy + 'px';
|
|
4217
|
+
}
|
|
4218
|
+
/* ---------------------------------------------------------- video dialog / embeds */
|
|
4219
|
+
/** Maps a page URL from a known video host to its iframe player URL. Returns
|
|
4220
|
+
* null for anything else (e.g. direct .mp4 file URLs). */
|
|
4221
|
+
videoEmbedUrl(url) {
|
|
4222
|
+
let m = url.match(/^https?:\/\/(?:www\.|m\.)?youtube\.com\/watch\?(?:[^\s#]*[?&])?v=([\w-]{5,})/i);
|
|
4223
|
+
if (!m)
|
|
4224
|
+
m = url.match(/^https?:\/\/(?:www\.)?(?:youtu\.be|youtube\.com\/(?:embed|shorts|live))\/([\w-]{5,})/i);
|
|
4225
|
+
if (m)
|
|
4226
|
+
return 'https://www.youtube.com/embed/' + m[1];
|
|
4227
|
+
m = url.match(/^https?:\/\/(?:www\.)?vimeo\.com\/(\d+)/i);
|
|
4228
|
+
if (m)
|
|
4229
|
+
return 'https://player.vimeo.com/video/' + m[1];
|
|
4230
|
+
m = url.match(/^https?:\/\/(?:www\.)?dailymotion\.com\/video\/(\w+)/i);
|
|
4231
|
+
if (m)
|
|
4232
|
+
return 'https://www.dailymotion.com/embed/video/' + m[1];
|
|
4233
|
+
return null;
|
|
4234
|
+
}
|
|
4235
|
+
videoEmbedHTML(src, w = 560, h = 314) {
|
|
4236
|
+
return `<span class="video-embed" contenteditable="false"><iframe src="${src}" width="${w}" height="${h}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></span>`;
|
|
4237
|
+
}
|
|
4238
|
+
/** Insert/edit-video dialog with General (source/width/height), Embed (embed
|
|
4239
|
+
* code) and Advanced (alternative source, poster image) tabs. Hosted page
|
|
4240
|
+
* URLs (YouTube & co.) become iframe players; anything else becomes a
|
|
4241
|
+
* <video> tag. The General tab also offers the same upload path as before
|
|
4242
|
+
* (videoUploadHandler when configured, else inline base64). */
|
|
4243
|
+
videoDlg() {
|
|
4244
|
+
this.saveSel();
|
|
4245
|
+
let srcInp;
|
|
4246
|
+
let wInp;
|
|
4247
|
+
let hInp;
|
|
4248
|
+
let embedTa;
|
|
4249
|
+
let altInp;
|
|
4250
|
+
let posterInp;
|
|
4251
|
+
const field = (labelKey, input) => {
|
|
4252
|
+
const f = document.createElement('div');
|
|
4253
|
+
f.className = 'dfield';
|
|
4254
|
+
const l = document.createElement('label');
|
|
4255
|
+
l.textContent = this.t(labelKey);
|
|
4256
|
+
f.append(l, input);
|
|
4257
|
+
return f;
|
|
4258
|
+
};
|
|
4259
|
+
const urlInput = () => {
|
|
4260
|
+
const i = document.createElement('input');
|
|
4261
|
+
i.type = 'url';
|
|
4262
|
+
return i;
|
|
4263
|
+
};
|
|
4264
|
+
this.dialog(this.t('videodlgttl'), (body) => {
|
|
4265
|
+
const tabs = document.createElement('div');
|
|
4266
|
+
tabs.className = 'dtabs';
|
|
4267
|
+
const panels = [];
|
|
4268
|
+
const mkTab = (labelKey, build) => {
|
|
4269
|
+
const b = document.createElement('button');
|
|
4270
|
+
b.type = 'button';
|
|
4271
|
+
b.textContent = this.t(labelKey);
|
|
4272
|
+
const p = document.createElement('div');
|
|
4273
|
+
p.className = 'dtabpanel';
|
|
4274
|
+
build(p);
|
|
4275
|
+
const i = panels.length;
|
|
4276
|
+
panels.push(p);
|
|
4277
|
+
b.addEventListener('click', () => show(i));
|
|
4278
|
+
tabs.appendChild(b);
|
|
4279
|
+
};
|
|
4280
|
+
const show = (i) => {
|
|
4281
|
+
tabs.querySelectorAll('button').forEach((x, xi) => x.classList.toggle('on', xi === i));
|
|
4282
|
+
panels.forEach((x, xi) => (x.style.display = xi === i ? '' : 'none'));
|
|
4283
|
+
};
|
|
4284
|
+
mkTab('vidgeneral', (p) => {
|
|
4285
|
+
srcInp = urlInput();
|
|
4286
|
+
srcInp.placeholder = this.t('videourlph');
|
|
4287
|
+
const srcRow = document.createElement('div');
|
|
4288
|
+
srcRow.className = 'drow';
|
|
4289
|
+
const upBtn = document.createElement('button');
|
|
4290
|
+
upBtn.type = 'button';
|
|
4291
|
+
upBtn.className = 'txtbtn';
|
|
4292
|
+
upBtn.textContent = this.t('uploadvideo');
|
|
4293
|
+
const file = document.createElement('input');
|
|
4294
|
+
file.type = 'file';
|
|
4295
|
+
file.accept = this.options.videoFileTypes.join(',');
|
|
4296
|
+
file.style.display = 'none';
|
|
4297
|
+
upBtn.addEventListener('click', () => {
|
|
4298
|
+
file.value = '';
|
|
4299
|
+
file.click();
|
|
4300
|
+
});
|
|
4301
|
+
file.addEventListener('change', () => {
|
|
4302
|
+
const f = file.files?.[0];
|
|
4303
|
+
if (!f)
|
|
4304
|
+
return;
|
|
4305
|
+
upBtn.disabled = true;
|
|
4306
|
+
upBtn.textContent = this.t('uploading');
|
|
4307
|
+
const done = (url) => {
|
|
4308
|
+
srcInp.value = url;
|
|
4309
|
+
upBtn.disabled = false;
|
|
4310
|
+
upBtn.textContent = this.t('uploadvideo');
|
|
4311
|
+
};
|
|
4312
|
+
if (this.videoUploadHandler) {
|
|
4313
|
+
this.videoUploadHandler(f)
|
|
4314
|
+
.then(done)
|
|
4315
|
+
.catch((err) => {
|
|
4316
|
+
upBtn.disabled = false;
|
|
4317
|
+
upBtn.textContent = this.t('uploadfailed');
|
|
4318
|
+
this.onVideoUploadError?.(err, f);
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
else {
|
|
4322
|
+
const fr = new FileReader();
|
|
4323
|
+
fr.onload = () => done(fr.result);
|
|
4324
|
+
fr.readAsDataURL(f);
|
|
4325
|
+
}
|
|
4326
|
+
});
|
|
4327
|
+
srcRow.append(srcInp, upBtn, file);
|
|
4328
|
+
srcInp.style.flex = '1';
|
|
4329
|
+
p.appendChild(field('vidsource', srcRow));
|
|
4330
|
+
const dims = document.createElement('div');
|
|
4331
|
+
dims.className = 'drow';
|
|
4332
|
+
wInp = document.createElement('input');
|
|
4333
|
+
wInp.type = 'number';
|
|
4334
|
+
wInp.value = '560';
|
|
4335
|
+
hInp = document.createElement('input');
|
|
4336
|
+
hInp.type = 'number';
|
|
4337
|
+
hInp.value = '314';
|
|
4338
|
+
dims.append(field('tblwidth', wInp), field('tblheight', hInp));
|
|
4339
|
+
p.appendChild(dims);
|
|
4340
|
+
});
|
|
4341
|
+
mkTab('vidembed', (p) => {
|
|
4342
|
+
const hint = document.createElement('p');
|
|
4343
|
+
hint.textContent = this.t('vidembedhint');
|
|
4344
|
+
hint.style.cssText = 'margin:0 0 8px;color:#556;font-size:13.5px';
|
|
4345
|
+
embedTa = document.createElement('textarea');
|
|
4346
|
+
embedTa.style.cssText = 'width:100%;box-sizing:border-box;height:160px';
|
|
4347
|
+
p.append(hint, embedTa);
|
|
4348
|
+
});
|
|
4349
|
+
mkTab('vidadvanced', (p) => {
|
|
4350
|
+
altInp = urlInput();
|
|
4351
|
+
posterInp = urlInput();
|
|
4352
|
+
p.append(field('vidaltsource', altInp), field('vidposter', posterInp));
|
|
4353
|
+
});
|
|
4354
|
+
const wrap = document.createElement('div');
|
|
4355
|
+
wrap.className = 'dtabbody';
|
|
4356
|
+
panels.forEach((p) => wrap.appendChild(p));
|
|
4357
|
+
body.append(tabs, wrap);
|
|
4358
|
+
show(0);
|
|
4359
|
+
setTimeout(() => srcInp.focus(), 0);
|
|
4360
|
+
}, [
|
|
4361
|
+
{ label: this.t('cancel'), action: () => this.closeDlg() },
|
|
4362
|
+
{
|
|
4363
|
+
label: this.t('save'),
|
|
4364
|
+
primary: true,
|
|
4365
|
+
action: () => {
|
|
4366
|
+
const embedCode = embedTa.value.trim();
|
|
4367
|
+
const src = srcInp.value.trim();
|
|
4368
|
+
const w = parseInt(wInp.value, 10) || 560;
|
|
4369
|
+
const h = parseInt(hInp.value, 10) || 314;
|
|
4370
|
+
this.closeDlg();
|
|
4371
|
+
if (!embedCode && !src)
|
|
4372
|
+
return;
|
|
4373
|
+
this.restoreSel();
|
|
4374
|
+
if (embedCode) {
|
|
4375
|
+
document.execCommand('insertHTML', false, `<span class="video-embed" contenteditable="false">${embedCode}</span>`);
|
|
4376
|
+
this.onChange();
|
|
4377
|
+
return;
|
|
4378
|
+
}
|
|
4379
|
+
const embed = this.videoEmbedUrl(src);
|
|
4380
|
+
if (embed) {
|
|
4381
|
+
document.execCommand('insertHTML', false, this.videoEmbedHTML(embed, w, h));
|
|
4382
|
+
}
|
|
4383
|
+
else {
|
|
4384
|
+
const alt = altInp.value.trim();
|
|
4385
|
+
const poster = posterInp.value.trim();
|
|
4386
|
+
document.execCommand('insertHTML', false, `<video controls width="${w}" height="${h}"${poster ? ` poster="${this.escapeAttr(poster)}"` : ''}>` +
|
|
4387
|
+
`<source src="${this.escapeAttr(src)}">${alt ? `<source src="${this.escapeAttr(alt)}">` : ''}</video>`);
|
|
4388
|
+
}
|
|
4389
|
+
this.onChange();
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
]);
|
|
4393
|
+
}
|
|
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
|
+
}
|
|
4402
|
+
}
|
|
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)
|
|
4423
|
+
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 };
|
|
4433
|
+
}
|
|
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 };
|
|
4442
|
+
}
|
|
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(' \\\\ ');
|
|
4458
|
+
}
|
|
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) {
|
|
4464
|
+
const sel = window.getSelection();
|
|
4465
|
+
if (!sel || !sel.isCollapsed || !sel.anchorNode)
|
|
4466
|
+
return;
|
|
4467
|
+
const node = sel.anchorNode;
|
|
4468
|
+
if (node.nodeType !== Node.TEXT_NODE || !this.ed.contains(node))
|
|
4469
|
+
return;
|
|
4470
|
+
if (node.parentElement?.closest('.math-fable, pre, code'))
|
|
4471
|
+
return;
|
|
4472
|
+
const before = (node.textContent || '').slice(0, sel.anchorOffset);
|
|
4473
|
+
const m = before.match(/(?:^|[^\\$])\$([^$]+)$/);
|
|
4474
|
+
if (!m)
|
|
4475
|
+
return;
|
|
4476
|
+
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'))
|
|
4481
|
+
return;
|
|
4482
|
+
e.preventDefault();
|
|
4483
|
+
const range = document.createRange();
|
|
4484
|
+
range.setStart(node, sel.anchorOffset - src.length - 1);
|
|
4485
|
+
range.setEnd(node, sel.anchorOffset);
|
|
4486
|
+
sel.removeAllRanges();
|
|
4487
|
+
sel.addRange(range);
|
|
4488
|
+
this.insertMathElement(src, false);
|
|
4489
|
+
this.onChange();
|
|
4490
|
+
}
|
|
4491
|
+
mathDlg(existing) {
|
|
4492
|
+
this.saveSel();
|
|
4493
|
+
const isBlock = existing?.classList.contains('math-fable-block') ?? false;
|
|
4494
|
+
const initialLatex = existing?.dataset.latex ? this.unescapeAttr(existing.dataset.latex) : '';
|
|
4495
|
+
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) => {
|
|
4505
|
+
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);
|
|
4525
|
+
}, [
|
|
4526
|
+
{ label: this.t('cancel'), action: () => this.closeDlg() },
|
|
4527
|
+
{
|
|
4528
|
+
label: this.t('save'),
|
|
4529
|
+
primary: true,
|
|
4530
|
+
action: () => {
|
|
4531
|
+
const src = ta.value.trim();
|
|
4532
|
+
this.closeDlg();
|
|
4533
|
+
if (!src)
|
|
4534
|
+
return;
|
|
4535
|
+
const block = derivChk.checked;
|
|
4536
|
+
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();
|
|
4542
|
+
this.onChange();
|
|
4543
|
+
return;
|
|
4544
|
+
}
|
|
4545
|
+
this.restoreSel();
|
|
4546
|
+
this.insertMathElement(src, block);
|
|
4547
|
+
this.onChange();
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
]);
|
|
4551
|
+
}
|
|
4552
|
+
escapeAttr(s) {
|
|
4553
|
+
return s.replace(/&/g, '&').replace(/"/g, '"');
|
|
4554
|
+
}
|
|
4555
|
+
unescapeAttr(s) {
|
|
4556
|
+
return s.replace(/"/g, '"').replace(/&/g, '&');
|
|
4557
|
+
}
|
|
4558
|
+
clearMathSel() {
|
|
4559
|
+
this.mathCtx?.remove();
|
|
4560
|
+
this.mathCtx = null;
|
|
4561
|
+
this.mathActive?.classList.remove('math-selected');
|
|
4562
|
+
this.mathActive = null;
|
|
4563
|
+
}
|
|
4564
|
+
selectMathEl(el) {
|
|
4565
|
+
if (this.mathActive === el)
|
|
4566
|
+
return;
|
|
4567
|
+
this.clearMathSel();
|
|
4568
|
+
this.mathActive = el;
|
|
4569
|
+
el.classList.add('math-selected');
|
|
4570
|
+
this.mathCtx = this.buildMathCtxToolbar(el);
|
|
4571
|
+
this.positionMathCtx();
|
|
4572
|
+
}
|
|
4573
|
+
buildMathCtxToolbar(el) {
|
|
4574
|
+
const ctx = document.createElement('div');
|
|
4575
|
+
ctx.className = 'imgctx';
|
|
4576
|
+
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'), () => {
|
|
4578
|
+
el.remove();
|
|
4579
|
+
this.clearMathSel();
|
|
4580
|
+
this.onChange();
|
|
4581
|
+
}));
|
|
4582
|
+
ctx.addEventListener('mousedown', (e) => e.preventDefault());
|
|
4583
|
+
document.body.appendChild(ctx);
|
|
4584
|
+
return ctx;
|
|
4585
|
+
}
|
|
4586
|
+
positionMathCtx() {
|
|
4587
|
+
if (!this.mathActive || !document.body.contains(this.mathActive)) {
|
|
4588
|
+
this.clearMathSel();
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
const r = this.mathActive.getBoundingClientRect();
|
|
4592
|
+
if (!this.mathCtx)
|
|
4593
|
+
return;
|
|
4594
|
+
const cw = this.mathCtx.offsetWidth;
|
|
4595
|
+
const ch = this.mathCtx.offsetHeight;
|
|
4596
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4597
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4598
|
+
let cy = r.top + scrollY - ch - 8;
|
|
4599
|
+
if (cy < this.ctxMinTop())
|
|
4600
|
+
cy = r.bottom + scrollY + 8;
|
|
4601
|
+
this.mathCtx.style.left = cx + 'px';
|
|
4602
|
+
this.mathCtx.style.top = cy + 'px';
|
|
4603
|
+
}
|
|
4604
|
+
/* ---------------------------------------------------------- templates */
|
|
4605
|
+
templateMenu(anchor) {
|
|
4606
|
+
this.popup(anchor, (el) => this.buildTemplatePickInto(el));
|
|
4607
|
+
}
|
|
4608
|
+
/** Miniature div-based mockup of a layout, mirroring what the block looks
|
|
4609
|
+
like in the editor: grey box = image slot, bars = heading/paragraph. */
|
|
4610
|
+
tplPreviewHTML(layout) {
|
|
4611
|
+
const img = '<span class="pv-img"></span>';
|
|
4612
|
+
const head = '<span class="pv-h"></span>';
|
|
4613
|
+
const line = (w) => `<span class="pv-l" style="width:${w}%"></span>`;
|
|
4614
|
+
const col = (inner) => `<span class="pv-col">${inner}</span>`;
|
|
4615
|
+
const row = (inner) => `<span class="pv-row">${inner}</span>`;
|
|
4616
|
+
const textCol = col(head + line(100) + line(88));
|
|
4617
|
+
if (layout === 'img-left')
|
|
4618
|
+
return `<span class="pv">${row(img + textCol)}${line(100)}${line(72)}</span>`;
|
|
4619
|
+
if (layout === 'img-right')
|
|
4620
|
+
return `<span class="pv">${row(textCol + img)}${line(100)}${line(72)}</span>`;
|
|
4621
|
+
if (layout === 'img-top')
|
|
4622
|
+
return `<span class="pv pv-c">${img}${head}${line(100)}${line(72)}</span>`;
|
|
4623
|
+
return `<span class="pv pv-c">${head}${line(100)}${line(72)}${img}</span>`;
|
|
4624
|
+
}
|
|
4625
|
+
buildTemplatePickInto(el, after) {
|
|
4626
|
+
const wrap = document.createElement('div');
|
|
4627
|
+
wrap.className = 'tplpick';
|
|
4628
|
+
TPL_LAYOUTS.forEach((layout) => {
|
|
4629
|
+
const tile = document.createElement('button');
|
|
4630
|
+
tile.type = 'button';
|
|
4631
|
+
tile.className = 'tpltile';
|
|
4632
|
+
tile.title = this.t(TPL_LABEL[layout]);
|
|
4633
|
+
tile.innerHTML = this.tplPreviewHTML(layout) + `<span class="tpllbl">${this.t(TPL_LABEL[layout])}</span>`;
|
|
4634
|
+
tile.addEventListener('click', () => {
|
|
4635
|
+
this.closePop();
|
|
4636
|
+
this.insertTemplate(layout);
|
|
4637
|
+
after?.();
|
|
4638
|
+
});
|
|
4639
|
+
wrap.appendChild(tile);
|
|
4640
|
+
});
|
|
4641
|
+
el.appendChild(wrap);
|
|
4642
|
+
}
|
|
4643
|
+
insertTemplate(layout) {
|
|
4644
|
+
this.restoreSel();
|
|
4645
|
+
const media = `<div class="tpl-media" contenteditable="false">${this.imgPhHTML()}</div>`;
|
|
4646
|
+
const text = `<div class="tpl-text"><h2>${this.t('heading')}</h2><p>${this.t('para')}</p></div>`;
|
|
4647
|
+
/* text-above-image layout keeps DOM order = visual order so the caret
|
|
4648
|
+
travels naturally; every other layout has the media slot first */
|
|
4649
|
+
const inner = layout === 'img-center' ? text + media : media + text;
|
|
4650
|
+
document.execCommand('insertHTML', false, `<div class="tpl tpl-${layout}">${inner}</div><p><br></p>`);
|
|
4651
|
+
this.saveSel();
|
|
4652
|
+
this.onChange();
|
|
4653
|
+
}
|
|
4654
|
+
clearTplSel() {
|
|
4655
|
+
this.tplCtx?.remove();
|
|
4656
|
+
this.tplCtx = null;
|
|
4657
|
+
this.tplActive?.classList.remove('tpl-selected');
|
|
4658
|
+
this.tplActive = null;
|
|
4659
|
+
}
|
|
4660
|
+
selectTemplate(tpl) {
|
|
4661
|
+
if (this.tplActive === tpl)
|
|
4662
|
+
return;
|
|
4663
|
+
this.clearTplSel();
|
|
4664
|
+
this.tplActive = tpl;
|
|
4665
|
+
tpl.classList.add('tpl-selected');
|
|
4666
|
+
this.tplCtx = document.createElement('div');
|
|
4667
|
+
this.tplCtx.className = 'imgctx tplctx';
|
|
4668
|
+
this.tplCtx.dir = this.dir();
|
|
4669
|
+
document.body.appendChild(this.tplCtx);
|
|
4670
|
+
this.renderTplCtxButtons();
|
|
4671
|
+
}
|
|
4672
|
+
renderTplCtxButtons() {
|
|
4673
|
+
const el = this.tplCtx;
|
|
4674
|
+
const tpl = this.tplActive;
|
|
4675
|
+
if (!el || !tpl)
|
|
4676
|
+
return;
|
|
4677
|
+
el.innerHTML = '';
|
|
4678
|
+
TPL_LAYOUTS.forEach((layout) => {
|
|
4679
|
+
const b = this.ctxBtn(TPL_ICON[layout], this.t(TPL_LABEL[layout]), () => this.setTplLayout(layout));
|
|
4680
|
+
b.classList.toggle('on', tpl.classList.contains('tpl-' + layout));
|
|
4681
|
+
el.appendChild(b);
|
|
4682
|
+
});
|
|
4683
|
+
el.append(this.ctxSep(), this.ctxBtn(IC.trash, this.t('deltemplate'), () => this.removeTemplate()));
|
|
4684
|
+
this.positionTplCtx();
|
|
4685
|
+
}
|
|
4686
|
+
setTplLayout(layout) {
|
|
4687
|
+
const tpl = this.tplActive;
|
|
4688
|
+
if (!tpl)
|
|
4689
|
+
return;
|
|
4690
|
+
TPL_LAYOUTS.forEach((l) => tpl.classList.remove('tpl-' + l));
|
|
4691
|
+
tpl.classList.add('tpl-' + layout);
|
|
4692
|
+
/* keep DOM order = visual order (see insertTemplate) */
|
|
4693
|
+
const media = tpl.querySelector(':scope > .tpl-media');
|
|
4694
|
+
const text = tpl.querySelector(':scope > .tpl-text');
|
|
4695
|
+
if (media && text) {
|
|
4696
|
+
if (layout === 'img-center')
|
|
4697
|
+
tpl.appendChild(media);
|
|
4698
|
+
else
|
|
4699
|
+
tpl.insertBefore(media, text);
|
|
4700
|
+
}
|
|
4701
|
+
this.renderTplCtxButtons();
|
|
4702
|
+
this.positionImgPhCtx();
|
|
4703
|
+
this.positionImageHandles();
|
|
4704
|
+
this.onChange();
|
|
4705
|
+
}
|
|
4706
|
+
removeTemplate() {
|
|
4707
|
+
const tpl = this.tplActive;
|
|
4708
|
+
if (!tpl)
|
|
4709
|
+
return;
|
|
4710
|
+
this.clearTplSel();
|
|
4711
|
+
if (this.phActive && tpl.contains(this.phActive))
|
|
4712
|
+
this.clearImgPlaceholderSel();
|
|
4713
|
+
if (this.imgActive && tpl.contains(this.imgActive))
|
|
4714
|
+
this.clearImageHandles();
|
|
4715
|
+
tpl.remove();
|
|
4716
|
+
this.refreshState();
|
|
4717
|
+
this.onChange();
|
|
4718
|
+
}
|
|
4719
|
+
positionTplCtx() {
|
|
4720
|
+
if (this.tplActive && !document.body.contains(this.tplActive)) {
|
|
4721
|
+
this.clearTplSel();
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
if (!this.tplActive || !this.tplCtx)
|
|
4725
|
+
return;
|
|
4726
|
+
const r = this.tplActive.getBoundingClientRect();
|
|
4727
|
+
const cw = this.tplCtx.offsetWidth;
|
|
4728
|
+
const ch = this.tplCtx.offsetHeight;
|
|
4729
|
+
let cx = r.left + scrollX + (r.width - cw) / 2;
|
|
4730
|
+
cx = Math.max(8 + scrollX, Math.min(cx, scrollX + innerWidth - cw - 8));
|
|
4731
|
+
let cy = r.top + scrollY - ch - 8;
|
|
4732
|
+
if (cy < this.ctxMinTop())
|
|
4733
|
+
cy = r.bottom + scrollY + 8;
|
|
4734
|
+
this.tplCtx.style.left = cx + 'px';
|
|
4735
|
+
this.tplCtx.style.top = cy + 'px';
|
|
4736
|
+
}
|
|
3451
4737
|
pickWordDoc() {
|
|
3452
4738
|
this.docInput.value = '';
|
|
3453
4739
|
this.docInput.click();
|
|
@@ -3554,7 +4840,7 @@ class FableEditor {
|
|
|
3554
4840
|
b.addEventListener('mouseleave', () => this.setOpTarget(null));
|
|
3555
4841
|
return b;
|
|
3556
4842
|
};
|
|
3557
|
-
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.
|
|
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()));
|
|
3558
4844
|
document.body.appendChild(el);
|
|
3559
4845
|
return el;
|
|
3560
4846
|
}
|
|
@@ -3761,7 +5047,13 @@ class FableEditor {
|
|
|
3761
5047
|
this.flipImage(img, 'y');
|
|
3762
5048
|
rebuild();
|
|
3763
5049
|
}), this.ctxSep(), mkBtn(IC.trash, this.t('deleteimg'), false, () => {
|
|
3764
|
-
|
|
5050
|
+
/* inside a template media slot the image swaps back to an
|
|
5051
|
+
upload placeholder so the slot is not lost */
|
|
5052
|
+
const slot = img.closest('.tpl-media');
|
|
5053
|
+
if (slot && this.ed.contains(slot))
|
|
5054
|
+
img.outerHTML = this.imgPhHTML();
|
|
5055
|
+
else
|
|
5056
|
+
img.remove();
|
|
3765
5057
|
this.clearImageHandles();
|
|
3766
5058
|
this.onChange();
|
|
3767
5059
|
}));
|
|
@@ -4028,6 +5320,26 @@ class FableEditor {
|
|
|
4028
5320
|
fr.readAsDataURL(imgItem.getAsFile());
|
|
4029
5321
|
return;
|
|
4030
5322
|
}
|
|
5323
|
+
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. */
|
|
5329
|
+
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
|
+
const embed = /^\S+$/.test(text.trim()) ? this.videoEmbedUrl(text.trim()) : null;
|
|
5337
|
+
if (embed) {
|
|
5338
|
+
document.execCommand('insertHTML', false, this.videoEmbedHTML(embed));
|
|
5339
|
+
this.onChange();
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
4031
5343
|
if (html) {
|
|
4032
5344
|
document.execCommand('insertHTML', false, cleanPastedHTML(html, this.dir()));
|
|
4033
5345
|
}
|
|
@@ -4042,6 +5354,9 @@ class FableEditor {
|
|
|
4042
5354
|
this.closeDlg();
|
|
4043
5355
|
this.clearTableHandles();
|
|
4044
5356
|
this.clearImgPlaceholderSel();
|
|
5357
|
+
this.clearVphPlaceholderSel();
|
|
5358
|
+
this.clearVidHandles();
|
|
5359
|
+
this.clearMathSel();
|
|
4045
5360
|
this.shell.dir = this.dir();
|
|
4046
5361
|
this.ed.dir = this.dir();
|
|
4047
5362
|
if (this.options.menubar)
|