@vanilla-bean/components 1.0.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 (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. package/utils/string.js +72 -0
@@ -0,0 +1,720 @@
1
+ /* eslint-disable spellcheck/spell-checker */
2
+ import { styled } from '../../styled';
3
+ import { Component } from '../../Component';
4
+
5
+ import CalendarEvent from './CalendarEvent';
6
+ import Toolbar from './Toolbar';
7
+ import { getDaysInMonth, toNth } from './utils';
8
+
9
+ const StyledComponent = styled(
10
+ Component,
11
+ ({ colors }) => `
12
+ user-select: none;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 6px;
16
+
17
+ &.month {
18
+ & tr.title {
19
+ font-size: 1.3em;
20
+ height: 1.3em;
21
+ color: ${colors.black};
22
+ background-color: ${colors.lightest(colors.gray)};
23
+
24
+ & td {
25
+ text-align: center;
26
+ }
27
+ }
28
+ }
29
+
30
+ &.day {
31
+ & div.event-container {
32
+ position: absolute;
33
+ height: 100%;
34
+ width: 90%;
35
+ top: 0;
36
+ right: 0;
37
+ pointer-events: none;
38
+
39
+ & div.event {
40
+ position: absolute;
41
+ pointer-events: all;
42
+ width: 100%;
43
+ text-indent: 6px;
44
+ }
45
+ }
46
+
47
+ & div.time-block {
48
+ height: 3em;
49
+ cursor: pointer;
50
+ color: black;
51
+ background-color: ${colors.light(colors.gray)};
52
+
53
+ &:nth-child(odd) {
54
+ background-color: ${colors.lighter(colors.gray)};
55
+ }
56
+
57
+ &:last-of-type{
58
+ border-bottom: none;
59
+ }
60
+
61
+ & span.time {
62
+ font-size: 12px;
63
+ margin: 2px;
64
+ pointer-events: none;
65
+ }
66
+ }
67
+ }
68
+ `,
69
+ );
70
+
71
+ const CalendarWrapper = styled(
72
+ Component,
73
+ () => `
74
+ position: relative;
75
+ overflow: auto;
76
+ width: 100%;
77
+ flex: 1;
78
+ `,
79
+ );
80
+
81
+ const MonthCalendar = styled(
82
+ Component,
83
+ () => `
84
+ width: 100%;
85
+ height: 100%;
86
+
87
+ td {
88
+ max-width: 0;
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
91
+ white-space: nowrap;
92
+ }
93
+ `,
94
+ );
95
+
96
+ const MonthDayCell = styled(
97
+ Component,
98
+ ({ colors }) => `
99
+ position: relative;
100
+ border-collapse: collapse;
101
+ vertical-align: top;
102
+ background-color: ${colors.lighter(colors.gray)};
103
+ cursor: pointer;
104
+ padding: 3px;
105
+
106
+ & span.day-title {
107
+ display: block;
108
+ font-size: 1em;
109
+ padding: 2px;
110
+ background-color: ${colors.black.setAlpha(0.2)};
111
+ color: ${colors.black};
112
+ cursor: pointer;
113
+ }
114
+
115
+ &:hover {
116
+ background-color: ${colors.blue};
117
+
118
+ & span.day-title {
119
+ background-color: ${colors.white.setAlpha(0.3)};
120
+ }
121
+ }
122
+
123
+ &.today {
124
+ background-color: ${colors.purple};
125
+
126
+ & span.day-title {
127
+ background-color: ${colors.white.setAlpha(0.3)};
128
+ }
129
+ }
130
+
131
+ &.not-in-month {
132
+ background: #222;
133
+
134
+ & span.day-title {
135
+ color: ${colors.gray};
136
+ }
137
+ }
138
+ `,
139
+ );
140
+
141
+ const WeekDayCell = styled(
142
+ Component,
143
+ ({ colors }) => `
144
+ cursor: pointer;
145
+ background-color: ${colors.lighter(colors.gray)};
146
+ min-height: 14.285714285714286%;
147
+
148
+ &.today {
149
+ background-color: ${colors.blue};
150
+ }
151
+
152
+ & div.day-title {
153
+ cursor: pointer;
154
+ padding: 6px;
155
+ background-color: ${colors.black.setAlpha(0.2)};
156
+ font-size: 1em;
157
+ color: black;
158
+ }
159
+
160
+ & .event {
161
+ text-indent: 6px;
162
+ }
163
+ `,
164
+ );
165
+
166
+ const DayNowIndicator = styled(
167
+ Component,
168
+ ({ colors }) => `
169
+ position: absolute;
170
+ width: 105%;
171
+ height: 2px;
172
+ background-color: ${colors.red};
173
+ z-index: 34;
174
+ left: -5em;
175
+ `,
176
+ );
177
+
178
+ export const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
179
+ export const MONTHS = [
180
+ 'January',
181
+ 'February',
182
+ 'March',
183
+ 'April',
184
+ 'May',
185
+ 'June',
186
+ 'July',
187
+ 'August',
188
+ 'September',
189
+ 'October',
190
+ 'November',
191
+ 'December',
192
+ ];
193
+
194
+ /**
195
+ * Interactive calendar component supporting month, week, and day views with event management.
196
+ *
197
+ * Provides full calendar functionality with navigation, event display, and user interaction.
198
+ * Supports multiple view modes, event rendering, and date selection with customizable styling.
199
+ * @param {object} [options={}] - Calendar configuration options
200
+ * @param {('month'|'week'|'day')} [options.view='month'] - Initial calendar view mode
201
+ * @param {string} [options.height='420px'] - Calendar component height
202
+ * @param {number} [options.year] - Initial year to display, defaults to current year
203
+ * @param {number} [options.month] - Initial month to display (0-11), defaults to current month
204
+ * @param {number} [options.day] - Initial day to display, defaults to current day
205
+ * @param {Array<object>} [options.events=[]] - Calendar events array, automatically converted to CalendarEvent instances
206
+ * @param {Array<string>} [options.views] - Available view modes for toolbar
207
+ * @param {boolean} [options.display24h] - Whether to display time in 24-hour format in day view
208
+ * @param {...(Component|HTMLElement|string)} children - Child elements to append
209
+ * @returns {Calendar} Calendar component instance
210
+ */
211
+ class Calendar extends StyledComponent {
212
+ constructor(options = {}, ...children) {
213
+ super(
214
+ {
215
+ view: 'month',
216
+ height: '420px',
217
+ registeredEvents: new Set(['selectDay', 'selectTime', 'selectEvent', 'newEvent']),
218
+ ...options,
219
+ events: (options.events || []).map(eventItem => new CalendarEvent(eventItem)),
220
+ },
221
+ ...children,
222
+ );
223
+ }
224
+
225
+ static handlers = {
226
+ height(value) {
227
+ this.elem.style.height = value;
228
+ },
229
+ };
230
+
231
+ build() {
232
+ this.toolbar = new Toolbar({ appendTo: this, calendar: this, views: this.options.views, view: this.options.view });
233
+ this.wrapper = new CalendarWrapper({ appendTo: this });
234
+
235
+ if (this.options.month == null) {
236
+ const now = new Date();
237
+
238
+ this.setDate(now.getFullYear(), now.getMonth(), now.getDate());
239
+
240
+ this.adjustDateToView();
241
+ }
242
+
243
+ if (this.options.view === 'day') this.renderDay();
244
+ else if (this.options.view === 'week') this.renderWeek();
245
+ else this.renderMonth();
246
+
247
+ this.wrapper.elem.scrollTop = 0;
248
+
249
+ if (this.elem.contains(document.activeElement)) document.activeElement.blur();
250
+ }
251
+
252
+ renderDay() {
253
+ this.removeClass('month', 'week');
254
+ this.addClass('day');
255
+
256
+ this.toolbar.title.elem.textContent = `${MONTHS[this.options.month]} ${this.options.day}${toNth(
257
+ this.options.day,
258
+ )}, ${this.options.year}`;
259
+
260
+ const eventContainer = new Component({ addClass: 'event-container', appendTo: this.wrapper });
261
+ const events = this.eventsAt(new Date(this.options.year, this.options.month, this.options.day));
262
+ const minGap = 30;
263
+ const gapsPerHour = Math.ceil(60 / minGap);
264
+
265
+ for (let x = 0; x < 24; ++x) {
266
+ for (let y = 0; y < gapsPerHour; ++y) {
267
+ const minutesBlock = new Component({
268
+ addClass: 'time-block',
269
+ appendTo: this.wrapper,
270
+ onPointerPress: event => {
271
+ this.emit('selectTime', event);
272
+ },
273
+ });
274
+ const timeSpan = new Component({ tag: 'span', addClass: 'time', appendTo: minutesBlock });
275
+
276
+ const mins = y * minGap;
277
+ let h = x;
278
+ let am_pm = '';
279
+
280
+ // minutesBlock.style.height = (this.options.dayViewGapHeight - 1) +'px';
281
+
282
+ if (!this.options.display24h) {
283
+ am_pm = h < 12 ? 'AM' : 'PM';
284
+ h = h < 12 ? h : h % 12;
285
+ h = h === 0 ? 12 : h;
286
+ }
287
+
288
+ timeSpan.elem.textContent = `${h}:${mins < 10 ? '0' : ''}${mins} ${am_pm}`;
289
+ }
290
+ }
291
+
292
+ const groups = {};
293
+ let smallestGap = -1;
294
+
295
+ events.forEach(event => {
296
+ const elem = event.render(this.options.view, eventContainer.elem, this);
297
+ event.elem = elem;
298
+ event.ratio = 100;
299
+ elem.style.left = '0';
300
+
301
+ let totalGaps = event.gapCount;
302
+
303
+ if (Math.floor(event.gapCell) !== event.gapCell) ++totalGaps;
304
+
305
+ for (let x = 0; x < totalGaps; ++x) {
306
+ const gap = Math.floor(event.gapCell + x);
307
+
308
+ groups[gap] = groups[gap] || [];
309
+
310
+ groups[gap].push(event);
311
+ }
312
+
313
+ if (smallestGap === -1 || smallestGap > event.gapCell) smallestGap = event.gapCell;
314
+ });
315
+
316
+ Object.keys(groups).forEach(gap => {
317
+ const total = groups[gap].length;
318
+ const ratio = 100 / total;
319
+
320
+ groups[gap].forEach((event, index) => {
321
+ const minLeft = index * ratio;
322
+ const minRight = ratio * (total - index - 1);
323
+
324
+ if (ratio < event.ratio) {
325
+ event.ratio = ratio;
326
+ event.elem.style.left = minLeft + '%';
327
+ event.elem.style.right = minRight + '%';
328
+ }
329
+ });
330
+ });
331
+
332
+ new DayNowIndicator({
333
+ style: { top: `${(new Date().getHours() * 2 + new Date().getMinutes() / 30) * 3}em` },
334
+ appendTo: eventContainer,
335
+ });
336
+ }
337
+
338
+ renderWeek() {
339
+ this.removeClass('month', 'day');
340
+ this.addClass('week');
341
+
342
+ const dayMs = 1000 * 60 * 60 * 24;
343
+ const now = new Date();
344
+ const day = new Date(this.options.year, this.options.month, this.options.day);
345
+ const firstDay = new Date(day.getTime() - day.getDay() * dayMs);
346
+ const lastDay = new Date(firstDay.getTime() + 6 * dayMs);
347
+
348
+ let cDay = new Date(firstDay.getTime());
349
+
350
+ for (let x = 0; x < 7; ++x) {
351
+ const y = cDay.getFullYear();
352
+ const m = cDay.getMonth();
353
+ const d = cDay.getDate();
354
+ const w = cDay.getDay();
355
+ const t = cDay.getTime();
356
+
357
+ const cellDate = new Date(y, m, d);
358
+ const events = this.eventsAt(cellDate);
359
+ const isToday = y === now.getFullYear() && m === now.getMonth() && d === now.getDate();
360
+
361
+ const weekdayCell = new WeekDayCell({
362
+ addClass: isToday ? 'today' : undefined,
363
+ data: { at: cDay.getTime() },
364
+ onPointerPress: ({ target }) => {
365
+ this.emit('selectDay', { target, date: cellDate });
366
+ },
367
+ appendTo: this.wrapper,
368
+ });
369
+
370
+ new Component({
371
+ textContent: `${DAYS[w]}, ${MONTHS[m]} ${d}${toNth(d)}`,
372
+ addClass: 'day-title',
373
+ appendTo: weekdayCell,
374
+ onPointerPress: ({ target }) => {
375
+ this.emit('selectDay', { target, isTitle: true, date: cellDate });
376
+ },
377
+ });
378
+
379
+ for (let y = 0; y < events.length; ++y) events[y].render(this.options.view, weekdayCell.elem, this);
380
+
381
+ cDay = new Date(t + dayMs);
382
+ }
383
+
384
+ const firstDayAppend = toNth(firstDay.getDate());
385
+ const lastDayAppend = toNth(lastDay.getDate());
386
+
387
+ if (firstDay.getMonth() === lastDay.getMonth()) {
388
+ this.toolbar.title.elem.textContent =
389
+ MONTHS[firstDay.getMonth()] +
390
+ ' ' +
391
+ firstDay.getDate() +
392
+ firstDayAppend +
393
+ ' - ' +
394
+ lastDay.getDate() +
395
+ lastDayAppend +
396
+ ', ' +
397
+ this.options.year;
398
+ } else if (firstDay.getFullYear() === lastDay.getFullYear()) {
399
+ this.toolbar.title.elem.textContent =
400
+ MONTHS[firstDay.getMonth()] +
401
+ ' ' +
402
+ firstDay.getDate() +
403
+ firstDayAppend +
404
+ ' - ' +
405
+ MONTHS[lastDay.getMonth()] +
406
+ ' ' +
407
+ lastDay.getDate() +
408
+ lastDayAppend +
409
+ ', ' +
410
+ this.options.year;
411
+ } else {
412
+ this.toolbar.title.elem.textContent =
413
+ MONTHS[firstDay.getMonth()] +
414
+ ' ' +
415
+ firstDay.getDate() +
416
+ firstDayAppend +
417
+ ', ' +
418
+ firstDay.getFullYear() +
419
+ ' - ' +
420
+ MONTHS[lastDay.getMonth()] +
421
+ ' ' +
422
+ lastDay.getDate() +
423
+ lastDayAppend +
424
+ ', ' +
425
+ lastDay.getFullYear();
426
+ }
427
+ }
428
+
429
+ renderMonth() {
430
+ this.removeClass('week', 'day');
431
+ this.addClass('month');
432
+
433
+ const month = getDaysInMonth(this.options.year, this.options.month);
434
+ const now = new Date();
435
+ const isCurrentMonth = now.getMonth() === this.options.month && now.getFullYear() === this.options.year;
436
+ const currentDay = now.getDate();
437
+ const table = new MonthCalendar({ tag: 'table', appendTo: this.wrapper });
438
+ const titleRow = new Component({ tag: 'tr', addClass: 'title', appendTo: table });
439
+ let dayX = -month.firstDay;
440
+
441
+ for (let day = 0; day < 7; ++day) new Component({ tag: 'td', textContent: DAYS[day], appendTo: titleRow });
442
+
443
+ for (let row = 0; row < 6; ++row) {
444
+ const tr = new Component({ tag: 'tr', appendTo: table });
445
+
446
+ for (let col = 0; col < 7; ++col) {
447
+ const date = new Date(this.options.year, this.options.month, ++dayX);
448
+ const eventContainer = new Component({ addClass: 'event-container' });
449
+ const td = new MonthDayCell({
450
+ tag: 'td',
451
+ addClass: isCurrentMonth && currentDay === dayX ? 'today' : undefined,
452
+ data: { at: date.getTime() },
453
+ appendTo: tr,
454
+ onPointerPress: event => {
455
+ this.emit('selectDay', { target: event.target, date });
456
+ },
457
+ append: [
458
+ new Component({
459
+ tag: 'span',
460
+ textContent: date.getDate(),
461
+ addClass: 'day-title',
462
+ onPointerPress: event => {
463
+ this.emit('selectDay', { target: event.target, isTitle: true, date });
464
+ },
465
+ }),
466
+ eventContainer,
467
+ ],
468
+ });
469
+
470
+ if (dayX <= 0 || dayX > month.numberOfDays) td.addClass('not-in-month');
471
+
472
+ this.eventsAt(date).forEach(calendarEvent => {
473
+ if (calendarEvent.render) calendarEvent.render(this.options.view, eventContainer.elem, this);
474
+ });
475
+ }
476
+ }
477
+
478
+ this.toolbar.title.elem.textContent = MONTHS[this.options.month] + ' ' + this.options.year;
479
+ }
480
+
481
+ adjustDateToView() {
482
+ if (this.options.view !== 'week' || this.options.weekday >= this.options.day || this.options.day > 8) return;
483
+
484
+ let { year, month } = this.options;
485
+ --month;
486
+ if (month < 0) {
487
+ --year;
488
+ month = 11;
489
+ }
490
+
491
+ this.setDate(year, month, getDaysInMonth(year, month).numberOfDays);
492
+ }
493
+
494
+ /**
495
+ * Sets the calendar to display a specific date.
496
+ * @param {number} year - Year to set
497
+ * @param {number} month - Month to set (0-11)
498
+ * @param {number} day - Day to set
499
+ * @param {boolean} [reRender] - Whether to re-render the calendar after setting date
500
+ */
501
+ setDate(year, month, day, reRender) {
502
+ this.options.year = Number.parseInt(year);
503
+ this.options.month = Number.parseInt(month);
504
+ this.options.day = Number.parseInt(day);
505
+ this.options.weekday = new Date(year, month, day).getDay();
506
+
507
+ if (reRender) this.render();
508
+ }
509
+
510
+ /**
511
+ * Navigates the calendar to today's date and re-renders.
512
+ */
513
+ today() {
514
+ const now = new Date();
515
+
516
+ this.setDate(now.getFullYear(), now.getMonth(), now.getDate());
517
+
518
+ this.adjustDateToView();
519
+
520
+ this.render();
521
+ }
522
+
523
+ /**
524
+ * Navigates to the previous time period based on current view (day/week/month).
525
+ */
526
+ previous() {
527
+ let { year, month, day } = this.options;
528
+
529
+ if (this.options.view === 'day') {
530
+ --day;
531
+
532
+ if (day <= 0) {
533
+ --month;
534
+ if (month < 0) {
535
+ month = 11;
536
+ --year;
537
+ }
538
+ day = getDaysInMonth(year, month).numberOfDays;
539
+ }
540
+ } else if (this.options.view === 'month') {
541
+ --month;
542
+ if (month < 0) {
543
+ month = 11;
544
+ --year;
545
+ }
546
+ } else if (this.options.view === 'week') {
547
+ day -= 7;
548
+
549
+ if (day <= 0) {
550
+ --month;
551
+ if (month < 0) {
552
+ month = 11;
553
+ --year;
554
+ }
555
+ day = getDaysInMonth(year, month).numberOfDays + day;
556
+ }
557
+ }
558
+
559
+ this.setDate(year, month, day);
560
+ this.render();
561
+ }
562
+
563
+ /**
564
+ * Navigates to the next time period based on current view (day/week/month).
565
+ */
566
+ next() {
567
+ let { year, month, day } = this.options;
568
+
569
+ if (this.options.view === 'day') {
570
+ ++day;
571
+ const monthStat = getDaysInMonth(year, month);
572
+
573
+ if (day > monthStat.numberOfDays) {
574
+ day = 1;
575
+ ++month;
576
+ if (month > 11) {
577
+ month = 0;
578
+ ++year;
579
+ }
580
+ }
581
+ } else if (this.options.view === 'month') {
582
+ ++month;
583
+ if (month > 11) {
584
+ month = 0;
585
+ ++year;
586
+ }
587
+ } else if (this.options.view === 'week') {
588
+ day += 7;
589
+ const monthStat = getDaysInMonth(year, month);
590
+
591
+ if (day > monthStat.numberOfDays) {
592
+ day -= monthStat.numberOfDays;
593
+ ++month;
594
+ if (month > 11) {
595
+ month = 0;
596
+ ++year;
597
+ }
598
+ }
599
+ }
600
+
601
+ this.setDate(year, month, day);
602
+ this.render();
603
+ }
604
+
605
+ /**
606
+ * Navigates to a specific day and switches to day view.
607
+ * @param {number|Date} position - Date position as timestamp or Date object
608
+ */
609
+ goToDay(position) {
610
+ const date = new Date(position);
611
+
612
+ this.setDate(date.getFullYear(), date.getMonth(), date.getDate());
613
+
614
+ this.setView('day');
615
+ }
616
+
617
+ /**
618
+ * Changes the calendar view mode and updates the display.
619
+ * @param {string} view - View mode to set ('month', 'week', 'day')
620
+ * @returns {Calendar} This calendar instance for method chaining
621
+ */
622
+ setView(view) {
623
+ if (this.options.view !== view) {
624
+ this.options.view = view;
625
+
626
+ this.adjustDateToView();
627
+ this.wrapper.empty();
628
+
629
+ if (view === 'day') this.renderDay();
630
+ else if (view === 'week') this.renderWeek();
631
+ else this.renderMonth();
632
+ }
633
+
634
+ return this;
635
+ }
636
+
637
+ /**
638
+ * Retrieves all events occurring on a specific date.
639
+ * @param {string|Date} date - Date to query for events
640
+ * @returns {Array<CalendarEvent>} Array of events occurring on the specified date
641
+ */
642
+ eventsAt(date) {
643
+ if (!(date instanceof Date)) date = new Date(date);
644
+
645
+ const events = [];
646
+
647
+ this.options.events.forEach(event => {
648
+ if (!event.recurring) {
649
+ if (event.year === date.getFullYear() && event.month === date.getMonth() + 1 && event.day === date.getDate())
650
+ events.push(event);
651
+
652
+ return;
653
+ }
654
+
655
+ if (!event.daily) {
656
+ event.whitelist.forEach(dateItem => {
657
+ dateItem = new Date(dateItem);
658
+
659
+ if (
660
+ dateItem.getFullYear() === date.getFullYear() &&
661
+ dateItem.getMonth() === date.getMonth() &&
662
+ dateItem.getDate() === date.getDate()
663
+ )
664
+ events.push(event);
665
+ });
666
+
667
+ return;
668
+ }
669
+
670
+ for (let x = 0, count = event.blacklist.length; x < count; ++x) {
671
+ const dateItem = new Date(event.blacklist[x]);
672
+
673
+ // if(parseInt(dateItem[2]) === date.getFullYear() && parseInt(dateItem[1]) === date.getMonth() + 1 && parseInt(dateItem[0]) === date.getDate() + 1) return;
674
+ if (
675
+ dateItem.getFullYear() === date.getFullYear() &&
676
+ dateItem.getMonth() === date.getMonth() &&
677
+ dateItem.getDate() === date.getDate()
678
+ )
679
+ return;
680
+ }
681
+
682
+ if (
683
+ event.weekdays[DAYS[date.getDay()].toLowerCase()] &&
684
+ (date.getFullYear() > event.year ||
685
+ (date.getFullYear() === event.year && date.getMonth() + 1 > event.month) ||
686
+ (date.getFullYear() === event.year && date.getMonth() + 1 === event.month && date.getDate() >= event.day))
687
+ )
688
+ events.push(event);
689
+ });
690
+
691
+ return events;
692
+ }
693
+
694
+ /**
695
+ * Adds a new event to the calendar and re-renders.
696
+ * @param {object} eventItem - Event data object to add
697
+ * @returns {Calendar} This calendar instance for method chaining
698
+ */
699
+ addEvent(eventItem) {
700
+ if (eventItem) {
701
+ this.options.events.push(new CalendarEvent(eventItem, this));
702
+
703
+ this.emit('newEvent', eventItem);
704
+
705
+ this.render();
706
+ }
707
+
708
+ return this;
709
+ }
710
+ }
711
+
712
+ export default Calendar;
713
+
714
+ // Zero-arg scenarios for LLD verification
715
+ export const navigateBackFromJanuary = () => {
716
+ const c = new Calendar({ view: 'month', year: 2024, month: 0, autoRender: false });
717
+ c.render();
718
+ c.previous();
719
+ return { month: c.options.month, year: c.options.year };
720
+ };