Skip to content

Commit 32affed

Browse files
committed
fix(form): implement drag and drop for reordering items
1 parent 7982b2c commit 32affed

File tree

5 files changed

+558
-124
lines changed

5 files changed

+558
-124
lines changed

src/components/form/form.scss

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@use '../../style/mixins.scss';
12
@use '../../style/internal/shared_input-select-picker';
23

34
/**
@@ -9,26 +10,61 @@
910
* @prop --form-background-color-of-odd-rows:Background of odd rows in the form, when layout type is `row`. Defaults to `--contrast-200`.
1011
*/
1112

13+
*,
14+
*::after,
15+
*::before {
16+
box-sizing: border-box;
17+
}
18+
1219
.form-group {
1320
min-width: 0;
1421
}
1522

1623
.limel-form-array-item--simple {
1724
display: flex;
1825
align-items: center;
19-
padding-bottom: var(--form-row-gap, 1rem);
26+
padding: 0.25rem;
2027

21-
*:first-child {
22-
flex-grow: 1;
28+
> *:first-child {
29+
// this is the input element, which is followed
30+
// by the drag handle and the delete controls
31+
flex: 1;
32+
min-width: 0;
2333
}
2434
}
2535

26-
limel-code-editor {
27-
margin-bottom: 0.75rem;
36+
.array-items {
37+
isolation: isolate;
38+
display: flex;
39+
flex-direction: column;
40+
row-gap: var(--form-row-gap, 0.5rem);
41+
margin-top: 1rem;
42+
43+
&.has-an-item-which-is-being-dragged {
44+
limel-collapsible-section {
45+
// This ensures that all collapsible sections which are open will
46+
// temporarily collapse, while they are being dragged around.
47+
// This makes it way easier for the end user to see where they are dropping
48+
// the item; since sections can be really tall and hard to reorder
49+
// by drag and drop.
50+
--limel-cs-grid-template-rows: 0fr;
51+
--limel-cs-opacity-transition-speed: 0.2s;
52+
--limel-cs-grid-template-rows-transition-speed: 0.2s;
53+
--limel-cs-open-header-bottom-border-radius: 0.75rem;
54+
}
55+
}
2856
}
2957

30-
.limel-form-array-item--object {
31-
margin-bottom: 0.25rem;
58+
.array-item {
59+
@include mixins.is-draggable-item();
60+
}
61+
62+
limel-drag-handle {
63+
order: 10; // ensure drag handle is always last, specifically in the collapsible section
64+
}
65+
66+
limel-collapsible-section.is-being-dragged {
67+
border-radius: 0.75rem !important;
3268
}
3369

3470
.limel-form-layout--default {
@@ -181,13 +217,6 @@ p {
181217
font-size: var(--limel-theme-default-font-size);
182218
}
183219

184-
p + limel-collapsible-section,
185-
p + .limel-form-array-item--simple,
186-
h1 + limel-collapsible-section,
187-
h1 + .limel-form-array-item--simple {
188-
margin-top: 1rem;
189-
}
190-
191220
.form-group {
192221
position: relative;
193222

src/components/form/templates/array-field-collapsible-item.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface CollapsibleItemProps {
3737
allowItemRemoval: boolean;
3838

3939
/**
40-
* Control whether items can be reordered.
40+
* Whether this particular item can be reordered.
4141
*/
4242
allowItemReorder: boolean;
4343
}
@@ -84,41 +84,36 @@ export class CollapsibleItemTemplate extends React.Component<CollapsibleItemProp
8484
children = this.props.item.children;
8585
}
8686

87+
const dragHandle = this.props.allowItemReorder
88+
? React.createElement('limel-drag-handle', {
89+
slot: 'header',
90+
class: 'drag-handle',
91+
})
92+
: null;
93+
8794
return React.createElement(
8895
'limel-collapsible-section',
8996
{
9097
header: findTitle(data, schema, formSchema) || 'New item',
91-
class: 'limel-form-array-item--object',
98+
class: 'array-item limel-form-array-item--object',
9299
ref: (section: HTMLLimelCollapsibleSectionElement) => {
93100
this.section = section;
94101
},
95102
'is-open': this.state.isOpen,
103+
'data-reorder-id': String(this.props.index),
104+
'data-reorderable': this.props.allowItemReorder
105+
? 'true'
106+
: 'false',
96107
},
108+
dragHandle,
97109
children
98110
);
99111
}
100112

101113
private setActions(element: HTMLLimelCollapsibleSectionElement) {
102-
const { item, index, allowItemRemoval, allowItemReorder } = this.props;
114+
const { item, index, allowItemRemoval } = this.props;
103115
const actions: Array<Action & Runnable> = [];
104116

105-
if (allowItemReorder) {
106-
actions.push(
107-
{
108-
id: 'down',
109-
icon: 'down_arrow',
110-
disabled: !item.hasMoveDown,
111-
run: item.onReorderClick(index, index + 1),
112-
},
113-
{
114-
id: 'up',
115-
icon: 'up_arrow',
116-
disabled: !item.hasMoveUp,
117-
run: item.onReorderClick(index, index - 1),
118-
}
119-
);
120-
}
121-
122117
if (allowItemRemoval) {
123118
actions.push({
124119
id: 'remove',

src/components/form/templates/array-field-simple-item.ts

Lines changed: 25 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface SimpleItemProps {
66
index: number;
77
allowItemRemoval: boolean;
88
allowItemReorder: boolean;
9+
dataIndex: number;
910
}
1011

1112
const LIMEL_ICON_BUTTON = 'limel-icon-button';
@@ -15,58 +16,47 @@ export class SimpleItemTemplate extends React.Component<SimpleItemProps> {
1516
super(props);
1617
}
1718

18-
private removeButton?: HTMLLimelButtonElement;
19-
private moveUpButton?: HTMLLimelButtonElement;
20-
private moveDownButton?: HTMLLimelButtonElement;
19+
private removeButton?: HTMLLimelIconButtonElement;
2120

2221
public componentWillUnmount() {
2322
this.setRemoveButton(undefined);
24-
this.setMoveUpButton(undefined);
25-
this.setMoveDownButton(undefined);
2623
}
2724

2825
public render() {
29-
const { item } = this.props;
26+
const { item, allowItemReorder } = this.props;
3027

3128
return React.createElement(
3229
'div',
3330
{
34-
className: 'limel-form-array-item--simple',
31+
className: 'array-item limel-form-array-item--simple',
32+
'data-reorder-id': String(this.props.dataIndex),
33+
'data-reorderable': allowItemReorder ? 'true' : 'false',
3534
},
3635
this.props.item.children,
37-
this.props.allowItemReorder
38-
? this.renderMoveDownButton(item)
39-
: null,
40-
this.props.allowItemReorder ? this.renderMoveUpButton(item) : null,
41-
this.props.allowItemRemoval ? this.renderRemoveButton(item) : null
36+
this.renderRemoveButton(item),
37+
this.renderDragHandle()
4238
);
4339
}
4440

45-
private renderRemoveButton(item: ArrayFieldItem) {
46-
const props: any = {
47-
icon: 'trash',
48-
disabled: !item.hasRemove,
49-
ref: this.setRemoveButton,
50-
};
41+
private renderDragHandle() {
42+
if (!this.props.allowItemReorder) {
43+
return;
44+
}
5145

52-
return React.createElement(LIMEL_ICON_BUTTON, props);
46+
return React.createElement('limel-drag-handle', {
47+
class: 'drag-handle',
48+
});
5349
}
5450

55-
private renderMoveUpButton(item: ArrayFieldItem) {
56-
const props: any = {
57-
icon: 'up_arrow',
58-
disabled: !item.hasMoveUp,
59-
ref: this.setMoveUpButton,
60-
};
61-
62-
return React.createElement(LIMEL_ICON_BUTTON, props);
63-
}
51+
private renderRemoveButton(item: ArrayFieldItem) {
52+
if (!this.props.allowItemRemoval) {
53+
return;
54+
}
6455

65-
private renderMoveDownButton(item: ArrayFieldItem) {
6656
const props: any = {
67-
icon: 'down_arrow',
68-
disabled: !item.hasMoveDown,
69-
ref: this.setMoveDownButton,
57+
icon: 'trash',
58+
disabled: !item.hasRemove,
59+
ref: this.setRemoveButton,
7060
};
7161

7262
return React.createElement(LIMEL_ICON_BUTTON, props);
@@ -77,17 +67,9 @@ export class SimpleItemTemplate extends React.Component<SimpleItemProps> {
7767
item.onDropIndexClick(index)(event);
7868
};
7969

80-
private handleMoveUp = (event: PointerEvent): void => {
81-
const { item, index } = this.props;
82-
item.onReorderClick(index, index - 1)(event);
83-
};
84-
85-
private handleMoveDown = (event: PointerEvent): void => {
86-
const { item, index } = this.props;
87-
item.onReorderClick(index, index + 1)(event);
88-
};
89-
90-
private setRemoveButton = (button?: HTMLLimelButtonElement | null) => {
70+
private readonly setRemoveButton = (
71+
button?: HTMLLimelIconButtonElement | null
72+
) => {
9173
if (this.removeButton) {
9274
this.removeButton.removeEventListener('click', this.handleRemove);
9375
}
@@ -98,31 +80,4 @@ export class SimpleItemTemplate extends React.Component<SimpleItemProps> {
9880
this.removeButton.addEventListener('click', this.handleRemove);
9981
}
10082
};
101-
102-
private setMoveUpButton = (button?: HTMLLimelButtonElement | null) => {
103-
if (this.moveUpButton) {
104-
this.moveUpButton.removeEventListener('click', this.handleMoveUp);
105-
}
106-
107-
this.moveUpButton = button || undefined;
108-
109-
if (this.moveUpButton) {
110-
this.moveUpButton.addEventListener('click', this.handleMoveUp);
111-
}
112-
};
113-
114-
private setMoveDownButton = (button?: HTMLLimelButtonElement | null) => {
115-
if (this.moveDownButton) {
116-
this.moveDownButton.removeEventListener(
117-
'click',
118-
this.handleMoveDown
119-
);
120-
}
121-
122-
this.moveDownButton = button || undefined;
123-
124-
if (this.moveDownButton) {
125-
this.moveDownButton.addEventListener('click', this.handleMoveDown);
126-
}
127-
};
12883
}

0 commit comments

Comments
 (0)