neiki-editor 2.7.0 → 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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NeikiEditor - A Modern WYSIWYG Editor
3
- * Version: 2.7.0
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.7.0</div>
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
- return this.contentArea.innerHTML;
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
  }