@window-splitter/state 0.5.8 → 0.6.1

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/src/index.ts CHANGED
@@ -1,12 +1,4 @@
1
- import * as Cookies from "tiny-cookie";
2
1
  import { raf } from "@react-spring/rafz";
3
- import {
4
- createMachine,
5
- assign,
6
- enqueueActions,
7
- fromPromise,
8
- Snapshot,
9
- } from "xstate";
10
2
  import invariant from "tiny-invariant";
11
3
  import Big from "big.js";
12
4
 
@@ -328,10 +320,13 @@ export interface GroupMachineContextValue {
328
320
  autosaveStrategy?: "localStorage" | "cookie";
329
321
  }
330
322
 
331
- export type GroupMachineSnapshot = Snapshot<unknown> & {
332
- context: GroupMachineContextValue;
333
- value: string;
334
- };
323
+ interface LockGroupEvent {
324
+ type: "lockGroup";
325
+ }
326
+
327
+ interface UnlockGroupEvent {
328
+ type: "unlockGroup";
329
+ }
335
330
 
336
331
  export type GroupMachineEvent =
337
332
  | RegisterPanelEvent
@@ -350,7 +345,9 @@ export type GroupMachineEvent =
350
345
  | ApplyDeltaEvent
351
346
  | SetActualItemsSizeEvent
352
347
  | RebindPanelCallbacksEvent
353
- | UpdateConstraintsEvent;
348
+ | UpdateConstraintsEvent
349
+ | LockGroupEvent
350
+ | UnlockGroupEvent;
354
351
 
355
352
  type EventForType<T extends GroupMachineEvent["type"]> = Extract<
356
353
  GroupMachineEvent,
@@ -383,12 +380,19 @@ export function getCursor(
383
380
  }
384
381
  }
385
382
 
386
- export function prepareSnapshot(snapshot: GroupMachineSnapshot) {
387
- const snapshotContext = snapshot.context;
383
+ export function prepareSnapshot(snapshot: GroupMachineContextValue) {
384
+ // convert from old format
385
+ if ("context" in snapshot) {
386
+ snapshot = snapshot.context as GroupMachineContextValue;
387
+ }
388
+
389
+ if (!("items" in snapshot)) {
390
+ return;
391
+ }
388
392
 
389
- snapshotContext.dragOvershoot = new Big(snapshotContext.dragOvershoot);
393
+ snapshot.dragOvershoot = new Big(snapshot.dragOvershoot || 0);
390
394
 
391
- for (const item of snapshotContext.items) {
395
+ for (const item of snapshot.items) {
392
396
  if (isPanelData(item)) {
393
397
  item.currentValue.value = new Big(item.currentValue.value);
394
398
  item.collapsedSize.value = new Big(item.collapsedSize.value);
@@ -1554,16 +1558,14 @@ function clearLastKnownSize(items: Item[]) {
1554
1558
  return items.map((i) => ({ ...i, lastKnownSize: undefined }));
1555
1559
  }
1556
1560
 
1561
+ function setCookie(name: string, jsonData: unknown) {
1562
+ document.cookie = `${name}=${encodeURIComponent(`${jsonData}`)};path=/;max-age=31536000`;
1563
+ }
1564
+
1557
1565
  // #endregion
1558
1566
 
1559
1567
  // #region Machine
1560
1568
 
1561
- interface AnimationActorInput {
1562
- context: GroupMachineContextValue;
1563
- event: CollapsePanelEvent | ExpandPanelEvent;
1564
- send: (event: GroupMachineEvent) => void;
1565
- }
1566
-
1567
1569
  interface AnimationActorOutput {
1568
1570
  panelId: string;
1569
1571
  action: "expand" | "collapse";
@@ -1585,593 +1587,598 @@ function getDeltaForEvent(
1585
1587
  return panel.currentValue.value.minus(collapsedSize);
1586
1588
  }
1587
1589
 
1588
- const animationActor = fromPromise<
1589
- AnimationActorOutput | undefined,
1590
- AnimationActorInput
1591
- >(
1592
- ({ input: { send, context, event } }) =>
1593
- new Promise<AnimationActorOutput | undefined>((resolve) => {
1594
- const panel = getPanelWithId(context, event.panelId);
1595
- const handle = getHandleForPanelId(context, event.panelId);
1590
+ function animationActor(
1591
+ context: GroupMachineContextValue,
1592
+ event: CollapsePanelEvent | ExpandPanelEvent,
1593
+ send: (e: GroupMachineEvent) => void,
1594
+ abortController: AbortController
1595
+ ) {
1596
+ const panel = getPanelWithId(context, event.panelId);
1597
+ const handle = getHandleForPanelId(context, event.panelId);
1596
1598
 
1597
- let direction = new Big(handle.direction);
1598
- const fullDelta = getDeltaForEvent(context, event);
1599
+ let direction = new Big(handle.direction);
1600
+ const fullDelta = getDeltaForEvent(context, event);
1599
1601
 
1600
- if (event.type === "collapsePanel") {
1601
- panel.sizeBeforeCollapse = panel.currentValue.value.toNumber();
1602
- direction = direction.mul(new Big(-1));
1603
- }
1602
+ return new Promise<AnimationActorOutput | undefined>((resolve, reject) => {
1603
+ abortController.signal.addEventListener("abort", () => {
1604
+ reject(new Error("Operation was canceled"));
1605
+ });
1604
1606
 
1605
- const fps = 60;
1606
- const { duration, ease } = getCollapseAnimation(panel);
1607
- const totalFrames = Math.ceil(
1608
- panel.collapseAnimation ? duration / (1000 / fps) : 1
1607
+ if (event.type === "collapsePanel") {
1608
+ panel.sizeBeforeCollapse = panel.currentValue.value.toNumber();
1609
+ direction = direction.mul(new Big(-1));
1610
+ }
1611
+
1612
+ const fps = 60;
1613
+ const { duration, ease } = getCollapseAnimation(panel);
1614
+ const totalFrames = Math.ceil(
1615
+ panel.collapseAnimation ? duration / (1000 / fps) : 1
1616
+ );
1617
+ let frame = 0;
1618
+ let appliedDelta = new Big(0);
1619
+
1620
+ function renderFrame() {
1621
+ const progress = ++frame / totalFrames;
1622
+ const e = new Big(panel.collapseAnimation ? ease(progress) : 1);
1623
+ const delta = e.mul(fullDelta).sub(appliedDelta).mul(direction);
1624
+
1625
+ send({
1626
+ type: "applyDelta",
1627
+ handleId: handle.item.id,
1628
+ delta: delta.toNumber(),
1629
+ });
1630
+
1631
+ appliedDelta = appliedDelta.add(
1632
+ delta
1633
+ .abs()
1634
+ .mul(
1635
+ (delta.gt(0) && direction.lt(0)) || (delta.lt(0) && direction.gt(0))
1636
+ ? -1
1637
+ : 1
1638
+ )
1609
1639
  );
1610
- let frame = 0;
1611
- let appliedDelta = new Big(0);
1612
1640
 
1613
- function renderFrame() {
1614
- const progress = ++frame / totalFrames;
1615
- const e = new Big(panel.collapseAnimation ? ease(progress) : 1);
1616
- const delta = e.mul(fullDelta).sub(appliedDelta).mul(direction);
1641
+ if (e.eq(1)) {
1642
+ const action = event.type === "expandPanel" ? "expand" : "collapse";
1643
+ resolve({ panelId: panel.id, action });
1644
+ return false;
1645
+ }
1617
1646
 
1618
- send({
1619
- type: "applyDelta",
1620
- handleId: handle.item.id,
1621
- delta: delta.toNumber(),
1622
- });
1647
+ return true;
1648
+ }
1623
1649
 
1624
- appliedDelta = appliedDelta.add(
1625
- delta
1626
- .abs()
1627
- .mul(
1628
- (delta.gt(0) && direction.lt(0)) ||
1629
- (delta.lt(0) && direction.gt(0))
1630
- ? -1
1631
- : 1
1632
- )
1633
- );
1650
+ raf(renderFrame);
1651
+ });
1652
+ }
1634
1653
 
1635
- if (e.eq(1)) {
1636
- const action = event.type === "expandPanel" ? "expand" : "collapse";
1637
- resolve({ panelId: panel.id, action });
1638
- return false;
1639
- }
1654
+ function assign<T>(target: T, source: Partial<T>) {
1655
+ for (const key in source) {
1656
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1657
+ (target as any)[key] = source[key];
1658
+ }
1659
+ }
1660
+
1661
+ export interface GroupMachineInput {
1662
+ orientation?: Orientation;
1663
+ groupId: string;
1664
+ items?: Item[];
1665
+ autosaveStrategy?: "localStorage" | "cookie";
1666
+ }
1667
+
1668
+ export type State = "idle" | "dragging" | "togglingCollapse";
1640
1669
 
1641
- return true;
1670
+ let groupId = 0;
1671
+
1672
+ export function groupMachine(
1673
+ input: Partial<GroupMachineContextValue>,
1674
+ onUpdate?: (context: GroupMachineContextValue) => void
1675
+ ) {
1676
+ const abortController = new AbortController();
1677
+ const state = {
1678
+ current: "idle" as State,
1679
+ };
1680
+ let locked = false;
1681
+
1682
+ const context: GroupMachineContextValue = {
1683
+ size: { width: 0, height: 0 },
1684
+ items: input.items || [],
1685
+ orientation: input.orientation || "horizontal",
1686
+ dragOvershoot: new Big(0),
1687
+ groupId: input.groupId || `group-${groupId++}`,
1688
+ autosaveStrategy: input.autosaveStrategy,
1689
+ };
1690
+
1691
+ const actions = {
1692
+ prepare: () => {
1693
+ context.items = prepareItems(context);
1694
+ },
1695
+ clearLastKnownSize: () => {
1696
+ context.items = clearLastKnownSize(context.items);
1697
+ },
1698
+ commit: () => {
1699
+ context.dragOvershoot = new Big(0);
1700
+ context.items = commitLayout(context);
1701
+ },
1702
+ onResize: () => {
1703
+ for (const item of context.items) {
1704
+ if (isPanelData(item)) {
1705
+ const pixel = item.lastKnownSize
1706
+ ? new Big(
1707
+ context.orientation === "horizontal"
1708
+ ? item.lastKnownSize.width
1709
+ : item.lastKnownSize.height
1710
+ )
1711
+ : clampUnit(
1712
+ context,
1713
+ item,
1714
+ getUnitPixelValue(context, item.currentValue)
1715
+ );
1716
+ const groupSize = getGroupSize(context);
1717
+
1718
+ item.onResize?.current?.({
1719
+ pixel: pixel.toNumber(),
1720
+ percentage:
1721
+ groupSize > 0 ? pixel.div(getGroupSize(context)).toNumber() : -1,
1722
+ });
1723
+ }
1724
+ }
1725
+ },
1726
+ onAutosave: () => {
1727
+ if (!context.autosaveStrategy || typeof window === "undefined") {
1728
+ return;
1642
1729
  }
1643
1730
 
1644
- raf(renderFrame);
1645
- })
1646
- );
1647
-
1648
- export const groupMachine = createMachine(
1649
- {
1650
- initial: "idle",
1651
- types: {
1652
- context: {} as GroupMachineContextValue,
1653
- events: {} as GroupMachineEvent,
1654
- input: {} as {
1655
- orientation?: Orientation;
1656
- groupId: string;
1657
- initialItems?: Item[];
1658
- autosaveStrategy?: "localStorage" | "cookie";
1659
- },
1731
+ const snapshot = { ...context };
1732
+ snapshot.items = clearLastKnownSize(context.items);
1733
+ snapshot.activeDragHandleId = context.activeDragHandleId;
1734
+ const data = JSON.stringify(snapshot);
1735
+
1736
+ if (context.autosaveStrategy === "localStorage") {
1737
+ localStorage.setItem(context.groupId, data);
1738
+ } else {
1739
+ setCookie(context.groupId, data);
1740
+ }
1660
1741
  },
1661
- context: ({ input }) => ({
1662
- size: { width: 0, height: 0 },
1663
- items: input.initialItems || [],
1664
- orientation: input.orientation || "horizontal",
1665
- dragOvershoot: new Big(0),
1666
- groupId: input.groupId,
1667
- autosaveStrategy: input.autosaveStrategy,
1668
- }),
1669
- states: {
1670
- idle: {
1671
- entry: ["onAutosave"],
1672
- on: {
1673
- dragHandleStart: {
1674
- target: "dragging",
1675
- actions: ["onDragHandleStart"],
1676
- },
1677
- setPanelPixelSize: {
1678
- actions: [
1679
- "prepare",
1680
- "onClearLastKnownSize",
1681
- "onSetPanelSize",
1682
- "commit",
1683
- "onResize",
1684
- "onAutosave",
1685
- ],
1686
- },
1687
- collapsePanel: [
1688
- {
1689
- actions: "notifyCollapseToggle",
1690
- guard: "shouldNotifyCollapseToggle",
1691
- },
1692
- { target: "togglingCollapse" },
1693
- ],
1694
- expandPanel: [
1695
- // This will match if we can't expand and the expansion won't happen
1696
- { guard: "cannotExpandPanel" },
1697
- {
1698
- actions: "notifyCollapseToggle",
1699
- guard: "shouldNotifyCollapseToggle",
1700
- },
1701
- { target: "togglingCollapse" },
1702
- ],
1703
- },
1704
- },
1705
- dragging: {
1706
- entry: ["prepare"],
1707
- on: {
1708
- dragHandle: {
1709
- actions: ["onClearLastKnownSize", "onDragHandle", "onResize"],
1710
- },
1711
- dragHandleEnd: { target: "idle" },
1712
- collapsePanel: {
1713
- guard: "shouldCollapseToggle",
1714
- actions: "runCollapseToggle",
1715
- },
1716
- expandPanel: {
1717
- guard: "shouldCollapseToggle",
1718
- actions: "runCollapseToggle",
1719
- },
1720
- },
1721
- exit: ["commit", "clearDragHandle"],
1722
- },
1723
- togglingCollapse: {
1724
- entry: ["prepare", "onClearLastKnownSize"],
1725
- invoke: {
1726
- src: "animation",
1727
- input: (i) => ({ ...i, send: i.self.send }),
1728
- onDone: {
1729
- target: "idle",
1730
- actions: ["onToggleCollapseComplete", "commit"],
1731
- },
1732
- },
1733
- on: {
1734
- applyDelta: { actions: ["onApplyDelta", "onResize"] },
1735
- },
1736
- },
1742
+ removeItem: (id: string) => {
1743
+ const itemIndex = context.items.findIndex((item) => item.id === id);
1744
+ const item = context.items[itemIndex];
1745
+
1746
+ if (!item) {
1747
+ return;
1748
+ }
1749
+
1750
+ const newItems = context.items.filter((i) => i.id !== id);
1751
+ const removedSize = isPanelData(item)
1752
+ ? item.currentValue.value
1753
+ : item.size.value;
1754
+
1755
+ applyDeltaInBothDirections(context, newItems, itemIndex, removedSize);
1756
+
1757
+ context.items = newItems;
1737
1758
  },
1738
- on: {
1739
- setActualItemsSize: { actions: ["recordActualItemSize", "onResize"] },
1740
- registerPanel: { actions: ["assignPanelData"] },
1741
- rebindPanelCallbacks: {
1742
- actions: ["rebindPanelCallbacks"],
1743
- },
1744
- updateConstraints: {
1745
- actions: [
1746
- "prepare",
1747
- "updateConstraints",
1748
- "onClearLastKnownSize",
1749
- "commit",
1750
- "onResize",
1751
- "onAutosave",
1752
- ],
1753
- },
1754
- registerDynamicPanel: {
1755
- actions: [
1756
- "prepare",
1757
- "onRegisterDynamicPanel",
1758
- "onClearLastKnownSize",
1759
- "commit",
1760
- "onResize",
1761
- "onAutosave",
1762
- ],
1763
- },
1764
- unregisterPanel: {
1765
- actions: [
1766
- "prepare",
1767
- "removeItem",
1768
- "onClearLastKnownSize",
1769
- "commit",
1770
- "onResize",
1771
- "onAutosave",
1772
- ],
1773
- },
1774
- registerPanelHandle: { actions: ["assignPanelHandleData"] },
1775
- unregisterPanelHandle: {
1776
- actions: [
1777
- "prepare",
1778
- "removeItem",
1779
- "onClearLastKnownSize",
1780
- "commit",
1781
- "onResize",
1782
- "onAutosave",
1783
- ],
1784
- },
1785
- setSize: { actions: ["updateSize", "onResize"] },
1786
- setOrientation: {
1787
- actions: ["updateOrientation", "onClearLastKnownSize", "onResize"],
1788
- },
1759
+ notifyCollapseToggle: (event: GroupMachineEvent) => {
1760
+ isEvent(event, ["collapsePanel", "expandPanel"]);
1761
+ const panel = getPanelWithId(context, event.panelId);
1762
+ panel.onCollapseChange?.current?.(!panel.collapsed);
1789
1763
  },
1790
- },
1791
- {
1792
- guards: {
1793
- shouldNotifyCollapseToggle: ({ context, event }) => {
1794
- isEvent(event, ["collapsePanel", "expandPanel"]);
1795
- const panel = getPanelWithId(context, event.panelId);
1796
- return !event.controlled && panel.collapseIsControlled === true;
1797
- },
1798
- shouldCollapseToggle: ({ context, event }) => {
1799
- isEvent(event, ["collapsePanel", "expandPanel"]);
1800
- const panel = getPanelWithId(context, event.panelId);
1801
- return panel.collapseIsControlled === true;
1802
- },
1803
- cannotExpandPanel: ({ context, event }) => {
1804
- isEvent(event, ["expandPanel"]);
1805
- const delta = getDeltaForEvent(context, event);
1806
- const handle = getHandleForPanelId(context, event.panelId);
1807
- const pixelItems = prepareItems(context);
1808
-
1809
- let interimContext = { ...context, items: pixelItems };
1810
- interimContext = {
1811
- ...interimContext,
1812
- ...iterativelyUpdateLayout({
1813
- context: interimContext,
1814
- handleId: handle.item.id,
1815
- controlled: event.controlled,
1816
- delta: delta,
1817
- direction: handle.direction,
1818
- }),
1819
- };
1820
- const updatedPanel = interimContext.items.find(
1821
- (i) => i.id === event.panelId
1822
- );
1823
- const totalSize = interimContext.items.reduce(
1824
- (acc, i) =>
1825
- acc.add(isPanelData(i) ? i.currentValue.value : i.size.value),
1826
- new Big(0)
1827
- );
1828
- const didExpand =
1829
- updatedPanel &&
1830
- isPanelData(updatedPanel) &&
1831
- !updatedPanel.currentValue.value.eq(updatedPanel.collapsedSize.value);
1764
+ runCollapseToggle: (event: GroupMachineEvent) => {
1765
+ isEvent(event, ["collapsePanel", "expandPanel"]);
1832
1766
 
1833
- return totalSize.gt(getGroupSize(context)) || !didExpand;
1834
- },
1767
+ const handle = getHandleForPanelId(context, event.panelId);
1768
+ // When collapsing a panel it will be in the opposite direction
1769
+ // that handle assumes
1770
+ const delta =
1771
+ event.type === "collapsePanel"
1772
+ ? handle.direction * -1
1773
+ : handle.direction;
1774
+ const newContext = updateLayout(context, {
1775
+ handleId: handle.item.id,
1776
+ type: "dragHandle",
1777
+ controlled: event.controlled,
1778
+ value: dragHandlePayload({ delta, orientation: context.orientation }),
1779
+ });
1780
+
1781
+ assign(context, newContext);
1835
1782
  },
1836
- actors: {
1837
- animation: animationActor,
1783
+ onAnimationEnd: (output: AnimationActorOutput | undefined) => {
1784
+ invariant(output, "Expected output from animation actor");
1785
+
1786
+ const panel = getPanelWithId(context, output.panelId);
1787
+ panel.collapsed = output.action === "collapse";
1788
+
1789
+ if (panel.collapsed) {
1790
+ panel.currentValue = panel.collapsedSize;
1791
+ }
1792
+
1793
+ actions.commit();
1838
1794
  },
1839
- actions: {
1840
- onDragHandleStart: assign({
1841
- activeDragHandleId: ({ event }) => {
1842
- isEvent(event, ["dragHandleStart"]);
1843
- return event.handleId;
1844
- },
1845
- }),
1846
- clearDragHandle: assign({
1847
- activeDragHandleId: undefined,
1848
- }),
1849
- onAutosave: ({ context, self }) => {
1850
- if (!context.autosaveStrategy || typeof window === "undefined") {
1851
- return;
1852
- }
1795
+ };
1853
1796
 
1854
- const snapshot = self.getPersistedSnapshot() as GroupMachineSnapshot;
1855
- snapshot.context.items = clearLastKnownSize(context.items);
1856
- snapshot.context.activeDragHandleId = context.activeDragHandleId;
1857
- snapshot.value = "idle";
1858
- const data = JSON.stringify(snapshot);
1797
+ actions.onAutosave();
1859
1798
 
1860
- if (context.autosaveStrategy === "localStorage") {
1861
- localStorage.setItem(context.groupId, data);
1862
- } else {
1863
- Cookies.set(context.groupId, data, {
1864
- path: "/",
1865
- "max-age": 31536000,
1866
- });
1867
- }
1868
- },
1869
- notifyCollapseToggle: ({ context, event }) => {
1870
- isEvent(event, ["collapsePanel", "expandPanel"]);
1871
- const panel = getPanelWithId(context, event.panelId);
1872
- panel.onCollapseChange?.current?.(!panel.collapsed);
1873
- },
1874
- runCollapseToggle: enqueueActions(({ context, event, enqueue }) => {
1875
- isEvent(event, ["collapsePanel", "expandPanel"]);
1876
-
1877
- const handle = getHandleForPanelId(context, event.panelId);
1878
- // When collapsing a panel it will be in the opposite direction
1879
- // that handle assumes
1880
- const delta =
1881
- event.type === "collapsePanel"
1882
- ? handle.direction * -1
1883
- : handle.direction;
1884
- const newContext = updateLayout(context, {
1799
+ const guards = {
1800
+ shouldNotifyCollapseToggle: (event: GroupMachineEvent) => {
1801
+ isEvent(event, ["collapsePanel", "expandPanel"]);
1802
+ const panel = getPanelWithId(context, event.panelId);
1803
+ return !event.controlled && panel.collapseIsControlled === true;
1804
+ },
1805
+ shouldCollapseToggle: (event: GroupMachineEvent) => {
1806
+ isEvent(event, ["collapsePanel", "expandPanel"]);
1807
+ const panel = getPanelWithId(context, event.panelId);
1808
+ return panel.collapseIsControlled === true;
1809
+ },
1810
+ cannotExpandPanel: (event: GroupMachineEvent) => {
1811
+ isEvent(event, ["expandPanel"]);
1812
+ const delta = getDeltaForEvent(context, event);
1813
+ const handle = getHandleForPanelId(context, event.panelId);
1814
+ const pixelItems = prepareItems(context);
1815
+
1816
+ let interimContext = { ...context, items: pixelItems };
1817
+ interimContext = {
1818
+ ...interimContext,
1819
+ ...iterativelyUpdateLayout({
1820
+ context: interimContext,
1885
1821
  handleId: handle.item.id,
1886
- type: "dragHandle",
1887
1822
  controlled: event.controlled,
1888
- value: dragHandlePayload({ delta, orientation: context.orientation }),
1889
- });
1823
+ delta: delta,
1824
+ direction: handle.direction,
1825
+ }),
1826
+ };
1827
+ const updatedPanel = interimContext.items.find(
1828
+ (i) => i.id === event.panelId
1829
+ );
1830
+ const totalSize = interimContext.items.reduce(
1831
+ (acc, i) =>
1832
+ acc.add(isPanelData(i) ? i.currentValue.value : i.size.value),
1833
+ new Big(0)
1834
+ );
1835
+ const didExpand =
1836
+ updatedPanel &&
1837
+ isPanelData(updatedPanel) &&
1838
+ !updatedPanel.currentValue.value.eq(updatedPanel.collapsedSize.value);
1890
1839
 
1891
- enqueue.assign(newContext);
1892
- }),
1893
- onToggleCollapseComplete: assign({
1894
- items: ({ context, event: e }) => {
1895
- const { output } = e as unknown as { output: AnimationActorOutput };
1896
- invariant(output, "Expected output from animation actor");
1840
+ return totalSize.gt(getGroupSize(context)) || !didExpand;
1841
+ },
1842
+ };
1897
1843
 
1898
- const panel = getPanelWithId(context, output.panelId);
1899
- panel.collapsed = output.action === "collapse";
1844
+ function transition(to: State) {
1845
+ // exit
1846
+ switch (state.current) {
1847
+ case "dragging":
1848
+ actions.commit();
1849
+ context.activeDragHandleId = undefined;
1850
+ break;
1851
+ }
1900
1852
 
1901
- if (panel.collapsed) {
1902
- panel.currentValue = panel.collapsedSize;
1903
- }
1853
+ // enter
1854
+ switch (to) {
1855
+ case "idle":
1856
+ actions.onAutosave();
1857
+ break;
1858
+ case "dragging":
1859
+ actions.prepare();
1860
+ break;
1861
+ case "togglingCollapse":
1862
+ actions.prepare();
1863
+ actions.clearLastKnownSize();
1864
+ break;
1865
+ }
1904
1866
 
1905
- return context.items;
1906
- },
1907
- }),
1908
- updateSize: assign({
1909
- size: ({ event }) => {
1910
- isEvent(event, ["setSize"]);
1911
- return event.size;
1912
- },
1913
- }),
1914
- recordActualItemSize: assign({
1915
- items: ({ context, event }) => {
1916
- isEvent(event, ["setActualItemsSize"]);
1917
-
1918
- const withLastKnownSize = context.items.map((i) => {
1919
- if (!isPanelData(i)) return i;
1920
- const lastKnownSize = event.childrenSizes[i.id] || i.lastKnownSize;
1921
- return { ...i, lastKnownSize };
1922
- });
1867
+ state.current = to;
1868
+ }
1923
1869
 
1924
- let totalSize = 0;
1870
+ function send(event: GroupMachineEvent) {
1871
+ switch (event.type) {
1872
+ case "lockGroup":
1873
+ locked = true;
1874
+ break;
1875
+ case "unlockGroup":
1876
+ locked = false;
1877
+ break;
1878
+ }
1925
1879
 
1926
- for (const item of withLastKnownSize) {
1927
- if (isPanelData(item)) {
1928
- const size =
1929
- item.lastKnownSize?.[
1930
- context.orientation === "horizontal" ? "width" : "height"
1931
- ];
1880
+ if (locked) {
1881
+ return;
1882
+ }
1932
1883
 
1933
- // If any size is 0 don't handle overflow
1934
- if (!size) {
1935
- return withLastKnownSize;
1936
- }
1884
+ switch (event.type) {
1885
+ case "registerPanel":
1886
+ context.items = addDeDuplicatedItems(context.items, {
1887
+ type: "panel",
1888
+ currentValue: makePixelUnit(-1),
1889
+ ...event.data,
1890
+ });
1891
+ break;
1892
+ case "unregisterPanel":
1893
+ actions.prepare();
1894
+ actions.removeItem(event.id);
1895
+ actions.clearLastKnownSize();
1896
+ actions.commit();
1897
+ actions.onResize();
1898
+ actions.onAutosave();
1899
+ break;
1900
+ case "registerPanelHandle": {
1901
+ const unit =
1902
+ typeof event.data.size === "string"
1903
+ ? parseUnit(event.data.size)
1904
+ : event.data.size;
1905
+
1906
+ context.items = addDeDuplicatedItems(context.items, {
1907
+ type: "handle",
1908
+ ...event.data,
1909
+ size: {
1910
+ type: "pixel",
1911
+ value: new Big(unit.value),
1912
+ },
1913
+ });
1914
+ break;
1915
+ }
1916
+ case "unregisterPanelHandle":
1917
+ actions.prepare();
1918
+ actions.removeItem(event.id);
1919
+ actions.clearLastKnownSize();
1920
+ actions.commit();
1921
+ actions.onResize();
1922
+ actions.onAutosave();
1923
+ break;
1924
+ case "registerDynamicPanel": {
1925
+ actions.prepare();
1937
1926
 
1938
- totalSize += size;
1939
- } else {
1940
- totalSize += item.size.value.toNumber();
1941
- }
1942
- }
1927
+ let currentValue: ParsedUnit = makePixelUnit(0);
1943
1928
 
1944
- if (totalSize > getGroupSize(context)) {
1945
- return handleOverflow({
1946
- ...context,
1947
- items: prepareItems({ ...context, items: withLastKnownSize }),
1948
- }).items;
1949
- }
1929
+ if (
1930
+ event.data.collapsible &&
1931
+ event.data.collapsed &&
1932
+ event.data.collapsedSize
1933
+ ) {
1934
+ currentValue = event.data.collapsedSize;
1935
+ } else if (event.data.default) {
1936
+ currentValue = event.data.default;
1937
+ } else {
1938
+ currentValue = event.data.min;
1939
+ }
1950
1940
 
1951
- return withLastKnownSize;
1952
- },
1953
- }),
1954
- updateOrientation: assign({
1955
- orientation: ({ event }) => {
1956
- isEvent(event, ["setOrientation"]);
1957
- return event.orientation;
1958
- },
1959
- }),
1960
- onClearLastKnownSize: assign({
1961
- items: ({ context }) => clearLastKnownSize(context.items),
1962
- }),
1963
- assignPanelData: assign({
1964
- items: ({ context, event }) => {
1965
- isEvent(event, ["registerPanel"]);
1966
-
1967
- return addDeDuplicatedItems(context.items, {
1968
- type: "panel",
1969
- currentValue: makePixelUnit(-1),
1970
- ...event.data,
1971
- });
1972
- },
1973
- }),
1974
- rebindPanelCallbacks: assign({
1975
- items: ({ context, event }) => {
1976
- isEvent(event, ["rebindPanelCallbacks"]);
1977
-
1978
- for (const item of context.items) {
1979
- if (isPanelData(item) && item.id === event.data.id) {
1980
- item.onCollapseChange = event.data.onCollapseChange;
1981
- item.onResize = event.data.onResize;
1941
+ const newItems = addDeDuplicatedItems(context.items, {
1942
+ type: "panel",
1943
+ ...event.data,
1944
+ currentValue,
1945
+ });
1946
+ const itemIndex = newItems.findIndex(
1947
+ (item) => item.id === event.data.id
1948
+ );
1949
+ const newContext = { ...context, items: newItems };
1950
+ const overflowDueToHandles = context.items
1951
+ .reduce((acc, i) => {
1952
+ if (isPanelHandle(i)) {
1953
+ return acc.add(getUnitPixelValue(context, i.size));
1982
1954
  }
1955
+
1956
+ return acc.add(i.currentValue.value);
1957
+ }, new Big(0))
1958
+ .minus(getGroupSize(context));
1959
+
1960
+ applyDeltaInBothDirections(
1961
+ newContext,
1962
+ newItems,
1963
+ itemIndex,
1964
+ currentValue.value.add(overflowDueToHandles).neg()
1965
+ );
1966
+
1967
+ context.items = newItems;
1968
+
1969
+ actions.clearLastKnownSize();
1970
+ actions.commit();
1971
+ actions.onResize();
1972
+ actions.onAutosave();
1973
+ break;
1974
+ }
1975
+ case "rebindPanelCallbacks":
1976
+ for (const item of context.items) {
1977
+ if (isPanelData(item) && item.id === event.data.id) {
1978
+ item.onCollapseChange = event.data.onCollapseChange;
1979
+ item.onResize = event.data.onResize;
1983
1980
  }
1981
+ }
1982
+ break;
1983
+ case "updateConstraints":
1984
+ actions.prepare();
1984
1985
 
1985
- return context.items;
1986
- },
1987
- }),
1988
- updateConstraints: assign({
1989
- items: ({ context, event }) => {
1990
- isEvent(event, ["updateConstraints"]);
1991
-
1992
- for (const item of context.items) {
1993
- if (isPanelData(item) && item.id === event.data.id) {
1994
- const panel = event.data as PanelData;
1995
- item.min = panel.min;
1996
- item.max = panel.max;
1997
- item.default = panel.default;
1998
- item.collapsedSize = panel.collapsedSize;
1999
- item.isStaticAtRest = panel.isStaticAtRest;
2000
- item.collapseAnimation = panel.collapseAnimation;
2001
- item.collapsible = panel.collapsible;
2002
- } else if (isPanelHandle(item) && item.id === event.data.id) {
2003
- const handle = event.data as PanelHandleData;
2004
- item.size = handle.size;
2005
- }
1986
+ for (const item of context.items) {
1987
+ if (isPanelData(item) && item.id === event.data.id) {
1988
+ const panel = event.data as PanelData;
1989
+ item.min = panel.min;
1990
+ item.max = panel.max;
1991
+ item.default = panel.default;
1992
+ item.collapsedSize = panel.collapsedSize;
1993
+ item.isStaticAtRest = panel.isStaticAtRest;
1994
+ item.collapseAnimation = panel.collapseAnimation;
1995
+ item.collapsible = panel.collapsible;
1996
+ } else if (isPanelHandle(item) && item.id === event.data.id) {
1997
+ const handle = event.data as PanelHandleData;
1998
+ item.size = handle.size;
2006
1999
  }
2000
+ }
2007
2001
 
2008
- return context.items;
2009
- },
2010
- }),
2011
- onRegisterDynamicPanel: assign({
2012
- items: ({ context, event }) => {
2013
- isEvent(event, ["registerDynamicPanel"]);
2002
+ actions.clearLastKnownSize();
2003
+ actions.commit();
2004
+ actions.onResize();
2005
+ actions.onAutosave();
2006
+ break;
2007
+ case "setActualItemsSize": {
2008
+ const withLastKnownSize = context.items.map((i) => {
2009
+ if (!isPanelData(i)) return i;
2010
+ const lastKnownSize = event.childrenSizes[i.id] || i.lastKnownSize;
2011
+ return { ...i, lastKnownSize };
2012
+ });
2014
2013
 
2015
- let currentValue: ParsedUnit = makePixelUnit(0);
2014
+ let totalSize = 0;
2015
+ let useLastKnownSize = false;
2016
2016
 
2017
- if (
2018
- event.data.collapsible &&
2019
- event.data.collapsed &&
2020
- event.data.collapsedSize
2021
- ) {
2022
- currentValue = event.data.collapsedSize;
2023
- } else if (event.data.default) {
2024
- currentValue = event.data.default;
2017
+ for (const item of withLastKnownSize) {
2018
+ if (isPanelData(item)) {
2019
+ const size =
2020
+ item.lastKnownSize?.[
2021
+ context.orientation === "horizontal" ? "width" : "height"
2022
+ ];
2023
+
2024
+ // If any size is 0 don't handle overflow
2025
+ if (!size) {
2026
+ context.items = withLastKnownSize;
2027
+ useLastKnownSize = true;
2028
+ break;
2029
+ }
2030
+
2031
+ totalSize += size;
2025
2032
  } else {
2026
- currentValue = event.data.min;
2033
+ totalSize += item.size.value.toNumber();
2027
2034
  }
2035
+ }
2028
2036
 
2029
- const newItems = addDeDuplicatedItems(context.items, {
2030
- type: "panel",
2031
- ...event.data,
2032
- currentValue,
2033
- });
2034
- const itemIndex = newItems.findIndex(
2035
- (item) => item.id === event.data.id
2036
- );
2037
- const newContext = { ...context, items: newItems };
2038
- const overflowDueToHandles = context.items
2039
- .reduce((acc, i) => {
2040
- if (isPanelHandle(i)) {
2041
- return acc.add(getUnitPixelValue(context, i.size));
2042
- }
2037
+ if (useLastKnownSize) {
2038
+ context.items = withLastKnownSize;
2039
+ } else if (totalSize > getGroupSize(context)) {
2040
+ context.items = handleOverflow({
2041
+ ...context,
2042
+ items: prepareItems({ ...context, items: withLastKnownSize }),
2043
+ }).items;
2044
+ } else {
2045
+ context.items = withLastKnownSize;
2046
+ }
2043
2047
 
2044
- return acc.add(i.currentValue.value);
2045
- }, new Big(0))
2046
- .minus(getGroupSize(context));
2048
+ actions.onResize();
2049
+ break;
2050
+ }
2051
+ case "setSize": {
2052
+ context.size = event.size;
2053
+ actions.onResize();
2054
+ break;
2055
+ }
2056
+ case "setOrientation":
2057
+ context.orientation = event.orientation;
2058
+ actions.clearLastKnownSize();
2059
+ actions.onResize();
2060
+ break;
2061
+ default:
2062
+ break;
2063
+ }
2047
2064
 
2048
- applyDeltaInBothDirections(
2049
- newContext,
2050
- newItems,
2051
- itemIndex,
2052
- currentValue.value.add(overflowDueToHandles).neg()
2065
+ if (state.current === "idle") {
2066
+ switch (event.type) {
2067
+ case "dragHandleStart":
2068
+ transition("dragging");
2069
+ context.activeDragHandleId = event.handleId;
2070
+ break;
2071
+ case "setPanelPixelSize": {
2072
+ actions.prepare();
2073
+ actions.clearLastKnownSize();
2074
+
2075
+ const panel = getPanelWithId(context, event.panelId);
2076
+ const handle = getHandleForPanelId(context, event.panelId);
2077
+ const current = panel.currentValue.value;
2078
+ const newSize = clampUnit(
2079
+ context,
2080
+ panel,
2081
+ getUnitPixelValue(context, parseUnit(event.size))
2053
2082
  );
2083
+ const isBigger = newSize > current;
2084
+ const delta = isBigger
2085
+ ? newSize.minus(current)
2086
+ : current.minus(newSize);
2054
2087
 
2055
- return newItems;
2056
- },
2057
- }),
2058
- assignPanelHandleData: assign({
2059
- items: ({ context, event }) => {
2060
- isEvent(event, ["registerPanelHandle"]);
2061
-
2062
- const unit =
2063
- typeof event.data.size === "string"
2064
- ? parseUnit(event.data.size)
2065
- : event.data.size;
2066
-
2067
- return addDeDuplicatedItems(context.items, {
2068
- type: "handle",
2069
- ...event.data,
2070
- size: {
2071
- type: "pixel",
2072
- value: new Big(unit.value),
2073
- },
2074
- });
2075
- },
2076
- }),
2077
- removeItem: assign({
2078
- items: ({ context, event }) => {
2079
- isEvent(event, ["unregisterPanel", "unregisterPanelHandle"]);
2080
- const itemIndex = context.items.findIndex(
2081
- (item) => item.id === event.id
2088
+ Object.assign(
2089
+ context,
2090
+ iterativelyUpdateLayout({
2091
+ context,
2092
+ direction: (handle.direction * (isBigger ? 1 : -1)) as -1 | 1,
2093
+ handleId: handle.item.id,
2094
+ delta,
2095
+ })
2082
2096
  );
2083
- const item = context.items[itemIndex];
2084
2097
 
2085
- if (!item) {
2086
- return context.items;
2098
+ actions.commit();
2099
+ actions.onResize();
2100
+ actions.onAutosave();
2101
+ break;
2102
+ }
2103
+ case "collapsePanel":
2104
+ if (guards.shouldNotifyCollapseToggle(event)) {
2105
+ actions.notifyCollapseToggle(event);
2106
+ } else {
2107
+ transition("togglingCollapse");
2108
+ abortController.abort();
2109
+ animationActor(context, event, send, abortController).then(
2110
+ (output) => {
2111
+ actions.onAnimationEnd(output);
2112
+ transition("idle");
2113
+ }
2114
+ );
2115
+ }
2116
+ break;
2117
+ case "expandPanel":
2118
+ if (guards.cannotExpandPanel(event)) {
2119
+ break;
2120
+ } else {
2121
+ if (guards.shouldNotifyCollapseToggle(event)) {
2122
+ actions.notifyCollapseToggle(event);
2123
+ } else {
2124
+ transition("togglingCollapse");
2125
+ abortController.abort();
2126
+ animationActor(context, event, send, abortController).then(
2127
+ (output) => {
2128
+ actions.onAnimationEnd(output);
2129
+ transition("idle");
2130
+ }
2131
+ );
2132
+ }
2087
2133
  }
2088
2134
 
2089
- const newItems = context.items.filter((i) => i.id !== event.id);
2090
- const removedSize = isPanelData(item)
2091
- ? item.currentValue.value
2092
- : item.size.value;
2135
+ break;
2136
+ default:
2137
+ break;
2138
+ }
2139
+ } else if (state.current === "dragging") {
2140
+ switch (event.type) {
2141
+ case "dragHandle":
2142
+ actions.clearLastKnownSize();
2143
+ assign(context, updateLayout(context, event));
2144
+ actions.onResize();
2145
+ break;
2146
+ case "dragHandleEnd":
2147
+ transition("idle");
2148
+ break;
2149
+ case "expandPanel":
2150
+ case "collapsePanel":
2151
+ if (guards.shouldCollapseToggle(event)) {
2152
+ actions.runCollapseToggle(event);
2153
+ }
2154
+ break;
2155
+ }
2156
+ } else if (state.current === "togglingCollapse") {
2157
+ switch (event.type) {
2158
+ case "applyDelta":
2159
+ assign(
2160
+ context,
2161
+ updateLayout(context, {
2162
+ handleId: event.handleId,
2163
+ type: "collapsePanel",
2164
+ disregardCollapseBuffer: true,
2165
+ value: dragHandlePayload({
2166
+ delta: event.delta,
2167
+ orientation: context.orientation,
2168
+ }),
2169
+ })
2170
+ );
2171
+ actions.onResize();
2172
+ break;
2173
+ }
2174
+ }
2093
2175
 
2094
- applyDeltaInBothDirections(context, newItems, itemIndex, removedSize);
2176
+ onUpdate?.(context);
2177
+ }
2095
2178
 
2096
- return newItems;
2097
- },
2098
- }),
2099
- prepare: assign({
2100
- items: ({ context }) => prepareItems(context),
2101
- }),
2102
- onDragHandle: enqueueActions(({ context, event, enqueue }) => {
2103
- isEvent(event, ["dragHandle"]);
2104
- enqueue.assign(updateLayout(context, event));
2105
- }),
2106
- commit: assign({
2107
- dragOvershoot: new Big(0),
2108
- items: ({ context }) => commitLayout(context),
2109
- }),
2110
- onApplyDelta: assign(({ context, event }) => {
2111
- isEvent(event, ["applyDelta"]);
2112
- return updateLayout(context, {
2113
- handleId: event.handleId,
2114
- type: "collapsePanel",
2115
- disregardCollapseBuffer: true,
2116
- value: dragHandlePayload({
2117
- delta: event.delta,
2118
- orientation: context.orientation,
2119
- }),
2120
- });
2121
- }),
2122
- onSetPanelSize: enqueueActions(({ context, event, enqueue }) => {
2123
- isEvent(event, ["setPanelPixelSize"]);
2124
-
2125
- const panel = getPanelWithId(context, event.panelId);
2126
- const handle = getHandleForPanelId(context, event.panelId);
2127
- const current = panel.currentValue.value;
2128
- const newSize = clampUnit(
2129
- context,
2130
- panel,
2131
- getUnitPixelValue(context, parseUnit(event.size))
2132
- );
2133
- const isBigger = newSize > current;
2134
- const delta = isBigger
2135
- ? newSize.minus(current)
2136
- : current.minus(newSize);
2179
+ return [context, send, state] as const;
2180
+ }
2137
2181
 
2138
- enqueue.assign(
2139
- iterativelyUpdateLayout({
2140
- context,
2141
- direction: (handle.direction * (isBigger ? 1 : -1)) as -1 | 1,
2142
- handleId: handle.item.id,
2143
- delta,
2144
- })
2145
- );
2146
- }),
2147
- onResize: ({ context }) => {
2148
- for (const item of context.items) {
2149
- if (isPanelData(item)) {
2150
- const pixel = item.lastKnownSize
2151
- ? new Big(
2152
- context.orientation === "horizontal"
2153
- ? item.lastKnownSize.width
2154
- : item.lastKnownSize.height
2155
- )
2156
- : clampUnit(
2157
- context,
2158
- item,
2159
- getUnitPixelValue(context, item.currentValue)
2160
- );
2161
- const groupSize = getGroupSize(context);
2162
-
2163
- item.onResize?.current?.({
2164
- pixel: pixel.toNumber(),
2165
- percentage:
2166
- groupSize > 0
2167
- ? pixel.div(getGroupSize(context)).toNumber()
2168
- : -1,
2169
- });
2170
- }
2171
- }
2172
- },
2173
- },
2174
- }
2175
- );
2182
+ export type SendFn = ReturnType<typeof groupMachine>[1];
2176
2183
 
2177
2184
  // #endregion