first commit
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
import './scheduler.css';
|
||||
|
||||
import { El } from '../../utils/dom';
|
||||
|
||||
export class Scheduler {
|
||||
|
||||
/**
|
||||
* Scheduler, Constructor
|
||||
*/
|
||||
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.ui = {};
|
||||
|
||||
this.eventList = [];
|
||||
|
||||
this.resetSelection();
|
||||
|
||||
this.renderContainer();
|
||||
|
||||
this.setUiEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler, Render Container
|
||||
*/
|
||||
renderContainer() {
|
||||
|
||||
this.ui.wrapper = this.target.appendChild( El('div', { class: 'sp-scheduler' }) );
|
||||
|
||||
this.ui.header = this.ui.wrapper.appendChild(
|
||||
El('div', { class: 'header-bar' },
|
||||
El('span', { class: 'label-title' }, this.opts.title)
|
||||
)
|
||||
)
|
||||
|
||||
this.ui.scrollbar = this.ui.wrapper.appendChild( El('div', { class: 'sp-scrollbar' }) );
|
||||
this.ui.container = this.ui.scrollbar.appendChild( El('div', { class: 'scale-container' }) );
|
||||
this.ui.scale = this.ui.container.appendChild( El('div', { class: 'scale-holder' }) );
|
||||
this.ui.events = this.ui.container.appendChild( El('div', { class: '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', { class: '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', { class: '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', { class: '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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduler, Set UI Events
|
||||
*/
|
||||
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('slotSelected');
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedData() {
|
||||
|
||||
this.selectedTime = {
|
||||
start: this.slotStart.dataset.time
|
||||
};
|
||||
|
||||
if (this.slotEnd) {
|
||||
this.selectedTime.end = this.slotEnd.dataset.time
|
||||
}
|
||||
else {
|
||||
this.selectedTime.end = this.opts.endTime[0].toString().padStart(2, '0') + ':' + this.opts.endTime[1].toString().padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
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.getTimeFormat() + ' )';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
getTimeFormat() {
|
||||
|
||||
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 > 0) {
|
||||
|
||||
if (hours == 1) {
|
||||
raw += 'o oră';
|
||||
}
|
||||
else {
|
||||
raw += hours + ' ore';
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
raw += ' și ';
|
||||
}
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
raw += mins + ' minute';
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
addEvent(content) {
|
||||
|
||||
let count = 0;
|
||||
this.findSelection(_ => count++);
|
||||
|
||||
let selStart = this.selectedTime.start;
|
||||
let selEnd = this.selectedTime.end;
|
||||
let first = null;
|
||||
|
||||
this.ui.events.dxChildren('.ev-box', t => {
|
||||
|
||||
if ((selStart <= t.dataset.start && selEnd > t.dataset.start)
|
||||
|| (selStart < t.dataset.end && selEnd > t.dataset.end)
|
||||
|| (selStart >= t.dataset.start && selEnd <= t.dataset.end)
|
||||
) {
|
||||
first = t;
|
||||
}
|
||||
});
|
||||
|
||||
let evBox = this.ui.events.appendChild(
|
||||
El('div', { class: 'ev-box' },
|
||||
El('div', { class: 'ev-content' }, content)
|
||||
)
|
||||
);
|
||||
|
||||
let index = first ? first.dataset.index : Date.now();
|
||||
|
||||
evBox.dataset.index = index;
|
||||
evBox.dataset.start = this.selectedTime.start;
|
||||
evBox.dataset.end = this.selectedTime.end;
|
||||
|
||||
evBox.style.top = this.slotStart.offsetTop + 'px';
|
||||
evBox.style.height = (this.opts.cellSize * count - 1) + 'px';
|
||||
|
||||
this.resizeGroup(index);
|
||||
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
hasOverlap(target) {
|
||||
|
||||
if (this.opts.maxEvents == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let time = target.dataset.time;
|
||||
|
||||
let found = 0;
|
||||
|
||||
this.ui.events.dxChildren('.ev-box', t => {
|
||||
if (time >= t.dataset.start && time < t.dataset.end ) {
|
||||
found++;
|
||||
}
|
||||
});
|
||||
|
||||
return found >= this.opts.maxEvents;
|
||||
}
|
||||
|
||||
resizeGroup(index) {
|
||||
|
||||
let cols = this.ui.events.dxChildren('.ev-box[data-index="'+ index +'"]', true);
|
||||
|
||||
cols = cols.sort((a, b) => {
|
||||
|
||||
if (a.dataset.start < b.dataset.start) {
|
||||
return -1;
|
||||
}
|
||||
else if (a.dataset.start > b.dataset.start) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
let unit = 100 / cols.length;
|
||||
let space = 5 / cols.length;
|
||||
|
||||
cols.forEach((t, i) => {
|
||||
|
||||
t.style.width = (unit - space - 0.5) + '%'
|
||||
t.style.left = (i * (unit - space) ) + '%';
|
||||
});
|
||||
}
|
||||
|
||||
deleteEvent(ref) {
|
||||
|
||||
ref.container.remove();
|
||||
this.resizeGroup(ref.index);
|
||||
|
||||
this.trigger('eventDeleted');
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
this.trigger('eventClick',
|
||||
this, {
|
||||
container: t,
|
||||
content: t.dxChildren('.ev-content'),
|
||||
index: t.dataset.index,
|
||||
interval: {
|
||||
start: t.dataset.start,
|
||||
end: t.dataset.end
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getStartTime() {
|
||||
return this.selectedTime.start;
|
||||
}
|
||||
|
||||
getEndTime() {
|
||||
return this.selectedTime.end;
|
||||
}
|
||||
|
||||
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.resetSelection();
|
||||
|
||||
this.trigger('slotCleared');
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user