import './scheduler.css'; import { El } from '../../utils/dom'; import { Boot } from '../../core/Boot'; export class Scheduler { constructor(target, opts) { this.target = target; if (this.target.dxData('scheduler')) { return; } this.opts = { ... { startTime: [8, 0], endTime: [21, 30], unitTime: 15, cellSize: 22, maxEvents: 1, title: 'Title' }, ... opts }; this.slots = 60 / this.opts.unitTime; if (!Number.isInteger(this.slots)) { console.log('Invalid unit time'); return; } this.target.dxData('scheduler', this); this.data = {}; this.group = []; this.ui = {}; this.enabled = true; this.busy = false; this.widget = Boot.intData?.widget?.scheduler?.children; if (!this.widget) { return; } this.resetSelection(); this.renderContainer(); this.setUiEvents(); } renderContainer() { this.ui.wrapper = this.target.appendChild( El('div', { className: 'sp-scheduler' }) ); this.ui.header = this.ui.wrapper.appendChild( El('div', { className: 'header-bar' }, El('span', { className: 'label-title' }, this.opts.title) ) ) this.ui.scrollbar = this.ui.wrapper.appendChild( El('div', { className: 'sp-scrollbar' }) ); this.ui.container = this.ui.scrollbar.appendChild( El('div', { className: 'scale-container' }) ); this.ui.scale = this.ui.container.appendChild( El('div', { className: 'scale-holder' }) ); this.ui.events = this.ui.container.appendChild( El('div', { className: 'scale-events' }) ); let remSlots = Math.floor(this.opts.startTime[1] / this.opts.unitTime); let unitHeight = this.opts.cellSize * this.slots; let i; for (i = this.opts.startTime[0]; i <= this.opts.endTime[0]; i++) { let mins = 0; let height = unitHeight; if (i == this.opts.startTime[0] && this.opts.startTime[1] != 0) { height -= this.opts.cellSize * remSlots; mins = this.opts.startTime[1]; } this.ui.scale.append( El('div', { className: 'scale-hour', style: 'height: '+ height + 'px;' }, i.toString().padStart(2, '0') + ':' + mins.toString().padStart(2, '0') ) ); } let n = remSlots; let hours = this.opts.startTime[0]; let mins = 0; let uiSlot; for (; n < this.slots * (this.opts.endTime[0] - this.opts.startTime[0]) ; n++) { uiSlot = El('div', { className: 'time-slot', style: 'height: '+ this.opts.cellSize + 'px;' }); uiSlot.dataset.time = hours.toString().padStart(2, '0') + ':' + mins.toString().padStart(2, '0'); mins += this.opts.unitTime; if ((n + 1) % this.slots == 0) { hours++; mins = 0; uiSlot.className += ' last-line'; } this.ui.events.append( uiSlot ); } remSlots = Math.ceil(this.opts.endTime[1] / this.opts.unitTime); for (n = 0; n < remSlots; n++) { uiSlot = El('div', { className: 'time-slot', style: 'height: '+ this.opts.cellSize + 'px;' }); uiSlot.dataset.time = hours.toString().padStart(2, '0') + ':' + mins.toString().padStart(2, '0'); mins += this.opts.unitTime; this.ui.events.append( uiSlot ); } this.trigger('onInit'); } setUiEvents() { this.ui.events.dxOn('mousedown', '.time-slot', (e, t) => !e.button && this.mouseDownHandler(e, t)); this.ui.events.dxOn('mouseup', e => !e.button && this.selTarget && this.mouseUpHandler()); this.ui.events.dxOn('mouseleave', _ => this.selTarget && this.leaveSelection()); this.ui.events.dxOn('click', '.ev-box', (_, t) => this.eventClickHandler(t) ); } mouseUpHandler() { this.ui.events.dxOff('mouseover'); this.ui.events.dxOff('mousemove'); this.ui.events.classList.remove('scale-select'); this.ui.label.classList.add('hide'); this.trigger('onSlotSelect'); this.selTarget = null; } mouseDownHandler(e, t) { if (this.hasOverlap(t)) { return; } this.selectedTime = {} t.classList.add('main-selected'); this.selTarget = t; this.slotStart = t; this.slotEnd = t.dxNext('.time-slot'); this.setSelectedData(); this.showLabel(); this.setLabelPosition(e); this.ui.events.dxOn('mouseover', '.time-slot', (_, t) => this.mouseOverHandler(t)); this.ui.events.dxOn('mousemove', e => this.setLabelPosition(e)); this.ui.events.classList.add('scale-select'); } mouseOverHandler(t) { if (!this.selTarget) { return; } let oldNext = t.dxNext('.selected'); while (oldNext) { oldNext.classList.remove('selected'); oldNext = oldNext.dxNext('.selected'); } let oldPrev = t.dxPrev('.selected'); while (oldPrev) { oldPrev.classList.remove('selected'); oldPrev = oldPrev.dxPrev('.selected'); } let ref = this.selTarget.compareDocumentPosition(t); if (ref & Node.DOCUMENT_POSITION_PRECEDING) { let prev = this.selTarget.dxPrev('.time-slot'); while (prev) { if (this.hasOverlap(prev)) { return; } prev.classList.add('selected'); if (prev === t) { this.slotStart = t; this.setSelectedData(); this.showLabel(); return; } prev = prev.dxPrev('.time-slot'); } } else if (ref & Node.DOCUMENT_POSITION_FOLLOWING) { this.slotStart = this.selTarget; let next = this.selTarget.dxNext('.time-slot'); while (next) { if (this.hasOverlap(next)) { return; } next.classList.add('selected'); if (next === t) { this.slotEnd = t.dxNext('.time-slot'); this.setSelectedData(); this.showLabel(); return; } next = next.dxNext('.time-slot'); } } else { this.slotStart = this.selTarget; this.slotEnd = this.selTarget.dxNext('.time-slot'); this.setSelectedData(); this.showLabel(); } } getFormattedTime(hour, min) { return hour.toString().padStart(2, '0') + ':' + min.toString().padStart(2, '0') } setSelectedData() { this.selectedTime = { start: this.slotStart.dataset.time }; if (this.slotEnd) { this.selectedTime.end = this.slotEnd.dataset.time } else { this.selectedTime.end = this.getFormattedTime(this.opts.endTime[0], this.opts.endTime[1]); } } showLabel() { if (!this.ui.label) { this.ui.label = this.ui.events.appendChild( El('div', { class: 'time-label' }) ); } this.ui.label.textContent = this.selectedTime.start + ' - ' + this.selectedTime.end + ' ( ' + this.getFormattedLabel() + ' )'; this.ui.label.classList.remove('hide'); } setLabelPosition(e) { this.ui.label.style.top = (e.clientY - this.ui.events.dxOffset().top + 14) + 'px'; this.ui.label.style.left = (e.pageX - this.ui.events.dxOffset().left + 14) + 'px'; } getFormattedLabel() { let raw = ''; let start = this.selectedTime.start.split(':').map(t => parseInt(t)); let end = this.selectedTime.end.split(':').map(t => parseInt(t)); let total = (end[0] * 60 + end[1]) - (start[0] * 60 + start[1]); let hours = total >= 60 ? Math.floor(total / 60) : 0; let mins = total % 60; if (!hours) { raw = this.widget.label.minutes.replace('%minutes', mins); } else if (hours == 1) { raw = !mins ? this.widget.label.one_hour : this.widget.label.one_hour_minutes.replace('%minutes', mins); } else { raw = !mins ? this.widget.label.hours.replace('%hours', hours) : this.widget.label.hours_minutes.replace('%hours', hours).replace('%minutes', mins); } return raw; } addEventBox(id) { let evBox = this.ui.events.appendChild( El('div', { class: 'ev-box' }, El('div', { class: 'ev-content' }, ... [...arguments].slice(1) ) ) ); evBox.dataset.id = id; this.resizeEvents(); this.clearSelection(); } addEvent(id, startTime, endTime, data) { this.data.push([ id, startTime, endTime, data ]); this.addEventBox(id, El('span', { class: 'ev-time text-semibold' }, startTime + ' - ' + endTime), ' - ', El('span', { class: 'ev-data' }, data) ); } updateEvent(id, startTime, endTime, data) { let index = this.data.findIndex(item => id == item[0]); if (index == -1) { return; } this.data[ index ] = [ id, startTime, endTime, data ]; this.resizeEvents(); } deleteEvent(ev) { let index = this.data.findIndex(item => ev.id == item[0]); if (index == -1) { return; } this.data.splice(index, 1); ev.container.remove(); this.resizeEvents(); this.trigger('onEventDelete'); } hasOverlap(target) { if (this.opts.maxEvents == -1) { return false; } let time = target.dataset.time; let found = 0; for (const item of this.data) { if (time >= item[1] && time < item[2]) { found++; } } return found >= this.opts.maxEvents; } findSelection(callback) { if (this.slotStart) { callback(this.slotStart); } let next = this.slotStart.dxNext('.time-slot'); while (next) { if (next === this.slotEnd) { break; } callback(next); next = next.dxNext('.time-slot'); } } eventClickHandler(t) { let id = t.dataset.id; let index = this.data.findIndex(item => id == item[0]); if (index == -1) { return; } let item = this.data[ index ]; this.trigger('onEventClick', this, { scheduler: this, container: t, content: t.dxChildren('.ev-content'), id: id, interval: { start: item[1], end: item[2] } } ); } getStartTime() { return this.selectedTime.start; } getEndTime() { return this.selectedTime.end; } getData() { return this.data; } leaveSelection() { this.ui.events.dxOff('mouseover'); this.ui.events.dxOff('mousemove'); this.ui.label.classList.add('hide'); this.findSelection(t => t.classList.remove('selected', 'main-selected')); //this.ui.events.dxChildren('.time-slot', t => t.classList.remove('selected', 'main-selected')); this.resetSelection(); } resetSelection() { this.selTarget = null; this.slotStart = null; this.slotEnd = null; this.selectedTime = {}; } clearSelection() { if (!this.slotStart) { return; } //this.findSelection(t => t.classList.remove('selected', 'main-selected')); this.ui.events.dxChildren('.time-slot', t => t.classList.remove('selected', 'main-selected')); this.resetSelection(); this.trigger('onSlotClear'); } getOverlapIndex(startTime, endTime, id) { let index = -1; for (let i = 0; i < this.group.length; i++) { for (const item of this.group[i]) { if (id == item[0]) { index = i; continue; } if ((startTime <= item[1] && endTime > item[2]) || (startTime < item[2] && endTime > item[2]) || (startTime >= item[1] && endTime <= item[2]) ) { return i; } } } return index != -1 ? index : this.group.length; } disable() { if (this.enabled) { this.enabled = false; this.ui.events.classList.add('scale-disabled'); } } enable() { if (!this.enabled) { this.enabled = true; this.ui.events.classList.remove('scale-disabled'); } } load() { if (!this.busy) { this.busy = true; this.ui.wrapper.append( El('div', { class: 'scheduler-modal' }, El('div', { class: 'sp-loader' }) ) ); } } unload() { if (this.busy) { this.busy = false; this.ui.wrapper.dxChildren('.scheduler-modal').remove(); } } sortData() { this.data = this.data.sort((a, b) => { if (a[1] < b[1]) { return -1; } else if (a[1] > b[1]) { return 1; } return 0; }); } update(data) { this.data = data; this.group = []; this.sortData(); this.data.forEach(item => { let index = this.getOverlapIndex(item[1], item[2], null); if (!this.group[ index ]) { this.group[ index ] = []; } this.group[ index ].push(item); }, this); for (const group of this.group) { let unit = 100 / group.length; let space = 5 / group.length; let index = 0; for (const item of group) { let evBox = this.ui.events.appendChild( El('div', { class: 'ev-box' }, El('div', { class: 'ev-content' }, El('span', { class: 'ev-time text-semibold' }, item[1] + ' - ' + item[2]), ' - ', El('span', { class: 'ev-data' }, item[3]) ) ) ); evBox.dataset.id = item[0]; const slotStart = this.ui.events.dxChildren('.time-slot[data-time="'+ item[1] +'"]'); const slotEnd = this.ui.events.dxChildren('.time-slot[data-time="'+ item[2] +'"]'); evBox.style.top = slotStart.offsetTop + 'px'; evBox.style.left = (index * (unit - space) ) + '%'; evBox.style.width = (unit - space - 0.5) + '%' evBox.style.height = (slotEnd.offsetTop - slotStart.offsetTop - 1) + 'px'; index++; } } } reset() { this.data = []; this.group = []; this.ui.events.dxChildren('.ev-box', t => t.remove()); } resizeEvents() { this.group = []; this.sortData(); this.data.forEach(item => { let index = this.getOverlapIndex(item[1], item[2], null); if (!this.group[ index ]) { this.group[ index ] = []; } this.group[ index ].push(item); }, this); for (const group of this.group) { let unit = 100 / group.length; let space = 5 / group.length; let index = 0; for (const item of group) { let evBox = this.ui.events.dxChildren('.ev-box[data-id="'+ item[0] +'"]') if (!evBox) { continue; } let slotStart = this.ui.events.dxChildren('.time-slot[data-time="'+ item[1] +'"]'); let slotEnd = this.ui.events.dxChildren('.time-slot[data-time="'+ item[2] +'"]'); evBox.style.top = slotStart.offsetTop + 'px'; evBox.style.left = (index * (unit - space) ) + '%'; evBox.style.width = (unit - space - 0.5) + '%' evBox.style.height = (slotEnd.offsetTop - slotStart.offsetTop - 1) + 'px'; index++; } } } trigger(event) { let handler = this.opts[ event ]; let args = [... arguments ].slice(1); if (!args.length) { args = [ this ]; } if (typeof handler === 'function') { handler(... args); } this.target.dxTrigger(event, { scheduler: this, args: args }); } }