@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,105 @@
1
+ import type { SettableSubject } from '@txstate-mws/svelte-store'
2
+ import { bodyOffset, watchForPositionChange } from '../util/index.js'
3
+ import type { ElementOffsets } from '../util/index.js'
4
+
5
+ export interface StickyArgs <T extends StickyStore = StickyStore> {
6
+ target?: HTMLElement
7
+ store?: SettableSubject<T>
8
+ }
9
+
10
+ export interface StickyStore {
11
+ floating?: boolean
12
+ translateY?: number
13
+ }
14
+
15
+ let scrolled = false
16
+
17
+ /**
18
+ * A robust implementation of stickyness based on `transform: translateY()`. Only use when
19
+ * `position: sticky` won't work for you. If `position: sticky` does work for you, use it,
20
+ * it performs better.
21
+ *
22
+ * Reasons `position: sticky` might not work:
23
+ * 1) it cannot work on a thead or tr, only td and th (and non-table elements)
24
+ * 2) it gets confused if any parent elements have an `overflow` set, even `overflow: hidden`
25
+ * 3) it always stays within the boundaries of the closest parent element that has a `position`
26
+ * * use:sticky accepts a `target` element as a parameter, defining the bounding container
27
+ *
28
+ * Note that if the element you place this on has other transforms, this is going to overwrite them.
29
+ *
30
+ * The main drawback to this approach is that it cannot do its work before scroll on mobile, only after,
31
+ * so on mobile you are going to see it visually lag a little during scrolling.
32
+ *
33
+ * You may also want to try use:stickyfixed. It is based on setting `position: fixed` when appropriate,
34
+ * and therefore animates better than this. However, it has a lot more work to do and is more
35
+ * fragile if your markup/CSS situation is very complex.
36
+ */
37
+ export function sticky (el: HTMLElement, config?: StickyArgs) {
38
+ let floating: boolean
39
+ let translateY: number
40
+ let scrollstart = Number.MAX_SAFE_INTEGER
41
+ let scrollend = Number.MAX_SAFE_INTEGER
42
+
43
+ function recalc (scroll: number) {
44
+ const newTranslateY = Math.max(Math.min(scroll, scrollend) - scrollstart, 0)
45
+ if (newTranslateY !== translateY) {
46
+ el.style.transform = `translateY(${newTranslateY}px)`
47
+ translateY = newTranslateY
48
+ floating = scroll >= scrollstart
49
+ config?.store?.update(v => ({ ...v, translateY, floating }))
50
+ }
51
+ }
52
+
53
+ function onscroll (_: Event) {
54
+ recalc(window.scrollY)
55
+ }
56
+
57
+ function onwheel (e: WheelEvent) {
58
+ e.preventDefault()
59
+ const scroll = window.scrollY + e.deltaY
60
+ recalc(scroll)
61
+ if (!scrolled) {
62
+ window.scrollTo({ top: scroll })
63
+ scrolled = true
64
+ setTimeout(() => {
65
+ scrolled = false
66
+ }, 0)
67
+ }
68
+ }
69
+
70
+ let lastOffsetParent: HTMLElement = el.offsetParent as HTMLElement
71
+ function onpositionchange (offset: Required<ElementOffsets>[]) {
72
+ const offsetParent = el.offsetParent
73
+ if (!(offsetParent instanceof HTMLElement)) return
74
+ if (lastOffsetParent !== offsetParent) {
75
+ offset[0] = bodyOffset(offsetParent)
76
+ update(offsetParent, onpositionchange)
77
+ lastOffsetParent = offsetParent
78
+ }
79
+ scrollstart = offset[0].top + el.offsetTop
80
+ const containeroffset = config?.target ? offset[1] : offset[0]
81
+ scrollend = Math.max(scrollstart, containeroffset.top + (config?.target ?? offsetParent).clientHeight - el.offsetHeight)
82
+ recalc(window.scrollY)
83
+ }
84
+
85
+ const { destroy, update } = watchForPositionChange([el.offsetParent as HTMLElement, config?.target], onpositionchange)
86
+ window.addEventListener('scroll', onscroll, { passive: true })
87
+ window.addEventListener('wheel', onwheel, { passive: false })
88
+
89
+ function stickyUpdate (newConfig?: StickyArgs) {
90
+ if (newConfig?.store !== config?.store) newConfig?.store?.update(v => ({ ...v, translateY, floating }))
91
+ config = newConfig
92
+ update([el.offsetParent as HTMLElement, config?.target], onpositionchange)
93
+ }
94
+
95
+ function stickyDestroy () {
96
+ window.removeEventListener('scroll', onscroll)
97
+ window.removeEventListener('wheel', onwheel)
98
+ destroy()
99
+ }
100
+
101
+ return {
102
+ update: stickyUpdate,
103
+ destroy: stickyDestroy
104
+ }
105
+ }
@@ -0,0 +1,160 @@
1
+ import { bodyOffset, watchForPositionChange } from '../util/index.js'
2
+ import type { ElementOffsets } from '../util/index.js'
3
+ import type { StickyArgs } from './sticky.js'
4
+
5
+ /**
6
+ * An implementation of stickyness based on `positon: fixed`. Only use when
7
+ * `position: sticky` won't work for you. If `position: sticky` does work for you, use it,
8
+ * it performs better.
9
+ *
10
+ * Reasons `position: sticky` might not work:
11
+ * 1) it cannot work on a thead or tr, only td and th (and non-table elements)
12
+ * 2) it gets confused if any parent elements have an `overflow` set, even `overflow: hidden`
13
+ * 3) it always stays within the boundaries of the closest parent element that has a `position`
14
+ * * use:sticky accepts a `target` element as a parameter, defining the bounding container
15
+ *
16
+ * Note that this implementation gets great animation performance at the cost of extra complexity.
17
+ * It does a lot of work like cloning the element you place it on, placing the cloned element next
18
+ * to yours in the DOM, setting widths to match between them, watching for mutations to keep them
19
+ * in sync, etc. Some of this work may get foiled if you have a complex html/css/js situation.
20
+ *
21
+ * You may also want to try use:sticky. It is based on setting `transform: translateY()` as appropriate,
22
+ * and is less fragile than this. However, it cannot do its work before the scroll on mobile, only after,
23
+ * so it will visually lag a bit on mobile.
24
+ */
25
+ export function stickyfixed (el: HTMLElement, config?: StickyArgs) {
26
+ let floating: boolean
27
+ let translateY: number
28
+ let scrollstart = Number.MAX_SAFE_INTEGER
29
+ let scrollend = Number.MAX_SAFE_INTEGER
30
+ let spacer: HTMLElement
31
+ let elheight: number
32
+ let left: number
33
+ let top = 0
34
+
35
+ function generatespacer (mutations?: MutationRecord[]) {
36
+ if (mutations?.every(m => m.attributeName === 'style')) return
37
+ spacer?.remove()
38
+ spacer = el.cloneNode(true) as HTMLElement
39
+ spacer.setAttribute('aria-hidden', 'true')
40
+ spacer.style.position = ''
41
+ spacer.style.top = ''
42
+ spacer.style.left = ''
43
+ spacer.style.width = ''
44
+ const tds = Array.from(spacer.querySelectorAll<HTMLElement>('td,th'))
45
+ for (const td of tds) td.style.width = ''
46
+ if (floating) el.parentElement?.insertBefore(spacer, el)
47
+ }
48
+ const mutationobserver = new MutationObserver(generatespacer)
49
+ mutationobserver.observe(el, {
50
+ subtree: true,
51
+ childList: true,
52
+ attributes: true,
53
+ characterData: true
54
+ })
55
+ generatespacer()
56
+
57
+ function adjustwidths () {
58
+ const tds = Array.from(el.querySelectorAll<HTMLElement>('td,th'))
59
+ if (floating) {
60
+ const spacertds = Array.from(spacer.querySelectorAll<HTMLElement>('td,th'))
61
+ const widths = spacertds.map(td => window.getComputedStyle(td).width)
62
+ const table = spacer.closest('table')
63
+ let extrawidth = 0
64
+ if (table) {
65
+ const cell = el.querySelector('th:last-child,td:last-child')
66
+ if (cell) {
67
+ const tstyle = window.getComputedStyle(table)
68
+ extrawidth = tstyle.borderCollapse === 'collapse' ? parseInt(window.getComputedStyle(cell).borderRightWidth) : 0
69
+ }
70
+ }
71
+ const width = `${parseInt(window.getComputedStyle(spacer).width) + extrawidth}px`
72
+ elheight = spacer.offsetHeight
73
+ for (let i = 0; i < tds.length; i++) tds[i].style.width = widths[i]
74
+ el.style.width = width
75
+ } else {
76
+ for (const td of tds) td.style.width = ''
77
+ el.style.width = ''
78
+ elheight = el.offsetHeight
79
+ }
80
+ }
81
+
82
+ const resizeobserver = new ResizeObserver(adjustwidths)
83
+
84
+ function recalc (scroll: number) {
85
+ const newTranslateY = Math.max(Math.min(scroll - 1, scrollend) - scrollstart, 0)
86
+ const newFloating = newTranslateY > 0
87
+ const newTop = Math.max(Math.min(scrollend - scroll, 0), -2 * elheight)
88
+ if (newFloating !== floating) {
89
+ floating = newFloating
90
+ if (floating) {
91
+ el.parentElement?.insertBefore(spacer, el)
92
+ el.style.position = 'fixed'
93
+ el.style.top = `${Math.min(scrollend - scroll, 0)}px`
94
+ el.style.left = `${left}px`
95
+ resizeobserver.observe(spacer)
96
+ } else {
97
+ el.style.position = ''
98
+ el.style.top = ''
99
+ el.style.left = ''
100
+ resizeobserver.disconnect()
101
+ spacer.remove()
102
+ }
103
+ adjustwidths()
104
+ }
105
+ if (newTranslateY !== translateY) {
106
+ translateY = newTranslateY
107
+ config?.store?.update(v => ({ ...v, translateY, floating }))
108
+ }
109
+ if (top !== newTop) {
110
+ top = newTop
111
+ if (floating) el.style.top = `${top}px`
112
+ }
113
+ }
114
+
115
+ function onscroll (_: Event) {
116
+ recalc(window.scrollY)
117
+ }
118
+
119
+ let lastOffsetParent: HTMLElement = el.offsetParent as HTMLElement
120
+ function onpositionchange (offset: Required<ElementOffsets>[]) {
121
+ const offsetParent = el.offsetParent
122
+ if (!(offsetParent instanceof HTMLElement)) return
123
+ if (lastOffsetParent !== offsetParent) {
124
+ offset[0] = bodyOffset(offsetParent)
125
+ update(offsetParent, onpositionchange)
126
+ lastOffsetParent = offsetParent
127
+ }
128
+ const table = el.closest('table')
129
+ const borderspacing = (table ? parseInt(window.getComputedStyle(table).borderSpacing) : 0)
130
+ scrollstart = offset[0].top + el.offsetTop
131
+ left = offset[0].left + (spacer.parentElement ? spacer.offsetLeft : el.offsetLeft)
132
+ if (el.tagName === 'TR') {
133
+ scrollstart -= borderspacing
134
+ }
135
+ const containeroffset = config?.target ? offset[1] ?? offset[0] : offset[0]
136
+ scrollend = Math.max(scrollstart, containeroffset.top + (config?.target ?? offsetParent).clientHeight - el.offsetHeight)
137
+ recalc(window.scrollY)
138
+ }
139
+
140
+ const { destroy, update } = watchForPositionChange([el.offsetParent as HTMLElement, config?.target], onpositionchange)
141
+ window.addEventListener('scroll', onscroll, { passive: true })
142
+
143
+ function stickyUpdate (newConfig?: StickyArgs) {
144
+ if (newConfig?.store !== config?.store) newConfig?.store?.update(v => ({ ...v, translateY, floating }))
145
+ config = newConfig
146
+ update([el.offsetParent as HTMLElement, config?.target], onpositionchange)
147
+ }
148
+
149
+ function stickyDestroy () {
150
+ resizeobserver.disconnect()
151
+ mutationobserver.disconnect()
152
+ window.removeEventListener('scroll', onscroll)
153
+ destroy()
154
+ }
155
+
156
+ return {
157
+ update: stickyUpdate,
158
+ destroy: stickyDestroy
159
+ }
160
+ }
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ export let className = ''
3
+ import { getContext } from 'svelte'
4
+ import { resize } from '$lib/actions'
5
+ import { CARDLAYOUT } from '$lib/types'
6
+ import type { CardLayoutContext, Block } from '$lib/types'
7
+ const block: Block = { element: undefined }
8
+ const { registerBlock, gutter, recalculate } = getContext<CardLayoutContext>(CARDLAYOUT)
9
+ const { width, order, linebreak } = registerBlock(block)
10
+ </script>
11
+
12
+ <li class="cardlayout-card {className}" bind:this={block.element} use:resize on:resize={recalculate} style="margin-bottom: {$gutter}px; width: {$width}; order: {$order};">
13
+ <slot></slot>
14
+ </li>
15
+ {#if $linebreak}
16
+ <li aria-hidden="true" class="cardlayout-columnbreak" style:order={$order}></li>
17
+ {/if}
18
+
19
+ <style>
20
+ .cardlayout-columnbreak {
21
+ width: 0;
22
+ height: 100%;
23
+ visibility: hidden;
24
+ }
25
+ </style>
@@ -0,0 +1,188 @@
1
+ <script lang="ts">
2
+ import { onDestroy, tick, setContext, onMount } from 'svelte'
3
+ import { writable } from 'svelte/store'
4
+ import { browser } from '$app/env'
5
+ import { ElementSize, resize } from '$lib/actions'
6
+ import { CARDLAYOUT } from '$lib/types'
7
+ import type { Block } from '$lib/types'
8
+ export let maxwidth = 500
9
+ export let preserveorder = false
10
+ export let gutter = 10
11
+ export let className = ''
12
+ const blocks = []
13
+ const gutterstore = writable(gutter)
14
+ let defaultOrder = 0
15
+ setContext(CARDLAYOUT, {
16
+ registerBlock: (block: Block) => {
17
+ blocks.push(block)
18
+ hardrecalc()
19
+ onDestroy(() => {
20
+ blocks.splice(blocks.indexOf(block), 1)
21
+ hardrecalc()
22
+ })
23
+ block.order = writable(defaultOrder++)
24
+ block.linebreak = writable(false)
25
+ const cols = savecolumns || Math.ceil(1024 / maxwidth)
26
+ block.width = writable(`calc(${100.0 / cols}% - ${gutter * (cols - 1) / cols}px)`)
27
+ return block
28
+ },
29
+ recalculate: () => {
30
+ hardrecalc()
31
+ },
32
+ gutter: gutterstore
33
+ })
34
+ $: gutterstore.set(gutter)
35
+ let layoutelement: HTMLElement
36
+ let cycle1 = 0
37
+ let cycle2 = 0
38
+ function detectcycle (w: number) {
39
+ if (cycle1 === 0 && Math.abs(w - cycle2) > 5) {
40
+ cycle1 = w
41
+ } else if (cycle2 === 0 && Math.abs(w - cycle1) > 5) {
42
+ cycle2 = w
43
+ } else if (w === cycle1 || w === cycle2) {
44
+ return true
45
+ } else {
46
+ cycle1 = 0
47
+ cycle2 = 0
48
+ }
49
+ return false
50
+ }
51
+ let savecolumns = 0
52
+ let optimal: Block[][]
53
+ let fullheight = 0
54
+ let hardrequired = false
55
+ async function recalculate (realw: number) {
56
+ const columns = Math.ceil(realw / maxwidth)
57
+ const guttereach = gutter * (columns - 1) / columns
58
+ const cycling = detectcycle(realw)
59
+ if (columns !== savecolumns) {
60
+ for (const block of blocks) block.width.set(`calc(${100.0 / columns}% - ${guttereach}px`)
61
+ await tick()
62
+ }
63
+ // collect all the card heights at this new column width
64
+ for (const block of blocks) block.height = block.element?.offsetHeight ?? 200
65
+ if (columns !== savecolumns || hardrequired) { // only do real work if number of columns has changed, recalculate triggers on resize
66
+ if (preserveorder) {
67
+ optimal = [blocks]
68
+ if (columns > 1) {
69
+ const totalheight = blocks.reduce((totalheight, block) => totalheight + block.height + gutter, 0)
70
+ const tallestblock = Math.max(...blocks.map(b => b.height))
71
+ const minheight = Math.max(totalheight / columns, tallestblock)
72
+ let shortestoverall = totalheight
73
+ // 2d packing problem is NP-hard so this is an O(n) heuristic
74
+ // optimal configuration would be if each column is `minheight` so start there
75
+ // then relax it in increments and see if the overall height shrinks
76
+ const increment = Math.max(1, Math.floor(Math.min(minheight, tallestblock) * 0.05))
77
+ for (let colmaxheight = minheight; colmaxheight < minheight + tallestblock + gutter; colmaxheight += increment) {
78
+ let colheight = 0
79
+ let colidx = 0
80
+ let tallestcol = 0
81
+ let tallestcolidx = 0
82
+ const arrangement = []
83
+ for (const block of blocks) {
84
+ if (colheight + block.height > colmaxheight && colidx < columns - 1) {
85
+ if (colheight > tallestcol) {
86
+ tallestcol = colheight
87
+ tallestcolidx = colidx
88
+ }
89
+ colidx++
90
+ colheight = 0
91
+ }
92
+ if (!arrangement[colidx]) arrangement[colidx] = []
93
+ arrangement[colidx].push(block)
94
+ colheight += block.height + gutter
95
+ }
96
+ if (colheight > tallestcol) {
97
+ tallestcol = colheight
98
+ tallestcolidx = colidx
99
+ }
100
+ if (tallestcol < shortestoverall) {
101
+ shortestoverall = tallestcol
102
+ optimal = arrangement
103
+ }
104
+ // if leftmost column is tallest, increasing the allowable height can never make the overall height shorter
105
+ if (tallestcolidx === 0) break
106
+ }
107
+ }
108
+ } else {
109
+ optimal = []
110
+ const heights: number[] = Array.apply(null, Array(columns)).map((_: any) => 0) // initializes heights to an array of zeroes
111
+ // begin sorting blocks into columns
112
+ for (const block of blocks) {
113
+ // find the column with the smallest current height
114
+ const colidx = heights.reduce((acc, curr, curridx) => curr < heights[acc] ? curridx : acc, 0)
115
+ // record the height we are adding to the chosen column
116
+ heights[colidx] += block.height + gutter
117
+ // move the current card to the chosen column
118
+ if (!optimal[colidx]) optimal[colidx] = []
119
+ optimal[colidx].push(block)
120
+ }
121
+ }
122
+ // we created the optimal arrangement, now move the block elements to
123
+ // reflect it
124
+ let order = 0
125
+ for (let i = 0; i < optimal.length; i++) {
126
+ for (let j = 0; j < optimal[i].length; j++) {
127
+ const block = optimal[i][j]
128
+ block.order.set(order++)
129
+ block.linebreak.set(j === optimal[i].length - 1 && i < optimal.length - 1)
130
+ }
131
+ }
132
+ savecolumns = columns
133
+ hardrequired = false
134
+ }
135
+ // re-adjust container height
136
+ const saveheight = fullheight
137
+ fullheight = 0
138
+ for (let i = 0; i < optimal.length; i++) {
139
+ let top = 0
140
+ for (const block of optimal[i]) {
141
+ top += block.height + gutter
142
+ }
143
+ fullheight = Math.max(fullheight, top)
144
+ }
145
+ if (cycling && saveheight < fullheight) fullheight = saveheight
146
+ }
147
+
148
+ let savewidth = 0
149
+ let timer = 0
150
+ function triggerrecalc (width: number) {
151
+ if (width === savewidth && !hardrequired) return
152
+ cancelAnimationFrame(timer)
153
+ timer = requestAnimationFrame(() => {
154
+ recalculate(width)
155
+ savewidth = width
156
+ })
157
+ }
158
+ function hardrecalc () {
159
+ if (!layoutelement) return
160
+ hardrequired = true
161
+ cycle1 = 0
162
+ cycle2 = 0
163
+ triggerrecalc(layoutelement.clientWidth)
164
+ }
165
+ function onResize (e: UIEvent & { currentTarget: any, detail: ElementSize }) {
166
+ triggerrecalc(e.detail.clientWidth)
167
+ }
168
+ onMount(() => triggerrecalc(layoutelement.clientWidth))
169
+ </script>
170
+
171
+ <ul class="cardlayout {className}" class:ssr={!browser} style:height={browser ? `${fullheight + gutter}px` : undefined} bind:this={layoutelement} use:resize on:resize={onResize}>
172
+ <slot></slot>
173
+ </ul>
174
+
175
+ <style>
176
+ .cardlayout {
177
+ list-style: none;
178
+ padding: 0;
179
+ margin: 0;
180
+ display: flex;
181
+ flex-wrap: wrap;
182
+ flex-direction: column;
183
+ align-content: space-between;
184
+ }
185
+ .cardlayout.ssr {
186
+ flex-direction: row;
187
+ }
188
+ </style>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
- import { Store, classes } from '../lib'
3
- import type { CollapsingTableColumn, PopupMenuItem } from '../types'
2
+ import { Store } from '@txstate-mws/svelte-store'
4
3
  import { derived } from 'svelte/store'
4
+ import type { CollapsingTableColumn, PopupMenuItem } from '$lib/types'
5
+ import { classes } from '$lib/util'
5
6
  import DefaultPopupMenu from './PopupMenu.svelte'
6
7
  import ScreenReaderOnly from './ScreenReaderOnly.svelte'
7
8
 
@@ -31,7 +32,7 @@
31
32
  columns[0].neverhide = true
32
33
  }
33
34
  const sortedcolumns = columns // stable sort
34
- .map((c, i) => ({ col: c, idx: i}))
35
+ .map((c, i) => ({ col: c, idx: i }))
35
36
  .sort((a, b) => ((a.col.neverhide ? 0 : 1) - (b.col.neverhide ? 0 : 1)) || a.idx - b.idx)
36
37
  .map(o => o.col)
37
38
  if (!selected) {
@@ -69,7 +70,7 @@
69
70
  state.set({ keepcolumns, hiddencolumns, dropdowncolumn })
70
71
  }
71
72
  $: react(width, selected)
72
- let headers: HTMLElement[] = []
73
+ const headers: HTMLElement[] = []
73
74
  $: menubuttonelement = headers[$state.keepcolumns.length - 1]
74
75
  </script>
75
76
 
@@ -113,7 +114,7 @@
113
114
  </table>
114
115
  </div>
115
116
  {#if $menuitems.length > 0}
116
- <svelte:component this={PopupMenu} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={$menuitems} buttonelement={menubuttonelement} on:change={e => selected = e.detail}></svelte:component>
117
+ <svelte:component this={PopupMenu} {menuContainerClass} {menuClass} {menuItemClass} {menuItemHilitedClass} items={$menuitems} buttonelement={menubuttonelement} on:change={e => { selected = e.detail }}></svelte:component>
117
118
  {/if}
118
119
 
119
120
  <style>
@@ -6,7 +6,7 @@
6
6
  export let returnfocusto: HTMLElement|null|undefined = undefined
7
7
  export let initialfocus: string|undefined = undefined
8
8
  export let hidefocus = true
9
- export let hidefocuslabel = "focus is above modal dialog, start tabbing"
9
+ export let hidefocuslabel = 'focus is above modal dialog, start tabbing'
10
10
  export let escapable = true
11
11
  let className = ''
12
12
  export { className as class }
@@ -33,13 +33,14 @@
33
33
  returnfocusto = document.querySelector(':focus') as HTMLElement
34
34
  }
35
35
  await sleep(1) // sapper resets focus on page load, so we'll wait a millisecond in case our modal is on screen at load
36
+ if (!lockelement) return // in case we already unmounted
36
37
  if (initialfocus) {
37
38
  const focusEl = lockelement.querySelector(initialfocus)
38
39
  if (focusEl instanceof HTMLElement) focusEl.focus()
39
40
  } else {
40
41
  setInitialFocus()
41
42
  }
42
- setTimeout(() => listenforescape = true, 0)
43
+ setTimeout(() => { listenforescape = true }, 0)
43
44
  })
44
45
  onDestroy(async () => {
45
46
  const wasactive = active
@@ -53,19 +54,19 @@
53
54
  if (prevFocusLock) prevFocusLock.unpause()
54
55
  })
55
56
  const setInitialFocus = () => {
56
- const firstfocus = tabbable(lockelement)[0]
57
+ const firstfocus = lockelement ? tabbable(lockelement)[0] : undefined
57
58
  if (firstfocus && firstfocus.focus) {
58
59
  firstfocus.focus()
59
60
  }
60
61
  }
61
62
  const setLastFocus = () => {
62
- const lastfocus = tabbable(lockelement).slice(-1)[0]
63
+ const lastfocus = lockelement ? tabbable(lockelement).slice(-1)[0] : undefined
63
64
  if (lastfocus && lastfocus.focus) {
64
65
  lastfocus.focus()
65
66
  }
66
67
  }
67
68
  const keydown = (e: KeyboardEvent) => {
68
- if (active && e.key === "Escape" && escapable) {
69
+ if (active && e.key === 'Escape' && escapable) {
69
70
  e.preventDefault()
70
71
  dispatch('escape')
71
72
  }
@@ -87,7 +88,7 @@
87
88
  <div class={className} role="alertdialog" aria-modal="true" on:click|stopPropagation on:keydown|stopPropagation={keydown} on:focusin={focusin}>
88
89
  <div bind:this={abovelockelement} tabindex="0"></div>
89
90
  <div bind:this={lockelement} on:keydown={keydown}>
90
- {#if hidefocus}<div class="hiddenfocus" use:buttonify on:blur={() =>{ hidefocus = false }} on:click={() => escapable && dispatch('escape')}><ScreenReaderOnly>{hidefocuslabel}{#if escapable}, click to escape or use escape key at any time{/if}</ScreenReaderOnly></div>{/if}
91
+ {#if hidefocus}<div class="hiddenfocus" use:buttonify on:blur={() => { hidefocus = false }} on:click={() => escapable && dispatch('escape')}><ScreenReaderOnly>{hidefocuslabel}{#if escapable}, click to escape or use escape key at any time{/if}</ScreenReaderOnly></div>{/if}
91
92
  <slot></slot>
92
93
  </div>
93
94
  <div tabindex="0"></div>
@@ -6,9 +6,10 @@
6
6
  export let initialfocus: string|undefined = undefined
7
7
  export let containerClass = ''
8
8
  export let opaque = false
9
- import FocusLock from './FocusLock.svelte'
9
+ export let usePortal: HTMLElement|undefined = undefined
10
10
  import { createEventDispatcher, onMount } from 'svelte'
11
- import { portal } from '../actions';
11
+ import FocusLock from './FocusLock.svelte'
12
+ import { portal } from '$lib/actions'
12
13
 
13
14
  const dispatch = createEventDispatcher()
14
15
  const endmodal = () => {
@@ -24,7 +25,7 @@
24
25
  })
25
26
  </script>
26
27
 
27
- <div use:portal class="modal-backdrop" class:opaque on:click|stopPropagation|preventDefault={() => escapable && endmodal()}>
28
+ <div use:portal={usePortal} class="modal-backdrop" class:opaque on:click|stopPropagation|preventDefault={() => escapable && endmodal()}>
28
29
  <FocusLock class="modal-container {containerClass}" {escapable} on:escape {hidefocus} {hidefocuslabel} {returnfocusto} {initialfocus}>
29
30
  <slot></slot>
30
31
  </FocusLock>
@@ -38,7 +39,7 @@
38
39
  width: 100vw;
39
40
  height: 100vh;
40
41
  background-color: var(--modal-bg, rgba(0, 0, 0, 0.7));
41
- z-index: 100;
42
+ z-index: 2147483000;
42
43
  display: flex;
43
44
  justify-content: center;
44
45
  align-items: center