@dosgato/dialog 0.0.65 → 0.0.66

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.
@@ -73,16 +73,12 @@ function reactToValue(value, setVal) {
73
73
  setVal(finaldeserialize(''));
74
74
  }
75
75
  }
76
- let portal;
77
- onMount(() => {
78
- portal = inputelement.closest('.dialog-content');
79
- });
80
76
  </script>
81
77
 
82
78
  <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} serialize={finalserialize} deserialize={finaldeserialize} let:value let:setVal let:valid let:invalid let:id let:onBlur let:messagesid let:helptextid>
83
79
  {@const _ = reactToValue(value, setVal)}
84
80
  <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, extradescid])}>
85
- <PopupMenu bind:menushown bind:menuid align="bottomleft" usePortal={portal} items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
81
+ <PopupMenu bind:menushown bind:menuid align="bottomleft" items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
86
82
  <ScreenReaderOnly arialive="polite" ariaatomic={true} id={liveTextId}>
87
83
  {filteredChoices.length} {filteredChoices.length === 1 ? 'option' : 'options'} available.
88
84
  </ScreenReaderOnly>
@@ -1,17 +1,12 @@
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>
6
- <script>import { FORM_CONTEXT, FORM_INHERITED_PATH } from '@txstate-mws/svelte-forms';
1
+ <script>import arrowClockwiseFill from '@iconify-icons/ph/arrow-clockwise-fill';
2
+ import deleteOutline from '@iconify-icons/mdi/delete-outline';
3
+ import { FORM_CONTEXT, FORM_INHERITED_PATH } from '@txstate-mws/svelte-forms';
7
4
  import { getContext } from 'svelte';
8
5
  import { isBlank, isNotBlank, randomid } from 'txstate-utils';
9
- import { Chooser, ChooserStore, CHOOSER_API_CONTEXT } from './chooser';
6
+ import { Chooser, ChooserStore, CHOOSER_API_CONTEXT, cleanUrl } from './chooser';
10
7
  import Details from './chooser/Details.svelte';
11
8
  import Thumbnail from './chooser/Thumbnail.svelte';
12
- import FieldStandard from './FieldStandard.svelte';
13
- import InlineMessage from './InlineMessage.svelte';
14
- import { getDescribedBy } from './';
9
+ import { getDescribedBy, FieldStandard, InlineMessage, Icon } from './';
15
10
  export let id = undefined;
16
11
  export let path;
17
12
  export let label = '';
@@ -61,6 +56,7 @@ function userUrlEntry() {
61
56
  }
62
57
  async function userUrlEntryDebounced() {
63
58
  const url = this.value;
59
+ const cleanedUrl = cleanUrl(url);
64
60
  store.clearPreview();
65
61
  if (isBlank(url)) {
66
62
  selectedAsset = undefined;
@@ -69,7 +65,9 @@ async function userUrlEntryDebounced() {
69
65
  }
70
66
  let found = false;
71
67
  if (chooserClient.findByUrl) {
72
- const item = await chooserClient.findByUrl(url);
68
+ let item = await chooserClient.findByUrl(url);
69
+ if (!item && isNotBlank(cleanedUrl))
70
+ item = await chooserClient.findByUrl(cleanedUrl);
73
71
  if (url !== this.value)
74
72
  return;
75
73
  if (item) {
@@ -98,20 +96,19 @@ async function userUrlEntryDebounced() {
98
96
  selectedAsset = { type: 'raw', id: urlToValueCache[url], url };
99
97
  }
100
98
  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 {
99
+ if (isBlank(cleanedUrl) || cleanedUrl.startsWith('/')) {
110
100
  // here we "select" a raw url so that we do not interrupt the users' typing, but
111
101
  // we set its id to 'undefined' so that nothing makes it into the form until it's
112
102
  // a valid URL
113
103
  selectedAsset = { type: 'raw', id: undefined, url };
114
104
  }
105
+ else {
106
+ selectedAsset = {
107
+ type: 'raw',
108
+ id: chooserClient.urlToValue?.(cleanedUrl) ?? cleanedUrl,
109
+ url
110
+ };
111
+ }
115
112
  }
116
113
  }
117
114
  formStore.setField(finalPath, selectedAsset?.id);
@@ -128,14 +125,19 @@ async function updateSelected(..._) {
128
125
  try {
129
126
  if (!selectedAsset) {
130
127
  const urlFromValue = chooserClient.valueToUrl?.($value) ?? $value;
131
- try {
132
- selectedAsset = { type: 'raw', id: $value, url: reconstructUrl(urlFromValue) };
128
+ const cleanedUrlFromValue = cleanUrl(urlFromValue);
129
+ if (isBlank(cleanedUrlFromValue)) {
130
+ selectedAsset = undefined;
131
+ }
132
+ else if (cleanedUrlFromValue.startsWith('/')) {
133
+ selectedAsset = { type: 'broken', id: $value, url: cleanedUrlFromValue };
133
134
  }
134
- catch {
135
- selectedAsset = { type: 'broken', id: $value, url: $value };
135
+ else {
136
+ selectedAsset = { type: 'raw', id: $value, url: cleanedUrlFromValue };
136
137
  }
137
138
  }
138
- urlToValueCache[selectedAsset.url] = $value;
139
+ if (selectedAsset)
140
+ urlToValueCache[selectedAsset.url] = $value;
139
141
  }
140
142
  catch (e) {
141
143
  console.error(e);
@@ -149,15 +151,27 @@ $: updateSelected($value);
149
151
  {#if selectedAsset?.id}
150
152
  <div class="dialog-chooser-container">
151
153
  <Thumbnail item={selectedAsset} />
152
- <Details item={selectedAsset} />
154
+ <div class="dialog-chooser-right">
155
+ <Details item={selectedAsset} singleLine />
156
+ {#if !urlEntry}
157
+ <button type="button" on:click={show} aria-describedby={getDescribedBy([descid, messagesid, helptextid, extradescid])}>
158
+ <Icon icon={arrowClockwiseFill} /> Replace
159
+ </button>
160
+ {/if}
161
+ <button type="button" on:click={() => { selectedAsset = undefined; setVal(undefined) }} aria-describedby={getDescribedBy([descid, messagesid, helptextid, extradescid])}>
162
+ <Icon icon={deleteOutline} /> Remove
163
+ </button>
164
+ </div>
165
+ </div>
166
+ {/if}
167
+ {#if urlEntry || !selectedAsset?.id}
168
+ <div class="dialog-chooser-entry">
169
+ {#if urlEntry}
170
+ <input type="text" value={selectedAsset?.url ?? ''} on:change={userUrlEntry} on:keyup={userUrlEntry}>
171
+ {/if}
172
+ <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>
153
173
  </div>
154
174
  {/if}
155
- <div class="dialog-chooser-entry">
156
- {#if urlEntry}
157
- <input type="text" value={selectedAsset?.url ?? ''} on:change={userUrlEntry} on:keyup={userUrlEntry}>
158
- {/if}
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>
160
- </div>
161
175
  {#if selectedAsset?.url.length && !selectedAsset.id}
162
176
  <InlineMessage message={{ message: 'Entry does not match an internal resource and is not a valid URL. Nothing will be saved.', type: 'warning' }} />
163
177
  {/if}
@@ -169,6 +183,7 @@ $: updateSelected($value);
169
183
  <style>
170
184
  .dialog-chooser-container {
171
185
  display: flex;
186
+ flex-wrap: wrap;
172
187
  margin-bottom: 0.25em;
173
188
  font-size: 0.9em;
174
189
  }
@@ -177,6 +192,9 @@ $: updateSelected($value);
177
192
  padding-top: 0;
178
193
  height: 5em;
179
194
  }
195
+ .dialog-chooser-right button {
196
+ margin-top: 0.5em;
197
+ }
180
198
  .dialog-chooser-entry {
181
199
  display: flex;
182
200
  }
@@ -2,8 +2,9 @@
2
2
  import { randomid } from 'txstate-utils';
3
3
  export let path;
4
4
  export let conditional = undefined;
5
+ export let length = 10;
5
6
  </script>
6
7
 
7
- <Field {path} {conditional} defaultValue={randomid()} serialize={nullableSerialize} deserialize={nullableDeserialize} let:value>
8
+ <Field {path} {conditional} defaultValue={randomid(length)} serialize={nullableSerialize} deserialize={nullableDeserialize} let:value>
8
9
  <input type="hidden" name={path} {value}>
9
10
  </Field>
@@ -3,6 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  path: string;
5
5
  conditional?: boolean | undefined;
6
+ length?: number | undefined;
6
7
  };
7
8
  events: {
8
9
  [evt: string]: CustomEvent<any>;
@@ -47,9 +47,7 @@ async function wrapGetOptions(search) {
47
47
  const selectedStore = new Store([]);
48
48
  let hasInit = !defaultValue.length;
49
49
  let inputelement;
50
- let portal;
51
50
  onMount(async () => {
52
- portal = inputelement.closest('.dialog-content');
53
51
  await reactToValue(defaultValue);
54
52
  hasInit = true;
55
53
  });
@@ -73,7 +71,7 @@ async function reactToValue(value) {
73
71
  <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} let:value let:valid let:invalid let:id let:onBlur let:setVal let:messagesid let:helptextid>
74
72
  {@const _ = reactToValue(value)}
75
73
  <div class:valid class:invalid>
76
- <MultiSelect {id} name={path} usePortal={portal} descid={getDescribedBy([messagesid, helptextid, extradescid])}
74
+ <MultiSelect {id} name={path} descid={getDescribedBy([messagesid, helptextid, extradescid])}
77
75
  {disabled} {maxSelections} selected={$selectedStore} {placeholder} getOptions={wrapGetOptions}
78
76
  inputClass='multiselect-input'
79
77
  on:change={e => setVal(e.detail.map(itm => itm.value))} on:blur={onBlur}
@@ -10,7 +10,7 @@ export let placeholder = 'Select' + (label ? ' ' + label : '');
10
10
  export let notNull = false;
11
11
  export let disabled = false;
12
12
  export let choices;
13
- export let defaultValue = notNull ? choices[0].value : undefined;
13
+ export let defaultValue = notNull ? choices[0]?.value : undefined;
14
14
  export let conditional = undefined;
15
15
  export let required = false;
16
16
  export let inputelement = undefined;
@@ -32,10 +32,12 @@ function updateValue(valu, sVal, fDes) {
32
32
  function reactToChoices(..._) {
33
33
  if (!stVal)
34
34
  return;
35
- if (!choices.length)
35
+ if (!choices.length) {
36
36
  stVal(finalDeserialize(''));
37
+ return;
38
+ }
37
39
  if (!choices.some(o => o.value === val))
38
- stVal(notNull ? choices[0].value : finalDeserialize(''));
40
+ stVal(notNull ? defaultValue : finalDeserialize(''));
39
41
  }
40
42
  $: reactToChoices(choices);
41
43
  onMount(reactToChoices);
@@ -1,6 +1,8 @@
1
1
  <script>import { nullableSerialize, nullableDeserialize } from '@txstate-mws/svelte-forms';
2
2
  import FieldStandard from './FieldStandard.svelte';
3
3
  import Input from './Input.svelte';
4
+ import MaxLength from './MaxLength.svelte';
5
+ import { isNotNull } from 'txstate-utils';
4
6
  let className = '';
5
7
  export { className as class };
6
8
  export let id = undefined;
@@ -22,5 +24,8 @@ export let helptext = undefined;
22
24
  </script>
23
25
 
24
26
  <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} serialize={!notNull ? nullableSerialize : undefined} deserialize={!notNull ? nullableDeserialize : undefined} let:value let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid>
25
- <Input bind:inputelement {type} {autocomplete} name={path} {value} {id} class="dialog-input {className}" {allowlastpass} {onChange} {onBlur} {valid} {invalid} {maxlength} {messagesid} {helptextid} {extradescid} {use}></Input>
27
+ <Input bind:inputelement {type} {autocomplete} name={path} {value} {id} class="dialog-input {className}" {allowlastpass} {onChange} {onBlur} {valid} {invalid} {messagesid} {helptextid} {extradescid} {use}></Input>
28
+ {#if isNotNull(maxlength)}
29
+ <MaxLength {value} {maxlength}/>
30
+ {/if}
26
31
  </FieldStandard>
@@ -2,6 +2,8 @@
2
2
  import { passActions } from '@txstate-mws/svelte-components';
3
3
  import { nullableSerialize, nullableDeserialize } from '@txstate-mws/svelte-forms';
4
4
  import FieldStandard from './FieldStandard.svelte';
5
+ import MaxLength from './MaxLength.svelte';
6
+ import { isNotNull } from 'txstate-utils';
5
7
  let className = '';
6
8
  export { className as class };
7
9
  export let id = undefined;
@@ -21,7 +23,10 @@ export let helptext = undefined;
21
23
  </script>
22
24
 
23
25
  <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} serialize={!notNull ? nullableSerialize : undefined} deserialize={!notNull ? nullableDeserialize : undefined} let:value let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid>
24
- <textarea bind:this={inputelement} name={path} {value} {id} {rows} class="dialog-input dialog-textarea {className}" class:valid class:invalid aria-invalid={invalid} aria-describedby={getDescribedBy([messagesid, helptextid, extradescid])} on:change={onChange} on:blur={onBlur} on:keyup={onChange} on:paste {maxlength} use:passActions={use}></textarea>
26
+ <textarea bind:this={inputelement} name={path} {value} {id} {rows} class="dialog-input dialog-textarea {className}" class:valid class:invalid aria-invalid={invalid} aria-describedby={getDescribedBy([messagesid, helptextid, extradescid])} on:change={onChange} on:blur={onBlur} on:keyup={onChange} on:paste use:passActions={use}></textarea>
27
+ {#if isNotNull(maxlength)}
28
+ <MaxLength {value} {maxlength}/>
29
+ {/if}
25
30
  </FieldStandard>
26
31
 
27
32
  <style>
@@ -21,7 +21,7 @@ async function onSubmit() {
21
21
  setContext(CHOOSER_API_CONTEXT, chooserClient);
22
22
  </script>
23
23
 
24
- <Dialog continueText="Save" continueIcon={contentSave} cancelText="Cancel" on:escape on:continue={onSubmit} {title} {icon} {size}>
24
+ <Dialog continueText="Save" continueIcon={contentSave} cancelText="Cancel" on:escape on:continue={onSubmit} {title} {icon} {size} escapable={false}>
25
25
  <Form bind:store {submit} {validate} {chooserClient} {autocomplete} {name} {preload} on:saved let:messages let:allMessages let:showingInlineErrors let:saved let:valid let:invalid let:validating let:submitting let:data>
26
26
  <slot {messages} {allMessages} {saved} {validating} {submitting} {valid} {invalid} {data} {showingInlineErrors} />
27
27
  </Form>
@@ -0,0 +1,36 @@
1
+ <script>export let maxlength;
2
+ export let value;
3
+ let overLimit = false;
4
+ $: message = getMaxlengthMessage(value.length);
5
+ function getMaxlengthMessage(length) {
6
+ overLimit = false;
7
+ if (length === 0) {
8
+ return `${maxlength} characters allowed`;
9
+ }
10
+ else if (length <= maxlength) {
11
+ const diff = maxlength - length;
12
+ return `${diff} character${diff === 1 ? '' : 's'} remaining`;
13
+ }
14
+ else {
15
+ overLimit = true;
16
+ const diff = length - maxlength;
17
+ return `${diff} character${diff === 1 ? '' : 's'} over limit`;
18
+ }
19
+ }
20
+ </script>
21
+
22
+ <div class:over={overLimit} aria-live="polite">{message}</div>
23
+
24
+ <style>
25
+ div {
26
+ font-size: 0.9em;
27
+ color: #595959;
28
+ line-height: 1.25em;
29
+ margin-top: 0.4em;
30
+ }
31
+ .over {
32
+ color: #9a3332;
33
+ font-weight: bold;
34
+ }
35
+
36
+ </style>
@@ -0,0 +1,17 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ maxlength: number;
5
+ value: string;
6
+ };
7
+ events: {
8
+ [evt: string]: CustomEvent<any>;
9
+ };
10
+ slots: {};
11
+ };
12
+ export type MaxLengthProps = typeof __propDef.props;
13
+ export type MaxLengthEvents = typeof __propDef.events;
14
+ export type MaxLengthSlots = typeof __propDef.slots;
15
+ export default class MaxLength extends SvelteComponentTyped<MaxLengthProps, MaxLengthEvents, MaxLengthSlots> {
16
+ }
17
+ export {};
@@ -4,8 +4,6 @@ export let right = false;
4
4
  export let bottom = false;
5
5
  export let left = false;
6
6
  export let active = false;
7
- export let color = "#fff";
8
- let style = `background-color: ${color}; border: 1px solid black;`;
9
7
  </script>
10
8
 
11
9
  <style>
@@ -7,7 +7,6 @@ declare const __propDef: {
7
7
  bottom?: boolean | undefined;
8
8
  left?: boolean | undefined;
9
9
  active?: boolean | undefined;
10
- color?: string | undefined;
11
10
  };
12
11
  events: {
13
12
  [evt: string]: CustomEvent<any>;
@@ -61,4 +61,5 @@ export declare class ChooserStore<F = any> extends Store<IAssetStore> {
61
61
  clearPreview(): void;
62
62
  setSource(name?: string, init?: boolean): void;
63
63
  }
64
+ export declare function cleanUrl(url: string): string;
64
65
  export {};
@@ -95,3 +95,23 @@ export class ChooserStore extends Store {
95
95
  });
96
96
  }
97
97
  }
98
+ export function cleanUrl(url) {
99
+ if (url.startsWith('//'))
100
+ url = 'https:' + url;
101
+ if (url.startsWith('/'))
102
+ return url;
103
+ try {
104
+ const _ = new URL(url);
105
+ return url;
106
+ }
107
+ catch (e) {
108
+ const fixed = 'https://' + url;
109
+ try {
110
+ const _ = new URL(fixed);
111
+ return fixed;
112
+ }
113
+ catch (e) {
114
+ return '';
115
+ }
116
+ }
117
+ }
@@ -1,54 +1,53 @@
1
- <script>import { bytesToHuman } from './ChooserStore';
1
+ <script>import { bytesToHuman, cleanUrl } from './ChooserStore';
2
2
  export let item;
3
+ export let singleLine = false;
3
4
  </script>
4
5
 
5
- <dl class="dialog-chooser-info" aria-live="polite">
6
+ <dl class="dialog-chooser-info" aria-live="polite" class:multiLine={!singleLine}>
6
7
  {#if item.type === 'raw' && item.id}
7
- <div>
8
+ <div class="top-row">
8
9
  <dt>External Link:</dt>
9
- <dd>{item.url}</dd>
10
+ <dd>{cleanUrl(item.url)}</dd>
10
11
  </div>
11
12
  {:else if item.type === 'broken'}
12
- <div>
13
- <dt>Unknown Link (this resource may have been deleted):</dt>
13
+ <div class="top-row">
14
+ <dt>Unknown Link (may have been deleted):</dt>
14
15
  <dd>{item.url}</dd>
15
16
  </div>
16
17
  {:else if item.type !== 'raw'}
17
18
  <div class="top-row">
18
19
  <dt>Name:</dt>
19
- <dd>{item.path}</dd>
20
+ <dd>{item.name}</dd>
20
21
  </div>
21
22
  {/if}
22
23
  {#if item.type === 'asset'}
23
- <div class="horizontal-group">
24
- {#if item.image}
25
- <div>
26
- <dt>Dimensions:</dt>
27
- <dd>{item.image.width}x{item.image.height}</dd>
28
- </div>
29
- {/if}
24
+ {#if item.image}
30
25
  <div>
31
- <dt>File Type:</dt>
32
- <dd>{item.mime}</dd>
33
- </div>
34
- <div>
35
- <dt>File Size:</dt>
36
- <dd>{bytesToHuman(item.bytes)}</dd>
26
+ <dt>Dimensions:</dt>
27
+ <dd>{item.image.width}x{item.image.height}</dd>
37
28
  </div>
29
+ {/if}
30
+ <div>
31
+ <dt>File Type:</dt>
32
+ <dd>{item.mime}</dd>
33
+ </div>
34
+ <div>
35
+ <dt>File Size:</dt>
36
+ <dd>{bytesToHuman(item.bytes)}</dd>
38
37
  </div>
39
38
  {:else if item.type === 'page' && item.title}
40
- <dt>Title:</dt>
41
- <dd>{item.title}</dd>
39
+ <div>
40
+ <dt>Title:</dt>
41
+ <dd>{item.title}</dd>
42
+ </div>
42
43
  {:else if item.type === 'folder'}
43
- <div class="horizontal-group">
44
- <div>
45
- <dt>Path:</dt>
46
- <dd>{item.path}</dd>
47
- </div>
48
- <div>
49
- <dt>Contents:</dt>
50
- <dd>{item.childCount} sub-items</dd>
51
- </div>
44
+ <div>
45
+ <dt>Path:</dt>
46
+ <dd>{item.path}</dd>
47
+ </div>
48
+ <div>
49
+ <dt>Contents:</dt>
50
+ <dd>{item.childCount} sub-items</dd>
52
51
  </div>
53
52
  {/if}
54
53
  <slot />
@@ -56,6 +55,10 @@ export let item;
56
55
 
57
56
  <style>
58
57
  .dialog-chooser-info {
58
+ display: flex;
59
+ flex-wrap: wrap;
60
+ justify-content: flex-start;
61
+ gap: 1em 1.5em;
59
62
  padding: 0;
60
63
  margin: 0;
61
64
  list-style: none;
@@ -66,18 +69,8 @@ export let item;
66
69
  .dialog-chooser-info dd {
67
70
  margin: 0;
68
71
  }
69
- .top-row {
70
- margin-bottom: 1em;
71
- }
72
- .horizontal-group {
73
- display: flex;
74
- justify-content: space-between;
75
- }
76
- :global([data-eq~="1400px"]) .horizontal-group {
77
- flex-direction: column;
78
- }
79
- :global([data-eq~="1400px"]) .horizontal-group div:not(:last-child){
80
- margin-bottom: 1em;
72
+ .multiLine .top-row {
73
+ width: 100%;
81
74
  }
82
75
 
83
76
  </style>
@@ -4,6 +4,7 @@ import { type BrokenURL, type RawURL } from './ChooserStore';
4
4
  declare const __propDef: {
5
5
  props: {
6
6
  item: AnyItem | RawURL | BrokenURL;
7
+ singleLine?: boolean | undefined;
7
8
  };
8
9
  events: {
9
10
  [evt: string]: CustomEvent<any>;
@@ -0,0 +1,85 @@
1
+ <script>import { passActions } from '@txstate-mws/svelte-components';
2
+ import { createEventDispatcher, onDestroy, onMount } from 'svelte';
3
+ import { getDescribedBy } from '..';
4
+ let className = '';
5
+ export { className as class };
6
+ export let id = undefined;
7
+ export let rows = 8;
8
+ export let language;
9
+ export let use = [];
10
+ export let inputelement = undefined;
11
+ export let extradescid = undefined;
12
+ export let helptextid = undefined;
13
+ export let messagesid = undefined;
14
+ export let valid = undefined;
15
+ export let invalid = undefined;
16
+ export let value;
17
+ const dispatch = createEventDispatcher();
18
+ let updateCode;
19
+ let editorelement;
20
+ let editor;
21
+ onMount(async () => {
22
+ const { EditorView, minimalSetup } = await import('codemirror');
23
+ const { indentWithTab } = await import('@codemirror/commands');
24
+ const { lineNumbers, highlightActiveLine, highlightActiveLineGutter, keymap } = await import('@codemirror/view');
25
+ const { langmap } = await import('./langs.js');
26
+ editor = new EditorView({
27
+ extensions: [
28
+ minimalSetup,
29
+ lineNumbers(),
30
+ highlightActiveLine(),
31
+ highlightActiveLineGutter(),
32
+ langmap[language],
33
+ keymap.of([indentWithTab]),
34
+ EditorView.updateListener.of((v) => {
35
+ if (v.docChanged) {
36
+ const newval = editor.state.doc.toString();
37
+ if (value !== newval)
38
+ dispatch('change', newval);
39
+ }
40
+ })
41
+ ],
42
+ parent: editorelement
43
+ });
44
+ updateCode = code => {
45
+ if (editor.state.doc.toString() !== code)
46
+ editor.update([editor.state.update({ changes: { from: 0, to: editor.state.doc.length, insert: code } })]);
47
+ };
48
+ inputelement = editorelement?.querySelector('.cm-content') ?? undefined;
49
+ if (id)
50
+ inputelement?.setAttribute('id', id);
51
+ if (className)
52
+ inputelement?.classList.add(...className.split(' '), ...[]);
53
+ updateValidState();
54
+ });
55
+ onDestroy(() => {
56
+ editor?.destroy();
57
+ });
58
+ $: updateCode?.(value ?? '');
59
+ function updateValidState(..._) {
60
+ inputelement?.setAttribute('aria-invalid', String(!!invalid));
61
+ const descby = getDescribedBy([messagesid, helptextid, extradescid]);
62
+ if (descby)
63
+ inputelement?.setAttribute('aria-describedby', descby);
64
+ else
65
+ inputelement?.removeAttribute('aria-describedby');
66
+ }
67
+ $: updateValidState(invalid, messagesid);
68
+ </script>
69
+
70
+ <div bind:this={editorelement} style:--cm-editor-minh="{rows * 1.5}em" class:valid class:invalid on:paste on:focusout use:passActions={use}></div>
71
+
72
+ <style>
73
+ div {
74
+ background-color: white;
75
+ }
76
+ div :global(.cm-content), div :global(.cm-gutter) {
77
+ min-height: var(--cm-editor-minh, 10em);
78
+ }
79
+
80
+ div :global(.cm-scroller) {
81
+ overflow: auto;
82
+ resize: vertical;
83
+ }
84
+
85
+ </style>
@@ -0,0 +1,32 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ import { type HTMLActionEntry } from '@txstate-mws/svelte-components';
3
+ declare const __propDef: {
4
+ props: {
5
+ class?: string | undefined;
6
+ id?: string | undefined;
7
+ rows?: number | undefined;
8
+ language: 'js' | 'css' | 'html';
9
+ use?: HTMLActionEntry[] | undefined;
10
+ inputelement?: HTMLElement | undefined;
11
+ extradescid?: string | undefined;
12
+ helptextid?: string | undefined;
13
+ messagesid?: string | undefined;
14
+ valid?: boolean | undefined;
15
+ invalid?: boolean | undefined;
16
+ value: any;
17
+ };
18
+ events: {
19
+ paste: ClipboardEvent;
20
+ focusout: FocusEvent;
21
+ change: CustomEvent<any>;
22
+ } & {
23
+ [evt: string]: CustomEvent<any>;
24
+ };
25
+ slots: {};
26
+ };
27
+ export type CodeEditorProps = typeof __propDef.props;
28
+ export type CodeEditorEvents = typeof __propDef.events;
29
+ export type CodeEditorSlots = typeof __propDef.slots;
30
+ export default class CodeEditor extends SvelteComponentTyped<CodeEditorProps, CodeEditorEvents, CodeEditorSlots> {
31
+ }
32
+ export {};
@@ -0,0 +1,24 @@
1
+ <script>import { nullableSerialize, nullableDeserialize } from '@txstate-mws/svelte-forms';
2
+ import { FieldStandard } from '..';
3
+ import CodeEditor from './CodeEditor.svelte';
4
+ let className = '';
5
+ export { className as class };
6
+ export let id = undefined;
7
+ export let path;
8
+ export let label = '';
9
+ export let notNull = false;
10
+ export let defaultValue = notNull ? '' : undefined;
11
+ export let rows = 8;
12
+ export let conditional = undefined;
13
+ export let required = false;
14
+ export let use = [];
15
+ export let inputelement = undefined;
16
+ export let related = 0;
17
+ export let extradescid = undefined;
18
+ export let helptext = undefined;
19
+ export let language;
20
+ </script>
21
+
22
+ <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} serialize={!notNull ? nullableSerialize : undefined} deserialize={!notNull ? nullableDeserialize : undefined} let:value let:valid let:invalid let:id let:onBlur let:setVal let:messagesid let:helptextid>
23
+ <CodeEditor {id} bind:inputelement {language} {rows} class={className} {value} {valid} {invalid} {helptextid} {messagesid} {extradescid} on:paste on:focusout={onBlur} {use} on:change={e => setVal(e.detail)}/>
24
+ </FieldStandard>
@@ -1,5 +1,5 @@
1
1
  import { SvelteComponentTyped } from "svelte";
2
- import { type HTMLActionEntry } from '@txstate-mws/svelte-components';
2
+ import type { HTMLActionEntry } from '@txstate-mws/svelte-components';
3
3
  declare const __propDef: {
4
4
  props: {
5
5
  class?: string | undefined;
@@ -12,7 +12,7 @@ declare const __propDef: {
12
12
  conditional?: boolean | undefined;
13
13
  required?: boolean | undefined;
14
14
  use?: HTMLActionEntry[] | undefined;
15
- inputelement?: HTMLTextAreaElement | undefined;
15
+ inputelement?: HTMLElement | undefined;
16
16
  related?: number | true | undefined;
17
17
  extradescid?: string | undefined;
18
18
  helptext?: string | undefined;
@@ -0,0 +1 @@
1
+ export { default as FieldCodeEditor } from './FieldCodeEditor.svelte';
@@ -0,0 +1 @@
1
+ export { default as FieldCodeEditor } from './FieldCodeEditor.svelte';
@@ -0,0 +1,5 @@
1
+ export declare const langmap: {
2
+ js: import("@codemirror/language").LanguageSupport;
3
+ css: import("@codemirror/language").LanguageSupport;
4
+ html: import("@codemirror/language").LanguageSupport;
5
+ };
@@ -0,0 +1,8 @@
1
+ import { javascript } from '@codemirror/lang-javascript';
2
+ import { css } from '@codemirror/lang-css';
3
+ import { html } from '@codemirror/lang-html';
4
+ export const langmap = {
5
+ js: javascript(),
6
+ css: css(),
7
+ html: html()
8
+ };
package/dist/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export { default as FieldAutocomplete } from './FieldAutocomplete.svelte';
7
7
  export { default as FieldCheckbox } from './FieldCheckbox.svelte';
8
8
  export { default as FieldChoices } from './FieldChoices.svelte';
9
9
  export { default as FieldChooserLink } from './FieldChooserLink.svelte';
10
- export { default as FieldCodeEditor } from './FieldCodeEditor.svelte';
11
10
  export { default as FieldDate } from './FieldDate.svelte';
12
11
  export { default as FieldDateTime } from './FieldDateTime.svelte';
13
12
  export { default as FieldDualListbox } from './FieldDualListbox.svelte';
@@ -41,3 +40,4 @@ export * from './colorpicker/index.js';
41
40
  export * from './helpers.js';
42
41
  export * from './tree/index.js';
43
42
  export * from './cropper/index.js';
43
+ export * from './code/index.js';
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ export { default as FieldAutocomplete } from './FieldAutocomplete.svelte';
7
7
  export { default as FieldCheckbox } from './FieldCheckbox.svelte';
8
8
  export { default as FieldChoices } from './FieldChoices.svelte';
9
9
  export { default as FieldChooserLink } from './FieldChooserLink.svelte';
10
- export { default as FieldCodeEditor } from './FieldCodeEditor.svelte';
11
10
  export { default as FieldDate } from './FieldDate.svelte';
12
11
  export { default as FieldDateTime } from './FieldDateTime.svelte';
13
12
  export { default as FieldDualListbox } from './FieldDualListbox.svelte';
@@ -41,3 +40,4 @@ export * from './colorpicker/index.js';
41
40
  export * from './helpers.js';
42
41
  export * from './tree/index.js';
43
42
  export * from './cropper/index.js';
43
+ export * from './code/index.js';
@@ -303,7 +303,7 @@ $: if ($dragging) {
303
303
  </div>
304
304
  {#each headers as header, i (header.id)}
305
305
  <div
306
- class={(header.class ? toArray(header.class(item)) : []).concat([header.id]).join(' ')}
306
+ class={(header.class ? toArray(header.class(item)) : []).concat([header.id, 'tree-cell']).join(' ')}
307
307
  style:width={$headerSizes?.[i] ?? '1px'}
308
308
  style:padding-left={i === 0 ? `calc(min(${(level - 1) * 1.6}em, ${(level - 1) * 2.7}vw) + 1.4em)` : undefined}
309
309
  class:left={i === 0}
@@ -418,6 +418,10 @@ $: if ($dragging) {
418
418
  background-position: left top, right bottom, left bottom, right top;
419
419
  animation: border-dance 1s infinite linear;
420
420
  }
421
+ .tree-cell {
422
+ text-overflow: ellipsis;
423
+ overflow: hidden;
424
+ }
421
425
  @keyframes border-dance {
422
426
  0% {
423
427
  background-position: left top, right bottom, left bottom, right top;
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.65",
4
+ "version": "0.0.66",
5
5
  "scripts": {
6
6
  "prepublishOnly": "svelte-package",
7
7
  "dev": "vite dev --force",
@@ -20,19 +20,21 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "dependencies": {
23
- "@txstate-mws/svelte-components": "^1.4.5",
24
- "@txstate-mws/svelte-forms": "^1.3.4",
23
+ "@codemirror/lang-javascript": "^6.1.7",
24
+ "@codemirror/lang-css": "^6.2.0",
25
+ "@codemirror/lang-html": "^6.4.3",
25
26
  "@iconify/svelte": "^3.0.0",
26
27
  "@iconify-icons/mdi": "^1.2.22",
27
28
  "@iconify-icons/ph": "^1.2.2",
28
- "codeflask": "^1.4.1",
29
+ "@txstate-mws/svelte-components": "^1.5.3",
30
+ "@txstate-mws/svelte-forms": "^1.3.4",
31
+ "codemirror": "^6.0.1",
29
32
  "txstate-utils": "^1.8.0"
30
33
  },
31
34
  "devDependencies": {
32
35
  "@sveltejs/adapter-auto": "^2.0.0",
33
36
  "@sveltejs/kit": "^1.0.1",
34
37
  "@sveltejs/package": "^2.0.1",
35
- "@types/codeflask": "^1.4.3",
36
38
  "eslint-config-standard-with-typescript": "^34.0.0",
37
39
  "eslint-plugin-svelte3": "^4.0.0",
38
40
  "svelte-check": "^3.0.1",
@@ -1,71 +0,0 @@
1
- <script>import { passActions } from '@txstate-mws/svelte-components';
2
- import { nullableSerialize, nullableDeserialize, FORM_CONTEXT, FormStore, FORM_INHERITED_PATH } from '@txstate-mws/svelte-forms';
3
- import CodeFlask from 'codeflask';
4
- import { getContext, onMount } from 'svelte';
5
- import { isNotBlank } from 'txstate-utils';
6
- import { getDescribedBy, FieldStandard } from './';
7
- let className = '';
8
- export { className as class };
9
- export let id = undefined;
10
- export let path;
11
- export let label = '';
12
- export let notNull = false;
13
- export let defaultValue = notNull ? '' : undefined;
14
- export let rows = 8;
15
- export let conditional = undefined;
16
- export let required = false;
17
- export let use = [];
18
- export let inputelement = undefined;
19
- export let related = 0;
20
- export let extradescid = undefined;
21
- export let helptext = undefined;
22
- export let language;
23
- const store = getContext(FORM_CONTEXT);
24
- const inheritedPath = getContext(FORM_INHERITED_PATH);
25
- const finalPath = [inheritedPath, path].filter(isNotBlank).join('.');
26
- const value = store.getField(finalPath);
27
- let editorelement;
28
- let flask;
29
- onMount(() => {
30
- flask = new CodeFlask(editorelement, { language, lineNumbers: true, areaId: id });
31
- inputelement = editorelement.querySelector(`#${id}`);
32
- if (className)
33
- inputelement.classList.add(...className.split(' '));
34
- inputelement.addEventListener('change', onChange);
35
- updateValidState(invalid, messagesid);
36
- });
37
- $: flask?.updateCode($value ?? '');
38
- let invalid;
39
- let messagesid;
40
- let helptextid;
41
- let onChange;
42
- function setSlotProps(helptextidIn, onChangeIn) {
43
- helptextid = helptextidIn;
44
- onChange = onChangeIn;
45
- }
46
- function updateValidState(invalidIn, messagesIdIn) {
47
- invalid = !!invalidIn;
48
- messagesid = messagesIdIn;
49
- inputelement?.setAttribute('aria-invalid', String(!!invalid));
50
- const descby = getDescribedBy([messagesid, helptextid, extradescid]);
51
- if (descby)
52
- inputelement?.setAttribute('aria-describedby', descby);
53
- else
54
- inputelement?.removeAttribute('aria-describedby');
55
- }
56
- </script>
57
-
58
- <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {related} {helptext} serialize={!notNull ? nullableSerialize : undefined} deserialize={!notNull ? nullableDeserialize : undefined} let:value let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid>
59
- {@const _ = setSlotProps(helptextid, onChange)}
60
- {@const __ = updateValidState(invalid, messagesid)}
61
- <div bind:this={editorelement} style:height="{rows * 1.333}em" class:valid class:invalid on:paste on:focusout={onBlur} use:passActions={use}></div>
62
- </FieldStandard>
63
-
64
- <style>
65
- div {
66
- position: relative;
67
- overflow: hidden;
68
- resize: vertical;
69
- }
70
-
71
- </style>