@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.
- package/.eslintrc.cjs +37 -0
- package/README.md +33 -14
- package/docs/stores.md +1 -1
- package/package.json +24 -74
- package/playwright.config.js +112 -0
- package/src/app.html +14 -0
- package/src/global.d.ts +1 -0
- package/src/lib/actions/buttonify.ts +27 -0
- package/src/lib/actions/glue.ts +96 -0
- package/src/lib/actions/index.ts +7 -0
- package/src/lib/actions/offset.ts +43 -0
- package/src/lib/actions/portal.ts +13 -0
- package/src/lib/actions/resize.ts +72 -0
- package/src/lib/actions/sticky.ts +105 -0
- package/src/lib/actions/stickyfixed.ts +160 -0
- package/src/lib/components/Card.svelte +25 -0
- package/src/lib/components/CardLayout.svelte +188 -0
- package/{dist/ts → src/lib}/components/CollapsingTable.svelte +6 -5
- package/{dist/ts → src/lib}/components/ConditionalWrapper.svelte +0 -0
- package/{dist/ts → src/lib}/components/FocusLock.svelte +7 -6
- package/{dist/ts → src/lib}/components/Modal.svelte +5 -4
- package/src/lib/components/MultiSelect.svelte +174 -0
- package/{dist/ts → src/lib}/components/PopupMenu.svelte +22 -16
- package/{dist/ts → src/lib}/components/ScreenReaderOnly.svelte +2 -2
- package/src/lib/components/index.ts +9 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/types/CardLayout.ts +14 -0
- package/src/lib/types/CollapsingTable.ts +8 -0
- package/src/lib/types/PopupMenu.ts +5 -0
- package/src/lib/types/index.ts +3 -0
- package/src/lib/util/bodyoffset.ts +77 -0
- package/src/lib/util/index.ts +3 -0
- package/src/lib/util/repositioned.ts +91 -0
- package/src/lib/util/util.ts +36 -0
- package/src/routes/__layout.svelte +125 -0
- package/src/routes/actions/index.svelte +1 -0
- package/src/routes/actions/offset.svelte +4 -0
- package/src/routes/actions/offsetdebounce.svelte +4 -0
- package/src/routes/actions/resize.svelte +4 -0
- package/src/routes/actions/resizedebounce.svelte +4 -0
- package/src/routes/actions/sticky.svelte +4 -0
- package/src/routes/cards/index.svelte +4 -0
- package/src/routes/cards/preserve.svelte +4 -0
- package/src/routes/collapsingtable/index.svelte +4 -0
- package/src/routes/collapsingtable/stickyheader.svelte +4 -0
- package/src/routes/index.svelte +2 -0
- package/src/routes/modal.svelte +4 -0
- package/src/routes/multiselect.svelte +4 -0
- package/src/routes/popupmenu/body.svelte +4 -0
- package/src/routes/popupmenu/complex.svelte +4 -0
- package/src/routes/popupmenu/empty.svelte +5 -0
- package/src/routes/popupmenu/index.svelte +4 -0
- package/src/routes/popupmenu/parent.svelte +4 -0
- package/src/routes/popupmenu/scrollable.svelte +27 -0
- package/src/routes/popupmenu/showselected.svelte +4 -0
- package/src/test/Cards.svelte +64 -0
- package/{dist/ts/tests/CollapsingTable → src/test}/CollapsingTableTest.svelte +2 -2
- package/{dist/ts/tests/Modal → src/test}/ModalTest.svelte +3 -3
- package/src/test/MultiSelectTest.svelte +26 -0
- package/{dist/ts/tests/Actions → src/test}/Offset.svelte +2 -4
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuBody.svelte +1 -1
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuButtonIsParent.svelte +2 -2
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuComplex.svelte +2 -2
- package/{dist/ts/tests/PopupMenu → src/test}/PopupMenuSimple.svelte +2 -2
- package/{dist/ts/tests/Actions → src/test}/Resize.svelte +2 -4
- package/{dist/ts/tests/Actions → src/test}/Sticky.svelte +1 -1
- package/static/layout.css +263 -0
- package/svelte.config.js +15 -0
- package/tests/.eslintrc.cjs +19 -0
- package/tests/CardLayout.test.js +41 -0
- package/tests/CollapsingTable.test.js +37 -0
- package/tests/Modal.test.js +18 -0
- package/tests/MultiSelect.test.js +58 -0
- package/tests/PopupMenu.test.js +55 -0
- package/tests/common.js +4 -0
- package/tsconfig.json +31 -0
- package/LICENSE +0 -21
- package/dist/actions/buttonify.js +0 -31
- package/dist/actions/buttonify.js.map +0 -1
- package/dist/actions/glue.js +0 -87
- package/dist/actions/glue.js.map +0 -1
- package/dist/actions/index.js +0 -20
- package/dist/actions/index.js.map +0 -1
- package/dist/actions/offset.js +0 -35
- package/dist/actions/offset.js.map +0 -1
- package/dist/actions/portal.js +0 -19
- package/dist/actions/portal.js.map +0 -1
- package/dist/actions/resize.js +0 -48
- package/dist/actions/resize.js.map +0 -1
- package/dist/actions/sticky.js +0 -93
- package/dist/actions/sticky.js.map +0 -1
- package/dist/actions/stickyfixed.js +0 -166
- package/dist/actions/stickyfixed.js.map +0 -1
- package/dist/components/CollapsingTable.svelte +0 -147
- package/dist/components/ConditionalWrapper.svelte +0 -38
- package/dist/components/FocusLock.svelte +0 -105
- package/dist/components/Modal.svelte +0 -52
- package/dist/components/PopupMenu.svelte +0 -282
- package/dist/components/ScreenReaderOnly.svelte +0 -25
- package/dist/components/index.js +0 -19
- package/dist/components/index.js.map +0 -1
- package/dist/index.js +0 -17
- package/dist/index.js.map +0 -1
- package/dist/lib/activestore.js +0 -123
- package/dist/lib/activestore.js.map +0 -1
- package/dist/lib/bodyoffset.js +0 -76
- package/dist/lib/bodyoffset.js.map +0 -1
- package/dist/lib/convertedstore.js +0 -41
- package/dist/lib/convertedstore.js.map +0 -1
- package/dist/lib/derivedstore.js +0 -65
- package/dist/lib/derivedstore.js.map +0 -1
- package/dist/lib/index.js +0 -22
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/repositioned.js +0 -92
- package/dist/lib/repositioned.js.map +0 -1
- package/dist/lib/safestore.js +0 -16
- package/dist/lib/safestore.js.map +0 -1
- package/dist/lib/store.js +0 -27
- package/dist/lib/store.js.map +0 -1
- package/dist/lib/substore.js +0 -47
- package/dist/lib/substore.js.map +0 -1
- package/dist/lib/util.js +0 -16
- package/dist/lib/util.js.map +0 -1
- package/dist/ts/actions/buttonify.d.ts +0 -8
- package/dist/ts/actions/glue.d.ts +0 -16
- package/dist/ts/actions/index.d.ts +0 -7
- package/dist/ts/actions/offset.d.ts +0 -15
- package/dist/ts/actions/portal.d.ts +0 -4
- package/dist/ts/actions/resize.d.ts +0 -16
- package/dist/ts/actions/sticky.d.ts +0 -33
- package/dist/ts/actions/stickyfixed.d.ts +0 -25
- package/dist/ts/components/index.d.ts +0 -6
- package/dist/ts/index.d.ts +0 -4
- package/dist/ts/lib/activestore.d.ts +0 -60
- package/dist/ts/lib/bodyoffset.d.ts +0 -13
- package/dist/ts/lib/convertedstore.d.ts +0 -16
- package/dist/ts/lib/derivedstore.d.ts +0 -23
- package/dist/ts/lib/index.d.ts +0 -9
- package/dist/ts/lib/repositioned.d.ts +0 -13
- package/dist/ts/lib/safestore.d.ts +0 -4
- package/dist/ts/lib/store.d.ts +0 -16
- package/dist/ts/lib/substore.d.ts +0 -24
- package/dist/ts/lib/util.d.ts +0 -2
- package/dist/ts/tests/Actions/Actions.stories.d.ts +0 -1
- package/dist/ts/tests/CollapsingTable/CollapsingTable.stories.d.ts +0 -1
- package/dist/ts/tests/CollapsingTable/CollapsingTable.test.d.ts +0 -1
- package/dist/ts/tests/Modal/Modal.stories.d.ts +0 -1
- package/dist/ts/tests/Modal/Modal.test.d.ts +0 -1
- package/dist/ts/tests/PopupMenu/PopupMenu.stories.d.ts +0 -1
- package/dist/ts/tests/PopupMenu/PopupMenu.test.d.ts +0 -1
- package/dist/ts/tests/stores/deepstore.test.d.ts +0 -1
- package/dist/ts/types/CollapsingTable.d.ts +0 -8
- package/dist/ts/types/PopupMenu.d.ts +0 -5
- package/dist/ts/types/index.d.ts +0 -2
- package/dist/types/CollapsingTable.js +0 -3
- package/dist/types/CollapsingTable.js.map +0 -1
- package/dist/types/PopupMenu.js +0 -3
- package/dist/types/PopupMenu.js.map +0 -1
- package/dist/types/index.js +0 -15
- package/dist/types/index.js.map +0 -1
- package/esm/index.js +0 -1
- 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
|
|
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
|
-
|
|
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>
|
|
File without changes
|
|
@@ -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 =
|
|
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 ===
|
|
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
|
-
|
|
9
|
+
export let usePortal: HTMLElement|undefined = undefined
|
|
10
10
|
import { createEventDispatcher, onMount } from 'svelte'
|
|
11
|
-
import
|
|
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:
|
|
42
|
+
z-index: 2147483000;
|
|
42
43
|
display: flex;
|
|
43
44
|
justify-content: center;
|
|
44
45
|
align-items: center
|