@txstate-mws/svelte-components 1.0.3 → 1.1.2
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/README.md +22 -26
- package/actions/resize.js +14 -2
- 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 +179 -0
- package/components/MultiSelect.svelte.d.ts +35 -0
- package/components/PopupMenu.svelte +17 -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 +14 -9
- 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/repositioned.js +1 -2
- package/util/util.d.ts +3 -0
- package/util/util.js +23 -0
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
# svelte-components
|
|
2
|
+
A set of generically useful components for svelte.
|
|
3
|
+
|
|
4
|
+
## Components
|
|
5
|
+
- [CollapsingTable](/docs/CollapsingTable.md)
|
|
6
|
+
- [ConditionalWrapper](/docs/ConditionalWrapper.md)
|
|
7
|
+
- [FocusLock](/docs/Modal.md#FocusLock)
|
|
8
|
+
- [Modal](/docs/Modal.md#Modal)
|
|
9
|
+
- [PopupMenu](/docs/PopupMenu.md)
|
|
10
|
+
- [ScreenReaderOnly](/docs/ScreenReaderOnly.md)
|
|
11
|
+
|
|
12
|
+
## Actions
|
|
13
|
+
- [use:buttonify](/docs/actions.md#use:buttonify)
|
|
14
|
+
- [use:glue](/docs/actions.md#use:glue)
|
|
15
|
+
- [use:offset](/docs/actions.md#use:offset)
|
|
16
|
+
- [use:portal](/docs/actions.md#use:portal)
|
|
17
|
+
- [use:resize](/docs/actions.md#use:resize)
|
|
18
18
|
|
|
19
19
|
## Developing
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Installed dependencies with `npm install`, then start the development server:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
npm run dev
|
|
@@ -27,14 +27,10 @@ npm run dev
|
|
|
27
27
|
npm run dev -- --open
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
To create a production version of your app:
|
|
30
|
+
## Publishing to npm
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
|
-
npm run
|
|
33
|
+
npm run package
|
|
34
|
+
cd package
|
|
35
|
+
npm publish
|
|
36
36
|
```
|
|
37
|
-
|
|
38
|
-
You can preview the production build with `npm run preview`.
|
|
39
|
-
|
|
40
|
-
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment.
|
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) {
|
|
@@ -19,7 +31,7 @@ export function resize(el, config) {
|
|
|
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);
|
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,179 @@
|
|
|
1
|
+
<script >import { createEventDispatcher } from 'svelte';
|
|
2
|
+
import { randomid } 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
|
+
if (typeof document !== 'undefined' && inputelement === document.activeElement)
|
|
33
|
+
menushown = true;
|
|
34
|
+
}
|
|
35
|
+
$: reactToInput(inputvalue, debouncedGetOptions, selectedSet);
|
|
36
|
+
$: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available';
|
|
37
|
+
function addSelection(e) {
|
|
38
|
+
inputvalue = '';
|
|
39
|
+
const opt = { ...e.detail, label: e.detail.label || e.detail.value };
|
|
40
|
+
selected = [...selected, opt];
|
|
41
|
+
popupvalue = undefined;
|
|
42
|
+
}
|
|
43
|
+
function removeSelection(opt, idx, nextfocus = 1) {
|
|
44
|
+
if (hilitedpill === opt.value) {
|
|
45
|
+
hilitedpill = selected[idx + nextfocus]?.value;
|
|
46
|
+
}
|
|
47
|
+
selected = selected.filter(s => s.value !== opt.value);
|
|
48
|
+
}
|
|
49
|
+
function inputkeydown(e) {
|
|
50
|
+
if (modifierKey(e))
|
|
51
|
+
return;
|
|
52
|
+
if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
|
|
53
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length;
|
|
54
|
+
if (idx !== 0)
|
|
55
|
+
hilitedpill = selected[idx - 1].value;
|
|
56
|
+
}
|
|
57
|
+
else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
|
|
58
|
+
if (hilitedpill)
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined;
|
|
61
|
+
if (idx != null) {
|
|
62
|
+
if (idx === selected.length - 1)
|
|
63
|
+
hilitedpill = undefined;
|
|
64
|
+
else
|
|
65
|
+
hilitedpill = selected[idx + 1].value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
|
|
69
|
+
if (hilitedpill) {
|
|
70
|
+
const idx = selected.findIndex(s => s.value === hilitedpill);
|
|
71
|
+
removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1);
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
}
|
|
74
|
+
else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
|
|
75
|
+
removeSelection(selected[selected.length - 1], selected.length - 1, -1);
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function inputfocus() {
|
|
81
|
+
reactToInput();
|
|
82
|
+
}
|
|
83
|
+
let popuphilited;
|
|
84
|
+
$: if (popuphilited != null)
|
|
85
|
+
hilitedpill = undefined;
|
|
86
|
+
function reactToHilite(..._) {
|
|
87
|
+
if (!inputelement)
|
|
88
|
+
return;
|
|
89
|
+
if (hilitedpill) {
|
|
90
|
+
popuphilited = undefined;
|
|
91
|
+
inputelement.setAttribute('aria-activedescendant', id + hilitedpill);
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
inputelement.removeAttribute('aria-activedescendant');
|
|
95
|
+
}
|
|
96
|
+
$: reactToHilite(hilitedpill, id);
|
|
97
|
+
const dispatch = createEventDispatcher();
|
|
98
|
+
$: dispatch('change', selected);
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<fieldset>
|
|
102
|
+
<ul class="multiselect-selected" role="listbox" on:click={() => inputelement.focus()}>
|
|
103
|
+
{#each selected as option, i}
|
|
104
|
+
<li id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
|
|
105
|
+
on:click|preventDefault|stopPropagation={() => removeSelection(option, i, 1)}
|
|
106
|
+
aria-selected="true">
|
|
107
|
+
{option.label || option.value}
|
|
108
|
+
<ScreenReaderOnly>, click to remove</ScreenReaderOnly>
|
|
109
|
+
</li>
|
|
110
|
+
{/each}
|
|
111
|
+
<li class="input">
|
|
112
|
+
<input type="text" id={id} name={name} placeholder={placeholder}
|
|
113
|
+
bind:this={inputelement} bind:value={inputvalue}
|
|
114
|
+
on:focus={inputfocus} on:keydown={inputkeydown}
|
|
115
|
+
autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list"
|
|
116
|
+
aria-describedby="{descriptionid}">
|
|
117
|
+
</li>
|
|
118
|
+
</ul>
|
|
119
|
+
<ScreenReaderOnly id={descriptionid} arialive="assertive">
|
|
120
|
+
<span>{selected.length ? selected.length + ' selected' : 'select multiple, none selected'}, up down to choose, left right to hilite existing choices</span>
|
|
121
|
+
{#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
|
|
122
|
+
</ScreenReaderOnly>
|
|
123
|
+
<slot></slot>
|
|
124
|
+
</fieldset>
|
|
125
|
+
<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>
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
fieldset {
|
|
129
|
+
border: 0;
|
|
130
|
+
padding: 0;
|
|
131
|
+
margin: 0;
|
|
132
|
+
}
|
|
133
|
+
fieldset * {
|
|
134
|
+
box-sizing: border-box;
|
|
135
|
+
}
|
|
136
|
+
.multiselect-selected {
|
|
137
|
+
margin: 0;
|
|
138
|
+
padding: 0;
|
|
139
|
+
list-style: none;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
align-items: stretch;
|
|
143
|
+
padding: var(--multiselect-padding, 0.3em);
|
|
144
|
+
border: var(--multiselect-border, 1px solid #666666);
|
|
145
|
+
border-radius: var(--multiselect-radius, 0.3em);
|
|
146
|
+
}
|
|
147
|
+
.multiselect-selected:focus-within {
|
|
148
|
+
outline-width: var(--multiselect-focus-width, 2px);
|
|
149
|
+
outline-style: solid;
|
|
150
|
+
outline-color: var(--multiselect-focus-color, Highlight);
|
|
151
|
+
}
|
|
152
|
+
li.input {
|
|
153
|
+
flex-grow: 1;
|
|
154
|
+
}
|
|
155
|
+
input {
|
|
156
|
+
outline: 0;
|
|
157
|
+
border: 0;
|
|
158
|
+
min-width: 0;
|
|
159
|
+
width: 100%;
|
|
160
|
+
height: 100%;
|
|
161
|
+
}
|
|
162
|
+
.multiselect-pill {
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
flex-grow: 0;
|
|
165
|
+
margin-right: 0.3em;
|
|
166
|
+
line-height: 1;
|
|
167
|
+
border-radius: var(--multiselect-pill-radius, 0.8em);
|
|
168
|
+
border: var(--multiselect-pill-border, 1px solid gray);
|
|
169
|
+
background-color: var(--multiselect-pill-bg, transparent);
|
|
170
|
+
color: var(--multiselect-pill-text, black);
|
|
171
|
+
padding: var(--multiselect-pill-padding, 0.3em 0.5em);
|
|
172
|
+
}
|
|
173
|
+
.multiselect-pill.hilited {
|
|
174
|
+
outline: 0;
|
|
175
|
+
background-color: var(--multiselect-pill-selected, gray);
|
|
176
|
+
border: var(--multiselect-pill-selected-border, 1px solid transparent);
|
|
177
|
+
color: var(--multiselect-pill-selected-text, white);
|
|
178
|
+
}
|
|
179
|
+
</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,17 @@ 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
|
+
buttonelement.focus();
|
|
53
57
|
}
|
|
54
|
-
buttonelement.focus();
|
|
55
58
|
}
|
|
56
59
|
$: reactToMenuShown(menushown);
|
|
57
60
|
function move(idx) {
|
|
@@ -64,6 +67,8 @@ function move(idx) {
|
|
|
64
67
|
buttonelement.setAttribute('aria-activedescendant', `${menuid}-${hilited}`);
|
|
65
68
|
}
|
|
66
69
|
function onkeydown(e) {
|
|
70
|
+
if (modifierKey(e))
|
|
71
|
+
return;
|
|
67
72
|
if (e.key === 'ArrowDown') {
|
|
68
73
|
e.preventDefault();
|
|
69
74
|
if (menushown) {
|
|
@@ -74,7 +79,7 @@ function onkeydown(e) {
|
|
|
74
79
|
}
|
|
75
80
|
else {
|
|
76
81
|
menushown = true;
|
|
77
|
-
move(firstactive);
|
|
82
|
+
tick().then(() => move(firstactive));
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
else if (e.key === 'ArrowUp') {
|
|
@@ -87,7 +92,7 @@ function onkeydown(e) {
|
|
|
87
92
|
}
|
|
88
93
|
else {
|
|
89
94
|
menushown = true;
|
|
90
|
-
move(lastactive);
|
|
95
|
+
tick().then(() => move(lastactive));
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
else if (e.key === 'Enter') {
|
|
@@ -101,7 +106,7 @@ function onkeydown(e) {
|
|
|
101
106
|
}
|
|
102
107
|
else {
|
|
103
108
|
menushown = true;
|
|
104
|
-
move(firstactive);
|
|
109
|
+
tick().then(() => move(firstactive));
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
else if (e.key === ' ') {
|
|
@@ -125,7 +130,7 @@ function onkeydown(e) {
|
|
|
125
130
|
if (buttonelement.tagName !== 'INPUT') {
|
|
126
131
|
e.preventDefault();
|
|
127
132
|
menushown = true;
|
|
128
|
-
move(firstactive);
|
|
133
|
+
tick().then(() => move(firstactive));
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
}
|
|
@@ -135,14 +140,17 @@ function onkeydown(e) {
|
|
|
135
140
|
menushown = false;
|
|
136
141
|
}
|
|
137
142
|
}
|
|
143
|
+
let blurTimer;
|
|
138
144
|
function onbuttonclick(e) {
|
|
139
145
|
e.preventDefault();
|
|
146
|
+
cancelAnimationFrame(blurTimer);
|
|
140
147
|
menushown = !menushown;
|
|
141
148
|
}
|
|
142
149
|
async function onblur(e) {
|
|
143
150
|
// 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;
|
|
151
|
+
if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
|
|
152
|
+
blurTimer = requestAnimationFrame(() => { menushown = false; });
|
|
153
|
+
}
|
|
146
154
|
}
|
|
147
155
|
function cleanup(element) {
|
|
148
156
|
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,32 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@txstate-mws/svelte-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Svelte components that are generically useful.",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@txstate-mws/svelte-store": "^1.0.
|
|
6
|
+
"@txstate-mws/svelte-store": "^1.0.3",
|
|
7
7
|
"tabbable": "^5.2.1",
|
|
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/repositioned.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import equal from '
|
|
2
|
-
import { isNotNull, randomid, toArray } from 'txstate-utils';
|
|
1
|
+
import { equal, isNotNull, randomid, toArray } from 'txstate-utils';
|
|
3
2
|
import { bodyOffset, targetOffset } from './bodyoffset.js';
|
|
4
3
|
const observers = new Map();
|
|
5
4
|
let mutationobserver;
|
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
|
+
}
|