vira 33.2.0 → 33.4.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 +17 -1
- package/dist/elements/vira-form.element.js +187 -7
- package/dist/elements/vira-json-form.element.js +4 -4
- 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/styles/shadows.d.ts +2 -2
- package/dist/styles/shadows.js +2 -2
- 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/shared-text-input-logic.js +3 -4
- package/dist/util/vira-form-fields.d.ts +12 -0
- package/dist/util/vira-form-fields.js +16 -0
- package/dist/util/vira-json-schema.js +1 -4
- package/package.json +12 -12
|
@@ -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,10 @@ export declare const ViraForm: import("element-vir").DeclarativeElementDefinitio
|
|
|
53
63
|
validChange: import("element-vir").DefineElementEvent<{
|
|
54
64
|
allFieldsAreValid: boolean;
|
|
55
65
|
}>;
|
|
56
|
-
|
|
66
|
+
/** Emitted when focus leaves a field entirely, not when it moves within the field. */
|
|
67
|
+
fieldBlur: import("element-vir").DefineElementEvent<{
|
|
68
|
+
key: string;
|
|
69
|
+
} & ViraFormField>;
|
|
70
|
+
/** Every field key, hidden ones included, in the order the user dropped them. */
|
|
71
|
+
fieldOrderChange: import("element-vir").DefineElementEvent<string[]>;
|
|
72
|
+
}, "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,20 @@ 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
|
+
/** Emitted when focus leaves a field entirely, not when it moves within the field. */
|
|
34
|
+
fieldBlur: defineElementEvent(),
|
|
35
|
+
/** Every field key, hidden ones included, in the order the user dropped them. */
|
|
36
|
+
fieldOrderChange: defineElementEvent(),
|
|
28
37
|
},
|
|
38
|
+
testIds: [
|
|
39
|
+
'dragHandle',
|
|
40
|
+
],
|
|
29
41
|
styles: css `
|
|
30
42
|
:host {
|
|
31
43
|
display: flex;
|
|
@@ -43,6 +55,58 @@ export const ViraForm = defineViraElement()({
|
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
.drag-ghost {
|
|
59
|
+
opacity: 0.6;
|
|
60
|
+
|
|
61
|
+
& .drop-above,
|
|
62
|
+
& .drop-above > *,
|
|
63
|
+
& .drop-below,
|
|
64
|
+
& .drop-below > * {
|
|
65
|
+
box-shadow: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
& .horizontal-fields {
|
|
69
|
+
border-spacing: 0;
|
|
70
|
+
table-layout: fixed;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.reorder-rows {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 10px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.reorder-row {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: 4px;
|
|
84
|
+
|
|
85
|
+
& > :last-child {
|
|
86
|
+
flex-grow: 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.drag-handle {
|
|
91
|
+
display: flex;
|
|
92
|
+
cursor: grab;
|
|
93
|
+
color: ${viraFormCssVars['vira-form-placeholder-color'].value};
|
|
94
|
+
|
|
95
|
+
&:active {
|
|
96
|
+
cursor: grabbing;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
div.drop-above,
|
|
101
|
+
tr.drop-above > * {
|
|
102
|
+
box-shadow: 0 -5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
div.drop-below,
|
|
106
|
+
tr.drop-below > * {
|
|
107
|
+
box-shadow: 0 5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
|
|
108
|
+
}
|
|
109
|
+
|
|
46
110
|
.horizontal-fields {
|
|
47
111
|
width: 100%;
|
|
48
112
|
border-collapse: separate;
|
|
@@ -53,6 +117,11 @@ export const ViraForm = defineViraElement()({
|
|
|
53
117
|
padding: 0;
|
|
54
118
|
}
|
|
55
119
|
|
|
120
|
+
& .handle-cell {
|
|
121
|
+
width: 0;
|
|
122
|
+
vertical-align: middle;
|
|
123
|
+
}
|
|
124
|
+
|
|
56
125
|
& th {
|
|
57
126
|
padding: 0 8px;
|
|
58
127
|
vertical-align: middle;
|
|
@@ -80,7 +149,7 @@ export const ViraForm = defineViraElement()({
|
|
|
80
149
|
}
|
|
81
150
|
}
|
|
82
151
|
`,
|
|
83
|
-
render({ inputs, dispatch, events, state, updateState }) {
|
|
152
|
+
render({ inputs, dispatch, events, state, updateState, testIds }) {
|
|
84
153
|
const currentIsValid = areFormFieldsValid(inputs.fields);
|
|
85
154
|
if (currentIsValid !== state.lastIsValid) {
|
|
86
155
|
updateState({
|
|
@@ -92,15 +161,75 @@ export const ViraForm = defineViraElement()({
|
|
|
92
161
|
},
|
|
93
162
|
}));
|
|
94
163
|
}
|
|
95
|
-
|
|
164
|
+
const visibleKeys = getObjectTypedEntries(inputs.fields)
|
|
165
|
+
.filter(([, field,]) => !field.isHidden)
|
|
166
|
+
.map(([key]) => key);
|
|
167
|
+
function dropField({ fromIndex, toIndex }) {
|
|
168
|
+
const movedKey = visibleKeys[fromIndex];
|
|
169
|
+
if (movedKey == undefined) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
dispatch(new events.fieldOrderChange({
|
|
173
|
+
detail: moveFormFieldKey({
|
|
174
|
+
keys: Object.keys(inputs.fields),
|
|
175
|
+
movedKey,
|
|
176
|
+
beforeKey: visibleKeys[toIndex],
|
|
177
|
+
}),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
function listenToFieldBlur(key, field) {
|
|
181
|
+
/**
|
|
182
|
+
* `blur` doesn't bubble out of the field's shadow root, but `focusout` does, and the
|
|
183
|
+
* browser skips it on the field's host when focus moves between elements inside it.
|
|
184
|
+
*/
|
|
185
|
+
return listen('focusout', () => {
|
|
186
|
+
dispatch(new events.fieldBlur({
|
|
187
|
+
detail: {
|
|
188
|
+
key,
|
|
189
|
+
...field,
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function wrapFormField({ fieldTemplate, label, key, }) {
|
|
195
|
+
const dropLine = state.dragReorder.readDropLine(visibleKeys.indexOf(key));
|
|
196
|
+
const rowClasses = classMap({
|
|
197
|
+
'reorder-row': !inputs.useHorizontalLabels,
|
|
198
|
+
'drop-above': dropLine.isAbove,
|
|
199
|
+
'drop-below': dropLine.isBelow,
|
|
200
|
+
});
|
|
201
|
+
const dragHandle = inputs.isReorderable
|
|
202
|
+
? html `
|
|
203
|
+
<div
|
|
204
|
+
class="drag-handle"
|
|
205
|
+
title="drag to reorder"
|
|
206
|
+
${testId(testIds.dragHandle)}
|
|
207
|
+
${dragReorderHandle(state.dragReorder)}
|
|
208
|
+
>
|
|
209
|
+
<${ViraIcon.assign({
|
|
210
|
+
icon: lucideIcons.GripVertical,
|
|
211
|
+
})}></${ViraIcon}>
|
|
212
|
+
</div>
|
|
213
|
+
`
|
|
214
|
+
: nothing;
|
|
96
215
|
if (inputs.useHorizontalLabels) {
|
|
97
216
|
return html `
|
|
98
|
-
<tr>
|
|
217
|
+
<tr class=${rowClasses}>
|
|
218
|
+
${inputs.isReorderable
|
|
219
|
+
? html `
|
|
220
|
+
<td class="handle-cell">${dragHandle}</td>
|
|
221
|
+
`
|
|
222
|
+
: nothing}
|
|
99
223
|
<th scope="row">${label}</th>
|
|
100
224
|
<td>${fieldTemplate}</td>
|
|
101
225
|
</tr>
|
|
102
226
|
`;
|
|
103
227
|
}
|
|
228
|
+
else if (inputs.isReorderable) {
|
|
229
|
+
return html `
|
|
230
|
+
<div class=${rowClasses}>${dragHandle} ${fieldTemplate}</div>
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
104
233
|
else {
|
|
105
234
|
return fieldTemplate;
|
|
106
235
|
}
|
|
@@ -114,6 +243,7 @@ export const ViraForm = defineViraElement()({
|
|
|
114
243
|
else if (field.type === ViraFormFieldType.Checkbox) {
|
|
115
244
|
const checkboxLabel = applyRequiredLabel(field.label, showRequiredMarker);
|
|
116
245
|
return wrapFormField({
|
|
246
|
+
key,
|
|
117
247
|
label: checkboxLabel,
|
|
118
248
|
fieldTemplate: html `
|
|
119
249
|
<${ViraCheckbox.assign({
|
|
@@ -125,6 +255,7 @@ export const ViraForm = defineViraElement()({
|
|
|
125
255
|
fillWhenUnchecked: field.fillWhenUnchecked,
|
|
126
256
|
})}
|
|
127
257
|
${field.testId ? testId(field.testId) : nothing}
|
|
258
|
+
${listenToFieldBlur(key, field)}
|
|
128
259
|
${listen(ViraCheckbox.events.valueChange, (event) => {
|
|
129
260
|
dispatch(new events.valueChange({
|
|
130
261
|
detail: {
|
|
@@ -157,6 +288,7 @@ export const ViraForm = defineViraElement()({
|
|
|
157
288
|
: {};
|
|
158
289
|
if (field.type === ViraFormFieldType.Select) {
|
|
159
290
|
return wrapFormField({
|
|
291
|
+
key,
|
|
160
292
|
label,
|
|
161
293
|
fieldTemplate: html `
|
|
162
294
|
<${ViraDropdown.assign({
|
|
@@ -171,6 +303,7 @@ export const ViraForm = defineViraElement()({
|
|
|
171
303
|
attributePassthrough: horizontalLabelAttributes,
|
|
172
304
|
})}
|
|
173
305
|
${field.testId ? testId(field.testId) : nothing}
|
|
306
|
+
${listenToFieldBlur(key, field)}
|
|
174
307
|
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
175
308
|
dispatch(new events.valueChange({
|
|
176
309
|
detail: {
|
|
@@ -186,6 +319,7 @@ export const ViraForm = defineViraElement()({
|
|
|
186
319
|
}
|
|
187
320
|
else if (field.type === ViraFormFieldType.TextArea) {
|
|
188
321
|
return wrapFormField({
|
|
322
|
+
key,
|
|
189
323
|
label,
|
|
190
324
|
fieldTemplate: html `
|
|
191
325
|
<${ViraTextArea.assign({
|
|
@@ -200,6 +334,7 @@ export const ViraForm = defineViraElement()({
|
|
|
200
334
|
attributePassthrough: horizontalLabelAttributes,
|
|
201
335
|
})}
|
|
202
336
|
${field.testId ? testId(field.testId) : nothing}
|
|
337
|
+
${listenToFieldBlur(key, field)}
|
|
203
338
|
${listen(ViraTextArea.events.valueChange, (event) => {
|
|
204
339
|
dispatch(new events.valueChange({
|
|
205
340
|
detail: {
|
|
@@ -215,6 +350,7 @@ export const ViraForm = defineViraElement()({
|
|
|
215
350
|
}
|
|
216
351
|
else if (field.type === ViraFormFieldType.Number) {
|
|
217
352
|
return wrapFormField({
|
|
353
|
+
key,
|
|
218
354
|
label,
|
|
219
355
|
fieldTemplate: html `
|
|
220
356
|
<${ViraInput.assign({
|
|
@@ -248,6 +384,7 @@ export const ViraForm = defineViraElement()({
|
|
|
248
384
|
},
|
|
249
385
|
})}
|
|
250
386
|
${field.testId ? testId(field.testId) : nothing}
|
|
387
|
+
${listenToFieldBlur(key, field)}
|
|
251
388
|
${listen(ViraInput.events.valueChange, (event) => {
|
|
252
389
|
const numericValue = event.detail === '' ? undefined : Number(event.detail);
|
|
253
390
|
dispatch(new events.valueChange({
|
|
@@ -264,6 +401,7 @@ export const ViraForm = defineViraElement()({
|
|
|
264
401
|
}
|
|
265
402
|
else if (field.type === ViraFormFieldType.Date) {
|
|
266
403
|
return wrapFormField({
|
|
404
|
+
key,
|
|
267
405
|
label,
|
|
268
406
|
fieldTemplate: html `
|
|
269
407
|
<${ViraDateInput.assign({
|
|
@@ -279,6 +417,7 @@ export const ViraForm = defineViraElement()({
|
|
|
279
417
|
label: childLabel,
|
|
280
418
|
})}
|
|
281
419
|
${field.testId ? testId(field.testId) : nothing}
|
|
420
|
+
${listenToFieldBlur(key, field)}
|
|
282
421
|
${listen(ViraDateInput.events.valueChange, (event) => {
|
|
283
422
|
dispatch(new events.valueChange({
|
|
284
423
|
detail: {
|
|
@@ -294,6 +433,7 @@ export const ViraForm = defineViraElement()({
|
|
|
294
433
|
}
|
|
295
434
|
else {
|
|
296
435
|
return wrapFormField({
|
|
436
|
+
key,
|
|
297
437
|
label,
|
|
298
438
|
fieldTemplate: html `
|
|
299
439
|
<${ViraInput.assign({
|
|
@@ -336,6 +476,7 @@ export const ViraForm = defineViraElement()({
|
|
|
336
476
|
: ViraInputType.Default,
|
|
337
477
|
})}
|
|
338
478
|
${field.testId ? testId(field.testId) : nothing}
|
|
479
|
+
${listenToFieldBlur(key, field)}
|
|
339
480
|
${listen(ViraInput.events.valueChange, (event) => {
|
|
340
481
|
dispatch(new events.valueChange({
|
|
341
482
|
detail: {
|
|
@@ -353,14 +494,53 @@ export const ViraForm = defineViraElement()({
|
|
|
353
494
|
const formFieldsWrapper = inputs.useHorizontalLabels
|
|
354
495
|
? html `
|
|
355
496
|
<table class="horizontal-fields">
|
|
356
|
-
<tbody
|
|
497
|
+
<tbody ${dragReorderRows(state.dragReorder, dropField)}>
|
|
498
|
+
${formFieldTemplates}
|
|
499
|
+
</tbody>
|
|
357
500
|
</table>
|
|
358
501
|
`
|
|
359
|
-
:
|
|
502
|
+
: inputs.isReorderable
|
|
503
|
+
? html `
|
|
504
|
+
<div class="reorder-rows" ${dragReorderRows(state.dragReorder, dropField)}>
|
|
505
|
+
${formFieldTemplates}
|
|
506
|
+
</div>
|
|
507
|
+
`
|
|
508
|
+
: formFieldTemplates;
|
|
509
|
+
const draggedKey = state.dragReorder.value.draggedIndex == undefined
|
|
510
|
+
? undefined
|
|
511
|
+
: visibleKeys[state.dragReorder.value.draggedIndex];
|
|
512
|
+
const draggedRow = draggedKey == undefined
|
|
513
|
+
? undefined
|
|
514
|
+
: formFieldTemplates[Object.keys(inputs.fields).indexOf(draggedKey)];
|
|
515
|
+
const dragGhost = draggedRow
|
|
516
|
+
? html `
|
|
517
|
+
<div class="drag-ghost" ${dragReorderGhost(state.dragReorder)}>
|
|
518
|
+
${inputs.useHorizontalLabels
|
|
519
|
+
? html `
|
|
520
|
+
<table class="horizontal-fields">
|
|
521
|
+
<colgroup>
|
|
522
|
+
${state.dragReorder.value.draggedCellWidths.map((cellWidth) => {
|
|
523
|
+
return html `
|
|
524
|
+
<col
|
|
525
|
+
style=${css `
|
|
526
|
+
width: ${cellWidth}px;
|
|
527
|
+
`}
|
|
528
|
+
/>
|
|
529
|
+
`;
|
|
530
|
+
})}
|
|
531
|
+
</colgroup>
|
|
532
|
+
<tbody>${draggedRow}</tbody>
|
|
533
|
+
</table>
|
|
534
|
+
`
|
|
535
|
+
: draggedRow}
|
|
536
|
+
</div>
|
|
537
|
+
`
|
|
538
|
+
: nothing;
|
|
360
539
|
return html `
|
|
361
540
|
<form ${listen('submit', (event) => event.preventDefault())}>
|
|
362
541
|
${formFieldsWrapper}
|
|
363
542
|
<slot></slot>
|
|
543
|
+
${dragGhost}
|
|
364
544
|
</form>
|
|
365
545
|
`;
|
|
366
546
|
},
|
|
@@ -472,7 +472,7 @@ export const ViraJsonForm = defineViraElement()({
|
|
|
472
472
|
selected: toSelectedValues(selectedType),
|
|
473
473
|
})}
|
|
474
474
|
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
475
|
-
setPendingType(pathKey, assertWrap.
|
|
475
|
+
setPendingType(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonType));
|
|
476
476
|
})}
|
|
477
477
|
></${ViraDropdown}>
|
|
478
478
|
${renderPlusButton({
|
|
@@ -536,7 +536,7 @@ export const ViraJsonForm = defineViraElement()({
|
|
|
536
536
|
selected: toSelectedValues(selectedType),
|
|
537
537
|
})}
|
|
538
538
|
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
539
|
-
setPendingType(pathKey, assertWrap.
|
|
539
|
+
setPendingType(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonType));
|
|
540
540
|
})}
|
|
541
541
|
></${ViraDropdown}>
|
|
542
542
|
${renderPlusButton({
|
|
@@ -849,7 +849,7 @@ export const ViraJsonForm = defineViraElement()({
|
|
|
849
849
|
})}
|
|
850
850
|
title="Choose from options or enter a custom value"
|
|
851
851
|
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
852
|
-
setStringMode(pathKey, assertWrap.
|
|
852
|
+
setStringMode(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonStringMode));
|
|
853
853
|
})}
|
|
854
854
|
></${ViraDropdown}>
|
|
855
855
|
</div>
|
|
@@ -904,7 +904,7 @@ export const ViraJsonForm = defineViraElement()({
|
|
|
904
904
|
})}
|
|
905
905
|
title="Change type"
|
|
906
906
|
${listen(ViraDropdown.events.selectedValuesChange, (event) => {
|
|
907
|
-
const newType = assertWrap.
|
|
907
|
+
const newType = assertWrap.isEnumValue(event.detail[0], ViraJsonType);
|
|
908
908
|
emitReplaceAt(path, createDefaultForJsonType(newType));
|
|
909
909
|
})}
|
|
910
910
|
></${ViraDropdown}>
|
|
@@ -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
|
*/
|
package/dist/styles/shadows.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ import { type CSSResult } from 'element-vir';
|
|
|
5
5
|
* @category Styles
|
|
6
6
|
*/
|
|
7
7
|
export declare const shadowCssVars: import("lit-css-vars").CssVarDefinitions<{
|
|
8
|
-
readonly 'menu-shadow-color': "
|
|
9
|
-
readonly 'modal-shadow-color': "
|
|
8
|
+
readonly 'menu-shadow-color': "rgba(0, 0, 0, 0.11)";
|
|
9
|
+
readonly 'modal-shadow-color': "rgba(0, 0, 0, 0.69)";
|
|
10
10
|
}>;
|
|
11
11
|
/**
|
|
12
12
|
* CSS chunks for default Vira shadow styles.
|
package/dist/styles/shadows.js
CHANGED
|
@@ -6,8 +6,8 @@ import { defineCssVars } from 'lit-css-vars';
|
|
|
6
6
|
* @category Styles
|
|
7
7
|
*/
|
|
8
8
|
export const shadowCssVars = defineCssVars({
|
|
9
|
-
'menu-shadow-color': '
|
|
10
|
-
'modal-shadow-color': '
|
|
9
|
+
'menu-shadow-color': 'rgba(0, 0, 0, 0.11)',
|
|
10
|
+
'modal-shadow-color': 'rgba(0, 0, 0, 0.69)',
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
13
13
|
* CSS chunks for default Vira shadow styles.
|
|
@@ -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';
|
|
@@ -21,12 +21,11 @@ function doesMatch({ input, matcher }) {
|
|
|
21
21
|
}
|
|
22
22
|
function isAllowed({ value: rawValue, allowed, blocked }) {
|
|
23
23
|
const value = String(rawValue);
|
|
24
|
-
const isAllowedCharacter = allowed
|
|
25
|
-
|
|
24
|
+
const isAllowedCharacter = !allowed ||
|
|
25
|
+
doesMatch({
|
|
26
26
|
input: value,
|
|
27
27
|
matcher: allowed,
|
|
28
|
-
})
|
|
29
|
-
: true;
|
|
28
|
+
});
|
|
30
29
|
const isBlockedCharacter = blocked
|
|
31
30
|
? doesMatch({
|
|
32
31
|
input: value,
|
|
@@ -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
|
+
}
|
|
@@ -83,10 +83,7 @@ export function normalizeSchema(schema) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
function jsonTypeFromSchemaType(type) {
|
|
86
|
-
|
|
87
|
-
return type;
|
|
88
|
-
}
|
|
89
|
-
return undefined;
|
|
86
|
+
return check.isEnumValue(type, ViraJsonType) ? type : undefined;
|
|
90
87
|
}
|
|
91
88
|
/** @category Internal */
|
|
92
89
|
export function createResolveContext(schema) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vira",
|
|
3
|
-
"version": "33.
|
|
3
|
+
"version": "33.4.0",
|
|
4
4
|
"description": "A simple and highly versatile design system using element-vir.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design",
|
|
@@ -38,24 +38,24 @@
|
|
|
38
38
|
"test:docs": "virmator docs check"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@augment-vir/assert": "^32.
|
|
42
|
-
"@augment-vir/common": "^32.
|
|
43
|
-
"@augment-vir/web": "^32.
|
|
41
|
+
"@augment-vir/assert": "^32.5.0",
|
|
42
|
+
"@augment-vir/common": "^32.5.0",
|
|
43
|
+
"@augment-vir/web": "^32.5.0",
|
|
44
44
|
"@electrovir/color": "^2.0.0",
|
|
45
45
|
"@electrovir/local-storage-client": "^0.1.1",
|
|
46
|
-
"date-vir": "^9.
|
|
46
|
+
"date-vir": "^9.4.0",
|
|
47
47
|
"device-navigation": "^5.0.0",
|
|
48
48
|
"json-schema-to-ts": "^3.1.1",
|
|
49
49
|
"lit": "^3.3.3",
|
|
50
50
|
"lit-css-vars": "^3.6.3",
|
|
51
|
-
"object-shape-tester": "^6.15.
|
|
52
|
-
"observavir": "^2.
|
|
51
|
+
"object-shape-tester": "^6.15.1",
|
|
52
|
+
"observavir": "^2.4.2",
|
|
53
53
|
"page-active": "^1.0.4",
|
|
54
54
|
"spa-router-vir": "^6.8.0",
|
|
55
55
|
"typed-event-target": "^4.3.4"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@augment-vir/test": "^32.
|
|
58
|
+
"@augment-vir/test": "^32.5.0",
|
|
59
59
|
"@web/dev-server-esbuild": "^2.0.0",
|
|
60
60
|
"@web/test-runner": "^1.0.0",
|
|
61
61
|
"@web/test-runner-commands": "^1.0.1",
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
"@web/test-runner-visual-regression": "^1.0.1",
|
|
64
64
|
"esbuild": "^0.28.2",
|
|
65
65
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
66
|
-
"lucide-static": "^1.
|
|
67
|
-
"markdown-code-example-inserter": "^3.
|
|
68
|
-
"theme-vir": "^29.0.
|
|
66
|
+
"lucide-static": "^1.54.0",
|
|
67
|
+
"markdown-code-example-inserter": "^3.1.1",
|
|
68
|
+
"theme-vir": "^29.0.1",
|
|
69
69
|
"typedoc": "^0.28.20",
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
|
-
"vite": "^8.3.
|
|
71
|
+
"vite": "^8.3.4",
|
|
72
72
|
"vite-tsconfig-paths": "^6.1.1"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|