Skip to content

Commit a7eaf00

Browse files
committed
feat: drag and drop for array items
1 parent b15e8d3 commit a7eaf00

File tree

3 files changed

+179
-61
lines changed

3 files changed

+179
-61
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,8 @@ Actual **features flags** list:
737737
- Tests, browser based (due to the WC nature).
738738
- Tests, tests, even more tests in the field to reveal shortcomings.
739739
- Support for other UI library (MWC? FAST?)
740+
- Drag and drop for array items, using native API.
741+
- Autofocuses (for added array item, etc.)
740742
-
741743
- Have an idea? [Discussions are open](https://github.com/json-schema-form-element/core/discussions)!
742744

lib/components/array.ts

Lines changed: 119 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable max-lines */
2+
/* eslint-disable arrow-body-style */
13
import type { JSONSchema7 } from 'json-schema';
24
import { html } from 'lit';
35

@@ -13,43 +15,108 @@ import type { Jsf, Path, UiSchema } from '../json-schema-form.js';
1315

1416
export const arrayField = (
1517
schema: JSONSchema7,
16-
dataLevel: any[],
18+
dataLevel: unknown,
1719
path: Path,
18-
uiState: any,
20+
uiState: unknown,
1921
uiSchema: UiSchema,
2022
handleChange: Jsf['_handleChange'],
2123
dig: Jsf['_dig'],
24+
schemaPath: Path,
2225
) => {
26+
if (!Array.isArray(dataLevel)) return html``;
27+
2328
return html` <!-- -->
24-
<fieldset part="array">
29+
<fieldset part="array" class="array">
30+
${JSON.stringify(schemaPath)} arr
2531
<!-- -->
2632
<legend>${schema.title}</legend>
2733
${dataLevel?.map?.((_item, index) => {
28-
if (typeof schema.items !== 'object' || Array.isArray(schema.items))
29-
return;
30-
return html` <sl-card>
34+
if (
35+
typeof schema.items !== 'object' ||
36+
Array.isArray(schema.items) ||
37+
!Array.isArray(dataLevel)
38+
)
39+
return '';
40+
return html` <sl-card
41+
@dragover=${(event: DragEvent) => {
42+
event.preventDefault();
43+
// event.stopPropagation();
44+
const dataTransfer = event.dataTransfer;
45+
if (dataTransfer) dataTransfer.dropEffect = 'move';
46+
47+
// (event.target as HTMLElement)
48+
// .closest('sl-card')
49+
// ?.setAttribute('data-dropzone', '');
50+
}}
51+
@dragenter=${(event: DragEvent) => {
52+
// event.stopPropagation();
53+
(event.target as HTMLElement)
54+
.closest('sl-card')
55+
?.setAttribute('data-dropzone', '');
56+
}}
57+
@dragleave=${(event: DragEvent) => {
58+
// event.stopPropagation();
59+
(event.target as HTMLElement)
60+
.closest('sl-card')
61+
?.removeAttribute('data-dropzone');
62+
}}
63+
@drop=${(event: DragEvent) => {
64+
// event.stopPropagation();
65+
const idx = event.dataTransfer?.getData('integer');
66+
if (!idx) return;
67+
const originIndex = Number.parseInt(idx, 10);
68+
if (!Array.isArray(dataLevel)) return;
69+
const hold = dataLevel[index] as unknown;
70+
// eslint-disable-next-line no-param-reassign
71+
dataLevel[index] = dataLevel[originIndex] as unknown;
72+
// eslint-disable-next-line no-param-reassign
73+
dataLevel[originIndex] = hold;
74+
handleChange([...path], dataLevel, schemaPath);
75+
76+
(event.target as HTMLElement)
77+
.closest('sl-card')
78+
?.removeAttribute('data-dropzone');
79+
}}
80+
>
3181
${dig(
3282
schema.items,
3383
dataLevel[index],
34-
[...path, String(index)],
84+
[...path, index],
3585
uiState,
3686
uiSchema,
87+
schemaPath,
3788
)}
3889
3990
<div slot="header" class="array-card-header">
40-
<div>
41-
<sl-tag size="medium" pill>${index + 1}</sl-tag>
42-
</div>
43-
<div class="handle">
44-
<sl-icon name="grip-horizontal" label="Settings"></sl-icon>
91+
<!-- <div></div> -->
92+
<div
93+
class="handle"
94+
.draggable=${true}
95+
@mousedown=${(_event: MouseEvent) => {
96+
// FIXME:
97+
// event.target!.style.cursor = 'grab';
98+
}}
99+
@dragstart=${(event: DragEvent) => {
100+
console.log(event);
101+
if (!event.dataTransfer) return;
102+
event.dataTransfer.setData('integer', String(index));
103+
}}
104+
>
105+
<sl-tag size="small" pill>${index + 1}</sl-tag>
106+
<div class="grip">
107+
<sl-icon name="grip-horizontal" label="Settings"></sl-icon>
108+
</div>
45109
</div>
46110
<div>
47111
<sl-tooltip content="Delete">
48112
<sl-button
49113
size="small"
50114
@click=${(_event: Event) => {
115+
if (!Array.isArray(dataLevel)) return;
116+
117+
// eslint-disable-next-line no-param-reassign
51118
dataLevel = dataLevel.filter((_, i) => i !== index);
52-
handleChange([...path], dataLevel);
119+
handleChange([...path], dataLevel, schemaPath);
53120
}}
54121
>
55122
<sl-icon name="trash3" label="Settings"></sl-icon>
@@ -59,31 +126,41 @@ export const arrayField = (
59126
<sl-divider vertical></sl-divider>
60127
61128
<sl-button-group>
62-
<sl-button
63-
size="small"
64-
@click=${(_event: Event) => {
65-
const hold = dataLevel[index];
66-
dataLevel[index] = dataLevel[index - 1];
67-
dataLevel[index - 1] = hold;
68-
handleChange([...path], dataLevel);
69-
}}
70-
.disabled=${typeof dataLevel?.[index - 1] === 'undefined'}
71-
>
72-
<sl-icon name="arrow-up" label="Up"></sl-icon>
73-
</sl-button>
74-
<sl-button
75-
size="small"
76-
@click=${(_event: Event) => {
77-
const hold = dataLevel[index];
129+
<sl-tooltip content="Move item up">
130+
<sl-button
131+
size="small"
132+
@click=${(_event: Event) => {
133+
if (!Array.isArray(dataLevel)) return;
134+
const hold = dataLevel[index] as unknown;
135+
// eslint-disable-next-line no-param-reassign
136+
dataLevel[index] = dataLevel[index - 1] as unknown;
137+
// eslint-disable-next-line no-param-reassign
138+
dataLevel[index - 1] = hold;
139+
handleChange([...path], dataLevel, schemaPath);
140+
}}
141+
.disabled=${typeof dataLevel?.[index - 1] === 'undefined'}
142+
>
143+
<sl-icon name="arrow-up" label="Up"></sl-icon>
144+
</sl-button>
145+
</sl-tooltip>
146+
<sl-tooltip content="Move item down">
147+
<sl-button
148+
size="small"
149+
@click=${(_event: Event) => {
150+
if (!Array.isArray(dataLevel)) return;
151+
const hold = dataLevel[index] as unknown;
78152
79-
dataLevel[index] = dataLevel[index + 1];
80-
dataLevel[index + 1] = hold;
81-
handleChange([...path], dataLevel);
82-
}}
83-
.disabled=${typeof dataLevel?.[index + 1] === 'undefined'}
84-
>
85-
<sl-icon name="arrow-down" label="Down"></sl-icon>
86-
</sl-button>
153+
// eslint-disable-next-line no-param-reassign
154+
dataLevel[index] = dataLevel[index + 1] as unknown;
155+
// eslint-disable-next-line no-param-reassign
156+
dataLevel[index + 1] = hold;
157+
handleChange([...path], dataLevel, schemaPath);
158+
}}
159+
.disabled=${typeof dataLevel?.[index + 1] === 'undefined'}
160+
>
161+
<sl-icon name="arrow-down" label="Down"></sl-icon>
162+
</sl-button>
163+
</sl-tooltip>
87164
</sl-button-group>
88165
</div>
89166
</div>
@@ -93,17 +170,21 @@ export const arrayField = (
93170
<div class="add-zone">
94171
<sl-button
95172
@click=${(_event: Event) => {
173+
// eslint-disable-next-line no-param-reassign
96174
dataLevel ||= [];
175+
if (!Array.isArray(dataLevel)) return;
97176
98177
if (typeof schema.items !== 'object' || Array.isArray(schema.items))
99178
return;
100179
if (schema.items?.type === 'string') {
101180
dataLevel.push(schema.items?.default || '');
102181
} else if (schema.items.properties) {
103182
dataLevel.push(schema.items?.default || {});
183+
} else if (schema.items?.type === 'array') {
184+
dataLevel.push(schema.items?.default || []);
104185
}
105186
106-
handleChange([...path], dataLevel);
187+
handleChange([...path], dataLevel, schemaPath);
107188
}}
108189
size="large"
109190
>

lib/styles.scss

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,71 @@ fieldset {
4343
}
4444
}
4545

46+
.array {
47+
sl-card {
48+
// transition: outline var(--sl-transition-slow);
49+
50+
&[data-dropzone] {
51+
border-radius: var(--sl-border-radius-medium);
52+
outline: 1px solid var(--sl-color-primary-500);
53+
// transition: outline var(--sl-transition-fast);
54+
55+
// cursor: grab;
56+
57+
* {
58+
pointer-events: none;
59+
}
60+
}
61+
}
62+
}
63+
4664
.array-card-header {
4765
display: flex;
4866
align-items: center;
4967
justify-content: space-between;
5068
width: 100%;
69+
font-size: 0.8em;
70+
71+
// sl-icon::pa {
72+
// height: 0.2rem;
73+
// }
74+
// display: none;
5175

5276
sl-tag::part(base) {
53-
background: var(--sl-color-neutral-0);
77+
background: var(--sl-color-neutral-100);
78+
}
79+
80+
.handle {
81+
display: flex;
82+
flex-grow: 1;
83+
align-items: center;
84+
justify-content: space-between;
85+
height: 2rem;
86+
padding-left: var(--sl-spacing-2x-small);
87+
margin: 0 var(--sl-spacing-medium) 0 0;
88+
font-size: 1.25em;
89+
color: var(--sl-color-neutral-500);
90+
// cursor: grab;
91+
cursor: move;
92+
transition: opacity, var(--sl-transition-fast);
93+
94+
&:hover {
95+
color: var(--sl-color-neutral-600);
96+
background: var(--sl-color-neutral-100);
97+
border-radius: var(--sl-border-radius-x-large);
98+
transition: var(--sl-transition-medium);
99+
}
100+
101+
&:active {
102+
// cursor: grabbing;
103+
user-select: none;
104+
}
105+
106+
.grip {
107+
display: flex;
108+
flex-grow: 1;
109+
justify-content: center;
110+
}
54111
}
55112
}
56113

@@ -69,28 +126,6 @@ fieldset {
69126
padding: var(--sl-spacing-medium) var(--sl-spacing-2x-large);
70127
}
71128

72-
.handle {
73-
display: flex;
74-
flex-grow: 1;
75-
align-items: center;
76-
justify-content: center;
77-
height: 2rem;
78-
font-size: 1.25em;
79-
color: var(--sl-color-neutral-500);
80-
cursor: grab;
81-
transition: opacity, var(--sl-transition-fast);
82-
83-
&:hover {
84-
color: var(--sl-color-neutral-600);
85-
transition: opacity, var(--sl-transition-x-fast);
86-
}
87-
88-
&:active {
89-
cursor: grabbing;
90-
user-select: none;
91-
}
92-
}
93-
94129
// -----------------------------------------------------------------------------
95130

96131
sl-radio-group::part(form-control-input) {

0 commit comments

Comments
 (0)