@sveltia/ui 0.71.1 → 0.72.1
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/dist/components/alert/infobar.svelte +1 -1
- package/dist/components/button/button.svelte +1 -1
- package/dist/components/button/select-button.svelte +2 -1
- package/dist/components/calendar/calendar.d.ts +20 -0
- package/dist/components/calendar/calendar.js +81 -0
- package/dist/components/calendar/calendar.svelte +23 -45
- package/dist/components/checkbox/checkbox-group.svelte +1 -1
- package/dist/components/checkbox/checkbox.svelte +6 -1
- package/dist/components/dialog/dialog.svelte +1 -1
- package/dist/components/drawer/drawer.svelte +1 -1
- package/dist/components/emoji/emoji-suggestions.svelte +14 -40
- package/dist/components/emoji/position.d.ts +44 -0
- package/dist/components/emoji/position.js +60 -0
- package/dist/components/progressbar/progressbar.svelte +1 -1
- package/dist/components/radio/radio-group.svelte +1 -1
- package/dist/components/resizable-pane/resizable-handle.svelte +15 -47
- package/dist/components/resizable-pane/resizable-pane-group.svelte +37 -100
- package/dist/components/resizable-pane/resizable-pane.svelte +1 -1
- package/dist/components/resizable-pane/sizing.d.ts +48 -0
- package/dist/components/resizable-pane/sizing.js +190 -0
- package/dist/components/scroll/infinite-scroll.svelte +2 -0
- package/dist/components/select/combobox.svelte +3 -1
- package/dist/components/select/select-tags.d.ts +17 -0
- package/dist/components/select/select-tags.js +95 -0
- package/dist/components/select/select-tags.svelte +26 -28
- package/dist/components/slider/slider.d.ts +17 -0
- package/dist/components/slider/slider.js +96 -0
- package/dist/components/slider/slider.svelte +30 -56
- package/dist/components/tabs/tab-box.svelte +1 -1
- package/dist/components/tabs/tab-list.d.ts +6 -0
- package/dist/components/tabs/tab-list.js +19 -0
- package/dist/components/tabs/tab-list.svelte +2 -14
- package/dist/components/text-editor/code-editor.d.ts +5 -0
- package/dist/components/text-editor/code-editor.js +28 -0
- package/dist/components/text-editor/code-editor.svelte +7 -8
- package/dist/components/text-editor/emoji-autocomplete.svelte +9 -1
- package/dist/components/text-editor/lexical-root.svelte +3 -1
- package/dist/components/text-editor/shiki/facade.js +1 -1
- package/dist/components/text-editor/shiki/highlighter.js +40 -15
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/store.svelte.js +17 -7
- package/dist/components/text-editor/text-editor.svelte +4 -0
- package/dist/components/text-editor/toolbar/code-editor-toolbar.svelte +1 -1
- package/dist/components/text-editor/toolbar/code-language-switcher.svelte +4 -1
- package/dist/components/text-editor/toolbar/format-text-button.svelte +3 -1
- package/dist/components/text-editor/toolbar/insert-item-button.svelte +1 -1
- package/dist/components/text-editor/toolbar/insert-link-button.svelte +10 -3
- package/dist/components/text-editor/toolbar/text-editor-toolbar.svelte +4 -5
- package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +2 -0
- package/dist/components/text-field/emoji-autocomplete.svelte +2 -0
- package/dist/components/text-field/number-input.d.ts +15 -0
- package/dist/components/text-field/number-input.js +98 -0
- package/dist/components/text-field/number-input.svelte +27 -23
- package/dist/components/text-field/text-input.svelte +9 -3
- package/dist/components/toast/toast.svelte +7 -2
- package/dist/components/toolbar/toolbar.svelte +1 -1
- package/dist/components/typography/truncated-text.svelte +2 -1
- package/dist/components/util/app-shell.d.ts +1 -0
- package/dist/components/util/app-shell.js +29 -0
- package/dist/components/util/app-shell.svelte +4 -18
- package/dist/components/util/modal.d.ts +5 -0
- package/dist/components/util/modal.js +22 -0
- package/dist/components/util/modal.svelte +9 -10
- package/dist/components/util/popup.svelte +11 -0
- package/dist/locales/ar.yaml +2 -2
- package/dist/locales/ca.yaml +2 -2
- package/dist/locales/cs.yaml +2 -2
- package/dist/locales/da.yaml +2 -2
- package/dist/locales/de.yaml +1 -1
- package/dist/locales/el.yaml +2 -2
- package/dist/locales/en-CA.yaml +1 -1
- package/dist/locales/en-GB.yaml +1 -1
- package/dist/locales/en-US.yaml +1 -1
- package/dist/locales/es-CO.yaml +2 -2
- package/dist/locales/fa.yaml +2 -2
- package/dist/locales/fi.yaml +1 -1
- package/dist/locales/fr.yaml +2 -2
- package/dist/locales/hr.yaml +116 -0
- package/dist/locales/it.yaml +2 -2
- package/dist/locales/nl.yaml +1 -1
- package/dist/locales/pl.yaml +2 -2
- package/dist/locales/pt-BR.yaml +2 -2
- package/dist/locales/pt-PT.yaml +2 -2
- package/dist/locales/sv.yaml +2 -2
- package/dist/locales/zh-CN.yaml +1 -1
- package/dist/locales/zh-TW.yaml +1 -1
- package/package.json +8 -4
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
-->
|
|
7
7
|
<script>
|
|
8
8
|
import { setContext } from 'svelte';
|
|
9
|
+
import {
|
|
10
|
+
clampResizeDelta,
|
|
11
|
+
getInitialSizes,
|
|
12
|
+
resolvePaneConstraints,
|
|
13
|
+
resolveToPercent,
|
|
14
|
+
} from './sizing.js';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* @import { Snippet } from 'svelte';
|
|
@@ -68,113 +74,44 @@
|
|
|
68
74
|
let _handleCount = 0;
|
|
69
75
|
|
|
70
76
|
/**
|
|
71
|
-
* Get the
|
|
72
|
-
* @returns {
|
|
77
|
+
* Get the sizes the pane lengths are relative to.
|
|
78
|
+
* @returns {import('./sizing.js').SizeEnvironment} Environment.
|
|
73
79
|
*/
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* @param {number} fallback Fallback percentage when resolution fails.
|
|
84
|
-
* @returns {number} Size in percentage points.
|
|
85
|
-
*/
|
|
86
|
-
const resolveToPercent = (value, fallback) => {
|
|
87
|
-
if (typeof value === 'number') {
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!value || typeof value !== 'string') {
|
|
92
|
-
return fallback;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const trimmed = value.trim();
|
|
96
|
-
const matchedPercent = trimmed.match(/^(-?\d+(?:\.\d+)?)%$/);
|
|
97
|
-
|
|
98
|
-
if (matchedPercent) {
|
|
99
|
-
return Number(matchedPercent[1]);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const containerSize = getContainerSize();
|
|
103
|
-
|
|
104
|
-
if (!containerSize) {
|
|
105
|
-
return fallback;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const matchedPx = trimmed.match(/^(-?\d+(?:\.\d+)?)px$/i);
|
|
109
|
-
|
|
110
|
-
if (matchedPx) {
|
|
111
|
-
return (Number(matchedPx[1]) / containerSize) * 100;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const matchedViewport = trimmed.match(/^(-?\d+(?:\.\d+)?)(dvw|vw|dvh|vh)$/i);
|
|
115
|
-
|
|
116
|
-
if (matchedViewport) {
|
|
117
|
-
const viewportValue = Number(matchedViewport[1]);
|
|
118
|
-
const unit = matchedViewport[2].toLowerCase();
|
|
119
|
-
const viewportSize = unit.endsWith('w') ? window.innerWidth : window.innerHeight;
|
|
120
|
-
const pixels = (viewportValue / 100) * viewportSize;
|
|
121
|
-
|
|
122
|
-
return (pixels / containerSize) * 100;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return fallback;
|
|
126
|
-
};
|
|
80
|
+
const getSizeEnvironment = () => ({
|
|
81
|
+
containerSize: element
|
|
82
|
+
? direction === 'horizontal'
|
|
83
|
+
? element.clientWidth
|
|
84
|
+
: element.clientHeight
|
|
85
|
+
: 0,
|
|
86
|
+
viewportWidth: window.innerWidth,
|
|
87
|
+
viewportHeight: window.innerHeight,
|
|
88
|
+
});
|
|
127
89
|
|
|
128
90
|
/**
|
|
129
91
|
* Get pane constraints converted to percentages for the current container size.
|
|
130
92
|
* @param {number} paneIndex Pane index.
|
|
131
93
|
* @returns {{ minSize: number, maxSize: number }} Min/max in percentages.
|
|
132
94
|
*/
|
|
133
|
-
const getPaneConstraints = (paneIndex) =>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!paneDef) {
|
|
137
|
-
return { minSize: 0, maxSize: 100 };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const minSize = Math.max(0, resolveToPercent(paneDef.minSize, 0));
|
|
141
|
-
const maxSize = Math.min(100, resolveToPercent(paneDef.maxSize, 100));
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
minSize,
|
|
145
|
-
maxSize: Math.max(minSize, maxSize),
|
|
146
|
-
};
|
|
147
|
-
};
|
|
95
|
+
const getPaneConstraints = (paneIndex) =>
|
|
96
|
+
resolvePaneConstraints(_paneDefs[paneIndex], getSizeEnvironment());
|
|
148
97
|
|
|
149
98
|
/**
|
|
150
|
-
* Initialize pane sizes from `defaultSize` props. Called
|
|
151
|
-
*
|
|
99
|
+
* Initialize pane sizes from `defaultSize` props. Called once all panes have registered. Panes
|
|
100
|
+
* without `defaultSize` share the remaining space equally.
|
|
152
101
|
*/
|
|
153
102
|
const initSizes = () => {
|
|
103
|
+
// Only called once panes have registered; see the effect below
|
|
104
|
+
/* v8 ignore next */
|
|
154
105
|
if (!_paneDefs.length) return;
|
|
155
106
|
|
|
156
|
-
|
|
157
|
-
// Resolving once ensures totalSpecified and newSizes use the same resolved value.
|
|
158
|
-
const resolvedDefaults = _paneDefs.map((p) => {
|
|
159
|
-
if (p.defaultSize === undefined) {
|
|
160
|
-
return NaN;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const resolved = resolveToPercent(p.defaultSize, NaN);
|
|
164
|
-
|
|
165
|
-
return resolved;
|
|
166
|
-
});
|
|
107
|
+
const env = getSizeEnvironment();
|
|
167
108
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.
|
|
109
|
+
// Resolve each pane’s defaultSize to a percentage (NaN if unspecified or unresolvable)
|
|
110
|
+
const resolvedDefaults = _paneDefs.map((p) =>
|
|
111
|
+
p.defaultSize === undefined ? NaN : resolveToPercent(p.defaultSize, NaN, env),
|
|
112
|
+
);
|
|
171
113
|
|
|
172
|
-
|
|
173
|
-
const remaining = Math.max(0, 100 - totalSpecified);
|
|
174
|
-
const defaultSize = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
|
|
175
|
-
const newSizes = resolvedDefaults.map((v) => (Number.isNaN(v) ? defaultSize : v));
|
|
176
|
-
|
|
177
|
-
sizes.splice(0, sizes.length, ...newSizes);
|
|
114
|
+
sizes.splice(0, sizes.length, ...getInitialSizes(resolvedDefaults));
|
|
178
115
|
};
|
|
179
116
|
|
|
180
117
|
/**
|
|
@@ -188,16 +125,16 @@
|
|
|
188
125
|
|
|
189
126
|
if (beforeIdx < 0 || afterIdx >= sizes.length) return;
|
|
190
127
|
|
|
191
|
-
const { minSize: minBefore, maxSize: maxBefore } = getPaneConstraints(beforeIdx);
|
|
192
|
-
const { minSize: minAfter, maxSize: maxAfter } = getPaneConstraints(afterIdx);
|
|
193
128
|
const prevBefore = sizes[beforeIdx];
|
|
194
129
|
const prevAfter = sizes[afterIdx];
|
|
195
|
-
// Clamp delta so neither pane exceeds its min/max constraints
|
|
196
|
-
const canGrow = Math.min(maxBefore - prevBefore, prevAfter - minAfter);
|
|
197
|
-
const canShrink = Math.min(prevBefore - minBefore, maxAfter - prevAfter);
|
|
198
130
|
|
|
199
|
-
|
|
200
|
-
|
|
131
|
+
// Clamp delta so neither pane exceeds its min/max constraints
|
|
132
|
+
const delta = clampResizeDelta(deltaPercent, {
|
|
133
|
+
sizeBefore: prevBefore,
|
|
134
|
+
sizeAfter: prevAfter,
|
|
135
|
+
constraintsBefore: getPaneConstraints(beforeIdx),
|
|
136
|
+
constraintsAfter: getPaneConstraints(afterIdx),
|
|
137
|
+
});
|
|
201
138
|
|
|
202
139
|
sizes[beforeIdx] = prevBefore + delta;
|
|
203
140
|
sizes[afterIdx] = prevAfter - delta;
|
|
@@ -265,7 +202,7 @@
|
|
|
265
202
|
bind:this={element}
|
|
266
203
|
{...restProps}
|
|
267
204
|
role="none"
|
|
268
|
-
class=
|
|
205
|
+
class={['sui', 'resizable-pane-group', direction, className]}
|
|
269
206
|
data-direction={direction}
|
|
270
207
|
>
|
|
271
208
|
{@render children?.()}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function resolveToPercent(value: number | string | undefined, fallback: number, env: SizeEnvironment): number;
|
|
2
|
+
export function resolvePaneConstraints(paneDef: {
|
|
3
|
+
minSize: number | string;
|
|
4
|
+
maxSize: number | string;
|
|
5
|
+
} | undefined, env: SizeEnvironment): PaneConstraints;
|
|
6
|
+
export function getInitialSizes(resolvedDefaults: number[]): number[];
|
|
7
|
+
export function clampResizeDelta(deltaPercent: number, { sizeBefore, sizeAfter, constraintsBefore, constraintsAfter }: {
|
|
8
|
+
sizeBefore: number;
|
|
9
|
+
sizeAfter: number;
|
|
10
|
+
constraintsBefore: PaneConstraints;
|
|
11
|
+
constraintsAfter: PaneConstraints;
|
|
12
|
+
}): number;
|
|
13
|
+
export function getHandleKeyAction({ key, shiftKey, isHorizontal, rtl }: {
|
|
14
|
+
key: string;
|
|
15
|
+
shiftKey: boolean;
|
|
16
|
+
isHorizontal: boolean;
|
|
17
|
+
rtl: boolean;
|
|
18
|
+
}): {
|
|
19
|
+
type: "toggle";
|
|
20
|
+
} | {
|
|
21
|
+
type: "resize";
|
|
22
|
+
delta: number;
|
|
23
|
+
} | undefined;
|
|
24
|
+
export type SizeEnvironment = {
|
|
25
|
+
/**
|
|
26
|
+
* Size of the pane group in pixels along its direction, or `0`
|
|
27
|
+
* if it hasn’t been laid out yet.
|
|
28
|
+
*/
|
|
29
|
+
containerSize: number;
|
|
30
|
+
/**
|
|
31
|
+
* Width of the viewport in pixels.
|
|
32
|
+
*/
|
|
33
|
+
viewportWidth: number;
|
|
34
|
+
/**
|
|
35
|
+
* Height of the viewport in pixels.
|
|
36
|
+
*/
|
|
37
|
+
viewportHeight: number;
|
|
38
|
+
};
|
|
39
|
+
export type PaneConstraints = {
|
|
40
|
+
/**
|
|
41
|
+
* Minimum size.
|
|
42
|
+
*/
|
|
43
|
+
minSize: number;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum size, never below `minSize`.
|
|
46
|
+
*/
|
|
47
|
+
maxSize: number;
|
|
48
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size arithmetic behind `<ResizablePaneGroup>` and `<ResizableHandle>`, kept apart from the
|
|
3
|
+
* components so the conversions and clamping can be tested without laying anything out. Every
|
|
4
|
+
* size here is a percentage of the pane group unless stated otherwise.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} SizeEnvironment
|
|
9
|
+
* @property {number} containerSize Size of the pane group in pixels along its direction, or `0`
|
|
10
|
+
* if it hasn’t been laid out yet.
|
|
11
|
+
* @property {number} viewportWidth Width of the viewport in pixels.
|
|
12
|
+
* @property {number} viewportHeight Height of the viewport in pixels.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} PaneConstraints
|
|
17
|
+
* @property {number} minSize Minimum size.
|
|
18
|
+
* @property {number} maxSize Maximum size, never below `minSize`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a size given as a percentage number or a CSS length string to a percentage.
|
|
23
|
+
* @param {number | string | undefined} value Size as a percentage number, or a CSS size string
|
|
24
|
+
* such as `240px`, `20%` or `20dvw`.
|
|
25
|
+
* @param {number} fallback Percentage to return when the value can’t be resolved.
|
|
26
|
+
* @param {SizeEnvironment} env Sizes the lengths are relative to. A length other than a percentage
|
|
27
|
+
* can’t be resolved while the container has no size.
|
|
28
|
+
* @returns {number} Size in percentage points.
|
|
29
|
+
*/
|
|
30
|
+
export const resolveToPercent = (value, fallback, env) => {
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!value || typeof value !== 'string') {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
const matchedPercent = trimmed.match(/^(-?\d+(?:\.\d+)?)%$/);
|
|
41
|
+
|
|
42
|
+
if (matchedPercent) {
|
|
43
|
+
return Number(matchedPercent[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { containerSize } = env;
|
|
47
|
+
|
|
48
|
+
if (!containerSize) {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const matchedPx = trimmed.match(/^(-?\d+(?:\.\d+)?)px$/i);
|
|
53
|
+
|
|
54
|
+
if (matchedPx) {
|
|
55
|
+
return (Number(matchedPx[1]) / containerSize) * 100;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const matchedViewport = trimmed.match(/^(-?\d+(?:\.\d+)?)(dvw|vw|dvh|vh)$/i);
|
|
59
|
+
|
|
60
|
+
if (matchedViewport) {
|
|
61
|
+
const viewportValue = Number(matchedViewport[1]);
|
|
62
|
+
const unit = matchedViewport[2].toLowerCase();
|
|
63
|
+
const viewportSize = unit.endsWith('w') ? env.viewportWidth : env.viewportHeight;
|
|
64
|
+
const pixels = (viewportValue / 100) * viewportSize;
|
|
65
|
+
|
|
66
|
+
return (pixels / containerSize) * 100;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return fallback;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a pane’s size constraints to percentages, clamped to the `0`–`100` range, with the
|
|
74
|
+
* maximum never below the minimum.
|
|
75
|
+
* @param {{ minSize: number | string, maxSize: number | string } | undefined} paneDef Pane
|
|
76
|
+
* definition, or `undefined` for a pane that hasn’t registered, which is unconstrained.
|
|
77
|
+
* @param {SizeEnvironment} env Sizes the lengths are relative to.
|
|
78
|
+
* @returns {PaneConstraints} Constraints.
|
|
79
|
+
*/
|
|
80
|
+
export const resolvePaneConstraints = (paneDef, env) => {
|
|
81
|
+
if (!paneDef) {
|
|
82
|
+
return { minSize: 0, maxSize: 100 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const minSize = Math.max(0, resolveToPercent(paneDef.minSize, 0, env));
|
|
86
|
+
const maxSize = Math.min(100, resolveToPercent(paneDef.maxSize, 100, env));
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
minSize,
|
|
90
|
+
maxSize: Math.max(minSize, maxSize),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Work out the initial size of each pane. Panes with a default size get it; the others share
|
|
96
|
+
* whatever is left of the group equally.
|
|
97
|
+
* @param {number[]} resolvedDefaults Each pane’s default size as a percentage, or `NaN` for a pane
|
|
98
|
+
* that has none, or whose default couldn’t be resolved.
|
|
99
|
+
* @returns {number[]} Size of each pane.
|
|
100
|
+
*/
|
|
101
|
+
export const getInitialSizes = (resolvedDefaults) => {
|
|
102
|
+
const specified = resolvedDefaults.filter((v) => !Number.isNaN(v));
|
|
103
|
+
const totalSpecified = specified.reduce((sum, v) => sum + v, 0);
|
|
104
|
+
const unspecifiedCount = resolvedDefaults.length - specified.length;
|
|
105
|
+
const remaining = Math.max(0, 100 - totalSpecified);
|
|
106
|
+
const defaultSize = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
|
|
107
|
+
|
|
108
|
+
return resolvedDefaults.map((v) => (Number.isNaN(v) ? defaultSize : v));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clamp the delta a handle is moved by, so neither of the two panes around it leaves its size
|
|
113
|
+
* constraints. A positive delta grows the pane before the handle at the expense of the one after
|
|
114
|
+
* it, and a negative delta does the opposite.
|
|
115
|
+
* @param {number} deltaPercent Requested delta.
|
|
116
|
+
* @param {object} panes The panes around the handle.
|
|
117
|
+
* @param {number} panes.sizeBefore Current size of the pane before the handle.
|
|
118
|
+
* @param {number} panes.sizeAfter Current size of the pane after the handle.
|
|
119
|
+
* @param {PaneConstraints} panes.constraintsBefore Constraints of the pane before the handle.
|
|
120
|
+
* @param {PaneConstraints} panes.constraintsAfter Constraints of the pane after the handle.
|
|
121
|
+
* @returns {number} Delta both panes can accommodate.
|
|
122
|
+
*/
|
|
123
|
+
export const clampResizeDelta = (
|
|
124
|
+
deltaPercent,
|
|
125
|
+
{ sizeBefore, sizeAfter, constraintsBefore, constraintsAfter },
|
|
126
|
+
) => {
|
|
127
|
+
const canGrow = Math.min(
|
|
128
|
+
constraintsBefore.maxSize - sizeBefore,
|
|
129
|
+
sizeAfter - constraintsAfter.minSize,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const canShrink = Math.min(
|
|
133
|
+
sizeBefore - constraintsBefore.minSize,
|
|
134
|
+
constraintsAfter.maxSize - sizeAfter,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return deltaPercent > 0 ? Math.min(deltaPercent, canGrow) : -Math.min(-deltaPercent, canShrink);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Interpret a key pressed on a resize handle. Arrow keys move the handle by 1%, or 10% with Shift.
|
|
142
|
+
* Enter collapses or restores the pane before the handle. Home and End move it to its minimum and
|
|
143
|
+
* maximum; the delta for those is oversized, and gets clamped by {@link clampResizeDelta}.
|
|
144
|
+
* @param {object} options Options.
|
|
145
|
+
* @param {string} options.key The key pressed.
|
|
146
|
+
* @param {boolean} options.shiftKey Whether Shift was held.
|
|
147
|
+
* @param {boolean} options.isHorizontal Whether the panes are laid out horizontally, in which case
|
|
148
|
+
* the handle moves left and right.
|
|
149
|
+
* @param {boolean} options.rtl Whether the layout is right-to-left, which swaps the horizontal
|
|
150
|
+
* arrow keys.
|
|
151
|
+
* @returns {{ type: 'toggle' } | { type: 'resize', delta: number } | undefined} What to do, or
|
|
152
|
+
* `undefined` if the key has no meaning here.
|
|
153
|
+
*/
|
|
154
|
+
export const getHandleKeyAction = ({ key, shiftKey, isHorizontal, rtl }) => {
|
|
155
|
+
if (key === 'Enter') {
|
|
156
|
+
return { type: 'toggle' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (key === 'Home') {
|
|
160
|
+
return { type: 'resize', delta: -100 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (key === 'End') {
|
|
164
|
+
return { type: 'resize', delta: 100 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const step = shiftKey ? 10 : 1;
|
|
168
|
+
|
|
169
|
+
if (isHorizontal) {
|
|
170
|
+
if (key === 'ArrowLeft') {
|
|
171
|
+
return { type: 'resize', delta: rtl ? step : -step };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (key === 'ArrowRight') {
|
|
175
|
+
return { type: 'resize', delta: rtl ? -step : step };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (key === 'ArrowUp') {
|
|
182
|
+
return { type: 'resize', delta: -step };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (key === 'ArrowDown') {
|
|
186
|
+
return { type: 'resize', delta: step };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return undefined;
|
|
190
|
+
};
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
|
|
38
38
|
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
|
|
39
39
|
if (isIntersecting) {
|
|
40
|
+
// The spinner is unmounted as soon as everything is loaded, so it can’t intersect afterwards
|
|
41
|
+
/* v8 ignore else */
|
|
40
42
|
if (loading) {
|
|
41
43
|
loadedItemSize += itemChunkSize;
|
|
42
44
|
} else {
|
|
@@ -184,6 +184,8 @@
|
|
|
184
184
|
// Move the options into the popup while it’s expanded, and back out before it’s unmounted. Only
|
|
185
185
|
// the wrapper is moved, never its children, so Svelte keeps full ownership of the subtree.
|
|
186
186
|
$effect(() => {
|
|
187
|
+
// The host is bound before any effect runs
|
|
188
|
+
/* v8 ignore next */
|
|
187
189
|
if (!optionHost) {
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
@@ -274,7 +276,7 @@
|
|
|
274
276
|
{invalid}
|
|
275
277
|
aria-expanded={isPopupOpen}
|
|
276
278
|
aria-haspopup="listbox"
|
|
277
|
-
|
|
279
|
+
{ariaLabel}
|
|
278
280
|
/>
|
|
279
281
|
{/if}
|
|
280
282
|
<Button
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function moveItem<T>(list: T[], from: number, to: number): T[];
|
|
2
|
+
export function getKeyboardMoveTarget({ key, index, length, rtl }: {
|
|
3
|
+
key: string;
|
|
4
|
+
index: number;
|
|
5
|
+
length: number;
|
|
6
|
+
rtl: boolean;
|
|
7
|
+
}): number;
|
|
8
|
+
export function getDropIndex({ index, clientX, rect, rtl }: {
|
|
9
|
+
index: number;
|
|
10
|
+
clientX: number;
|
|
11
|
+
rect: {
|
|
12
|
+
left: number;
|
|
13
|
+
width: number;
|
|
14
|
+
};
|
|
15
|
+
rtl: boolean;
|
|
16
|
+
}): number;
|
|
17
|
+
export function getDropTarget(fromIndex: number, dropIndex: number): number | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reordering logic behind `<SelectTags>`, kept apart from the component so it can be tested
|
|
3
|
+
* without dragging anything.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Move an item within a list.
|
|
8
|
+
* @template T
|
|
9
|
+
* @param {T[]} list List.
|
|
10
|
+
* @param {number} from Index of the item to move.
|
|
11
|
+
* @param {number} to Index the item should end up at.
|
|
12
|
+
* @returns {T[]} A new list with the item moved, or the same list if `from` and `to` are equal.
|
|
13
|
+
*/
|
|
14
|
+
export const moveItem = (list, from, to) => {
|
|
15
|
+
if (from === to) {
|
|
16
|
+
return list;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const newList = [...list];
|
|
20
|
+
const [item] = newList.splice(from, 1);
|
|
21
|
+
|
|
22
|
+
newList.splice(to, 0, item);
|
|
23
|
+
|
|
24
|
+
return newList;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the index a tag should move to when a key is pressed on it. The arrow keys swap it with a
|
|
29
|
+
* neighbor, following the visual direction of the list, and Home/End move it to either end.
|
|
30
|
+
* @param {object} options Options.
|
|
31
|
+
* @param {string} options.key The key pressed.
|
|
32
|
+
* @param {number} options.index Index of the tag.
|
|
33
|
+
* @param {number} options.length Number of tags.
|
|
34
|
+
* @param {boolean} options.rtl Whether the layout is right-to-left, in which case the arrow keys
|
|
35
|
+
* are swapped.
|
|
36
|
+
* @returns {number} Target index, or `-1` if the key doesn’t move the tag, including when it’s
|
|
37
|
+
* already at the end the key would move it to.
|
|
38
|
+
*/
|
|
39
|
+
export const getKeyboardMoveTarget = ({ key, index, length, rtl }) => {
|
|
40
|
+
const prevKey = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
41
|
+
const nextKey = rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
42
|
+
|
|
43
|
+
if (key === prevKey && index > 0) {
|
|
44
|
+
return index - 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (key === nextKey && index < length - 1) {
|
|
48
|
+
return index + 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key === 'Home' && index > 0) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key === 'End' && index < length - 1) {
|
|
56
|
+
return length - 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return -1;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get where a dragged tag would be inserted when the pointer is over another tag: before it when
|
|
64
|
+
* the pointer is in the half nearer to the start of the list, after it otherwise.
|
|
65
|
+
* @param {object} options Options.
|
|
66
|
+
* @param {number} options.index Index of the tag under the pointer.
|
|
67
|
+
* @param {number} options.clientX Horizontal position of the pointer.
|
|
68
|
+
* @param {{ left: number, width: number }} options.rect Bounds of the tag under the pointer.
|
|
69
|
+
* @param {boolean} options.rtl Whether the layout is right-to-left, in which case the start of the
|
|
70
|
+
* list is on the right.
|
|
71
|
+
* @returns {number} Insertion index: the dragged tag would be placed before the tag at this index,
|
|
72
|
+
* so `length` means after the last one.
|
|
73
|
+
*/
|
|
74
|
+
export const getDropIndex = ({ index, clientX, rect, rtl }) => {
|
|
75
|
+
const inFirstHalf = clientX < rect.left + rect.width / 2;
|
|
76
|
+
|
|
77
|
+
return inFirstHalf !== rtl ? index : index + 1;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Turn an insertion index into the index the dragged tag ends up at once it’s been taken out of
|
|
82
|
+
* the list.
|
|
83
|
+
* @param {number} fromIndex Index of the dragged tag.
|
|
84
|
+
* @param {number} dropIndex Insertion index, as returned by {@link getDropIndex}.
|
|
85
|
+
* @returns {number | undefined} Destination index for {@link moveItem}, or `undefined` if the
|
|
86
|
+
* drop leaves the order unchanged, which is the case when the tag is dropped on either side of
|
|
87
|
+
* its own position.
|
|
88
|
+
*/
|
|
89
|
+
export const getDropTarget = (fromIndex, dropIndex) => {
|
|
90
|
+
if (dropIndex === fromIndex || dropIndex === fromIndex + 1) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return dropIndex > fromIndex ? dropIndex - 1 : dropIndex;
|
|
95
|
+
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import Icon from '../icon/icon.svelte';
|
|
7
7
|
import Option from '../listbox/option.svelte';
|
|
8
8
|
import Select from './select.svelte';
|
|
9
|
+
import { getDropIndex, getDropTarget, getKeyboardMoveTarget, moveItem } from './select-tags.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @import { Snippet } from 'svelte';
|
|
@@ -66,8 +67,6 @@
|
|
|
66
67
|
* @type {Set<any>}
|
|
67
68
|
*/
|
|
68
69
|
const selectedValues = $derived(new Set(values));
|
|
69
|
-
const prevKey = $derived(isRTL() ? 'ArrowRight' : 'ArrowLeft');
|
|
70
|
-
const nextKey = $derived(isRTL() ? 'ArrowLeft' : 'ArrowRight');
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
72
|
* Reference to the wrapper element.
|
|
@@ -99,12 +98,12 @@
|
|
|
99
98
|
* @param {number} to Destination index.
|
|
100
99
|
*/
|
|
101
100
|
const moveValue = (from, to) => {
|
|
101
|
+
// The callers only ask for actual moves
|
|
102
|
+
/* v8 ignore next */
|
|
102
103
|
if (from === to) return;
|
|
103
104
|
|
|
104
|
-
const newValues =
|
|
105
|
-
const [item] = newValues.splice(from, 1);
|
|
105
|
+
const newValues = moveItem(values, from, to);
|
|
106
106
|
|
|
107
|
-
newValues.splice(to, 0, item);
|
|
108
107
|
values = newValues;
|
|
109
108
|
onReorder?.(new CustomEvent('Reorder', { detail: { values: newValues } }));
|
|
110
109
|
};
|
|
@@ -162,10 +161,12 @@
|
|
|
162
161
|
event.dataTransfer.dropEffect = 'move';
|
|
163
162
|
}
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
dropIndex = getDropIndex({
|
|
165
|
+
index,
|
|
166
|
+
clientX: event.clientX,
|
|
167
|
+
rect: event.currentTarget.getBoundingClientRect(),
|
|
168
|
+
rtl: isRTL(),
|
|
169
|
+
});
|
|
169
170
|
}}
|
|
170
171
|
ondrop={async (event) => {
|
|
171
172
|
event.preventDefault();
|
|
@@ -176,13 +177,14 @@
|
|
|
176
177
|
dragIndex = undefined;
|
|
177
178
|
dropIndex = undefined;
|
|
178
179
|
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
if (fromIndex === undefined || toIndex === undefined) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const target = getDropTarget(fromIndex, toIndex);
|
|
185
|
+
|
|
186
|
+
if (target !== undefined) {
|
|
187
|
+
await moveAndFocus(fromIndex, target);
|
|
186
188
|
}
|
|
187
189
|
}}
|
|
188
190
|
ondragend={() => {
|
|
@@ -197,18 +199,12 @@
|
|
|
197
199
|
aria-selected="true"
|
|
198
200
|
tabindex={disabled || readonly ? undefined : 0}
|
|
199
201
|
onkeydown={async (event) => {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
? index + 1
|
|
207
|
-
: key === 'Home' && index > 0
|
|
208
|
-
? 0
|
|
209
|
-
: key === 'End' && index < values.length - 1
|
|
210
|
-
? values.length - 1
|
|
211
|
-
: -1;
|
|
202
|
+
const targetIndex = getKeyboardMoveTarget({
|
|
203
|
+
key: event.key,
|
|
204
|
+
index,
|
|
205
|
+
length: values.length,
|
|
206
|
+
rtl: isRTL(),
|
|
207
|
+
});
|
|
212
208
|
|
|
213
209
|
if (targetIndex === -1) return;
|
|
214
210
|
|
|
@@ -246,6 +242,8 @@
|
|
|
246
242
|
{required}
|
|
247
243
|
{invalid}
|
|
248
244
|
onChange={() => {
|
|
245
|
+
// The select only reports a change once an option has been picked
|
|
246
|
+
/* v8 ignore else */
|
|
249
247
|
if (selectedValue) {
|
|
250
248
|
values = [...values, selectedValue];
|
|
251
249
|
onAddValue?.(new CustomEvent('AddValue', { detail: { value: selectedValue } }));
|