UNPKG

timebrush

Version:

🖌️ Excel-style timetable grid with draw and erase support

1 lines 5.43 kB
window.TimeBrush=function(){function t(t){if(this.container=document.querySelector(t.selector),!this.container)return;const e=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],i=t.firstDay||"Sunday",s=e.indexOf(i);this.days=t.days||[...e.slice(s),...e.slice(0,s)],this.fetchUrl=t.fetchUrl,this.saveUrl=t.saveUrl,this.timeStep=t.timeStep||10,this.locale=t.locale||"en-US",this.timeFormat=t.timeFormat||"HH:mm",this.labels=t.labels||{draw:"✍️ Draw",erase:"❌ Erase",save:"💾 Save"},this.mode="draw",this.selectedCells=new Set,this.gridElement=null,this.init()}return t.prototype.init=function(){this.createToolbar(),this.buildGrid(),this.load()},t.prototype.createToolbar=function(){const t=document.createElement("div"),e=this._uid();t.innerHTML=`\n <label><input type="radio" name="mode-${e}" value="draw" checked> ${this.labels.draw}</label>\n <label style="margin-left: 15px;"><input type="radio" name="mode-${e}" value="erase"> ${this.labels.erase}</label>\n <button style="margin-left: 15px;" onclick="window['${e}'].save()">${this.labels.save}</button>\n `,this.container.appendChild(t),t.addEventListener("change",(t=>{t.target.name.startsWith("mode-")&&this.setMode(t.target.value)}))},t.prototype._uid=function(){return this.uid||(this.uid=this.container.id||"timebrush_"+Math.random().toString(36).substr(2,9)),this.uid},t.prototype.setMode=function(t){this.mode=t},t.prototype.buildGrid=function(){const t=document.createElement("div");t.className="timebrush",this.container.appendChild(t),this.gridElement=t,t.appendChild(this._cell("","header time-col")),this.days.forEach((e=>t.appendChild(this._cell(e,"header cell"))));for(let e=0;e<24;e++)for(let i=0;i<60;i+=this.timeStep){const s=`${String(e).padStart(2,"0")}:${String(i).padStart(2,"0")}`,a=this._rangeLabel(s);t.appendChild(this._cell(a,"time-col")),this.days.forEach((e=>{const i=this._cell("","cell");i.dataset.day=e,i.dataset.time=s,this._attachListeners(i),t.appendChild(i)}))}},t.prototype._formatTime=function(t,e){const i=new Date;return i.setHours(t),i.setMinutes(e),i.toLocaleTimeString(this.locale,{hour:"2-digit",minute:"2-digit",hour12:this.timeFormat.includes("a")})},t.prototype._rangeLabel=function(t){const[e,i]=t.split(":").map(Number);let s=i+this.timeStep-1,a=e;return s>=60&&(a+=Math.floor(s/60),s%=60),`${this._formatTime(e,i)}–${this._formatTime(a,s)}`},t.prototype._cell=function(t,e){const i=document.createElement("div");return i.className=e,i.textContent=t,i},t.prototype._attachListeners=function(t){t.addEventListener("mousedown",(e=>{this.isMouseDown=!0,this.selectionStart={day:t.dataset.day,time:t.dataset.time},this._clearTemp(),e.preventDefault()})),t.addEventListener("mouseover",(()=>{this.isMouseDown&&this.selectionStart&&(this._clearTemp(),this._markRange(this.selectionStart,{day:t.dataset.day,time:t.dataset.time},!0))})),t.addEventListener("mouseup",(()=>{this.selectionStart&&(this._markRange(this.selectionStart,{day:t.dataset.day,time:t.dataset.time},!1),this.selectionStart=null),this.isMouseDown=!1})),document.body.addEventListener("mouseup",(()=>{this.isMouseDown=!1,this.selectionStart=null,this._clearTemp()}))},t.prototype._markRange=function(t,e,i=!1){const s=this.days.indexOf(t.day),a=this.days.indexOf(e.day),n=this._toMinutes(t.time),o=this._toMinutes(e.time),[r,l]=[Math.min(s,a),Math.max(s,a)],[d,c]=[Math.min(n,o),Math.max(n,o)];for(let t=r;t<=l;t++)for(let e=d;e<=c;e+=this.timeStep){const s=this._toTime(e),a=this.days[t],n=`${a}_${s}`,o=this.gridElement.querySelector(`.cell[data-day="${a}"][data-time="${s}"]`);o&&(i?o.classList.add("temp-selected"):"draw"===this.mode?(this.selectedCells.add(n),o.classList.add("selected")):(this.selectedCells.delete(n),o.classList.remove("selected")))}},t.prototype._clearTemp=function(){this.gridElement.querySelectorAll(".temp-selected").forEach((t=>t.classList.remove("temp-selected")))},t.prototype._toMinutes=function(t){const[e,i]=t.split(":").map(Number);return 60*e+i},t.prototype._toTime=function(t){const e=Math.floor(t/60),i=t%60;return`${String(e).padStart(2,"0")}:${String(i).padStart(2,"0")}`},t.prototype.save=function(){const t={};this.selectedCells.forEach((e=>{const[i,s]=e.split("_");t[i]||(t[i]=[]),t[i].push(s)}));const e={};Object.keys(t).forEach((i=>{const s=t[i].sort(),a=[];let n=null;for(let t=0;t<s.length;t++){const e=s[t],i=s[t+1];n||(n=e);const[o,r]=e.split(":").map(Number),[l,d]=i?i.split(":").map(Number):[null,null];if((i?60*(l-o)+(d-r):null)!==this.timeStep){const[t,i]=e.split(":").map(Number);let s=i+this.timeStep-1,o=t;s>=60&&(o+=Math.floor(s/60),s%=60);const r=`${n}:00`,l=`${String(o).padStart(2,"0")}:${String(s).padStart(2,"0")}:59`;a.push([r,l]),n=null}}const o=this.days.indexOf(i);e[o]=a})),fetch(this.saveUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}).then((t=>{t.ok?alert("Saved!"):alert("Failed to save.")}))},t.prototype.load=function(){fetch(this.fetchUrl).then((t=>t.json())).then((t=>{Object.entries(t).forEach((([t,e])=>{const i=this.days[+t];e.forEach((([t,e])=>{const s=this._toMinutes(t),a=this._toMinutes(e);for(let t=s;t<=a;t+=this.timeStep){const e=this._toTime(t),s=`${i}_${e}`;this.selectedCells.add(s);const a=this.gridElement.querySelector(`.cell[data-day="${i}"][data-time="${e}"]`);a&&a.classList.add("selected")}}))}))}))},{init:function(e){const i=new t(e);return window[i._uid()]=i,i}}}();