695 lines
14 KiB
JavaScript
695 lines
14 KiB
JavaScript
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 });
|
|
}
|
|
} |