neiki-editor 2.10.0 → 3.0.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 -17
- package/dist/neiki-editor.css +243 -12
- package/dist/neiki-editor.js +765 -133
- 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.0
|
|
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,19 +1443,20 @@
|
|
|
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
|
|
|
1372
1452
|
const currentParent = () => stack[stack.length - 1];
|
|
1373
1453
|
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
return
|
|
1378
|
-
|
|
1454
|
+
const ENTITY_MAP = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00A0' };
|
|
1455
|
+
const decodeEntities = (text) => text.replace(/&(?:#x([0-9a-fA-F]+)|#([0-9]+)|([a-zA-Z]+));/g, (m, hex, dec, named) => {
|
|
1456
|
+
if (hex) return String.fromCodePoint(parseInt(hex, 16));
|
|
1457
|
+
if (dec) return String.fromCodePoint(parseInt(dec, 10));
|
|
1458
|
+
return ENTITY_MAP[named] || m;
|
|
1459
|
+
});
|
|
1379
1460
|
|
|
1380
1461
|
while (index < input.length) {
|
|
1381
1462
|
const tagStart = input.indexOf('<', index);
|
|
@@ -1442,7 +1523,8 @@
|
|
|
1442
1523
|
const attrValue = attr.value.trim();
|
|
1443
1524
|
|
|
1444
1525
|
if (!Utils.isSafeHTMLAttribute(attrName)) return;
|
|
1445
|
-
|
|
1526
|
+
const dataMediaType = tagName === 'img' ? 'image' : (tagName === 'video' || tagName === 'source' ? 'video' : null);
|
|
1527
|
+
if (urlAttrs.has(attrName) && !Utils.isSafeUrl(attrValue, dataMediaType)) return;
|
|
1446
1528
|
if (attrName === 'style' && !Utils.isSafeStyleValue(attrValue)) return;
|
|
1447
1529
|
|
|
1448
1530
|
el.setAttribute(attr.name, attr.value);
|
|
@@ -1583,7 +1665,7 @@
|
|
|
1583
1665
|
html += ' ' + attr.name + '="' + Utils.escapeHTML(attr.value) + '"';
|
|
1584
1666
|
});
|
|
1585
1667
|
|
|
1586
|
-
if (new Set(['br', 'col', 'hr', 'img']).has(tagName)) {
|
|
1668
|
+
if (new Set(['br', 'col', 'hr', 'img', 'source']).has(tagName)) {
|
|
1587
1669
|
html += '>';
|
|
1588
1670
|
} else {
|
|
1589
1671
|
html += '>' + Utils.serializeHTML(child) + '</' + tagName + '>';
|
|
@@ -1593,9 +1675,10 @@
|
|
|
1593
1675
|
return html;
|
|
1594
1676
|
},
|
|
1595
1677
|
|
|
1596
|
-
isSafeUrl(value,
|
|
1678
|
+
isSafeUrl(value, dataMediaType = null) {
|
|
1597
1679
|
if (!value) return true;
|
|
1598
1680
|
if (value.startsWith('#') || value.startsWith('/') || value.startsWith('./') || value.startsWith('../')) return true;
|
|
1681
|
+
const mediaType = dataMediaType === true ? 'image' : dataMediaType;
|
|
1599
1682
|
|
|
1600
1683
|
try {
|
|
1601
1684
|
const parsed = new URL(value, window.location.href);
|
|
@@ -1604,7 +1687,8 @@
|
|
|
1604
1687
|
protocol === 'https:' ||
|
|
1605
1688
|
protocol === 'mailto:' ||
|
|
1606
1689
|
protocol === 'tel:' ||
|
|
1607
|
-
(
|
|
1690
|
+
(mediaType === 'image' && protocol === 'data:' && /^data:image\//i.test(value)) ||
|
|
1691
|
+
(mediaType === 'video' && protocol === 'data:' && /^data:video\//i.test(value));
|
|
1608
1692
|
} catch (e) {
|
|
1609
1693
|
return false;
|
|
1610
1694
|
}
|
|
@@ -1731,6 +1815,7 @@
|
|
|
1731
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>',
|
|
1732
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>',
|
|
1733
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>',
|
|
1734
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>',
|
|
1735
1820
|
quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
|
|
1736
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>',
|
|
@@ -1756,7 +1841,7 @@
|
|
|
1756
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>',
|
|
1757
1842
|
'chevron-down': '<svg viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>',
|
|
1758
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>',
|
|
1759
|
-
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>',
|
|
1760
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>',
|
|
1761
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>',
|
|
1762
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>'
|
|
@@ -1913,7 +1998,10 @@
|
|
|
1913
1998
|
}
|
|
1914
1999
|
|
|
1915
2000
|
createOverlay() {
|
|
1916
|
-
if (this.overlay)
|
|
2001
|
+
if (this.overlay) {
|
|
2002
|
+
this.syncThemeClasses();
|
|
2003
|
+
return this.overlay;
|
|
2004
|
+
}
|
|
1917
2005
|
|
|
1918
2006
|
this.overlay = Utils.createElement('div', {
|
|
1919
2007
|
className: 'neiki-modal-overlay',
|
|
@@ -1925,9 +2013,18 @@
|
|
|
1925
2013
|
});
|
|
1926
2014
|
|
|
1927
2015
|
document.body.appendChild(this.overlay);
|
|
2016
|
+
this.syncThemeClasses();
|
|
1928
2017
|
return this.overlay;
|
|
1929
2018
|
}
|
|
1930
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
|
+
|
|
1931
2028
|
open(type, data = {}) {
|
|
1932
2029
|
if (this.editor.saveCurrentSelection) {
|
|
1933
2030
|
this.editor.saveCurrentSelection();
|
|
@@ -1944,6 +2041,9 @@
|
|
|
1944
2041
|
case 'image':
|
|
1945
2042
|
modal = this.createImageModal(data);
|
|
1946
2043
|
break;
|
|
2044
|
+
case 'video':
|
|
2045
|
+
modal = this.createVideoModal(data);
|
|
2046
|
+
break;
|
|
1947
2047
|
case 'table':
|
|
1948
2048
|
modal = this.createTableModal(data);
|
|
1949
2049
|
break;
|
|
@@ -2262,6 +2362,179 @@
|
|
|
2262
2362
|
return modal;
|
|
2263
2363
|
}
|
|
2264
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
|
+
|
|
2265
2538
|
createTableModal(data) {
|
|
2266
2539
|
const modal = Utils.createElement('div', { className: 'neiki-modal' });
|
|
2267
2540
|
|
|
@@ -2528,7 +2801,7 @@
|
|
|
2528
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;">
|
|
2529
2802
|
<div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
|
|
2530
2803
|
<div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
2531
|
-
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong>
|
|
2804
|
+
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.0.0</div>
|
|
2532
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>
|
|
2533
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>
|
|
2534
2807
|
</div>
|
|
@@ -3292,6 +3565,19 @@
|
|
|
3292
3565
|
this.editor.triggerChange();
|
|
3293
3566
|
}
|
|
3294
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
|
+
|
|
3295
3581
|
insertTable(rows, cols, hasHeader = true) {
|
|
3296
3582
|
let html = '<table class="neiki-table">';
|
|
3297
3583
|
|
|
@@ -3381,10 +3667,11 @@
|
|
|
3381
3667
|
|
|
3382
3668
|
// Load theme preference
|
|
3383
3669
|
const savedTheme = StorageManager.getGlobal('theme', this.config.theme);
|
|
3384
|
-
this.config.theme = savedTheme;
|
|
3670
|
+
this.config.theme = THEMES.includes(savedTheme) ? savedTheme : 'light';
|
|
3385
3671
|
|
|
3386
3672
|
this.createStructure();
|
|
3387
3673
|
this.createToolbar();
|
|
3674
|
+
this.applyTheme(this.config.theme);
|
|
3388
3675
|
this.createContentArea();
|
|
3389
3676
|
this.createStatusBar();
|
|
3390
3677
|
|
|
@@ -3403,6 +3690,7 @@
|
|
|
3403
3690
|
|
|
3404
3691
|
this.bindEvents();
|
|
3405
3692
|
this.initDragDrop();
|
|
3693
|
+
this.initSelectionDragDrop();
|
|
3406
3694
|
this.initPlugins();
|
|
3407
3695
|
|
|
3408
3696
|
// Sync restored content to original element
|
|
@@ -3432,7 +3720,7 @@
|
|
|
3432
3720
|
createStructure() {
|
|
3433
3721
|
const langClass = _currentLanguage !== 'en' ? `neiki-lang-${_currentLanguage}` : '';
|
|
3434
3722
|
this.container = Utils.createElement('div', {
|
|
3435
|
-
className: `neiki-editor ${this.config.theme
|
|
3723
|
+
className: `neiki-editor ${this.getThemeClasses(this.config.theme)} ${langClass}`.trim(),
|
|
3436
3724
|
id: this.id
|
|
3437
3725
|
});
|
|
3438
3726
|
|
|
@@ -3440,6 +3728,37 @@
|
|
|
3440
3728
|
this.originalElement.parentNode.insertBefore(this.container, this.originalElement);
|
|
3441
3729
|
}
|
|
3442
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
|
+
|
|
3443
3762
|
createAutosaveStorageId(element) {
|
|
3444
3763
|
const customKey = this.config.autosaveKey ||
|
|
3445
3764
|
this.originalElement.getAttribute('data-neiki-autosave-key');
|
|
@@ -3755,6 +4074,39 @@
|
|
|
3755
4074
|
return;
|
|
3756
4075
|
}
|
|
3757
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
|
+
|
|
3758
4110
|
// Handle Insert dropdown
|
|
3759
4111
|
if (config.type === 'insertDropdown') {
|
|
3760
4112
|
const btn = Utils.createElement('button', {
|
|
@@ -3775,6 +4127,10 @@
|
|
|
3775
4127
|
key: 'image', icon: Icons.image, labelKey: 'insert.image',
|
|
3776
4128
|
action: () => { this.saveCurrentSelection(); this.modal.open('image', {}); }
|
|
3777
4129
|
},
|
|
4130
|
+
{
|
|
4131
|
+
key: 'video', icon: Icons.video, labelKey: 'insert.video',
|
|
4132
|
+
action: () => { this.saveCurrentSelection(); this.modal.open('video', {}); }
|
|
4133
|
+
},
|
|
3778
4134
|
{
|
|
3779
4135
|
key: 'table', icon: Icons.table, labelKey: 'insert.table',
|
|
3780
4136
|
action: () => { this.saveCurrentSelection(); this.modal.open('table', {}); }
|
|
@@ -3851,7 +4207,7 @@
|
|
|
3851
4207
|
{ key: 'autosave', icon: Icons.save, labelKey: 'menu.autosave', action: () => this.toggleAutosave(), toggle: true },
|
|
3852
4208
|
{ key: 'divider' },
|
|
3853
4209
|
{ key: 'clearAll', icon: Icons.trash, labelKey: 'menu.clearAll', action: () => this.clearAll(), danger: true },
|
|
3854
|
-
{ key: '
|
|
4210
|
+
{ key: 'themeSelect', icon: Icons.sun, labelKey: 'menu.toggleTheme' },
|
|
3855
4211
|
{ key: 'fullscreen', icon: Icons.fullscreen, labelKey: 'menu.fullscreen', action: () => this.toggleFullscreen() }
|
|
3856
4212
|
];
|
|
3857
4213
|
|
|
@@ -3865,6 +4221,35 @@
|
|
|
3865
4221
|
dropdown.appendChild(Utils.createElement('div', { className: 'neiki-dropdown-divider' }));
|
|
3866
4222
|
return;
|
|
3867
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
|
+
|
|
3868
4253
|
const menuItem = Utils.createElement('div', {
|
|
3869
4254
|
className: 'neiki-dropdown-item' + (danger ? ' neiki-dropdown-item-danger' : '')
|
|
3870
4255
|
});
|
|
@@ -3878,11 +4263,6 @@
|
|
|
3878
4263
|
this._autosaveBadge = badge;
|
|
3879
4264
|
}
|
|
3880
4265
|
|
|
3881
|
-
if (key === 'themeToggle') {
|
|
3882
|
-
this._themeMenuItem = menuItem;
|
|
3883
|
-
this._themeMenuIcon = menuItem.querySelector('.neiki-dropdown-item-icon');
|
|
3884
|
-
}
|
|
3885
|
-
|
|
3886
4266
|
menuItem.addEventListener('click', (e) => {
|
|
3887
4267
|
e.preventDefault();
|
|
3888
4268
|
e.stopPropagation();
|
|
@@ -4006,8 +4386,20 @@
|
|
|
4006
4386
|
className: 'neiki-code-view-textarea',
|
|
4007
4387
|
spellcheck: 'false'
|
|
4008
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
|
+
});
|
|
4009
4401
|
this.codeView.appendChild(codeViewHeader);
|
|
4010
|
-
this.codeView.appendChild(this.
|
|
4402
|
+
this.codeView.appendChild(this.codeViewEditor);
|
|
4011
4403
|
this.contentWrapper.appendChild(this.codeView);
|
|
4012
4404
|
|
|
4013
4405
|
this.container.appendChild(this.contentWrapper);
|
|
@@ -4016,6 +4408,7 @@
|
|
|
4016
4408
|
bindEvents() {
|
|
4017
4409
|
// Content changes
|
|
4018
4410
|
this.contentArea.addEventListener('input', Utils.debounce(() => {
|
|
4411
|
+
this.removeStrayGripSvgs();
|
|
4019
4412
|
this._ensureDefaultBlock();
|
|
4020
4413
|
this.history.record();
|
|
4021
4414
|
this.syncToOriginal();
|
|
@@ -4327,6 +4720,9 @@
|
|
|
4327
4720
|
}
|
|
4328
4721
|
}
|
|
4329
4722
|
break;
|
|
4723
|
+
case 'themeToggle':
|
|
4724
|
+
isActive = this.config.theme === 'dark' || this.config.theme === 'dark-blue';
|
|
4725
|
+
break;
|
|
4330
4726
|
}
|
|
4331
4727
|
} catch (e) {
|
|
4332
4728
|
// queryCommandState can throw in some browsers
|
|
@@ -4397,28 +4793,14 @@
|
|
|
4397
4793
|
}
|
|
4398
4794
|
|
|
4399
4795
|
toggleTheme() {
|
|
4400
|
-
const
|
|
4401
|
-
const
|
|
4402
|
-
|
|
4403
|
-
this.container.classList.toggle('neiki-dark', !isDark);
|
|
4404
|
-
this.config.theme = newTheme;
|
|
4405
|
-
|
|
4406
|
-
// Persist theme choice
|
|
4407
|
-
StorageManager.setGlobal('theme', newTheme);
|
|
4408
|
-
|
|
4409
|
-
// Update button icon and active state
|
|
4410
|
-
if (this.toolbarButtons.themeToggle) {
|
|
4411
|
-
this.toolbarButtons.themeToggle.innerHTML = isDark ? Icons.sun : Icons.moon;
|
|
4412
|
-
this.toolbarButtons.themeToggle.classList.toggle('active', !isDark);
|
|
4413
|
-
this.toolbarButtons.themeToggle.title = isDark ? 'Switch to Dark Mode' : 'Switch to Light Mode';
|
|
4414
|
-
}
|
|
4415
|
-
this._updateThemeMenuItem();
|
|
4796
|
+
const currentIndex = THEMES.indexOf(this.config.theme);
|
|
4797
|
+
const nextTheme = THEMES[(currentIndex + 1) % THEMES.length];
|
|
4798
|
+
this.applyTheme(nextTheme);
|
|
4416
4799
|
}
|
|
4417
4800
|
|
|
4418
4801
|
_updateThemeMenuItem() {
|
|
4419
|
-
if (this.
|
|
4420
|
-
|
|
4421
|
-
this._themeMenuIcon.innerHTML = isDark ? Icons.moon : Icons.sun;
|
|
4802
|
+
if (this._themeMenuSelect) {
|
|
4803
|
+
this._themeMenuSelect.value = this.config.theme;
|
|
4422
4804
|
}
|
|
4423
4805
|
}
|
|
4424
4806
|
|
|
@@ -4583,15 +4965,30 @@
|
|
|
4583
4965
|
const clone = this.contentArea.cloneNode(true);
|
|
4584
4966
|
// Unwrap image resizer wrappers
|
|
4585
4967
|
clone.querySelectorAll('.neiki-img-resizable').forEach(wrapper => {
|
|
4586
|
-
const
|
|
4587
|
-
if (
|
|
4968
|
+
const media = wrapper.querySelector('img, video');
|
|
4969
|
+
if (media) wrapper.parentNode.insertBefore(media, wrapper);
|
|
4588
4970
|
wrapper.remove();
|
|
4589
4971
|
});
|
|
4590
4972
|
// Remove grip handles, placeholders, resize handles
|
|
4591
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);
|
|
4592
4975
|
return clone.innerHTML;
|
|
4593
4976
|
}
|
|
4594
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
|
+
|
|
4595
4992
|
setContent(html) {
|
|
4596
4993
|
if (this.imageResizer) this.imageResizer.deselect();
|
|
4597
4994
|
this.savedSelectionRange = null;
|
|
@@ -4667,9 +5064,7 @@
|
|
|
4667
5064
|
}
|
|
4668
5065
|
|
|
4669
5066
|
setTheme(theme) {
|
|
4670
|
-
this.
|
|
4671
|
-
this.container.classList.toggle('neiki-dark', theme === 'dark');
|
|
4672
|
-
StorageManager.setGlobal('theme', theme);
|
|
5067
|
+
this.applyTheme(theme);
|
|
4673
5068
|
}
|
|
4674
5069
|
|
|
4675
5070
|
createStatusBar() {
|
|
@@ -4726,9 +5121,85 @@
|
|
|
4726
5121
|
return 'p';
|
|
4727
5122
|
}
|
|
4728
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 = input.match(/<!--[\s\S]*?-->|<\/?[^>]+>|[^<]+/g) || [];
|
|
5131
|
+
const lines = [];
|
|
5132
|
+
let indent = 0;
|
|
5133
|
+
let inlineDepth = 0;
|
|
5134
|
+
let hasInlineContent = false;
|
|
5135
|
+
|
|
5136
|
+
tokens.forEach(token => {
|
|
5137
|
+
const trimmed = token.trim();
|
|
5138
|
+
if (!trimmed) return;
|
|
5139
|
+
const textPrefix = !/^</.test(trimmed) && /^\s/.test(token) ? ' ' : '';
|
|
5140
|
+
|
|
5141
|
+
const tagMatch = trimmed.match(/^<\/?\s*([a-zA-Z0-9:-]+)/);
|
|
5142
|
+
const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';
|
|
5143
|
+
const isClosing = /^<\//.test(trimmed);
|
|
5144
|
+
const isTag = /^</.test(trimmed);
|
|
5145
|
+
const isSelfClosing = /\/>$/.test(trimmed) || voidTags.has(tagName);
|
|
5146
|
+
const isBlock = blockTags.has(tagName);
|
|
5147
|
+
|
|
5148
|
+
if (isTag && isClosing) {
|
|
5149
|
+
if (!isBlock && lines.length > 0) {
|
|
5150
|
+
lines[lines.length - 1] += trimmed;
|
|
5151
|
+
inlineDepth = Math.max(inlineDepth - 1, 0);
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5154
|
+
indent = Math.max(indent - 1, 0);
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
const prefix = ' '.repeat(indent);
|
|
5158
|
+
if (!isTag && (inlineDepth > 0 || hasInlineContent) && lines.length > 0) {
|
|
5159
|
+
lines[lines.length - 1] += (textPrefix || (lines[lines.length - 1].endsWith('>') ? '' : ' ')) + trimmed;
|
|
5160
|
+
} else if (!isTag || isBlock || lines.length === 0 || isClosing) {
|
|
5161
|
+
lines.push(prefix + trimmed);
|
|
5162
|
+
} else {
|
|
5163
|
+
lines[lines.length - 1] += trimmed;
|
|
5164
|
+
}
|
|
5165
|
+
|
|
5166
|
+
if (isTag && !isClosing && !isSelfClosing && isBlock) {
|
|
5167
|
+
indent++;
|
|
5168
|
+
hasInlineContent = false;
|
|
5169
|
+
} else if (isTag && !isClosing && !isSelfClosing && !isBlock) {
|
|
5170
|
+
inlineDepth++;
|
|
5171
|
+
hasInlineContent = true;
|
|
5172
|
+
} else if (!isTag) {
|
|
5173
|
+
hasInlineContent = true;
|
|
5174
|
+
}
|
|
5175
|
+
});
|
|
5176
|
+
|
|
5177
|
+
return lines.join('\n');
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
renderCodeViewHighlight() {
|
|
5181
|
+
if (!this.codeViewHighlight || !this.codeViewTextarea) return;
|
|
5182
|
+
const source = this.codeViewTextarea.value;
|
|
5183
|
+
const html = Utils.escapeHTML(source).replace(/(<!--[\s\S]*?-->)|(<\/?[\s\S]*?>)/g, (match, comment) => {
|
|
5184
|
+
if (comment) return `<span class="neiki-html-comment">${match}</span>`;
|
|
5185
|
+
|
|
5186
|
+
const tagParts = match.match(/^(<\/?)([a-zA-Z0-9:-]+)([\s\S]*?)(\/?>)$/);
|
|
5187
|
+
if (!tagParts) return match;
|
|
5188
|
+
|
|
5189
|
+
const attrs = tagParts[3].replace(/(\s+)([a-zA-Z_:][-a-zA-Z0-9_:.]*)(=)(".*?"|'.*?'|[^\s&]+)?/g, (attrMatch, space, name, eq, value) => {
|
|
5190
|
+
return `${space}<span class="neiki-html-attr">${name}</span>${eq}<span class="neiki-html-string">${value || ''}</span>`;
|
|
5191
|
+
});
|
|
5192
|
+
|
|
5193
|
+
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>`;
|
|
5194
|
+
});
|
|
5195
|
+
|
|
5196
|
+
this.codeViewHighlight.innerHTML = html + (source.endsWith('\n') ? ' ' : '');
|
|
5197
|
+
}
|
|
5198
|
+
|
|
4729
5199
|
toggleCodeView() {
|
|
4730
5200
|
if (!this.isCodeViewOpen) {
|
|
4731
|
-
this.codeViewTextarea.value = this.contentArea.innerHTML;
|
|
5201
|
+
this.codeViewTextarea.value = this.formatHTMLSource(this.contentArea.innerHTML);
|
|
5202
|
+
this.renderCodeViewHighlight();
|
|
4732
5203
|
this.codeView.classList.add('show');
|
|
4733
5204
|
this.isCodeViewOpen = true;
|
|
4734
5205
|
this.codeViewTextarea.focus();
|
|
@@ -5044,6 +5515,131 @@
|
|
|
5044
5515
|
// DRAG & DROP
|
|
5045
5516
|
// ============================================
|
|
5046
5517
|
|
|
5518
|
+
getCaretRangeFromPoint(x, y) {
|
|
5519
|
+
if (document.caretRangeFromPoint) {
|
|
5520
|
+
return document.caretRangeFromPoint(x, y);
|
|
5521
|
+
}
|
|
5522
|
+
if (document.caretPositionFromPoint) {
|
|
5523
|
+
const pos = document.caretPositionFromPoint(x, y);
|
|
5524
|
+
if (pos) {
|
|
5525
|
+
const range = document.createRange();
|
|
5526
|
+
range.setStart(pos.offsetNode, pos.offset);
|
|
5527
|
+
range.collapse(true);
|
|
5528
|
+
return range;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
return null;
|
|
5532
|
+
}
|
|
5533
|
+
|
|
5534
|
+
showDropIndicator(range, x, y) {
|
|
5535
|
+
if (!this.dropIndicator) {
|
|
5536
|
+
this.dropIndicator = document.createElement('div');
|
|
5537
|
+
this.dropIndicator.className = 'neiki-drop-indicator';
|
|
5538
|
+
this.dropIndicator.setAttribute('aria-hidden', 'true');
|
|
5539
|
+
this.contentWrapper.appendChild(this.dropIndicator);
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
const wrapperRect = this.contentWrapper.getBoundingClientRect();
|
|
5543
|
+
let rect = null;
|
|
5544
|
+
|
|
5545
|
+
if (range) {
|
|
5546
|
+
const rects = range.getClientRects();
|
|
5547
|
+
rect = rects.length ? rects[0] : range.getBoundingClientRect();
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
const left = rect && rect.left ? rect.left - wrapperRect.left : x - wrapperRect.left;
|
|
5551
|
+
const top = rect && rect.top ? rect.top - wrapperRect.top : y - wrapperRect.top;
|
|
5552
|
+
const height = rect && rect.height ? Math.max(rect.height, 18) : 22;
|
|
5553
|
+
|
|
5554
|
+
this.dropIndicator.style.left = Math.max(6, Math.min(left, wrapperRect.width - 6)) + 'px';
|
|
5555
|
+
this.dropIndicator.style.top = Math.max(6, Math.min(top, wrapperRect.height - 6)) + 'px';
|
|
5556
|
+
this.dropIndicator.style.height = height + 'px';
|
|
5557
|
+
this.dropIndicator.classList.add('show');
|
|
5558
|
+
}
|
|
5559
|
+
|
|
5560
|
+
hideDropIndicator() {
|
|
5561
|
+
if (this.dropIndicator) {
|
|
5562
|
+
this.dropIndicator.classList.remove('show');
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
initSelectionDragDrop() {
|
|
5567
|
+
this.draggedSelectionRange = null;
|
|
5568
|
+
|
|
5569
|
+
this.contentArea.addEventListener('dragstart', (e) => {
|
|
5570
|
+
const sel = window.getSelection();
|
|
5571
|
+
if (!sel || !sel.rangeCount || sel.isCollapsed) return;
|
|
5572
|
+
if (!this.contentArea.contains(sel.anchorNode) || !this.contentArea.contains(sel.focusNode)) return;
|
|
5573
|
+
|
|
5574
|
+
const range = sel.getRangeAt(0);
|
|
5575
|
+
const targetNode = e.target.nodeType === Node.TEXT_NODE ? e.target.parentNode : e.target;
|
|
5576
|
+
try {
|
|
5577
|
+
if (targetNode && !range.intersectsNode(targetNode)) return;
|
|
5578
|
+
} catch (err) {
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
const container = this.cleanDraggedFragment(range.cloneContents());
|
|
5583
|
+
|
|
5584
|
+
this.draggedSelectionRange = range.cloneRange();
|
|
5585
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
5586
|
+
e.dataTransfer.setData('application/x-neiki-selection', '1');
|
|
5587
|
+
e.dataTransfer.setData('text/html', container.innerHTML);
|
|
5588
|
+
e.dataTransfer.setData('text/plain', sel.toString());
|
|
5589
|
+
});
|
|
5590
|
+
|
|
5591
|
+
this.contentArea.addEventListener('dragend', () => {
|
|
5592
|
+
this.draggedSelectionRange = null;
|
|
5593
|
+
this.hideDropIndicator();
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
cleanDraggedFragment(fragment) {
|
|
5598
|
+
const container = document.createElement('div');
|
|
5599
|
+
container.appendChild(fragment);
|
|
5600
|
+
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());
|
|
5601
|
+
this.removeStrayGripSvgs(container);
|
|
5602
|
+
return container;
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
handleSelectionDrop(e) {
|
|
5606
|
+
if (!this.draggedSelectionRange || !e.dataTransfer.types.includes('application/x-neiki-selection')) {
|
|
5607
|
+
return false;
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
const dropRange = this.getCaretRangeFromPoint(e.clientX, e.clientY);
|
|
5611
|
+
if (!dropRange || !this.contentArea.contains(dropRange.startContainer)) return false;
|
|
5612
|
+
this.hideDropIndicator();
|
|
5613
|
+
|
|
5614
|
+
const sourceRange = this.draggedSelectionRange;
|
|
5615
|
+
const container = this.cleanDraggedFragment(sourceRange.cloneContents());
|
|
5616
|
+
const fragment = document.createDocumentFragment();
|
|
5617
|
+
while (container.firstChild) fragment.appendChild(container.firstChild);
|
|
5618
|
+
const marker = document.createTextNode('');
|
|
5619
|
+
dropRange.insertNode(marker);
|
|
5620
|
+
|
|
5621
|
+
try {
|
|
5622
|
+
if (sourceRange.intersectsNode(marker)) {
|
|
5623
|
+
marker.remove();
|
|
5624
|
+
this.draggedSelectionRange = null;
|
|
5625
|
+
this.hideDropIndicator();
|
|
5626
|
+
return true;
|
|
5627
|
+
}
|
|
5628
|
+
} catch (err) {}
|
|
5629
|
+
|
|
5630
|
+
sourceRange.deleteContents();
|
|
5631
|
+
marker.parentNode.insertBefore(fragment, marker);
|
|
5632
|
+
marker.remove();
|
|
5633
|
+
|
|
5634
|
+
this.draggedSelectionRange = null;
|
|
5635
|
+
this._ensureDefaultBlock();
|
|
5636
|
+
this.history.record();
|
|
5637
|
+
this.syncToOriginal();
|
|
5638
|
+
this.triggerChange();
|
|
5639
|
+
this.updateStatusBar();
|
|
5640
|
+
return true;
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5047
5643
|
initDragDrop() {
|
|
5048
5644
|
let dragCounter = 0;
|
|
5049
5645
|
|
|
@@ -5060,28 +5656,39 @@
|
|
|
5060
5656
|
dragCounter--;
|
|
5061
5657
|
if (dragCounter === 0) {
|
|
5062
5658
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5659
|
+
this.hideDropIndicator();
|
|
5063
5660
|
}
|
|
5064
5661
|
});
|
|
5065
5662
|
|
|
5066
5663
|
this.contentArea.addEventListener('dragover', (e) => {
|
|
5067
5664
|
e.preventDefault();
|
|
5665
|
+
if (e.dataTransfer.types.includes('Files') || e.dataTransfer.types.includes('application/x-neiki-selection')) {
|
|
5666
|
+
const range = this.getCaretRangeFromPoint(e.clientX, e.clientY);
|
|
5667
|
+
if (range && this.contentArea.contains(range.startContainer)) {
|
|
5668
|
+
this.showDropIndicator(range, e.clientX, e.clientY);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5068
5671
|
});
|
|
5069
5672
|
|
|
5070
5673
|
this.contentArea.addEventListener('drop', (e) => {
|
|
5071
5674
|
e.preventDefault();
|
|
5072
5675
|
dragCounter = 0;
|
|
5073
5676
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5677
|
+
this.hideDropIndicator();
|
|
5678
|
+
|
|
5679
|
+
if (this.handleSelectionDrop(e)) return;
|
|
5074
5680
|
|
|
5075
5681
|
const files = Array.from(e.dataTransfer.files);
|
|
5076
5682
|
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
|
5683
|
+
const videoFiles = files.filter(file => file.type.startsWith('video/'));
|
|
5077
5684
|
|
|
5078
|
-
if (imageFiles.length > 0) {
|
|
5685
|
+
if (imageFiles.length > 0 || videoFiles.length > 0) {
|
|
5079
5686
|
// Get cursor position from drop event
|
|
5080
5687
|
const dropX = e.clientX;
|
|
5081
5688
|
const dropY = e.clientY;
|
|
5082
5689
|
|
|
5083
5690
|
const setCursorAtDrop = () => {
|
|
5084
|
-
const range =
|
|
5691
|
+
const range = this.getCaretRangeFromPoint(dropX, dropY);
|
|
5085
5692
|
if (range) {
|
|
5086
5693
|
const sel = window.getSelection();
|
|
5087
5694
|
sel.removeAllRanges();
|
|
@@ -5089,30 +5696,53 @@
|
|
|
5089
5696
|
}
|
|
5090
5697
|
};
|
|
5091
5698
|
|
|
5092
|
-
const
|
|
5699
|
+
const insertFile = async (file, type) => {
|
|
5700
|
+
setCursorAtDrop();
|
|
5701
|
+
const isImage = type === 'image';
|
|
5702
|
+
const handler = isImage ? this.config.imageUploadHandler : this.config.videoUploadHandler;
|
|
5703
|
+
const hasUploadHandler = typeof handler === 'function';
|
|
5093
5704
|
|
|
5094
|
-
|
|
5705
|
+
if (hasUploadHandler) {
|
|
5706
|
+
const url = await handler(file);
|
|
5707
|
+
if (url) {
|
|
5708
|
+
if (isImage) this.commands.insertImage(url, file.name, '');
|
|
5709
|
+
else this.commands.insertVideo(url, file.name, '');
|
|
5710
|
+
}
|
|
5711
|
+
return;
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
await new Promise((resolve) => {
|
|
5715
|
+
const reader = new FileReader();
|
|
5716
|
+
reader.onload = (readerEvent) => {
|
|
5717
|
+
setCursorAtDrop();
|
|
5718
|
+
if (isImage) this.commands.insertImage(readerEvent.target.result, file.name, '');
|
|
5719
|
+
else this.commands.insertVideo(readerEvent.target.result, file.name, '');
|
|
5720
|
+
resolve();
|
|
5721
|
+
};
|
|
5722
|
+
reader.readAsDataURL(file);
|
|
5723
|
+
});
|
|
5724
|
+
};
|
|
5725
|
+
|
|
5726
|
+
if (typeof this.config.imageUploadHandler === 'function' || typeof this.config.videoUploadHandler === 'function') {
|
|
5095
5727
|
(async () => {
|
|
5096
5728
|
for (const file of imageFiles) {
|
|
5097
5729
|
try {
|
|
5098
|
-
|
|
5099
|
-
const url = await this.config.imageUploadHandler(file);
|
|
5100
|
-
if (url) {
|
|
5101
|
-
this.commands.insertImage(url, file.name, '');
|
|
5102
|
-
}
|
|
5730
|
+
await insertFile(file, 'image');
|
|
5103
5731
|
} catch (err) {
|
|
5104
5732
|
console.error('NeikiEditor: Image upload failed', err);
|
|
5105
5733
|
}
|
|
5106
5734
|
}
|
|
5735
|
+
for (const file of videoFiles) {
|
|
5736
|
+
try {
|
|
5737
|
+
await insertFile(file, 'video');
|
|
5738
|
+
} catch (err) {
|
|
5739
|
+
console.error('NeikiEditor: Video upload failed', err);
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5107
5742
|
})();
|
|
5108
5743
|
} else {
|
|
5109
|
-
imageFiles.forEach(file => {
|
|
5110
|
-
|
|
5111
|
-
reader.onload = (readerEvent) => {
|
|
5112
|
-
setCursorAtDrop();
|
|
5113
|
-
this.commands.insertImage(readerEvent.target.result, file.name, '');
|
|
5114
|
-
};
|
|
5115
|
-
reader.readAsDataURL(file);
|
|
5744
|
+
[...imageFiles.map(file => [file, 'image']), ...videoFiles.map(file => [file, 'video'])].forEach(([file, type]) => {
|
|
5745
|
+
insertFile(file, type);
|
|
5116
5746
|
});
|
|
5117
5747
|
}
|
|
5118
5748
|
}
|
|
@@ -5145,8 +5775,8 @@
|
|
|
5145
5775
|
this._dragStarted = false;
|
|
5146
5776
|
|
|
5147
5777
|
this.editor.contentArea.addEventListener('mousedown', (e) => {
|
|
5148
|
-
const img = e.target.closest('img');
|
|
5149
|
-
if (!img || !this.editor.contentArea.contains(img)) return;
|
|
5778
|
+
const img = e.target.closest('img, video');
|
|
5779
|
+
if (!img || !this.editor.contentArea.contains(img) || this.isEditorUiElement(img)) return;
|
|
5150
5780
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5151
5781
|
|
|
5152
5782
|
e.preventDefault();
|
|
@@ -5191,8 +5821,8 @@
|
|
|
5191
5821
|
|
|
5192
5822
|
this.editor.contentArea.addEventListener('click', (e) => {
|
|
5193
5823
|
if (this._dragStarted) return;
|
|
5194
|
-
const img = e.target.closest('img');
|
|
5195
|
-
if (img && this.editor.contentArea.contains(img)) {
|
|
5824
|
+
const img = e.target.closest('img, video');
|
|
5825
|
+
if (img && this.editor.contentArea.contains(img) && !this.isEditorUiElement(img)) {
|
|
5196
5826
|
e.preventDefault();
|
|
5197
5827
|
if (!this.wrapper || this.currentImg !== img) {
|
|
5198
5828
|
this.selectImage(img);
|
|
@@ -5204,8 +5834,8 @@
|
|
|
5204
5834
|
|
|
5205
5835
|
// Touch: tap image to select (drag only via grip handle in img toolbar)
|
|
5206
5836
|
this.editor.contentArea.addEventListener('touchend', (e) => {
|
|
5207
|
-
const img = e.target.closest('img');
|
|
5208
|
-
if (!img || !this.editor.contentArea.contains(img)) return;
|
|
5837
|
+
const img = e.target.closest('img, video');
|
|
5838
|
+
if (!img || !this.editor.contentArea.contains(img) || this.isEditorUiElement(img)) return;
|
|
5209
5839
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5210
5840
|
e.preventDefault();
|
|
5211
5841
|
if (!this.wrapper || this.currentImg !== img) {
|
|
@@ -5215,7 +5845,7 @@
|
|
|
5215
5845
|
|
|
5216
5846
|
// Prevent native image drag inside editor (causes duplicate on drop)
|
|
5217
5847
|
this.editor.contentArea.addEventListener('dragstart', (e) => {
|
|
5218
|
-
if (e.target.tagName === 'IMG') {
|
|
5848
|
+
if (e.target.tagName === 'IMG' || e.target.tagName === 'VIDEO') {
|
|
5219
5849
|
e.preventDefault();
|
|
5220
5850
|
}
|
|
5221
5851
|
});
|
|
@@ -5233,9 +5863,14 @@
|
|
|
5233
5863
|
});
|
|
5234
5864
|
}
|
|
5235
5865
|
|
|
5866
|
+
isEditorUiElement(node) {
|
|
5867
|
+
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'));
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5236
5870
|
selectImage(img) {
|
|
5237
5871
|
this.deselect();
|
|
5238
5872
|
this.currentImg = img;
|
|
5873
|
+
const isVideo = img.tagName === 'VIDEO';
|
|
5239
5874
|
|
|
5240
5875
|
// Create wrapper around image
|
|
5241
5876
|
this.wrapper = document.createElement('span');
|
|
@@ -5322,7 +5957,7 @@
|
|
|
5322
5957
|
const replaceBtn = document.createElement('button');
|
|
5323
5958
|
replaceBtn.className = 'neiki-img-toolbar-btn';
|
|
5324
5959
|
replaceBtn.type = 'button';
|
|
5325
|
-
replaceBtn.title = t('imageToolbar.replaceImage');
|
|
5960
|
+
replaceBtn.title = isVideo ? t('videoToolbar.replaceVideo') : t('imageToolbar.replaceImage');
|
|
5326
5961
|
replaceBtn.innerHTML = Icons.replaceImage;
|
|
5327
5962
|
replaceBtn.addEventListener('click', (e) => {
|
|
5328
5963
|
e.preventDefault();
|
|
@@ -5333,7 +5968,7 @@
|
|
|
5333
5968
|
const deleteBtn = document.createElement('button');
|
|
5334
5969
|
deleteBtn.className = 'neiki-img-toolbar-btn neiki-img-toolbar-btn-danger';
|
|
5335
5970
|
deleteBtn.type = 'button';
|
|
5336
|
-
deleteBtn.title = t('imageToolbar.deleteImage');
|
|
5971
|
+
deleteBtn.title = isVideo ? t('videoToolbar.deleteVideo') : t('imageToolbar.deleteImage');
|
|
5337
5972
|
deleteBtn.innerHTML = Icons.trash;
|
|
5338
5973
|
deleteBtn.addEventListener('click', (e) => {
|
|
5339
5974
|
e.preventDefault();
|
|
@@ -5350,7 +5985,7 @@
|
|
|
5350
5985
|
this.wrapper.appendChild(this.imgToolbar);
|
|
5351
5986
|
|
|
5352
5987
|
this.positionImgToolbar();
|
|
5353
|
-
this.
|
|
5988
|
+
this.clearNativeSelection();
|
|
5354
5989
|
}
|
|
5355
5990
|
|
|
5356
5991
|
getImageBlock() {
|
|
@@ -5401,19 +6036,24 @@
|
|
|
5401
6036
|
|
|
5402
6037
|
replaceImage() {
|
|
5403
6038
|
if (!this.currentImg) return;
|
|
6039
|
+
const isVideo = this.currentImg.tagName === 'VIDEO';
|
|
5404
6040
|
const input = document.createElement('input');
|
|
5405
6041
|
input.type = 'file';
|
|
5406
|
-
input.accept = 'image/*';
|
|
6042
|
+
input.accept = isVideo ? 'video/*' : 'image/*';
|
|
5407
6043
|
input.addEventListener('change', async () => {
|
|
5408
6044
|
const file = input.files[0];
|
|
5409
6045
|
if (!file) return;
|
|
5410
|
-
const hasUploadHandler =
|
|
6046
|
+
const hasUploadHandler = isVideo
|
|
6047
|
+
? typeof this.editor.config.videoUploadHandler === 'function'
|
|
6048
|
+
: typeof this.editor.config.imageUploadHandler === 'function';
|
|
5411
6049
|
if (hasUploadHandler) {
|
|
5412
6050
|
try {
|
|
5413
|
-
const url =
|
|
6051
|
+
const url = isVideo
|
|
6052
|
+
? await this.editor.config.videoUploadHandler(file)
|
|
6053
|
+
: await this.editor.config.imageUploadHandler(file);
|
|
5414
6054
|
if (url) this.currentImg.src = url;
|
|
5415
6055
|
} catch (err) {
|
|
5416
|
-
console.error(
|
|
6056
|
+
console.error(`NeikiEditor: ${isVideo ? 'Video' : 'Image'} upload failed`, err);
|
|
5417
6057
|
}
|
|
5418
6058
|
} else {
|
|
5419
6059
|
const reader = new FileReader();
|
|
@@ -5433,6 +6073,7 @@
|
|
|
5433
6073
|
const img = this.currentImg;
|
|
5434
6074
|
const wrapper = this.wrapper;
|
|
5435
6075
|
const contentArea = this.editor.contentArea;
|
|
6076
|
+
if (this.editor.blockDragDrop) this.editor.blockDragDrop.hideGrip();
|
|
5436
6077
|
|
|
5437
6078
|
// Save image dimensions
|
|
5438
6079
|
const imgWidth = img.style.width;
|
|
@@ -5447,6 +6088,7 @@
|
|
|
5447
6088
|
const ghostImg = img.cloneNode(true);
|
|
5448
6089
|
ghostImg.style.width = '100%';
|
|
5449
6090
|
ghostImg.style.height = 'auto';
|
|
6091
|
+
ghostImg.removeAttribute('controls');
|
|
5450
6092
|
ghost.appendChild(ghostImg);
|
|
5451
6093
|
document.body.appendChild(ghost);
|
|
5452
6094
|
|
|
@@ -5461,22 +6103,6 @@
|
|
|
5461
6103
|
// Caret marker for drop position
|
|
5462
6104
|
let dropRange = null;
|
|
5463
6105
|
|
|
5464
|
-
const getCaretRange = (x, y) => {
|
|
5465
|
-
if (document.caretRangeFromPoint) {
|
|
5466
|
-
return document.caretRangeFromPoint(x, y);
|
|
5467
|
-
}
|
|
5468
|
-
if (document.caretPositionFromPoint) {
|
|
5469
|
-
const pos = document.caretPositionFromPoint(x, y);
|
|
5470
|
-
if (pos) {
|
|
5471
|
-
const r = document.createRange();
|
|
5472
|
-
r.setStart(pos.offsetNode, pos.offset);
|
|
5473
|
-
r.collapse(true);
|
|
5474
|
-
return r;
|
|
5475
|
-
}
|
|
5476
|
-
}
|
|
5477
|
-
return null;
|
|
5478
|
-
};
|
|
5479
|
-
|
|
5480
6106
|
const onMove = (ev) => {
|
|
5481
6107
|
let cx, cy;
|
|
5482
6108
|
if (isTouch) {
|
|
@@ -5491,19 +6117,10 @@
|
|
|
5491
6117
|
}
|
|
5492
6118
|
ghost.style.left = (cx - offsetX) + 'px';
|
|
5493
6119
|
ghost.style.top = (cy - offsetY) + 'px';
|
|
5494
|
-
const range =
|
|
5495
|
-
if (
|
|
5496
|
-
// Avoid dropping inside the wrapper itself
|
|
5497
|
-
let node = range.startContainer;
|
|
5498
|
-
while (node && node !== contentArea) {
|
|
5499
|
-
if (node === wrapper) return;
|
|
5500
|
-
node = node.parentNode;
|
|
5501
|
-
}
|
|
6120
|
+
const range = this.editor.getCaretRangeFromPoint(cx, cy);
|
|
6121
|
+
if (this.isSafeDropRange(range, wrapper)) {
|
|
5502
6122
|
dropRange = range;
|
|
5503
|
-
|
|
5504
|
-
const sel = window.getSelection();
|
|
5505
|
-
sel.removeAllRanges();
|
|
5506
|
-
sel.addRange(range);
|
|
6123
|
+
this.editor.showDropIndicator(range, cx, cy);
|
|
5507
6124
|
}
|
|
5508
6125
|
};
|
|
5509
6126
|
|
|
@@ -5533,7 +6150,7 @@
|
|
|
5533
6150
|
this.sizeLabel = null;
|
|
5534
6151
|
this.imgToolbar = null;
|
|
5535
6152
|
|
|
5536
|
-
if (
|
|
6153
|
+
if (this.isSafeDropRange(dropRange, wrapper)) {
|
|
5537
6154
|
// Insert image at the caret drop position
|
|
5538
6155
|
dropRange.insertNode(img);
|
|
5539
6156
|
} else {
|
|
@@ -5545,6 +6162,7 @@
|
|
|
5545
6162
|
|
|
5546
6163
|
// Clean up empty parent blocks left behind
|
|
5547
6164
|
this.editor._ensureDefaultBlock();
|
|
6165
|
+
this.editor.removeStrayGripSvgs();
|
|
5548
6166
|
|
|
5549
6167
|
// Re-select the image at its new position
|
|
5550
6168
|
this.selectImage(img);
|
|
@@ -5563,14 +6181,23 @@
|
|
|
5563
6181
|
}
|
|
5564
6182
|
}
|
|
5565
6183
|
|
|
5566
|
-
|
|
5567
|
-
if (!this.
|
|
5568
|
-
|
|
6184
|
+
isSafeDropRange(range, wrapper) {
|
|
6185
|
+
if (!range || !this.editor.contentArea.contains(range.startContainer)) return false;
|
|
6186
|
+
let node = range.startContainer;
|
|
6187
|
+
while (node && node !== this.editor.contentArea) {
|
|
6188
|
+
if (node === wrapper) return false;
|
|
6189
|
+
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')) {
|
|
6190
|
+
return false;
|
|
6191
|
+
}
|
|
6192
|
+
this.editor.hideDropIndicator();
|
|
6193
|
+
node = node.parentNode;
|
|
6194
|
+
}
|
|
6195
|
+
return true;
|
|
6196
|
+
}
|
|
6197
|
+
|
|
6198
|
+
clearNativeSelection() {
|
|
5569
6199
|
const sel = window.getSelection();
|
|
5570
|
-
|
|
5571
|
-
range.selectNode(this.wrapper);
|
|
5572
|
-
sel.removeAllRanges();
|
|
5573
|
-
sel.addRange(range);
|
|
6200
|
+
if (sel) sel.removeAllRanges();
|
|
5574
6201
|
}
|
|
5575
6202
|
|
|
5576
6203
|
getSelectedImageClipboardData() {
|
|
@@ -5905,7 +6532,8 @@
|
|
|
5905
6532
|
}
|
|
5906
6533
|
});
|
|
5907
6534
|
|
|
5908
|
-
this.editor.contentArea.addEventListener('mouseleave', () => {
|
|
6535
|
+
this.editor.contentArea.addEventListener('mouseleave', (e) => {
|
|
6536
|
+
if (this.gripEl && e.relatedTarget && this.gripEl.contains(e.relatedTarget)) return;
|
|
5909
6537
|
if (!this.isDragging) this.hideGrip();
|
|
5910
6538
|
});
|
|
5911
6539
|
}
|
|
@@ -5934,10 +6562,10 @@
|
|
|
5934
6562
|
grip._block = block;
|
|
5935
6563
|
|
|
5936
6564
|
// Position grip
|
|
5937
|
-
const
|
|
6565
|
+
const wrapperRect = this.editor.contentWrapper.getBoundingClientRect();
|
|
5938
6566
|
const blockRect = block.getBoundingClientRect();
|
|
5939
|
-
grip.style.top = (blockRect.top -
|
|
5940
|
-
grip.style.left =
|
|
6567
|
+
grip.style.top = (blockRect.top - wrapperRect.top) + 'px';
|
|
6568
|
+
grip.style.left = Math.max(2, blockRect.left - wrapperRect.left - 28) + 'px';
|
|
5941
6569
|
|
|
5942
6570
|
grip.addEventListener('mousedown', (e) => {
|
|
5943
6571
|
e.preventDefault();
|
|
@@ -5945,7 +6573,7 @@
|
|
|
5945
6573
|
this.startDrag(e, block);
|
|
5946
6574
|
});
|
|
5947
6575
|
|
|
5948
|
-
this.editor.
|
|
6576
|
+
this.editor.contentWrapper.appendChild(grip);
|
|
5949
6577
|
this.gripEl = grip;
|
|
5950
6578
|
}
|
|
5951
6579
|
|
|
@@ -5963,6 +6591,8 @@
|
|
|
5963
6591
|
|
|
5964
6592
|
// Create ghost
|
|
5965
6593
|
this.ghostEl = block.cloneNode(true);
|
|
6594
|
+
this.editor.removeStrayGripSvgs(this.ghostEl);
|
|
6595
|
+
this.ghostEl.querySelectorAll('.neiki-block-grip, .neiki-img-toolbar, .neiki-img-resize-handle, .neiki-img-size-label').forEach(el => el.remove());
|
|
5966
6596
|
this.ghostEl.className = (this.ghostEl.className || '') + ' neiki-block-ghost';
|
|
5967
6597
|
this.ghostEl.style.width = block.offsetWidth + 'px';
|
|
5968
6598
|
document.body.appendChild(this.ghostEl);
|
|
@@ -6012,7 +6642,9 @@
|
|
|
6012
6642
|
this.placeholder = null;
|
|
6013
6643
|
this.ghostEl = null;
|
|
6014
6644
|
|
|
6645
|
+
this.editor.removeStrayGripSvgs();
|
|
6015
6646
|
this.editor.history.record();
|
|
6647
|
+
this.editor.syncToOriginal();
|
|
6016
6648
|
this.editor.triggerChange();
|
|
6017
6649
|
};
|
|
6018
6650
|
|