@grafloria/element 0.4.43 → 0.4.45

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.43",
3
+ "version": "0.4.45",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -55,6 +55,7 @@ import { LiveRegionController, registerTool } from '@grafloria/renderer';
55
55
  import { buildCommitCommands, cellFromGridItem, cellToRect, columnUnitFor, gridItemFromCell, pointToCell, rowHeightFor, sizeToSpan, } from './grid-mapping.js';
56
56
  import { ensureDashboardKitStyles } from './styles.js';
57
57
  import { captionOfGroup, captionPainted, captionPassThrough, captionKey, paintCaptionBand, sectionCaptionReserve, sizeCaptionBand } from './caption.js';
58
+ import { TAB_STRIP_HEIGHT } from './tabs.js';
58
59
  /** The class of the painted grip element (a child of the node host). */
59
60
  export const GRIP_CLASS = 'axdb-grip';
60
61
  /** The container class that turns the caption strip's grip dots on. */
@@ -994,6 +995,19 @@ export function bindDashboardGrid(api, group, options = {}) {
994
995
  }
995
996
  return out;
996
997
  };
998
+ /** The member groups a gesture displaced, as their own cell commands — `buildCommitCommands` skips groups by design. */
999
+ const groupCellCommands = (deltas) => {
1000
+ const out = [];
1001
+ for (const d of deltas) {
1002
+ if (!d.isGroup || (d.cellBefore.x === d.cellAfter.x && d.cellBefore.y === d.cellAfter.y && d.cellBefore.w === d.cellAfter.w && d.cellBefore.h === d.cellAfter.h))
1003
+ continue;
1004
+ const og = diagram.getGroup(d.id);
1005
+ if (!og)
1006
+ continue;
1007
+ out.push(new SetGroupCellCommand(d.id, d.cellBefore, d.cellAfter, { x: d.posBefore.x, y: d.posBefore.y, width: d.sizeBefore.width, height: d.sizeBefore.height }, frameOfGroup(og)));
1008
+ }
1009
+ return out;
1010
+ };
997
1011
  const execute = (name, commands) => {
998
1012
  if (commands.length === 0)
999
1013
  return false;
@@ -1249,6 +1263,109 @@ export function bindDashboardGrid(api, group, options = {}) {
1249
1263
  */
1250
1264
  const TAB_FRAME_GRIP = 3;
1251
1265
  const edgeGripFor = (grp) => (isTabsGroup(grp) ? TAB_FRAME_GRIP : EDGE_GRIP);
1266
+ const BESIDE_BAND = 0.2;
1267
+ const besideZoneAt = (wx, wy) => {
1268
+ var _a;
1269
+ for (const id of (_a = group.members) !== null && _a !== void 0 ? _a : []) {
1270
+ const grp = diagram.getGroup(id);
1271
+ if (!grp || diagram.getNode(id) || !isTabsGroup(grp))
1272
+ continue;
1273
+ const p = grp.position;
1274
+ const sz = sizeOf(grp);
1275
+ if (wx < p.x || wx > p.x + sz.width || wy < p.y || wy > p.y + sz.height)
1276
+ continue;
1277
+ const bodyY = p.y + TAB_STRIP_HEIGHT;
1278
+ const bodyH = Math.max(1, sz.height - TAB_STRIP_HEIGHT);
1279
+ const rx = (wx - p.x) / Math.max(1, sz.width);
1280
+ const ry = (wy - bodyY) / bodyH;
1281
+ if (ry < 0)
1282
+ return null; // the strip: a new tab, the strip's own business
1283
+ if (rx >= BESIDE_BAND && rx <= 1 - BESIDE_BAND && ry >= BESIDE_BAND && ry <= 1 - BESIDE_BAND)
1284
+ return null; // the middle: into the page
1285
+ const d = [['left', rx], ['right', 1 - rx], ['top', ry], ['bottom', 1 - ry]];
1286
+ d.sort((a, b) => a[1] - b[1]);
1287
+ return { id, side: d[0][0] };
1288
+ }
1289
+ return null;
1290
+ };
1291
+ /** The container a BESIDE drop unlocked (and maybe shifted): relocked, and put back if asked, when the zone or the gesture ends. */
1292
+ let beside = null;
1293
+ const endBeside = (restore) => {
1294
+ if (!beside)
1295
+ return;
1296
+ const it = engine.getItem(beside.id);
1297
+ if (it && restore && (it.x !== beside.from.x || it.y !== beside.from.y))
1298
+ engine.moveCheck(beside.id, beside.from.x, beside.from.y, { gate: false });
1299
+ if (it)
1300
+ it.locked = true;
1301
+ beside = null;
1302
+ };
1303
+ const applyBeside = (g, z) => {
1304
+ var _a;
1305
+ const tc = engine.getItem(z.id);
1306
+ if (!tc)
1307
+ return;
1308
+ if (g.leg) {
1309
+ g.leg.adopted.abort();
1310
+ g.leg = null;
1311
+ }
1312
+ const w = g.spans.w;
1313
+ const h = g.spans.h;
1314
+ if (g.removedFromBoard) {
1315
+ g.removedFromBoard = false;
1316
+ (_a = hostOf(g.id)) === null || _a === void 0 ? void 0 : _a.classList.remove('axdb-out');
1317
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1318
+ }
1319
+ if (beside && beside.id !== z.id)
1320
+ endBeside(true);
1321
+ const from = beside ? beside.from : { x: tc.x, y: tc.y };
1322
+ let cell;
1323
+ let shiftTo = null;
1324
+ switch (z.side) {
1325
+ case 'right':
1326
+ cell = { x: from.x + tc.w, y: from.y };
1327
+ if (cell.x + w > columns) {
1328
+ shiftTo = { x: columns - w - tc.w, y: from.y };
1329
+ cell = { x: columns - w, y: from.y };
1330
+ }
1331
+ break;
1332
+ case 'left':
1333
+ cell = { x: from.x - w, y: from.y };
1334
+ if (cell.x < 0) {
1335
+ shiftTo = { x: w, y: from.y };
1336
+ cell = { x: 0, y: from.y };
1337
+ }
1338
+ break;
1339
+ case 'top':
1340
+ cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y }; // the ghost takes the container's rows; the container is pushed down under it
1341
+ break;
1342
+ default:
1343
+ cell = { x: Math.max(0, Math.min(from.x, columns - w)), y: from.y + tc.h };
1344
+ }
1345
+ if (shiftTo && shiftTo.x < 0)
1346
+ shiftTo = null; // no room even shifted: the widget goes where it can
1347
+ if (!beside)
1348
+ beside = { id: z.id, from, vacated: cell };
1349
+ tc.locked = false; // for the gesture: it shifts, or the ghost pushes it
1350
+ // The ghost steps off the board while the container shifts: on a bounded
1351
+ // board a chart as wide as the panel had its own cell as the panel's
1352
+ // target, could not be pushed down, and the shift was refused (lab L94).
1353
+ // Then it takes the cell the shift vacated.
1354
+ if (shiftTo && (tc.x !== shiftTo.x || tc.y !== shiftTo.y)) {
1355
+ if (engine.getItem(g.id))
1356
+ engine.remove(g.id);
1357
+ engine.moveCheck(z.id, shiftTo.x, shiftTo.y, { gate: false });
1358
+ }
1359
+ beside.vacated = cell;
1360
+ if (!engine.getItem(g.id))
1361
+ engine.add({ id: g.id, x: 0, y: engine.rows(), w, h });
1362
+ if (!engine.moveCheck(g.id, cell.x, cell.y, { gate: false }).changed) {
1363
+ const at = engine.getItem(g.id);
1364
+ if (!at || at.x !== cell.x || at.y !== cell.y)
1365
+ placeNear(g.id, cell.x, cell.y, w);
1366
+ }
1367
+ project();
1368
+ };
1252
1369
  /**
1253
1370
  * CARRIED (0.4.43): a group dragged by its strip or band moves as ONE thing.
1254
1371
  * The held TILE is transition-exempt (the ghost), but a group has no host of
@@ -1476,13 +1593,36 @@ export function bindDashboardGrid(api, group, options = {}) {
1476
1593
  api.container.style.cursor = cursorFor(edges);
1477
1594
  };
1478
1595
  /**
1479
- * MOVE a section by its caption band or its strip's empty space. A section
1480
- * is a LOCKED tile so that nothing pushes it; for the gesture's duration the
1481
- * tile is unlocked (it is the one moving), other sections stay locked and
1482
- * still refuse it (E4b), and every unlocked tile gets pushed the way a
1483
- * widget pushes it. Its children ride along: the frame moves, the nested
1484
- * board re-projects.
1596
+ * MOVE a section by its caption band, its strip's empty space or its frame
1597
+ * margin. A section is a LOCKED tile so that no WIDGET pushes it; for the
1598
+ * gesture's duration the moving tile is unlocked, and so are the OTHER
1599
+ * member groups (0.4.44): a moved section or group pushes the sections in
1600
+ * its way, the way a dock does. They used to stay locked against it too
1601
+ * (E4b), so the fluid demo's eight-row side panel, with the Operations
1602
+ * section spanning the row beneath it, had no legal column to its left —
1603
+ * the refusal tone at every cell, and the release moved nothing ("I can't
1604
+ * drag the tab group"). Every unlocked tile gets pushed the way a widget
1605
+ * pushes it; a group's children ride along: the frame moves, the nested
1606
+ * board re-projects. The others relock on release.
1485
1607
  */
1608
+ let slabUnlocked = [];
1609
+ const unlockOthersForSlab = (id) => {
1610
+ slabUnlocked = [];
1611
+ for (const o of engine.getItems()) {
1612
+ if (o.id !== id && o.locked && isGroupMember(o.id)) {
1613
+ o.locked = false;
1614
+ slabUnlocked.push(o.id);
1615
+ }
1616
+ }
1617
+ };
1618
+ const relockOthersForSlab = () => {
1619
+ for (const oid of slabUnlocked) {
1620
+ const o = engine.getItem(oid);
1621
+ if (o)
1622
+ o.locked = true;
1623
+ }
1624
+ slabUnlocked = [];
1625
+ };
1486
1626
  const beginSlabMove = (id, ev) => {
1487
1627
  const grp = diagram.getGroup(id);
1488
1628
  const it = engine.getItem(id);
@@ -1491,6 +1631,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1491
1631
  engine.beginGesture();
1492
1632
  const snap = snapshotAll();
1493
1633
  it.locked = false;
1634
+ unlockOthersForSlab(id);
1494
1635
  slabGesture = {
1495
1636
  id,
1496
1637
  edges: NO_EDGES,
@@ -1628,6 +1769,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1628
1769
  releasePointer(g.pointerId);
1629
1770
  api.container.style.cursor = '';
1630
1771
  relockSlab(g.id);
1772
+ relockOthersForSlab();
1631
1773
  if (g.move && g.started)
1632
1774
  setCarried(g.id, false); // exempt through the drop write, then the glides resume
1633
1775
  if (!g.started) {
@@ -1638,7 +1780,13 @@ export function bindDashboardGrid(api, group, options = {}) {
1638
1780
  project();
1639
1781
  const it = engine.getItem(g.id);
1640
1782
  const grp = diagram.getGroup(g.id);
1641
- const commands = buildCommitCommands(deltasSince(g.startCells, g.startGeom, g.id));
1783
+ const deltas = deltasSince(g.startCells, g.startGeom, g.id);
1784
+ const commands = buildCommitCommands(deltas);
1785
+ // The sections and groups the move PUSHED (0.4.44): the tile commit skips
1786
+ // groups by design (a group's cell is its own command), so they are
1787
+ // committed here the way a dock commits the groups it displaced — or the
1788
+ // model snapped every one of them, and the mover, straight back.
1789
+ commands.push(...groupCellCommands(deltas));
1642
1790
  const b = g.cellBefore;
1643
1791
  if (it && grp && (b.x !== it.x || b.y !== it.y || b.w !== it.w || b.h !== it.h)) {
1644
1792
  commands.push(new SetGroupCellCommand(g.id, b, { x: it.x, y: it.y, w: it.w, h: it.h }, g.frameBefore, frameOfGroup(grp)));
@@ -1658,6 +1806,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1658
1806
  releasePointer(g.pointerId);
1659
1807
  api.container.style.cursor = '';
1660
1808
  relockSlab(g.id);
1809
+ relockOthersForSlab();
1661
1810
  if (g.move && g.started)
1662
1811
  setCarried(g.id, false);
1663
1812
  if (g.started)
@@ -1759,6 +1908,8 @@ export function bindDashboardGrid(api, group, options = {}) {
1759
1908
  project();
1760
1909
  const deltas = deltasSince(g.startCells, g.startGeom);
1761
1910
  const commands = buildCommitCommands(deltas);
1911
+ commands.push(...groupCellCommands(deltas)); // a container a BESIDE drop shifted (0.4.45)
1912
+ endBeside(false);
1762
1913
  if (g.esc && g.esc.rowsAdded !== 0) {
1763
1914
  commands.push(new SetGroupCellCommand(group.id, g.esc.cellBefore, g.esc.cellAfter, g.esc.frameBefore, g.esc.frameAfter));
1764
1915
  }
@@ -1797,6 +1948,7 @@ export function bindDashboardGrid(api, group, options = {}) {
1797
1948
  g.esc.peer.resizeMemberBy(group.id, -g.esc.rowsAdded); // slab back down
1798
1949
  g.esc = null;
1799
1950
  }
1951
+ endBeside(true);
1800
1952
  if (g.started) {
1801
1953
  if (g.removedFromBoard || g.kind === 'palette') {
1802
1954
  // The engine cannot resurrect a removed item — rebuild from the
@@ -1915,6 +2067,31 @@ export function bindDashboardGrid(api, group, options = {}) {
1915
2067
  g.strip = null;
1916
2068
  }
1917
2069
  }
2070
+ // BESIDE a tab container (0.4.45): its outer band puts the widget next
2071
+ // to it — at the board's edge the container shifts over to make room.
2072
+ if (!isStatic) {
2073
+ const z = besideZoneAt(ev.world.x, ev.world.y);
2074
+ if (z) {
2075
+ applyBeside(g, z);
2076
+ syncPlaceholder();
2077
+ return;
2078
+ }
2079
+ if (beside) {
2080
+ // Off the band: the container comes back unless the POINTER is
2081
+ // still over the cell it vacated — the widget's own landing, which
2082
+ // is what "beside" leaves under the pointer once the container has
2083
+ // shifted away (the ghost's wanted cell carries the grab offset and
2084
+ // read as "elsewhere" a move later, and the shift undid itself).
2085
+ const r = cellToRect({ x: beside.vacated.x, y: beside.vacated.y, w: g.spans.w, h: g.spans.h }, frame(), geom(), rows());
2086
+ const over = ev.world.x >= r.x - gap && ev.world.x <= r.x + r.width + gap && ev.world.y >= r.y - gap && ev.world.y <= r.y + r.height + gap;
2087
+ if (!over)
2088
+ endBeside(true);
2089
+ else {
2090
+ syncPlaceholder();
2091
+ return;
2092
+ }
2093
+ }
2094
+ }
1918
2095
  // Deepest board under the pointer wins: the nested KPI strip beats the
1919
2096
  // tab that contains it; a foreign board beats "outside". Strict frames
1920
2097
  // first; the one-row grace band below each board (gridstack's extra