Files
jandl-taskmanager-app/app/src/plugins/tm@_components/z-datepicker.vue
T

332 lines
8.5 KiB
Vue

<template>
<div
class="z-datepicker-wrapper"
:class="{ 'has-value': hasValue, 'is-focused': isFocused }"
@click="handleClick"
>
<label v-if="label" class="z-datepicker-label">{{ label }}</label>
<Datepicker
ref="datepickerRef"
:model-value="modelValue"
v-bind="$attrs"
:placeholder="hasValue ? ' ' : placeholder"
@update:model-value="handleUpdate"
@open="handleOpen"
@closed="handleClose"
/>
</div>
</template>
<script>
import Datepicker from '@vuepic/vue-datepicker';
export default {
name: 'ZDatepicker',
components: {
Datepicker,
},
props: {
modelValue: {
type: [Date, String, null],
default: null,
},
label: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'open', 'closed'],
data() {
return {
isFocused: false,
preventInputFocus: false,
savedScrollPosition: null,
focusListeners: new Map(),
};
},
computed: {
hasValue() {
return this.modelValue != null && this.modelValue !== '';
},
},
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);
},
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;
}
// CRITICAL: Blur any currently focused input
this.blurFocusedInputs(false); // Pass false to not restore scroll yet
// Open the menu
if (this.$refs.datepickerRef?.openMenu) {
this.$refs.datepickerRef.openMenu();
}
},
openMenu() {
if (this.$refs.datepickerRef?.openMenu) {
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();
},
};
</script>
<style scoped lang="sass">
.z-datepicker-wrapper
position: relative
width: 100%
cursor: pointer
.z-datepicker-label
position: absolute
left: 16px
top: 50%
transform: translateY(-50%)
font-size: 16px
color: rgba(0, 0, 0, 0.38)
background: transparent
padding: 0
z-index: 1
pointer-events: none
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1)
opacity: 0
visibility: hidden
&.has-value .z-datepicker-label
opacity: 1
visibility: visible
top: 8px
transform: translateY(0)
font-size: 12px
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)
top: 33px !important
&.has-value :deep(.dp__input)
padding-top: 20px !important
padding-bottom: 8px !important
:deep(.dp__input)
padding-top: 14px !important
padding-bottom: 14px !important
:deep(.dp__input_wrap)
pointer-events: none
cursor: pointer
:deep(.dp__input)
cursor: pointer
pointer-events: none !important
:deep(.dp__clear_icon)
pointer-events: auto !important
cursor: pointer !important
top: 28px !important
:deep(.dp__input_icon)
top: 27px !important
left: 3px !important
</style>