@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 +16 -4
- package/actions/sticky.js +1 -1
- package/actions/stickyfixed.js +1 -1
- package/components/Card.svelte +23 -0
- package/components/Card.svelte.d.ts +18 -0
- package/components/CardLayout.svelte +197 -0
- package/components/CardLayout.svelte.d.ts +21 -0
- package/components/MultiSelect.svelte +181 -0
- package/components/MultiSelect.svelte.d.ts +35 -0
- package/components/PopupMenu.svelte +19 -9
- package/components/PopupMenu.svelte.d.ts +1 -0
- package/components/index.d.ts +3 -0
- package/components/index.js +3 -0
- package/package.json +13 -8
- package/types/CardLayout.d.ts +14 -0
- package/types/CardLayout.js +1 -0
- package/types/index.d.ts +1 -0
- package/types/index.js +1 -0
- package/util/util.d.ts +3 -0
- package/util/util.js +23 -0
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 ??
|
|
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.
|
|
30
|
+
config.store.update(v => ({ ...v, ...current }));
|
|
19
31
|
el.dispatchEvent(new CustomEvent('resize', { detail: current }));
|
|
20
32
|
}
|
|
21
33
|
}
|
|
22
|
-
if (
|
|
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?.
|
|
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
package/actions/stickyfixed.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/components/index.d.ts
CHANGED
|
@@ -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/components/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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
package/types/index.js
CHANGED
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
|
+
}
|