@dosgato/dialog 0.0.39 → 0.0.42
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/FieldAutocomplete.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>import { nullableSerialize, nullableDeserialize } from '@txstate-mws/svelte-forms';
|
|
2
2
|
import FieldStandard from './FieldStandard.svelte';
|
|
3
|
-
import { randomid } from 'txstate-utils';
|
|
4
|
-
import { PopupMenu, ScreenReaderOnly } from '@txstate-mws/svelte-components';
|
|
3
|
+
import { isBlank, isNotBlank, randomid } from 'txstate-utils';
|
|
4
|
+
import { modifierKey, PopupMenu, ScreenReaderOnly } from '@txstate-mws/svelte-components';
|
|
5
5
|
import { getDescribedBy } from './';
|
|
6
6
|
import { onMount } from 'svelte';
|
|
7
7
|
export let id = undefined;
|
|
@@ -13,47 +13,63 @@ export { className as class };
|
|
|
13
13
|
export let notNull = false;
|
|
14
14
|
export let disabled = false;
|
|
15
15
|
export let choices;
|
|
16
|
-
export let defaultValue =
|
|
16
|
+
export let defaultValue = undefined;
|
|
17
17
|
export let conditional = undefined;
|
|
18
18
|
export let required = false;
|
|
19
19
|
export let inputelement = undefined;
|
|
20
20
|
export let helptext = undefined;
|
|
21
21
|
let inputvalue = '';
|
|
22
22
|
let popupvalue = undefined;
|
|
23
|
-
let savedLabel = '';
|
|
24
|
-
let changed = false;
|
|
25
23
|
let menuid;
|
|
26
24
|
const liveTextId = randomid();
|
|
27
|
-
$:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
$: finalserialize = !notNull ? nullableSerialize : v => v;
|
|
26
|
+
$: finaldeserialize = !notNull ? nullableDeserialize : v => v;
|
|
27
|
+
$: valueToLabel = Object.fromEntries(choices.map(c => [c.value, c.label || c.value]));
|
|
28
|
+
$: labelToValue = Object.fromEntries(choices.map(c => [c.label || c.value, c.value]));
|
|
29
|
+
$: filteredChoices = choices.filter((item) => {
|
|
30
|
+
return item.label?.toLowerCase().includes(inputvalue.toLowerCase()) || item.value.toLowerCase().includes(inputvalue.toLowerCase());
|
|
31
|
+
});
|
|
32
|
+
let menushown = false;
|
|
33
|
+
let savedVal = defaultValue;
|
|
34
|
+
function onKeyUp(setVal) {
|
|
35
|
+
return (e) => {
|
|
36
|
+
if (!modifierKey(e)) {
|
|
37
|
+
const val = labelToValue[inputvalue.trim()];
|
|
38
|
+
menushown = !val;
|
|
39
|
+
// we need to make sure the form state stays up to date with what's shown in the text field
|
|
40
|
+
// if the text field has only a substring of a valid label the form state value should remain
|
|
41
|
+
// empty, but once the text field has a valid label the form state should be updated
|
|
42
|
+
if (savedVal !== val) {
|
|
43
|
+
savedVal = val;
|
|
44
|
+
setVal(val);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
32
49
|
function onchangepopup(setVal) {
|
|
33
50
|
return (e) => {
|
|
34
51
|
inputvalue = e.detail.label || e.detail.value;
|
|
35
|
-
savedLabel = inputvalue;
|
|
36
52
|
popupvalue = undefined;
|
|
53
|
+
savedVal = e.detail.value;
|
|
37
54
|
setVal(e.detail.value);
|
|
38
|
-
changed = false;
|
|
39
55
|
};
|
|
40
56
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
function reactToValue(value, setVal) {
|
|
58
|
+
const dsvalue = finaldeserialize(value);
|
|
59
|
+
if (dsvalue !== savedVal) {
|
|
60
|
+
const label = valueToLabel[dsvalue];
|
|
61
|
+
// if the form state value changes from the outside we need to replace the text field content
|
|
62
|
+
// with the new label
|
|
63
|
+
// if the new form state value is undefined, we should let them keep any half-finished typing they
|
|
64
|
+
// might have in the field, but if they have a fully valid entry in the field, it needs to disappear
|
|
65
|
+
// to reflect the new reality that the form value has gone away
|
|
66
|
+
if (label != null || (savedVal && valueToLabel[savedVal]))
|
|
67
|
+
inputvalue = label ?? '';
|
|
68
|
+
savedVal = dsvalue;
|
|
69
|
+
// if the form state value changes from the outside to an invalid value, we have to clear it out
|
|
70
|
+
if (isNotBlank(dsvalue) && isBlank(label))
|
|
71
|
+
setVal(finaldeserialize(''));
|
|
72
|
+
}
|
|
57
73
|
}
|
|
58
74
|
let portal;
|
|
59
75
|
onMount(() => {
|
|
@@ -61,10 +77,10 @@ onMount(() => {
|
|
|
61
77
|
});
|
|
62
78
|
</script>
|
|
63
79
|
|
|
64
|
-
<FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {helptext} serialize={
|
|
65
|
-
{@const _ =
|
|
66
|
-
<input bind:this={inputelement} bind:value={inputvalue} {id} {placeholder} class="dialog-input {className}" class:valid class:invalid aria-invalid={invalid} aria-expanded={false} aria-controls={menuid} on:blur={onBlur} on:
|
|
67
|
-
<PopupMenu bind:menuid align="bottomleft" usePortal={portal} items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
|
|
80
|
+
<FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {helptext} serialize={finalserialize} deserialize={finaldeserialize} let:value let:setVal let:valid let:invalid let:id let:onBlur let:messagesid let:helptextid>
|
|
81
|
+
{@const _ = reactToValue(value, setVal)}
|
|
82
|
+
<input bind:this={inputelement} bind:value={inputvalue} {id} {placeholder} class="dialog-input {className}" class:valid class:invalid aria-invalid={invalid} aria-expanded={false} aria-controls={menuid} on:blur={onBlur} on:keyup={onKeyUp(setVal)} autocapitalize="none" type="text" autocomplete="off" aria-autocomplete="list" role="combobox" {disabled} aria-describedby={getDescribedBy([messagesid, helptextid])}>
|
|
83
|
+
<PopupMenu bind:menushown bind:menuid align="bottomleft" usePortal={portal} items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
|
|
68
84
|
<ScreenReaderOnly arialive="polite" ariaatomic={true} id={liveTextId}>
|
|
69
85
|
{filteredChoices.length} {filteredChoices.length === 1 ? 'option' : 'options'} available.
|
|
70
86
|
</ScreenReaderOnly>
|
|
@@ -13,7 +13,7 @@ declare const __propDef: {
|
|
|
13
13
|
value: string;
|
|
14
14
|
disabled?: boolean | undefined;
|
|
15
15
|
}[];
|
|
16
|
-
defaultValue?:
|
|
16
|
+
defaultValue?: string | undefined;
|
|
17
17
|
conditional?: boolean | undefined;
|
|
18
18
|
required?: boolean | undefined;
|
|
19
19
|
inputelement?: HTMLInputElement | undefined;
|
package/FieldMultiselect.svelte
CHANGED
|
@@ -11,6 +11,7 @@ export let disabled = false;
|
|
|
11
11
|
export let defaultValue = [];
|
|
12
12
|
export let conditional = undefined;
|
|
13
13
|
export let required = false;
|
|
14
|
+
export let maxSelections = 0;
|
|
14
15
|
export let getOptions;
|
|
15
16
|
// each time we run getOptions we will save the value -> label mappings
|
|
16
17
|
// that it finds, so that we can display labels on pills
|
|
@@ -54,7 +55,7 @@ async function reactToValue(value) {
|
|
|
54
55
|
<FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} let:value let:valid let:invalid let:id let:onBlur let:setVal>
|
|
55
56
|
{@const _ = reactToValue(value)}
|
|
56
57
|
<div class:valid class:invalid>
|
|
57
|
-
<MultiSelect {id} name={path} usePortal={portal} {disabled} selected={$selectedStore} {placeholder} getOptions={wrapGetOptions} on:change={e => setVal(e.detail.map(itm => itm.value))} on:blur={onBlur}></MultiSelect>
|
|
58
|
+
<MultiSelect {id} name={path} usePortal={portal} {disabled} {maxSelections} selected={$selectedStore} {placeholder} getOptions={wrapGetOptions} on:change={e => setVal(e.detail.map(itm => itm.value))} on:blur={onBlur}></MultiSelect>
|
|
58
59
|
</div>
|
|
59
60
|
</FieldStandard>
|
|
60
61
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dosgato/dialog",
|
|
3
3
|
"description": "A component library for building forms that edit a JSON document.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.42",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@txstate-mws/svelte-components": "^1.3.
|
|
6
|
+
"@txstate-mws/svelte-components": "^1.3.9",
|
|
7
7
|
"@txstate-mws/svelte-forms": "^1.3.4",
|
|
8
8
|
"@iconify/svelte": "^3.0.0",
|
|
9
9
|
"@iconify-icons/mdi": "^1.2.22",
|
package/tree/Tree.svelte
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { derivedStore, Store } from '@txstate-mws/svelte-store';
|
|
3
3
|
import { afterUpdate, beforeUpdate, onDestroy, onMount, setContext } from 'svelte';
|
|
4
4
|
import { hashid } from 'txstate-utils';
|
|
5
|
+
import LoadIcon from './LoadIcon.svelte';
|
|
5
6
|
import TreeNode from './TreeNode.svelte';
|
|
6
7
|
import { TreeStore, TREE_STORE_CONTEXT } from './treestore';
|
|
7
8
|
export let headers;
|
|
@@ -92,16 +93,15 @@ function headerDragEnd() {
|
|
|
92
93
|
function headerDragReset() {
|
|
93
94
|
store.resetHeaderOverride();
|
|
94
95
|
}
|
|
95
|
-
let mounted = false;
|
|
96
96
|
onMount(async () => {
|
|
97
97
|
document.addEventListener('dragend', onDragEnd);
|
|
98
|
+
const saveFocusId = $store.focused?.id;
|
|
98
99
|
await store.refresh();
|
|
99
|
-
if ($store.focused?.id) {
|
|
100
|
+
if ($store.focused?.id && $store.focused.id === saveFocusId) {
|
|
100
101
|
const el = document.getElementById(hashid($store.focused.id));
|
|
101
102
|
el?.scrollIntoView({ block: 'center' });
|
|
102
103
|
}
|
|
103
104
|
headerSizes.set(calcHeaderSizes()); // seems to need a kick on first mount
|
|
104
|
-
mounted = true;
|
|
105
105
|
});
|
|
106
106
|
onDestroy(() => {
|
|
107
107
|
document.removeEventListener('dragend', onDragEnd);
|
|
@@ -123,15 +123,15 @@ afterUpdate(() => {
|
|
|
123
123
|
|
|
124
124
|
<svelte:window on:mouseup={headerDragEnd} />
|
|
125
125
|
|
|
126
|
-
<div class="tree-header" class:
|
|
126
|
+
<div class="tree-header" class:resizing={!!dragheaderid} use:resize={{ store: treeWidth }} aria-hidden="true" on:mouseup={headerDragEnd} on:touchend={headerDragEnd} on:mousemove={dragheaderid ? headerDrag : undefined} on:touchmove={dragheaderid ? headerDrag : undefined}>
|
|
127
127
|
<div class="checkbox" bind:this={checkboxelement}> </div>
|
|
128
128
|
{#each headers as header, i (header.label)}
|
|
129
|
-
<div bind:this={headerelements[i]} id={header.id} class="tree-header-cell {header.id}" style:width={$headerOverride[header.id] ?? $headerSizes?.[i]}>{header.label}</div>
|
|
129
|
+
<div bind:this={headerelements[i]} id={header.id} class="tree-header-cell {header.id}" style:width={$headerOverride[header.id] ?? $headerSizes?.[i]}>{header.label}{#if i === 0 && $store.loading}<LoadIcon />{/if}</div>
|
|
130
130
|
{#if enableResize && i !== headers.length - 1}<div class="tree-separator" on:mousedown={headerDragStart(header, i)} on:touchstart={headerDragStart(header, i)} on:dblclick={headerDragReset}> </div>{/if}
|
|
131
131
|
{/each}
|
|
132
132
|
</div>
|
|
133
133
|
{#if $rootItems?.length}
|
|
134
|
-
<ul bind:this={store.treeElement}
|
|
134
|
+
<ul bind:this={store.treeElement} role="tree" class:resizing={!!dragheaderid} on:mousemove={dragheaderid ? headerDrag : undefined} on:touchmove={dragheaderid ? headerDrag : undefined} on:mouseup={headerDragEnd} on:touchend={headerDragEnd}>
|
|
135
135
|
{#each $rootItems as item, i (item.id)}
|
|
136
136
|
<TreeNode
|
|
137
137
|
{item}
|
|
@@ -161,10 +161,6 @@ afterUpdate(() => {
|
|
|
161
161
|
left: 0;
|
|
162
162
|
z-index: 1;
|
|
163
163
|
font-size: 0.9em;
|
|
164
|
-
opacity: 0;
|
|
165
|
-
}
|
|
166
|
-
.tree-header.mounted {
|
|
167
|
-
opacity: 1;
|
|
168
164
|
}
|
|
169
165
|
.tree-header.resizing {
|
|
170
166
|
cursor: col-resize;
|
|
@@ -200,10 +196,6 @@ afterUpdate(() => {
|
|
|
200
196
|
margin: 0;
|
|
201
197
|
list-style: none;
|
|
202
198
|
font-size: 0.9em;
|
|
203
|
-
opacity: 0;
|
|
204
|
-
}
|
|
205
|
-
ul.mounted {
|
|
206
|
-
opacity: 1;
|
|
207
199
|
}
|
|
208
200
|
:global([data-eq~="650px"]) ul {
|
|
209
201
|
font-size: 0.8em;
|
package/tree/treestore.js
CHANGED
|
@@ -101,6 +101,8 @@ export class TreeStore extends ActiveStore {
|
|
|
101
101
|
this.value.rootItems = children;
|
|
102
102
|
}
|
|
103
103
|
this.addLookup(children);
|
|
104
|
+
// if any selected items disappeared in the refresh, we need to remove them from the selection map
|
|
105
|
+
this.cleanSelected();
|
|
104
106
|
// if the focused item disappeared in the refresh, we need to replace it,
|
|
105
107
|
// as without a focus the tree becomes invisible to keyboard nav
|
|
106
108
|
if (!this.value.itemsById[this.value.focused?.id ?? ''])
|