MFCalendarPopup / beta / js / mf-calendar-popup.js
file last updated : 2024-07-04
/*! mf-calendar-popup.js (@ https://methodfish.com/Projects/MFCalendarPopup)
 * Version: 1.0
 * Licence: MIT
 */

// Calendar popup
// getPopup(document.querySelector('button'), new Date('2024-05-08'), setDate);
//
// function setDate(selectedDt) {
//     alert('Set to '+selectedDt);
// }
//
let _mfCalendarPopupEvents=false;
function MFCalendarPopup() {

    const _today = new Date();
    var _selectedYearN, _selectedMonthN, _selectedDayN;
    let _popup;

    function getPopup(elem, selectedDt, cb) {

        function generateDaysGrid(year, month) {
            let parts = document.querySelector('#cal-dt').value.split('-');
            _selectedYearN = parts[0];
            _selectedMonthN = parts[1];
            _selectedDayN = parts[2];

            daysContainer.innerHTML = '';
            const daysInMonth = new Date(year, month + 1, 0).getDate();
            const firstDayOfMonth = new Date(year, month, 1).getDay();
            for (let i = 0; i < firstDayOfMonth; i++) {
                const emptyCell = document.createElement('div');
                emptyCell.classList.add('day');
                daysContainer.appendChild(emptyCell);
            }

            for (let day = 1; day <= daysInMonth; day++) {
                const dayCell = document.createElement('div');
                dayCell.classList.add('day');
                if ( year==_selectedYearN && month==_selectedMonthN && day==_selectedDayN ) dayCell.classList.add('selectedDate');
                dayCell.textContent = day;

                if (year === _today.getFullYear() && month === _today.getMonth() && day === _today.getDate()) {
                    dayCell.classList.add('today');
                }
                const currentDate = new Date(year, month, day);
                if (currentDate.getDay() === 0 || currentDate.getDay() === 6) {
                    dayCell.classList.add('weekend'); // Add 'weekend' class for Saturday and Sunday
                }
                dayCell.addEventListener('click', () => selectDate(year, month, day));
                daysContainer.appendChild(dayCell);
            }

        }

        
        function selectDate(year, month, day) {
            const selectedDt = new Date(year, month, day);
            cb(selectedDt);
            hidePopup();
        }


        function hidePopup() {
            _popup.style.display = 'none';
        }


        function updateYearList(yearSelect, selectedYear) {
            yearSelect.innerHTML = '';
            const currentYear = selectedYear;
            for (let i = currentYear - 50; i <= currentYear + 50; i++) {
                const option = document.createElement('option');
                option.value = i;
                option.textContent = i;
                if ( i === selectedYear ) {
                    option.selected = true;
                }
                yearSelect.appendChild(option);
            }
        }


        function removePX(x){ // Return "a" from inside "apx"
            if ( x=='' ) return parseInt(0);
            var x2=''+x;
            return parseInt(x2.replace(/px/, ''),10);
        }


        function isnull(tx) {
            if ( ! tx ) return "[null]";
            return tx;
        }


        function isNumeric(n) {
            return !isNaN(parseFloat(n)) && isFinite(n);
        }


        function getComputedStyleX(elem,attr,doRemovePX,nulls) {
            if ( nulls==undefined ) nulls='[null]';
            if ( doRemovePX==undefined ) doRemovePX=false;
            if ( typeof elem==='string' ) {
                let elems = document.querySelectorAll(elem);
                for(let i=elems.length-1; i>=0; i-- ) getComputedStyleX(elems[i],attr,doRemovePX,nulls);
                return;
            }
            if ( !elem ) return "Null given in getComputedStyleX(NULL,'"+attr+"')";

            var style = window.getComputedStyle(elem);
            var result = style.getPropertyValue(attr);
            if ( doRemovePX ) {
                var r = parseFloat(removePX(result));
                if (!r) return nulls;
                else return r;
            }
            return isnull(result);
        }


        function getMaxZIndex() {
            var elems= document.getElementsByTagName('div');
            var max=0;
            for(var i=0;i<elems.length;i++) {
                var t=getComputedStyleX(elems[i],'z-index'); // elems[i].style.zIndex;
                if ( t!='auto' ) {
                    if (isNumeric(t) && parseInt(t) > max) {
                        //console.log('16249:'+elems[i].className+' ('+elems[i].id+')='+t);
                        max = parseInt(t);
                    }
                }
            }
            var elems=document.getElementsByTagName('canvas');
            for(var i=0;i<elems.length;i++) {
                var t= getComputedStyleX(elems[i],'z-index'); // elems[i].style.zIndex;
                if ( isNumeric(t) && parseInt(t)>max ) {
                    max = parseInt(t);
                }
            }
            return parseInt(max);
        }


        function moveDivOnscreen(elem, container) {
            // Get the dimensions of the viewport
            let viewportWidth, viewportHeight, containerLeft = 0, containerTop = 0, containerScrollLeft = 0, containerScrollTop = 0;

            if (container === undefined) container = window;

            if (container === window) {
                viewportWidth = window.innerWidth - 30;
                viewportHeight = window.innerHeight - 30;
                containerScrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
                containerScrollTop = (window.pageYOffset || document.documentElement.scrollTop);
            } else {
                const containerRect = container.getBoundingClientRect();
                containerLeft = containerRect.left;
                containerTop = containerRect.top;
                viewportWidth = containerRect.width - 30;
                viewportHeight = containerRect.height - 30;
                containerScrollLeft = container.scrollLeft;
                containerScrollTop = container.scrollTop;
            }

            // Get the bounding rectangle of the element
            const elemRect = elem.getBoundingClientRect();

            // Calculate the desired position of the element relative to the viewport
            let newTop = elemRect.top + containerScrollTop;
            let newLeft = elemRect.left + containerScrollLeft;

            // Check if the element would exceed the viewport width
            if ((elemRect.right - containerLeft) > viewportWidth) {
                newLeft -= (elemRect.right - containerLeft - viewportWidth);
            }

            // Check if the element would exceed the viewport height
            if (elemRect.bottom > viewportHeight + containerTop) {
                newTop -= (elemRect.bottom - viewportHeight - containerTop);
            }

            // Set the new position of the element
            elem.style.position = 'absolute';
            elem.style.top = newTop + 'px';
            elem.style.left = newLeft + 'px';

            // Ensure the element doesn't go off the left side of the viewport
            if (elem.getBoundingClientRect().left < 5) {
                elem.style.left = (5 + containerScrollLeft) + 'px';
            }
        }


        // Create popup container
        let daysContainer;
        _popup = document.querySelector('.mf-calendar-popup');

        if ( !_popup ) {
            _popup = document.createElement('div');
            _popup.classList.add('mf-calendar-popup');
            _popup.classList.add('removeOnEscape');

            // Create header container
            const header = document.createElement('div');
            if ( 1 ) {
                header.classList.add('header');


                const row1        = document.createElement('div');

                
                // Create hide button
                const hideBtn       = document.createElement('i');
                hideBtn.textContent = 'close';
                hideBtn.classList.add('material-icons');
                hideBtn.classList.add('hideBtn');
                hideBtn.addEventListener('click', hidePopup);
                row1.appendChild(hideBtn);


                // Create previous month button
                const prevMonthBtn       = document.createElement('i');
                prevMonthBtn.classList.add('prevMonthBtn');
                prevMonthBtn.classList.add('material-icons');
                prevMonthBtn.textContent = 'navigate_before';
                prevMonthBtn.addEventListener('click', () => {
                    const selectedMonth = parseInt(monthSelect.value);
                    if (selectedMonth > 0) {
                        monthSelect.selectedIndex = selectedMonth - 1;
                    }
                    else {
                        monthSelect.selectedIndex = 11; // December
                        yearSelect.value          = parseInt(yearSelect.value) - 1;
                    }
                    generateDaysGrid(parseInt(yearSelect.value), parseInt(monthSelect.value));
                });
                row1.appendChild(prevMonthBtn);


                // Create month selector
                const monthSelect = document.createElement('select');
                monthSelect.classList.add('month');
                const months      = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
                months.forEach((month, index) => {
                    const option       = document.createElement('option');
                    option.value       = index;
                    option.textContent = month;
                    monthSelect.appendChild(option);
                });
                monthSelect.selectedIndex = selectedDt.getMonth();
                monthSelect.addEventListener('change', () => {
                    generateDaysGrid(parseInt(yearSelect.value), parseInt(monthSelect.value));
                });
                row1.appendChild(monthSelect);


                // Create year selector
                const yearSelect  = document.createElement('select');
                yearSelect.classList.add('year');
                const currentYear = new Date().getFullYear();
                for (let i = currentYear - 50; i <= currentYear + 50; i++) {
                    const option       = document.createElement('option');
                    option.value       = i;
                    option.textContent = i;
                    yearSelect.appendChild(option);
                }
                yearSelect.value = selectedDt.getFullYear();
                yearSelect.addEventListener('change', () => {
                    generateDaysGrid(parseInt(yearSelect.value), parseInt(monthSelect.value));
                }, _selectedYearN, _selectedMonthN, _selectedDayN);
                row1.appendChild(yearSelect);
                yearSelect.addEventListener('change', function() {
                    let selectedYear = parseInt(yearSelect.value); // Update the selected year
                    updateYearList(yearSelect, selectedYear);
                });

                
                // Create next month button
                const nextMonthBtn       = document.createElement('i');
                nextMonthBtn.classList.add('nextMonthBtn');
                nextMonthBtn.classList.add('material-icons');
                nextMonthBtn.textContent = 'navigate_next';
                nextMonthBtn.addEventListener('click', () => {
                    const selectedMonth = parseInt(monthSelect.value);
                    if (selectedMonth < 11) {
                        monthSelect.selectedIndex = selectedMonth + 1;
                    }
                    else {
                        monthSelect.selectedIndex = 0; // January
                        yearSelect.value          = parseInt(yearSelect.value) + 1;
                    }
                    generateDaysGrid(parseInt(yearSelect.value), parseInt(monthSelect.value));
                });
                row1.appendChild(nextMonthBtn);

                const hidden = document.createElement('input');
                hidden.type='hidden';
                hidden.id = 'cal-dt';
                hidden.value = _selectedYearN+'-'+_selectedMonthN+'-'+_selectedDayN;
                row1.appendChild(hidden);

                header.appendChild(row1);

                
                // Append days of the week
                let dayNamesContainer = document.createElement('div');
                dayNamesContainer.classList.add('header');
                const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
                daysOfWeek.forEach(day => {
                    const dayName       = document.createElement('div');
                    dayName.textContent = day;
                    dayName.classList.add('day-name');
                    if (day == 'Sat' || day == 'Sun') dayName.classList.add('weekend');
                    dayNamesContainer.appendChild(dayName);
                });

                _popup.appendChild(header);
                _popup.appendChild(dayNamesContainer);

            }
            
            daysContainer = document.createElement('div');
            daysContainer.classList.add('days');
            _popup.appendChild(daysContainer);
            
            document.body.appendChild(_popup);
        }
        else {
            _popup.style.display='';
            daysContainer = _popup.querySelector('.days');

            _popup.querySelector('.month').value = _selectedMonthN;
            _popup.querySelector('.year').value = _selectedYearN;

            let hidden = document.querySelector('#cal-dt');
            hidden.value = _selectedYearN+'-'+_selectedMonthN+'-'+_selectedDayN;
        }
        _popup.style.zIndex = getMaxZIndex()+1;


        generateDaysGrid(_selectedYearN, _selectedMonthN);

        
        const rect = elem.getBoundingClientRect();
        _popup.style.top = `${rect.bottom}px`;
        _popup.style.left = `${rect.left}px`;

        moveDivOnscreen(_popup, undefined, elem);

        if ( !_mfCalendarPopupEvents ) {
            _mfCalendarPopupEvents = true;
            window.addEventListener('resize', function() {
                hidePopup(); // _popup.style.display = 'none';
            });
            window.addEventListener('scroll', function() {
                hidePopup(); // _popup.style.display = 'none';
            });
        }
    }


    // Function to show the popup each time it's called
    this.getDate = function(elem, selectedDt, cb) {
        if (!_popup || _popup.style.display === 'none') {
            _selectedYearN = parseInt(selectedDt.getFullYear());
            _selectedMonthN = parseInt(selectedDt.getMonth()); // 0 - 11
            _selectedDayN = parseInt(selectedDt.getDate());
            getPopup(elem, selectedDt, cb);
        } else {
            _popup.style.display = 'block';
        }

        return _popup;
    };


    this.removePopup = function() {
        var element = document.querySelector('.mf-calendar-popup');
        if (element) {
            element.remove();
        }
        _popup = null;
    }


}

About

License

Latest Release

Version 1.022024-07-04