feat: resize grid columns with drag and drop (#748)
* feat: resize grid columns with drag and drop * fix: column resizing only in designable * fix: batch patch * fix: does not scroll when dragging to a blank block * fix: overflow-x auto
This commit is contained in:
parent
56bd996bd4
commit
259393f626
@ -1,3 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
@ -25,8 +26,15 @@ export const ActionBar = observer((props: any) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', ...style }} {...others}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', overflowX: 'auto', ...style }}
|
||||
{...others}
|
||||
>
|
||||
<div className={css`
|
||||
.ant-space:last-child {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}>
|
||||
<DndContext>
|
||||
<Space>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { useDndContext, useDndMonitor, useDroppable } from '@dnd-kit/core';
|
||||
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import cls from 'classnames';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useFormBlockContext, useSchemaInitializer } from '../../../';
|
||||
import { useDesignable, useFormBlockContext, useSchemaInitializer } from '../../../';
|
||||
import { DndContext } from '../../common/dnd-context';
|
||||
|
||||
const GridRowContext = createContext(null);
|
||||
const GridColContext = createContext(null);
|
||||
const GridRowContext = createContext<any>({});
|
||||
const GridColContext = createContext<any>({});
|
||||
const GridContext = createContext<any>({});
|
||||
|
||||
const breakRemoveOnGrid = (s: Schema) => s['x-component'] === 'Grid';
|
||||
@ -19,6 +19,8 @@ const ColDivider = (props) => {
|
||||
id: props.id,
|
||||
data: props.data,
|
||||
});
|
||||
const { dn, designable } = useDesignable();
|
||||
const dividerRef = useRef<HTMLElement>();
|
||||
|
||||
const droppableStyle = {
|
||||
backgroundColor: isOver ? 'rgba(241, 139, 98, .1)' : undefined,
|
||||
@ -38,10 +40,89 @@ const ColDivider = (props) => {
|
||||
visible = activeSchema !== currentSchema && downSchema !== activeSchema;
|
||||
}
|
||||
}
|
||||
const prevSchema = props.cols[props.index];
|
||||
const nextSchema = props.cols[props.index + 1];
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef: setDraggableNodeRef,
|
||||
isDragging,
|
||||
} = useDraggable({
|
||||
disabled: props.first || props.last || !designable,
|
||||
id: props.id,
|
||||
data: {
|
||||
dividerRef,
|
||||
prevSchema,
|
||||
nextSchema,
|
||||
},
|
||||
});
|
||||
|
||||
const [clientWidths, setClientWidths] = useState([0, 0]);
|
||||
|
||||
useDndMonitor({
|
||||
onDragStart(event) {
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
const el = dividerRef.current;
|
||||
const prev = el.previousElementSibling as HTMLDivElement;
|
||||
const next = el.nextElementSibling as HTMLDivElement;
|
||||
setClientWidths([prev.clientWidth, next.clientWidth]);
|
||||
},
|
||||
onDragMove(event) {
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
const el = dividerRef.current;
|
||||
const prev = el.previousElementSibling as HTMLDivElement;
|
||||
const next = el.nextElementSibling as HTMLDivElement;
|
||||
prev.style.width = `calc(${clientWidths[0]}px + ${event.delta.x}px)`;
|
||||
next.style.width = `calc(${clientWidths[1]}px - ${event.delta.x}px)`;
|
||||
},
|
||||
onDragEnd(event) {
|
||||
if (clientWidths[0] <= 0 || clientWidths[1] <= 0) {
|
||||
return;
|
||||
}
|
||||
setClientWidths([0, 0]);
|
||||
if (!prevSchema || !nextSchema) {
|
||||
return;
|
||||
}
|
||||
const el = dividerRef.current;
|
||||
const prev = el.previousElementSibling as HTMLDivElement;
|
||||
const next = el.nextElementSibling as HTMLDivElement;
|
||||
prevSchema['x-component-props'] = prevSchema['x-component-props'] || {};
|
||||
nextSchema['x-component-props'] = nextSchema['x-component-props'] || {};
|
||||
prevSchema['x-component-props']['width'] =
|
||||
(100 * (prev?.clientWidth + 24 + 24 / props.cols.length)) / el.parentElement.clientWidth;
|
||||
nextSchema['x-component-props']['width'] =
|
||||
(100 * (next?.clientWidth + 24 + 24 / props.cols.length)) / el.parentElement.clientWidth;
|
||||
dn.emit('batchPatch', {
|
||||
schemas: [
|
||||
{
|
||||
['x-uid']: prevSchema['x-uid'],
|
||||
'x-component-props': {
|
||||
...prevSchema['x-component-props'],
|
||||
},
|
||||
},
|
||||
{
|
||||
['x-uid']: nextSchema['x-uid'],
|
||||
'x-component-props': {
|
||||
...nextSchema['x-component-props'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={visible ? setNodeRef : null}
|
||||
ref={(el) => {
|
||||
if (visible) {
|
||||
setNodeRef(el);
|
||||
dividerRef.current = el;
|
||||
}
|
||||
}}
|
||||
className={cls(
|
||||
'nb-col-divider',
|
||||
css`
|
||||
@ -49,7 +130,36 @@ const ColDivider = (props) => {
|
||||
`,
|
||||
)}
|
||||
style={{ ...droppableStyle }}
|
||||
>
|
||||
<div
|
||||
ref={setDraggableNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className={
|
||||
props.first || props.last || !designable
|
||||
? null
|
||||
: css`
|
||||
&::before {
|
||||
content: ' ';
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
left: 6px;
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
}
|
||||
&:hover {
|
||||
&::before {
|
||||
background: rgba(241, 139, 98, 0.06) !important;
|
||||
}
|
||||
}
|
||||
width: 24px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
`
|
||||
}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -255,7 +365,7 @@ Grid.Row = observer((props) => {
|
||||
const cols = useColProperties();
|
||||
|
||||
return (
|
||||
<GridRowContext.Provider value={{ cols }}>
|
||||
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
|
||||
<div
|
||||
className={cls(
|
||||
'nb-grid-row',
|
||||
@ -285,6 +395,7 @@ Grid.Row = observer((props) => {
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
index={index}
|
||||
last={index === cols.length - 1}
|
||||
id={`${addr}_${index + 1}`}
|
||||
data={{
|
||||
breakRemoveOn: breakRemoveOnRow,
|
||||
@ -303,10 +414,22 @@ Grid.Row = observer((props) => {
|
||||
|
||||
Grid.Col = observer((props: any) => {
|
||||
const { cols } = useContext(GridRowContext);
|
||||
const w = props.width || 100 / cols.length;
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
|
||||
const width = `calc(${w}% - 24px - 24px / ${cols.length})`;
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: field.address.toString(),
|
||||
data: {
|
||||
insertAdjacent: 'beforeEnd',
|
||||
schema,
|
||||
wrapSchema: (s) => s,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<GridColContext.Provider value={{ cols, schema }}>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={{ width }}
|
||||
className={cls(
|
||||
'nb-grid-col',
|
||||
@ -318,5 +441,6 @@ Grid.Col = observer((props: any) => {
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</GridColContext.Provider>
|
||||
);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DndContext as DndKitContext, DragEndEvent, DragOverlay, rectIntersection } from '@dnd-kit/core';
|
||||
import { Props } from '@dnd-kit/core/dist/components/DndContext/DndContext';
|
||||
import { observer } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../';
|
||||
import { createDesignable, useDesignable } from '../../hooks';
|
||||
@ -18,6 +18,7 @@ const useDragEnd = (props?: any) => {
|
||||
const insertAdjacent = over?.data?.current?.insertAdjacent;
|
||||
const breakRemoveOn = over?.data?.current?.breakRemoveOn;
|
||||
const wrapSchema = over?.data?.current?.wrapSchema;
|
||||
const onSuccess = over?.data?.current?.onSuccess;
|
||||
|
||||
if (!activeSchema || !overSchema) {
|
||||
props?.onDragEnd?.(event);
|
||||
@ -49,6 +50,7 @@ const useDragEnd = (props?: any) => {
|
||||
wrap: wrapSchema,
|
||||
breakRemoveOn,
|
||||
removeParentsIfNoChildren: true,
|
||||
onSuccess,
|
||||
});
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
@ -58,15 +60,25 @@ const useDragEnd = (props?: any) => {
|
||||
|
||||
export const DndContext = observer((props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(true);
|
||||
return (
|
||||
<DndKitContext collisionDetection={rectIntersection} {...props} onDragEnd={useDragEnd(props)}>
|
||||
<DndKitContext
|
||||
collisionDetection={rectIntersection}
|
||||
{...props}
|
||||
onDragStart={(event) => {
|
||||
const { active } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
setVisible(!!activeSchema);
|
||||
}}
|
||||
onDragEnd={useDragEnd(props)}
|
||||
>
|
||||
<DragOverlay
|
||||
dropAnimation={{
|
||||
duration: 10,
|
||||
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
|
||||
}}
|
||||
>
|
||||
<span style={{ whiteSpace: 'nowrap' }}>{t('Dragging')}</span>
|
||||
{visible && <span style={{ whiteSpace: 'nowrap' }}>{t('Dragging')}</span>}
|
||||
</DragOverlay>
|
||||
{props.children}
|
||||
</DndKitContext>
|
||||
|
@ -111,7 +111,35 @@ export class Designable {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, removed }) => {
|
||||
const updateColumnSize = (parent: Schema) => {
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
const len = Object.values(parent.properties).length;
|
||||
const schemas = [];
|
||||
parent.mapProperties((s) => {
|
||||
s['x-component-props'] = s['x-component-props'] || {};
|
||||
s['x-component-props']['width'] = 100 / len;
|
||||
if (s['x-uid']) {
|
||||
schemas.push({
|
||||
'x-uid': s['x-uid'],
|
||||
'x-component-props': s['x-component-props'],
|
||||
});
|
||||
}
|
||||
});
|
||||
if (parent['x-uid'] && schemas.length) {
|
||||
return schemas;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, wrapped, removed }) => {
|
||||
let schemas = [];
|
||||
if (wrapped?.['x-component'] === 'Grid.Col') {
|
||||
schemas = updateColumnSize(wrapped.parent);
|
||||
}
|
||||
if (removed?.['x-component'] === 'Grid.Col') {
|
||||
schemas = updateColumnSize(removed.parent);
|
||||
}
|
||||
refresh();
|
||||
if (!current['x-uid']) {
|
||||
return;
|
||||
@ -124,6 +152,13 @@ export class Designable {
|
||||
wrap,
|
||||
},
|
||||
});
|
||||
if (schemas.length) {
|
||||
await api.request({
|
||||
url: `/uiSchemas:batchPatch`,
|
||||
method: 'post',
|
||||
data: schemas,
|
||||
});
|
||||
}
|
||||
if (removed?.['x-uid']) {
|
||||
await api.request({
|
||||
url: `/uiSchemas:remove/${removed['x-uid']}`,
|
||||
@ -147,7 +182,20 @@ export class Designable {
|
||||
});
|
||||
message.success(t('Saved successfully'), 0.2);
|
||||
});
|
||||
this.on('batchPatch', async ({ schemas }) => {
|
||||
refresh();
|
||||
await api.request({
|
||||
url: `/uiSchemas:batchPatch`,
|
||||
method: 'post',
|
||||
data: schemas,
|
||||
});
|
||||
message.success(t('Saved successfully'), 0.2);
|
||||
});
|
||||
this.on('remove', async ({ removed }) => {
|
||||
let schemas = [];
|
||||
if (removed?.['x-component'] === 'Grid.Col') {
|
||||
schemas = updateColumnSize(removed.parent);
|
||||
}
|
||||
refresh();
|
||||
if (!removed?.['x-uid']) {
|
||||
return;
|
||||
@ -156,6 +204,13 @@ export class Designable {
|
||||
url: `/uiSchemas:remove/${removed['x-uid']}`,
|
||||
method: 'post',
|
||||
});
|
||||
if (schemas.length) {
|
||||
await api.request({
|
||||
url: `/uiSchemas:batchPatch`,
|
||||
method: 'post',
|
||||
data: schemas,
|
||||
});
|
||||
}
|
||||
message.success(t('Saved successfully'), 0.2);
|
||||
});
|
||||
}
|
||||
@ -177,14 +232,14 @@ export class Designable {
|
||||
generateUid(schema);
|
||||
}
|
||||
|
||||
on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch', listener: any) {
|
||||
on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', listener: any) {
|
||||
if (!this.events[name]) {
|
||||
this.events[name] = [];
|
||||
}
|
||||
this.events[name].push(listener);
|
||||
}
|
||||
|
||||
emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch', ...args) {
|
||||
emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', ...args) {
|
||||
if (!this.events[name]) {
|
||||
return;
|
||||
}
|
||||
@ -356,6 +411,7 @@ export class Designable {
|
||||
this.emit('insertAdjacent', {
|
||||
position: 'beforeBegin',
|
||||
schema: schema2,
|
||||
wrapped,
|
||||
wrap: schema1,
|
||||
...opts,
|
||||
});
|
||||
@ -407,6 +463,7 @@ export class Designable {
|
||||
position: 'afterBegin',
|
||||
schema: schema2,
|
||||
wrap: schema1,
|
||||
wrapped,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
@ -421,6 +478,7 @@ export class Designable {
|
||||
if (!Schema.isSchemaInstance(this.current)) {
|
||||
return;
|
||||
}
|
||||
delete schema['x-index'];
|
||||
const opts = { onSuccess: options.onSuccess };
|
||||
const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options;
|
||||
if (Schema.isSchemaInstance(schema)) {
|
||||
@ -447,6 +505,7 @@ export class Designable {
|
||||
position: 'beforeEnd',
|
||||
schema: schema2,
|
||||
wrap: schema1,
|
||||
wrapped,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
@ -508,6 +567,7 @@ export class Designable {
|
||||
position: 'afterEnd',
|
||||
schema: schema2,
|
||||
wrap: schema1,
|
||||
wrapped,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ export const uiSchemaActions = {
|
||||
insertNewSchema: callRepositoryMethod('insertNewSchema', 'values'),
|
||||
remove: callRepositoryMethod('remove', 'resourceIndex'),
|
||||
patch: callRepositoryMethod('patch', 'values'),
|
||||
batchPatch: callRepositoryMethod('batchPatch', 'values'),
|
||||
clearAncestor: callRepositoryMethod('clearAncestor', 'resourceIndex'),
|
||||
|
||||
async insertAdjacent(ctx: Context, next) {
|
||||
|
@ -303,6 +303,14 @@ export class UiSchemaRepository extends Repository {
|
||||
await traverSchemaTree(newSchema);
|
||||
}
|
||||
|
||||
@transaction()
|
||||
async batchPatch(schemas: any[], options?) {
|
||||
const { transaction } = options;
|
||||
for (const schema of schemas) {
|
||||
await this.patch(schema, { ...options, transaction });
|
||||
}
|
||||
}
|
||||
|
||||
async updateNode(uid: string, schema: any, transaction?: Transaction) {
|
||||
const nodeModel = await this.findOne({
|
||||
filter: {
|
||||
|
Loading…
Reference in New Issue
Block a user