@dosgato/dialog 1.3.7 → 1.3.9

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.
@@ -31,6 +31,7 @@
31
31
  export let selectedAsset: AnyItem | RawURL | BrokenURL | undefined = undefined
32
32
  export let altTextPath : string | undefined = undefined
33
33
  export let altTextRequired: boolean = false
34
+ export let altTextHelp: string = 'Describes the asset for visually-impaired users and search engines.'
34
35
 
35
36
  // TODO: add a mime type acceptance prop, maybe a regex or function, to prevent users from
36
37
  // choosing unacceptable mime types
@@ -38,6 +39,7 @@
38
39
  const formStore = getContext<FormStore>(FORM_CONTEXT)
39
40
  const inheritedPath = getContext<string>(FORM_INHERITED_PATH)
40
41
  const finalPath = [inheritedPath, path].filter(isNotBlank).join('.')
42
+ const finalAltTextPath = altTextPath ? [inheritedPath, altTextPath].filter(isNotBlank).join('.') : undefined
41
43
  const value = formStore.getField<string>(finalPath)
42
44
  const chooserClient = getContext<Client>(CHOOSER_API_CONTEXT)
43
45
  const store = new ChooserStore(chooserClient)
@@ -57,12 +59,12 @@
57
59
  return (e) => {
58
60
  selectedAsset = e.detail.preview
59
61
  if (
60
- altTextPath &&
62
+ finalAltTextPath &&
61
63
  e.detail.copyAltText &&
62
64
  selectedAsset?.type === 'asset' &&
63
65
  selectedAsset?.image?.altText
64
66
  ) {
65
- formStore.setField(altTextPath, selectedAsset.image.altText).catch(console.error)
67
+ formStore.setField(finalAltTextPath, selectedAsset.image.altText).catch(console.error)
66
68
  }
67
69
  setVal(selectedAsset?.id)
68
70
  hide()
@@ -175,8 +177,8 @@
175
177
  return (e) => {
176
178
  selectedAsset = undefined
177
179
  setVal(undefined)
178
- if (altTextPath) {
179
- formStore.setField(altTextPath, undefined).catch(console.error)
180
+ if (finalAltTextPath) {
181
+ formStore.setField(finalAltTextPath, undefined).catch(console.error)
180
182
  }
181
183
  }
182
184
  }
@@ -205,7 +207,7 @@
205
207
  <span class="chooser-data">{selectedAsset.name}</span>
206
208
  </div>
207
209
  <div class="tabs-container">
208
- <AssetTabs id={assetTabsId} {selectedAsset} showMetadata={$$slots.metadata != null} {altTextPath} {altTextRequired}>
210
+ <AssetTabs id={assetTabsId} {selectedAsset} showMetadata={$$slots.metadata != null} {altTextPath} {altTextRequired} {altTextHelp} >
209
211
  <slot name="metadata" slot="metadata" {selectedAsset} />
210
212
  </AssetTabs>
211
213
  </div>
@@ -22,6 +22,7 @@ declare const __propDef: {
22
22
  selectedAsset?: AnyItem | RawURL | BrokenURL | undefined;
23
23
  altTextPath?: string | undefined;
24
24
  altTextRequired?: boolean;
25
+ altTextHelp?: string;
25
26
  };
26
27
  events: {
27
28
  [evt: string]: CustomEvent<any>;
@@ -16,6 +16,7 @@
16
16
  export let altTextRequired: boolean = false
17
17
  // if they say it's required but don't provide a path, default to 'altText'
18
18
  export let altTextPath: string | undefined = altTextRequired ? 'altText' : undefined
19
+ export let altTextHelp: string | undefined
19
20
  const chooserClient = getContext<Client>(CHOOSER_API_CONTEXT)
20
21
 
21
22
  let tabListEl: HTMLUListElement
@@ -93,7 +94,7 @@
93
94
 
94
95
  {#if activeTab === `${id}-alttext`}
95
96
  <div id={`${id}-alttext`} class="tab-content" role="tabpanel" aria-labelledby="{id}-alttext-tab">
96
- {#if altTextPath}<FieldTextArea required={altTextRequired} path={altTextPath} label="Alt. Text" helptext="Describes the asset for visually-impaired users and search engines."/>{/if}
97
+ {#if altTextPath}<FieldTextArea required={altTextRequired} path={altTextPath} label="Alt. Text" helptext={altTextHelp} />{/if}
97
98
  </div>
98
99
  {/if}
99
100
  {/if}
@@ -7,6 +7,7 @@ declare const __propDef: {
7
7
  showMetadata?: boolean;
8
8
  altTextRequired?: boolean;
9
9
  altTextPath?: string | undefined;
10
+ altTextHelp: string | undefined;
10
11
  };
11
12
  events: {
12
13
  [evt: string]: CustomEvent<any>;
@@ -105,7 +105,7 @@
105
105
  {/if}
106
106
  </section>
107
107
  <ChooserPreview {thumbnailExpanded} {previewId} {store} on:thumbnailsizechange={() => { thumbnailExpanded = !thumbnailExpanded }}/>
108
- {#if showAltTextOption && $preview && $preview.type === 'asset' && $preview.image}
108
+ {#if showAltTextOption}
109
109
  <section class="alt-text-options">
110
110
  <label>
111
111
  <input bind:this={altTextCheckbox} type="checkbox" />
@@ -1,12 +1,13 @@
1
1
  <script lang="ts">
2
+ import arrowsCircleIcon from '@iconify-icons/ph/arrows-clockwise-fill'
3
+ import arrowsOutIcon from '@iconify-icons/ph/arrows-out-fill'
2
4
  import { resize, ScreenReaderOnly } from '@txstate-mws/svelte-components'
3
- import { onMount, tick } from 'svelte'
5
+ import { FORM_CONTEXT, FORM_INHERITED_PATH, type FormStore } from '@txstate-mws/svelte-forms'
6
+ import { getContext, onMount, tick } from 'svelte'
4
7
  import { isNotBlank, randomid } from 'txstate-utils'
5
8
  import FieldStandard from '../FieldStandard.svelte'
6
9
  import { CropperStore, type CropOutput } from './cropper'
7
10
  import { Button } from '..'
8
- import arrowsOutIcon from '@iconify-icons/ph/arrows-out-fill'
9
- import arrowsCircleIcon from '@iconify-icons/ph/arrows-clockwise-fill'
10
11
 
11
12
  export let id: string | undefined = undefined
12
13
  export let path: string
@@ -18,20 +19,17 @@
18
19
  export let conditional: boolean | undefined = undefined
19
20
  export let helptext: string | undefined = undefined
20
21
 
21
- const store = new CropperStore({ width: 0, height: 0, minSelection, targetAspect: selectionAspectRatio })
22
- const { output, outputPct, selection } = store
22
+ const inheritedPath = getContext<string>(FORM_INHERITED_PATH)
23
+ const finalPath = [inheritedPath, path].filter(isNotBlank).join('.')
24
+ const store = getContext<FormStore>(FORM_CONTEXT)
25
+ const val = store.getField<CropOutput>(finalPath)
23
26
 
24
- let setVal: (val: any) => void
25
- let value: CropOutput | undefined
26
- const initialAspectRatio: number = selectionAspectRatio
27
- function init (spValue, spSetVal) {
28
- setVal = spSetVal
29
- value = spValue
30
- }
27
+ const cropperStore = new CropperStore({ width: 0, height: 0, minSelection, targetAspect: selectionAspectRatio })
28
+ const { output, outputPct, selection } = cropperStore
31
29
 
32
- $: store.setOutput(value)
30
+ $: cropperStore.setOutput($val)
33
31
  function reactToOutput (...args: any[]) {
34
- if (mounted) setVal?.($output)
32
+ if (mounted) store.setField(finalPath, $output)
35
33
  }
36
34
  $: reactToOutput($output)
37
35
 
@@ -39,7 +37,7 @@
39
37
  function updateRect (..._: any) {
40
38
  if (!container) return false
41
39
  rect = container.getBoundingClientRect()
42
- store.updateDimensions(rect.width, rect.height)
40
+ cropperStore.updateDimensions(rect.width, rect.height)
43
41
  return true
44
42
  }
45
43
 
@@ -59,7 +57,7 @@
59
57
  const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
60
58
  if (isInside(clientX, clientY)) {
61
59
  e.preventDefault()
62
- store.startDrag(clientX - rect.left, clientY - rect.top)
60
+ cropperStore.startDrag(clientX - rect.left, clientY - rect.top)
63
61
  }
64
62
  }
65
63
 
@@ -68,24 +66,24 @@
68
66
  if (window.TouchEvent && e instanceof TouchEvent && e.touches.length > 1) return
69
67
  const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
70
68
  const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
71
- if (e instanceof MouseEvent && !e.buttons && $store.drag) store.endDrag()
72
- if (isInside(clientX, clientY) || $store.drag) store.mouseMove(...relativeToRect(clientX, clientY))
69
+ if (e instanceof MouseEvent && !e.buttons && $cropperStore.drag) cropperStore.endDrag()
70
+ if (isInside(clientX, clientY) || $cropperStore.drag) cropperStore.mouseMove(...relativeToRect(clientX, clientY))
73
71
  }
74
72
 
75
73
  function onMouseUp (e: MouseEvent | TouchEvent) {
76
74
  if (!updateRect()) return
77
- store.endDrag()
75
+ cropperStore.endDrag()
78
76
  const clientX = e instanceof MouseEvent ? e.clientX : e.changedTouches[0].clientX
79
77
  const clientY = e instanceof MouseEvent ? e.clientY : e.changedTouches[0].clientY
80
78
  if (isInside(clientX, clientY)) {
81
- store.mouseMove(...relativeToRect(clientX, clientY))
79
+ cropperStore.mouseMove(...relativeToRect(clientX, clientY))
82
80
  container?.querySelector<HTMLDivElement>('.selectionHilite')?.focus()
83
81
  }
84
82
  }
85
83
 
86
84
  function onMaximize () {
87
85
  if (!updateRect()) return
88
- store.maximize()
86
+ cropperStore.maximize()
89
87
  }
90
88
 
91
89
  function onKeyDown (type: 'move' | 'tl' | 'tr' | 'bl' | 'br') {
@@ -104,9 +102,9 @@
104
102
  } else return
105
103
  const step = e.shiftKey ? (e.altKey || e.metaKey ? 80 : 20) : (e.altKey || e.metaKey ? 40 : 1)
106
104
  if (type === 'move') {
107
- store.move(left ? -1 * step : (right ? step : 0), up ? -1 * step : (down ? step : 0))
105
+ cropperStore.move(left ? -1 * step : (right ? step : 0), up ? -1 * step : (down ? step : 0))
108
106
  } else {
109
- store.expand(type, ((tl || bl) && right) || ((tl || tr) && down) || ((tr || br) && left) || ((bl || br) && up) ? -1 * step : step)
107
+ cropperStore.expand(type, ((tl || bl) && right) || ((tl || tr) && down) || ((tr || br) && left) || ((bl || br) && up) ? -1 * step : step)
110
108
  }
111
109
  }
112
110
  }
@@ -121,15 +119,9 @@
121
119
  const movedescid = randomid()
122
120
  let focusWithin = false
123
121
 
124
- let arChanged = false
125
122
  async function reactToAspectRatio (ar) {
126
123
  if (!ar) return
127
- store.updateTargetAspect(ar)
128
- await tick()
129
- if (ar !== initialAspectRatio || arChanged) {
130
- store.maximize()
131
- arChanged = true
132
- }
124
+ cropperStore.updateTargetAspect(ar)
133
125
  }
134
126
  $: void reactToAspectRatio(selectionAspectRatio)
135
127
 
@@ -143,7 +135,7 @@
143
135
  } else {
144
136
  if (e.target.src !== initialVal || srcChanged) {
145
137
  await tick()
146
- store.maximize()
138
+ cropperStore.maximize()
147
139
  srcChanged = true
148
140
  }
149
141
  }
@@ -152,18 +144,17 @@
152
144
 
153
145
  <svelte:window on:mousemove={onMouseMove} on:mouseup={onMouseUp} on:touchend={onMouseUp} on:touchcancel={onMouseUp} />
154
146
  <FieldStandard bind:id {label} {path} {required} conditional={conditional && isNotBlank(imageSrc)} {helptext} {descid} let:value let:setVal let:helptextid>
155
- {@const _ = init(value, setVal)}
156
147
  {#if isNotBlank(imageSrc)}
157
148
  <div on:focusin={() => { focusWithin = true }} on:focusout={() => { focusWithin = false }}>
158
149
  <div class="action-buttons">
159
150
  <Button type="button" on:click={onMaximize} icon={arrowsOutIcon}>Center and Maximize</Button>
160
- <Button type="button" on:click={() => store.reset()} icon={arrowsCircleIcon} class="btn-clear">Clear</Button>
151
+ <Button type="button" on:click={() => cropperStore.reset()} icon={arrowsCircleIcon} class="btn-clear">Clear</Button>
161
152
  </div>
162
153
  <div class="cropper-instructions">
163
154
  Click and drag to select a section of your image to use.
164
155
  </div>
165
156
  <!-- svelte-ignore a11y-no-static-element-interactions -->
166
- <div bind:this={container} use:resize on:resize={() => updateRect()} class="crop-image-container" on:mousedown={onMouseDown} on:touchstart={onMouseDown} on:touchmove={onMouseMove} style:cursor={$store.cursor}>
157
+ <div bind:this={container} use:resize on:resize={() => updateRect()} class="crop-image-container" on:mousedown={onMouseDown} on:touchstart={onMouseDown} on:touchmove={onMouseMove} style:cursor={$cropperStore.cursor}>
167
158
  <img class="crop-image" src={imageSrc} alt="" on:load={onimageload}/>
168
159
  {#if $selection && $outputPct}
169
160
  <div class='crop-bg'>
@@ -182,7 +173,7 @@
182
173
  <ScreenReaderOnly id={movedescid}>arrows move crop selection, hold shift and/or cmd/alt for bigger steps</ScreenReaderOnly>
183
174
  {#if focusWithin}
184
175
  <ScreenReaderOnly arialive="polite">top left x y coordinate is ({Math.round($selection.left)}, {Math.round($selection.top)}) bottom right x y coordinate is ({Math.round($selection.right)}, {Math.round($selection.bottom)})</ScreenReaderOnly>
185
- <ScreenReaderOnly arialive="polite">crop area is {Math.round($store.width)} pixels wide by {Math.round($store.height)} pixels tall</ScreenReaderOnly>
176
+ <ScreenReaderOnly arialive="polite">crop area is {Math.round($cropperStore.width)} pixels wide by {Math.round($cropperStore.height)} pixels tall</ScreenReaderOnly>
186
177
  {/if}
187
178
  <!-- svelte-ignore a11y-no-static-element-interactions -->
188
179
  <div class='selectionCorner tl'
@@ -19,7 +19,50 @@ export class CropperStore extends Store {
19
19
  this.update(v => ({ ...v, width, height }));
20
20
  }
21
21
  updateTargetAspect(ar) {
22
- this.update(v => ({ ...v, targetAspect: ar }));
22
+ this.update(v => {
23
+ if (v.selection) {
24
+ const selection = this.convertToPx(v.selection, v.width, v.height);
25
+ const selar = (selection.right - selection.left) / (selection.bottom - selection.top);
26
+ // adjust the aspect ratio to match the new aspect ratio, keep the center the same
27
+ // and preserve the pixel area of the selection. Compute new width and height
28
+ // from the original area and the new aspect ratio, then recenter the box.
29
+ const currWidth = selection.right - selection.left;
30
+ const currHeight = selection.bottom - selection.top;
31
+ const area = currWidth * currHeight;
32
+ const newWidth = Math.sqrt(area * ar);
33
+ const newHeight = Math.sqrt(area / ar);
34
+ const centerX = (selection.left + selection.right) / 2;
35
+ const centerY = (selection.top + selection.bottom) / 2;
36
+ selection.left = centerX - newWidth / 2;
37
+ selection.right = centerX + newWidth / 2;
38
+ selection.top = centerY - newHeight / 2;
39
+ selection.bottom = centerY + newHeight / 2;
40
+ // shrink the box until its width and height are less than the width and height of the drawing area
41
+ // maintaining the center
42
+ let selWidth = selection.right - selection.left;
43
+ let selHeight = selection.bottom - selection.top;
44
+ if (selWidth > v.width || selHeight > v.height) {
45
+ const scale = Math.min(v.width / selWidth, v.height / selHeight);
46
+ const cX = (selection.left + selection.right) / 2;
47
+ const cY = (selection.top + selection.bottom) / 2;
48
+ const halfW = (selWidth * scale) / 2;
49
+ const halfH = (selHeight * scale) / 2;
50
+ selection.left = cX - halfW;
51
+ selection.right = cX + halfW;
52
+ selection.top = cY - halfH;
53
+ selection.bottom = cY + halfH;
54
+ }
55
+ // translate selection until all edges are within the drawing area
56
+ const dx = Math.max(0 - selection.left, Math.min(v.width - selection.right, 0));
57
+ const dy = Math.max(0 - selection.top, Math.min(v.height - selection.bottom, 0));
58
+ selection.left += dx;
59
+ selection.right += dx;
60
+ selection.top += dy;
61
+ selection.bottom += dy;
62
+ return { ...v, selection: this.convertToPct(selection, v.width, v.height), targetAspect: ar };
63
+ }
64
+ return { ...v, targetAspect: ar };
65
+ });
23
66
  }
24
67
  /**
25
68
  * The svelte component is responsible for making sure x and y are relative to the drawing area,
@@ -114,7 +114,7 @@
114
114
  </script>
115
115
 
116
116
  <FieldStandard bind:id {path} {descid} {label} {required} {defaultValue} {conditional} {helptext} let:value let:valid let:invalid let:id let:onBlur let:setVal let:messagesid let:helptextid>
117
- <Icon icon={`${value.prefix === 'fab' ? 'fa6-brands' : 'fa6-solid'}:${value.icon?.slice(3) ?? 'graduation-cap'}`}/>
117
+ <Icon icon={`${value.prefix === 'fab' ? 'fa7-brands' : 'fa7-solid'}:${value.icon?.slice(3) ?? 'graduation-cap'}`}/>
118
118
  <button type="button" {id} class="select-icon" on:click={() => { modalOpen = true }} aria-describedby={getDescribedBy([descid, messagesid, helptextid])} on:blur={onBlur}>Select New Icon</button>
119
119
  {#if modalOpen}
120
120
  <Modal>
@@ -142,7 +142,7 @@
142
142
  {#each visibleIcons as icon, idx (icon.class)}
143
143
 
144
144
  <div bind:this={iconElements[idx]} id={icon.class} class="icon-picker-item" role="radio" aria-checked={icon.class === selected.icon} tabindex={icon.class === selected.icon ? 0 : -1} data-index={idx} on:click={() => onSelectIcon(icon.class)} on:keydown={onKeyDown}>
145
- <Icon icon={`${iconToPrefix[icon.class] === 'fab' ? 'fa6-brands' : 'fa6-solid'}:${icon.class.slice(3)}`}/>
145
+ <Icon icon={`${iconToPrefix[icon.class] === 'fab' ? 'fa7-brands' : 'fa7-solid'}:${icon.class.slice(3)}`}/>
146
146
  <ScreenReaderOnly>{icon.label}</ScreenReaderOnly>
147
147
  </div>
148
148
  {:else}