neiki-editor 3.5.0 → 3.6.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.5.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.6.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.5.0/neiki-editor.min.js"></script>
143
+ <script src="https://cdn.neikiri.dev/neiki-editor/3.6.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.5.0/neiki-editor.css">
155
- <script src="https://cdn.neikiri.dev/neiki-editor/3.5.0/neiki-editor.js"></script>
154
+ <link rel="stylesheet" href="https://cdn.neikiri.dev/neiki-editor/3.6.0/neiki-editor.css">
155
+ <script src="https://cdn.neikiri.dev/neiki-editor/3.6.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.5.0/dist/neiki-editor.min.js"></script>
163
+ <script src="https://cdn.jsdelivr.net/gh/neikiri/neiki-editor@3.6.0/dist/neiki-editor.min.js"></script>
164
164
  ```
165
165
 
166
166
  **Package manager**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * NeikiEditor - Production-Ready WYSIWYG Rich Text Editor
3
3
  * CSS Stylesheet
4
- * Version: 3.5.0
4
+ * Version: 3.6.0
5
5
  */
6
6
 
7
7
  /* ============================================
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NeikiEditor - A Modern WYSIWYG Editor
3
- * Version: 3.5.0
3
+ * Version: 3.6.0
4
4
  *
5
5
  * A lightweight, feature-rich text editor with support for:
6
6
  * - Rich text formatting (bold, italic, underline, etc.)
@@ -3114,7 +3114,7 @@
3114
3114
  <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;">
3115
3115
  <div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
3116
3116
  <div><strong>${Utils.escapeHTML(t('help.author'))}:</strong> neikiri (Jindřich Stoklasa)</div>
3117
- <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.5.0</div>
3117
+ <div><strong>${Utils.escapeHTML(t('help.version'))}:</strong> 3.6.0</div>
3118
3118
  <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>
3119
3119
  <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>
3120
3120
  </div>
@@ -3900,6 +3900,24 @@
3900
3900
  if (!Utils.isSafeUrl(url)) return;
3901
3901
  const selection = Utils.getSelection();
3902
3902
  const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
3903
+ const existingLink = this.editor.getLinkAtSelection();
3904
+
3905
+ if (existingLink) {
3906
+ existingLink.setAttribute('href', url);
3907
+ existingLink.textContent = text || url;
3908
+ if (newTab) {
3909
+ existingLink.setAttribute('target', '_blank');
3910
+ existingLink.setAttribute('rel', 'noopener noreferrer');
3911
+ } else {
3912
+ existingLink.removeAttribute('target');
3913
+ existingLink.removeAttribute('rel');
3914
+ }
3915
+
3916
+ this.editor.history.record();
3917
+ this.editor.syncToOriginal();
3918
+ this.editor.triggerChange();
3919
+ return;
3920
+ }
3903
3921
 
3904
3922
  if (range && !range.collapsed) {
3905
3923
  this.exec('createLink', url);
@@ -4499,7 +4517,7 @@
4499
4517
  const insertItems = [
4500
4518
  {
4501
4519
  key: 'link', icon: Icons.link, labelKey: 'insert.link',
4502
- action: () => { this.saveCurrentSelection(); this.modal.open('link', { text: Utils.getSelection().toString() }); }
4520
+ action: () => { this.saveCurrentSelection(); this.modal.open('link', this.getLinkModalData()); }
4503
4521
  },
4504
4522
  {
4505
4523
  key: 'image', icon: Icons.image, labelKey: 'insert.image',
@@ -4879,8 +4897,7 @@
4879
4897
  let data = {};
4880
4898
 
4881
4899
  if (item === 'link') {
4882
- const sel = Utils.getSelection();
4883
- data.text = sel.toString();
4900
+ data = this.getLinkModalData();
4884
4901
  }
4885
4902
 
4886
4903
  this.modal.open(item, data);
@@ -4910,6 +4927,13 @@
4910
4927
  }
4911
4928
 
4912
4929
  handleKeydown(e) {
4930
+ if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') &&
4931
+ this.imageResizer &&
4932
+ this.imageResizer.moveCaretFromSelectedImage(e.key === 'ArrowUp' ? 'before' : 'after')) {
4933
+ e.preventDefault();
4934
+ return;
4935
+ }
4936
+
4913
4937
  if ((e.key === 'Backspace' || e.key === 'Delete') &&
4914
4938
  this.imageResizer &&
4915
4939
  this.imageResizer.deleteSelectedImage()) {
@@ -4935,7 +4959,7 @@
4935
4959
  case 'k':
4936
4960
  e.preventDefault();
4937
4961
  this.saveCurrentSelection();
4938
- this.modal.open('link', { text: Utils.getSelection().toString() });
4962
+ this.modal.open('link', this.getLinkModalData());
4939
4963
  break;
4940
4964
  case 'z':
4941
4965
  e.preventDefault();
@@ -4997,6 +5021,37 @@
4997
5021
  }
4998
5022
  }
4999
5023
 
5024
+ getLinkAtSelection() {
5025
+ const selection = window.getSelection();
5026
+ if (!selection || !selection.rangeCount) return null;
5027
+
5028
+ const range = selection.getRangeAt(0);
5029
+ if (!Utils.isRangeInside(range, this.contentArea)) return null;
5030
+
5031
+ const getClosestLink = (node) => {
5032
+ const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
5033
+ const link = element && element.closest ? element.closest('a') : null;
5034
+ return link && this.contentArea.contains(link) ? link : null;
5035
+ };
5036
+
5037
+ const startLink = getClosestLink(range.startContainer);
5038
+ const endLink = getClosestLink(range.endContainer);
5039
+ return startLink && startLink === endLink ? startLink : null;
5040
+ }
5041
+
5042
+ getLinkModalData() {
5043
+ const link = this.getLinkAtSelection();
5044
+ if (link) {
5045
+ return {
5046
+ url: link.getAttribute('href') || '',
5047
+ text: link.textContent || '',
5048
+ newTab: link.getAttribute('target') === '_blank'
5049
+ };
5050
+ }
5051
+
5052
+ return { text: Utils.getSelection().toString() };
5053
+ }
5054
+
5000
5055
  handlePaste(e) {
5001
5056
  // Check for pasted images first
5002
5057
  if (e.clipboardData && e.clipboardData.files.length > 0) {
@@ -6313,6 +6368,9 @@
6313
6368
  this.currentImg = img;
6314
6369
  const isVideo = img.tagName === 'VIDEO';
6315
6370
 
6371
+ // Keep keyboard events in the editor while the media selection is active.
6372
+ this.editor.contentArea.focus({ preventScroll: true });
6373
+
6316
6374
  // Create wrapper around image
6317
6375
  this.wrapper = document.createElement('span');
6318
6376
  this.wrapper.className = 'neiki-img-resizable';
@@ -6645,6 +6703,30 @@
6645
6703
  if (sel) sel.removeAllRanges();
6646
6704
  }
6647
6705
 
6706
+ moveCaretFromSelectedImage(position) {
6707
+ if (!this.currentImg || !this.wrapper || !this.editor.contentArea.contains(this.wrapper)) {
6708
+ return false;
6709
+ }
6710
+
6711
+ const img = this.currentImg;
6712
+ this.deselect();
6713
+
6714
+ const range = document.createRange();
6715
+ if (position === 'before') {
6716
+ range.setStartBefore(img);
6717
+ } else {
6718
+ range.setStartAfter(img);
6719
+ }
6720
+ range.collapse(true);
6721
+
6722
+ const selection = window.getSelection();
6723
+ selection.removeAllRanges();
6724
+ selection.addRange(range);
6725
+ this.editor.savedSelectionRange = range.cloneRange();
6726
+ this.editor.updateToolbar();
6727
+ return true;
6728
+ }
6729
+
6648
6730
  getSelectedImageClipboardData() {
6649
6731
  if (!this.currentImg || !this.wrapper || !this.editor.contentArea.contains(this.wrapper)) {
6650
6732
  return null;
@@ -7104,7 +7186,9 @@
7104
7186
  if (prev && prev.parentNode === this.editor.contentArea) {
7105
7187
  prev.parentNode.insertBefore(block, prev);
7106
7188
  this.editor.history.record();
7189
+ this.editor.syncToOriginal();
7107
7190
  this.editor.triggerChange();
7191
+ this.editor.updateStatusBar();
7108
7192
  }
7109
7193
  }
7110
7194
 
@@ -7115,7 +7199,9 @@
7115
7199
  if (next && next.parentNode === this.editor.contentArea) {
7116
7200
  next.parentNode.insertBefore(block, next.nextSibling);
7117
7201
  this.editor.history.record();
7202
+ this.editor.syncToOriginal();
7118
7203
  this.editor.triggerChange();
7204
+ this.editor.updateStatusBar();
7119
7205
  }
7120
7206
  }
7121
7207
 
@@ -7281,8 +7367,7 @@
7281
7367
 
7282
7368
  handleButtonClick(item) {
7283
7369
  if (item === 'link') {
7284
- const sel = Utils.getSelection();
7285
- this.editor.modal.open('link', { text: sel.toString() });
7370
+ this.editor.modal.open('link', this.editor.getLinkModalData());
7286
7371
  } else if (item === 'moveUp') {
7287
7372
  if (this.editor.blockDragDrop) this.editor.blockDragDrop.moveBlockUp();
7288
7373
  } else if (item === 'moveDown') {