mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 22:39:34 +00:00
Merge branch 'task000/concepts' of bitbucket.org:studiotem/jandl-taskmanager-app
This commit is contained in:
commit
b889d153cf
126 files changed
+744
-21762
No files matched your search
@@ -0,0 +1,331 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user