neiki-editor 2.7.1 → 2.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 +24 -9
- package/dist/neiki-editor.css +146 -12
- package/dist/neiki-editor.js +579 -4
- 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: 2.
|
|
3
|
+
* Version: 2.8.0
|
|
4
4
|
*
|
|
5
5
|
* A lightweight, feature-rich text editor with support for:
|
|
6
6
|
* - Rich text formatting (bold, italic, underline, etc.)
|
|
@@ -1342,7 +1342,10 @@
|
|
|
1342
1342
|
eye: '<svg viewBox="0 0 24 24"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>',
|
|
1343
1343
|
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>',
|
|
1344
1344
|
'chevron-down': '<svg viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></svg>',
|
|
1345
|
-
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>'
|
|
1345
|
+
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>',
|
|
1346
|
+
grip: '<svg viewBox="0 0 24 24"><circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/><circle cx="9" cy="10" r="1.5"/><circle cx="15" cy="10" r="1.5"/><circle cx="9" cy="15" r="1.5"/><circle cx="15" cy="15" r="1.5"/><circle cx="9" cy="20" r="1.5"/><circle cx="15" cy="20" r="1.5"/></svg>',
|
|
1347
|
+
moveUp: '<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>',
|
|
1348
|
+
moveDown: '<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>'
|
|
1346
1349
|
};
|
|
1347
1350
|
|
|
1348
1351
|
// ============================================
|
|
@@ -1938,7 +1941,7 @@
|
|
|
1938
1941
|
<img src="https://github.com/neikiri/neiki-editor/raw/main/logo.png" alt="Neiki Editor" style="width: 120px; height: auto; margin: 0 auto 16px; display: block;">
|
|
1939
1942
|
<div style="font-size: 14px; line-height: 2; color: var(--neiki-text-primary);">
|
|
1940
1943
|
<div><strong>${t('help.author')}:</strong> neikiri (Jindřich Stoklasa)</div>
|
|
1941
|
-
<div><strong>${t('help.version')}:</strong> 2.
|
|
1944
|
+
<div><strong>${t('help.version')}:</strong> 2.8.0</div>
|
|
1942
1945
|
<div><strong>${t('help.github')}:</strong> <a href="https://github.com/neikiri/neiki-editor" target="_blank" style="color: var(--neiki-accent);">github.com/neikiri/neiki-editor</a></div>
|
|
1943
1946
|
<div><strong>${t('help.documentation')}:</strong> <a href="https://github.com/neikiri/neiki-editor/wiki" target="_blank" style="color: var(--neiki-accent);">Wiki</a></div>
|
|
1944
1947
|
</div>
|
|
@@ -2653,6 +2656,9 @@
|
|
|
2653
2656
|
this.commands = new Commands(this);
|
|
2654
2657
|
this.tableContextMenu = new TableContextMenu(this);
|
|
2655
2658
|
this.floatingToolbar = new FloatingToolbar(this);
|
|
2659
|
+
this.imageResizer = new ImageResizer(this);
|
|
2660
|
+
this.tableColumnResizer = new TableColumnResizer(this);
|
|
2661
|
+
this.blockDragDrop = new BlockDragDrop(this);
|
|
2656
2662
|
|
|
2657
2663
|
this.bindEvents();
|
|
2658
2664
|
this.initDragDrop();
|
|
@@ -3570,7 +3576,17 @@
|
|
|
3570
3576
|
// ============================================
|
|
3571
3577
|
|
|
3572
3578
|
getContent() {
|
|
3573
|
-
|
|
3579
|
+
// Clone content and clean up editor UI elements
|
|
3580
|
+
const clone = this.contentArea.cloneNode(true);
|
|
3581
|
+
// Unwrap image resizer wrappers
|
|
3582
|
+
clone.querySelectorAll('.neiki-img-resizable').forEach(wrapper => {
|
|
3583
|
+
const img = wrapper.querySelector('img');
|
|
3584
|
+
if (img) wrapper.parentNode.insertBefore(img, wrapper);
|
|
3585
|
+
wrapper.remove();
|
|
3586
|
+
});
|
|
3587
|
+
// Remove grip handles, placeholders, resize handles
|
|
3588
|
+
clone.querySelectorAll('.neiki-block-grip, .neiki-block-placeholder, .neiki-table-col-resize-handle, .neiki-img-resize-handle, .neiki-img-size-label').forEach(el => el.remove());
|
|
3589
|
+
return clone.innerHTML;
|
|
3574
3590
|
}
|
|
3575
3591
|
|
|
3576
3592
|
setContent(html) {
|
|
@@ -3611,6 +3627,10 @@
|
|
|
3611
3627
|
this.dropdown.close();
|
|
3612
3628
|
this.colorPicker.close();
|
|
3613
3629
|
|
|
3630
|
+
if (this.imageResizer) this.imageResizer.destroy();
|
|
3631
|
+
if (this.tableColumnResizer) this.tableColumnResizer.destroy();
|
|
3632
|
+
if (this.blockDragDrop) this.blockDragDrop.destroy();
|
|
3633
|
+
|
|
3614
3634
|
this.container.remove();
|
|
3615
3635
|
this.originalElement.style.display = '';
|
|
3616
3636
|
|
|
@@ -4053,6 +4073,528 @@
|
|
|
4053
4073
|
}
|
|
4054
4074
|
}
|
|
4055
4075
|
|
|
4076
|
+
// ============================================
|
|
4077
|
+
// SECTION 10a: IMAGE RESIZER
|
|
4078
|
+
// ============================================
|
|
4079
|
+
|
|
4080
|
+
class ImageResizer {
|
|
4081
|
+
constructor(editor) {
|
|
4082
|
+
this.editor = editor;
|
|
4083
|
+
this.wrapper = null;
|
|
4084
|
+
this.currentImg = null;
|
|
4085
|
+
this.isResizing = false;
|
|
4086
|
+
this.startX = 0;
|
|
4087
|
+
this.startY = 0;
|
|
4088
|
+
this.startWidth = 0;
|
|
4089
|
+
this.startHeight = 0;
|
|
4090
|
+
this.aspectRatio = 1;
|
|
4091
|
+
this.handle = null;
|
|
4092
|
+
|
|
4093
|
+
this.bindEvents();
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
bindEvents() {
|
|
4097
|
+
this.editor.contentArea.addEventListener('click', (e) => {
|
|
4098
|
+
const img = e.target.closest('img');
|
|
4099
|
+
if (img && this.editor.contentArea.contains(img)) {
|
|
4100
|
+
e.preventDefault();
|
|
4101
|
+
this.selectImage(img);
|
|
4102
|
+
} else if (!e.target.closest('.neiki-img-resize-handle')) {
|
|
4103
|
+
this.deselect();
|
|
4104
|
+
}
|
|
4105
|
+
});
|
|
4106
|
+
|
|
4107
|
+
// Prevent native image drag inside editor (causes duplicate on drop)
|
|
4108
|
+
this.editor.contentArea.addEventListener('dragstart', (e) => {
|
|
4109
|
+
if (e.target.tagName === 'IMG') {
|
|
4110
|
+
e.preventDefault();
|
|
4111
|
+
}
|
|
4112
|
+
});
|
|
4113
|
+
|
|
4114
|
+
document.addEventListener('mousedown', (e) => {
|
|
4115
|
+
if (this.wrapper && !this.wrapper.contains(e.target) && !this.editor.contentArea.contains(e.target)) {
|
|
4116
|
+
this.deselect();
|
|
4117
|
+
}
|
|
4118
|
+
});
|
|
4119
|
+
|
|
4120
|
+
document.addEventListener('touchstart', (e) => {
|
|
4121
|
+
if (this.wrapper && !this.wrapper.contains(e.target) && !this.editor.contentArea.contains(e.target)) {
|
|
4122
|
+
this.deselect();
|
|
4123
|
+
}
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
selectImage(img) {
|
|
4128
|
+
this.deselect();
|
|
4129
|
+
this.currentImg = img;
|
|
4130
|
+
|
|
4131
|
+
// Create wrapper around image
|
|
4132
|
+
this.wrapper = document.createElement('span');
|
|
4133
|
+
this.wrapper.className = 'neiki-img-resizable';
|
|
4134
|
+
this.wrapper.contentEditable = 'false';
|
|
4135
|
+
this.wrapper.setAttribute('data-neiki-resizer', 'true');
|
|
4136
|
+
|
|
4137
|
+
img.parentNode.insertBefore(this.wrapper, img);
|
|
4138
|
+
this.wrapper.appendChild(img);
|
|
4139
|
+
|
|
4140
|
+
// Add resize handles
|
|
4141
|
+
['nw', 'ne', 'sw', 'se'].forEach(pos => {
|
|
4142
|
+
const handle = document.createElement('span');
|
|
4143
|
+
handle.className = 'neiki-img-resize-handle ' + pos;
|
|
4144
|
+
handle.setAttribute('data-pos', pos);
|
|
4145
|
+
handle.addEventListener('mousedown', (e) => this.startResize(e, pos));
|
|
4146
|
+
handle.addEventListener('touchstart', (e) => {
|
|
4147
|
+
e.preventDefault();
|
|
4148
|
+
const touch = e.touches[0];
|
|
4149
|
+
this.startResize(touch, pos, true);
|
|
4150
|
+
}, { passive: false });
|
|
4151
|
+
this.wrapper.appendChild(handle);
|
|
4152
|
+
});
|
|
4153
|
+
|
|
4154
|
+
// Add size label
|
|
4155
|
+
this.sizeLabel = document.createElement('span');
|
|
4156
|
+
this.sizeLabel.className = 'neiki-img-size-label';
|
|
4157
|
+
this.sizeLabel.textContent = Math.round(img.offsetWidth) + ' × ' + Math.round(img.offsetHeight);
|
|
4158
|
+
this.wrapper.appendChild(this.sizeLabel);
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
deselect() {
|
|
4162
|
+
if (this.wrapper && this.currentImg) {
|
|
4163
|
+
const img = this.currentImg;
|
|
4164
|
+
const parent = this.wrapper.parentNode;
|
|
4165
|
+
if (parent) {
|
|
4166
|
+
parent.insertBefore(img, this.wrapper);
|
|
4167
|
+
this.wrapper.remove();
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
this.wrapper = null;
|
|
4171
|
+
this.currentImg = null;
|
|
4172
|
+
this.sizeLabel = null;
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
startResize(e, pos, isTouch = false) {
|
|
4176
|
+
if (e.preventDefault) { e.preventDefault(); }
|
|
4177
|
+
if (e.stopPropagation) { e.stopPropagation(); }
|
|
4178
|
+
|
|
4179
|
+
this.isResizing = true;
|
|
4180
|
+
this.handle = pos;
|
|
4181
|
+
this.startX = e.clientX;
|
|
4182
|
+
this.startY = e.clientY;
|
|
4183
|
+
this.startWidth = this.currentImg.offsetWidth;
|
|
4184
|
+
this.startHeight = this.currentImg.offsetHeight;
|
|
4185
|
+
this.aspectRatio = this.startWidth / this.startHeight;
|
|
4186
|
+
|
|
4187
|
+
if (isTouch) {
|
|
4188
|
+
const onTouchMove = (ev) => {
|
|
4189
|
+
ev.preventDefault();
|
|
4190
|
+
this.onResize(ev.touches[0]);
|
|
4191
|
+
};
|
|
4192
|
+
const onTouchEnd = () => {
|
|
4193
|
+
this.isResizing = false;
|
|
4194
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
4195
|
+
document.removeEventListener('touchend', onTouchEnd);
|
|
4196
|
+
this.editor.history.record();
|
|
4197
|
+
this.editor.triggerChange();
|
|
4198
|
+
};
|
|
4199
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
4200
|
+
document.addEventListener('touchend', onTouchEnd);
|
|
4201
|
+
} else {
|
|
4202
|
+
const onMove = (ev) => this.onResize(ev);
|
|
4203
|
+
const onUp = () => {
|
|
4204
|
+
this.isResizing = false;
|
|
4205
|
+
document.removeEventListener('mousemove', onMove);
|
|
4206
|
+
document.removeEventListener('mouseup', onUp);
|
|
4207
|
+
this.editor.history.record();
|
|
4208
|
+
this.editor.triggerChange();
|
|
4209
|
+
};
|
|
4210
|
+
document.addEventListener('mousemove', onMove);
|
|
4211
|
+
document.addEventListener('mouseup', onUp);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
onResize(e) {
|
|
4216
|
+
if (!this.isResizing || !this.currentImg) return;
|
|
4217
|
+
|
|
4218
|
+
const dx = e.clientX - this.startX;
|
|
4219
|
+
const dy = e.clientY - this.startY;
|
|
4220
|
+
|
|
4221
|
+
let newWidth, newHeight;
|
|
4222
|
+
|
|
4223
|
+
switch (this.handle) {
|
|
4224
|
+
case 'se':
|
|
4225
|
+
newWidth = this.startWidth + dx;
|
|
4226
|
+
break;
|
|
4227
|
+
case 'sw':
|
|
4228
|
+
newWidth = this.startWidth - dx;
|
|
4229
|
+
break;
|
|
4230
|
+
case 'ne':
|
|
4231
|
+
newWidth = this.startWidth + dx;
|
|
4232
|
+
break;
|
|
4233
|
+
case 'nw':
|
|
4234
|
+
newWidth = this.startWidth - dx;
|
|
4235
|
+
break;
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
newWidth = Math.max(30, newWidth);
|
|
4239
|
+
newHeight = Math.round(newWidth / this.aspectRatio);
|
|
4240
|
+
|
|
4241
|
+
this.currentImg.style.width = newWidth + 'px';
|
|
4242
|
+
this.currentImg.style.height = newHeight + 'px';
|
|
4243
|
+
this.currentImg.removeAttribute('width');
|
|
4244
|
+
this.currentImg.removeAttribute('height');
|
|
4245
|
+
|
|
4246
|
+
if (this.sizeLabel) {
|
|
4247
|
+
this.sizeLabel.textContent = Math.round(newWidth) + ' × ' + Math.round(newHeight);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
destroy() {
|
|
4252
|
+
this.deselect();
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
// ============================================
|
|
4257
|
+
// SECTION 10b: TABLE COLUMN RESIZER
|
|
4258
|
+
// ============================================
|
|
4259
|
+
|
|
4260
|
+
class TableColumnResizer {
|
|
4261
|
+
constructor(editor) {
|
|
4262
|
+
this.editor = editor;
|
|
4263
|
+
this.isResizing = false;
|
|
4264
|
+
this.currentHandle = null;
|
|
4265
|
+
this.startX = 0;
|
|
4266
|
+
this.startWidthLeft = 0;
|
|
4267
|
+
this.startWidthRight = 0;
|
|
4268
|
+
this.cellLeft = null;
|
|
4269
|
+
this.cellRight = null;
|
|
4270
|
+
|
|
4271
|
+
this.bindEvents();
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
bindEvents() {
|
|
4275
|
+
this.editor.contentArea.addEventListener('mousemove', (e) => {
|
|
4276
|
+
if (this.isResizing) return;
|
|
4277
|
+
const cell = e.target.closest('td, th');
|
|
4278
|
+
if (!cell) {
|
|
4279
|
+
this.removeHandles();
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
this.showHandle(cell, e);
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
this.editor.contentArea.addEventListener('mouseleave', () => {
|
|
4286
|
+
if (!this.isResizing) this.removeHandles();
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
showHandle(cell, e) {
|
|
4291
|
+
const rect = cell.getBoundingClientRect();
|
|
4292
|
+
const threshold = 6;
|
|
4293
|
+
const isNearRight = (e.clientX > rect.right - threshold);
|
|
4294
|
+
const isNearLeft = (e.clientX < rect.left + threshold);
|
|
4295
|
+
|
|
4296
|
+
this.removeHandles();
|
|
4297
|
+
|
|
4298
|
+
if (!isNearRight && !isNearLeft) return;
|
|
4299
|
+
|
|
4300
|
+
const table = cell.closest('table');
|
|
4301
|
+
if (!table) return;
|
|
4302
|
+
|
|
4303
|
+
let leftCell, rightCell;
|
|
4304
|
+
const row = cell.closest('tr');
|
|
4305
|
+
const cellIndex = Array.from(row.cells).indexOf(cell);
|
|
4306
|
+
|
|
4307
|
+
if (isNearRight && cellIndex < row.cells.length - 1) {
|
|
4308
|
+
leftCell = cell;
|
|
4309
|
+
rightCell = row.cells[cellIndex + 1];
|
|
4310
|
+
} else if (isNearLeft && cellIndex > 0) {
|
|
4311
|
+
leftCell = row.cells[cellIndex - 1];
|
|
4312
|
+
rightCell = cell;
|
|
4313
|
+
} else {
|
|
4314
|
+
return;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
const handle = document.createElement('div');
|
|
4318
|
+
handle.className = 'neiki-table-col-resize-handle';
|
|
4319
|
+
const leftRect = leftCell.getBoundingClientRect();
|
|
4320
|
+
const contentRect = this.editor.contentArea.getBoundingClientRect();
|
|
4321
|
+
|
|
4322
|
+
handle.style.left = (leftRect.right - contentRect.left - 3 + this.editor.contentArea.scrollLeft) + 'px';
|
|
4323
|
+
handle.style.top = (table.getBoundingClientRect().top - contentRect.top + this.editor.contentArea.scrollTop) + 'px';
|
|
4324
|
+
handle.style.height = table.offsetHeight + 'px';
|
|
4325
|
+
|
|
4326
|
+
handle.addEventListener('mousedown', (ev) => {
|
|
4327
|
+
ev.preventDefault();
|
|
4328
|
+
ev.stopPropagation();
|
|
4329
|
+
this.startResize(ev, leftCell, rightCell, table);
|
|
4330
|
+
});
|
|
4331
|
+
|
|
4332
|
+
this.editor.contentArea.appendChild(handle);
|
|
4333
|
+
this.currentHandle = handle;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
removeHandles() {
|
|
4337
|
+
if (this.currentHandle) {
|
|
4338
|
+
this.currentHandle.remove();
|
|
4339
|
+
this.currentHandle = null;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
startResize(e, leftCell, rightCell, table) {
|
|
4344
|
+
this.isResizing = true;
|
|
4345
|
+
this.startX = e.clientX;
|
|
4346
|
+
this.cellLeft = leftCell;
|
|
4347
|
+
this.cellRight = rightCell;
|
|
4348
|
+
|
|
4349
|
+
// Set table to fixed layout
|
|
4350
|
+
table.style.tableLayout = 'fixed';
|
|
4351
|
+
|
|
4352
|
+
// Initialize all cell widths as px if not set
|
|
4353
|
+
const firstRow = table.rows[0];
|
|
4354
|
+
if (firstRow) {
|
|
4355
|
+
Array.from(firstRow.cells).forEach(c => {
|
|
4356
|
+
if (!c.style.width) c.style.width = c.offsetWidth + 'px';
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
this.startWidthLeft = leftCell.offsetWidth;
|
|
4361
|
+
this.startWidthRight = rightCell.offsetWidth;
|
|
4362
|
+
|
|
4363
|
+
this.removeHandles();
|
|
4364
|
+
|
|
4365
|
+
const onMove = (ev) => {
|
|
4366
|
+
const dx = ev.clientX - this.startX;
|
|
4367
|
+
const newLeft = Math.max(40, this.startWidthLeft + dx);
|
|
4368
|
+
const newRight = Math.max(40, this.startWidthRight - dx);
|
|
4369
|
+
|
|
4370
|
+
// Apply to all cells in same column
|
|
4371
|
+
const leftIdx = Array.from(leftCell.closest('tr').cells).indexOf(leftCell);
|
|
4372
|
+
const rightIdx = Array.from(rightCell.closest('tr').cells).indexOf(rightCell);
|
|
4373
|
+
Array.from(table.rows).forEach(row => {
|
|
4374
|
+
if (row.cells[leftIdx]) row.cells[leftIdx].style.width = newLeft + 'px';
|
|
4375
|
+
if (row.cells[rightIdx]) row.cells[rightIdx].style.width = newRight + 'px';
|
|
4376
|
+
});
|
|
4377
|
+
};
|
|
4378
|
+
|
|
4379
|
+
const onUp = () => {
|
|
4380
|
+
this.isResizing = false;
|
|
4381
|
+
document.removeEventListener('mousemove', onMove);
|
|
4382
|
+
document.removeEventListener('mouseup', onUp);
|
|
4383
|
+
this.editor.history.record();
|
|
4384
|
+
this.editor.triggerChange();
|
|
4385
|
+
};
|
|
4386
|
+
|
|
4387
|
+
document.addEventListener('mousemove', onMove);
|
|
4388
|
+
document.addEventListener('mouseup', onUp);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
destroy() {
|
|
4392
|
+
this.removeHandles();
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4396
|
+
// ============================================
|
|
4397
|
+
// SECTION 10c: BLOCK DRAG & DROP REORDERING
|
|
4398
|
+
// ============================================
|
|
4399
|
+
|
|
4400
|
+
class BlockDragDrop {
|
|
4401
|
+
constructor(editor) {
|
|
4402
|
+
this.editor = editor;
|
|
4403
|
+
this.gripEl = null;
|
|
4404
|
+
this.dragBlock = null;
|
|
4405
|
+
this.placeholder = null;
|
|
4406
|
+
this.isDragging = false;
|
|
4407
|
+
this.offsetY = 0;
|
|
4408
|
+
this.ghostEl = null;
|
|
4409
|
+
|
|
4410
|
+
this.bindEvents();
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
getTopLevelBlocks() {
|
|
4414
|
+
return Array.from(this.editor.contentArea.children).filter(el =>
|
|
4415
|
+
el.nodeType === Node.ELEMENT_NODE &&
|
|
4416
|
+
!el.classList.contains('neiki-block-placeholder') &&
|
|
4417
|
+
!el.classList.contains('neiki-block-grip')
|
|
4418
|
+
);
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
getBlockFromPoint(y) {
|
|
4422
|
+
const blocks = this.getTopLevelBlocks();
|
|
4423
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
4424
|
+
const rect = blocks[i].getBoundingClientRect();
|
|
4425
|
+
if (y >= rect.top) return { block: blocks[i], index: i };
|
|
4426
|
+
}
|
|
4427
|
+
return blocks.length > 0 ? { block: blocks[0], index: 0 } : null;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
bindEvents() {
|
|
4431
|
+
// Show grip on hover
|
|
4432
|
+
this.editor.contentArea.addEventListener('mousemove', (e) => {
|
|
4433
|
+
if (this.isDragging) return;
|
|
4434
|
+
const block = this.getBlockAt(e.target);
|
|
4435
|
+
if (block) {
|
|
4436
|
+
this.showGrip(block);
|
|
4437
|
+
} else {
|
|
4438
|
+
this.hideGrip();
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
|
|
4442
|
+
this.editor.contentArea.addEventListener('mouseleave', () => {
|
|
4443
|
+
if (!this.isDragging) this.hideGrip();
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
getBlockAt(target) {
|
|
4448
|
+
if (!target || target === this.editor.contentArea) return null;
|
|
4449
|
+
let node = target;
|
|
4450
|
+
while (node && node.parentNode !== this.editor.contentArea) {
|
|
4451
|
+
node = node.parentNode;
|
|
4452
|
+
}
|
|
4453
|
+
if (node && node.nodeType === Node.ELEMENT_NODE && node.parentNode === this.editor.contentArea) {
|
|
4454
|
+
return node;
|
|
4455
|
+
}
|
|
4456
|
+
return null;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
showGrip(block) {
|
|
4460
|
+
if (this.gripEl && this.gripEl._block === block) return;
|
|
4461
|
+
this.hideGrip();
|
|
4462
|
+
|
|
4463
|
+
const grip = document.createElement('div');
|
|
4464
|
+
grip.className = 'neiki-block-grip';
|
|
4465
|
+
grip.innerHTML = Icons.grip;
|
|
4466
|
+
grip.title = 'Drag to reorder';
|
|
4467
|
+
grip.contentEditable = 'false';
|
|
4468
|
+
grip._block = block;
|
|
4469
|
+
|
|
4470
|
+
// Position grip
|
|
4471
|
+
const contentRect = this.editor.contentArea.getBoundingClientRect();
|
|
4472
|
+
const blockRect = block.getBoundingClientRect();
|
|
4473
|
+
grip.style.top = (blockRect.top - contentRect.top + this.editor.contentArea.scrollTop) + 'px';
|
|
4474
|
+
grip.style.left = '-28px';
|
|
4475
|
+
|
|
4476
|
+
grip.addEventListener('mousedown', (e) => {
|
|
4477
|
+
e.preventDefault();
|
|
4478
|
+
e.stopPropagation();
|
|
4479
|
+
this.startDrag(e, block);
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4482
|
+
this.editor.contentArea.appendChild(grip);
|
|
4483
|
+
this.gripEl = grip;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
hideGrip() {
|
|
4487
|
+
if (this.gripEl) {
|
|
4488
|
+
this.gripEl.remove();
|
|
4489
|
+
this.gripEl = null;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
startDrag(e, block) {
|
|
4494
|
+
this.isDragging = true;
|
|
4495
|
+
this.dragBlock = block;
|
|
4496
|
+
this.hideGrip();
|
|
4497
|
+
|
|
4498
|
+
// Create ghost
|
|
4499
|
+
this.ghostEl = block.cloneNode(true);
|
|
4500
|
+
this.ghostEl.className = (this.ghostEl.className || '') + ' neiki-block-ghost';
|
|
4501
|
+
this.ghostEl.style.width = block.offsetWidth + 'px';
|
|
4502
|
+
document.body.appendChild(this.ghostEl);
|
|
4503
|
+
|
|
4504
|
+
const rect = block.getBoundingClientRect();
|
|
4505
|
+
this.offsetY = e.clientY - rect.top;
|
|
4506
|
+
this.ghostEl.style.left = rect.left + 'px';
|
|
4507
|
+
this.ghostEl.style.top = (e.clientY - this.offsetY) + 'px';
|
|
4508
|
+
|
|
4509
|
+
// Create placeholder
|
|
4510
|
+
this.placeholder = document.createElement('div');
|
|
4511
|
+
this.placeholder.className = 'neiki-block-placeholder';
|
|
4512
|
+
this.placeholder.style.height = block.offsetHeight + 'px';
|
|
4513
|
+
block.parentNode.insertBefore(this.placeholder, block);
|
|
4514
|
+
|
|
4515
|
+
// Hide original
|
|
4516
|
+
block.style.display = 'none';
|
|
4517
|
+
|
|
4518
|
+
const onMove = (ev) => {
|
|
4519
|
+
this.ghostEl.style.top = (ev.clientY - this.offsetY) + 'px';
|
|
4520
|
+
|
|
4521
|
+
// Find target position
|
|
4522
|
+
const target = this.getBlockFromPoint(ev.clientY);
|
|
4523
|
+
if (target && target.block !== this.dragBlock && target.block !== this.placeholder) {
|
|
4524
|
+
const targetRect = target.block.getBoundingClientRect();
|
|
4525
|
+
const mid = targetRect.top + targetRect.height / 2;
|
|
4526
|
+
if (ev.clientY < mid) {
|
|
4527
|
+
target.block.parentNode.insertBefore(this.placeholder, target.block);
|
|
4528
|
+
} else {
|
|
4529
|
+
target.block.parentNode.insertBefore(this.placeholder, target.block.nextSibling);
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
};
|
|
4533
|
+
|
|
4534
|
+
const onUp = () => {
|
|
4535
|
+
document.removeEventListener('mousemove', onMove);
|
|
4536
|
+
document.removeEventListener('mouseup', onUp);
|
|
4537
|
+
|
|
4538
|
+
// Move block to placeholder position
|
|
4539
|
+
this.placeholder.parentNode.insertBefore(this.dragBlock, this.placeholder);
|
|
4540
|
+
this.dragBlock.style.display = '';
|
|
4541
|
+
this.placeholder.remove();
|
|
4542
|
+
this.ghostEl.remove();
|
|
4543
|
+
|
|
4544
|
+
this.isDragging = false;
|
|
4545
|
+
this.dragBlock = null;
|
|
4546
|
+
this.placeholder = null;
|
|
4547
|
+
this.ghostEl = null;
|
|
4548
|
+
|
|
4549
|
+
this.editor.history.record();
|
|
4550
|
+
this.editor.triggerChange();
|
|
4551
|
+
};
|
|
4552
|
+
|
|
4553
|
+
document.addEventListener('mousemove', onMove);
|
|
4554
|
+
document.addEventListener('mouseup', onUp);
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
moveBlockUp(block) {
|
|
4558
|
+
if (!block) block = this.getSelectedBlock();
|
|
4559
|
+
if (!block) return;
|
|
4560
|
+
const prev = block.previousElementSibling;
|
|
4561
|
+
if (prev && prev.parentNode === this.editor.contentArea) {
|
|
4562
|
+
prev.parentNode.insertBefore(block, prev);
|
|
4563
|
+
this.editor.history.record();
|
|
4564
|
+
this.editor.triggerChange();
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
moveBlockDown(block) {
|
|
4569
|
+
if (!block) block = this.getSelectedBlock();
|
|
4570
|
+
if (!block) return;
|
|
4571
|
+
const next = block.nextElementSibling;
|
|
4572
|
+
if (next && next.parentNode === this.editor.contentArea) {
|
|
4573
|
+
next.parentNode.insertBefore(block, next.nextSibling);
|
|
4574
|
+
this.editor.history.record();
|
|
4575
|
+
this.editor.triggerChange();
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
getSelectedBlock() {
|
|
4580
|
+
const sel = window.getSelection();
|
|
4581
|
+
if (!sel || !sel.rangeCount) return null;
|
|
4582
|
+
let node = sel.getRangeAt(0).startContainer;
|
|
4583
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentNode;
|
|
4584
|
+
while (node && node.parentNode !== this.editor.contentArea) {
|
|
4585
|
+
node = node.parentNode;
|
|
4586
|
+
}
|
|
4587
|
+
if (node && node.parentNode === this.editor.contentArea) return node;
|
|
4588
|
+
return null;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
destroy() {
|
|
4592
|
+
this.hideGrip();
|
|
4593
|
+
if (this.ghostEl) this.ghostEl.remove();
|
|
4594
|
+
if (this.placeholder) this.placeholder.remove();
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4056
4598
|
// ============================================
|
|
4057
4599
|
// SECTION 10: FLOATING SELECTION TOOLBAR
|
|
4058
4600
|
// ============================================
|
|
@@ -4071,6 +4613,35 @@
|
|
|
4071
4613
|
createToolbar() {
|
|
4072
4614
|
this.toolbar = Utils.createElement('div', { className: 'neiki-floating-toolbar' });
|
|
4073
4615
|
|
|
4616
|
+
// Move block buttons (left side)
|
|
4617
|
+
const moveButtons = [
|
|
4618
|
+
{ item: 'moveUp', icon: Icons.moveUp, title: 'Move block up' },
|
|
4619
|
+
{ item: 'moveDown', icon: Icons.moveDown, title: 'Move block down' }
|
|
4620
|
+
];
|
|
4621
|
+
|
|
4622
|
+
moveButtons.forEach(({ item, icon, title }) => {
|
|
4623
|
+
const button = Utils.createElement('button', {
|
|
4624
|
+
className: 'neiki-toolbar-btn neiki-floating-btn neiki-floating-move-btn',
|
|
4625
|
+
title: title,
|
|
4626
|
+
type: 'button',
|
|
4627
|
+
innerHTML: icon,
|
|
4628
|
+
'data-command': item
|
|
4629
|
+
});
|
|
4630
|
+
|
|
4631
|
+
button.addEventListener('click', (e) => {
|
|
4632
|
+
e.preventDefault();
|
|
4633
|
+
e.stopPropagation();
|
|
4634
|
+
this.handleButtonClick(item);
|
|
4635
|
+
});
|
|
4636
|
+
|
|
4637
|
+
this.toolbar.appendChild(button);
|
|
4638
|
+
});
|
|
4639
|
+
|
|
4640
|
+
// Divider
|
|
4641
|
+
const divider = Utils.createElement('span', { className: 'neiki-floating-divider' });
|
|
4642
|
+
this.toolbar.appendChild(divider);
|
|
4643
|
+
|
|
4644
|
+
// Formatting buttons
|
|
4074
4645
|
const buttons = [
|
|
4075
4646
|
{ item: 'bold', icon: Icons.bold, title: 'Bold' },
|
|
4076
4647
|
{ item: 'italic', icon: Icons.italic, title: 'Italic' },
|
|
@@ -4163,6 +4734,10 @@
|
|
|
4163
4734
|
if (item === 'link') {
|
|
4164
4735
|
const sel = Utils.getSelection();
|
|
4165
4736
|
this.editor.modal.open('link', { text: sel.toString() });
|
|
4737
|
+
} else if (item === 'moveUp') {
|
|
4738
|
+
if (this.editor.blockDragDrop) this.editor.blockDragDrop.moveBlockUp();
|
|
4739
|
+
} else if (item === 'moveDown') {
|
|
4740
|
+
if (this.editor.blockDragDrop) this.editor.blockDragDrop.moveBlockDown();
|
|
4166
4741
|
} else {
|
|
4167
4742
|
this.editor.commands[item]();
|
|
4168
4743
|
}
|