@rbuljan/jsort
Version:
A JavaScript library for sorting and reordering grid or list items.
9 lines (8 loc) • 13.6 kB
JavaScript
/**
* Small yet powerful drag and drop sortable library with touch support, smooth animations, for a great UX.
* @class JSort
* @author Roko C. Buljan <https://github.com/rokobuljan>
* @license MIT
* @see {@link https://github.com/rokobuljan/jsort}
*/
class t{static version="1.10.3";selectorItemsFull="";elGhost;elGrab;elTarget;elDrop;elDropParent;indexGrab=-1;indexDrop=-1;affectedElements=[];scrollParent=null;scrollDirection=null;scrollAnim=null;edgePressure=0;moveTimeout;isScrollPrevented=!1;hasPointerMoved=!1;hasTouchMoved=!1;destroyParentClass=!1;pointerGrab=null;touchStart=null;constructor(t,e={}){this.elGrabParent=t,this.group="",this.swap=!1,this.duration=420,this.easing="cubic-bezier(0.6, 0, 0.6, 1)",this.scale=1.1,this.opacity=.8,this.grabTimeout=140,this.parentDrop=!0,this.moveThreshold=0,this.scrollThreshold=8,this.edgeThreshold=50,this.scrollSpeed=10,this.zIndex=2147483647,this.selectorParent=".jsort",this.selectorItems="*",this.selectorItemsIgnore=".jsort-ignore",this.selectorHandler=".jsort-handler",this.selectorIgnoreTarget="",this.selectorIgnoreFields=':is(input, select, textarea, button, label, [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex^="-"]), a[href]:not(a[href=""]), area[href]):not(:disabled)',this.classGhost="is-jsort-ghost",this.classActive="is-jsort-active",this.classTouch="is-jsort-touch",this.classGrab="is-jsort-grab",this.classTarget="is-jsort-target",this.classAnimated="is-jsort-animated",this.classAnimatedDrop="is-jsort-animated-drop",this.classInvalid="is-jsort-invalid",this.onBeforeGrab=()=>{},this.onGrab=()=>{},this.onMove=()=>{},this.onBeforeDrop=()=>{},this.onDrop=()=>{},this.onAnimationEnd=()=>{},this.init(e)}getChildren(t){if(!t)return[];return[...t.children].filter(t=>!t.matches(`.${this.classGhost}`))}parseDataAttribute(t){return t?.dataset.jsort?.replace(/\s/g,"").replace(/;$/,"").split(/;/).reduce((t,e)=>{const[s,i]=e.split(":");return t[s]=isNaN(Number(i))?/^(true|false)$/.test(i)?JSON.parse(i):i:Number(i),t},{})??{}}isSignificantMove(t,e,s){const{clientX:i,clientY:r}=e,l=i-t.clientX,n=r-t.clientY;return Math.hypot(l,n)>=s}insertGhost(){if(!this.elGrab)return;const{x:t,y:e,width:s,height:i}=this.elGrab.getBoundingClientRect();this.elGhost=this.elGrab.cloneNode(!0),Object.assign(this.elGhost.style,{position:"fixed",left:`${t}px`,top:`${e}px`,width:`${s}px`,height:`${i}px`,pointerEvents:"none",zIndex:this.zIndex,opacity:this.opacity}),this.elGhost.classList.remove(this.classActive,this.classTarget),this.elGhost.classList.add(this.classGhost),this.elGhost.animate([{scale:this.scale}],{duration:250,easing:this.easing,fill:"forwards"}),this.elGrabParent.append(this.elGhost)}removeGhost(){this.elGhost&&(this.elGhost.remove(),this.elGhost=null)}animateItem({el:t,x:e,y:s}){const{left:i,top:r}=t.getBoundingClientRect();if(e===i&&s===r)return;t.classList.add(this.classAnimated);const l=t===this.elGrab?[{position:"relative",zIndex:1,translate:`${e-i}px ${s-r}px`,opacity:.9,scale:this.scale},{position:"relative",zIndex:1,translate:"0",opacity:1,scale:1}]:[{position:"relative",zIndex:0,scale:1,translate:`${e-i}px ${s-r}px`},{position:"relative",zIndex:0,scale:2-this.scale},{position:"relative",zIndex:0,scale:1,translate:"0"}],n=t.animate(l,{duration:this.duration,easing:this.easing,fill:"forwards"});return n.addEventListener("finish",()=>{t.classList.remove(this.classAnimated),n.cancel()}),n}closestElement(t,e){for(;t&&t!==e;)t=t.parentElement;return t===e?t:null}checkValidity({clientX:t=0,clientY:e=0,el:s}){const i=s??document.elementFromPoint(t,e),r=i?.closest(`${this.selectorItemsFull}, ${this.selectorParent}`),l=i?.closest(`${this.selectorItemsIgnore}`);if(l)return!1;const n=i?.closest(this.selectorParent),o=r===n,h=r&&this.closestElement(r,this.elGrab)===this.elGrab,a=n===this.elGrabParent,c=n?.dataset.jsortGroup,d=!a&&Boolean(c&&this.group===c);if(!this.parentDrop&&o)return!1;return!h&&r&&n&&d||a}engine(t,e=60){const s=1e3/e;let i=0,r=null;const l=()=>{r=requestAnimationFrame(l);const e=window.performance.now(),n=e-i;if(n<s)return;i=e-n%s,t()},n=()=>{cancelAnimationFrame(r),r=null,i=0};return{start:()=>{n(),i=performance.now(),r=requestAnimationFrame(l)},stop:n,tick:l}}findScrollParent(t){for(;t&&t!==document.documentElement;){const e=getComputedStyle(t);if((t.scrollHeight>t.clientHeight||t.scrollWidth>t.clientWidth)&&/^(auto|scroll)$/.test(e.overflowY))return t;t=t.parentElement}return document.documentElement}scrollStep(){if(!this.scrollDirection||!this.scrollParent)return;const t=this.scrollSpeed*(this.edgePressure/Math.max(this.edgeThreshold,1));"up"===this.scrollDirection?this.scrollParent.scrollTop-=t:"down"===this.scrollDirection?this.scrollParent.scrollTop+=t:"left"===this.scrollDirection?this.scrollParent.scrollLeft-=t:"right"===this.scrollDirection&&(this.scrollParent.scrollLeft+=t)}startEdgeScroll(t){this.scrollDirection!==t&&(this.scrollDirection=t,this.scrollAnim||(this.scrollAnim=this.engine(this.scrollStep.bind(this)),this.scrollAnim?.start()))}stopEdgeScroll(){this.scrollDirection=null,this.scrollAnim&&(this.scrollAnim.stop(),this.scrollAnim=null)}handleScrollParent(t){this.scrollParent||(this.scrollParent=this.findScrollParent(this.elGrab));const e=this.scrollParent.getBoundingClientRect(),s=document.documentElement,i=this.scrollParent===s,r=i?0:e.top,l=i?window.innerHeight:e.bottom,n=i?0:e.left,o=i?window.innerWidth:e.left;t.clientY<r+this.edgeThreshold?(this.edgePressure=Math.min(this.edgeThreshold,this.edgeThreshold-(t.clientY-r)),this.startEdgeScroll("up")):t.clientY>l-this.edgeThreshold?(this.edgePressure=Math.min(this.edgeThreshold,this.edgeThreshold-(l-t.clientY)),this.startEdgeScroll("down")):t.clientX<n+this.edgeThreshold?(this.edgePressure=Math.min(this.edgeThreshold,this.edgeThreshold-(t.clientX-n)),this.startEdgeScroll("left")):t.clientX>o-this.edgeThreshold?(this.edgePressure=Math.min(this.edgeThreshold,this.edgeThreshold-(o-t.clientX)),this.startEdgeScroll("right")):this.stopEdgeScroll()}insert(t,e){const s=t.closest(this.selectorParent),i=this.getChildren(s);this.indexGrab=i.indexOf(t);const r=i.filter(e=>e!==t);this.elDrop=e?.closest(`${this.selectorItemsFull}, ${this.selectorParent}`);const l=this.elDrop?.matches(this.selectorParent);this.elDropParent=this.elDrop?.closest(this.selectorParent);const n=this.getChildren(this.elDropParent),o=s===this.elDropParent;this.indexDrop=l?Math.max(0,o?r.length:n.length):n.indexOf(this.elDrop),this.elGhost?.animate([{scale:1}],{duration:0,fill:"forwards"});const h=this.elGhost?.getBoundingClientRect();if(this.affectedElements=[],this.swap)this.affectedElements=this.elDrop?[this.elDrop]:[];else if(o){const t=l?this.indexGrab:Math.min(this.indexDrop,this.indexGrab),e=l?r.length:Math.max(this.indexDrop,this.indexGrab);this.affectedElements=r.slice(t,e)}else this.affectedElements=[...r.slice(this.indexGrab),...n.slice(this.indexDrop)];const a=this.checkValidity({el:e}),c=this.onBeforeDrop?.call(this,{elGrab:this.elGrab,elGrabParent:s,elGhost:this.elGhost,elDrop:this.elDrop,elDropParent:this.elDropParent,indexGrab:this.indexGrab,indexDrop:this.indexDrop,isValidTarget:a,isSameParent:o,event:this._currentEvent})??!0,d=o&&this.indexGrab===this.indexDrop,p=Boolean(this.elDrop)&&a&&c&&!d;if(p){const e=this.affectedElements.map(t=>{const{x:e,y:s}=t.getBoundingClientRect();return{el:t,x:e,y:s}});if(this.swap&&!l){const e=t.nextSibling;this.elDropParent?.insertBefore(t,this.elDrop.nextSibling),s?.insertBefore(this.elDrop,e)}else l?this.elDropParent?.append(t):o?this.elDropParent?.insertBefore(t,this.indexDrop<this.indexGrab?this.elDrop:this.elDrop.nextSibling):this.elDropParent?.insertBefore(t,this.elDrop);e.forEach(e=>{e.el!==t&&this.animateItem(e)})}if(h){t.classList.add(`${this.classAnimatedDrop}`);const e=this.animateItem({el:t,x:h.left,y:h.top});e?e.addEventListener("finish",()=>{t.classList.remove(`${this.classAnimatedDrop}`),this.onAnimationEnd?.call(this)}):t.classList.remove(`${this.classAnimatedDrop}`)}return this.removeGhost(),p}grab=t=>{if(this.elGrab)return;const e=t.target,s=e.closest(`${this.selectorItemsFull}`);if(Boolean(e!==s&&(this.selectorIgnoreTarget&&e.closest(this.selectorIgnoreTarget)||this.selectorIgnoreFields&&e.closest(this.selectorIgnoreFields)))||!s||s.parentElement!==this.elGrabParent)return;const i=s.querySelector(this.selectorHandler),r=i?.checkVisibility?.(),l=Boolean(i),n=e?.closest(this.selectorHandler);if(l&&r&&!n)return;const{clientX:o,clientY:h}=t;this.pointerGrab={clientX:o,clientY:h},this.elGrab=s;const a=this.getChildren(this.elGrabParent);this.indexGrab=a.indexOf(this.elGrab);this.onBeforeGrab?.call(this,{elGrab:this.elGrab,elGrabParent:this.elGrabParent,indexGrab:this.indexGrab,event:t})??!0?(t.preventDefault(),this.elGrab.classList.add(this.classActive),this.elGrab.style.cursor="move",this.elGrab.style.userSelect="none","mouse"===t.pointerType&&(this.isScrollPrevented=!0),this.onGrab?.call(this,{elGrab:this.elGrab,elGrabParent:this.elGrabParent,indexGrab:this.indexGrab,event:t})):this.reset()};move=t=>{if(!this.elGrab||!this.isScrollPrevented||this.hasPointerMoved&&!this.elGrab?.hasPointerCapture(t.pointerId))return;const e=this.isSignificantMove(this.pointerGrab,t,this.moveThreshold);!this.hasPointerMoved&&e&&(this.hasPointerMoved=!0,this.elGrab.setPointerCapture(t.pointerId),this.elGrab.classList.add(this.classGrab),this.insertGhost());const{clientX:s,clientY:i}=t,r=this.checkValidity({clientX:s,clientY:i});this.elGhost&&this.pointerGrab&&(this.elGhost.style.translate=`${s-this.pointerGrab.clientX}px ${i-this.pointerGrab.clientY}px`,this.elGhost.classList.toggle(this.classInvalid,!r)),this.elGrab.style.cursor=r?"grab":"not-allowed";const l=document.elementFromPoint(s,i),n=l?.closest(`${this.selectorItemsFull}, ${this.selectorParent}`);n!==this.elTarget&&(this.elTarget?.classList.remove(this.classTarget),r&&(this.elTarget=n,this.elTarget?.classList.add(this.classTarget))),this.handleScrollParent(t),this.onMove?.call(this,{elGrab:this.elGrab,elGrabParent:this.elGrabParent,elGhost:this.elGhost,elTarget:this.elTarget,isValidTarget:r,event:t})};drop=t=>{if(!this.elGrab)return;this.stopEdgeScroll(),this.isScrollPrevented=!1,this.elGrab.style.removeProperty("user-select"),this.elGrab.style.removeProperty("cursor"),this.elGrab.classList.remove(this.classActive,this.classGrab,this.classTouch),this.elTarget?.classList.remove(this.classTarget);const{clientX:e,clientY:s}=t,i=document.elementFromPoint(e,s);this._currentEvent=t;this.insert(this.elGrab,i)&&this.onDrop?.call(this,{elGrab:this.elGrab,elGrabParent:this.elGrabParent,elDrop:this.elDrop,elDropParent:this.elDropParent,indexGrab:this.indexGrab,indexDrop:this.indexDrop,event:t}),this.reset(),this.removeGhost()};handleTouchStart=t=>{if(!this.elGrab||this.touchStart)return;const{clientX:e,clientY:s}=t.touches[0];this.touchStart={clientX:e,clientY:s},this.moveTimeout&&clearTimeout(this.moveTimeout),this.moveTimeout=setTimeout(()=>{this.hasTouchMoved||(this.elGrab?.classList.add(`${this.classTouch}`),this.isScrollPrevented=!0)},this.grabTimeout)};handleTouchMove=t=>{if(!this.elGrab||!this.touchStart)return;if(this.isScrollPrevented)return void(t.cancelable&&t.preventDefault());const e=this.isSignificantMove(this.touchStart,t.touches[0],this.scrollThreshold);!this.hasTouchMoved&&e&&(this.hasTouchMoved=!0,clearTimeout(this.moveTimeout),this.moveTimeout=void 0)};sort(t){const e=this.getChildren(this.elGrabParent),s=e.map(t=>{const{x:e,y:s}=t.getBoundingClientRect();return{el:t,x:e,y:s}}),i=[...e].sort(t);return this.elGrabParent.append(...i),s.forEach(t=>this.animateItem(t)),i}reset(){this.elGhost=null,this.elGrab=null,this.elTarget=null,this.elDrop=null,this.elDropParent=null,this.indexGrab=-1,this.indexDrop=-1,this.affectedElements=[],this.scrollParent=null,this.scrollDirection=null,this.scrollAnim=null,this.edgePressure=0,this.moveTimeout=void 0,this.isScrollPrevented=!1,this.pointerGrab=null,this.touchStart=null,this.hasPointerMoved=!1,this.hasTouchMoved=!1}init(t){this.destroy();const e=this.parseDataAttribute(this.elGrabParent);Object.assign(this,t,e),this.reset(),"."!==this.selectorParent[0]?.trim()||this.elGrabParent.matches(this.selectorParent)||(this.elGrabParent.classList.add(`${this.selectorParent.replace(/^\./,"")}`),this.destroyParentClass=!0),this.selectorItems=(this.selectorItems??"*").replace(/^(?! *>)/,"> $&"),this.selectorItemsFull=`${this.selectorParent}${this.selectorItems}${this.selectorItemsIgnore?`:not(${this.selectorItemsIgnore})`:""}`,this.elGrabParent&&(this.elGrabParent.addEventListener("touchstart",this.handleTouchStart),this.elGrabParent.addEventListener("touchmove",this.handleTouchMove),this.elGrabParent.addEventListener("pointerdown",this.grab),this.elGrabParent.addEventListener("pointermove",this.move),this.elGrabParent.addEventListener("pointerup",this.drop),this.elGrabParent.addEventListener("pointercancel",this.drop),""!==this.group&&(this.elGrabParent.dataset.jsortGroup=this.group))}destroy(){this.removeGhost(),this.elGrabParent&&(this.elGrabParent.removeEventListener("touchstart",this.handleTouchStart),this.elGrabParent.removeEventListener("touchmove",this.handleTouchMove),this.elGrabParent.removeEventListener("pointerdown",this.grab),this.elGrabParent.removeEventListener("pointermove",this.move),this.elGrabParent.removeEventListener("pointerup",this.drop),this.elGrabParent.removeEventListener("pointercancel",this.drop),""!==this.group&&delete this.elGrabParent.dataset.jsortGroup,this.destroyParentClass&&this.elGrabParent.classList.remove(`${this.selectorParent.replace(/^\./,"")}`))}}export{t as default};