@grafloria/element 0.4.30 → 0.4.31

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.30",
3
+ "version": "0.4.31",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -761,57 +761,157 @@ export function createDashboardHandle(ctx) {
761
761
  new RegisterWidgetCommand(born, 'register'),
762
762
  ]),
763
763
  ];
764
- if (remaining.length > 0)
765
- return { move, collapse: [] };
766
- // The last page left: the container closes. Its registry goes FIRST
767
- // so that on undo it comes back AFTER the group and its slab do.
768
- let fromSlot = -1;
769
- const gone = {
764
+ return { move, collapse: remaining.length > 0 ? [] : collapseOf(boardId) };
765
+ },
766
+ joinTargets: [...ctx.layoutOf.keys()].filter((id) => { var _a, _b; return id !== containerId && id !== pageId && ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && !!model.getGroup(id) && !insidePage(id); }),
767
+ dropIndex: (targetId, cx, cy) => {
768
+ const strip = ctx.tabStrips.get(targetId);
769
+ const n = liveCount(targetId);
770
+ if (!strip)
771
+ return n;
772
+ const r = strip.getBoundingClientRect();
773
+ if (r.width === 0 || cx < r.left || cx > r.right || cy < r.top || cy > r.bottom)
774
+ return n;
775
+ const rtl = strip.getAttribute('dir') === 'rtl';
776
+ let i = 0;
777
+ for (const t of Array.from(strip.querySelectorAll('.axdb-tab'))) {
778
+ const tr = t.getBoundingClientRect();
779
+ const mid = tr.left + tr.width / 2;
780
+ if (rtl ? cx < mid : cx > mid)
781
+ i++;
782
+ else
783
+ break;
784
+ }
785
+ return i;
786
+ },
787
+ markDrop: (targetId, index) => {
788
+ for (const [id, el] of ctx.tabStrips) {
789
+ const on = id === targetId;
790
+ el.classList.toggle('axdb-tabs--drop', on);
791
+ const tabs = Array.from(el.querySelectorAll('.axdb-tab'));
792
+ tabs.forEach((t, i) => t.classList.toggle('axdb-tab--drop-before', on && index !== null && i === index));
793
+ el.classList.toggle('axdb-tabs--drop-end', on && index !== null && index >= tabs.length);
794
+ }
795
+ },
796
+ join: (targetId, index, boardId) => {
797
+ var _a;
798
+ const target = (_a = ctx.boardGroups.get(targetId)) !== null && _a !== void 0 ? _a : model.getGroup(targetId);
799
+ const targetSpec = specById.get(targetId);
800
+ if (!target || !targetSpec)
801
+ return { move: [], collapse: [] };
802
+ const prevActive = ctx.activeTab.get(targetId);
803
+ const cwOf = (g) => { var _a; return ((_a = g.getMetadata('containerWidget')) !== null && _a !== void 0 ? _a : {}); };
804
+ let slot = -1;
805
+ const joined = {
770
806
  register: () => {
771
807
  var _a;
772
- const live = model.getGroup(containerId);
773
- if (!live)
774
- return;
775
- ctx.boardGroups.set(containerId, live);
776
- ctx.layoutOf.set(containerId, 'tabs');
777
- ctx.viewOfBoard.set(containerId, viewId);
778
- if (fromSpec)
779
- specById.set(containerId, fromSpec);
780
- const arr = ctx.boardWidgets.get(boardId);
781
- if (arr && fromSpec && !arr.some((w) => w.id === containerId)) {
782
- arr.splice(fromSlot < 0 ? arr.length : Math.min(fromSlot, arr.length), 0, fromSpec);
808
+ viewOfWidget.set(pageId, targetId);
809
+ if (fromSpec === null || fromSpec === void 0 ? void 0 : fromSpec.widgets) {
810
+ const i = fromSpec.widgets.findIndex((p) => p.id === pageId);
811
+ if (i >= 0) {
812
+ slot = i;
813
+ fromSpec.widgets.splice(i, 1);
814
+ }
815
+ }
816
+ targetSpec.widgets = (_a = targetSpec.widgets) !== null && _a !== void 0 ? _a : [];
817
+ if (!targetSpec.widgets.some((p) => p.id === pageId)) {
818
+ targetSpec.widgets.splice(Math.max(0, Math.min(index, targetSpec.widgets.length)), 0, pageSpec);
783
819
  }
784
- (_a = ctx.attachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
820
+ ctx.boardWidgets.set(targetId, targetSpec.widgets);
821
+ // The moved tab is the one showing, as it is in VS Code.
822
+ ctx.activeTab.set(targetId, pageId);
823
+ targetSpec.active = pageId;
824
+ target.setMetadata('containerWidget', Object.assign(Object.assign({}, cwOf(target)), { active: pageId }));
785
825
  },
786
826
  unregister: () => {
787
- var _a;
788
- (_a = ctx.detachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
789
- ctx.boardGroups.delete(containerId);
790
- ctx.layoutOf.delete(containerId);
791
- ctx.viewOfBoard.delete(containerId);
792
- specById.delete(containerId);
793
- const arr = ctx.boardWidgets.get(boardId);
794
- if (arr) {
795
- const i = arr.findIndex((w) => w.id === containerId);
796
- if (i >= 0) {
797
- fromSlot = i;
798
- arr.splice(i, 1);
799
- }
827
+ viewOfWidget.set(pageId, containerId);
828
+ if (targetSpec.widgets) {
829
+ const i = targetSpec.widgets.findIndex((p) => p.id === pageId);
830
+ if (i >= 0)
831
+ targetSpec.widgets.splice(i, 1);
832
+ }
833
+ if ((fromSpec === null || fromSpec === void 0 ? void 0 : fromSpec.widgets) && !fromSpec.widgets.some((p) => p.id === pageId)) {
834
+ fromSpec.widgets.splice(slot < 0 ? fromSpec.widgets.length : Math.min(slot, fromSpec.widgets.length), 0, pageSpec);
835
+ }
836
+ if (prevActive) {
837
+ ctx.activeTab.set(targetId, prevActive);
838
+ targetSpec.active = prevActive;
839
+ target.setMetadata('containerWidget', Object.assign(Object.assign({}, cwOf(target)), { active: prevActive }));
800
840
  }
801
841
  },
802
842
  };
803
- return {
804
- move,
805
- collapse: [
806
- new SequenceCommand('Close empty tab container', [
807
- new RegisterWidgetCommand(gone, 'unregister'),
808
- new RemoveFromGroupCommand(boardId, containerId),
809
- new RemoveGroupCommand(containerId),
810
- ]),
811
- ],
812
- };
843
+ const move = [
844
+ new SequenceCommand('Move tab', [
845
+ new RemoveFromGroupCommand(containerId, pageId),
846
+ new AddToGroupCommand(targetId, pageId),
847
+ new RegisterWidgetCommand(joined, 'register'),
848
+ ]),
849
+ ];
850
+ return { move, collapse: remaining.length > 0 ? [] : collapseOf(boardId) };
813
851
  },
814
852
  };
853
+ /** A tab container nested inside the page being moved cannot be its target. */
854
+ function insidePage(id) {
855
+ let cur = model.getGroup(id);
856
+ for (let i = 0; cur && i < 32; i++) {
857
+ if (cur.id === pageId)
858
+ return true;
859
+ cur = cur.parentGroupId ? model.getGroup(cur.parentGroupId) : undefined;
860
+ }
861
+ return false;
862
+ }
863
+ function liveCount(id) {
864
+ var _a, _b;
865
+ return [...((_b = (_a = model.getGroup(id)) === null || _a === void 0 ? void 0 : _a.members) !== null && _b !== void 0 ? _b : [])].filter((m) => !!model.getGroup(m)).length;
866
+ }
867
+ /**
868
+ * The last page left: the container closes. Its registry goes FIRST so
869
+ * that on undo it comes back AFTER the group and its slab do.
870
+ */
871
+ function collapseOf(boardId) {
872
+ let fromSlot = -1;
873
+ const gone = {
874
+ register: () => {
875
+ var _a;
876
+ const live = model.getGroup(containerId);
877
+ if (!live)
878
+ return;
879
+ ctx.boardGroups.set(containerId, live);
880
+ ctx.layoutOf.set(containerId, 'tabs');
881
+ ctx.viewOfBoard.set(containerId, viewId);
882
+ if (fromSpec)
883
+ specById.set(containerId, fromSpec);
884
+ const arr = ctx.boardWidgets.get(boardId);
885
+ if (arr && fromSpec && !arr.some((w) => w.id === containerId)) {
886
+ arr.splice(fromSlot < 0 ? arr.length : Math.min(fromSlot, arr.length), 0, fromSpec);
887
+ }
888
+ (_a = ctx.attachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
889
+ },
890
+ unregister: () => {
891
+ var _a;
892
+ (_a = ctx.detachTabsContainer) === null || _a === void 0 ? void 0 : _a.call(ctx, containerId);
893
+ ctx.boardGroups.delete(containerId);
894
+ ctx.layoutOf.delete(containerId);
895
+ ctx.viewOfBoard.delete(containerId);
896
+ specById.delete(containerId);
897
+ const arr = ctx.boardWidgets.get(boardId);
898
+ if (arr) {
899
+ const i = arr.findIndex((w) => w.id === containerId);
900
+ if (i >= 0) {
901
+ fromSlot = i;
902
+ arr.splice(i, 1);
903
+ }
904
+ }
905
+ },
906
+ };
907
+ return [
908
+ new SequenceCommand('Close empty tab container', [
909
+ new RegisterWidgetCommand(gone, 'unregister'),
910
+ new RemoveFromGroupCommand(boardId, containerId),
911
+ new RemoveGroupCommand(containerId),
912
+ ]),
913
+ ];
914
+ }
815
915
  };
816
916
  /** Bookkeeping closures for one widget, shared by the add and remove
817
917
  * commands (and applied synchronously by the handle — idempotent). */
@@ -538,6 +538,26 @@ export interface TearOutPlan {
538
538
  move: Command[];
539
539
  collapse: Command[];
540
540
  };
541
+ /**
542
+ * The tab containers the page may JOIN instead of forming a group of its
543
+ * own — VS Code's editor dragged from one group into another. Every one it
544
+ * may legally enter (not the container it left, none inside itself); the
545
+ * board hit-tests them against the pointer, and a release over one joins.
546
+ */
547
+ joinTargets: string[];
548
+ /** The tab index a release at this CLIENT point means for `targetId`: over the strip, between the tabs there; over the body, the end. */
549
+ dropIndex(targetId: string, clientX: number, clientY: number): number;
550
+ /** Paint (or, with null, clear) the insertion mark on `targetId`'s strip. */
551
+ markDrop(targetId: string | null, index: number | null): void;
552
+ /**
553
+ * The commands for joining `targetId` at `index`: the page becomes its tab
554
+ * — and the active one — and the container it left closes when it was the
555
+ * last page. No cell is taken on any board.
556
+ */
557
+ join(targetId: string, index: number, boardId: string): {
558
+ move: Command[];
559
+ collapse: Command[];
560
+ };
541
561
  }
542
562
  /** A torn-out page never arrives shorter than this: a strip with no room under it is not a group. */
543
563
  export declare const TEAR_OUT_MIN_ROWS = 2;
@@ -559,6 +579,16 @@ interface AdoptedLeg {
559
579
  x: number;
560
580
  y: number;
561
581
  }): void;
582
+ /**
583
+ * Take the tile OFF the board while the pointer is somewhere the board is
584
+ * not the target (over a group the page will join instead); the tiles it
585
+ * displaced come home. `enter` puts it back at the pointer.
586
+ */
587
+ leave(): void;
588
+ enter(world: {
589
+ x: number;
590
+ y: number;
591
+ }): void;
562
592
  /** Undo the adoption: target board back to its pre-entry layout. */
563
593
  abort(): void;
564
594
  /**
@@ -2314,8 +2314,29 @@ export function bindDashboardGrid(api, group, options = {}) {
2314
2314
  project();
2315
2315
  syncPlaceholder();
2316
2316
  },
2317
+ leave: () => {
2318
+ if (!engine.getItem(node.id))
2319
+ return;
2320
+ engine.remove(node.id); // displaced tiles come home (gesture memory)
2321
+ lastWant = null;
2322
+ project();
2323
+ syncPlaceholder();
2324
+ },
2325
+ enter: (w) => {
2326
+ if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: span.w, h: hNatural }))
2327
+ return;
2328
+ const item = engine.getItem(node.id);
2329
+ if (!item)
2330
+ return;
2331
+ const cell = wantedCell(w.x, w.y, item.w, opts.fit === 'shrink' ? hNatural : item.h);
2332
+ lastWant = cell;
2333
+ place(cell, item.w);
2334
+ project();
2335
+ syncPlaceholder();
2336
+ },
2317
2337
  abort: () => {
2318
- engine.remove(node.id);
2338
+ if (engine.getItem(node.id))
2339
+ engine.remove(node.id);
2319
2340
  engine.cancelGesture(); // pre-entry layout, memory cleared
2320
2341
  adoptedGhostId = null;
2321
2342
  disarmGlideSoon();
@@ -2920,6 +2941,48 @@ export function bindDashboardGrid(api, group, options = {}) {
2920
2941
  chip.className = 'axdb-drag-chip axdb-tab-chip';
2921
2942
  chip.textContent = plan.label;
2922
2943
  doc.body.appendChild(chip);
2944
+ // Over ANOTHER tab container the page joins it instead of taking a cell:
2945
+ // the tile leaves the board (its displaced tiles come home), the target's
2946
+ // frame lights up and its strip marks where the tab will go.
2947
+ const layer = htmlLayer();
2948
+ const targets = plan.joinTargets
2949
+ .map((id) => diagram.getGroup(id))
2950
+ .filter((g) => !!g && g.id !== fromGroupId);
2951
+ const targetAt = (wx, wy) => { var _a; return (_a = targets.find((t) => worldInsideGroup(t, wx, wy))) !== null && _a !== void 0 ? _a : null; };
2952
+ let over = null;
2953
+ let joinEl = null;
2954
+ const showJoin = (g) => {
2955
+ if (!layer)
2956
+ return;
2957
+ if (!joinEl) {
2958
+ joinEl = doc.createElement('div');
2959
+ joinEl.className = 'axdb-join';
2960
+ layer.prepend(joinEl);
2961
+ }
2962
+ const sz = sizeOf(g);
2963
+ joinEl.style.left = `${g.position.x}px`;
2964
+ joinEl.style.top = `${g.position.y}px`;
2965
+ joinEl.style.width = `${sz.width}px`;
2966
+ joinEl.style.height = `${sz.height}px`;
2967
+ };
2968
+ const hideJoin = () => {
2969
+ joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
2970
+ joinEl = null;
2971
+ plan.markDrop(null, null);
2972
+ };
2973
+ const setOver = (g, world) => {
2974
+ if (g === over)
2975
+ return;
2976
+ over = g;
2977
+ if (g) {
2978
+ leg.leave();
2979
+ showJoin(g);
2980
+ }
2981
+ else {
2982
+ hideJoin();
2983
+ leg.enter(world);
2984
+ }
2985
+ };
2923
2986
  const moveChip = (cx, cy) => {
2924
2987
  chip.style.left = `${cx + 6}px`;
2925
2988
  chip.style.top = `${cy + 6}px`;
@@ -2941,27 +3004,48 @@ export function bindDashboardGrid(api, group, options = {}) {
2941
3004
  moveChip(e.clientX, e.clientY);
2942
3005
  const world = toWorld(e.clientX, e.clientY);
2943
3006
  const home = worldInsideGroup(from, world.x, world.y);
3007
+ const target = home ? null : targetAt(world.x, world.y);
3008
+ setOver(target, world);
2944
3009
  // Over its own container it goes home: a tab dragged around its own
2945
3010
  // strip must not tear itself out.
2946
- chip.classList.toggle('axdb-out', home || !worldInsideBoardGrace(world.x, world.y));
2947
- if (!home)
3011
+ chip.classList.toggle('axdb-out', home || (!target && !worldInsideBoardGrace(world.x, world.y)));
3012
+ if (target)
3013
+ plan.markDrop(target.id, plan.dropIndex(target.id, e.clientX, e.clientY));
3014
+ else if (!home)
2948
3015
  leg.move(world);
2949
3016
  api.render();
2950
3017
  };
2951
3018
  const finish = (commit) => {
2952
- var _a, _b;
3019
+ var _a, _b, _c;
2953
3020
  detach();
2954
3021
  chip.remove();
3022
+ hideJoin();
2955
3023
  tearing = null;
2956
3024
  if (disposed)
2957
3025
  return;
2958
3026
  const world = toWorld(last.x, last.y);
2959
3027
  const home = worldInsideGroup(from, world.x, world.y);
3028
+ const target = commit && !home ? targetAt(world.x, world.y) : null;
3029
+ if (target) {
3030
+ // JOIN: no cell on this board — the leg is abandoned (its displaced
3031
+ // tiles are already home) and the page becomes the target's tab.
3032
+ const index = plan.dropIndex(target.id, last.x, last.y);
3033
+ leg.abort();
3034
+ const planned = plan.join(target.id, index, group.id);
3035
+ const collapse = planned.collapse.length > 0 ? [...planRemovalOf(fromGroupId), ...planned.collapse] : [];
3036
+ const changed = execute('Move tab', [...planned.move, ...collapse]);
3037
+ disarmGlideSoon();
3038
+ enforceBoardHeight();
3039
+ persistLayouts();
3040
+ api.renderNow();
3041
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3042
+ return;
3043
+ }
2960
3044
  if (!commit || home || !worldInsideBoardGrace(world.x, world.y)) {
2961
3045
  leg.abort();
2962
3046
  disarmGlideSoon();
2963
3047
  api.renderNow();
2964
- (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
3048
+ (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
2965
3049
  return;
2966
3050
  }
2967
3051
  const fin = leg.finalize();
@@ -2980,7 +3064,7 @@ export function bindDashboardGrid(api, group, options = {}) {
2980
3064
  enforceBoardHeight();
2981
3065
  persistLayouts();
2982
3066
  api.renderNow();
2983
- (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3067
+ (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
2984
3068
  };
2985
3069
  const onUp = () => finish(true);
2986
3070
  const onCancel = () => finish(false);
@@ -132,6 +132,28 @@ const CSS = `
132
132
  box-shadow: 0 0 0 1px rgba(236, 238, 244, .16);
133
133
  }
134
134
  }
135
+ /* A tab held over ANOTHER group: it will join that group, not the board. The
136
+ group's frame lights up, and its strip marks the slot the tab will take —
137
+ a bar before the tab it lands in front of, or after the last one. */
138
+ .grafloria-html-layer > .axdb-join {
139
+ position: absolute;
140
+ z-index: 3;
141
+ pointer-events: none;
142
+ box-sizing: border-box;
143
+ border: 2px dashed var(--axdb-accent, #3b52d9);
144
+ background: var(--axdb-accent-soft, rgba(59, 82, 217, .08));
145
+ border-radius: var(--axdb-rs-radius, 3px);
146
+ }
147
+ .axdb-tabs.axdb-tabs--drop { box-shadow: inset 0 -2px 0 var(--axdb-accent, #3b52d9); }
148
+ .axdb-tab.axdb-tab--drop-before { position: relative; }
149
+ .axdb-tab.axdb-tab--drop-before::before {
150
+ content: ""; position: absolute; left: -3px; top: 4px; bottom: 4px; width: 3px;
151
+ border-radius: 2px; background: var(--axdb-accent, #3b52d9);
152
+ }
153
+ .axdb-tabs.axdb-tabs--drop-end::after {
154
+ content: ""; display: inline-block; width: 3px; height: 18px; margin: 0 4px;
155
+ border-radius: 2px; background: var(--axdb-accent, #3b52d9); vertical-align: middle; align-self: center;
156
+ }
135
157
 
136
158
  /* ===========================================================================
137
159
  BUILT-IN WIDGET CARDS — what widgets.ts paints when a page writes no