vira 33.2.0 → 33.3.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/dist/directives/drag-reorder.directive.d.ts +68 -0
- package/dist/directives/drag-reorder.directive.js +114 -0
- package/dist/directives/index.d.ts +1 -0
- package/dist/directives/index.js +1 -0
- package/dist/elements/popover/vira-menu-item.element.d.ts +2 -0
- package/dist/elements/popover/vira-menu-item.element.js +8 -4
- package/dist/elements/vira-dropdown.element.js +8 -6
- package/dist/elements/vira-form.element.d.ts +13 -1
- package/dist/elements/vira-form.element.js +165 -7
- package/dist/icons/icon-css-vars.d.ts +1 -1
- package/dist/icons/icon-css-vars.js +1 -1
- package/dist/icons/lucide-icons.d.ts +2 -1
- package/dist/icons/lucide-icons.js +4 -1
- package/dist/util/drag-reorder-manager.d.ts +84 -0
- package/dist/util/drag-reorder-manager.js +213 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/vira-form-fields.d.ts +12 -0
- package/dist/util/vira-form-fields.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Directive, type PartInfo } from 'element-vir';
|
|
2
|
+
import { type DragReorderManager, type DragReorderMove } from '../util/drag-reorder-manager.js';
|
|
3
|
+
/**
|
|
4
|
+
* The directive class behind {@link dragReorderHandle}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export declare class DragReorderHandleDirective extends Directive {
|
|
9
|
+
protected manager: DragReorderManager | undefined;
|
|
10
|
+
protected element: HTMLElement | undefined;
|
|
11
|
+
constructor(partInfo: PartInfo);
|
|
12
|
+
/** Applies the latest directive arguments. */
|
|
13
|
+
update(partInfo: PartInfo, [manager]: [DragReorderManager]): undefined;
|
|
14
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
15
|
+
render(manager: DragReorderManager): undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Makes the element it is attached to start dragging the row that contains it. That row must be a
|
|
19
|
+
* direct child of the element that `dragReorderRows` with the same manager is attached to.
|
|
20
|
+
*
|
|
21
|
+
* @category Reorder
|
|
22
|
+
*/
|
|
23
|
+
export declare const dragReorderHandle: (manager: DragReorderManager) => import("element-vir").DirectiveResult<typeof DragReorderHandleDirective>;
|
|
24
|
+
/**
|
|
25
|
+
* The directive class behind {@link dragReorderRows}.
|
|
26
|
+
*
|
|
27
|
+
* @category Internal
|
|
28
|
+
*/
|
|
29
|
+
export declare class DragReorderRowsDirective extends Directive {
|
|
30
|
+
protected manager: DragReorderManager | undefined;
|
|
31
|
+
protected element: HTMLElement | undefined;
|
|
32
|
+
constructor(partInfo: PartInfo);
|
|
33
|
+
/** Applies the latest directive arguments. */
|
|
34
|
+
update(partInfo: PartInfo, [manager, onReorder,]: [
|
|
35
|
+
DragReorderManager,
|
|
36
|
+
(move: Readonly<DragReorderMove>) => void
|
|
37
|
+
]): undefined;
|
|
38
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
39
|
+
render(manager: DragReorderManager, onReorder: (move: Readonly<DragReorderMove>) => void): undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Marks the element whose direct children are the reorderable rows, and receives every finished
|
|
43
|
+
* {@link DragReorderMove}. Indexes count those children, so render nothing (not an empty wrapper)
|
|
44
|
+
* for rows that should not take part.
|
|
45
|
+
*
|
|
46
|
+
* @category Reorder
|
|
47
|
+
*/
|
|
48
|
+
export declare const dragReorderRows: (manager: DragReorderManager, onReorder: (move: Readonly<DragReorderMove>) => void) => import("element-vir").DirectiveResult<typeof DragReorderRowsDirective>;
|
|
49
|
+
/**
|
|
50
|
+
* The directive class behind {@link dragReorderGhost}.
|
|
51
|
+
*
|
|
52
|
+
* @category Internal
|
|
53
|
+
*/
|
|
54
|
+
export declare class DragReorderGhostDirective extends Directive {
|
|
55
|
+
constructor(partInfo: PartInfo);
|
|
56
|
+
/** Applies the latest directive arguments. */
|
|
57
|
+
update(partInfo: PartInfo, [manager]: [DragReorderManager]): undefined;
|
|
58
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
59
|
+
render(manager: DragReorderManager): undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Turns the element it is attached to into the translucent copy of the dragged row: it is
|
|
63
|
+
* absolutely positioned over the dragged row and then follows the pointer. Render it only while the
|
|
64
|
+
* manager's `value.draggedIndex` is set, with a copy of that row inside it.
|
|
65
|
+
*
|
|
66
|
+
* @category Reorder
|
|
67
|
+
*/
|
|
68
|
+
export declare const dragReorderGhost: (manager: DragReorderManager) => import("element-vir").DirectiveResult<typeof DragReorderGhostDirective>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { assertIsElementPartInfo, directive, Directive } from 'element-vir';
|
|
3
|
+
/**
|
|
4
|
+
* The directive class behind {@link dragReorderHandle}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export class DragReorderHandleDirective extends Directive {
|
|
9
|
+
manager;
|
|
10
|
+
element;
|
|
11
|
+
constructor(partInfo) {
|
|
12
|
+
super(partInfo);
|
|
13
|
+
assertIsElementPartInfo(partInfo, 'dragReorderHandle');
|
|
14
|
+
}
|
|
15
|
+
/** Applies the latest directive arguments. */
|
|
16
|
+
update(partInfo, [manager]) {
|
|
17
|
+
assertIsElementPartInfo(partInfo, 'dragReorderHandle');
|
|
18
|
+
this.manager = manager;
|
|
19
|
+
const element = assertWrap.instanceOf(partInfo.element, HTMLElement);
|
|
20
|
+
if (element !== this.element) {
|
|
21
|
+
this.element = element;
|
|
22
|
+
element.draggable = true;
|
|
23
|
+
element.addEventListener('dragstart', (event) => {
|
|
24
|
+
this.manager?.startDrag(event, element);
|
|
25
|
+
});
|
|
26
|
+
element.addEventListener('dragend', () => this.manager?.endDrag());
|
|
27
|
+
}
|
|
28
|
+
return this.render(manager);
|
|
29
|
+
}
|
|
30
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
32
|
+
render(manager) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Makes the element it is attached to start dragging the row that contains it. That row must be a
|
|
38
|
+
* direct child of the element that `dragReorderRows` with the same manager is attached to.
|
|
39
|
+
*
|
|
40
|
+
* @category Reorder
|
|
41
|
+
*/
|
|
42
|
+
export const dragReorderHandle = directive(DragReorderHandleDirective);
|
|
43
|
+
/**
|
|
44
|
+
* The directive class behind {@link dragReorderRows}.
|
|
45
|
+
*
|
|
46
|
+
* @category Internal
|
|
47
|
+
*/
|
|
48
|
+
export class DragReorderRowsDirective extends Directive {
|
|
49
|
+
manager;
|
|
50
|
+
element;
|
|
51
|
+
constructor(partInfo) {
|
|
52
|
+
super(partInfo);
|
|
53
|
+
assertIsElementPartInfo(partInfo, 'dragReorderRows');
|
|
54
|
+
}
|
|
55
|
+
/** Applies the latest directive arguments. */
|
|
56
|
+
update(partInfo, [manager, onReorder,]) {
|
|
57
|
+
assertIsElementPartInfo(partInfo, 'dragReorderRows');
|
|
58
|
+
this.manager = manager;
|
|
59
|
+
const element = assertWrap.instanceOf(partInfo.element, HTMLElement);
|
|
60
|
+
manager.attachRows(element, onReorder);
|
|
61
|
+
if (element !== this.element) {
|
|
62
|
+
this.element = element;
|
|
63
|
+
element.addEventListener('dragover', (event) => this.manager?.dragOver(event));
|
|
64
|
+
element.addEventListener('drop', (event) => this.manager?.drop(event));
|
|
65
|
+
}
|
|
66
|
+
return this.render(manager, onReorder);
|
|
67
|
+
}
|
|
68
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
69
|
+
render(
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
71
|
+
manager,
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
73
|
+
onReorder) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Marks the element whose direct children are the reorderable rows, and receives every finished
|
|
79
|
+
* {@link DragReorderMove}. Indexes count those children, so render nothing (not an empty wrapper)
|
|
80
|
+
* for rows that should not take part.
|
|
81
|
+
*
|
|
82
|
+
* @category Reorder
|
|
83
|
+
*/
|
|
84
|
+
export const dragReorderRows = directive(DragReorderRowsDirective);
|
|
85
|
+
/**
|
|
86
|
+
* The directive class behind {@link dragReorderGhost}.
|
|
87
|
+
*
|
|
88
|
+
* @category Internal
|
|
89
|
+
*/
|
|
90
|
+
export class DragReorderGhostDirective extends Directive {
|
|
91
|
+
constructor(partInfo) {
|
|
92
|
+
super(partInfo);
|
|
93
|
+
assertIsElementPartInfo(partInfo, 'dragReorderGhost');
|
|
94
|
+
}
|
|
95
|
+
/** Applies the latest directive arguments. */
|
|
96
|
+
update(partInfo, [manager]) {
|
|
97
|
+
assertIsElementPartInfo(partInfo, 'dragReorderGhost');
|
|
98
|
+
manager.attachGhost(assertWrap.instanceOf(partInfo.element, HTMLElement));
|
|
99
|
+
return this.render(manager);
|
|
100
|
+
}
|
|
101
|
+
/** Renders nothing, this directive only attaches behavior to its element. */
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
103
|
+
render(manager) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Turns the element it is attached to into the translucent copy of the dragged row: it is
|
|
109
|
+
* absolutely positioned over the dragged row and then follows the pointer. Render it only while the
|
|
110
|
+
* manager's `value.draggedIndex` is set, with a copy of that row inside it.
|
|
111
|
+
*
|
|
112
|
+
* @category Reorder
|
|
113
|
+
*/
|
|
114
|
+
export const dragReorderGhost = directive(DragReorderGhostDirective);
|
package/dist/directives/index.js
CHANGED
|
@@ -19,6 +19,8 @@ export declare const ViraMenuItem: import("element-vir").DeclarativeElementDefin
|
|
|
19
19
|
* _always_ be shown, even if `selected` is set to `false`.
|
|
20
20
|
*/
|
|
21
21
|
iconOverride: ViraIconSvg;
|
|
22
|
+
/** Removes the icon entirely, including the space it reserves when not shown. */
|
|
23
|
+
hideIcon: boolean;
|
|
22
24
|
/**
|
|
23
25
|
* When `true`, activating this item will _not_ close the containing popover.
|
|
24
26
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
-
import { css, defineElementEvent, html } from 'element-vir';
|
|
2
|
+
import { css, defineElementEvent, html, nothing } from 'element-vir';
|
|
3
3
|
import { listenTo } from 'typed-event-target';
|
|
4
4
|
import { lucideIcons } from '../../icons/lucide-icons.js';
|
|
5
5
|
import { createSizedIcon } from '../../icons/sized-icon.js';
|
|
@@ -241,9 +241,13 @@ export const ViraMenuItem = defineViraElement()({
|
|
|
241
241
|
},
|
|
242
242
|
render({ inputs }) {
|
|
243
243
|
return html `
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
${inputs.hideIcon
|
|
245
|
+
? nothing
|
|
246
|
+
: html `
|
|
247
|
+
<${ViraIcon.assign({
|
|
248
|
+
icon: inputs.iconOverride || selectedCheckIcon,
|
|
249
|
+
})}></${ViraIcon}>
|
|
250
|
+
`}
|
|
247
251
|
<div class="slot-wrapper">
|
|
248
252
|
<slot> </slot>
|
|
249
253
|
</div>
|
|
@@ -7,7 +7,7 @@ import { ChevronUp16Icon } from '../icons/index.js';
|
|
|
7
7
|
import { createFocusStyles } from '../styles/focus.js';
|
|
8
8
|
import { viraFormCssVars } from '../styles/form-styles.js';
|
|
9
9
|
import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
|
|
10
|
-
import { noNativeFormStyles, noUserSelect, viraAnimationDurations } from '../styles/index.js';
|
|
10
|
+
import { noNativeFormStyles, noUserSelect, viraAnimationDurations, viraTheme, } from '../styles/index.js';
|
|
11
11
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
12
12
|
import { fuzzyMatch } from '../util/fuzzy-match.js';
|
|
13
13
|
import { renderMenuItemEntries } from '../util/menu-helpers.js';
|
|
@@ -149,20 +149,21 @@ export const ViraDropdown = defineViraElement()({
|
|
|
149
149
|
.option-group-label,
|
|
150
150
|
.no-options {
|
|
151
151
|
${noUserSelect};
|
|
152
|
-
/* Aligns with the label text of each ViraMenuItem, past its check icon. */
|
|
153
|
-
padding: 8px 12px 4px
|
|
154
|
-
calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
|
|
155
|
-
opacity: 0.6;
|
|
156
152
|
text-align: left;
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
.option-group-label {
|
|
156
|
+
color: ${viraTheme.colors['vira-grey-foreground-placeholder'].foreground.value};
|
|
157
|
+
padding: 8px 12px 4px calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 2);
|
|
160
158
|
font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
.no-options {
|
|
164
162
|
cursor: default;
|
|
165
|
-
|
|
163
|
+
opacity: 0.6;
|
|
164
|
+
/* Aligns with the label text of each ViraMenuItem, past its check icon. */
|
|
165
|
+
padding: 8px 12px 8px
|
|
166
|
+
calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
${ViraMenuItem}.option-group-label {
|
|
@@ -335,6 +336,7 @@ export const ViraDropdown = defineViraElement()({
|
|
|
335
336
|
? html `
|
|
336
337
|
<${ViraMenuItem.assign({
|
|
337
338
|
disablePointerStyles: true,
|
|
339
|
+
hideIcon: true,
|
|
338
340
|
})}
|
|
339
341
|
class="option-group-label"
|
|
340
342
|
${state.navController ? nav(state.navController) : nothing}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { DragReorderManager } from '../util/drag-reorder-manager.js';
|
|
2
3
|
import { type ViraFormField, type ViraFormFields } from '../util/vira-form-fields.js';
|
|
3
4
|
/**
|
|
4
5
|
* A form element.
|
|
@@ -44,8 +45,17 @@ export declare const ViraForm: import("element-vir").DeclarativeElementDefinitio
|
|
|
44
45
|
* @default false
|
|
45
46
|
*/
|
|
46
47
|
isReadonly: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* When `true`, each visible field gets a drag handle. Dropping a field emits
|
|
50
|
+
* `fieldOrderChange` with every field key in its new order. The form does not reorder
|
|
51
|
+
* `fields` itself: pass them back in the emitted order.
|
|
52
|
+
*
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
isReorderable: boolean;
|
|
47
56
|
}>, {
|
|
48
57
|
lastIsValid: boolean;
|
|
58
|
+
dragReorder: DragReorderManager;
|
|
49
59
|
}, {
|
|
50
60
|
valueChange: import("element-vir").DefineElementEvent<{
|
|
51
61
|
key: string;
|
|
@@ -53,4 +63,6 @@ export declare const ViraForm: import("element-vir").DeclarativeElementDefinitio
|
|
|
53
63
|
validChange: import("element-vir").DefineElementEvent<{
|
|
54
64
|
allFieldsAreValid: boolean;
|
|
55
65
|
}>;
|
|
56
|
-
|
|
66
|
+
/** Every field key, hidden ones included, in the order the user dropped them. */
|
|
67
|
+
fieldOrderChange: import("element-vir").DefineElementEvent<string[]>;
|
|
68
|
+
}, "vira-form-", "vira-form-", readonly [], readonly ["dragHandle"]>;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
2
|
import { getObjectTypedEntries } from '@augment-vir/common';
|
|
3
|
-
import { css, defineElementEvent, html, listen, nothing, testId, } from 'element-vir';
|
|
3
|
+
import { classMap, css, defineElementEvent, html, listen, nothing, testId, } from 'element-vir';
|
|
4
|
+
import { dragReorderGhost, dragReorderHandle, dragReorderRows, } from '../directives/drag-reorder.directive.js';
|
|
5
|
+
import { lucideIcons } from '../icons/lucide-icons.js';
|
|
4
6
|
import { viraFormCssVars } from '../styles/form-styles.js';
|
|
5
7
|
import { defineViraElement } from '../util/define-vira-element.js';
|
|
6
|
-
import {
|
|
8
|
+
import { DragReorderManager } from '../util/drag-reorder-manager.js';
|
|
9
|
+
import { applyRequiredLabel, areFormFieldsValid, moveFormFieldKey, ViraFormFieldType, } from '../util/vira-form-fields.js';
|
|
7
10
|
import { ViraCheckbox } from './vira-checkbox.element.js';
|
|
8
11
|
import { ViraDateInput } from './vira-date-input.element.js';
|
|
9
12
|
import { ViraDropdown } from './vira-dropdown.element.js';
|
|
13
|
+
import { ViraIcon } from './vira-icon.element.js';
|
|
10
14
|
import { ViraInput, ViraInputType } from './vira-input.element.js';
|
|
11
15
|
import { ViraTextArea } from './vira-text-area.element.js';
|
|
12
16
|
/**
|
|
@@ -20,12 +24,18 @@ export const ViraForm = defineViraElement()({
|
|
|
20
24
|
state() {
|
|
21
25
|
return {
|
|
22
26
|
lastIsValid: false,
|
|
27
|
+
dragReorder: new DragReorderManager(),
|
|
23
28
|
};
|
|
24
29
|
},
|
|
25
30
|
events: {
|
|
26
31
|
valueChange: defineElementEvent(),
|
|
27
32
|
validChange: defineElementEvent(),
|
|
33
|
+
/** Every field key, hidden ones included, in the order the user dropped them. */
|
|
34
|
+
fieldOrderChange: defineElementEvent(),
|
|
28
35
|
},
|
|
36
|
+
testIds: [
|
|
37
|
+
'dragHandle',
|
|
38
|
+
],
|
|
29
39
|
styles: css `
|
|
30
40
|
:host {
|
|
31
41
|
display: flex;
|
|
@@ -43,6 +53,58 @@ export const ViraForm = defineViraElement()({
|
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
.drag-ghost {
|
|
57
|
+
opacity: 0.6;
|
|
58
|
+
|
|
59
|
+
& .drop-above,
|
|
60
|
+
& .drop-above > *,
|
|
61
|
+
& .drop-below,
|
|
62
|
+
& .drop-below > * {
|
|
63
|
+
box-shadow: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
& .horizontal-fields {
|
|
67
|
+
border-spacing: 0;
|
|
68
|
+
table-layout: fixed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.reorder-rows {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 10px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.reorder-row {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 4px;
|
|
82
|
+
|
|
83
|
+
& > :last-child {
|
|
84
|
+
flex-grow: 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.drag-handle {
|
|
89
|
+
display: flex;
|
|
90
|
+
cursor: grab;
|
|
91
|
+
color: ${viraFormCssVars['vira-form-placeholder-color'].value};
|
|
92
|
+
|
|
93
|
+
&:active {
|
|
94
|
+
cursor: grabbing;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
div.drop-above,
|
|
99
|
+
tr.drop-above > * {
|
|
100
|
+
box-shadow: 0 -5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
div.drop-below,
|
|
104
|
+
tr.drop-below > * {
|
|
105
|
+
box-shadow: 0 5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
|
|
106
|
+
}
|
|
107
|
+
|
|
46
108
|
.horizontal-fields {
|
|
47
109
|
width: 100%;
|
|
48
110
|
border-collapse: separate;
|
|
@@ -53,6 +115,11 @@ export const ViraForm = defineViraElement()({
|
|
|
53
115
|
padding: 0;
|
|
54
116
|
}
|
|
55
117
|
|
|
118
|
+
& .handle-cell {
|
|
119
|
+
width: 0;
|
|
120
|
+
vertical-align: middle;
|
|
121
|
+
}
|
|
122
|
+
|
|
56
123
|
& th {
|
|
57
124
|
padding: 0 8px;
|
|
58
125
|
vertical-align: middle;
|
|
@@ -80,7 +147,7 @@ export const ViraForm = defineViraElement()({
|
|
|
80
147
|
}
|
|
81
148
|
}
|
|
82
149
|
`,
|
|
83
|
-
render({ inputs, dispatch, events, state, updateState }) {
|
|
150
|
+
render({ inputs, dispatch, events, state, updateState, testIds }) {
|
|
84
151
|
const currentIsValid = areFormFieldsValid(inputs.fields);
|
|
85
152
|
if (currentIsValid !== state.lastIsValid) {
|
|
86
153
|
updateState({
|
|
@@ -92,15 +159,61 @@ export const ViraForm = defineViraElement()({
|
|
|
92
159
|
},
|
|
93
160
|
}));
|
|
94
161
|
}
|
|
95
|
-
|
|
162
|
+
const visibleKeys = getObjectTypedEntries(inputs.fields)
|
|
163
|
+
.filter(([, field,]) => !field.isHidden)
|
|
164
|
+
.map(([key]) => key);
|
|
165
|
+
function dropField({ fromIndex, toIndex }) {
|
|
166
|
+
const movedKey = visibleKeys[fromIndex];
|
|
167
|
+
if (movedKey == undefined) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
dispatch(new events.fieldOrderChange({
|
|
171
|
+
detail: moveFormFieldKey({
|
|
172
|
+
keys: Object.keys(inputs.fields),
|
|
173
|
+
movedKey,
|
|
174
|
+
beforeKey: visibleKeys[toIndex],
|
|
175
|
+
}),
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function wrapFormField({ fieldTemplate, label, key, }) {
|
|
179
|
+
const dropLine = state.dragReorder.readDropLine(visibleKeys.indexOf(key));
|
|
180
|
+
const rowClasses = classMap({
|
|
181
|
+
'reorder-row': !inputs.useHorizontalLabels,
|
|
182
|
+
'drop-above': dropLine.isAbove,
|
|
183
|
+
'drop-below': dropLine.isBelow,
|
|
184
|
+
});
|
|
185
|
+
const dragHandle = inputs.isReorderable
|
|
186
|
+
? html `
|
|
187
|
+
<div
|
|
188
|
+
class="drag-handle"
|
|
189
|
+
title="drag to reorder"
|
|
190
|
+
${testId(testIds.dragHandle)}
|
|
191
|
+
${dragReorderHandle(state.dragReorder)}
|
|
192
|
+
>
|
|
193
|
+
<${ViraIcon.assign({
|
|
194
|
+
icon: lucideIcons.GripVertical,
|
|
195
|
+
})}></${ViraIcon}>
|
|
196
|
+
</div>
|
|
197
|
+
`
|
|
198
|
+
: nothing;
|
|
96
199
|
if (inputs.useHorizontalLabels) {
|
|
97
200
|
return html `
|
|
98
|
-
<tr>
|
|
201
|
+
<tr class=${rowClasses}>
|
|
202
|
+
${inputs.isReorderable
|
|
203
|
+
? html `
|
|
204
|
+
<td class="handle-cell">${dragHandle}</td>
|
|
205
|
+
`
|
|
206
|
+
: nothing}
|
|
99
207
|
<th scope="row">${label}</th>
|
|
100
208
|
<td>${fieldTemplate}</td>
|
|
101
209
|
</tr>
|
|
102
210
|
`;
|
|
103
211
|
}
|
|
212
|
+
else if (inputs.isReorderable) {
|
|
213
|
+
return html `
|
|
214
|
+
<div class=${rowClasses}>${dragHandle} ${fieldTemplate}</div>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
104
217
|
else {
|
|
105
218
|
return fieldTemplate;
|
|
106
219
|
}
|
|
@@ -114,6 +227,7 @@ export const ViraForm = defineViraElement()({
|
|
|
114
227
|
else if (field.type === ViraFormFieldType.Checkbox) {
|
|
115
228
|
const checkboxLabel = applyRequiredLabel(field.label, showRequiredMarker);
|
|
116
229
|
return wrapFormField({
|
|
230
|
+
key,
|
|
117
231
|
label: checkboxLabel,
|
|
118
232
|
fieldTemplate: html `
|
|
119
233
|
<${ViraCheckbox.assign({
|
|
@@ -157,6 +271,7 @@ export const ViraForm = defineViraElement()({
|
|
|
157
271
|
: {};
|
|
158
272
|
if (field.type === ViraFormFieldType.Select) {
|
|
159
273
|
return wrapFormField({
|
|
274
|
+
key,
|
|
160
275
|
label,
|
|
161
276
|
fieldTemplate: html `
|
|
162
277
|
<${ViraDropdown.assign({
|
|
@@ -186,6 +301,7 @@ export const ViraForm = defineViraElement()({
|
|
|
186
301
|
}
|
|
187
302
|
else if (field.type === ViraFormFieldType.TextArea) {
|
|
188
303
|
return wrapFormField({
|
|
304
|
+
key,
|
|
189
305
|
label,
|
|
190
306
|
fieldTemplate: html `
|
|
191
307
|
<${ViraTextArea.assign({
|
|
@@ -215,6 +331,7 @@ export const ViraForm = defineViraElement()({
|
|
|
215
331
|
}
|
|
216
332
|
else if (field.type === ViraFormFieldType.Number) {
|
|
217
333
|
return wrapFormField({
|
|
334
|
+
key,
|
|
218
335
|
label,
|
|
219
336
|
fieldTemplate: html `
|
|
220
337
|
<${ViraInput.assign({
|
|
@@ -264,6 +381,7 @@ export const ViraForm = defineViraElement()({
|
|
|
264
381
|
}
|
|
265
382
|
else if (field.type === ViraFormFieldType.Date) {
|
|
266
383
|
return wrapFormField({
|
|
384
|
+
key,
|
|
267
385
|
label,
|
|
268
386
|
fieldTemplate: html `
|
|
269
387
|
<${ViraDateInput.assign({
|
|
@@ -294,6 +412,7 @@ export const ViraForm = defineViraElement()({
|
|
|
294
412
|
}
|
|
295
413
|
else {
|
|
296
414
|
return wrapFormField({
|
|
415
|
+
key,
|
|
297
416
|
label,
|
|
298
417
|
fieldTemplate: html `
|
|
299
418
|
<${ViraInput.assign({
|
|
@@ -353,14 +472,53 @@ export const ViraForm = defineViraElement()({
|
|
|
353
472
|
const formFieldsWrapper = inputs.useHorizontalLabels
|
|
354
473
|
? html `
|
|
355
474
|
<table class="horizontal-fields">
|
|
356
|
-
<tbody
|
|
475
|
+
<tbody ${dragReorderRows(state.dragReorder, dropField)}>
|
|
476
|
+
${formFieldTemplates}
|
|
477
|
+
</tbody>
|
|
357
478
|
</table>
|
|
358
479
|
`
|
|
359
|
-
:
|
|
480
|
+
: inputs.isReorderable
|
|
481
|
+
? html `
|
|
482
|
+
<div class="reorder-rows" ${dragReorderRows(state.dragReorder, dropField)}>
|
|
483
|
+
${formFieldTemplates}
|
|
484
|
+
</div>
|
|
485
|
+
`
|
|
486
|
+
: formFieldTemplates;
|
|
487
|
+
const draggedKey = state.dragReorder.value.draggedIndex == undefined
|
|
488
|
+
? undefined
|
|
489
|
+
: visibleKeys[state.dragReorder.value.draggedIndex];
|
|
490
|
+
const draggedRow = draggedKey == undefined
|
|
491
|
+
? undefined
|
|
492
|
+
: formFieldTemplates[Object.keys(inputs.fields).indexOf(draggedKey)];
|
|
493
|
+
const dragGhost = draggedRow
|
|
494
|
+
? html `
|
|
495
|
+
<div class="drag-ghost" ${dragReorderGhost(state.dragReorder)}>
|
|
496
|
+
${inputs.useHorizontalLabels
|
|
497
|
+
? html `
|
|
498
|
+
<table class="horizontal-fields">
|
|
499
|
+
<colgroup>
|
|
500
|
+
${state.dragReorder.value.draggedCellWidths.map((cellWidth) => {
|
|
501
|
+
return html `
|
|
502
|
+
<col
|
|
503
|
+
style=${css `
|
|
504
|
+
width: ${cellWidth}px;
|
|
505
|
+
`}
|
|
506
|
+
/>
|
|
507
|
+
`;
|
|
508
|
+
})}
|
|
509
|
+
</colgroup>
|
|
510
|
+
<tbody>${draggedRow}</tbody>
|
|
511
|
+
</table>
|
|
512
|
+
`
|
|
513
|
+
: draggedRow}
|
|
514
|
+
</div>
|
|
515
|
+
`
|
|
516
|
+
: nothing;
|
|
360
517
|
return html `
|
|
361
518
|
<form ${listen('submit', (event) => event.preventDefault())}>
|
|
362
519
|
${formFieldsWrapper}
|
|
363
520
|
<slot></slot>
|
|
521
|
+
${dragGhost}
|
|
364
522
|
</form>
|
|
365
523
|
`;
|
|
366
524
|
},
|
|
@@ -10,5 +10,5 @@ export declare const viraIconCssVars: import("lit-css-vars").CssVarDefinitions<{
|
|
|
10
10
|
readonly 'vira-icon-stroke-color': "currentColor";
|
|
11
11
|
/** To be used for coloring an icon's fill. */
|
|
12
12
|
readonly 'vira-icon-fill-color': "none";
|
|
13
|
-
readonly 'vira-icon-stroke-width': "
|
|
13
|
+
readonly 'vira-icon-stroke-width': "1px";
|
|
14
14
|
}>;
|
|
@@ -8,7 +8,8 @@ import { type ViraIconSvg } from './icon-svg.js';
|
|
|
8
8
|
export type LucideIconKey = keyof typeof lucideStaticImport;
|
|
9
9
|
/**
|
|
10
10
|
* All [Lucide icons](https://lucide.dev) in a format compatible with `ViraIcon`. Each icon entry
|
|
11
|
-
* can be accessed directly as a {@link ViraIconSvg}.
|
|
11
|
+
* can be accessed directly as a {@link ViraIconSvg}. Icons render at 16px instead of Lucide's
|
|
12
|
+
* default 24px; use `createSizedIcon` for other sizes.
|
|
12
13
|
*
|
|
13
14
|
* @category Icon
|
|
14
15
|
*/
|
|
@@ -5,6 +5,8 @@ const defaultLucideAttributes = {
|
|
|
5
5
|
fill: String(viraIconCssVars['vira-icon-fill-color'].value),
|
|
6
6
|
stroke: String(viraIconCssVars['vira-icon-stroke-color'].value),
|
|
7
7
|
'stroke-width': String(viraIconCssVars['vira-icon-stroke-width'].value),
|
|
8
|
+
width: '16',
|
|
9
|
+
height: '16',
|
|
8
10
|
};
|
|
9
11
|
function setSvgAttribute({ svgString, attributeName, value, }) {
|
|
10
12
|
const escapedName = attributeName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
@@ -43,7 +45,8 @@ function createLucideIconEntry(iconKey) {
|
|
|
43
45
|
const lucideIconCache = new Map();
|
|
44
46
|
/**
|
|
45
47
|
* All [Lucide icons](https://lucide.dev) in a format compatible with `ViraIcon`. Each icon entry
|
|
46
|
-
* can be accessed directly as a {@link ViraIconSvg}.
|
|
48
|
+
* can be accessed directly as a {@link ViraIconSvg}. Icons render at 16px instead of Lucide's
|
|
49
|
+
* default 24px; use `createSizedIcon` for other sizes.
|
|
47
50
|
*
|
|
48
51
|
* @category Icon
|
|
49
52
|
*/
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Observable } from 'observavir';
|
|
2
|
+
/**
|
|
3
|
+
* A finished drag from {@link DragReorderManager}.
|
|
4
|
+
*
|
|
5
|
+
* @category Reorder
|
|
6
|
+
*/
|
|
7
|
+
export type DragReorderMove = {
|
|
8
|
+
fromIndex: number;
|
|
9
|
+
/**
|
|
10
|
+
* Index, in the order before the move, of the row that the dragged row was dropped in front of.
|
|
11
|
+
* Equals the row count when it was dropped after the last row.
|
|
12
|
+
*/
|
|
13
|
+
toIndex: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The current value of a {@link DragReorderManager}.
|
|
17
|
+
*
|
|
18
|
+
* @category Reorder
|
|
19
|
+
*/
|
|
20
|
+
export type DragReorderState = {
|
|
21
|
+
draggedIndex: number | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Same meaning as {@link DragReorderMove.toIndex}. `undefined` while the pointer is where a drop
|
|
24
|
+
* would not move the row.
|
|
25
|
+
*/
|
|
26
|
+
dropIndex: number | undefined;
|
|
27
|
+
rowCount: number;
|
|
28
|
+
/**
|
|
29
|
+
* Widths of the dragged row's direct children, so a ghost of a table row can keep the real
|
|
30
|
+
* column widths. Empty while nothing is dragged.
|
|
31
|
+
*/
|
|
32
|
+
draggedCellWidths: number[];
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Drag-to-reorder for rows that the host element renders itself, so the host keeps its own markup
|
|
36
|
+
* and styles (including table rows). Store one in an element's `state` so the element re-renders as
|
|
37
|
+
* the drag progresses, then attach `dragReorderRows` to the element whose direct children are the
|
|
38
|
+
* rows, `dragReorderHandle` to each row's handle, and `dragReorderGhost` to a copy of the dragged
|
|
39
|
+
* row rendered while `value.draggedIndex` is set. The ghost is positioned over the dragged row and
|
|
40
|
+
* follows the pointer; style its opacity yourself.
|
|
41
|
+
*
|
|
42
|
+
* Nothing is reordered for you: apply each {@link DragReorderMove} to your own data.
|
|
43
|
+
*
|
|
44
|
+
* @category Reorder
|
|
45
|
+
*/
|
|
46
|
+
export declare class DragReorderManager extends Observable<DragReorderState> {
|
|
47
|
+
protected rowsElement: HTMLElement | undefined;
|
|
48
|
+
/** The callback given to `attachRows`. */
|
|
49
|
+
protected onReorder: ((move: Readonly<DragReorderMove>) => void) | undefined;
|
|
50
|
+
protected draggedRow: Element | undefined;
|
|
51
|
+
protected ghostElement: HTMLElement | undefined;
|
|
52
|
+
protected pointerStart: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
};
|
|
56
|
+
/** Removes the global listeners that move the ghost. Set only while a drag is in progress. */
|
|
57
|
+
protected stopTrackingPointer: (() => void) | undefined;
|
|
58
|
+
constructor();
|
|
59
|
+
/** Whether to draw the drop line above or below the row at `rowIndex`. */
|
|
60
|
+
readDropLine(rowIndex: number): {
|
|
61
|
+
isAbove: boolean;
|
|
62
|
+
isBelow: boolean;
|
|
63
|
+
};
|
|
64
|
+
/** Used by `dragReorderRows`. */
|
|
65
|
+
attachRows(rowsElement: HTMLElement, onReorder: (move: Readonly<DragReorderMove>) => void): void;
|
|
66
|
+
/** Used by `dragReorderGhost`. */
|
|
67
|
+
attachGhost(ghostElement: HTMLElement): void;
|
|
68
|
+
/** Used by `dragReorderHandle`. */
|
|
69
|
+
startDrag(event: DragEvent, handle: Element): void;
|
|
70
|
+
/** Used by `dragReorderRows`. */
|
|
71
|
+
dragOver(event: DragEvent): void;
|
|
72
|
+
/** Used by `dragReorderRows`. */
|
|
73
|
+
drop(event: DragEvent): void;
|
|
74
|
+
/**
|
|
75
|
+
* Used by `dragReorderHandle`. A release outside the rows still lands where the drop line was
|
|
76
|
+
* last drawn.
|
|
77
|
+
*/
|
|
78
|
+
endDrag(): void;
|
|
79
|
+
destroy(): void;
|
|
80
|
+
/** Counts the rows whose vertical midpoint is above the pointer. */
|
|
81
|
+
protected readDropIndex(clientY: number): number | undefined;
|
|
82
|
+
/** Resets to the idle state and only calls `onReorder` when `dropIndex` is defined. */
|
|
83
|
+
protected finishDrag(dropIndex: number | undefined): void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Observable } from 'observavir';
|
|
2
|
+
import { listenToGlobal } from 'typed-event-target';
|
|
3
|
+
const idleState = {
|
|
4
|
+
draggedIndex: undefined,
|
|
5
|
+
dropIndex: undefined,
|
|
6
|
+
rowCount: 0,
|
|
7
|
+
draggedCellWidths: [],
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The ghost row replaces the browser's own drag preview, which only shows the handle that the drag
|
|
11
|
+
* started on.
|
|
12
|
+
*/
|
|
13
|
+
function setEmptyDragImage(dataTransfer) {
|
|
14
|
+
const emptyImage = document.createElement('div');
|
|
15
|
+
emptyImage.style.position = 'fixed';
|
|
16
|
+
emptyImage.style.top = '0';
|
|
17
|
+
emptyImage.style.left = '0';
|
|
18
|
+
emptyImage.style.width = '1px';
|
|
19
|
+
emptyImage.style.height = '1px';
|
|
20
|
+
emptyImage.style.pointerEvents = 'none';
|
|
21
|
+
document.body.append(emptyImage);
|
|
22
|
+
dataTransfer.setDragImage(emptyImage, 0, 0);
|
|
23
|
+
globalThis.requestAnimationFrame(() => emptyImage.remove());
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Drag-to-reorder for rows that the host element renders itself, so the host keeps its own markup
|
|
27
|
+
* and styles (including table rows). Store one in an element's `state` so the element re-renders as
|
|
28
|
+
* the drag progresses, then attach `dragReorderRows` to the element whose direct children are the
|
|
29
|
+
* rows, `dragReorderHandle` to each row's handle, and `dragReorderGhost` to a copy of the dragged
|
|
30
|
+
* row rendered while `value.draggedIndex` is set. The ghost is positioned over the dragged row and
|
|
31
|
+
* follows the pointer; style its opacity yourself.
|
|
32
|
+
*
|
|
33
|
+
* Nothing is reordered for you: apply each {@link DragReorderMove} to your own data.
|
|
34
|
+
*
|
|
35
|
+
* @category Reorder
|
|
36
|
+
*/
|
|
37
|
+
export class DragReorderManager extends Observable {
|
|
38
|
+
rowsElement;
|
|
39
|
+
/** The callback given to `attachRows`. */
|
|
40
|
+
onReorder;
|
|
41
|
+
draggedRow;
|
|
42
|
+
ghostElement;
|
|
43
|
+
pointerStart = {
|
|
44
|
+
x: 0,
|
|
45
|
+
y: 0,
|
|
46
|
+
};
|
|
47
|
+
/** Removes the global listeners that move the ghost. Set only while a drag is in progress. */
|
|
48
|
+
stopTrackingPointer;
|
|
49
|
+
constructor() {
|
|
50
|
+
super({
|
|
51
|
+
defaultValue: idleState,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Whether to draw the drop line above or below the row at `rowIndex`. */
|
|
55
|
+
readDropLine(rowIndex) {
|
|
56
|
+
return {
|
|
57
|
+
isAbove: this.value.dropIndex === rowIndex,
|
|
58
|
+
isBelow: rowIndex === this.value.rowCount - 1 &&
|
|
59
|
+
this.value.dropIndex === this.value.rowCount,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Used by `dragReorderRows`. */
|
|
63
|
+
attachRows(rowsElement, onReorder) {
|
|
64
|
+
this.rowsElement = rowsElement;
|
|
65
|
+
this.onReorder = onReorder;
|
|
66
|
+
}
|
|
67
|
+
/** Used by `dragReorderGhost`. */
|
|
68
|
+
attachGhost(ghostElement) {
|
|
69
|
+
if (ghostElement === this.ghostElement) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.ghostElement = ghostElement;
|
|
73
|
+
ghostElement.style.position = 'absolute';
|
|
74
|
+
ghostElement.style.boxSizing = 'border-box';
|
|
75
|
+
ghostElement.style.margin = '0';
|
|
76
|
+
ghostElement.style.pointerEvents = 'none';
|
|
77
|
+
ghostElement.style.visibility = 'hidden';
|
|
78
|
+
/** The ghost is not laid out until after the current render. */
|
|
79
|
+
globalThis.requestAnimationFrame(() => {
|
|
80
|
+
if (ghostElement !== this.ghostElement || !this.draggedRow) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const rowRect = this.draggedRow.getBoundingClientRect();
|
|
84
|
+
const translate = ghostElement.style.translate;
|
|
85
|
+
ghostElement.style.translate = '';
|
|
86
|
+
ghostElement.style.left = '0';
|
|
87
|
+
ghostElement.style.top = '0';
|
|
88
|
+
ghostElement.style.width = `${rowRect.width}px`;
|
|
89
|
+
/**
|
|
90
|
+
* Measuring from a zero offset works whatever the ghost's containing block is, so the
|
|
91
|
+
* host does not have to position anything.
|
|
92
|
+
*/
|
|
93
|
+
const ghostRect = ghostElement.getBoundingClientRect();
|
|
94
|
+
ghostElement.style.left = `${rowRect.left - ghostRect.left}px`;
|
|
95
|
+
ghostElement.style.top = `${rowRect.top - ghostRect.top}px`;
|
|
96
|
+
ghostElement.style.translate = translate;
|
|
97
|
+
ghostElement.style.visibility = '';
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** Used by `dragReorderHandle`. */
|
|
101
|
+
startDrag(event, handle) {
|
|
102
|
+
const rows = [...(this.rowsElement?.children || [])];
|
|
103
|
+
const draggedIndex = rows.findIndex((row) => row.contains(handle));
|
|
104
|
+
const draggedRow = rows[draggedIndex];
|
|
105
|
+
if (!draggedRow) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.stopTrackingPointer?.();
|
|
109
|
+
this.draggedRow = draggedRow;
|
|
110
|
+
this.pointerStart = {
|
|
111
|
+
x: event.clientX,
|
|
112
|
+
y: event.clientY,
|
|
113
|
+
};
|
|
114
|
+
if (event.dataTransfer) {
|
|
115
|
+
event.dataTransfer.setData('text/plain', '');
|
|
116
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
117
|
+
setEmptyDragImage(event.dataTransfer);
|
|
118
|
+
}
|
|
119
|
+
const stopListeners = [
|
|
120
|
+
listenToGlobal('dragover', (dragEvent) => {
|
|
121
|
+
/**
|
|
122
|
+
* Accepting the drop everywhere stops the browser from animating its drag preview
|
|
123
|
+
* back to the start when the pointer is released.
|
|
124
|
+
*/
|
|
125
|
+
dragEvent.preventDefault();
|
|
126
|
+
if (this.ghostElement) {
|
|
127
|
+
this.ghostElement.style.translate = [
|
|
128
|
+
`${dragEvent.clientX - this.pointerStart.x}px`,
|
|
129
|
+
`${dragEvent.clientY - this.pointerStart.y}px`,
|
|
130
|
+
].join(' ');
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
listenToGlobal('drop', (dropEvent) => dropEvent.preventDefault()),
|
|
134
|
+
];
|
|
135
|
+
this.stopTrackingPointer = () => {
|
|
136
|
+
stopListeners.forEach((stopListener) => stopListener());
|
|
137
|
+
};
|
|
138
|
+
this.setValue({
|
|
139
|
+
draggedIndex,
|
|
140
|
+
dropIndex: undefined,
|
|
141
|
+
rowCount: rows.length,
|
|
142
|
+
draggedCellWidths: [...draggedRow.children].map((cell) => cell.getBoundingClientRect().width),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Used by `dragReorderRows`. */
|
|
146
|
+
dragOver(event) {
|
|
147
|
+
if (this.value.draggedIndex == undefined) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
if (event.dataTransfer) {
|
|
152
|
+
event.dataTransfer.dropEffect = 'move';
|
|
153
|
+
}
|
|
154
|
+
const dropIndex = this.readDropIndex(event.clientY);
|
|
155
|
+
if (dropIndex !== this.value.dropIndex) {
|
|
156
|
+
this.setValue({
|
|
157
|
+
...this.value,
|
|
158
|
+
dropIndex,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Used by `dragReorderRows`. */
|
|
163
|
+
drop(event) {
|
|
164
|
+
if (this.value.draggedIndex == undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
this.finishDrag(this.readDropIndex(event.clientY));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Used by `dragReorderHandle`. A release outside the rows still lands where the drop line was
|
|
172
|
+
* last drawn.
|
|
173
|
+
*/
|
|
174
|
+
endDrag() {
|
|
175
|
+
this.finishDrag(this.value.dropIndex);
|
|
176
|
+
}
|
|
177
|
+
destroy() {
|
|
178
|
+
this.stopTrackingPointer?.();
|
|
179
|
+
super.destroy();
|
|
180
|
+
}
|
|
181
|
+
/** Counts the rows whose vertical midpoint is above the pointer. */
|
|
182
|
+
readDropIndex(clientY) {
|
|
183
|
+
const draggedIndex = this.value.draggedIndex;
|
|
184
|
+
const dropIndex = [...(this.rowsElement?.children || [])].filter((row) => {
|
|
185
|
+
const rect = row.getBoundingClientRect();
|
|
186
|
+
return clientY >= rect.top + rect.height / 2;
|
|
187
|
+
}).length;
|
|
188
|
+
/** Dropping a row directly above or below itself would not move it. */
|
|
189
|
+
return dropIndex === draggedIndex ||
|
|
190
|
+
draggedIndex == undefined ||
|
|
191
|
+
dropIndex === draggedIndex + 1
|
|
192
|
+
? undefined
|
|
193
|
+
: dropIndex;
|
|
194
|
+
}
|
|
195
|
+
/** Resets to the idle state and only calls `onReorder` when `dropIndex` is defined. */
|
|
196
|
+
finishDrag(dropIndex) {
|
|
197
|
+
const fromIndex = this.value.draggedIndex;
|
|
198
|
+
this.stopTrackingPointer?.();
|
|
199
|
+
this.stopTrackingPointer = undefined;
|
|
200
|
+
this.draggedRow = undefined;
|
|
201
|
+
this.ghostElement = undefined;
|
|
202
|
+
if (fromIndex == undefined) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.setValue(idleState);
|
|
206
|
+
if (dropIndex != undefined) {
|
|
207
|
+
this.onReorder?.({
|
|
208
|
+
fromIndex,
|
|
209
|
+
toIndex: dropIndex,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/dist/util/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './anchored-popover.js';
|
|
2
2
|
export * from './define-table.js';
|
|
3
3
|
export * from './define-vira-element.js';
|
|
4
|
+
export * from './drag-reorder-manager.js';
|
|
4
5
|
export * from './dynamic-element.js';
|
|
5
6
|
export * from './fuzzy-match.js';
|
|
6
7
|
export * from './menu-helpers.js';
|
package/dist/util/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './anchored-popover.js';
|
|
2
2
|
export * from './define-table.js';
|
|
3
3
|
export * from './define-vira-element.js';
|
|
4
|
+
export * from './drag-reorder-manager.js';
|
|
4
5
|
export * from './dynamic-element.js';
|
|
5
6
|
export * from './fuzzy-match.js';
|
|
6
7
|
export * from './menu-helpers.js';
|
|
@@ -138,3 +138,15 @@ export declare function applyRequiredLabel(label: string | HtmlInterpolation | u
|
|
|
138
138
|
* @category Internal
|
|
139
139
|
*/
|
|
140
140
|
export declare function areFormFieldsValid(formFields: Readonly<ViraFormFields>): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Moves one field key in front of another, keeping every other key (including hidden fields) in
|
|
143
|
+
* place. Used by `ViraForm` when its fields are reordered.
|
|
144
|
+
*
|
|
145
|
+
* @category Internal
|
|
146
|
+
*/
|
|
147
|
+
export declare function moveFormFieldKey({ keys, movedKey, beforeKey, }: Readonly<{
|
|
148
|
+
keys: ReadonlyArray<string>;
|
|
149
|
+
movedKey: string;
|
|
150
|
+
/** The key that `movedKey` lands in front of. `undefined` moves it to the end. */
|
|
151
|
+
beforeKey: string | undefined;
|
|
152
|
+
}>): string[];
|
|
@@ -68,3 +68,19 @@ export function areFormFieldsValid(formFields) {
|
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Moves one field key in front of another, keeping every other key (including hidden fields) in
|
|
73
|
+
* place. Used by `ViraForm` when its fields are reordered.
|
|
74
|
+
*
|
|
75
|
+
* @category Internal
|
|
76
|
+
*/
|
|
77
|
+
export function moveFormFieldKey({ keys, movedKey, beforeKey, }) {
|
|
78
|
+
const remainingKeys = keys.filter((key) => key !== movedKey);
|
|
79
|
+
const insertIndex = beforeKey == undefined ? -1 : remainingKeys.indexOf(beforeKey);
|
|
80
|
+
return insertIndex === -1
|
|
81
|
+
? [
|
|
82
|
+
...remainingKeys,
|
|
83
|
+
movedKey,
|
|
84
|
+
]
|
|
85
|
+
: remainingKeys.toSpliced(insertIndex, 0, movedKey);
|
|
86
|
+
}
|