diff --git a/app/src/plugins/tm@_components/z-datepicker.vue b/app/src/plugins/tm@_components/z-datepicker.vue index 039efd1..96aaeb9 100644 --- a/app/src/plugins/tm@_components/z-datepicker.vue +++ b/app/src/plugins/tm@_components/z-datepicker.vue @@ -1,7 +1,8 @@ @@ -46,10 +48,7 @@ export default { data() { return { - isFocused: false, - preventInputFocus: false, - savedScrollPosition: null, - focusListeners: new Map(), + uniqueId: `z-datepicker-${crypto.randomUUID()}`, }; }, @@ -59,74 +58,26 @@ export default { }, }, - mounted() { - // Attach focus listeners on mount to catch any focus events - // Use immediate attachment (no $nextTick) to catch early focus events - this.attachFocusListeners(); - }, - methods: { handleUpdate(value) { this.$emit('update:modelValue', value); }, handleOpen(event) { - this.isFocused = true; - - // IMPORTANT: If scroll position wasn't saved yet (handleClick wasn't called), - // save it now but it might be too late and already scrolled to 0 - if (this.savedScrollPosition === null) { - this.preventInputFocus = true; - const modalContent = document.querySelector('.v-dialog .modal-content'); - if (modalContent) { - this.savedScrollPosition = modalContent.scrollTop; - } - } - - // CRITICAL: Blur any currently focused input (v-dialog autofocus issue) - this.blurFocusedInputs(); - - // Reattach focus event listeners (for dynamically added inputs) - this.attachFocusListeners(); - - // Position the calendar menu relative to the modal - this.positionCalendar(); - - // Reset after datepicker is opened - setTimeout(() => { - this.preventInputFocus = false; - this.savedScrollPosition = null; - }, 500); - - // Emit to parent (for backward compatibility) - const wrapperElement = - event?.target?.closest?.('.z-datepicker-wrapper') || true; - this.$emit('open', wrapperElement); + this.$emit('open', event); }, handleClose(event) { - this.isFocused = false; this.$emit('closed', event); }, handleClick(event) { - // Prevent any default behavior - event.preventDefault(); - event.stopPropagation(); - - // CRITICAL: Set prevention flag BEFORE opening menu to catch early focus events - this.preventInputFocus = true; - - // Save scroll position FIRST (before blur or opening menu) - const modalContent = document.querySelector('.v-dialog .modal-content'); - if (modalContent) { - this.savedScrollPosition = modalContent.scrollTop; + // Only handle clicks on the wrapper itself, not on the datepicker's internal clear button + if (event.target.closest('.dp__clear_icon')) { + return; // Let the clear button work normally } - // CRITICAL: Blur any currently focused input - this.blurFocusedInputs(false); // Pass false to not restore scroll yet - - // Open the menu + // Open the menu programmatically if (this.$refs.datepickerRef?.openMenu) { this.$refs.datepickerRef.openMenu(); } @@ -137,129 +88,6 @@ export default { this.$refs.datepickerRef.openMenu(); } }, - - positionCalendar() { - setTimeout(() => { - const calendar = document.querySelector( - '.dp__menu.dp__menu_index.dp__theme_light', - ); - - if (!calendar) return; - - // Get the datepicker wrapper element - const datepickerWrapper = this.$el; - - // Get the modal container - const modalContainer = document.querySelector('.v-overlay__content'); - - if (!datepickerWrapper || !modalContainer) return; - - // Get bounding rectangles - const wrapperRect = datepickerWrapper.getBoundingClientRect(); - const modalRect = modalContainer.getBoundingClientRect(); - - // Calculate position relative to modal - const topPosition = - wrapperRect.top - modalRect.top + wrapperRect.height; - const leftPosition = wrapperRect.left - modalRect.left; - - // Apply positioning - calendar.style.top = `${topPosition + 210}px`; - }, 100); - }, - - blurFocusedInputs(restoreScroll = true) { - const activeElement = document.activeElement; - - if (activeElement && activeElement.tagName === 'INPUT') { - // Don't blur the datepicker's own input - if ( - activeElement.closest('.dp__input_wrap') || - activeElement.closest('.z-datepicker-wrapper') - ) { - return; - } - - activeElement.blur(); - - // Restore scroll position after blur (only if requested) - if (restoreScroll && this.savedScrollPosition !== null) { - const modalContent = document.querySelector( - '.v-dialog .modal-content', - ); - if (modalContent) { - modalContent.scrollTop = this.savedScrollPosition; - } - } - } - }, - - attachFocusListeners() { - // Remove old listeners first - this.removeFocusListeners(); - - // Attach new listeners to autocomplete inputs only (like the mixin) - // Trying to catch all inputs causes issues with event timing - const inputs = document.querySelectorAll('.v-autocomplete input'); - - let totalListeners = 0; - inputs.forEach((input) => { - // Skip the datepicker's own input - if (input.closest('.dp__input_wrap')) { - return; - } - - const listener = this.onInputFocus.bind(this); - this.focusListeners.set(input, listener); - input.addEventListener('focus', listener); - totalListeners++; - }); - }, - - onInputFocus(event) { - const target = event.target; - - // Skip if it's the datepicker's own input - if ( - target.closest('.dp__input_wrap') || - target.closest('.z-datepicker-wrapper') - ) { - return; - } - - // If prevention flag is set, block the focus - if (this.preventInputFocus && this.savedScrollPosition !== null) { - const scrollBefore = document.querySelector( - '.v-dialog .modal-content', - )?.scrollTop; - - // Blur the focused element with a slight delay (like the mixin does) - setTimeout(() => { - target.blur(); - - // Restore scroll position after blur - const modalContent = document.querySelector( - '.v-dialog .modal-content', - ); - if (modalContent && this.savedScrollPosition !== null) { - modalContent.scrollTop = this.savedScrollPosition; - const scrollAfter = modalContent.scrollTop; - } - }, 0); - } - }, - - removeFocusListeners() { - this.focusListeners.forEach((listener, input) => { - input.removeEventListener('focus', listener); - }); - this.focusListeners.clear(); - }, - }, - - beforeUnmount() { - // Cleanup listeners on component unmount - this.removeFocusListeners(); }, }; @@ -294,11 +122,6 @@ export default { background: white padding: 0 4px left: 13px - - &.is-focused .z-datepicker-label - color: rgba(0, 0, 0, 0.87) - - &.has-value .z-datepicker-label color: rgba(0, 0, 0, 0.6) &.has-value :deep(.dp__input_icon) @@ -313,19 +136,21 @@ export default { padding-bottom: 14px !important :deep(.dp__input_wrap) - pointer-events: none cursor: pointer :deep(.dp__input) cursor: pointer pointer-events: none !important + user-select: none :deep(.dp__clear_icon) pointer-events: auto !important cursor: pointer !important top: 28px !important + z-index: 10 :deep(.dp__input_icon) top: 27px !important left: 3px !important + pointer-events: none !important diff --git a/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts b/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts deleted file mode 100644 index 8d46da1..0000000 --- a/app/src/plugins/tm@modal/_mixins/vuetify-fix-focus.ts +++ /dev/null @@ -1,72 +0,0 @@ -export default { - // Mixin to fix autofocus on v-input in modals where date selects are used - data() { - return { - preventInputFocus: false, - savedScrollPosition: null, - }; - }, - - methods: { - setFocusEvent() { - const inputs = document.querySelectorAll('.v-autocomplete input'); - inputs.forEach((input) => { - input.addEventListener('focus', this.onInputFocus); - }); - }, - - onDateFocus($event) { - if ($event == undefined) { - this.preventInputFocus = true; - } - }, - - onDateOpen($event) { - // $event should be the element reference (or any truthy value to trigger saving) - if ($event) { - // Find the scrollable modal container - const modalContent = document.querySelector('.v-dialog .modal-content'); - if (modalContent) { - this.savedScrollPosition = modalContent.scrollTop; - } - - this.preventInputFocus = true; - - // Reset after datepicker is opened - setTimeout(() => { - this.preventInputFocus = false; - this.savedScrollPosition = null; - }, 500); - } - }, - - onInputFocus($event) { - const target = $event.target; - - // If we have a saved scroll position and an unwanted focus happened - if (this.preventInputFocus && this.savedScrollPosition !== null) { - const target = $event.target; - - // Only intercept text input focus (autocomplete fields) - if (target.tagName === 'INPUT' && target.type === 'text') { - const scrollBefore = document.querySelector( - '.v-dialog .modal-content', - )?.scrollTop; - - // Blur the focused element - setTimeout(() => { - target.blur(); - - const modalContent = document.querySelector( - '.v-dialog .modal-content', - ); - if (modalContent && this.savedScrollPosition !== null) { - modalContent.scrollTop = this.savedScrollPosition; - const scrollAfter = modalContent.scrollTop; - } - }, 0); - } - } - }, - }, -}; diff --git a/app/src/plugins/tm@modal/blocker/z-blocker-modal.vue b/app/src/plugins/tm@modal/blocker/z-blocker-modal.vue index 9b04962..71cf9d6 100644 --- a/app/src/plugins/tm@modal/blocker/z-blocker-modal.vue +++ b/app/src/plugins/tm@modal/blocker/z-blocker-modal.vue @@ -1,197 +1,278 @@ diff --git a/app/src/plugins/tm@modal/blocker/z-delete-blocker-modal.vue b/app/src/plugins/tm@modal/blocker/z-delete-blocker-modal.vue index 4eb39af..d0badd6 100644 --- a/app/src/plugins/tm@modal/blocker/z-delete-blocker-modal.vue +++ b/app/src/plugins/tm@modal/blocker/z-delete-blocker-modal.vue @@ -1,5 +1,5 @@