mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
8 new toolbar buttons (Align L/R/T/B, Center H/V, Distribute H/V)
plus a Select All button. Each operation takes the current
selection and applies a pure-function transform to the (x, y)
positions, then commits each result via store.moveNode().
Architecture:
- src/flowchart/flowchart-align.js: pure module with 8 helpers
+ nodeWidth / nodeHeight utility. UMD wrapper for browser
global + CommonJS, same pattern as the other pure modules.
- src/renderer/flowchart-bundle.js: pure module inlined so the
standalone window doesn't need a separate bundle build; the
inlined functions are exposed as window.FlowchartAlign for
jsdom tests.
- Bundle's _selectedNodeIds Set tracks the alignment selection.
Empty by default; populated by Select All or by future
canvas-side shift+click (item 1: multi-select).
- Each align button no-ops with a status hint if fewer than 2
nodes are selected. Distribute needs 3+.
- Each moveNode call creates its own undo snapshot, which is
fine for typical 2-10 node selections and lets the user undo
a misalignment one node at a time.
Tests: 14 new (1376 total) covering all 8 functions plus
immutability, no-op for insufficient nodes, and unsorted-input
sorting for distribute.
Amit Haridas
135 lines
4.4 KiB
JavaScript
135 lines
4.4 KiB
JavaScript
/**
|
|
* Pure alignment + distribution helpers for the flowchart editor (v4.13.0).
|
|
*
|
|
* Each function takes an array of node objects and returns a new array
|
|
* with the same nodes but their (x, y) adjusted per the operation.
|
|
* Pure module — no DOM, no globals, no store mutation — so it's
|
|
* unit-testable in isolation and the caller decides how to apply the
|
|
* result (one moveNode per node, or a future batch API).
|
|
*
|
|
* All operations are no-ops when given fewer than two nodes (nothing
|
|
* to align against). Distribute needs three or more to have a meaningful
|
|
* "even space" between extremes.
|
|
*
|
|
* Coordinates are SVG units. A node's height is the canvas default
|
|
* (60px) since the editor doesn't store height — only width.
|
|
*
|
|
* @module flowchart-align
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const DEFAULT_HEIGHT = 60;
|
|
|
|
function nodeWidth(node) {
|
|
return Number(node.width) > 0 ? Number(node.width) : 120;
|
|
}
|
|
|
|
function nodeHeight(_node) {
|
|
return DEFAULT_HEIGHT;
|
|
}
|
|
|
|
function clone(node) {
|
|
return { ...node };
|
|
}
|
|
|
|
/** All nodes line up at the leftmost x. */
|
|
function alignLeft(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const minX = Math.min(...nodes.map((n) => n.x));
|
|
return nodes.map((n) => Object.assign(clone(n), { x: minX }));
|
|
}
|
|
|
|
/** All nodes line up at the rightmost (x + width). */
|
|
function alignRight(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const maxRight = Math.max(...nodes.map((n) => n.x + nodeWidth(n)));
|
|
return nodes.map((n) => Object.assign(clone(n), { x: maxRight - nodeWidth(n) }));
|
|
}
|
|
|
|
/** All nodes line up at the topmost y. */
|
|
function alignTop(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const minY = Math.min(...nodes.map((n) => n.y));
|
|
return nodes.map((n) => Object.assign(clone(n), { y: minY }));
|
|
}
|
|
|
|
/** All nodes line up at the bottommost (y + height). */
|
|
function alignBottom(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const maxBottom = Math.max(...nodes.map((n) => n.y + nodeHeight(n)));
|
|
return nodes.map((n) => Object.assign(clone(n), { y: maxBottom - nodeHeight(n) }));
|
|
}
|
|
|
|
/** All nodes share the same horizontal center (mean of centers). */
|
|
function alignCenterHorizontal(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const centers = nodes.map((n) => n.x + nodeWidth(n) / 2);
|
|
const avg = centers.reduce((a, b) => a + b, 0) / centers.length;
|
|
return nodes.map((n) => Object.assign(clone(n), { x: avg - nodeWidth(n) / 2 }));
|
|
}
|
|
|
|
/** All nodes share the same vertical center (mean of centers). */
|
|
function alignCenterVertical(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 2) return nodes;
|
|
const centers = nodes.map((n) => n.y + nodeHeight(n) / 2);
|
|
const avg = centers.reduce((a, b) => a + b, 0) / centers.length;
|
|
return nodes.map((n) => Object.assign(clone(n), { y: avg - nodeHeight(n) / 2 }));
|
|
}
|
|
|
|
/**
|
|
* Distribute horizontally — equal gap between consecutive node left edges.
|
|
* The first and last nodes keep their positions; everything in between
|
|
* is spaced evenly.
|
|
*/
|
|
function distributeHorizontally(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 3) return nodes;
|
|
const sorted = [...nodes].sort((a, b) => a.x - b.x);
|
|
const leftmost = sorted[0].x;
|
|
const rightmost = sorted[sorted.length - 1].x;
|
|
const gap = (rightmost - leftmost) / (sorted.length - 1);
|
|
return sorted.map((n, i) => Object.assign(clone(n), { x: leftmost + gap * i }));
|
|
}
|
|
|
|
/**
|
|
* Distribute vertically — equal gap between consecutive node top edges.
|
|
* Same first/last-anchored semantics as the horizontal version.
|
|
*/
|
|
function distributeVertically(nodes) {
|
|
if (!Array.isArray(nodes) || nodes.length < 3) return nodes;
|
|
const sorted = [...nodes].sort((a, b) => a.y - b.y);
|
|
const topmost = sorted[0].y;
|
|
const bottommost = sorted[sorted.length - 1].y;
|
|
const gap = (bottommost - topmost) / (sorted.length - 1);
|
|
return sorted.map((n, i) => Object.assign(clone(n), { y: topmost + gap * i }));
|
|
}
|
|
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = {
|
|
alignLeft,
|
|
alignRight,
|
|
alignTop,
|
|
alignBottom,
|
|
alignCenterHorizontal,
|
|
alignCenterVertical,
|
|
distributeHorizontally,
|
|
distributeVertically,
|
|
nodeWidth,
|
|
nodeHeight,
|
|
};
|
|
}
|
|
if (typeof window !== 'undefined') {
|
|
window.FlowchartAlign = {
|
|
alignLeft,
|
|
alignRight,
|
|
alignTop,
|
|
alignBottom,
|
|
alignCenterHorizontal,
|
|
alignCenterVertical,
|
|
distributeHorizontally,
|
|
distributeVertically,
|
|
nodeWidth,
|
|
nodeHeight,
|
|
};
|
|
}
|