UNPKG

@lemonadejs/calendar

Version:

LemonadeJS reactive JavaScript calendar plugin

991 lines (896 loc) 34.3 kB
/** * render: () * valid-ranges: [] * disabled * dateToNum UTC * navigation with icons Enter key */ if (! lemonade && typeof (require) === 'function') { var lemonade = require('lemonadejs'); } if (! Modal && typeof (require) === 'function') { var Modal = require('@lemonadejs/modal'); } ; (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : global.Calendar = factory(); }(this, (function () { const Helpers = (function() { const component = {}; component.weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; component.months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; // Excel like dates const excelInitialTime = Date.UTC(1900, 0, 0); const excelLeapYearBug = Date.UTC(1900, 1, 29); const millisecondsPerDay = 86400000; // Transform in two digits component.Two = function(value) { value = '' + value; if (value.length === 1) { value = '0' + value; } return value; } component.isValidDate = function(d) { return d instanceof Date && !isNaN(d.getTime()); } component.toString = function (date, dateOnly) { let y = null; let m = null; let d = null; let h = null; let i = null; let s = null; if (Array.isArray(date)) { y = date[0]; m = date[1]; d = date[2]; h = date[3]; i = date[4]; s = date[5]; } else { if (! date) { date = new Date(); } y = date.getFullYear(); m = date.getMonth() + 1; d = date.getDate(); h = date.getHours(); i = date.getMinutes(); s = date.getSeconds(); } if (dateOnly === true) { return component.Two(y) + '-' + component.Two(m) + '-' + component.Two(d); } else { return component.Two(y) + '-' + component.Two(m) + '-' + component.Two(d) + ' ' + component.Two(h) + ':' + component.Two(i) + ':' + component.Two(s); } } component.toArray = function (value) { let date = value.split(((value.indexOf('T') !== -1) ? 'T' : ' ')); let time = date[1]; date = date[0].split('-'); let y = parseInt(date[0]); let m = parseInt(date[1]); let d = parseInt(date[2]); let h = 0; let i = 0; if (time) { time = time.split(':'); h = parseInt(time[0]); i = parseInt(time[1]); } return [y, m, d, h, i, 0]; } component.arrayToStringDate = function(arr) { return component.toString(arr, true); } component.dateToNum = function(jsDate) { if (typeof(jsDate) === 'string') { jsDate = new Date(jsDate + ' GMT+0'); } let jsDateInMilliseconds = jsDate.getTime(); if (jsDateInMilliseconds >= excelLeapYearBug) { jsDateInMilliseconds += millisecondsPerDay; } jsDateInMilliseconds -= excelInitialTime; return jsDateInMilliseconds / millisecondsPerDay; } component.numToDate = function(excelSerialNumber, asString){ if (! excelSerialNumber) { return ''; } let jsDateInMilliseconds = excelInitialTime + excelSerialNumber * millisecondsPerDay; if (jsDateInMilliseconds >= excelLeapYearBug) { jsDateInMilliseconds -= millisecondsPerDay; } const d = new Date(jsDateInMilliseconds); let arr = [ d.getUTCFullYear(), component.Two(d.getUTCMonth() + 1), component.Two(d.getUTCDate()), component.Two(d.getUTCHours()), component.Two(d.getUTCMinutes()), component.Two(d.getUTCSeconds()), ]; if (asString) { return component.arrayToStringDate(arr); } else { return arr; } } return component; })(); const isNumber = function (num) { if (typeof(num) === 'string') { num = num.trim(); } return !isNaN(num) && num !== null && num !== ''; } /** * Create a data calendar object based on the view */ const views = { years: function(date) { let year = date.getFullYear(); let result = []; let start = year % 16; let complement = 16 - start; for (let i = year-start; i < year+complement; i++) { let item = { title: i, value: i }; result.push(item); // Select cursor if (this.cursor.y === i) { // Select item item.selected = true; // Cursor this.cursor.index = result.length - 1; } } return result; }, months: function(date) { let year = date.getFullYear(); let result = []; for (let i = 0; i < 12; i++) { let item = { title: Helpers.months[i].substring(0,3), value: i } // Add the item to the data result.push(item); // Select cursor if (this.cursor.y === year && this.cursor.m === i) { // Select item item.selected = true; // Cursor this.cursor.index = result.length - 1; } } return result; }, days: function(date) { let year = date.getFullYear(); let month = date.getMonth(); let data = filterData.call(this, year, month); // First day let tmp = new Date(year, month, 1, 0, 0, 0); let firstDay = tmp.getDay(); let result = []; for (let i = 1-firstDay; i <= 42-firstDay; i++) { // Get the day tmp = new Date(year, month, i, 0, 0, 0); // Day let day = tmp.getDate(); // Create the item let item = { title: day, value: i, number: Helpers.dateToNum(tmp.toString()) } // Add the item to the date result.push(item); // Check selections if (tmp.getMonth() !== month) { // Days are not in the current month item.grey = true; } else { // Check for data let d = [ year, Helpers.Two(month+1), Helpers.Two(day)].join('-'); if (data && data[d]) { item.data = data[d]; } } // Month let m = tmp.getMonth(); // Select cursor if (this.cursor.y === year && this.cursor.m === m && this.cursor.d === day) { // Select item item.selected = true; // Cursor this.cursor.index = result.length - 1; } // Select range if (this.range && this.rangeValues) { // Mark the start and end points if (this.rangeValues[0] === item.number) { item.range = true; item.start = true; } if (this.rangeValues[1] === item.number) { item.range = true; item.end = true; } // Re-recreate teh range if (this.rangeValues[0] && this.rangeValues[1]) { if (this.rangeValues[0] <= item.number && this.rangeValues[1] >= item.number) { item.range = true; } } } } return result; }, hours: function() { let result = []; for (let i = 0; i < 24; i++) { let item = { title: Helpers.Two(i), value: i }; result.push(item); } return result; }, minutes: function() { let result = []; for (let i = 0; i < 60; i=i+5) { let item = { title: Helpers.Two(i), value: i }; result.push(item); } return result; } } const filterData = function(year, month) { // Data for the month let data = {}; if (Array.isArray(this.data)) { this.data.map(function (v) { let d = year + '-' + Helpers.Two(month + 1); if (v.date.substring(0, 7) === d) { if (!data[v.date]) { data[v.date] = []; } data[v.date].push(v); } }); } return data; } // Get the short weekdays name const getWeekdays = function() { return Helpers.weekdays.map(w => { return { title: w.substring(0, 1) }; }) } // Define the hump based on the view const getJump = function(e) { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { return this.view === 'days' ? 7 : 4; } return 1; } // Get the position of the data based on the view const getPosition = function() { let position = 2; if (this.view === 'years') { position = 0; } else if (this.view === 'months') { position = 1; } return position; } const Calendar = function() { let self = this; const onchange = self.onchange; // Weekdays self.weekdays = getWeekdays(); // Cursor self.cursor = {}; // Calendar date let date = new Date(); // Range self.rangeValues = null; /** * Update the internal date * @param {Date|string|number[]} d instance of Date * */ const setDate = function(d) { if (Array.isArray(d)) { d = new Date(Date.UTC(...d)); } else if (typeof(d) === 'string') { d = new Date(d); } // Update internal date date = d; // Update the headers of the calendar let value = d.toISOString().substring(0,10).split('-'); // Update the month label self.month = Helpers.months[parseInt(value[1])-1]; // Update the year label self.year = parseInt(value[0]); // Load data if (! self.view) { // Start on the days view will start the data self.view = 'days'; } else { // Reload the data for the same view self.options = views[self.view].call(self, date); } } const getDate = function() { let v = [ self.cursor.y, self.cursor.m, self.cursor.d ]; let d = new Date(Date.UTC(...v)); // Update the headers of the calendar return d.toISOString().substring(0, 10); } /** * Set the internal cursor * @param {object} s */ const setCursor = function(s) { // Remove selection from the current object let item = self.options[self.cursor.index]; if (typeof(item) !== 'undefined') { item.selected = false; } // Update the date based on the click let v = updateDate(s.value, getPosition.call(self)); let d = new Date(Date.UTC(...v)); // Update cursor controller self.cursor = { y: d.getFullYear(), m: d.getMonth(), d: d.getDate(), }; // Update cursor based on the object position if (s) { // Update selected property s.selected = true; // New cursor self.cursor.index = self.options.indexOf(s); } if (typeof (self.onupdate) === 'function') { self.onupdate(self, renderValue()); } return d; } /** * Update the current date * @param {number} v new value for year, month or day * @param {number} position (0,1,2 - year,month,day) * @returns {number[]} */ const updateDate = function(v, position) { // Current internal date let value = [date.getFullYear(), date.getMonth(), date.getDate(),0,0,0]; // Update internal date value[position] = v; // Return new value return value; } /** * This method move the data from the view up or down * @param direction */ const move = function(direction) { let value; // Update the new internal date if (self.view === 'days') { // Select the new internal date value = updateDate(date.getMonth()+direction, 1); } else if (self.view === 'months') { // Select the new internal date value = updateDate(date.getFullYear()+direction, 0); } else if (self.view === 'years') { // Select the new internal date value = updateDate(date.getFullYear()+(direction*16), 0); } // Update view if (value) { setDate(value); } } /** * Keyboard handler * @param {number} direction of the action * @param {object} e keyboard event */ const moveCursor = function(direction, e) { direction = direction * getJump.call(self, e); // Remove the selected from the current selection let s = self.options[self.cursor.index]; // If the selection is going outside the viewport if (typeof(s) === 'undefined' || ! s.selected) { // Go back to the view setDate([ self.cursor.y, self.cursor.m, self.cursor.d ]); } // Jump to the index let index = self.cursor.index + direction; // See if the new position is in the viewport if (typeof(self.options[index]) === 'undefined') { // Adjust the index for next collection of data if (self.view === 'days') { if (index < 0) { index = 42 + index; } else { index = index - 42; } } else if (self.view === 'years') { if (index < 0) { index = 4 + index; } else { index = index - 4; } } else if (self.view === 'months') { if (index < 0) { index = 12 + index; } else { index = index - 12; } } // Move the data up or down move(direction > 0 ? 1 : -1); } // Update the date based on the click setCursor(self.options[index]); // Update ranges updateRange(self.options[index]) } /** * Handler blur * @param e */ const blur = function(e) { if (self.modal) { if (!(e.relatedTarget && self.modal.el.contains(e.relatedTarget))) { if (self.modal.closed === false) { self.modal.closed = true } } } } /** * Set the limits of a range * @param s */ const setRange = function(s) { if (self.view === 'days' && self.range) { let d = getDate(); // Date to number let number = Helpers.dateToNum(d); // Start a new range if (self.rangeValues && (self.rangeValues[0] >= number || self.rangeValues[1])) { destroyRange(); } // Range s.range = true; // Update range if (! self.rangeValues) { s.start = true; self.rangeValues = [number, null]; } else { s.end = true; self.rangeValues[1] = number; } } } /** * Update the visible range * @param s */ const updateRange = function(s) { if (self.range && self.view === 'days' && self.rangeValues) { // Creating a range if (self.rangeValues[0] && ! self.rangeValues[1]) { let number = s.number; if (number) { // Update range properties for (let i = 0; i < self.options.length; i++) { // Item number let v = self.options[i].number; // Update property condition self.options[i].range = v >= self.rangeValues[0] && v <= number; } } } } } /** * Destroy the range */ const destroyRange = function() { for (let i = 0; i < self.options.length; i++) { self.options[i].range = false; self.options[i].start = false; self.options[i].end = false; } self.rangeValues = null; } const renderValue = function() { let value = null; if (self.range) { if (Array.isArray(self.rangeValues)) { if (self.numeric) { value = self.rangeValues; } else { value = [ Helpers.numToDate(self.rangeValues[0], true), Helpers.numToDate(self.rangeValues[1], true) ]; } } } else { value = getDate(); if (self.numeric) { value = Helpers.dateToNum(value); } } return value; } const updateValue = function(v) { if (self.range) { if (v) { if (! Array.isArray(v)) { v = v.split(','); } self.rangeValues = [...v]; if (v[0] && typeof(v[0]) === 'string' && v[0].indexOf('-')) { self.rangeValues[0] = Helpers.dateToNum(v[0]); } if (v[1] && typeof(v[1]) === 'string' && v[1].indexOf('-')) { self.rangeValues[1] = Helpers.dateToNum(v[1]); } v = v[0]; } } let d; if (v) { v = isNumber(v) ? Helpers.numToDate(v, true) : v; d = new Date(v); } // if no date is defined if (! Helpers.isValidDate(d)) { d = new Date(); } // Update my index self.cursor = { y: d.getFullYear(), m: d.getMonth(), d: d.getDate(), }; // Update the internal calendar date setDate(d); } let autoInput = null; const getInput = function() { let input = self.input; if (input && input.current) { input = input.current; } else { if (input === 'auto') { if (! autoInput) { autoInput = document.createElement('input'); autoInput.type = 'text'; if (self.class) { autoInput.class = self.class; } if (self.name) { autoInput.name = self.name; } if (self.placeholder) { autoInput.placeholder = self.placeholder; } self.el.parentNode.insertBefore(autoInput, self.el); } input = autoInput; } } return input; } /** * Select an item with the enter or mouse * @param {object} e - mouse event * @param {object} item - selected cell */ self.select = function(e, item) { // Update cursor generic let value = setCursor(item); // Based where was the click if (self.view !== 'days') { // Update the internal date setDate(value); // Back to the days self.view = 'days'; } else { if (! self.range) { self.update(); } } } self.selectRange = function(e, item) { if (self.view === 'days' && self.range === true) { // Update cursor generic setCursor(item); // Update range setRange(item); } } /** * Next handler * @param {object?} e mouse event */ self.next = function(e) { if (! e || e.type === 'click') { // Icon click move(1); } else { // Keyboard handler moveCursor(1, e); } } /** * Next handler * @param {object?} e mouse event */ self.prev = function(e) { if (! e || e.type === 'click') { // Icon click move(-1); } else { // Keyboard handler moveCursor(-1, e); } } /** * Open the modal */ self.open = function(e) { if (self.modal && self.modal.closed) { let input = getInput(); // Open modal self.modal.closed = false; // Set the focus on the content to use the keyboard if (! (input && e.target.getAttribute('readonly') === null)) { self.content.focus(); } // Populate components self.hours = views.hours(); self.minutes = views.minutes(); // Update the internal date values updateValue(self.value); } } /** * Close the modal */ self.close = function() { if (self.modal && self.modal.closed === false) { // Close modal self.modal.closed = true; // Cancel range events destroyRange(); } } self.reset = function() { self.setValue(''); self.close(); } self.update = function() { self.setValue(renderValue()); self.close(); } /** * Change the view */ self.setView = function() { let v = this.getAttribute('data-view'); if (v) { self.view = v; } } /** * Get value from cursor * @returns {string} */ self.getValue = function() { return self.value; } self.setValue = function(v) { // Update the internal controllers updateValue(v); // Destroy range destroyRange(); // Update input if (self.input) { let input = getInput(); input.value = v; } if (v !== self.value) { // Update value self.value = v; } } self.onchange = function(prop) { if (prop === 'view') { if (typeof(views[self.view]) === 'function') { // When change the view update the data self.options = views[self.view].call(self, date); } } else if (prop === 'value') { if (typeof (onchange) === 'function') { onchange(self, self.value); } if (typeof (self.onupdate) === 'function') { self.onupdate(self, self.value); } if (typeof(self.onChange) === 'function') { let input = getInput(); if (input) { input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); } } self.setValue(self.value); } else if (prop === 'options') { self.content.focus(); } } self.onload = function() { // Populate components self.hours = views.hours(); self.minutes = views.minutes(); if (self.type !== "inline") { // Create modal instance self.modal = { width: 300, closed: true, focus: false, position: 'absolute', 'auto-close': false, 'auto-adjust': true, }; // Generate modal Modal(self.el, self.modal); } // Create input controls if (self.input) { let input = getInput(); input.classList.add('lm-calendar-input'); input.addEventListener('focus', self.open); input.addEventListener('click', self.open); input.addEventListener('blur', blur); if (self.onChange) { input.addEventListener('change', self.onChange); } // Retrieve the value if (self.value) { input.value = self.value; } else if (input.value && input.value !== self.value) { self.value = input.value; } } // Update the internal date values updateValue(self.value); /** * Handler keyboard * @param {object} e - event */ self.content.addEventListener('keydown', function(e){ let prevent = false; if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { self.prev(e); prevent = true; } else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { self.next(e); prevent = true; } else if (e.key === 'Enter') { // Current view let view = self.view; // Select self.selectRange(e, self.options[self.cursor.index]); self.select(e, self.options[self.cursor.index]); // If is range do something different if (view === 'days' && ! self.range) { self.update(); } prevent = true; } else { if (self.input) { // TODO: mask //jSuites.mask(e); } } if (prevent) { e.preventDefault(); e.stopImmediatePropagation(); } }); /** * Mouse wheel handler * @param {object} e - mouse event */ self.content.addEventListener('wheel', function(e){ if (self.wheel !== false) { if (e.deltaY < 0) { self.prev(); } else { self.next(); } e.preventDefault(); } }); /** * Range handler * @param {object} e - mouse event */ self.content.addEventListener('mouseover', function(e){ let parent = e.target.parentNode if (parent === self.content) { let index = Array.prototype.indexOf.call(parent.children, e.target); updateRange(self.options[index]); } }); // Create event for focus out self.el.addEventListener("focusout", (e) => { let input = getInput(); if (e.relatedTarget !== input && ! self.el.contains(e.relatedTarget)) { self.close(); } }); } return `<div class="lm-calendar" :value="self.value" data-grid="{{self.grid}}"> <div class="lm-calendar-options"> <button type="button" onclick="self.reset">Reset</button> <button type="button" onclick="self.update">Done</button> </div> <div class="lm-calendar-container" data-view="{{self.view}}"> <div class="lm-calendar-header"> <div> <div class="lm-calendar-labels"><button type="button" onclick="self.setView" data-view="months">{{self.month}}</button> <button type="button" onclick="self.setView" data-view="years">{{self.year}}</button></div> <div class="lm-calendar-navigation"> <button type="button" class="material-icons lm-ripple" onclick="self.prev" tabindex="0">arrow_drop_up</button> <button type="button" class="material-icons lm-ripple" onclick="self.next" tabindex="0">arrow_drop_down</button> </div> </div> <div class="lm-calendar-weekdays" :loop="self.weekdays"><div>{{self.title}}</div></div> </div> <div class="lm-calendar-content" :loop="self.options" tabindex="0" :ref="self.content"> <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" onmousedown="self.parent.selectRange">{{self.title}}</div> </div> <div class="lm-calendar-footer" data-visible="{{self.footer}}"> <div class="lm-calendar-time" data-visible="{{self.time}}"><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> <div class="lm-calendar-update"><input type="button" value="Update" onclick="self.update" class="lm-ripple"></div> </div> </div> </div>` } // Register the LemonadeJS Component lemonade.setComponents({ Calendar: Calendar }); // Register the web component lemonade.createWebComponent('calendar', Calendar); return function (root, options) { if (typeof (root) === 'object') { lemonade.render(Calendar, root, options) return options; } else { return Calendar.call(this, root) } } })));