Skip to content

Commit f3cc1b7

Browse files
committed
Externalize sortablejs, add cloneGhost/hideOnLeave/bodyClass plugins
Externalize sortablejs as a peer dependency instead of bundling it into the dist. Consumers share a single sortablejs instance, so plugin registration via Sortable.mount() works from consumer code. Add three SortableJS plugins (src/plugins.ts) mounted at module load: - CloneGhost: destination-specific drag preview on cross-list entry. - HideOnLeave: hides placeholder when cursor exits destination rect. - BodyClass: toggles body.sortable-dragging during any drag. Add draggedData reactive ref on UseDraggableReturn. Add cancellable onAdd (return false to skip auto-insert for cross-type drops). Dist is checked in so git-dep consumers get the built output. Upstream sortablejs bug: SortableJS/Sortable#2465.
1 parent 1b82b5f commit f3cc1b7

16 files changed

Lines changed: 1225 additions & 166 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
.temp
55
*.log
66
components.d.ts
7-
dist
7+
# dist is checked in so git-dep consumers get the built output.
8+
# dist
89
node_modules
910
coverage
1011
.eslintcache

dist/component.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { UseDraggableOptions } from './useDraggable';
2+
interface IProps extends UseDraggableOptions<any> {
3+
modelValue: any[];
4+
tag?: string;
5+
target?: string;
6+
}
7+
export declare const VueDraggable: import("vue").DefineComponent<IProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<IProps>, {}, {}>;
8+
export {};

dist/directive.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { ObjectDirective } from 'vue';
2+
import type { MaybeRef } from './types';
3+
import { UseDraggableOptions } from './useDraggable';
4+
type VDraggableBinding = [
5+
list: MaybeRef<any[]>,
6+
options?: MaybeRef<UseDraggableOptions<any>>
7+
];
8+
export declare const vDraggable: ObjectDirective<HTMLElement, VDraggableBinding | MaybeRef<any[]>>;
9+
export {};

dist/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './component';
2+
export * from './directive';
3+
export * from './useDraggable';
4+
export * from './types';

dist/types/index.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Ref, ShallowRef, WritableComputedRef } from 'vue';
2+
export { type Options, type SortableEvent, type MoveEvent } from 'sortablejs';
3+
/**
4+
* copied from vueuse: https://github.com/vueuse/vueuse/blob/main/packages/shared/tryOnUnmounted/index.ts
5+
* Maybe it's a ref, or a plain value.
6+
*/
7+
export type MaybeRef<T = any> = T | Ref<T> | ShallowRef<T> | WritableComputedRef<T>;
8+
export type RefOrElement<T = HTMLElement> = T | Ref<T | undefined | null> | string;
9+
export type Fn = (...args: any[]) => any;

dist/useDraggable.d.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import Sortable, { type Options, type SortableEvent } from 'sortablejs';
2+
import { type Ref } from 'vue';
3+
import type { RefOrElement, MaybeRef } from './types';
4+
declare const CLONE_ELEMENT_KEY: unique symbol;
5+
export interface DraggableEvent<T = any> extends SortableEvent {
6+
item: HTMLElement & {
7+
[CLONE_ELEMENT_KEY]: any;
8+
};
9+
data: T;
10+
clonedData: T;
11+
}
12+
type SortableMethod = 'closest' | 'save' | 'toArray' | 'destroy' | 'option';
13+
export interface UseDraggableReturn extends Pick<Sortable, SortableMethod> {
14+
/**
15+
* Start the sortable.
16+
* @param {HTMLElement} target - The target element to be sorted.
17+
* @default By default the root element of the VueDraggablePlus instance is used
18+
*/
19+
start: (target?: HTMLElement) => void;
20+
pause: () => void;
21+
resume: () => void;
22+
/**
23+
* Reactive reference to the source data of whatever drag is currently in
24+
* progress. Non-null while any sortable is actively dragging, null
25+
* otherwise. Shared across all sortable instances.
26+
*/
27+
draggedData: Ref<unknown>;
28+
}
29+
export interface UseDraggableOptions<T> extends Options {
30+
clone?: (element: T) => T;
31+
immediate?: boolean;
32+
customUpdate?: (event: DraggableEvent<T>) => void;
33+
/**
34+
* Factory for a destination-specific drag preview. When a cross-list drag
35+
* from another sortable enters this one, the dragged element's innerHTML
36+
* is replaced with the result of this factory so the user sees the
37+
* element as it will look once dropped (e.g. a scene card for an app
38+
* dropped into a playlist). The original innerHTML is restored when the
39+
* drag leaves this sortable, ends, or cancels. Return `null` to leave
40+
* the default in place.
41+
*/
42+
cloneGhost?: () => HTMLElement | string | null;
43+
/**
44+
* Hide the dragged element's placeholder while the cursor is outside this
45+
* sortable's bounding rect. Pairs with `cloneGhost` for a symmetric feel:
46+
* the destination preview appears on entry and disappears on leave,
47+
* rather than lingering until drop.
48+
*/
49+
hideOnLeave?: boolean;
50+
/**
51+
* Element dragging started
52+
*/
53+
onStart?: ((event: DraggableEvent<T>) => void) | undefined;
54+
/**
55+
* Element dragging ended
56+
*/
57+
onEnd?: ((event: DraggableEvent<T>) => void) | undefined;
58+
/**
59+
* Element is dropped into the list from another list.
60+
*
61+
* Runs BEFORE the library's default list insertion. Return `false` to
62+
* cancel that insertion entirely — useful for heterogeneous cross-list
63+
* drops where the consumer takes over (e.g. source is `App[]`, destination
64+
* is `Scene[]`, and the real insertion happens via a server mutation).
65+
* Any other return value (including `undefined`) lets the library insert
66+
* the cloned source data into the destination list as normal.
67+
*/
68+
onAdd?: ((event: DraggableEvent<T>) => boolean | void) | undefined;
69+
/**
70+
* Created a clone of an element
71+
*/
72+
onClone?: ((event: DraggableEvent<T>) => void) | undefined;
73+
/**
74+
* Element is chosen
75+
*/
76+
onChoose?: ((event: DraggableEvent<T>) => void) | undefined;
77+
/**
78+
* Element is unchosen
79+
*/
80+
onUnchoose?: ((event: DraggableEvent<T>) => void) | undefined;
81+
/**
82+
* Changed sorting within list
83+
*/
84+
onUpdate?: ((event: DraggableEvent<T>) => void) | undefined;
85+
/**
86+
* Called by any change to the list (add / update / remove)
87+
*/
88+
onSort?: ((event: DraggableEvent<T>) => void) | undefined;
89+
/**
90+
* Element is removed from the list into another list
91+
*/
92+
onRemove?: ((event: DraggableEvent<T>) => void) | undefined;
93+
/**
94+
* Attempt to drag a filtered element
95+
*/
96+
onFilter?: ((event: DraggableEvent<T>) => void) | undefined;
97+
/**
98+
* Called when dragging element changes position
99+
*/
100+
onChange?: ((evt: DraggableEvent<T>) => void) | undefined;
101+
}
102+
/**
103+
* A custom compositionApi utils that allows you to drag and drop elements in lists.
104+
* @param el
105+
* @param {Array} list - The list to be dragged
106+
* @param {Object} options - The options of the sortable
107+
* @returns {Object} - The return of the sortable
108+
*/
109+
export declare function useDraggable<T>(el: RefOrElement, list?: Ref<T[] | undefined>, options?: MaybeRef<UseDraggableOptions<T>>): UseDraggableReturn;
110+
export declare function useDraggable<T>(el: null | undefined, list?: Ref<T[] | undefined>, options?: MaybeRef<UseDraggableOptions<T>>): UseDraggableReturn;
111+
export declare function useDraggable<T>(el: RefOrElement<HTMLElement | null | undefined>, options?: MaybeRef<UseDraggableOptions<T>>): UseDraggableReturn;
112+
export {};

dist/utils/index.d.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Moves an element in an array from one position to another.
3+
* @param {T[]} array
4+
* @param {number} from
5+
* @param {number} to
6+
* @returns {T[]}
7+
*/
8+
export declare function moveArrayElement<T>(array: T[], from: number, to: number): T[];
9+
/**
10+
* Convert a hyphen-delimited string to camelCase.
11+
* @param {string} str
12+
* @returns {string}
13+
*/
14+
export declare function camelize(str: string): string;
15+
/**
16+
* Convert an object's keys from hyphen-delimited to camelCase.
17+
* @param {Record<string, any>} object
18+
* @returns {Record<string, any>}
19+
*/
20+
export declare function objectMap(object: Record<any, any>): Record<string, any>;
21+
/**
22+
* Removes an element from an array.
23+
* @param {T[]} array
24+
* @param {number} index
25+
* @returns {T[]}
26+
*/
27+
export declare function removeElement<T>(array: T[], index: number): T[];
28+
/**
29+
* Inserts an element into an array.
30+
* @param {T[]} array
31+
* @param {number} index
32+
* @param element
33+
* @returns {T[]}
34+
*/
35+
export declare function insertElement<T>(array: T[], index: number, element: any): T[];
36+
/**
37+
* If the value is undefined, return true, otherwise return false.
38+
* @param {any} value - any
39+
* @returns {value is undefined}
40+
*/
41+
export declare function isUndefined(value: any): value is undefined;
42+
/**
43+
* If the value is string, return true, otherwise return false.
44+
* @param value
45+
* @returns {value is string}
46+
*/
47+
export declare function isString(value: any): value is string;
48+
/**
49+
* Inserts a element into the DOM at a given index.
50+
* @param parentElement
51+
* @param element
52+
* @param {number} index
53+
*/
54+
export declare function insertNodeAt(parentElement: Element, element: Element, index: number): void;
55+
/**
56+
* Removes a node from the DOM.
57+
* @param {Node} node
58+
*/
59+
export declare function removeNode(node: Node): void;
60+
/**
61+
* Get an element by selector.
62+
* @param {string} selector
63+
* @param parentElement
64+
* @returns {Element}
65+
*/
66+
export declare function getElementBySelector(selector: string, parentElement?: Document | Element): HTMLElement;
67+
/**
68+
* It takes a function and returns a function that executes the original function and then executes the second function.
69+
* @param {Function} fn - The function to be executed
70+
* @param {Function} afterFn - The function to be executed after the original function.
71+
* @param {any} [ctx=null] - The context of the function.
72+
* @returns {Function}
73+
*/
74+
export declare function mergeExecuted<T extends (...args: []) => any>(fn: T, afterFn: T, ctx?: any): (...args: any[]) => any;
75+
/**
76+
* Merge the options and events.
77+
* @param {Record<string, any>} options
78+
* @param {Record<string, any>} events
79+
* @returns {Record<string, any>}
80+
*/
81+
export declare function mergeOptionsEvents(options: Record<string, any>, events: Record<string, any>): {
82+
[x: string]: any;
83+
};
84+
export declare function isHTMLElement(el: any): el is HTMLElement;
85+
/**
86+
* @param obj
87+
* @param fn
88+
*/
89+
export declare function forEachObject<T extends Record<string, any>>(obj: T, fn: (key: keyof T, value: T[keyof T]) => void): void;
90+
/**
91+
*
92+
* @param key
93+
*/
94+
export declare function isOn(key: any): boolean;
95+
export declare const extend: {
96+
<T extends {}, U>(target: T, source: U): T & U;
97+
<T_1 extends {}, U_1, V>(target: T_1, source1: U_1, source2: V): T_1 & U_1 & V;
98+
<T_2 extends {}, U_2, V_1, W>(target: T_2, source1: U_2, source2: V_1, source3: W): T_2 & U_2 & V_1 & W;
99+
(target: object, ...sources: any[]): any;
100+
};

dist/utils/log.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Logs a warning message.
3+
* @param {string} msg
4+
*/
5+
export declare function warn(msg: string): void;
6+
/**
7+
* Logs an error message.
8+
* @param {string} msg
9+
*/
10+
export declare function error(msg: string): void;

dist/vue-draggable-plus.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"use strict";var Le=Object.defineProperty,Se=Object.defineProperties;var Ce=Object.getOwnPropertyDescriptors;var P=Object.getOwnPropertySymbols;var Z=Object.prototype.hasOwnProperty,ee=Object.prototype.propertyIsEnumerable;var Q=(e,n,t)=>n in e?Le(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t,C=(e,n)=>{for(var t in n||(n={}))Z.call(n,t)&&Q(e,t,n[t]);if(P)for(var t of P(n))ee.call(n,t)&&Q(e,t,n[t]);return e},ne=(e,n)=>Se(e,Ce(n));var q=(e,n)=>{var t={};for(var o in e)Z.call(e,o)&&n.indexOf(o)<0&&(t[o]=e[o]);if(e!=null&&P)for(var o of P(e))n.indexOf(o)<0&&ee.call(e,o)&&(t[o]=e[o]);return t};Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("vue"),D=require("sortablejs"),fe="[vue-draggable-plus]: ";function De(e){console.warn(fe+e)}function Ae(e){console.error(fe+e)}function te(e,n,t){return t>=0&&t<e.length&&e.splice(t,0,e.splice(n,1)[0]),e}function we(e){return e.replace(/-(\w)/g,(n,t)=>t?t.toUpperCase():"")}function Ie(e){return Object.keys(e).reduce((n,t)=>(typeof e[t]!="undefined"&&(n[we(t)]=e[t]),n),{})}function oe(e,n){return Array.isArray(e)&&e.splice(n,1),e}function re(e,n,t){return Array.isArray(e)&&e.splice(n,0,t),e}function Ge(e){return typeof e=="undefined"}function Te(e){return typeof e=="string"}function le(e,n,t){const o=e.children[t];e.insertBefore(n,o)}function F(e){e.parentNode&&e.parentNode.removeChild(e)}function Ne(e,n=document){var o;let t=null;return typeof(n==null?void 0:n.querySelector)=="function"?t=(o=n==null?void 0:n.querySelector)==null?void 0:o.call(n,e):t=document.querySelector(e),t||De(`Element not found: ${e}`),t}function xe(e,n,t=null){return function(...o){return e.apply(t,o),n.apply(t,o)}}function Me(e,n){const t=C({},e);return Object.keys(n).forEach(o=>{t[o]?t[o]=xe(e[o],n[o]):t[o]=n[o]}),t}function Be(e){return e instanceof HTMLElement}function ie(e,n){Object.keys(e).forEach(t=>{n(t,e[t])})}function Ee(e){return e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97)}const He=Object.assign,z=Symbol("cloneGhostOriginalHtml"),x=Symbol("cloneGhostAppliedBy");function G(){return D.dragged}function T(e){if(!e||!e[x])return;const n=e[z];typeof n=="string"&&(e.innerHTML=n),e[z]=void 0,e[x]=void 0}function Pe(e,n,t){const o=n();o&&(e[z]=e.innerHTML,e.innerHTML=typeof o=="string"?o:o.innerHTML,e[x]=t)}function R(){}R.prototype={dragOverValid(e){if(e.isOwner)return;const n=e.sortable.options.cloneGhost;if(!n)return;const t=G();t&&t[x]!==e.sortable.el&&(t[x]&&T(t),Pe(t,n,e.sortable.el))},dragOverGlobal(e){e.isOwner&&T(G())},revertGlobal(){const e=G();T(e),e&&e.style.display==="none"&&(e.style.display="")},dropGlobal(){T(G())},nullingGlobal(){T(G())}};R.pluginName="cloneGhost";R.initializeByDefault=!0;let N=null;function Re(e,n,t){return e>=t.left&&e<=t.right&&n>=t.top&&n<=t.bottom}function Ve(){if(N)return;let e=null;const n=t=>{var L;const o=D.dragged;if(!o||!o.parentNode){e=null;return}const u=o.parentNode,m=D.get(u);if(!((L=m==null?void 0:m.options)==null?void 0:L.hideOnLeave)){e=null;return}const O=Re(t.clientX,t.clientY,u.getBoundingClientRect());O!==e&&(o.style.display=O?"":"none",e=O)};document.addEventListener("pointermove",n),N=n}function _e(){N&&(document.removeEventListener("pointermove",N),N=null)}function V(){}V.prototype={dragStartGlobal(){Ve()},nullingGlobal(){_e();const e=D.dragged;e&&e.style.display==="none"&&(e.style.display="")}};V.pluginName="hideOnLeave";V.initializeByDefault=!0;const ue="sortable-dragging";function _(){}_.prototype={dragStartGlobal(){document.body.classList.add(ue)},nullingGlobal(){document.body.classList.remove(ue)}};_.pluginName="bodyClass";_.initializeByDefault=!0;let se=!1;function Ue(){se||(se=!0,D.mount(R,V,_))}Ue();function je(e){return e==null?e:JSON.parse(JSON.stringify(e))}function qe(e){l.getCurrentInstance()&&l.onUnmounted(e)}function Fe(e){l.getCurrentInstance()?l.onMounted(e):l.nextTick(e)}let ge=null,pe=null;const me=l.shallowRef(null);function ae(e=null,n=null){ge=e,pe=n,me.value=n}function $e(){return{data:ge,clonedData:pe}}const ce=Symbol("cloneElement");function k(...e){var K,W;const n=(K=l.getCurrentInstance())==null?void 0:K.proxy;let t=null;const o=e[0];let[,u,m]=e;Array.isArray(l.unref(u))||(m=u,u=null);let i=null;const{immediate:O=!0,clone:L=je,forceFallback:M,fallbackOnBody:b,customUpdate:S}=(W=l.unref(m))!=null?W:{};function A(r){var p;const{from:s,oldIndex:c,item:f}=r,a=Array.from(s.childNodes);t=M&&!b?a.slice(0,-1):a;const d=l.unref((p=l.unref(u))==null?void 0:p[c]),g=L(d);ae(d,g),f[ce]=g}function h(r){const s=r.item[ce];if(!Ge(s)){if(F(r.item),l.isRef(u)){const c=[...l.unref(u)];u.value=re(c,r.newDraggableIndex,s);return}re(l.unref(u),r.newDraggableIndex,s)}}function B(r){const{from:s,item:c,oldIndex:f,oldDraggableIndex:a,pullMode:d,clone:g}=r;if(le(s,c,f),d==="clone"){F(g);return}if(l.isRef(u)){const p=[...l.unref(u)];u.value=oe(p,a);return}oe(l.unref(u),a)}function U(r){if(S){S(r);return}const{from:s,item:c,oldIndex:f,oldDraggableIndex:a,newDraggableIndex:d}=r;if(F(c),le(s,c,f),l.isRef(u)){const g=[...l.unref(u)];u.value=te(g,a,d);return}te(l.unref(u),a,d)}function j(r){const{newIndex:s,oldIndex:c,from:f,to:a}=r;let d=null;const g=s===c&&f===a;try{if(g){let p=null;t==null||t.some((w,v)=>{if(p&&(t==null?void 0:t.length)!==a.childNodes.length)return f.insertBefore(p,w.nextSibling),!0;const H=a.childNodes[v];p=a==null?void 0:a.replaceChild(w,H)})}}catch(p){d=p}finally{t=null}l.nextTick(()=>{if(ae(),d)throw d})}const E={onUpdate:U,onStart:A,onAdd:h,onRemove:B,onEnd:j};function be(r){const s=l.unref(o);return r||(r=Te(s)?Ne(s,n==null?void 0:n.$el):s),r&&!Be(r)&&(r=r.$el),r||Ae("Root element not found"),r}function J(){var p;const w=(p=l.unref(m))!=null?p:{},{immediate:r,clone:s}=w,c=q(w,["immediate","clone"]);ie(c,(v,H)=>{Ee(v)&&(c[v]=(I,...ve)=>{const Oe=$e();return He(I,Oe),H(I,...ve)})});const f=c.onAdd;delete c.onAdd;const a=u===null?{}:E,d=Me(a,c),g=a.onAdd;return(f||g)&&(d.onAdd=function(v){var I;if(((I=v.item)==null?void 0:I.style.display)==="none")return;(f==null?void 0:f.call(this,v))!==!1&&(g==null||g.call(this,v))}),d}const X=r=>{r=be(r),i&&y.destroy(),i=new D(r,J())};l.watch(()=>m,()=>{i&&ie(J(),(r,s)=>{i==null||i.option(r,s)})},{deep:!0});const y={option:(r,s)=>i==null?void 0:i.option(r,s),destroy:()=>{i==null||i.destroy(),i=null},save:()=>i==null?void 0:i.save(),toArray:()=>i==null?void 0:i.toArray(),closest:(...r)=>i==null?void 0:i.closest(...r)},ye=()=>y==null?void 0:y.option("disabled",!0),he=()=>y==null?void 0:y.option("disabled",!1);return Fe(()=>{O&&X()}),qe(y.destroy),ne(C({start:X,pause:ye,resume:he},y),{draggedData:me})}const Y=["update","start","add","remove","choose","unchoose","end","sort","filter","clone","move","change"],ze=["clone","animation","ghostClass","group","sort","disabled","store","handle","draggable","swapThreshold","invertSwap","invertedSwapThreshold","removeCloneOnHide","direction","chosenClass","dragClass","ignore","filter","preventOnFilter","easing","setData","dropBubble","dragoverBubble","dataIdAttr","delay","delayOnTouchOnly","touchStartThreshold","forceFallback","fallbackClass","fallbackOnBody","fallbackTolerance","fallbackOffset","supportPointer","emptyInsertThreshold","scroll","forceAutoScrollFallback","scrollSensitivity","scrollSpeed","bubbleScroll","modelValue","tag","target","customUpdate",...Y.map(e=>`on${e.replace(/^\S/,n=>n.toUpperCase())}`)],Ye=l.defineComponent({name:"VueDraggable",model:{prop:"modelValue",event:"update:modelValue"},props:ze,emits:["update:modelValue",...Y],setup(e,{slots:n,emit:t,expose:o,attrs:u}){const m=Y.reduce((b,S)=>{const A=`on${S.replace(/^\S/,h=>h.toUpperCase())}`;return b[A]=(...h)=>t(S,...h),b},{}),i=l.computed(()=>{const h=l.toRefs(e),{modelValue:b}=h,S=q(h,["modelValue"]),A=Object.entries(S).reduce((B,[U,j])=>{const E=l.unref(j);return E!==void 0&&(B[U]=E),B},{});return C(C({},m),Ie(C(C({},u),A)))}),O=l.computed({get:()=>e.modelValue,set:b=>t("update:modelValue",b)}),L=l.ref(),M=l.reactive(k(e.target||L,O,i));return o(M),()=>{var b;return l.h(e.tag||"div",{ref:L},(b=n==null?void 0:n.default)==null?void 0:b.call(n,M))}}}),de={mounted:"mounted",unmounted:"unmounted"},$=new WeakMap,ke={[de.mounted](e,n){const t=l.isProxy(n.value)?[n.value]:n.value,[o,u]=t,m=k(e,o,u);$.set(e,m.destroy)},[de.unmounted](e){var n;(n=$.get(e))==null||n(),$.delete(e)}};exports.VueDraggable=Ye;exports.useDraggable=k;exports.vDraggable=ke;

0 commit comments

Comments
 (0)