@svar-ui/svelte-toolbar 2.5.2 → 2.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svar-ui/svelte-toolbar",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "Svelte toolbar component. Lightweight, customizable, easy-to-use.",
5
5
  "productTag": "toolbar",
6
6
  "productTrial": false,
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "homepage": "https://svar.dev/svelte/core/",
35
35
  "dependencies": {
36
- "@svar-ui/svelte-core": "2.5.1",
37
- "@svar-ui/lib-dom": "0.12.1"
36
+ "@svar-ui/svelte-core": "2.6.0",
37
+ "@svar-ui/lib-dom": "0.13.1"
38
38
  },
39
39
  "files": [
40
40
  "src",
@@ -1,7 +1,17 @@
1
1
  <script>
2
2
  import { Button } from "@svar-ui/svelte-core";
3
3
 
4
- let { icon, text = "", css, type, disabled, menu, onclick } = $props();
4
+ let {
5
+ icon,
6
+ title,
7
+ text = "",
8
+ tooltip,
9
+ css,
10
+ type,
11
+ disabled,
12
+ menu,
13
+ onclick,
14
+ } = $props();
5
15
  </script>
6
16
 
7
17
  {#if menu}
@@ -12,7 +22,7 @@
12
22
  {text}
13
23
  </div>
14
24
  {:else}
15
- <Button {icon} {type} {css} {text} {disabled} {onclick} />
25
+ <Button {icon} {type} {css} {title} {text} {tooltip} {disabled} {onclick} />
16
26
  {/if}
17
27
 
18
28
  <style>
@@ -1,7 +1,8 @@
1
1
  <script>
2
2
  import { Button } from "@svar-ui/svelte-core";
3
3
 
4
- let { icon, text, css, type, disabled, menu, onclick } = $props();
4
+ let { icon, title, text, tooltip, css, type, disabled, menu, onclick } =
5
+ $props();
5
6
  </script>
6
7
 
7
8
  {#if menu}
@@ -12,7 +13,7 @@
12
13
  {text}
13
14
  </div>
14
15
  {:else}
15
- <Button {icon} {type} {css} title={text} {disabled} {onclick} />
16
+ <Button {icon} {type} {css} {disabled} {title} {tooltip} {onclick} />
16
17
  {/if}
17
18
 
18
19
  <style>
@@ -3,6 +3,7 @@
3
3
 
4
4
  import BarComponent from "./BarComponent.svelte";
5
5
  import { Dropdown } from "@svar-ui/svelte-core";
6
+ import { collapseSeparators } from "../helpers";
6
7
 
7
8
  let { item, values = null, menu = false, onchange, onclick } = $props();
8
9
 
@@ -49,7 +50,7 @@
49
50
  {/if}
50
51
  {:else}
51
52
  <div class="wx-tb-body">
52
- {#each item.items as sub}
53
+ {#each collapseSeparators(item.items) as sub}
53
54
  {#if sub.items}
54
55
  <Group item={sub} {values} onclick={onClick} {onchange} />
55
56
  {:else}
@@ -3,8 +3,8 @@
3
3
  import Group from "./Group.svelte";
4
4
  import BarComponent from "./BarComponent.svelte";
5
5
 
6
- import { uid } from "@svar-ui/lib-dom";
7
6
  import { onMount, tick } from "svelte";
7
+ import { normalizeToolbarItems } from "../helpers";
8
8
 
9
9
  let {
10
10
  items = $bindable([]),
@@ -26,78 +26,28 @@
26
26
  }
27
27
 
28
28
  let div = null;
29
- let lastToolbarState = -1;
30
29
  let menuItems = $state([]);
31
30
 
32
- function processOverflow() {
33
- if (overflow === "wrap") return;
34
-
35
- const visibleWidth = div.clientWidth;
36
- const fullWidth = div.scrollWidth;
37
- const needMenu = fullWidth > visibleWidth;
38
-
39
- if (needMenu) {
40
- if (overflow === "collapse") return collapseGroups(visibleWidth);
41
- // we need to decide how many nodes need to be
42
- const nodes = div.children;
43
- let sum = 0;
44
-
45
- for (let i = 0; i < items.length; i++) {
46
- sum += nodes[i].clientWidth;
47
- if (items[i].comp == "separator") sum += 8;
48
- if (sum > visibleWidth - 40) {
49
- // skip updates, as visibility state was not changed
50
- if (lastToolbarState === i) return;
51
- lastToolbarState = i;
52
-
53
- // we need to hide nodes[i] and all next nodes
54
- menuItems = [];
55
- for (let j = i; j < items.length; j++) {
56
- menuItems.push(items[j]);
57
- nodes[j].style.visibility = "hidden";
58
- }
59
- // hide the ending separator
60
- if (i > 0 && items[i - 1].comp == "separator") {
61
- nodes[i - 1].style.visibility = "hidden";
62
- }
63
- break;
64
- }
65
- nodes[i].style.visibility = "";
66
- }
67
- } else {
68
- const freeWidth = visibleWidth - getTotalWidth();
69
- if (freeWidth <= 0) return;
70
- if (overflow === "collapse") return expandGroups(freeWidth);
71
-
72
- if (menuItems.length) {
73
- lastToolbarState = null;
74
- const nodes = div.children;
75
- for (let i = 0; i < items.length; i++) {
76
- nodes[i].style.visibility = "";
77
- }
78
- menuItems = [];
79
- }
80
- }
81
- }
31
+ const barItems = $derived(normalizeToolbarItems(items));
82
32
 
83
33
  function getTotalWidth() {
84
34
  const nodes = div.children;
85
35
  let sum = 0;
86
- for (let i = 0; i < items.length; i++) {
87
- if (items[i].comp != "spacer") {
36
+ for (let i = 0; i < barItems.length; i++) {
37
+ if (barItems[i].comp != "spacer") {
88
38
  sum += nodes[i]?.clientWidth || 0;
89
- if (items[i].comp == "separator") sum += 8;
39
+ if (barItems[i].comp == "separator") sum += 8;
90
40
  }
91
41
  }
92
42
  return sum;
93
43
  }
94
44
 
95
45
  function collapseGroups() {
96
- for (let i = items.length - 1; i >= 0; i--) {
46
+ for (let i = barItems.length - 1; i >= 0; i--) {
97
47
  // close rightmost open group
98
- if (items[i].items && !items[i].collapsed) {
99
- items[i].collapsed = true;
100
- items[i].$width = div.children[i].offsetWidth;
48
+ if (barItems[i].items && !barItems[i].collapsed) {
49
+ barItems[i].collapsed = true;
50
+ barItems[i].$width = div.children[i].offsetWidth;
101
51
  // check after dom update, maybe we need to close more
102
52
  tick().then(processOverflow);
103
53
 
@@ -109,52 +59,98 @@
109
59
  }
110
60
 
111
61
  function expandGroups(freeSpace) {
112
- for (let i = 0; i < items.length; i++) {
113
- // close leftmost closed group, that was closed previously
114
- if (items[i].collapsed && items[i].$width) {
62
+ for (let i = 0; i < barItems.length; i++) {
63
+ // open leftmost closed group that was collapsed by overflow
64
+ if (barItems[i].collapsed && barItems[i].$width) {
115
65
  // check if group can fit in free space
116
66
  if (
117
- items[i].$width - div.children[i].offsetWidth <
67
+ barItems[i].$width - div.children[i].offsetWidth <
118
68
  freeSpace + 10
119
69
  ) {
120
- items[i].collapsed = false;
70
+ barItems[i].collapsed = false;
71
+ // items are not deep reactive, so we need to trigger the update
72
+ items = [...items];
121
73
  // check after dom update, maybe we can open one more
122
74
  tick().then(processOverflow);
123
75
  }
124
-
125
- items = [...items];
126
76
  return;
127
77
  }
128
78
  }
129
79
  }
130
80
 
131
- // rebuild toolbar items, inject group nodes
132
- function normalize(items) {
133
- items.forEach(item => {
134
- if (!item.id) item.id = uid();
135
- });
136
- return items;
81
+ function processOverflow() {
82
+ if (overflow === "wrap") return;
83
+
84
+ const nodes = div.children;
85
+ // restore all items so widths can be measured
86
+ for (let i = 0; i < barItems.length; i++) {
87
+ if (nodes[i]) nodes[i].style.display = "";
88
+ }
89
+
90
+ const visibleWidth = div.clientWidth;
91
+ const fullWidth = div.scrollWidth;
92
+ const needMenu = fullWidth > visibleWidth;
93
+
94
+ if (needMenu) {
95
+ if (overflow === "collapse") return collapseGroups();
96
+
97
+ // pinned items always stay visible
98
+ let pinnedWidth = 0;
99
+ for (let i = 0; i < barItems.length; i++) {
100
+ if (barItems[i].pinned) pinnedWidth += nodes[i].clientWidth;
101
+ }
102
+
103
+ let sum = 0;
104
+ for (let i = 0; i < barItems.length; i++) {
105
+ if (barItems[i].pinned) continue;
106
+ sum += nodes[i].clientWidth;
107
+ if (barItems[i].comp == "separator") sum += 8;
108
+ if (sum > visibleWidth - 40 - pinnedWidth) {
109
+ // we need to hide nodes[i] and all next non-pinned nodes
110
+ menuItems = [];
111
+ for (let j = i; j < barItems.length; j++) {
112
+ if (barItems[j].pinned) continue;
113
+ menuItems.push(barItems[j]);
114
+ nodes[j].style.display = "none";
115
+ }
116
+ // hide the ending separator
117
+ if (
118
+ i > 0 &&
119
+ barItems[i - 1].comp == "separator" &&
120
+ !barItems[i - 1].pinned
121
+ ) {
122
+ nodes[i - 1].style.display = "none";
123
+ }
124
+ break;
125
+ }
126
+ }
127
+ } else {
128
+ const freeWidth = visibleWidth - getTotalWidth();
129
+ if (freeWidth <= 0) return;
130
+ if (overflow === "collapse") return expandGroups(freeWidth);
131
+
132
+ if (menuItems.length) menuItems = [];
133
+ }
137
134
  }
138
135
 
139
136
  onMount(() => {
140
- const resizeObserver = new ResizeObserver(() => processOverflow(div));
137
+ const resizeObserver = new ResizeObserver(() => processOverflow());
141
138
  resizeObserver.observe(div);
142
139
 
143
140
  return () => {
144
- if (resizeObserver) resizeObserver.unobserve(div);
141
+ resizeObserver.unobserve(div);
145
142
  };
146
143
  });
147
-
148
- const visibleItems = $derived(normalize(items));
149
144
  </script>
150
145
 
151
146
  <div
152
147
  class="wx-toolbar {css}"
153
148
  class:wx-wrap={overflow === "wrap"}
154
149
  class:wx-column={layout == "column"}
150
+ class:wx-has-menu={menuItems.length}
155
151
  bind:this={div}
156
152
  >
157
- {#each visibleItems as item}
153
+ {#each barItems as item}
158
154
  {#if item.items}
159
155
  <Group {item} {values} {onclick} onchange={handleChange} />
160
156
  {:else}
@@ -180,6 +176,10 @@
180
176
  align-items: stretch;
181
177
  padding: 4px;
182
178
  position: relative;
179
+ min-height: 48px;
180
+ }
181
+ .wx-toolbar.wx-has-menu {
182
+ padding-right: 48px;
183
183
  }
184
184
  .wx-toolbar.wx-wrap {
185
185
  flex-wrap: wrap;
package/src/helpers.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { uid } from "@svar-ui/lib-dom";
2
+
1
3
  const handlers = {};
2
4
  export function getItemHandler(type) {
3
5
  return handlers[type] || type;
@@ -5,3 +7,26 @@ export function getItemHandler(type) {
5
7
  export function registerToolbarItem(type, handler) {
6
8
  handlers[type] = handler;
7
9
  }
10
+
11
+ // Assign stable ids and collapse consecutive separators
12
+ export function normalizeToolbarItems(items) {
13
+ if (!items?.length) return [];
14
+ for (const item of items) {
15
+ if (!item.id) item.id = uid();
16
+ }
17
+ return collapseSeparators(items);
18
+ }
19
+
20
+ // Keep the first of consecutive separator
21
+ export function collapseSeparators(items) {
22
+ if (!items?.length) return items || [];
23
+ const out = [];
24
+ let prevWasSeparator = false;
25
+ for (const item of items) {
26
+ const isSeparator = item.comp === "separator";
27
+ if (isSeparator && prevWasSeparator) continue;
28
+ out.push(item);
29
+ prevWasSeparator = isSeparator;
30
+ }
31
+ return out;
32
+ }
package/types/index.d.ts CHANGED
@@ -5,14 +5,19 @@ export interface IToolbarItem {
5
5
  comp?: string | Component<any>;
6
6
  icon?: string;
7
7
  css?: string;
8
+ type?: string;
9
+ title?: string;
8
10
  text?: string;
11
+ tooltip?: string;
9
12
  menuText?: string;
10
13
  key?: string;
14
+ disabled?: boolean;
11
15
  spacer?: boolean;
12
16
  collapsed?: boolean;
13
17
  handler?: (item: IToolbarItem, value?: any) => void;
14
18
  layout?: "column";
15
19
  items?: IToolbarItem[];
20
+ pinned?: boolean;
16
21
  [key: string]: any;
17
22
  }
18
23
 
package/whatsnew.md CHANGED
@@ -1,8 +1,19 @@
1
- ## 2.5.2
1
+ ## 2.6.0
2
+
3
+ ### New features
4
+
5
+ - Tooltip for toolbar items
6
+
7
+ ### Fixes
8
+
9
+ - Duplicate separators are auto removed
10
+
11
+ ## 2.5.3
2
12
 
3
13
  ### Fixes
4
14
 
5
15
  - Impossible to arrange first level items as a column
16
+ - Impossible to fix certain items as always visible
6
17
 
7
18
  ## 2.5.1
8
19