@marianmeres/stuic 2.1.8 → 2.1.10

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.
@@ -65,6 +65,10 @@
65
65
  return out;
66
66
  });
67
67
 
68
+ let CmpButtonOk = $derived(current.CmpButtonOk ?? Button);
69
+ let CmpButtonCancel = $derived(current.CmpButtonCancel ?? Button);
70
+ let CmpButtonCustom = $derived(current.CmpButtonCustom ?? Button);
71
+
68
72
  let inputEl = $state<any>();
69
73
  let okButtonEl = $state<any>();
70
74
 
@@ -189,28 +193,28 @@
189
193
  <menu class={twMerge(_classMenu, classMenu)}>
190
194
  {#if current.type !== ALERT}
191
195
  <li class={twMerge(_classMenuLi, classMenuLi)}>
192
- <Button
196
+ <CmpButtonCancel
193
197
  class={twMerge("cancel", _classButton, classButton)}
194
198
  disabled={isPending}
195
199
  onclick={createOnClick("cancel", current.onCancel)}
196
200
  >
197
201
  <Thc thc={current.labelCancel} {forceAsHtml} />
198
- </Button>
202
+ </CmpButtonCancel>
199
203
  </li>
200
204
  {/if}
201
205
  {#if current.labelCustom && typeof current.onCustom === "function"}
202
206
  <li class={twMerge(_classMenuLi, classMenuLi)}>
203
- <Button
207
+ <CmpButtonCustom
204
208
  class={twMerge("custom", _classButton, classButton)}
205
209
  disabled={isPending}
206
210
  onclick={createOnClick("custom", current.onCustom)}
207
211
  >
208
212
  <Thc thc={current.labelCustom} {forceAsHtml} />
209
- </Button>
213
+ </CmpButtonCustom>
210
214
  </li>
211
215
  {/if}
212
216
  <li class={twMerge(_classMenuLi, classMenuLi)}>
213
- <Button
217
+ <CmpButtonOk
214
218
  class={twMerge("ok", _classButton, classButton)}
215
219
  variant="primary"
216
220
  disabled={isPending}
@@ -218,7 +222,7 @@
218
222
  bind:el={okButtonEl}
219
223
  >
220
224
  <Thc thc={current.labelOk} {forceAsHtml} />
221
- </Button>
225
+ </CmpButtonOk>
222
226
  </li>
223
227
  </menu>
224
228
  {#if isPending}
@@ -25,6 +25,9 @@ export interface AlertConfirmPromptObj extends Record<string, any> {
25
25
  variant: AlertConfirmPromptVariant;
26
26
  iconFn: (() => string) | boolean;
27
27
  forceAsHtml?: boolean;
28
+ CmpButtonOk?: any;
29
+ CmpButtonCancel?: any;
30
+ CmpButtonCustom?: any;
28
31
  }
29
32
  export declare class AlertConfirmPromptStack {
30
33
  #private;
@@ -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.10",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",