astro-tractstack 2.0.7 → 2.0.9
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/package.json
CHANGED
|
@@ -21,6 +21,8 @@ import { cloneDeep } from '@/utils/helpers';
|
|
|
21
21
|
import { PatchOp } from '@/stores/nodesHistory';
|
|
22
22
|
import type { FlatNode, PaneNode } from '@/types/compositorTypes';
|
|
23
23
|
import type { NodeProps } from '@/types/nodeProps';
|
|
24
|
+
import { useStore } from '@nanostores/react';
|
|
25
|
+
import { XMarkIcon } from '@heroicons/react/20/solid';
|
|
24
26
|
|
|
25
27
|
export type NodeTagProps = NodeProps & { tagName: keyof JSX.IntrinsicElements };
|
|
26
28
|
|
|
@@ -39,6 +41,8 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
39
41
|
const originalContentRef = useRef<string>('');
|
|
40
42
|
const cursorPositionRef = useRef<{ node: Node; offset: number } | null>(null);
|
|
41
43
|
|
|
44
|
+
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
45
|
+
|
|
42
46
|
// Get node data
|
|
43
47
|
const node = ctx.allNodes.get().get(nodeId) as FlatNode;
|
|
44
48
|
const children = ctx.getChildNodeIDs(nodeId);
|
|
@@ -228,8 +232,65 @@ export const NodeBasicTag = (props: NodeTagProps) => {
|
|
|
228
232
|
}
|
|
229
233
|
}, [editState]);
|
|
230
234
|
|
|
231
|
-
// For formatting nodes
|
|
232
|
-
if (['em', 'strong'
|
|
235
|
+
// For formatting nodes <em> and <strong>
|
|
236
|
+
if (['em', 'strong'].includes(props.tagName)) {
|
|
237
|
+
const isEditorActive = toolModeVal === 'text' || toolModeVal === 'styles';
|
|
238
|
+
|
|
239
|
+
const handleUnwrapClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
e.stopPropagation();
|
|
242
|
+
ctx.unwrapNode(nodeId);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
let baseClasses = ctx.getNodeClasses(nodeId, viewportKeyStore.get().value);
|
|
246
|
+
|
|
247
|
+
if (isEditorActive) {
|
|
248
|
+
baseClasses += ' outline outline-1 outline-dotted outline-gray-400/60';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return createElement(
|
|
252
|
+
Tag,
|
|
253
|
+
{
|
|
254
|
+
className: baseClasses,
|
|
255
|
+
onClick: (e: MouseEvent) => {
|
|
256
|
+
if (isEditableMode) {
|
|
257
|
+
ctx.setClickedNodeId(nodeId);
|
|
258
|
+
} else {
|
|
259
|
+
ctx.setClickedNodeId(nodeId);
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
'data-node-id': nodeId,
|
|
264
|
+
tabIndex: isEditableMode ? -1 : undefined,
|
|
265
|
+
style: {
|
|
266
|
+
position: isEditorActive ? 'relative' : undefined,
|
|
267
|
+
outlineOffset: '1px',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
[
|
|
271
|
+
<RenderChildren key="children" children={children} nodeProps={props} />,
|
|
272
|
+
isEditorActive && (
|
|
273
|
+
<span
|
|
274
|
+
key="chip"
|
|
275
|
+
className="absolute z-10 select-none"
|
|
276
|
+
style={{ top: '-.85rem', right: '0' }}
|
|
277
|
+
>
|
|
278
|
+
<button
|
|
279
|
+
type="button"
|
|
280
|
+
onClick={handleUnwrapClick}
|
|
281
|
+
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-100/90 text-gray-700 shadow-sm hover:bg-gray-300/50 focus:outline-none"
|
|
282
|
+
aria-label="Remove formatting"
|
|
283
|
+
>
|
|
284
|
+
<XMarkIcon className="h-3.5 w-3.5" />
|
|
285
|
+
</button>
|
|
286
|
+
</span>
|
|
287
|
+
),
|
|
288
|
+
]
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// For interactive elements like <a> and <button>
|
|
293
|
+
if (['a', 'button'].includes(props.tagName)) {
|
|
233
294
|
return createElement(
|
|
234
295
|
Tag,
|
|
235
296
|
{
|
|
@@ -14,31 +14,43 @@ export default function BunnyMomentSelector({
|
|
|
14
14
|
const [timestamp, setTimestamp] = useState('0');
|
|
15
15
|
|
|
16
16
|
useEffect(() => {
|
|
17
|
-
if (value) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
if (!value || value.trim() === '()') {
|
|
18
|
+
setSelectedVideoId('');
|
|
19
|
+
setTimestamp('0');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const match = value.match(/^\(\s*([^\s]+)\s+(\d+)\s*\)$/);
|
|
25
|
+
if (match && match[1] && match[2]) {
|
|
26
|
+
setSelectedVideoId(match[1]);
|
|
27
|
+
setTimestamp(match[2]);
|
|
28
|
+
} else {
|
|
29
|
+
console.warn('Could not parse BunnyMoment value:', value);
|
|
30
|
+
setSelectedVideoId('');
|
|
31
|
+
setTimestamp('0');
|
|
28
32
|
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error('Error parsing value:', e);
|
|
35
|
+
setSelectedVideoId('');
|
|
36
|
+
setTimestamp('0');
|
|
29
37
|
}
|
|
30
38
|
}, [value]);
|
|
31
39
|
|
|
32
40
|
const handleTimeSelect = (time: string, videoId?: string) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const finalVideoId = videoId || selectedVideoId;
|
|
42
|
+
if (!finalVideoId) return;
|
|
43
|
+
|
|
44
|
+
setSelectedVideoId(finalVideoId);
|
|
36
45
|
setTimestamp(time);
|
|
37
|
-
updateValue(
|
|
46
|
+
updateValue(finalVideoId, time);
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
const updateValue = (videoId: string, time: string) => {
|
|
41
|
-
if (!videoId)
|
|
50
|
+
if (!videoId) {
|
|
51
|
+
onChange('');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
42
54
|
onChange(`(bunnyMoment (${videoId} ${time}))`);
|
|
43
55
|
};
|
|
44
56
|
|
|
@@ -2295,6 +2295,194 @@ export class NodesContext {
|
|
|
2295
2295
|
return { dirtyPaneIds, classes };
|
|
2296
2296
|
}
|
|
2297
2297
|
|
|
2298
|
+
/**
|
|
2299
|
+
* "Unwraps" a formatting node (like <strong> or <em>), merging its text
|
|
2300
|
+
* content with any adjacent text nodes.
|
|
2301
|
+
* @param nodeId - The ID of the formatting node (e.g., the <strong> tag) to unwrap.
|
|
2302
|
+
*/
|
|
2303
|
+
unwrapNode(nodeId: string) {
|
|
2304
|
+
const formatNode = this.allNodes.get().get(nodeId) as FlatNode;
|
|
2305
|
+
if (!formatNode || !formatNode.parentId) {
|
|
2306
|
+
console.warn('unwrapNode: Node or parentId not found.');
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
const parentId = formatNode.parentId;
|
|
2311
|
+
const parentNode = this.allNodes.get().get(parentId) as FlatNode;
|
|
2312
|
+
if (!parentNode) {
|
|
2313
|
+
console.warn('unwrapNode: Parent node not found.');
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// --- 1. Gather all node information for the operation ---
|
|
2318
|
+
|
|
2319
|
+
// Get the children of the formatting node (these are the nodes we want to keep)
|
|
2320
|
+
const childrenToKeepIds = this.getChildNodeIDs(nodeId);
|
|
2321
|
+
const childrenToKeepNodes = childrenToKeepIds
|
|
2322
|
+
.map((id) => this.allNodes.get().get(id))
|
|
2323
|
+
.filter((n): n is BaseNode => n !== undefined);
|
|
2324
|
+
|
|
2325
|
+
// Get the siblings of the formatting node
|
|
2326
|
+
const parentChildrenIds = this.getChildNodeIDs(parentId);
|
|
2327
|
+
const formatNodeIndex = parentChildrenIds.indexOf(nodeId);
|
|
2328
|
+
if (formatNodeIndex === -1) {
|
|
2329
|
+
console.warn('unwrapNode: Node not found in parent children list.');
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Find adjacent siblings
|
|
2334
|
+
const prevSiblingId =
|
|
2335
|
+
formatNodeIndex > 0 ? parentChildrenIds[formatNodeIndex - 1] : null;
|
|
2336
|
+
const nextSiblingId =
|
|
2337
|
+
formatNodeIndex < parentChildrenIds.length - 1
|
|
2338
|
+
? parentChildrenIds[formatNodeIndex + 1]
|
|
2339
|
+
: null;
|
|
2340
|
+
|
|
2341
|
+
const prevSibling = prevSiblingId
|
|
2342
|
+
? (this.allNodes.get().get(prevSiblingId) as FlatNode)
|
|
2343
|
+
: null;
|
|
2344
|
+
const nextSibling = nextSiblingId
|
|
2345
|
+
? (this.allNodes.get().get(nextSiblingId) as FlatNode)
|
|
2346
|
+
: null;
|
|
2347
|
+
|
|
2348
|
+
// Check if siblings are 'text' nodes
|
|
2349
|
+
const isPrevText =
|
|
2350
|
+
prevSibling &&
|
|
2351
|
+
prevSibling.nodeType === 'TagElement' &&
|
|
2352
|
+
prevSibling.tagName === 'text';
|
|
2353
|
+
const isNextText =
|
|
2354
|
+
nextSibling &&
|
|
2355
|
+
nextSibling.nodeType === 'TagElement' &&
|
|
2356
|
+
nextSibling.tagName === 'text';
|
|
2357
|
+
|
|
2358
|
+
// Get the combined text content from the formatting node's children
|
|
2359
|
+
const unwrappedText = childrenToKeepNodes
|
|
2360
|
+
.map((n) => (n as FlatNode).copy || '')
|
|
2361
|
+
.join('');
|
|
2362
|
+
|
|
2363
|
+
// --- 2. Prepare state snapshots for history ---
|
|
2364
|
+
const originalAllNodes = new Map(this.allNodes.get());
|
|
2365
|
+
const originalParentNodes = new Map(this.parentNodes.get());
|
|
2366
|
+
const originalPaneNode = cloneDeep(
|
|
2367
|
+
this.allNodes
|
|
2368
|
+
.get()
|
|
2369
|
+
.get(this.getClosestNodeTypeFromId(nodeId, 'Pane')) as PaneNode
|
|
2370
|
+
);
|
|
2371
|
+
|
|
2372
|
+
// --- 3. Define the atomic "redo" (forward) operation ---
|
|
2373
|
+
const applyUnwrap = () => {
|
|
2374
|
+
const newAllNodes = new Map(this.allNodes.get());
|
|
2375
|
+
const newParentNodes = new Map(this.parentNodes.get());
|
|
2376
|
+
const newParentChildren = [...(newParentNodes.get(parentId) || [])];
|
|
2377
|
+
|
|
2378
|
+
const nodesToDelete = [formatNode, ...childrenToKeepNodes];
|
|
2379
|
+
const nodesToModify: BaseNode[] = [];
|
|
2380
|
+
const nodesToAdd: BaseNode[] = [];
|
|
2381
|
+
|
|
2382
|
+
let mergedText = unwrappedText;
|
|
2383
|
+
let targetNode: FlatNode | null = null;
|
|
2384
|
+
|
|
2385
|
+
if (isPrevText && prevSibling) {
|
|
2386
|
+
// --- Merge with PREVIOUS sibling ---
|
|
2387
|
+
mergedText = (prevSibling.copy || '') + mergedText;
|
|
2388
|
+
targetNode = cloneDeep(prevSibling);
|
|
2389
|
+
|
|
2390
|
+
if (isNextText && nextSibling) {
|
|
2391
|
+
// --- Also merge with NEXT sibling (3-way merge) ---
|
|
2392
|
+
mergedText += nextSibling.copy || '';
|
|
2393
|
+
nodesToDelete.push(nextSibling);
|
|
2394
|
+
// Remove nextSibling from parent's children list
|
|
2395
|
+
const nextSiblingIndex = newParentChildren.indexOf(nextSiblingId!);
|
|
2396
|
+
if (nextSiblingIndex > -1) {
|
|
2397
|
+
newParentChildren.splice(nextSiblingIndex, 1);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
targetNode.copy = mergedText;
|
|
2402
|
+
nodesToModify.push(targetNode);
|
|
2403
|
+
} else if (isNextText && nextSibling) {
|
|
2404
|
+
// --- Merge with NEXT sibling only ---
|
|
2405
|
+
mergedText = mergedText + (nextSibling.copy || '');
|
|
2406
|
+
targetNode = cloneDeep(nextSibling);
|
|
2407
|
+
targetNode.copy = mergedText;
|
|
2408
|
+
nodesToModify.push(targetNode);
|
|
2409
|
+
} else {
|
|
2410
|
+
// --- No merge. Just insert unwrapped text as new node(s) ---
|
|
2411
|
+
// For simplicity, we merge all children into a single new text node
|
|
2412
|
+
const newTextNode: FlatNode = {
|
|
2413
|
+
id: ulid(),
|
|
2414
|
+
nodeType: 'TagElement',
|
|
2415
|
+
parentId: parentId,
|
|
2416
|
+
tagName: 'text',
|
|
2417
|
+
copy: unwrappedText,
|
|
2418
|
+
};
|
|
2419
|
+
nodesToAdd.push(newTextNode);
|
|
2420
|
+
// Add new text node to parent's children list
|
|
2421
|
+
newParentChildren.splice(formatNodeIndex, 0, newTextNode.id);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Apply deletions
|
|
2425
|
+
for (const node of nodesToDelete) {
|
|
2426
|
+
newAllNodes.delete(node.id);
|
|
2427
|
+
const childIndex = newParentChildren.indexOf(node.id);
|
|
2428
|
+
if (childIndex > -1) {
|
|
2429
|
+
newParentChildren.splice(childIndex, 1);
|
|
2430
|
+
}
|
|
2431
|
+
// Remove from parentNodes map if it's a parent itself (unlikely for text)
|
|
2432
|
+
newParentNodes.delete(node.id);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Apply modifications
|
|
2436
|
+
for (const node of nodesToModify) {
|
|
2437
|
+
newAllNodes.set(node.id, node);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// Apply additions
|
|
2441
|
+
for (const node of nodesToAdd) {
|
|
2442
|
+
newAllNodes.set(node.id, node);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Update the parent's children array in the map
|
|
2446
|
+
newParentNodes.set(parentId, newParentChildren);
|
|
2447
|
+
|
|
2448
|
+
// Set the new state
|
|
2449
|
+
this.allNodes.set(newAllNodes);
|
|
2450
|
+
this.parentNodes.set(newParentNodes);
|
|
2451
|
+
|
|
2452
|
+
// Mark pane as dirty
|
|
2453
|
+
this.modifyNodes([{ ...originalPaneNode, isChanged: true }], {
|
|
2454
|
+
notify: false,
|
|
2455
|
+
recordHistory: false,
|
|
2456
|
+
});
|
|
2457
|
+
|
|
2458
|
+
this.notifyNode(parentId);
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// --- 4. Define the atomic "undo" (backward) operation ---
|
|
2462
|
+
const applyRewrap = () => {
|
|
2463
|
+
// Just restore the original maps
|
|
2464
|
+
this.allNodes.set(originalAllNodes);
|
|
2465
|
+
this.parentNodes.set(originalParentNodes);
|
|
2466
|
+
|
|
2467
|
+
// Restore original pane state
|
|
2468
|
+
this.modifyNodes([originalPaneNode], {
|
|
2469
|
+
notify: false,
|
|
2470
|
+
recordHistory: false,
|
|
2471
|
+
});
|
|
2472
|
+
|
|
2473
|
+
this.notifyNode(parentId);
|
|
2474
|
+
};
|
|
2475
|
+
|
|
2476
|
+
// --- 5. Execute the operation and add to history ---
|
|
2477
|
+
applyUnwrap();
|
|
2478
|
+
|
|
2479
|
+
this.history.addPatch({
|
|
2480
|
+
op: PatchOp.REPLACE, // Using REPLACE as it's a complex operation
|
|
2481
|
+
undo: () => applyRewrap(),
|
|
2482
|
+
redo: () => applyUnwrap(),
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2298
2486
|
/**
|
|
2299
2487
|
* Executes a series of updates on a temporary context and then applies the
|
|
2300
2488
|
* results to the main context in a single operation, triggering one UI update.
|