neiki-editor 2.10.1 → 3.0.1
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 -17
- package/dist/neiki-editor.css +243 -12
- package/dist/neiki-editor.js +791 -128
- package/dist/neiki-editor.min.css +1 -1
- package/dist/neiki-editor.min.js +1 -1
- package/package.json +1 -1
package/dist/neiki-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NeikiEditor - A Modern WYSIWYG Editor
|
|
3
|
-
* Version:
|
|
3
|
+
* Version: 3.0.1
|
|
4
4
|
*
|
|
5
5
|
* A lightweight, feature-rich text editor with support for:
|
|
6
6
|
* - Rich text formatting (bold, italic, underline, etc.)
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
'toolbar.outdent': 'Decrease Indent',
|
|
50
50
|
'toolbar.link': 'Insert Link (Ctrl+K)',
|
|
51
51
|
'toolbar.image': 'Insert Image',
|
|
52
|
+
'toolbar.video': 'Insert Video',
|
|
52
53
|
'toolbar.table': 'Insert Table',
|
|
53
54
|
'toolbar.blockquote': 'Blockquote',
|
|
54
55
|
'toolbar.viewCode': 'View Code (Toggle HTML)',
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
'toolbar.specialChars': 'Special Characters',
|
|
62
63
|
'toolbar.fullscreen': 'Fullscreen',
|
|
63
64
|
'toolbar.autosave': 'Toggle Autosave',
|
|
64
|
-
'toolbar.themeToggle': '
|
|
65
|
+
'toolbar.themeToggle': 'Change theme',
|
|
65
66
|
'toolbar.print': 'Print',
|
|
66
67
|
'toolbar.code': 'Code',
|
|
67
68
|
'toolbar.insert': 'Insert',
|
|
@@ -87,6 +88,7 @@
|
|
|
87
88
|
// Insert dropdown
|
|
88
89
|
'insert.link': 'Link',
|
|
89
90
|
'insert.image': 'Image',
|
|
91
|
+
'insert.video': 'Video',
|
|
90
92
|
'insert.table': 'Table',
|
|
91
93
|
'insert.emoji': 'Emoji',
|
|
92
94
|
'insert.symbol': 'Symbol',
|
|
@@ -98,9 +100,13 @@
|
|
|
98
100
|
'menu.print': 'Print',
|
|
99
101
|
'menu.autosave': 'Autosave',
|
|
100
102
|
'menu.clearAll': 'Clear all',
|
|
101
|
-
'menu.toggleTheme': '
|
|
103
|
+
'menu.toggleTheme': 'Change theme',
|
|
102
104
|
'menu.fullscreen': 'Fullscreen',
|
|
103
105
|
'menu.help': 'Help',
|
|
106
|
+
'theme.light': 'Light',
|
|
107
|
+
'theme.dark': 'Dark',
|
|
108
|
+
'theme.blue': 'Blue',
|
|
109
|
+
'theme.darkBlue': 'Dark Blue',
|
|
104
110
|
|
|
105
111
|
// Help modal
|
|
106
112
|
'help.author': 'Author',
|
|
@@ -130,6 +136,15 @@
|
|
|
130
136
|
'modal.describeImage': 'Describe the image',
|
|
131
137
|
'modal.widthOptional': 'Width (optional)',
|
|
132
138
|
'modal.invalidImageFile': 'Please select a valid image file.',
|
|
139
|
+
'modal.insertVideo': 'Insert Video',
|
|
140
|
+
'modal.uploadVideo': 'Upload Video',
|
|
141
|
+
'modal.convertedVideoToBase64': 'Will be converted to base64',
|
|
142
|
+
'modal.videoUrl': 'Video URL',
|
|
143
|
+
'modal.videoTitle': 'Video title',
|
|
144
|
+
'modal.describeVideo': 'Describe the video',
|
|
145
|
+
'modal.invalidVideoFile': 'Please select a valid video file.',
|
|
146
|
+
'modal.uploadingVideo': 'Uploading...',
|
|
147
|
+
'modal.videoUploadError': 'Video upload failed. Please try again.',
|
|
133
148
|
|
|
134
149
|
// Table modal
|
|
135
150
|
'modal.insertTable': 'Insert Table',
|
|
@@ -179,6 +194,8 @@
|
|
|
179
194
|
'imageToolbar.replaceImage': 'Replace Image',
|
|
180
195
|
'imageToolbar.deleteImage': 'Delete Image',
|
|
181
196
|
'imageToolbar.dragToMove': 'Drag to move',
|
|
197
|
+
'videoToolbar.replaceVideo': 'Replace Video',
|
|
198
|
+
'videoToolbar.deleteVideo': 'Delete Video',
|
|
182
199
|
|
|
183
200
|
// Placeholder
|
|
184
201
|
'placeholder': 'Start typing...'
|
|
@@ -206,6 +223,7 @@
|
|
|
206
223
|
'toolbar.outdent': 'Zmenšit odsazení',
|
|
207
224
|
'toolbar.link': 'Vložit odkaz (Ctrl+K)',
|
|
208
225
|
'toolbar.image': 'Vložit obrázek',
|
|
226
|
+
'toolbar.video': 'Vložit video',
|
|
209
227
|
'toolbar.table': 'Vložit tabulku',
|
|
210
228
|
'toolbar.blockquote': 'Citace',
|
|
211
229
|
'toolbar.viewCode': 'Zobrazit kód (HTML)',
|
|
@@ -218,7 +236,7 @@
|
|
|
218
236
|
'toolbar.specialChars': 'Speciální znaky',
|
|
219
237
|
'toolbar.fullscreen': 'Celá obrazovka',
|
|
220
238
|
'toolbar.autosave': 'Auto. ukládání',
|
|
221
|
-
'toolbar.themeToggle': '
|
|
239
|
+
'toolbar.themeToggle': 'Změnit motiv',
|
|
222
240
|
'toolbar.print': 'Tisk',
|
|
223
241
|
'toolbar.code': 'Kód',
|
|
224
242
|
'toolbar.insert': 'Vložit',
|
|
@@ -244,6 +262,7 @@
|
|
|
244
262
|
// Insert dropdown
|
|
245
263
|
'insert.link': 'Odkaz',
|
|
246
264
|
'insert.image': 'Obrázek',
|
|
265
|
+
'insert.video': 'Video',
|
|
247
266
|
'insert.table': 'Tabulka',
|
|
248
267
|
'insert.emoji': 'Emoji',
|
|
249
268
|
'insert.symbol': 'Symbol',
|
|
@@ -255,9 +274,13 @@
|
|
|
255
274
|
'menu.print': 'Tisk',
|
|
256
275
|
'menu.autosave': 'Auto. ukládání',
|
|
257
276
|
'menu.clearAll': 'Vymazat vše',
|
|
258
|
-
'menu.toggleTheme': '
|
|
277
|
+
'menu.toggleTheme': 'Změnit motiv',
|
|
259
278
|
'menu.fullscreen': 'Celá obrazovka',
|
|
260
279
|
'menu.help': 'Nápověda',
|
|
280
|
+
'theme.light': 'Světlý',
|
|
281
|
+
'theme.dark': 'Tmavý',
|
|
282
|
+
'theme.blue': 'Modrý',
|
|
283
|
+
'theme.darkBlue': 'Tmavě modrý',
|
|
261
284
|
'help.author': 'Autor',
|
|
262
285
|
'help.version': 'Verze',
|
|
263
286
|
'help.github': 'GitHub',
|
|
@@ -285,6 +308,15 @@
|
|
|
285
308
|
'modal.describeImage': 'Popis obrázku',
|
|
286
309
|
'modal.widthOptional': 'Šířka (volitelné)',
|
|
287
310
|
'modal.invalidImageFile': 'Vyberte prosím platný soubor obrázku.',
|
|
311
|
+
'modal.insertVideo': 'Vložit video',
|
|
312
|
+
'modal.uploadVideo': 'Nahrát video',
|
|
313
|
+
'modal.convertedVideoToBase64': 'Bude převedeno na base64',
|
|
314
|
+
'modal.videoUrl': 'URL videa',
|
|
315
|
+
'modal.videoTitle': 'Název videa',
|
|
316
|
+
'modal.describeVideo': 'Popis videa',
|
|
317
|
+
'modal.invalidVideoFile': 'Vyberte prosím platný soubor videa.',
|
|
318
|
+
'modal.uploadingVideo': 'Nahrávání...',
|
|
319
|
+
'modal.videoUploadError': 'Nahrávání videa selhalo. Zkuste to znovu.',
|
|
288
320
|
|
|
289
321
|
// Table modal
|
|
290
322
|
'modal.insertTable': 'Vložit tabulku',
|
|
@@ -331,6 +363,8 @@
|
|
|
331
363
|
'imageToolbar.replaceImage': 'Nahradit obrázek',
|
|
332
364
|
'imageToolbar.deleteImage': 'Smazat obrázek',
|
|
333
365
|
'imageToolbar.dragToMove': 'Přetáhněte pro přesun',
|
|
366
|
+
'videoToolbar.replaceVideo': 'Nahradit video',
|
|
367
|
+
'videoToolbar.deleteVideo': 'Smazat video',
|
|
334
368
|
|
|
335
369
|
'placeholder': 'Začněte psát...'
|
|
336
370
|
},
|
|
@@ -369,7 +403,7 @@
|
|
|
369
403
|
'toolbar.specialChars': '特殊字符',
|
|
370
404
|
'toolbar.fullscreen': '全屏',
|
|
371
405
|
'toolbar.autosave': '自动保存',
|
|
372
|
-
'toolbar.themeToggle': '
|
|
406
|
+
'toolbar.themeToggle': '更改主题',
|
|
373
407
|
'toolbar.print': '打印',
|
|
374
408
|
'toolbar.code': '代码',
|
|
375
409
|
'toolbar.insert': '插入',
|
|
@@ -398,9 +432,13 @@
|
|
|
398
432
|
'menu.print': '打印',
|
|
399
433
|
'menu.autosave': '自动保存',
|
|
400
434
|
'menu.clearAll': '清除全部',
|
|
401
|
-
'menu.toggleTheme': '
|
|
435
|
+
'menu.toggleTheme': '更改主题',
|
|
402
436
|
'menu.fullscreen': '全屏',
|
|
403
437
|
'menu.help': '帮助',
|
|
438
|
+
'theme.light': '浅色',
|
|
439
|
+
'theme.dark': '深色',
|
|
440
|
+
'theme.blue': '蓝色',
|
|
441
|
+
'theme.darkBlue': '深蓝色',
|
|
404
442
|
'help.author': '作者',
|
|
405
443
|
'help.version': '版本',
|
|
406
444
|
'help.github': 'GitHub',
|
|
@@ -458,6 +496,8 @@
|
|
|
458
496
|
'imageToolbar.replaceImage': '替换图片',
|
|
459
497
|
'imageToolbar.deleteImage': '删除图片',
|
|
460
498
|
'imageToolbar.dragToMove': '拖动移动',
|
|
499
|
+
'videoToolbar.replaceVideo': '替换视频',
|
|
500
|
+
'videoToolbar.deleteVideo': '删除视频',
|
|
461
501
|
|
|
462
502
|
'placeholder': '开始输入...'
|
|
463
503
|
},
|
|
@@ -527,6 +567,10 @@
|
|
|
527
567
|
'menu.toggleTheme': 'Cambiar tema',
|
|
528
568
|
'menu.fullscreen': 'Pantalla completa',
|
|
529
569
|
'menu.help': 'Ayuda',
|
|
570
|
+
'theme.light': 'Claro',
|
|
571
|
+
'theme.dark': 'Oscuro',
|
|
572
|
+
'theme.blue': 'Azul',
|
|
573
|
+
'theme.darkBlue': 'Azul oscuro',
|
|
530
574
|
'help.author': 'Autor',
|
|
531
575
|
'help.version': 'Versión',
|
|
532
576
|
'help.github': 'GitHub',
|
|
@@ -584,6 +628,8 @@
|
|
|
584
628
|
'imageToolbar.replaceImage': 'Reemplazar imagen',
|
|
585
629
|
'imageToolbar.deleteImage': 'Eliminar imagen',
|
|
586
630
|
'imageToolbar.dragToMove': 'Arrastrar para mover',
|
|
631
|
+
'videoToolbar.replaceVideo': 'Reemplazar video',
|
|
632
|
+
'videoToolbar.deleteVideo': 'Eliminar video',
|
|
587
633
|
|
|
588
634
|
'placeholder': 'Empiece a escribir...'
|
|
589
635
|
},
|
|
@@ -621,7 +667,7 @@
|
|
|
621
667
|
'toolbar.specialChars': 'Sonderzeichen',
|
|
622
668
|
'toolbar.fullscreen': 'Vollbild',
|
|
623
669
|
'toolbar.autosave': 'Automatisch speichern',
|
|
624
|
-
'toolbar.themeToggle': 'Design
|
|
670
|
+
'toolbar.themeToggle': 'Design ändern',
|
|
625
671
|
'toolbar.print': 'Drucken',
|
|
626
672
|
'toolbar.code': 'Code',
|
|
627
673
|
'toolbar.insert': 'Einfügen',
|
|
@@ -650,9 +696,13 @@
|
|
|
650
696
|
'menu.print': 'Drucken',
|
|
651
697
|
'menu.autosave': 'Automatisch speichern',
|
|
652
698
|
'menu.clearAll': 'Alles löschen',
|
|
653
|
-
'menu.toggleTheme': 'Design
|
|
699
|
+
'menu.toggleTheme': 'Design ändern',
|
|
654
700
|
'menu.fullscreen': 'Vollbild',
|
|
655
701
|
'menu.help': 'Hilfe',
|
|
702
|
+
'theme.light': 'Hell',
|
|
703
|
+
'theme.dark': 'Dunkel',
|
|
704
|
+
'theme.blue': 'Blau',
|
|
705
|
+
'theme.darkBlue': 'Dunkelblau',
|
|
656
706
|
'help.author': 'Autor',
|
|
657
707
|
'help.version': 'Version',
|
|
658
708
|
'help.github': 'GitHub',
|
|
@@ -710,6 +760,8 @@
|
|
|
710
760
|
'imageToolbar.replaceImage': 'Bild ersetzen',
|
|
711
761
|
'imageToolbar.deleteImage': 'Bild löschen',
|
|
712
762
|
'imageToolbar.dragToMove': 'Ziehen zum Verschieben',
|
|
763
|
+
'videoToolbar.replaceVideo': 'Video ersetzen',
|
|
764
|
+
'videoToolbar.deleteVideo': 'Video löschen',
|
|
713
765
|
|
|
714
766
|
'placeholder': 'Hier schreiben...'
|
|
715
767
|
},
|
|
@@ -779,6 +831,10 @@
|
|
|
779
831
|
'menu.toggleTheme': 'Changer de thème',
|
|
780
832
|
'menu.fullscreen': 'Plein écran',
|
|
781
833
|
'menu.help': 'Aide',
|
|
834
|
+
'theme.light': 'Clair',
|
|
835
|
+
'theme.dark': 'Sombre',
|
|
836
|
+
'theme.blue': 'Bleu',
|
|
837
|
+
'theme.darkBlue': 'Bleu foncé',
|
|
782
838
|
'help.author': 'Auteur',
|
|
783
839
|
'help.version': 'Version',
|
|
784
840
|
'help.github': 'GitHub',
|
|
@@ -836,6 +892,8 @@
|
|
|
836
892
|
'imageToolbar.replaceImage': 'Remplacer l\'image',
|
|
837
893
|
'imageToolbar.deleteImage': 'Supprimer l\'image',
|
|
838
894
|
'imageToolbar.dragToMove': 'Glisser pour déplacer',
|
|
895
|
+
'videoToolbar.replaceVideo': 'Remplacer la vidéo',
|
|
896
|
+
'videoToolbar.deleteVideo': 'Supprimer la vidéo',
|
|
839
897
|
|
|
840
898
|
'placeholder': 'Commencez à écrire...'
|
|
841
899
|
},
|
|
@@ -873,7 +931,7 @@
|
|
|
873
931
|
'toolbar.specialChars': 'Caracteres especiais',
|
|
874
932
|
'toolbar.fullscreen': 'Tela cheia',
|
|
875
933
|
'toolbar.autosave': 'Salvamento automático',
|
|
876
|
-
'toolbar.themeToggle': '
|
|
934
|
+
'toolbar.themeToggle': 'Alterar tema',
|
|
877
935
|
'toolbar.print': 'Imprimir',
|
|
878
936
|
'toolbar.code': 'Código',
|
|
879
937
|
'toolbar.insert': 'Inserir',
|
|
@@ -902,9 +960,13 @@
|
|
|
902
960
|
'menu.print': 'Imprimir',
|
|
903
961
|
'menu.autosave': 'Salvamento automático',
|
|
904
962
|
'menu.clearAll': 'Limpar tudo',
|
|
905
|
-
'menu.toggleTheme': '
|
|
963
|
+
'menu.toggleTheme': 'Alterar tema',
|
|
906
964
|
'menu.fullscreen': 'Tela cheia',
|
|
907
965
|
'menu.help': 'Ajuda',
|
|
966
|
+
'theme.light': 'Claro',
|
|
967
|
+
'theme.dark': 'Escuro',
|
|
968
|
+
'theme.blue': 'Azul',
|
|
969
|
+
'theme.darkBlue': 'Azul escuro',
|
|
908
970
|
'help.author': 'Autor',
|
|
909
971
|
'help.version': 'Versão',
|
|
910
972
|
'help.github': 'GitHub',
|
|
@@ -962,6 +1024,8 @@
|
|
|
962
1024
|
'imageToolbar.replaceImage': 'Substituir imagem',
|
|
963
1025
|
'imageToolbar.deleteImage': 'Excluir imagem',
|
|
964
1026
|
'imageToolbar.dragToMove': 'Arraste para mover',
|
|
1027
|
+
'videoToolbar.replaceVideo': 'Substituir vídeo',
|
|
1028
|
+
'videoToolbar.deleteVideo': 'Excluir vídeo',
|
|
965
1029
|
|
|
966
1030
|
'placeholder': 'Comece a digitar...'
|
|
967
1031
|
},
|
|
@@ -999,7 +1063,7 @@
|
|
|
999
1063
|
'toolbar.specialChars': '特殊文字',
|
|
1000
1064
|
'toolbar.fullscreen': '全画面',
|
|
1001
1065
|
'toolbar.autosave': '自動保存',
|
|
1002
|
-
'toolbar.themeToggle': '
|
|
1066
|
+
'toolbar.themeToggle': 'テーマを変更',
|
|
1003
1067
|
'toolbar.print': '印刷',
|
|
1004
1068
|
'toolbar.code': 'コード',
|
|
1005
1069
|
'toolbar.insert': '挿入',
|
|
@@ -1028,9 +1092,13 @@
|
|
|
1028
1092
|
'menu.print': '印刷',
|
|
1029
1093
|
'menu.autosave': '自動保存',
|
|
1030
1094
|
'menu.clearAll': 'すべて消去',
|
|
1031
|
-
'menu.toggleTheme': '
|
|
1095
|
+
'menu.toggleTheme': 'テーマを変更',
|
|
1032
1096
|
'menu.fullscreen': '全画面',
|
|
1033
1097
|
'menu.help': 'ヘルプ',
|
|
1098
|
+
'theme.light': 'ライト',
|
|
1099
|
+
'theme.dark': 'ダーク',
|
|
1100
|
+
'theme.blue': 'ブルー',
|
|
1101
|
+
'theme.darkBlue': 'ダークブルー',
|
|
1034
1102
|
'help.author': '作成者',
|
|
1035
1103
|
'help.version': 'バージョン',
|
|
1036
1104
|
'help.github': 'GitHub',
|
|
@@ -1088,6 +1156,8 @@
|
|
|
1088
1156
|
'imageToolbar.replaceImage': '画像を置換',
|
|
1089
1157
|
'imageToolbar.deleteImage': '画像を削除',
|
|
1090
1158
|
'imageToolbar.dragToMove': 'ドラッグで移動',
|
|
1159
|
+
'videoToolbar.replaceVideo': '動画を置換',
|
|
1160
|
+
'videoToolbar.deleteVideo': '動画を削除',
|
|
1091
1161
|
|
|
1092
1162
|
'placeholder': '入力してください...'
|
|
1093
1163
|
}
|
|
@@ -1117,6 +1187,14 @@
|
|
|
1117
1187
|
return text;
|
|
1118
1188
|
}
|
|
1119
1189
|
|
|
1190
|
+
const THEMES = ['light', 'dark', 'blue', 'dark-blue'];
|
|
1191
|
+
const THEME_OPTIONS = [
|
|
1192
|
+
{ value: 'light', labelKey: 'theme.light' },
|
|
1193
|
+
{ value: 'dark', labelKey: 'theme.dark' },
|
|
1194
|
+
{ value: 'blue', labelKey: 'theme.blue' },
|
|
1195
|
+
{ value: 'dark-blue', labelKey: 'theme.darkBlue' }
|
|
1196
|
+
];
|
|
1197
|
+
|
|
1120
1198
|
const DEFAULT_CONFIG = {
|
|
1121
1199
|
toolbar: [
|
|
1122
1200
|
'viewCode', 'undo', 'redo', 'findReplace', '|',
|
|
@@ -1147,6 +1225,7 @@
|
|
|
1147
1225
|
onReady: null,
|
|
1148
1226
|
showHelp: true,
|
|
1149
1227
|
imageUploadHandler: null,
|
|
1228
|
+
videoUploadHandler: null,
|
|
1150
1229
|
customClass: null
|
|
1151
1230
|
};
|
|
1152
1231
|
|
|
@@ -1172,6 +1251,7 @@
|
|
|
1172
1251
|
outdent: { icon: 'outdent', titleKey: 'toolbar.outdent', command: 'outdent' },
|
|
1173
1252
|
link: { icon: 'link', titleKey: 'toolbar.link', command: 'createLink', modal: true },
|
|
1174
1253
|
image: { icon: 'image', titleKey: 'toolbar.image', command: 'insertImage', modal: true },
|
|
1254
|
+
video: { icon: 'video', titleKey: 'toolbar.video', command: 'insertVideo', modal: true },
|
|
1175
1255
|
table: { icon: 'table', titleKey: 'toolbar.table', command: 'insertTable', modal: true },
|
|
1176
1256
|
blockquote: { icon: 'quote', titleKey: 'toolbar.blockquote', command: 'formatBlock', value: 'blockquote' },
|
|
1177
1257
|
viewCode: { icon: 'code', titleKey: 'toolbar.viewCode', command: 'viewCode' },
|
|
@@ -1185,7 +1265,7 @@
|
|
|
1185
1265
|
specialChars: { icon: 'specialChars', titleKey: 'toolbar.specialChars', command: 'specialChars', picker: 'specialChars' },
|
|
1186
1266
|
fullscreen: { icon: 'fullscreen', titleKey: 'toolbar.fullscreen', command: 'fullscreen' },
|
|
1187
1267
|
autosave: { icon: 'save', titleKey: 'toolbar.autosave', command: 'autosave', toggle: true },
|
|
1188
|
-
themeToggle: {
|
|
1268
|
+
themeToggle: { titleKey: 'toolbar.themeToggle', command: 'themeToggle', type: 'themeSelect' },
|
|
1189
1269
|
print: { icon: 'print', titleKey: 'toolbar.print', command: 'print' },
|
|
1190
1270
|
insertDropdown: { icon: 'plus', titleKey: 'toolbar.insert', type: 'insertDropdown' },
|
|
1191
1271
|
moreMenu: { icon: 'more', titleKey: 'toolbar.moreOptions', type: 'moreMenu' }
|
|
@@ -1363,9 +1443,9 @@
|
|
|
1363
1443
|
'a', 'b', 'blockquote', 'br', 'caption', 'code', 'col', 'colgroup', 'div', 'em',
|
|
1364
1444
|
'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol',
|
|
1365
1445
|
'p', 'pre', 's', 'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody',
|
|
1366
|
-
'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul'
|
|
1446
|
+
'source', 'td', 'tfoot', 'th', 'thead', 'tr', 'u', 'ul', 'video'
|
|
1367
1447
|
]);
|
|
1368
|
-
const voidTags = new Set(['br', 'col', 'hr', 'img']);
|
|
1448
|
+
const voidTags = new Set(['br', 'col', 'hr', 'img', 'source']);
|
|
1369
1449
|
const urlAttrs = new Set(['href', 'src', 'xlink:href', 'poster']);
|
|
1370
1450
|
let index = 0;
|
|
1371
1451
|
|
|
@@ -1443,7 +1523,8 @@
|
|
|
1443
1523
|
const attrValue = attr.value.trim();
|
|
1444
1524
|
|
|
1445
1525
|
if (!Utils.isSafeHTMLAttribute(attrName)) return;
|
|
1446
|
-
|
|
1526
|
+
const dataMediaType = tagName === 'img' ? 'image' : (tagName === 'video' || tagName === 'source' ? 'video' : null);
|
|
1527
|
+
if (urlAttrs.has(attrName) && !Utils.isSafeUrl(attrValue, dataMediaType)) return;
|
|
1447
1528
|
if (attrName === 'style' && !Utils.isSafeStyleValue(attrValue)) return;
|
|
1448
1529
|
|
|
1449
1530
|
el.setAttribute(attr.name, attr.value);
|
|
@@ -1584,7 +1665,7 @@
|
|
|
1584
1665
|
html += ' ' + attr.name + '="' + Utils.escapeHTML(attr.value) + '"';
|
|
1585
1666
|
});
|
|
1586
1667
|
|
|
1587
|
-
if (new Set(['br', 'col', 'hr', 'img']).has(tagName)) {
|
|
1668
|
+
if (new Set(['br', 'col', 'hr', 'img', 'source']).has(tagName)) {
|
|
1588
1669
|
html += '>';
|
|
1589
1670
|
} else {
|
|
1590
1671
|
html += '>' + Utils.serializeHTML(child) + '</' + tagName + '>';
|
|
@@ -1594,9 +1675,10 @@
|
|
|
1594
1675
|
return html;
|
|
1595
1676
|
},
|
|
1596
1677
|
|
|
1597
|
-
isSafeUrl(value,
|
|
1678
|
+
isSafeUrl(value, dataMediaType = null) {
|
|
1598
1679
|
if (!value) return true;
|
|
1599
1680
|
if (value.startsWith('#') || value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) return true;
|
|
1681
|
+
const mediaType = dataMediaType === true ? 'image' : dataMediaType;
|
|
1600
1682
|
|
|
1601
1683
|
try {
|
|
1602
1684
|
const parsed = new URL(value, window.location.href);
|
|
@@ -1605,7 +1687,8 @@
|
|
|
1605
1687
|
protocol === 'https:' ||
|
|
1606
1688
|
protocol === 'mailto:' ||
|
|
1607
1689
|
protocol === 'tel:' ||
|
|
1608
|
-
(
|
|
1690
|
+
(mediaType === 'image' && protocol === 'data:' && /^data:image\//i.test(value)) ||
|
|
1691
|
+
(mediaType === 'video' && protocol === 'data:' && /^data:video\//i.test(value));
|
|
1609
1692
|
} catch (e) {
|
|
1610
1693
|
return false;
|
|
1611
1694
|
}
|
|
@@ -1732,6 +1815,7 @@
|
|
|
1732
1815
|
outdent: '<svg viewBox="0 0 24 24"><path d="M11 17h10v-2H11v2zm-8-5l4 4V8l-4 4zm0 9h18v-2H3v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z"/></svg>',
|
|
1733
1816
|
link: '<svg viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>',
|
|
1734
1817
|
image: '<svg viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>',
|
|
1818
|
+
video: '<svg viewBox="0 0 24 24"><path d="M17 10.5V6c0-1.1-.9-2-2-2H4C2.9 4 2 4.9 2 6v12c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2v-4.5l5 5v-13l-5 5zM9 16V8l5 4-5 4z"/></svg>',
|
|
1735
1819
|
table: '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z"/></svg>',
|
|
1736
1820
|
quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
|
|
1737
1821
|
code: '<svg viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>',
|
|
@@ -1757,7 +1841,7 @@
|
|
|
1757
1841
|
trash: '<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>',
|
|
1758
1842
|
'chevron-down': '<svg viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>',
|
|
1759
1843
|
help: '<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/></svg>',
|
|
1760
|
-
grip: '<svg
|
|
1844
|
+
grip: '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path fill="none" stroke="currentColor" stroke-linecap="square" stroke-width="2" d="m15 5l-3-3l-3 3m0 14l3 3l3-3m4-4l3-3l-3-3M5 9l-3 3l3 3m7-12v9m0 0v9m0-9h9m-9 0H3"/></svg>',
|
|
1761
1845
|
moveUp: '<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>',
|
|
1762
1846
|
moveDown: '<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>',
|
|
1763
1847
|
replaceImage: '<svg viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/><path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>'
|
|
@@ -1914,7 +1998,10 @@
|
|
|
1914
1998
|
}
|
|
1915
1999
|
|
|
1916
2000
|
createOverlay() {
|
|
1917
|
-
if (this.overlay)
|
|
2001
|
+
if (this.overlay) {
|
|
2002
|
+
this.syncThemeClasses();
|
|
2003
|
+
return this.overlay;
|
|
2004
|
+
}
|
|
1918
2005
|
|
|
1919
2006
|
this.overlay = Utils.createElement('div', {
|
|
1920
2007
|
className: 'neiki-modal-overlay',
|
|
@@ -1926,9 +2013,18 @@
|
|
|
1926
2013
|
});
|
|
1927
2014
|
|
|
1928
2015
|
document.body.appendChild(this.overlay);
|
|
2016
|
+
this.syncThemeClasses();
|
|
1929
2017
|
return this.overlay;
|
|
1930
2018
|
}
|
|
1931
2019
|
|
|
2020
|
+
syncThemeClasses() {
|
|
2021
|
+
if (!this.overlay || !this.editor.getThemeClasses) return;
|
|
2022
|
+
this.overlay.classList.remove('neiki-dark', 'neiki-theme-blue', 'neiki-theme-dark-blue');
|
|
2023
|
+
this.editor.getThemeClasses(this.editor.config.theme).split(' ').filter(Boolean).forEach(className => {
|
|
2024
|
+
this.overlay.classList.add(className);
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1932
2028
|
open(type, data = {}) {
|
|
1933
2029
|
if (this.editor.saveCurrentSelection) {
|
|
1934
2030
|
this.editor.saveCurrentSelection();
|
|
@@ -1945,6 +2041,9 @@
|
|
|
1945
2041
|
case 'image':
|
|
1946
2042
|
modal = this.createImageModal(data);
|
|
1947
2043
|
break;
|
|
2044
|
+
case 'video':
|
|
2045
|
+
modal = this.createVideoModal(data);
|
|
2046
|
+
break;
|
|
1948
2047
|
case 'table':
|
|
1949
2048
|
modal = this.createTableModal(data);
|
|
1950
2049
|
break;
|
|
@@ -2263,6 +2362,179 @@
|
|
|
2263
2362
|
return modal;
|
|
2264
2363
|
}
|
|
2265
2364
|
|
|
2365
|
+
createVideoModal(data) {
|
|
2366
|
+
const modal = Utils.createElement('div', { className: 'neiki-modal' });
|
|
2367
|
+
const hasUploadHandler = typeof this.editor.config.videoUploadHandler === 'function';
|
|
2368
|
+
const uploadHint = hasUploadHandler ? t('modal.handledViaUploader') : t('modal.convertedVideoToBase64');
|
|
2369
|
+
|
|
2370
|
+
modal.innerHTML = `
|
|
2371
|
+
<div class="neiki-modal-header">
|
|
2372
|
+
<h3>${Utils.escapeHTML(t('modal.insertVideo'))}</h3>
|
|
2373
|
+
<button class="neiki-modal-close" type="button">${Icons.close}</button>
|
|
2374
|
+
</div>
|
|
2375
|
+
<div class="neiki-modal-body">
|
|
2376
|
+
<div class="neiki-form-group">
|
|
2377
|
+
<label>${Utils.escapeHTML(t('modal.uploadVideo'))}</label>
|
|
2378
|
+
<div class="neiki-image-upload-zone" role="button" tabindex="0">
|
|
2379
|
+
<input type="file" class="neiki-image-upload-input" name="upload" accept="video/*">
|
|
2380
|
+
<div class="neiki-image-upload-icon">${Icons.video}</div>
|
|
2381
|
+
<div class="neiki-image-upload-title">${Utils.escapeHTML(t('modal.uploadVideo'))}</div>
|
|
2382
|
+
<div class="neiki-image-upload-hint">${Utils.escapeHTML(uploadHint)}</div>
|
|
2383
|
+
<div class="neiki-image-upload-files" aria-live="polite"></div>
|
|
2384
|
+
</div>
|
|
2385
|
+
</div>
|
|
2386
|
+
<div class="neiki-form-divider">
|
|
2387
|
+
<span>${Utils.escapeHTML(t('modal.or'))}</span>
|
|
2388
|
+
</div>
|
|
2389
|
+
<div class="neiki-form-group">
|
|
2390
|
+
<label>${Utils.escapeHTML(t('modal.videoUrl'))}</label>
|
|
2391
|
+
<input type="url" class="neiki-input" name="url" placeholder="https://example.com/video.mp4">
|
|
2392
|
+
</div>
|
|
2393
|
+
<div class="neiki-form-group">
|
|
2394
|
+
<label>${Utils.escapeHTML(t('modal.videoTitle'))}</label>
|
|
2395
|
+
<input type="text" class="neiki-input" name="title">
|
|
2396
|
+
</div>
|
|
2397
|
+
<div class="neiki-form-group">
|
|
2398
|
+
<label>${Utils.escapeHTML(t('modal.widthOptional'))}</label>
|
|
2399
|
+
<input type="text" class="neiki-input" name="width" placeholder="e.g. 640px or 100%">
|
|
2400
|
+
</div>
|
|
2401
|
+
</div>
|
|
2402
|
+
<div class="neiki-modal-footer">
|
|
2403
|
+
<button class="neiki-btn neiki-btn-secondary" type="button" data-action="cancel">${Utils.escapeHTML(t('modal.cancel'))}</button>
|
|
2404
|
+
<button class="neiki-btn neiki-btn-primary" type="button" data-action="insert">${Utils.escapeHTML(t('modal.insert'))}</button>
|
|
2405
|
+
</div>
|
|
2406
|
+
`;
|
|
2407
|
+
|
|
2408
|
+
const uploadInput = modal.querySelector('[name="upload"]');
|
|
2409
|
+
const uploadZone = modal.querySelector('.neiki-image-upload-zone');
|
|
2410
|
+
const uploadFiles = modal.querySelector('.neiki-image-upload-files');
|
|
2411
|
+
const urlInput = modal.querySelector('[name="url"]');
|
|
2412
|
+
const insertBtn = modal.querySelector('[data-action="insert"]');
|
|
2413
|
+
let pendingFile = null;
|
|
2414
|
+
let uploadDragCounter = 0;
|
|
2415
|
+
|
|
2416
|
+
urlInput.value = data.url || '';
|
|
2417
|
+
modal.querySelector('[name="title"]').placeholder = t('modal.describeVideo');
|
|
2418
|
+
modal.querySelector('[name="title"]').value = data.title || '';
|
|
2419
|
+
modal.querySelector('[name="width"]').value = data.width || '';
|
|
2420
|
+
|
|
2421
|
+
const updateUploadFeedback = (file) => {
|
|
2422
|
+
uploadZone.classList.toggle('has-files', !!file);
|
|
2423
|
+
uploadFiles.textContent = file ? file.name : '';
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
const handleSelectedFiles = (fileList) => {
|
|
2427
|
+
const file = Array.from(fileList || []).find(f => f.type.startsWith('video/'));
|
|
2428
|
+
|
|
2429
|
+
if (!file) {
|
|
2430
|
+
if (fileList && fileList.length > 0) alert(t('modal.invalidVideoFile'));
|
|
2431
|
+
pendingFile = null;
|
|
2432
|
+
updateUploadFeedback(null);
|
|
2433
|
+
urlInput.disabled = false;
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
pendingFile = file;
|
|
2438
|
+
updateUploadFeedback(file);
|
|
2439
|
+
|
|
2440
|
+
if (!hasUploadHandler) {
|
|
2441
|
+
const reader = new FileReader();
|
|
2442
|
+
reader.onload = (ev) => {
|
|
2443
|
+
urlInput.value = ev.target.result;
|
|
2444
|
+
urlInput.disabled = true;
|
|
2445
|
+
};
|
|
2446
|
+
reader.readAsDataURL(file);
|
|
2447
|
+
} else {
|
|
2448
|
+
urlInput.value = '';
|
|
2449
|
+
urlInput.disabled = true;
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
uploadInput.addEventListener('change', (e) => {
|
|
2454
|
+
handleSelectedFiles(e.target.files);
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
uploadZone.addEventListener('click', (e) => {
|
|
2458
|
+
if (e.target !== uploadInput) uploadInput.click();
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
uploadZone.addEventListener('keydown', (e) => {
|
|
2462
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
2463
|
+
e.preventDefault();
|
|
2464
|
+
uploadInput.click();
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
uploadZone.addEventListener('dragenter', (e) => {
|
|
2469
|
+
e.preventDefault();
|
|
2470
|
+
uploadDragCounter++;
|
|
2471
|
+
uploadZone.classList.add('drag-over');
|
|
2472
|
+
});
|
|
2473
|
+
|
|
2474
|
+
uploadZone.addEventListener('dragover', (e) => {
|
|
2475
|
+
e.preventDefault();
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
uploadZone.addEventListener('dragleave', (e) => {
|
|
2479
|
+
e.preventDefault();
|
|
2480
|
+
uploadDragCounter--;
|
|
2481
|
+
if (uploadDragCounter <= 0) {
|
|
2482
|
+
uploadDragCounter = 0;
|
|
2483
|
+
uploadZone.classList.remove('drag-over');
|
|
2484
|
+
}
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
2488
|
+
e.preventDefault();
|
|
2489
|
+
uploadDragCounter = 0;
|
|
2490
|
+
uploadZone.classList.remove('drag-over');
|
|
2491
|
+
handleSelectedFiles(e.dataTransfer.files);
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
urlInput.addEventListener('input', () => {
|
|
2495
|
+
if (!urlInput.value) {
|
|
2496
|
+
urlInput.disabled = false;
|
|
2497
|
+
uploadInput.value = '';
|
|
2498
|
+
pendingFile = null;
|
|
2499
|
+
updateUploadFeedback(null);
|
|
2500
|
+
}
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2503
|
+
modal.querySelector('.neiki-modal-close').addEventListener('click', () => this.close());
|
|
2504
|
+
modal.querySelector('[data-action="cancel"]').addEventListener('click', () => this.close());
|
|
2505
|
+
modal.querySelector('[data-action="insert"]').addEventListener('click', async () => {
|
|
2506
|
+
const title = modal.querySelector('[name="title"]').value;
|
|
2507
|
+
const width = modal.querySelector('[name="width"]').value;
|
|
2508
|
+
|
|
2509
|
+
if (pendingFile && hasUploadHandler) {
|
|
2510
|
+
insertBtn.disabled = true;
|
|
2511
|
+
insertBtn.textContent = t('modal.uploadingVideo');
|
|
2512
|
+
|
|
2513
|
+
try {
|
|
2514
|
+
const url = await this.editor.config.videoUploadHandler(pendingFile);
|
|
2515
|
+
if (url) {
|
|
2516
|
+
this.editor.restoreSavedSelection();
|
|
2517
|
+
this.editor.commands.insertVideo(url, title || pendingFile.name, width);
|
|
2518
|
+
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
alert(t('modal.videoUploadError'));
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
this.close();
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const url = modal.querySelector('[name="url"]').value;
|
|
2528
|
+
if (url) {
|
|
2529
|
+
this.editor.restoreSavedSelection();
|
|
2530
|
+
this.editor.commands.insertVideo(url, title, width);
|
|
2531
|
+
}
|
|
2532
|
+
this.close();
|
|
2533
|
+
});
|
|
2534
|
+
|
|
2535
|
+
return modal;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2266
2538
|
createTableModal(data) {
|
|
2267
2539
|
const modal = Utils.createElement('div', { className: 'neiki-modal' });
|
|
2268
2540
|
|
|
@@ -2529,7 +2801,7 @@
|
|
|
2529
2801
|
<img src="https://github.com/neikiri/neiki-editor/raw/main/logo.png" alt="Neiki's Editor" style="width: 120px; height: auto; margin: 0 auto 16px; display: block;">
|
|
2530
2802
|
<div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
|
|
2531
2803
|
<div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
2532
|
-
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong>
|
|
2804
|
+
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.0.1</div>
|
|
2533
2805
|
<div><strong>${Utils.escapeHTML(t('help.github'))}:</strong> <a href="https://github.com/neikiri/neiki-editor" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">github.com/neikiri/neiki-editor</a></div>
|
|
2534
2806
|
<div><strong>${Utils.escapeHTML(t('help.documentation'))}:</strong> <a href="https://github.com/neikiri/neiki-editor/wiki" target="_blank" rel="noopener noreferrer" style="color: var(--neiki-accent);">Wiki</a></div>
|
|
2535
2807
|
</div>
|
|
@@ -3293,6 +3565,19 @@
|
|
|
3293
3565
|
this.editor.triggerChange();
|
|
3294
3566
|
}
|
|
3295
3567
|
|
|
3568
|
+
insertVideo(url, title = '', width = '') {
|
|
3569
|
+
if (!Utils.isSafeUrl(url, 'video')) return;
|
|
3570
|
+
let html = `<video controls src="${Utils.escapeHTML(url)}"`;
|
|
3571
|
+
if (title) html += ` title="${Utils.escapeHTML(title)}"`;
|
|
3572
|
+
if (width) html += ` width="${Utils.escapeHTML(width)}"`;
|
|
3573
|
+
html += '></video><p><br></p>';
|
|
3574
|
+
|
|
3575
|
+
this.editor.focus();
|
|
3576
|
+
document.execCommand('insertHTML', false, html);
|
|
3577
|
+
this.editor.history.record();
|
|
3578
|
+
this.editor.triggerChange();
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3296
3581
|
insertTable(rows, cols, hasHeader = true) {
|
|
3297
3582
|
let html = '<table class="neiki-table">';
|
|
3298
3583
|
|
|
@@ -3382,10 +3667,11 @@
|
|
|
3382
3667
|
|
|
3383
3668
|
// Load theme preference
|
|
3384
3669
|
const savedTheme = StorageManager.getGlobal('theme', this.config.theme);
|
|
3385
|
-
this.config.theme = savedTheme;
|
|
3670
|
+
this.config.theme = THEMES.includes(savedTheme) ? savedTheme : 'light';
|
|
3386
3671
|
|
|
3387
3672
|
this.createStructure();
|
|
3388
3673
|
this.createToolbar();
|
|
3674
|
+
this.applyTheme(this.config.theme);
|
|
3389
3675
|
this.createContentArea();
|
|
3390
3676
|
this.createStatusBar();
|
|
3391
3677
|
|
|
@@ -3404,6 +3690,7 @@
|
|
|
3404
3690
|
|
|
3405
3691
|
this.bindEvents();
|
|
3406
3692
|
this.initDragDrop();
|
|
3693
|
+
this.initSelectionDragDrop();
|
|
3407
3694
|
this.initPlugins();
|
|
3408
3695
|
|
|
3409
3696
|
// Sync restored content to original element
|
|
@@ -3433,7 +3720,7 @@
|
|
|
3433
3720
|
createStructure() {
|
|
3434
3721
|
const langClass = _currentLanguage !== 'en' ? `neiki-lang-${_currentLanguage}` : '';
|
|
3435
3722
|
this.container = Utils.createElement('div', {
|
|
3436
|
-
className: `neiki-editor ${this.config.theme
|
|
3723
|
+
className: `neiki-editor ${this.getThemeClasses(this.config.theme)} ${langClass}`.trim(),
|
|
3437
3724
|
id: this.id
|
|
3438
3725
|
});
|
|
3439
3726
|
|
|
@@ -3441,6 +3728,37 @@
|
|
|
3441
3728
|
this.originalElement.parentNode.insertBefore(this.container, this.originalElement);
|
|
3442
3729
|
}
|
|
3443
3730
|
|
|
3731
|
+
getThemeClasses(theme) {
|
|
3732
|
+
const normalizedTheme = THEMES.includes(theme) ? theme : 'light';
|
|
3733
|
+
const classes = [];
|
|
3734
|
+
|
|
3735
|
+
if (normalizedTheme === 'dark' || normalizedTheme === 'dark-blue') {
|
|
3736
|
+
classes.push('neiki-dark');
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
if (normalizedTheme !== 'light' && normalizedTheme !== 'dark') {
|
|
3740
|
+
classes.push('neiki-theme-' + normalizedTheme);
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
return classes.join(' ');
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
applyTheme(theme) {
|
|
3747
|
+
const normalizedTheme = THEMES.includes(theme) ? theme : 'light';
|
|
3748
|
+
this.config.theme = normalizedTheme;
|
|
3749
|
+
this.container.classList.remove('neiki-dark', 'neiki-theme-blue', 'neiki-theme-dark-blue');
|
|
3750
|
+
this.getThemeClasses(normalizedTheme).split(' ').filter(Boolean).forEach(className => {
|
|
3751
|
+
this.container.classList.add(className);
|
|
3752
|
+
});
|
|
3753
|
+
StorageManager.setGlobal('theme', normalizedTheme);
|
|
3754
|
+
|
|
3755
|
+
if (this._themeSelect) {
|
|
3756
|
+
this._themeSelect.value = normalizedTheme;
|
|
3757
|
+
}
|
|
3758
|
+
if (this.modal) this.modal.syncThemeClasses();
|
|
3759
|
+
this._updateThemeMenuItem();
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3444
3762
|
createAutosaveStorageId(element) {
|
|
3445
3763
|
const customKey = this.config.autosaveKey ||
|
|
3446
3764
|
this.originalElement.getAttribute('data-neiki-autosave-key');
|
|
@@ -3756,6 +4074,39 @@
|
|
|
3756
4074
|
return;
|
|
3757
4075
|
}
|
|
3758
4076
|
|
|
4077
|
+
// Handle theme select
|
|
4078
|
+
if (config.type === 'themeSelect') {
|
|
4079
|
+
const wrapper = Utils.createElement('label', {
|
|
4080
|
+
className: 'neiki-theme-select-wrapper',
|
|
4081
|
+
title: t(config.titleKey)
|
|
4082
|
+
});
|
|
4083
|
+
wrapper.appendChild(Utils.createElement('span', {
|
|
4084
|
+
className: 'neiki-theme-select-icon',
|
|
4085
|
+
innerHTML: Icons.sun
|
|
4086
|
+
}));
|
|
4087
|
+
|
|
4088
|
+
const select = Utils.createElement('select', {
|
|
4089
|
+
className: 'neiki-select neiki-theme-select',
|
|
4090
|
+
'aria-label': t(config.titleKey)
|
|
4091
|
+
});
|
|
4092
|
+
|
|
4093
|
+
THEME_OPTIONS.forEach(option => {
|
|
4094
|
+
select.appendChild(Utils.createElement('option', {
|
|
4095
|
+
value: option.value,
|
|
4096
|
+
textContent: t(option.labelKey)
|
|
4097
|
+
}));
|
|
4098
|
+
});
|
|
4099
|
+
|
|
4100
|
+
select.value = this.config.theme;
|
|
4101
|
+
select.addEventListener('change', () => this.setTheme(select.value));
|
|
4102
|
+
wrapper.appendChild(select);
|
|
4103
|
+
|
|
4104
|
+
this.toolbarButtons[item] = select;
|
|
4105
|
+
this._themeSelect = select;
|
|
4106
|
+
appendToGroup(wrapper);
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
3759
4110
|
// Handle Insert dropdown
|
|
3760
4111
|
if (config.type === 'insertDropdown') {
|
|
3761
4112
|
const btn = Utils.createElement('button', {
|
|
@@ -3776,6 +4127,10 @@
|
|
|
3776
4127
|
key: 'image', icon: Icons.image, labelKey: 'insert.image',
|
|
3777
4128
|
action: () => { this.saveCurrentSelection(); this.modal.open('image', {}); }
|
|
3778
4129
|
},
|
|
4130
|
+
{
|
|
4131
|
+
key: 'video', icon: Icons.video, labelKey: 'insert.video',
|
|
4132
|
+
action: () => { this.saveCurrentSelection(); this.modal.open('video', {}); }
|
|
4133
|
+
},
|
|
3779
4134
|
{
|
|
3780
4135
|
key: 'table', icon: Icons.table, labelKey: 'insert.table',
|
|
3781
4136
|
action: () => { this.saveCurrentSelection(); this.modal.open('table', {}); }
|
|
@@ -3852,7 +4207,7 @@
|
|
|
3852
4207
|
{ key: 'autosave', icon: Icons.save, labelKey: 'menu.autosave', action: () => this.toggleAutosave(), toggle: true },
|
|
3853
4208
|
{ key: 'divider' },
|
|
3854
4209
|
{ key: 'clearAll', icon: Icons.trash, labelKey: 'menu.clearAll', action: () => this.clearAll(), danger: true },
|
|
3855
|
-
{ key: '
|
|
4210
|
+
{ key: 'themeSelect', icon: Icons.sun, labelKey: 'menu.toggleTheme' },
|
|
3856
4211
|
{ key: 'fullscreen', icon: Icons.fullscreen, labelKey: 'menu.fullscreen', action: () => this.toggleFullscreen() }
|
|
3857
4212
|
];
|
|
3858
4213
|
|
|
@@ -3866,6 +4221,35 @@
|
|
|
3866
4221
|
dropdown.appendChild(Utils.createElement('div', { className: 'neiki-dropdown-divider' }));
|
|
3867
4222
|
return;
|
|
3868
4223
|
}
|
|
4224
|
+
|
|
4225
|
+
if (key === 'themeSelect') {
|
|
4226
|
+
const menuItem = Utils.createElement('label', {
|
|
4227
|
+
className: 'neiki-dropdown-item neiki-theme-menu-item'
|
|
4228
|
+
});
|
|
4229
|
+
menuItem.innerHTML = '<span class="neiki-dropdown-item-icon">' + icon + '</span>';
|
|
4230
|
+
|
|
4231
|
+
const select = Utils.createElement('select', {
|
|
4232
|
+
className: 'neiki-theme-menu-select',
|
|
4233
|
+
'aria-label': t(labelKey)
|
|
4234
|
+
});
|
|
4235
|
+
THEME_OPTIONS.forEach(option => {
|
|
4236
|
+
select.appendChild(Utils.createElement('option', {
|
|
4237
|
+
value: option.value,
|
|
4238
|
+
textContent: t(option.labelKey)
|
|
4239
|
+
}));
|
|
4240
|
+
});
|
|
4241
|
+
select.value = this.config.theme;
|
|
4242
|
+
select.addEventListener('click', (e) => e.stopPropagation());
|
|
4243
|
+
select.addEventListener('change', () => {
|
|
4244
|
+
this.setTheme(select.value);
|
|
4245
|
+
});
|
|
4246
|
+
menuItem.appendChild(select);
|
|
4247
|
+
this._themeMenuItem = menuItem;
|
|
4248
|
+
this._themeMenuSelect = select;
|
|
4249
|
+
dropdown.appendChild(menuItem);
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
3869
4253
|
const menuItem = Utils.createElement('div', {
|
|
3870
4254
|
className: 'neiki-dropdown-item' + (danger ? ' neiki-dropdown-item-danger' : '')
|
|
3871
4255
|
});
|
|
@@ -3879,11 +4263,6 @@
|
|
|
3879
4263
|
this._autosaveBadge = badge;
|
|
3880
4264
|
}
|
|
3881
4265
|
|
|
3882
|
-
if (key === 'themeToggle') {
|
|
3883
|
-
this._themeMenuItem = menuItem;
|
|
3884
|
-
this._themeMenuIcon = menuItem.querySelector('.neiki-dropdown-item-icon');
|
|
3885
|
-
}
|
|
3886
|
-
|
|
3887
4266
|
menuItem.addEventListener('click', (e) => {
|
|
3888
4267
|
e.preventDefault();
|
|
3889
4268
|
e.stopPropagation();
|
|
@@ -4007,8 +4386,20 @@
|
|
|
4007
4386
|
className: 'neiki-code-view-textarea',
|
|
4008
4387
|
spellcheck: 'false'
|
|
4009
4388
|
});
|
|
4389
|
+
this.codeViewHighlight = Utils.createElement('pre', {
|
|
4390
|
+
className: 'neiki-code-view-highlight',
|
|
4391
|
+
'aria-hidden': 'true'
|
|
4392
|
+
});
|
|
4393
|
+
this.codeViewEditor = Utils.createElement('div', { className: 'neiki-code-view-editor' });
|
|
4394
|
+
this.codeViewEditor.appendChild(this.codeViewHighlight);
|
|
4395
|
+
this.codeViewEditor.appendChild(this.codeViewTextarea);
|
|
4396
|
+
this.codeViewTextarea.addEventListener('input', () => this.renderCodeViewHighlight());
|
|
4397
|
+
this.codeViewTextarea.addEventListener('scroll', () => {
|
|
4398
|
+
this.codeViewHighlight.scrollTop = this.codeViewTextarea.scrollTop;
|
|
4399
|
+
this.codeViewHighlight.scrollLeft = this.codeViewTextarea.scrollLeft;
|
|
4400
|
+
});
|
|
4010
4401
|
this.codeView.appendChild(codeViewHeader);
|
|
4011
|
-
this.codeView.appendChild(this.
|
|
4402
|
+
this.codeView.appendChild(this.codeViewEditor);
|
|
4012
4403
|
this.contentWrapper.appendChild(this.codeView);
|
|
4013
4404
|
|
|
4014
4405
|
this.container.appendChild(this.contentWrapper);
|
|
@@ -4017,6 +4408,7 @@
|
|
|
4017
4408
|
bindEvents() {
|
|
4018
4409
|
// Content changes
|
|
4019
4410
|
this.contentArea.addEventListener('input', Utils.debounce(() => {
|
|
4411
|
+
this.removeStrayGripSvgs();
|
|
4020
4412
|
this._ensureDefaultBlock();
|
|
4021
4413
|
this.history.record();
|
|
4022
4414
|
this.syncToOriginal();
|
|
@@ -4328,6 +4720,9 @@
|
|
|
4328
4720
|
}
|
|
4329
4721
|
}
|
|
4330
4722
|
break;
|
|
4723
|
+
case 'themeToggle':
|
|
4724
|
+
isActive = this.config.theme === 'dark' || this.config.theme === 'dark-blue';
|
|
4725
|
+
break;
|
|
4331
4726
|
}
|
|
4332
4727
|
} catch (e) {
|
|
4333
4728
|
// queryCommandState can throw in some browsers
|
|
@@ -4398,28 +4793,14 @@
|
|
|
4398
4793
|
}
|
|
4399
4794
|
|
|
4400
4795
|
toggleTheme() {
|
|
4401
|
-
const
|
|
4402
|
-
const
|
|
4403
|
-
|
|
4404
|
-
this.container.classList.toggle('neiki-dark', !isDark);
|
|
4405
|
-
this.config.theme = newTheme;
|
|
4406
|
-
|
|
4407
|
-
// Persist theme choice
|
|
4408
|
-
StorageManager.setGlobal('theme', newTheme);
|
|
4409
|
-
|
|
4410
|
-
// Update button icon and active state
|
|
4411
|
-
if (this.toolbarButtons.themeToggle) {
|
|
4412
|
-
this.toolbarButtons.themeToggle.innerHTML = isDark ? Icons.sun : Icons.moon;
|
|
4413
|
-
this.toolbarButtons.themeToggle.classList.toggle('active', !isDark);
|
|
4414
|
-
this.toolbarButtons.themeToggle.title = isDark ? 'Switch to Dark Mode' : 'Switch to Light Mode';
|
|
4415
|
-
}
|
|
4416
|
-
this._updateThemeMenuItem();
|
|
4796
|
+
const currentIndex = THEMES.indexOf(this.config.theme);
|
|
4797
|
+
const nextTheme = THEMES[(currentIndex + 1) % THEMES.length];
|
|
4798
|
+
this.applyTheme(nextTheme);
|
|
4417
4799
|
}
|
|
4418
4800
|
|
|
4419
4801
|
_updateThemeMenuItem() {
|
|
4420
|
-
if (this.
|
|
4421
|
-
|
|
4422
|
-
this._themeMenuIcon.innerHTML = isDark ? Icons.moon : Icons.sun;
|
|
4802
|
+
if (this._themeMenuSelect) {
|
|
4803
|
+
this._themeMenuSelect.value = this.config.theme;
|
|
4423
4804
|
}
|
|
4424
4805
|
}
|
|
4425
4806
|
|
|
@@ -4584,15 +4965,30 @@
|
|
|
4584
4965
|
const clone = this.contentArea.cloneNode(true);
|
|
4585
4966
|
// Unwrap image resizer wrappers
|
|
4586
4967
|
clone.querySelectorAll('.neiki-img-resizable').forEach(wrapper => {
|
|
4587
|
-
const
|
|
4588
|
-
if (
|
|
4968
|
+
const media = wrapper.querySelector('img, video');
|
|
4969
|
+
if (media) wrapper.parentNode.insertBefore(media, wrapper);
|
|
4589
4970
|
wrapper.remove();
|
|
4590
4971
|
});
|
|
4591
4972
|
// Remove grip handles, placeholders, resize handles
|
|
4592
4973
|
clone.querySelectorAll('.neiki-block-grip, .neiki-block-placeholder, .neiki-table-col-resize-handle, .neiki-img-resize-handle, .neiki-img-size-label, .neiki-img-toolbar').forEach(el => el.remove());
|
|
4974
|
+
this.removeStrayGripSvgs(clone);
|
|
4593
4975
|
return clone.innerHTML;
|
|
4594
4976
|
}
|
|
4595
4977
|
|
|
4978
|
+
removeStrayGripSvgs(root = this.contentArea) {
|
|
4979
|
+
root.querySelectorAll('svg[viewBox="0 0 24 24"]').forEach(svg => {
|
|
4980
|
+
if (svg.closest('.neiki-toolbar, .neiki-floating-toolbar, .neiki-img-toolbar, .neiki-block-grip, button')) return;
|
|
4981
|
+
const circles = Array.from(svg.children);
|
|
4982
|
+
const isGrip = circles.length === 8 && circles.every(child =>
|
|
4983
|
+
child.tagName && child.tagName.toLowerCase() === 'circle' &&
|
|
4984
|
+
child.getAttribute('r') === '1.5' &&
|
|
4985
|
+
['9', '15'].includes(child.getAttribute('cx')) &&
|
|
4986
|
+
['5', '10', '15', '20'].includes(child.getAttribute('cy'))
|
|
4987
|
+
);
|
|
4988
|
+
if (isGrip) svg.remove();
|
|
4989
|
+
});
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4596
4992
|
setContent(html) {
|
|
4597
4993
|
if (this.imageResizer) this.imageResizer.deselect();
|
|
4598
4994
|
this.savedSelectionRange = null;
|
|
@@ -4668,9 +5064,7 @@
|
|
|
4668
5064
|
}
|
|
4669
5065
|
|
|
4670
5066
|
setTheme(theme) {
|
|
4671
|
-
this.
|
|
4672
|
-
this.container.classList.toggle('neiki-dark', theme === 'dark');
|
|
4673
|
-
StorageManager.setGlobal('theme', theme);
|
|
5067
|
+
this.applyTheme(theme);
|
|
4674
5068
|
}
|
|
4675
5069
|
|
|
4676
5070
|
createStatusBar() {
|
|
@@ -4727,9 +5121,117 @@
|
|
|
4727
5121
|
return 'p';
|
|
4728
5122
|
}
|
|
4729
5123
|
|
|
5124
|
+
formatHTMLSource(html) {
|
|
5125
|
+
const input = String(html || '').trim();
|
|
5126
|
+
if (!input) return '';
|
|
5127
|
+
|
|
5128
|
+
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
5129
|
+
const blockTags = new Set(['article', 'aside', 'blockquote', 'caption', 'colgroup', 'div', 'figure', 'figcaption', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'li', 'main', 'ol', 'p', 'pre', 'section', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'ul', 'video']);
|
|
5130
|
+
const tokens = [];
|
|
5131
|
+
let index = 0;
|
|
5132
|
+
|
|
5133
|
+
while (index < input.length) {
|
|
5134
|
+
const tagStart = input.indexOf('<', index);
|
|
5135
|
+
|
|
5136
|
+
if (tagStart === -1) {
|
|
5137
|
+
tokens.push(input.slice(index));
|
|
5138
|
+
break;
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
if (tagStart > index) {
|
|
5142
|
+
tokens.push(input.slice(index, tagStart));
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
if (input.slice(tagStart, tagStart + 4) === '<!--') {
|
|
5146
|
+
const commentEnd = input.indexOf('-->', tagStart + 4);
|
|
5147
|
+
const end = commentEnd === -1 ? input.length : commentEnd + 3;
|
|
5148
|
+
tokens.push(input.slice(tagStart, end));
|
|
5149
|
+
index = end;
|
|
5150
|
+
continue;
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
const tagEnd = Utils.findTagEnd(input, tagStart + 1);
|
|
5154
|
+
if (tagEnd === -1) {
|
|
5155
|
+
tokens.push(input.slice(tagStart));
|
|
5156
|
+
break;
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
tokens.push(input.slice(tagStart, tagEnd + 1));
|
|
5160
|
+
index = tagEnd + 1;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
const lines = [];
|
|
5164
|
+
let indent = 0;
|
|
5165
|
+
let inlineDepth = 0;
|
|
5166
|
+
let hasInlineContent = false;
|
|
5167
|
+
|
|
5168
|
+
tokens.forEach(token => {
|
|
5169
|
+
const trimmed = token.trim();
|
|
5170
|
+
if (!trimmed) return;
|
|
5171
|
+
const textPrefix = !/^</.test(trimmed) && /^\s/.test(token) ? ' ' : '';
|
|
5172
|
+
|
|
5173
|
+
const tagMatch = trimmed.match(/^<\/?\s*([a-zA-Z0-9:-]+)/);
|
|
5174
|
+
const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';
|
|
5175
|
+
const isClosing = /^<\//.test(trimmed);
|
|
5176
|
+
const isTag = /^</.test(trimmed);
|
|
5177
|
+
const isSelfClosing = /\/>$/.test(trimmed) || voidTags.has(tagName);
|
|
5178
|
+
const isBlock = blockTags.has(tagName);
|
|
5179
|
+
|
|
5180
|
+
if (isTag && isClosing) {
|
|
5181
|
+
if (!isBlock && lines.length > 0) {
|
|
5182
|
+
lines[lines.length - 1] += trimmed;
|
|
5183
|
+
inlineDepth = Math.max(inlineDepth - 1, 0);
|
|
5184
|
+
return;
|
|
5185
|
+
}
|
|
5186
|
+
indent = Math.max(indent - 1, 0);
|
|
5187
|
+
}
|
|
5188
|
+
|
|
5189
|
+
const prefix = ' '.repeat(indent);
|
|
5190
|
+
if (!isTag && (inlineDepth > 0 || hasInlineContent) && lines.length > 0) {
|
|
5191
|
+
lines[lines.length - 1] += (textPrefix || (lines[lines.length - 1].endsWith('>') ? '' : ' ')) + trimmed;
|
|
5192
|
+
} else if (!isTag || isBlock || lines.length === 0 || isClosing) {
|
|
5193
|
+
lines.push(prefix + trimmed);
|
|
5194
|
+
} else {
|
|
5195
|
+
lines[lines.length - 1] += trimmed;
|
|
5196
|
+
}
|
|
5197
|
+
|
|
5198
|
+
if (isTag && !isClosing && !isSelfClosing && isBlock) {
|
|
5199
|
+
indent++;
|
|
5200
|
+
hasInlineContent = false;
|
|
5201
|
+
} else if (isTag && !isClosing && !isSelfClosing && !isBlock) {
|
|
5202
|
+
inlineDepth++;
|
|
5203
|
+
hasInlineContent = true;
|
|
5204
|
+
} else if (!isTag) {
|
|
5205
|
+
hasInlineContent = true;
|
|
5206
|
+
}
|
|
5207
|
+
});
|
|
5208
|
+
|
|
5209
|
+
return lines.join('\n');
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
renderCodeViewHighlight() {
|
|
5213
|
+
if (!this.codeViewHighlight || !this.codeViewTextarea) return;
|
|
5214
|
+
const source = this.codeViewTextarea.value;
|
|
5215
|
+
const html = Utils.escapeHTML(source).replace(/(<!--[\s\S]*?-->)|(<\/?[\s\S]*?>)/g, (match, comment) => {
|
|
5216
|
+
if (comment) return `<span class="neiki-html-comment">${match}</span>`;
|
|
5217
|
+
|
|
5218
|
+
const tagParts = match.match(/^(<\/?)([a-zA-Z0-9:-]+)([\s\S]*?)(\/?>)$/);
|
|
5219
|
+
if (!tagParts) return match;
|
|
5220
|
+
|
|
5221
|
+
const attrs = tagParts[3].replace(/(\s+)([a-zA-Z_:][-a-zA-Z0-9_:.]*)(=)(".*?"|'.*?'|[^\s&]+)?/g, (attrMatch, space, name, eq, value) => {
|
|
5222
|
+
return `${space}<span class="neiki-html-attr">${name}</span>${eq}<span class="neiki-html-string">${value || ''}</span>`;
|
|
5223
|
+
});
|
|
5224
|
+
|
|
5225
|
+
return `<span class="neiki-html-punct">${tagParts[1]}</span><span class="neiki-html-tag">${tagParts[2]}</span>${attrs}<span class="neiki-html-punct">${tagParts[4]}</span>`;
|
|
5226
|
+
});
|
|
5227
|
+
|
|
5228
|
+
this.codeViewHighlight.innerHTML = html + (source.endsWith('\n') ? ' ' : '');
|
|
5229
|
+
}
|
|
5230
|
+
|
|
4730
5231
|
toggleCodeView() {
|
|
4731
5232
|
if (!this.isCodeViewOpen) {
|
|
4732
|
-
this.codeViewTextarea.value = this.contentArea.innerHTML;
|
|
5233
|
+
this.codeViewTextarea.value = this.formatHTMLSource(this.contentArea.innerHTML);
|
|
5234
|
+
this.renderCodeViewHighlight();
|
|
4733
5235
|
this.codeView.classList.add('show');
|
|
4734
5236
|
this.isCodeViewOpen = true;
|
|
4735
5237
|
this.codeViewTextarea.focus();
|
|
@@ -5045,6 +5547,131 @@
|
|
|
5045
5547
|
// DRAG & DROP
|
|
5046
5548
|
// ============================================
|
|
5047
5549
|
|
|
5550
|
+
getCaretRangeFromPoint(x, y) {
|
|
5551
|
+
if (document.caretRangeFromPoint) {
|
|
5552
|
+
return document.caretRangeFromPoint(x, y);
|
|
5553
|
+
}
|
|
5554
|
+
if (document.caretPositionFromPoint) {
|
|
5555
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
5556
|
+
if (pos) {
|
|
5557
|
+
const range = document.createRange();
|
|
5558
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
5559
|
+
range.collapse(true);
|
|
5560
|
+
return range;
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
return null;
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
showDropIndicator(range, x, y) {
|
|
5567
|
+
if (!this.dropIndicator) {
|
|
5568
|
+
this.dropIndicator = document.createElement('div');
|
|
5569
|
+
this.dropIndicator.className = 'neiki-drop-indicator';
|
|
5570
|
+
this.dropIndicator.setAttribute('aria-hidden', 'true');
|
|
5571
|
+
this.contentWrapper.appendChild(this.dropIndicator);
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
const wrapperRect = this.contentWrapper.getBoundingClientRect();
|
|
5575
|
+
let rect = null;
|
|
5576
|
+
|
|
5577
|
+
if (range) {
|
|
5578
|
+
const rects = range.getClientRects();
|
|
5579
|
+
rect = rects.length ? rects[0] : range.getBoundingClientRect();
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
const left = rect && rect.left ? rect.left - wrapperRect.left : x - wrapperRect.left;
|
|
5583
|
+
const top = rect && rect.top ? rect.top - wrapperRect.top : y - wrapperRect.top;
|
|
5584
|
+
const height = rect && rect.height ? Math.max(rect.height, 18) : 22;
|
|
5585
|
+
|
|
5586
|
+
this.dropIndicator.style.left = Math.max(6, Math.min(left, wrapperRect.width - 6)) + 'px';
|
|
5587
|
+
this.dropIndicator.style.top = Math.max(6, Math.min(top, wrapperRect.height - 6)) + 'px';
|
|
5588
|
+
this.dropIndicator.style.height = height + 'px';
|
|
5589
|
+
this.dropIndicator.classList.add('show');
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
hideDropIndicator() {
|
|
5593
|
+
if (this.dropIndicator) {
|
|
5594
|
+
this.dropIndicator.classList.remove('show');
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5598
|
+
initSelectionDragDrop() {
|
|
5599
|
+
this.draggedSelectionRange = null;
|
|
5600
|
+
|
|
5601
|
+
this.contentArea.addEventListener('dragstart', (e) => {
|
|
5602
|
+
const sel = window.getSelection();
|
|
5603
|
+
if (!sel || !sel.rangeCount || sel.isCollapsed) return;
|
|
5604
|
+
if (!this.contentArea.contains(sel.anchorNode) || !this.contentArea.contains(sel.focusNode)) return;
|
|
5605
|
+
|
|
5606
|
+
const range = sel.getRangeAt(0);
|
|
5607
|
+
const targetNode = e.target.nodeType === Node.TEXT_NODE ? e.target.parentNode : e.target;
|
|
5608
|
+
try {
|
|
5609
|
+
if (targetNode && !range.intersectsNode(targetNode)) return;
|
|
5610
|
+
} catch (err) {
|
|
5611
|
+
return;
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
const container = this.cleanDraggedFragment(range.cloneContents());
|
|
5615
|
+
|
|
5616
|
+
this.draggedSelectionRange = range.cloneRange();
|
|
5617
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
5618
|
+
e.dataTransfer.setData('application/x-neiki-selection', '1');
|
|
5619
|
+
e.dataTransfer.setData('text/html', container.innerHTML);
|
|
5620
|
+
e.dataTransfer.setData('text/plain', sel.toString());
|
|
5621
|
+
});
|
|
5622
|
+
|
|
5623
|
+
this.contentArea.addEventListener('dragend', () => {
|
|
5624
|
+
this.draggedSelectionRange = null;
|
|
5625
|
+
this.hideDropIndicator();
|
|
5626
|
+
});
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
cleanDraggedFragment(fragment) {
|
|
5630
|
+
const container = document.createElement('div');
|
|
5631
|
+
container.appendChild(fragment);
|
|
5632
|
+
container.querySelectorAll('.neiki-block-grip, .neiki-block-placeholder, .neiki-table-col-resize-handle, .neiki-img-resize-handle, .neiki-img-size-label, .neiki-img-toolbar').forEach(el => el.remove());
|
|
5633
|
+
this.removeStrayGripSvgs(container);
|
|
5634
|
+
return container;
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
handleSelectionDrop(e) {
|
|
5638
|
+
if (!this.draggedSelectionRange || !e.dataTransfer.types.includes('application/x-neiki-selection')) {
|
|
5639
|
+
return false;
|
|
5640
|
+
}
|
|
5641
|
+
|
|
5642
|
+
const dropRange = this.getCaretRangeFromPoint(e.clientX, e.clientY);
|
|
5643
|
+
if (!dropRange || !this.contentArea.contains(dropRange.startContainer)) return false;
|
|
5644
|
+
this.hideDropIndicator();
|
|
5645
|
+
|
|
5646
|
+
const sourceRange = this.draggedSelectionRange;
|
|
5647
|
+
const container = this.cleanDraggedFragment(sourceRange.cloneContents());
|
|
5648
|
+
const fragment = document.createDocumentFragment();
|
|
5649
|
+
while (container.firstChild) fragment.appendChild(container.firstChild);
|
|
5650
|
+
const marker = document.createTextNode('');
|
|
5651
|
+
dropRange.insertNode(marker);
|
|
5652
|
+
|
|
5653
|
+
try {
|
|
5654
|
+
if (sourceRange.intersectsNode(marker)) {
|
|
5655
|
+
marker.remove();
|
|
5656
|
+
this.draggedSelectionRange = null;
|
|
5657
|
+
this.hideDropIndicator();
|
|
5658
|
+
return true;
|
|
5659
|
+
}
|
|
5660
|
+
} catch (err) {}
|
|
5661
|
+
|
|
5662
|
+
sourceRange.deleteContents();
|
|
5663
|
+
marker.parentNode.insertBefore(fragment, marker);
|
|
5664
|
+
marker.remove();
|
|
5665
|
+
|
|
5666
|
+
this.draggedSelectionRange = null;
|
|
5667
|
+
this._ensureDefaultBlock();
|
|
5668
|
+
this.history.record();
|
|
5669
|
+
this.syncToOriginal();
|
|
5670
|
+
this.triggerChange();
|
|
5671
|
+
this.updateStatusBar();
|
|
5672
|
+
return true;
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5048
5675
|
initDragDrop() {
|
|
5049
5676
|
let dragCounter = 0;
|
|
5050
5677
|
|
|
@@ -5061,28 +5688,39 @@
|
|
|
5061
5688
|
dragCounter--;
|
|
5062
5689
|
if (dragCounter === 0) {
|
|
5063
5690
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5691
|
+
this.hideDropIndicator();
|
|
5064
5692
|
}
|
|
5065
5693
|
});
|
|
5066
5694
|
|
|
5067
5695
|
this.contentArea.addEventListener('dragover', (e) => {
|
|
5068
5696
|
e.preventDefault();
|
|
5697
|
+
if (e.dataTransfer.types.includes('Files') || e.dataTransfer.types.includes('application/x-neiki-selection')) {
|
|
5698
|
+
const range = this.getCaretRangeFromPoint(e.clientX, e.clientY);
|
|
5699
|
+
if (range && this.contentArea.contains(range.startContainer)) {
|
|
5700
|
+
this.showDropIndicator(range, e.clientX, e.clientY);
|
|
5701
|
+
}
|
|
5702
|
+
}
|
|
5069
5703
|
});
|
|
5070
5704
|
|
|
5071
5705
|
this.contentArea.addEventListener('drop', (e) => {
|
|
5072
5706
|
e.preventDefault();
|
|
5073
5707
|
dragCounter = 0;
|
|
5074
5708
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5709
|
+
this.hideDropIndicator();
|
|
5710
|
+
|
|
5711
|
+
if (this.handleSelectionDrop(e)) return;
|
|
5075
5712
|
|
|
5076
5713
|
const files = Array.from(e.dataTransfer.files);
|
|
5077
5714
|
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
|
5715
|
+
const videoFiles = files.filter(file => file.type.startsWith('video/'));
|
|
5078
5716
|
|
|
5079
|
-
if (imageFiles.length > 0) {
|
|
5717
|
+
if (imageFiles.length > 0 || videoFiles.length > 0) {
|
|
5080
5718
|
// Get cursor position from drop event
|
|
5081
5719
|
const dropX = e.clientX;
|
|
5082
5720
|
const dropY = e.clientY;
|
|
5083
5721
|
|
|
5084
5722
|
const setCursorAtDrop = () => {
|
|
5085
|
-
const range =
|
|
5723
|
+
const range = this.getCaretRangeFromPoint(dropX, dropY);
|
|
5086
5724
|
if (range) {
|
|
5087
5725
|
const sel = window.getSelection();
|
|
5088
5726
|
sel.removeAllRanges();
|
|
@@ -5090,30 +5728,53 @@
|
|
|
5090
5728
|
}
|
|
5091
5729
|
};
|
|
5092
5730
|
|
|
5093
|
-
const
|
|
5731
|
+
const insertFile = async (file, type) => {
|
|
5732
|
+
setCursorAtDrop();
|
|
5733
|
+
const isImage = type === 'image';
|
|
5734
|
+
const handler = isImage ? this.config.imageUploadHandler : this.config.videoUploadHandler;
|
|
5735
|
+
const hasUploadHandler = typeof handler === 'function';
|
|
5094
5736
|
|
|
5095
|
-
|
|
5737
|
+
if (hasUploadHandler) {
|
|
5738
|
+
const url = await handler(file);
|
|
5739
|
+
if (url) {
|
|
5740
|
+
if (isImage) this.commands.insertImage(url, file.name, '');
|
|
5741
|
+
else this.commands.insertVideo(url, file.name, '');
|
|
5742
|
+
}
|
|
5743
|
+
return;
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
await new Promise((resolve) => {
|
|
5747
|
+
const reader = new FileReader();
|
|
5748
|
+
reader.onload = (readerEvent) => {
|
|
5749
|
+
setCursorAtDrop();
|
|
5750
|
+
if (isImage) this.commands.insertImage(readerEvent.target.result, file.name, '');
|
|
5751
|
+
else this.commands.insertVideo(readerEvent.target.result, file.name, '');
|
|
5752
|
+
resolve();
|
|
5753
|
+
};
|
|
5754
|
+
reader.readAsDataURL(file);
|
|
5755
|
+
});
|
|
5756
|
+
};
|
|
5757
|
+
|
|
5758
|
+
if (typeof this.config.imageUploadHandler === 'function' || typeof this.config.videoUploadHandler === 'function') {
|
|
5096
5759
|
(async () => {
|
|
5097
5760
|
for (const file of imageFiles) {
|
|
5098
5761
|
try {
|
|
5099
|
-
|
|
5100
|
-
const url = await this.config.imageUploadHandler(file);
|
|
5101
|
-
if (url) {
|
|
5102
|
-
this.commands.insertImage(url, file.name, '');
|
|
5103
|
-
}
|
|
5762
|
+
await insertFile(file, 'image');
|
|
5104
5763
|
} catch (err) {
|
|
5105
5764
|
console.error('NeikiEditor: Image upload failed', err);
|
|
5106
5765
|
}
|
|
5107
5766
|
}
|
|
5767
|
+
for (const file of videoFiles) {
|
|
5768
|
+
try {
|
|
5769
|
+
await insertFile(file, 'video');
|
|
5770
|
+
} catch (err) {
|
|
5771
|
+
console.error('NeikiEditor: Video upload failed', err);
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5108
5774
|
})();
|
|
5109
5775
|
} else {
|
|
5110
|
-
imageFiles.forEach(file => {
|
|
5111
|
-
|
|
5112
|
-
reader.onload = (readerEvent) => {
|
|
5113
|
-
setCursorAtDrop();
|
|
5114
|
-
this.commands.insertImage(readerEvent.target.result, file.name, '');
|
|
5115
|
-
};
|
|
5116
|
-
reader.readAsDataURL(file);
|
|
5776
|
+
[...imageFiles.map(file => [file, 'image']), ...videoFiles.map(file => [file, 'video'])].forEach(([file, type]) => {
|
|
5777
|
+
insertFile(file, type);
|
|
5117
5778
|
});
|
|
5118
5779
|
}
|
|
5119
5780
|
}
|
|
@@ -5146,8 +5807,8 @@
|
|
|
5146
5807
|
this._dragStarted = false;
|
|
5147
5808
|
|
|
5148
5809
|
this.editor.contentArea.addEventListener('mousedown', (e) => {
|
|
5149
|
-
const img = e.target.closest('img');
|
|
5150
|
-
if (!img || !this.editor.contentArea.contains(img)) return;
|
|
5810
|
+
const img = e.target.closest('img, video');
|
|
5811
|
+
if (!img || !this.editor.contentArea.contains(img) || this.isEditorUiElement(img)) return;
|
|
5151
5812
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5152
5813
|
|
|
5153
5814
|
e.preventDefault();
|
|
@@ -5192,8 +5853,8 @@
|
|
|
5192
5853
|
|
|
5193
5854
|
this.editor.contentArea.addEventListener('click', (e) => {
|
|
5194
5855
|
if (this._dragStarted) return;
|
|
5195
|
-
const img = e.target.closest('img');
|
|
5196
|
-
if (img && this.editor.contentArea.contains(img)) {
|
|
5856
|
+
const img = e.target.closest('img, video');
|
|
5857
|
+
if (img && this.editor.contentArea.contains(img) && !this.isEditorUiElement(img)) {
|
|
5197
5858
|
e.preventDefault();
|
|
5198
5859
|
if (!this.wrapper || this.currentImg !== img) {
|
|
5199
5860
|
this.selectImage(img);
|
|
@@ -5205,8 +5866,8 @@
|
|
|
5205
5866
|
|
|
5206
5867
|
// Touch: tap image to select (drag only via grip handle in img toolbar)
|
|
5207
5868
|
this.editor.contentArea.addEventListener('touchend', (e) => {
|
|
5208
|
-
const img = e.target.closest('img');
|
|
5209
|
-
if (!img || !this.editor.contentArea.contains(img)) return;
|
|
5869
|
+
const img = e.target.closest('img, video');
|
|
5870
|
+
if (!img || !this.editor.contentArea.contains(img) || this.isEditorUiElement(img)) return;
|
|
5210
5871
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5211
5872
|
e.preventDefault();
|
|
5212
5873
|
if (!this.wrapper || this.currentImg !== img) {
|
|
@@ -5216,7 +5877,7 @@
|
|
|
5216
5877
|
|
|
5217
5878
|
// Prevent native image drag inside editor (causes duplicate on drop)
|
|
5218
5879
|
this.editor.contentArea.addEventListener('dragstart', (e) => {
|
|
5219
|
-
if (e.target.tagName === 'IMG') {
|
|
5880
|
+
if (e.target.tagName === 'IMG' || e.target.tagName === 'VIDEO') {
|
|
5220
5881
|
e.preventDefault();
|
|
5221
5882
|
}
|
|
5222
5883
|
});
|
|
@@ -5234,9 +5895,14 @@
|
|
|
5234
5895
|
});
|
|
5235
5896
|
}
|
|
5236
5897
|
|
|
5898
|
+
isEditorUiElement(node) {
|
|
5899
|
+
return !!(node && node.closest && node.closest('.neiki-block-grip, .neiki-img-toolbar, .neiki-img-resize-handle, .neiki-img-size-label, .neiki-table-col-resize-handle'));
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5237
5902
|
selectImage(img) {
|
|
5238
5903
|
this.deselect();
|
|
5239
5904
|
this.currentImg = img;
|
|
5905
|
+
const isVideo = img.tagName === 'VIDEO';
|
|
5240
5906
|
|
|
5241
5907
|
// Create wrapper around image
|
|
5242
5908
|
this.wrapper = document.createElement('span');
|
|
@@ -5323,7 +5989,7 @@
|
|
|
5323
5989
|
const replaceBtn = document.createElement('button');
|
|
5324
5990
|
replaceBtn.className = 'neiki-img-toolbar-btn';
|
|
5325
5991
|
replaceBtn.type = 'button';
|
|
5326
|
-
replaceBtn.title = t('imageToolbar.replaceImage');
|
|
5992
|
+
replaceBtn.title = isVideo ? t('videoToolbar.replaceVideo') : t('imageToolbar.replaceImage');
|
|
5327
5993
|
replaceBtn.innerHTML = Icons.replaceImage;
|
|
5328
5994
|
replaceBtn.addEventListener('click', (e) => {
|
|
5329
5995
|
e.preventDefault();
|
|
@@ -5334,7 +6000,7 @@
|
|
|
5334
6000
|
const deleteBtn = document.createElement('button');
|
|
5335
6001
|
deleteBtn.className = 'neiki-img-toolbar-btn neiki-img-toolbar-btn-danger';
|
|
5336
6002
|
deleteBtn.type = 'button';
|
|
5337
|
-
deleteBtn.title = t('imageToolbar.deleteImage');
|
|
6003
|
+
deleteBtn.title = isVideo ? t('videoToolbar.deleteVideo') : t('imageToolbar.deleteImage');
|
|
5338
6004
|
deleteBtn.innerHTML = Icons.trash;
|
|
5339
6005
|
deleteBtn.addEventListener('click', (e) => {
|
|
5340
6006
|
e.preventDefault();
|
|
@@ -5351,7 +6017,7 @@
|
|
|
5351
6017
|
this.wrapper.appendChild(this.imgToolbar);
|
|
5352
6018
|
|
|
5353
6019
|
this.positionImgToolbar();
|
|
5354
|
-
this.
|
|
6020
|
+
this.clearNativeSelection();
|
|
5355
6021
|
}
|
|
5356
6022
|
|
|
5357
6023
|
getImageBlock() {
|
|
@@ -5402,19 +6068,24 @@
|
|
|
5402
6068
|
|
|
5403
6069
|
replaceImage() {
|
|
5404
6070
|
if (!this.currentImg) return;
|
|
6071
|
+
const isVideo = this.currentImg.tagName === 'VIDEO';
|
|
5405
6072
|
const input = document.createElement('input');
|
|
5406
6073
|
input.type = 'file';
|
|
5407
|
-
input.accept = 'image/*';
|
|
6074
|
+
input.accept = isVideo ? 'video/*' : 'image/*';
|
|
5408
6075
|
input.addEventListener('change', async () => {
|
|
5409
6076
|
const file = input.files[0];
|
|
5410
6077
|
if (!file) return;
|
|
5411
|
-
const hasUploadHandler =
|
|
6078
|
+
const hasUploadHandler = isVideo
|
|
6079
|
+
? typeof this.editor.config.videoUploadHandler === 'function'
|
|
6080
|
+
: typeof this.editor.config.imageUploadHandler === 'function';
|
|
5412
6081
|
if (hasUploadHandler) {
|
|
5413
6082
|
try {
|
|
5414
|
-
const url =
|
|
6083
|
+
const url = isVideo
|
|
6084
|
+
? await this.editor.config.videoUploadHandler(file)
|
|
6085
|
+
: await this.editor.config.imageUploadHandler(file);
|
|
5415
6086
|
if (url) this.currentImg.src = url;
|
|
5416
6087
|
} catch (err) {
|
|
5417
|
-
console.error(
|
|
6088
|
+
console.error(`NeikiEditor: ${isVideo ? 'Video' : 'Image'} upload failed`, err);
|
|
5418
6089
|
}
|
|
5419
6090
|
} else {
|
|
5420
6091
|
const reader = new FileReader();
|
|
@@ -5434,6 +6105,7 @@
|
|
|
5434
6105
|
const img = this.currentImg;
|
|
5435
6106
|
const wrapper = this.wrapper;
|
|
5436
6107
|
const contentArea = this.editor.contentArea;
|
|
6108
|
+
if (this.editor.blockDragDrop) this.editor.blockDragDrop.hideGrip();
|
|
5437
6109
|
|
|
5438
6110
|
// Save image dimensions
|
|
5439
6111
|
const imgWidth = img.style.width;
|
|
@@ -5448,6 +6120,7 @@
|
|
|
5448
6120
|
const ghostImg = img.cloneNode(true);
|
|
5449
6121
|
ghostImg.style.width = '100%';
|
|
5450
6122
|
ghostImg.style.height = 'auto';
|
|
6123
|
+
ghostImg.removeAttribute('controls');
|
|
5451
6124
|
ghost.appendChild(ghostImg);
|
|
5452
6125
|
document.body.appendChild(ghost);
|
|
5453
6126
|
|
|
@@ -5462,22 +6135,6 @@
|
|
|
5462
6135
|
// Caret marker for drop position
|
|
5463
6136
|
let dropRange = null;
|
|
5464
6137
|
|
|
5465
|
-
const getCaretRange = (x, y) => {
|
|
5466
|
-
if (document.caretRangeFromPoint) {
|
|
5467
|
-
return document.caretRangeFromPoint(x, y);
|
|
5468
|
-
}
|
|
5469
|
-
if (document.caretPositionFromPoint) {
|
|
5470
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
5471
|
-
if (pos) {
|
|
5472
|
-
const r = document.createRange();
|
|
5473
|
-
r.setStart(pos.offsetNode, pos.offset);
|
|
5474
|
-
r.collapse(true);
|
|
5475
|
-
return r;
|
|
5476
|
-
}
|
|
5477
|
-
}
|
|
5478
|
-
return null;
|
|
5479
|
-
};
|
|
5480
|
-
|
|
5481
6138
|
const onMove = (ev) => {
|
|
5482
6139
|
let cx, cy;
|
|
5483
6140
|
if (isTouch) {
|
|
@@ -5492,19 +6149,10 @@
|
|
|
5492
6149
|
}
|
|
5493
6150
|
ghost.style.left = (cx - offsetX) + 'px';
|
|
5494
6151
|
ghost.style.top = (cy - offsetY) + 'px';
|
|
5495
|
-
const range =
|
|
5496
|
-
if (
|
|
5497
|
-
// Avoid dropping inside the wrapper itself
|
|
5498
|
-
let node = range.startContainer;
|
|
5499
|
-
while (node && node !== contentArea) {
|
|
5500
|
-
if (node === wrapper) return;
|
|
5501
|
-
node = node.parentNode;
|
|
5502
|
-
}
|
|
6152
|
+
const range = this.editor.getCaretRangeFromPoint(cx, cy);
|
|
6153
|
+
if (this.isSafeDropRange(range, wrapper)) {
|
|
5503
6154
|
dropRange = range;
|
|
5504
|
-
|
|
5505
|
-
const sel = window.getSelection();
|
|
5506
|
-
sel.removeAllRanges();
|
|
5507
|
-
sel.addRange(range);
|
|
6155
|
+
this.editor.showDropIndicator(range, cx, cy);
|
|
5508
6156
|
}
|
|
5509
6157
|
};
|
|
5510
6158
|
|
|
@@ -5534,7 +6182,7 @@
|
|
|
5534
6182
|
this.sizeLabel = null;
|
|
5535
6183
|
this.imgToolbar = null;
|
|
5536
6184
|
|
|
5537
|
-
if (
|
|
6185
|
+
if (this.isSafeDropRange(dropRange, wrapper)) {
|
|
5538
6186
|
// Insert image at the caret drop position
|
|
5539
6187
|
dropRange.insertNode(img);
|
|
5540
6188
|
} else {
|
|
@@ -5546,6 +6194,7 @@
|
|
|
5546
6194
|
|
|
5547
6195
|
// Clean up empty parent blocks left behind
|
|
5548
6196
|
this.editor._ensureDefaultBlock();
|
|
6197
|
+
this.editor.removeStrayGripSvgs();
|
|
5549
6198
|
|
|
5550
6199
|
// Re-select the image at its new position
|
|
5551
6200
|
this.selectImage(img);
|
|
@@ -5564,14 +6213,23 @@
|
|
|
5564
6213
|
}
|
|
5565
6214
|
}
|
|
5566
6215
|
|
|
5567
|
-
|
|
5568
|
-
if (!this.
|
|
5569
|
-
|
|
6216
|
+
isSafeDropRange(range, wrapper) {
|
|
6217
|
+
if (!range || !this.editor.contentArea.contains(range.startContainer)) return false;
|
|
6218
|
+
let node = range.startContainer;
|
|
6219
|
+
while (node && node !== this.editor.contentArea) {
|
|
6220
|
+
if (node === wrapper) return false;
|
|
6221
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.closest && node.closest('.neiki-block-grip, .neiki-img-toolbar, .neiki-img-resize-handle, .neiki-img-size-label, .neiki-table-col-resize-handle')) {
|
|
6222
|
+
return false;
|
|
6223
|
+
}
|
|
6224
|
+
this.editor.hideDropIndicator();
|
|
6225
|
+
node = node.parentNode;
|
|
6226
|
+
}
|
|
6227
|
+
return true;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
clearNativeSelection() {
|
|
5570
6231
|
const sel = window.getSelection();
|
|
5571
|
-
|
|
5572
|
-
range.selectNode(this.wrapper);
|
|
5573
|
-
sel.removeAllRanges();
|
|
5574
|
-
sel.addRange(range);
|
|
6232
|
+
if (sel) sel.removeAllRanges();
|
|
5575
6233
|
}
|
|
5576
6234
|
|
|
5577
6235
|
getSelectedImageClipboardData() {
|
|
@@ -5906,7 +6564,8 @@
|
|
|
5906
6564
|
}
|
|
5907
6565
|
});
|
|
5908
6566
|
|
|
5909
|
-
this.editor.contentArea.addEventListener('mouseleave', () => {
|
|
6567
|
+
this.editor.contentArea.addEventListener('mouseleave', (e) => {
|
|
6568
|
+
if (this.gripEl && e.relatedTarget && this.gripEl.contains(e.relatedTarget)) return;
|
|
5910
6569
|
if (!this.isDragging) this.hideGrip();
|
|
5911
6570
|
});
|
|
5912
6571
|
}
|
|
@@ -5935,10 +6594,10 @@
|
|
|
5935
6594
|
grip._block = block;
|
|
5936
6595
|
|
|
5937
6596
|
// Position grip
|
|
5938
|
-
const
|
|
6597
|
+
const wrapperRect = this.editor.contentWrapper.getBoundingClientRect();
|
|
5939
6598
|
const blockRect = block.getBoundingClientRect();
|
|
5940
|
-
grip.style.top = (blockRect.top -
|
|
5941
|
-
grip.style.left =
|
|
6599
|
+
grip.style.top = (blockRect.top - wrapperRect.top) + 'px';
|
|
6600
|
+
grip.style.left = Math.max(2, blockRect.left - wrapperRect.left - 28) + 'px';
|
|
5942
6601
|
|
|
5943
6602
|
grip.addEventListener('mousedown', (e) => {
|
|
5944
6603
|
e.preventDefault();
|
|
@@ -5946,7 +6605,7 @@
|
|
|
5946
6605
|
this.startDrag(e, block);
|
|
5947
6606
|
});
|
|
5948
6607
|
|
|
5949
|
-
this.editor.
|
|
6608
|
+
this.editor.contentWrapper.appendChild(grip);
|
|
5950
6609
|
this.gripEl = grip;
|
|
5951
6610
|
}
|
|
5952
6611
|
|
|
@@ -5964,6 +6623,8 @@
|
|
|
5964
6623
|
|
|
5965
6624
|
// Create ghost
|
|
5966
6625
|
this.ghostEl = block.cloneNode(true);
|
|
6626
|
+
this.editor.removeStrayGripSvgs(this.ghostEl);
|
|
6627
|
+
this.ghostEl.querySelectorAll('.neiki-block-grip, .neiki-img-toolbar, .neiki-img-resize-handle, .neiki-img-size-label').forEach(el => el.remove());
|
|
5967
6628
|
this.ghostEl.className = (this.ghostEl.className || '') + ' neiki-block-ghost';
|
|
5968
6629
|
this.ghostEl.style.width = block.offsetWidth + 'px';
|
|
5969
6630
|
document.body.appendChild(this.ghostEl);
|
|
@@ -6013,7 +6674,9 @@
|
|
|
6013
6674
|
this.placeholder = null;
|
|
6014
6675
|
this.ghostEl = null;
|
|
6015
6676
|
|
|
6677
|
+
this.editor.removeStrayGripSvgs();
|
|
6016
6678
|
this.editor.history.record();
|
|
6679
|
+
this.editor.syncToOriginal();
|
|
6017
6680
|
this.editor.triggerChange();
|
|
6018
6681
|
};
|
|
6019
6682
|
|