neiki-editor 3.7.0 → 3.8.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 CHANGED
@@ -11,7 +11,7 @@
11
11
  <img src="https://img.shields.io/badge/web%20components-29ABE2.svg?style=for-the-badge&logo=webcomponentsdotorg&logoColor=white" alt="Web Components">
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-3.7.0-2563EB?style=for-the-badge&logo=semantic-release&logoColor=white&labelColor=000F15&logoWidth=20" alt="Version">
14
+ <img src="https://img.shields.io/badge/Version-3.8.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">
@@ -140,7 +140,7 @@ The recommended install is the single bundled script from the CDN. CSS is includ
140
140
  **Pin a specific version**
141
141
 
142
142
  ```html
143
- <script src="https://cdn.neikiri.dev/neiki-editor/3.7.0/neiki-editor.min.js"></script>
143
+ <script src="https://cdn.neikiri.dev/neiki-editor/3.8.0/neiki-editor.min.js"></script>
144
144
  ```
145
145
 
146
146
  **Load CSS and JS separately**
@@ -151,8 +151,8 @@ The recommended install is the single bundled script from the CDN. CSS is includ
151
151
  <script src="https://cdn.neikiri.dev/neiki-editor/neiki-editor.js"></script>
152
152
 
153
153
  <!-- Or pinned -->
154
- <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/3.7.0/neiki-editor.css">
155
- <script src="https://cdn.neikiri.dev/neiki-editor/3.7.0/neiki-editor.js"></script>
154
+ <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/3.8.0/neiki-editor.css">
155
+ <script src="https://cdn.neikiri.dev/neiki-editor/3.8.0/neiki-editor.js"></script>
156
156
  ```
157
157
 
158
158
  **Alternative CDN — jsDelivr**
@@ -160,7 +160,7 @@ The recommended install is the single bundled script from the CDN. CSS is includ
160
160
  ```html
161
161
  <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@latest/dist/neiki-editor.min.js"></script>
162
162
  <!-- Pinned -->
163
- <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@3.7.0/dist/neiki-editor.min.js"></script>
163
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@3.8.0/dist/neiki-editor.min.js"></script>
164
164
  ```
165
165
 
166
166
  **Package manager**
@@ -293,7 +293,7 @@ new NeikiEditor('#editor', {
293
293
  });
294
294
  ```
295
295
 
296
- Available identifiers include text formatting (`bold`, `italic`, `underline`, `strikethrough`, `subscript`, `superscript`, `code`, `removeFormat`), style (`heading`, `fontFamily`, `fontSize`, `foreColor`, `backColor`), alignment and lists (`alignLeft`, `alignCenter`, `alignRight`, `alignJustify`, `bulletList`, `numberedList`, `indent`, `outdent`), structure (`blockquote`, `horizontalRule`), tools (`undo`, `redo`, `findReplace`, `viewCode`), the grouped `insertDropdown`, the right-aligned `moreMenu`, and a standalone `themeToggle`. See the [Toolbar Reference](https://github.com/neikiri/neiki-editor/wiki/Toolbar-Reference) for the full list.
296
+ Available identifiers include text formatting (`bold`, `italic`, `underline`, `strikethrough`, `subscript`, `superscript`, `code`, `formatPainter`, `removeFormat`), style (`heading`, `fontFamily`, `fontSize`, `foreColor`, `backColor`), alignment and lists (`alignLeft`, `alignCenter`, `alignRight`, `alignJustify`, `bulletList`, `numberedList`, `indent`, `outdent`), structure (`blockquote`, `horizontalRule`), tools (`undo`, `redo`, `findReplace`, `viewCode`), the grouped `insertDropdown`, the right-aligned `moreMenu`, and a standalone `themeToggle`. See the [Toolbar Reference](https://github.com/neikiri/neiki-editor/wiki/Toolbar-Reference) for the full list.
297
297
 
298
298
  ### Customizing the floating toolbar
299
299
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * NeikiEditor - Production-Ready WYSIWYG Rich Text Editor
3
3
  * CSS Stylesheet
4
- * Version: 3.7.0
4
+ * Version: 3.8.0
5
5
  */
6
6
 
7
7
  /* ============================================
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NeikiEditor - A Modern WYSIWYG Editor
3
- * Version: 3.7.0
3
+ * Version: 3.8.0
4
4
  *
5
5
  * A lightweight, feature-rich text editor with support for:
6
6
  * - Rich text formatting (bold, italic, underline, etc.)
@@ -60,6 +60,7 @@
60
60
  'toolbar.horizontalRule': 'Horizontal Line',
61
61
  'toolbar.subscript': 'Subscript',
62
62
  'toolbar.superscript': 'Superscript',
63
+ 'toolbar.formatPainter': 'Copy Formatting',
63
64
  'toolbar.removeFormat': 'Remove Formatting',
64
65
  'toolbar.findReplace': 'Find & Replace',
65
66
  'toolbar.emoji': 'Insert Emoji',
@@ -260,6 +261,7 @@
260
261
  'toolbar.horizontalRule': 'Vodorovná čára',
261
262
  'toolbar.subscript': 'Dolní index',
262
263
  'toolbar.superscript': 'Horní index',
264
+ 'toolbar.formatPainter': 'Kopírovat formátování',
263
265
  'toolbar.removeFormat': 'Odstranit formátování',
264
266
  'toolbar.findReplace': 'Najít a nahradit',
265
267
  'toolbar.emoji': 'Vložit emoji',
@@ -453,6 +455,7 @@
453
455
  'toolbar.horizontalRule': '水平线',
454
456
  'toolbar.subscript': '下标',
455
457
  'toolbar.superscript': '上标',
458
+ 'toolbar.formatPainter': '复制格式',
456
459
  'toolbar.removeFormat': '清除格式',
457
460
  'toolbar.findReplace': '查找和替换',
458
461
  'toolbar.emoji': '插入表情',
@@ -621,6 +624,7 @@
621
624
  'toolbar.horizontalRule': 'Línea horizontal',
622
625
  'toolbar.subscript': 'Subíndice',
623
626
  'toolbar.superscript': 'Superíndice',
627
+ 'toolbar.formatPainter': 'Copiar formato',
624
628
  'toolbar.removeFormat': 'Eliminar formato',
625
629
  'toolbar.findReplace': 'Buscar y reemplazar',
626
630
  'toolbar.emoji': 'Insertar emoji',
@@ -789,6 +793,7 @@
789
793
  'toolbar.horizontalRule': 'Horizontale Linie',
790
794
  'toolbar.subscript': 'Tiefgestellt',
791
795
  'toolbar.superscript': 'Hochgestellt',
796
+ 'toolbar.formatPainter': 'Format kopieren',
792
797
  'toolbar.removeFormat': 'Formatierung entfernen',
793
798
  'toolbar.findReplace': 'Suchen und Ersetzen',
794
799
  'toolbar.emoji': 'Emoji einfügen',
@@ -957,6 +962,7 @@
957
962
  'toolbar.horizontalRule': 'Ligne horizontale',
958
963
  'toolbar.subscript': 'Indice',
959
964
  'toolbar.superscript': 'Exposant',
965
+ 'toolbar.formatPainter': 'Copier la mise en forme',
960
966
  'toolbar.removeFormat': 'Supprimer la mise en forme',
961
967
  'toolbar.findReplace': 'Rechercher et remplacer',
962
968
  'toolbar.emoji': 'Insérer un emoji',
@@ -1125,6 +1131,7 @@
1125
1131
  'toolbar.horizontalRule': 'Linha horizontal',
1126
1132
  'toolbar.subscript': 'Subscrito',
1127
1133
  'toolbar.superscript': 'Sobrescrito',
1134
+ 'toolbar.formatPainter': 'Copiar formatação',
1128
1135
  'toolbar.removeFormat': 'Remover formatação',
1129
1136
  'toolbar.findReplace': 'Localizar e substituir',
1130
1137
  'toolbar.emoji': 'Inserir emoji',
@@ -1293,6 +1300,7 @@
1293
1300
  'toolbar.horizontalRule': '水平線',
1294
1301
  'toolbar.subscript': '下付き',
1295
1302
  'toolbar.superscript': '上付き',
1303
+ 'toolbar.formatPainter': '書式のコピー',
1296
1304
  'toolbar.removeFormat': '書式解除',
1297
1305
  'toolbar.findReplace': '検索と置換',
1298
1306
  'toolbar.emoji': '絵文字挿入',
@@ -1473,7 +1481,7 @@
1473
1481
  const DEFAULT_CONFIG = {
1474
1482
  toolbar: [
1475
1483
  'viewCode', 'undo', 'redo', 'findReplace', '|',
1476
- 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'removeFormat', '|',
1484
+ 'bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript', 'code', 'formatPainter', 'removeFormat', '|',
1477
1485
  'heading', 'fontFamily', 'fontSize', '|',
1478
1486
  'foreColor', 'backColor', '|',
1479
1487
  'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', '|',
@@ -1536,6 +1544,7 @@
1536
1544
  subscript: { icon: 'subscript', titleKey: 'toolbar.subscript', command: 'subscript' },
1537
1545
  superscript: { icon: 'superscript', titleKey: 'toolbar.superscript', command: 'superscript' },
1538
1546
  code: { icon: 'code-inline', titleKey: 'toolbar.code', command: 'toggleCode' },
1547
+ formatPainter: { icon: 'format-painter', titleKey: 'toolbar.formatPainter', command: 'copyFormat' },
1539
1548
  removeFormat: { icon: 'eraser', titleKey: 'toolbar.removeFormat', command: 'removeFormat' },
1540
1549
  findReplace: { icon: 'search', titleKey: 'toolbar.findReplace', command: 'findReplace', modal: true },
1541
1550
  emoji: { icon: 'emoji', titleKey: 'toolbar.emoji', command: 'emoji', picker: 'emoji' },
@@ -2097,6 +2106,7 @@
2097
2106
  quote: '<svg viewBox="0 0 24 24"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>',
2098
2107
  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>',
2099
2108
  '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>',
2109
+ 'format-painter': '<svg width="1em" height="1em" viewBox="0 0 32 32"><g transform="translate(0 32) scale(1 -1)"><path d="M0 0h32v32H0z" fill="none"/><path fill="currentColor" d="M28.83 23.17L23 17.33V13a1 1 0 0 0-.29-.71l-10-10a1 1 0 0 0-1.42 0l-9 9a1 1 0 0 0 0 1.42l10 10A1 1 0 0 0 13 23h4.34l5.83 5.84a4 4 0 0 0 5.66-5.66ZM6 10.41l2.29 2.3l1.42-1.42L7.41 9L9 7.41l4.29 4.3l1.42-1.42L10.41 6L12 4.41L18.59 11L11 18.59L4.41 12Zm21.41 17a2 2 0 0 1-2.82 0l-6.13-6.12a1.8 1.8 0 0 0-.71-.29h-4.34l-1-1L20 12.41l1 1v4.34a1 1 0 0 0 .29.7l6.12 6.14a2 2 0 0 1 0 2.82" stroke-width="0.45" stroke="currentColor"/></g></svg>',
2100
2110
  minus: '<svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>',
2101
2111
  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>',
2102
2112
  fullscreen: '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>',
@@ -3115,7 +3125,7 @@
3115
3125
  <img src="https://github.com/neikiri/neiki-editor/raw/main/assets/logo.svg" alt="Neiki's Editor" style="width: 240px; height: auto; margin: 0 auto 16px; display: block;">
3116
3126
  <div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
3117
3127
  <div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
3118
- <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.7.0</div>
3128
+ <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.8.0</div>
3119
3129
  <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>
3120
3130
  <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>
3121
3131
  </div>
@@ -3533,6 +3543,7 @@
3533
3543
  class Commands {
3534
3544
  constructor(editor) {
3535
3545
  this.editor = editor;
3546
+ this.formatPainter = null;
3536
3547
  }
3537
3548
 
3538
3549
  exec(command, value = null) {
@@ -3693,6 +3704,102 @@
3693
3704
 
3694
3705
  removeFormat() { this.exec('removeFormat'); }
3695
3706
 
3707
+ isFormatPainterActive() {
3708
+ return !!this.formatPainter;
3709
+ }
3710
+
3711
+ copyFormat() {
3712
+ if (this.formatPainter) {
3713
+ this.formatPainter = null;
3714
+ this.editor.updateToolbar();
3715
+ return;
3716
+ }
3717
+
3718
+ if (!this.editor.restoreSavedSelection()) return;
3719
+ this._expandToWordIfCollapsed();
3720
+
3721
+ const selection = window.getSelection();
3722
+ if (!selection || !selection.rangeCount) return;
3723
+ const range = selection.getRangeAt(0);
3724
+ if (!Utils.isRangeInside(range, this.editor.contentArea) || range.collapsed) return;
3725
+
3726
+ let sourceNode = range.startContainer;
3727
+ if (sourceNode.nodeType === Node.TEXT_NODE) sourceNode = sourceNode.parentNode;
3728
+ const sourceStyle = window.getComputedStyle(sourceNode);
3729
+ const inlineCommands = ['bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript'];
3730
+ const states = {};
3731
+
3732
+ inlineCommands.forEach(command => {
3733
+ try {
3734
+ states[command] = document.queryCommandState(command);
3735
+ } catch (e) {
3736
+ states[command] = false;
3737
+ }
3738
+ });
3739
+
3740
+ this.formatPainter = {
3741
+ sourceRange: range.cloneRange(),
3742
+ states,
3743
+ fontFamily: sourceStyle.fontFamily,
3744
+ fontSize: sourceStyle.fontSize,
3745
+ color: sourceStyle.color,
3746
+ backgroundColor: sourceStyle.backgroundColor
3747
+ };
3748
+ this.editor.saveCurrentSelection();
3749
+ this.editor.updateToolbar();
3750
+ }
3751
+
3752
+ applyCopiedFormat() {
3753
+ if (!this.formatPainter) return false;
3754
+
3755
+ const selection = window.getSelection();
3756
+ if (!selection || !selection.rangeCount) return false;
3757
+ const range = selection.getRangeAt(0);
3758
+ if (!Utils.isRangeInside(range, this.editor.contentArea) || this._areRangesEqual(range, this.formatPainter.sourceRange)) {
3759
+ return false;
3760
+ }
3761
+
3762
+ this._expandToWordIfCollapsed();
3763
+ const style = this.formatPainter;
3764
+ this.formatPainter = null;
3765
+
3766
+ Object.entries(style.states).forEach(([command, shouldBeActive]) => {
3767
+ try {
3768
+ if (document.queryCommandState(command) !== shouldBeActive) {
3769
+ document.execCommand(command, false, null);
3770
+ }
3771
+ } catch (e) { /* Ignore unsupported command states. */ }
3772
+ });
3773
+
3774
+ if (style.fontFamily) document.execCommand('fontName', false, style.fontFamily);
3775
+ if (style.fontSize) this._applyFontSize(style.fontSize);
3776
+ if (style.color) document.execCommand('foreColor', false, style.color);
3777
+ if (this._isTransparentColor(style.backgroundColor)) {
3778
+ this._resetColorProperty('backgroundColor', false);
3779
+ } else if (style.backgroundColor) {
3780
+ document.execCommand('backColor', false, style.backgroundColor);
3781
+ }
3782
+
3783
+ this.editor.history.record();
3784
+ this.editor.updateToolbar();
3785
+ this.editor.triggerChange();
3786
+ return true;
3787
+ }
3788
+
3789
+ _areRangesEqual(first, second) {
3790
+ if (!first || !second) return false;
3791
+ try {
3792
+ return first.compareBoundaryPoints(Range.START_TO_START, second) === 0 &&
3793
+ first.compareBoundaryPoints(Range.END_TO_END, second) === 0;
3794
+ } catch (e) {
3795
+ return false;
3796
+ }
3797
+ }
3798
+
3799
+ _isTransparentColor(color) {
3800
+ return !color || color === 'transparent' || /^rgba\(0,\s*0,\s*0,\s*0\)$/.test(color);
3801
+ }
3802
+
3696
3803
  insertHorizontalRule() { this.exec('insertHorizontalRule'); }
3697
3804
 
3698
3805
  formatBlock(tag) {
@@ -3774,9 +3881,7 @@
3774
3881
  return ['ADDRESS', 'ARTICLE', 'ASIDE', 'BLOCKQUOTE', 'DIV', 'DL', 'FIGURE', 'FOOTER', 'FORM', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HR', 'MAIN', 'OL', 'P', 'PRE', 'SECTION', 'TABLE', 'UL'].includes(node.tagName);
3775
3882
  }
3776
3883
 
3777
- fontSize(sizeStr) {
3778
- this.editor.focus();
3779
- this._expandToWordIfCollapsed();
3884
+ _applyFontSize(sizeStr) {
3780
3885
  document.execCommand('fontSize', false, '7');
3781
3886
  const marked = this.editor.contentArea.querySelectorAll('font[size="7"]');
3782
3887
  const spans = [];
@@ -3799,6 +3904,12 @@
3799
3904
  sel.addRange(range);
3800
3905
  } catch (e) { /* ignore */ }
3801
3906
  }
3907
+ }
3908
+
3909
+ fontSize(sizeStr) {
3910
+ this.editor.focus();
3911
+ this._expandToWordIfCollapsed();
3912
+ this._applyFontSize(sizeStr);
3802
3913
  this.editor.history.record();
3803
3914
  this.editor.updateToolbar();
3804
3915
  this.editor.triggerChange();
@@ -3824,7 +3935,7 @@
3824
3935
  this._resetColorProperty('backgroundColor');
3825
3936
  }
3826
3937
 
3827
- _resetColorProperty(cssProp) {
3938
+ _resetColorProperty(cssProp, shouldRecord = true) {
3828
3939
  this.editor.focus();
3829
3940
  const sel = window.getSelection();
3830
3941
  if (!sel || !sel.rangeCount) return;
@@ -3873,9 +3984,11 @@
3873
3984
  sel.removeAllRanges();
3874
3985
  sel.addRange(range);
3875
3986
 
3876
- this.editor.history.record();
3877
- this.editor.updateToolbar();
3878
- this.editor.triggerChange();
3987
+ if (shouldRecord) {
3988
+ this.editor.history.record();
3989
+ this.editor.updateToolbar();
3990
+ this.editor.triggerChange();
3991
+ }
3879
3992
  }
3880
3993
 
3881
3994
  viewCode() {
@@ -4831,6 +4944,13 @@
4831
4944
  }
4832
4945
  });
4833
4946
 
4947
+ // Format Painter applies its copied inline formatting once the target selection is complete.
4948
+ this.contentArea.addEventListener('mouseup', () => {
4949
+ if (this.commands && this.commands.isFormatPainterActive()) {
4950
+ this.commands.applyCopiedFormat();
4951
+ }
4952
+ });
4953
+
4834
4954
  // Focus/Blur
4835
4955
  this.contentArea.addEventListener('focus', () => {
4836
4956
  if (this.config.onFocus) this.config.onFocus(this);
@@ -5176,6 +5296,10 @@
5176
5296
  if (this.toolbarButtons.viewCode) {
5177
5297
  this.toolbarButtons.viewCode.classList.toggle('active', this.isCodeViewOpen);
5178
5298
  }
5299
+ if (this.toolbarButtons.formatPainter) {
5300
+ this.toolbarButtons.formatPainter.classList.toggle('active',
5301
+ !!(this.commands && this.commands.isFormatPainterActive()));
5302
+ }
5179
5303
 
5180
5304
  // Sync heading select
5181
5305
  if (this.toolbarSelects.heading) {