@dosgato/dialog 0.0.54 → 0.0.56

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.
@@ -46,7 +46,7 @@ function onKeyDown(choice, idx) {
46
46
  {/if}
47
47
  <!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
48
48
  <ul class="dialog-btn-group" class:disabled class:valid class:invalid aria-disabled={disabled} role="radiogroup" aria-labelledby={groupid} on:blur>
49
- {#each choices as choice, i}
49
+ {#each choices as choice, i (choice.value)}
50
50
  {@const selected = choice.value === value}
51
51
  <li bind:this={elements[i]} role="radio" class:selected aria-checked={selected} tabindex={selected ? 0 : -1} aria-controls={ariaControls} on:click={onClick(choice, i)} on:keydown={onKeyDown(choice, i)} on:blur aria-describedby="{groupid} {messagesid}">{choice.label || choice.value}</li>
52
52
  {/each}
@@ -4,7 +4,7 @@
4
4
  flex. Ordering is top down by default but can be order horizontally by toggling `leftToRight`.
5
5
  The value of the field will be an array corresponding to the values of the checkboxes that are checked.
6
6
  -->
7
- <script>import { getContext } from 'svelte';
7
+ <script>import { getContext, onMount } from 'svelte';
8
8
  import { Field, FORM_CONTEXT } from '@txstate-mws/svelte-forms';
9
9
  import { derivedStore } from '@txstate-mws/svelte-store';
10
10
  import { randomid } from 'txstate-utils';
@@ -46,12 +46,28 @@ function onChangeCheckbox(setVal, choice, included) {
46
46
  });
47
47
  }
48
48
  const descid = randomid();
49
+ let val, stVal;
50
+ function updateValue(valu, sVal) {
51
+ val = valu;
52
+ stVal = sVal;
53
+ }
54
+ function reactToChoices(..._) {
55
+ if (!stVal)
56
+ return;
57
+ const choiceSet = new Set(choices?.map(c => c.value));
58
+ const filtered = val?.filter(v => choiceSet.has(v));
59
+ if (filtered?.length !== val?.length)
60
+ stVal(filtered);
61
+ }
62
+ $: reactToChoices(choices);
63
+ onMount(reactToChoices);
49
64
  </script>
50
65
 
51
66
  <Field {path} {defaultValue} {conditional} let:path let:value let:onBlur let:setVal let:messages let:valid let:invalid>
67
+ {@const _ = updateValue(value, setVal)}
52
68
  <Container {id} {label} {messages} {descid} {related} {helptext} let:messagesid let:helptextid>
53
69
  <div class="dialog-choices {className}" class:valid class:invalid>
54
- {#each choices as choice, idx}
70
+ {#each choices as choice, idx (choice.value)}
55
71
  {@const checkid = `${path}.${idx}`}
56
72
  {@const included = value && value.includes(choice.value)}
57
73
  {@const label = choice.label || (typeof choice.value === 'string' ? choice.value : '')}
@@ -1,10 +1,16 @@
1
+ <script context="module">function reconstructUrl(url) {
2
+ const urlToTest = (/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])/.test(url)) ? `https://${url}` : url;
3
+ return new URL(urlToTest).toString();
4
+ }
5
+ </script>
1
6
  <script>import { FORM_CONTEXT, FORM_INHERITED_PATH } from '@txstate-mws/svelte-forms';
2
7
  import { getContext } from 'svelte';
3
- import { isNotBlank, randomid } from 'txstate-utils';
8
+ import { isBlank, isNotBlank, randomid } from 'txstate-utils';
4
9
  import { Chooser, ChooserStore, CHOOSER_API_CONTEXT } from './chooser';
5
10
  import Details from './chooser/Details.svelte';
6
11
  import Thumbnail from './chooser/Thumbnail.svelte';
7
12
  import FieldStandard from './FieldStandard.svelte';
13
+ import InlineMessage from './InlineMessage.svelte';
8
14
  import { getDescribedBy } from './';
9
15
  export let id = undefined;
10
16
  export let path;
@@ -56,6 +62,11 @@ function userUrlEntry() {
56
62
  async function userUrlEntryDebounced() {
57
63
  const url = this.value;
58
64
  store.clearPreview();
65
+ if (isBlank(url)) {
66
+ selectedAsset = undefined;
67
+ formStore.setField(finalPath, undefined);
68
+ return;
69
+ }
59
70
  let found = false;
60
71
  if (chooserClient.findByUrl) {
61
72
  const item = await chooserClient.findByUrl(url);
@@ -83,18 +94,24 @@ async function userUrlEntryDebounced() {
83
94
  }
84
95
  }
85
96
  if (!found) {
86
- try {
87
- selectedAsset = {
88
- type: 'raw',
89
- id: urlToValueCache[url] ?? chooserClient.urlToValue?.(new URL(url).toString()),
90
- url
91
- };
97
+ if (urlToValueCache[url]) {
98
+ selectedAsset = { type: 'raw', id: urlToValueCache[url], url };
92
99
  }
93
- catch {
94
- // here we "select" a raw url so that we do not interrupt the users' typing, but
95
- // we set its id to 'undefined' so that nothing makes it into the form until it's
96
- // a valid URL
97
- selectedAsset = { type: 'raw', url, id: undefined };
100
+ else {
101
+ try {
102
+ const reconstructed = reconstructUrl(url);
103
+ selectedAsset = {
104
+ type: 'raw',
105
+ id: chooserClient.urlToValue?.(reconstructed) ?? reconstructed,
106
+ url
107
+ };
108
+ }
109
+ catch {
110
+ // here we "select" a raw url so that we do not interrupt the users' typing, but
111
+ // we set its id to 'undefined' so that nothing makes it into the form until it's
112
+ // a valid URL
113
+ selectedAsset = { type: 'raw', id: undefined, url };
114
+ }
98
115
  }
99
116
  }
100
117
  formStore.setField(finalPath, selectedAsset?.id);
@@ -112,7 +129,7 @@ async function updateSelected(..._) {
112
129
  if (!selectedAsset) {
113
130
  const urlFromValue = chooserClient.valueToUrl?.($value) ?? $value;
114
131
  try {
115
- selectedAsset = { type: 'raw', id: $value, url: new URL(urlFromValue).toString() };
132
+ selectedAsset = { type: 'raw', id: $value, url: reconstructUrl(urlFromValue) };
116
133
  }
117
134
  catch {
118
135
  selectedAsset = { type: 'broken', id: $value, url: $value };
@@ -129,7 +146,7 @@ $: updateSelected($value);
129
146
  </script>
130
147
 
131
148
  <FieldStandard bind:id {path} {descid} {label} {defaultValue} {conditional} {required} {related} {helptext} let:value let:messagesid let:helptextid let:valid let:invalid let:id let:onBlur let:setVal>
132
- {#if selectedAsset}
149
+ {#if selectedAsset?.id}
133
150
  <div class="dialog-chooser-container">
134
151
  <Thumbnail item={selectedAsset} />
135
152
  <Details item={selectedAsset} />
@@ -141,6 +158,9 @@ $: updateSelected($value);
141
158
  {/if}
142
159
  <button type="button" on:click={show} aria-describedby={getDescribedBy([descid, messagesid, helptextid, extradescid])}>Select {#if value}New{/if} {#if assets && pages}Link Target{:else if images}Image{:else if assets}Asset{:else}Page{/if}</button>
143
160
  </div>
161
+ {#if selectedAsset?.url.length && !selectedAsset.id}
162
+ <InlineMessage message={{ message: 'Entry does not match an internal resource and is not a valid URL. Nothing will be saved.', type: 'warning' }} />
163
+ {/if}
144
164
  {#if modalshown}
145
165
  <Chooser {store} {label} {pages} {assets} {images} {initialSource} {initialPath} {folders} {required} on:change={onChange(setVal)} on:escape={hide} />
146
166
  {/if}
@@ -1,4 +1,5 @@
1
1
  <script>import { Field } from '@txstate-mws/svelte-forms';
2
+ import { onMount } from 'svelte';
2
3
  import Switcher from './Switcher.svelte';
3
4
  let className = '';
4
5
  export { className as class };
@@ -20,8 +21,25 @@ export let datetime = false;
20
21
  export let boolean = false;
21
22
  export let serialize = undefined;
22
23
  export let deserialize = undefined;
24
+ let val, stVal, finalDeserialize;
25
+ function updateValue(valu, sVal, fDes) {
26
+ val = valu;
27
+ stVal = sVal;
28
+ finalDeserialize = fDes;
29
+ }
30
+ function reactToChoices(..._) {
31
+ if (!stVal)
32
+ return;
33
+ if (!choices.length)
34
+ stVal(finalDeserialize(''));
35
+ if (!choices.some(o => o.value === val))
36
+ stVal(notNull ? choices[0].value : finalDeserialize(''));
37
+ }
38
+ $: reactToChoices(choices);
39
+ onMount(reactToChoices);
23
40
  </script>
24
41
 
25
- <Field {path} {defaultValue} {conditional} {notNull} {number} {date} {datetime} {boolean} {serialize} {deserialize} let:value let:valid let:invalid let:onBlur let:onChange let:messages let:serialize>
42
+ <Field {path} {defaultValue} {conditional} {notNull} {number} {date} {datetime} {boolean} {serialize} {deserialize} let:value let:valid let:invalid let:onBlur let:onChange let:messages let:serialize let:setVal let:deserialize>
43
+ {@const _ = updateValue(value, setVal, deserialize)}
26
44
  <Switcher bind:id class={className} name={path} {horizontal} {label} iptValue={value} {valid} {invalid} {required} {related} {extradescid} {helptext} {messages} on:change={onChange} {onBlur} choices={choices.map(c => ({ ...c, value: serialize(c.value) }))} />
27
45
  </Field>
@@ -1,4 +1,5 @@
1
- <script>import { getDescribedBy } from './';
1
+ <script>import { onMount } from 'svelte';
2
+ import { getDescribedBy } from './';
2
3
  import FieldStandard from './FieldStandard.svelte';
3
4
  let className = '';
4
5
  export { className as class };
@@ -22,9 +23,26 @@ export let datetime = false;
22
23
  export let boolean = false;
23
24
  export let serialize = undefined;
24
25
  export let deserialize = undefined;
26
+ let val, stVal, finalDeserialize;
27
+ function updateValue(valu, sVal, fDes) {
28
+ val = valu;
29
+ stVal = sVal;
30
+ finalDeserialize = fDes;
31
+ }
32
+ function reactToChoices(..._) {
33
+ if (!stVal)
34
+ return;
35
+ if (!choices.length)
36
+ stVal(finalDeserialize(''));
37
+ if (!choices.some(o => o.value === val))
38
+ stVal(notNull ? choices[0].value : finalDeserialize(''));
39
+ }
40
+ $: reactToChoices(choices);
41
+ onMount(reactToChoices);
25
42
  </script>
26
43
 
27
- <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} {notNull} {number} {date} {datetime} {boolean} {serialize} {deserialize} let:value let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid let:serialize>
44
+ <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} {notNull} {number} {date} {datetime} {boolean} {serialize} {deserialize} let:value let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid let:serialize let:deserialize let:setVal>
45
+ {@const _ = updateValue(value, setVal, deserialize)}
28
46
  <select bind:this={inputelement} {id} name={path} {disabled} class="dialog-input dialog-select {className}" on:change={onChange} on:blur={onBlur} class:valid class:invalid aria-describedby={getDescribedBy([messagesid, helptextid, extradescid])}>
29
47
  {#if !notNull}<option value="" selected={!value}>{placeholder}</option>{/if}
30
48
  {#each choices as choice (choice.value)}
@@ -32,7 +32,7 @@ $: width = (horizontal ? 100 / Math.min(choices.length, choices.length === 4 &&
32
32
  </script>
33
33
  <Container {id} {label} {messages} descid={groupid} {required} {related} {helptext} let:helptextid>
34
34
  <div class="dialog-radio {className}" use:eq={{ store }} class:horizontal role="radiogroup" aria-labelledby={groupid} class:valid class:invalid>
35
- {#each choices as choice, idx}
35
+ {#each choices as choice, idx (choice.value)}
36
36
  {@const radioid = `${groupid}.${idx}`}
37
37
  <label for={radioid} style:width>
38
38
  <Radio id={radioid} {name} value={choice.value} selected={iptValue === choice.value} disabled={choice.disabled} {onChange} {onBlur} {helptextid} {extradescid} />
@@ -17,6 +17,7 @@ interface ITabStore extends ElementSize {
17
17
  visited: Record<string, boolean>;
18
18
  tabids: Record<string, string>;
19
19
  panelids: Record<string, string>;
20
+ accordionOnMobile: boolean;
20
21
  }
21
22
  export declare class TabStore extends Store<ITabStore> {
22
23
  initialTab?: string | undefined;
package/dist/TabStore.js CHANGED
@@ -19,7 +19,8 @@ export class TabStore extends Store {
19
19
  requireNext: false,
20
20
  tabids: tabs.reduce((acc, curr) => ({ ...acc, [curr.name]: randomid() }), {}),
21
21
  panelids: tabs.reduce((acc, curr) => ({ ...acc, [curr.name]: randomid() }), {}),
22
- clientWidth: 1024
22
+ clientWidth: 1024,
23
+ accordionOnMobile: true
23
24
  }));
24
25
  this.initialTab = initialTab;
25
26
  }
@@ -49,7 +50,7 @@ export class TabStore extends Store {
49
50
  return derivedStore(this, v => v.panelids[v.tabs[v.current].name]);
50
51
  }
51
52
  accordion() {
52
- return derivedStore(this, v => v.clientWidth && v.clientWidth < 500);
53
+ return derivedStore(this, v => v.accordionOnMobile && v.clientWidth && v.clientWidth < 500);
53
54
  }
54
55
  left() {
55
56
  this.update(v => ({ ...v, current: Math.max(0, v.current - 1) }));
package/dist/Tabs.svelte CHANGED
@@ -10,7 +10,7 @@ export let active = undefined;
10
10
  export let store = new TabStore(tabs, active);
11
11
  export let disableDialogControl = false;
12
12
  export let accordionOnMobile = true;
13
- $: store.update(v => ({ ...v, tabs }));
13
+ $: store.update(v => ({ ...v, tabs, accordionOnMobile }));
14
14
  const activeStore = new Store({});
15
15
  const tabelements = [];
16
16
  let activeelement;
@@ -30,7 +30,7 @@ const accordion = store.accordion();
30
30
  $: cols = Math.min(Math.floor(($store.clientWidth ?? 1024) / 90), $store.tabs.length);
31
31
  $: scalefactor = Math.min(roundTo(($store.clientWidth ?? 1024) / (cols * 130), 4), 1);
32
32
  $: wrapping = cols !== $store.tabs.length;
33
- $: dialogContext.hasTabs = !$accordion || !accordionOnMobile;
33
+ $: dialogContext.hasTabs = !$accordion;
34
34
  function onClick(idx) {
35
35
  return () => store.activate(idx);
36
36
  }
@@ -87,7 +87,7 @@ onMount(reactToCurrent);
87
87
  </script>
88
88
 
89
89
  {#if $store.tabs.length > 1}
90
- {#if !$accordion || !accordionOnMobile}
90
+ {#if !$accordion}
91
91
  <!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
92
92
  <ul use:resize={{ store }} class="tabs-buttons" role="tablist">
93
93
  {#each $store.tabs as tab, idx (tab.name)}
@@ -1,6 +1,7 @@
1
- <script>import FieldStandard from '../FieldStandard.svelte';
2
- import { Radio } from '..';
1
+ <script>import { onMount } from 'svelte';
3
2
  import { randomid, shouldUseWhiteText } from 'txstate-utils';
3
+ import FieldStandard from '../FieldStandard.svelte';
4
+ import { Radio } from '..';
4
5
  export let id = undefined;
5
6
  let className = '';
6
7
  export { className as class };
@@ -14,15 +15,31 @@ export let defaultValue = notNull ? (addAllOption ? 'alternating' : options[0].v
14
15
  export let conditional = undefined;
15
16
  export let helptext = undefined;
16
17
  const groupid = randomid();
18
+ let val, stVal;
19
+ function updateValue(valu, sVal) {
20
+ val = valu;
21
+ stVal = sVal;
22
+ }
23
+ function reactToOptions(..._) {
24
+ if (!stVal)
25
+ return;
26
+ if (!options.length)
27
+ stVal(addAllOption ? 'alternating' : undefined);
28
+ if (val !== 'alternating' && !options.some(o => o.value === val))
29
+ stVal(notNull ? options[0].value : (addAllOption ? 'alternating' : undefined));
30
+ }
31
+ $: reactToOptions(options);
32
+ onMount(reactToOptions);
17
33
  </script>
18
34
 
19
- <FieldStandard bind:id descid={groupid} {path} {label} {required} {defaultValue} {conditional} {helptext} let:value let:valid let:invalid let:onBlur let:onChange let:messagesid let:helptextid>
35
+ <FieldStandard bind:id descid={groupid} {path} {label} {required} {defaultValue} {conditional} {helptext} let:value let:valid let:invalid let:onBlur let:onChange let:messagesid let:helptextid let:setVal>
36
+ {@const _ = updateValue(value, setVal)}
20
37
  <div class="color-container {className}" role="radiogroup" aria-labelledby={groupid} class:invalid class:valid>
21
38
  {#if addAllOption}
22
39
  <label for={`${path}.alt`} class="colorsel alternating">
23
40
  <Radio id={`${path}.alt`} name={path} value="alternating" selected={value === 'alternating'} {onChange} {onBlur} {helptextid}/>
24
41
  <span class="alternating-bg">
25
- {#each options as option}
42
+ {#each options as option (option.value)}
26
43
  <span style:background-color={option.color}></span>
27
44
  {/each}
28
45
  </span>
@@ -1,12 +1,13 @@
1
1
  <script>import { resize } from '@txstate-mws/svelte-components';
2
2
  import { derivedStore, Store } from '@txstate-mws/svelte-store';
3
3
  import { afterUpdate, beforeUpdate, onDestroy, onMount, setContext } from 'svelte';
4
- import { toArray } from 'txstate-utils';
4
+ import { isNotBlank } from 'txstate-utils';
5
5
  import LoadIcon from './LoadIcon.svelte';
6
6
  import TreeNode from './TreeNode.svelte';
7
- import { getHashId, TreeStore, TREE_STORE_CONTEXT } from './treestore';
7
+ import { getHashId, transformSearchable, TreeStore, TREE_STORE_CONTEXT } from './treestore';
8
8
  export let headers;
9
9
  export let searchable = undefined;
10
+ export let filter = '';
10
11
  export let nodeClass = undefined;
11
12
  export let singleSelect = undefined;
12
13
  export let enableResize = false;
@@ -24,8 +25,10 @@ export let moveHandler = undefined;
24
25
  export let copyHandler = undefined;
25
26
  export let dropEffect = undefined;
26
27
  export let store = new TreeStore(fetchChildren, { copyHandler, dragEligible, dropEffect, moveHandler });
28
+ if (searchable)
29
+ store.searchableFn = transformSearchable(searchable);
27
30
  setContext(TREE_STORE_CONTEXT, store);
28
- const { rootItems, headerOverride } = store;
31
+ const { filteredRootItems, headerOverride } = store;
29
32
  let checkboxelement;
30
33
  const headerelements = [];
31
34
  const treeWidth = new Store({});
@@ -57,14 +60,14 @@ function onDragEnd() {
57
60
  }
58
61
  let search = '';
59
62
  let searchTimer = 0;
60
- $: searchableFn = searchable == null ? undefined : (typeof searchable === 'function' ? (itm) => toArray(searchable(itm)) : (itm) => [itm[searchable]]);
63
+ $: store.filter(filter);
61
64
  function onKeyUp(e) {
62
- if (!searchableFn)
65
+ if (!store.searchableFn)
63
66
  return;
64
67
  if (e.key.length === 1) {
65
68
  search += e.key;
66
69
  const searchItems = $store.focused?.parent ? $store.focused.parent.children : $store.rootItems;
67
- const newFocus = searchItems?.find(itm => searchableFn(itm).some(str => str.toLocaleLowerCase().startsWith(search.toLocaleLowerCase())));
70
+ const newFocus = searchItems?.find(itm => store.searchableFn(itm).some(str => str.toLocaleLowerCase().startsWith(search.toLocaleLowerCase())));
68
71
  if (newFocus)
69
72
  store.focus(newFocus);
70
73
  clearTimeout(searchTimer);
@@ -150,24 +153,24 @@ afterUpdate(() => {
150
153
  <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}>
151
154
  <div class="checkbox" bind:this={checkboxelement}>&nbsp;</div>
152
155
  {#each headers as header, i (header.label)}
153
- <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}{#if i === 0 && search}&nbsp;(searching: {search}){/if}</div>
156
+ <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}{#if i === 0 && isNotBlank(search)}&nbsp;(searching: {search}){/if}</div>
154
157
  {#if enableResize && i !== headers.length - 1}<div class="tree-separator" on:mousedown={headerDragStart(header, i)} on:touchstart={headerDragStart(header, i)} on:dblclick={headerDragReset}>&nbsp;</div>{/if}
155
158
  {/each}
156
159
  </div>
157
- {#if mounted && $rootItems?.length}
160
+ {#if mounted && $filteredRootItems?.length}
158
161
  <!-- svelte-ignore a11y-no-noninteractive-element-to-interactive-role -->
159
162
  <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} on:keyup={onKeyUp}>
160
- {#each $rootItems as item, i (item.id)}
163
+ {#each $filteredRootItems as item, i (item.id)}
161
164
  <TreeNode
162
165
  {item}
163
166
  {headers}
164
167
  {headerSizes}
165
168
  {nodeClass}
166
169
  posinset={i + 1}
167
- setsize={$rootItems.length}
170
+ setsize={$filteredRootItems.length}
168
171
  level={item.level}
169
- prev={$rootItems[i - 1]}
170
- next={$rootItems[i + 1]}
172
+ prev={$filteredRootItems[i - 1]}
173
+ next={$filteredRootItems[i + 1]}
171
174
  on:choose
172
175
  on:deselect
173
176
  />
@@ -1,10 +1,11 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
2
  import { TreeStore } from './treestore';
3
- import type { DragEligibleFn, CopyHandlerFn, DropEffectFn, FetchChildrenFn, MoveHandlerFn, TreeHeader, TreeItemFromDB } from './treestore';
3
+ import type { DragEligibleFn, CopyHandlerFn, DropEffectFn, FetchChildrenFn, MoveHandlerFn, TreeHeader, TreeItemFromDB, SearchableType } from './treestore';
4
4
  declare class __sveltets_Render<T extends TreeItemFromDB> {
5
5
  props(): {
6
6
  headers: TreeHeader<T>[];
7
- searchable?: keyof T | ((item: T) => string | string[]) | undefined;
7
+ searchable?: SearchableType<T>;
8
+ filter?: string | undefined;
8
9
  nodeClass?: ((itm: T) => string) | undefined;
9
10
  singleSelect?: boolean | undefined;
10
11
  enableResize?: boolean | undefined;
@@ -18,6 +18,7 @@ export interface ITreeStore<T extends TreeItemFromDB> {
18
18
  loading?: boolean;
19
19
  rootItems?: TypedTreeItem<T>[];
20
20
  itemsById: Record<string, TypedTreeItem<T> | undefined>;
21
+ filter?: string;
21
22
  focused?: TypedTreeItem<T>;
22
23
  selected: Map<string, TypedTreeItem<T>>;
23
24
  selectedItems: TypedTreeItem<T>[];
@@ -33,6 +34,7 @@ export type DragEligibleFn<T extends TreeItemFromDB> = (selectedItems: TypedTree
33
34
  export type DropEffectFn<T extends TreeItemFromDB> = (selectedItems: TypedTreeItem<T>[], dropTarget: TypedTreeItem<T>, above: boolean, userWantsCopy: boolean) => 'move' | 'copy' | 'none';
34
35
  export type MoveHandlerFn<T extends TreeItemFromDB> = (selectedItems: TypedTreeItem<T>[], dropTarget: TypedTreeItem<T>, above: boolean) => boolean | Promise<boolean>;
35
36
  export type CopyHandlerFn<T extends TreeItemFromDB> = (selectedItems: TypedTreeItem<T>[], dropTarget: TypedTreeItem<T>, above: boolean, userWantsRecursive: boolean | undefined) => boolean | Promise<boolean>;
37
+ export type SearchableFn<T extends TreeItemFromDB> = (item: TypedTreeItem<T>) => string[];
36
38
  export interface TreeHeader<T extends TreeItemFromDB> {
37
39
  id: string;
38
40
  label: string;
@@ -48,7 +50,9 @@ export declare class TreeStore<T extends TreeItemFromDB> extends ActiveStore<ITr
48
50
  #private;
49
51
  fetchChildren: FetchChildrenFn<T>;
50
52
  treeElement?: HTMLElement;
51
- rootItems: import("svelte/store").Readable<any>;
53
+ rootItems: import("@txstate-mws/svelte-store").DerivedStore<TypedTreeItem<T>[] | undefined, ITreeStore<T>>;
54
+ filterTerm: import("@txstate-mws/svelte-store").DerivedStore<string | undefined, ITreeStore<T>>;
55
+ filteredRootItems: import("svelte/store").Readable<TypedTreeItem<T>[] | undefined>;
52
56
  draggable: import("@txstate-mws/svelte-store").DerivedStore<boolean, ITreeStore<T>>;
53
57
  dragging: import("@txstate-mws/svelte-store").DerivedStore<boolean, ITreeStore<T>>;
54
58
  selectedUndraggable: import("@txstate-mws/svelte-store").DerivedStore<boolean | undefined, ITreeStore<T>>;
@@ -60,13 +64,15 @@ export declare class TreeStore<T extends TreeItemFromDB> extends ActiveStore<ITr
60
64
  copyHandler?: CopyHandlerFn<T>;
61
65
  dragEligibleHandler?: DragEligibleFn<T>;
62
66
  dropEffectHandler?: DropEffectFn<T>;
67
+ searchableFn?: SearchableFn<T>;
63
68
  singleSelect?: boolean;
64
69
  private refreshPromise?;
65
- constructor(fetchChildren: FetchChildrenFn<T>, { moveHandler, copyHandler, dragEligible, dropEffect, singleSelect }?: {
70
+ constructor(fetchChildren: FetchChildrenFn<T>, { moveHandler, copyHandler, dragEligible, dropEffect, searchableFn, singleSelect }?: {
66
71
  moveHandler?: MoveHandlerFn<T>;
67
72
  copyHandler?: CopyHandlerFn<T>;
68
73
  dragEligible?: DragEligibleFn<T>;
69
74
  dropEffect?: DropEffectFn<T>;
75
+ searchableFn?: SearchableFn<T>;
70
76
  singleSelect?: boolean;
71
77
  });
72
78
  visit(item: TypedTreeItem<T>, cb: (item: TypedTreeItem<T>) => Promise<void>): Promise<void>;
@@ -77,6 +83,7 @@ export declare class TreeStore<T extends TreeItemFromDB> extends ActiveStore<ITr
77
83
  trigger(): void;
78
84
  fetch(item?: TypedTreeItem<T>): Promise<TypedTreeItem<T>[]>;
79
85
  refresh(item?: TypedTreeItem<T>, skipNotify?: boolean): Promise<void>;
86
+ filter(term: string | undefined, notify?: boolean): void;
80
87
  focus(item: TypedTreeItem<T> | undefined, notify?: boolean): void;
81
88
  select(item: TypedTreeItem<T>, { clear, notify, toggle }: {
82
89
  clear?: boolean | undefined;
@@ -118,3 +125,5 @@ export declare class TreeStore<T extends TreeItemFromDB> extends ActiveStore<ITr
118
125
  export declare const hashedIds: Record<string, string>;
119
126
  export declare function getHashId(str: string): string;
120
127
  export declare const lazyObserver: IntersectionObserver | undefined;
128
+ export type SearchableType<T> = keyof T | (keyof T)[] | ((item: T) => string | string[]) | undefined;
129
+ export declare function transformSearchable<T>(searchable: SearchableType<T>): undefined | ((itm: T) => string[]);
@@ -1,12 +1,27 @@
1
1
  import { ActiveStore, derivedStore } from '@txstate-mws/svelte-store';
2
2
  import { derived } from 'svelte/store';
3
- import { hashid, keyby } from 'txstate-utils';
3
+ import { hashid, isBlank, keyby, toArray } from 'txstate-utils';
4
4
  export const TREE_STORE_CONTEXT = {};
5
- const rootItems = v => v.rootItems;
6
5
  export class TreeStore extends ActiveStore {
7
6
  fetchChildren;
8
7
  treeElement;
9
- rootItems = derived(this, rootItems);
8
+ rootItems = derivedStore(this, 'rootItems');
9
+ filterTerm = derivedStore(this, 'filter');
10
+ filteredRootItems = derived([this.rootItems, this.filterTerm], ([rootItems, filter]) => {
11
+ if (!this.searchableFn || !rootItems?.length || isBlank(filter))
12
+ return this.value.rootItems;
13
+ const ret = [];
14
+ for (const itm of this.value.rootItems ?? []) {
15
+ let found = false;
16
+ for (const val of this.searchableFn(itm)) {
17
+ if (val.startsWith(filter))
18
+ found = true;
19
+ }
20
+ if (found)
21
+ ret.push(itm);
22
+ }
23
+ return ret;
24
+ });
10
25
  draggable = derivedStore(this, v => v.draggable && !v.loading);
11
26
  dragging = derivedStore(this, 'dragging');
12
27
  selectedUndraggable = derivedStore(this, 'selectedUndraggable');
@@ -18,15 +33,17 @@ export class TreeStore extends ActiveStore {
18
33
  copyHandler;
19
34
  dragEligibleHandler;
20
35
  dropEffectHandler;
36
+ searchableFn;
21
37
  singleSelect;
22
38
  refreshPromise;
23
- constructor(fetchChildren, { moveHandler, copyHandler, dragEligible, dropEffect, singleSelect } = {}) {
39
+ constructor(fetchChildren, { moveHandler, copyHandler, dragEligible, dropEffect, searchableFn, singleSelect } = {}) {
24
40
  super({ itemsById: {}, selected: new Map(), selectedItems: [], copied: new Map(), dragging: false, draggable: !!moveHandler, headerWidthOverrides: {} });
25
41
  this.fetchChildren = fetchChildren;
26
42
  this.moveHandler = moveHandler;
27
43
  this.copyHandler = copyHandler;
28
44
  this.dragEligibleHandler = dragEligible;
29
45
  this.dropEffectHandler = dropEffect;
46
+ this.searchableFn = searchableFn;
30
47
  this.singleSelect = singleSelect;
31
48
  }
32
49
  async visit(item, cb) {
@@ -125,6 +142,11 @@ export class TreeStore extends ActiveStore {
125
142
  await this.refreshPromise;
126
143
  this.refreshPromise = undefined;
127
144
  }
145
+ filter(term, notify = true) {
146
+ this.value.filter = term;
147
+ if (notify)
148
+ this.trigger();
149
+ }
128
150
  focus(item, notify = true) {
129
151
  this.value.focused = item;
130
152
  if (notify)
@@ -357,3 +379,13 @@ export const lazyObserver = typeof IntersectionObserver !== 'undefined'
357
379
  }
358
380
  }, { rootMargin: '500px' })
359
381
  : undefined;
382
+ export function transformSearchable(searchable) {
383
+ console.log('transformSearchable', searchable);
384
+ return searchable == null
385
+ ? undefined
386
+ : (typeof searchable === 'function'
387
+ ? (itm) => toArray(searchable(itm))
388
+ : (Array.isArray(searchable)
389
+ ? (itm) => searchable.map(k => itm[k])
390
+ : (itm) => [itm[searchable]]));
391
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
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.54",
4
+ "version": "0.0.56",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",
7
7
  "dev": "vite dev --force",