@lemonadejs/calendar 3.0.3 → 3.1.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/dist/index.js CHANGED
@@ -1,791 +1,794 @@
1
- /**
2
- * render: ()
3
- * valid-ranges: []
4
- * disabled
5
- * dateToNum UTC
6
- * navigation with icons Enter key
7
- */
8
- if (! lemonade && typeof (require) === 'function') {
9
- var lemonade = require('lemonadejs');
10
- }
11
-
12
- if (! Modal && typeof (require) === 'function') {
13
- var Modal = require('@lemonadejs/modal');
14
- }
15
-
16
- ; (function (global, factory) {
17
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
18
- typeof define === 'function' && define.amd ? define(factory) :
19
- global.Calendar = factory();
20
- }(this, (function () {
21
-
22
- // Weekdays
23
- const Weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
24
-
25
- // Months
26
- const Months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
27
-
28
- // Excel like dates
29
- const excelInitialTime = Date.UTC(1900, 0, 0);
30
- const excelLeapYearBug = Date.UTC(1900, 1, 29);
31
- const millisecondsPerDay = 86400000;
32
-
33
- function isValidDate(d) {
34
- return d instanceof Date && !isNaN(d.getTime());
35
- }
36
-
37
- /**
38
- * Date to number
39
- */
40
- const dateToNum = function (jsDate) {
41
- if (typeof(jsDate) === 'string') {
42
- jsDate = new Date(jsDate + ' GMT+0');
43
- }
44
- let jsDateInMilliseconds = jsDate.getTime();
45
- if (jsDateInMilliseconds >= excelLeapYearBug) {
46
- jsDateInMilliseconds += millisecondsPerDay;
47
- }
48
- jsDateInMilliseconds -= excelInitialTime;
49
-
50
- return jsDateInMilliseconds / millisecondsPerDay;
51
- }
52
-
53
- const numToDate = function (excelSerialNumber, asString) {
54
- if (! excelSerialNumber) {
55
- return '';
56
- }
57
-
58
- let jsDateInMilliseconds = excelInitialTime + excelSerialNumber * millisecondsPerDay;
59
- if (jsDateInMilliseconds >= excelLeapYearBug) {
60
- jsDateInMilliseconds -= millisecondsPerDay;
61
- }
62
-
63
- const d = new Date(jsDateInMilliseconds);
64
-
65
- let arr = [
66
- d.getUTCFullYear(),
67
- Two(d.getUTCMonth() + 1),
68
- Two(d.getUTCDate()),
69
- Two(d.getUTCHours()),
70
- Two(d.getUTCMinutes()),
71
- Two(d.getUTCSeconds()),
72
- ];
73
-
74
- if (asString) {
75
- return arrayToStringDate(arr);
76
- } else {
77
- return arr;
78
- }
79
- }
80
-
81
- const arrayToStringDate = function(arr) {
82
- return [arr[0],Two(arr[1]),Two(arr[2])].join('-');
83
- }
84
-
85
- /**
86
- * Create a data calendar object based on the view
87
- */
88
- const views = {
89
- years: function(date) {
90
- let year = date.getFullYear();
91
- let result = [];
92
- let start = year % 16;
93
- let complement = 16 - start;
94
-
95
- for (let i = year-start; i < year+complement; i++) {
96
- let item = {
97
- title: i,
98
- value: i
99
- };
100
- result.push(item);
101
- // Select cursor
102
- if (this.cursor.y === i) {
103
- // Select item
104
- item.selected = true;
105
- // Cursor
106
- this.cursor.index = result.length - 1;
107
- }
108
- }
109
- return result;
110
- },
111
- months: function(date) {
112
- let year = date.getFullYear();
113
- let result = [];
114
- for (let i = 0; i < 12; i++) {
115
- let item = {
116
- title: Months[i].substring(0,3),
117
- value: i
118
- }
119
- // Add the item to the data
120
- result.push(item);
121
- // Select cursor
122
- if (this.cursor.y === year && this.cursor.m === i) {
123
- // Select item
124
- item.selected = true;
125
- // Cursor
126
- this.cursor.index = result.length - 1;
127
- }
128
- }
129
-
130
- return result;
131
- },
132
- days: function(date) {
133
- let year = date.getFullYear();
134
- let month = date.getMonth();
135
- let data = filterData.call(this, year, month);
136
-
137
- // First day
138
- let tmp = new Date(year, month, 1, 0, 0, 0);
139
- let firstDay = tmp.getDay();
140
-
141
- let result = [];
142
- for (let i = 1-firstDay; i <= 42-firstDay; i++) {
143
- // Get the day
144
- tmp = new Date(year, month, i, 0, 0, 0);
145
- // Day
146
- let day = tmp.getDate();
147
- // Create the item
148
- let item = {
149
- title: day,
150
- value: i,
151
- number: dateToNum(tmp.toString())
152
- }
153
- // Add the item to the date
154
- result.push(item);
155
- // Check selections
156
- if (tmp.getMonth() !== month) {
157
- // Days are not in the current month
158
- item.grey = true;
159
- } else {
160
- // Check for data
161
- let d = [ year, Two(month+1), Two(day)].join('-');
162
- if (data && data[d]) {
163
- item.data = data[d];
164
- }
165
- // Select cursor
166
- if (this.cursor.y === year && this.cursor.m === month && this.cursor.d === day) {
167
- // Select item
168
- item.selected = true;
169
- // Cursor
170
- this.cursor.index = result.length - 1;
171
- }
172
- }
173
-
174
- // Select range
175
- if (this.range && this.rangeValues) {
176
- // Mark the start and end points
177
- if (this.rangeValues[0] === item.number) {
178
- item.range = true;
179
- item.start = true;
180
- }
181
- if (this.rangeValues[1] === item.number) {
182
- item.range = true;
183
- item.end = true;
184
- }
185
- // Re-recreate teh range
186
- if (this.rangeValues[0] && this.rangeValues[1]) {
187
- if (this.rangeValues[0] <= item.number && this.rangeValues[1] >= item.number) {
188
- item.range = true;
189
- }
190
- }
191
- }
192
- }
193
-
194
- return result;
195
- },
196
- hours: function() {
197
- let result = [];
198
- for (let i = 0; i < 24; i++) {
199
- let item = {
200
- title: Two(i),
201
- value: i
202
- };
203
- result.push(item);
204
- }
205
- return result;
206
- },
207
- minutes: function() {
208
- let result = [];
209
- for (let i = 0; i < 60; i=i+5) {
210
- let item = {
211
- title: Two(i),
212
- value: i
213
- };
214
- result.push(item);
215
- }
216
- return result;
217
- }
218
- }
219
-
220
- const filterData = function(year, month) {
221
- // Data for the month
222
- let data = {};
223
- if (Array.isArray(this.data)) {
224
- this.data.map(function (v) {
225
- let d = year + '-' + Two(month + 1);
226
- if (v.date.substring(0, 7) === d) {
227
- if (!data[v.date]) {
228
- data[v.date] = [];
229
- }
230
- data[v.date].push(v);
231
- }
232
- });
233
- }
234
- return data;
235
- }
236
- // Get the short weekdays name
237
- const getWeekdays = function() {
238
- return Weekdays.map(w => {
239
- return { title: w.substring(0, 1) };
240
- })
241
- }
242
-
243
- // Define the hump based on the view
244
- const getJump = function(e) {
245
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
246
- return this.view === 'days' ? 7 : 4;
247
- }
248
-
249
- return 1;
250
- }
251
-
252
- // Get the position of the data based on the view
253
- const getPosition = function() {
254
- let position = 2;
255
- if (this.view === 'years') {
256
- position = 0;
257
- } else if (this.view === 'months') {
258
- position = 1;
259
- }
260
- return position;
261
- }
262
-
263
- // Transform in two digits
264
- const Two = function(value) {
265
- value = '' + value;
266
- if (value.length === 1) {
267
- value = '0' + value;
268
- }
269
- return value;
270
- }
271
-
272
- const Calendar = function() {
273
- let self = this;
274
-
275
- // Weekdays
276
- self.weekdays = getWeekdays();
277
-
278
- // Cursor
279
- self.cursor = {};
280
-
281
- // Calendar date
282
- let date = new Date();
283
-
284
- // Range
285
- self.rangeValues = null;
286
-
287
- /**
288
- * Update the internal date
289
- * @param {Date|string|number[]} d instance of Date
290
- *
291
- */
292
- const setDate = function(d) {
293
- if (Array.isArray(d)) {
294
- d = new Date(Date.UTC(...d));
295
- } else if (typeof(d) === 'string') {
296
- d = new Date(d);
297
- }
298
- // Update internal date
299
- date = d;
300
- // Update the headers of the calendar
301
- let value = d.toISOString().substring(0,10).split('-');
302
- // Update the month label
303
- self.month = Months[parseInt(value[1])-1];
304
- // Update the year label
305
- self.year = parseInt(value[0]);
306
- // Load data
307
- if (! self.view) {
308
- // Start on the days view will start the data
309
- self.view = 'days';
310
- } else {
311
- // Reload the data for the same view
312
- self.options = views[self.view].call(self, date);
313
- }
314
- }
315
-
316
- /**
317
- * Set the internal cursor
318
- * @param {object} s
319
- */
320
- const setCursor = function(s) {
321
- // Remove selection from the current object
322
- let item = self.options[self.cursor.index];
323
- if (typeof(item) !== 'undefined') {
324
- item.selected = false;
325
- }
326
- // Update the date based on the click
327
- let v = updateDate(s.value, getPosition.call(self));
328
- let d = new Date(Date.UTC(...v));
329
- // Update cursor controller
330
- self.cursor = {
331
- y: d.getFullYear(),
332
- m: d.getMonth(),
333
- d: d.getDate(),
334
- };
335
- // Update cursor based on the object position
336
- if (s) {
337
- // Update selected property
338
- s.selected = true;
339
- // New cursor
340
- self.cursor.index = self.options.indexOf(s);
341
- }
342
- return d;
343
- }
344
-
345
- /**
346
- * Update the current date
347
- * @param {number} v new value for year, month or day
348
- * @param {number} position (0,1,2 - year,month,day)
349
- * @returns {number[]}
350
- */
351
- const updateDate = function(v, position) {
352
- // Current internal date
353
- let value = [date.getFullYear(), date.getMonth(), date.getDate(),0,0,0];
354
- // Update internal date
355
- value[position] = v;
356
- // Return new value
357
- return value;
358
- }
359
-
360
- /**
361
- * This method move the data from the view up or down
362
- * @param direction
363
- */
364
- const move = function(direction) {
365
- let value;
366
-
367
- // Update the new internal date
368
- if (self.view === 'days') {
369
- // Select the new internal date
370
- value = updateDate(date.getMonth()+direction, 1);
371
- } else if (self.view === 'months') {
372
- // Select the new internal date
373
- value = updateDate(date.getFullYear()+direction, 0);
374
- } else if (self.view === 'years') {
375
- // Select the new internal date
376
- value = updateDate(date.getFullYear()+(direction*16), 0);
377
- }
378
-
379
- // Update view
380
- if (value) {
381
- setDate(value);
382
- }
383
- }
384
-
385
- /**
386
- * Keyboard handler
387
- * @param {number} direction of the action
388
- * @param {object} e keyboard event
389
- */
390
- const moveCursor = function(direction, e) {
391
- direction = direction * getJump.call(self, e);
392
- // Remove the selected from the current selection
393
- let s = self.options[self.cursor.index];
394
- // If the selection is going outside the viewport
395
- if (typeof(s) === 'undefined' || ! s.selected) {
396
- // Go back to the view
397
- setDate([ self.cursor.y, self.cursor.m, self.cursor.d ]);
398
- }
399
-
400
- // Jump to the index
401
- let index = self.cursor.index + direction;
402
-
403
- // See if the new position is in the viewport
404
- if (typeof(self.options[index]) === 'undefined') {
405
- // Adjust the index for next collection of data
406
- if (self.view === 'days') {
407
- if (index < 0) {
408
- index = 42 + index;
409
- } else {
410
- index = index - 42;
411
- }
412
- } else if (self.view === 'years') {
413
- if (index < 0) {
414
- index = 4 + index;
415
- } else {
416
- index = index - 4;
417
- }
418
- } else if (self.view === 'months') {
419
- if (index < 0) {
420
- index = 12 + index;
421
- } else {
422
- index = index - 12;
423
- }
424
- }
425
-
426
- // Move the data up or down
427
- move(direction > 0 ? 1 : -1);
428
- }
429
-
430
- // Update the date based on the click
431
- setCursor(self.options[index]);
432
-
433
- // Update ranges
434
- updateRange(self.options[index])
435
- }
436
-
437
- /**
438
- * Handler blur
439
- * @param e
440
- */
441
- const blur = function(e) {
442
- if (self.modal) {
443
- if (!(e.relatedTarget && self.modal.el.contains(e.relatedTarget))) {
444
- if (self.modal.closed === false) {
445
- self.modal.closed = true
446
- }
447
- }
448
- }
449
- }
450
-
451
- /**
452
- * Set the limits of a range
453
- * @param s
454
- */
455
- const setRange = function(s) {
456
- if (self.view === 'days' && self.range) {
457
- let d = self.getValue();
458
- // Date to number
459
- let number = dateToNum(d);
460
- // Start a new range
461
- if (self.rangeValues && (self.rangeValues[0] > number || self.rangeValues[1])) {
462
- destroyRange();
463
- }
464
- // Range
465
- s.range = true;
466
- // Update range
467
- if (! self.rangeValues) {
468
- s.start = true;
469
- self.rangeValues = [number, null];
470
- } else {
471
- s.end = true;
472
- self.rangeValues[1] = number;
473
- }
474
- }
475
- }
476
-
477
- /**
478
- * Update the visible range
479
- * @param s
480
- */
481
- const updateRange = function(s) {
482
- if (self.range && self.view === 'days' && self.rangeValues) {
483
- // Creating a range
484
- if (self.rangeValues[0] && ! self.rangeValues[1]) {
485
- let number = s.number;
486
- if (number) {
487
- // Update range properties
488
- for (let i = 0; i < self.options.length; i++) {
489
- // Item number
490
- let v = self.options[i].number;
491
- // Update property condition
492
- self.options[i].range = v >= self.rangeValues[0] && v <= number;
493
- }
494
- }
495
- }
496
- }
497
- }
498
-
499
- /**
500
- * Destroy the range
501
- */
502
- const destroyRange = function() {
503
- for (let i = 0; i < self.options.length; i++) {
504
- self.options[i].range = false;
505
- self.options[i].start = false;
506
- self.options[i].end = false;
507
- }
508
- self.rangeValues = null;
509
- }
510
-
511
- const setValue = function(reset) {
512
- if (reset) {
513
- self.value = '';
514
- self.input.value = '';
515
- destroyRange();
516
- } else {
517
- if (self.range) {
518
- self.value = self.rangeValues[0];
519
- if (self.input) {
520
- if (self.numeric) {
521
- self.input.value = self.rangeValues;
522
- } else {
523
- self.input.value = [numToDate(self.rangeValues[0], true), numToDate(self.rangeValues[1], true)];
524
- }
525
- }
526
- } else {
527
- self.value = self.getValue();
528
- if (self.input) {
529
- if (self.numeric) {
530
- self.input.value = dateToNum(self.value);
531
- } else {
532
- self.input.value = self.value;
533
- }
534
- }
535
- }
536
- }
537
- }
538
-
539
- /**
540
- * Select an item with the enter or mouse
541
- * @param {object} e - mouse event
542
- * @param {object} item - selected cell
543
- */
544
- self.select = function(e, item) {
545
- // Update cursor generic
546
- let value = setCursor(item);
547
- // Update range
548
- setRange(item);
549
- // Based where was the click
550
- if (! (self.view === 'days' && ! item.grey)) {
551
- // Update the internal date
552
- setDate(value);
553
- }
554
- // Go back to the view of days
555
- if (self.view !== 'days') {
556
- self.view = 'days';
557
- }
558
- }
559
-
560
- /**
561
- * Next handler
562
- * @param {object?} e mouse event
563
- */
564
- self.next = function(e) {
565
- if (! e || e.type === 'click') {
566
- // Icon click
567
- move(1);
568
- } else {
569
- // Keyboard handler
570
- moveCursor(1, e);
571
- }
572
- }
573
-
574
- /**
575
- * Next handler
576
- * @param {object?} e mouse event
577
- */
578
- self.prev = function(e) {
579
- if (! e || e.type === 'click') {
580
- // Icon click
581
- move(-1);
582
- } else {
583
- // Keyboard handler
584
- moveCursor(-1, e);
585
- }
586
- }
587
-
588
- /**
589
- * Open the modal
590
- */
591
- self.open = function(e) {
592
- if (self.modal && self.modal.closed) {
593
- // Open modal
594
- self.modal.closed = false;
595
- // Set the focus on the content to use the keyboard
596
- if (! (self.input && e.target.getAttribute('readonly') === null)) {
597
- self.content.focus();
598
- }
599
- }
600
- }
601
-
602
- /**
603
- * Close the modal
604
- */
605
- self.close = function() {
606
- if (self.modal && self.modal.closed === false) {
607
- // Close modal
608
- self.modal.closed = true;
609
- }
610
- }
611
-
612
- self.reset = function() {
613
- setValue(true);
614
- self.close();
615
- }
616
-
617
- self.update = function() {
618
- setValue();
619
- self.close();
620
- }
621
-
622
- /**
623
- * Change the view
624
- */
625
- self.setView = function() {
626
- let v = this.getAttribute('data-view');
627
- if (v) {
628
- self.view = v;
629
- }
630
- }
631
-
632
- /**
633
- * Get value from cursor
634
- * @returns {string}
635
- */
636
- self.getValue = function() {
637
- let v = [ self.cursor.y, self.cursor.m, self.cursor.d ];
638
- let d = new Date(Date.UTC(...v));
639
- // Update the headers of the calendar
640
- return d.toISOString().substring(0,10);
641
- }
642
-
643
- self.onchange = function(prop) {
644
- if (prop === 'view') {
645
- if (typeof(views[self.view]) === 'function') {
646
- // When change the view update the data
647
- self.options = views[self.view].call(self, date);
648
- }
649
- } else if (prop === 'value') {
650
- if (typeof(self.onupdate) === 'function') {
651
- self.onupdate.call(self, self.value);
652
- }
653
- } else if (prop === 'options') {
654
- self.content.focus();
655
- }
656
- }
657
-
658
- self.onload = function() {
659
- let d;
660
- if (self.value) {
661
- d = new Date(self.value);
662
- }
663
- // if no date is defined
664
- if (! isValidDate(d)) {
665
- d = new Date();
666
- }
667
- // Update my index
668
- self.cursor = {
669
- y: d.getFullYear(),
670
- m: d.getMonth(),
671
- d: d.getDate(),
672
- };
673
- // Populate components
674
- self.hours = views.hours();
675
- self.minutes = views.minutes();
676
-
677
- // Update the internal calendar date
678
- setDate(d);
679
-
680
- if (self.type !== "inline") {
681
- // Create modal instance
682
- self.modal = {
683
- width: 300,
684
- closed: true,
685
- focus: false,
686
- };
687
- // Generate modal
688
- Modal(self.el, self.modal);
689
- }
690
-
691
- // Create input controls
692
- if (self.input) {
693
- self.input.classList.add('lm-calendar-input');
694
- self.input.addEventListener('focus', self.open);
695
- self.input.addEventListener('click', self.open);
696
- self.input.addEventListener('blur', blur);
697
-
698
- // Retrieve the value
699
- if (self.value) {
700
- self.input.value = self.value;
701
- } else if (self.input.value) {
702
- self.value = self.input.value;
703
- }
704
- }
705
-
706
- /**
707
- * Handler keyboard
708
- * @param {object} e - event
709
- */
710
- self.content.addEventListener('keydown', function(e){
711
- if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
712
- self.prev(e);
713
- } else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
714
- self.next(e);
715
- } else if (e.key === 'Enter') {
716
- // Current view
717
- let view = self.view;
718
- // Select
719
- self.select(e, self.options[self.cursor.index]);
720
- // If is range do something diferent
721
- if (view === 'days' && ! self.range) {
722
- self.update();
723
- }
724
- }
725
- });
726
-
727
- /**
728
- * Mouse wheel handler
729
- * @param {object} e - mouse event
730
- */
731
- self.content.addEventListener('wheel', function(e){
732
- if (e.deltaY < 0) {
733
- self.prev();
734
- } else {
735
- self.next();
736
- }
737
- });
738
-
739
- /**
740
- * Range handler
741
- * @param {object} e - mouse event
742
- */
743
- self.content.addEventListener('mouseover', function(e){
744
- if (e.target.lemon) {
745
- updateRange(e.target.lemon.self);
746
- }
747
- });
748
-
749
- // Create event for focus out
750
- self.el.addEventListener("focusout", (e) => {
751
- if (e.relatedTarget !== self.input && ! self.el.contains(e.relatedTarget)) {
752
- self.close();
753
- }
754
- });
755
- }
756
-
757
- return `<div class="lm-calendar">
758
- <div class="lm-calendar-options">
759
- <button onclick="self.reset">Reset</button>
760
- <button onclick="self.update">Done</button>
761
- </div>
762
- <div class="lm-calendar-container" data-view="{{self.view}}">
763
- <div class="lm-calendar-header">
764
- <div>
765
- <div class="lm-calendar-labels"><button onclick="self.setView" data-view="months">{{self.month}}</button> <button onclick="self.setView" data-view="years">{{self.year}}</button></div>
766
- <div class="lm-calendar-navigation"><button type="button" class="material-icons ripple" onclick="self.prev" tabindex="0">arrow_drop_up</button> <button type="button" class="material-icons ripple" onclick="self.next" tabindex="0">arrow_drop_down</button></div>
767
- </div>
768
- <div class="lm-calendar-weekdays" :loop="self.weekdays"><div>{{self.title}}</div></div>
769
- </div>
770
- <div class="lm-calendar-content" :loop="self.options" tabindex="0" :ref="self.content">
771
- <div data-start="{{self.start}}" data-end="{{self.end}}" data-range="{{self.range}}" data-event="{{self.data}}" data-grey="{{self.grey}}" data-bold="{{self.bold}}" data-selected="{{self.selected}}" onclick="self.parent.select">{{self.title}}</div>
772
- </div>
773
- <div class="lm-calendar-footer">
774
- <div><select :loop="self.hours"><option value="{{self.value}}">{{self.title}}</option></select>:<select :loop="self.minutes"><option value="{{self.value}}">{{self.title}}</option></select></div>
775
- <div><input type="button" value="Update" onclick="self.update" class="ripple"></div>
776
- </div>
777
- </div>
778
- </div>`
779
- }
780
-
781
- lemonade.setComponents({ Calendar: Calendar });
782
-
783
- return function (root, options) {
784
- if (typeof (root) === 'object') {
785
- lemonade.render(Calendar, root, options)
786
- return options;
787
- } else {
788
- return Calendar.call(this, root)
789
- }
790
- }
1
+ /**
2
+ * render: ()
3
+ * valid-ranges: []
4
+ * disabled
5
+ * dateToNum UTC
6
+ * navigation with icons Enter key
7
+ */
8
+ if (! lemonade && typeof (require) === 'function') {
9
+ var lemonade = require('lemonadejs');
10
+ }
11
+
12
+ if (! Modal && typeof (require) === 'function') {
13
+ var Modal = require('@lemonadejs/modal');
14
+ }
15
+
16
+ ; (function (global, factory) {
17
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
18
+ typeof define === 'function' && define.amd ? define(factory) :
19
+ global.Calendar = factory();
20
+ }(this, (function () {
21
+
22
+ // Weekdays
23
+ const Weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
24
+
25
+ // Months
26
+ const Months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
27
+
28
+ // Excel like dates
29
+ const excelInitialTime = Date.UTC(1900, 0, 0);
30
+ const excelLeapYearBug = Date.UTC(1900, 1, 29);
31
+ const millisecondsPerDay = 86400000;
32
+
33
+ function isValidDate(d) {
34
+ return d instanceof Date && !isNaN(d.getTime());
35
+ }
36
+
37
+ /**
38
+ * Date to number
39
+ */
40
+ const dateToNum = function (jsDate) {
41
+ if (typeof(jsDate) === 'string') {
42
+ jsDate = new Date(jsDate + ' GMT+0');
43
+ }
44
+ let jsDateInMilliseconds = jsDate.getTime();
45
+ if (jsDateInMilliseconds >= excelLeapYearBug) {
46
+ jsDateInMilliseconds += millisecondsPerDay;
47
+ }
48
+ jsDateInMilliseconds -= excelInitialTime;
49
+
50
+ return jsDateInMilliseconds / millisecondsPerDay;
51
+ }
52
+
53
+ const numToDate = function (excelSerialNumber, asString) {
54
+ if (! excelSerialNumber) {
55
+ return '';
56
+ }
57
+
58
+ let jsDateInMilliseconds = excelInitialTime + excelSerialNumber * millisecondsPerDay;
59
+ if (jsDateInMilliseconds >= excelLeapYearBug) {
60
+ jsDateInMilliseconds -= millisecondsPerDay;
61
+ }
62
+
63
+ const d = new Date(jsDateInMilliseconds);
64
+
65
+ let arr = [
66
+ d.getUTCFullYear(),
67
+ Two(d.getUTCMonth() + 1),
68
+ Two(d.getUTCDate()),
69
+ Two(d.getUTCHours()),
70
+ Two(d.getUTCMinutes()),
71
+ Two(d.getUTCSeconds()),
72
+ ];
73
+
74
+ if (asString) {
75
+ return arrayToStringDate(arr);
76
+ } else {
77
+ return arr;
78
+ }
79
+ }
80
+
81
+ const arrayToStringDate = function(arr) {
82
+ return [arr[0],Two(arr[1]),Two(arr[2])].join('-');
83
+ }
84
+
85
+ /**
86
+ * Create a data calendar object based on the view
87
+ */
88
+ const views = {
89
+ years: function(date) {
90
+ let year = date.getFullYear();
91
+ let result = [];
92
+ let start = year % 16;
93
+ let complement = 16 - start;
94
+
95
+ for (let i = year-start; i < year+complement; i++) {
96
+ let item = {
97
+ title: i,
98
+ value: i
99
+ };
100
+ result.push(item);
101
+ // Select cursor
102
+ if (this.cursor.y === i) {
103
+ // Select item
104
+ item.selected = true;
105
+ // Cursor
106
+ this.cursor.index = result.length - 1;
107
+ }
108
+ }
109
+ return result;
110
+ },
111
+ months: function(date) {
112
+ let year = date.getFullYear();
113
+ let result = [];
114
+ for (let i = 0; i < 12; i++) {
115
+ let item = {
116
+ title: Months[i].substring(0,3),
117
+ value: i
118
+ }
119
+ // Add the item to the data
120
+ result.push(item);
121
+ // Select cursor
122
+ if (this.cursor.y === year && this.cursor.m === i) {
123
+ // Select item
124
+ item.selected = true;
125
+ // Cursor
126
+ this.cursor.index = result.length - 1;
127
+ }
128
+ }
129
+
130
+ return result;
131
+ },
132
+ days: function(date) {
133
+ let year = date.getFullYear();
134
+ let month = date.getMonth();
135
+ let data = filterData.call(this, year, month);
136
+
137
+ // First day
138
+ let tmp = new Date(year, month, 1, 0, 0, 0);
139
+ let firstDay = tmp.getDay();
140
+
141
+ let result = [];
142
+ for (let i = 1-firstDay; i <= 42-firstDay; i++) {
143
+ // Get the day
144
+ tmp = new Date(year, month, i, 0, 0, 0);
145
+ // Day
146
+ let day = tmp.getDate();
147
+ // Create the item
148
+ let item = {
149
+ title: day,
150
+ value: i,
151
+ number: dateToNum(tmp.toString())
152
+ }
153
+ // Add the item to the date
154
+ result.push(item);
155
+ // Check selections
156
+ if (tmp.getMonth() !== month) {
157
+ // Days are not in the current month
158
+ item.grey = true;
159
+ } else {
160
+ // Check for data
161
+ let d = [ year, Two(month+1), Two(day)].join('-');
162
+ if (data && data[d]) {
163
+ item.data = data[d];
164
+ }
165
+ // Select cursor
166
+ if (this.cursor.y === year && this.cursor.m === month && this.cursor.d === day) {
167
+ // Select item
168
+ item.selected = true;
169
+ // Cursor
170
+ this.cursor.index = result.length - 1;
171
+ }
172
+ }
173
+
174
+ // Select range
175
+ if (this.range && this.rangeValues) {
176
+ // Mark the start and end points
177
+ if (this.rangeValues[0] === item.number) {
178
+ item.range = true;
179
+ item.start = true;
180
+ }
181
+ if (this.rangeValues[1] === item.number) {
182
+ item.range = true;
183
+ item.end = true;
184
+ }
185
+ // Re-recreate teh range
186
+ if (this.rangeValues[0] && this.rangeValues[1]) {
187
+ if (this.rangeValues[0] <= item.number && this.rangeValues[1] >= item.number) {
188
+ item.range = true;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ return result;
195
+ },
196
+ hours: function() {
197
+ let result = [];
198
+ for (let i = 0; i < 24; i++) {
199
+ let item = {
200
+ title: Two(i),
201
+ value: i
202
+ };
203
+ result.push(item);
204
+ }
205
+ return result;
206
+ },
207
+ minutes: function() {
208
+ let result = [];
209
+ for (let i = 0; i < 60; i=i+5) {
210
+ let item = {
211
+ title: Two(i),
212
+ value: i
213
+ };
214
+ result.push(item);
215
+ }
216
+ return result;
217
+ }
218
+ }
219
+
220
+ const filterData = function(year, month) {
221
+ // Data for the month
222
+ let data = {};
223
+ if (Array.isArray(this.data)) {
224
+ this.data.map(function (v) {
225
+ let d = year + '-' + Two(month + 1);
226
+ if (v.date.substring(0, 7) === d) {
227
+ if (!data[v.date]) {
228
+ data[v.date] = [];
229
+ }
230
+ data[v.date].push(v);
231
+ }
232
+ });
233
+ }
234
+ return data;
235
+ }
236
+ // Get the short weekdays name
237
+ const getWeekdays = function() {
238
+ return Weekdays.map(w => {
239
+ return { title: w.substring(0, 1) };
240
+ })
241
+ }
242
+
243
+ // Define the hump based on the view
244
+ const getJump = function(e) {
245
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
246
+ return this.view === 'days' ? 7 : 4;
247
+ }
248
+
249
+ return 1;
250
+ }
251
+
252
+ // Get the position of the data based on the view
253
+ const getPosition = function() {
254
+ let position = 2;
255
+ if (this.view === 'years') {
256
+ position = 0;
257
+ } else if (this.view === 'months') {
258
+ position = 1;
259
+ }
260
+ return position;
261
+ }
262
+
263
+ // Transform in two digits
264
+ const Two = function(value) {
265
+ value = '' + value;
266
+ if (value.length === 1) {
267
+ value = '0' + value;
268
+ }
269
+ return value;
270
+ }
271
+
272
+ const Calendar = function() {
273
+ let self = this;
274
+
275
+ // Weekdays
276
+ self.weekdays = getWeekdays();
277
+
278
+ // Cursor
279
+ self.cursor = {};
280
+
281
+ // Calendar date
282
+ let date = new Date();
283
+
284
+ // Range
285
+ self.rangeValues = null;
286
+
287
+ /**
288
+ * Update the internal date
289
+ * @param {Date|string|number[]} d instance of Date
290
+ *
291
+ */
292
+ const setDate = function(d) {
293
+ if (Array.isArray(d)) {
294
+ d = new Date(Date.UTC(...d));
295
+ } else if (typeof(d) === 'string') {
296
+ d = new Date(d);
297
+ }
298
+ // Update internal date
299
+ date = d;
300
+ // Update the headers of the calendar
301
+ let value = d.toISOString().substring(0,10).split('-');
302
+ // Update the month label
303
+ self.month = Months[parseInt(value[1])-1];
304
+ // Update the year label
305
+ self.year = parseInt(value[0]);
306
+ // Load data
307
+ if (! self.view) {
308
+ // Start on the days view will start the data
309
+ self.view = 'days';
310
+ } else {
311
+ // Reload the data for the same view
312
+ self.options = views[self.view].call(self, date);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Set the internal cursor
318
+ * @param {object} s
319
+ */
320
+ const setCursor = function(s) {
321
+ // Remove selection from the current object
322
+ let item = self.options[self.cursor.index];
323
+ if (typeof(item) !== 'undefined') {
324
+ item.selected = false;
325
+ }
326
+ // Update the date based on the click
327
+ let v = updateDate(s.value, getPosition.call(self));
328
+ let d = new Date(Date.UTC(...v));
329
+ // Update cursor controller
330
+ self.cursor = {
331
+ y: d.getFullYear(),
332
+ m: d.getMonth(),
333
+ d: d.getDate(),
334
+ };
335
+ // Update cursor based on the object position
336
+ if (s) {
337
+ // Update selected property
338
+ s.selected = true;
339
+ // New cursor
340
+ self.cursor.index = self.options.indexOf(s);
341
+ }
342
+ return d;
343
+ }
344
+
345
+ /**
346
+ * Update the current date
347
+ * @param {number} v new value for year, month or day
348
+ * @param {number} position (0,1,2 - year,month,day)
349
+ * @returns {number[]}
350
+ */
351
+ const updateDate = function(v, position) {
352
+ // Current internal date
353
+ let value = [date.getFullYear(), date.getMonth(), date.getDate(),0,0,0];
354
+ // Update internal date
355
+ value[position] = v;
356
+ // Return new value
357
+ return value;
358
+ }
359
+
360
+ /**
361
+ * This method move the data from the view up or down
362
+ * @param direction
363
+ */
364
+ const move = function(direction) {
365
+ let value;
366
+
367
+ // Update the new internal date
368
+ if (self.view === 'days') {
369
+ // Select the new internal date
370
+ value = updateDate(date.getMonth()+direction, 1);
371
+ } else if (self.view === 'months') {
372
+ // Select the new internal date
373
+ value = updateDate(date.getFullYear()+direction, 0);
374
+ } else if (self.view === 'years') {
375
+ // Select the new internal date
376
+ value = updateDate(date.getFullYear()+(direction*16), 0);
377
+ }
378
+
379
+ // Update view
380
+ if (value) {
381
+ setDate(value);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Keyboard handler
387
+ * @param {number} direction of the action
388
+ * @param {object} e keyboard event
389
+ */
390
+ const moveCursor = function(direction, e) {
391
+ direction = direction * getJump.call(self, e);
392
+ // Remove the selected from the current selection
393
+ let s = self.options[self.cursor.index];
394
+ // If the selection is going outside the viewport
395
+ if (typeof(s) === 'undefined' || ! s.selected) {
396
+ // Go back to the view
397
+ setDate([ self.cursor.y, self.cursor.m, self.cursor.d ]);
398
+ }
399
+
400
+ // Jump to the index
401
+ let index = self.cursor.index + direction;
402
+
403
+ // See if the new position is in the viewport
404
+ if (typeof(self.options[index]) === 'undefined') {
405
+ // Adjust the index for next collection of data
406
+ if (self.view === 'days') {
407
+ if (index < 0) {
408
+ index = 42 + index;
409
+ } else {
410
+ index = index - 42;
411
+ }
412
+ } else if (self.view === 'years') {
413
+ if (index < 0) {
414
+ index = 4 + index;
415
+ } else {
416
+ index = index - 4;
417
+ }
418
+ } else if (self.view === 'months') {
419
+ if (index < 0) {
420
+ index = 12 + index;
421
+ } else {
422
+ index = index - 12;
423
+ }
424
+ }
425
+
426
+ // Move the data up or down
427
+ move(direction > 0 ? 1 : -1);
428
+ }
429
+
430
+ // Update the date based on the click
431
+ setCursor(self.options[index]);
432
+
433
+ // Update ranges
434
+ updateRange(self.options[index])
435
+ }
436
+
437
+ /**
438
+ * Handler blur
439
+ * @param e
440
+ */
441
+ const blur = function(e) {
442
+ if (self.modal) {
443
+ if (!(e.relatedTarget && self.modal.el.contains(e.relatedTarget))) {
444
+ if (self.modal.closed === false) {
445
+ self.modal.closed = true
446
+ }
447
+ }
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Set the limits of a range
453
+ * @param s
454
+ */
455
+ const setRange = function(s) {
456
+ if (self.view === 'days' && self.range) {
457
+ let d = self.getValue();
458
+ // Date to number
459
+ let number = dateToNum(d);
460
+ // Start a new range
461
+ if (self.rangeValues && (self.rangeValues[0] > number || self.rangeValues[1])) {
462
+ destroyRange();
463
+ }
464
+ // Range
465
+ s.range = true;
466
+ // Update range
467
+ if (! self.rangeValues) {
468
+ s.start = true;
469
+ self.rangeValues = [number, null];
470
+ } else {
471
+ s.end = true;
472
+ self.rangeValues[1] = number;
473
+ }
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Update the visible range
479
+ * @param s
480
+ */
481
+ const updateRange = function(s) {
482
+ if (self.range && self.view === 'days' && self.rangeValues) {
483
+ // Creating a range
484
+ if (self.rangeValues[0] && ! self.rangeValues[1]) {
485
+ let number = s.number;
486
+ if (number) {
487
+ // Update range properties
488
+ for (let i = 0; i < self.options.length; i++) {
489
+ // Item number
490
+ let v = self.options[i].number;
491
+ // Update property condition
492
+ self.options[i].range = v > self.rangeValues[0] && v <= number;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Destroy the range
501
+ */
502
+ const destroyRange = function() {
503
+ for (let i = 0; i < self.options.length; i++) {
504
+ self.options[i].range = false;
505
+ self.options[i].start = false;
506
+ self.options[i].end = false;
507
+ }
508
+ self.rangeValues = null;
509
+ }
510
+
511
+ const setValue = function(reset) {
512
+ if (reset) {
513
+ self.value = '';
514
+ self.input.value = '';
515
+ destroyRange();
516
+ } else {
517
+ if (self.range) {
518
+ self.value = self.rangeValues[0];
519
+ if (self.input) {
520
+ if (self.numeric) {
521
+ self.input.value = self.rangeValues;
522
+ } else {
523
+ self.input.value = [numToDate(self.rangeValues[0], true), numToDate(self.rangeValues[1], true)];
524
+ }
525
+ }
526
+ } else {
527
+ self.value = self.getValue();
528
+ if (self.input) {
529
+ if (self.numeric) {
530
+ self.input.value = dateToNum(self.value);
531
+ } else {
532
+ self.input.value = self.value;
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Select an item with the enter or mouse
541
+ * @param {object} e - mouse event
542
+ * @param {object} item - selected cell
543
+ */
544
+ self.select = function(e, item) {
545
+ // Update cursor generic
546
+ let value = setCursor(item);
547
+ // Update range
548
+ setRange(item);
549
+ // Based where was the click
550
+ if (! (self.view === 'days' && ! item.grey)) {
551
+ // Update the internal date
552
+ setDate(value);
553
+ }
554
+ // Go back to the view of days
555
+ if (self.view !== 'days') {
556
+ self.view = 'days';
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Next handler
562
+ * @param {object?} e mouse event
563
+ */
564
+ self.next = function(e) {
565
+ if (! e || e.type === 'click') {
566
+ // Icon click
567
+ move(1);
568
+ } else {
569
+ // Keyboard handler
570
+ moveCursor(1, e);
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Next handler
576
+ * @param {object?} e mouse event
577
+ */
578
+ self.prev = function(e) {
579
+ if (! e || e.type === 'click') {
580
+ // Icon click
581
+ move(-1);
582
+ } else {
583
+ // Keyboard handler
584
+ moveCursor(-1, e);
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Open the modal
590
+ */
591
+ self.open = function(e) {
592
+ if (self.modal && self.modal.closed) {
593
+ // Open modal
594
+ self.modal.closed = false;
595
+ // Set the focus on the content to use the keyboard
596
+ if (! (self.input && e.target.getAttribute('readonly') === null)) {
597
+ self.content.focus();
598
+ }
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Close the modal
604
+ */
605
+ self.close = function() {
606
+ if (self.modal && self.modal.closed === false) {
607
+ // Close modal
608
+ self.modal.closed = true;
609
+ }
610
+ }
611
+
612
+ self.reset = function() {
613
+ setValue(true);
614
+ self.close();
615
+ }
616
+
617
+ self.update = function() {
618
+ setValue();
619
+ self.close();
620
+ }
621
+
622
+ /**
623
+ * Change the view
624
+ */
625
+ self.setView = function() {
626
+ let v = this.getAttribute('data-view');
627
+ if (v) {
628
+ self.view = v;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Get value from cursor
634
+ * @returns {string}
635
+ */
636
+ self.getValue = function() {
637
+ let v = [ self.cursor.y, self.cursor.m, self.cursor.d ];
638
+ let d = new Date(Date.UTC(...v));
639
+ // Update the headers of the calendar
640
+ return d.toISOString().substring(0,10);
641
+ }
642
+
643
+ self.onchange = function(prop) {
644
+ if (prop === 'view') {
645
+ if (typeof(views[self.view]) === 'function') {
646
+ // When change the view update the data
647
+ self.options = views[self.view].call(self, date);
648
+ }
649
+ } else if (prop === 'value') {
650
+ if (typeof(self.onupdate) === 'function') {
651
+ self.onupdate.call(self, self.value);
652
+ }
653
+ } else if (prop === 'options') {
654
+ self.content.focus();
655
+ }
656
+ }
657
+
658
+ self.onload = function() {
659
+ let d;
660
+ if (self.value) {
661
+ d = new Date(self.value);
662
+ }
663
+ // if no date is defined
664
+ if (! isValidDate(d)) {
665
+ d = new Date();
666
+ }
667
+ // Update my index
668
+ self.cursor = {
669
+ y: d.getFullYear(),
670
+ m: d.getMonth(),
671
+ d: d.getDate(),
672
+ };
673
+ // Populate components
674
+ self.hours = views.hours();
675
+ self.minutes = views.minutes();
676
+
677
+ // Update the internal calendar date
678
+ setDate(d);
679
+
680
+ if (self.type !== "inline") {
681
+ // Create modal instance
682
+ self.modal = {
683
+ width: 300,
684
+ closed: true,
685
+ focus: false,
686
+ position: 'absolute',
687
+ 'auto-adjust': true,
688
+ 'auto-close': false,
689
+ };
690
+ // Generate modal
691
+ Modal(self.el, self.modal);
692
+ }
693
+
694
+ // Create input controls
695
+ if (self.input) {
696
+ self.input.classList.add('lm-calendar-input');
697
+ self.input.addEventListener('focus', self.open);
698
+ self.input.addEventListener('click', self.open);
699
+ self.input.addEventListener('blur', blur);
700
+
701
+ // Retrieve the value
702
+ if (self.value) {
703
+ self.input.value = self.value;
704
+ } else if (self.input.value) {
705
+ self.value = self.input.value;
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Handler keyboard
711
+ * @param {object} e - event
712
+ */
713
+ self.content.addEventListener('keydown', function(e){
714
+ if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
715
+ self.prev(e);
716
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
717
+ self.next(e);
718
+ } else if (e.key === 'Enter') {
719
+ // Current view
720
+ let view = self.view;
721
+ // Select
722
+ self.select(e, self.options[self.cursor.index]);
723
+ // If is range do something diferent
724
+ if (view === 'days' && ! self.range) {
725
+ self.update();
726
+ }
727
+ }
728
+ });
729
+
730
+ /**
731
+ * Mouse wheel handler
732
+ * @param {object} e - mouse event
733
+ */
734
+ self.content.addEventListener('wheel', function(e){
735
+ if (e.deltaY < 0) {
736
+ self.prev();
737
+ } else {
738
+ self.next();
739
+ }
740
+ });
741
+
742
+ /**
743
+ * Range handler
744
+ * @param {object} e - mouse event
745
+ */
746
+ self.content.addEventListener('mouseover', function(e){
747
+ if (e.target.lemon) {
748
+ updateRange(e.target.lemon.self);
749
+ }
750
+ });
751
+
752
+ // Create event for focus out
753
+ self.el.addEventListener("focusout", (e) => {
754
+ if (e.relatedTarget !== self.input && ! self.el.contains(e.relatedTarget)) {
755
+ self.close();
756
+ }
757
+ });
758
+ }
759
+
760
+ return `<div class="lm-calendar">
761
+ <div class="lm-calendar-options">
762
+ <button onclick="self.reset">Reset</button>
763
+ <button onclick="self.update">Done</button>
764
+ </div>
765
+ <div class="lm-calendar-container" data-view="{{self.view}}">
766
+ <div class="lm-calendar-header">
767
+ <div>
768
+ <div class="lm-calendar-labels"><button onclick="self.setView" data-view="months">{{self.month}}</button> <button onclick="self.setView" data-view="years">{{self.year}}</button></div>
769
+ <div class="lm-calendar-navigation"><button type="button" class="material-icons ripple" onclick="self.prev" tabindex="0">arrow_drop_up</button> <button type="button" class="material-icons ripple" onclick="self.next" tabindex="0">arrow_drop_down</button></div>
770
+ </div>
771
+ <div class="lm-calendar-weekdays" :loop="self.weekdays"><div>{{self.title}}</div></div>
772
+ </div>
773
+ <div class="lm-calendar-content" :loop="self.options" tabindex="0" :ref="self.content">
774
+ <div data-start="{{self.start}}" data-end="{{self.end}}" data-range="{{self.range}}" data-event="{{self.data}}" data-grey="{{self.grey}}" data-bold="{{self.bold}}" data-selected="{{self.selected}}" onclick="self.parent.select">{{self.title}}</div>
775
+ </div>
776
+ <div class="lm-calendar-footer">
777
+ <div><select :loop="self.hours"><option value="{{self.value}}">{{self.title}}</option></select>:<select :loop="self.minutes"><option value="{{self.value}}">{{self.title}}</option></select></div>
778
+ <div><input type="button" value="Update" onclick="self.update" class="ripple"></div>
779
+ </div>
780
+ </div>
781
+ </div>`
782
+ }
783
+
784
+ lemonade.setComponents({ Calendar: Calendar });
785
+
786
+ return function (root, options) {
787
+ if (typeof (root) === 'object') {
788
+ lemonade.render(Calendar, root, options)
789
+ return options;
790
+ } else {
791
+ return Calendar.call(this, root)
792
+ }
793
+ }
791
794
  })));