@txstate-mws/svelte-components 1.6.10 → 1.6.12

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.
@@ -7,15 +7,15 @@ class ElementQueries {
7
7
  #subscribers = 0;
8
8
  #stores = new Map();
9
9
  #processwidths() {
10
- for (let i = 0; i < this.#watchlist.length; i++) {
10
+ for (const layer of this.#watchlist) {
11
11
  const widths = [];
12
12
  const attrs = [];
13
- for (let j = 0; j < this.#watchlist[i].length; j++) {
14
- widths.push(this.#watchlist[i][j].offsetWidth);
15
- attrs.push(this.#watchlist[i][j].getAttribute('data-eq'));
13
+ for (const el of layer) {
14
+ widths.push(el.offsetWidth);
15
+ attrs.push(el.getAttribute('data-eq'));
16
16
  }
17
- for (let j = 0; j < this.#watchlist[i].length; j++) {
18
- const el = this.#watchlist[i][j];
17
+ for (let j = 0; j < layer.length; j++) {
18
+ const el = layer[j];
19
19
  const w = widths[j];
20
20
  let attrstr = '';
21
21
  let finalw = 1600;
@@ -62,10 +62,10 @@ class ElementQueries {
62
62
  #gotoanchor() {
63
63
  // since we are dramatically resizing, we need to scroll to the proper place for an
64
64
  // anchor tag
65
- const m = document.location.hash.match(/^#([a-z][\w:.-]*)$/i);
65
+ const m = document.location.hash.match(/^#([a-z][a-zA-Z0-9_:.\-]*)$/iv);
66
66
  if (m) {
67
67
  const id = m[1];
68
- requestAnimationFrame(() => { (document.getElementById(id) || document.querySelector(`[name="${id}"]`))?.scrollIntoView(true); });
68
+ requestAnimationFrame(() => { (document.getElementById(id) ?? document.querySelector(`[name="${id}"]`))?.scrollIntoView(true); });
69
69
  }
70
70
  }
71
71
  fullresync() {
@@ -100,7 +100,7 @@ class ElementQueries {
100
100
  // instead of faithfully subscribing and unsubscribing, so we'll add a subscriber that can't be
101
101
  // decremented and it will stay activated forever
102
102
  activate() {
103
- this.#subscribers++;
103
+ this.#subscribers += 1;
104
104
  this.#activateRaw();
105
105
  }
106
106
  #deactivate() {
@@ -111,14 +111,15 @@ class ElementQueries {
111
111
  this.#hasInit = false;
112
112
  }
113
113
  subscribe(el, store) {
114
- this.#subscribers++;
114
+ this.#subscribers += 1;
115
115
  if (store)
116
116
  this.#stores.set(el, store);
117
117
  else
118
118
  this.#stores.delete(el);
119
119
  this.#activateRaw();
120
120
  return () => {
121
- if (--this.#subscribers === 0)
121
+ this.#subscribers -= 1;
122
+ if (this.#subscribers === 0)
122
123
  this.#deactivate();
123
124
  };
124
125
  }
@@ -1,17 +1,74 @@
1
1
  import type { SettableSubject } from '@txstate-mws/svelte-store';
2
- export type GlueAlignOpts = 'auto' | 'bottomleft' | 'bottomright' | 'topleft' | 'topright';
2
+ export type GlueAlignOpts = 'auto' | 'bottomleft' | 'bottomright' | 'topleft' | 'topright' | 'top' | 'bottom' | 'left' | 'right' | 'autoleft' | 'autoright' | 'topauto' | 'bottomauto' | 'automiddle' | 'middleauto' | 'middle';
3
3
  export interface GlueArgs<T extends GlueAlignStore = GlueAlignStore> {
4
- target: HTMLElement;
4
+ /**
5
+ * The HTML element to which the element should be glued. The target remains where
6
+ * it is; the element you placed the glue action on will become position: fixed and
7
+ * move to align with the target.
8
+ */
9
+ target?: HTMLElement;
10
+ /**
11
+ * How to align the element relative to the target.
12
+ *
13
+ * Corner alignments:
14
+ * - `topleft`: element's bottom-left corner meets target's top-left corner
15
+ * - `topright`: element's bottom-right corner meets target's top-right corner
16
+ * - `bottomleft`: element's top-left corner meets target's bottom-left corner
17
+ * - `bottomright`: element's top-right corner meets target's bottom-right corner
18
+ *
19
+ * Side alignments (centered on the midpoint of the side):
20
+ * - `top`: element centered above target
21
+ * - `bottom`: element centered below target
22
+ * - `left`: element centered to the left of target
23
+ * - `right`: element centered to the right of target
24
+ *
25
+ * Auto alignments (choose based on available viewport space):
26
+ * - `auto`: choose between all four corners
27
+ * - `autoleft`: choose between `topleft` and `bottomleft`
28
+ * - `autoright`: choose between `topright` and `bottomright`
29
+ * - `topauto`: choose between `topleft` and `topright`
30
+ * - `bottomauto`: choose between `bottomleft` and `bottomright`
31
+ * - `automiddle`: choose between `top` and `bottom`
32
+ * - `middleauto`: choose between `left` and `right`
33
+ * - `middle`: choose between `top`, `bottom`, `left`, and `right`, or with `cover` set to true, simply center over the target
34
+ *
35
+ * When `cover` is false (default), the element is placed adjacent to the target.
36
+ * When `cover` is true, the element overlaps the target from the aligned side.
37
+ *
38
+ * @default 'auto'
39
+ */
5
40
  align?: GlueAlignOpts;
41
+ /**
42
+ * Adjusts the alignment setting behavior such that now the element will cover the target.
43
+ */
6
44
  cover?: boolean;
45
+ /**
46
+ * A pixel margin between the element and the target. When used with `cover`, will reveal
47
+ * a gap's worth of pixels of the target.
48
+ */
7
49
  gap?: number;
50
+ /**
51
+ * When true, increases the parent element's `min-height` to account for
52
+ * the positioned element's size, preventing the parent from collapsing
53
+ * when its child becomes `position: fixed`. Ignored if the parent has
54
+ * `overflow-y: auto`. The original `min-height` is restored on destroy.
55
+ *
56
+ * @default false
57
+ */
8
58
  adjustparentheight?: boolean;
59
+ /**
60
+ * The store that contains information about the alignment of the element. With align
61
+ * set to `auto`, this will tell you quickly which alignment is currently active.
62
+ */
9
63
  store?: SettableSubject<T>;
10
64
  }
11
65
  export interface GlueAlignStore {
12
- valign: 'bottom' | 'top';
13
- halign: 'left' | 'right';
66
+ valign: 'bottom' | 'top' | 'center';
67
+ halign: 'left' | 'right' | 'center';
14
68
  }
69
+ /**
70
+ * Make this element `position: fixed` and move it around to align with the target element.
71
+ */
15
72
  export declare function glue(el: HTMLElement, { target, align, cover, gap, adjustparentheight, store }: GlueArgs): {
16
73
  update({ target: utarget, align: ualign, cover: ucover, gap: ugap, store: ustore }: GlueArgs): void;
17
74
  destroy(): void;
@@ -1,22 +1,23 @@
1
1
  import { watchForMutations } from '../util/index.js';
2
2
  import { equal } from 'txstate-utils';
3
+ /**
4
+ * Make this element `position: fixed` and move it around to align with the target element.
5
+ */
3
6
  export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjustparentheight = false, store }) {
4
7
  let halign;
5
8
  let valign;
6
- const parent = el.offsetParent;
9
+ const parent = el.offsetParent instanceof HTMLElement ? el.offsetParent : undefined;
7
10
  const formerMinHeight = parent?.style.minHeight;
8
11
  if (target)
9
12
  el.style.position = 'fixed';
10
- function adjustParentHeight() {
13
+ function adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY) {
11
14
  if (!adjustparentheight)
12
15
  return;
13
- if (parent?.style.overflowY === 'auto')
16
+ if (parentOverflowY === 'auto')
14
17
  return;
15
- const minHeight = el.offsetTop + el.offsetHeight;
16
- if (minHeight > (parent?.clientHeight ?? 0)) {
17
- requestAnimationFrame(() => {
18
- parent.style.minHeight = String(minHeight) + 'px';
19
- });
18
+ const minHeight = elOffsetTop + elOffsetHeight;
19
+ if (minHeight > parentClientHeight) {
20
+ parent.style.minHeight = String(minHeight) + 'px';
20
21
  }
21
22
  }
22
23
  let lastrect;
@@ -34,13 +35,55 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
34
35
  const rect = { left: tmpRect.left - fixedRect.left, right: document.documentElement.clientWidth - tmpRect.right - fixedRect.right, top: tmpRect.top - fixedRect.top, bottom: document.documentElement.clientHeight - tmpRect.bottom - fixedRect.bottom, width: tmpRect.width, height: tmpRect.height };
35
36
  if (equal(rect, lastrect))
36
37
  return;
38
+ lastrect = rect;
37
39
  let autoalign = align;
40
+ const leftright = rect.right > rect.left ? 'left' : 'right';
41
+ const topbottom = rect.bottom > rect.top ? 'bottom' : 'top';
38
42
  if (align === 'auto') {
39
- const leftright = rect.right > rect.left ? 'left' : 'right';
40
- const topbottom = rect.bottom > rect.top ? 'bottom' : 'top';
41
43
  autoalign = topbottom + leftright;
42
44
  }
45
+ else if (align === 'autoleft') {
46
+ autoalign = topbottom + 'left';
47
+ }
48
+ else if (align === 'autoright') {
49
+ autoalign = topbottom + 'right';
50
+ }
51
+ else if (align === 'topauto') {
52
+ autoalign = 'top' + leftright;
53
+ }
54
+ else if (align === 'bottomauto') {
55
+ autoalign = 'bottom' + leftright;
56
+ }
57
+ else if (align === 'automiddle') {
58
+ autoalign = topbottom;
59
+ }
60
+ else if (align === 'middleauto') {
61
+ autoalign = leftright;
62
+ }
63
+ else if (align === 'middle') {
64
+ if (cover) {
65
+ autoalign = 'middle';
66
+ }
67
+ else {
68
+ const maxSpace = Math.max(rect.top, rect.bottom, rect.left, rect.right);
69
+ if (maxSpace === rect.bottom)
70
+ autoalign = 'bottom';
71
+ else if (maxSpace === rect.top)
72
+ autoalign = 'top';
73
+ else if (maxSpace === rect.right)
74
+ autoalign = 'right';
75
+ else
76
+ autoalign = 'left';
77
+ }
78
+ }
43
79
  const targetHeight = rect.height;
80
+ const elRect = el.getBoundingClientRect();
81
+ const elWidth = elRect.width;
82
+ const elHeight = elRect.height;
83
+ const elOffsetTop = el.offsetTop;
84
+ const elOffsetHeight = el.offsetHeight;
85
+ const parentClientHeight = parent?.clientHeight ?? 0;
86
+ const parentOverflowY = parent?.style.overflowY;
44
87
  if (autoalign === 'bottomleft') {
45
88
  cancelAnimationFrame(timer);
46
89
  timer = requestAnimationFrame(() => {
@@ -48,7 +91,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
48
91
  el.style.left = `${rect.left}px`;
49
92
  el.style.removeProperty('bottom');
50
93
  el.style.removeProperty('right');
51
- adjustParentHeight();
94
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
52
95
  });
53
96
  valign = 'bottom';
54
97
  halign = 'left';
@@ -60,7 +103,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
60
103
  el.style.removeProperty('left');
61
104
  el.style.removeProperty('bottom');
62
105
  el.style.right = `${rect.right}px`;
63
- adjustParentHeight();
106
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
64
107
  });
65
108
  valign = 'bottom';
66
109
  halign = 'right';
@@ -72,7 +115,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
72
115
  el.style.left = `${rect.left}px`;
73
116
  el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
74
117
  el.style.removeProperty('right');
75
- adjustParentHeight();
118
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
76
119
  });
77
120
  valign = 'top';
78
121
  halign = 'left';
@@ -84,14 +127,85 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
84
127
  el.style.removeProperty('left');
85
128
  el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
86
129
  el.style.right = `${rect.right}px`;
87
- adjustParentHeight();
130
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
88
131
  });
89
132
  valign = 'top';
90
133
  halign = 'right';
91
134
  }
135
+ else if (autoalign === 'top') {
136
+ cancelAnimationFrame(timer);
137
+ timer = requestAnimationFrame(() => {
138
+ el.style.removeProperty('top');
139
+ el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
140
+ el.style.bottom = `${rect.bottom + (cover ? 0 : targetHeight) + gap}px`;
141
+ el.style.removeProperty('right');
142
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
143
+ });
144
+ valign = 'top';
145
+ halign = 'center';
146
+ }
147
+ else if (autoalign === 'bottom') {
148
+ cancelAnimationFrame(timer);
149
+ timer = requestAnimationFrame(() => {
150
+ el.style.top = `${rect.top + (cover ? 0 : targetHeight) + gap}px`;
151
+ el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
152
+ el.style.removeProperty('bottom');
153
+ el.style.removeProperty('right');
154
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
155
+ });
156
+ valign = 'bottom';
157
+ halign = 'center';
158
+ }
159
+ else if (autoalign === 'left') {
160
+ cancelAnimationFrame(timer);
161
+ timer = requestAnimationFrame(() => {
162
+ el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
163
+ el.style.removeProperty('bottom');
164
+ if (cover) {
165
+ el.style.left = `${rect.left + gap}px`;
166
+ el.style.removeProperty('right');
167
+ }
168
+ else {
169
+ el.style.removeProperty('left');
170
+ el.style.right = `${rect.right + rect.width + gap}px`;
171
+ }
172
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
173
+ });
174
+ valign = 'center';
175
+ halign = 'left';
176
+ }
177
+ else if (autoalign === 'right') {
178
+ cancelAnimationFrame(timer);
179
+ timer = requestAnimationFrame(() => {
180
+ el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
181
+ el.style.removeProperty('bottom');
182
+ if (cover) {
183
+ el.style.removeProperty('left');
184
+ el.style.right = `${rect.right + gap}px`;
185
+ }
186
+ else {
187
+ el.style.left = `${rect.left + rect.width + gap}px`;
188
+ el.style.removeProperty('right');
189
+ }
190
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
191
+ });
192
+ valign = 'center';
193
+ halign = 'right';
194
+ }
195
+ else if (autoalign === 'middle') {
196
+ cancelAnimationFrame(timer);
197
+ timer = requestAnimationFrame(() => {
198
+ el.style.top = `${rect.top + rect.height / 2 - elHeight / 2}px`;
199
+ el.style.left = `${rect.left + rect.width / 2 - elWidth / 2}px`;
200
+ el.style.removeProperty('bottom');
201
+ el.style.removeProperty('right');
202
+ adjustParentHeight(elOffsetTop, elOffsetHeight, parentClientHeight, parentOverflowY);
203
+ });
204
+ valign = 'center';
205
+ halign = 'center';
206
+ }
92
207
  store?.update(v => ({ ...v, valign, halign }));
93
208
  }
94
- // eslint-disable-next-line @typescript-eslint/unbound-method
95
209
  const { destroy: watchDestroy } = watchForMutations(reposition);
96
210
  document.addEventListener('scroll', reposition, { capture: true });
97
211
  return {
@@ -116,7 +230,7 @@ export function glue(el, { target, align = 'auto', cover = false, gap = 0, adjus
116
230
  destroy() {
117
231
  watchDestroy();
118
232
  document.removeEventListener('scroll', reposition);
119
- if (adjustparentheight) {
233
+ if (adjustparentheight && parent) {
120
234
  if (formerMinHeight)
121
235
  parent.style.minHeight = formerMinHeight;
122
236
  parent.style.removeProperty('min-height');
@@ -16,11 +16,11 @@ export function offset(el, config) {
16
16
  el.dispatchEvent(new CustomEvent('offset', { detail: current }));
17
17
  }
18
18
  let resolvedchange = config?.debounce ? debounced(lookforoffsetchange, config.debounce) : lookforoffsetchange;
19
- const offsetContainer = !config?.container ? document.body : (config.container === 'nearest' ? el.offsetParent ?? document.body : config.container);
19
+ const offsetContainer = !config?.container ? document.body : (config.container === 'nearest' ? el.offsetParent instanceof HTMLElement ? el.offsetParent : document.body : config.container);
20
20
  const { destroy, update } = watchForPositionChangeInContainer(el, offsetContainer, resolvedchange);
21
21
  return {
22
22
  update(newConfig) {
23
- const offsetContainer = !config?.container ? document.body : (config.container === 'nearest' ? el.offsetParent ?? document.body : config.container);
23
+ const offsetContainer = !config?.container ? document.body : (config.container === 'nearest' ? el.offsetParent instanceof HTMLElement ? el.offsetParent : document.body : config.container);
24
24
  if (newConfig?.debounce === true)
25
25
  newConfig.debounce = 100;
26
26
  if (newConfig?.store !== config?.store) {
@@ -6,5 +6,5 @@ export type SvelteHTMLActionType<P = any> = (node: HTMLElement, params?: P) => S
6
6
  export type HTMLActionEntry<P = any> = SvelteHTMLActionType<P> | [SvelteHTMLActionType<P>, P];
7
7
  export type SvelteSVGActionType<P = any> = (node: SVGElement, params?: P) => SvelteActionReturnType<P> | undefined;
8
8
  export type SVGActionEntry<P = any> = SvelteSVGActionType<P> | [SvelteSVGActionType<P>, P];
9
- export declare function passActions(node: HTMLElement, actions: HTMLActionEntry[]): SvelteActionReturnType<HTMLActionEntry[]>;
10
- export declare function passActions(node: SVGElement, actions: SVGActionEntry[]): SvelteActionReturnType<SVGActionEntry[]>;
9
+ export declare function passActions(node: HTMLElement, actions: HTMLActionEntry[] | undefined): SvelteActionReturnType<HTMLActionEntry[]>;
10
+ export declare function passActions(node: SVGElement, actions: SVGActionEntry[] | undefined): SvelteActionReturnType<SVGActionEntry[]>;
@@ -63,10 +63,10 @@ export function stickyfixed(el, config) {
63
63
  const cell = el.querySelector('th:last-child,td:last-child');
64
64
  if (cell) {
65
65
  const tstyle = window.getComputedStyle(table);
66
- extrawidth = tstyle.borderCollapse === 'collapse' ? parseInt(window.getComputedStyle(cell).borderRightWidth) : 0;
66
+ extrawidth = tstyle.borderCollapse === 'collapse' ? parseInt(window.getComputedStyle(cell).borderRightWidth, 10) : 0;
67
67
  }
68
68
  }
69
- const width = `${parseInt(window.getComputedStyle(spacer).width) + extrawidth}px`;
69
+ const width = `${parseInt(window.getComputedStyle(spacer).width, 10) + extrawidth}px`;
70
70
  elheight = spacer.offsetHeight;
71
71
  for (let i = 0; i < tds.length; i++)
72
72
  tds[i].style.width = widths[i];
@@ -126,7 +126,7 @@ export function stickyfixed(el, config) {
126
126
  lastOffsetParent = offsetParent;
127
127
  }
128
128
  const table = el.closest('table');
129
- const borderspacing = (table ? parseInt(window.getComputedStyle(table).borderSpacing) : 0);
129
+ const borderspacing = (table ? parseInt(window.getComputedStyle(table).borderSpacing, 10) : 0);
130
130
  scrollstart = offset[0].top + el.offsetTop;
131
131
  left = offset[0].left + (spacer.parentElement ? spacer.offsetLeft : el.offsetLeft);
132
132
  if (el.tagName === 'TR') {
@@ -22,7 +22,6 @@ export let menuClass = '';
22
22
  export let menuItemClass = '';
23
23
  export let menuItemHilitedClass = '';
24
24
  export let menuItemSelectedClass = '';
25
- export let menuDividerClass = '';
26
25
  const usemenurole = true;
27
26
  </script>
28
27
 
@@ -49,4 +48,4 @@ const usemenurole = true;
49
48
  {menuItemClass}
50
49
  {menuItemHilitedClass}
51
50
  {menuItemSelectedClass}
52
- {menuDividerClass}/>
51
+ />
@@ -1,11 +1,11 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import { Store } from '@txstate-mws/svelte-store';
3
3
  import type { GlueAlignOpts, GlueAlignStore } from '../actions';
4
- import type { PopupMenuTypes } from '../types';
4
+ import type { PopupMenuItem } from '../types';
5
5
  declare const __propDef: {
6
6
  props: {
7
7
  buttonelement: HTMLButtonElement;
8
- items?: PopupMenuTypes[];
8
+ items?: PopupMenuItem[];
9
9
  menushown?: boolean;
10
10
  value?: string | undefined;
11
11
  align?: GlueAlignOpts;
@@ -25,7 +25,6 @@ declare const __propDef: {
25
25
  menuItemClass?: string;
26
26
  menuItemHilitedClass?: string;
27
27
  menuItemSelectedClass?: string;
28
- menuDividerClass?: string;
29
28
  };
30
29
  events: {
31
30
  [evt: string]: CustomEvent<any>;
@@ -22,7 +22,8 @@ setContext(CARDLAYOUT, {
22
22
  blocks.splice(blocks.indexOf(block), 1);
23
23
  hardrecalc();
24
24
  });
25
- block.order = writable(defaultOrder++);
25
+ block.order = writable(defaultOrder);
26
+ defaultOrder += 1;
26
27
  block.linebreak = writable(false);
27
28
  const cols = savecolumns || Math.ceil(1024 / maxwidth);
28
29
  block.width = writable(`calc(${100.0 / cols}% - ${gutter * (cols - 1) / cols}px)`);
@@ -63,7 +64,7 @@ async function recalculate(realw) {
63
64
  const cycling = detectcycle(realw);
64
65
  if (columns !== savecolumns) {
65
66
  for (const block of blocks)
66
- block.width.set(`calc(${100.0 / columns}% - ${guttereach}px`);
67
+ block.width.set(`calc(${100.0 / columns}% - ${guttereach}px)`);
67
68
  await tick();
68
69
  }
69
70
  // collect all the card heights at this new column width
@@ -93,11 +94,10 @@ async function recalculate(realw) {
93
94
  tallestcol = colheight;
94
95
  tallestcolidx = colidx;
95
96
  }
96
- colidx++;
97
+ colidx += 1;
97
98
  colheight = 0;
98
99
  }
99
- if (!arrangement[colidx])
100
- arrangement[colidx] = [];
100
+ arrangement[colidx] ??= [];
101
101
  arrangement[colidx].push(block);
102
102
  colheight += block.height + gutter;
103
103
  }
@@ -117,7 +117,7 @@ async function recalculate(realw) {
117
117
  }
118
118
  else {
119
119
  optimal = [];
120
- const heights = Array.apply(null, Array(columns)).map((_) => 0); // initializes heights to an array of zeroes
120
+ const heights = [...Array(columns)].map((_) => 0); // initializes heights to an array of zeroes
121
121
  // begin sorting blocks into columns
122
122
  for (const block of blocks) {
123
123
  // find the column with the smallest current height
@@ -125,8 +125,7 @@ async function recalculate(realw) {
125
125
  // record the height we are adding to the chosen column
126
126
  heights[colidx] += block.height + gutter;
127
127
  // move the current card to the chosen column
128
- if (!optimal[colidx])
129
- optimal[colidx] = [];
128
+ optimal[colidx] ??= [];
130
129
  optimal[colidx].push(block);
131
130
  }
132
131
  }
@@ -136,7 +135,8 @@ async function recalculate(realw) {
136
135
  for (let i = 0; i < optimal.length; i++) {
137
136
  for (let j = 0; j < optimal[i].length; j++) {
138
137
  const block = optimal[i][j];
139
- block.order.set(order++);
138
+ block.order.set(order);
139
+ order += 1;
140
140
  block.linebreak.set(j === optimal[i].length - 1 && i < optimal.length - 1);
141
141
  }
142
142
  }
@@ -35,7 +35,6 @@ $: passthrough = (({ condition, component, a, span, element, ...passthrough }) =
35
35
  <svelte:component this={component} bind:element {...passthrough}><slot></slot></svelte:component>
36
36
  {:else}
37
37
  {#if a}
38
- <!-- svelte-ignore a11y-missing-attribute -->
39
38
  <a bind:this={element} {...passthrough}><slot></slot></a>
40
39
  {:else if span}
41
40
  <span bind:this={element} {...passthrough}><slot></slot></span>
@@ -3,7 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  [x: string]: any;
5
5
  condition: boolean | undefined;
6
- component?: any | undefined;
6
+ component?: any;
7
7
  a?: boolean | undefined;
8
8
  span?: boolean | undefined;
9
9
  element?: HTMLElement | undefined;
@@ -13,7 +13,7 @@ When they do, the escape event will be fired, allowing you to remove the dialog.
13
13
  When it goes away, they'll be trapped inside the previous `FocusLock`, and so on.
14
14
  -->
15
15
  <script context="module" lang="ts">export const FocusLockStack = [];
16
- const waitAtick = typeof requestAnimationFrame !== 'undefined' ? (resolve) => requestAnimationFrame(resolve) : resolve => resolve(0);
16
+ const waitAtick = typeof requestAnimationFrame !== 'undefined' ? (resolve) => requestAnimationFrame(resolve) : (resolve) => resolve(0);
17
17
  </script>
18
18
 
19
19
  <script lang="ts">import { onMount, onDestroy, createEventDispatcher } from 'svelte';
@@ -48,7 +48,7 @@ onMount(async () => {
48
48
  pause: () => { state = 'paused'; },
49
49
  unpause: () => { if (state === 'paused')
50
50
  state = 'active'; },
51
- update: () => dispatch('focuslockupdate')
51
+ update: () => { dispatch('focuslockupdate'); }
52
52
  });
53
53
  for (const entry of FocusLockStack)
54
54
  entry.update();
@@ -123,7 +123,7 @@ const windowclick = (e) => {
123
123
  };
124
124
  </script>
125
125
  <svelte:window on:mousedown={windowclick} />
126
- <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
126
+ <!-- svelte-ignore a11y_interactive_supports_focus -->
127
127
  <div class={className} role="alertdialog" aria-modal="true" on:click|stopPropagation on:mousedown|stopPropagation on:keydown|stopPropagation={keydown} on:focusin={focusin}>
128
128
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
129
129
  <div bind:this={abovelockelement} tabindex="0"></div>
@@ -19,7 +19,7 @@ declare const __propDef: {
19
19
  focusId?: string;
20
20
  };
21
21
  events: {
22
- click: MouseEvent;
22
+ click: PointerEvent;
23
23
  mousedown: MouseEvent;
24
24
  escape: CustomEvent<any>;
25
25
  focuslockupdate: CustomEvent<any>;
@@ -41,7 +41,6 @@ onMount(() => {
41
41
  });
42
42
  </script>
43
43
 
44
- <!-- svelte-ignore a11y-click-events-have-key-events -->
45
44
  <!-- svelte-ignore a11y-no-static-element-interactions -->
46
45
  <div use:portal={usePortal} class="modal-backdrop" style:--modal-z={stackPosition * 10 + 3000} class:opaque on:mousedown|stopPropagation|preventDefault={() => { escapable && endmodal() }}>
47
46
  <FocusLock bind:focusId class="modal-container {containerClass}" {includeselector} {escapable} on:escape {hidefocus} {hidefocuslabel} {returnfocusto} {initialfocus} on:focuslockupdate={onFocusLockUpdate}>
@@ -25,7 +25,7 @@ export let menuClass = '';
25
25
  export let menuItemClass = '';
26
26
  export let menuItemHilitedClass = '';
27
27
  export let inputClass = '';
28
- export let menuDividerClass = '';
28
+ export let menuCategoryClass = '';
29
29
  /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */
30
30
  export let maxSelections = 0;
31
31
  /** Consuming components may need to make decisions about what to display or return as options in the popup based
@@ -41,6 +41,13 @@ export let usePortal = undefined;
41
41
  export let descid = undefined;
42
42
  /** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */
43
43
  export let PopupMenu = DefaultPopupMenu;
44
+ /** Add a Delete All button to clear all selected items */
45
+ export let includeDeleteAll = false;
46
+ /** Show a confirmation message before clearing all selections */
47
+ export let confirmDelete = undefined;
48
+ /** A function that can be used to customize the labels on the selected item pills. Default is a function
49
+ * that takes a PopupMenuItem and returns its label, if available, or its value when no label is provided. */
50
+ export let selectedItemLabel = item => item.label || item.value;
44
51
  let menushown;
45
52
  let loading = false;
46
53
  let options = [];
@@ -51,9 +58,7 @@ let menuid;
51
58
  let announcement = '';
52
59
  const instructionid = randomid();
53
60
  const dispatch = createEventDispatcher();
54
- const optionsCache = new Cache(async (ipt) => {
55
- return await getOptions(ipt);
56
- }, { freshseconds: 5 });
61
+ const optionsCache = new Cache(async (ipt) => await getOptions(ipt), { freshseconds: 5 });
57
62
  const selectedStore = new SafeStore(selected);
58
63
  $: $selectedStore = selected;
59
64
  $: selectedSet = new Set($selectedStore.map(s => s.value));
@@ -135,8 +140,24 @@ function inputfocus() {
135
140
  }
136
141
  function inputblur() {
137
142
  active = false;
143
+ hilitedpill = undefined;
138
144
  dispatch('blur');
139
145
  }
146
+ function removeAllSelections() {
147
+ selected = [];
148
+ dispatch('change', selected);
149
+ }
150
+ function confirmRemoveAllSelections() {
151
+ if (confirmDelete) {
152
+ // eslint-disable-next-line no-alert -- just doing something super simple here
153
+ if (window.confirm(confirmDelete)) {
154
+ removeAllSelections();
155
+ }
156
+ }
157
+ else {
158
+ removeAllSelections();
159
+ }
160
+ }
140
161
  let popuphilited;
141
162
  $: if (popuphilited != null)
142
163
  hilitedpill = undefined;
@@ -156,11 +177,18 @@ let gap = 0;
156
177
  onMount(() => {
157
178
  const ul = inputelement.closest('.multiselect-selected');
158
179
  if (ul != null)
159
- gap = ((ul.offsetHeight - ul.clientHeight) / 2) + parseInt(window.getComputedStyle(ul).paddingBottom);
180
+ gap = ((ul.offsetHeight - ul.clientHeight) / 2) + parseInt(window.getComputedStyle(ul).paddingBottom, 10);
160
181
  });
161
182
  </script>
162
183
 
163
184
  <div class="multiselect" class:disabled role="combobox" aria-controls={menushown ? menuid : undefined} aria-expanded={menushown}>
185
+ {#if includeDeleteAll && selected.length}
186
+ <div class="delete-all-container">
187
+ <button on:click|preventDefault|stopPropagation={confirmRemoveAllSelections}>
188
+ <slot name="deleteall">Delete All <ScreenReaderOnly>{selected.length} items</ScreenReaderOnly></slot>
189
+ </button>
190
+ </div>
191
+ {/if}
164
192
  {#if selected.length}
165
193
  <div class="multiselect-selected" role="listbox" aria-label="selected options">
166
194
  {#each selected as option, i (option.value)}
@@ -168,14 +196,13 @@ onMount(() => {
168
196
  <div id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
169
197
  on:click|preventDefault|stopPropagation={() => { !disabled && removeSelection(option, i, 1) }} on:mousedown={e => { disabled && e.preventDefault() }}
170
198
  aria-selected="true" aria-posinset={i + 1} aria-setsize={selected.length}>
171
- {option.label || option.value}
199
+ {selectedItemLabel(option)}
172
200
  <i aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="currentColor" d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128L50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z"/></svg></i>
173
201
  <ScreenReaderOnly>, click to deselect</ScreenReaderOnly>
174
202
  </div>
175
203
  {/each}
176
204
  </div>
177
205
  {/if}
178
- <!-- svelte-ignore a11y-role-has-required-aria-props -->
179
206
  <input type="text" class={inputClass} {id} {name} {disabled} {placeholder}
180
207
  bind:this={inputelement} on:input={reactToInput} on:blur={inputblur}
181
208
  on:focus={inputfocus} on:keydown={inputkeydown}
@@ -188,7 +215,7 @@ onMount(() => {
188
215
 
189
216
  <svelte:component this={PopupMenu} bind:menushown bind:menuid bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft'
190
217
  {usePortal} {loading} {emptyText} {gap}
191
- {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuDividerClass}
218
+ {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} {menuCategoryClass}
192
219
  items={options} buttonelement={inputelement}
193
220
  on:change={addSelection}/>
194
221
 
@@ -257,4 +284,9 @@ onMount(() => {
257
284
  border: var(--multiselect-pill-selected-border, 1px solid transparent);
258
285
  color: var(--multiselect-pill-selected-text, white);
259
286
  }
287
+ .delete-all-container {
288
+ width: 100%;
289
+ display: flex;
290
+ justify-content: flex-end;
291
+ }
260
292
  </style>
@@ -1,13 +1,13 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import DefaultPopupMenu from './PopupMenu.svelte';
3
- import type { PopupMenuItem, PopupMenuTypes } from '../types';
3
+ import type { PopupMenuItem } from '../types';
4
4
  declare const __propDef: {
5
5
  props: {
6
6
  name: string;
7
7
  /**
8
8
  * Function to pass to the component that tells it how to use the text in the text input to determine what
9
9
  * `PopupMenuItem[]` should be displayed in the `PopupMenu`. Items already 'selected' from the popup menu will
10
- * be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<PopupMenuTypes[]> | PopupMenuTypes[];
10
+ * be tracked and automatically filtered from the popup if returned as one of the `PopupMenuItem[]` by `getOptions`. */ getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[];
11
11
  id?: string;
12
12
  disabled?: boolean;
13
13
  menuContainerClass?: string;
@@ -15,7 +15,7 @@ declare const __propDef: {
15
15
  menuItemClass?: string;
16
16
  menuItemHilitedClass?: string;
17
17
  inputClass?: string;
18
- menuDividerClass?: string;
18
+ menuCategoryClass?: string;
19
19
  /** The maximum number of selections allowed before making new selections is disabled. Default of 0 is unlimited. */ maxSelections?: number;
20
20
  /** Consuming components may need to make decisions about what to display or return as options in the popup based
21
21
  * on what's already selected. This component property can be used to both pass an initial set of selections but
@@ -26,6 +26,10 @@ declare const __propDef: {
26
26
  usePortal?: HTMLElement | true | undefined;
27
27
  descid?: string | undefined;
28
28
  /** You can define your own PopupMenu and pass for that to be used or accept DefaultPopupMenu. */ PopupMenu?: typeof DefaultPopupMenu;
29
+ /** Add a Delete All button to clear all selected items */ includeDeleteAll?: boolean;
30
+ /** Show a confirmation message before clearing all selections */ confirmDelete?: string | undefined;
31
+ /** A function that can be used to customize the labels on the selected item pills. Default is a function
32
+ * that takes a PopupMenuItem and returns its label, if available, or its value when no label is provided. */ selectedItemLabel?: (item: PopupMenuItem) => string;
29
33
  };
30
34
  events: {
31
35
  change: CustomEvent<any>;
@@ -35,6 +39,7 @@ declare const __propDef: {
35
39
  [evt: string]: CustomEvent<any>;
36
40
  };
37
41
  slots: {
42
+ deleteall: {};
38
43
  default: {};
39
44
  };
40
45
  };
@@ -2,8 +2,8 @@
2
2
  The purpose of [`PopupMenu`](https://github.com/txstate-etc/svelte-components/blob/main/docs/PopupMenu.md) is to display a menu of options to the user, activated by clicking any element
3
3
  bound to `buttonelement`. It can also be controlled from the parent, by binding the `menushown` boolean.
4
4
  -->
5
- <script lang="ts">import { createEventDispatcher, onDestroy, tick } from 'svelte';
6
- import { isNotBlank, randomid } from 'txstate-utils';
5
+ <script lang="ts" generics="MenuItem extends PopupMenuItem = PopupMenuItem">import { createEventDispatcher, onDestroy, tick } from 'svelte';
6
+ import { groupby, isNull, randomid } from 'txstate-utils';
7
7
  import { Store } from '@txstate-mws/svelte-store';
8
8
  import { glue, portal } from '../actions';
9
9
  import ScreenReaderOnly from './ScreenReaderOnly.svelte';
@@ -60,22 +60,40 @@ export let menuClass = '';
60
60
  export let menuItemClass = '';
61
61
  export let menuItemHilitedClass = '';
62
62
  export let menuItemSelectedClass = '';
63
- export let menuDividerClass = '';
63
+ export let menuCategoryClass = '';
64
64
  export let hideSelectedIndicator = false;
65
65
  export let hideEmptyText = false;
66
+ /** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */
66
67
  export let usemenurole = false;
67
68
  let menuelement;
68
69
  const itemelements = [];
69
70
  let firstactive = 0;
70
71
  let lastactive = items.length - 1;
71
- $: hasMeaningfulItems = showSelected ? !!items.filter(itm => 'value' in itm).length : !!items.filter(itm => 'value' in itm && itm.value !== value).length;
72
+ $: hasMeaningfulItems = showSelected ? !!items.length : !!items.filter(itm => itm.value !== value).length;
72
73
  function hiddenItem(item) {
73
74
  return !!item && !showSelected && item.value === value;
74
75
  }
76
+ $: grouped = groupby(items, item => item.group ?? 'default-group');
77
+ let itemsOrderedByGroup = [];
78
+ function getItemIndex(item) {
79
+ return itemsOrderedByGroup.findIndex(itm => {
80
+ // need to handle default group options
81
+ if (itm.value === item.value) {
82
+ if (item.group === 'default-group') {
83
+ return isNull(itm.group);
84
+ }
85
+ else {
86
+ return item.group === itm.group;
87
+ }
88
+ }
89
+ return false;
90
+ });
91
+ }
75
92
  async function reactToItems(..._) {
76
- firstactive = items.findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm));
77
- lastactive = items.length - [...items].reverse().findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm)) - 1;
78
- if (hilited && items[hilited]?.disabled)
93
+ itemsOrderedByGroup = Object.values(grouped).flat();
94
+ firstactive = itemsOrderedByGroup.findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm));
95
+ lastactive = itemsOrderedByGroup.length - [...itemsOrderedByGroup].reverse().findIndex(itm => 'value' in itm && !itm.disabled && !hiddenItem(itm)) - 1;
96
+ if (hilited && (itemsOrderedByGroup[hilited])?.disabled)
79
97
  hilited = firstactive;
80
98
  }
81
99
  $: void reactToItems(items, value);
@@ -107,13 +125,11 @@ $: void reactToMenuShown(menushown);
107
125
  function move(idx) {
108
126
  if (!menushown)
109
127
  return;
110
- while (idx <= lastactive && items[idx] != null && 'divider' in items[idx])
111
- idx++;
112
- if (items[idx] == null || items[idx]?.disabled)
128
+ if (itemsOrderedByGroup[idx] == null || (itemsOrderedByGroup[idx])?.disabled)
113
129
  return;
114
130
  hilited = Math.max(firstactive, Math.min(lastactive, idx));
115
131
  itemelements[hilited].scrollIntoView({ block: 'center' });
116
- buttonelement.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
132
+ buttonelement?.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
117
133
  }
118
134
  function isSelectable(itm) {
119
135
  return 'value' in itm && !itm.disabled && !hiddenItem(itm);
@@ -125,8 +141,8 @@ function onkeydown(e) {
125
141
  e.preventDefault();
126
142
  if (menushown) {
127
143
  let i = (hilited ?? firstactive - 1) + 1;
128
- while (items[i] && !isSelectable(items[i]))
129
- i++;
144
+ while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
145
+ i += 1;
130
146
  move(i);
131
147
  }
132
148
  else {
@@ -138,8 +154,8 @@ function onkeydown(e) {
138
154
  e.preventDefault();
139
155
  if (menushown) {
140
156
  let i = (hilited ?? lastactive + 1) - 1;
141
- while (items[i] && !isSelectable(items[i]))
142
- i--;
157
+ while (itemsOrderedByGroup[i] && !isSelectable(itemsOrderedByGroup[i]))
158
+ i -= 1;
143
159
  move(i);
144
160
  }
145
161
  else {
@@ -164,8 +180,8 @@ function onkeydown(e) {
164
180
  if (menushown) {
165
181
  menushown = false;
166
182
  if (typeof hilited !== 'undefined') {
167
- value = items[hilited]?.value;
168
- dispatch('change', items[hilited]);
183
+ value = (itemsOrderedByGroup[hilited])?.value;
184
+ dispatch('change', itemsOrderedByGroup[hilited]);
169
185
  }
170
186
  }
171
187
  else {
@@ -180,23 +196,19 @@ function onkeydown(e) {
180
196
  if (typeof hilited !== 'undefined') {
181
197
  e.preventDefault();
182
198
  menushown = false;
183
- value = items[hilited]?.value;
184
- dispatch('change', items[hilited]);
199
+ value = (itemsOrderedByGroup[hilited])?.value;
200
+ dispatch('change', itemsOrderedByGroup[hilited]);
185
201
  }
186
- else {
187
- if (buttonelement.tagName !== 'INPUT') {
188
- e.preventDefault();
189
- menushown = false;
190
- }
191
- }
192
- }
193
- else {
194
- if (buttonelement.tagName !== 'INPUT') {
202
+ else if (buttonelement?.tagName !== 'INPUT') {
195
203
  e.preventDefault();
196
- menushown = true;
197
- tick().then(() => { move(firstactive); }).catch(console.error);
204
+ menushown = false;
198
205
  }
199
206
  }
207
+ else if (buttonelement?.tagName !== 'INPUT') {
208
+ e.preventDefault();
209
+ menushown = true;
210
+ tick().then(() => { move(firstactive); }).catch(console.error);
211
+ }
200
212
  }
201
213
  else if (menushown && e.key === 'Escape') {
202
214
  e.preventDefault();
@@ -247,13 +259,16 @@ function reactToButtonElement(buttonelement) {
247
259
  if (buttonelement) {
248
260
  buttonelement.setAttribute('aria-haspopup', (usemenurole ? 'true' : 'listbox'));
249
261
  buttonelement.setAttribute('aria-expanded', menushown ? 'true' : 'false');
262
+ if (!usemenurole) {
263
+ buttonelement.setAttribute('role', 'combobox');
264
+ }
250
265
  buttonelement.addEventListener('click', onbuttonclick);
251
266
  buttonelement.addEventListener('keydown', onkeydown);
252
267
  buttonelement.addEventListener('blur', onblur);
253
268
  buttonelement.addEventListener('input', oninput);
254
269
  const parents = getScrollParents(buttonelement);
255
270
  for (const parent of [...parents, undefined]) {
256
- const observer = new IntersectionObserver((entries) => {
271
+ const observer = new IntersectionObserver(entries => {
257
272
  for (const entry of entries) {
258
273
  if (entry.intersectionRatio < 0.1)
259
274
  menushown = false;
@@ -278,41 +293,62 @@ $: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'va
278
293
  </script>
279
294
 
280
295
  {#if menushown && !loading && (hasMeaningfulItems || !hideEmptyText)}
281
- <div use:portal={usePortal === true ? undefined : (usePortal || null)}
282
- use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }}
283
- class={menuContainerClass}>
284
- <ul bind:this={menuelement} id={menuid} role={usemenurole ? 'menu' : 'listbox'} tabindex="-1" style={width ? `width: ${width}` : ''}
285
- class={menuClass} class:hasSelected class:defaultmenu={!menuClass && !menuContainerClass}
286
- on:keydown={onkeydown}>
287
- {#each items as item, i ('value' in item ? item.value : `popupmenu_divider_${i}`)}
288
- {#if 'value' in item && (showSelected || item.value !== value)}
289
- <!-- svelte-ignore a11y-click-events-have-key-events -->
290
- <li
291
- id={`${menuid}-${i}`}
292
- bind:this={itemelements[i]}
293
- class={`${menuItemClass} ${i === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
294
- class:disabled={!!item.disabled}
295
- class:hilited={!menuItemHilitedClass && i === hilited}
296
- class:selected={showSelected && !hideSelectedIndicator && value === item.value}
297
- on:click={onclick(item)}
298
- role={usemenurole ? 'menuitem' : 'option'}
299
- tabindex=-1
300
- aria-selected={usemenurole ? undefined : (value === item.value)}
301
- aria-disabled={item.disabled}
302
- ><slot {item} label={item.label || item.value} hilited={i === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
303
- {:else if 'divider' in item && item.divider}
304
- <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
305
- <li class={`divider ${menuDividerClass}`} on:mousedown|stopPropagation|preventDefault={() => {}} class:group={isNotBlank(item.label)}>{item.label}</li>
296
+ <div use:portal={usePortal === true ? undefined : (usePortal ?? null)} use:glue={{ target: buttonelement, align, cover, gap, adjustparentheight, store: computedalign }} class={menuContainerClass}>
297
+ <ul bind:this={menuelement} id={menuid} role={usemenurole ? 'menu' : 'listbox'} tabindex="-1" style={width ? `width: ${width}` : ''} class={menuClass} class:hasSelected class:defaultmenu={!menuClass && !menuContainerClass} on:keydown={onkeydown}>
298
+ {#each Object.entries(grouped) as [group, groupitems] (group)}
299
+ {#if group === 'default-group'}
300
+ {#each groupitems as item, i (item.value)}
301
+ {@const itemIndex = getItemIndex(item)}
302
+ {#if (showSelected || item.value !== value)}
303
+ <li
304
+ id={`${menuid}-${itemIndex}`}
305
+ bind:this={itemelements[itemIndex]}
306
+ class={`${menuItemClass} ${itemIndex === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
307
+ class:disabled={!!item.disabled}
308
+ class:hilited={!menuItemHilitedClass && itemIndex === hilited}
309
+ class:selected={showSelected && !hideSelectedIndicator && value === item.value}
310
+ on:click={onclick(item)}
311
+ role={usemenurole ? 'menuitem' : 'option'}
312
+ tabindex=-1
313
+ aria-selected={usemenurole ? undefined : (value === item.value)}
314
+ aria-disabled={item.disabled}
315
+ ><slot {item} label={item.label || item.value} hilited={itemIndex === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
316
+ {/if}
317
+ {/each}
318
+ {:else}
319
+ <li role="group" aria-labelledby="{menuid}-{group}" class="group">
320
+ <span id="{menuid}-{group}" class="category {menuCategoryClass}">{group}</span>
321
+ <ul role="presentation">
322
+ {#each groupitems as item, i (item.value)}
323
+ {@const itemIndex = getItemIndex(item)}
324
+ {#if (showSelected || item.value !== value)}
325
+ <li
326
+ id={`${menuid}-${itemIndex}`}
327
+ bind:this={itemelements[itemIndex]}
328
+ class={`${menuItemClass} ${itemIndex === hilited ? menuItemHilitedClass : ''} ${value === item.value ? menuItemSelectedClass : ''}`}
329
+ class:disabled={!!item.disabled}
330
+ class:hilited={!menuItemHilitedClass && itemIndex === hilited}
331
+ class:selected={showSelected && !hideSelectedIndicator && value === item.value}
332
+ on:click={onclick(item)}
333
+ role={usemenurole ? 'menuitem' : 'option'}
334
+ tabindex=-1
335
+ aria-selected={usemenurole ? undefined : (value === item.value)}
336
+ aria-disabled={item.disabled}
337
+ ><slot {item} label={item.label || item.value} hilited={itemIndex === hilited} selected={value === item.value}>{item.label || item.value}</slot></li>
338
+ {/if}
339
+ {/each}
340
+ </ul>
341
+ </li>
306
342
  {/if}
307
343
  {/each}
308
344
  {#if !hasMeaningfulItems}
309
345
  <li role={usemenurole ? 'menuitem' : 'option'} class={`${menuItemClass} disabled`} aria-live="assertive" aria-selected={usemenurole ? undefined : false}>
310
346
  <slot name="noresults">
311
- {#if !emptyText}
312
- <span aria-hidden="true">{'¯\\_(ツ)_/¯'}</span><ScreenReaderOnly>{emptyText || 'no results found'}</ScreenReaderOnly>
313
- {:else}
314
- {emptyText}
315
- {/if}
347
+ {#if !emptyText}
348
+ <span aria-hidden="true">{'¯\\_(ツ)_/¯'}</span><ScreenReaderOnly>{emptyText || 'no results found'}</ScreenReaderOnly>
349
+ {:else}
350
+ {emptyText}
351
+ {/if}
316
352
  </slot>
317
353
  </li>
318
354
  {/if}
@@ -389,4 +425,11 @@ $: hasSelected = showSelected && !hideSelectedIndicator && items.some(itm => 'va
389
425
  border-right: 0.2em solid;
390
426
  box-sizing: border-box;
391
427
  }
428
+ span.category {
429
+ display: block;
430
+ font-weight: bold;
431
+ }
432
+ .group ul {
433
+ padding-inline-start: 20px;
434
+ }
392
435
  </style>
@@ -1,13 +1,13 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import { Store } from '@txstate-mws/svelte-store';
3
3
  import type { GlueAlignOpts, GlueAlignStore } from '../actions';
4
- import type { PopupMenuItem, PopupMenuTypes } from '../types';
5
- declare const __propDef: {
6
- props: {
4
+ import type { PopupMenuItem } from '../types';
5
+ declare class __sveltets_Render<MenuItem extends PopupMenuItem = PopupMenuItem> {
6
+ props(): {
7
7
  /** The DOM element that will act as the "button" for this menu. The menu will be placed next to the
8
8
  button and be controlled by the button. Keyboard access will be enabled when the button has focus.
9
- This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */ buttonelement: HTMLElement;
10
- /** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?: PopupMenuTypes[];
9
+ This component adds all appropriate attributes (tabindex, roles, and aria) automatically. */ buttonelement: HTMLElement | undefined;
10
+ /** The list of menu items to be shown. Parent may change this at any time based on user activity. */ items?: MenuItem[] | undefined;
11
11
  menushown?: boolean;
12
12
  value?: string | undefined;
13
13
  /** Control where the menu will appear. Default is to use the current viewport to make a decision to
@@ -41,31 +41,31 @@ declare const __propDef: {
41
41
  menuItemClass?: string;
42
42
  menuItemHilitedClass?: string;
43
43
  menuItemSelectedClass?: string;
44
- menuDividerClass?: string;
44
+ menuCategoryClass?: string;
45
45
  hideSelectedIndicator?: boolean;
46
46
  hideEmptyText?: boolean;
47
- usemenurole?: boolean;
47
+ /** If true, the role for the popup menu will be 'menu' instead of the default 'listbox' value */ usemenurole?: boolean;
48
48
  };
49
- slots: {
49
+ events(): {
50
+ change: CustomEvent<MenuItem>;
51
+ };
52
+ slots(): {
50
53
  default: {
51
- item: PopupMenuTypes;
52
- label: any;
54
+ item: MenuItem;
55
+ label: string;
53
56
  hilited: boolean;
54
57
  selected: boolean;
55
58
  };
56
59
  noresults: {};
57
60
  };
58
- events: {
59
- change: CustomEvent<PopupMenuItem>;
60
- };
61
- };
62
- export type PopupMenuProps = typeof __propDef.props;
63
- export type PopupMenuEvents = typeof __propDef.events;
64
- export type PopupMenuSlots = typeof __propDef.slots;
61
+ }
62
+ export type PopupMenuProps<MenuItem extends PopupMenuItem = PopupMenuItem> = ReturnType<__sveltets_Render<MenuItem>['props']>;
63
+ export type PopupMenuEvents<MenuItem extends PopupMenuItem = PopupMenuItem> = ReturnType<__sveltets_Render<MenuItem>['events']>;
64
+ export type PopupMenuSlots<MenuItem extends PopupMenuItem = PopupMenuItem> = ReturnType<__sveltets_Render<MenuItem>['slots']>;
65
65
  /**
66
66
  * The purpose of [`PopupMenu`](https://github.com/txstate-etc/svelte-components/blob/main/docs/PopupMenu.md) is to display a menu of options to the user, activated by clicking any element
67
67
  * bound to `buttonelement`. It can also be controlled from the parent, by binding the `menushown` boolean.
68
68
  */
69
- export default class PopupMenu extends SvelteComponentTyped<PopupMenuProps, PopupMenuEvents, PopupMenuSlots> {
69
+ export default class PopupMenu<MenuItem extends PopupMenuItem = PopupMenuItem> extends SvelteComponentTyped<PopupMenuProps<MenuItem>, PopupMenuEvents<MenuItem>, PopupMenuSlots<MenuItem>> {
70
70
  }
71
71
  export {};
@@ -7,8 +7,12 @@ export interface PopupMenuItem {
7
7
  /** Toggles the choice as one that's visible but is disabled and unclickable until conditions
8
8
  * are met that would cause it to become enabled. */
9
9
  disabled?: boolean;
10
+ /** Allows PopupMenuItems to be grouped by categories in the menu */
11
+ group?: string;
10
12
  }
11
- /** A PopupMenu list native type for displaying */
13
+ /** A PopupMenu list native type for displaying
14
+ * @note Not currently in use
15
+ */
12
16
  export interface PopupMenuDivider {
13
17
  /** Toggle for whether this instance of this type is shown. */
14
18
  divider: boolean;
@@ -23,7 +27,7 @@ export interface PopupMenuDivider {
23
27
  * @note Not currently in use. */
24
28
  export interface PopupMenuGroup {
25
29
  /** The label for the group. */
26
- lable: string;
30
+ label: string;
27
31
  /** The listing of options that would be listed under the parent `<ul>`. */
28
32
  options: PopupMenuTypes[];
29
33
  }
@@ -1,3 +1,3 @@
1
1
  export * from './CardLayout.js';
2
- export * from './CollapsingTable.js';
3
- export * from './PopupMenu.js';
2
+ export type * from './CollapsingTable.js';
3
+ export type * from './PopupMenu.js';
@@ -1,3 +1 @@
1
1
  export * from './CardLayout.js';
2
- export * from './CollapsingTable.js';
3
- export * from './PopupMenu.js';
@@ -44,7 +44,7 @@ function getTranslate(element) {
44
44
  }
45
45
  // Can either be 2d or 3d transform
46
46
  const matrixType = matrix.includes('3d') ? '3d' : '2d';
47
- const matrixValues = matrix.match(/matrix.*\((.+)\)/)[1].split(', ');
47
+ const matrixValues = matrix.match(/matrix.*\((.+)\)/v)[1].split(', ');
48
48
  // 2d matrices have 6 values
49
49
  // Last 2 values are X and Y.
50
50
  // 2d matrices does not have Z value.
package/dist/util/util.js CHANGED
@@ -5,7 +5,6 @@ export function debounced(fn, timeout) {
5
5
  let timer;
6
6
  return (...args) => {
7
7
  clearTimeout(timer);
8
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
9
8
  timer = setTimeout(() => fn(...args), timeout);
10
9
  };
11
10
  }
@@ -25,7 +24,7 @@ export function getScrollParents(element) {
25
24
  if (excludeStaticParent && style.position === 'static') {
26
25
  continue;
27
26
  }
28
- if (/(auto|scroll|hidden)/.test(style.overflow + style.overflowY + style.overflowX))
27
+ if (/(auto|scroll|hidden)/v.test(style.overflow + style.overflowY + style.overflowX))
29
28
  return [parent, ...getScrollParents(parent)];
30
29
  }
31
30
  return [document.body];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.6.10",
3
+ "version": "1.6.12",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",
@@ -10,7 +10,7 @@
10
10
  "preview": "vite preview",
11
11
  "check": "svelte-check --tsconfig ./tsconfig.json",
12
12
  "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
13
- "lint": "eslint --ignore-path .gitignore .",
13
+ "lint": "eslint .",
14
14
  "test": "playwright test tests"
15
15
  },
16
16
  "dependencies": {
@@ -20,16 +20,18 @@
20
20
  "txstate-utils": "^1.7.12"
21
21
  },
22
22
  "devDependencies": {
23
+ "@eslint/compat": "^2.0.3",
23
24
  "@playwright/test": "^1.18.1",
24
- "@sveltejs/adapter-auto": "^3.0.0",
25
+ "@stylistic/eslint-plugin": "^2.10.1",
26
+ "@sveltejs/adapter-auto": "^7.0.0",
25
27
  "@sveltejs/kit": "^2.0.0",
26
28
  "@sveltejs/package": "^2.0.2",
27
- "@types/tabbable": "^3.1.2",
28
- "eslint-config-standard-with-typescript": "^43.0.0",
29
- "eslint-plugin-svelte": "^2.0.0",
30
- "svelte-check": "^3.0.1",
31
- "svelte-preprocess": "^5.0.0",
32
- "svelte2tsx": "^0.7.0",
29
+ "@types/tabbable": "^5.0.0",
30
+ "eslint-config-love": "^151.0.0",
31
+ "eslint-plugin-svelte": "^3.0.0",
32
+ "svelte": "^5.0.0",
33
+ "svelte-check": "^4.0.0",
34
+ "svelte-preprocess": "^6.0.0",
33
35
  "tslib": "^2.3.1",
34
36
  "typescript": "^5.0.4"
35
37
  },
@@ -46,7 +48,9 @@
46
48
  },
47
49
  "./package.json": "./package.json"
48
50
  },
49
- "files": ["dist"],
51
+ "files": [
52
+ "dist"
53
+ ],
50
54
  "repository": {
51
55
  "type": "git",
52
56
  "url": "git@github.com:txstate-etc/svelte-components.git"