astro-tractstack 2.0.8 → 2.0.10
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/dist/index.js +4 -6
- package/package.json +1 -1
- package/templates/css/custom.css +0 -6
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +1 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +2 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +4 -4
- package/templates/src/components/compositor/Compositor.tsx +335 -16
- package/templates/src/components/compositor/Node.tsx +86 -6
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +3 -6
- package/templates/src/components/compositor/nodes/tagElements/NodeA.tsx +2 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeAnchorComponent.tsx +11 -19
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +120 -6
- package/templates/src/components/compositor/nodes/tagElements/NodeButton.tsx +1 -1
- package/templates/src/components/compositor/nodes/tagElements/NodeText.tsx +78 -8
- package/templates/src/components/edit/SettingsPanel.tsx +1 -1
- package/templates/src/components/edit/ToolMode.tsx +93 -22
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +1 -1
- package/templates/src/components/edit/pane/PageGen_preview.tsx +2 -1
- package/templates/src/components/edit/panels/StyleElementPanel_update.tsx +9 -5
- package/templates/src/components/edit/state/SaveModal.tsx +84 -14
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +2 -2
- package/templates/src/components/search/SearchModal.tsx +2 -1
- package/templates/src/components/search/SearchResults.tsx +2 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +3 -5
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +1 -1
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +1 -1
- package/templates/src/hooks/useFormState.ts +3 -4
- package/templates/src/stores/nodes.ts +813 -19
- package/templates/src/stores/selection.ts +41 -0
- package/templates/src/types/compositorTypes.ts +1 -0
- package/templates/src/types/nodeProps.ts +12 -0
- package/templates/src/utils/compositor/nodesHelper.ts +2 -2
- package/utils/inject-files.ts +4 -6
- package/templates/src/components/compositor/elements/PlayButton.tsx +0 -19
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { atom, map } from 'nanostores';
|
|
2
|
+
import { ulid } from 'ulid';
|
|
3
|
+
import { processClassesForViewports } from '@/utils/compositor/reduceNodesClassNames';
|
|
4
|
+
import { NotificationSystem } from '@/stores/notificationSystem';
|
|
5
|
+
import { cloneDeep, isDeepEqual } from '@/utils/helpers';
|
|
6
|
+
import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
|
|
7
|
+
import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
|
|
8
|
+
import allowInsert from '@/utils/compositor/allowInsert';
|
|
9
|
+
import { reservedSlugs } from '@/constants';
|
|
10
|
+
import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
|
|
11
|
+
import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
|
|
12
|
+
import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
|
|
2
13
|
import {
|
|
3
14
|
hasButtonPayload,
|
|
4
15
|
hasTagName,
|
|
@@ -40,22 +51,14 @@ import type {
|
|
|
40
51
|
} from '@/types/compositorTypes';
|
|
41
52
|
import type { NodeProps, WidgetProps } from '@/types/nodeProps';
|
|
42
53
|
import type { CSSProperties } from 'react';
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import { NotificationSystem } from '@/stores/notificationSystem';
|
|
46
|
-
import { cloneDeep, isDeepEqual } from '@/utils/helpers';
|
|
47
|
-
import { extractClassesFromNodes } from '@/utils/compositor/nodesHelper';
|
|
48
|
-
import { handleClickEventDefault } from '@/utils/compositor/handleClickEvent';
|
|
49
|
-
import allowInsert from '@/utils/compositor/allowInsert';
|
|
50
|
-
import { reservedSlugs } from '@/constants';
|
|
51
|
-
import { NodesHistory, PatchOp } from '@/stores/nodesHistory';
|
|
52
|
-
import { moveNodeAtLocationInContext } from '@/utils/compositor/nodesHelper';
|
|
53
|
-
import { MarkdownGenerator } from '@/utils/compositor/nodesMarkdownGenerator';
|
|
54
|
+
import { selectionStore } from '@/stores/selection';
|
|
55
|
+
import type { SelectionRange, SelectionStoreState } from '@/stores/selection';
|
|
54
56
|
import type { CompositorProps } from '@/components/compositor/Compositor';
|
|
55
57
|
|
|
56
58
|
const blockedClickNodes = new Set<string>(['em', 'strong']);
|
|
57
59
|
export const ROOT_NODE_NAME = 'root';
|
|
58
60
|
export const UNDO_REDO_HISTORY_CAPACITY = 500;
|
|
61
|
+
const VERBOSE = false;
|
|
59
62
|
|
|
60
63
|
function strippedStyles(obj: Record<string, string[]>) {
|
|
61
64
|
return Object.fromEntries(
|
|
@@ -337,10 +340,17 @@ export class NodesContext {
|
|
|
337
340
|
// click handler based on toolModeVal
|
|
338
341
|
switch (toolModeVal) {
|
|
339
342
|
case `styles`:
|
|
340
|
-
|
|
343
|
+
const selection = selectionStore.get();
|
|
344
|
+
if (!selection.isActive)
|
|
345
|
+
handleClickEventDefault(
|
|
346
|
+
node,
|
|
347
|
+
dblClick,
|
|
348
|
+
this.clickedParentLayer.get()
|
|
349
|
+
);
|
|
341
350
|
break;
|
|
342
351
|
case `text`:
|
|
343
352
|
if (
|
|
353
|
+
dblClick &&
|
|
344
354
|
node.nodeType === 'TagElement' &&
|
|
345
355
|
'tagName' in node &&
|
|
346
356
|
(node.tagName === 'a' || node.tagName === 'button')
|
|
@@ -359,12 +369,6 @@ export class NodesContext {
|
|
|
359
369
|
dblClick,
|
|
360
370
|
this.clickedParentLayer.get()
|
|
361
371
|
);
|
|
362
|
-
} else {
|
|
363
|
-
handleClickEventDefault(
|
|
364
|
-
node,
|
|
365
|
-
dblClick,
|
|
366
|
-
this.clickedParentLayer.get()
|
|
367
|
-
);
|
|
368
372
|
}
|
|
369
373
|
break;
|
|
370
374
|
case `eraser`:
|
|
@@ -377,6 +381,578 @@ export class NodesContext {
|
|
|
377
381
|
this.setClickedParentLayer(null);
|
|
378
382
|
}
|
|
379
383
|
|
|
384
|
+
public async wrapRangeInAnchor(
|
|
385
|
+
range: SelectionStoreState
|
|
386
|
+
): Promise<string | null> {
|
|
387
|
+
if (
|
|
388
|
+
!range.startNodeId ||
|
|
389
|
+
!range.endNodeId ||
|
|
390
|
+
!range.lcaNodeId ||
|
|
391
|
+
!range.blockNodeId
|
|
392
|
+
) {
|
|
393
|
+
return Promise.resolve(null);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const originalAllNodes = new Map(this.allNodes.get());
|
|
397
|
+
const originalParentNodes = new Map(this.parentNodes.get());
|
|
398
|
+
const paneNodeId = this.getClosestNodeTypeFromId(range.blockNodeId, 'Pane');
|
|
399
|
+
const originalPaneNode = this.allNodes.get().get(paneNodeId)
|
|
400
|
+
? (cloneDeep(this.allNodes.get().get(paneNodeId)) as PaneNode)
|
|
401
|
+
: null;
|
|
402
|
+
const wrapperNodeId = `a-${ulid()}`;
|
|
403
|
+
|
|
404
|
+
const redoLogic = (): string | null => {
|
|
405
|
+
if (!range.startNodeId || !range.endNodeId || !range.lcaNodeId)
|
|
406
|
+
return null;
|
|
407
|
+
|
|
408
|
+
let startNodeToFind: string | null = null;
|
|
409
|
+
let endNodeToFind: string | null = null;
|
|
410
|
+
|
|
411
|
+
if (range.startNodeId === range.endNodeId) {
|
|
412
|
+
const { left: nodeSplitAtEnd } = this._splitTextNode(
|
|
413
|
+
range.endNodeId,
|
|
414
|
+
range.endCharOffset
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const { right: middleNode } = this._splitTextNode(
|
|
418
|
+
nodeSplitAtEnd.id,
|
|
419
|
+
range.startCharOffset
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
if (!middleNode) {
|
|
423
|
+
console.error(
|
|
424
|
+
'wrapRangeInAnchor: Single-node split failed to create a middle node.'
|
|
425
|
+
);
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
startNodeToFind = middleNode.id;
|
|
429
|
+
endNodeToFind = middleNode.id;
|
|
430
|
+
} else {
|
|
431
|
+
const { left: endNodeAfterSplit } = this._splitTextNode(
|
|
432
|
+
range.endNodeId,
|
|
433
|
+
range.endCharOffset
|
|
434
|
+
);
|
|
435
|
+
endNodeToFind = endNodeAfterSplit.id;
|
|
436
|
+
|
|
437
|
+
const { right: startNodeAfterSplit } = this._splitTextNode(
|
|
438
|
+
range.startNodeId,
|
|
439
|
+
range.startCharOffset
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (!startNodeAfterSplit) {
|
|
443
|
+
console.error(
|
|
444
|
+
'wrapRangeInAnchor: Multi-node split failed to create a start node.'
|
|
445
|
+
);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
startNodeToFind = startNodeAfterSplit.id;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const newAllNodes = new Map(this.allNodes.get());
|
|
452
|
+
const newParentNodes = new Map(this.parentNodes.get());
|
|
453
|
+
const parentChildren = newParentNodes.get(range.lcaNodeId!);
|
|
454
|
+
|
|
455
|
+
if (!parentChildren) {
|
|
456
|
+
console.error('wrapRangeInAnchor: Could not find parent children');
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const startIndex = parentChildren.indexOf(startNodeToFind!);
|
|
461
|
+
const endIndex = parentChildren.indexOf(endNodeToFind!);
|
|
462
|
+
|
|
463
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
464
|
+
console.error(
|
|
465
|
+
'wrapRangeInAnchor: Could not find split nodes in parent',
|
|
466
|
+
{
|
|
467
|
+
startIndex,
|
|
468
|
+
endIndex,
|
|
469
|
+
startNodeToFind,
|
|
470
|
+
endNodeToFind,
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const actualStartIndex = Math.min(startIndex, endIndex);
|
|
477
|
+
const actualEndIndex = Math.max(startIndex, endIndex);
|
|
478
|
+
|
|
479
|
+
const newParentChildren = [...parentChildren];
|
|
480
|
+
const nodesToWrapIds = newParentChildren.slice(
|
|
481
|
+
actualStartIndex,
|
|
482
|
+
actualEndIndex + 1
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (nodesToWrapIds.length === 0) {
|
|
486
|
+
console.error('wrapRangeInAnchor: No nodes to wrap.');
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const nodesToWrap = nodesToWrapIds
|
|
491
|
+
.map((id) => newAllNodes.get(id))
|
|
492
|
+
.filter((n): n is BaseNode => !!n);
|
|
493
|
+
|
|
494
|
+
const wrapperNode: FlatNode = {
|
|
495
|
+
id: wrapperNodeId,
|
|
496
|
+
nodeType: 'TagElement',
|
|
497
|
+
parentId: range.lcaNodeId,
|
|
498
|
+
tagName: 'a',
|
|
499
|
+
href: '#',
|
|
500
|
+
overrideClasses: {},
|
|
501
|
+
};
|
|
502
|
+
newAllNodes.set(wrapperNode.id, wrapperNode);
|
|
503
|
+
newParentNodes.set(wrapperNode.id, []);
|
|
504
|
+
|
|
505
|
+
for (const node of nodesToWrap) {
|
|
506
|
+
const nodeToUpdate = newAllNodes.get(node.id);
|
|
507
|
+
if (nodeToUpdate) {
|
|
508
|
+
nodeToUpdate.parentId = wrapperNode.id;
|
|
509
|
+
}
|
|
510
|
+
newParentNodes.get(wrapperNode.id)!.push(node.id);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
newParentChildren.splice(
|
|
514
|
+
actualStartIndex,
|
|
515
|
+
nodesToWrapIds.length,
|
|
516
|
+
wrapperNode.id
|
|
517
|
+
);
|
|
518
|
+
newParentNodes.set(range.lcaNodeId!, newParentChildren);
|
|
519
|
+
|
|
520
|
+
this.allNodes.set(newAllNodes);
|
|
521
|
+
this.parentNodes.set(newParentNodes);
|
|
522
|
+
|
|
523
|
+
if (originalPaneNode) {
|
|
524
|
+
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
525
|
+
notify: false,
|
|
526
|
+
recordHistory: false,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.notifyNode(range.blockNodeId!);
|
|
531
|
+
return wrapperNode.id;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const undoLogic = () => {
|
|
535
|
+
this.allNodes.set(originalAllNodes);
|
|
536
|
+
this.parentNodes.set(originalParentNodes);
|
|
537
|
+
|
|
538
|
+
if (originalPaneNode) {
|
|
539
|
+
this.modifyNodes([originalPaneNode], {
|
|
540
|
+
notify: false,
|
|
541
|
+
recordHistory: false,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
this.notifyNode(range.blockNodeId!);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const newAnchorId = redoLogic();
|
|
548
|
+
|
|
549
|
+
this.history.addPatch({
|
|
550
|
+
op: PatchOp.REPLACE,
|
|
551
|
+
undo: undoLogic,
|
|
552
|
+
redo: redoLogic,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return new Promise((resolve) =>
|
|
556
|
+
setTimeout(() => resolve(newAnchorId), 310)
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Splits a text node at a given character offset.
|
|
562
|
+
* This is a robust function that correctly handles splits at offset 0
|
|
563
|
+
* by creating an empty left node, and splits at the end by returning
|
|
564
|
+
* the original node as 'left' and null as 'right'.
|
|
565
|
+
*
|
|
566
|
+
* @param nodeId - The ID of the 'text' node to split.
|
|
567
|
+
* @param offset - The character offset at which to split.
|
|
568
|
+
* @returns An object containing the left and (optional) right node.
|
|
569
|
+
*/
|
|
570
|
+
private _splitTextNode(
|
|
571
|
+
nodeId: string,
|
|
572
|
+
offset: number
|
|
573
|
+
): { left: FlatNode; right: FlatNode | null } {
|
|
574
|
+
console.log(`%c[_splitTextNode] CALLED`, 'color: #f59e0b;', {
|
|
575
|
+
nodeId,
|
|
576
|
+
offset,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const allNodes = new Map(this.allNodes.get());
|
|
580
|
+
const parentNodes = new Map(this.parentNodes.get());
|
|
581
|
+
const originalNode = allNodes.get(nodeId) as FlatNode;
|
|
582
|
+
|
|
583
|
+
if (
|
|
584
|
+
!originalNode ||
|
|
585
|
+
originalNode.tagName !== 'text' ||
|
|
586
|
+
typeof originalNode.copy !== 'string' // Check for copy existence
|
|
587
|
+
) {
|
|
588
|
+
console.warn('_splitTextNode: Invalid node or type.', {
|
|
589
|
+
nodeId,
|
|
590
|
+
offset,
|
|
591
|
+
originalNode,
|
|
592
|
+
});
|
|
593
|
+
return { left: originalNode, right: null };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const text = originalNode.copy;
|
|
597
|
+
|
|
598
|
+
// Handle split at the end of the string (no-op)
|
|
599
|
+
if (offset >= text.length) {
|
|
600
|
+
console.warn(
|
|
601
|
+
`%c[_splitTextNode] OFFSET >= LENGTH DETECTED. Returning right: null`,
|
|
602
|
+
'color: #f59e0b;',
|
|
603
|
+
{ nodeId, text, offset }
|
|
604
|
+
);
|
|
605
|
+
return { left: originalNode, right: null };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Handle split at the beginning of the string (THE FIX)
|
|
609
|
+
if (offset === 0) {
|
|
610
|
+
console.log(
|
|
611
|
+
`%c[_splitTextNode] OFFSET 0 DETECTED. Creating empty left node.`,
|
|
612
|
+
'color: #f59e0b; font-weight: bold;',
|
|
613
|
+
{ nodeId, text }
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
// Create a new empty node for the "left" half
|
|
617
|
+
const leftNode: FlatNode = {
|
|
618
|
+
id: ulid(),
|
|
619
|
+
nodeType: 'TagElement',
|
|
620
|
+
parentId: originalNode.parentId,
|
|
621
|
+
tagName: 'text',
|
|
622
|
+
copy: '', // Empty text
|
|
623
|
+
};
|
|
624
|
+
allNodes.set(leftNode.id, leftNode);
|
|
625
|
+
|
|
626
|
+
// The original node becomes the "right" half
|
|
627
|
+
const rightNode = originalNode;
|
|
628
|
+
|
|
629
|
+
// Insert the new empty node *before* the original node
|
|
630
|
+
const parentChildren = parentNodes.get(leftNode.parentId!)!;
|
|
631
|
+
if (!parentChildren) {
|
|
632
|
+
console.error(
|
|
633
|
+
'_splitTextNode (offset 0): Could not find parent children list for',
|
|
634
|
+
{
|
|
635
|
+
parentId: leftNode.parentId,
|
|
636
|
+
}
|
|
637
|
+
);
|
|
638
|
+
// We still return the nodes so the operation MIGHT succeed
|
|
639
|
+
return { left: leftNode, right: rightNode };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const nodeIndex = parentChildren.indexOf(rightNode.id);
|
|
643
|
+
const newParentChildren = [...parentChildren];
|
|
644
|
+
|
|
645
|
+
if (nodeIndex > -1) {
|
|
646
|
+
newParentChildren.splice(nodeIndex, 0, leftNode.id);
|
|
647
|
+
} else {
|
|
648
|
+
console.warn(
|
|
649
|
+
'_splitTextNode (offset 0): Could not find node in parent, prepending.',
|
|
650
|
+
{
|
|
651
|
+
nodeId: rightNode.id,
|
|
652
|
+
parentId: leftNode.parentId,
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
newParentChildren.unshift(leftNode.id);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
parentNodes.set(leftNode.parentId!, newParentChildren);
|
|
659
|
+
this.allNodes.set(allNodes);
|
|
660
|
+
this.parentNodes.set(parentNodes);
|
|
661
|
+
|
|
662
|
+
// Return the new empty node as 'left' and the original node as 'right'
|
|
663
|
+
// This allows wrapRangeInSpan to find the original node as 'middleNode'
|
|
664
|
+
return { left: leftNode, right: rightNode };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Standard split (offset > 0 and < text.length)
|
|
668
|
+
console.log(
|
|
669
|
+
`%c[_splitTextNode] Performing standard split...`,
|
|
670
|
+
'color: green;',
|
|
671
|
+
{ text, offset }
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const leftText = text.substring(0, offset);
|
|
675
|
+
const rightText = text.substring(offset);
|
|
676
|
+
|
|
677
|
+
// Modify the original node to become the 'left' node
|
|
678
|
+
const leftNode = cloneDeep(originalNode);
|
|
679
|
+
leftNode.copy = leftText;
|
|
680
|
+
allNodes.set(leftNode.id, leftNode);
|
|
681
|
+
|
|
682
|
+
// Create a new node for the 'right' half
|
|
683
|
+
const rightNode: FlatNode = {
|
|
684
|
+
id: ulid(),
|
|
685
|
+
nodeType: 'TagElement',
|
|
686
|
+
parentId: leftNode.parentId,
|
|
687
|
+
tagName: 'text',
|
|
688
|
+
copy: rightText,
|
|
689
|
+
};
|
|
690
|
+
allNodes.set(rightNode.id, rightNode);
|
|
691
|
+
|
|
692
|
+
const parentChildren = parentNodes.get(leftNode.parentId!)!;
|
|
693
|
+
if (!parentChildren) {
|
|
694
|
+
console.error(
|
|
695
|
+
'_splitTextNode (standard): Could not find parent children list for',
|
|
696
|
+
{
|
|
697
|
+
parentId: leftNode.parentId,
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
return { left: leftNode, right: null };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const nodeIndex = parentChildren.indexOf(leftNode.id);
|
|
704
|
+
const newParentChildren = [...parentChildren];
|
|
705
|
+
|
|
706
|
+
if (nodeIndex > -1) {
|
|
707
|
+
// Insert the new 'right' node immediately after the 'left' node
|
|
708
|
+
newParentChildren.splice(nodeIndex + 1, 0, rightNode.id);
|
|
709
|
+
} else {
|
|
710
|
+
console.warn(
|
|
711
|
+
'_splitTextNode (standard): Could not find node in parent, appending.',
|
|
712
|
+
{
|
|
713
|
+
nodeId: leftNode.id,
|
|
714
|
+
parentId: leftNode.parentId,
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
newParentChildren.push(rightNode.id);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
parentNodes.set(leftNode.parentId!, newParentChildren);
|
|
721
|
+
|
|
722
|
+
this.allNodes.set(allNodes);
|
|
723
|
+
this.parentNodes.set(parentNodes);
|
|
724
|
+
|
|
725
|
+
return { left: leftNode, right: rightNode };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Wraps a range of nodes (typically text nodes) inside a new formatting
|
|
730
|
+
* element, like a <span>. This is an atomic operation with history support.
|
|
731
|
+
*
|
|
732
|
+
* @param range - The selection range state.
|
|
733
|
+
* @param tagName - The tag name for the wrapper element (e.g., 'span').
|
|
734
|
+
* @returns A Promise that resolves with the new wrapper node's ID, or null.
|
|
735
|
+
*/
|
|
736
|
+
// Replacement for wrapRangeInSpan in src/stores/nodes.ts
|
|
737
|
+
|
|
738
|
+
public async wrapRangeInSpan(
|
|
739
|
+
range: SelectionRange,
|
|
740
|
+
tagName: 'span'
|
|
741
|
+
): Promise<string | null> {
|
|
742
|
+
if (
|
|
743
|
+
!range.startNodeId ||
|
|
744
|
+
!range.endNodeId ||
|
|
745
|
+
!range.lcaNodeId ||
|
|
746
|
+
!range.blockNodeId
|
|
747
|
+
) {
|
|
748
|
+
return Promise.resolve(null);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const originalAllNodes = new Map(this.allNodes.get());
|
|
752
|
+
const originalParentNodes = new Map(this.parentNodes.get());
|
|
753
|
+
const paneNodeId = this.getClosestNodeTypeFromId(range.blockNodeId, 'Pane');
|
|
754
|
+
const originalPaneNode = this.allNodes.get().get(paneNodeId)
|
|
755
|
+
? (cloneDeep(this.allNodes.get().get(paneNodeId)) as PaneNode)
|
|
756
|
+
: null;
|
|
757
|
+
const wrapperNodeId = ulid();
|
|
758
|
+
|
|
759
|
+
const redoLogic = (): string | null => {
|
|
760
|
+
if (VERBOSE)
|
|
761
|
+
console.log(
|
|
762
|
+
'%c[wrapRangeInSpan] START',
|
|
763
|
+
'color: blue; font-weight: bold;',
|
|
764
|
+
{
|
|
765
|
+
range: cloneDeep(range),
|
|
766
|
+
lcaChildren_BEFORE: cloneDeep(
|
|
767
|
+
this.parentNodes.get().get(range.lcaNodeId!)
|
|
768
|
+
),
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
if (!range.startNodeId || !range.endNodeId || !range.lcaNodeId)
|
|
772
|
+
return null;
|
|
773
|
+
|
|
774
|
+
let startNodeToFind: string | null = null;
|
|
775
|
+
let endNodeToFind: string | null = null;
|
|
776
|
+
|
|
777
|
+
if (range.startNodeId === range.endNodeId) {
|
|
778
|
+
// SINGLE-NODE SELECTION
|
|
779
|
+
const { left: nodeSplitAtEnd } = this._splitTextNode(
|
|
780
|
+
range.endNodeId,
|
|
781
|
+
range.endCharOffset
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
const { right: middleNode } = this._splitTextNode(
|
|
785
|
+
nodeSplitAtEnd.id,
|
|
786
|
+
range.startCharOffset
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
if (!middleNode) {
|
|
790
|
+
console.error(
|
|
791
|
+
'wrapRangeInSpan: Single-node split failed to create a middle node.'
|
|
792
|
+
);
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
startNodeToFind = middleNode.id;
|
|
796
|
+
endNodeToFind = middleNode.id;
|
|
797
|
+
} else {
|
|
798
|
+
// MULTI-NODE SELECTION
|
|
799
|
+
const { left: endNodeAfterSplit } = this._splitTextNode(
|
|
800
|
+
range.endNodeId,
|
|
801
|
+
range.endCharOffset
|
|
802
|
+
);
|
|
803
|
+
endNodeToFind = endNodeAfterSplit.id;
|
|
804
|
+
|
|
805
|
+
const { right: startNodeAfterSplit } = this._splitTextNode(
|
|
806
|
+
range.startNodeId,
|
|
807
|
+
range.startCharOffset
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
if (!startNodeAfterSplit) {
|
|
811
|
+
console.error(
|
|
812
|
+
'wrapRangeInSpan: Multi-node split failed to create a start node.'
|
|
813
|
+
);
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
startNodeToFind = startNodeAfterSplit.id;
|
|
817
|
+
}
|
|
818
|
+
if (VERBOSE)
|
|
819
|
+
console.log('%c[wrapRangeInSpan] SPLIT COMPLETE', 'color: blue;', {
|
|
820
|
+
startNodeToFind,
|
|
821
|
+
endNodeToFind,
|
|
822
|
+
lcaChildren_AFTER_SPLIT: cloneDeep(
|
|
823
|
+
this.parentNodes.get().get(range.lcaNodeId!)
|
|
824
|
+
),
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const newAllNodes = new Map(this.allNodes.get());
|
|
828
|
+
const newParentNodes = new Map(this.parentNodes.get());
|
|
829
|
+
const parentChildren = newParentNodes.get(range.lcaNodeId!);
|
|
830
|
+
|
|
831
|
+
if (!parentChildren) {
|
|
832
|
+
console.error('wrapRangeInSpan: Could not find parent children');
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const startIndex = parentChildren.indexOf(startNodeToFind!);
|
|
837
|
+
const endIndex = parentChildren.indexOf(endNodeToFind!);
|
|
838
|
+
|
|
839
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
840
|
+
console.error('wrapRangeInSpan: Could not find split nodes in parent', {
|
|
841
|
+
startIndex,
|
|
842
|
+
endIndex,
|
|
843
|
+
startNodeToFind,
|
|
844
|
+
endNodeToFind,
|
|
845
|
+
});
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const actualStartIndex = Math.min(startIndex, endIndex);
|
|
850
|
+
const actualEndIndex = Math.max(startIndex, endIndex);
|
|
851
|
+
|
|
852
|
+
const newParentChildren = [...parentChildren];
|
|
853
|
+
const nodesToWrapIds = newParentChildren.slice(
|
|
854
|
+
actualStartIndex,
|
|
855
|
+
actualEndIndex + 1
|
|
856
|
+
);
|
|
857
|
+
if (VERBOSE)
|
|
858
|
+
console.log('%c[wrapRangeInSpan] INDEXES', 'color: blue;', {
|
|
859
|
+
startIndex,
|
|
860
|
+
endIndex,
|
|
861
|
+
actualStartIndex,
|
|
862
|
+
actualEndIndex,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
if (nodesToWrapIds.length === 0) {
|
|
866
|
+
console.error('wrapRangeInSpan: No nodes to wrap.');
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
if (VERBOSE)
|
|
870
|
+
console.log('%c[wrapRangeInSpan] NODES TO WRAP', 'color: orange;', {
|
|
871
|
+
nodesToWrapIds: cloneDeep(nodesToWrapIds),
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
const nodesToWrap = nodesToWrapIds
|
|
875
|
+
.map((id) => newAllNodes.get(id))
|
|
876
|
+
.filter((n): n is BaseNode => !!n);
|
|
877
|
+
|
|
878
|
+
const wrapperNode: FlatNode = {
|
|
879
|
+
id: wrapperNodeId,
|
|
880
|
+
nodeType: 'TagElement',
|
|
881
|
+
parentId: range.lcaNodeId,
|
|
882
|
+
tagName: tagName,
|
|
883
|
+
};
|
|
884
|
+
newAllNodes.set(wrapperNode.id, wrapperNode);
|
|
885
|
+
newParentNodes.set(wrapperNode.id, []);
|
|
886
|
+
|
|
887
|
+
for (const node of nodesToWrap) {
|
|
888
|
+
node.parentId = wrapperNode.id;
|
|
889
|
+
newParentNodes.get(wrapperNode.id)!.push(node.id);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
newParentChildren.splice(
|
|
893
|
+
actualStartIndex,
|
|
894
|
+
nodesToWrapIds.length,
|
|
895
|
+
wrapperNode.id
|
|
896
|
+
);
|
|
897
|
+
newParentNodes.set(range.lcaNodeId!, newParentChildren);
|
|
898
|
+
|
|
899
|
+
this.allNodes.set(newAllNodes);
|
|
900
|
+
this.parentNodes.set(newParentNodes);
|
|
901
|
+
|
|
902
|
+
if (originalPaneNode) {
|
|
903
|
+
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
904
|
+
notify: false,
|
|
905
|
+
recordHistory: false,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const lcaChildrenIds = newParentNodes.get(range.lcaNodeId!);
|
|
909
|
+
const finalLCAChildrenNodes = lcaChildrenIds
|
|
910
|
+
? lcaChildrenIds.map((id) => newAllNodes.get(id))
|
|
911
|
+
: [];
|
|
912
|
+
|
|
913
|
+
const wrapperChildrenIds = newParentNodes.get(wrapperNodeId);
|
|
914
|
+
const finalWrapperChildrenNodes = wrapperChildrenIds
|
|
915
|
+
? wrapperChildrenIds.map((id) => newAllNodes.get(id))
|
|
916
|
+
: [];
|
|
917
|
+
|
|
918
|
+
if (VERBOSE)
|
|
919
|
+
console.log(
|
|
920
|
+
'%c[wrapRangeInSpan] END (FINAL PAYLOAD)',
|
|
921
|
+
'color: green; font-weight: bold;',
|
|
922
|
+
{
|
|
923
|
+
wrapperNodeId,
|
|
924
|
+
lcaChildren_FINAL: finalLCAChildrenNodes,
|
|
925
|
+
wrapperChildren_FINAL: finalWrapperChildrenNodes,
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
this.notifyNode(range.blockNodeId!);
|
|
929
|
+
return wrapperNode.id;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const undoLogic = () => {
|
|
933
|
+
this.allNodes.set(originalAllNodes);
|
|
934
|
+
this.parentNodes.set(originalParentNodes);
|
|
935
|
+
|
|
936
|
+
if (originalPaneNode) {
|
|
937
|
+
this.modifyNodes([originalPaneNode], {
|
|
938
|
+
notify: false,
|
|
939
|
+
recordHistory: false,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
this.notifyNode(range.blockNodeId!);
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const newSpanId = redoLogic();
|
|
946
|
+
|
|
947
|
+
this.history.addPatch({
|
|
948
|
+
op: PatchOp.REPLACE,
|
|
949
|
+
undo: undoLogic,
|
|
950
|
+
redo: redoLogic,
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return new Promise((resolve) => setTimeout(() => resolve(newSpanId), 310));
|
|
954
|
+
}
|
|
955
|
+
|
|
380
956
|
private clickTimer: number | null = null;
|
|
381
957
|
private DOUBLE_CLICK_DELAY = 300;
|
|
382
958
|
private isProcessingDoubleClick = false;
|
|
@@ -877,6 +1453,35 @@ export class NodesContext {
|
|
|
877
1453
|
: ``
|
|
878
1454
|
}`;
|
|
879
1455
|
}
|
|
1456
|
+
|
|
1457
|
+
if ('tagName' in node && node.tagName === 'span') {
|
|
1458
|
+
const spanNode = node as FlatNode;
|
|
1459
|
+
const [all, mobile, tablet, desktop] = processClassesForViewports(
|
|
1460
|
+
{ mobile: {}, tablet: {}, desktop: {} },
|
|
1461
|
+
spanNode.overrideClasses || {},
|
|
1462
|
+
1
|
|
1463
|
+
);
|
|
1464
|
+
const outlineClass =
|
|
1465
|
+
this.toolModeValStore.get().value === 'styles'
|
|
1466
|
+
? ' outline outline-1 outline-dotted outline-gray-400/60'
|
|
1467
|
+
: '';
|
|
1468
|
+
|
|
1469
|
+
const getClassString = (classes: string[]): string =>
|
|
1470
|
+
classes && classes.length > 0 ? classes[0] : '';
|
|
1471
|
+
|
|
1472
|
+
if (isPreview) return getClassString(desktop) + outlineClass;
|
|
1473
|
+
switch (viewport) {
|
|
1474
|
+
case 'desktop':
|
|
1475
|
+
return getClassString(desktop) + outlineClass;
|
|
1476
|
+
case 'tablet':
|
|
1477
|
+
return getClassString(tablet) + outlineClass;
|
|
1478
|
+
case 'mobile':
|
|
1479
|
+
return getClassString(mobile) + outlineClass;
|
|
1480
|
+
default:
|
|
1481
|
+
return getClassString(all) + outlineClass;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
880
1485
|
const closestPaneId = this.getClosestNodeTypeFromId(
|
|
881
1486
|
nodeId,
|
|
882
1487
|
'Markdown'
|
|
@@ -1362,7 +1967,8 @@ export class NodesContext {
|
|
|
1362
1967
|
});
|
|
1363
1968
|
break;
|
|
1364
1969
|
}
|
|
1365
|
-
this.toolModeValStore.set({ value: '
|
|
1970
|
+
this.toolModeValStore.set({ value: 'styles' });
|
|
1971
|
+
this.notifyNode('root');
|
|
1366
1972
|
}
|
|
1367
1973
|
|
|
1368
1974
|
addTemplateImpressionNode(targetId: string, node: ImpressionNode) {
|
|
@@ -2295,6 +2901,194 @@ export class NodesContext {
|
|
|
2295
2901
|
return { dirtyPaneIds, classes };
|
|
2296
2902
|
}
|
|
2297
2903
|
|
|
2904
|
+
/**
|
|
2905
|
+
* "Unwraps" a formatting node (like <strong> or <em>), merging its text
|
|
2906
|
+
* content with any adjacent text nodes.
|
|
2907
|
+
* @param nodeId - The ID of the formatting node (e.g., the <strong> tag) to unwrap.
|
|
2908
|
+
*/
|
|
2909
|
+
unwrapNode(nodeId: string) {
|
|
2910
|
+
const formatNode = this.allNodes.get().get(nodeId) as FlatNode;
|
|
2911
|
+
if (!formatNode || !formatNode.parentId) {
|
|
2912
|
+
console.warn('unwrapNode: Node or parentId not found.');
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
const parentId = formatNode.parentId;
|
|
2917
|
+
const parentNode = this.allNodes.get().get(parentId) as FlatNode;
|
|
2918
|
+
if (!parentNode) {
|
|
2919
|
+
console.warn('unwrapNode: Parent node not found.');
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// --- 1. Gather all node information for the operation ---
|
|
2924
|
+
|
|
2925
|
+
// Get the children of the formatting node (these are the nodes we want to keep)
|
|
2926
|
+
const childrenToKeepIds = this.getChildNodeIDs(nodeId);
|
|
2927
|
+
const childrenToKeepNodes = childrenToKeepIds
|
|
2928
|
+
.map((id) => this.allNodes.get().get(id))
|
|
2929
|
+
.filter((n): n is BaseNode => n !== undefined);
|
|
2930
|
+
|
|
2931
|
+
// Get the siblings of the formatting node
|
|
2932
|
+
const parentChildrenIds = this.getChildNodeIDs(parentId);
|
|
2933
|
+
const formatNodeIndex = parentChildrenIds.indexOf(nodeId);
|
|
2934
|
+
if (formatNodeIndex === -1) {
|
|
2935
|
+
console.warn('unwrapNode: Node not found in parent children list.');
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// Find adjacent siblings
|
|
2940
|
+
const prevSiblingId =
|
|
2941
|
+
formatNodeIndex > 0 ? parentChildrenIds[formatNodeIndex - 1] : null;
|
|
2942
|
+
const nextSiblingId =
|
|
2943
|
+
formatNodeIndex < parentChildrenIds.length - 1
|
|
2944
|
+
? parentChildrenIds[formatNodeIndex + 1]
|
|
2945
|
+
: null;
|
|
2946
|
+
|
|
2947
|
+
const prevSibling = prevSiblingId
|
|
2948
|
+
? (this.allNodes.get().get(prevSiblingId) as FlatNode)
|
|
2949
|
+
: null;
|
|
2950
|
+
const nextSibling = nextSiblingId
|
|
2951
|
+
? (this.allNodes.get().get(nextSiblingId) as FlatNode)
|
|
2952
|
+
: null;
|
|
2953
|
+
|
|
2954
|
+
// Check if siblings are 'text' nodes
|
|
2955
|
+
const isPrevText =
|
|
2956
|
+
prevSibling &&
|
|
2957
|
+
prevSibling.nodeType === 'TagElement' &&
|
|
2958
|
+
prevSibling.tagName === 'text';
|
|
2959
|
+
const isNextText =
|
|
2960
|
+
nextSibling &&
|
|
2961
|
+
nextSibling.nodeType === 'TagElement' &&
|
|
2962
|
+
nextSibling.tagName === 'text';
|
|
2963
|
+
|
|
2964
|
+
// Get the combined text content from the formatting node's children
|
|
2965
|
+
const unwrappedText = childrenToKeepNodes
|
|
2966
|
+
.map((n) => (n as FlatNode).copy || '')
|
|
2967
|
+
.join('');
|
|
2968
|
+
|
|
2969
|
+
// --- 2. Prepare state snapshots for history ---
|
|
2970
|
+
const originalAllNodes = new Map(this.allNodes.get());
|
|
2971
|
+
const originalParentNodes = new Map(this.parentNodes.get());
|
|
2972
|
+
const originalPaneNode = cloneDeep(
|
|
2973
|
+
this.allNodes
|
|
2974
|
+
.get()
|
|
2975
|
+
.get(this.getClosestNodeTypeFromId(nodeId, 'Pane')) as PaneNode
|
|
2976
|
+
);
|
|
2977
|
+
|
|
2978
|
+
// --- 3. Define the atomic "redo" (forward) operation ---
|
|
2979
|
+
const applyUnwrap = () => {
|
|
2980
|
+
const newAllNodes = new Map(this.allNodes.get());
|
|
2981
|
+
const newParentNodes = new Map(this.parentNodes.get());
|
|
2982
|
+
const newParentChildren = [...(newParentNodes.get(parentId) || [])];
|
|
2983
|
+
|
|
2984
|
+
const nodesToDelete = [formatNode, ...childrenToKeepNodes];
|
|
2985
|
+
const nodesToModify: BaseNode[] = [];
|
|
2986
|
+
const nodesToAdd: BaseNode[] = [];
|
|
2987
|
+
|
|
2988
|
+
let mergedText = unwrappedText;
|
|
2989
|
+
let targetNode: FlatNode | null = null;
|
|
2990
|
+
|
|
2991
|
+
if (isPrevText && prevSibling) {
|
|
2992
|
+
// --- Merge with PREVIOUS sibling ---
|
|
2993
|
+
mergedText = (prevSibling.copy || '') + mergedText;
|
|
2994
|
+
targetNode = cloneDeep(prevSibling);
|
|
2995
|
+
|
|
2996
|
+
if (isNextText && nextSibling) {
|
|
2997
|
+
// --- Also merge with NEXT sibling (3-way merge) ---
|
|
2998
|
+
mergedText += nextSibling.copy || '';
|
|
2999
|
+
nodesToDelete.push(nextSibling);
|
|
3000
|
+
// Remove nextSibling from parent's children list
|
|
3001
|
+
const nextSiblingIndex = newParentChildren.indexOf(nextSiblingId!);
|
|
3002
|
+
if (nextSiblingIndex > -1) {
|
|
3003
|
+
newParentChildren.splice(nextSiblingIndex, 1);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
targetNode.copy = mergedText;
|
|
3008
|
+
nodesToModify.push(targetNode);
|
|
3009
|
+
} else if (isNextText && nextSibling) {
|
|
3010
|
+
// --- Merge with NEXT sibling only ---
|
|
3011
|
+
mergedText = mergedText + (nextSibling.copy || '');
|
|
3012
|
+
targetNode = cloneDeep(nextSibling);
|
|
3013
|
+
targetNode.copy = mergedText;
|
|
3014
|
+
nodesToModify.push(targetNode);
|
|
3015
|
+
} else {
|
|
3016
|
+
// --- No merge. Just insert unwrapped text as new node(s) ---
|
|
3017
|
+
// For simplicity, we merge all children into a single new text node
|
|
3018
|
+
const newTextNode: FlatNode = {
|
|
3019
|
+
id: ulid(),
|
|
3020
|
+
nodeType: 'TagElement',
|
|
3021
|
+
parentId: parentId,
|
|
3022
|
+
tagName: 'text',
|
|
3023
|
+
copy: unwrappedText,
|
|
3024
|
+
};
|
|
3025
|
+
nodesToAdd.push(newTextNode);
|
|
3026
|
+
// Add new text node to parent's children list
|
|
3027
|
+
newParentChildren.splice(formatNodeIndex, 0, newTextNode.id);
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// Apply deletions
|
|
3031
|
+
for (const node of nodesToDelete) {
|
|
3032
|
+
newAllNodes.delete(node.id);
|
|
3033
|
+
const childIndex = newParentChildren.indexOf(node.id);
|
|
3034
|
+
if (childIndex > -1) {
|
|
3035
|
+
newParentChildren.splice(childIndex, 1);
|
|
3036
|
+
}
|
|
3037
|
+
// Remove from parentNodes map if it's a parent itself (unlikely for text)
|
|
3038
|
+
newParentNodes.delete(node.id);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// Apply modifications
|
|
3042
|
+
for (const node of nodesToModify) {
|
|
3043
|
+
newAllNodes.set(node.id, node);
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// Apply additions
|
|
3047
|
+
for (const node of nodesToAdd) {
|
|
3048
|
+
newAllNodes.set(node.id, node);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// Update the parent's children array in the map
|
|
3052
|
+
newParentNodes.set(parentId, newParentChildren);
|
|
3053
|
+
|
|
3054
|
+
// Set the new state
|
|
3055
|
+
this.allNodes.set(newAllNodes);
|
|
3056
|
+
this.parentNodes.set(newParentNodes);
|
|
3057
|
+
|
|
3058
|
+
// Mark pane as dirty
|
|
3059
|
+
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
3060
|
+
notify: false,
|
|
3061
|
+
recordHistory: false,
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
this.notifyNode(`root`);
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
// --- 4. Define the atomic "undo" (backward) operation ---
|
|
3068
|
+
const applyRewrap = () => {
|
|
3069
|
+
// Just restore the original maps
|
|
3070
|
+
this.allNodes.set(originalAllNodes);
|
|
3071
|
+
this.parentNodes.set(originalParentNodes);
|
|
3072
|
+
|
|
3073
|
+
// Restore original pane state
|
|
3074
|
+
this.modifyNodes([originalPaneNode], {
|
|
3075
|
+
notify: false,
|
|
3076
|
+
recordHistory: false,
|
|
3077
|
+
});
|
|
3078
|
+
|
|
3079
|
+
this.notifyNode(`root`);
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
// --- 5. Execute the operation and add to history ---
|
|
3083
|
+
applyUnwrap();
|
|
3084
|
+
|
|
3085
|
+
this.history.addPatch({
|
|
3086
|
+
op: PatchOp.REPLACE, // Using REPLACE as it's a complex operation
|
|
3087
|
+
undo: () => applyRewrap(),
|
|
3088
|
+
redo: () => applyUnwrap(),
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
|
|
2298
3092
|
/**
|
|
2299
3093
|
* Executes a series of updates on a temporary context and then applies the
|
|
2300
3094
|
* results to the main context in a single operation, triggering one UI update.
|