@marianmeres/stuic 2.1.8 → 2.1.9

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.
@@ -1,35 +1,6 @@
1
- <script lang="ts" module>
2
- interface BodyStyles {
3
- position: string | null;
4
- top: string | null;
5
- width: string | null;
6
- overflow: string | null;
7
- }
8
-
9
- function get_body_style(): BodyStyles {
10
- // const style = window.getComputedStyle(document.body);
11
- const style = document.body.style; // we want only explicitly defined, not computed
12
- return {
13
- position: style.position || null,
14
- top: style.top || null,
15
- width: style.width || null,
16
- overflow: style.overflow || null,
17
- };
18
- }
19
-
20
- function restore_body_styles(original: BodyStyles) {
21
- (["position", "top", "width", "overflow"] as (keyof BodyStyles)[]).forEach((k) => {
22
- if (original[k] !== null) {
23
- document.body.style[k] = original[k];
24
- } else {
25
- document.body.style.removeProperty(k);
26
- }
27
- });
28
- }
29
- </script>
30
-
31
1
  <script lang="ts">
32
2
  import {
3
+ BodyScroll,
33
4
  focusTrap as focusTrapAction,
34
5
  twMerge,
35
6
  waitForNextRepaint,
@@ -37,7 +8,7 @@
37
8
  } from "../../index.js";
38
9
  import { createClog } from "@marianmeres/clog";
39
10
  import { PressedKeys, watch } from "runed";
40
- import { onDestroy, tick, type Snippet } from "svelte";
11
+ import { onDestroy, onMount, tick, type Snippet } from "svelte";
41
12
  import { fade } from "svelte/transition";
42
13
 
43
14
  const clog = createClog("Backdrop").debug;
@@ -120,36 +91,15 @@
120
91
  }
121
92
  );
122
93
 
123
- // lock body scroll when open and restore back
124
- let _original: BodyStyles = {
125
- position: null,
126
- top: null,
127
- width: null,
128
- overflow: null,
129
- };
130
-
131
- function _restore() {
132
- const scrollY = document.body.style.top;
133
- restore_body_styles(_original);
134
- // Restore scroll position
135
- window.scrollTo(0, parseInt(scrollY || "0") * -1);
136
- }
137
-
138
94
  $effect(() => {
139
95
  if (noScrollLock) return;
140
- if (visible) {
141
- _original = get_body_style();
142
- const scrollY = window.scrollY;
143
- document.body.style.position = "fixed";
144
- document.body.style.top = `-${scrollY}px`;
145
- document.body.style.width = "100%";
146
- document.body.style.overflow = "hidden";
147
- } else {
148
- _restore();
149
- }
150
- return _restore; // onDestroy as well
96
+ visible ? BodyScroll.lock() : BodyScroll.unlock();
151
97
  });
152
98
 
99
+ // we need onDestroy as well
100
+ // Note, that this will also reset if nested... (which is not desired, but ignoring)
101
+ onDestroy(BodyScroll.unlock);
102
+
153
103
  $effect(() => {
154
104
  function onkeydown(e: KeyboardEvent) {
155
105
  if (e.key === "Escape" && typeof onEscape === "function") {
@@ -1,33 +1,3 @@
1
- <script lang="ts" module>
2
- interface BodyStyles {
3
- position: string | null;
4
- top: string | null;
5
- width: string | null;
6
- overflow: string | null;
7
- }
8
-
9
- function get_body_style(): BodyStyles {
10
- // const style = window.getComputedStyle(document.body);
11
- const style = document.body.style; // we want only explicitly defined, not computed
12
- return {
13
- position: style.position || null,
14
- top: style.top || null,
15
- width: style.width || null,
16
- overflow: style.overflow || null,
17
- };
18
- }
19
-
20
- function restore_body_styles(original: BodyStyles) {
21
- (["position", "top", "width", "overflow"] as (keyof BodyStyles)[]).forEach((k) => {
22
- if (original[k] !== null) {
23
- document.body.style[k] = original[k];
24
- } else {
25
- document.body.style.removeProperty(k);
26
- }
27
- });
28
- }
29
- </script>
30
-
31
1
  <script lang="ts">
32
2
  import { onClickOutside } from "runed";
33
3
  import { onDestroy, onMount, tick, type Snippet } from "svelte";
@@ -36,6 +6,7 @@
36
6
  import { twMerge } from "../../utils/tw-merge.js";
37
7
  import { createClog } from "@marianmeres/clog";
38
8
  import { waitForNextRepaint } from "../../utils/paint.js";
9
+ import { BodyScroll } from "../../utils/body-scroll-locker.js";
39
10
 
40
11
  const clog = createClog("ModalDialog").debug;
41
12
 
@@ -65,7 +36,9 @@
65
36
  preClose,
66
37
  }: Props = $props();
67
38
 
68
- let visible = $state(false);
39
+ // important to start as undefined (because of scroll save/restore)
40
+ let visible: boolean | undefined = $state(undefined);
41
+
69
42
  let dialog = $state<HTMLDialogElement>()!;
70
43
  let box = $state<HTMLDivElement>()!;
71
44
  let _opener: undefined | null | HTMLElement = $state();
@@ -106,36 +79,16 @@
106
79
  () => !noClickOutsideClose && close()
107
80
  );
108
81
 
109
- // lock body scroll when open and restore back
110
- let _original: BodyStyles = {
111
- position: null,
112
- top: null,
113
- width: null,
114
- overflow: null,
115
- };
116
-
117
- function _restore() {
118
- const scrollY = document.body.style.top;
119
- restore_body_styles(_original);
120
- // Restore scroll position
121
- window.scrollTo(0, parseInt(scrollY || "0") * -1);
122
- }
123
-
124
82
  $effect(() => {
125
- if (visible) {
126
- _original = get_body_style();
127
- const scrollY = window.scrollY;
128
- document.body.style.position = "fixed";
129
- document.body.style.top = `-${scrollY}px`;
130
- document.body.style.width = "100%";
131
- document.body.style.overflow = "hidden";
132
- } else {
133
- _restore();
134
- }
135
- // also onDestroy (will also reset if nested... which is not desired, but ignoring currently)
136
- return _restore;
83
+ // noop if we're undefined ($effect runs immediately as onMount)
84
+ if (visible === undefined) return;
85
+ visible ? BodyScroll.lock() : BodyScroll.unlock();
137
86
  });
138
87
 
88
+ // we need onDestroy as well
89
+ // Note, that this will also reset if nested... (which is not desired, but ignoring)
90
+ onDestroy(BodyScroll.unlock);
91
+
139
92
  // $inspect("Modal dialog mounted, is visible:", visible).with(clog);
140
93
  </script>
141
94
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Helper for "locking" and "unlocking" body scroll (window.scrollY) position
3
+ */
4
+ export declare class BodyScroll {
5
+ static lock(): void;
6
+ static unlock(): void;
7
+ private static _get_body_style;
8
+ private static _restore_body_styles;
9
+ }
@@ -0,0 +1,76 @@
1
+ import { createClog } from "@marianmeres/clog";
2
+ const clog = createClog("BodyScroll").debug;
3
+ /**
4
+ * Helper for "locking" and "unlocking" body scroll (window.scrollY) position
5
+ */
6
+ export class BodyScroll {
7
+ static lock() {
8
+ const data = document.body.dataset;
9
+ // Only save the scroll position if it hasn't been saved already
10
+ if (data.originalScrollY === undefined) {
11
+ const scrollY = window.scrollY || window.pageYOffset;
12
+ // Save body styles as serialized json
13
+ data.originalScrollStyleBackup = BodyScroll._get_body_style();
14
+ // Save the original scroll position in dataset
15
+ data.originalScrollY = `${scrollY}`;
16
+ data.scrollLockCount = "1";
17
+ // Apply the fixed positioning
18
+ document.body.style.position = "fixed";
19
+ document.body.style.top = `-${scrollY}px`;
20
+ document.body.style.width = "100%";
21
+ document.body.style.overflow = "hidden";
22
+ }
23
+ else {
24
+ // Another component already locked the scroll, just increment the counter
25
+ const currentCount = parseInt(data.scrollLockCount, 10);
26
+ data.scrollLockCount = `${currentCount + 1}`;
27
+ }
28
+ }
29
+ static unlock() {
30
+ const data = document.body.dataset;
31
+ // Only proceed if scroll is currently locked
32
+ if (data.scrollLockCount !== undefined) {
33
+ const count = parseInt(data.scrollLockCount, 10);
34
+ if (count > 1) {
35
+ // Other components still need the lock, just decrement the counter
36
+ data.scrollLockCount = `${count - 1}`;
37
+ }
38
+ else {
39
+ // This is the last component, restore everything
40
+ const originalScrollY = parseInt(data.originalScrollY, 10);
41
+ BodyScroll._restore_body_styles(data.originalScrollStyleBackup);
42
+ // Remove our data attributes
43
+ delete data.originalScrollY;
44
+ delete data.originalScrollStyleBackup;
45
+ delete data.scrollLockCount;
46
+ // Restore the scroll position
47
+ window.scrollTo(0, originalScrollY);
48
+ }
49
+ }
50
+ }
51
+ static _get_body_style() {
52
+ // we want only explicitly defined, not computed
53
+ const style = document.body.style;
54
+ return JSON.stringify({
55
+ position: style.position || null,
56
+ top: style.position || null,
57
+ width: style.width || null,
58
+ overflow: style.overflow || null,
59
+ });
60
+ }
61
+ static _restore_body_styles(originalJsonString) {
62
+ let original = { position: null, top: null, width: null, overflow: null };
63
+ try {
64
+ original = JSON.parse(originalJsonString);
65
+ }
66
+ catch (e) { }
67
+ ["position", "top", "width", "overflow"].forEach((k) => {
68
+ if (original[k] !== null) {
69
+ document.body.style[k] = original[k];
70
+ }
71
+ else {
72
+ document.body.style.removeProperty(k);
73
+ }
74
+ });
75
+ }
76
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./body-scroll-locker.js";
1
2
  export * from "./breakpoint.svelte.js";
2
3
  export * from "./colors.js";
3
4
  export * from "./debounce.js";
@@ -1,3 +1,4 @@
1
+ export * from "./body-scroll-locker.js";
1
2
  export * from "./breakpoint.svelte.js";
2
3
  export * from "./colors.js";
3
4
  export * from "./debounce.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",