Plus 组件

拖拽移动

可通过拖拽改变位置

使用场景

用于设置元素可被拖动改变位置,支持限制拖拽范围,支持自定义触发拖动的元素。

代码演示

如何引入

import { DragMove } from '@fibrous/semi-ui-solid';

基本用法

被 DragMove 包裹的元素将能够通过拖拽改变位置。

注意

  • DragMove 默认会将可拖拽的元素设置为 absolute 定位;需要保留元素原有布局位置(例如居中的 Modal)时,可设置 positionStrategy="relative"
  • DragMove 把监听绑定在 children 渲染出的每个顶层 DOM 元素上,每个顶层元素各自可拖动。文本节点无法拖动,需要包在元素中;自定义组件渲染出的顶层元素同样会被绑定,无需透传 props 或转发 ref
Drag me
代码
import { DragMove } from '@fibrous/semi-ui-solid';

export default function DragMoveDemo() {
    // DragMove positions the element absolutely, so the wrapper keeps room for it in the preview
    return (
        <div style={{ height: '120px' }}>
            <DragMove>
                <div
                    style={{
                        'background-color': 'var(--semi-color-primary)',
                        width: '80px',
                        height: '80px',
                        display: 'flex',
                        'align-items': 'center',
                        'justify-content': 'center',
                        'border-radius': '10px',
                        'font-weight': '500',
                        position: 'absolute',
                        color: 'var(--semi-white)',
                    }}
                >
                    Drag me
                </div>
            </DragMove>
        </div>
    );
}

限制拖动范围

传入 constrainer,可以是 'parent',也可以是返回限制可拖拽范围的元素的函数。

注意:constrainer 设置的元素需要为 relative 定位

Constrainer
Drag me
代码
import { DragMove } from '@fibrous/semi-ui-solid';

export default function DragMoveDemo() {
    let container: HTMLDivElement | undefined;

    return (
        <div
            ref={(element) => (container = element)}
            style={{
                'background-color': 'var(--semi-grey-2)',
                width: '300px',
                height: '300px',
                padding: '5px',
                position: 'relative',
                color: 'var(--semi-white)',
                'font-weight': '500',
            }}
        >
            <span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
            <DragMove constrainer={() => container ?? null}>
                <div
                    style={{
                        'background-color': 'var(--semi-color-primary)',
                        width: '80px',
                        height: '80px',
                        'border-radius': '10px',
                        display: 'flex',
                        'align-items': 'center',
                        'justify-content': 'center',
                        position: 'absolute',
                        top: '80px',
                        left: '80px',
                    }}
                >
                    Drag me
                </div>
            </DragMove>
        </div>
    );
}

自定义触发拖动的元素

可通过 handler 自定义触发拖动的元素。如果不设置, 则点击任意位置均可拖动;如果设置,则仅点击 handler 部分可拖动。

Constrainer
代码
import { IconTransparentStroked } from '@fibrous/semi-icons-solid';
import { DragMove } from '@fibrous/semi-ui-solid';

export default function DragMoveDemo() {
    let handlerElement: HTMLDivElement | undefined;
    let container: HTMLDivElement | undefined;

    return (
        <div
            ref={(element) => (container = element)}
            style={{
                'background-color': 'var(--semi-grey-2)',
                width: '300px',
                height: '300px',
                padding: '5px',
                position: 'relative',
                color: 'var(--semi-white)',
                'font-weight': '500',
            }}
        >
            <span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
            <DragMove handler={() => handlerElement ?? null} constrainer={() => container ?? null}>
                <div
                    style={{
                        'background-color': 'var(--semi-color-primary)',
                        width: '80px',
                        height: '80px',
                        'border-radius': '10px',
                        position: 'absolute',
                        top: '50px',
                        left: '50px',
                        display: 'flex',
                        'align-items': 'center',
                        'justify-content': 'center',
                    }}
                >
                    <div ref={(element) => (handlerElement = element)} style={{ width: 'fit-content', height: 'fit-content' }}>
                        <IconTransparentStroked size="large" />
                    </div>
                </div>
            </DragMove>
        </div>
    );
}

自定义拖动后的位置处理

可通过 customMove 自定义拖动后的位置处理,该参数设置后,DragMove 组件内部将仅通过参数返回计算后的位置,不做设置,用户按需自行设置新位置。

蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制

Constrainer
代码
import { DragMove } from '@fibrous/semi-ui-solid';

export default function DragMoveDemo() {
    let containerRef: HTMLDivElement | undefined;
    let elementRef: HTMLButtonElement | undefined;
    let startPoint: { x: number; y: number } | null = null;

    const customMove = (element: HTMLElement, top: number, left: number) => {
        const container = containerRef;
        if (container && left + 100 > container.offsetWidth) {
            element.style.right = `${container.offsetWidth - left - element.offsetWidth}px`;
            element.style.left = 'auto';
        } else {
            element.style.left = left + 'px';
        }
        element.style.top = top + 'px';
    };

    const onMouseDown = (e: MouseEvent) => {
        startPoint = {
            x: e.clientX,
            y: e.clientY,
        };
    };

    const toggleWidth = () => {
        if (!elementRef) return;
        elementRef.style.width = elementRef.style.width === '60px' ? '100px' : '60px';
    };

    const onMouseUp = (e: MouseEvent) => {
        if (startPoint) {
            const { x, y } = startPoint;
            if (Math.abs(e.clientX - x) < 5 && Math.abs(e.clientY - y) < 5) {
                toggleWidth();
            }
        }
        startPoint = null;
    };

    const onKeyDown = (e: KeyboardEvent) => {
        if (e.key === 'Enter') {
            toggleWidth();
        }
    };

    return (
        <>
            <span>蓝色色块点击可改变宽度,改变前后蓝色色块均不会超出范围限制 </span>
            <br />
            <br />
            <div
                ref={(element) => (containerRef = element)}
                style={{
                    'background-color': 'var(--semi-grey-2)',
                    width: '300px',
                    height: '300px',
                    position: 'relative',
                    padding: '10px',
                    color: 'var(--semi-white)',
                    'font-weight': '500',
                }}
            >
                <span style={{ color: 'var(--semi-color-text-0)' }}>Constrainer</span>
                <DragMove constrainer={() => containerRef ?? null} customMove={customMove}>
                    <button
                        type="button"
                        style={{
                            'background-color': 'var(--semi-color-primary)',
                            'box-sizing': 'content-box',
                            width: '60px',
                            height: '50px',
                            display: 'flex',
                            'align-items': 'center',
                            'justify-content': 'center',
                            position: 'absolute',
                            top: '50px',
                            left: '50px',
                            border: 'none',
                            'border-radius': '10px',
                            padding: '5px',
                            color: 'inherit',
                            font: 'inherit',
                        }}
                        onMouseDown={onMouseDown}
                        onMouseUp={onMouseUp}
                        onKeyDown={onKeyDown}
                        ref={(element) => (elementRef = element)}
                    >
                        Drag me
                    </button>
                </DragMove>
            </div>
        </>
    );
}

API

Props

属性说明类型默认值
allowInputDrag在原生 input、textarea 上按下时是否允许拖动。booleanfalse
allowMove按下时判断是否允许拖动的函数,返回 false 则不拖动。Type(e: PointerEvent, element: HTMLElement) => boolean-
children被拖动的内容。每个顶层 DOM 元素各自可拖动,文本节点无法拖动。JSX.Element-
constrainer限制拖动范围的元素:'parent' 表示父级元素,或返回该元素的函数;该元素需要为 relative 定位。Type'parent' | (() => HTMLElement | null)-
customMove自定义拖动后的位置处理;设置后组件只在 requestAnimationFrame 中回传计算出的位置,不再改写元素的 top 与 left。Type(element: HTMLElement, top: number, left: number) => void-
handler返回触发拖动的元素,返回 null 时回退为被拖动元素本身。Type() => HTMLElement | null-
onMouseDown鼠标或触控笔按下时触发。Type(e: PointerEvent) => void-
onMouseMove鼠标或触控笔移动时触发。Type(e: PointerEvent) => void-
onMouseUp鼠标或触控笔抬起时触发。Type(e: PointerEvent) => void-
onTouchCancel指针被系统取消(pointercancel)时触发。Type(e: PointerEvent) => void-
onTouchEnd触摸结束时触发。Type(e: PointerEvent) => void-
onTouchMove触摸移动时触发。Type(e: PointerEvent) => void-
onTouchStart触摸开始时触发。Type(e: PointerEvent) => void-
positionStrategy定位策略,relative 保留元素原有的布局位置。'absolute' | 'relative'"absolute"

DragMove 没有自己的根元素,class、style、ref 与原生属性等未声明的属性无处可落,会被丢弃;需要的属性请直接写在被拖动的元素上。