@window-splitter/state 0.3.2 → 0.4.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/dist/esm/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import Cookies from "universal-cookie";
1
2
  import { raf } from "@react-spring/rafz";
2
3
  import { createMachine, assign, enqueueActions, fromPromise, } from "xstate";
3
4
  import invariant from "invariant";
@@ -70,9 +71,7 @@ export function getCursor(context) {
70
71
  }
71
72
  }
72
73
  export function prepareSnapshot(snapshot) {
73
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
- const snapshotContext = snapshot
75
- .context;
74
+ const snapshotContext = snapshot.context;
76
75
  snapshotContext.dragOvershoot = new Big(snapshotContext.dragOvershoot);
77
76
  for (const item of snapshotContext.items) {
78
77
  if (isPanelData(item)) {
@@ -138,6 +137,7 @@ export function initializePanel(item) {
138
137
  id: item.id,
139
138
  collapseAnimation: item.collapseAnimation,
140
139
  default: item.default ? parseUnit(item.default) : undefined,
140
+ isStaticAtRest: item.isStaticAtRest,
141
141
  };
142
142
  return { ...data, currentValue: makePixelUnit(-1) };
143
143
  }
@@ -280,8 +280,7 @@ function panelHasSpace(context, item, adjustment) {
280
280
  return (item.currentValue.value.gte(getUnitPixelValue(context, item.min)) &&
281
281
  item.currentValue.value.lt(getUnitPixelValue(context, item.max)));
282
282
  }
283
- return (item.currentValue.value.gt(getUnitPixelValue(context, item.min)) &&
284
- item.currentValue.value.lte(getUnitPixelValue(context, item.max)));
283
+ return item.currentValue.value.gt(getUnitPixelValue(context, item.min));
285
284
  }
286
285
  /** Search in a `direction` for a panel that still has space to expand. */
287
286
  function findPanelWithSpace(context, items, start, direction, adjustment, disregardCollapseBuffer) {
@@ -305,9 +304,10 @@ function getStaticWidth(context) {
305
304
  if (isPanelHandle(item)) {
306
305
  width = width.add(item.size.value);
307
306
  }
308
- else if (isPanelData(item) &&
309
- item.collapsed &&
310
- item.currentValue.type === "pixel") {
307
+ else if (item.collapsed && item.currentValue.type === "pixel") {
308
+ width = width.add(item.currentValue.value);
309
+ }
310
+ else if (item.isStaticAtRest) {
311
311
  width = width.add(item.currentValue.value);
312
312
  }
313
313
  }
@@ -348,15 +348,26 @@ export function getPanelPercentageSize(context, panelId) {
348
348
  /** Build the grid template from the item values. */
349
349
  export function buildTemplate(context) {
350
350
  const staticWidth = getStaticWidth(context);
351
+ let hasSeenFillPanel = false;
351
352
  return context.items
352
353
  .map((item) => {
353
354
  if (item.type === "panel") {
354
355
  const min = formatUnit(item.min);
355
356
  if (item.currentValue.type === "pixel" &&
356
357
  item.currentValue.value.toNumber() !== -1) {
358
+ if (item.isStaticAtRest) {
359
+ const max = item.max === "1fr" ? "100%" : formatUnit(item.max);
360
+ return `clamp(${min}, ${formatUnit(item.currentValue)}, ${max})`;
361
+ }
357
362
  return formatUnit(item.currentValue);
358
363
  }
359
364
  else if (item.currentValue.type === "percent") {
365
+ if (!hasSeenFillPanel &&
366
+ (item.max === "1fr" ||
367
+ (item.max.type === "percent" && item.max.value.eq(100)))) {
368
+ hasSeenFillPanel = true;
369
+ return `minmax(${min}, 1fr)`;
370
+ }
360
371
  const max = item.max === "1fr" ? "100%" : formatUnit(item.max);
361
372
  return `minmax(${min}, min(calc(${item.currentValue.value} * (100% - ${staticWidth}px)), ${max}))`;
362
373
  }
@@ -459,11 +470,18 @@ export function prepareItems(context) {
459
470
  const newItems = [];
460
471
  for (const item of context.items) {
461
472
  if (!item || !isPanelData(item)) {
462
- newItems.push(item);
473
+ newItems.push({ ...item });
474
+ continue;
475
+ }
476
+ if (item.lastKnownSize) {
477
+ const lastSize = makePixelUnit(context.orientation === "horizontal"
478
+ ? item.lastKnownSize.width
479
+ : item.lastKnownSize.height);
480
+ newItems.push({ ...item, currentValue: lastSize });
463
481
  continue;
464
482
  }
465
483
  if (item.currentValue.type === "pixel") {
466
- newItems.push(item);
484
+ newItems.push({ ...item });
467
485
  continue;
468
486
  }
469
487
  const pixel = new Big(getGroupSize(context))
@@ -517,6 +535,10 @@ function updateLayout(context, dragEvent) {
517
535
  const potentialNewValue = panelAfter.currentValue.value.add(new Big(newDragOvershoot).mul(isInRightBuffer ? moveDirection : 1));
518
536
  const min = getUnitPixelValue(context, panelAfter.min);
519
537
  const isInDragBugger = newDragOvershoot.abs().lt(COLLAPSE_THRESHOLD) &&
538
+ // Let the panel expand at it's min size
539
+ !panelAfter.currentValue.value
540
+ .add(newDragOvershoot.abs())
541
+ .gte(panelAfter.min.value) &&
520
542
  panelAfter.collapsible &&
521
543
  panelAfter.collapsed &&
522
544
  (isInLeftOvershoot || isInRightOvershoot);
@@ -529,9 +551,9 @@ function updateLayout(context, dragEvent) {
529
551
  }
530
552
  }
531
553
  // Don't let the panel collapse until the threshold is reached
532
- if (panelBefore.collapsible &&
533
- panelBefore.currentValue.value ===
534
- getUnitPixelValue(context, panelBefore.min)) {
554
+ if (!dragEvent.disregardCollapseBuffer &&
555
+ panelBefore.collapsible &&
556
+ panelBefore.currentValue.value.eq(getUnitPixelValue(context, panelBefore.min))) {
535
557
  const potentialNewValue = panelBefore.currentValue.value.sub(newDragOvershoot.abs());
536
558
  if (newDragOvershoot.abs().lt(COLLAPSE_THRESHOLD) &&
537
559
  potentialNewValue.gt(getUnitPixelValue(context, panelBefore.collapsedSize))) {
@@ -578,7 +600,6 @@ function updateLayout(context, dragEvent) {
578
600
  .sub(panelAfterNewValue
579
601
  // Then re-add the move amount
580
602
  .add(Math.abs(moveAmount)));
581
- panelAfter.collapsed = false;
582
603
  if (extra.gt(0)) {
583
604
  panelAfterNewValue = panelAfterNewValue.add(extra);
584
605
  }
@@ -588,14 +609,18 @@ function updateLayout(context, dragEvent) {
588
609
  .minus(panelAfterPreviousValue)
589
610
  // And then re-apply the movement value
590
611
  .minus(Math.abs(moveAmount)));
612
+ if (panelBeforeNewValue.lt(panelBefore.min.value)) {
613
+ // TODO this should probably distribute the space between the panels?
614
+ return { dragOvershoot: newDragOvershoot };
615
+ }
616
+ panelAfter.collapsed = false;
591
617
  if (panelAfter.onCollapseChange?.current &&
592
618
  !panelAfter.collapseIsControlled &&
593
619
  !dragEvent.controlled) {
594
620
  panelAfter.onCollapseChange.current(false);
595
621
  }
596
622
  }
597
- const panelBeforeIsAboutToCollapse = panelBefore.currentValue.value ===
598
- getUnitPixelValue(context, panelBefore.min);
623
+ const panelBeforeIsAboutToCollapse = panelBefore.currentValue.value.eq(getUnitPixelValue(context, panelBefore.min));
599
624
  // If the panel was expanded and now is at it's min size, collapse it
600
625
  if (!dragEvent.disregardCollapseBuffer &&
601
626
  panelBefore.collapsible &&
@@ -621,8 +646,7 @@ function updateLayout(context, dragEvent) {
621
646
  panelAfter.currentValue = { type: "pixel", value: panelAfterNewValue };
622
647
  const leftoverSpace = new Big(getGroupSize(context)).minus(newItems.reduce((acc, b) => acc.add(isPanelData(b) ? b.currentValue.value : b.size.value), new Big(0)));
623
648
  if (!leftoverSpace.eq(0)) {
624
- panelBefore.currentValue.value =
625
- panelBefore.currentValue.value.add(leftoverSpace);
649
+ panelBefore.currentValue.value = clampUnit(context, panelBefore, panelBefore.currentValue.value.add(leftoverSpace));
626
650
  }
627
651
  return { items: newItems };
628
652
  }
@@ -643,7 +667,7 @@ function commitLayout(context) {
643
667
  });
644
668
  const staticWidth = getStaticWidth({ ...context, items: newItems });
645
669
  newItems.forEach((item, index) => {
646
- if (item.type !== "panel" || item.collapsed) {
670
+ if (item.type !== "panel" || item.collapsed || item.isStaticAtRest) {
647
671
  return;
648
672
  }
649
673
  newItems[index] = {
@@ -713,19 +737,77 @@ function applyDeltaInBothDirections(context, newItems, itemIndex, delta) {
713
737
  direction = direction === 1 ? -1 : 1;
714
738
  }
715
739
  }
740
+ /**
741
+ * A layout might overflow at small screen sizes.
742
+ * This function tries to fix that by:
743
+ *
744
+ * 1. It will try to collapse a panel if it can.
745
+ */
746
+ function handleOverflow(context) {
747
+ // If we haven't measured yet we can't do anything
748
+ if (context.items.some((i) => isPanelData(i) && i.currentValue.value.eq(-1))) {
749
+ return context;
750
+ }
751
+ const groupSize = new Big(getGroupSize(context));
752
+ const nonStaticWidth = groupSize.sub(getStaticWidth(context).toNumber());
753
+ const pixelItems = context.items.map((i) => {
754
+ if (isPanelHandle(i))
755
+ return i.size.value;
756
+ if (i.collapsed)
757
+ return getUnitPixelValue(context, i.currentValue);
758
+ const pixel = (i.currentValue.type === "pixel" && i.currentValue.value) ||
759
+ i.currentValue.value.mul(nonStaticWidth);
760
+ return clampUnit(context, i, pixel);
761
+ });
762
+ const totalSize = pixelItems.reduce((acc, i) => acc.add(i), new Big(0));
763
+ const overflow = totalSize.abs().sub(groupSize);
764
+ if (overflow.eq(0) || groupSize.eq(0)) {
765
+ return context;
766
+ }
767
+ let newContext = { ...context, items: prepareItems(context) };
768
+ const collapsiblePanel = newContext.items.find((i) => Boolean(isPanelData(i) && i.collapsible));
769
+ if (collapsiblePanel) {
770
+ const collapsiblePanelIndex = newContext.items.findIndex((i) => i.id === collapsiblePanel.id);
771
+ const handleId = getHandleForPanelId(newContext, collapsiblePanel.id);
772
+ const sizeChange = collapsiblePanel.currentValue.value.sub(getUnitPixelValue(newContext, collapsiblePanel.collapsedSize));
773
+ // Try to collapse the panel
774
+ newContext = {
775
+ ...newContext,
776
+ ...iterativelyUpdateLayout({
777
+ handleId: handleId.item.id,
778
+ delta: sizeChange,
779
+ direction: (handleId.direction * -1),
780
+ context: {
781
+ ...newContext,
782
+ // act like its the old size so the space is distributed correctly
783
+ size: { width: totalSize.toNumber(), height: totalSize.toNumber() },
784
+ },
785
+ }),
786
+ };
787
+ // Then remove all the overflow
788
+ applyDeltaInBothDirections(newContext, newContext.items, collapsiblePanelIndex, overflow.neg());
789
+ }
790
+ return { ...newContext, items: commitLayout(newContext) };
791
+ }
792
+ function clearLastKnownSize(items) {
793
+ return items.map((i) => ({ ...i, lastKnownSize: undefined }));
794
+ }
795
+ function getDeltaForEvent(context, event) {
796
+ const panel = getPanelWithId(context, event.panelId);
797
+ if (event.type === "expandPanel") {
798
+ return new Big(panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)).minus(panel.currentValue.value);
799
+ }
800
+ const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
801
+ return panel.currentValue.value.minus(collapsedSize);
802
+ }
716
803
  const animationActor = fromPromise(({ input: { send, context, event } }) => new Promise((resolve) => {
717
804
  const panel = getPanelWithId(context, event.panelId);
718
805
  const handle = getHandleForPanelId(context, event.panelId);
719
806
  let direction = new Big(handle.direction);
720
- let fullDelta = new Big(0);
721
- if (event.type === "expandPanel") {
722
- fullDelta = new Big(panel.sizeBeforeCollapse ?? getUnitPixelValue(context, panel.min)).minus(panel.currentValue.value);
723
- }
724
- else {
725
- const collapsedSize = getUnitPixelValue(context, panel.collapsedSize);
807
+ const fullDelta = getDeltaForEvent(context, event);
808
+ if (event.type === "collapsePanel") {
726
809
  panel.sizeBeforeCollapse = panel.currentValue.value.toNumber();
727
810
  direction = direction.mul(new Big(-1));
728
- fullDelta = panel.currentValue.value.minus(collapsedSize);
729
811
  }
730
812
  const fps = 60;
731
813
  const { duration, ease } = getCollapseAnimation(panel);
@@ -769,15 +851,17 @@ export const groupMachine = createMachine({
769
851
  orientation: input.orientation || "horizontal",
770
852
  dragOvershoot: new Big(0),
771
853
  groupId: input.groupId,
854
+ autosaveStrategy: input.autosaveStrategy,
772
855
  }),
773
856
  states: {
774
857
  idle: {
858
+ entry: ["onAutosave"],
775
859
  on: {
776
- setActualItemsSize: { actions: ["recordActualItemSize", "onResize"] },
777
860
  dragHandleStart: { target: "dragging" },
778
861
  setPanelPixelSize: {
779
862
  actions: [
780
863
  "prepare",
864
+ "onClearLastKnownSize",
781
865
  "onSetPanelSize",
782
866
  "commit",
783
867
  "onResize",
@@ -792,6 +876,8 @@ export const groupMachine = createMachine({
792
876
  { target: "togglingCollapse" },
793
877
  ],
794
878
  expandPanel: [
879
+ // This will match if we can't expand and the expansion won't happen
880
+ { guard: "cannotExpandPanel" },
795
881
  {
796
882
  actions: "notifyCollapseToggle",
797
883
  guard: "shouldNotifyCollapseToggle",
@@ -803,7 +889,9 @@ export const groupMachine = createMachine({
803
889
  dragging: {
804
890
  entry: ["prepare"],
805
891
  on: {
806
- dragHandle: { actions: ["onDragHandle", "onResize"] },
892
+ dragHandle: {
893
+ actions: ["onClearLastKnownSize", "onDragHandle", "onResize"],
894
+ },
807
895
  dragHandleEnd: { target: "idle" },
808
896
  collapsePanel: {
809
897
  guard: "shouldCollapseToggle",
@@ -814,16 +902,16 @@ export const groupMachine = createMachine({
814
902
  actions: "runCollapseToggle",
815
903
  },
816
904
  },
817
- exit: ["commit", "onAutosave"],
905
+ exit: ["commit"],
818
906
  },
819
907
  togglingCollapse: {
820
- entry: ["prepare"],
908
+ entry: ["prepare", "onClearLastKnownSize"],
821
909
  invoke: {
822
910
  src: "animation",
823
911
  input: (i) => ({ ...i, send: i.self.send }),
824
912
  onDone: {
825
913
  target: "idle",
826
- actions: ["onToggleCollapseComplete", "commit", "onAutosave"],
914
+ actions: ["onToggleCollapseComplete", "commit"],
827
915
  },
828
916
  },
829
917
  on: {
@@ -832,25 +920,43 @@ export const groupMachine = createMachine({
832
920
  },
833
921
  },
834
922
  on: {
923
+ setActualItemsSize: { actions: ["recordActualItemSize", "onResize"] },
835
924
  registerPanel: { actions: ["assignPanelData"] },
836
925
  registerDynamicPanel: {
837
926
  actions: [
838
927
  "prepare",
839
928
  "onRegisterDynamicPanel",
929
+ "onClearLastKnownSize",
840
930
  "commit",
841
931
  "onResize",
842
932
  "onAutosave",
843
933
  ],
844
934
  },
845
935
  unregisterPanel: {
846
- actions: ["prepare", "removeItem", "commit", "onResize", "onAutosave"],
936
+ actions: [
937
+ "prepare",
938
+ "removeItem",
939
+ "onClearLastKnownSize",
940
+ "commit",
941
+ "onResize",
942
+ "onAutosave",
943
+ ],
847
944
  },
848
945
  registerPanelHandle: { actions: ["assignPanelHandleData"] },
849
946
  unregisterPanelHandle: {
850
- actions: ["prepare", "removeItem", "commit", "onResize", "onAutosave"],
947
+ actions: [
948
+ "prepare",
949
+ "removeItem",
950
+ "onClearLastKnownSize",
951
+ "commit",
952
+ "onResize",
953
+ "onAutosave",
954
+ ],
851
955
  },
852
956
  setSize: { actions: ["updateSize", "onResize"] },
853
- setOrientation: { actions: ["updateOrientation", "onResize"] },
957
+ setOrientation: {
958
+ actions: ["updateOrientation", "onClearLastKnownSize", "onResize"],
959
+ },
854
960
  },
855
961
  }, {
856
962
  guards: {
@@ -864,11 +970,52 @@ export const groupMachine = createMachine({
864
970
  const panel = getPanelWithId(context, event.panelId);
865
971
  return panel.collapseIsControlled === true;
866
972
  },
973
+ cannotExpandPanel: ({ context, event }) => {
974
+ isEvent(event, ["expandPanel"]);
975
+ const delta = getDeltaForEvent(context, event);
976
+ const handle = getHandleForPanelId(context, event.panelId);
977
+ const pixelItems = prepareItems(context);
978
+ let interimContext = { ...context, items: pixelItems };
979
+ interimContext = {
980
+ ...interimContext,
981
+ ...iterativelyUpdateLayout({
982
+ context: interimContext,
983
+ handleId: handle.item.id,
984
+ controlled: event.controlled,
985
+ delta: delta,
986
+ direction: handle.direction,
987
+ }),
988
+ };
989
+ const updatedPanel = interimContext.items.find((i) => i.id === event.panelId);
990
+ const totalSize = interimContext.items.reduce((acc, i) => acc.add(isPanelData(i) ? i.currentValue.value : i.size.value), new Big(0));
991
+ const didExpand = updatedPanel &&
992
+ isPanelData(updatedPanel) &&
993
+ !updatedPanel.currentValue.value.eq(updatedPanel.collapsedSize.value);
994
+ return totalSize.gt(getGroupSize(context)) || !didExpand;
995
+ },
867
996
  },
868
997
  actors: {
869
998
  animation: animationActor,
870
999
  },
871
1000
  actions: {
1001
+ onAutosave: ({ context, self }) => {
1002
+ if (!context.autosaveStrategy || typeof window === "undefined") {
1003
+ return;
1004
+ }
1005
+ const snapshot = self.getPersistedSnapshot();
1006
+ snapshot.context.items = clearLastKnownSize(context.items);
1007
+ snapshot.value = "idle";
1008
+ const data = JSON.stringify(snapshot);
1009
+ if (context.autosaveStrategy === "localStorage") {
1010
+ localStorage.setItem(context.groupId, data);
1011
+ }
1012
+ else {
1013
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1014
+ const ActualClass = Cookies.default || Cookies;
1015
+ const cookies = new ActualClass(null, { path: "/" });
1016
+ cookies.set(context.groupId, data, { path: "/", maxAge: 31536000 });
1017
+ }
1018
+ },
872
1019
  notifyCollapseToggle: ({ context, event }) => {
873
1020
  isEvent(event, ["collapsePanel", "expandPanel"]);
874
1021
  const panel = getPanelWithId(context, event.panelId);
@@ -892,8 +1039,7 @@ export const groupMachine = createMachine({
892
1039
  }),
893
1040
  onToggleCollapseComplete: assign({
894
1041
  items: ({ context, event: e }) => {
895
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
896
- const output = e.output;
1042
+ const { output } = e;
897
1043
  invariant(output, "Expected output from animation actor");
898
1044
  const panel = getPanelWithId(context, output.panelId);
899
1045
  panel.collapsed = output.action === "collapse";
@@ -912,15 +1058,33 @@ export const groupMachine = createMachine({
912
1058
  recordActualItemSize: assign({
913
1059
  items: ({ context, event }) => {
914
1060
  isEvent(event, ["setActualItemsSize"]);
915
- const orientation = context.orientation;
916
- for (const [id, size] of Object.entries(event.childrenSizes)) {
917
- const item = context.items.find((i) => i.id === id);
918
- if (!isPanelData(item)) {
919
- continue;
1061
+ const withLastKnownSize = context.items.map((i) => {
1062
+ if (!isPanelData(i))
1063
+ return i;
1064
+ const lastKnownSize = event.childrenSizes[i.id] || i.lastKnownSize;
1065
+ return { ...i, lastKnownSize };
1066
+ });
1067
+ let totalSize = 0;
1068
+ for (const item of withLastKnownSize) {
1069
+ if (isPanelData(item)) {
1070
+ const size = item.lastKnownSize?.[context.orientation === "horizontal" ? "width" : "height"];
1071
+ // If any size is 0 don't handle overflow
1072
+ if (!size) {
1073
+ return withLastKnownSize;
1074
+ }
1075
+ totalSize += size;
920
1076
  }
921
- item.currentValue = makePixelUnit(orientation === "horizontal" ? size.width : size.height);
1077
+ else {
1078
+ totalSize += item.size.value.toNumber();
1079
+ }
1080
+ }
1081
+ if (totalSize > getGroupSize(context)) {
1082
+ return handleOverflow({
1083
+ ...context,
1084
+ items: prepareItems({ ...context, items: withLastKnownSize }),
1085
+ }).items;
922
1086
  }
923
- return commitLayout(context);
1087
+ return withLastKnownSize;
924
1088
  },
925
1089
  }),
926
1090
  updateOrientation: assign({
@@ -929,6 +1093,9 @@ export const groupMachine = createMachine({
929
1093
  return event.orientation;
930
1094
  },
931
1095
  }),
1096
+ onClearLastKnownSize: assign({
1097
+ items: ({ context }) => clearLastKnownSize(context.items),
1098
+ }),
932
1099
  assignPanelData: assign({
933
1100
  items: ({ context, event }) => {
934
1101
  isEvent(event, ["registerPanel"]);
@@ -1048,10 +1215,17 @@ export const groupMachine = createMachine({
1048
1215
  onResize: ({ context }) => {
1049
1216
  for (const item of context.items) {
1050
1217
  if (isPanelData(item)) {
1051
- const pixel = clampUnit(context, item, getUnitPixelValue(context, item.currentValue));
1218
+ const pixel = item.lastKnownSize
1219
+ ? new Big(context.orientation === "horizontal"
1220
+ ? item.lastKnownSize.width
1221
+ : item.lastKnownSize.height)
1222
+ : clampUnit(context, item, getUnitPixelValue(context, item.currentValue));
1223
+ const groupSize = getGroupSize(context);
1052
1224
  item.onResize?.current?.({
1053
1225
  pixel: pixel.toNumber(),
1054
- percentage: pixel.div(getGroupSize(context)).toNumber(),
1226
+ percentage: groupSize > 0
1227
+ ? pixel.div(getGroupSize(context)).toNumber()
1228
+ : -1,
1055
1229
  });
1056
1230
  }
1057
1231
  }