@kws3/ui 1.8.0 → 1.8.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/CHANGELOG.mdx CHANGED
@@ -1,3 +1,6 @@
1
+ ## 1.8.1
2
+ - New `ScrollableList` component
3
+
1
4
  ## 1.8.0
2
5
  - `Modal`, `CardModal` and `ActionSheet` components now play an outro transition instead of abruptly disappearing.
3
6
  - Usability fixes for `SearchableSelect` and `MultiSelect`.
@@ -0,0 +1,231 @@
1
+ <!--
2
+ @component
3
+
4
+
5
+ @param {array} [items=[]] - Array of items, Default: `[]`
6
+ @param {string} [height="100%"] - Height of the wrapper, CSS String, Default: `"100%"`
7
+ @param {number | null} [item_height=null] - Height of each list item. If not set, height will be calculated automatically based on each item's offsetHeight, Default: `null`
8
+ @param {number} [start=0] - First item index rendered inside viewport - readonly, Default: `0`
9
+ @param {number} [end=0] - Last item index rendered inside viewport - readonly, Default: `0`
10
+ @param {number} [end_threshold=10] - `end` event will be fired when the list reaches this many items before the end of the list., Default: `10`
11
+ @param {string} [style=""] - Inline CSS for scroller container, Default: `""`
12
+ @param {string} [class=""] - CSS classes for scroller container, Default: `""`
13
+
14
+ ### Events
15
+ - `end` - Fired when the list reaches `end_threshold` items before the end of the list.
16
+
17
+ ### Slots
18
+ - `<slot name="default" {item} {index} />` - Default slot for list view items
19
+ - `<slot name="loader" />` - Optional slot to display a loading graphic at the bottom of the list
20
+ while more items are loading
21
+
22
+ -->
23
+ {#if hasResizeObserver}
24
+ <div
25
+ bind:this={viewport}
26
+ class="kws-scrollable-list with-resize-observer {klass}"
27
+ on:scroll={handle_scroll}
28
+ style="height:{height};{style}"
29
+ use:resizeObserver
30
+ on:resize={resize}>
31
+ <div
32
+ bind:this={contents}
33
+ style="padding-top: {top}px; padding-bottom: {bottom}px;">
34
+ {#each visible as item (item.index)}
35
+ <div class="row">
36
+ <!--Default slot for list view items-->
37
+ <slot item={item.data} index={item.index} />
38
+ </div>
39
+ {/each}
40
+ <!--Optional slot to display a loading graphic at the bottom of the list
41
+ while more items are loading-->
42
+ <slot name="loader" />
43
+ </div>
44
+ </div>
45
+ {:else}
46
+ <div
47
+ bind:this={viewport}
48
+ class="kws-scrollable-list {klass}"
49
+ on:scroll={handle_scroll}
50
+ style="height:{height};{style}"
51
+ bind:offsetHeight={viewport_height}>
52
+ <div
53
+ bind:this={contents}
54
+ style="padding-top: {top}px; padding-bottom: {bottom}px;">
55
+ {#each visible as item (item.index)}
56
+ <div class="row">
57
+ <!--Default slot for list view items-->
58
+ <slot item={item.data} index={item.index} />
59
+ </div>
60
+ {/each}
61
+ <!--Optional slot to display a loading graphic at the bottom of the list
62
+ while more items are loading-->
63
+ <slot name="loader" />
64
+ </div>
65
+ </div>
66
+ {/if}
67
+
68
+ <style>
69
+ .kws-scrollable-list {
70
+ overflow: auto;
71
+ -webkit-overflow-scrolling: touch;
72
+ position: relative;
73
+ height: 100%;
74
+ }
75
+ </style>
76
+
77
+ <script>
78
+ import { onMount, tick } from "svelte";
79
+ import { createEventDispatcher } from "svelte";
80
+ import {
81
+ resizeObserver,
82
+ hasResizeObserver,
83
+ } from "@kws3/ui/utils/resizeObserver";
84
+
85
+ const fire = createEventDispatcher();
86
+ /**
87
+ * Array of items
88
+ */
89
+ export let items = [],
90
+ /**
91
+ * Height of the wrapper, CSS String
92
+ */
93
+ height = "100%",
94
+ /**
95
+ * Height of each list item. If not set, height will be calculated automatically based on each item's offsetHeight
96
+ * @type {number | null}
97
+ */
98
+ item_height = null,
99
+ /**
100
+ * First item index rendered inside viewport - readonly
101
+ */
102
+ start = 0,
103
+ /**
104
+ * Last item index rendered inside viewport - readonly
105
+ */
106
+ end = 0,
107
+ /**
108
+ * `end` event will be fired when the list reaches this many items before the end of the list.
109
+ */
110
+ end_threshold = 10,
111
+ /**
112
+ * Inline CSS for scroller container
113
+ */
114
+ style = "";
115
+
116
+ /**
117
+ * CSS classes for scroller container
118
+ */
119
+ let klass = "";
120
+ export { klass as class };
121
+
122
+ // local state
123
+ let height_map = [],
124
+ rows,
125
+ viewport,
126
+ contents,
127
+ viewport_height = 0,
128
+ visible,
129
+ mounted,
130
+ top = 0,
131
+ bottom = 0,
132
+ average_height,
133
+ items_count = 0;
134
+
135
+ $: visible = items.slice(start, end).map((data, i) => {
136
+ return { index: i + start, data };
137
+ });
138
+
139
+ // whenever `items` changes, invalidate the current heightmap
140
+ $: items, viewport_height, item_height, mounted, refresh();
141
+
142
+ async function refresh() {
143
+ if (!mounted) return;
144
+ const scrollTop = viewport.scrollTop;
145
+ await tick(); // wait until the DOM is up to date
146
+ let content_height = top - scrollTop;
147
+ let i = start;
148
+ while (content_height < viewport_height && i < items.length) {
149
+ let row = rows[i - start];
150
+ if (!row) {
151
+ end = i + 1;
152
+ await tick(); // render the newly visible row
153
+ row = rows[i - start];
154
+ }
155
+ const row_height = (height_map[i] =
156
+ item_height || (row ? row.offsetHeight : 0));
157
+ content_height += row_height;
158
+ i += 1;
159
+ }
160
+ end = i;
161
+ const remaining = items.length - end;
162
+ average_height = (top + content_height) / end;
163
+ bottom = remaining * average_height;
164
+ height_map.length = items.length;
165
+ }
166
+
167
+ async function handle_scroll() {
168
+ const scrollTop = viewport.scrollTop;
169
+ const old_start = start;
170
+ for (let v = 0; v < rows.length; v += 1) {
171
+ height_map[start + v] = item_height || rows[v].offsetHeight;
172
+ }
173
+ let i = 0;
174
+ let y = 0;
175
+ while (i < items.length) {
176
+ const row_height = height_map[i] || average_height;
177
+ if (y + row_height > scrollTop) {
178
+ start = i;
179
+ top = y;
180
+ break;
181
+ }
182
+ y += row_height;
183
+ i += 1;
184
+ }
185
+ while (i < items.length) {
186
+ y += height_map[i] || average_height;
187
+ i += 1;
188
+ if (y > scrollTop + viewport_height) break;
189
+ }
190
+ end = i;
191
+ const remaining = items.length - end;
192
+ average_height = y / end;
193
+ while (i < items.length) height_map[i++] = average_height;
194
+ bottom = remaining * average_height;
195
+ // prevent jumping if we scrolled up into unknown territory
196
+ if (start < old_start) {
197
+ await tick();
198
+ let expected_height = 0;
199
+ let actual_height = 0;
200
+ for (let i = start; i < old_start; i += 1) {
201
+ if (rows[i - start]) {
202
+ expected_height += height_map[i];
203
+ actual_height += item_height || rows[i - start].offsetHeight;
204
+ }
205
+ }
206
+ const d = actual_height - expected_height;
207
+ viewport.scrollTo(0, scrollTop + d);
208
+ }
209
+ // fire on:end event if we scrolled past the end of the list
210
+ if (end > items.length - end_threshold) {
211
+ if (items_count !== items.length) {
212
+ items_count = items.length;
213
+ await tick();
214
+ /**
215
+ * Fired when the list reaches `end_threshold` items before the end of the list.
216
+ */
217
+ fire("end", { start, end });
218
+ }
219
+ }
220
+ }
221
+
222
+ const resize = () => {
223
+ viewport_height = viewport.offsetHeight;
224
+ };
225
+
226
+ // trigger initial refresh
227
+ onMount(() => {
228
+ rows = contents.getElementsByClassName("row");
229
+ mounted = true;
230
+ });
231
+ </script>
package/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as TimelineItem } from "./helpers/Timeline/TimelineItem.svelte"
17
17
  export { default as TimelineHeader } from "./helpers/Timeline/TimelineHeader.svelte";
18
18
  export { default as Nl2br } from "./helpers/Nl2br.svelte";
19
19
  export { default as ClipboardCopier } from "./helpers/ClipboardCopier.svelte";
20
+ export { default as ScrollableList } from "./helpers/ScrollableList.svelte";
20
21
  export { alert, confirm, prompt, default as Dialog } from "./helpers/Dialog";
21
22
  export {
22
23
  Notifications,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kws3/ui",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "UI components for use with Svelte v3 applications.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,5 +30,5 @@
30
30
  "text-mask-core": "^5.1.2",
31
31
  "tippy.js": "^6.3.1"
32
32
  },
33
- "gitHead": "8da0698c240f93ae1b371a9279d2473409646b10"
33
+ "gitHead": "2fdaba3e13397494f06b5881bf92f68b1c7c695d"
34
34
  }
@@ -25,6 +25,8 @@ This will work only when `track_height` is set to `true`
25
25
  : ''} {h_center ? 'h-centered' : ''} {active ? 'is-active' : ''} {klass}"
26
26
  {style}>
27
27
  <div
28
+ use:resizeObserver
29
+ on:resize={debouncedFireSizeChange}
28
30
  bind:this={slideInner}
29
31
  class="sliding-pane-inner {v_center ? 'v-centered' : ''} {h_center
30
32
  ? 'h-centered'
@@ -52,6 +54,10 @@ This will work only when `track_height` is set to `true`
52
54
  <script>
53
55
  import { onMount, createEventDispatcher } from "svelte";
54
56
  import { debounce } from "@kws3/ui/utils";
57
+ import {
58
+ resizeObserver,
59
+ hasResizeObserver,
60
+ } from "@kws3/ui/utils/resizeObserver";
55
61
 
56
62
  const fire = createEventDispatcher();
57
63
 
@@ -76,8 +82,7 @@ This will work only when `track_height` is set to `true`
76
82
  */
77
83
  track_height = true;
78
84
 
79
- const hasResizeObserver = typeof window.ResizeObserver != "undefined";
80
- let _height, slideInner, Observer;
85
+ let _height, slideInner;
81
86
 
82
87
  /**
83
88
  * CSS classes for the panel
@@ -91,18 +96,22 @@ This will work only when `track_height` is set to `true`
91
96
  }
92
97
  }
93
98
 
99
+ const max_retries_for_render = 10;
100
+ let try_count = 0;
94
101
  function pollForRender() {
95
102
  if (slideInner && typeof slideInner != "undefined") {
96
103
  init();
97
104
  } else {
98
105
  setTimeout(() => {
99
- pollForRender();
106
+ try_count++;
107
+ if (try_count < max_retries_for_render) {
108
+ pollForRender();
109
+ }
100
110
  }, 50);
101
111
  }
102
112
  }
103
113
 
104
114
  function init() {
105
- setupResizeObserver();
106
115
  fireSizeChange();
107
116
  }
108
117
 
@@ -129,23 +138,7 @@ This will work only when `track_height` is set to `true`
129
138
 
130
139
  const debouncedFireSizeChange = debounce(fireSizeChange, 150, false);
131
140
 
132
- const setupResizeObserver = () => {
133
- if (hasResizeObserver) {
134
- if (!slideInner || typeof slideInner == "undefined") {
135
- pollForRender();
136
- } else {
137
- Observer = new ResizeObserver(() => {
138
- debouncedFireSizeChange();
139
- });
140
- Observer.observe(slideInner);
141
- }
142
- }
143
- };
144
-
145
141
  onMount(() => {
146
142
  pollForRender();
147
- return () => {
148
- Observer && Observer.disconnect();
149
- };
150
143
  });
151
144
  </script>
@@ -0,0 +1,24 @@
1
+ export const hasResizeObserver = typeof window.ResizeObserver != "undefined";
2
+
3
+ /**
4
+ * Usage: `<div use:resizeObserver on:resize={resizeHandler}>`
5
+ * @param {HTMLElement} node
6
+ * @returns {Object}
7
+ */
8
+ export function resizeObserver(node) {
9
+ let ro;
10
+ if (hasResizeObserver) {
11
+ ro = new ResizeObserver(() => {
12
+ const e = new CustomEvent("resize", { bubbles: false });
13
+ node.dispatchEvent(e);
14
+ });
15
+
16
+ ro.observe(node);
17
+ }
18
+
19
+ return {
20
+ destroy() {
21
+ hasResizeObserver && ro.disconnect();
22
+ },
23
+ };
24
+ }