Passed
Push — main ( 0c6882...1f9a40 )
by Stefan
03:05
created

script/dtsel.js   F

Complexity

Total Complexity 175
Complexity/F 2.92

Size

Lines of Code 950
Function Count 60

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 175
eloc 607
mnd 115
bc 115
fnc 60
dl 0
loc 950
rs 1.993
bpm 1.9166
cpm 2.9166
noi 19
c 0
b 0
f 0

20 Functions

Rating   Name   Duplication   Size   Complexity  
A dtsel.js ➔ tryAppendChild 0 8 2
A dtsel.js ➔ renderDate 0 17 5
F dtsel.js ➔ DTBox 0 83 20
A dtsel.js ➔ setDefaults 0 10 3
A dtsel.js ➔ isEqualDate 0 6 2
F dtsel.js ➔ makeRow 0 24 27
B dtsel.js ➔ DTS 0 49 5
A dtsel.js ➔ empty 0 3 2
D dtsel.js ➔ makeGrid 0 19 12
C dtsel.js ➔ renderTime 0 24 11
D dtsel.js ➔ parseData 0 66 13
A dtsel.js ➔ hookFuncs 0 4 1
A dtsel.js ➔ handler 0 6 2
F dtsel.js ➔ setPosition 0 19 15
A dtsel.js ➔ parseDate 0 21 5
B dtsel.js ➔ parseTime 0 28 8
A dtsel.js ➔ filterFormatKeys 0 12 3
A dtsel.js ➔ padded 0 6 1
F dtsel.js ➔ getOffset 0 8 27
C dtsel.js ➔ sortByStringIndex 0 17 10

How to fix   Complexity   

Complexity

Complex classes like script/dtsel.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
(function () {
2
    "use strict";
3
4
    var BODYTYPES = ["DAYS", "MONTHS", "YEARS"];
5
6
    /** @typedef {Object.<string, Function[]>} Handlers */
7
    /** @typedef {function(String, Function): null} AddHandler */
8
    /** @typedef {("DAYS"|"MONTHS"|"YEARS")} BodyType */
9
    /** @typedef {string|number} StringNum */
10
    /** @typedef {Object.<string, StringNum>} StringNumObj */
11
12
    /**
13
     * The local state
14
     * @typedef {Object} InstanceState
15
     * @property {Date} value
16
     * @property {Number} year
17
     * @property {Number} month
18
     * @property {Number} day
19
     * @property {Number} time
20
     * @property {Number} hours
21
     * @property {Number} minutes
22
     * @property {Number} seconds
23
     * @property {BodyType} bodyType
24
     * @property {Boolean} visible
25
     * @property {Number} cancelBlur
26
     */
27
28
    /** 
29
     * @typedef {Object} Config
30
     * @property {String} dateFormat
31
     * @property {String} timeFormat
32
     * @property {Boolean} showDate
33
     * @property {Boolean} showTime
34
     * @property {Boolean} showSeconds
35
     * @property {Number} paddingX
36
     * @property {Number} paddingY
37
     * @property {BodyType} defaultView
38
     * @property {"TOP"|"BOTTOM"} direction
39
     * @property {Array} months
40
     * @property {Array} monthsShort
41
     * @property {Array} weekdaysShort
42
     * @property {Array} timeDescr
43
    */
44
45
    /**
46
     * @class
47
     * @param {HTMLElement} elem 
48
     * @param {Config} config 
49
     */
50
    function DTS(elem, config) {
51
        var config = config || {};
52
53
        /** @type {Config} */
54
        var defaultConfig = {
55
            defaultView: BODYTYPES[0],
56
            dateFormat: "yyyy-mm-dd",
57
            timeFormat: "HH:MM:SS",
58
            showDate: true,
59
            showTime: false,
60
            showSeconds: true,
61
            paddingX: 5,
62
            paddingY: 5,
63
            direction: 'TOP',
64
            months: [
65
                "January", "February", "March", "April", "May", "June",
66
                "July", "August", "September", "October", "November", "December"
67
            ],
68
            monthsShort: [
69
                "Jan", "Feb", "Mar", "Apr", "May", "Jun",
70
                "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
71
            ], 
72
            weekdaysShort: [
73
                "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"
74
            ],
75
            timeDescr: [
76
                "HH:", "MM:", "SS:"
77
            ]
78
        }
79
80
        if (!elem) {
81
            throw TypeError("input element or selector required for contructor");
82
        }
83
        if (Object.getPrototypeOf(elem) === String.prototype) {
84
            var _elem = document.querySelectorAll(elem);
85
            if (!_elem[0]){
86
                throw Error('"' + elem + '" not found.');
87
            }
88
            elem = _elem[0];
89
        }
90
        this.config = setDefaults(config, defaultConfig);
91
        this.dateFormat = this.config.dateFormat;
92
        this.timeFormat = this.config.timeFormat;
93
        this.dateFormatRegEx = new RegExp("yyyy|yy|mm|dd", "gi");
94
        this.timeFormatRegEx = new RegExp("hh|mm|ss|a", "gi");
95
        this.inputElem = elem;
96
        this.dtbox = null;
97
        this.setup();
98
    }
99
    DTS.prototype.setup = function () {
100
        var handler = this.inputElemHandler.bind(this);
101
        this.inputElem.addEventListener("focus", handler, false)
102
        this.inputElem.addEventListener("blur", handler, false);
103
    }
104
    DTS.prototype.inputElemHandler = function (e) {
105
        if (e.type == "focus") {
106
            if (!this.dtbox) {
107
                this.dtbox = new DTBox(e.target, this);
108
            }
109
            this.dtbox.visible = true;
110
        } else if (e.type == "blur" && this.dtbox && this.dtbox.visible) {
111
            var self = this;
112
            setTimeout(function () {
113
                if (self.dtbox.cancelBlur > 0) {
114
                    self.dtbox.cancelBlur -= 1;
115
                 } else {
116
                    self.dtbox.visible = false;
117
                    self.inputElem.blur();
118
                 }
119
            }, 100);
120
        }
121
    }
122
    /**
123
     * @class
124
     * @param {HTMLElement} elem 
125
     * @param {DTS} settings 
126
     */
127
    function DTBox(elem, settings) {
128
        /** @type {DTBox} */
129
        var self = this;
130
131
        /** @type {Handlers} */
132
        var handlers = {};
133
134
        /** @type {InstanceState} */
135
        var localState = {};
136
137
        /**
138
         * @param {String} key 
139
         * @param {*} default_val 
140
         */
141
        function getterSetter(key, default_val) {
142
            return {
143
                get: function () {
144
                    var val = localState[key];
145
                    return val === undefined ? default_val : val;
146
                },
147
                set: function (val) {
148
                    var prevState = self.state;
149
                    var _handlers = handlers[key] || [];
150
                    localState[key] = val;
151
                    for (var i = 0; i < _handlers.length; i++) {
152
                        _handlers[i].bind(self)(localState, prevState);
153
                    }
154
                },
155
            };
156
        };
157
158
        /** @type {AddHandler} */
159
        function addHandler(key, handlerFn) {
160
            if (!key || !handlerFn) {
161
                return false;
162
            }
163
            if (!handlers[key]) {
164
                handlers[key] = [];
165
            }
166
            handlers[key].push(handlerFn);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
167
        }
168
169
        Object.defineProperties(this, {
170
            visible: getterSetter("visible", false),
171
            bodyType: getterSetter("bodyType", settings.config.defaultView),
172
            value: getterSetter("value"),
173
            year: getterSetter("year", 0),
174
            month: getterSetter("month", 0),
175
            day: getterSetter("day", 0),
176
            hours: getterSetter("hours", 0),
177
            minutes: getterSetter("minutes", 0),
178
            seconds: getterSetter("seconds", 0),
179
            cancelBlur: getterSetter("cancelBlur", 0),
180
            addHandler: {value: addHandler},
181
            month_long: {
182
                get: function () {
183
                    return self.settings.config.months[self.month];
184
                },
185
            },
186
            month_short: {
187
                get: function () {
188
                    return self.settings.config.monthsShort[self.month]
189
                },
190
            },
191
            state: {
192
                get: function () {
193
                    return Object.assign({}, localState);
194
                },
195
            },
196
            time: {
197
                get: function() {
198
                    var hours = self.hours * 60 * 60 * 1000;
199
                    var minutes = self.minutes * 60 * 1000;
200
                    var seconds = self.seconds * 1000;
201
                    return  hours + minutes + seconds;
202
                }
203
            },
204
        });
205
        this.el = {};
206
        this.settings = settings;
207
        this.elem = elem;
208
        this.setup();
209
    }
210
    DTBox.prototype.setup = function () {
211
        Object.defineProperties(this.el, {
212
            wrapper: { value: null, configurable: true },
213
            header: { value: null, configurable: true },
214
            body: { value: null, configurable: true },
215
            footer: { value: null, configurable: true }
216
        });
217
        this.setupWrapper();
218
        if (this.settings.config.showDate) {
219
            this.setupHeader();
220
            this.setupBody();
221
        }
222
        if (this.settings.config.showTime) {
223
            this.setupFooter();
224
        }
225
226
        var self = this;
227
        this.addHandler("visible", function (state, prevState) {
228
            if (state.visible && !prevState.visible){
229
                document.body.appendChild(this.el.wrapper);
230
231
                var parts = self.elem.value.split(/\s*,\s*/);
232
                var startDate = undefined;
0 ignored issues
show
Unused Code Comprehensibility introduced by
The assignment of undefined is not necessary as startDate is implicitly marked as undefined by the declaration.
Loading history...
233
                var startTime = 0;
234
                if (self.settings.config.showDate) {
235
                    startDate = parseDate(parts[0], self.settings);
236
                }
237
                if (self.settings.config.showTime) {
238
                    startTime = parseTime(parts[parts.length-1], self.settings);
239
                    startTime = startTime || 0;
240
                }
241
                if (!(startDate && startDate.getTime())) {
242
                    startDate = new Date();
243
                    startDate = new Date(
244
                        startDate.getFullYear(),
245
                        startDate.getMonth(),
246
                        startDate.getDate()
247
                    );
248
                }
249
                var value = new Date(startDate.getTime() + startTime);
250
                self.value = value;
251
                self.year = value.getFullYear();
252
                self.month = value.getMonth();
253
                self.day = value.getDate();
254
                self.hours = value.getHours();
255
                self.minutes = value.getMinutes();
256
                self.seconds = value.getSeconds();
257
258
                if (self.settings.config.showDate) {
259
                    self.setHeaderContent();
260
                    self.setBodyContent();
261
                }
262
                if (self.settings.config.showTime) {
263
                    self.setFooterContent();
264
                }
265
            } else if (!state.visible && prevState.visible) {
266
                document.body.removeChild(this.el.wrapper);
267
            }
268
        });
269
    }
270
    DTBox.prototype.setupWrapper = function () {
271
        if (!this.el.wrapper) {
272
            var el = document.createElement("div");
273
            el.classList.add("date-selector-wrapper");
274
            Object.defineProperty(this.el, "wrapper", { value: el });
275
        }
276
        var self = this;
277
        var htmlRoot = document.getElementsByTagName('html')[0];
278
        function setPosition(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
279
            var minTopSpace = 300;
280
            var box = getOffset(self.elem);
281
            var config = self.settings.config;
282
            var paddingY = config.paddingY || 5;
283
            var paddingX = config.paddingX || 5;
284
            var top = box.top + self.elem.offsetHeight + paddingY;
285
            var left = box.left + paddingX;
286
            var bottom = htmlRoot.clientHeight - box.top + paddingY;
287
288
            self.el.wrapper.style.left = `${left}px`;
289
            if (box.top > minTopSpace && config.direction != 'BOTTOM') {
290
                self.el.wrapper.style.bottom = `${bottom}px`;
291
                self.el.wrapper.style.top = '';
292
            } else {
293
                self.el.wrapper.style.top = `${top}px`;
294
                self.el.wrapper.style.bottom = ''; 
295
            }
296
        }
297
298
        function handler(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
299
            self.cancelBlur += 1;
300
            setTimeout(function(){
301
                self.elem.focus();
302
            }, 50);
303
        }
304
        setPosition();
305
        this.setPosition = setPosition;
306
        this.el.wrapper.addEventListener("mousedown", handler, false);
307
        this.el.wrapper.addEventListener("touchstart", handler, false);
308
        window.addEventListener('resize', this.setPosition);
309
    }
310
    DTBox.prototype.setupHeader = function () {
311
        if (!this.el.header) {
312
            var row = document.createElement("div");
313
            var classes = ["cal-nav-prev", "cal-nav-current", "cal-nav-next"];
314
            row.classList.add("cal-header");
315
            for (var i = 0; i < 3; i++) {
316
                var cell = document.createElement("div");
317
                cell.classList.add("cal-nav", classes[i]);
318
                cell.onclick = this.onHeaderChange.bind(this);
319
                row.appendChild(cell);
320
            }
321
            row.children[0].innerHTML = "&#9664;"; // "&lt;";
322
            row.children[2].innerHTML = "&#9654;"; // "&gt;";
323
            Object.defineProperty(this.el, "header", { value: row });
324
            tryAppendChild(row, this.el.wrapper);
325
        }
326
        this.setHeaderContent();
327
    }
328
    DTBox.prototype.setHeaderContent = function () {
329
        var content = this.year;
330
        if ("DAYS" == this.bodyType) {
331
            content = this.month_long + " " + content;
332
        } else if ("YEARS" == this.bodyType) {
333
            var start = this.year + 10 - (this.year % 10);
334
            content = start - 10 + "-" + (start - 1);
335
        }
336
        this.el.header.children[1].innerText = content;
337
    }
338
    DTBox.prototype.setupBody = function () {
339
        if (!this.el.body) {
340
            var el = document.createElement("div");
341
            el.classList.add("cal-body");
342
            Object.defineProperty(this.el, "body", { value: el });
343
            tryAppendChild(el, this.el.wrapper);
344
        }
345
        var toAppend = null;
346
        function makeGrid(rows, cols, className, firstRowClass, clickHandler) {
347
            var grid = document.createElement("div");
348
            grid.classList.add(className);
349
            for (var i = 1; i < rows + 1; i++) {
350
                var row = document.createElement("div");
351
                row.classList.add("cal-row", "cal-row-" + i);
352
                if (i == 1 && firstRowClass) {
0 ignored issues
show
Best Practice introduced by
Comparing i to 1 using the == operator is not safe. Consider using === instead.
Loading history...
353
                    row.classList.add(firstRowClass);
354
                }
355
                for (var j = 1; j < cols + 1; j++) {
356
                    var col = document.createElement("div");
357
                    col.classList.add("cal-cell", "cal-col-" + j);
358
                    col.onclick = clickHandler;
359
                    row.appendChild(col);
360
                }
361
                grid.appendChild(row);
362
            }
363
            return grid;
364
        }
365
        if ("DAYS" == this.bodyType) {
366
            toAppend = this.el.body.calDays;
367
            if (!toAppend) {
368
                toAppend = makeGrid(7, 7, "cal-days", "cal-day-names", this.onDateSelected.bind(this));
369
                for (var i = 0; i < 7; i++) {
370
                    var cell = toAppend.children[0].children[i];
371
                    cell.innerText = this.settings.config.weekdaysShort[i];
372
                    cell.onclick = null;
373
                }
374
                this.el.body.calDays = toAppend;
375
            }
376
        } else if ("MONTHS" == this.bodyType) {
377
            toAppend = this.el.body.calMonths;
378
            if (!toAppend) {
379
                toAppend = makeGrid(3, 4, "cal-months", null, this.onMonthSelected.bind(this));
380
                for (var i = 0; i < 3; i++) {
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable i already seems to be declared on line 369. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
381
                    for (var j = 0; j < 4; j++) {
382
                        var monthShort = this.settings.config.monthsShort[4 * i + j];
383
                        toAppend.children[i].children[j].innerText = monthShort;
384
                    }
385
                }
386
                this.el.body.calMonths = toAppend;
387
            }
388
        } else if ("YEARS" == this.bodyType) {
389
            toAppend = this.el.body.calYears;
390
            if (!toAppend) {
391
                toAppend = makeGrid(3, 4, "cal-years", null, this.onYearSelected.bind(this));
392
                this.el.body.calYears = toAppend;
393
            }
394
        }
395
        empty(this.el.body);
396
        tryAppendChild(toAppend, this.el.body);
397
        this.setBodyContent();
398
    }
399
    DTBox.prototype.setBodyContent = function () {
400
        var grid = this.el.body.children[0];
401
        var classes = ["cal-cell-prev", "cal-cell-next", "cal-value"];
402
        if ("DAYS" == this.bodyType) {
403
            var oneDayMilliSecs = 24 * 60 * 60 * 1000;
404
            var start = new Date(this.year, this.month, 1);
405
            var adjusted = new Date(start.getTime() - oneDayMilliSecs * start.getDay());
406
407
            grid.children[6].style.display = "";
408
            for (var i = 1; i < 7; i++) {
409
                for (var j = 0; j < 7; j++) {
410
                    var cell = grid.children[i].children[j];
411
                    var month = adjusted.getMonth();
412
                    var date = adjusted.getDate();
413
                    
414
                    cell.innerText = date;
415
                    cell.classList.remove(classes[0], classes[1], classes[2]);
416
                    if (month != this.month) {
417
                        if (i == 6 && j == 0) {
0 ignored issues
show
Best Practice introduced by
Comparing j to 0 using the == operator is not safe. Consider using === instead.
Loading history...
418
                            grid.children[6].style.display = "none";
419
                            break;
420
                        }
421
                        cell.classList.add(month < this.month ? classes[0] : classes[1]);
422
                    } else if (isEqualDate(adjusted, this.value)){
423
                        cell.classList.add(classes[2]);
424
                    }
425
                    adjusted = new Date(adjusted.getTime() + oneDayMilliSecs);
426
                }
427
            }
428
        } else if ("YEARS" == this.bodyType) {
429
            var year = this.year - (this.year % 10) - 1;
430
            for (i = 0; i < 3; i++) {
431
                for (j = 0; j < 4; j++) {
432
                    grid.children[i].children[j].innerText = year;
433
                    year += 1;
434
                }
435
            }
436
            grid.children[0].children[0].classList.add(classes[0]);
437
            grid.children[2].children[3].classList.add(classes[1]);
438
        }
439
    }
440
441
    /** @param {Event} e */
442
    DTBox.prototype.onTimeChange = function(e) {
443
        e.stopPropagation();
444
        if (e.type == 'mousedown') {
445
            this.cancelBlur += 1;
446
            return;
447
        }
448
        if (e.type == 'mouseup') {
449
            var self = this;
450
            setTimeout(function(){
451
                self.elem.focus();
452
            }, 50);
453
            return;
454
        }
455
        
456
        var el = e.target;
457
        this[el.name] = parseInt(el.value) || 0;
458
        this.setupFooter();
459
        this.setInputValue();
460
    }
461
462
    DTBox.prototype.setupFooter = function() {
463
        if (!this.el.footer) {
464
            var footer = document.createElement("div");
465
            var handler = this.onTimeChange.bind(this);
466
            var self = this;
467
            
468
            function makeRow(label, name, range, changeHandler) {
0 ignored issues
show
Bug introduced by
The function makeRow is declared conditionally. This is not supported by all runtimes. Consider moving it to root scope or using var makeRow = function() { /* ... */ }; instead.
Loading history...
469
                var row = document.createElement("div");
470
                row.classList.add('cal-time');
471
472
                var labelCol = row.appendChild(document.createElement("div"));
473
                labelCol.classList.add('cal-time-label');
474
                labelCol.innerText = label;
475
476
                var valueCol = row.appendChild(document.createElement("div"));
477
                valueCol.classList.add('cal-time-value');
478
                valueCol.innerText = '00';
479
480
                var inputCol = row.appendChild(document.createElement("div"));
481
                var slider = inputCol.appendChild(document.createElement("input"));
482
                Object.assign(slider, {step:1, min:0, max:range, name:name, type:'range'});
483
                Object.defineProperty(footer, name, {value: slider});
484
                inputCol.classList.add('cal-time-slider');
485
                slider.onchange = changeHandler;
486
                slider.oninput = changeHandler;
487
                slider.onmousedown = changeHandler;
488
                slider.onmouseup = changeHandler;
489
                self[name] = self[name] || parseInt(slider.value) || 0;
490
                footer.appendChild(row)
491
            }
492
            makeRow(this.settings.config.timeDescr[0], 'hours', 23, handler);
493
            makeRow(this.settings.config.timeDescr[1], 'minutes', 59, handler);
494
            if (this.settings.config.showSeconds) {
495
                makeRow(this.settings.config.timeDescr[2], 'seconds', 59, handler);
496
            }
497
498
            footer.classList.add("cal-footer");
499
            Object.defineProperty(this.el, "footer", { value: footer });
500
            tryAppendChild(footer, this.el.wrapper);
501
        }
502
        this.setFooterContent();
503
    }
504
505
    DTBox.prototype.setFooterContent = function() {
506
        if (this.el.footer) {
507
            var footer = this.el.footer;
508
            footer.hours.value = this.hours;
509
            footer.children[0].children[1].innerText = padded(this.hours, 2);
510
            footer.minutes.value = this.minutes;
511
            footer.children[1].children[1].innerText = padded(this.minutes, 2);
512
            if (this.settings.config.showSeconds) {
513
                footer.seconds.value = this.seconds;
514
                footer.children[2].children[1].innerText = padded(this.seconds, 2);
515
            }
516
        }
517
    }
518
519
    DTBox.prototype.setInputValue = function() {
520
        var date = new Date(this.year, this.month, this.day);
521
        var strings = [];
522
        if (this.settings.config.showDate) {
523
            strings.push(renderDate(date, this.settings));
524
        }
525
        if (this.settings.config.showTime) {
526
            var joined = new Date(date.getTime() + this.time);
527
            strings.push(renderTime(joined, this.settings));
528
        }
529
        this.elem.value = strings.join(', ');
530
    }
531
532
    DTBox.prototype.onDateSelected = function (e) {
533
        var row = e.target.parentNode;
534
        var date = parseInt(e.target.innerText);
535
        if (!(row.nextSibling && row.nextSibling.nextSibling) && date < 8) {
536
            this.month += 1;
537
        } else if (!(row.previousSibling && row.previousSibling.previousSibling) && date > 7) {
538
            this.month -= 1;
539
        }
540
        this.day = parseInt(e.target.innerText);
541
        this.value = new Date(this.year, this.month, this.day);
542
        this.setInputValue();
543
        this.setHeaderContent();
544
        this.setBodyContent();
545
    }
546
547
    /** @param {Event} e */
548
    DTBox.prototype.onMonthSelected = function (e) {
549
        var col = 0;
550
        var row = 2;
551
        var cell = e.target;
552
        if (cell.parentNode.nextSibling){
553
            row = cell.parentNode.previousSibling ? 1: 0;
554
        }
555
        if (cell.previousSibling) {
556
            col = 3;
557
            if (cell.nextSibling) {
558
                col = cell.previousSibling.previousSibling ? 2 : 1;
559
            }
560
        }
561
        this.month = 4 * row + col;
562
        this.bodyType = "DAYS";
563
        this.setHeaderContent();
564
        this.setupBody();
565
    }
566
567
    /** @param {Event} e */
568
    DTBox.prototype.onYearSelected = function (e) {
569
        this.year = parseInt(e.target.innerText);
570
        this.bodyType = "MONTHS";
571
        this.setHeaderContent();
572
        this.setupBody();
573
    }
574
575
    /** @param {Event} e */
576
    DTBox.prototype.onHeaderChange = function (e) {
577
        var cell = e.target;
578
        if (cell.previousSibling && cell.nextSibling) {
579
            var idx = BODYTYPES.indexOf(this.bodyType);
580
            if (idx < 0 || !BODYTYPES[idx + 1]) {
581
                return;
582
            }
583
            this.bodyType = BODYTYPES[idx + 1];
584
            this.setupBody();
585
        } else {
586
            var sign = cell.previousSibling ? 1 : -1;
587
            switch (this.bodyType) {
588
                case "DAYS":
589
                    this.month += sign * 1;
590
                    break;
591
                case "MONTHS":
592
                    this.year += sign * 1;
593
                    break;
594
                case "YEARS":
595
                    this.year += sign * 10;
596
            }
597
            if (this.month > 11 || this.month < 0) {
598
                this.year += Math.floor(this.month / 11);
599
                this.month = this.month > 11 ? 0 : 11;
600
            }
601
        }
602
        this.setHeaderContent();
603
        this.setBodyContent();
604
    }
605
606
607
    /**
608
     * @param {HTMLElement} elem 
609
     * @returns {{left:number, top:number}}
610
     */
611
    function getOffset(elem) {
612
        var box = elem.getBoundingClientRect();
613
        var left = window.pageXOffset !== undefined ? window.pageXOffset : 
614
            (document.documentElement || document.body.parentNode || document.body).scrollLeft;
615
        var top = window.pageYOffset !== undefined ? window.pageYOffset : 
616
            (document.documentElement || document.body.parentNode || document.body).scrollTop;
617
        return { left: box.left + left, top: box.top + top };
618
    }
619
    function empty(e) {
620
        for (; e.children.length; ) e.removeChild(e.children[0]);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
621
    }
622
    function tryAppendChild(newChild, refNode) {
623
        try {
624
            refNode.appendChild(newChild);
625
            return newChild;
626
        } catch (e) {
627
            console.trace(e);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
628
        }
629
    }
630
631
    /** @class */
632
    function hookFuncs() {
633
        /** @type {Handlers} */
634
        this._funcs = {};
635
    }
636
    /**
637
     * @param {string} key 
638
     * @param {Function} func 
639
     */
640
    hookFuncs.prototype.add = function(key, func){
641
        if (!this._funcs[key]){
642
            this._funcs[key] = [];
643
        }
644
        this._funcs[key].push(func)
645
    }
646
    /**
647
     * @param {String} key 
648
     * @returns {Function[]} handlers
649
     */
650
    hookFuncs.prototype.get = function(key){
651
        return this._funcs[key] ? this._funcs[key] : [];
652
    }
653
654
    /**
655
     * @param {Array.<string>} arr 
656
     * @param {String} string 
657
     * @returns {Array.<string>} sorted string
658
     */
659
    function sortByStringIndex(arr, string) {
660
        return arr.sort(function(a, b){
661
            var h = string.indexOf(a);
662
            var l = string.indexOf(b);
663
            var rank = 0;
664
            if (h < l) {
665
                rank = -1;
666
            } else if (l < h) {
667
                rank = 1;
668
            } else if (a.length > b.length) {
669
                rank = -1;
670
            } else if (b.length > a.length) {
671
                rank = 1;
672
            }
673
            return rank;
674
        });
675
    }
676
677
    /**
678
     * Remove keys from array that are not in format
679
     * @param {string[]} keys 
680
     * @param {string} format 
681
     * @returns {string[]} new filtered array
682
     */
683
    function filterFormatKeys(keys, format) {
684
        var out = [];
685
        var formatIdx = 0;
686
        for (var i = 0; i<keys.length; i++) {
687
            var key = keys[i];
688
            if (format.slice(formatIdx).indexOf(key) > -1) {
689
                formatIdx += key.length;
690
                out.push(key);
691
            }
692
        }
693
        return out;
694
    }
695
696
    /**
697
     * @template {StringNumObj} FormatObj
698
     * @param {string} value 
699
     * @param {string} format 
700
     * @param {FormatObj} formatObj 
701
     * @param {function(Object.<string, hookFuncs>): null} setHooks 
702
     * @returns {FormatObj} formatObj
703
     */
704
    function parseData(value, format, formatObj, setHooks) {
705
        var hooks = {
706
            canSkip: new hookFuncs(),
0 ignored issues
show
Coding Style Best Practice introduced by
By convention, constructors like hookFuncs should be capitalized.
Loading history...
707
            updateValue: new hookFuncs(),
0 ignored issues
show
Coding Style Best Practice introduced by
By convention, constructors like hookFuncs should be capitalized.
Loading history...
708
        }
709
        var keys = sortByStringIndex(Object.keys(formatObj), format);
710
        var filterdKeys = filterFormatKeys(keys, format);
711
        var vstart = 0; // value start
712
        if (setHooks) {
713
            setHooks(hooks);
714
        }
715
716
        for (var i = 0; i < keys.length; i++) {
717
            var key = keys[i];
718
            var fstart = format.indexOf(key);
719
            var _vstart = vstart; // next value start
720
            var val = null;
721
            var canSkip = false;
722
            var funcs = hooks.canSkip.get(key);
723
724
            vstart = vstart || fstart;
725
726
            for (var j = 0; j < funcs.length; j++) {
727
                if (funcs[j](formatObj)){
728
                    canSkip = true;
729
                    break;
730
                }
731
            }
732
            if (fstart > -1 && !canSkip) {
733
                var sep = null;
0 ignored issues
show
Unused Code introduced by
The assignment to sep seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
734
                var stop = vstart + key.length;
735
                var fnext = -1;
736
                var nextKeyIdx = i + 1;
737
                _vstart += key.length; // set next value start if current key is found
738
739
                // get next format token used to determine separator
740
                while (fnext == -1 && nextKeyIdx < keys.length){
741
                    var nextKey = keys[nextKeyIdx];
742
                    nextKeyIdx += 1;
743
                    if (filterdKeys.indexOf(nextKey) === -1) {
744
                        continue;
745
                    }
746
                    fnext = nextKey ? format.indexOf(nextKey) : -1; // next format start
747
                }
748
                if (fnext > -1){
749
                    sep = format.slice(stop, fnext);
750
                    if (sep) {
751
                        var _stop = value.slice(vstart).indexOf(sep);
752
                        if (_stop && _stop > -1){
753
                            stop = _stop + vstart;
754
                            _vstart = stop + sep.length;
755
                        }
756
                    }
757
                }
758
                val = parseInt(value.slice(vstart, stop));
759
760
                var funcs = hooks.updateValue.get(key);
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable funcs already seems to be declared on line 722. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
761
                for (var k = 0; k < funcs.length; k++) {
762
                    val = funcs[k](val, formatObj, vstart, stop);
763
                }
764
            }
765
            formatObj[key] = { index: vstart, value: val };
766
            vstart = _vstart; // set next value start
767
        }
768
        return formatObj;
769
    }
770
771
    /**
772
     * @param {String} value 
773
     * @param {DTS} settings 
774
     * @returns {Date} date object
775
     */
776
    function parseDate(value, settings) {
777
        /** @type {{yyyy:number=, yy:number=, mm:number=, dd:number=}} */
778
        var formatObj = {yyyy:null, yy:null, mm:null, dd:null};
779
        var format = ((settings.dateFormat) || '').toLowerCase();
780
        if (!format) {
781
            throw new TypeError('dateFormat not found (' + settings.dateFormat + ')');
782
        }
783
        var formatObj = parseData(value, format, formatObj, function(hooks){
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable formatObj already seems to be declared on line 778. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
784
            hooks.canSkip.add("yy", function(data){
785
                return data["yyyy"].value;
786
            });
787
            hooks.updateValue.add("yy", function(val){
788
                return 100 * Math.floor(new Date().getFullYear() / 100) + val;
789
            });
790
        });
791
        var year = formatObj["yyyy"].value || formatObj["yy"].value;
792
        var month = formatObj["mm"].value - 1;
793
        var date = formatObj["dd"].value;
794
        var result = new Date(year, month, date);
795
        return result;
796
    }
797
798
    /**
799
     * @param {String} value 
800
     * @param {DTS} settings 
801
     * @returns {Number} time in milliseconds <= (24 * 60 * 60 * 1000) - 1
802
     */
803
    function parseTime(value, settings) {
804
        var format = ((settings.timeFormat) || '').toLowerCase();
805
        if (!format) {
806
            throw new TypeError('timeFormat not found (' + settings.timeFormat + ')');
807
        }
808
809
        /** @type {{hh:number=, mm:number=, ss:number=, a:string=}} */
810
        var formatObj = {hh:null, mm:null, ss:null, a:null};
811
        var formatObj = parseData(value, format, formatObj, function(hooks){
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable formatObj already seems to be declared on line 810. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
812
            hooks.updateValue.add("a", function(val, data, start, stop){
0 ignored issues
show
Unused Code introduced by
The parameter stop is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
813
                return value.slice(start, start + 2);
814
            });
815
        });
816
        var hours = formatObj["hh"].value;
817
        var minutes = formatObj["mm"].value;
818
        var seconds = formatObj["ss"].value;
819
        var am_pm = formatObj["a"].value;
820
        var am_pm_lower = am_pm ? am_pm.toLowerCase() : am_pm;
821
        if (am_pm && ["am", "pm"].indexOf(am_pm_lower) > -1){
822
            if (am_pm_lower == 'am' && hours == 12){
823
                hours = 0;
824
            } else if (am_pm_lower == 'pm') {
825
                hours += 12;
826
            }
827
        }
828
        var time = hours * 60 * 60 * 1000 + minutes * 60 * 1000 + seconds * 1000;
829
        return time;
830
    }
831
832
    /**
833
     * @param {Date} value 
834
     * @param {DTS} settings 
835
     * @returns {String} date string
836
     */
837
    function renderDate(value, settings) {
838
        var format = settings.dateFormat.toLowerCase();
839
        var date = value.getDate();
840
        var month = value.getMonth() + 1;
841
        var year = value.getFullYear();
842
        var yearShort = year % 100;
843
        var formatObj = {
844
            dd: date < 10 ? "0" + date : date,
845
            mm: month < 10 ? "0" + month : month,
846
            yyyy: year,
847
            yy: yearShort < 10 ? "0" + yearShort : yearShort
848
        };
849
        var str = format.replace(settings.dateFormatRegEx, function (found) {
850
            return formatObj[found];
851
        });
852
        return str;
853
    }
854
855
    /**
856
     * @param {Date} value 
857
     * @param {DTS} settings 
858
     * @returns {String} date string
859
     */
860
    function renderTime(value, settings) {
861
        var Format = settings.timeFormat;
862
        var format = Format.toLowerCase();
863
        var hours = value.getHours();
864
        var minutes = value.getMinutes();
865
        var seconds = value.getSeconds();
866
        var am_pm = null;
867
        var hh_am_pm = null;
868
        if (format.indexOf('a') > -1) {
869
            am_pm = hours >= 12 ? 'pm' : 'am';
870
            am_pm = Format.indexOf('A') > -1 ? am_pm.toUpperCase() : am_pm;
871
            hh_am_pm = hours == 0 ? '12' : (hours > 12 ? hours%12 : hours);
0 ignored issues
show
Best Practice introduced by
Comparing hours to 0 using the == operator is not safe. Consider using === instead.
Loading history...
872
        }
873
        var formatObj = {
874
            hh: am_pm ? hh_am_pm : (hours < 10 ? "0" + hours : hours),
875
            mm: minutes < 10 ? "0" + minutes : minutes,
876
            ss: seconds < 10 ? "0" + seconds : seconds,
877
            a: am_pm,
878
        };
879
        var str = format.replace(settings.timeFormatRegEx, function (found) {
880
            return formatObj[found];
881
        });
882
        return str;
883
    }
884
885
    /**
886
     * checks if two dates are equal
887
     * @param {Date} date1 
888
     * @param {Date} date2 
889
     * @returns {Boolean} true or false
890
     */
891
    function isEqualDate(date1, date2) {
892
        if (!(date1 && date2)) return false;
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
893
        return (date1.getFullYear() == date2.getFullYear() && 
894
                date1.getMonth() == date2.getMonth() && 
895
                date1.getDate() == date2.getDate());
896
    }
897
898
    /**
899
     * @param {Number} val 
900
     * @param {Number} pad 
901
     * @param {*} default_val 
902
     * @returns {String} padded string
903
     */
904
    function padded(val, pad, default_val) {
905
        var default_val = default_val || 0;
906
        var valStr = '' + (parseInt(val) || default_val);
907
        var diff = Math.max(pad, valStr.length) - valStr.length;
908
        return ('' + default_val).repeat(diff) + valStr;
909
    }
910
911
    /**
912
     * @template X
913
     * @template Y
914
     * @param {X} obj 
915
     * @param {Y} objDefaults 
916
     * @returns {X|Y} merged object
917
     */
918
    function setDefaults(obj, objDefaults) {
919
        var keys = Object.keys(objDefaults);
920
        for (var i=0; i<keys.length; i++) {
921
            var key = keys[i];
922
            if (!Object.prototype.hasOwnProperty.call(obj, key)) {
923
                obj[key] = objDefaults[key];
924
            }
925
        }
926
        return obj;
927
    }
928
929
930
    window.dtsel = Object.create({},{
931
        DTS: { value: DTS },
932
        DTObj: { value: DTBox },
933
        fn: {
934
            value: Object.defineProperties({}, {
935
                empty: { value: empty },
936
                appendAfter: {
937
                    value: function (newElem, refNode) {
938
                        refNode.parentNode.insertBefore(newElem, refNode.nextSibling);
939
                    },
940
                },
941
                getOffset: { value: getOffset },
942
                parseDate: { value: parseDate },
943
                renderDate: { value: renderDate },
944
                parseTime: {value: parseTime},
945
                renderTime: {value: renderTime},
946
                setDefaults: {value: setDefaults},
947
            }),
948
        },
949
    });
950
})();
951