@txstate-mws/svelte-components 1.0.4 → 1.1.3

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/actions/resize.js CHANGED
@@ -3,7 +3,19 @@ import { Store } from '@txstate-mws/svelte-store';
3
3
  import { debounced } from '../util/index.js';
4
4
  export class ResizeStore extends Store {
5
5
  constructor(initialState) {
6
- super(initialState ?? { clientWidth: 1024, clientHeight: 1024, offsetWidth: 1024, offsetHeight: 1024 });
6
+ super(initialState ?? typeof window === 'undefined'
7
+ ? {
8
+ clientWidth: 1024,
9
+ clientHeight: 1024,
10
+ offsetWidth: 1024,
11
+ offsetHeight: 1024
12
+ }
13
+ : {
14
+ clientWidth: window.innerWidth,
15
+ clientHeight: window.innerHeight,
16
+ offsetWidth: window.innerWidth,
17
+ offsetHeight: window.innerHeight
18
+ });
7
19
  }
8
20
  }
9
21
  export function resize(el, config) {
@@ -15,11 +27,11 @@ export function resize(el, config) {
15
27
  if (!equal(lastSize, current)) {
16
28
  lastSize = current;
17
29
  if (config?.store)
18
- config.store.set(current);
30
+ config.store.update(v => ({ ...v, ...current }));
19
31
  el.dispatchEvent(new CustomEvent('resize', { detail: current }));
20
32
  }
21
33
  }
22
- if (!ResizeObserver)
34
+ if (typeof ResizeObserver === 'undefined')
23
35
  return watch(); // support SSR
24
36
  let observer = new ResizeObserver(config?.debounce ? debounced(watch, config.debounce) : watch);
25
37
  observer.observe(el);
@@ -29,7 +41,7 @@ export function resize(el, config) {
29
41
  if (newConfig?.debounce === true)
30
42
  newConfig.debounce = 100;
31
43
  if (newConfig?.store !== config?.store) {
32
- newConfig?.store?.set(lastSize);
44
+ newConfig?.store?.update(v => ({ ...v, ...lastSize }));
33
45
  }
34
46
  if (!!newConfig?.debounce !== !!config?.debounce) {
35
47
  observer.disconnect();
package/actions/sticky.js CHANGED
@@ -34,7 +34,7 @@ export function sticky(el, config) {
34
34
  config?.store?.update(v => ({ ...v, translateY, floating }));
35
35
  }
36
36
  }
37
- function onscroll(e) {
37
+ function onscroll(_) {
38
38
  recalc(window.scrollY);
39
39
  }
40
40
  function onwheel(e) {
@@ -112,7 +112,7 @@ export function stickyfixed(el, config) {
112
112
  el.style.top = `${top}px`;
113
113
  }
114
114
  }
115
- function onscroll(e) {
115
+ function onscroll(_) {
116
116
  recalc(window.scrollY);
117
117
  }
118
118
  let lastOffsetParent = el.offsetParent;
@@ -0,0 +1,23 @@
1
+ <script >export let className = '';
2
+ import { getContext } from 'svelte';
3
+ import { resize } from '../actions';
4
+ import { CARDLAYOUT } from '../types';
5
+ const block = { element: undefined };
6
+ const { registerBlock, gutter, recalculate } = getContext(CARDLAYOUT);
7
+ const { width, order, linebreak } = registerBlock(block);
8
+ </script>
9
+
10
+ <li class="cardlayout-card {className}" bind:this={block.element} use:resize on:resize={recalculate} style="margin-bottom: {$gutter}px; width: {$width}; order: {$order};">
11
+ <slot></slot>
12
+ </li>
13
+ {#if $linebreak}
14
+ <li aria-hidden="true" class="cardlayout-columnbreak" style:order={$order}></li>
15
+ {/if}
16
+
17
+ <style>
18
+ .cardlayout-columnbreak {
19
+ width: 0;
20
+ height: 100%;
21
+ visibility: hidden;
22
+ }
23
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ className?: string;
5
+ };
6
+ events: {
7
+ [evt: string]: CustomEvent<any>;
8
+ };
9
+ slots: {
10
+ default: {};
11
+ };
12
+ };
13
+ export declare type CardProps = typeof __propDef.props;
14
+ export declare type CardEvents = typeof __propDef.events;
15
+ export declare type CardSlots = typeof __propDef.slots;
16
+ export default class Card extends SvelteComponentTyped<CardProps, CardEvents, CardSlots> {
17
+ }
18
+ export {};
@@ -0,0 +1,197 @@
1
+ <script >import { onDestroy, tick, setContext, onMount } from 'svelte';
2
+ import { writable } from 'svelte/store';
3
+ import { browser } from '$app/env';
4
+ import { resize } from '../actions';
5
+ import { CARDLAYOUT } from '../types';
6
+ export let maxwidth = 500;
7
+ export let preserveorder = false;
8
+ export let gutter = 10;
9
+ export let className = '';
10
+ const blocks = [];
11
+ const gutterstore = writable(gutter);
12
+ let defaultOrder = 0;
13
+ setContext(CARDLAYOUT, {
14
+ registerBlock: (block) => {
15
+ blocks.push(block);
16
+ hardrecalc();
17
+ onDestroy(() => {
18
+ blocks.splice(blocks.indexOf(block), 1);
19
+ hardrecalc();
20
+ });
21
+ block.order = writable(defaultOrder++);
22
+ block.linebreak = writable(false);
23
+ const cols = savecolumns || Math.ceil(1024 / maxwidth);
24
+ block.width = writable(`calc(${100.0 / cols}% - ${gutter * (cols - 1) / cols}px)`);
25
+ return block;
26
+ },
27
+ recalculate: () => {
28
+ hardrecalc();
29
+ },
30
+ gutter: gutterstore
31
+ });
32
+ $: gutterstore.set(gutter);
33
+ let layoutelement;
34
+ let cycle1 = 0;
35
+ let cycle2 = 0;
36
+ function detectcycle(w) {
37
+ if (cycle1 === 0 && Math.abs(w - cycle2) > 5) {
38
+ cycle1 = w;
39
+ }
40
+ else if (cycle2 === 0 && Math.abs(w - cycle1) > 5) {
41
+ cycle2 = w;
42
+ }
43
+ else if (w === cycle1 || w === cycle2) {
44
+ return true;
45
+ }
46
+ else {
47
+ cycle1 = 0;
48
+ cycle2 = 0;
49
+ }
50
+ return false;
51
+ }
52
+ let savecolumns = 0;
53
+ let optimal;
54
+ let fullheight = 0;
55
+ let hardrequired = false;
56
+ async function recalculate(realw) {
57
+ const columns = Math.ceil(realw / maxwidth);
58
+ const guttereach = gutter * (columns - 1) / columns;
59
+ const cycling = detectcycle(realw);
60
+ if (columns !== savecolumns) {
61
+ for (const block of blocks)
62
+ block.width.set(`calc(${100.0 / columns}% - ${guttereach}px`);
63
+ await tick();
64
+ }
65
+ // collect all the card heights at this new column width
66
+ for (const block of blocks)
67
+ block.height = block.element?.offsetHeight ?? 200;
68
+ if (columns !== savecolumns || hardrequired) { // only do real work if number of columns has changed, recalculate triggers on resize
69
+ if (preserveorder) {
70
+ optimal = [blocks];
71
+ if (columns > 1) {
72
+ const totalheight = blocks.reduce((totalheight, block) => totalheight + block.height + gutter, 0);
73
+ const tallestblock = Math.max(...blocks.map(b => b.height));
74
+ const minheight = Math.max(totalheight / columns, tallestblock);
75
+ let shortestoverall = totalheight;
76
+ // 2d packing problem is NP-hard so this is an O(n) heuristic
77
+ // optimal configuration would be if each column is `minheight` so start there
78
+ // then relax it in increments and see if the overall height shrinks
79
+ const increment = Math.max(1, Math.floor(Math.min(minheight, tallestblock) * 0.05));
80
+ for (let colmaxheight = minheight; colmaxheight < minheight + tallestblock + gutter; colmaxheight += increment) {
81
+ let colheight = 0;
82
+ let colidx = 0;
83
+ let tallestcol = 0;
84
+ let tallestcolidx = 0;
85
+ const arrangement = [];
86
+ for (const block of blocks) {
87
+ if (colheight + block.height > colmaxheight && colidx < columns - 1) {
88
+ if (colheight > tallestcol) {
89
+ tallestcol = colheight;
90
+ tallestcolidx = colidx;
91
+ }
92
+ colidx++;
93
+ colheight = 0;
94
+ }
95
+ if (!arrangement[colidx])
96
+ arrangement[colidx] = [];
97
+ arrangement[colidx].push(block);
98
+ colheight += block.height + gutter;
99
+ }
100
+ if (colheight > tallestcol) {
101
+ tallestcol = colheight;
102
+ tallestcolidx = colidx;
103
+ }
104
+ if (tallestcol < shortestoverall) {
105
+ shortestoverall = tallestcol;
106
+ optimal = arrangement;
107
+ }
108
+ // if leftmost column is tallest, increasing the allowable height can never make the overall height shorter
109
+ if (tallestcolidx === 0)
110
+ break;
111
+ }
112
+ }
113
+ }
114
+ else {
115
+ optimal = [];
116
+ const heights = Array.apply(null, Array(columns)).map((_) => 0); // initializes heights to an array of zeroes
117
+ // begin sorting blocks into columns
118
+ for (const block of blocks) {
119
+ // find the column with the smallest current height
120
+ const colidx = heights.reduce((acc, curr, curridx) => curr < heights[acc] ? curridx : acc, 0);
121
+ // record the height we are adding to the chosen column
122
+ heights[colidx] += block.height + gutter;
123
+ // move the current card to the chosen column
124
+ if (!optimal[colidx])
125
+ optimal[colidx] = [];
126
+ optimal[colidx].push(block);
127
+ }
128
+ }
129
+ // we created the optimal arrangement, now move the block elements to
130
+ // reflect it
131
+ let order = 0;
132
+ for (let i = 0; i < optimal.length; i++) {
133
+ for (let j = 0; j < optimal[i].length; j++) {
134
+ const block = optimal[i][j];
135
+ block.order.set(order++);
136
+ block.linebreak.set(j === optimal[i].length - 1 && i < optimal.length - 1);
137
+ }
138
+ }
139
+ savecolumns = columns;
140
+ hardrequired = false;
141
+ }
142
+ // re-adjust container height
143
+ const saveheight = fullheight;
144
+ fullheight = 0;
145
+ for (let i = 0; i < optimal.length; i++) {
146
+ let top = 0;
147
+ for (const block of optimal[i]) {
148
+ top += block.height + gutter;
149
+ }
150
+ fullheight = Math.max(fullheight, top);
151
+ }
152
+ if (cycling && saveheight < fullheight)
153
+ fullheight = saveheight;
154
+ }
155
+ let savewidth = 0;
156
+ let timer = 0;
157
+ function triggerrecalc(width) {
158
+ if (width === savewidth && !hardrequired)
159
+ return;
160
+ cancelAnimationFrame(timer);
161
+ timer = requestAnimationFrame(() => {
162
+ recalculate(width);
163
+ savewidth = width;
164
+ });
165
+ }
166
+ function hardrecalc() {
167
+ if (!layoutelement)
168
+ return;
169
+ hardrequired = true;
170
+ cycle1 = 0;
171
+ cycle2 = 0;
172
+ triggerrecalc(layoutelement.clientWidth);
173
+ }
174
+ function onResize(e) {
175
+ triggerrecalc(e.detail.clientWidth);
176
+ }
177
+ onMount(() => triggerrecalc(layoutelement.clientWidth));
178
+ </script>
179
+
180
+ <ul class="cardlayout {className}" class:ssr={!browser} style:height={browser ? `${fullheight + gutter}px` : undefined} bind:this={layoutelement} use:resize on:resize={onResize}>
181
+ <slot></slot>
182
+ </ul>
183
+
184
+ <style>
185
+ .cardlayout {
186
+ list-style: none;
187
+ padding: 0;
188
+ margin: 0;
189
+ display: flex;
190
+ flex-wrap: wrap;
191
+ flex-direction: column;
192
+ align-content: space-between;
193
+ }
194
+ .cardlayout.ssr {
195
+ flex-direction: row;
196
+ }
197
+ </style>
@@ -0,0 +1,21 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ maxwidth?: number;
5
+ preserveorder?: boolean;
6
+ gutter?: number;
7
+ className?: string;
8
+ };
9
+ events: {
10
+ [evt: string]: CustomEvent<any>;
11
+ };
12
+ slots: {
13
+ default: {};
14
+ };
15
+ };
16
+ export declare type CardLayoutProps = typeof __propDef.props;
17
+ export declare type CardLayoutEvents = typeof __propDef.events;
18
+ export declare type CardLayoutSlots = typeof __propDef.slots;
19
+ export default class CardLayout extends SvelteComponentTyped<CardLayoutProps, CardLayoutEvents, CardLayoutSlots> {
20
+ }
21
+ export {};
@@ -0,0 +1,181 @@
1
+ <script >import { createEventDispatcher } from 'svelte';
2
+ import { randomid, sleep } from 'txstate-utils';
3
+ import ScreenReaderOnly from './ScreenReaderOnly.svelte';
4
+ import DefaultPopupMenu from './PopupMenu.svelte';
5
+ import { debouncedPromise, modifierKey, selectionIsLeft } from '../util';
6
+ export let id = randomid();
7
+ export let name;
8
+ export let getOptions;
9
+ export let menuContainerClass = undefined;
10
+ export let menuClass = undefined;
11
+ export let menuItemClass = undefined;
12
+ export let menuItemHilitedClass = undefined;
13
+ export let selected = [];
14
+ export let placeholder = '';
15
+ export let emptyText = undefined;
16
+ export let PopupMenu = DefaultPopupMenu;
17
+ let menushown;
18
+ let options = [];
19
+ let hilitedpill;
20
+ let inputvalue = '';
21
+ let popupvalue = undefined;
22
+ let inputelement;
23
+ const descriptionid = randomid();
24
+ $: debouncedGetOptions = debouncedPromise(getOptions, 100, []);
25
+ $: selectedSet = new Set(selected.map(s => s.value));
26
+ async function reactToInput(..._) {
27
+ const saveval = inputvalue;
28
+ const rawOptions = await debouncedGetOptions(saveval);
29
+ if (inputvalue !== saveval)
30
+ return; // ignore any results that are out of date
31
+ options = rawOptions.filter(o => !selectedSet.has(o.value));
32
+ await sleep(100);
33
+ // needed to sleep a little bit or this will turn the menu on only to have popupmenu's click handler toggle it back off
34
+ if (typeof document !== 'undefined' && inputelement === document.activeElement)
35
+ menushown = true;
36
+ }
37
+ $: reactToInput(inputvalue, debouncedGetOptions, selectedSet);
38
+ $: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available';
39
+ function addSelection(e) {
40
+ inputvalue = '';
41
+ const opt = { ...e.detail, label: e.detail.label || e.detail.value };
42
+ selected = [...selected, opt];
43
+ popupvalue = undefined;
44
+ }
45
+ function removeSelection(opt, idx, nextfocus = 1) {
46
+ if (hilitedpill === opt.value) {
47
+ hilitedpill = selected[idx + nextfocus]?.value;
48
+ }
49
+ selected = selected.filter(s => s.value !== opt.value);
50
+ }
51
+ function inputkeydown(e) {
52
+ if (modifierKey(e))
53
+ return;
54
+ if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
55
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length;
56
+ if (idx !== 0)
57
+ hilitedpill = selected[idx - 1].value;
58
+ }
59
+ else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
60
+ if (hilitedpill)
61
+ e.preventDefault();
62
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined;
63
+ if (idx != null) {
64
+ if (idx === selected.length - 1)
65
+ hilitedpill = undefined;
66
+ else
67
+ hilitedpill = selected[idx + 1].value;
68
+ }
69
+ }
70
+ else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
71
+ if (hilitedpill) {
72
+ const idx = selected.findIndex(s => s.value === hilitedpill);
73
+ removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1);
74
+ e.preventDefault();
75
+ }
76
+ else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
77
+ removeSelection(selected[selected.length - 1], selected.length - 1, -1);
78
+ e.preventDefault();
79
+ }
80
+ }
81
+ }
82
+ function inputfocus() {
83
+ reactToInput(true);
84
+ }
85
+ let popuphilited;
86
+ $: if (popuphilited != null)
87
+ hilitedpill = undefined;
88
+ function reactToHilite(..._) {
89
+ if (!inputelement)
90
+ return;
91
+ if (hilitedpill) {
92
+ popuphilited = undefined;
93
+ inputelement.setAttribute('aria-activedescendant', id + hilitedpill);
94
+ }
95
+ else
96
+ inputelement.removeAttribute('aria-activedescendant');
97
+ }
98
+ $: reactToHilite(hilitedpill, id);
99
+ const dispatch = createEventDispatcher();
100
+ $: dispatch('change', selected);
101
+ </script>
102
+
103
+ <fieldset>
104
+ <ul class="multiselect-selected" role="listbox">
105
+ {#each selected as option, i}
106
+ <li id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
107
+ on:click|preventDefault|stopPropagation={() => removeSelection(option, i, 1)}
108
+ aria-selected="true">
109
+ {option.label || option.value}
110
+ <ScreenReaderOnly>, click to remove</ScreenReaderOnly>
111
+ </li>
112
+ {/each}
113
+ <li class="input">
114
+ <input type="text" id={id} name={name} placeholder={placeholder}
115
+ bind:this={inputelement} bind:value={inputvalue}
116
+ on:focus={inputfocus} on:keydown={inputkeydown}
117
+ autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list"
118
+ aria-describedby="{descriptionid}">
119
+ </li>
120
+ </ul>
121
+ <ScreenReaderOnly id={descriptionid} arialive="assertive">
122
+ <span>{selected.length ? selected.length + ' selected' : 'select multiple, none selected'}, up down to choose, left right to hilite existing choices</span>
123
+ {#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
124
+ </ScreenReaderOnly>
125
+ <slot></slot>
126
+ </fieldset>
127
+ <svelte:component this={PopupMenu} bind:menushown bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft' {emptyText} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={options} buttonelement={inputelement} on:change={addSelection}></svelte:component>
128
+
129
+ <style>
130
+ fieldset {
131
+ border: 0;
132
+ padding: 0;
133
+ margin: 0;
134
+ }
135
+ fieldset * {
136
+ box-sizing: border-box;
137
+ }
138
+ .multiselect-selected {
139
+ margin: 0;
140
+ padding: 0;
141
+ list-style: none;
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ align-items: stretch;
145
+ padding: var(--multiselect-padding, 0.3em);
146
+ border: var(--multiselect-border, 1px solid #666666);
147
+ border-radius: var(--multiselect-radius, 0.3em);
148
+ }
149
+ .multiselect-selected:focus-within {
150
+ outline-width: var(--multiselect-focus-width, 2px);
151
+ outline-style: solid;
152
+ outline-color: var(--multiselect-focus-color, Highlight);
153
+ }
154
+ li.input {
155
+ flex-grow: 1;
156
+ }
157
+ input {
158
+ outline: 0;
159
+ border: 0;
160
+ min-width: 0;
161
+ width: 100%;
162
+ height: 100%;
163
+ }
164
+ .multiselect-pill {
165
+ cursor: pointer;
166
+ flex-grow: 0;
167
+ margin-right: 0.3em;
168
+ line-height: 1;
169
+ border-radius: var(--multiselect-pill-radius, 0.8em);
170
+ border: var(--multiselect-pill-border, 1px solid gray);
171
+ background-color: var(--multiselect-pill-bg, transparent);
172
+ color: var(--multiselect-pill-text, black);
173
+ padding: var(--multiselect-pill-padding, 0.3em 0.5em);
174
+ }
175
+ .multiselect-pill.hilited {
176
+ outline: 0;
177
+ background-color: var(--multiselect-pill-selected, gray);
178
+ border: var(--multiselect-pill-selected-border, 1px solid transparent);
179
+ color: var(--multiselect-pill-selected-text, white);
180
+ }
181
+ </style>
@@ -0,0 +1,35 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ import DefaultPopupMenu from './PopupMenu.svelte';
3
+ import type { PopupMenuItem } from '../types';
4
+ declare const __propDef: {
5
+ props: {
6
+ id?: string;
7
+ name: string;
8
+ getOptions: (search: string) => Promise<PopupMenuItem[]> | PopupMenuItem[];
9
+ menuContainerClass?: string | undefined;
10
+ menuClass?: string | undefined;
11
+ menuItemClass?: string | undefined;
12
+ menuItemHilitedClass?: string | undefined;
13
+ selected?: {
14
+ value: string;
15
+ label: string;
16
+ }[];
17
+ placeholder?: string;
18
+ emptyText?: string | undefined;
19
+ PopupMenu?: typeof DefaultPopupMenu;
20
+ };
21
+ events: {
22
+ change: CustomEvent<any>;
23
+ } & {
24
+ [evt: string]: CustomEvent<any>;
25
+ };
26
+ slots: {
27
+ default: {};
28
+ };
29
+ };
30
+ export declare type MultiSelectProps = typeof __propDef.props;
31
+ export declare type MultiSelectEvents = typeof __propDef.events;
32
+ export declare type MultiSelectSlots = typeof __propDef.slots;
33
+ export default class MultiSelect extends SvelteComponentTyped<MultiSelectProps, MultiSelectEvents, MultiSelectSlots> {
34
+ }
35
+ export {};
@@ -3,6 +3,7 @@ import { randomid } from 'txstate-utils';
3
3
  import { Store } from '@txstate-mws/svelte-store';
4
4
  import { glue, portal } from '../actions';
5
5
  import ScreenReaderOnly from './ScreenReaderOnly.svelte';
6
+ import { modifierKey } from '../util';
6
7
  const dispatch = createEventDispatcher();
7
8
  export let menushown = false;
8
9
  export let menuContainerClass = '';
@@ -20,7 +21,7 @@ export let computedalign = new Store({ valign: 'bottom', halign: 'left' });
20
21
  export let usePortal = undefined;
21
22
  export let emptyText = undefined;
22
23
  export let value = undefined;
23
- let hilited = undefined;
24
+ export let hilited = undefined;
24
25
  let menuelement;
25
26
  const itemelements = [];
26
27
  const menuid = randomid();
@@ -43,15 +44,18 @@ async function reactToMenuShown(_) {
43
44
  }
44
45
  await tick();
45
46
  if (!menushown) {
46
- hilited = undefined;
47
47
  buttonelement.removeAttribute('aria-controls');
48
48
  buttonelement.setAttribute('aria-expanded', 'false');
49
+ if (hilited)
50
+ buttonelement.removeAttribute('aria-activedescendant');
51
+ hilited = undefined;
49
52
  }
50
53
  else {
51
54
  buttonelement.setAttribute('aria-controls', menuid);
52
55
  buttonelement.setAttribute('aria-expanded', 'true');
56
+ if (buttonelement !== document.activeElement)
57
+ buttonelement.focus();
53
58
  }
54
- buttonelement.focus();
55
59
  }
56
60
  $: reactToMenuShown(menushown);
57
61
  function move(idx) {
@@ -64,6 +68,8 @@ function move(idx) {
64
68
  buttonelement.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
65
69
  }
66
70
  function onkeydown(e) {
71
+ if (modifierKey(e))
72
+ return;
67
73
  if (e.key === 'ArrowDown') {
68
74
  e.preventDefault();
69
75
  if (menushown) {
@@ -74,7 +80,7 @@ function onkeydown(e) {
74
80
  }
75
81
  else {
76
82
  menushown = true;
77
- move(firstactive);
83
+ tick().then(() => move(firstactive));
78
84
  }
79
85
  }
80
86
  else if (e.key === 'ArrowUp') {
@@ -87,7 +93,7 @@ function onkeydown(e) {
87
93
  }
88
94
  else {
89
95
  menushown = true;
90
- move(lastactive);
96
+ tick().then(() => move(lastactive));
91
97
  }
92
98
  }
93
99
  else if (e.key === 'Enter') {
@@ -101,7 +107,7 @@ function onkeydown(e) {
101
107
  }
102
108
  else {
103
109
  menushown = true;
104
- move(firstactive);
110
+ tick().then(() => move(firstactive));
105
111
  }
106
112
  }
107
113
  else if (e.key === ' ') {
@@ -125,7 +131,7 @@ function onkeydown(e) {
125
131
  if (buttonelement.tagName !== 'INPUT') {
126
132
  e.preventDefault();
127
133
  menushown = true;
128
- move(firstactive);
134
+ tick().then(() => move(firstactive));
129
135
  }
130
136
  }
131
137
  }
@@ -135,14 +141,18 @@ function onkeydown(e) {
135
141
  menushown = false;
136
142
  }
137
143
  }
144
+ let blurTimer;
138
145
  function onbuttonclick(e) {
139
146
  e.preventDefault();
147
+ cancelAnimationFrame(blurTimer);
140
148
  menushown = !menushown;
141
149
  }
142
150
  async function onblur(e) {
143
151
  // tabindex=-1 on our menu elements means e.relatedTarget will be set
144
- if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget)))
145
- menushown = false;
152
+ if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
153
+ blurTimer = requestAnimationFrame(() => { if (buttonelement !== document.activeElement)
154
+ menushown = false; });
155
+ }
146
156
  }
147
157
  function cleanup(element) {
148
158
  if (element) {
@@ -20,6 +20,7 @@ declare const __propDef: {
20
20
  usePortal?: HTMLElement | true | undefined;
21
21
  emptyText?: string | undefined;
22
22
  value?: string | undefined;
23
+ hilited?: number | undefined;
23
24
  };
24
25
  events: {
25
26
  change: CustomEvent<any>;
@@ -1,6 +1,9 @@
1
+ export { default as CardLayout } from './CardLayout.svelte';
2
+ export { default as Card } from './Card.svelte';
1
3
  export { default as CollapsingTable } from './CollapsingTable.svelte';
2
4
  export { default as FocusLock } from './FocusLock.svelte';
3
5
  export { default as ConditionalWrapper } from './ConditionalWrapper.svelte';
4
6
  export { default as Modal } from './Modal.svelte';
7
+ export { default as MultiSelect } from './MultiSelect.svelte';
5
8
  export { default as PopupMenu } from './PopupMenu.svelte';
6
9
  export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
@@ -1,6 +1,9 @@
1
+ export { default as CardLayout } from './CardLayout.svelte';
2
+ export { default as Card } from './Card.svelte';
1
3
  export { default as CollapsingTable } from './CollapsingTable.svelte';
2
4
  export { default as FocusLock } from './FocusLock.svelte';
3
5
  export { default as ConditionalWrapper } from './ConditionalWrapper.svelte';
4
6
  export { default as Modal } from './Modal.svelte';
7
+ export { default as MultiSelect } from './MultiSelect.svelte';
5
8
  export { default as PopupMenu } from './PopupMenu.svelte';
6
9
  export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@txstate-mws/svelte-components",
3
- "version": "1.0.4",
3
+ "version": "1.1.3",
4
4
  "description": "Svelte components that are generically useful.",
5
5
  "dependencies": {
6
6
  "@txstate-mws/svelte-store": "^1.0.3",
@@ -8,25 +8,26 @@
8
8
  "txstate-utils": "^1.6.0"
9
9
  },
10
10
  "devDependencies": {
11
- "@babel/preset-env": "^7.16.11",
11
+ "@playwright/test": "^1.18.1",
12
12
  "@sveltejs/adapter-auto": "next",
13
13
  "@sveltejs/kit": "next",
14
- "@testing-library/jest-dom": "^5.16.1",
15
- "@testing-library/svelte": "^3.0.3",
16
- "@testing-library/user-event": "^13.5.0",
17
14
  "@types/tabbable": "^3.1.2",
18
15
  "eslint-config-standard-with-typescript": "^21.0.1",
19
16
  "eslint-plugin-svelte3": "^3.2.1",
20
17
  "svelte": "^3.44.0",
21
- "svelte2tsx": "^0.5.1",
22
18
  "svelte-check": "^2.2.6",
23
- "svelte-jester": "^2.3.1",
24
19
  "svelte-preprocess": "^4.9.4",
25
- "ts-jest": "^27.1.3",
20
+ "svelte2tsx": "^0.5.2",
26
21
  "tslib": "^2.3.1",
27
22
  "typescript": "^4.4.3"
28
23
  },
29
24
  "type": "module",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git@github.com:txstate-etc/svelte-components.git"
28
+ },
29
+ "author": "Nick Wing",
30
+ "license": "MIT",
30
31
  "exports": {
31
32
  "./package.json": "./package.json",
32
33
  "./actions/buttonify": "./actions/buttonify.js",
@@ -37,14 +38,18 @@
37
38
  "./actions/resize": "./actions/resize.js",
38
39
  "./actions/sticky": "./actions/sticky.js",
39
40
  "./actions/stickyfixed": "./actions/stickyfixed.js",
41
+ "./components/Card.svelte": "./components/Card.svelte",
42
+ "./components/CardLayout.svelte": "./components/CardLayout.svelte",
40
43
  "./components/CollapsingTable.svelte": "./components/CollapsingTable.svelte",
41
44
  "./components/ConditionalWrapper.svelte": "./components/ConditionalWrapper.svelte",
42
45
  "./components/FocusLock.svelte": "./components/FocusLock.svelte",
43
46
  "./components/Modal.svelte": "./components/Modal.svelte",
47
+ "./components/MultiSelect.svelte": "./components/MultiSelect.svelte",
44
48
  "./components/PopupMenu.svelte": "./components/PopupMenu.svelte",
45
49
  "./components/ScreenReaderOnly.svelte": "./components/ScreenReaderOnly.svelte",
46
50
  "./components": "./components/index.js",
47
51
  ".": "./index.js",
52
+ "./types/CardLayout": "./types/CardLayout.js",
48
53
  "./types/CollapsingTable": "./types/CollapsingTable.js",
49
54
  "./types/PopupMenu": "./types/PopupMenu.js",
50
55
  "./types": "./types/index.js",
@@ -0,0 +1,14 @@
1
+ import type { SettableSubject } from '@txstate-mws/svelte-store';
2
+ export declare const CARDLAYOUT: {};
3
+ export interface Block {
4
+ element: HTMLElement;
5
+ order?: SettableSubject<number>;
6
+ linebreak?: SettableSubject<boolean>;
7
+ width?: SettableSubject<string>;
8
+ height?: number;
9
+ }
10
+ export interface CardLayoutContext {
11
+ registerBlock: (block: Block) => Block;
12
+ recalculate: () => void;
13
+ gutter: SettableSubject<number>;
14
+ }
@@ -0,0 +1 @@
1
+ export const CARDLAYOUT = {};
package/types/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './CardLayout.js';
1
2
  export * from './CollapsingTable.js';
2
3
  export * from './PopupMenu.js';
package/types/index.js CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './CardLayout.js';
1
2
  export * from './CollapsingTable.js';
2
3
  export * from './PopupMenu.js';
package/util/util.d.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export declare function classes(...classNames: (string | undefined)[]): string;
2
2
  export declare function debounced(fn: Function, timeout: number): (...args: any[]) => void;
3
+ export declare function debouncedPromise<T>(fn: (..._: any) => Promise<T> | T, timeout: number, defaultValue: T): (..._: any) => Promise<T>;
4
+ export declare function modifierKey(e: any): any;
5
+ export declare function selectionIsLeft(ele: HTMLInputElement | HTMLTextAreaElement): boolean;
package/util/util.js CHANGED
@@ -8,3 +8,26 @@ export function debounced(fn, timeout) {
8
8
  timer = setTimeout(() => fn(...args), timeout);
9
9
  };
10
10
  }
11
+ export function debouncedPromise(fn, timeout, defaultValue) {
12
+ let version = 0;
13
+ return async (...args) => {
14
+ const myversion = ++version;
15
+ return await new Promise(resolve => {
16
+ setTimeout(() => {
17
+ if (version === myversion) {
18
+ version = 0;
19
+ resolve(fn(...args));
20
+ }
21
+ else {
22
+ resolve(defaultValue);
23
+ }
24
+ }, timeout);
25
+ });
26
+ };
27
+ }
28
+ export function modifierKey(e) {
29
+ return e.ctrlKey || e.altKey || e.metaKey || e.key === 'Insert';
30
+ }
31
+ export function selectionIsLeft(ele) {
32
+ return ele.selectionStart === 0 && ele.selectionEnd === 0;
33
+ }