neiki-editor 2.10.1 → 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 +759 -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.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,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.0</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,85 @@
|
|
|
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 = 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
|
+
|
|
4730
5199
|
toggleCodeView() {
|
|
4731
5200
|
if (!this.isCodeViewOpen) {
|
|
4732
|
-
this.codeViewTextarea.value = this.contentArea.innerHTML;
|
|
5201
|
+
this.codeViewTextarea.value = this.formatHTMLSource(this.contentArea.innerHTML);
|
|
5202
|
+
this.renderCodeViewHighlight();
|
|
4733
5203
|
this.codeView.classList.add('show');
|
|
4734
5204
|
this.isCodeViewOpen = true;
|
|
4735
5205
|
this.codeViewTextarea.focus();
|
|
@@ -5045,6 +5515,131 @@
|
|
|
5045
5515
|
// DRAG & DROP
|
|
5046
5516
|
// ============================================
|
|
5047
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
|
+
|
|
5048
5643
|
initDragDrop() {
|
|
5049
5644
|
let dragCounter = 0;
|
|
5050
5645
|
|
|
@@ -5061,28 +5656,39 @@
|
|
|
5061
5656
|
dragCounter--;
|
|
5062
5657
|
if (dragCounter === 0) {
|
|
5063
5658
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5659
|
+
this.hideDropIndicator();
|
|
5064
5660
|
}
|
|
5065
5661
|
});
|
|
5066
5662
|
|
|
5067
5663
|
this.contentArea.addEventListener('dragover', (e) => {
|
|
5068
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
|
+
}
|
|
5069
5671
|
});
|
|
5070
5672
|
|
|
5071
5673
|
this.contentArea.addEventListener('drop', (e) => {
|
|
5072
5674
|
e.preventDefault();
|
|
5073
5675
|
dragCounter = 0;
|
|
5074
5676
|
this.contentArea.classList.remove('neiki-drag-over');
|
|
5677
|
+
this.hideDropIndicator();
|
|
5678
|
+
|
|
5679
|
+
if (this.handleSelectionDrop(e)) return;
|
|
5075
5680
|
|
|
5076
5681
|
const files = Array.from(e.dataTransfer.files);
|
|
5077
5682
|
const imageFiles = files.filter(file => file.type.startsWith('image/'));
|
|
5683
|
+
const videoFiles = files.filter(file => file.type.startsWith('video/'));
|
|
5078
5684
|
|
|
5079
|
-
if (imageFiles.length > 0) {
|
|
5685
|
+
if (imageFiles.length > 0 || videoFiles.length > 0) {
|
|
5080
5686
|
// Get cursor position from drop event
|
|
5081
5687
|
const dropX = e.clientX;
|
|
5082
5688
|
const dropY = e.clientY;
|
|
5083
5689
|
|
|
5084
5690
|
const setCursorAtDrop = () => {
|
|
5085
|
-
const range =
|
|
5691
|
+
const range = this.getCaretRangeFromPoint(dropX, dropY);
|
|
5086
5692
|
if (range) {
|
|
5087
5693
|
const sel = window.getSelection();
|
|
5088
5694
|
sel.removeAllRanges();
|
|
@@ -5090,30 +5696,53 @@
|
|
|
5090
5696
|
}
|
|
5091
5697
|
};
|
|
5092
5698
|
|
|
5093
|
-
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';
|
|
5094
5704
|
|
|
5095
|
-
|
|
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') {
|
|
5096
5727
|
(async () => {
|
|
5097
5728
|
for (const file of imageFiles) {
|
|
5098
5729
|
try {
|
|
5099
|
-
|
|
5100
|
-
const url = await this.config.imageUploadHandler(file);
|
|
5101
|
-
if (url) {
|
|
5102
|
-
this.commands.insertImage(url, file.name, '');
|
|
5103
|
-
}
|
|
5730
|
+
await insertFile(file, 'image');
|
|
5104
5731
|
} catch (err) {
|
|
5105
5732
|
console.error('NeikiEditor: Image upload failed', err);
|
|
5106
5733
|
}
|
|
5107
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
|
+
}
|
|
5108
5742
|
})();
|
|
5109
5743
|
} 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);
|
|
5744
|
+
[...imageFiles.map(file => [file, 'image']), ...videoFiles.map(file => [file, 'video'])].forEach(([file, type]) => {
|
|
5745
|
+
insertFile(file, type);
|
|
5117
5746
|
});
|
|
5118
5747
|
}
|
|
5119
5748
|
}
|
|
@@ -5146,8 +5775,8 @@
|
|
|
5146
5775
|
this._dragStarted = false;
|
|
5147
5776
|
|
|
5148
5777
|
this.editor.contentArea.addEventListener('mousedown', (e) => {
|
|
5149
|
-
const img = e.target.closest('img');
|
|
5150
|
-
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;
|
|
5151
5780
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5152
5781
|
|
|
5153
5782
|
e.preventDefault();
|
|
@@ -5192,8 +5821,8 @@
|
|
|
5192
5821
|
|
|
5193
5822
|
this.editor.contentArea.addEventListener('click', (e) => {
|
|
5194
5823
|
if (this._dragStarted) return;
|
|
5195
|
-
const img = e.target.closest('img');
|
|
5196
|
-
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)) {
|
|
5197
5826
|
e.preventDefault();
|
|
5198
5827
|
if (!this.wrapper || this.currentImg !== img) {
|
|
5199
5828
|
this.selectImage(img);
|
|
@@ -5205,8 +5834,8 @@
|
|
|
5205
5834
|
|
|
5206
5835
|
// Touch: tap image to select (drag only via grip handle in img toolbar)
|
|
5207
5836
|
this.editor.contentArea.addEventListener('touchend', (e) => {
|
|
5208
|
-
const img = e.target.closest('img');
|
|
5209
|
-
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;
|
|
5210
5839
|
if (e.target.closest('.neiki-img-resize-handle') || e.target.closest('.neiki-img-toolbar')) return;
|
|
5211
5840
|
e.preventDefault();
|
|
5212
5841
|
if (!this.wrapper || this.currentImg !== img) {
|
|
@@ -5216,7 +5845,7 @@
|
|
|
5216
5845
|
|
|
5217
5846
|
// Prevent native image drag inside editor (causes duplicate on drop)
|
|
5218
5847
|
this.editor.contentArea.addEventListener('dragstart', (e) => {
|
|
5219
|
-
if (e.target.tagName === 'IMG') {
|
|
5848
|
+
if (e.target.tagName === 'IMG' || e.target.tagName === 'VIDEO') {
|
|
5220
5849
|
e.preventDefault();
|
|
5221
5850
|
}
|
|
5222
5851
|
});
|
|
@@ -5234,9 +5863,14 @@
|
|
|
5234
5863
|
});
|
|
5235
5864
|
}
|
|
5236
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
|
+
|
|
5237
5870
|
selectImage(img) {
|
|
5238
5871
|
this.deselect();
|
|
5239
5872
|
this.currentImg = img;
|
|
5873
|
+
const isVideo = img.tagName === 'VIDEO';
|
|
5240
5874
|
|
|
5241
5875
|
// Create wrapper around image
|
|
5242
5876
|
this.wrapper = document.createElement('span');
|
|
@@ -5323,7 +5957,7 @@
|
|
|
5323
5957
|
const replaceBtn = document.createElement('button');
|
|
5324
5958
|
replaceBtn.className = 'neiki-img-toolbar-btn';
|
|
5325
5959
|
replaceBtn.type = 'button';
|
|
5326
|
-
replaceBtn.title = t('imageToolbar.replaceImage');
|
|
5960
|
+
replaceBtn.title = isVideo ? t('videoToolbar.replaceVideo') : t('imageToolbar.replaceImage');
|
|
5327
5961
|
replaceBtn.innerHTML = Icons.replaceImage;
|
|
5328
5962
|
replaceBtn.addEventListener('click', (e) => {
|
|
5329
5963
|
e.preventDefault();
|
|
@@ -5334,7 +5968,7 @@
|
|
|
5334
5968
|
const deleteBtn = document.createElement('button');
|
|
5335
5969
|
deleteBtn.className = 'neiki-img-toolbar-btn neiki-img-toolbar-btn-danger';
|
|
5336
5970
|
deleteBtn.type = 'button';
|
|
5337
|
-
deleteBtn.title = t('imageToolbar.deleteImage');
|
|
5971
|
+
deleteBtn.title = isVideo ? t('videoToolbar.deleteVideo') : t('imageToolbar.deleteImage');
|
|
5338
5972
|
deleteBtn.innerHTML = Icons.trash;
|
|
5339
5973
|
deleteBtn.addEventListener('click', (e) => {
|
|
5340
5974
|
e.preventDefault();
|
|
@@ -5351,7 +5985,7 @@
|
|
|
5351
5985
|
this.wrapper.appendChild(this.imgToolbar);
|
|
5352
5986
|
|
|
5353
5987
|
this.positionImgToolbar();
|
|
5354
|
-
this.
|
|
5988
|
+
this.clearNativeSelection();
|
|
5355
5989
|
}
|
|
5356
5990
|
|
|
5357
5991
|
getImageBlock() {
|
|
@@ -5402,19 +6036,24 @@
|
|
|
5402
6036
|
|
|
5403
6037
|
replaceImage() {
|
|
5404
6038
|
if (!this.currentImg) return;
|
|
6039
|
+
const isVideo = this.currentImg.tagName === 'VIDEO';
|
|
5405
6040
|
const input = document.createElement('input');
|
|
5406
6041
|
input.type = 'file';
|
|
5407
|
-
input.accept = 'image/*';
|
|
6042
|
+
input.accept = isVideo ? 'video/*' : 'image/*';
|
|
5408
6043
|
input.addEventListener('change', async () => {
|
|
5409
6044
|
const file = input.files[0];
|
|
5410
6045
|
if (!file) return;
|
|
5411
|
-
const hasUploadHandler =
|
|
6046
|
+
const hasUploadHandler = isVideo
|
|
6047
|
+
? typeof this.editor.config.videoUploadHandler === 'function'
|
|
6048
|
+
: typeof this.editor.config.imageUploadHandler === 'function';
|
|
5412
6049
|
if (hasUploadHandler) {
|
|
5413
6050
|
try {
|
|
5414
|
-
const url =
|
|
6051
|
+
const url = isVideo
|
|
6052
|
+
? await this.editor.config.videoUploadHandler(file)
|
|
6053
|
+
: await this.editor.config.imageUploadHandler(file);
|
|
5415
6054
|
if (url) this.currentImg.src = url;
|
|
5416
6055
|
} catch (err) {
|
|
5417
|
-
console.error(
|
|
6056
|
+
console.error(`NeikiEditor: ${isVideo ? 'Video' : 'Image'} upload failed`, err);
|
|
5418
6057
|
}
|
|
5419
6058
|
} else {
|
|
5420
6059
|
const reader = new FileReader();
|
|
@@ -5434,6 +6073,7 @@
|
|
|
5434
6073
|
const img = this.currentImg;
|
|
5435
6074
|
const wrapper = this.wrapper;
|
|
5436
6075
|
const contentArea = this.editor.contentArea;
|
|
6076
|
+
if (this.editor.blockDragDrop) this.editor.blockDragDrop.hideGrip();
|
|
5437
6077
|
|
|
5438
6078
|
// Save image dimensions
|
|
5439
6079
|
const imgWidth = img.style.width;
|
|
@@ -5448,6 +6088,7 @@
|
|
|
5448
6088
|
const ghostImg = img.cloneNode(true);
|
|
5449
6089
|
ghostImg.style.width = '100%';
|
|
5450
6090
|
ghostImg.style.height = 'auto';
|
|
6091
|
+
ghostImg.removeAttribute('controls');
|
|
5451
6092
|
ghost.appendChild(ghostImg);
|
|
5452
6093
|
document.body.appendChild(ghost);
|
|
5453
6094
|
|
|
@@ -5462,22 +6103,6 @@
|
|
|
5462
6103
|
// Caret marker for drop position
|
|
5463
6104
|
let dropRange = null;
|
|
5464
6105
|
|
|
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
6106
|
const onMove = (ev) => {
|
|
5482
6107
|
let cx, cy;
|
|
5483
6108
|
if (isTouch) {
|
|
@@ -5492,19 +6117,10 @@
|
|
|
5492
6117
|
}
|
|
5493
6118
|
ghost.style.left = (cx - offsetX) + 'px';
|
|
5494
6119
|
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
|
-
}
|
|
6120
|
+
const range = this.editor.getCaretRangeFromPoint(cx, cy);
|
|
6121
|
+
if (this.isSafeDropRange(range, wrapper)) {
|
|
5503
6122
|
dropRange = range;
|
|
5504
|
-
|
|
5505
|
-
const sel = window.getSelection();
|
|
5506
|
-
sel.removeAllRanges();
|
|
5507
|
-
sel.addRange(range);
|
|
6123
|
+
this.editor.showDropIndicator(range, cx, cy);
|
|
5508
6124
|
}
|
|
5509
6125
|
};
|
|
5510
6126
|
|
|
@@ -5534,7 +6150,7 @@
|
|
|
5534
6150
|
this.sizeLabel = null;
|
|
5535
6151
|
this.imgToolbar = null;
|
|
5536
6152
|
|
|
5537
|
-
if (
|
|
6153
|
+
if (this.isSafeDropRange(dropRange, wrapper)) {
|
|
5538
6154
|
// Insert image at the caret drop position
|
|
5539
6155
|
dropRange.insertNode(img);
|
|
5540
6156
|
} else {
|
|
@@ -5546,6 +6162,7 @@
|
|
|
5546
6162
|
|
|
5547
6163
|
// Clean up empty parent blocks left behind
|
|
5548
6164
|
this.editor._ensureDefaultBlock();
|
|
6165
|
+
this.editor.removeStrayGripSvgs();
|
|
5549
6166
|
|
|
5550
6167
|
// Re-select the image at its new position
|
|
5551
6168
|
this.selectImage(img);
|
|
@@ -5564,14 +6181,23 @@
|
|
|
5564
6181
|
}
|
|
5565
6182
|
}
|
|
5566
6183
|
|
|
5567
|
-
|
|
5568
|
-
if (!this.
|
|
5569
|
-
|
|
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() {
|
|
5570
6199
|
const sel = window.getSelection();
|
|
5571
|
-
|
|
5572
|
-
range.selectNode(this.wrapper);
|
|
5573
|
-
sel.removeAllRanges();
|
|
5574
|
-
sel.addRange(range);
|
|
6200
|
+
if (sel) sel.removeAllRanges();
|
|
5575
6201
|
}
|
|
5576
6202
|
|
|
5577
6203
|
getSelectedImageClipboardData() {
|
|
@@ -5906,7 +6532,8 @@
|
|
|
5906
6532
|
}
|
|
5907
6533
|
});
|
|
5908
6534
|
|
|
5909
|
-
this.editor.contentArea.addEventListener('mouseleave', () => {
|
|
6535
|
+
this.editor.contentArea.addEventListener('mouseleave', (e) => {
|
|
6536
|
+
if (this.gripEl && e.relatedTarget && this.gripEl.contains(e.relatedTarget)) return;
|
|
5910
6537
|
if (!this.isDragging) this.hideGrip();
|
|
5911
6538
|
});
|
|
5912
6539
|
}
|
|
@@ -5935,10 +6562,10 @@
|
|
|
5935
6562
|
grip._block = block;
|
|
5936
6563
|
|
|
5937
6564
|
// Position grip
|
|
5938
|
-
const
|
|
6565
|
+
const wrapperRect = this.editor.contentWrapper.getBoundingClientRect();
|
|
5939
6566
|
const blockRect = block.getBoundingClientRect();
|
|
5940
|
-
grip.style.top = (blockRect.top -
|
|
5941
|
-
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';
|
|
5942
6569
|
|
|
5943
6570
|
grip.addEventListener('mousedown', (e) => {
|
|
5944
6571
|
e.preventDefault();
|
|
@@ -5946,7 +6573,7 @@
|
|
|
5946
6573
|
this.startDrag(e, block);
|
|
5947
6574
|
});
|
|
5948
6575
|
|
|
5949
|
-
this.editor.
|
|
6576
|
+
this.editor.contentWrapper.appendChild(grip);
|
|
5950
6577
|
this.gripEl = grip;
|
|
5951
6578
|
}
|
|
5952
6579
|
|
|
@@ -5964,6 +6591,8 @@
|
|
|
5964
6591
|
|
|
5965
6592
|
// Create ghost
|
|
5966
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());
|
|
5967
6596
|
this.ghostEl.className = (this.ghostEl.className || '') + ' neiki-block-ghost';
|
|
5968
6597
|
this.ghostEl.style.width = block.offsetWidth + 'px';
|
|
5969
6598
|
document.body.appendChild(this.ghostEl);
|
|
@@ -6013,7 +6642,9 @@
|
|
|
6013
6642
|
this.placeholder = null;
|
|
6014
6643
|
this.ghostEl = null;
|
|
6015
6644
|
|
|
6645
|
+
this.editor.removeStrayGripSvgs();
|
|
6016
6646
|
this.editor.history.record();
|
|
6647
|
+
this.editor.syncToOriginal();
|
|
6017
6648
|
this.editor.triggerChange();
|
|
6018
6649
|
};
|
|
6019
6650
|
|