@xiaohaih/drag
Version:
拖拽插件, 可通过指令或函数调用来拖拽元素移动
162 lines (155 loc) • 6.51 kB
text/typescript
import { ref, unref, watch } from 'vue';
import type { Ref } from 'vue';
import { isObject } from './utils/index';
import type { DOM, DragCore, DragCoreOption, EventOption } from './index';
import { drag } from './index';
type MaybeRef<T> = T | Ref<T>;
/** 元素拖拽 */
export function vDragsFunc(option: VDragCoreOption) {
const dragIns = ref<ReturnType<typeof drag> | null>(null);
/** 初始化拖拽 */
function init() {
destroy();
// if (option.disabled) return;
dragIns.value = drag({ ...option, disabled: unref(option.disabled) });
return destroy;
}
/** 销毁拖拽 */
function destroy() {
dragIns.value?.destroyed();
dragIns.value = null;
}
watch(() => [option.target, option.handle, option.disabled], init, { immediate: true });
return { init, destroy, dragIns };
}
export interface Binding {
value: BindingValue;
modifiers: {
/** 拖拽时只应用虚拟坐标, 元素坐标不更新 */
virtualAxis?: boolean;
/** 是否限制范围 */
boundaryLimit?: boolean;
/** 是否吸边 */
snap?: boolean;
/** 是否强制吸边 */
forceSnap?: boolean;
/** 靠近滚动条边缘时, 是否自动滚动 */
scrolling?: boolean;
/** 是否需要虚影跟随 */
shadowFollow?: boolean;
/** 虚影跟随时, 是否使用固定定位 */
shadowFollowFixed?: boolean;
};
}
export type BindingValue = DOM | VDragCoreOption;
export interface VDragCoreOption extends Omit<DragCoreOption, 'disabled'> {
/** 是否禁用拖拽 */
disabled?: MaybeRef<boolean>;
}
/**
* 拖拽指令
* @param {object} modifiers 修饰符的参数定义如下
* @param {boolean} [modifiers.virtualAxis] 拖拽时只应用虚拟坐标, 元素坐标不更新
* @param {boolean} [modifiers.boundaryLimit] 元素拖动不能超出范围
* @param {boolean} [modifiers.snap] 是否开启边缘吸附效果, 未开启时, 吸边相关配置项皆不生效
* @param {boolean} [modifiers.forceSnap] 是否强制边缘吸附效果
* @param {boolean} [modifiers.scrolling] 存在滚动条时, 靠近边缘自动触发滚动效果
* @param {boolean} [modifiers.shadowFollow] 是否克隆元素并跟随鼠标移动
* @param {boolean} [modifiers.shadowFollowFixed] 是否为固定定位元素
*
* @emit('axisBeforeUpdate', option: Omit<EventOption, 'native'>, ins: DragCore): 坐标更新前的回调事件 - 可直接修改坐标值来覆盖实际坐标
* @emit('axisUpdated', option: Omit<EventOption, 'native'>, ins: DragCore): 坐标更新后的回调事件
* @param {Omit<EventOption, 'native'>} option 更新的坐标信息
* @param {boolean} option.dragging 是否处于拖拽中
* @param {number} option.x 坐标
* @param {number} option.y 坐标
* @param {DragCore} ins 拖拽实例
*/
export const draggable = {
/** 获取拖拽所需的参数 */
getOptions(binding: Binding, el: HTMLElement) {
const params: VDragCoreOption = { target: el };
Object.keys(binding.modifiers).forEach((k) => {
setOptionByAttr[k as keyof typeof setOptionByAttr] ? setOptionByAttr[k as keyof typeof setOptionByAttr](params) : params[k as 'virtualAxis'] = true;
});
if (binding.value) {
if (isObject(binding.value)) {
const { target, handle, ...args } = binding.value as Exclude<typeof binding.value, DOM>;
target && (params.target = target);
handle && (params.handle = handle);
Object.assign(params, args);
}
else {
params.handle = binding.value;
}
}
return params;
},
/** vue3.0+ 自定义指令 */
mounted(el: HTMLElement, binding: Binding, vnode: any) {
const params = draggable.getOptions(binding, el);
const drags = vDragsFunc(params);
// @ts-expect-error 将事件挂载到元素上
el.drags = drags;
},
/** vue3.0+ 自定义指令 */
updated(el: HTMLElement, binding: Binding, vnode: any, prevVnode: any) {
// @ts-expect-error 获取挂载到元素上拖拽实例
if (!el.drags) el.drags = prevVnode.el?.drags;
// @ts-expect-error 获取挂载到元素上拖拽实例
if (!el.drags?.dragIns?.value) {
prevVnode.el?.drags?.destroy?.();
draggable.mounted(el, binding, vnode);
}
else {
// @ts-expect-error 将事件挂载到元素上
const dragIns = el.drags.dragIns.value as DragCore;
const params = draggable.getOptions(binding, el);
if (params.target === dragIns.option.target || params.handle === dragIns.option.handle) {
delete params.target;
delete params.handle;
}
params.disabled !== !dragIns.status && (params.disabled ? dragIns.disabled() : dragIns.enabled());
dragIns.updateOption(params);
}
},
/** vue3.0+ 自定义指令 */
beforeUnmount(el: HTMLElement) {
// @ts-expect-error 获取挂载到元素上拖拽实例
el.drags?.destroy?.();
},
/** vue2.0+ 自定义指令 */
bind(el: HTMLElement, binding: Binding, vnode: any) {
return draggable.mounted(el, binding, vnode);
},
/** vue2.0+ 自定义指令 */
componentUpdated(el: HTMLElement, binding: Binding, vnode: any, prevVnode: any) {
return draggable.updated(el, binding, vnode, prevVnode);
},
/** vue2.0+ 自定义指令 */
unbind(el: HTMLElement) {
return draggable.beforeUnmount(el);
},
};
const setOptionByAttr = {
boundaryLimit: (obj: Record<string, any>) => {
if (!obj.boundaryLimitOptions) obj.boundaryLimitOptions = {};
},
snap: (obj: Record<string, any>) => {
if (!obj.snapOptions) obj.snapOptions = {};
},
forceSnap: (obj: Record<string, any>) => {
if (!obj.snapOptions) obj.snapOptions = {};
obj.snapOptions.forceSnap = true;
},
scrolling: (obj: Record<string, any>) => {
if (!obj.scrollingOptions) obj.scrollingOptions = {};
},
shadowFollow: (obj: Record<string, any>) => {
if (!obj.shadowFollowOptions) obj.shadowFollowOptions = {};
},
shadowFollowFixed: (obj: Record<string, any>) => {
if (!obj.shadowFollowOptions) obj.shadowFollowOptions = {};
obj.shadowFollowOptions.fixed = true;
},
};