forked from MapComplete/MapComplete
A11y: Add aria label to opening hours table, see #1181
This commit is contained in:
parent
af4d9bb2bf
commit
b7175384f9
6 changed files with 296 additions and 60 deletions
|
@ -1,6 +1,8 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import opening_hours from "opening_hours"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Translation, TypedTranslation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export interface OpeningHour {
|
||||
weekday: number // 0 is monday, 1 is tuesday, ...
|
||||
|
@ -10,6 +12,14 @@ export interface OpeningHour {
|
|||
endMinutes: number
|
||||
}
|
||||
|
||||
export interface OpeningRange {
|
||||
isOpen: boolean
|
||||
isSpecial: boolean
|
||||
comment: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Various utilities manipulating opening hours
|
||||
*/
|
||||
|
@ -495,6 +505,7 @@ This list will be sorted
|
|||
|
||||
return [changeHours, changeHourText]
|
||||
}
|
||||
|
||||
public static CreateOhObjectStore(
|
||||
tags: Store<Record<string, string>>,
|
||||
key: string = "opening_hours",
|
||||
|
@ -533,6 +544,7 @@ This list will be sorted
|
|||
[country]
|
||||
)
|
||||
}
|
||||
|
||||
public static CreateOhObject(
|
||||
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
|
||||
textToParse: string,
|
||||
|
@ -553,21 +565,156 @@ This list will be sorted
|
|||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Calculates when the business is opened (or on holiday) between two dates.
|
||||
Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
|
||||
*/
|
||||
public static GetRanges(
|
||||
oh: any,
|
||||
from: Date,
|
||||
to: Date
|
||||
): {
|
||||
isOpen: boolean
|
||||
isSpecial: boolean
|
||||
comment: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
}[][] {
|
||||
/**
|
||||
* let ranges = <any> [
|
||||
* [
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-11T09:00:00.000Z"),
|
||||
* "endDate": new Date("2023-12-11T12:30:00.000Z")
|
||||
* },
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-11T13:30:00.000Z"),
|
||||
* "endDate": new Date("2023-12-11T18:00:00.000Z")
|
||||
* }
|
||||
* ],
|
||||
* [
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-12T09:00:00.000Z"),
|
||||
* "endDate": new Date("2023-12-12T12:30:00.000Z")
|
||||
* },
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-12T13:30:00.000Z"),
|
||||
* "endDate": new Date("2023-12-12T18:00:00.000Z")
|
||||
* }
|
||||
* ],
|
||||
* [
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-13T09:00:00.000Z"),
|
||||
* "endDate": new Date("2023-12-13T12:30:00.000Z")
|
||||
* },
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-13T13:30:00.000Z"),
|
||||
* "endDate": new Date("2023-12-13T18:00:00.000Z")
|
||||
* }
|
||||
* ],
|
||||
* [
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-14T09:00:00.000Z"),
|
||||
* "endDate": new Date("2023-12-14T12:30:00.000Z")
|
||||
* },
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-14T13:30:00.000Z"),
|
||||
* "endDate": new Date("2023-12-14T18:00:00.000Z")
|
||||
* }
|
||||
* ],
|
||||
* [
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-15T09:00:00.000Z"),
|
||||
* "endDate": new Date("2023-12-15T12:30:00.000Z")
|
||||
* },
|
||||
* {
|
||||
* "isSpecial": false,
|
||||
* "isOpen": true,
|
||||
* "startDate": new Date("2023-12-15T13:30:00.000Z"),
|
||||
* "endDate": new Date("2023-12-15T18:00:00.000Z")
|
||||
* }
|
||||
* ],
|
||||
* [],
|
||||
* []
|
||||
* ]
|
||||
* OH.weekdaysIdentical(ranges, 0, 1) // => true
|
||||
* OH.weekdaysIdentical(ranges, 0, 4) // => true
|
||||
* OH.weekdaysIdentical(ranges, 4, 5) // => false
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week
|
||||
*/
|
||||
public static createRangesForApplicableWeek(oh: opening_hours): {
|
||||
ranges: OpeningRange[][]
|
||||
startingMonday: Date
|
||||
} {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const lastMonday = OH.getMondayBefore(today)
|
||||
const nextSunday = new Date(lastMonday)
|
||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||
|
||||
if (!oh.getState() && !oh.getUnknown()) {
|
||||
// POI is currently closed
|
||||
const nextChange: Date = oh.getNextChange()
|
||||
if (
|
||||
// Shop isn't gonna open anymore in this timerange
|
||||
nextSunday < nextChange &&
|
||||
// And we are already in the weekend to show next week
|
||||
(today.getDay() == 0 || today.getDay() == 6)
|
||||
) {
|
||||
// We move the range to next week!
|
||||
lastMonday.setDate(lastMonday.getDate() + 7)
|
||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||
}
|
||||
}
|
||||
|
||||
/* We calculate the ranges when it is opened! */
|
||||
return { startingMonday: lastMonday, ranges: OH.GetRanges(oh, lastMonday, nextSunday) }
|
||||
}
|
||||
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
|
||||
console.log("Checking identical:", openingRanges)
|
||||
const monday = openingRanges[startday]
|
||||
for (let i = startday + 1; i <= endday; i++) {
|
||||
let weekday = openingRanges[i]
|
||||
if (weekday.length !== monday.length) {
|
||||
console.log("Mismatched length")
|
||||
return false
|
||||
}
|
||||
for (let j = 0; j < weekday.length; j++) {
|
||||
const openingRange = weekday[j]
|
||||
const mondayRange = monday[j]
|
||||
if (
|
||||
openingRange.isOpen !== mondayRange.isOpen &&
|
||||
openingRange.isSpecial !== mondayRange.isSpecial &&
|
||||
openingRange.comment !== mondayRange.comment
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
openingRange.startDate.toTimeString() !== mondayRange.startDate.toTimeString()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (openingRange.endDate.toTimeString() !== mondayRange.endDate.toTimeString()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates when the business is opened (or on holiday) between two dates.
|
||||
* Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
|
||||
*/
|
||||
public static GetRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
|
||||
const values = [[], [], [], [], [], [], []]
|
||||
|
||||
const start = new Date(from)
|
||||
|
@ -607,6 +754,13 @@ Returns a matrix of ranges, where [0] is a list of ranges when it is opened on m
|
|||
return values
|
||||
}
|
||||
|
||||
public static getMondayBefore(d) {
|
||||
d = new Date(d)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
|
||||
return new Date(d.setDate(diff))
|
||||
}
|
||||
|
||||
/**
|
||||
* OH.parseHHMM("12:30") // => {hours: 12, minutes: 30}
|
||||
*/
|
||||
|
@ -741,11 +895,104 @@ Returns a matrix of ranges, where [0] is a list of ranges when it is opened on m
|
|||
}
|
||||
return ohs
|
||||
}
|
||||
}
|
||||
|
||||
public static getMondayBefore(d) {
|
||||
d = new Date(d)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
|
||||
return new Date(d.setDate(diff))
|
||||
export class ToTextualDescription {
|
||||
public static createTextualDescriptionFor(
|
||||
oh: opening_hours,
|
||||
ranges: OpeningRange[][]
|
||||
): Translation {
|
||||
const t = Translations.t.general.opening_hours
|
||||
|
||||
if (!ranges?.some((r) => r.length > 0)) {
|
||||
// <!-- No changes to the opening hours in the next week; probably open 24/7, permanently closed, opening far in the future or unkown -->
|
||||
if (oh.getNextChange() === undefined) {
|
||||
// <!-- Permenantly in the same state -->
|
||||
if (oh.getComment() !== undefined) {
|
||||
return new Translation({ "*": oh.getComment() })
|
||||
}
|
||||
|
||||
if (oh.getUnknown()) {
|
||||
return t.unknown
|
||||
}
|
||||
if (oh.getState()) {
|
||||
return t.open_24_7
|
||||
} else {
|
||||
return t.closed_permanently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Opened at a more-or-less normal, weekly rhythm
|
||||
if (OH.weekdaysIdentical(ranges, 0, 6)) {
|
||||
return t.all_days_from.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
|
||||
}
|
||||
|
||||
if (OH.weekdaysIdentical(ranges, 0, 4) && OH.weekdaysIdentical(ranges, 5, 6)) {
|
||||
let result = []
|
||||
if (ranges[0].length > 0) {
|
||||
result.push(
|
||||
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
|
||||
)
|
||||
}
|
||||
if (ranges[6].length > 0) {
|
||||
result.push(
|
||||
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) })
|
||||
)
|
||||
}
|
||||
return ToTextualDescription.chain(result)
|
||||
}
|
||||
|
||||
const result: Translation[] = []
|
||||
const weekdays = [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
for (let i = 0; i < weekdays.length; i++) {
|
||||
const day = weekdays[i]
|
||||
console.log(day, "-->", ranges[i])
|
||||
if (ranges[i]?.length > 0) {
|
||||
result.push(
|
||||
t[day].Subs({ ranges: ToTextualDescription.createRangesFor(ranges[i]) })
|
||||
)
|
||||
}
|
||||
}
|
||||
return ToTextualDescription.chain(result)
|
||||
}
|
||||
|
||||
private static chain(trs: Translation[]): Translation {
|
||||
let chainer = new TypedTranslation<{ a; b }>({ "*": "{a}. {b}" })
|
||||
let tr = trs[0]
|
||||
for (let i = 1; i < trs.length; i++) {
|
||||
tr = chainer.Subs({ a: tr, b: trs[i] })
|
||||
}
|
||||
return tr
|
||||
}
|
||||
private static timeString(date: Date) {
|
||||
return OH.hhmm(date.getHours(), date.getMinutes())
|
||||
}
|
||||
|
||||
private static createRangeFor(range: OpeningRange): Translation {
|
||||
console.log(">>>", range)
|
||||
return Translations.t.general.opening_hours.ranges.Subs({
|
||||
starttime: ToTextualDescription.timeString(range.startDate),
|
||||
endtime: ToTextualDescription.timeString(range.endDate),
|
||||
})
|
||||
}
|
||||
|
||||
private static createRangesFor(ranges: OpeningRange[]): Translation {
|
||||
let tr = ToTextualDescription.createRangeFor(ranges[0])
|
||||
for (let i = 1; i < ranges.length; i++) {
|
||||
tr = Translations.t.general.opening_hours.rangescombined.Subs({
|
||||
range0: tr,
|
||||
range1: ToTextualDescription.createRangeFor(ranges[i]),
|
||||
})
|
||||
}
|
||||
return tr
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { OH } from "./OpeningHours"
|
||||
import { OH, OpeningRange, ToTextualDescription } from "./OpeningHours"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
|
@ -10,6 +10,7 @@ import Table from "../Base/Table"
|
|||
import { Translation } from "../i18n/Translation"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Loading from "../Base/Loading"
|
||||
import opening_hours from "opening_hours"
|
||||
|
||||
export default class OpeningHoursVisualization extends Toggle {
|
||||
private static readonly weekdays: Translation[] = [
|
||||
|
@ -41,7 +42,21 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
if (opening_hours_obj === "error") {
|
||||
return Translations.t.general.opening_hours.error_loading
|
||||
}
|
||||
return OpeningHoursVisualization.CreateFullVisualisation(opening_hours_obj)
|
||||
|
||||
const applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj)
|
||||
const textual = ToTextualDescription.createTextualDescriptionFor(
|
||||
opening_hours_obj,
|
||||
applicableWeek.ranges
|
||||
)
|
||||
const vis = OpeningHoursVisualization.CreateFullVisualisation(
|
||||
opening_hours_obj,
|
||||
applicableWeek.ranges,
|
||||
applicableWeek.startingMonday
|
||||
)
|
||||
textual.current.addCallbackAndRunD((descr) => {
|
||||
vis.ConstructElement().ariaLabel = descr
|
||||
})
|
||||
return vis
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -53,33 +68,11 @@ export default class OpeningHoursVisualization extends Toggle {
|
|||
this.SetClass("no-weblate")
|
||||
}
|
||||
|
||||
private static CreateFullVisualisation(oh: any): BaseUIElement {
|
||||
/** First, we determine which range of dates we want to visualize: this week or next week?**/
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const lastMonday = OH.getMondayBefore(today)
|
||||
const nextSunday = new Date(lastMonday)
|
||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||
|
||||
if (!oh.getState() && !oh.getUnknown()) {
|
||||
// POI is currently closed
|
||||
const nextChange: Date = oh.getNextChange()
|
||||
if (
|
||||
// Shop isn't gonna open anymore in this timerange
|
||||
nextSunday < nextChange &&
|
||||
// And we are already in the weekend to show next week
|
||||
(today.getDay() == 0 || today.getDay() == 6)
|
||||
) {
|
||||
// We move the range to next week!
|
||||
lastMonday.setDate(lastMonday.getDate() + 7)
|
||||
nextSunday.setDate(nextSunday.getDate() + 7)
|
||||
}
|
||||
}
|
||||
|
||||
/* We calculate the ranges when it is opened! */
|
||||
const ranges = OH.GetRanges(oh, lastMonday, nextSunday)
|
||||
|
||||
private static CreateFullVisualisation(
|
||||
oh: opening_hours,
|
||||
ranges: OpeningRange[][],
|
||||
lastMonday: Date
|
||||
): BaseUIElement {
|
||||
/* First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special
|
||||
* So, we have to handle the case that ranges is completely empty*/
|
||||
if (ranges.filter((range) => range.length > 0).length === 0) {
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
export let layer: LayerConfig
|
||||
export let config: TagRenderingConfig
|
||||
export let extraClasses: string | undefined = undefined
|
||||
|
||||
export let id : string = undefined
|
||||
|
||||
if (config === undefined) {
|
||||
throw "Config is undefined in tagRenderingAnswer"
|
||||
|
@ -26,7 +28,7 @@
|
|||
</script>
|
||||
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
|
||||
<div class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
|
||||
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
|
||||
{#if $trs.length === 1}
|
||||
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
|
||||
{/if}
|
||||
|
|
|
@ -102,9 +102,7 @@
|
|||
</TagRenderingQuestion>
|
||||
{:else}
|
||||
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
|
||||
<div id={answerId}>
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
</div>
|
||||
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
|
||||
<button
|
||||
on:click={() => {
|
||||
editMode = true
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
| "small-height"
|
||||
| "medium-height"
|
||||
| "large-height"
|
||||
| string
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -819,13 +819,8 @@ export default class SpecialVisualizations {
|
|||
example:
|
||||
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
|
||||
constr: (state, tagSource: UIEventSource<any>, args) => {
|
||||
return new OpeningHoursVisualization(
|
||||
tagSource,
|
||||
state,
|
||||
args[0],
|
||||
args[1],
|
||||
args[2]
|
||||
)
|
||||
const [key, prefix, postfix] = args
|
||||
return new OpeningHoursVisualization(tagSource, state, key, prefix, postfix)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue