fix(time-tracking) : keep calendar header sticky below page header
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="calendarEl" class="relative overflow-auto rounded-lg border border-neutral-200 bg-white" style="max-height: calc(100vh - 280px);">
|
<div ref="calendarEl" class="relative rounded-lg border border-neutral-200 bg-white">
|
||||||
<!-- Day headers -->
|
<!-- Day headers -->
|
||||||
<div class="sticky top-0 z-20 flex border-b border-neutral-200 bg-white">
|
<div
|
||||||
|
class="sticky z-20 flex border-b border-neutral-200 bg-white"
|
||||||
|
:style="{ top: `${stickyOffset}px` }"
|
||||||
|
>
|
||||||
<div class="w-16 shrink-0 border-r border-neutral-200" />
|
<div class="w-16 shrink-0 border-r border-neutral-200" />
|
||||||
<div
|
<div
|
||||||
v-for="day in days"
|
v-for="day in days"
|
||||||
@@ -131,6 +134,7 @@ const props = defineProps<{
|
|||||||
entries: TimeEntry[]
|
entries: TimeEntry[]
|
||||||
startDate: Date
|
startDate: Date
|
||||||
viewMode: 'week' | 'day'
|
viewMode: 'week' | 'day'
|
||||||
|
stickyOffset?: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -148,15 +152,28 @@ const dayLabels = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']
|
|||||||
const calendarEl = ref<HTMLElement | null>(null)
|
const calendarEl = ref<HTMLElement | null>(null)
|
||||||
const gridBodyEl = ref<HTMLElement | null>(null)
|
const gridBodyEl = ref<HTMLElement | null>(null)
|
||||||
const dayColumnEls = ref<HTMLElement[]>([])
|
const dayColumnEls = ref<HTMLElement[]>([])
|
||||||
|
const stickyOffset = computed(() => props.stickyOffset ?? 0)
|
||||||
|
|
||||||
|
function getScrollParent(): HTMLElement | null {
|
||||||
|
let el = calendarEl.value?.parentElement
|
||||||
|
while (el) {
|
||||||
|
if (el.scrollHeight > el.clientHeight && getComputedStyle(el).overflowY !== 'visible') return el
|
||||||
|
el = el.parentElement
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll to current hour on mount
|
// Scroll to current hour on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!calendarEl.value) return
|
if (!calendarEl.value) return
|
||||||
|
const scrollParent = getScrollParent()
|
||||||
|
if (!scrollParent) return
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const currentMinutes = now.getHours() * 60 + now.getMinutes()
|
const currentMinutes = now.getHours() * 60 + now.getMinutes()
|
||||||
const scrollTarget = (currentMinutes / 60) * hourHeight - calendarEl.value.clientHeight / 3
|
const calendarTop = calendarEl.value.offsetTop
|
||||||
calendarEl.value.scrollTop = Math.max(0, scrollTarget)
|
const scrollTarget = calendarTop + (currentMinutes / 60) * hourHeight - scrollParent.clientHeight / 3
|
||||||
|
scrollParent.scrollTop = Math.max(0, scrollTarget)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -470,12 +487,13 @@ function onDragMove(event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function autoScrollLoop() {
|
function autoScrollLoop() {
|
||||||
if (!autoScrollActive || !lastMouseEvent || !calendarEl.value || !dragState.value) {
|
const scrollParent = getScrollParent()
|
||||||
|
if (!autoScrollActive || !lastMouseEvent || !scrollParent || !dragState.value) {
|
||||||
autoScrollActive = false
|
autoScrollActive = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = calendarEl.value.getBoundingClientRect()
|
const rect = scrollParent.getBoundingClientRect()
|
||||||
const edgeSize = 60
|
const edgeSize = 60
|
||||||
const maxSpeed = 10
|
const maxSpeed = 10
|
||||||
|
|
||||||
@@ -484,10 +502,10 @@ function autoScrollLoop() {
|
|||||||
|
|
||||||
let scrolled = false
|
let scrolled = false
|
||||||
if (distFromTop < edgeSize && distFromTop > 0) {
|
if (distFromTop < edgeSize && distFromTop > 0) {
|
||||||
calendarEl.value.scrollTop -= maxSpeed * (1 - distFromTop / edgeSize)
|
scrollParent.scrollTop -= maxSpeed * (1 - distFromTop / edgeSize)
|
||||||
scrolled = true
|
scrolled = true
|
||||||
} else if (distFromBottom < edgeSize && distFromBottom > 0) {
|
} else if (distFromBottom < edgeSize && distFromBottom > 0) {
|
||||||
calendarEl.value.scrollTop += maxSpeed * (1 - distFromBottom / edgeSize)
|
scrollParent.scrollTop += maxSpeed * (1 - distFromBottom / edgeSize)
|
||||||
scrolled = true
|
scrolled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,9 +98,12 @@
|
|||||||
|
|
||||||
<div class="h-full flex-1 overflow-hidden flex flex-col">
|
<div class="h-full flex-1 overflow-hidden flex flex-col">
|
||||||
<AppTopNav :user="auth.user" />
|
<AppTopNav :user="auth.user" />
|
||||||
<main class="flex-1 overflow-y-auto px-16 py-24">
|
<div class="relative flex-1 overflow-hidden bg-white">
|
||||||
<slot/>
|
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 z-50 h-12 bg-white" />
|
||||||
</main>
|
<main class="h-full overflow-y-auto px-16 pt-12 pb-24">
|
||||||
|
<slot/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div ref="pageHeaderEl" class="sticky top-0 z-40 bg-white pb-4">
|
||||||
<h1 class="text-2xl font-bold text-primary-500">Suivi des temps</h1>
|
<div class="flex items-center justify-between">
|
||||||
<button
|
<h1 class="text-2xl font-bold text-primary-500">Suivi des temps</h1>
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition"
|
<button
|
||||||
@click="openCreateDrawer()"
|
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition"
|
||||||
>
|
@click="openCreateDrawer()"
|
||||||
+ Ajouter une Activité
|
>
|
||||||
</button>
|
+ Ajouter une Activité
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative z-30 mt-4 flex items-center gap-4">
|
<div class="relative z-30 mt-4 flex items-center gap-4">
|
||||||
<h2 class="text-lg font-bold text-orange-500">
|
<h2 class="text-lg font-bold text-orange-500">
|
||||||
{{ currentMonthLabel }}
|
{{ currentMonthLabel }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
text-field="text-sm"
|
text-field="text-sm"
|
||||||
text-value="text-sm"
|
text-value="text-sm"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -76,6 +78,7 @@
|
|||||||
:entries="filteredEntries"
|
:entries="filteredEntries"
|
||||||
:start-date="startDate"
|
:start-date="startDate"
|
||||||
:view-mode="viewMode"
|
:view-mode="viewMode"
|
||||||
|
:sticky-offset="pageHeaderHeight"
|
||||||
@edit-entry="openEditDrawer"
|
@edit-entry="openEditDrawer"
|
||||||
@create-entry="openCreateDrawer"
|
@create-entry="openCreateDrawer"
|
||||||
@move-entry="onMoveEntry"
|
@move-entry="onMoveEntry"
|
||||||
@@ -136,6 +139,8 @@ const drawerOpen = ref(false)
|
|||||||
const editingEntry = ref<TimeEntry | null>(null)
|
const editingEntry = ref<TimeEntry | null>(null)
|
||||||
const prefillStartedAt = ref<string | null>(null)
|
const prefillStartedAt = ref<string | null>(null)
|
||||||
const clipboard = ref<TimeEntry | null>(null)
|
const clipboard = ref<TimeEntry | null>(null)
|
||||||
|
const pageHeaderEl = ref<HTMLElement | null>(null)
|
||||||
|
const pageHeaderHeight = ref(0)
|
||||||
|
|
||||||
const contextMenu = reactive({
|
const contextMenu = reactive({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -163,6 +168,12 @@ const typeOptions = computed(() =>
|
|||||||
types.value.map(t => ({ label: t.label, value: t.id }))
|
types.value.map(t => ({ label: t.label, value: t.id }))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let pageHeaderResizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
function updatePageHeaderHeight() {
|
||||||
|
pageHeaderHeight.value = pageHeaderEl.value?.offsetHeight ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
const filteredEntries = computed(() => {
|
const filteredEntries = computed(() => {
|
||||||
let result = entries.value
|
let result = entries.value
|
||||||
if (selectedProjectId.value) {
|
if (selectedProjectId.value) {
|
||||||
@@ -263,6 +274,24 @@ async function onPaste() {
|
|||||||
await loadEntries()
|
await loadEntries()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updatePageHeaderHeight()
|
||||||
|
|
||||||
|
if (!pageHeaderEl.value || typeof ResizeObserver === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pageHeaderResizeObserver = new ResizeObserver(() => {
|
||||||
|
updatePageHeaderHeight()
|
||||||
|
})
|
||||||
|
pageHeaderResizeObserver.observe(pageHeaderEl.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
pageHeaderResizeObserver?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
async function onDelete(entry: TimeEntry) {
|
async function onDelete(entry: TimeEntry) {
|
||||||
await timeEntryService.remove(entry.id)
|
await timeEntryService.remove(entry.id)
|
||||||
await loadEntries()
|
await loadEntries()
|
||||||
|
|||||||
Reference in New Issue
Block a user