neiki-editor 2.9.5 → 2.10.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 +11 -10
- package/dist/neiki-editor.css +1 -1
- package/dist/neiki-editor.js +183 -12
- package/dist/neiki-editor.min.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<img src="https://img.shields.io/badge/css-%23663399.svg?style=for-the-badge&logo=css&logoColor=white" alt="CSS">
|
|
12
12
|
<br>
|
|
13
13
|
<img src="https://img.shields.io/badge/License-AGPL--3.0-2563EB?style=for-the-badge&logo=open-source-initiative&logoColor=white&labelColor=000F15&logoWidth=20" alt="License">
|
|
14
|
-
<img src="https://img.shields.io/badge/Version-2.
|
|
14
|
+
<img src="https://img.shields.io/badge/Version-2.10.0-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
@@ -62,7 +62,7 @@ Add this single line — CSS is included automatically, always the **latest vers
|
|
|
62
62
|
#### Pin a specific version
|
|
63
63
|
|
|
64
64
|
```html
|
|
65
|
-
<script src="https://cdn.neikiri.dev/neiki-editor/2.
|
|
65
|
+
<script src="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.min.js"></script>
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
#### Load CSS and JS separately
|
|
@@ -73,8 +73,8 @@ Add this single line — CSS is included automatically, always the **latest vers
|
|
|
73
73
|
<script src="https://cdn.neikiri.dev/neiki-editor/neiki-editor.js"></script>
|
|
74
74
|
|
|
75
75
|
<!-- Or pinned -->
|
|
76
|
-
<link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/2.
|
|
77
|
-
<script src="https://cdn.neikiri.dev/neiki-editor/2.
|
|
76
|
+
<link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.css">
|
|
77
|
+
<script src="https://cdn.neikiri.dev/neiki-editor/2.10.0/neiki-editor.js"></script>
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
#### Alternative CDN — jsDelivr
|
|
@@ -84,15 +84,15 @@ Add this single line — CSS is included automatically, always the **latest vers
|
|
|
84
84
|
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.min.js"></script>
|
|
85
85
|
|
|
86
86
|
<!-- Pinned -->
|
|
87
|
-
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.
|
|
87
|
+
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.min.js"></script>
|
|
88
88
|
|
|
89
89
|
<!-- Separate files (latest) -->
|
|
90
90
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.css">
|
|
91
91
|
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.js"></script>
|
|
92
92
|
|
|
93
93
|
<!-- Separate files (pinned) -->
|
|
94
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.
|
|
95
|
-
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.
|
|
94
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.css">
|
|
95
|
+
<script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@2.10.0/dist/neiki-editor.js"></script>
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
#### Package Manager
|
|
@@ -147,10 +147,10 @@ const editor = new NeikiEditor('#editor', {
|
|
|
147
147
|
language: 'en', // 'en', 'cs', or custom via addTranslation()
|
|
148
148
|
translations: null, // custom translation keys (merged with built-in)
|
|
149
149
|
autosaveKey: null, // optional custom localStorage scope for autosave
|
|
150
|
-
|
|
150
|
+
customClass: null, // optional custom CSS class for the content area
|
|
151
151
|
toolbar: [
|
|
152
152
|
'viewCode', 'undo', 'redo', 'findReplace', '|',
|
|
153
|
-
'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'removeFormat', '|',
|
|
153
|
+
'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
|
|
154
154
|
'heading', 'fontFamily', 'fontSize', '|',
|
|
155
155
|
'foreColor', 'backColor', '|',
|
|
156
156
|
'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
|
|
@@ -193,7 +193,7 @@ const editor = new NeikiEditor('#editor', {
|
|
|
193
193
|
| `onReady` | `function\|null` | `null` | Callback when editor is ready |
|
|
194
194
|
| `showHelp` | `boolean` | `true` | Show Help button in More menu (⋯) |
|
|
195
195
|
| `imageUploadHandler` | `function\|null` | `null` | Async callback `(file) => Promise<url>` for uploading images to a server/CDN instead of base64 |
|
|
196
|
-
| `
|
|
196
|
+
| `customClass` | `string\|null` | `null` | Custom CSS class appended to the editor content area (`neiki-content`) |
|
|
197
197
|
|
|
198
198
|
---
|
|
199
199
|
|
|
@@ -211,6 +211,7 @@ Use the `toolbar` array to customize which buttons appear and in what order. Use
|
|
|
211
211
|
| `strikethrough` | Strikethrough text |
|
|
212
212
|
| `subscript` | Subscript text |
|
|
213
213
|
| `superscript` | Superscript text |
|
|
214
|
+
| `code` | Toggle code formatting — inline `<code>` or `<pre><code>` block |
|
|
214
215
|
| `removeFormat` | Remove all formatting |
|
|
215
216
|
|
|
216
217
|
> **Note:** When no text is selected, formatting commands (including Remove Formatting) automatically expand to the word at the cursor position.
|
package/dist/neiki-editor.css
CHANGED
package/dist/neiki-editor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* NeikiEditor - A Modern WYSIWYG Editor
|
|
3
|
-
* Version: 2.
|
|
3
|
+
* Version: 2.10.0
|
|
4
4
|
*
|
|
5
5
|
* A lightweight, feature-rich text editor with support for:
|
|
6
6
|
* - Rich text formatting (bold, italic, underline, etc.)
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
'toolbar.autosave': 'Toggle Autosave',
|
|
64
64
|
'toolbar.themeToggle': 'Toggle Theme',
|
|
65
65
|
'toolbar.print': 'Print',
|
|
66
|
+
'toolbar.code': 'Code',
|
|
66
67
|
'toolbar.insert': 'Insert',
|
|
67
68
|
'toolbar.moreOptions': 'More options',
|
|
68
69
|
'toolbar.decreaseFontSize': 'Decrease font size',
|
|
@@ -219,6 +220,7 @@
|
|
|
219
220
|
'toolbar.autosave': 'Auto. ukládání',
|
|
220
221
|
'toolbar.themeToggle': 'Přepnout motiv',
|
|
221
222
|
'toolbar.print': 'Tisk',
|
|
223
|
+
'toolbar.code': 'Kód',
|
|
222
224
|
'toolbar.insert': 'Vložit',
|
|
223
225
|
'toolbar.moreOptions': 'Další možnosti',
|
|
224
226
|
'toolbar.decreaseFontSize': 'Zmenšit písmo',
|
|
@@ -369,6 +371,7 @@
|
|
|
369
371
|
'toolbar.autosave': '自动保存',
|
|
370
372
|
'toolbar.themeToggle': '切换主题',
|
|
371
373
|
'toolbar.print': '打印',
|
|
374
|
+
'toolbar.code': '代码',
|
|
372
375
|
'toolbar.insert': '插入',
|
|
373
376
|
'toolbar.moreOptions': '更多选项',
|
|
374
377
|
'toolbar.decreaseFontSize': '缩小字体',
|
|
@@ -494,6 +497,7 @@
|
|
|
494
497
|
'toolbar.autosave': 'Guardado automático',
|
|
495
498
|
'toolbar.themeToggle': 'Cambiar tema',
|
|
496
499
|
'toolbar.print': 'Imprimir',
|
|
500
|
+
'toolbar.code': 'Código',
|
|
497
501
|
'toolbar.insert': 'Insertar',
|
|
498
502
|
'toolbar.moreOptions': 'Más opciones',
|
|
499
503
|
'toolbar.decreaseFontSize': 'Reducir tamaño de fuente',
|
|
@@ -619,6 +623,7 @@
|
|
|
619
623
|
'toolbar.autosave': 'Automatisch speichern',
|
|
620
624
|
'toolbar.themeToggle': 'Design wechseln',
|
|
621
625
|
'toolbar.print': 'Drucken',
|
|
626
|
+
'toolbar.code': 'Code',
|
|
622
627
|
'toolbar.insert': 'Einfügen',
|
|
623
628
|
'toolbar.moreOptions': 'Weitere Optionen',
|
|
624
629
|
'toolbar.decreaseFontSize': 'Schrift verkleinern',
|
|
@@ -744,6 +749,7 @@
|
|
|
744
749
|
'toolbar.autosave': 'Sauvegarde automatique',
|
|
745
750
|
'toolbar.themeToggle': 'Changer de thème',
|
|
746
751
|
'toolbar.print': 'Imprimer',
|
|
752
|
+
'toolbar.code': 'Code',
|
|
747
753
|
'toolbar.insert': 'Insérer',
|
|
748
754
|
'toolbar.moreOptions': 'Plus d\'options',
|
|
749
755
|
'toolbar.decreaseFontSize': 'Réduire la taille de police',
|
|
@@ -869,6 +875,7 @@
|
|
|
869
875
|
'toolbar.autosave': 'Salvamento automático',
|
|
870
876
|
'toolbar.themeToggle': 'Alternar tema',
|
|
871
877
|
'toolbar.print': 'Imprimir',
|
|
878
|
+
'toolbar.code': 'Código',
|
|
872
879
|
'toolbar.insert': 'Inserir',
|
|
873
880
|
'toolbar.moreOptions': 'Mais opções',
|
|
874
881
|
'toolbar.decreaseFontSize': 'Diminuir fonte',
|
|
@@ -994,6 +1001,7 @@
|
|
|
994
1001
|
'toolbar.autosave': '自動保存',
|
|
995
1002
|
'toolbar.themeToggle': 'テーマ切替',
|
|
996
1003
|
'toolbar.print': '印刷',
|
|
1004
|
+
'toolbar.code': 'コード',
|
|
997
1005
|
'toolbar.insert': '挿入',
|
|
998
1006
|
'toolbar.moreOptions': 'その他',
|
|
999
1007
|
'toolbar.decreaseFontSize': 'フォント縮小',
|
|
@@ -1112,7 +1120,7 @@
|
|
|
1112
1120
|
const DEFAULT_CONFIG = {
|
|
1113
1121
|
toolbar: [
|
|
1114
1122
|
'viewCode', 'undo', 'redo', 'findReplace', '|',
|
|
1115
|
-
'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'removeFormat', '|',
|
|
1123
|
+
'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
|
|
1116
1124
|
'heading', 'fontFamily', 'fontSize', '|',
|
|
1117
1125
|
'foreColor', 'backColor', '|',
|
|
1118
1126
|
'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
|
|
@@ -1139,7 +1147,7 @@
|
|
|
1139
1147
|
onReady: null,
|
|
1140
1148
|
showHelp: true,
|
|
1141
1149
|
imageUploadHandler: null,
|
|
1142
|
-
|
|
1150
|
+
customClass: null
|
|
1143
1151
|
};
|
|
1144
1152
|
|
|
1145
1153
|
const TOOLBAR_ITEMS = {
|
|
@@ -1170,6 +1178,7 @@
|
|
|
1170
1178
|
horizontalRule: { icon: 'minus', titleKey: 'toolbar.horizontalRule', command: 'insertHorizontalRule' },
|
|
1171
1179
|
subscript: { icon: 'subscript', titleKey: 'toolbar.subscript', command: 'subscript' },
|
|
1172
1180
|
superscript: { icon: 'superscript', titleKey: 'toolbar.superscript', command: 'superscript' },
|
|
1181
|
+
code: { icon: 'code-inline', titleKey: 'toolbar.code', command: 'toggleCode' },
|
|
1173
1182
|
removeFormat: { icon: 'eraser', titleKey: 'toolbar.removeFormat', command: 'removeFormat' },
|
|
1174
1183
|
findReplace: { icon: 'search', titleKey: 'toolbar.findReplace', command: 'findReplace', modal: true },
|
|
1175
1184
|
emoji: { icon: 'emoji', titleKey: 'toolbar.emoji', command: 'emoji', picker: 'emoji' },
|
|
@@ -1362,16 +1371,22 @@
|
|
|
1362
1371
|
|
|
1363
1372
|
const currentParent = () => stack[stack.length - 1];
|
|
1364
1373
|
|
|
1374
|
+
const decodeEntities = (text) => {
|
|
1375
|
+
const el = document.createElement('span');
|
|
1376
|
+
el.innerHTML = text;
|
|
1377
|
+
return el.textContent;
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1365
1380
|
while (index < input.length) {
|
|
1366
1381
|
const tagStart = input.indexOf('<', index);
|
|
1367
1382
|
|
|
1368
1383
|
if (tagStart === -1) {
|
|
1369
|
-
currentParent().appendChild(document.createTextNode(input.slice(index)));
|
|
1384
|
+
currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index))));
|
|
1370
1385
|
break;
|
|
1371
1386
|
}
|
|
1372
1387
|
|
|
1373
1388
|
if (tagStart > index) {
|
|
1374
|
-
currentParent().appendChild(document.createTextNode(input.slice(index, tagStart)));
|
|
1389
|
+
currentParent().appendChild(document.createTextNode(decodeEntities(input.slice(index, tagStart))));
|
|
1375
1390
|
}
|
|
1376
1391
|
|
|
1377
1392
|
if (input.slice(tagStart, tagStart + 4) === '<!--') {
|
|
@@ -1719,6 +1734,7 @@
|
|
|
1719
1734
|
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>',
|
|
1720
1735
|
quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
|
|
1721
1736
|
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>',
|
|
1737
|
+
'code-inline': '<svg viewBox="0 0 256 256"><path d="M0 0h256v256H0z" fill="none"/><path fill="currentColor" d="M71.68 97.22L34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44m176 21.56l-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44M164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37a11.8 11.8 0 0 0 4.1.73a12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38"/></svg>',
|
|
1722
1738
|
minus: '<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>',
|
|
1723
1739
|
eraser: '<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 01-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zm-1.41 1.42L6.93 12.9l4.24 4.24 7.9-7.9-4.24-4.26z"/></svg>',
|
|
1724
1740
|
fullscreen: '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>',
|
|
@@ -2512,7 +2528,7 @@
|
|
|
2512
2528
|
<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;">
|
|
2513
2529
|
<div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
|
|
2514
2530
|
<div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
2515
|
-
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.
|
|
2531
|
+
<div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 2.10.0</div>
|
|
2516
2532
|
<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>
|
|
2517
2533
|
<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>
|
|
2518
2534
|
</div>
|
|
@@ -2971,6 +2987,112 @@
|
|
|
2971
2987
|
subscript() { this.exec('subscript'); }
|
|
2972
2988
|
superscript() { this.exec('superscript'); }
|
|
2973
2989
|
|
|
2990
|
+
toggleCode() {
|
|
2991
|
+
const sel = window.getSelection();
|
|
2992
|
+
if (!sel || !sel.rangeCount) return;
|
|
2993
|
+
|
|
2994
|
+
const range = sel.getRangeAt(0);
|
|
2995
|
+
|
|
2996
|
+
// Detect if selection is inside or contains <pre> or <code>
|
|
2997
|
+
const findCodeAncestor = (n) => {
|
|
2998
|
+
if (n.nodeType === Node.TEXT_NODE) n = n.parentNode;
|
|
2999
|
+
if (!n || !n.closest) return null;
|
|
3000
|
+
const pre = n.closest('pre');
|
|
3001
|
+
if (pre && this.editor.contentArea.contains(pre)) return { type: 'pre', el: pre };
|
|
3002
|
+
const code = n.closest('code');
|
|
3003
|
+
if (code && this.editor.contentArea.contains(code)) return { type: 'code', el: code };
|
|
3004
|
+
return null;
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
// Check start, end, and common ancestor
|
|
3008
|
+
const fromStart = findCodeAncestor(range.startContainer);
|
|
3009
|
+
const fromEnd = findCodeAncestor(range.endContainer);
|
|
3010
|
+
const fromCommon = findCodeAncestor(range.commonAncestorContainer);
|
|
3011
|
+
const existing = fromStart || fromEnd || fromCommon;
|
|
3012
|
+
|
|
3013
|
+
// Also check if selection contains <code> elements inside it
|
|
3014
|
+
let containedCode = null;
|
|
3015
|
+
if (!existing && !range.collapsed) {
|
|
3016
|
+
const container = range.commonAncestorContainer;
|
|
3017
|
+
const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
|
|
3018
|
+
if (el) containedCode = el.querySelector ? el.querySelector('code') : null;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
if (existing && existing.type === 'pre') {
|
|
3022
|
+
// Unwrap <pre><code> block: convert back to <p> paragraphs
|
|
3023
|
+
const pre = existing.el;
|
|
3024
|
+
const lines = pre.textContent.split('\n');
|
|
3025
|
+
const fragment = document.createDocumentFragment();
|
|
3026
|
+
lines.forEach(line => {
|
|
3027
|
+
const p = document.createElement('p');
|
|
3028
|
+
p.textContent = line || '\u00A0';
|
|
3029
|
+
fragment.appendChild(p);
|
|
3030
|
+
});
|
|
3031
|
+
pre.parentNode.replaceChild(fragment, pre);
|
|
3032
|
+
} else if (existing && existing.type === 'code') {
|
|
3033
|
+
// Unwrap inline <code>
|
|
3034
|
+
const codeEl = existing.el;
|
|
3035
|
+
const parent = codeEl.parentNode;
|
|
3036
|
+
while (codeEl.firstChild) {
|
|
3037
|
+
parent.insertBefore(codeEl.firstChild, codeEl);
|
|
3038
|
+
}
|
|
3039
|
+
parent.removeChild(codeEl);
|
|
3040
|
+
} else if (containedCode) {
|
|
3041
|
+
// Selection contains <code> elements — unwrap them all
|
|
3042
|
+
const container = range.commonAncestorContainer;
|
|
3043
|
+
const el = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
|
|
3044
|
+
const codes = el.querySelectorAll('code');
|
|
3045
|
+
codes.forEach(codeEl => {
|
|
3046
|
+
if (this.editor.contentArea.contains(codeEl)) {
|
|
3047
|
+
const p = codeEl.parentNode;
|
|
3048
|
+
while (codeEl.firstChild) p.insertBefore(codeEl.firstChild, codeEl);
|
|
3049
|
+
p.removeChild(codeEl);
|
|
3050
|
+
}
|
|
3051
|
+
});
|
|
3052
|
+
} else {
|
|
3053
|
+
// Apply code formatting
|
|
3054
|
+
if (sel.isCollapsed) this._expandToWordIfCollapsed();
|
|
3055
|
+
|
|
3056
|
+
const startBlock = this._getBlockParent(range.startContainer);
|
|
3057
|
+
const endBlock = this._getBlockParent(range.endContainer);
|
|
3058
|
+
|
|
3059
|
+
if (startBlock && endBlock && startBlock !== endBlock) {
|
|
3060
|
+
// Multi-block selection: create a <pre><code> block
|
|
3061
|
+
const blocks = [];
|
|
3062
|
+
let cur = startBlock;
|
|
3063
|
+
while (cur) {
|
|
3064
|
+
blocks.push(cur);
|
|
3065
|
+
if (cur === endBlock) break;
|
|
3066
|
+
cur = cur.nextElementSibling;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
const textLines = blocks.map(block => block.textContent);
|
|
3070
|
+
const pre = document.createElement('pre');
|
|
3071
|
+
const code = document.createElement('code');
|
|
3072
|
+
code.textContent = textLines.join('\n');
|
|
3073
|
+
pre.appendChild(code);
|
|
3074
|
+
|
|
3075
|
+
startBlock.parentNode.insertBefore(pre, startBlock);
|
|
3076
|
+
blocks.forEach(block => block.parentNode.removeChild(block));
|
|
3077
|
+
} else {
|
|
3078
|
+
// Single block: inline <code> wrap
|
|
3079
|
+
this.editor.wrapSelection('code');
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
this.editor.history.record();
|
|
3083
|
+
this.editor.updateToolbar();
|
|
3084
|
+
this.editor.triggerChange();
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
_getBlockParent(node) {
|
|
3088
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
|
|
3089
|
+
while (node && node !== this.editor.contentArea) {
|
|
3090
|
+
if (node.parentNode === this.editor.contentArea) return node;
|
|
3091
|
+
node = node.parentNode;
|
|
3092
|
+
}
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
2974
3096
|
justifyLeft() { this.exec('justifyLeft'); }
|
|
2975
3097
|
justifyCenter() { this.exec('justifyCenter'); }
|
|
2976
3098
|
justifyRight() { this.exec('justifyRight'); }
|
|
@@ -3228,6 +3350,11 @@
|
|
|
3228
3350
|
'neiki_' + (typeof element === 'string' ? element.replace(/[^a-zA-Z0-9]/g, '_') : 'editor');
|
|
3229
3351
|
|
|
3230
3352
|
this.config = Utils.deepMerge(DEFAULT_CONFIG, options);
|
|
3353
|
+
|
|
3354
|
+
// Backward compatibility: support old custom_class option
|
|
3355
|
+
if (this.config.custom_class && !this.config.customClass) {
|
|
3356
|
+
this.config.customClass = this.config.custom_class;
|
|
3357
|
+
}
|
|
3231
3358
|
this.instanceIndex = ++EDITOR_INSTANCE_COUNTER;
|
|
3232
3359
|
this.storageId = this.createAutosaveStorageId(element);
|
|
3233
3360
|
this.isFullscreen = false;
|
|
@@ -3815,8 +3942,8 @@
|
|
|
3815
3942
|
createContentArea() {
|
|
3816
3943
|
this.contentWrapper = Utils.createElement('div', { className: 'neiki-content-wrapper' });
|
|
3817
3944
|
|
|
3818
|
-
const contentClasses = this.config.
|
|
3819
|
-
? `neiki-content ${this.config.
|
|
3945
|
+
const contentClasses = this.config.customClass
|
|
3946
|
+
? `neiki-content ${this.config.customClass}`
|
|
3820
3947
|
: 'neiki-content';
|
|
3821
3948
|
|
|
3822
3949
|
this.contentArea = Utils.createElement('div', {
|
|
@@ -4179,6 +4306,17 @@
|
|
|
4179
4306
|
case 'justifyFull':
|
|
4180
4307
|
isActive = document.queryCommandState(config.command);
|
|
4181
4308
|
break;
|
|
4309
|
+
case 'toggleCode': {
|
|
4310
|
+
const sel = window.getSelection();
|
|
4311
|
+
if (sel && sel.rangeCount) {
|
|
4312
|
+
let node = sel.getRangeAt(0).startContainer;
|
|
4313
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
|
|
4314
|
+
const inCode = node.closest && node.closest('code') && this.contentArea.contains(node.closest('code'));
|
|
4315
|
+
const inPre = node.closest && node.closest('pre') && this.contentArea.contains(node.closest('pre'));
|
|
4316
|
+
isActive = !!(inCode || inPre);
|
|
4317
|
+
}
|
|
4318
|
+
break;
|
|
4319
|
+
}
|
|
4182
4320
|
case 'formatBlock':
|
|
4183
4321
|
if (config.value === 'blockquote') {
|
|
4184
4322
|
const sel = window.getSelection();
|
|
@@ -4372,15 +4510,48 @@
|
|
|
4372
4510
|
} catch (e) {}
|
|
4373
4511
|
return;
|
|
4374
4512
|
}
|
|
4375
|
-
// Wrap bare text nodes
|
|
4513
|
+
// Wrap bare text nodes and normalize non-block elements into <p>
|
|
4514
|
+
const BLOCK_TAGS = new Set(['P','H1','H2','H3','H4','H5','H6','UL','OL','BLOCKQUOTE','TABLE','HR','PRE','FIGURE','SECTION','ARTICLE']);
|
|
4376
4515
|
const childNodes = Array.from(this.contentArea.childNodes);
|
|
4516
|
+
|
|
4517
|
+
// First pass: convert stray <div> to <p>
|
|
4377
4518
|
childNodes.forEach(node => {
|
|
4378
|
-
if (node.nodeType === Node.
|
|
4519
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'DIV') {
|
|
4379
4520
|
const p = document.createElement('p');
|
|
4380
|
-
node.
|
|
4381
|
-
|
|
4521
|
+
while (node.firstChild) p.appendChild(node.firstChild);
|
|
4522
|
+
node.parentNode.replaceChild(p, node);
|
|
4382
4523
|
}
|
|
4383
4524
|
});
|
|
4525
|
+
|
|
4526
|
+
// Second pass: group consecutive inline/text nodes into <p>
|
|
4527
|
+
const updatedNodes = Array.from(this.contentArea.childNodes);
|
|
4528
|
+
let inlineGroup = [];
|
|
4529
|
+
|
|
4530
|
+
const flushGroup = () => {
|
|
4531
|
+
if (inlineGroup.length === 0) return;
|
|
4532
|
+
const p = document.createElement('p');
|
|
4533
|
+
const firstNode = inlineGroup[0];
|
|
4534
|
+
this.contentArea.insertBefore(p, firstNode);
|
|
4535
|
+
inlineGroup.forEach(n => p.appendChild(n));
|
|
4536
|
+
inlineGroup = [];
|
|
4537
|
+
};
|
|
4538
|
+
|
|
4539
|
+
updatedNodes.forEach(node => {
|
|
4540
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
4541
|
+
if (node.textContent.trim()) {
|
|
4542
|
+
inlineGroup.push(node);
|
|
4543
|
+
} else if (inlineGroup.length > 0) {
|
|
4544
|
+
inlineGroup.push(node);
|
|
4545
|
+
}
|
|
4546
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4547
|
+
if (BLOCK_TAGS.has(node.tagName)) {
|
|
4548
|
+
flushGroup();
|
|
4549
|
+
} else {
|
|
4550
|
+
inlineGroup.push(node);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
});
|
|
4554
|
+
flushGroup();
|
|
4384
4555
|
}
|
|
4385
4556
|
|
|
4386
4557
|
syncToOriginal() {
|