first commit

This commit is contained in:
2025-11-25 01:21:34 +02:00
commit 4af0db8263
84 changed files with 18906 additions and 0 deletions
+511
View File
@@ -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 });
}
}
+1
View File
@@ -0,0 +1 @@
export { Scheduler } from './Scheduler';
+97
View File
@@ -0,0 +1,97 @@
/* Scheduler UI */
.sp-scheduler {
width: 100%;
-webkit-user-select: none;
user-select: none;
}
.sp-scheduler .header-bar {
display: flex;
justify-content: center;
padding: 10px;
background-color: #d0dbe3;
margin-left: 49px;
}
.sp-scheduler .header-bar .label-title {
font-size: 14px;
font-weight: 600;
text-align: center;
}
.sp-scheduler .sp-scrollbar {
max-height: 600px;
overflow-x: hidden;
}
.sp-scheduler .scale-container {
color: #23272A;
display: flex;
}
.sp-scheduler .scale-holder {
border-right: 1px solid #d0dbe3;
width: 50px;
flex-shrink: 0;
}
.sp-scheduler .scale-hour {
border-bottom: 1px solid #d0dbe3;
font-size: 13px;
font-weight: 600;
color: #6c6c6c;
padding: 4px;
}
.sp-scheduler .scale-hour:last-child {
border-bottom: 0;
}
.sp-scheduler .scale-events {
flex: 1;
position: relative;
}
.sp-scheduler .time-slot {
border-bottom: 1px dotted #d0dbe3;
}
.sp-scheduler .time-slot.selected,
.sp-scheduler .time-slot.main-selected {
background-color: #e8fff2;
}
.sp-scheduler .time-slot.last-line {
border-bottom-style: solid;
}
.sp-scheduler .time-label {
position: absolute;
background-color: #fff;
border-radius: 5px;
box-shadow: 1px 1px 5px 0px rgba(204, 204, 204, 1);
padding: 8px;
font-size: 13px;
white-space: nowrap;
z-index: 101;
}
.sp-scheduler .ev-box {
position: absolute;
border-radius: 2px;
background-color: #c3ffdd;
left: 0;
right: 0;
cursor: pointer;
width: 20%;
}
.sp-scheduler .scale-events.scale-select .ev-box {
pointer-events: none;
}
.sp-scheduler .ev-box .ev-content {
padding: 4px 5px;
font-size: 12px;
}