diff --git a/resources/css/lib/calendar.css b/resources/css/lib/calendar.css index 09f4b5a..6fc3a31 100644 --- a/resources/css/lib/calendar.css +++ b/resources/css/lib/calendar.css @@ -26,6 +26,7 @@ /* day element */ li { @apply bg-white relative px-1 pt-8 border-t-md border-gray-900 overflow-y-auto; + transition: scale 150ms ease-in-out; /* day number */ &::before { @@ -54,6 +55,60 @@ @apply rounded-br-lg; }*/ + /* progressive "show more" button */ + div.more-events { + @apply absolute bottom-0 h-6 bg-inherit z-2 flex items-center; + width: calc(100% - 0.5rem); + } + button.day-more { + @apply text-xs px-1 h-4; + } + &[data-event-visible="0"] { + .event:nth-child(n+1) { @apply hidden; } + } + &[data-event-visible="1"] { + .event:nth-child(n+2) { @apply hidden; } + } + &[data-event-visible="2"] { + .event:nth-child(n+3) { @apply hidden; } + } + &[data-event-visible="3"] { + .event:nth-child(n+4) { @apply hidden; } + } + &[data-event-visible="4"] { + .event:nth-child(n+5) { @apply hidden; } + } + &[data-event-visible="5"] { + .event:nth-child(n+6) { @apply hidden; } + } + &[data-event-visible="6"] { + .event:nth-child(n+7) { @apply hidden; } + } + &[data-event-visible="7"] { + .event:nth-child(n+8) { @apply hidden; } + } + &[data-event-visible="8"] { + .event:nth-child(n+9) { @apply hidden; } + } + &[data-event-visible="9"] { + .event:nth-child(n+10) { @apply hidden; } + } + &.is-expanded { + position: relative; + height: min-content; + padding-bottom: 1px; + z-index: 3; + border: 1.5px solid black; + border-radius: 0.5rem; + scale: 1.05; + width: 120%; + margin-left: -10%; + + div.more-events { + @apply relative h-8; + } + } + /* events */ .event { @apply flex items-center text-xs gap-1 px-1 py-px font-medium truncate rounded-sm bg-transparent; diff --git a/resources/js/app.js b/resources/js/app.js index 6692998..9aa727d 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -9,6 +9,10 @@ const SELECTORS = { colorPickerColor: '[data-colorpicker-color]', colorPickerHex: '[data-colorpicker-hex]', colorPickerRandom: '[data-colorpicker-random]', + monthDay: '.calendar.month .day', + monthDayEvent: 'a.event', + monthDayMore: '[data-day-more]', + monthDayMoreWrap: '.more-events', }; /** @@ -193,8 +197,127 @@ function initColorPickers(root = document) { root.querySelectorAll(SELECTORS.colorPicker).forEach(wire); } +/** + * month view overflow handling (progressive enhancement) + */ +function initMonthOverflow(root = document) { + const days = root.querySelectorAll(SELECTORS.monthDay); + days.forEach((day) => updateMonthOverflow(day)); +} + +function ensureDayMoreButton(dayEl) { + let wrapper = dayEl.querySelector(SELECTORS.monthDayMoreWrap); + if (!wrapper) { + wrapper = document.createElement('div'); + wrapper.className = 'more-events'; + dayEl.appendChild(wrapper); + } + + let button = wrapper.querySelector(SELECTORS.monthDayMore); + if (!button) { + button = document.createElement('button'); + button.type = 'button'; + button.className = 'day-more hidden'; + button.setAttribute('data-day-more', ''); + wrapper.appendChild(button); + } + + return button; +} + +function formatMoreLabel(dayEl, count) { + const template = dayEl.getAttribute('data-more-label') || ':count more'; + return template.replace(':count', count); +} + +function lessLabel(dayEl) { + return dayEl.getAttribute('data-less-label') || 'Show less'; +} + +function updateMonthOverflow(dayEl) { + if (!dayEl) return; + + const events = Array.from(dayEl.querySelectorAll(SELECTORS.monthDayEvent)) + .filter((el) => !el.classList.contains('hidden')); + + const moreButton = ensureDayMoreButton(dayEl); + + if (!events.length) { + moreButton.textContent = ''; + moreButton.classList.add('hidden'); + moreButton.removeAttribute('aria-expanded'); + dayEl.classList.remove('day--event-overflow'); + dayEl.setAttribute('data-event-visible', '0'); + return; + } + + if (dayEl.classList.contains('is-expanded')) { + moreButton.textContent = lessLabel(dayEl); + moreButton.classList.remove('hidden'); + moreButton.setAttribute('aria-expanded', 'true'); + dayEl.classList.remove('day--event-overflow'); + dayEl.setAttribute('data-event-visible', String(events.length)); + return; + } + + const wrapper = moreButton.closest(SELECTORS.monthDayMoreWrap); + let wrapperHeight = wrapper ? wrapper.getBoundingClientRect().height : 0; + if (wrapperHeight === 0 && wrapper) { + const wasHidden = moreButton.classList.contains('hidden'); + const prevVisibility = moreButton.style.visibility; + + if (wasHidden) { + moreButton.classList.remove('hidden'); + moreButton.style.visibility = 'hidden'; + } + + wrapperHeight = wrapper.getBoundingClientRect().height || 0; + + if (wasHidden) { + moreButton.classList.add('hidden'); + moreButton.style.visibility = prevVisibility; + } + } + + const prevVisibility = dayEl.style.visibility; + dayEl.style.visibility = 'hidden'; + dayEl.removeAttribute('data-event-visible'); + dayEl.classList.remove('day--event-overflow'); + + const availableHeight = dayEl.clientHeight - wrapperHeight; + let hiddenCount = 0; + events.forEach((eventEl) => { + const bottom = eventEl.offsetTop + eventEl.offsetHeight; + if (bottom > availableHeight + 0.5) { + hiddenCount += 1; + } + }); + + dayEl.style.visibility = prevVisibility; + + const visibleCount = Math.max(0, events.length - hiddenCount); + dayEl.setAttribute('data-event-visible', String(visibleCount)); + + if (hiddenCount > 0) { + moreButton.textContent = formatMoreLabel(dayEl, hiddenCount); + moreButton.classList.remove('hidden'); + moreButton.setAttribute('aria-expanded', 'false'); + dayEl.classList.add('day--event-overflow'); + } else { + moreButton.textContent = ''; + moreButton.classList.add('hidden'); + moreButton.removeAttribute('aria-expanded'); + dayEl.classList.remove('day--event-overflow'); + } +} + +/** + * initialization + */ + function initUI() { initColorPickers(); + initMonthOverflow(); } // initial bind @@ -203,4 +326,23 @@ document.addEventListener('DOMContentLoaded', initUI); // rebind in htmx for swapped content document.addEventListener('htmx:afterSwap', (e) => { initColorPickers(e.target); + initMonthOverflow(e.target); +}); + +document.addEventListener('click', (event) => { + const button = event.target.closest(SELECTORS.monthDayMore); + if (!button) return; + + const dayEl = button.closest(SELECTORS.monthDay); + if (!dayEl) return; + + dayEl.classList.toggle('is-expanded'); + updateMonthOverflow(dayEl); +}); + +let monthResizeTimer; +window.addEventListener('resize', () => { + if (!document.querySelector(SELECTORS.monthDay)) return; + window.clearTimeout(monthResizeTimer); + monthResizeTimer = window.setTimeout(() => initMonthOverflow(), 100); });