UNPKG

jspanel4

Version:

A JavaScript library to create highly configurable multifunctional floating panels that can also be used as modal, tooltip, hint or contextmenu

403 lines (371 loc) 20.8 kB
/** * jsPanel - A JavaScript library to create highly configurable multifunctional floating panels that can also be used as modal, tooltip, hint or contextmenu * @version v4.16.1 * @homepage https://jspanel.de/ * @license MIT * @author Stefan Sträßer - info@jspanel.de * @author of dialog extension: Michael Daumling - michael@terrapinlogo.com * @github https://github.com/Flyer53/jsPanel4.git */ import {jsPanel} from '../../jspanel.js'; /** * requires moment.js < https://momentjs.com/ > to be loaded prior this extension */ // TODO: - cancelable events dateselect, rangeselect, selectionclear, etc. ?? // - alternative way to select a range, e.g start by Alt+Click end end by another Alt-Click ?? // - make dates not selectable and mark them accordingly ?? // - load list of days to highlight somehow (e.g. holidays) if (!jsPanel.datepicker) { // add some icons for the datepicker controls jsPanel.icons.chevronLeft = `<svg focusable="false" class="jsPanel-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><g transform="matrix(6.12323e-17,-1,1,6.12323e-17,0.0375,22.0375)"><path fill="currentColor" d="M2.1,15.2L2.9,16C3.1,16.2 3.4,16.2 3.6,16L11,8.7L18.4,16C18.6,16.2 18.9,16.2 19.1,16L19.9,15.2C20.1,15 20.1,14.7 19.9,14.5L11.3,6C11.1,5.8 10.8,5.8 10.6,6L2.1,14.5C2,14.7 2,15 2.1,15.2Z"/></g></svg>`; jsPanel.icons.chevronRight = `<svg focusable="false" class="jsPanel-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><g transform="matrix(6.12323e-17,1,-1,6.12323e-17,22.0375,-0.0375)"><path fill="currentColor" d="M2.1,15.2L2.9,16C3.1,16.2 3.4,16.2 3.6,16L11,8.7L18.4,16C18.6,16.2 18.9,16.2 19.1,16L19.9,15.2C20.1,15 20.1,14.7 19.9,14.5L11.3,6C11.1,5.8 10.8,5.8 10.6,6L2.1,14.5C2,14.7 2,15 2.1,15.2Z"/></g></svg>`; jsPanel.icons.square = `<svg focusable="false" class="jsPanel-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><g transform="matrix(0.0401786,0,0,0.0401786,2,0.714286)"><path fill="currentColor" d="M400,32L48,32C21.5,32 0,53.5 0,80L0,432C0,458.5 21.5,480 48,480L400,480C426.5,480 448,458.5 448,432L448,80C448,53.5 426.5,32 400,32ZM394,432L54,432C50.7,432 48,429.3 48,426L48,86C48,82.7 50.7,80 54,80L394,80C397.3,80 400,82.7 400,86L400,426C400,429.3 397.3,432 394,432Z"/></g></svg>`; jsPanel.icons.undo = `<svg focusable="false" class="jsPanel-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><g transform="matrix(2.18687e-18,0.0357143,-0.0357143,2.18687e-18,20.1429,2)"><path fill="currentColor" d="M12,8L39.711,8C46.45,8 51.868,13.548 51.708,20.286L49.361,118.854C93.925,51.834 170.212,7.73 256.793,8.001C393.18,8.428 504.213,120.009 504,256.396C503.786,393.181 392.835,504 256,504C192.074,504 133.798,479.813 89.822,440.092C84.709,435.474 84.468,427.531 89.34,422.659L109.078,402.921C113.576,398.423 120.831,398.136 125.579,402.369C160.213,433.246 205.895,452 256,452C364.322,452 452,364.338 452,256C452,147.678 364.338,60 256,60C176.455,60 108.059,107.282 77.325,175.302L203.714,172.293C210.451,172.133 216,177.55 216,184.29L216,212C216,218.627 210.627,224 204,224L12,224C5.373,224 0,218.627 0,212L0,20C0,13.373 5.373,8 12,8Z"/></g></svg>`; jsPanel.datepicker = { version: '0.3.2', date: '2020-06-19 09:48', defaults: { locale: 'en', startdate: undefined, months: 1, showWeekNumbers: true, ondateselect: undefined, onrangeselect: undefined, onselectionclear: undefined, callback: undefined }, keyValue: undefined, generateHTML() { let wrapper = document.createElement('div'); wrapper.className = 'jsPanel-cal-wrapper'; wrapper.innerHTML = `<div class="jsPanel-cal-sub jsPanel-cal-clear" title="Clear all selections">${jsPanel.icons.square}</div> <div class="jsPanel-cal-sub jsPanel-cal-back" title="Go back one month">${jsPanel.icons.chevronLeft}</div> <div class="jsPanel-cal-sub jsPanel-cal-month"></div> <div class="jsPanel-cal-sub jsPanel-cal-forward" title="Go forward one month">${jsPanel.icons.chevronRight}</div> <div class="jsPanel-cal-sub jsPanel-cal-reset" title="Reset to current month">${jsPanel.icons.undo}</div> <div class="jsPanel-cal-sub jsPanel-cal-blank3"></div> <div class="jsPanel-cal-sub day-name day-name-0"></div> <div class="jsPanel-cal-sub day-name day-name-1"></div> <div class="jsPanel-cal-sub day-name day-name-2"></div> <div class="jsPanel-cal-sub day-name day-name-3"></div> <div class="jsPanel-cal-sub day-name day-name-4"></div> <div class="jsPanel-cal-sub day-name day-name-5"></div> <div class="jsPanel-cal-sub day-name day-name-6"></div>`; for (let i = 0; i < 6; i++) { wrapper.innerHTML += `<div class="jsPanel-cal-sub week week-${i}"></div>`; } for (let i = 1; i < 43; i++) { wrapper.innerHTML += `<div class="jsPanel-cal-sub day day-${i}"></div>`; // ${i} is just a counter of days listed in the calendar, not a date value! } return wrapper; }, // method to fill a month with data fillMonth(datepicker, startdate = moment()) { moment.locale(datepicker.options.locale); // set locale globally let now = moment(startdate) || moment(datepicker.options.startdate); now.locale(datepicker.options.locale); let month = now.month(), // returns number 0 to 11 where 0 is January firstDay = now.date(1).weekday(), // returns locale aware number 0 to 6 where 0 is either Sunday or Monday localeData = now.localeData(); // fill selected month incl. year let monthBox = datepicker.querySelector('.jsPanel-cal-month'); monthBox.innerHTML = now.format('MMMM YYYY'); monthBox.dataset.date = now.format('YYYY-MM-DD'); // fill day names (Mo, Tu, etc.) considering used locale let dayNames = datepicker.querySelectorAll('.jsPanel-cal-sub.day-name'), weekdays = localeData.weekdaysMin(); if (localeData.firstDayOfWeek() === 1) { // week starts with Monday for (let i = 0, j = 1; i < 7; i++, j++) { dayNames[i].textContent = weekdays[j]; } dayNames[6].textContent = weekdays[0]; dayNames[5].classList.add('weekend'); dayNames[6].classList.add('weekend'); } else { for (let i = 0; i < 7; i++) { dayNames[i].textContent = weekdays[i]; } dayNames[0].classList.add('weekend'); dayNames[6].classList.add('weekend'); } // fill dates let firstEntry = now.subtract(++firstDay, 'days'); let days = datepicker.querySelectorAll('.jsPanel-cal-sub.day'); for (let day of days) { day.classList.remove('today', 'notInMonth','selected', 'range', 'remove-border-radius-right', 'remove-border-radius-left'); let value = firstEntry.add(1, 'days'); day.textContent = value.format('D'); day.dataset.date = now.format('YYYY-MM-DD'); if (value.month() !== month) { day.classList.add('notInMonth'); } else if (day.dataset.date === moment().format('YYYY-MM-DD')) { day.classList.add('today'); } } // fill week numbers if (datepicker.options.showWeekNumbers) { datepicker.querySelectorAll('.jsPanel-cal-sub.week').forEach((week, index) => { week.textContent = moment( datepicker.querySelector(`.jsPanel-cal-sub.day-${(index + 1) * 7}`).dataset.date ).week(); }); } }, // deselect all days (remove .selected class; does not empty selectedDays/selectedRange) // do not empty selectedDays/selectedRange -> selection would be lost when clicking forward/back buttons deselectAllDays(container) { for (let day of container.querySelectorAll('.jsPanel-cal-sub.day')) { day.classList.remove('selected', 'range', 'remove-border-radius-right', 'remove-border-radius-left'); } }, // method to restore selected dates or range restoreSelections(container) { // restore selections of days let days = container.querySelectorAll('.jsPanel-cal-sub.day'); for (let day of days) { if (container.selectedDays.has(day.dataset.date)) { day.classList.add('selected'); } } // restore selection of a range if (container.selectedRange.size) { let rangeIterator = container.selectedRange.values(); let rangeArray = rangeIterator.next().value.split('/'); for (let day of days) { let date = day.dataset.date; if (date >= rangeArray[0] && date <= rangeArray[1]) { day.classList.add('selected', 'range'); // remove border radius of dates between start and end if (date === rangeArray[0]) { day.classList.add('remove-border-radius-right'); } else if (date === rangeArray[1]) { day.classList.add('remove-border-radius-left'); } else if (date > rangeArray[0] || date < rangeArray[1]) { day.classList.add('remove-border-radius-right', 'remove-border-radius-left'); } } } } }, create(container, options = {}) { container.style.display = 'flex'; container.selectedDays = new Set(); container.selectedRange = new Set(); let opts = Object.assign({}, this.defaults, options); let wrapper; // fill container with monthly calendars according to option.months for (let i = 0; i < opts.months; i++) { wrapper = this.generateHTML(); wrapper.options = opts; container.append(wrapper); // fill month with data this.fillMonth(wrapper, opts.startdate); // increase startdate 1 month for next calendar opts.startdate = moment(opts.startdate).add(1, 'months'); } // add handlers for back, forward etc. buttons let pickers = container.querySelectorAll('.jsPanel-cal-wrapper'); for (let picker of pickers) { // clear buttons for (let clearbtn of picker.querySelectorAll('.jsPanel-cal-clear')) { clearbtn.addEventListener('click', (e) => { if (opts.onselectionclear && typeof opts.onselectionclear === 'function') { opts.onselectionclear.call(container, container, e); } if (!e.defaultPrevented) { jsPanel.datepicker.deselectAllDays(container); container.selectedDays.clear(); container.selectedRange.clear(); } }); } // back buttons for (let backbtn of picker.querySelectorAll('.jsPanel-cal-back')) { backbtn.addEventListener('click', () => { // get all wrappers and decrease their date for (let picker of pickers) { let monthshown = picker.querySelector('.jsPanel-cal-month').dataset.date, // string like '2020-02-12' monthwanted = moment(monthshown).subtract(1, 'months').format('YYYY-MM'); jsPanel.datepicker.fillMonth(picker, monthwanted); } jsPanel.datepicker.deselectAllDays(container); jsPanel.datepicker.restoreSelections(container); }); } // forward buttons for (let fwdbtn of picker.querySelectorAll('.jsPanel-cal-forward')) { fwdbtn.addEventListener('click', () => { // get all wrappers and increase their date for (let picker of pickers) { let monthshown = picker.querySelector('.jsPanel-cal-month').dataset.date, // string like '2020-02-12' monthwanted = moment(monthshown).add(1, 'months').format('YYYY-MM'); jsPanel.datepicker.fillMonth(picker, monthwanted); } jsPanel.datepicker.deselectAllDays(container); jsPanel.datepicker.restoreSelections(container); }); } // reset buttons for (let resetbtn of picker.querySelectorAll('.jsPanel-cal-reset')) { resetbtn.addEventListener('click', (e) => { // get month shown of clicked picker let picker = e.target.closest('.jsPanel-cal-wrapper'), counter = 0; while (picker.previousSibling) { counter++; picker = picker.previousSibling; } // counter is now the zero-based position of the clicked picker in the container // get month for first picker in sequence let month = moment().subtract(counter, 'months'); // reset each pickers month for (let picker of pickers) { jsPanel.datepicker.fillMonth(picker, month); month = moment(month).add(1, 'months'); } jsPanel.datepicker.deselectAllDays(container); jsPanel.datepicker.restoreSelections(container); }); } } /** * CLICK ON A DAY * MEANS SELECTION/DESELECTION OF SINGLE OR MULTIPLE DAYS; NO RANGES */ container.addEventListener('click', e => { e.preventDefault(); const target = e.target, altKey = e.altKey, ctrlKey = e.ctrlKey, shiftKey = e.shiftKey; if (target.classList.contains('day')) { // check whether day is already selected let selected = target.classList.contains('selected'); let date = target.dataset.date; /** * IF NO MODIFIER KEY IS PRESSED */ if (!ctrlKey && !shiftKey && !altKey) { // unselect all selected days and clear container.selectedDays jsPanel.datepicker.deselectAllDays(container); container.selectedDays.clear(); // select/unselect day depending on let selected if (selected) { target.classList.remove('selected'); } else { target.classList.add('selected'); // add selected day to storage container.selectedDays.add(date); } } else if (!altKey && ctrlKey && !shiftKey) { /** * IF CTRL KEY IS PRESSED */ container.selectedRange.clear(); for (let day of container.querySelectorAll('.day')) { if (day.classList.contains('selected') && day.classList.contains('range')) { day.classList.remove('range', 'selected'); } } // select/unselect day depending on let selected if (selected) { target.classList.remove('selected'); // remove selected day from storage container.selectedDays.delete(date); } else { target.classList.add('selected'); // add selected day to storage container.selectedDays.add(date); } } // custom callback if (opts.ondateselect && typeof opts.ondateselect === 'function') { opts.ondateselect.call(container, container, date, e); } } }); /** * POINTERDOWN HANDLER TO STARTING A RANGE SELECTION */ let rangeSelectionStarted; container.addEventListener('pointerdown', e => { e.preventDefault(); const target = e.target, altKey = e.altKey, ctrlKey = e.ctrlKey, shiftKey = e.shiftKey; let start = e.target.dataset.date, current = e.target.dataset.date, range = [start, start]; let calcRange = e => { e.preventDefault(); rangeSelectionStarted = true; if (container.selectedDays.size) { container.selectedDays.clear(); } // build range array and sort it if (e.target.classList.contains('day')) { current = e.target.dataset.date; range = [start, current].sort((a, b) => { return moment(a).unix() - moment(b).unix(); // convert values to number for comparison }); } // add needed classes to selected range for (let day of container.querySelectorAll('.day')) { let date = day.dataset.date; day.classList.remove('remove-border-radius-right', 'remove-border-radius-left'); if (date < range[0] || date > range[1]) { day.classList.remove('selected', 'range'); } else { day.classList.add('selected', 'range'); // remove border radius of dates between start and end if (date === range[0]) { day.classList.add('remove-border-radius-right'); } else if (date === range[1]) { day.classList.add('remove-border-radius-left'); } else if (date > range[0] || date < range[1]) { day.classList.add('remove-border-radius-right', 'remove-border-radius-left'); } } } // build range string for selectedRange container.selectedRange.clear(); container.selectedRange.add(container.querySelector(`.day[data-date="${range[0]}"]`).dataset.date + '/' + container.querySelector(`.day[data-date="${range[1]}"]`).dataset.date); }; // if pointerdown is on a day and Shift key is pressed if (target.classList.contains('day') && !altKey && !ctrlKey && shiftKey) { container.addEventListener('pointermove', calcRange); container.addEventListener('pointerup', () => { container.removeEventListener('pointermove', calcRange); }); } }); container.addEventListener('pointerup', (e) => { if (rangeSelectionStarted) { if (opts.onrangeselect && typeof opts.onrangeselect === 'function') { opts.onrangeselect.call(container, container, container.selectedRange, e); rangeSelectionStarted = undefined; } } rangeSelectionStarted = undefined; }); if (opts.callback) { opts.callback.call(container, container); } return container; } }; // jsPanel.datepicker.keyValue is set to the value of the pressed number key while key is down and if key's value is between 1 and 9 document.addEventListener('keydown', e => { if (e.key.match(/^[2-9]$/)) { jsPanel.datepicker.keyValue = e.key; } else if (e.key.match(/^1$/)) { jsPanel.datepicker.keyValue = 12; } }); document.addEventListener('keyup', () => { jsPanel.datepicker.keyValue = undefined; }); }