@lavalogic/scoria 0.40.0 → 0.40.2
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/README.md +58 -58
- package/dist/Components/AccordionGroup.svelte +46 -46
- package/dist/Components/AccordionGroupButton.svelte +76 -76
- package/dist/Components/Action.svelte +136 -136
- package/dist/Components/AlertModal.svelte +195 -195
- package/dist/Components/AppointmentCalendar/AppointmentCalendar.svelte +729 -729
- package/dist/Components/Base/BaseModal.svelte +125 -125
- package/dist/Components/Base/ContextWrapper.svelte +72 -72
- package/dist/Components/Base/Pagination.svelte +198 -198
- package/dist/Components/Base/Snippets.svelte +36 -36
- package/dist/Components/BigRadioSet.svelte +38 -38
- package/dist/Components/Bubble.svelte +101 -101
- package/dist/Components/Button.svelte +209 -209
- package/dist/Components/Checkbox.svelte +86 -86
- package/dist/Components/CollapsibleCard.svelte +78 -78
- package/dist/Components/Contexts/ContextMenu.svelte +164 -164
- package/dist/Components/Contexts/ContextMenuDivider.svelte +15 -15
- package/dist/Components/Contexts/ContextMenuOption.svelte +60 -60
- package/dist/Components/Contexts/Toast.svelte +86 -86
- package/dist/Components/Contexts/ToastContainer.svelte +65 -65
- package/dist/Components/Contexts/Tooltip.svelte +37 -37
- package/dist/Components/DataMatrixIcon.svelte +112 -112
- package/dist/Components/DateInput.svelte +148 -148
- package/dist/Components/DatePicker.svelte +300 -300
- package/dist/Components/DesktopModal.svelte +90 -90
- package/dist/Components/Dial.svelte +204 -204
- package/dist/Components/DimensionInput.svelte +170 -170
- package/dist/Components/DragMenu/DragMenuHolder.svelte +119 -119
- package/dist/Components/DragMenu/DraggableCard.svelte +77 -77
- package/dist/Components/FileCard.svelte +24 -24
- package/dist/Components/FileUpload.svelte +236 -236
- package/dist/Components/GridInput.svelte +196 -196
- package/dist/Components/GridInputSet.svelte +29 -29
- package/dist/Components/HorizontalTabGroup.svelte +82 -82
- package/dist/Components/HorizontalTabGroupButton.svelte +44 -44
- package/dist/Components/Icon.svelte +128 -128
- package/dist/Components/Icons.js +357 -357
- package/dist/Components/InfoAlert.svelte +35 -35
- package/dist/Components/LoadingAnimation.svelte +29 -29
- package/dist/Components/LoadingModal.svelte +45 -45
- package/dist/Components/LoadingOverlay.svelte +25 -25
- package/dist/Components/Modal.svelte +258 -258
- package/dist/Components/MultiSelect.svelte +220 -220
- package/dist/Components/Nav/Nav.svelte +110 -110
- package/dist/Components/Nav/NavServiceMenuList.svelte +43 -43
- package/dist/Components/Nav/NavTab.svelte +62 -62
- package/dist/Components/Nav/ServiceMenuItem.svelte +112 -112
- package/dist/Components/Nav/ServicesNav.svelte +80 -80
- package/dist/Components/NumberInput.svelte +157 -157
- package/dist/Components/PageHeading.svelte +25 -25
- package/dist/Components/PasswordInput.svelte +88 -88
- package/dist/Components/Portal.svelte +36 -36
- package/dist/Components/ProgressBubble.svelte +107 -107
- package/dist/Components/QuerySelect.svelte +366 -366
- package/dist/Components/SingleSelect.svelte +250 -250
- package/dist/Components/StepButton.svelte +70 -70
- package/dist/Components/StepForm.svelte +140 -140
- package/dist/Components/Switch.svelte +69 -69
- package/dist/Components/Table/ARCHITECTURE.md +298 -298
- package/dist/Components/Table/Body/QuickSearchCell.svelte +679 -679
- package/dist/Components/Table/Body/Rows/ColumnHighlight.svelte +51 -51
- package/dist/Components/Table/Body/Rows/ColumnList.svelte +59 -59
- package/dist/Components/Table/Body/Rows/ColumnListRow.svelte +454 -454
- package/dist/Components/Table/Body/Rows/Columns/AccessorComponent.svelte +79 -79
- package/dist/Components/Table/Body/Rows/Columns/ActionsCell.svelte +185 -185
- package/dist/Components/Table/Body/Rows/Columns/BubbleCell.svelte +124 -124
- package/dist/Components/Table/Body/Rows/Columns/ExpandCell.svelte +131 -120
- package/dist/Components/Table/Body/Rows/Columns/FocusableImmutableCell.svelte +234 -234
- package/dist/Components/Table/Body/Rows/Columns/ProgressCell.svelte +105 -105
- package/dist/Components/Table/Body/Rows/Columns/TableAction.svelte +87 -87
- package/dist/Components/Table/Body/Rows/Columns/TableCheckbox.svelte +126 -126
- package/dist/Components/Table/Body/Rows/Columns/TableColumn.svelte +159 -159
- package/dist/Components/Table/Body/Rows/Columns/TableDatePicker.svelte +246 -246
- package/dist/Components/Table/Body/Rows/Columns/TableNumberInput.svelte +216 -216
- package/dist/Components/Table/Body/Rows/Columns/TableQuerySelect.svelte +252 -252
- package/dist/Components/Table/Body/Rows/Columns/TableRowSelectionCheckbox.svelte +179 -179
- package/dist/Components/Table/Body/Rows/Columns/TableSelect.svelte +221 -221
- package/dist/Components/Table/Body/Rows/Columns/TableTextInput.svelte +197 -197
- package/dist/Components/Table/Body/Rows/Columns/ValidityCell.svelte +190 -190
- package/dist/Components/Table/Body/Rows/TableRow.svelte +290 -290
- package/dist/Components/Table/Body/TableBody.svelte +155 -155
- package/dist/Components/Table/ColumnFactory.svelte.js +1 -0
- package/dist/Components/Table/Headers/HighlightAllHeader.svelte +41 -41
- package/dist/Components/Table/Headers/SelectAllHeader.svelte +30 -30
- package/dist/Components/Table/Headers/TableHead.svelte +415 -415
- package/dist/Components/Table/Misc/ColumnPanel.svelte +39 -39
- package/dist/Components/Table/Misc/ColumnPanelModal.svelte +39 -39
- package/dist/Components/Table/Misc/FilterInput.svelte +641 -641
- package/dist/Components/Table/Misc/FilterPanel.svelte +190 -190
- package/dist/Components/Table/Misc/FilterPanelModal.svelte +31 -31
- package/dist/Components/Table/Misc/ShareViewModal.svelte +291 -291
- package/dist/Components/Table/Misc/SidePanel.svelte +35 -35
- package/dist/Components/Table/Misc/TableConfigurationModal.svelte +644 -644
- package/dist/Components/Table/Misc/TableFooter.svelte +340 -340
- package/dist/Components/Table/Misc/TableHorizontalBar.svelte +140 -140
- package/dist/Components/Table/Misc/TableSidebar.svelte +200 -200
- package/dist/Components/Table/Misc/TableViewDropdown.svelte +105 -105
- package/dist/Components/Table/Misc/ToolboxPanel.svelte +100 -100
- package/dist/Components/Table/Misc/ToolboxPanelModal.svelte +107 -107
- package/dist/Components/Table/Misc/UpdateViewOptionsModal.svelte +192 -192
- package/dist/Components/Table/NestedTable.svelte +130 -130
- package/dist/Components/Table/Table.svelte +692 -692
- package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.d.ts +5 -0
- package/dist/Components/Table/Types/Columns/Definitions/Expand/ExpandDef.svelte.js +3 -0
- package/dist/Components/Table/Types/Public/ColumnOptions.d.ts +10 -0
- package/dist/Components/Test/TestContextConsumer.svelte +13 -13
- package/dist/Components/TextArea.svelte +130 -130
- package/dist/Components/TextInput.svelte +176 -176
- package/dist/Components/ThreeStateRadio.svelte +131 -131
- package/dist/Components/Touch/BoolCard.svelte +45 -45
- package/dist/Components/Touch/DateModal.svelte +209 -209
- package/dist/Components/Touch/EditCard.svelte +59 -59
- package/dist/Components/Touch/InfoCard.svelte +31 -31
- package/dist/Components/Touch/InfoTextCard.svelte +28 -28
- package/dist/Components/Touch/ModalExplanation.svelte +17 -17
- package/dist/Components/Touch/NumberKeyboard.svelte +225 -225
- package/dist/Components/Touch/NumberModal.svelte +79 -79
- package/dist/Components/Touch/PlusMinusCard.svelte +46 -46
- package/dist/Components/Touch/SelectModal.svelte +127 -127
- package/dist/Components/Touch/SummaryCard.svelte +93 -93
- package/dist/Components/Touch/TextAreaModal.svelte +95 -95
- package/dist/Components/Touch/TextModal.svelte +106 -106
- package/dist/Components/Touch/TouchCard.svelte +33 -33
- package/dist/Components/Touch/TouchModal.svelte +274 -274
- package/dist/Components/Touch/UtilityButton.svelte +109 -109
- package/dist/Components/VerticalTabGroup.svelte +79 -79
- package/dist/Components/VerticalTabGroupButton.svelte +58 -58
- package/dist/Helpers/Datamatrix.d.ts +19 -19
- package/dist/Helpers/Datamatrix.js +291 -291
- package/dist/Types/Examples/ExampleModal.svelte +35 -35
- package/dist/scss/_basics.scss +12 -12
- package/dist/scss/_colours.scss +134 -134
- package/dist/scss/_mixins.scss +3392 -3392
- package/dist/scss/_motion.scss +57 -57
- package/dist/scss/_sizing.scss +85 -85
- package/dist/scss/_transitions.scss +8 -8
- package/package.json +1 -1
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script lang="ts">
|
|
4
|
-
import { ColourSet } from '../scss/colours.js';
|
|
5
|
-
import Button from './Button.svelte';
|
|
6
|
-
import type { DesktopModalProps } from './DesktopModalProps.js';
|
|
7
|
-
import LoadingOverlay from './LoadingOverlay.svelte';
|
|
8
|
-
import Modal from './Modal.svelte';
|
|
9
|
-
|
|
10
|
-
const {
|
|
11
|
-
onclose,
|
|
12
|
-
onsubmit,
|
|
13
|
-
noscroll,
|
|
14
|
-
headerLabel,
|
|
15
|
-
headerSubtitle,
|
|
16
|
-
buttons,
|
|
17
|
-
children,
|
|
18
|
-
isLoading,
|
|
19
|
-
mandatory,
|
|
20
|
-
loadingMessage,
|
|
21
|
-
staticHeight,
|
|
22
|
-
staticWidth,
|
|
23
|
-
minWidth,
|
|
24
|
-
minHeight,
|
|
25
|
-
width,
|
|
26
|
-
height,
|
|
27
|
-
}: DesktopModalProps = $props();
|
|
28
|
-
</script>
|
|
29
|
-
|
|
30
|
-
<Modal
|
|
31
|
-
{onclose}
|
|
32
|
-
mandatory={mandatory || isLoading}
|
|
33
|
-
{staticHeight}
|
|
34
|
-
{staticWidth}
|
|
35
|
-
{minWidth}
|
|
36
|
-
{minHeight}
|
|
37
|
-
{width}
|
|
38
|
-
{height}
|
|
39
|
-
{noscroll}
|
|
40
|
-
{onsubmit}
|
|
41
|
-
>
|
|
42
|
-
{#snippet header()}
|
|
43
|
-
<div class="outer">
|
|
44
|
-
<div class="column">
|
|
45
|
-
{#if typeof headerLabel === 'string'}
|
|
46
|
-
<h1>{headerLabel}</h1>
|
|
47
|
-
{:else}
|
|
48
|
-
{@render headerLabel()}
|
|
49
|
-
{/if}
|
|
50
|
-
{#if headerSubtitle}
|
|
51
|
-
<div class="one-line">
|
|
52
|
-
<span>{headerSubtitle}</span>
|
|
53
|
-
</div>
|
|
54
|
-
{/if}
|
|
55
|
-
</div>
|
|
56
|
-
<div class="spacer"></div>
|
|
57
|
-
</div>
|
|
58
|
-
{/snippet}
|
|
59
|
-
|
|
60
|
-
{#snippet footer()}
|
|
61
|
-
<div class="footer">
|
|
62
|
-
{#if buttons.length}
|
|
63
|
-
{#each buttons as button (button.label)}
|
|
64
|
-
<div class:left={button.left}>
|
|
65
|
-
<Button
|
|
66
|
-
type={button.type}
|
|
67
|
-
disabled={button.disabled}
|
|
68
|
-
onclick={button.callback}
|
|
69
|
-
colourSet={button.colourSet ??
|
|
70
|
-
(button.type === 'submit' ? ColourSet.Primary : undefined)}>{button.label}</Button
|
|
71
|
-
>
|
|
72
|
-
</div>
|
|
73
|
-
{/each}
|
|
74
|
-
{/if}
|
|
75
|
-
</div>
|
|
76
|
-
{/snippet}
|
|
77
|
-
|
|
78
|
-
<div class="body-outer">
|
|
79
|
-
{@render children()}
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{#if isLoading}
|
|
83
|
-
<LoadingOverlay
|
|
84
|
-
absolute
|
|
85
|
-
message={loadingMessage}
|
|
86
|
-
/>
|
|
87
|
-
{/if}
|
|
88
|
-
</Modal>
|
|
89
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { ColourSet } from '../scss/colours.js';
|
|
5
|
+
import Button from './Button.svelte';
|
|
6
|
+
import type { DesktopModalProps } from './DesktopModalProps.js';
|
|
7
|
+
import LoadingOverlay from './LoadingOverlay.svelte';
|
|
8
|
+
import Modal from './Modal.svelte';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
onclose,
|
|
12
|
+
onsubmit,
|
|
13
|
+
noscroll,
|
|
14
|
+
headerLabel,
|
|
15
|
+
headerSubtitle,
|
|
16
|
+
buttons,
|
|
17
|
+
children,
|
|
18
|
+
isLoading,
|
|
19
|
+
mandatory,
|
|
20
|
+
loadingMessage,
|
|
21
|
+
staticHeight,
|
|
22
|
+
staticWidth,
|
|
23
|
+
minWidth,
|
|
24
|
+
minHeight,
|
|
25
|
+
width,
|
|
26
|
+
height,
|
|
27
|
+
}: DesktopModalProps = $props();
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<Modal
|
|
31
|
+
{onclose}
|
|
32
|
+
mandatory={mandatory || isLoading}
|
|
33
|
+
{staticHeight}
|
|
34
|
+
{staticWidth}
|
|
35
|
+
{minWidth}
|
|
36
|
+
{minHeight}
|
|
37
|
+
{width}
|
|
38
|
+
{height}
|
|
39
|
+
{noscroll}
|
|
40
|
+
{onsubmit}
|
|
41
|
+
>
|
|
42
|
+
{#snippet header()}
|
|
43
|
+
<div class="outer">
|
|
44
|
+
<div class="column">
|
|
45
|
+
{#if typeof headerLabel === 'string'}
|
|
46
|
+
<h1>{headerLabel}</h1>
|
|
47
|
+
{:else}
|
|
48
|
+
{@render headerLabel()}
|
|
49
|
+
{/if}
|
|
50
|
+
{#if headerSubtitle}
|
|
51
|
+
<div class="one-line">
|
|
52
|
+
<span>{headerSubtitle}</span>
|
|
53
|
+
</div>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
<div class="spacer"></div>
|
|
57
|
+
</div>
|
|
58
|
+
{/snippet}
|
|
59
|
+
|
|
60
|
+
{#snippet footer()}
|
|
61
|
+
<div class="footer">
|
|
62
|
+
{#if buttons.length}
|
|
63
|
+
{#each buttons as button (button.label)}
|
|
64
|
+
<div class:left={button.left}>
|
|
65
|
+
<Button
|
|
66
|
+
type={button.type}
|
|
67
|
+
disabled={button.disabled}
|
|
68
|
+
onclick={button.callback}
|
|
69
|
+
colourSet={button.colourSet ??
|
|
70
|
+
(button.type === 'submit' ? ColourSet.Primary : undefined)}>{button.label}</Button
|
|
71
|
+
>
|
|
72
|
+
</div>
|
|
73
|
+
{/each}
|
|
74
|
+
{/if}
|
|
75
|
+
</div>
|
|
76
|
+
{/snippet}
|
|
77
|
+
|
|
78
|
+
<div class="body-outer">
|
|
79
|
+
{@render children()}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{#if isLoading}
|
|
83
|
+
<LoadingOverlay
|
|
84
|
+
absolute
|
|
85
|
+
message={loadingMessage}
|
|
86
|
+
/>
|
|
87
|
+
{/if}
|
|
88
|
+
</Modal>
|
|
89
|
+
|
|
90
90
|
<style>.outer {
|
|
91
91
|
padding: 1.5rem;
|
|
92
92
|
display: flex;
|
|
@@ -184,4 +184,4 @@
|
|
|
184
184
|
}
|
|
185
185
|
.outer .spacer {
|
|
186
186
|
flex-grow: 1;
|
|
187
|
-
}</style>
|
|
187
|
+
}</style>
|
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
<svelte:options runes />
|
|
2
|
-
|
|
3
|
-
<script
|
|
4
|
-
lang="ts"
|
|
5
|
-
module
|
|
6
|
-
>
|
|
7
|
-
import { Sizing } from '../scss/sizing.js';
|
|
8
|
-
|
|
9
|
-
// Pixel height of each dial option. Sourced from `sizing.ts` so the
|
|
10
|
-
// SCSS partial and TS constant stay in sync (see comment at the top
|
|
11
|
-
// of `sizing.ts`).
|
|
12
|
-
const OPTION_HEIGHT: number = Sizing['$dial-option-height'];
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<script
|
|
16
|
-
lang="ts"
|
|
17
|
-
generics="T"
|
|
18
|
-
>
|
|
19
|
-
import { onMount } from 'svelte';
|
|
20
|
-
import type { DialProps } from './DialProps.js';
|
|
21
|
-
|
|
22
|
-
let {
|
|
23
|
-
selected,
|
|
24
|
-
entries,
|
|
25
|
-
type,
|
|
26
|
-
ref = $bindable(),
|
|
27
|
-
onsubmit,
|
|
28
|
-
onPrevious,
|
|
29
|
-
onNext,
|
|
30
|
-
onchange,
|
|
31
|
-
}: DialProps<T> = $props();
|
|
32
|
-
|
|
33
|
-
// `position`, `offset`, `dragging` must all be `$state` so that the
|
|
34
|
-
// position-syncing `$effect` below re-runs when `dragging` flips back
|
|
35
|
-
// to `false` at the end of a drag. The previous `let` variables were
|
|
36
|
-
// invisible to Svelte's reactivity and made the `$effect`'s firing
|
|
37
|
-
// order depend on `selected` happening to change.
|
|
38
|
-
//
|
|
39
|
-
// `position` initialises to 0; the `$effect` below syncs it to the
|
|
40
|
-
// `selected`-derived position on first run. Reading `selected` /
|
|
41
|
-
// `values` directly in the initialiser would only capture their
|
|
42
|
-
// initial values (svelte warning `state_referenced_locally`), and
|
|
43
|
-
// since the effect already covers that path we keep the initialiser
|
|
44
|
-
// trivial.
|
|
45
|
-
let position = $state(0);
|
|
46
|
-
let offset = $state(0);
|
|
47
|
-
let dragging = $state(false);
|
|
48
|
-
|
|
49
|
-
let itemWrapper: HTMLUListElement | undefined = $state();
|
|
50
|
-
let previousY = 0;
|
|
51
|
-
|
|
52
|
-
// AbortController used to register/clean up window-level
|
|
53
|
-
// `mousemove`/`mouseup`/`touchmove`/`touchend` listeners. Stored in a
|
|
54
|
-
// single ref so a stray `mousedown` cannot leak a second set, and so
|
|
55
|
-
// the cleanup `$effect` below can detach everything on unmount even
|
|
56
|
-
// if the user navigates away mid-drag.
|
|
57
|
-
let dragAbort: AbortController | undefined;
|
|
58
|
-
|
|
59
|
-
onMount(() => {
|
|
60
|
-
setPosition();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
$effect(() => {
|
|
64
|
-
if (!dragging) {
|
|
65
|
-
const selectedPosition =
|
|
66
|
-
selected != null ? -entries.findIndex((e) => e.value === selected) * OPTION_HEIGHT : 0;
|
|
67
|
-
if (position !== selectedPosition) {
|
|
68
|
-
position = selectedPosition;
|
|
69
|
-
setPosition();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Ensure any in-flight drag listeners are detached on unmount. The
|
|
75
|
-
// listeners would otherwise leak if the component unmounted between
|
|
76
|
-
// `onmousedown` and `onMouseUp` (rare, but possible if the parent
|
|
77
|
-
// modal closes mid-drag).
|
|
78
|
-
$effect(() => () => {
|
|
79
|
-
dragAbort?.abort();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
function onChange(changedData: T) {
|
|
83
|
-
onchange({
|
|
84
|
-
type,
|
|
85
|
-
changedData,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function setPosition() {
|
|
90
|
-
if (!itemWrapper) {
|
|
91
|
-
// Silently no-op if the wrapper has not yet mounted. The previous
|
|
92
|
-
// `console.warn` fired during the initial render of every dial
|
|
93
|
-
// and was noise in production logs.
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Set transform / transition individually rather than rewriting the
|
|
98
|
-
// full `cssText`. The latter parses the entire declaration string
|
|
99
|
-
// per mousemove (~60 Hz); `setProperty` skips the parse and only
|
|
100
|
-
// updates the named declarations. The `transition-duration` scales
|
|
101
|
-
// with drag inertia, so we still need imperative updates rather
|
|
102
|
-
// than `style:transform={...}` for the dragging case.
|
|
103
|
-
const transitionDuration = `${Math.abs(offset) / 100 + 0.1}s`;
|
|
104
|
-
itemWrapper.style.setProperty('transition', `transform ${transitionDuration}`);
|
|
105
|
-
itemWrapper.style.setProperty('transform', `translateY(${position}px)`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const onmousedown = (event: MouseEvent | TouchEvent) => {
|
|
109
|
-
previousY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
110
|
-
dragging = true;
|
|
111
|
-
|
|
112
|
-
// One AbortController per drag. Attached to all four window
|
|
113
|
-
// listeners so a single `controller.abort()` detaches everything.
|
|
114
|
-
dragAbort?.abort();
|
|
115
|
-
dragAbort = new AbortController();
|
|
116
|
-
const { signal } = dragAbort;
|
|
117
|
-
|
|
118
|
-
window.addEventListener('mousemove', onMouseMove, { signal });
|
|
119
|
-
window.addEventListener('mouseup', onMouseUp, { signal });
|
|
120
|
-
window.addEventListener('touchmove', onMouseMove, { signal });
|
|
121
|
-
window.addEventListener('touchend', onMouseUp, { signal });
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const onMouseMove = (event: MouseEvent | TouchEvent) => {
|
|
125
|
-
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
126
|
-
offset = clientY - previousY;
|
|
127
|
-
|
|
128
|
-
const maxPosition = -entries.length * OPTION_HEIGHT;
|
|
129
|
-
const _position = position + offset;
|
|
130
|
-
position = Math.max(maxPosition, Math.min(OPTION_HEIGHT, _position));
|
|
131
|
-
previousY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
132
|
-
setPosition();
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const onMouseUp = () => {
|
|
136
|
-
const maxPosition = -(entries.length - 1) * OPTION_HEIGHT;
|
|
137
|
-
const rounderPosition = Math.round((position + offset * 5) / OPTION_HEIGHT) * OPTION_HEIGHT;
|
|
138
|
-
const finalPosition = Math.max(maxPosition, Math.min(0, rounderPosition));
|
|
139
|
-
|
|
140
|
-
dragging = false;
|
|
141
|
-
position = finalPosition;
|
|
142
|
-
|
|
143
|
-
dragAbort?.abort();
|
|
144
|
-
dragAbort = undefined;
|
|
145
|
-
|
|
146
|
-
setPosition();
|
|
147
|
-
onChange(entries[-finalPosition / OPTION_HEIGHT].value);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const onwheel = (e: WheelEvent) => {
|
|
151
|
-
if (e.deltaY > 0) {
|
|
152
|
-
position = position - OPTION_HEIGHT;
|
|
153
|
-
}
|
|
154
|
-
if (e.deltaY < 0) {
|
|
155
|
-
position = position + OPTION_HEIGHT;
|
|
156
|
-
}
|
|
157
|
-
onMouseUp();
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const handledKeys = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter']);
|
|
161
|
-
function onkeydown(e: KeyboardEvent) {
|
|
162
|
-
if (handledKeys.has(e.key)) {
|
|
163
|
-
e.preventDefault();
|
|
164
|
-
|
|
165
|
-
if (e.key === 'ArrowLeft') {
|
|
166
|
-
onPrevious(type);
|
|
167
|
-
} else if (e.key === 'ArrowRight') {
|
|
168
|
-
onNext(type);
|
|
169
|
-
} else if (e.key === 'ArrowUp') {
|
|
170
|
-
position = position + OPTION_HEIGHT;
|
|
171
|
-
onMouseUp();
|
|
172
|
-
} else if (e.key === 'ArrowDown') {
|
|
173
|
-
position = position - OPTION_HEIGHT;
|
|
174
|
-
onMouseUp();
|
|
175
|
-
} else if (e.key === 'Enter') {
|
|
176
|
-
onsubmit();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
</script>
|
|
181
|
-
|
|
182
|
-
<div
|
|
183
|
-
class="select-wrapper"
|
|
184
|
-
role="listbox"
|
|
185
|
-
tabindex="0"
|
|
186
|
-
bind:this={ref}
|
|
187
|
-
{onmousedown}
|
|
188
|
-
ontouchstart={onmousedown}
|
|
189
|
-
{onwheel}
|
|
190
|
-
{onkeydown}
|
|
191
|
-
>
|
|
192
|
-
<div class="selected-wrapper">
|
|
193
|
-
<ul
|
|
194
|
-
bind:this={itemWrapper}
|
|
195
|
-
class="touch-date-container"
|
|
196
|
-
>
|
|
197
|
-
{#each entries as entry (entry.value)}
|
|
198
|
-
<li>{entry.label}</li>
|
|
199
|
-
{/each}
|
|
200
|
-
</ul>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
1
|
+
<svelte:options runes />
|
|
2
|
+
|
|
3
|
+
<script
|
|
4
|
+
lang="ts"
|
|
5
|
+
module
|
|
6
|
+
>
|
|
7
|
+
import { Sizing } from '../scss/sizing.js';
|
|
8
|
+
|
|
9
|
+
// Pixel height of each dial option. Sourced from `sizing.ts` so the
|
|
10
|
+
// SCSS partial and TS constant stay in sync (see comment at the top
|
|
11
|
+
// of `sizing.ts`).
|
|
12
|
+
const OPTION_HEIGHT: number = Sizing['$dial-option-height'];
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script
|
|
16
|
+
lang="ts"
|
|
17
|
+
generics="T"
|
|
18
|
+
>
|
|
19
|
+
import { onMount } from 'svelte';
|
|
20
|
+
import type { DialProps } from './DialProps.js';
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
selected,
|
|
24
|
+
entries,
|
|
25
|
+
type,
|
|
26
|
+
ref = $bindable(),
|
|
27
|
+
onsubmit,
|
|
28
|
+
onPrevious,
|
|
29
|
+
onNext,
|
|
30
|
+
onchange,
|
|
31
|
+
}: DialProps<T> = $props();
|
|
32
|
+
|
|
33
|
+
// `position`, `offset`, `dragging` must all be `$state` so that the
|
|
34
|
+
// position-syncing `$effect` below re-runs when `dragging` flips back
|
|
35
|
+
// to `false` at the end of a drag. The previous `let` variables were
|
|
36
|
+
// invisible to Svelte's reactivity and made the `$effect`'s firing
|
|
37
|
+
// order depend on `selected` happening to change.
|
|
38
|
+
//
|
|
39
|
+
// `position` initialises to 0; the `$effect` below syncs it to the
|
|
40
|
+
// `selected`-derived position on first run. Reading `selected` /
|
|
41
|
+
// `values` directly in the initialiser would only capture their
|
|
42
|
+
// initial values (svelte warning `state_referenced_locally`), and
|
|
43
|
+
// since the effect already covers that path we keep the initialiser
|
|
44
|
+
// trivial.
|
|
45
|
+
let position = $state(0);
|
|
46
|
+
let offset = $state(0);
|
|
47
|
+
let dragging = $state(false);
|
|
48
|
+
|
|
49
|
+
let itemWrapper: HTMLUListElement | undefined = $state();
|
|
50
|
+
let previousY = 0;
|
|
51
|
+
|
|
52
|
+
// AbortController used to register/clean up window-level
|
|
53
|
+
// `mousemove`/`mouseup`/`touchmove`/`touchend` listeners. Stored in a
|
|
54
|
+
// single ref so a stray `mousedown` cannot leak a second set, and so
|
|
55
|
+
// the cleanup `$effect` below can detach everything on unmount even
|
|
56
|
+
// if the user navigates away mid-drag.
|
|
57
|
+
let dragAbort: AbortController | undefined;
|
|
58
|
+
|
|
59
|
+
onMount(() => {
|
|
60
|
+
setPosition();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
$effect(() => {
|
|
64
|
+
if (!dragging) {
|
|
65
|
+
const selectedPosition =
|
|
66
|
+
selected != null ? -entries.findIndex((e) => e.value === selected) * OPTION_HEIGHT : 0;
|
|
67
|
+
if (position !== selectedPosition) {
|
|
68
|
+
position = selectedPosition;
|
|
69
|
+
setPosition();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Ensure any in-flight drag listeners are detached on unmount. The
|
|
75
|
+
// listeners would otherwise leak if the component unmounted between
|
|
76
|
+
// `onmousedown` and `onMouseUp` (rare, but possible if the parent
|
|
77
|
+
// modal closes mid-drag).
|
|
78
|
+
$effect(() => () => {
|
|
79
|
+
dragAbort?.abort();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function onChange(changedData: T) {
|
|
83
|
+
onchange({
|
|
84
|
+
type,
|
|
85
|
+
changedData,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setPosition() {
|
|
90
|
+
if (!itemWrapper) {
|
|
91
|
+
// Silently no-op if the wrapper has not yet mounted. The previous
|
|
92
|
+
// `console.warn` fired during the initial render of every dial
|
|
93
|
+
// and was noise in production logs.
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set transform / transition individually rather than rewriting the
|
|
98
|
+
// full `cssText`. The latter parses the entire declaration string
|
|
99
|
+
// per mousemove (~60 Hz); `setProperty` skips the parse and only
|
|
100
|
+
// updates the named declarations. The `transition-duration` scales
|
|
101
|
+
// with drag inertia, so we still need imperative updates rather
|
|
102
|
+
// than `style:transform={...}` for the dragging case.
|
|
103
|
+
const transitionDuration = `${Math.abs(offset) / 100 + 0.1}s`;
|
|
104
|
+
itemWrapper.style.setProperty('transition', `transform ${transitionDuration}`);
|
|
105
|
+
itemWrapper.style.setProperty('transform', `translateY(${position}px)`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const onmousedown = (event: MouseEvent | TouchEvent) => {
|
|
109
|
+
previousY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
110
|
+
dragging = true;
|
|
111
|
+
|
|
112
|
+
// One AbortController per drag. Attached to all four window
|
|
113
|
+
// listeners so a single `controller.abort()` detaches everything.
|
|
114
|
+
dragAbort?.abort();
|
|
115
|
+
dragAbort = new AbortController();
|
|
116
|
+
const { signal } = dragAbort;
|
|
117
|
+
|
|
118
|
+
window.addEventListener('mousemove', onMouseMove, { signal });
|
|
119
|
+
window.addEventListener('mouseup', onMouseUp, { signal });
|
|
120
|
+
window.addEventListener('touchmove', onMouseMove, { signal });
|
|
121
|
+
window.addEventListener('touchend', onMouseUp, { signal });
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const onMouseMove = (event: MouseEvent | TouchEvent) => {
|
|
125
|
+
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
126
|
+
offset = clientY - previousY;
|
|
127
|
+
|
|
128
|
+
const maxPosition = -entries.length * OPTION_HEIGHT;
|
|
129
|
+
const _position = position + offset;
|
|
130
|
+
position = Math.max(maxPosition, Math.min(OPTION_HEIGHT, _position));
|
|
131
|
+
previousY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
132
|
+
setPosition();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const onMouseUp = () => {
|
|
136
|
+
const maxPosition = -(entries.length - 1) * OPTION_HEIGHT;
|
|
137
|
+
const rounderPosition = Math.round((position + offset * 5) / OPTION_HEIGHT) * OPTION_HEIGHT;
|
|
138
|
+
const finalPosition = Math.max(maxPosition, Math.min(0, rounderPosition));
|
|
139
|
+
|
|
140
|
+
dragging = false;
|
|
141
|
+
position = finalPosition;
|
|
142
|
+
|
|
143
|
+
dragAbort?.abort();
|
|
144
|
+
dragAbort = undefined;
|
|
145
|
+
|
|
146
|
+
setPosition();
|
|
147
|
+
onChange(entries[-finalPosition / OPTION_HEIGHT].value);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const onwheel = (e: WheelEvent) => {
|
|
151
|
+
if (e.deltaY > 0) {
|
|
152
|
+
position = position - OPTION_HEIGHT;
|
|
153
|
+
}
|
|
154
|
+
if (e.deltaY < 0) {
|
|
155
|
+
position = position + OPTION_HEIGHT;
|
|
156
|
+
}
|
|
157
|
+
onMouseUp();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handledKeys = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter']);
|
|
161
|
+
function onkeydown(e: KeyboardEvent) {
|
|
162
|
+
if (handledKeys.has(e.key)) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
|
|
165
|
+
if (e.key === 'ArrowLeft') {
|
|
166
|
+
onPrevious(type);
|
|
167
|
+
} else if (e.key === 'ArrowRight') {
|
|
168
|
+
onNext(type);
|
|
169
|
+
} else if (e.key === 'ArrowUp') {
|
|
170
|
+
position = position + OPTION_HEIGHT;
|
|
171
|
+
onMouseUp();
|
|
172
|
+
} else if (e.key === 'ArrowDown') {
|
|
173
|
+
position = position - OPTION_HEIGHT;
|
|
174
|
+
onMouseUp();
|
|
175
|
+
} else if (e.key === 'Enter') {
|
|
176
|
+
onsubmit();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<div
|
|
183
|
+
class="select-wrapper"
|
|
184
|
+
role="listbox"
|
|
185
|
+
tabindex="0"
|
|
186
|
+
bind:this={ref}
|
|
187
|
+
{onmousedown}
|
|
188
|
+
ontouchstart={onmousedown}
|
|
189
|
+
{onwheel}
|
|
190
|
+
{onkeydown}
|
|
191
|
+
>
|
|
192
|
+
<div class="selected-wrapper">
|
|
193
|
+
<ul
|
|
194
|
+
bind:this={itemWrapper}
|
|
195
|
+
class="touch-date-container"
|
|
196
|
+
>
|
|
197
|
+
{#each entries as entry (entry.value)}
|
|
198
|
+
<li>{entry.label}</li>
|
|
199
|
+
{/each}
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
204
|
<style>.select-wrapper {
|
|
205
205
|
--option-height: 50px;
|
|
206
206
|
height: calc(var(--option-height));
|
|
@@ -262,4 +262,4 @@
|
|
|
262
262
|
width: 80px;
|
|
263
263
|
height: var(--option-height);
|
|
264
264
|
user-select: none;
|
|
265
|
-
}</style>
|
|
265
|
+
}</style>
|