mirror of
https://bitbucket.org/studiotem/jandl-taskmanager-app.git
synced 2026-10-21 14:29:34 +00:00
feat(datepicker): introduce ZDatepicker component for enhanced date selection functionality
This commit is contained in:
1 parent
205b3a7e42
commit
a1e71006f0
4 files changed
+506
-29
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"
|
||||||
|
@update:model-value="handleUpdate"
|
||||||
|
@open="handleOpen"
|
||||||
|
@closed="handleClose"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:placeholder="hasValue ? ' ' : placeholder"
|
||||||
|
/>
|
||||||
|
</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
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.attachFocusListeners();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleUpdate(value) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleOpen(event) {
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] handleOpen called at:',
|
||||||
|
new Date().toISOString(),
|
||||||
|
);
|
||||||
|
this.isFocused = true;
|
||||||
|
|
||||||
|
// IMPORTANT: Set flag and save position BEFORE any other action
|
||||||
|
this.preventInputFocus = true;
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] preventInputFocus set to:',
|
||||||
|
this.preventInputFocus,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Save scroll position
|
||||||
|
const modalContent = document.querySelector('.v-dialog .modal-content');
|
||||||
|
if (modalContent) {
|
||||||
|
this.savedScrollPosition = modalContent.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] Saved scroll position:',
|
||||||
|
this.savedScrollPosition,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('🔵 [z-datepicker] Modal content not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reattach focus event listeners (for dynamically added inputs)
|
||||||
|
this.attachFocusListeners();
|
||||||
|
|
||||||
|
// Reset after datepicker is opened
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preventInputFocus = false;
|
||||||
|
this.savedScrollPosition = null;
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] Reset preventInputFocus and savedScrollPosition after 500ms',
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// Emit to parent (for backward compatibility)
|
||||||
|
const wrapperElement =
|
||||||
|
event?.target?.closest?.('.z-datepicker-wrapper') || true;
|
||||||
|
console.log('🔵 [z-datepicker] Emitting open event to parent');
|
||||||
|
this.$emit('open', wrapperElement);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose(event) {
|
||||||
|
this.isFocused = false;
|
||||||
|
this.$emit('closed', event);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClick(event) {
|
||||||
|
console.log('🔵 [z-datepicker] Wrapper clicked');
|
||||||
|
// 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 immediately
|
||||||
|
const modalContent = document.querySelector('.v-dialog .modal-content');
|
||||||
|
if (modalContent) {
|
||||||
|
this.savedScrollPosition = modalContent.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] Pre-saved scroll position in handleClick:',
|
||||||
|
this.savedScrollPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.datepickerRef?.openMenu) {
|
||||||
|
this.$refs.datepickerRef.openMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openMenu() {
|
||||||
|
if (this.$refs.datepickerRef?.openMenu) {
|
||||||
|
this.$refs.datepickerRef.openMenu();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
attachFocusListeners() {
|
||||||
|
console.log('🔵 [z-datepicker] attachFocusListeners called');
|
||||||
|
// Remove old listeners first
|
||||||
|
this.removeFocusListeners();
|
||||||
|
|
||||||
|
// Attach new listeners to all autocomplete inputs AND text fields
|
||||||
|
const selectors = [
|
||||||
|
'.v-autocomplete input',
|
||||||
|
'.v-text-field input',
|
||||||
|
'input[type="text"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
let totalListeners = 0;
|
||||||
|
selectors.forEach((selector) => {
|
||||||
|
const inputs = document.querySelectorAll(selector);
|
||||||
|
let selectorCount = 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, true); // Use capture phase
|
||||||
|
selectorCount++;
|
||||||
|
totalListeners++;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🔵 [z-datepicker] Found ${selectorCount} inputs for selector: ${selector}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`🔵 [z-datepicker] Total focus listeners attached: ${totalListeners}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputFocus(event) {
|
||||||
|
const target = event.target;
|
||||||
|
console.log('🔵 [z-datepicker] onInputFocus triggered on:', {
|
||||||
|
tagName: target.tagName,
|
||||||
|
type: target.type,
|
||||||
|
id: target.id,
|
||||||
|
className: target.className,
|
||||||
|
preventInputFocus: this.preventInputFocus,
|
||||||
|
savedScrollPosition: this.savedScrollPosition,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip if it's the datepicker's own input
|
||||||
|
if (
|
||||||
|
target.closest('.dp__input_wrap') ||
|
||||||
|
target.closest('.z-datepicker-wrapper')
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"🔵 [z-datepicker] Skipping - this is datepicker's own input",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If prevention flag is set, block the focus immediately
|
||||||
|
if (this.preventInputFocus) {
|
||||||
|
console.log('🔵 [z-datepicker] PREVENTING FOCUS on:', target);
|
||||||
|
const scrollBefore = document.querySelector(
|
||||||
|
'.v-dialog .modal-content',
|
||||||
|
)?.scrollTop;
|
||||||
|
console.log('🔵 [z-datepicker] Scroll position before:', scrollBefore);
|
||||||
|
|
||||||
|
// Prevent the focus - stop all propagation
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
// Blur the focused element immediately (no timeout)
|
||||||
|
target.blur();
|
||||||
|
|
||||||
|
// Restore scroll position if we have one saved
|
||||||
|
if (this.savedScrollPosition !== null) {
|
||||||
|
const modalContent = document.querySelector(
|
||||||
|
'.v-dialog .modal-content',
|
||||||
|
);
|
||||||
|
if (modalContent) {
|
||||||
|
modalContent.scrollTop = this.savedScrollPosition;
|
||||||
|
const scrollAfter = modalContent.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] Scroll position restored to:',
|
||||||
|
scrollAfter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'🔵 [z-datepicker] Focus allowed (preventInputFocus:',
|
||||||
|
this.preventInputFocus,
|
||||||
|
', savedScrollPosition:',
|
||||||
|
this.savedScrollPosition,
|
||||||
|
')',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
: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>
|
||||||
@@ -9,9 +9,20 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setFocusEvent() {
|
setFocusEvent() {
|
||||||
document.querySelectorAll('.v-autocomplete input').forEach((input) => {
|
const inputs = document.querySelectorAll('.v-autocomplete input');
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] setFocusEvent - Found',
|
||||||
|
inputs.length,
|
||||||
|
'autocomplete inputs',
|
||||||
|
);
|
||||||
|
inputs.forEach((input) => {
|
||||||
input.addEventListener('focus', this.onInputFocus);
|
input.addEventListener('focus', this.onInputFocus);
|
||||||
});
|
});
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Attached focus listeners to',
|
||||||
|
inputs.length,
|
||||||
|
'inputs',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onDateFocus($event) {
|
onDateFocus($event) {
|
||||||
@@ -21,31 +32,69 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onDateOpen($event) {
|
onDateOpen($event) {
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] onDateOpen called at:',
|
||||||
|
new Date().toISOString(),
|
||||||
|
'with event:',
|
||||||
|
$event,
|
||||||
|
);
|
||||||
// $event should be the element reference (or any truthy value to trigger saving)
|
// $event should be the element reference (or any truthy value to trigger saving)
|
||||||
if ($event) {
|
if ($event) {
|
||||||
// Find the scrollable modal container
|
// Find the scrollable modal container
|
||||||
const modalContent = document.querySelector('.v-dialog .modal-content');
|
const modalContent = document.querySelector('.v-dialog .modal-content');
|
||||||
if (modalContent) {
|
if (modalContent) {
|
||||||
this.savedScrollPosition = modalContent.scrollTop;
|
this.savedScrollPosition = modalContent.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Saved scroll position:',
|
||||||
|
this.savedScrollPosition,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('🟢 [VuetifyFixFocusMixin] Modal content not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preventInputFocus = true;
|
this.preventInputFocus = true;
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] preventInputFocus set to:',
|
||||||
|
this.preventInputFocus,
|
||||||
|
);
|
||||||
|
|
||||||
// Reset after datepicker is opened
|
// Reset after datepicker is opened
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.preventInputFocus = false;
|
this.preventInputFocus = false;
|
||||||
this.savedScrollPosition = null;
|
this.savedScrollPosition = null;
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Reset preventInputFocus and savedScrollPosition after 500ms',
|
||||||
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onInputFocus($event) {
|
onInputFocus($event) {
|
||||||
|
const target = $event.target;
|
||||||
|
console.log('🟢 [VuetifyFixFocusMixin] onInputFocus triggered on:', {
|
||||||
|
tagName: target.tagName,
|
||||||
|
type: target.type,
|
||||||
|
id: target.id,
|
||||||
|
className: target.className,
|
||||||
|
preventInputFocus: this.preventInputFocus,
|
||||||
|
savedScrollPosition: this.savedScrollPosition,
|
||||||
|
});
|
||||||
|
|
||||||
// If we have a saved scroll position and an unwanted focus happened
|
// If we have a saved scroll position and an unwanted focus happened
|
||||||
if (this.preventInputFocus && this.savedScrollPosition !== null) {
|
if (this.preventInputFocus && this.savedScrollPosition !== null) {
|
||||||
const target = $event.target;
|
const target = $event.target;
|
||||||
|
|
||||||
// Only intercept text input focus (autocomplete fields)
|
// Only intercept text input focus (autocomplete fields)
|
||||||
if (target.tagName === 'INPUT' && target.type === 'text') {
|
if (target.tagName === 'INPUT' && target.type === 'text') {
|
||||||
|
console.log('🟢 [VuetifyFixFocusMixin] PREVENTING FOCUS on:', target);
|
||||||
|
const scrollBefore = document.querySelector(
|
||||||
|
'.v-dialog .modal-content',
|
||||||
|
)?.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Scroll position before:',
|
||||||
|
scrollBefore,
|
||||||
|
);
|
||||||
|
|
||||||
// Blur the focused element
|
// Blur the focused element
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
target.blur();
|
target.blur();
|
||||||
@@ -55,9 +104,26 @@ export default {
|
|||||||
);
|
);
|
||||||
if (modalContent && this.savedScrollPosition !== null) {
|
if (modalContent && this.savedScrollPosition !== null) {
|
||||||
modalContent.scrollTop = this.savedScrollPosition;
|
modalContent.scrollTop = this.savedScrollPosition;
|
||||||
|
const scrollAfter = modalContent.scrollTop;
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Scroll position restored to:',
|
||||||
|
scrollAfter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Not a text input, skipping prevention',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
'🟢 [VuetifyFixFocusMixin] Focus allowed (preventInputFocus:',
|
||||||
|
this.preventInputFocus,
|
||||||
|
', savedScrollPosition:',
|
||||||
|
this.savedScrollPosition,
|
||||||
|
')',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,24 +57,22 @@
|
|||||||
|
|
||||||
<div class="splitted-inputs">
|
<div class="splitted-inputs">
|
||||||
<!-- TODO: Bialko validacie -->
|
<!-- TODO: Bialko validacie -->
|
||||||
<div
|
<div class="datepicker-wrapper left" ref="datepicker-wrapper">
|
||||||
@click="handleDatepickerClick"
|
<Z-datepicker
|
||||||
class="datepicker-wrapper left"
|
|
||||||
ref="datepicker-wrapper"
|
|
||||||
>
|
|
||||||
<Datepicker
|
|
||||||
v-model="mastertask.deadline"
|
v-model="mastertask.deadline"
|
||||||
ref="datepicker"
|
ref="datepicker"
|
||||||
|
label="Dátum"
|
||||||
:auto-apply="true"
|
:auto-apply="true"
|
||||||
:min-date="new Date()"
|
:min-date="new Date()"
|
||||||
class="modal-input datepicker-input"
|
class="modal-input"
|
||||||
format="dd.MM.yyyy"
|
format="dd.MM.yyyy"
|
||||||
locale="sk"
|
locale="sk"
|
||||||
select-text="Vybrať"
|
select-text="Vybrať"
|
||||||
cancel-text="Zrušiť"
|
cancel-text="Zrušiť"
|
||||||
placeholder="Dátum"
|
placeholder="Deadline"
|
||||||
:class="{ error: v$.mastertask.deadline.$errors?.length }"
|
:class="{ error: v$.mastertask.deadline.$errors?.length }"
|
||||||
:validations="v$.mastertask.deadline"
|
:validations="v$.mastertask.deadline"
|
||||||
|
@open="onDateOpen"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@
|
|||||||
class="modal-input"
|
class="modal-input"
|
||||||
variant="solo"
|
variant="solo"
|
||||||
type="number"
|
type="number"
|
||||||
:label="`${mastertask.duration} h`"
|
label="Naplánované hodiny"
|
||||||
:disabled="true"
|
:disabled="true"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
|
||||||
@@ -312,7 +310,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Datepicker from '@vuepic/vue-datepicker';
|
import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue';
|
||||||
import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue';
|
import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue';
|
||||||
import useVuelidate from '@vuelidate/core';
|
import useVuelidate from '@vuelidate/core';
|
||||||
import { required, minLength } from '@vuelidate/validators';
|
import { required, minLength } from '@vuelidate/validators';
|
||||||
@@ -344,7 +342,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
AMastertaskDescription,
|
AMastertaskDescription,
|
||||||
ATasksTable,
|
ATasksTable,
|
||||||
Datepicker,
|
ZDatepicker,
|
||||||
ZFilesUpload,
|
ZFilesUpload,
|
||||||
// ZAutocomplete,
|
// ZAutocomplete,
|
||||||
QuillEditor,
|
QuillEditor,
|
||||||
@@ -461,11 +459,29 @@ export default {
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleDatepickerClick() {
|
handleDatepickerClick() {
|
||||||
// Pass the datepicker wrapper element reference to the mixin
|
// Open the datepicker - it now opens automatically via click on wrapper
|
||||||
const scrollTarget = this.$refs['datepicker-wrapper'];
|
},
|
||||||
this.onDateOpen(scrollTarget);
|
|
||||||
// Open the datepicker
|
onDateOpen($event) {
|
||||||
this.$refs['datepicker'].openMenu();
|
// $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;
|
||||||
|
|
||||||
|
// Reattach focus event listeners
|
||||||
|
this.setFocusEvent();
|
||||||
|
|
||||||
|
// Reset after datepicker is opened
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preventInputFocus = false;
|
||||||
|
this.savedScrollPosition = null;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeBothModals() {
|
closeBothModals() {
|
||||||
@@ -527,11 +543,26 @@ export default {
|
|||||||
async showModal({ mastertask, editMode = false }) {
|
async showModal({ mastertask, editMode = false }) {
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
this.editMode = null;
|
this.editMode = null;
|
||||||
|
|
||||||
|
// CRITICAL: Prevent v-dialog autofocus BEFORE modal opens
|
||||||
|
this.preventInputFocus = true;
|
||||||
|
console.log(
|
||||||
|
'🟠 [z-master-task-modal] showModal - preventInputFocus set to true BEFORE modal opens',
|
||||||
|
);
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.mastertask = null;
|
this.mastertask = null;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.setFocusEvent();
|
this.setFocusEvent();
|
||||||
this.v$.$reset();
|
this.v$.$reset();
|
||||||
|
|
||||||
|
// Reset after modal is fully rendered and v-dialog autofocus has been blocked
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preventInputFocus = false;
|
||||||
|
console.log(
|
||||||
|
'🟠 [z-master-task-modal] showModal - preventInputFocus reset to false after 300ms',
|
||||||
|
);
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -787,8 +818,4 @@ export default {
|
|||||||
|
|
||||||
.datepicker-wrapper
|
.datepicker-wrapper
|
||||||
width: 100%
|
width: 100%
|
||||||
.datepicker-input
|
|
||||||
width: 100% !important
|
|
||||||
height: 100%
|
|
||||||
pointer-events: none
|
|
||||||
</style>
|
</style>
|
||||||
@@ -57,17 +57,15 @@
|
|||||||
|
|
||||||
<div class="splitted-inputs">
|
<div class="splitted-inputs">
|
||||||
<!-- TODO: Bialko validacie -->
|
<!-- TODO: Bialko validacie -->
|
||||||
<div
|
<div class="w-100 modal-input">
|
||||||
@click="openDatepicker('datepicker-01')"
|
<Z-datepicker
|
||||||
class="datepicker-wrapper"
|
|
||||||
>
|
|
||||||
<Datepicker
|
|
||||||
v-model="subtask.deadline"
|
v-model="subtask.deadline"
|
||||||
@open="onDateOpen"
|
@open="onDateOpen"
|
||||||
ref="datepicker-01"
|
ref="datepicker-01"
|
||||||
|
label="Deadline"
|
||||||
:auto-apply="true"
|
:auto-apply="true"
|
||||||
:min-date="new Date()"
|
:min-date="new Date()"
|
||||||
class="modal-input left datepicker-input"
|
class="modal-input"
|
||||||
format="dd.MM.yyyy"
|
format="dd.MM.yyyy"
|
||||||
locale="sk"
|
locale="sk"
|
||||||
select-text="Vybrať"
|
select-text="Vybrať"
|
||||||
@@ -348,7 +346,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Datepicker from '@vuepic/vue-datepicker';
|
import ZDatepicker from '@/plugins/tm@_components/z-datepicker.vue';
|
||||||
import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue';
|
import ZFilesUpload from '@/plugins/tm@_components/z-files-upload.vue';
|
||||||
import useVuelidate from '@vuelidate/core';
|
import useVuelidate from '@vuelidate/core';
|
||||||
import { required, requiredIf, optional } from '@vuelidate/validators';
|
import { required, requiredIf, optional } from '@vuelidate/validators';
|
||||||
@@ -375,7 +373,7 @@ export default {
|
|||||||
ZTaskReadonlyInfo,
|
ZTaskReadonlyInfo,
|
||||||
ATimeblock,
|
ATimeblock,
|
||||||
ZFilesUpload,
|
ZFilesUpload,
|
||||||
Datepicker,
|
ZDatepicker,
|
||||||
// ATaskTimeblocks,
|
// ATaskTimeblocks,
|
||||||
QuillEditor,
|
QuillEditor,
|
||||||
},
|
},
|
||||||
@@ -505,6 +503,46 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
// Override onDateOpen from VuetifyFixFocusMixin to add logging
|
||||||
|
onDateOpen($event) {
|
||||||
|
console.log(
|
||||||
|
'🟡 [z-task-modal] onDateOpen called at:',
|
||||||
|
new Date().toISOString(),
|
||||||
|
'with event:',
|
||||||
|
$event,
|
||||||
|
);
|
||||||
|
// Call the parent mixin's onDateOpen method
|
||||||
|
if (
|
||||||
|
VuetifyFixFocusMixin.methods &&
|
||||||
|
VuetifyFixFocusMixin.methods.onDateOpen
|
||||||
|
) {
|
||||||
|
VuetifyFixFocusMixin.methods.onDateOpen.call(this, $event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override onInputFocus from VuetifyFixFocusMixin to add logging
|
||||||
|
onInputFocus($event) {
|
||||||
|
const target = $event?.target;
|
||||||
|
console.log(
|
||||||
|
'🟡 [z-task-modal] onInputFocus called at:',
|
||||||
|
new Date().toISOString(),
|
||||||
|
'on:',
|
||||||
|
{
|
||||||
|
tagName: target?.tagName,
|
||||||
|
type: target?.type,
|
||||||
|
id: target?.id,
|
||||||
|
className: target?.className,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Call the parent mixin's onInputFocus method
|
||||||
|
if (
|
||||||
|
VuetifyFixFocusMixin.methods &&
|
||||||
|
VuetifyFixFocusMixin.methods.onInputFocus
|
||||||
|
) {
|
||||||
|
VuetifyFixFocusMixin.methods.onInputFocus.call(this, $event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
toggleEditMode() {
|
toggleEditMode() {
|
||||||
if (this.subtask.type == 'office365')
|
if (this.subtask.type == 'office365')
|
||||||
return this.$events.emit('show-unable-to-edit-office-task-modal', {
|
return this.$events.emit('show-unable-to-edit-office-task-modal', {
|
||||||
@@ -560,6 +598,13 @@ export default {
|
|||||||
async showModal({ subtask, editMode = false, mastertask = {} }) {
|
async showModal({ subtask, editMode = false, mastertask = {} }) {
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
this.subtask = null;
|
this.subtask = null;
|
||||||
|
|
||||||
|
// CRITICAL: Prevent v-dialog autofocus BEFORE modal opens
|
||||||
|
this.preventInputFocus = true;
|
||||||
|
console.log(
|
||||||
|
'🟡 [z-task-modal] showModal - preventInputFocus set to true BEFORE modal opens',
|
||||||
|
);
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.timeblocks = [
|
this.timeblocks = [
|
||||||
{
|
{
|
||||||
@@ -571,6 +616,14 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.v$.$reset();
|
this.v$.$reset();
|
||||||
this.setFocusEvent();
|
this.setFocusEvent();
|
||||||
|
|
||||||
|
// Reset after modal is fully rendered and v-dialog autofocus has been blocked
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preventInputFocus = false;
|
||||||
|
console.log(
|
||||||
|
'🟡 [z-task-modal] showModal - preventInputFocus reset to false after 300ms',
|
||||||
|
);
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user