@mintplayer/ng-bootstrap 21.22.0 → 21.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs +20 -20
- package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-alert.mjs +8 -8
- package/fesm2022/mintplayer-ng-bootstrap-alert.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-badge.mjs +5 -5
- package/fesm2022/mintplayer-ng-bootstrap-badge.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs +6 -6
- package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs +4 -4
- package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs +9 -9
- package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-card.mjs +8 -8
- package/fesm2022/mintplayer-ng-bootstrap-card.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs +25 -25
- package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-close.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-close.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs +58 -58
- package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-container.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-container.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-copy.mjs +4 -4
- package/fesm2022/mintplayer-ng-bootstrap-copy.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs +20 -20
- package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs +6 -6
- package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-dock.mjs +789 -1175
- package/fesm2022/mintplayer-ng-bootstrap-dock.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs +15 -15
- package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-enum.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-enum.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs +16 -16
- package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-for.mjs +4 -4
- package/fesm2022/mintplayer-ng-bootstrap-for.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-form.mjs +11 -11
- package/fesm2022/mintplayer-ng-bootstrap-form.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-grid.mjs +26 -26
- package/fesm2022/mintplayer-ng-bootstrap-grid.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs +4 -4
- package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs +14 -14
- package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-let.mjs +4 -4
- package/fesm2022/mintplayer-ng-bootstrap-let.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-linify.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-linify.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs +12 -12
- package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-modal.mjs +24 -24
- package/fesm2022/mintplayer-ng-bootstrap-modal.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs +24 -24
- package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs +5 -5
- package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs +58 -58
- package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs +8 -8
- package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs +40 -40
- package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs +12 -12
- package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs +6 -6
- package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs +5 -5
- package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-popover.mjs +20 -20
- package/fesm2022/mintplayer-ng-bootstrap-popover.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs +30 -30
- package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs +17 -17
- package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-range.mjs +9 -9
- package/fesm2022/mintplayer-ng-bootstrap-range.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-rating.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-rating.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs +25 -25
- package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs +16 -16
- package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs +14 -14
- package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs +24 -24
- package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-select.mjs +19 -19
- package/fesm2022/mintplayer-ng-bootstrap-select.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-select2.mjs +20 -20
- package/fesm2022/mintplayer-ng-bootstrap-select2.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-shell.mjs +11 -11
- package/fesm2022/mintplayer-ng-bootstrap-shell.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs +7 -7
- package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs +6 -6
- package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs +57 -67
- package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-table.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-table.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs +8 -8
- package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-toast.mjs +24 -24
- package/fesm2022/mintplayer-ng-bootstrap-toast.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs +22 -22
- package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs +14 -14
- package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs +10 -10
- package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs.map +1 -1
- package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs +1356 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs +3819 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs +731 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs +549 -0
- package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs.map +1 -0
- package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs +3 -3
- package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs.map +1 -1
- package/package.json +20 -6
- package/types/mintplayer-ng-bootstrap-dock.d.ts +55 -19
- package/types/mintplayer-ng-bootstrap-scheduler.d.ts +2 -2
- package/types/mintplayer-ng-bootstrap-tab-control.d.ts +7 -11
- package/types/mintplayer-ng-bootstrap-web-components-scheduler-core.d.ts +890 -0
- package/types/mintplayer-ng-bootstrap-web-components-scheduler.d.ts +354 -0
- package/types/mintplayer-ng-bootstrap-web-components-splitter.d.ts +165 -0
- package/types/mintplayer-ng-bootstrap-web-components-tab-control.d.ts +95 -0
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard to check if an item is a Resource
|
|
3
|
+
*/
|
|
4
|
+
function isResource(item) {
|
|
5
|
+
return 'events' in item || !('children' in item);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Type guard to check if an item is a ResourceGroup
|
|
9
|
+
*/
|
|
10
|
+
function isResourceGroup(item) {
|
|
11
|
+
return 'children' in item;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default options for the scheduler
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_OPTIONS = {
|
|
18
|
+
initialView: 'week',
|
|
19
|
+
initialDate: new Date(),
|
|
20
|
+
locale: 'en-US',
|
|
21
|
+
firstDayOfWeek: 1,
|
|
22
|
+
timeZone: 'local',
|
|
23
|
+
slotDuration: 1800,
|
|
24
|
+
slotLabelInterval: 3600,
|
|
25
|
+
slotMinTime: '00:00:00',
|
|
26
|
+
slotMaxTime: '24:00:00',
|
|
27
|
+
timeFormat: '24h',
|
|
28
|
+
businessHours: {
|
|
29
|
+
daysOfWeek: [1, 2, 3, 4, 5],
|
|
30
|
+
startTime: '09:00',
|
|
31
|
+
endTime: '17:00',
|
|
32
|
+
},
|
|
33
|
+
height: 'auto',
|
|
34
|
+
contentHeight: 'auto',
|
|
35
|
+
aspectRatio: 1.35,
|
|
36
|
+
expandRows: false,
|
|
37
|
+
headerToolbar: {
|
|
38
|
+
start: 'prev,next today',
|
|
39
|
+
center: 'title',
|
|
40
|
+
end: 'year,month,week,day,timeline',
|
|
41
|
+
},
|
|
42
|
+
editable: true,
|
|
43
|
+
selectable: true,
|
|
44
|
+
selectMirror: true,
|
|
45
|
+
eventDurationEditable: true,
|
|
46
|
+
eventStartEditable: true,
|
|
47
|
+
dragRevertDuration: 500,
|
|
48
|
+
dragScroll: true,
|
|
49
|
+
snapDuration: 1800,
|
|
50
|
+
nowIndicator: true,
|
|
51
|
+
weekNumbers: false,
|
|
52
|
+
weekText: 'W',
|
|
53
|
+
dayMaxEvents: true,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Types
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Service for date calculations and formatting
|
|
60
|
+
*/
|
|
61
|
+
class DateService {
|
|
62
|
+
/**
|
|
63
|
+
* Get the start of the week for a given date
|
|
64
|
+
*/
|
|
65
|
+
getWeekStart(date, firstDayOfWeek = 1) {
|
|
66
|
+
const d = new Date(date);
|
|
67
|
+
const day = d.getDay();
|
|
68
|
+
const diff = (day < firstDayOfWeek ? 7 : 0) + day - firstDayOfWeek;
|
|
69
|
+
d.setDate(d.getDate() - diff);
|
|
70
|
+
d.setHours(0, 0, 0, 0);
|
|
71
|
+
return d;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get all days in a week starting from a given date
|
|
75
|
+
*/
|
|
76
|
+
getWeekDays(date, firstDayOfWeek = 1) {
|
|
77
|
+
const weekStart = this.getWeekStart(date, firstDayOfWeek);
|
|
78
|
+
const days = [];
|
|
79
|
+
for (let i = 0; i < 7; i++) {
|
|
80
|
+
const day = new Date(weekStart);
|
|
81
|
+
day.setDate(weekStart.getDate() + i);
|
|
82
|
+
days.push(day);
|
|
83
|
+
}
|
|
84
|
+
return days;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the start of the month for a given date
|
|
88
|
+
*/
|
|
89
|
+
getMonthStart(date) {
|
|
90
|
+
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the end of the month for a given date
|
|
94
|
+
*/
|
|
95
|
+
getMonthEnd(date) {
|
|
96
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all days in a month
|
|
100
|
+
*/
|
|
101
|
+
getMonthDays(date) {
|
|
102
|
+
const start = this.getMonthStart(date);
|
|
103
|
+
const end = this.getMonthEnd(date);
|
|
104
|
+
const days = [];
|
|
105
|
+
const current = new Date(start);
|
|
106
|
+
while (current <= end) {
|
|
107
|
+
days.push(new Date(current));
|
|
108
|
+
current.setDate(current.getDate() + 1);
|
|
109
|
+
}
|
|
110
|
+
return days;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get weeks for a month view (includes days from adjacent months)
|
|
114
|
+
*/
|
|
115
|
+
getMonthWeeks(date, firstDayOfWeek = 1) {
|
|
116
|
+
const monthStart = this.getMonthStart(date);
|
|
117
|
+
const monthEnd = this.getMonthEnd(date);
|
|
118
|
+
const viewStart = this.getWeekStart(monthStart, firstDayOfWeek);
|
|
119
|
+
const weeks = [];
|
|
120
|
+
let current = new Date(viewStart);
|
|
121
|
+
// Generate 6 weeks to ensure consistent grid
|
|
122
|
+
for (let w = 0; w < 6; w++) {
|
|
123
|
+
const week = [];
|
|
124
|
+
for (let d = 0; d < 7; d++) {
|
|
125
|
+
week.push(new Date(current));
|
|
126
|
+
current.setDate(current.getDate() + 1);
|
|
127
|
+
}
|
|
128
|
+
weeks.push(week);
|
|
129
|
+
// Stop if we've passed the end of the month and completed the week
|
|
130
|
+
if (current > monthEnd && weeks.length >= 4) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return weeks;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get the start of the year for a given date
|
|
138
|
+
*/
|
|
139
|
+
getYearStart(date) {
|
|
140
|
+
return new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get all months in a year
|
|
144
|
+
*/
|
|
145
|
+
getYearMonths(date) {
|
|
146
|
+
const months = [];
|
|
147
|
+
for (let i = 0; i < 12; i++) {
|
|
148
|
+
months.push(new Date(date.getFullYear(), i, 1));
|
|
149
|
+
}
|
|
150
|
+
return months;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get time slots for a day
|
|
154
|
+
*/
|
|
155
|
+
getTimeSlots(date, slotDuration = 1800, minTime = '00:00:00', maxTime = '24:00:00') {
|
|
156
|
+
const slots = [];
|
|
157
|
+
const [minHour, minMinute] = minTime.split(':').map(Number);
|
|
158
|
+
const [maxHour, maxMinute] = maxTime.split(':').map(Number);
|
|
159
|
+
const startSeconds = minHour * 3600 + minMinute * 60;
|
|
160
|
+
const endSeconds = maxHour * 3600 + maxMinute * 60;
|
|
161
|
+
for (let seconds = startSeconds; seconds < endSeconds; seconds += slotDuration) {
|
|
162
|
+
const slotStart = new Date(date);
|
|
163
|
+
slotStart.setHours(0, 0, 0, 0);
|
|
164
|
+
slotStart.setSeconds(seconds);
|
|
165
|
+
const slotEnd = new Date(slotStart);
|
|
166
|
+
slotEnd.setSeconds(slotStart.getSeconds() + slotDuration);
|
|
167
|
+
slots.push({ start: slotStart, end: slotEnd });
|
|
168
|
+
}
|
|
169
|
+
return slots;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generate a scheduler grid for week view
|
|
173
|
+
*/
|
|
174
|
+
getWeekGrid(date, firstDayOfWeek = 1, slotDuration = 1800, minTime = '00:00:00', maxTime = '24:00:00', timeFormat = '24h') {
|
|
175
|
+
const columns = this.getWeekDays(date, firstDayOfWeek);
|
|
176
|
+
const rows = [];
|
|
177
|
+
const allSlots = [];
|
|
178
|
+
// Get time slots for the first day to determine row structure
|
|
179
|
+
const daySlots = this.getTimeSlots(columns[0], slotDuration, minTime, maxTime);
|
|
180
|
+
for (const slot of daySlots) {
|
|
181
|
+
const rowSlots = [];
|
|
182
|
+
for (const day of columns) {
|
|
183
|
+
const slotStart = new Date(day);
|
|
184
|
+
slotStart.setHours(slot.start.getHours(), slot.start.getMinutes(), 0, 0);
|
|
185
|
+
const slotEnd = new Date(day);
|
|
186
|
+
slotEnd.setHours(slot.end.getHours(), slot.end.getMinutes(), 0, 0);
|
|
187
|
+
const timeSlot = { start: slotStart, end: slotEnd };
|
|
188
|
+
rowSlots.push(timeSlot);
|
|
189
|
+
allSlots.push(timeSlot);
|
|
190
|
+
}
|
|
191
|
+
rows.push({
|
|
192
|
+
time: slot.start,
|
|
193
|
+
label: this.formatTime(slot.start, timeFormat),
|
|
194
|
+
slots: rowSlots,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return { columns, rows, allSlots };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Round a date to the nearest slot
|
|
201
|
+
*/
|
|
202
|
+
roundToSlot(date, slotDuration) {
|
|
203
|
+
const ms = date.getTime();
|
|
204
|
+
const slotMs = slotDuration * 1000;
|
|
205
|
+
const rounded = Math.round(ms / slotMs) * slotMs;
|
|
206
|
+
return new Date(rounded);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Floor a date to the slot start
|
|
210
|
+
*/
|
|
211
|
+
floorToSlot(date, slotDuration) {
|
|
212
|
+
const ms = date.getTime();
|
|
213
|
+
const slotMs = slotDuration * 1000;
|
|
214
|
+
const floored = Math.floor(ms / slotMs) * slotMs;
|
|
215
|
+
return new Date(floored);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Ceiling a date to the slot end
|
|
219
|
+
*/
|
|
220
|
+
ceilToSlot(date, slotDuration) {
|
|
221
|
+
const ms = date.getTime();
|
|
222
|
+
const slotMs = slotDuration * 1000;
|
|
223
|
+
const ceiled = Math.ceil(ms / slotMs) * slotMs;
|
|
224
|
+
return new Date(ceiled);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Detect time format preference based on locale
|
|
228
|
+
* Uses the Intl API to determine if the locale uses 12-hour or 24-hour time
|
|
229
|
+
*/
|
|
230
|
+
detectTimeFormat(locale) {
|
|
231
|
+
const resolvedLocale = locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
|
|
232
|
+
try {
|
|
233
|
+
const options = new Intl.DateTimeFormat(resolvedLocale, { hour: 'numeric' }).resolvedOptions();
|
|
234
|
+
return options.hour12 ? '12h' : '24h';
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Fallback to 24h if detection fails
|
|
238
|
+
return '24h';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Format time according to format preference
|
|
243
|
+
*/
|
|
244
|
+
formatTime(date, format = '24h') {
|
|
245
|
+
const hours = date.getHours();
|
|
246
|
+
const minutes = date.getMinutes();
|
|
247
|
+
if (format === '12h') {
|
|
248
|
+
const period = hours >= 12 ? 'PM' : 'AM';
|
|
249
|
+
const hour12 = hours % 12 || 12;
|
|
250
|
+
return `${hour12}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
251
|
+
}
|
|
252
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Format date for display
|
|
256
|
+
*/
|
|
257
|
+
formatDate(date, locale = 'en-US', options) {
|
|
258
|
+
return date.toLocaleDateString(locale, options);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Format date with weekday
|
|
262
|
+
*/
|
|
263
|
+
formatDateWithWeekday(date, locale = 'en-US') {
|
|
264
|
+
return this.formatDate(date, locale, { weekday: 'short', month: 'short', day: 'numeric' });
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Get month name
|
|
268
|
+
*/
|
|
269
|
+
getMonthName(date, locale = 'en-US', format = 'long') {
|
|
270
|
+
return date.toLocaleDateString(locale, { month: format });
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Get day name
|
|
274
|
+
*/
|
|
275
|
+
getDayName(date, locale = 'en-US', format = 'short') {
|
|
276
|
+
return date.toLocaleDateString(locale, { weekday: format });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if two dates are the same day
|
|
280
|
+
*/
|
|
281
|
+
isSameDay(date1, date2) {
|
|
282
|
+
return (date1.getFullYear() === date2.getFullYear() &&
|
|
283
|
+
date1.getMonth() === date2.getMonth() &&
|
|
284
|
+
date1.getDate() === date2.getDate());
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Check if two dates are the same month
|
|
288
|
+
*/
|
|
289
|
+
isSameMonth(date1, date2) {
|
|
290
|
+
return (date1.getFullYear() === date2.getFullYear() &&
|
|
291
|
+
date1.getMonth() === date2.getMonth());
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Check if a date is today
|
|
295
|
+
*/
|
|
296
|
+
isToday(date) {
|
|
297
|
+
return this.isSameDay(date, new Date());
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if a date is in the past
|
|
301
|
+
*/
|
|
302
|
+
isPast(date) {
|
|
303
|
+
const now = new Date();
|
|
304
|
+
now.setHours(0, 0, 0, 0);
|
|
305
|
+
const check = new Date(date);
|
|
306
|
+
check.setHours(0, 0, 0, 0);
|
|
307
|
+
return check < now;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check if a date falls within a range
|
|
311
|
+
*/
|
|
312
|
+
isInRange(date, start, end) {
|
|
313
|
+
return date >= start && date <= end;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Get the number of days between two dates
|
|
317
|
+
*/
|
|
318
|
+
getDaysDifference(date1, date2) {
|
|
319
|
+
const d1 = new Date(date1);
|
|
320
|
+
d1.setHours(0, 0, 0, 0);
|
|
321
|
+
const d2 = new Date(date2);
|
|
322
|
+
d2.setHours(0, 0, 0, 0);
|
|
323
|
+
return Math.round((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Add days to a date
|
|
327
|
+
*/
|
|
328
|
+
addDays(date, days) {
|
|
329
|
+
const result = new Date(date);
|
|
330
|
+
result.setDate(result.getDate() + days);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Add weeks to a date
|
|
335
|
+
*/
|
|
336
|
+
addWeeks(date, weeks) {
|
|
337
|
+
return this.addDays(date, weeks * 7);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Add months to a date
|
|
341
|
+
*/
|
|
342
|
+
addMonths(date, months) {
|
|
343
|
+
const result = new Date(date);
|
|
344
|
+
result.setMonth(result.getMonth() + months);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Add years to a date
|
|
349
|
+
*/
|
|
350
|
+
addYears(date, years) {
|
|
351
|
+
const result = new Date(date);
|
|
352
|
+
result.setFullYear(result.getFullYear() + years);
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Get week number of the year
|
|
357
|
+
*/
|
|
358
|
+
getWeekNumber(date) {
|
|
359
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
360
|
+
const dayNum = d.getUTCDay() || 7;
|
|
361
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
362
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
363
|
+
return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get seconds from midnight for a date
|
|
367
|
+
*/
|
|
368
|
+
getSecondsFromMidnight(date) {
|
|
369
|
+
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get duration in seconds between two dates
|
|
373
|
+
*/
|
|
374
|
+
getDurationInSeconds(start, end) {
|
|
375
|
+
return Math.round((end.getTime() - start.getTime()) / 1000);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Singleton instance of DateService
|
|
380
|
+
*/
|
|
381
|
+
const dateService = new DateService();
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Service for timeline calculations and event track assignment
|
|
385
|
+
*/
|
|
386
|
+
class TimelineService {
|
|
387
|
+
/**
|
|
388
|
+
* Split an event into daily parts
|
|
389
|
+
* Each part represents one day of a multi-day event
|
|
390
|
+
*/
|
|
391
|
+
splitInParts(event) {
|
|
392
|
+
const parts = [];
|
|
393
|
+
let currentStart = new Date(event.start);
|
|
394
|
+
let dayIndex = 0;
|
|
395
|
+
// Calculate total days
|
|
396
|
+
const totalDays = this.getTotalDays(event.start, event.end);
|
|
397
|
+
// For events within the same day
|
|
398
|
+
if (dateService.isSameDay(event.start, event.end)) {
|
|
399
|
+
const isFullEvent = 'id' in event;
|
|
400
|
+
parts.push({
|
|
401
|
+
id: isFullEvent ? `${event.id}-0` : `preview-0`,
|
|
402
|
+
event: isFullEvent ? event : null,
|
|
403
|
+
start: event.start,
|
|
404
|
+
end: event.end,
|
|
405
|
+
isStart: true,
|
|
406
|
+
isEnd: true,
|
|
407
|
+
dayIndex: 0,
|
|
408
|
+
totalDays: 1,
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
event: isFullEvent ? event : null,
|
|
412
|
+
parts,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// For multi-day events
|
|
416
|
+
while (!dateService.isSameDay(currentStart, event.end)) {
|
|
417
|
+
const dayEnd = new Date(currentStart);
|
|
418
|
+
dayEnd.setDate(dayEnd.getDate() + 1);
|
|
419
|
+
dayEnd.setHours(0, 0, 0, 0);
|
|
420
|
+
const isFullEvent = 'id' in event;
|
|
421
|
+
parts.push({
|
|
422
|
+
id: isFullEvent ? `${event.id}-${dayIndex}` : `preview-${dayIndex}`,
|
|
423
|
+
event: isFullEvent ? event : null,
|
|
424
|
+
start: new Date(currentStart),
|
|
425
|
+
end: dayEnd,
|
|
426
|
+
isStart: dayIndex === 0,
|
|
427
|
+
isEnd: false,
|
|
428
|
+
dayIndex,
|
|
429
|
+
totalDays,
|
|
430
|
+
});
|
|
431
|
+
currentStart = dayEnd;
|
|
432
|
+
dayIndex++;
|
|
433
|
+
}
|
|
434
|
+
// Add final part if the end time is not midnight
|
|
435
|
+
if (event.end.getHours() !== 0 || event.end.getMinutes() !== 0 || event.end.getSeconds() !== 0) {
|
|
436
|
+
const isFullEvent = 'id' in event;
|
|
437
|
+
parts.push({
|
|
438
|
+
id: isFullEvent ? `${event.id}-${dayIndex}` : `preview-${dayIndex}`,
|
|
439
|
+
event: isFullEvent ? event : null,
|
|
440
|
+
start: new Date(currentStart),
|
|
441
|
+
end: event.end,
|
|
442
|
+
isStart: false,
|
|
443
|
+
isEnd: true,
|
|
444
|
+
dayIndex,
|
|
445
|
+
totalDays,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
event: 'id' in event ? event : null,
|
|
450
|
+
parts,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Calculate total days an event spans
|
|
455
|
+
*/
|
|
456
|
+
getTotalDays(start, end) {
|
|
457
|
+
const startDay = new Date(start);
|
|
458
|
+
startDay.setHours(0, 0, 0, 0);
|
|
459
|
+
const endDay = new Date(end);
|
|
460
|
+
endDay.setHours(0, 0, 0, 0);
|
|
461
|
+
const diffMs = endDay.getTime() - startDay.getTime();
|
|
462
|
+
const days = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
463
|
+
// If end time is not midnight, add 1
|
|
464
|
+
if (end.getHours() !== 0 || end.getMinutes() !== 0 || end.getSeconds() !== 0) {
|
|
465
|
+
return days + 1;
|
|
466
|
+
}
|
|
467
|
+
return Math.max(1, days);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Assign events to tracks (rails) to minimize overlapping
|
|
471
|
+
* This method should be called when events are updated, NOT during drag operations
|
|
472
|
+
*/
|
|
473
|
+
getTimeline(events) {
|
|
474
|
+
if (events.length === 0) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
// Get all unique timestamps and sort them
|
|
478
|
+
const timestamps = this.getUniqueTimestamps(events);
|
|
479
|
+
const tracks = [];
|
|
480
|
+
// Process events starting at each timestamp
|
|
481
|
+
for (const timestamp of timestamps) {
|
|
482
|
+
const startingEvents = events.filter((e) => e.start.getTime() === timestamp.getTime());
|
|
483
|
+
for (const event of startingEvents) {
|
|
484
|
+
// Find a free track for this event
|
|
485
|
+
const freeTrack = tracks.find((track) => this.isTrackFreeForEvent(track, event));
|
|
486
|
+
if (freeTrack) {
|
|
487
|
+
freeTrack.events.push(event);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
// Create a new track
|
|
491
|
+
tracks.push({
|
|
492
|
+
index: tracks.length,
|
|
493
|
+
events: [event],
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return tracks;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Get track assignment for event parts using colspan algorithm (for rendering)
|
|
502
|
+
*
|
|
503
|
+
* This uses a colspan-based algorithm similar to Outlook/Google Calendar:
|
|
504
|
+
* 1. Build overlap groups (connected components of overlapping events)
|
|
505
|
+
* 2. Assign columns within each group using a greedy algorithm
|
|
506
|
+
* 3. Compute colspan - how many columns each event can span
|
|
507
|
+
*
|
|
508
|
+
* This ensures events are displayed as wide as possible:
|
|
509
|
+
* - An event with no overlapping events gets 100% width
|
|
510
|
+
* - Events only share space with events they actually overlap with
|
|
511
|
+
* - An event can span multiple columns if there's no blocking event to its right
|
|
512
|
+
*/
|
|
513
|
+
getTimelinedParts(eventParts) {
|
|
514
|
+
// Collect unique events in a single pass (O(N) instead of O(N*M))
|
|
515
|
+
const eventMap = new Map();
|
|
516
|
+
for (const part of eventParts) {
|
|
517
|
+
if (part.event && !eventMap.has(part.event.id)) {
|
|
518
|
+
eventMap.set(part.event.id, part.event);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const events = Array.from(eventMap.values());
|
|
522
|
+
// Get layout info using colspan algorithm
|
|
523
|
+
const layoutMap = this.getColspanLayout(events);
|
|
524
|
+
// Map parts to their layout info
|
|
525
|
+
return eventParts.map((part) => {
|
|
526
|
+
if (!part.event) {
|
|
527
|
+
return { part, trackIndex: 0, totalTracks: 1, colspan: 1 };
|
|
528
|
+
}
|
|
529
|
+
const layout = layoutMap.get(part.event.id);
|
|
530
|
+
if (!layout) {
|
|
531
|
+
return { part, trackIndex: 0, totalTracks: 1, colspan: 1 };
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
part,
|
|
535
|
+
trackIndex: layout.col,
|
|
536
|
+
totalTracks: layout.columnCount,
|
|
537
|
+
colspan: layout.colspan,
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Compute colspan-based layout for events (Outlook/Google Calendar algorithm)
|
|
543
|
+
*
|
|
544
|
+
* Phase 1: Build overlap groups (connected components)
|
|
545
|
+
* Phase 2: Assign columns within each group
|
|
546
|
+
* Phase 3: Compute colspan for each event
|
|
547
|
+
*/
|
|
548
|
+
getColspanLayout(events) {
|
|
549
|
+
const layoutMap = new Map();
|
|
550
|
+
if (events.length === 0) {
|
|
551
|
+
return layoutMap;
|
|
552
|
+
}
|
|
553
|
+
// Phase 1: Build overlap groups
|
|
554
|
+
const groups = this.buildOverlapGroups(events);
|
|
555
|
+
for (const group of groups) {
|
|
556
|
+
// Phase 2: Assign columns (returns assignments and column count)
|
|
557
|
+
const { assignments, columnCount } = this.assignColumns(group);
|
|
558
|
+
// Phase 3: Compute colspan for each event
|
|
559
|
+
for (const event of group) {
|
|
560
|
+
const col = assignments.get(event.id) ?? 0;
|
|
561
|
+
const colspan = this.computeColspan(event, col, group, assignments, columnCount);
|
|
562
|
+
layoutMap.set(event.id, {
|
|
563
|
+
col,
|
|
564
|
+
colspan,
|
|
565
|
+
columnCount,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return layoutMap;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Build overlap groups (connected components)
|
|
573
|
+
* Events belong to the same group if they overlap directly or indirectly
|
|
574
|
+
*
|
|
575
|
+
* Optimized to reduce comparisons by pre-sorting events by start time
|
|
576
|
+
* and breaking early when events can no longer overlap
|
|
577
|
+
*/
|
|
578
|
+
buildOverlapGroups(events) {
|
|
579
|
+
if (events.length === 0)
|
|
580
|
+
return [];
|
|
581
|
+
const groups = [];
|
|
582
|
+
const visited = new Set();
|
|
583
|
+
// Sort events by start time for efficient overlap checking
|
|
584
|
+
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
585
|
+
for (const event of sorted) {
|
|
586
|
+
if (visited.has(event.id))
|
|
587
|
+
continue;
|
|
588
|
+
// BFS to find all connected events
|
|
589
|
+
// Use pointer-based iteration instead of shift() to avoid O(N) re-indexing
|
|
590
|
+
const group = [];
|
|
591
|
+
const queue = [event];
|
|
592
|
+
let queueHead = 0;
|
|
593
|
+
while (queueHead < queue.length) {
|
|
594
|
+
const current = queue[queueHead++];
|
|
595
|
+
if (visited.has(current.id))
|
|
596
|
+
continue;
|
|
597
|
+
visited.add(current.id);
|
|
598
|
+
group.push(current);
|
|
599
|
+
const currentEnd = current.end.getTime();
|
|
600
|
+
// Find overlapping events efficiently using sorted order
|
|
601
|
+
for (const other of sorted) {
|
|
602
|
+
if (visited.has(other.id))
|
|
603
|
+
continue;
|
|
604
|
+
// Since sorted by start time, if other starts at or after current ends,
|
|
605
|
+
// no more events in sorted order can overlap with current
|
|
606
|
+
if (other.start.getTime() >= currentEnd)
|
|
607
|
+
break;
|
|
608
|
+
if (this.eventsOverlap(current, other)) {
|
|
609
|
+
queue.push(other);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
groups.push(group);
|
|
614
|
+
}
|
|
615
|
+
return groups;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Assign columns to events within an overlap group
|
|
619
|
+
* Uses a greedy algorithm: place each event in the first available column
|
|
620
|
+
* Returns the column assignments and total column count
|
|
621
|
+
*/
|
|
622
|
+
assignColumns(group) {
|
|
623
|
+
const assignments = new Map();
|
|
624
|
+
// Sort by start time, then end time, then id (for stability)
|
|
625
|
+
const sorted = [...group].sort((a, b) => a.start.getTime() - b.start.getTime() ||
|
|
626
|
+
a.end.getTime() - b.end.getTime() ||
|
|
627
|
+
a.id.localeCompare(b.id));
|
|
628
|
+
// Track end time of each column
|
|
629
|
+
const colEnds = [];
|
|
630
|
+
for (const event of sorted) {
|
|
631
|
+
let col = -1;
|
|
632
|
+
// Find first available column
|
|
633
|
+
for (let i = 0; i < colEnds.length; i++) {
|
|
634
|
+
if (colEnds[i] <= event.start.getTime()) {
|
|
635
|
+
col = i;
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (col === -1) {
|
|
640
|
+
col = colEnds.length;
|
|
641
|
+
colEnds.push(event.end.getTime());
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
colEnds[col] = event.end.getTime();
|
|
645
|
+
}
|
|
646
|
+
assignments.set(event.id, col);
|
|
647
|
+
}
|
|
648
|
+
return { assignments, columnCount: colEnds.length };
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Compute colspan for an event
|
|
652
|
+
* An event can span multiple columns if there's no overlapping event to its right
|
|
653
|
+
*/
|
|
654
|
+
computeColspan(event, eventCol, group, assignments, columnCount) {
|
|
655
|
+
// Find the nearest blocking column to the right
|
|
656
|
+
let block = Infinity;
|
|
657
|
+
for (const other of group) {
|
|
658
|
+
if (other.id === event.id)
|
|
659
|
+
continue;
|
|
660
|
+
if (!this.eventsOverlap(event, other))
|
|
661
|
+
continue;
|
|
662
|
+
const otherCol = assignments.get(other.id) ?? 0;
|
|
663
|
+
if (otherCol > eventCol) {
|
|
664
|
+
block = Math.min(block, otherCol);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// Calculate colspan
|
|
668
|
+
if (block !== Infinity) {
|
|
669
|
+
return block - eventCol;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
return columnCount - eventCol;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Check if two events overlap in time
|
|
677
|
+
*/
|
|
678
|
+
eventsOverlap(a, b) {
|
|
679
|
+
return a.start < b.end && b.start < a.end;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Calculate the relative track position for an event part (legacy method)
|
|
683
|
+
* @deprecated Use getColspanLayout instead for better layout
|
|
684
|
+
*/
|
|
685
|
+
getRelativeTrackPosition(tracks, part, globalTrackIndex) {
|
|
686
|
+
// Find all track indices that have events overlapping with this part
|
|
687
|
+
const overlappingTrackIndices = [];
|
|
688
|
+
for (const track of tracks) {
|
|
689
|
+
const hasOverlap = track.events.some((event) => event.start < part.end && event.end > part.start);
|
|
690
|
+
if (hasOverlap) {
|
|
691
|
+
overlappingTrackIndices.push(track.index);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Sort to ensure consistent ordering
|
|
695
|
+
overlappingTrackIndices.sort((a, b) => a - b);
|
|
696
|
+
// Find the relative position of this event's track among overlapping tracks
|
|
697
|
+
const relativeIndex = overlappingTrackIndices.indexOf(globalTrackIndex);
|
|
698
|
+
return {
|
|
699
|
+
relativeIndex: relativeIndex >= 0 ? relativeIndex : 0,
|
|
700
|
+
overlappingCount: Math.max(1, overlappingTrackIndices.length),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Filter events that fall within a date range
|
|
705
|
+
*/
|
|
706
|
+
filterByRange(events, start, end) {
|
|
707
|
+
return events.filter((event) => {
|
|
708
|
+
// Event overlaps with range if:
|
|
709
|
+
// event.start < rangeEnd AND event.end > rangeStart
|
|
710
|
+
return event.start < end && event.end > start;
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Filter event parts that fall within a date range
|
|
715
|
+
*/
|
|
716
|
+
filterPartsByRange(parts, start, end) {
|
|
717
|
+
return parts.filter((part) => {
|
|
718
|
+
return part.start < end && part.end > start;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Get all unique timestamps from events (both start and end times)
|
|
723
|
+
*/
|
|
724
|
+
getUniqueTimestamps(events) {
|
|
725
|
+
const timestampSet = new Set();
|
|
726
|
+
for (const event of events) {
|
|
727
|
+
timestampSet.add(event.start.getTime());
|
|
728
|
+
timestampSet.add(event.end.getTime());
|
|
729
|
+
}
|
|
730
|
+
return Array.from(timestampSet)
|
|
731
|
+
.sort((a, b) => a - b)
|
|
732
|
+
.map((ts) => new Date(ts));
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Check if a track has space for an event (no overlapping)
|
|
736
|
+
*/
|
|
737
|
+
isTrackFreeForEvent(track, event) {
|
|
738
|
+
return track.events.every((existingEvent) => {
|
|
739
|
+
// No overlap if: existingEvent ends before event starts OR event ends before existingEvent starts
|
|
740
|
+
return existingEvent.end <= event.start || event.end <= existingEvent.start;
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Get events for a specific day
|
|
745
|
+
*/
|
|
746
|
+
getEventsForDay(events, day) {
|
|
747
|
+
const dayStart = new Date(day);
|
|
748
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
749
|
+
const dayEnd = new Date(day);
|
|
750
|
+
dayEnd.setHours(23, 59, 59, 999);
|
|
751
|
+
return this.filterByRange(events, dayStart, dayEnd);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get event parts for a specific day
|
|
755
|
+
*/
|
|
756
|
+
getPartsForDay(parts, day) {
|
|
757
|
+
const dayStart = new Date(day);
|
|
758
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
759
|
+
const dayEnd = new Date(day);
|
|
760
|
+
dayEnd.setHours(23, 59, 59, 999);
|
|
761
|
+
return this.filterPartsByRange(parts, dayStart, dayEnd);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Singleton instance of TimelineService
|
|
766
|
+
*/
|
|
767
|
+
const timelineService = new TimelineService();
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Service for calculating event positions within the grid
|
|
771
|
+
*/
|
|
772
|
+
class PositionService {
|
|
773
|
+
/**
|
|
774
|
+
* Calculate position for an event part in week/day view
|
|
775
|
+
*/
|
|
776
|
+
calculateWeekPosition(part, trackIndex, totalTracks, dayIndex, totalDays, options = {}) {
|
|
777
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
778
|
+
const slotDuration = opts.slotDuration;
|
|
779
|
+
// Calculate vertical position (top and height)
|
|
780
|
+
const dayStart = new Date(part.start);
|
|
781
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
782
|
+
const startSeconds = dateService.getSecondsFromMidnight(part.start);
|
|
783
|
+
const endSeconds = dateService.getSecondsFromMidnight(part.end);
|
|
784
|
+
const durationSeconds = endSeconds - startSeconds;
|
|
785
|
+
// Parse min/max time
|
|
786
|
+
const [minHour] = opts.slotMinTime.split(':').map(Number);
|
|
787
|
+
const [maxHour] = opts.slotMaxTime.split(':').map(Number);
|
|
788
|
+
const visibleSeconds = (maxHour - minHour) * 3600;
|
|
789
|
+
const offsetSeconds = startSeconds - minHour * 3600;
|
|
790
|
+
// Top position as percentage
|
|
791
|
+
const top = (offsetSeconds / visibleSeconds) * 100;
|
|
792
|
+
const height = (durationSeconds / visibleSeconds) * 100;
|
|
793
|
+
// Calculate horizontal position (left and width)
|
|
794
|
+
// Account for time gutter width (assumed 60px or ~10%)
|
|
795
|
+
const gutterWidth = 10; // percentage
|
|
796
|
+
const availableWidth = 100 - gutterWidth;
|
|
797
|
+
const dayWidth = availableWidth / totalDays;
|
|
798
|
+
// Position within the day column based on track
|
|
799
|
+
const trackWidth = dayWidth / totalTracks;
|
|
800
|
+
const left = gutterWidth + dayIndex * dayWidth + trackIndex * trackWidth;
|
|
801
|
+
const width = trackWidth;
|
|
802
|
+
return {
|
|
803
|
+
top,
|
|
804
|
+
left,
|
|
805
|
+
width,
|
|
806
|
+
height,
|
|
807
|
+
zIndex: trackIndex + 1,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Calculate position for an event in timeline view
|
|
812
|
+
*/
|
|
813
|
+
calculateTimelinePosition(part, trackIndex, totalTracks, viewStart, viewEnd, options = {}) {
|
|
814
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
815
|
+
// Calculate horizontal position based on time
|
|
816
|
+
const totalDuration = viewEnd.getTime() - viewStart.getTime();
|
|
817
|
+
const eventStart = Math.max(part.start.getTime(), viewStart.getTime());
|
|
818
|
+
const eventEnd = Math.min(part.end.getTime(), viewEnd.getTime());
|
|
819
|
+
const startOffset = eventStart - viewStart.getTime();
|
|
820
|
+
const duration = eventEnd - eventStart;
|
|
821
|
+
const left = (startOffset / totalDuration) * 100;
|
|
822
|
+
const width = (duration / totalDuration) * 100;
|
|
823
|
+
// Calculate vertical position based on track
|
|
824
|
+
const trackHeight = 100 / totalTracks;
|
|
825
|
+
const top = trackIndex * trackHeight;
|
|
826
|
+
const height = trackHeight;
|
|
827
|
+
return {
|
|
828
|
+
top,
|
|
829
|
+
left,
|
|
830
|
+
width,
|
|
831
|
+
height,
|
|
832
|
+
zIndex: 1,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Calculate position for an all-day event in month view
|
|
837
|
+
*/
|
|
838
|
+
calculateMonthEventPosition(part, rowIndex, maxRows, startDayIndex, endDayIndex, totalDays = 7) {
|
|
839
|
+
const dayWidth = 100 / totalDays;
|
|
840
|
+
const rowHeight = 100 / maxRows;
|
|
841
|
+
const left = startDayIndex * dayWidth;
|
|
842
|
+
const width = (endDayIndex - startDayIndex + 1) * dayWidth;
|
|
843
|
+
const top = rowIndex * rowHeight;
|
|
844
|
+
const height = rowHeight;
|
|
845
|
+
return {
|
|
846
|
+
top,
|
|
847
|
+
left,
|
|
848
|
+
width,
|
|
849
|
+
height,
|
|
850
|
+
zIndex: rowIndex + 1,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Convert percentage-based position to pixel-based
|
|
855
|
+
*/
|
|
856
|
+
toPixelPosition(position, containerWidth, containerHeight) {
|
|
857
|
+
return {
|
|
858
|
+
top: (position.top / 100) * containerHeight,
|
|
859
|
+
left: (position.left / 100) * containerWidth,
|
|
860
|
+
width: (position.width / 100) * containerWidth,
|
|
861
|
+
height: (position.height / 100) * containerHeight,
|
|
862
|
+
zIndex: position.zIndex,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Generate CSS styles from position
|
|
867
|
+
*/
|
|
868
|
+
toStyleString(position, unit = '%') {
|
|
869
|
+
return `
|
|
870
|
+
position: absolute;
|
|
871
|
+
top: ${position.top}${unit};
|
|
872
|
+
left: ${position.left}${unit};
|
|
873
|
+
width: ${position.width}${unit};
|
|
874
|
+
height: ${position.height}${unit};
|
|
875
|
+
z-index: ${position.zIndex};
|
|
876
|
+
`.trim().replace(/\s+/g, ' ');
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Generate CSS object from position
|
|
880
|
+
*/
|
|
881
|
+
toStyleObject(position, unit = '%') {
|
|
882
|
+
return {
|
|
883
|
+
position: 'absolute',
|
|
884
|
+
top: `${position.top}${unit}`,
|
|
885
|
+
left: `${position.left}${unit}`,
|
|
886
|
+
width: `${position.width}${unit}`,
|
|
887
|
+
height: `${position.height}${unit}`,
|
|
888
|
+
zIndex: String(position.zIndex),
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Singleton instance of PositionService
|
|
894
|
+
*/
|
|
895
|
+
const positionService = new PositionService();
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Service for resource and resource group operations
|
|
899
|
+
*/
|
|
900
|
+
class ResourceService {
|
|
901
|
+
/**
|
|
902
|
+
* Flatten a hierarchical resource structure for rendering
|
|
903
|
+
*/
|
|
904
|
+
flatten(items, collapsedIds = new Set(), depth = 0, parentId, parentCollapsed = false) {
|
|
905
|
+
const result = [];
|
|
906
|
+
for (const item of items) {
|
|
907
|
+
const visible = !parentCollapsed;
|
|
908
|
+
result.push({ item, depth, visible, parentId });
|
|
909
|
+
if (isResourceGroup(item)) {
|
|
910
|
+
const isCollapsed = collapsedIds.has(item.id);
|
|
911
|
+
const children = this.flatten(item.children, collapsedIds, depth + 1, item.id, parentCollapsed || isCollapsed);
|
|
912
|
+
result.push(...children);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Get all resources (leaf nodes) from a hierarchical structure
|
|
919
|
+
*/
|
|
920
|
+
getAllResources(items) {
|
|
921
|
+
const resources = [];
|
|
922
|
+
for (const item of items) {
|
|
923
|
+
if (isResource(item)) {
|
|
924
|
+
resources.push(item);
|
|
925
|
+
}
|
|
926
|
+
else if (isResourceGroup(item)) {
|
|
927
|
+
resources.push(...this.getAllResources(item.children));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return resources;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get all events from all resources
|
|
934
|
+
*/
|
|
935
|
+
getAllEvents(items) {
|
|
936
|
+
const resources = this.getAllResources(items);
|
|
937
|
+
return resources.flatMap((r) => r.events ?? []);
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Find a resource by ID
|
|
941
|
+
*/
|
|
942
|
+
findResourceById(items, id) {
|
|
943
|
+
for (const item of items) {
|
|
944
|
+
if (isResource(item) && item.id === id) {
|
|
945
|
+
return item;
|
|
946
|
+
}
|
|
947
|
+
if (isResourceGroup(item)) {
|
|
948
|
+
const found = this.findResourceById(item.children, id);
|
|
949
|
+
if (found)
|
|
950
|
+
return found;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Find a resource group by ID
|
|
957
|
+
*/
|
|
958
|
+
findGroupById(items, id) {
|
|
959
|
+
for (const item of items) {
|
|
960
|
+
if (isResourceGroup(item)) {
|
|
961
|
+
if (item.id === id)
|
|
962
|
+
return item;
|
|
963
|
+
const found = this.findGroupById(item.children, id);
|
|
964
|
+
if (found)
|
|
965
|
+
return found;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return undefined;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Find any item (resource or group) by ID
|
|
972
|
+
*/
|
|
973
|
+
findById(items, id) {
|
|
974
|
+
return this.findResourceById(items, id) ?? this.findGroupById(items, id);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Add an event to a resource
|
|
978
|
+
*/
|
|
979
|
+
addEventToResource(items, resourceId, event) {
|
|
980
|
+
return this.mapResources(items, (resource) => {
|
|
981
|
+
if (resource.id === resourceId) {
|
|
982
|
+
return {
|
|
983
|
+
...resource,
|
|
984
|
+
events: [...(resource.events ?? []), event],
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return resource;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Update an event in a resource
|
|
992
|
+
*/
|
|
993
|
+
updateEventInResource(items, event) {
|
|
994
|
+
return this.mapResources(items, (resource) => {
|
|
995
|
+
const eventIndex = resource.events?.findIndex((e) => e.id === event.id);
|
|
996
|
+
if (eventIndex !== undefined && eventIndex >= 0 && resource.events) {
|
|
997
|
+
const newEvents = [...resource.events];
|
|
998
|
+
newEvents[eventIndex] = event;
|
|
999
|
+
return { ...resource, events: newEvents };
|
|
1000
|
+
}
|
|
1001
|
+
return resource;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Remove an event from all resources
|
|
1006
|
+
*/
|
|
1007
|
+
removeEvent(items, eventId) {
|
|
1008
|
+
return this.mapResources(items, (resource) => {
|
|
1009
|
+
if (resource.events?.some((e) => e.id === eventId)) {
|
|
1010
|
+
return {
|
|
1011
|
+
...resource,
|
|
1012
|
+
events: resource.events.filter((e) => e.id !== eventId),
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return resource;
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Move an event from one resource to another
|
|
1020
|
+
*/
|
|
1021
|
+
moveEventToResource(items, eventId, newResourceId, updatedEvent) {
|
|
1022
|
+
// Find the event first
|
|
1023
|
+
let foundEvent;
|
|
1024
|
+
for (const resource of this.getAllResources(items)) {
|
|
1025
|
+
foundEvent = resource.events?.find((e) => e.id === eventId);
|
|
1026
|
+
if (foundEvent)
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
if (!foundEvent)
|
|
1030
|
+
return items;
|
|
1031
|
+
// Merge with updates
|
|
1032
|
+
const newEvent = {
|
|
1033
|
+
...foundEvent,
|
|
1034
|
+
...updatedEvent,
|
|
1035
|
+
resourceId: newResourceId,
|
|
1036
|
+
};
|
|
1037
|
+
// Remove from old resource and add to new
|
|
1038
|
+
let result = this.removeEvent(items, eventId);
|
|
1039
|
+
result = this.addEventToResource(result, newResourceId, newEvent);
|
|
1040
|
+
return result;
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Toggle collapse state of a group
|
|
1044
|
+
*/
|
|
1045
|
+
toggleGroupCollapse(items, groupId) {
|
|
1046
|
+
return this.mapGroups(items, (group) => {
|
|
1047
|
+
if (group.id === groupId) {
|
|
1048
|
+
return { ...group, collapsed: !group.collapsed };
|
|
1049
|
+
}
|
|
1050
|
+
return group;
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Set collapse state of a group
|
|
1055
|
+
*/
|
|
1056
|
+
setGroupCollapse(items, groupId, collapsed) {
|
|
1057
|
+
return this.mapGroups(items, (group) => {
|
|
1058
|
+
if (group.id === groupId) {
|
|
1059
|
+
return { ...group, collapsed };
|
|
1060
|
+
}
|
|
1061
|
+
return group;
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Collapse all groups
|
|
1066
|
+
*/
|
|
1067
|
+
collapseAll(items) {
|
|
1068
|
+
return this.mapGroups(items, (group) => ({ ...group, collapsed: true }));
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Expand all groups
|
|
1072
|
+
*/
|
|
1073
|
+
expandAll(items) {
|
|
1074
|
+
return this.mapGroups(items, (group) => ({ ...group, collapsed: false }));
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Map over all resources in the hierarchy
|
|
1078
|
+
*/
|
|
1079
|
+
mapResources(items, mapper) {
|
|
1080
|
+
return items.map((item) => {
|
|
1081
|
+
if (isResource(item)) {
|
|
1082
|
+
return mapper(item);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
...item,
|
|
1086
|
+
children: this.mapResources(item.children, mapper),
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Map over all groups in the hierarchy
|
|
1092
|
+
*/
|
|
1093
|
+
mapGroups(items, mapper) {
|
|
1094
|
+
return items.map((item) => {
|
|
1095
|
+
if (isResourceGroup(item)) {
|
|
1096
|
+
const mappedGroup = mapper(item);
|
|
1097
|
+
return {
|
|
1098
|
+
...mappedGroup,
|
|
1099
|
+
children: this.mapGroups(mappedGroup.children, mapper),
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
return item;
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Get the total count of visible resources
|
|
1107
|
+
*/
|
|
1108
|
+
getVisibleResourceCount(items, collapsedIds = new Set()) {
|
|
1109
|
+
const flattened = this.flatten(items, collapsedIds);
|
|
1110
|
+
return flattened.filter((f) => f.visible && isResource(f.item)).length;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Sort resources by order property
|
|
1114
|
+
*/
|
|
1115
|
+
sortByOrder(items) {
|
|
1116
|
+
const sorted = [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1117
|
+
return sorted.map((item) => {
|
|
1118
|
+
if (isResourceGroup(item)) {
|
|
1119
|
+
return {
|
|
1120
|
+
...item,
|
|
1121
|
+
children: this.sortByOrder(item.children),
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
return item;
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Singleton instance of ResourceService
|
|
1130
|
+
*/
|
|
1131
|
+
const resourceService = new ResourceService();
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Generate a unique ID
|
|
1135
|
+
*/
|
|
1136
|
+
function generateId(prefix = 'evt') {
|
|
1137
|
+
const timestamp = Date.now().toString(36);
|
|
1138
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
1139
|
+
return `${prefix}-${timestamp}-${random}`;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Generate a unique event ID
|
|
1143
|
+
*/
|
|
1144
|
+
function generateEventId() {
|
|
1145
|
+
return generateId('evt');
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Generate a unique resource ID
|
|
1149
|
+
*/
|
|
1150
|
+
function generateResourceId() {
|
|
1151
|
+
return generateId('res');
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Generate a unique group ID
|
|
1155
|
+
*/
|
|
1156
|
+
function generateGroupId() {
|
|
1157
|
+
return generateId('grp');
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Default colors for events
|
|
1162
|
+
*/
|
|
1163
|
+
const DEFAULT_COLORS = [
|
|
1164
|
+
'#3788d8', // Blue
|
|
1165
|
+
'#28a745', // Green
|
|
1166
|
+
'#dc3545', // Red
|
|
1167
|
+
'#ffc107', // Yellow
|
|
1168
|
+
'#17a2b8', // Cyan
|
|
1169
|
+
'#6f42c1', // Purple
|
|
1170
|
+
'#fd7e14', // Orange
|
|
1171
|
+
'#20c997', // Teal
|
|
1172
|
+
'#e83e8c', // Pink
|
|
1173
|
+
'#6c757d', // Gray
|
|
1174
|
+
];
|
|
1175
|
+
/**
|
|
1176
|
+
* Get a color by index (cycles through default colors)
|
|
1177
|
+
*/
|
|
1178
|
+
function getColorByIndex(index) {
|
|
1179
|
+
return DEFAULT_COLORS[index % DEFAULT_COLORS.length];
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Calculate contrasting text color (black or white) for a background
|
|
1183
|
+
*/
|
|
1184
|
+
function getContrastColor(backgroundColor) {
|
|
1185
|
+
// Convert hex to RGB
|
|
1186
|
+
let hex = backgroundColor.replace('#', '');
|
|
1187
|
+
if (hex.length === 3) {
|
|
1188
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
1189
|
+
}
|
|
1190
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1191
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1192
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1193
|
+
// Calculate relative luminance
|
|
1194
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
1195
|
+
return luminance > 0.5 ? '#000000' : '#ffffff';
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Lighten a color by a percentage
|
|
1199
|
+
*/
|
|
1200
|
+
function lightenColor(color, percent) {
|
|
1201
|
+
let hex = color.replace('#', '');
|
|
1202
|
+
if (hex.length === 3) {
|
|
1203
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
1204
|
+
}
|
|
1205
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1206
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1207
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1208
|
+
const newR = Math.min(255, Math.round(r + (255 - r) * (percent / 100)));
|
|
1209
|
+
const newG = Math.min(255, Math.round(g + (255 - g) * (percent / 100)));
|
|
1210
|
+
const newB = Math.min(255, Math.round(b + (255 - b) * (percent / 100)));
|
|
1211
|
+
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Darken a color by a percentage
|
|
1215
|
+
*/
|
|
1216
|
+
function darkenColor(color, percent) {
|
|
1217
|
+
let hex = color.replace('#', '');
|
|
1218
|
+
if (hex.length === 3) {
|
|
1219
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
1220
|
+
}
|
|
1221
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1222
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1223
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1224
|
+
const newR = Math.max(0, Math.round(r * (1 - percent / 100)));
|
|
1225
|
+
const newG = Math.max(0, Math.round(g * (1 - percent / 100)));
|
|
1226
|
+
const newB = Math.max(0, Math.round(b * (1 - percent / 100)));
|
|
1227
|
+
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Add alpha (opacity) to a color
|
|
1231
|
+
*/
|
|
1232
|
+
function addAlpha(color, alpha) {
|
|
1233
|
+
let hex = color.replace('#', '');
|
|
1234
|
+
if (hex.length === 3) {
|
|
1235
|
+
hex = hex.split('').map((c) => c + c).join('');
|
|
1236
|
+
}
|
|
1237
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1238
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1239
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1240
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Get element by data attributes (used for finding time slots)
|
|
1245
|
+
*/
|
|
1246
|
+
function getElementByData(container, dataAttributes) {
|
|
1247
|
+
const selector = Object.entries(dataAttributes)
|
|
1248
|
+
.map(([key, value]) => `[data-${key}="${value}"]`)
|
|
1249
|
+
.join('');
|
|
1250
|
+
return container.querySelector(selector);
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get data attributes from an element
|
|
1254
|
+
*/
|
|
1255
|
+
function getDataAttributes(element) {
|
|
1256
|
+
const dataset = element.dataset;
|
|
1257
|
+
const result = {};
|
|
1258
|
+
for (const key in dataset) {
|
|
1259
|
+
const value = dataset[key];
|
|
1260
|
+
if (value !== undefined) {
|
|
1261
|
+
result[key] = value;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return result;
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Find closest ancestor with a data attribute
|
|
1268
|
+
*/
|
|
1269
|
+
function findClosestWithData(element, dataAttribute) {
|
|
1270
|
+
return element.closest(`[data-${dataAttribute}]`);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Get scroll position relative to an element
|
|
1274
|
+
*/
|
|
1275
|
+
function getScrollPosition(element) {
|
|
1276
|
+
return {
|
|
1277
|
+
top: element.scrollTop,
|
|
1278
|
+
left: element.scrollLeft,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Scroll to a specific time in the scheduler
|
|
1283
|
+
*/
|
|
1284
|
+
function scrollToTime(container, timeElement, behavior = 'smooth') {
|
|
1285
|
+
const containerRect = container.getBoundingClientRect();
|
|
1286
|
+
const timeRect = timeElement.getBoundingClientRect();
|
|
1287
|
+
const relativeTop = timeRect.top - containerRect.top + container.scrollTop;
|
|
1288
|
+
container.scrollTo({
|
|
1289
|
+
top: relativeTop,
|
|
1290
|
+
behavior,
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Check if an element is in viewport
|
|
1295
|
+
*/
|
|
1296
|
+
function isInViewport(element, container) {
|
|
1297
|
+
const rect = element.getBoundingClientRect();
|
|
1298
|
+
const containerRect = container
|
|
1299
|
+
? container.getBoundingClientRect()
|
|
1300
|
+
: { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth };
|
|
1301
|
+
return (rect.top >= containerRect.top &&
|
|
1302
|
+
rect.left >= containerRect.left &&
|
|
1303
|
+
rect.bottom <= containerRect.bottom &&
|
|
1304
|
+
rect.right <= containerRect.right);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Get pointer position relative to an element
|
|
1308
|
+
*/
|
|
1309
|
+
function getRelativePosition(event, element) {
|
|
1310
|
+
const rect = element.getBoundingClientRect();
|
|
1311
|
+
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
|
1312
|
+
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
1313
|
+
return {
|
|
1314
|
+
x: clientX - rect.left,
|
|
1315
|
+
y: clientY - rect.top,
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Create a CSS variable setter for theming
|
|
1320
|
+
*/
|
|
1321
|
+
function setCSSVariable(element, name, value) {
|
|
1322
|
+
element.style.setProperty(`--${name}`, value);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Get computed CSS variable value
|
|
1326
|
+
*/
|
|
1327
|
+
function getCSSVariable(element, name) {
|
|
1328
|
+
return getComputedStyle(element).getPropertyValue(`--${name}`).trim();
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Add multiple CSS classes
|
|
1332
|
+
*/
|
|
1333
|
+
function addClasses(element, ...classes) {
|
|
1334
|
+
element.classList.add(...classes.filter(Boolean));
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Remove multiple CSS classes
|
|
1338
|
+
*/
|
|
1339
|
+
function removeClasses(element, ...classes) {
|
|
1340
|
+
element.classList.remove(...classes.filter(Boolean));
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Toggle CSS class based on condition
|
|
1344
|
+
*/
|
|
1345
|
+
function toggleClass(element, className, condition) {
|
|
1346
|
+
element.classList.toggle(className, condition);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Models
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Generated bundle index. Do not edit.
|
|
1353
|
+
*/
|
|
1354
|
+
|
|
1355
|
+
export { DEFAULT_COLORS, DEFAULT_OPTIONS, DateService, PositionService, ResourceService, TimelineService, addAlpha, addClasses, darkenColor, dateService, findClosestWithData, generateEventId, generateGroupId, generateId, generateResourceId, getCSSVariable, getColorByIndex, getContrastColor, getDataAttributes, getElementByData, getRelativePosition, getScrollPosition, isInViewport, isResource, isResourceGroup, lightenColor, positionService, removeClasses, resourceService, scrollToTime, setCSSVariable, timelineService, toggleClass };
|
|
1356
|
+
//# sourceMappingURL=mintplayer-ng-bootstrap-web-components-scheduler-core.mjs.map
|