mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
fix(flowchart): wire selection + layout precedence; v4.9.4
- Add opts.onNodeClick + surgical applySelectionHighlight() so node/edge clicks immediately paint .selected and mirror id into panel state (Delete/Backspace works on freshly-clicked node). - Add Maximize/Restore button to flowchart toolbar: toggles .main-content.flowchart-takeover which hides .editor-container and lets the canvas + preview split the full window width instead of the 280px sidebar. - destroy() clears the takeover class so the editor stays usable after leaving the panel. - 8 new tests in tests/flowchart-panel.test.js for selection wiring and takeover. Amit Haridas
This commit is contained in:
@@ -131,6 +131,28 @@ function createCanvas(container, store, opts = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Surgical selection highlight — toggles the `.selected` class on the
|
||||
// existing SVG <g> / <line> elements without going through render() (which
|
||||
// replaces all children and would detach the very element the user's
|
||||
// pointer is still on, breaking pointermove/pointerup bubbling on the
|
||||
// same node during a drag). Called from onPointerDown after the internal
|
||||
// selectedNodeId/selectedEdgeId update. The existing
|
||||
// .flowchart-node.selected / .flowchart-edge.selected CSS rules (see
|
||||
// src/styles-sidebar.css) handle the visual highlight.
|
||||
function applySelectionHighlight() {
|
||||
if (destroyed) return;
|
||||
const nodeEls = nodesLayer.querySelectorAll('g[data-node-id]');
|
||||
nodeEls.forEach((g) => {
|
||||
const id = g.getAttribute('data-node-id');
|
||||
g.classList.toggle('selected', id === selectedNodeId);
|
||||
});
|
||||
const edgeEls = edgesLayer.querySelectorAll('line[data-edge-id]');
|
||||
edgeEls.forEach((l) => {
|
||||
const id = l.getAttribute('data-edge-id');
|
||||
l.classList.toggle('selected', id === selectedEdgeId);
|
||||
});
|
||||
}
|
||||
|
||||
// ----- pointer events -----
|
||||
let dragState = null;
|
||||
|
||||
@@ -152,6 +174,13 @@ function createCanvas(container, store, opts = {}) {
|
||||
if (!node) return;
|
||||
selectedNodeId = nodeId;
|
||||
selectedEdgeId = null;
|
||||
// Paint the .flowchart-node.selected highlight immediately on a bare
|
||||
// click (without a drag). store.subscribe would normally trigger
|
||||
// render() after moveNode; a click-only path has no store mutation, so
|
||||
// we apply the highlight ourselves. Surgical toggle — not a full
|
||||
// render() — so the pointerdown target stays attached and subsequent
|
||||
// pointermove/pointerup can still bubble on the same element.
|
||||
applySelectionHighlight();
|
||||
const start = getSvgPoint(ev.clientX, ev.clientY);
|
||||
if (ev.altKey) {
|
||||
// Alt+drag = create a new edge from this node to wherever the pointer
|
||||
@@ -167,6 +196,11 @@ function createCanvas(container, store, opts = {}) {
|
||||
pointerY: start.y,
|
||||
};
|
||||
}
|
||||
// Notify the panel so its internal selection state (used by Delete /
|
||||
// Backspace keyboard shortcuts) tracks the canvas selection.
|
||||
if (typeof opts.onNodeClick === 'function') {
|
||||
opts.onNodeClick(nodeId, ev);
|
||||
}
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -174,6 +208,10 @@ function createCanvas(container, store, opts = {}) {
|
||||
if (edgeLine) {
|
||||
selectedEdgeId = edgeLine.getAttribute('data-edge-id');
|
||||
selectedNodeId = null;
|
||||
// Same reasoning as the node branch above: paint the edge highlight
|
||||
// immediately so a click-without-drag isn't invisible until the next
|
||||
// store mutation triggers a re-render.
|
||||
applySelectionHighlight();
|
||||
if (typeof opts.onEdgeClick === 'function') {
|
||||
opts.onEdgeClick(selectedEdgeId, ev);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
* - debounced persistence to <userData>/flowchart-session.json
|
||||
* - panel-scoped keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z / Delete)
|
||||
* - "Insert at Cursor" button (reuses the existing `insert-content` IPC)
|
||||
* - "Maximize / Restore" toggle that promotes the panel to fill the
|
||||
* main-content area (hides the editor-container) so the canvas +
|
||||
* preview split get the full window width instead of the 280px sidebar.
|
||||
*
|
||||
* @param {HTMLElement} container Mount point inside the sidebar panel
|
||||
* @param {object} deps
|
||||
@@ -27,6 +30,10 @@ const { toMermaid } = require('../flowchart/flowchart-mermaid');
|
||||
const PREVIEW_DEBOUNCE_MS = 250;
|
||||
const PERSIST_DEBOUNCE_MS = 500;
|
||||
const PERSISTENCE_FILENAME = 'flowchart-session.json';
|
||||
// CSS class toggled on `.main-content` while the panel is in takeover mode.
|
||||
// Kept colocated with the panel so any reader can grep for it. See
|
||||
// src/styles-sidebar.css `.main-content.flowchart-takeover` rules.
|
||||
const TAKEOVER_CLASS = 'flowchart-takeover';
|
||||
|
||||
function renderFlowChartPanel(container, deps) {
|
||||
const { getUserDataPath, readFile, writeFile, insertAtCursor, renderMermaid = () => {} } = deps;
|
||||
@@ -51,6 +58,14 @@ function renderFlowChartPanel(container, deps) {
|
||||
Insert at Cursor
|
||||
</button>
|
||||
<span class="flowchart-status" aria-live="polite"></span>
|
||||
<button
|
||||
class="flowchart-maximize-btn"
|
||||
title="Maximize: hide the editor and let the canvas + preview fill the main area"
|
||||
aria-label="Maximize Flow Chart panel"
|
||||
>
|
||||
<span class="flowchart-maximize-icon" aria-hidden="true">⤢</span>
|
||||
<span class="flowchart-maximize-label">Maximize</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flowchart-split">
|
||||
<div class="flowchart-canvas-host"></div>
|
||||
@@ -67,9 +82,29 @@ function renderFlowChartPanel(container, deps) {
|
||||
const previewRenderEl = container.querySelector('.flowchart-preview-render');
|
||||
const insertBtn = container.querySelector('.flowchart-insert-btn');
|
||||
const statusEl = container.querySelector('.flowchart-status');
|
||||
const maximizeBtn = container.querySelector('.flowchart-maximize-btn');
|
||||
const maximizeLabel = container.querySelector('.flowchart-maximize-label');
|
||||
|
||||
let selectedNodeId = null;
|
||||
let selectedEdgeId = null;
|
||||
// Takeover state: when true, `.main-content` carries `flowchart-takeover`
|
||||
// and the editor-container is hidden so the panel + canvas + preview split
|
||||
// the full window width. Toggled by the maximize button (and cleaned up
|
||||
// by destroy() so leaving it doesn't leave the editor hidden).
|
||||
let takeoverActive = false;
|
||||
|
||||
// The sidebar lives inside `.main-content` (see src/styles-sidebar.css).
|
||||
// Look it up by walking up from the panel container — that way the panel
|
||||
// doesn't need to know whether the renderer mounted it via #sidebar or
|
||||
// any future container.
|
||||
function findMainContent() {
|
||||
let el = container;
|
||||
while (el && el.parentElement) {
|
||||
el = el.parentElement;
|
||||
if (el.classList && el.classList.contains('main-content')) return el;
|
||||
}
|
||||
return document.querySelector('.main-content');
|
||||
}
|
||||
|
||||
const store = createStore({
|
||||
persistencePath: persistenceFile,
|
||||
@@ -91,6 +126,9 @@ function renderFlowChartPanel(container, deps) {
|
||||
onEdgeClick: (edgeId) => {
|
||||
// Click an edge → prompt for kind and (optional) label. v2 can replace
|
||||
// this with a real popover menu; the prompts are intentionally simple.
|
||||
// Mirror the selection into panel state so Delete/Backspace on the
|
||||
// panel-scoped keydown handler routes to the right entity even after a
|
||||
// pure click (no drag, no store mutation to trigger render-based sync).
|
||||
selectedEdgeId = edgeId;
|
||||
selectedNodeId = null;
|
||||
const edge = store.getGraph().edges.find((e) => e.id === edgeId);
|
||||
@@ -104,6 +142,14 @@ function renderFlowChartPanel(container, deps) {
|
||||
store.setEdgeLabel(edgeId, nextLabel);
|
||||
}
|
||||
},
|
||||
onNodeClick: (nodeId) => {
|
||||
// Canvas → panel selection sync. The canvas already paints the
|
||||
// .flowchart-node.selected highlight (see flowchart-canvas.js render()
|
||||
// and the .flowchart-node.selected CSS rule). Mirror the id into the
|
||||
// panel's selection state so Delete/Backspace routes here.
|
||||
selectedNodeId = nodeId;
|
||||
selectedEdgeId = null;
|
||||
},
|
||||
onShapeMenu: (nodeId) => {
|
||||
// Prompt for a new shape kind. v2: replace with a real context menu.
|
||||
const next = window.prompt(
|
||||
@@ -157,6 +203,35 @@ function renderFlowChartPanel(container, deps) {
|
||||
}
|
||||
insertBtn.addEventListener('click', onInsertClick);
|
||||
|
||||
// Maximize / restore toggle. When active, .main-content.flowchart-takeover
|
||||
// hides the editor-container and lets the sidebar + canvas fill the row.
|
||||
// Click again to restore. Kept inside the panel so its lifecycle matches
|
||||
// the panel's destroy() cleanup.
|
||||
function setTakeover(active) {
|
||||
const mainContent = findMainContent();
|
||||
if (!mainContent) return;
|
||||
takeoverActive = active;
|
||||
mainContent.classList.toggle(TAKEOVER_CLASS, active);
|
||||
maximizeBtn.classList.toggle('active', active);
|
||||
maximizeBtn.setAttribute(
|
||||
'aria-label',
|
||||
active ? 'Restore Flow Chart panel' : 'Maximize Flow Chart panel'
|
||||
);
|
||||
maximizeBtn.setAttribute(
|
||||
'title',
|
||||
active
|
||||
? 'Restore: show the editor again'
|
||||
: 'Maximize: hide the editor and let the canvas + preview fill the main area'
|
||||
);
|
||||
if (maximizeLabel) {
|
||||
maximizeLabel.textContent = active ? 'Restore' : 'Maximize';
|
||||
}
|
||||
}
|
||||
function onMaximizeClick() {
|
||||
setTakeover(!takeoverActive);
|
||||
}
|
||||
maximizeBtn.addEventListener('click', onMaximizeClick);
|
||||
|
||||
// Keyboard shortcuts — panel-scoped.
|
||||
function onContainerKeyDown(ev) {
|
||||
if (ev.ctrlKey && !ev.metaKey && ev.key.toLowerCase() === 'z') {
|
||||
@@ -203,6 +278,10 @@ function renderFlowChartPanel(container, deps) {
|
||||
unsubscribeStore();
|
||||
container.removeEventListener('keydown', onContainerKeyDown);
|
||||
insertBtn.removeEventListener('click', onInsertClick);
|
||||
maximizeBtn.removeEventListener('click', onMaximizeClick);
|
||||
// Restore the editor if we were in takeover mode — leaving the class
|
||||
// on .main-content would hide the editor for the rest of the session.
|
||||
if (takeoverActive) setTakeover(false);
|
||||
canvas.destroy();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1111,3 +1111,66 @@ body[class*='dark'] .preview-content a.wiki-link {
|
||||
color: inherit;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Takeover mode — the Flow Chart panel promotes itself to fill the main
|
||||
* area, hiding the editor-container so the canvas + preview split the full
|
||||
* window width instead of the 280px sidebar. Toggled by the maximize
|
||||
* button in src/sidebar/flowchart-panel.js (adds/removes
|
||||
* `.flowchart-takeover` on `.main-content`). Restore on second click or on
|
||||
* panel destroy(). */
|
||||
.main-content.flowchart-takeover .editor-container {
|
||||
display: none;
|
||||
}
|
||||
.main-content.flowchart-takeover .sidebar {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.main-content.flowchart-takeover .sidebar:not(.collapsed) {
|
||||
/* The takeover is only meaningful with the flowchart panel open, so the
|
||||
* 48px collapsed-state width doesn't apply. Force the wide layout. */
|
||||
width: auto;
|
||||
}
|
||||
.main-content.flowchart-takeover .sidebar-panel {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
.main-content.flowchart-takeover .sidebar-panel-content {
|
||||
padding: 0;
|
||||
}
|
||||
/* The panel itself should fill the panel-content column instead of being
|
||||
* constrained by the normal 12px padding-block flow. */
|
||||
.main-content.flowchart-takeover .flowchart-panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Maximize button — small icon+label pill in the panel toolbar. */
|
||||
.flowchart-maximize-btn {
|
||||
margin-left: 8px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border-color, #444);
|
||||
background: var(--bg-primary, #fff);
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.flowchart-maximize-btn:hover {
|
||||
border-color: var(--accent, #4a9eff);
|
||||
color: var(--accent, #4a9eff);
|
||||
}
|
||||
.flowchart-maximize-btn:focus-visible {
|
||||
outline: 2px solid var(--accent, #4a9eff);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.flowchart-maximize-btn.active {
|
||||
background: var(--accent, #4a9eff);
|
||||
border-color: var(--accent, #4a9eff);
|
||||
color: var(--accent-fg, #fff);
|
||||
}
|
||||
.flowchart-maximize-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user