@txstate-mws/svelte-components 1.0.1 → 1.1.0

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.
Files changed (162) hide show
  1. package/.eslintrc.cjs +37 -0
  2. package/README.md +33 -14
  3. package/docs/stores.md +1 -1
  4. package/package.json +24 -74
  5. package/playwright.config.js +112 -0
  6. package/src/app.html +14 -0
  7. package/src/global.d.ts +1 -0
  8. package/src/lib/actions/buttonify.ts +27 -0
  9. package/src/lib/actions/glue.ts +96 -0
  10. package/src/lib/actions/index.ts +7 -0
  11. package/src/lib/actions/offset.ts +43 -0
  12. package/src/lib/actions/portal.ts +13 -0
  13. package/src/lib/actions/resize.ts +72 -0
  14. package/src/lib/actions/sticky.ts +105 -0
  15. package/src/lib/actions/stickyfixed.ts +160 -0
  16. package/src/lib/components/Card.svelte +25 -0
  17. package/src/lib/components/CardLayout.svelte +188 -0
  18. package/{dist/ts → src/lib}/components/CollapsingTable.svelte +6 -5
  19. package/{dist/ts → src/lib}/components/ConditionalWrapper.svelte +0 -0
  20. package/{dist/ts → src/lib}/components/FocusLock.svelte +7 -6
  21. package/{dist/ts → src/lib}/components/Modal.svelte +5 -4
  22. package/src/lib/components/MultiSelect.svelte +174 -0
  23. package/{dist/ts → src/lib}/components/PopupMenu.svelte +22 -16
  24. package/{dist/ts → src/lib}/components/ScreenReaderOnly.svelte +2 -2
  25. package/src/lib/components/index.ts +9 -0
  26. package/src/lib/index.ts +4 -0
  27. package/src/lib/types/CardLayout.ts +14 -0
  28. package/src/lib/types/CollapsingTable.ts +8 -0
  29. package/src/lib/types/PopupMenu.ts +5 -0
  30. package/src/lib/types/index.ts +3 -0
  31. package/src/lib/util/bodyoffset.ts +77 -0
  32. package/src/lib/util/index.ts +3 -0
  33. package/src/lib/util/repositioned.ts +91 -0
  34. package/src/lib/util/util.ts +36 -0
  35. package/src/routes/__layout.svelte +125 -0
  36. package/src/routes/actions/index.svelte +1 -0
  37. package/src/routes/actions/offset.svelte +4 -0
  38. package/src/routes/actions/offsetdebounce.svelte +4 -0
  39. package/src/routes/actions/resize.svelte +4 -0
  40. package/src/routes/actions/resizedebounce.svelte +4 -0
  41. package/src/routes/actions/sticky.svelte +4 -0
  42. package/src/routes/cards/index.svelte +4 -0
  43. package/src/routes/cards/preserve.svelte +4 -0
  44. package/src/routes/collapsingtable/index.svelte +4 -0
  45. package/src/routes/collapsingtable/stickyheader.svelte +4 -0
  46. package/src/routes/index.svelte +2 -0
  47. package/src/routes/modal.svelte +4 -0
  48. package/src/routes/multiselect.svelte +4 -0
  49. package/src/routes/popupmenu/body.svelte +4 -0
  50. package/src/routes/popupmenu/complex.svelte +4 -0
  51. package/src/routes/popupmenu/empty.svelte +5 -0
  52. package/src/routes/popupmenu/index.svelte +4 -0
  53. package/src/routes/popupmenu/parent.svelte +4 -0
  54. package/src/routes/popupmenu/scrollable.svelte +27 -0
  55. package/src/routes/popupmenu/showselected.svelte +4 -0
  56. package/src/test/Cards.svelte +64 -0
  57. package/{dist/ts/tests/CollapsingTable → src/test}/CollapsingTableTest.svelte +2 -2
  58. package/{dist/ts/tests/Modal → src/test}/ModalTest.svelte +3 -3
  59. package/src/test/MultiSelectTest.svelte +26 -0
  60. package/{dist/ts/tests/Actions → src/test}/Offset.svelte +2 -4
  61. package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuBody.svelte +1 -1
  62. package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuButtonIsParent.svelte +2 -2
  63. package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuComplex.svelte +2 -2
  64. package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuSimple.svelte +2 -2
  65. package/{dist/ts/tests/Actions → src/test}/Resize.svelte +2 -4
  66. package/{dist/ts/tests/Actions → src/test}/Sticky.svelte +1 -1
  67. package/static/layout.css +263 -0
  68. package/svelte.config.js +15 -0
  69. package/tests/.eslintrc.cjs +19 -0
  70. package/tests/CardLayout.test.js +41 -0
  71. package/tests/CollapsingTable.test.js +37 -0
  72. package/tests/Modal.test.js +18 -0
  73. package/tests/MultiSelect.test.js +58 -0
  74. package/tests/PopupMenu.test.js +55 -0
  75. package/tests/common.js +4 -0
  76. package/tsconfig.json +31 -0
  77. package/LICENSE +0 -21
  78. package/dist/actions/buttonify.js +0 -31
  79. package/dist/actions/buttonify.js.map +0 -1
  80. package/dist/actions/glue.js +0 -87
  81. package/dist/actions/glue.js.map +0 -1
  82. package/dist/actions/index.js +0 -20
  83. package/dist/actions/index.js.map +0 -1
  84. package/dist/actions/offset.js +0 -35
  85. package/dist/actions/offset.js.map +0 -1
  86. package/dist/actions/portal.js +0 -19
  87. package/dist/actions/portal.js.map +0 -1
  88. package/dist/actions/resize.js +0 -48
  89. package/dist/actions/resize.js.map +0 -1
  90. package/dist/actions/sticky.js +0 -93
  91. package/dist/actions/sticky.js.map +0 -1
  92. package/dist/actions/stickyfixed.js +0 -166
  93. package/dist/actions/stickyfixed.js.map +0 -1
  94. package/dist/components/CollapsingTable.svelte +0 -147
  95. package/dist/components/ConditionalWrapper.svelte +0 -38
  96. package/dist/components/FocusLock.svelte +0 -105
  97. package/dist/components/Modal.svelte +0 -52
  98. package/dist/components/PopupMenu.svelte +0 -282
  99. package/dist/components/ScreenReaderOnly.svelte +0 -25
  100. package/dist/components/index.js +0 -19
  101. package/dist/components/index.js.map +0 -1
  102. package/dist/index.js +0 -17
  103. package/dist/index.js.map +0 -1
  104. package/dist/lib/activestore.js +0 -123
  105. package/dist/lib/activestore.js.map +0 -1
  106. package/dist/lib/bodyoffset.js +0 -76
  107. package/dist/lib/bodyoffset.js.map +0 -1
  108. package/dist/lib/convertedstore.js +0 -41
  109. package/dist/lib/convertedstore.js.map +0 -1
  110. package/dist/lib/derivedstore.js +0 -65
  111. package/dist/lib/derivedstore.js.map +0 -1
  112. package/dist/lib/index.js +0 -22
  113. package/dist/lib/index.js.map +0 -1
  114. package/dist/lib/repositioned.js +0 -92
  115. package/dist/lib/repositioned.js.map +0 -1
  116. package/dist/lib/safestore.js +0 -16
  117. package/dist/lib/safestore.js.map +0 -1
  118. package/dist/lib/store.js +0 -27
  119. package/dist/lib/store.js.map +0 -1
  120. package/dist/lib/substore.js +0 -47
  121. package/dist/lib/substore.js.map +0 -1
  122. package/dist/lib/util.js +0 -16
  123. package/dist/lib/util.js.map +0 -1
  124. package/dist/ts/actions/buttonify.d.ts +0 -8
  125. package/dist/ts/actions/glue.d.ts +0 -16
  126. package/dist/ts/actions/index.d.ts +0 -7
  127. package/dist/ts/actions/offset.d.ts +0 -15
  128. package/dist/ts/actions/portal.d.ts +0 -4
  129. package/dist/ts/actions/resize.d.ts +0 -16
  130. package/dist/ts/actions/sticky.d.ts +0 -33
  131. package/dist/ts/actions/stickyfixed.d.ts +0 -25
  132. package/dist/ts/components/index.d.ts +0 -6
  133. package/dist/ts/index.d.ts +0 -4
  134. package/dist/ts/lib/activestore.d.ts +0 -60
  135. package/dist/ts/lib/bodyoffset.d.ts +0 -13
  136. package/dist/ts/lib/convertedstore.d.ts +0 -16
  137. package/dist/ts/lib/derivedstore.d.ts +0 -23
  138. package/dist/ts/lib/index.d.ts +0 -9
  139. package/dist/ts/lib/repositioned.d.ts +0 -13
  140. package/dist/ts/lib/safestore.d.ts +0 -4
  141. package/dist/ts/lib/store.d.ts +0 -16
  142. package/dist/ts/lib/substore.d.ts +0 -24
  143. package/dist/ts/lib/util.d.ts +0 -2
  144. package/dist/ts/tests/Actions/Actions.stories.d.ts +0 -1
  145. package/dist/ts/tests/CollapsingTable/CollapsingTable.stories.d.ts +0 -1
  146. package/dist/ts/tests/CollapsingTable/CollapsingTable.test.d.ts +0 -1
  147. package/dist/ts/tests/Modal/Modal.stories.d.ts +0 -1
  148. package/dist/ts/tests/Modal/Modal.test.d.ts +0 -1
  149. package/dist/ts/tests/PopupMenu/PopupMenu.stories.d.ts +0 -1
  150. package/dist/ts/tests/PopupMenu/PopupMenu.test.d.ts +0 -1
  151. package/dist/ts/tests/stores/deepstore.test.d.ts +0 -1
  152. package/dist/ts/types/CollapsingTable.d.ts +0 -8
  153. package/dist/ts/types/PopupMenu.d.ts +0 -5
  154. package/dist/ts/types/index.d.ts +0 -2
  155. package/dist/types/CollapsingTable.js +0 -3
  156. package/dist/types/CollapsingTable.js.map +0 -1
  157. package/dist/types/PopupMenu.js +0 -3
  158. package/dist/types/PopupMenu.js.map +0 -1
  159. package/dist/types/index.js +0 -15
  160. package/dist/types/index.js.map +0 -1
  161. package/esm/index.js +0 -1
  162. package/esm/package.json +0 -3
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from 'svelte'
3
+ import { randomid } from 'txstate-utils'
4
+ import ScreenReaderOnly from './ScreenReaderOnly.svelte'
5
+ import DefaultPopupMenu from './PopupMenu.svelte'
6
+ import { debouncedPromise, modifierKey, selectionIsLeft } from '$lib/util'
7
+ import type { PopupMenuItem } from '$lib/types'
8
+
9
+ export let id = randomid()
10
+ export let name: string
11
+ export let getOptions: (search: string) => Promise<PopupMenuItem[]>|PopupMenuItem[]
12
+ export let menuContainerClass: string|undefined = undefined
13
+ export let menuClass: string|undefined = undefined
14
+ export let menuItemClass: string|undefined = undefined
15
+ export let menuItemHilitedClass: string|undefined = undefined
16
+ export let selected: { value: string, label: string }[] = []
17
+ export let placeholder = ''
18
+ export let emptyText: string|undefined = undefined
19
+ export let PopupMenu = DefaultPopupMenu
20
+
21
+ let menushown: boolean
22
+ let options = []
23
+ let hilitedpill: string|undefined
24
+ let inputvalue = ''
25
+ let popupvalue = undefined
26
+ let inputelement: HTMLInputElement
27
+ const descriptionid = randomid()
28
+
29
+ $: debouncedGetOptions = debouncedPromise(getOptions, 100, [])
30
+ $: selectedSet = new Set(selected.map(s => s.value))
31
+ async function reactToInput (..._: any) {
32
+ const saveval = inputvalue
33
+ const rawOptions = await debouncedGetOptions(saveval)
34
+ if (inputvalue !== saveval) return // ignore any results that are out of date
35
+ options = rawOptions.filter(o => !selectedSet.has(o.value))
36
+ if (typeof document !== 'undefined' && inputelement === document.activeElement) menushown = true
37
+ }
38
+ $: reactToInput(inputvalue, debouncedGetOptions, selectedSet)
39
+ $: availablemessage = options.filter(o => o.value).length + ' autocomplete choices available'
40
+
41
+ function addSelection (e: CustomEvent & { detail: PopupMenuItem }) {
42
+ inputvalue = ''
43
+ const opt = { ...e.detail, label: e.detail.label || e.detail.value }
44
+ selected = [...selected, opt]
45
+ popupvalue = undefined
46
+ }
47
+ function removeSelection (opt: typeof selected[number], idx: number, nextfocus = 1) {
48
+ if (hilitedpill === opt.value) {
49
+ hilitedpill = selected[idx + nextfocus]?.value
50
+ }
51
+ selected = selected.filter(s => s.value !== opt.value)
52
+ }
53
+
54
+ function inputkeydown (e) {
55
+ if (modifierKey(e)) return
56
+ if (e.key === 'ArrowLeft' && selectionIsLeft(inputelement)) {
57
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : selected.length
58
+ if (idx !== 0) hilitedpill = selected[idx - 1].value
59
+ } else if (e.key === 'ArrowRight' && selectionIsLeft(inputelement)) {
60
+ if (hilitedpill) e.preventDefault()
61
+ const idx = hilitedpill ? selected.findIndex(s => s.value === hilitedpill) : undefined
62
+ if (idx != null) {
63
+ if (idx === selected.length - 1) hilitedpill = undefined
64
+ else hilitedpill = selected[idx + 1].value
65
+ }
66
+ } else if (['Backspace', 'Delete', 'Clear', ' ', 'Enter'].includes(e.key)) {
67
+ if (hilitedpill) {
68
+ const idx = selected.findIndex(s => s.value === hilitedpill)
69
+ removeSelection(selected[idx], idx, e.key === 'Delete' ? 1 : -1)
70
+ e.preventDefault()
71
+ } else if (e.key === 'Backspace' && selectionIsLeft(inputelement)) {
72
+ removeSelection(selected[selected.length - 1], selected.length - 1, -1)
73
+ e.preventDefault()
74
+ }
75
+ }
76
+ }
77
+ function inputfocus () {
78
+ reactToInput()
79
+ }
80
+
81
+ let popuphilited
82
+ $: if (popuphilited != null) hilitedpill = undefined
83
+ function reactToHilite (..._: any) {
84
+ if (!inputelement) return
85
+ if (hilitedpill) {
86
+ popuphilited = undefined
87
+ inputelement.setAttribute('aria-activedescendant', id + hilitedpill)
88
+ } else inputelement.removeAttribute('aria-activedescendant')
89
+ }
90
+ $: reactToHilite(hilitedpill, id)
91
+
92
+ const dispatch = createEventDispatcher()
93
+ $: dispatch('change', selected)
94
+ </script>
95
+
96
+ <fieldset>
97
+ <ul class="multiselect-selected" role="listbox" on:click={() => inputelement.focus()}>
98
+ {#each selected as option, i}
99
+ <li id={id + option.value} role="option" tabindex="-1" class="multiselect-pill" class:hilited={hilitedpill === option.value}
100
+ on:click|preventDefault|stopPropagation={() => removeSelection(option, i, 1)}
101
+ aria-selected="true">
102
+ {option.label || option.value}
103
+ <ScreenReaderOnly>, click to remove</ScreenReaderOnly>
104
+ </li>
105
+ {/each}
106
+ <li class="input">
107
+ <input type="text" id={id} name={name} placeholder={placeholder}
108
+ bind:this={inputelement} bind:value={inputvalue}
109
+ on:focus={inputfocus} on:keydown={inputkeydown}
110
+ autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list"
111
+ aria-describedby="{descriptionid}">
112
+ </li>
113
+ </ul>
114
+ <ScreenReaderOnly id={descriptionid} arialive="assertive">
115
+ <span>{selected.length ? selected.length + ' selected' : 'select multiple, none selected'}, up down to choose, left right to hilite existing choices</span>
116
+ {#if menushown}<span>{availablemessage}, touch users explore to find autocomplete menu</span>{/if}
117
+ </ScreenReaderOnly>
118
+ <slot></slot>
119
+ </fieldset>
120
+ <svelte:component this={PopupMenu} bind:menushown bind:hilited={popuphilited} bind:value={popupvalue} align='bottomleft' {emptyText} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={options} buttonelement={inputelement} on:change={addSelection}></svelte:component>
121
+
122
+ <style>
123
+ fieldset {
124
+ border: 0;
125
+ padding: 0;
126
+ margin: 0;
127
+ }
128
+ fieldset * {
129
+ box-sizing: border-box;
130
+ }
131
+ .multiselect-selected {
132
+ margin: 0;
133
+ padding: 0;
134
+ list-style: none;
135
+ display: flex;
136
+ flex-wrap: wrap;
137
+ align-items: stretch;
138
+ padding: var(--multiselect-padding, 0.3em);
139
+ border: var(--multiselect-border, 1px solid #666666);
140
+ border-radius: var(--multiselect-radius, 0.3em);
141
+ }
142
+ .multiselect-selected:focus-within {
143
+ outline-width: var(--multiselect-focus-width, 2px);
144
+ outline-style: solid;
145
+ outline-color: var(--multiselect-focus-color, Highlight);
146
+ }
147
+ li.input {
148
+ flex-grow: 1;
149
+ }
150
+ input {
151
+ outline: 0;
152
+ border: 0;
153
+ min-width: 0;
154
+ width: 100%;
155
+ height: 100%;
156
+ }
157
+ .multiselect-pill {
158
+ cursor: pointer;
159
+ flex-grow: 0;
160
+ margin-right: 0.3em;
161
+ line-height: 1;
162
+ border-radius: var(--multiselect-pill-radius, 0.8em);
163
+ border: var(--multiselect-pill-border, 1px solid gray);
164
+ background-color: var(--multiselect-pill-bg, transparent);
165
+ color: var(--multiselect-pill-text, black);
166
+ padding: var(--multiselect-pill-padding, 0.3em 0.5em);
167
+ }
168
+ .multiselect-pill.hilited {
169
+ outline: 0;
170
+ background-color: var(--multiselect-pill-selected, gray);
171
+ border: var(--multiselect-pill-selected-border, 1px solid transparent);
172
+ color: var(--multiselect-pill-selected-text, white);
173
+ }
174
+ </style>
@@ -1,11 +1,12 @@
1
1
  <script lang="ts">
2
- import type { GlueAlignOpts, GlueAlignStore } from '../actions'
3
- import { glue, portal } from '../actions'
4
2
  import { createEventDispatcher, onDestroy, tick } from 'svelte'
5
3
  import { randomid } from 'txstate-utils'
6
- import type { PopupMenuItem } from '../types'
7
- import { Store } from '../lib'
4
+ import { Store } from '@txstate-mws/svelte-store'
5
+ import { glue, portal } from '$lib/actions'
6
+ import type { GlueAlignOpts, GlueAlignStore } from '$lib/actions'
7
+ import type { PopupMenuItem } from '$lib/types'
8
8
  import ScreenReaderOnly from './ScreenReaderOnly.svelte'
9
+ import { modifierKey } from '$lib/util'
9
10
  const dispatch = createEventDispatcher()
10
11
 
11
12
  export let menushown = false
@@ -24,10 +25,9 @@
24
25
  export let usePortal: HTMLElement|true|undefined = undefined
25
26
  export let emptyText: string|undefined = undefined
26
27
  export let value: string|undefined = undefined
27
-
28
- let hilited: number|undefined = undefined
28
+ export let hilited: number|undefined = undefined
29
29
  let menuelement: HTMLElement|undefined
30
- let itemelements: HTMLElement[] = []
30
+ const itemelements: HTMLElement[] = []
31
31
  const menuid = randomid()
32
32
  let firstactive = 0
33
33
  let lastactive = items.length - 1
@@ -50,14 +50,15 @@
50
50
  }
51
51
  await tick()
52
52
  if (!menushown) {
53
- hilited = undefined
54
53
  buttonelement.removeAttribute('aria-controls')
55
54
  buttonelement.setAttribute('aria-expanded', 'false')
55
+ if (hilited) buttonelement.removeAttribute('aria-activedescendant')
56
+ hilited = undefined
56
57
  } else {
57
58
  buttonelement.setAttribute('aria-controls', menuid)
58
59
  buttonelement.setAttribute('aria-expanded', 'true')
60
+ buttonelement.focus()
59
61
  }
60
- buttonelement.focus()
61
62
  }
62
63
  $: reactToMenuShown(menushown)
63
64
 
@@ -70,25 +71,26 @@
70
71
  }
71
72
 
72
73
  function onkeydown (e: KeyboardEvent) {
74
+ if (modifierKey(e)) return
73
75
  if (e.key === 'ArrowDown') {
74
76
  e.preventDefault()
75
77
  if (menushown) {
76
78
  let i = (hilited ?? firstactive - 1) + 1
77
- while(items[i]?.disabled || hiddenItem(items[i])) i++
79
+ while (items[i]?.disabled || hiddenItem(items[i])) i++
78
80
  move(i)
79
81
  } else {
80
82
  menushown = true
81
- move(firstactive)
83
+ tick().then(() => move(firstactive))
82
84
  }
83
85
  } else if (e.key === 'ArrowUp') {
84
86
  e.preventDefault()
85
87
  if (menushown) {
86
88
  let i = (hilited ?? lastactive + 1) - 1
87
- while(items[i]?.disabled || hiddenItem(items[i])) i--
89
+ while (items[i]?.disabled || hiddenItem(items[i])) i--
88
90
  move(i)
89
91
  } else {
90
92
  menushown = true
91
- move(lastactive)
93
+ tick().then(() => move(lastactive))
92
94
  }
93
95
  } else if (e.key === 'Enter') {
94
96
  e.preventDefault()
@@ -100,7 +102,7 @@
100
102
  }
101
103
  } else {
102
104
  menushown = true
103
- move(firstactive)
105
+ tick().then(() => move(firstactive))
104
106
  }
105
107
  } else if (e.key === ' ') {
106
108
  // buttonelement might be a text input if this popup is searchable,
@@ -121,7 +123,7 @@
121
123
  if (buttonelement.tagName !== 'INPUT') {
122
124
  e.preventDefault()
123
125
  menushown = true
124
- move(firstactive)
126
+ tick().then(() => move(firstactive))
125
127
  }
126
128
  }
127
129
  } else if (menushown && e.key === 'Escape') {
@@ -131,14 +133,18 @@
131
133
  }
132
134
  }
133
135
 
136
+ let blurTimer: any
134
137
  function onbuttonclick (e: MouseEvent) {
135
138
  e.preventDefault()
139
+ cancelAnimationFrame(blurTimer)
136
140
  menushown = !menushown
137
141
  }
138
142
 
139
143
  async function onblur (e: FocusEvent) {
140
144
  // tabindex=-1 on our menu elements means e.relatedTarget will be set
141
- if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) menushown = false
145
+ if (!(e.relatedTarget instanceof HTMLElement && menuelement?.contains(e.relatedTarget))) {
146
+ blurTimer = requestAnimationFrame(() => { menushown = false })
147
+ }
142
148
  }
143
149
 
144
150
  function cleanup (element: HTMLElement) {
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  export let id: string|undefined = undefined
3
- export let arialive: svelte.JSX.AriaAttributes['aria-live'] = undefined
4
- export let ariaatomic: svelte.JSX.AriaAttributes['aria-atomic'] = undefined
3
+ export let arialive: 'off' | 'assertive' | 'polite' | undefined = undefined
4
+ export let ariaatomic: boolean | 'false' | 'true' | undefined = undefined
5
5
  </script>
6
6
 
7
7
  <style>
@@ -0,0 +1,9 @@
1
+ export { default as CardLayout } from './CardLayout.svelte'
2
+ export { default as Card } from './Card.svelte'
3
+ export { default as CollapsingTable } from './CollapsingTable.svelte'
4
+ export { default as FocusLock } from './FocusLock.svelte'
5
+ export { default as ConditionalWrapper } from './ConditionalWrapper.svelte'
6
+ export { default as Modal } from './Modal.svelte'
7
+ export { default as MultiSelect } from './MultiSelect.svelte'
8
+ export { default as PopupMenu } from './PopupMenu.svelte'
9
+ export { default as ScreenReaderOnly } from './ScreenReaderOnly.svelte'
@@ -0,0 +1,4 @@
1
+ export * from './components/index.js'
2
+ export * from './actions/index.js'
3
+ export * from './types/index.js'
4
+ export * from './util/index.js'
@@ -0,0 +1,14 @@
1
+ import type { SettableSubject } from '@txstate-mws/svelte-store'
2
+ export const CARDLAYOUT = {}
3
+ export interface Block {
4
+ element: HTMLElement
5
+ order?: SettableSubject<number>
6
+ linebreak?: SettableSubject<boolean>
7
+ width?: SettableSubject<string>
8
+ height?: number
9
+ }
10
+ export interface CardLayoutContext {
11
+ registerBlock: (block: Block) => Block
12
+ recalculate: () => void
13
+ gutter: SettableSubject<number>
14
+ }
@@ -0,0 +1,8 @@
1
+ export interface CollapsingTableColumn {
2
+ key: string
3
+ title?: string
4
+ width?: number
5
+ neverhide?: boolean
6
+ bodyCellClass?: string
7
+ headerCellClass?: string
8
+ }
@@ -0,0 +1,5 @@
1
+ export interface PopupMenuItem {
2
+ value: string
3
+ label?: string
4
+ disabled?: boolean
5
+ }
@@ -0,0 +1,3 @@
1
+ export * from './CardLayout.js'
2
+ export * from './CollapsingTable.js'
3
+ export * from './PopupMenu.js'
@@ -0,0 +1,77 @@
1
+ export type ElementOffsets = Partial<ReturnType<typeof bodyOffset>>
2
+
3
+ export function bodyOffset (ele: HTMLElement) {
4
+ return targetOffset(ele, document.body)
5
+ }
6
+
7
+ export function targetOffset (ele: HTMLElement, target: HTMLElement) {
8
+ const offset = targetOffsetRecursive(ele, target)
9
+ return {
10
+ ...offset,
11
+ right: target.offsetWidth - offset.left - ele.offsetWidth,
12
+ bottom: target.offsetHeight - offset.top - ele.offsetHeight
13
+ }
14
+ }
15
+
16
+ export function sharedOffsetParent (a: HTMLElement, b: HTMLElement) {
17
+ if (!a || !b) return undefined
18
+ if (a.contains(b) && (a.firstElementChild as HTMLElement).offsetParent === a) return a
19
+ if (b.contains(a) && (b.firstElementChild as HTMLElement).offsetParent === b) return b
20
+ let c = a.offsetParent as HTMLElement
21
+ while (c instanceof HTMLElement && !c.contains(b)) c = c.offsetParent as HTMLElement
22
+ return c
23
+ }
24
+
25
+ function targetOffsetRecursive (ele: HTMLElement, target: HTMLElement, currentOffset?: { top: number, left: number }): { top: number, left: number } {
26
+ const { x, y } = getTranslate(ele)
27
+ const offset = {
28
+ top: ele.offsetTop + (currentOffset?.top ?? 0) + y,
29
+ left: ele.offsetLeft + (currentOffset?.left ?? 0) + x
30
+ }
31
+ if (ele.offsetParent instanceof HTMLElement && target !== ele.offsetParent && target.contains(ele.offsetParent)) return targetOffsetRecursive(ele.offsetParent, target, offset)
32
+ return offset
33
+ }
34
+
35
+ function getTranslate (element: HTMLElement) {
36
+ const style = window.getComputedStyle(element)
37
+ const matrix = style.transform
38
+
39
+ // No transform property. Simply return 0 values.
40
+ if (matrix === 'none' || !matrix) {
41
+ return {
42
+ x: 0,
43
+ y: 0,
44
+ z: 0
45
+ }
46
+ }
47
+
48
+ // Can either be 2d or 3d transform
49
+ const matrixType = matrix.includes('3d') ? '3d' : '2d'
50
+ const matrixValues = matrix.match(/matrix.*\((.+)\)/)![1].split(', ')
51
+
52
+ // 2d matrices have 6 values
53
+ // Last 2 values are X and Y.
54
+ // 2d matrices does not have Z value.
55
+ if (matrixType === '2d') {
56
+ return {
57
+ x: Number(matrixValues[4]),
58
+ y: Number(matrixValues[5]),
59
+ z: 0
60
+ }
61
+ }
62
+
63
+ // 3d matrices have 16 values
64
+ // The 13th, 14th, and 15th values are X, Y, and Z
65
+ if (matrixType === '3d') {
66
+ return {
67
+ x: Number(matrixValues[12]),
68
+ y: Number(matrixValues[13]),
69
+ z: Number(matrixValues[14])
70
+ }
71
+ }
72
+ return {
73
+ x: 0,
74
+ y: 0,
75
+ z: 0
76
+ }
77
+ }
@@ -0,0 +1,3 @@
1
+ export * from './bodyoffset.js'
2
+ export * from './repositioned.js'
3
+ export * from './util.js'
@@ -0,0 +1,91 @@
1
+ import { equal, isNotNull, randomid, toArray } from 'txstate-utils'
2
+ import { bodyOffset, targetOffset } from './bodyoffset.js'
3
+ import type { ElementOffsets } from './bodyoffset.js'
4
+
5
+ const observers = new Map<string, () => void>()
6
+ let mutationobserver: MutationObserver
7
+ export function watchForMutations (cb: () => void) {
8
+ function watch () {
9
+ for (const cb of observers.values()) cb()
10
+ }
11
+
12
+ if (observers.size === 0) {
13
+ mutationobserver ??= new MutationObserver(watch)
14
+ window.addEventListener('resize', watch, { passive: true })
15
+ mutationobserver.observe(document.body, {
16
+ subtree: true,
17
+ childList: true,
18
+ attributes: true,
19
+ characterData: true
20
+ })
21
+ }
22
+
23
+ const id = randomid()
24
+ observers.set(id, cb)
25
+ cb()
26
+
27
+ return {
28
+ destroy () {
29
+ observers.delete(id)
30
+ if (observers.size === 0) {
31
+ window.removeEventListener('resize', watch)
32
+ mutationobserver.disconnect()
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ export function watchForPositionChange (el: (HTMLElement|undefined)[]|HTMLElement|undefined, cb: ((offset: Required<ElementOffsets>) => void)|((offsets: Required<ElementOffsets>[]) => void)) {
39
+ let lastoffsets: ElementOffsets[] = []
40
+ let els = toArray(el).filter(isNotNull)
41
+ function watch () {
42
+ const offsets = els.map(bodyOffset)
43
+ if (offsets.some((offset, i) => !equal(offset, lastoffsets[i]))) {
44
+ lastoffsets = offsets
45
+ Array.isArray(el) ? (cb as any)(offsets) : (cb as any)(offsets[0])
46
+ }
47
+ }
48
+
49
+ const { destroy } = watchForMutations(watch)
50
+
51
+ return {
52
+ update (newEl: (HTMLElement|undefined)[]|HTMLElement|undefined, newCb: ((offset: Required<ElementOffsets>) => void)|((offsets: Required<ElementOffsets>[]) => void)) {
53
+ const changed = newEl !== el || newCb !== cb
54
+ if (changed) {
55
+ el = newEl
56
+ els = toArray(el).filter(isNotNull)
57
+ cb = newCb
58
+ watch()
59
+ }
60
+ },
61
+ destroy
62
+ }
63
+ }
64
+
65
+ export function watchForPositionChangeInContainer (el: HTMLElement|undefined, container: HTMLElement|undefined, cb: (offset: Required<ElementOffsets>) => void) {
66
+ let lastoffset: ElementOffsets
67
+
68
+ function watch () {
69
+ if (!el || !container) return
70
+ const offset = targetOffset(el, container)
71
+ if (!equal(offset, lastoffset)) {
72
+ lastoffset = offset
73
+ cb(offset)
74
+ }
75
+ }
76
+
77
+ const { destroy } = watchForMutations(watch)
78
+
79
+ return {
80
+ update (newEl: HTMLElement|undefined, newContainer: HTMLElement|undefined, newCb: (offset: Required<ElementOffsets>) => void) {
81
+ const changed = newEl !== el || newContainer !== container || newCb !== cb
82
+ if (changed) {
83
+ el = newEl
84
+ container = newContainer
85
+ cb = newCb
86
+ watch()
87
+ }
88
+ },
89
+ destroy
90
+ }
91
+ }
@@ -0,0 +1,36 @@
1
+ export function classes (...classNames: (string | undefined)[]) {
2
+ return classNames.filter(Boolean).join(' ')
3
+ }
4
+
5
+ export function debounced (fn: Function, timeout: number) {
6
+ let timer: NodeJS.Timeout
7
+ return (...args: any[]) => {
8
+ clearTimeout(timer)
9
+ timer = setTimeout(() => fn(...args), timeout)
10
+ }
11
+ }
12
+
13
+ export function debouncedPromise <T> (fn: (..._: any) => Promise<T>|T, timeout: number, defaultValue: T): (..._: any) => Promise<T> {
14
+ let version = 0
15
+ return async (...args: any[]) => {
16
+ const myversion = ++version
17
+ return await new Promise(resolve => {
18
+ setTimeout(() => {
19
+ if (version === myversion) {
20
+ version = 0
21
+ resolve(fn(...args))
22
+ } else {
23
+ resolve(defaultValue)
24
+ }
25
+ }, timeout)
26
+ })
27
+ }
28
+ }
29
+
30
+ export function modifierKey (e) {
31
+ return e.ctrlKey || e.altKey || e.metaKey || e.key === 'Insert'
32
+ }
33
+
34
+ export function selectionIsLeft (ele: HTMLInputElement|HTMLTextAreaElement) {
35
+ return ele.selectionStart === 0 && ele.selectionEnd === 0
36
+ }