diff --git a/bricks/basic/docs/eo-actions.md b/bricks/basic/docs/eo-actions.md index 1c2955b44..bb672fa8f 100644 --- a/bricks/basic/docs/eo-actions.md +++ b/bricks/basic/docs/eo-actions.md @@ -61,3 +61,30 @@ events: args: - click delete button ``` + +### item draggable + +```yaml preview +brick: eo-actions +properties: + itemDraggable: true + actions: + - text: document + icon: + lib: antd + icon: folder + dragConf: + format: text/plain + data: + category: document + title: 文档 + - text: file + icon: + lib: antd + icon: file + dragConf: + format: text/plain + data: + category: file + title: 文件 +``` diff --git a/bricks/basic/src/actions/index.spec.tsx b/bricks/basic/src/actions/index.spec.tsx index 936feb4c1..030366384 100644 --- a/bricks/basic/src/actions/index.spec.tsx +++ b/bricks/basic/src/actions/index.spec.tsx @@ -124,4 +124,54 @@ describe("eo-actions", () => { }); expect(element.shadowRoot?.childNodes.length).toBe(0); }); + + test("item draggable", async () => { + const onItemDragStart = jest.fn(); + const onItemDragEnd = jest.fn(); + + const element = document.createElement("eo-actions") as EoActions; + element.itemDraggable = true; + element.actions = [ + { + text: "drag item 1", + dragConf: { + format: "text/plain", + data: { + a: 1, + }, + }, + }, + { + text: "drag item 2", + dragConf: { + format: "text/plain", + data: { + a: 2, + }, + }, + }, + ]; + element.addEventListener("item.drag.start", onItemDragStart); + element.addEventListener("item.drag.start", onItemDragEnd); + + expect(element.shadowRoot).toBeFalsy(); + + act(() => { + document.body.appendChild(element); + }); + + act(() => { + fireEvent.dragStart( + element.shadowRoot?.querySelectorAll("eo-menu-item")[0] as HTMLElement + ); + }); + expect(onItemDragStart).toBeCalled(); + + act(() => { + fireEvent.dragEnd( + element.shadowRoot?.querySelectorAll("eo-menu-item")[0] as HTMLElement + ); + }); + expect(onItemDragEnd).toBeCalled(); + }); }); diff --git a/bricks/basic/src/actions/index.tsx b/bricks/basic/src/actions/index.tsx index 8d0fce72b..39817991c 100644 --- a/bricks/basic/src/actions/index.tsx +++ b/bricks/basic/src/actions/index.tsx @@ -129,6 +129,10 @@ export interface SimpleAction { href?: string; target?: Target; danger?: boolean; + dragConf?: { + format: string; + data: unknown; + }; } export interface SubeMenuItemAction extends SimpleAction { @@ -148,15 +152,20 @@ export type Action = SimpleAction | Divider | SubeMenuAction; export interface ActionsProps { actions?: Action[]; + itemDraggable?: boolean; checkedKeys?: string[]; } export interface ActionsEvents { "action.click": CustomEvent; + "item.drag.start": CustomEvent; + "item.drag.end": CustomEvent; } export interface ActionsEventsMapping { onActionClick: "action.click"; + onItemDragEnd: "item.drag.end"; + onItemDragStart: "item.drag.start"; } /** @@ -185,6 +194,12 @@ class EoActions extends ReactNextElement implements ActionsProps { }) accessor checkedKeys: string[] = []; + /** + * action中的菜单项是否可拖拽 + */ + @property({ type: Boolean }) + accessor itemDraggable: boolean | undefined; + /** * 点击按钮时触发 * @detail 该按钮配置 @@ -192,6 +207,22 @@ class EoActions extends ReactNextElement implements ActionsProps { @event({ type: "action.click" }) accessor #actionClickEvent!: EventEmitter; + /** + * 开始拖拽菜单项时触发 + * + * @detail 该菜单项动作配置 + */ + @event({ type: "item.drag.start" }) + accessor #itemDragStartEvent!: EventEmitter; + + /** + * 完成拖拽菜单项时触发 + * + * @detail 该菜单项动作配置 + */ + @event({ type: "item.drag.end" }) + accessor #itemDragEndEvent!: EventEmitter; + #handleActionClick = (action: SimpleAction): void => { this.#actionClickEvent.emit(action); if (action.event) { @@ -199,11 +230,22 @@ class EoActions extends ReactNextElement implements ActionsProps { } }; + #handleItemDragEnd = (action: SimpleAction): void => { + this.#itemDragEndEvent.emit(action); + }; + + #handleItemDragStart = (action: SimpleAction): void => { + this.#itemDragStartEvent.emit(action); + }; + render() { return ( ); @@ -211,13 +253,19 @@ class EoActions extends ReactNextElement implements ActionsProps { } export interface ActionsComponentProps extends ActionsProps { + itemDraggable?: boolean; onActionClick?: (action: SimpleAction) => void; + onItemDragEnd?: (action: SimpleAction) => void; + onItemDragStart?: (action: SimpleAction) => void; } export function EoActionsComponent({ actions, checkedKeys, onActionClick, + itemDraggable, + onItemDragStart, + onItemDragEnd, }: ActionsComponentProps) { const filteredActions = useMemo(() => { return actions?.filter((action) => !action.hidden); @@ -248,12 +296,27 @@ export function EoActionsComponent({ className={classnames({ "menu-item-danger": action.danger, })} + draggable={itemDraggable} icon={action.icon} disabled={action.disabled} onClick={(e: React.MouseEvent) => { e.stopPropagation(); onActionClick?.(action); }} + onDragStart={(e: React.DragEvent) => { + if (action.dragConf) { + e.dataTransfer?.setData( + action.dragConf.format, + JSON.stringify(action.dragConf.data) + ); + (e.target as HTMLElement).classList.add("dragging"); + } + onItemDragStart?.(action); + }} + onDragEnd={(e: React.DragEvent) => { + (e.target as HTMLElement).classList.remove("dragging"); + onItemDragEnd?.(action); + }} > {action.text} diff --git a/bricks/basic/src/actions/styles.shadow.css b/bricks/basic/src/actions/styles.shadow.css index d5709d0d9..902e6efe9 100644 --- a/bricks/basic/src/actions/styles.shadow.css +++ b/bricks/basic/src/actions/styles.shadow.css @@ -25,6 +25,14 @@ eo-menu-item:not([disabled]):hover::part(menu-item) { background-color: var(--left-sidebar-item-hover-bg); } +eo-menu-item.dragging { + opacity: 0.5; +} + +eo-menu-item[draggable="true"]::part(menu-item) { + cursor: grab; +} + eo-tooltip { display: block; } diff --git a/bricks/basic/src/context-menu/index.spec.tsx b/bricks/basic/src/context-menu/index.spec.tsx index 0d24c2a55..6ec344f4f 100644 --- a/bricks/basic/src/context-menu/index.spec.tsx +++ b/bricks/basic/src/context-menu/index.spec.tsx @@ -154,4 +154,47 @@ describe("eo-context-menu", () => { document.body.removeChild(element); }); }); + + test("item draggable", async () => { + const onItemDragStart = jest.fn(); + const onItemDragEnd = jest.fn(); + const element = document.createElement("eo-context-menu") as EoContextMenu; + element.addEventListener("item.drag.start", (e: Event) => + onItemDragStart((e as CustomEvent).detail) + ); + + element.addEventListener("item.drag.end", (e: Event) => + onItemDragEnd((e as CustomEvent).detail) + ); + + act(() => { + document.body.appendChild(element); + }); + + act(() => { + fireEvent( + element.shadowRoot!.querySelector("eo-actions")!, + new CustomEvent("item.drag.start", { + detail: { text: "item", dragConf: { key: "text", data: {} } }, + }) + ); + }); + expect(onItemDragStart).toBeCalledWith({ + text: "item", + dragConf: { key: "text", data: {} }, + }); + + act(() => { + fireEvent( + element.shadowRoot!.querySelector("eo-actions")!, + new CustomEvent("item.drag.end", { + detail: { text: "item", dragConf: { key: "text", data: {} } }, + }) + ); + }); + expect(onItemDragEnd).toBeCalledWith({ + text: "item", + dragConf: { key: "text", data: {} }, + }); + }); }); diff --git a/bricks/basic/src/context-menu/index.tsx b/bricks/basic/src/context-menu/index.tsx index 3e08548e8..735832f3b 100644 --- a/bricks/basic/src/context-menu/index.tsx +++ b/bricks/basic/src/context-menu/index.tsx @@ -27,6 +27,8 @@ const WrappedActions = wrapBrick< ActionsEventsMapping >("eo-actions", { onActionClick: "action.click", + onItemDragEnd: "item.drag.end", + onItemDragStart: "item.drag.start", }); const MINIMAL_PADDING = 8; @@ -73,6 +75,12 @@ class EoContextMenu extends ReactNextElement { @property({ attribute: false }) accessor position: Position | undefined; + /** + * action中的菜单项是否可拖拽 + */ + @property({ type: Boolean }) + accessor itemDraggable: boolean | undefined; + /** * 点击菜单项动作时触发 * @@ -81,6 +89,22 @@ class EoContextMenu extends ReactNextElement { @event({ type: "action.click" }) accessor #actionClickEvent!: EventEmitter; + /** + * 开始拖拽菜单项时触发 + * + * @detail 该菜单项动作配置 + */ + @event({ type: "item.drag.start" }) + accessor #itemDragStartEvent!: EventEmitter; + + /** + * 完成拖拽菜单项时触发 + * + * @detail 该菜单项动作配置 + */ + @event({ type: "item.drag.end" }) + accessor #itemDragEndEvent!: EventEmitter; + @method() open({ position }: OpenInfo) { this.active = true; @@ -100,6 +124,14 @@ class EoContextMenu extends ReactNextElement { } }; + #handleItemDragStart = (action: SimpleAction): void => { + this.#itemDragStartEvent.emit(action); + }; + + #handleItemDragEnd = (action: SimpleAction): void => { + this.#itemDragEndEvent.emit(action); + }; + disconnectedCallback() { super.disconnectedCallback(); lockBodyScroll(this, false); @@ -113,6 +145,9 @@ class EoContextMenu extends ReactNextElement { active={this.active} position={this.position} onActionClick={this.#handleActionClick} + itemDraggable={this.itemDraggable} + onItemDragStart={this.#handleItemDragStart} + onItemDragEnd={this.#handleItemDragEnd} /> ); } @@ -120,6 +155,9 @@ class EoContextMenu extends ReactNextElement { export interface EoContextMenuComponentProps extends EoContextMenuProps { onActionClick?(action: SimpleAction): void; + onItemDragStart?(action: SimpleAction): void; + onItemDragEnd?(action: SimpleAction): void; + itemDraggable?: boolean; element?: EoContextMenu; } @@ -129,6 +167,9 @@ export function EoContextMenuComponent({ active: _active, position, onActionClick, + itemDraggable, + onItemDragStart, + onItemDragEnd, }: EoContextMenuComponentProps) { const active = _active ?? false; @@ -143,7 +184,6 @@ export function EoContextMenuComponent({ }, [element] ); - const handleActionClick = useCallback( (e: CustomEvent) => { element?.close(); @@ -152,6 +192,14 @@ export function EoContextMenuComponent({ [element, onActionClick] ); + const handleItemDragEnd = useCallback( + (e: CustomEvent) => { + element?.close(); + onItemDragEnd?.(e.detail); + }, + [element, onItemDragEnd] + ); + const actionsRef = useRef(null); const [fixedPosition, setFixedPosition] = useState(null); @@ -185,7 +233,12 @@ export function EoContextMenuComponent({ ) => + onItemDragStart?.(e.detail) + } + onItemDragEnd={handleItemDragEnd} style={{ left: (fixedPosition ?? position)?.[0], top: (fixedPosition ?? position)?.[1], diff --git a/bricks/basic/src/dropdown-actions/index.tsx b/bricks/basic/src/dropdown-actions/index.tsx index cbdda7fc2..da5d98631 100644 --- a/bricks/basic/src/dropdown-actions/index.tsx +++ b/bricks/basic/src/dropdown-actions/index.tsx @@ -37,6 +37,8 @@ const WrappedActions = wrapBrick< ActionsEventsMapping >("eo-actions", { onActionClick: "action.click", + onItemDragEnd: "item.drag.end", + onItemDragStart: "item.drag.start", }); export interface DropdownActionsProps { diff --git a/bricks/basic/src/mini-actions/index.tsx b/bricks/basic/src/mini-actions/index.tsx index 95ce15566..9f020226d 100644 --- a/bricks/basic/src/mini-actions/index.tsx +++ b/bricks/basic/src/mini-actions/index.tsx @@ -45,6 +45,8 @@ const WrappedActions = wrapBrick< ActionsEventsMapping >("eo-actions", { onActionClick: "action.click", + onItemDragEnd: "item.drag.end", + onItemDragStart: "item.drag.start", }); export interface SimpleActionType extends Omit {