fix: overhaul canvas shape creation, connections, and sync
- Fix CSS position:absolute missing for 5 trip planning shapes - Expand arrow connection mode to all 21 shape types (was only 2) - Center new shapes in viewport instead of clustering top-left - Extract createAndAddShape() utility, eliminating ~270 lines of duplication - Add missing Google Item toolbar button - Add error handling on remote shape creation (try-catch-finally) - Implement actual WebSocket keep-alive ping (was a no-op) - Use shape.toJSON() in sync layer to capture all shape properties (was only 3 types) - Add index signature to ShapeData for arbitrary shape-specific properties Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eedc6b1b4a
commit
38636862d8
|
|
@ -23,6 +23,8 @@ export interface ShapeData {
|
|||
isMinimized?: boolean;
|
||||
isPinned?: boolean;
|
||||
tags?: string[];
|
||||
// Allow arbitrary shape-specific properties from toJSON()
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Automerge document structure
|
||||
|
|
@ -255,6 +257,13 @@ export class CommunitySync extends EventTarget {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a keep-alive ping to prevent WebSocket idle timeout
|
||||
*/
|
||||
ping(): void {
|
||||
this.#send({ type: "ping" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a shape element for syncing
|
||||
*/
|
||||
|
|
@ -309,42 +318,27 @@ export class CommunitySync extends EventTarget {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert FolkShape to serializable data
|
||||
* Convert FolkShape to serializable data using the shape's own toJSON() method.
|
||||
* This ensures all shape-specific properties are captured for every shape type.
|
||||
*/
|
||||
#shapeToData(shape: FolkShape): ShapeData {
|
||||
const json = (shape as any).toJSON?.() ?? {};
|
||||
|
||||
const data: ShapeData = {
|
||||
type: shape.tagName.toLowerCase(),
|
||||
id: shape.id,
|
||||
x: shape.x,
|
||||
y: shape.y,
|
||||
width: shape.width,
|
||||
height: shape.height,
|
||||
rotation: shape.rotation,
|
||||
type: (json.type as string) || shape.tagName.toLowerCase(),
|
||||
id: (json.id as string) || shape.id,
|
||||
x: (json.x as number) ?? shape.x,
|
||||
y: (json.y as number) ?? shape.y,
|
||||
width: (json.width as number) ?? shape.width,
|
||||
height: (json.height as number) ?? shape.height,
|
||||
rotation: (json.rotation as number) ?? shape.rotation,
|
||||
};
|
||||
|
||||
// Add content for markdown shapes
|
||||
if ("content" in shape && typeof (shape as any).content === "string") {
|
||||
data.content = (shape as any).content;
|
||||
}
|
||||
|
||||
// Add arrow properties
|
||||
if (shape.tagName.toLowerCase() === "folk-arrow") {
|
||||
const arrow = shape as any;
|
||||
if (arrow.sourceId) data.sourceId = arrow.sourceId;
|
||||
if (arrow.targetId) data.targetId = arrow.targetId;
|
||||
if (arrow.color) data.color = arrow.color;
|
||||
if (arrow.strokeWidth) data.strokeWidth = arrow.strokeWidth;
|
||||
}
|
||||
|
||||
// Add wrapper properties
|
||||
if (shape.tagName.toLowerCase() === "folk-wrapper") {
|
||||
const wrapper = shape as any;
|
||||
if (wrapper.title) data.title = wrapper.title;
|
||||
if (wrapper.icon) data.icon = wrapper.icon;
|
||||
if (wrapper.primaryColor) data.primaryColor = wrapper.primaryColor;
|
||||
if (wrapper.isMinimized !== undefined) data.isMinimized = wrapper.isMinimized;
|
||||
if (wrapper.isPinned !== undefined) data.isPinned = wrapper.isPinned;
|
||||
if (wrapper.tags?.length) data.tags = wrapper.tags;
|
||||
// Merge all extra properties from toJSON
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
if (!(key in data)) {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -162,17 +162,30 @@
|
|||
folk-transcription,
|
||||
folk-video-chat,
|
||||
folk-obs-note,
|
||||
folk-workflow-block {
|
||||
folk-workflow-block,
|
||||
folk-itinerary,
|
||||
folk-destination,
|
||||
folk-budget,
|
||||
folk-packing-list,
|
||||
folk-booking {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.connect-mode folk-markdown,
|
||||
.connect-mode folk-wrapper {
|
||||
.connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
|
||||
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
|
||||
folk-image-gen, folk-video-gen, folk-prompt, folk-transcription,
|
||||
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||
folk-booking) {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.connect-mode folk-markdown:hover,
|
||||
.connect-mode folk-wrapper:hover {
|
||||
.connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
|
||||
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
|
||||
folk-image-gen, folk-video-gen, folk-prompt, folk-transcription,
|
||||
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||
folk-booking):hover {
|
||||
outline: 2px dashed #3b82f6;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
|
@ -196,6 +209,7 @@
|
|||
<button id="add-chat" title="Add Chat">💬 Chat</button>
|
||||
<button id="add-piano" title="Add Piano">🎹 Piano</button>
|
||||
<button id="add-embed" title="Add Web Embed">🔗 Embed</button>
|
||||
<button id="add-google-item" title="Add Google Item">📎 Google</button>
|
||||
<button id="add-calendar" title="Add Calendar">📅 Calendar</button>
|
||||
<button id="add-map" title="Add Map">🗺️ Map</button>
|
||||
<button id="add-image-gen" title="AI Image Generation">🎨 Image</button>
|
||||
|
|
@ -293,6 +307,16 @@
|
|||
const statusText = document.getElementById("status-text");
|
||||
let shapeCounter = 0;
|
||||
|
||||
// All shape tag names that can be arrow connection endpoints
|
||||
const CONNECTABLE_SELECTOR = [
|
||||
"folk-markdown", "folk-wrapper", "folk-slide", "folk-chat",
|
||||
"folk-google-item", "folk-piano", "folk-embed", "folk-calendar",
|
||||
"folk-map", "folk-image-gen", "folk-video-gen", "folk-prompt",
|
||||
"folk-transcription", "folk-video-chat", "folk-obs-note",
|
||||
"folk-workflow-block", "folk-itinerary", "folk-destination",
|
||||
"folk-budget", "folk-packing-list", "folk-booking"
|
||||
].join(", ");
|
||||
|
||||
// Initialize CommunitySync
|
||||
const sync = new CommunitySync(communitySlug);
|
||||
|
||||
|
|
@ -369,12 +393,19 @@
|
|||
return;
|
||||
}
|
||||
|
||||
isProcessingRemote = true;
|
||||
const shape = createShapeElement(data);
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
isProcessingRemote = false;
|
||||
try {
|
||||
isProcessingRemote = true;
|
||||
const shape = createShapeElement(data);
|
||||
if (shape) {
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Canvas] Failed to create remote shape ${data.id} (${data.type}):`, err);
|
||||
} finally {
|
||||
isProcessingRemote = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle shape deletion from remote
|
||||
|
|
@ -558,317 +589,129 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Add markdown note button
|
||||
document.getElementById("add-markdown").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-markdown");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 300;
|
||||
shape.height = 200;
|
||||
shape.content = "# New Note\n\nStart typing...";
|
||||
// Default dimensions for each shape type
|
||||
const SHAPE_DEFAULTS = {
|
||||
"folk-markdown": { width: 300, height: 200 },
|
||||
"folk-wrapper": { width: 320, height: 240 },
|
||||
"folk-slide": { width: 720, height: 480 },
|
||||
"folk-chat": { width: 400, height: 500 },
|
||||
"folk-google-item": { width: 280, height: 180 },
|
||||
"folk-piano": { width: 800, height: 600 },
|
||||
"folk-embed": { width: 480, height: 360 },
|
||||
"folk-calendar": { width: 320, height: 380 },
|
||||
"folk-map": { width: 500, height: 400 },
|
||||
"folk-image-gen": { width: 400, height: 500 },
|
||||
"folk-video-gen": { width: 450, height: 550 },
|
||||
"folk-prompt": { width: 450, height: 500 },
|
||||
"folk-transcription": { width: 400, height: 450 },
|
||||
"folk-video-chat": { width: 480, height: 400 },
|
||||
"folk-obs-note": { width: 450, height: 500 },
|
||||
"folk-workflow-block": { width: 240, height: 180 },
|
||||
"folk-itinerary": { width: 320, height: 400 },
|
||||
"folk-destination": { width: 280, height: 220 },
|
||||
"folk-budget": { width: 300, height: 350 },
|
||||
"folk-packing-list": { width: 280, height: 350 },
|
||||
"folk-booking": { width: 300, height: 240 },
|
||||
};
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
// Get the center of the current viewport in canvas coordinates
|
||||
function getViewportCenter() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const viewCenterX = rect.width / 2;
|
||||
const viewCenterY = rect.height / 2;
|
||||
// Reverse the canvas transform to get canvas coordinates
|
||||
const canvasX = (viewCenterX - panX) / scale;
|
||||
const canvasY = (viewCenterY - panY) / scale;
|
||||
// Add jitter so shapes don't stack perfectly
|
||||
return {
|
||||
x: canvasX + (Math.random() - 0.5) * 40,
|
||||
y: canvasY + (Math.random() - 0.5) * 40
|
||||
};
|
||||
}
|
||||
|
||||
// Create a shape, position it at viewport center, add to canvas, and register for sync
|
||||
function createAndAddShape(tagName, props = {}) {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
||||
|
||||
const shape = document.createElement(tagName);
|
||||
shape.id = id;
|
||||
|
||||
const center = getViewportCenter();
|
||||
shape.x = center.x - defaults.width / 2;
|
||||
shape.y = center.y - defaults.height / 2;
|
||||
shape.width = defaults.width;
|
||||
shape.height = defaults.height;
|
||||
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
shape[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
} catch (e) {
|
||||
console.error(`[Canvas] Failed to create shape ${tagName}:`, e);
|
||||
shape.remove?.();
|
||||
return null;
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
// Toolbar button handlers
|
||||
document.getElementById("add-markdown").addEventListener("click", () => {
|
||||
createAndAddShape("folk-markdown", { content: "# New Note\n\nStart typing..." });
|
||||
});
|
||||
|
||||
// Add wrapper card button
|
||||
document.getElementById("add-wrapper").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
||||
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
||||
|
||||
const shape = document.createElement("folk-wrapper");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 320;
|
||||
shape.height = 240;
|
||||
shape.title = "New Card";
|
||||
shape.icon = icons[Math.floor(Math.random() * icons.length)];
|
||||
shape.primaryColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
// Add some placeholder content inside the wrapper
|
||||
const content = document.createElement("div");
|
||||
content.style.padding = "16px";
|
||||
content.style.color = "#374151";
|
||||
content.innerHTML = "<p>Click to edit this card...</p>";
|
||||
shape.appendChild(content);
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
const shape = createAndAddShape("folk-wrapper", {
|
||||
title: "New Card",
|
||||
icon: icons[Math.floor(Math.random() * icons.length)],
|
||||
primaryColor: colors[Math.floor(Math.random() * colors.length)],
|
||||
});
|
||||
if (shape) {
|
||||
const content = document.createElement("div");
|
||||
content.style.padding = "16px";
|
||||
content.style.color = "#374151";
|
||||
content.innerHTML = "<p>Click to edit this card...</p>";
|
||||
shape.appendChild(content);
|
||||
}
|
||||
});
|
||||
|
||||
// Add slide button
|
||||
document.getElementById("add-slide").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-slide");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 720;
|
||||
shape.height = 480;
|
||||
shape.label = `Slide ${shapeCounter}`;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
createAndAddShape("folk-slide", { label: `Slide ${shapeCounter}` });
|
||||
});
|
||||
|
||||
// Add chat button
|
||||
document.getElementById("add-chat").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-chat");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 400;
|
||||
shape.height = 500;
|
||||
shape.roomId = `room-${id}`;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
const id = `shape-${Date.now()}-${shapeCounter}`;
|
||||
createAndAddShape("folk-chat", { roomId: `room-${id}` });
|
||||
});
|
||||
|
||||
// Add piano button
|
||||
document.getElementById("add-piano").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-piano");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 800;
|
||||
shape.height = 600;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add embed button
|
||||
document.getElementById("add-embed").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-embed");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 480;
|
||||
shape.height = 360;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add calendar button
|
||||
document.getElementById("add-calendar").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-calendar");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 320;
|
||||
shape.height = 380;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add map button
|
||||
document.getElementById("add-map").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-map");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 500;
|
||||
shape.height = 400;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add image gen button
|
||||
document.getElementById("add-image-gen").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-image-gen");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 400;
|
||||
shape.height = 500;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add video gen button
|
||||
document.getElementById("add-video-gen").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-video-gen");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 450;
|
||||
shape.height = 550;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add prompt button
|
||||
document.getElementById("add-prompt").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-prompt");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 450;
|
||||
shape.height = 500;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add transcription button
|
||||
document.getElementById("add-transcription").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-transcription");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 400;
|
||||
shape.height = 450;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add video chat button
|
||||
document.getElementById("add-video-chat").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-video-chat");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 480;
|
||||
shape.height = 400;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add rich note button
|
||||
document.getElementById("add-obs-note").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-obs-note");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 450;
|
||||
shape.height = 500;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add workflow block button
|
||||
document.getElementById("add-workflow").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-workflow-block");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 240;
|
||||
shape.height = 180;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
document.getElementById("add-piano").addEventListener("click", () => createAndAddShape("folk-piano"));
|
||||
document.getElementById("add-embed").addEventListener("click", () => createAndAddShape("folk-embed"));
|
||||
document.getElementById("add-calendar").addEventListener("click", () => createAndAddShape("folk-calendar"));
|
||||
document.getElementById("add-map").addEventListener("click", () => createAndAddShape("folk-map"));
|
||||
document.getElementById("add-image-gen").addEventListener("click", () => createAndAddShape("folk-image-gen"));
|
||||
document.getElementById("add-video-gen").addEventListener("click", () => createAndAddShape("folk-video-gen"));
|
||||
document.getElementById("add-prompt").addEventListener("click", () => createAndAddShape("folk-prompt"));
|
||||
document.getElementById("add-transcription").addEventListener("click", () => createAndAddShape("folk-transcription"));
|
||||
document.getElementById("add-video-chat").addEventListener("click", () => createAndAddShape("folk-video-chat"));
|
||||
document.getElementById("add-obs-note").addEventListener("click", () => createAndAddShape("folk-obs-note"));
|
||||
document.getElementById("add-workflow").addEventListener("click", () => createAndAddShape("folk-workflow-block"));
|
||||
document.getElementById("add-google-item").addEventListener("click", () => {
|
||||
createAndAddShape("folk-google-item", { service: "drive", title: "New Google Item" });
|
||||
});
|
||||
|
||||
// Trip planning components
|
||||
document.getElementById("add-itinerary").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-itinerary");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 320;
|
||||
shape.height = 400;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
document.getElementById("add-destination").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-destination");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 280;
|
||||
shape.height = 220;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
document.getElementById("add-budget").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-budget");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 300;
|
||||
shape.height = 350;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
document.getElementById("add-packing-list").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-packing-list");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 280;
|
||||
shape.height = 350;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
document.getElementById("add-booking").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const shape = document.createElement("folk-booking");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 300;
|
||||
shape.height = 240;
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
document.getElementById("add-itinerary").addEventListener("click", () => createAndAddShape("folk-itinerary"));
|
||||
document.getElementById("add-destination").addEventListener("click", () => createAndAddShape("folk-destination"));
|
||||
document.getElementById("add-budget").addEventListener("click", () => createAndAddShape("folk-budget"));
|
||||
document.getElementById("add-packing-list").addEventListener("click", () => createAndAddShape("folk-packing-list"));
|
||||
document.getElementById("add-booking").addEventListener("click", () => createAndAddShape("folk-booking"));
|
||||
|
||||
// Arrow connection mode
|
||||
let connectMode = false;
|
||||
|
|
@ -890,7 +733,7 @@
|
|||
canvas.addEventListener("click", (e) => {
|
||||
if (!connectMode) return;
|
||||
|
||||
const target = e.target.closest("folk-markdown, folk-wrapper");
|
||||
const target = e.target.closest(CONNECTABLE_SELECTOR);
|
||||
if (!target || !target.id) return;
|
||||
|
||||
e.stopPropagation();
|
||||
|
|
@ -1014,10 +857,12 @@
|
|||
updateCanvasTransform();
|
||||
}, { passive: false });
|
||||
|
||||
// Keep-alive ping
|
||||
// Keep-alive ping to prevent WebSocket idle timeout
|
||||
setInterval(() => {
|
||||
if (sync.doc) {
|
||||
// Sync is connected, nothing to do
|
||||
try {
|
||||
sync.ping();
|
||||
} catch (e) {
|
||||
console.warn("[Canvas] Keep-alive ping failed:", e);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue