@xlit/grid
Version:
xlit draggable grid layout custom element
75 lines (65 loc) • 9.01 kB
JavaScript
var S=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var u=(o,i,t,e)=>{for(var r=e>1?void 0:e?A(i,t):i,s=o.length-1,n;s>=0;s--)(n=o[s])&&(r=(e?n(i,t,r):n(r))||r);return e&&r&&S(i,t,r),r};import{LitElement as O,html as x,css as E}from"lit";import{customElement as k,property as w,query as z,state as v}from"lit/decorators.js";import{styleMap as l}from"lit/directives/style-map.js";import{repeat as $}from"lit/directives/repeat.js";var h=class extends Error{};var d=class extends h{};var p=class{constructor(i){this.cols=i;this.items=[]}get maxHeight(){return this.items.reduce((i,t)=>{let e=t.y+t.h;return i>e?i:e},0)}add(i){this.assertItemOutOfBound(i),this.assertDuplicateItem(i),this.assertCollisionFound(i),this.items.push(i)}get(i){let t=this.items.find(e=>e.key===i);if(!t)throw new h("item not found");return t}getCollisions(i){return this.items.filter(t=>t.collide(i))}shiftOthersOnCollision(i){let t=this.getCollisions(i);t.length&&t.forEach(e=>{let r=e.clone();r.y=i.y+i.h,this.move(r)})}move(i){let t=this.get(i.key);this.assertItemOutOfBound(i),this.shiftOthersOnCollision(i),t.x=i.x,t.y=i.y}resize(i){let t=this.get(i.key);this.assertItemOutOfBound(i),this.shiftOthersOnCollision(i),t.w=i.w,t.h=i.h}getMaxHeightAbove(i){return this.items.reduce((t,e)=>{if(e.y>=i.y||e.x+e.w<=i.x||e.x>=i.x+i.w)return t;let r=e.y+e.h;return t>r?t:r},0)}pack(){this.sortedItems().forEach(t=>{t.y=this.getMaxHeightAbove(t)})}sortedItems(){return this.items.slice(0).sort((i,t)=>i.y>t.y||i.y===t.y&&i.x>t.x?1:i.y===t.y&&i.x===t.x?0:-1)}assertItemOutOfBound(i){if(i.x<0)throw new h("item out of bound");if(i.y<0)throw new h("item out of bound");if(i.x+i.w>this.cols)throw new h("item out of bound")}assertDuplicateItem(i){if(this.items.find(t=>t.key===i.key))throw new h("duplicate item key")}assertCollisionFound(i){if(this.getCollisions(i).length)throw new d("item collision found")}};var D=0,f=class o{constructor(i,{x:t=0,y:e=0,w:r=1,h:s=1}={}){this.key=i;this.x=t,this.y=e,this.w=r,this.h=s}static nextId(){return`${D++}`}clone(){return new o(this.key,this)}collide(i){return this.key===i.key?!1:this.x+this.w>i.x&&this.x<i.x+i.w&&this.y+this.h>i.y&&this.y<i.y+i.h}};var c=class extends O{constructor(){super(...arguments);this.cols=12;this.gutter=5;this.hfactor=.5;this.layout=new p(this.cols);this.nextKey=0}get unitGutterDimension(){return{w:this.unitDimension.w+this.gutter,h:this.unitDimension.h+this.gutter}}async connectedCallback(){super.connectedCallback(),await new Promise(t=>requestAnimationFrame(t)),this.calculateViewport(),this.scan(),this.mutationObserver=new MutationObserver(()=>{this.scan().length&&this.requestUpdate()}),this.mutationObserver.observe(this,{childList:!0}),this.resizeObserver=new ResizeObserver(()=>{this.calculateViewport()}),this.resizeObserver.observe(this)}disconnectedCallback(){super.disconnectedCallback(),this.mutationObserver.disconnect(),this.resizeObserver.disconnect()}scan(){let t=[...this.children].filter(e=>{if(e.item)return!1;let r=`${this.nextKey++}`;e.slot=r,e.setAttribute("draggable","true");let s=this.readRectFromItemElement(e),n=50,a=new f(r,s),y=0;for(;y++<n;)try{return this.layout.add(a),e.item=a,!0}catch(m){if(m instanceof d){a=a.clone(),a.y=a.y+1;continue}throw m}throw new h(`try positioning item with no luck after ${n} tries`)});return t.length&&this.layout.pack(),t}readRectFromItemElement(t){let e=t.x??(Number(t.getAttribute("x"))||0),r=t.y??(Number(t.getAttribute("y"))||0),s=t.w??(Number(t.getAttribute("w"))||1),n=t.h??(Number(t.getAttribute("h"))||1);return e+s>this.cols&&(e=this.cols-s),{x:e,y:r,w:s,h:n}}render(){let t=this._calculateContainerStyle();return x`
<div class="container"
style="${t}"
="${this.handleDragStarted}"
="${this.handleDragStarted}"
="${this.handleDragged}"
="${this.handleDragged}"
="${this.handleDropped}"
="${this.handleDropped}">
${$(this.layout.items,e=>e.key,e=>this.renderItem(e))}
${this.renderShadowItem()}
</div>
`}renderItem(t){let e=this._calculateItemStyle(t);return this.dragState&&this.dragState.item.key===t.key&&(this.dragState.kind==="move"?e=this._calculatedMoveItemStyle(t):e=this._calculatedResizeItemStyle(t)),x`
<div class="item ${this.dragState?"dragged":""}" style="${e}">
<div class="item-container">
<slot name="${t.key}"></slot>
<div class="item-resizer" draggable="true" .item="${t}"></div>
</div>
</div>
`}renderShadowItem(){if(this.dragState)return x`<div class="shadow-item" style="${this._calculateItemStyle(this.dragState.item)}"></div>`}_calculateContainerStyle(){return this.container?l({height:this.layout.maxHeight*this.unitGutterDimension.h-this.gutter+"px"}):l({})}_calculatedMoveItemStyle(t){if(!this.dragState)return l({});let e=this.unitGutterDimension,r=this.dragState.pointer.x-this.dragState.offset.x,s=this.dragState.pointer.y-this.dragState.offset.y,n=t.w*e.w-this.gutter,a=t.h*e.h-this.gutter;return l({width:n+"px",height:a+"px",transform:`translate(${r}px, ${s}px)`})}_calculatedResizeItemStyle(t){if(!this.dragState)return l({});let e=this.unitGutterDimension,r=t.x*e.w,s=t.y*e.h,n=this.dragState.pointer.x-this.containerOffset.x-r+15,a=this.dragState.pointer.y-this.containerOffset.y-s+15;return l({width:n+"px",height:a+"px",transform:`translate(${r}px, ${s}px)`})}_calculateItemStyle(t){let e=this.unitGutterDimension,r=t.x*e.w,s=t.y*e.h,n=t.w*e.w-this.gutter,a=t.h*e.h-this.gutter;return l({width:n+"px",height:a+"px",transform:`translate(${r}px, ${s}px)`})}calculateViewport(){let{x:t,y:e,width:r}=this.container.getBoundingClientRect(),s=(r-this.gutter*(this.cols-1))/this.cols,n=s*this.hfactor;this.unitDimension={w:s,h:n},this.containerOffset={x:t,y:e}}async handleDragStarted(t){let e=t.composedPath()[0];if(!e?.item)return;if(t instanceof DragEvent&&t.dataTransfer){t.dataTransfer.effectAllowed="move";let g=document.createElement("img");g.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",t.dataTransfer.setDragImage(g,0,0)}let r=e.matches(".item-resizer")?"resize":"move";this.classList.add(r==="resize"?"grid-resize":"grid-move");let s=e.item,n=this.unitGutterDimension,a=s.x*n.w,y=s.y*n.h,m=b(t),I={x:m.x-a,y:m.y-y};this.dragState={kind:r,item:s,offset:I,pointer:m},await new Promise(g=>requestAnimationFrame(g)),this.requestUpdate()}async handleDragged(t){if(t.preventDefault(),!this.dragState)return;let e=b(t);if(!(this.dragState.pointer.x===e.x&&this.dragState.pointer.y===e.y)){if(this.dragState.pointer=e,this.requestUpdate(),await new Promise(r=>requestAnimationFrame(r)),this.dragState.kind==="move"){let r=this.pxToUnit({x:e.x-this.dragState.offset.x,y:e.y-this.dragState.offset.y});try{let s=this.dragState.item.clone();s.x=r.x,s.y=r.y,this.layout.move(s),this.layout.pack()}catch{}}else{let r=this.pxToUnit({x:e.x-this.containerOffset.x,y:e.y-this.containerOffset.y});try{let s=this.dragState.item.clone();s.w=(r.x>=1?r.x:1)-s.x,s.h=r.y-s.y,this.layout.resize(s),this.layout.pack()}catch{}}await new Promise(r=>requestAnimationFrame(r)),this.requestUpdate()}}async handleDropped(t){t.preventDefault(),this.dragState&&(this.dragState=void 0,this.classList.remove("grid-resize","grid-move"),await new Promise(e=>requestAnimationFrame(e)),this.requestUpdate())}pxToUnit(t){let e=this.unitGutterDimension,r=Math.round(t.x/e.w),s=Math.round(t.y/e.h);return{x:r,y:s}}};c.styles=[E`
* {
box-sizing: border-box;
}
.container {
position: relative;
background-color: #eee;
}
:host(.grid-move) .container {
cursor: move;
}
:host(.grid-resize) .container {
cursor: se-resize
}
.item {
position: absolute;
transition: transform 200ms ease;
cursor: move;
}
.item.dragged {
transition: none;
z-index: 999;
}
.item-container {
position: relative;
width: 100%;
height: 100%;
}
.item-resizer {
width: 10px;
height: 10px;
position: absolute;
bottom: 5px;
right: 5px;
border-bottom: 2px solid grey;
border-right: 2px solid grey;
cursor: se-resize;
}
.shadow-item {
position: absolute;
background-color: red;
opacity: 0.5;
transition: transform 200ms ease;
}
`],u([z(".container")],c.prototype,"container",2),u([w({type:Number})],c.prototype,"cols",2),u([w({type:Number})],c.prototype,"gutter",2),u([w({type:Number})],c.prototype,"hfactor",2),u([v()],c.prototype,"layout",2),u([v()],c.prototype,"containerOffset",2),u([v()],c.prototype,"unitDimension",2),c=u([k("xlit-grid")],c);function b(o){return o instanceof DragEvent?{x:o.clientX,y:o.clientY}:{x:o.touches[0].clientX,y:o.touches[0].clientY}}export{c as Grid,h as GridError,d as ItemCollisionGridError};
//# sourceMappingURL=index.js.map