feat: SVG drawing persistence, click-to-edit markdown, quick-add rApps, dblclick pencil
- SVG whiteboard drawings now persist via Automerge (addShapeData for DOM-less shapes, wb-svg type in newShapeElement, eraser deletes from doc) - folk-markdown: click preview to edit, edit-enter/edit-exit events sync with folk-shape editing state, refactored into enter/exitMarkdownEdit - Desktop quick-add (+) button opens rApps popout panel directly - Double-click empty canvas background activates pencil draw mode - Canvas background click exits editing mode on all shapes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9f3c9abf5b
commit
645f1fc015
|
|
@ -25,6 +25,8 @@ export interface ShapeData {
|
|||
isMinimized?: boolean;
|
||||
isPinned?: boolean;
|
||||
tags?: string[];
|
||||
// Whiteboard SVG drawing
|
||||
svgMarkup?: string;
|
||||
// Allow arbitrary shape-specific properties from toJSON()
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
|
@ -495,6 +497,18 @@ export class CommunitySync extends EventTarget {
|
|||
this.forgetShape(shapeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add raw shape data directly (for shapes without DOM elements, like wb-svg drawings).
|
||||
*/
|
||||
addShapeData(shapeData: ShapeData): void {
|
||||
this.#doc = Automerge.change(this.#doc, `Add shape ${shapeData.id}`, (doc) => {
|
||||
if (!doc.shapes) doc.shapes = {};
|
||||
doc.shapes[shapeData.id] = shapeData;
|
||||
});
|
||||
this.#scheduleSave();
|
||||
this.#syncToServer();
|
||||
}
|
||||
|
||||
/**
|
||||
* FUN: Update — explicitly update specific fields of a shape.
|
||||
* Use this for programmatic updates (API calls, module callbacks).
|
||||
|
|
|
|||
|
|
@ -186,23 +186,51 @@ export class FolkMarkdown extends FolkShape {
|
|||
const editBtn = wrapper.querySelector(".edit-btn") as HTMLButtonElement;
|
||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||
|
||||
// Edit toggle
|
||||
// Helper to enter/exit markdown edit mode
|
||||
const enterMarkdownEdit = () => {
|
||||
if (this.#isEditing) return;
|
||||
this.#isEditing = true;
|
||||
editor.style.display = "block";
|
||||
preview.style.display = "none";
|
||||
editor.value = this.#content;
|
||||
editor.focus();
|
||||
};
|
||||
|
||||
const exitMarkdownEdit = () => {
|
||||
if (!this.#isEditing) return;
|
||||
this.#isEditing = false;
|
||||
editor.style.display = "none";
|
||||
preview.style.display = "block";
|
||||
this.content = editor.value;
|
||||
preview.innerHTML = this.#renderMarkdown(this.#content);
|
||||
};
|
||||
|
||||
// Edit toggle button
|
||||
editBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.#isEditing = !this.#isEditing;
|
||||
if (this.#isEditing) {
|
||||
editor.style.display = "block";
|
||||
preview.style.display = "none";
|
||||
editor.value = this.#content;
|
||||
editor.focus();
|
||||
exitMarkdownEdit();
|
||||
} else {
|
||||
editor.style.display = "none";
|
||||
preview.style.display = "block";
|
||||
this.content = editor.value;
|
||||
preview.innerHTML = this.#renderMarkdown(this.#content);
|
||||
enterMarkdownEdit();
|
||||
}
|
||||
});
|
||||
|
||||
// Click on preview enters edit mode (when shape is focused/editing)
|
||||
preview.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
enterMarkdownEdit();
|
||||
});
|
||||
|
||||
// When parent shape enters edit mode, also enter markdown edit
|
||||
this.addEventListener("edit-enter", () => {
|
||||
enterMarkdownEdit();
|
||||
});
|
||||
|
||||
// When parent shape exits edit mode, also exit markdown edit
|
||||
this.addEventListener("edit-exit", () => {
|
||||
exitMarkdownEdit();
|
||||
});
|
||||
|
||||
// Close button
|
||||
closeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -215,11 +243,7 @@ export class FolkMarkdown extends FolkShape {
|
|||
});
|
||||
|
||||
editor.addEventListener("blur", () => {
|
||||
this.#isEditing = false;
|
||||
editor.style.display = "none";
|
||||
preview.style.display = "block";
|
||||
this.content = editor.value;
|
||||
preview.innerHTML = this.#renderMarkdown(this.#content);
|
||||
exitMarkdownEdit();
|
||||
});
|
||||
|
||||
// Initial render
|
||||
|
|
|
|||
|
|
@ -1240,6 +1240,10 @@
|
|||
if (document.getElementById(data.id)) {
|
||||
return;
|
||||
}
|
||||
// Check if wb-svg already exists in the overlay
|
||||
if (data.type === "wb-svg" && wbOverlay.querySelector(`[data-wb-id="${data.id}"]`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isProcessingRemote = true;
|
||||
|
|
@ -1262,6 +1266,9 @@
|
|||
if (shape && shape.parentNode) {
|
||||
shape.remove();
|
||||
}
|
||||
// Also remove wb-svg elements from the overlay
|
||||
const wbEl = wbOverlay?.querySelector(`[data-wb-id="${shapeId}"]`);
|
||||
if (wbEl) wbEl.remove();
|
||||
});
|
||||
|
||||
// Create a shape element from data
|
||||
|
|
@ -1487,6 +1494,18 @@
|
|||
if (data.maxItems) shape.maxItems = data.maxItems;
|
||||
if (data.refreshInterval) shape.refreshInterval = data.refreshInterval;
|
||||
break;
|
||||
case "wb-svg":
|
||||
// Whiteboard SVG drawing — recreate in SVG overlay, not as a folk-shape
|
||||
if (data.svgMarkup) {
|
||||
const temp = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
temp.innerHTML = data.svgMarkup;
|
||||
const svgEl = temp.firstElementChild;
|
||||
if (svgEl) {
|
||||
svgEl.setAttribute("data-wb-id", data.id);
|
||||
wbOverlay.appendChild(svgEl);
|
||||
}
|
||||
}
|
||||
return null; // Not a folk-shape element
|
||||
case "folk-markdown":
|
||||
default:
|
||||
shape = document.createElement("folk-markdown");
|
||||
|
|
@ -2170,15 +2189,34 @@
|
|||
wbOverlay.addEventListener("pointerup", (e) => {
|
||||
if (!wbDrawing) return;
|
||||
wbDrawing = false;
|
||||
|
||||
// Persist the completed SVG element to Automerge
|
||||
if (wbPreviewEl) {
|
||||
const wbId = `wb-${Date.now()}-${++shapeCounter}`;
|
||||
wbPreviewEl.setAttribute("data-wb-id", wbId);
|
||||
const svgMarkup = wbPreviewEl.outerHTML;
|
||||
|
||||
sync.addShapeData({
|
||||
type: "wb-svg",
|
||||
id: wbId,
|
||||
svgMarkup,
|
||||
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
||||
});
|
||||
}
|
||||
|
||||
wbPreviewEl = null;
|
||||
wbCurrentPath = [];
|
||||
});
|
||||
|
||||
// Eraser: click on existing SVG strokes to delete them
|
||||
// Eraser: click on existing SVG strokes to delete them + remove from Automerge
|
||||
wbOverlay.addEventListener("click", (e) => {
|
||||
if (wbTool !== "eraser") return;
|
||||
const hit = e.target;
|
||||
if (hit && hit !== wbOverlay && wbOverlay.contains(hit)) {
|
||||
const wbId = hit.getAttribute("data-wb-id");
|
||||
if (wbId) {
|
||||
sync.deleteShape(wbId);
|
||||
}
|
||||
hit.remove();
|
||||
}
|
||||
});
|
||||
|
|
@ -2407,6 +2445,21 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Desktop quick-add button → opens the rApps popout panel
|
||||
document.getElementById("quick-add")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const rAppsGroup = toolbarEl.querySelector(".toolbar-group:has(#embed-notes)")
|
||||
|| [...toolbarEl.querySelectorAll(".toolbar-group")].find(g =>
|
||||
g.querySelector(".toolbar-group-toggle")?.textContent.includes("rApps"));
|
||||
if (rAppsGroup) {
|
||||
if (activeToolbarGroup === rAppsGroup) {
|
||||
closeToolbarPanel();
|
||||
} else {
|
||||
openToolbarPanel(rAppsGroup);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collapse/expand toolbar
|
||||
const collapseBtn = document.getElementById("toolbar-collapse");
|
||||
collapseBtn.addEventListener("click", () => {
|
||||
|
|
@ -2556,6 +2609,13 @@
|
|||
canvas.style.cursor = "";
|
||||
});
|
||||
|
||||
// Double-click on empty canvas background → quick-draw (pencil) mode
|
||||
canvas.addEventListener("dblclick", (e) => {
|
||||
if (e.target === canvas || e.target === canvasContent) {
|
||||
setWbTool("pencil");
|
||||
}
|
||||
});
|
||||
|
||||
// Keep-alive ping to prevent WebSocket idle timeout
|
||||
setInterval(() => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue