@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.
Files changed (182) hide show
  1. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs +20 -20
  2. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs.map +1 -1
  3. package/fesm2022/mintplayer-ng-bootstrap-alert.mjs +8 -8
  4. package/fesm2022/mintplayer-ng-bootstrap-alert.mjs.map +1 -1
  5. package/fesm2022/mintplayer-ng-bootstrap-badge.mjs +5 -5
  6. package/fesm2022/mintplayer-ng-bootstrap-badge.mjs.map +1 -1
  7. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs +6 -6
  8. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs.map +1 -1
  9. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs +3 -3
  10. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs.map +1 -1
  11. package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs +4 -4
  12. package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs.map +1 -1
  13. package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs +9 -9
  14. package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs.map +1 -1
  15. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs +10 -10
  16. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs.map +1 -1
  17. package/fesm2022/mintplayer-ng-bootstrap-card.mjs +8 -8
  18. package/fesm2022/mintplayer-ng-bootstrap-card.mjs.map +1 -1
  19. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs +25 -25
  20. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-bootstrap-close.mjs +3 -3
  22. package/fesm2022/mintplayer-ng-bootstrap-close.mjs.map +1 -1
  23. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs +7 -7
  24. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs.map +1 -1
  25. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs +58 -58
  26. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs.map +1 -1
  27. package/fesm2022/mintplayer-ng-bootstrap-container.mjs +3 -3
  28. package/fesm2022/mintplayer-ng-bootstrap-container.mjs.map +1 -1
  29. package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs +3 -3
  30. package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs.map +1 -1
  31. package/fesm2022/mintplayer-ng-bootstrap-copy.mjs +4 -4
  32. package/fesm2022/mintplayer-ng-bootstrap-copy.mjs.map +1 -1
  33. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs +20 -20
  34. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs.map +1 -1
  35. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs +6 -6
  36. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs.map +1 -1
  37. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs +789 -1175
  38. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs.map +1 -1
  39. package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs +3 -3
  40. package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs.map +1 -1
  41. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs +10 -10
  42. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs.map +1 -1
  43. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs +15 -15
  44. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs.map +1 -1
  45. package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs +3 -3
  46. package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs.map +1 -1
  47. package/fesm2022/mintplayer-ng-bootstrap-enum.mjs +3 -3
  48. package/fesm2022/mintplayer-ng-bootstrap-enum.mjs.map +1 -1
  49. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs +16 -16
  50. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs.map +1 -1
  51. package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs +3 -3
  52. package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs.map +1 -1
  53. package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs +3 -3
  54. package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs.map +1 -1
  55. package/fesm2022/mintplayer-ng-bootstrap-for.mjs +4 -4
  56. package/fesm2022/mintplayer-ng-bootstrap-for.mjs.map +1 -1
  57. package/fesm2022/mintplayer-ng-bootstrap-form.mjs +11 -11
  58. package/fesm2022/mintplayer-ng-bootstrap-form.mjs.map +1 -1
  59. package/fesm2022/mintplayer-ng-bootstrap-grid.mjs +26 -26
  60. package/fesm2022/mintplayer-ng-bootstrap-grid.mjs.map +1 -1
  61. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs +4 -4
  62. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs.map +1 -1
  63. package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs +3 -3
  64. package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs.map +1 -1
  65. package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs +3 -3
  66. package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs.map +1 -1
  67. package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs +3 -3
  68. package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs.map +1 -1
  69. package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs +14 -14
  70. package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs.map +1 -1
  71. package/fesm2022/mintplayer-ng-bootstrap-let.mjs +4 -4
  72. package/fesm2022/mintplayer-ng-bootstrap-let.mjs.map +1 -1
  73. package/fesm2022/mintplayer-ng-bootstrap-linify.mjs +3 -3
  74. package/fesm2022/mintplayer-ng-bootstrap-linify.mjs.map +1 -1
  75. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs +7 -7
  76. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs.map +1 -1
  77. package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs +12 -12
  78. package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs.map +1 -1
  79. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs +3 -3
  80. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs.map +1 -1
  81. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs +24 -24
  82. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs.map +1 -1
  83. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs +24 -24
  84. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs.map +1 -1
  85. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs +5 -5
  86. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs.map +1 -1
  87. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs +58 -58
  88. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs.map +1 -1
  89. package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs +8 -8
  90. package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs.map +1 -1
  91. package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs +3 -3
  92. package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs.map +1 -1
  93. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs +40 -40
  94. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs.map +1 -1
  95. package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs +3 -3
  96. package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs.map +1 -1
  97. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs +12 -12
  98. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs.map +1 -1
  99. package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs +6 -6
  100. package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs.map +1 -1
  101. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs +7 -7
  102. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs.map +1 -1
  103. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs +5 -5
  104. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs.map +1 -1
  105. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs +20 -20
  106. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs.map +1 -1
  107. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs +30 -30
  108. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs.map +1 -1
  109. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs +17 -17
  110. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs.map +1 -1
  111. package/fesm2022/mintplayer-ng-bootstrap-range.mjs +9 -9
  112. package/fesm2022/mintplayer-ng-bootstrap-range.mjs.map +1 -1
  113. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs +7 -7
  114. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs.map +1 -1
  115. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs +25 -25
  116. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs.map +1 -1
  117. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs +16 -16
  118. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs.map +1 -1
  119. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs +14 -14
  120. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs.map +1 -1
  121. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs +24 -24
  122. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs.map +1 -1
  123. package/fesm2022/mintplayer-ng-bootstrap-select.mjs +19 -19
  124. package/fesm2022/mintplayer-ng-bootstrap-select.mjs.map +1 -1
  125. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs +20 -20
  126. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs.map +1 -1
  127. package/fesm2022/mintplayer-ng-bootstrap-shell.mjs +11 -11
  128. package/fesm2022/mintplayer-ng-bootstrap-shell.mjs.map +1 -1
  129. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs +7 -7
  130. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs.map +1 -1
  131. package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs +3 -3
  132. package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs.map +1 -1
  133. package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs +7 -7
  134. package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs.map +1 -1
  135. package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs +3 -3
  136. package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs.map +1 -1
  137. package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs +6 -6
  138. package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs.map +1 -1
  139. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs +57 -67
  140. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs.map +1 -1
  141. package/fesm2022/mintplayer-ng-bootstrap-table.mjs +10 -10
  142. package/fesm2022/mintplayer-ng-bootstrap-table.mjs.map +1 -1
  143. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs +8 -8
  144. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs.map +1 -1
  145. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs +24 -24
  146. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs.map +1 -1
  147. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs +22 -22
  148. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs.map +1 -1
  149. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs +10 -10
  150. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs.map +1 -1
  151. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs +14 -14
  152. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs.map +1 -1
  153. package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs +3 -3
  154. package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs.map +1 -1
  155. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs +10 -10
  156. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs.map +1 -1
  157. package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs +3 -3
  158. package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs.map +1 -1
  159. package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs +3 -3
  160. package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs.map +1 -1
  161. package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs +3 -3
  162. package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs.map +1 -1
  163. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs +10 -10
  164. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs.map +1 -1
  165. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs +1356 -0
  166. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs.map +1 -0
  167. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs +3819 -0
  168. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs.map +1 -0
  169. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs +731 -0
  170. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs.map +1 -0
  171. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs +549 -0
  172. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs.map +1 -0
  173. package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs +3 -3
  174. package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs.map +1 -1
  175. package/package.json +20 -6
  176. package/types/mintplayer-ng-bootstrap-dock.d.ts +55 -19
  177. package/types/mintplayer-ng-bootstrap-scheduler.d.ts +2 -2
  178. package/types/mintplayer-ng-bootstrap-tab-control.d.ts +7 -11
  179. package/types/mintplayer-ng-bootstrap-web-components-scheduler-core.d.ts +890 -0
  180. package/types/mintplayer-ng-bootstrap-web-components-scheduler.d.ts +354 -0
  181. package/types/mintplayer-ng-bootstrap-web-components-splitter.d.ts +165 -0
  182. 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