forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
423618847b
334 changed files with 9307 additions and 6025 deletions
64
src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
Normal file
64
src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
|
||||
/**
|
||||
* This is a pile of hacks to get the events working on mobile too
|
||||
*/
|
||||
export let wd: number
|
||||
export let h: number
|
||||
export let type: "full" | "half"
|
||||
let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>()
|
||||
let element: HTMLElement
|
||||
|
||||
function send(signal: "start" | "end" | "move", ev: Event) {
|
||||
ev?.preventDefault()
|
||||
dispatch(signal)
|
||||
return true
|
||||
}
|
||||
|
||||
let lastElement: HTMLElement
|
||||
|
||||
function elementUnderTouch(ev: TouchEvent): HTMLElement {
|
||||
for (const k in ev.targetTouches) {
|
||||
const touch = ev.targetTouches[k]
|
||||
if (touch.clientX === undefined || touch.clientY === undefined) {
|
||||
continue
|
||||
}
|
||||
const el = document.elementFromPoint(touch.clientX, touch.clientY)
|
||||
if (!el) {
|
||||
continue
|
||||
}
|
||||
lastElement = <any>el
|
||||
return <any>el
|
||||
}
|
||||
return lastElement
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
element.addEventListener("mousedown", (ev) => send("start", ev))
|
||||
element.onmouseenter = (ev) => send("move", ev)
|
||||
element.onmouseup = (ev) => send("end", ev)
|
||||
|
||||
element.addEventListener("touchstart", ev => dispatch("start", ev))
|
||||
element.addEventListener("touchend", ev => {
|
||||
|
||||
const el = elementUnderTouch(ev)
|
||||
if (el?.onmouseup) {
|
||||
el?.onmouseup(<any>ev)
|
||||
}else{
|
||||
dispatch("clear")
|
||||
}
|
||||
|
||||
})
|
||||
element.addEventListener("touchmove", ev => {
|
||||
elementUnderTouch(ev)?.onmouseenter(<any>ev)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd}
|
||||
class:border-black={(h + 1) % 6 === 0}
|
||||
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
|
||||
/>
|
||||
238
src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
Normal file
238
src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<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,
|
||||
]
|
||||
|
||||
function range(n: number) {
|
||||
return Utils.TimesT(n, n => n)
|
||||
}
|
||||
|
||||
|
||||
function clearSelection() {
|
||||
const allCells = Array.from(document.getElementsByClassName("oh-timecell"))
|
||||
for (const timecell of allCells) {
|
||||
timecell.classList.remove("oh-timecell-selected")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
|
||||
for (let wd = weekdayStart; wd <= weekdayEnd; wd++) {
|
||||
for (let h = (hourStart); h < (hourEnd); h++) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
|
||||
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
|
||||
}
|
||||
setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd),
|
||||
hourA, hourB)
|
||||
}
|
||||
|
||||
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]
|
||||
for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
function moved(weekday: number, hour: number) {
|
||||
if (selectionStart) {
|
||||
clearSelection()
|
||||
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24
|
||||
const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24
|
||||
return `top: ${top}px; height: ${height}px; z-index: 20`
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<table class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
|
||||
<th style="width: 9%">
|
||||
<!-- Top-left cell -->
|
||||
<button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10">
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</th>
|
||||
{#each days as wd}
|
||||
<th style="width: 13%">
|
||||
<Tr cls="w-full" t={wd} />
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
<tr class="h-0">
|
||||
<!-- Virtual row to add the ranges to-->
|
||||
<td style="width: 9%" />
|
||||
{#each range(7) as wd}
|
||||
<td style="width: 13%; position: relative;">
|
||||
|
||||
<div class="h-0 pointer-events-none" style="z-index: 10">
|
||||
{#each $value.filter(oh => oh.weekday === wd) as range }
|
||||
<div class="absolute pointer-events-none px-1 md:px-2 w-full "
|
||||
style={rangeStyle(range, totalHeight)}
|
||||
>
|
||||
<div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between">
|
||||
<div class:hidden={range.endHour - range.startHour < 3}>
|
||||
{OpeningHours.hhmm(range.startHour, range.startMinutes)}
|
||||
</div>
|
||||
<button class="w-fit rounded-full p-1 self-center pointer-events-auto"
|
||||
on:click={() => {
|
||||
const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range))
|
||||
console.log("Cleaned", cleaned, value.data)
|
||||
value.set(cleaned)
|
||||
}}>
|
||||
<TrashIcon class="w-6 h-6" />
|
||||
</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}
|
||||
<tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour -->
|
||||
<td rowspan={ h < 23 ? 2: 1 }
|
||||
class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive"
|
||||
style={ h < 23 ? "top: 0.75rem" : "height:0; top: 0.75rem"}>
|
||||
{#if h < 23}
|
||||
{h + 1}:00
|
||||
{/if}
|
||||
</td>
|
||||
{#each range(7) as wd}
|
||||
<OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)}
|
||||
on:move={() => moved(wd, h)} on:clear={() => clearSelection()} />
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
<tr style="height: 0.75rem"> <!-- odd row, for the half hour -->
|
||||
{#if h === 23}
|
||||
<td/>
|
||||
{/if}
|
||||
{#each range(7) as wd}
|
||||
<OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)}
|
||||
on:move={() => moved(wd, h)} on:clear={() => clearSelection()} />
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
<style>
|
||||
|
||||
th {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,9 +4,39 @@
|
|||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput"
|
||||
import OpeningHoursInput from "../../OpeningHours/OpeningHoursState"
|
||||
import PublicHolidaySelector from "../../OpeningHours/PublicHolidaySelector.svelte"
|
||||
import OHTable from "./OpeningHours/OHTable.svelte"
|
||||
import OpeningHoursState from "../../OpeningHours/OpeningHoursState"
|
||||
import Popup from "../../Base/Popup.svelte"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
</script>
|
||||
export let args: string
|
||||
let prefix = ""
|
||||
let postfix = ""
|
||||
if (args) {
|
||||
try {
|
||||
|
||||
<ToSvelte construct={new OpeningHoursInput(value)} />
|
||||
const data = JSON.stringify(args)
|
||||
if (data["prefix"]) {
|
||||
prefix = data["prefix"]
|
||||
}
|
||||
if (data["postfix"]) {
|
||||
postfix = data["postfix"]
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not parse arguments")
|
||||
}
|
||||
}
|
||||
|
||||
const state = new OpeningHoursState(value)
|
||||
let expanded = new UIEventSource(false)
|
||||
</script>
|
||||
<Popup bodyPadding="p-0" shown={expanded}>
|
||||
<OHTable value={state.normalOhs} />
|
||||
<div class="absolute w-full pointer-events-none bottom-0 flex justify-end">
|
||||
<button on:click={() => expanded.set(false)} class="primary pointer-events-auto">Done</button>
|
||||
</div>
|
||||
</Popup>
|
||||
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
|
||||
<PublicHolidaySelector value={state.phSelectorValue} />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ValidatorType } from "./Validators"
|
||||
import InputHelpers from "./InputHelpers"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import ImageHelper from "./Helpers/ImageHelper.svelte"
|
||||
import TranslationInput from "./Helpers/TranslationInput.svelte"
|
||||
|
|
@ -19,7 +18,6 @@
|
|||
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
|
||||
import SlopeInput from "./Helpers/SlopeInput.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import WikidataInput from "./Helpers/WikidataInput.svelte"
|
||||
import WikidataInputHelper from "./WikidataInputHelper.svelte"
|
||||
|
||||
export let type: ValidatorType
|
||||
|
|
@ -48,7 +46,7 @@
|
|||
{:else if type === "simple_tag"}
|
||||
<SimpleTagInput {value} {args} on:submit />
|
||||
{:else if type === "opening_hours"}
|
||||
<OpeningHoursInput {value} />
|
||||
<OpeningHoursInput {value} {args} />
|
||||
{:else if type === "slope"}
|
||||
<SlopeInput {value} {feature} {state} />
|
||||
{:else if type === "wikidata"}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
}
|
||||
|
||||
function onKeyPress(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Enter" && (!validator.textArea || e.ctrlKey)) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
dispatch("submit")
|
||||
|
|
|
|||
|
|
@ -13,12 +13,10 @@ export default class UrlValidator extends Validator {
|
|||
"tripadvisor.co.uk",
|
||||
"tripadvisor.com.au",
|
||||
"katestravelexperience.eu",
|
||||
"hoteldetails.eu"
|
||||
"hoteldetails.eu",
|
||||
])
|
||||
|
||||
private static readonly discouragedWebsites = new Set<string>([
|
||||
"facebook.com"
|
||||
])
|
||||
private static readonly discouragedWebsites = new Set<string>(["facebook.com"])
|
||||
|
||||
constructor(name?: string, explanation?: string, forceHttps?: boolean) {
|
||||
super(
|
||||
|
|
@ -93,14 +91,10 @@ export default class UrlValidator extends Validator {
|
|||
* v.getFeedback("https://booking.com/some-hotel.html").textFor("en") // => Translations.t.validation.url.spamSite.Subs({host: "booking.com"}).textFor("en")
|
||||
*/
|
||||
getFeedback(s: string, getCountry?: () => string): Translation | undefined {
|
||||
if (
|
||||
!s.startsWith("http://") &&
|
||||
!s.startsWith("https://") &&
|
||||
!s.startsWith("http:")
|
||||
) {
|
||||
if (!s.startsWith("http://") && !s.startsWith("https://") && !s.startsWith("http:")) {
|
||||
s = "https://" + s
|
||||
}
|
||||
try{
|
||||
try {
|
||||
const url = new URL(s)
|
||||
let host = url.host.toLowerCase()
|
||||
if (host.startsWith("www.")) {
|
||||
|
|
@ -112,9 +106,7 @@ export default class UrlValidator extends Validator {
|
|||
if (UrlValidator.discouragedWebsites.has(host)) {
|
||||
return Translations.t.validation.url.aggregator.Subs({ host })
|
||||
}
|
||||
|
||||
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
const upstream = super.getFeedback(s, getCountry)
|
||||
|
|
@ -122,7 +114,6 @@ export default class UrlValidator extends Validator {
|
|||
return upstream
|
||||
}
|
||||
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +122,6 @@ export default class UrlValidator extends Validator {
|
|||
* v.isValid("https://booking.com/some-hotel.html") // => false
|
||||
*/
|
||||
isValid(str: string): boolean {
|
||||
|
||||
try {
|
||||
if (
|
||||
!str.startsWith("http://") &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue