2024-09-05 17:14:47 +02:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { UIEventSource } from "../../../../Logic/UIEventSource"
|
|
|
|
|
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
|
|
|
|
|
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"
|
|
|
|
|
import { Translation } from "../../../i18n/Translation"
|
|
|
|
|
import Translations from "../../../i18n/Translations"
|
|
|
|
|
import Tr from "../../../Base/Tr.svelte"
|
|
|
|
|
import { Utils } from "../../../../Utils"
|
|
|
|
|
import { onMount } from "svelte"
|
|
|
|
|
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
|
|
|
|
import OHCell from "./OHCell.svelte"
|
|
|
|
|
|
|
|
|
|
export let value: UIEventSource<OpeningHour[]>
|
|
|
|
|
|
|
|
|
|
const wd = Translations.t.general.weekdays.abbreviations
|
|
|
|
|
const days: Translation[] = [
|
|
|
|
|
wd.monday,
|
|
|
|
|
wd.tuesday,
|
|
|
|
|
wd.wednesday,
|
|
|
|
|
wd.thursday,
|
|
|
|
|
wd.friday,
|
|
|
|
|
wd.saturday,
|
|
|
|
|
wd.sunday,
|
|
|
|
|
]
|
|
|
|
|
|
2024-09-19 18:01:17 +02:00
|
|
|
let element: HTMLTableElement
|
|
|
|
|
|
2024-09-05 17:14:47 +02:00
|
|
|
function range(n: number) {
|
2024-10-19 14:44:55 +02:00
|
|
|
return Utils.TimesT(n, (n) => n)
|
2024-09-05 17:14:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearSelection() {
|
|
|
|
|
const allCells = Array.from(document.getElementsByClassName("oh-timecell"))
|
|
|
|
|
for (const timecell of allCells) {
|
|
|
|
|
timecell.classList.remove("oh-timecell-selected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
function setSelectionNormalized(
|
|
|
|
|
weekdayStart: number,
|
|
|
|
|
weekdayEnd: number,
|
|
|
|
|
hourStart: number,
|
|
|
|
|
hourEnd: number
|
|
|
|
|
) {
|
2024-09-05 17:14:47 +02:00
|
|
|
for (let wd = weekdayStart; wd <= weekdayEnd; wd++) {
|
2024-10-19 14:44:55 +02:00
|
|
|
for (let h = hourStart; h < hourEnd; h++) {
|
2024-09-05 17:14:47 +02:00
|
|
|
h = Math.floor(h)
|
|
|
|
|
if (h >= hourStart && h < hourEnd) {
|
|
|
|
|
const elFull = document.getElementById("oh-full-" + h + "-" + wd)
|
|
|
|
|
elFull?.classList?.add("oh-timecell-selected")
|
|
|
|
|
}
|
|
|
|
|
if (h + 0.5 < hourEnd) {
|
|
|
|
|
const elHalf = document.getElementById("oh-half-" + h + "-" + wd)
|
|
|
|
|
elHalf?.classList?.add("oh-timecell-selected")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
function setSelection(
|
|
|
|
|
weekdayStart: number,
|
|
|
|
|
weekdayEnd: number,
|
|
|
|
|
hourStart: number,
|
|
|
|
|
hourEnd: number
|
|
|
|
|
) {
|
2024-09-05 17:14:47 +02:00
|
|
|
let hourA = hourStart
|
|
|
|
|
let hourB = hourEnd
|
|
|
|
|
if (hourA > hourB) {
|
|
|
|
|
hourA = hourEnd - 0.5
|
|
|
|
|
hourB = hourStart + 0.5
|
|
|
|
|
}
|
|
|
|
|
if (hourA == hourB) {
|
|
|
|
|
hourA -= 0.5
|
|
|
|
|
hourB += 0.5
|
|
|
|
|
}
|
2024-10-19 14:44:55 +02:00
|
|
|
setSelectionNormalized(
|
|
|
|
|
Math.min(weekdayStart, weekdayEnd),
|
|
|
|
|
Math.max(weekdayStart, weekdayEnd),
|
|
|
|
|
hourA,
|
|
|
|
|
hourB
|
|
|
|
|
)
|
2024-09-05 17:14:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let selectionStart: [number, number] = undefined
|
|
|
|
|
|
|
|
|
|
function startSelection(weekday: number, hour: number) {
|
|
|
|
|
selectionStart = [weekday, hour]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function endSelection(weekday: number, hour: number) {
|
|
|
|
|
if (!selectionStart) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
|
|
|
|
|
|
|
|
|
|
hour += 0.5
|
|
|
|
|
let start = Math.min(selectionStart[1], hour)
|
|
|
|
|
let end = Math.max(selectionStart[1], hour)
|
|
|
|
|
if (selectionStart[1] > hour) {
|
|
|
|
|
end += 0.5
|
|
|
|
|
start -= 0.5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (end === start) {
|
|
|
|
|
end += 0.5
|
|
|
|
|
start -= 0.5
|
|
|
|
|
}
|
|
|
|
|
let startMinutes = Math.round((start * 60) % 60)
|
|
|
|
|
let endMinutes = Math.round((end * 60) % 60)
|
|
|
|
|
let newOhs = [...value.data]
|
2024-10-19 14:44:55 +02:00
|
|
|
for (
|
|
|
|
|
let wd = Math.min(selectionStart[0], weekday);
|
|
|
|
|
wd <= Math.max(selectionStart[0], weekday);
|
|
|
|
|
wd++
|
|
|
|
|
) {
|
2024-09-05 17:14:47 +02:00
|
|
|
const oh: OpeningHour = {
|
|
|
|
|
startHour: Math.floor(start),
|
|
|
|
|
endHour: Math.floor(end),
|
|
|
|
|
startMinutes,
|
|
|
|
|
endMinutes,
|
|
|
|
|
weekday: wd,
|
|
|
|
|
}
|
|
|
|
|
newOhs.push(oh)
|
|
|
|
|
}
|
|
|
|
|
value.set(OpeningHours.MergeTimes(newOhs))
|
|
|
|
|
selectionStart = undefined
|
|
|
|
|
clearSelection()
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-19 18:01:17 +02:00
|
|
|
let lasttouched: [number, number] = undefined
|
|
|
|
|
|
2024-09-05 17:14:47 +02:00
|
|
|
function moved(weekday: number, hour: number) {
|
2024-09-19 18:01:17 +02:00
|
|
|
lasttouched = [weekday, hour]
|
2024-09-05 17:14:47 +02:00
|
|
|
if (selectionStart) {
|
|
|
|
|
clearSelection()
|
|
|
|
|
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
|
|
|
|
|
}
|
2024-10-19 14:44:55 +02:00
|
|
|
const allRows = Array.from(element.getElementsByTagName("tr"))
|
2024-09-19 18:01:17 +02:00
|
|
|
for (const r of allRows) {
|
|
|
|
|
r.classList.remove("hover")
|
|
|
|
|
r.classList.remove("hovernext")
|
|
|
|
|
}
|
|
|
|
|
const selectedRow = allRows[hour * 2 + 2]
|
|
|
|
|
selectedRow?.classList?.add("hover")
|
|
|
|
|
const selectedNextRow = allRows[hour * 2 + 3]
|
|
|
|
|
selectedNextRow?.classList?.add("hovernext")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mouseLeft() {
|
|
|
|
|
endSelection(...lasttouched)
|
2024-09-05 17:14:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let totalHeight = 0
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
const mondayMorning = document.getElementById("oh-full-" + 0 + "-" + 0)
|
|
|
|
|
const sundayEvening = document.getElementById("oh-half-" + 23 + "-" + 6)
|
|
|
|
|
const top = mondayMorning.getBoundingClientRect().top
|
|
|
|
|
const bottom = sundayEvening.getBoundingClientRect().bottom
|
|
|
|
|
totalHeight = bottom - top
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines 'top' and 'height-attributes, returns a CSS-string'
|
|
|
|
|
* @param oh
|
|
|
|
|
*/
|
|
|
|
|
function rangeStyle(oh: OpeningHour, totalHeight: number): string {
|
2024-10-19 14:44:55 +02:00
|
|
|
const top = ((oh.startHour + oh.startMinutes / 60) * totalHeight) / 24
|
|
|
|
|
const height =
|
|
|
|
|
((oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight) / 24
|
2024-09-05 17:14:47 +02:00
|
|
|
return `top: ${top}px; height: ${height}px; z-index: 20`
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
2024-09-19 18:01:17 +02:00
|
|
|
<table
|
|
|
|
|
bind:this={element}
|
2024-10-19 14:44:55 +02:00
|
|
|
class="oh-table no-weblate w-full"
|
|
|
|
|
cellspacing="0"
|
|
|
|
|
cellpadding="0"
|
|
|
|
|
class:hasselection={selectionStart !== undefined}
|
|
|
|
|
class:hasnoselection={selectionStart === undefined}
|
|
|
|
|
on:mouseleave={mouseLeft}
|
|
|
|
|
>
|
2024-09-05 17:14:47 +02:00
|
|
|
<tr>
|
2024-09-19 18:01:17 +02:00
|
|
|
<!-- Header row -->
|
2024-09-05 17:14:47 +02:00
|
|
|
<th style="width: 9%">
|
|
|
|
|
<!-- Top-left cell -->
|
2024-09-19 18:01:17 +02:00
|
|
|
<slot name="top-left">
|
2024-10-19 14:44:55 +02:00
|
|
|
<button
|
|
|
|
|
class="absolute top-0 left-0 rounded-full p-1"
|
|
|
|
|
on:click={() => value.set([])}
|
|
|
|
|
style="z-index: 10"
|
|
|
|
|
>
|
|
|
|
|
<TrashIcon class="h-5 w-5" />
|
2024-09-19 18:01:17 +02:00
|
|
|
</button>
|
|
|
|
|
</slot>
|
2024-09-05 17:14:47 +02:00
|
|
|
</th>
|
|
|
|
|
{#each days as wd}
|
|
|
|
|
<th style="width: 13%">
|
|
|
|
|
<Tr cls="w-full" t={wd} />
|
|
|
|
|
</th>
|
|
|
|
|
{/each}
|
|
|
|
|
</tr>
|
|
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
<tr class="nobold h-0">
|
2024-09-05 17:14:47 +02:00
|
|
|
<!-- Virtual row to add the ranges to-->
|
|
|
|
|
<td style="width: 9%" />
|
|
|
|
|
{#each range(7) as wd}
|
|
|
|
|
<td style="width: 13%; position: relative;">
|
2024-10-19 14:44:55 +02:00
|
|
|
<div class="pointer-events-none h-0" style="z-index: 10">
|
|
|
|
|
{#each $value
|
|
|
|
|
.filter((oh) => oh.weekday === wd)
|
|
|
|
|
.map((oh) => OpeningHours.rangeAs24Hr(oh)) as range}
|
|
|
|
|
<div
|
|
|
|
|
class="pointer-events-none absolute w-full px-1 md:px-2"
|
|
|
|
|
style={rangeStyle(range, totalHeight)}
|
2024-09-05 17:14:47 +02:00
|
|
|
>
|
2024-10-19 14:44:55 +02:00
|
|
|
<div
|
|
|
|
|
class="border-interactive low-interaction flex h-full flex-col justify-between rounded-xl"
|
|
|
|
|
>
|
2024-09-05 17:14:47 +02:00
|
|
|
<div class:hidden={range.endHour - range.startHour < 3}>
|
|
|
|
|
{OpeningHours.hhmm(range.startHour, range.startMinutes)}
|
|
|
|
|
</div>
|
2024-10-19 14:44:55 +02:00
|
|
|
<button
|
|
|
|
|
class="pointer-events-auto w-fit self-center rounded-full p-1"
|
|
|
|
|
on:click={() => {
|
|
|
|
|
const cleaned = value.data.filter((v) => !OpeningHours.isSame(v, range))
|
|
|
|
|
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
|
|
|
|
|
value.set(cleaned)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<TrashIcon class="h-6 w-6" />
|
2024-09-05 17:14:47 +02:00
|
|
|
</button>
|
|
|
|
|
<div class:hidden={range.endHour - range.startHour < 3}>
|
|
|
|
|
{OpeningHours.hhmm(range.endHour, range.endMinutes)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
{/each}
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
{#each range(24) as h}
|
2024-10-19 14:44:55 +02:00
|
|
|
<tr style="height: 0.75rem; width: 9%">
|
|
|
|
|
<!-- even row, for the hour -->
|
|
|
|
|
<td
|
|
|
|
|
rowspan={h > 0 ? 2 : 1}
|
|
|
|
|
class="oh-left-col oh-timecell-full border-box interactive relative text-sm sm:text-base"
|
|
|
|
|
style={h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}
|
|
|
|
|
>
|
2024-09-19 18:01:17 +02:00
|
|
|
{#if h > 0}
|
|
|
|
|
<span class="hour-header w-full">
|
2024-10-19 14:44:55 +02:00
|
|
|
{h}:00
|
2024-09-19 18:01:17 +02:00
|
|
|
</span>
|
2024-09-05 17:14:47 +02:00
|
|
|
{/if}
|
|
|
|
|
</td>
|
|
|
|
|
{#each range(7) as wd}
|
2024-10-19 14:44:55 +02:00
|
|
|
<OHCell
|
|
|
|
|
type="full"
|
|
|
|
|
{h}
|
|
|
|
|
{wd}
|
|
|
|
|
on:start={() => startSelection(wd, h)}
|
|
|
|
|
on:end={() => endSelection(wd, h)}
|
|
|
|
|
on:move={() => moved(wd, h)}
|
|
|
|
|
on:clear={() => clearSelection()}
|
|
|
|
|
/>
|
2024-09-05 17:14:47 +02:00
|
|
|
{/each}
|
|
|
|
|
</tr>
|
|
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
<tr style="height: calc( 0.75rem - 1px) ">
|
|
|
|
|
<!-- odd row, for the half hour -->
|
2024-09-19 18:01:17 +02:00
|
|
|
{#if h === 0}
|
2024-10-19 14:44:55 +02:00
|
|
|
<td />
|
|
|
|
|
<!-- extra cell to compensate for irregular header-->
|
2024-09-19 18:01:17 +02:00
|
|
|
{/if}
|
2024-09-05 17:14:47 +02:00
|
|
|
{#each range(7) as wd}
|
2024-10-19 14:44:55 +02:00
|
|
|
<OHCell
|
|
|
|
|
type="half"
|
|
|
|
|
{h}
|
|
|
|
|
{wd}
|
|
|
|
|
on:start={() => startSelection(wd, h + 0.5)}
|
|
|
|
|
on:end={() => endSelection(wd, h + 0.5)}
|
|
|
|
|
on:move={() => moved(wd, h + 0.5)}
|
|
|
|
|
on:clear={() => clearSelection()}
|
|
|
|
|
/>
|
2024-09-05 17:14:47 +02:00
|
|
|
{/each}
|
|
|
|
|
</tr>
|
|
|
|
|
{/each}
|
|
|
|
|
</table>
|
2024-09-19 18:01:17 +02:00
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
<style>
|
|
|
|
|
th {
|
|
|
|
|
top: 0;
|
|
|
|
|
position: sticky;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
2024-09-19 18:01:17 +02:00
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
.hasselection tr:hover .hour-header,
|
|
|
|
|
.hasselection tr.hover .hour-header {
|
|
|
|
|
border-bottom: 2px solid black;
|
|
|
|
|
}
|
2024-09-19 18:01:17 +02:00
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
.hasselection tr:hover + tr {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
2024-09-19 18:01:17 +02:00
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
.hasselection tr.hovernext {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
2024-09-19 18:01:17 +02:00
|
|
|
|
2024-10-19 14:44:55 +02:00
|
|
|
.hasnoselection tr:hover,
|
|
|
|
|
.hasnoselection tr.hover {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
2024-09-05 17:14:47 +02:00
|
|
|
</style>
|