forked from MapComplete/MapComplete
		
	Fix opening hours input element
This commit is contained in:
		
							parent
							
								
									94f9a0de56
								
							
						
					
					
						commit
						64ec06bfc8
					
				
					 19 changed files with 643 additions and 599 deletions
				
			
		|  | @ -102,9 +102,13 @@ export default class SimpleMetaTagger { | ||||||
| 
 | 
 | ||||||
|             SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { |             SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { | ||||||
|                 try { |                 try { | ||||||
|  |                     const oldCountry = feature.properties["_country"]; | ||||||
|                     feature.properties["_country"] = countries[0].trim().toLowerCase(); |                     feature.properties["_country"] = countries[0].trim().toLowerCase(); | ||||||
|  |                     if (oldCountry !== feature.properties["_country"]) { | ||||||
|                         const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); |                         const tagsSource = State.state.allElements.getEventSourceById(feature.properties.id); | ||||||
|                         tagsSource.ping(); |                         tagsSource.ping(); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|                 } catch (e) { |                 } catch (e) { | ||||||
|                     console.warn(e) |                     console.warn(e) | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -19,6 +19,9 @@ export default class Combine extends BaseUIElement { | ||||||
|     protected InnerConstructElement(): HTMLElement { |     protected InnerConstructElement(): HTMLElement { | ||||||
|         const el = document.createElement("span") |         const el = document.createElement("span") | ||||||
| 
 | 
 | ||||||
|  |         try{ | ||||||
|  |              | ||||||
|  |       | ||||||
|         for (const subEl of this.uiElements) { |         for (const subEl of this.uiElements) { | ||||||
|             if(subEl === undefined || subEl === null){ |             if(subEl === undefined || subEl === null){ | ||||||
|                 continue; |                 continue; | ||||||
|  | @ -28,6 +31,11 @@ export default class Combine extends BaseUIElement { | ||||||
|                 el.appendChild(subHtml) |                 el.appendChild(subHtml) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         }catch(e){ | ||||||
|  |             const domExc = e as DOMException | ||||||
|  |             console.error("DOMException: ", domExc.name) | ||||||
|  |             el.appendChild(new FixedUiElement("Could not generate this combine!").SetClass("alert").ConstructElement()) | ||||||
|  |         } | ||||||
|          |          | ||||||
|         return el; |         return el; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -6,10 +6,14 @@ export default class Table extends BaseUIElement { | ||||||
| 
 | 
 | ||||||
|     private readonly _header: BaseUIElement[]; |     private readonly _header: BaseUIElement[]; | ||||||
|     private readonly _contents: BaseUIElement[][]; |     private readonly _contents: BaseUIElement[][]; | ||||||
|  |     private readonly _contentStyle: string[][]; | ||||||
| 
 | 
 | ||||||
|     constructor(header: (BaseUIElement | string)[], contents: (BaseUIElement | string)[][]) { |     constructor(header: (BaseUIElement | string)[],  | ||||||
|  |                 contents: (BaseUIElement | string)[][], | ||||||
|  |                 contentStyle?: string[][]) { | ||||||
|         super(); |         super(); | ||||||
|         this._header = header.map(Translations.W); |         this._contentStyle = contentStyle ?? []; | ||||||
|  |         this._header = header?.map(Translations.W); | ||||||
|         this._contents = contents.map(row => row.map(Translations.W)); |         this._contents = contents.map(row => row.map(Translations.W)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -28,15 +32,23 @@ export default class Table extends BaseUIElement { | ||||||
|             table.appendChild(tr) |             table.appendChild(tr) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         for (const row of this._contents) { |         for (let i = 0; i < this._contents.length; i++){ | ||||||
|  |             let row = this._contents[i]; | ||||||
|             const tr = document.createElement("tr") |             const tr = document.createElement("tr") | ||||||
|             for (const elem of row) { |             for (let j = 0; j < row.length; j++){ | ||||||
|                 const htmlElem = elem.ConstructElement() |                 let elem = row[j]; | ||||||
|  |                 const htmlElem = elem?.ConstructElement() | ||||||
|                 if (htmlElem === undefined) { |                 if (htmlElem === undefined) { | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |                 let style = undefined; | ||||||
|  |                 if(this._contentStyle !== undefined && this._contentStyle[i] !== undefined && this._contentStyle[j]!== undefined){ | ||||||
|  |                     style = this._contentStyle[i][j] | ||||||
|  |                 } | ||||||
|  |          | ||||||
|                 const td = document.createElement("td") |                 const td = document.createElement("td") | ||||||
|  |                 td.style.cssText = style; | ||||||
|                 td.appendChild(htmlElem) |                 td.appendChild(htmlElem) | ||||||
|                 tr.appendChild(td) |                 tr.appendChild(td) | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ export class VariableUiElement extends BaseUIElement { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (contents === undefined) { |             if (contents === undefined) { | ||||||
|                 return |                 return el; | ||||||
|             } |             } | ||||||
|             if (typeof contents === "string") { |             if (typeof contents === "string") { | ||||||
|                 el.innerHTML = contents |                 el.innerHTML = contents | ||||||
|  |  | ||||||
|  | @ -102,6 +102,8 @@ export default abstract class BaseUIElement { | ||||||
|         if(this.InnerConstructElement === undefined){ |         if(this.InnerConstructElement === undefined){ | ||||||
|             throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name |             throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name | ||||||
|         } |         } | ||||||
|  | try{ | ||||||
|  |              | ||||||
| 
 | 
 | ||||||
|         const el = this.InnerConstructElement(); |         const el = this.InnerConstructElement(); | ||||||
| 
 | 
 | ||||||
|  | @ -149,7 +151,13 @@ export default abstract class BaseUIElement { | ||||||
|             el.addEventListener('mouseout', () => self._onHover.setData(false)); |             el.addEventListener('mouseout', () => self._onHover.setData(false)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return el |         return el}catch(e){ | ||||||
|  |             const domExc = e as DOMException; | ||||||
|  |             if(domExc){ | ||||||
|  |                 console.log("An exception occured", domExc.code, domExc.message, domExc.name ) | ||||||
|  |             } | ||||||
|  |             console.error(e) | ||||||
|  | } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public AsMarkdown(): string{ |     public AsMarkdown(): string{ | ||||||
|  |  | ||||||
|  | @ -1,324 +0,0 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import {UIElement} from "../UIElement"; |  | ||||||
| import Combine from "../Base/Combine"; |  | ||||||
| import State from "../../State"; |  | ||||||
| import {FixedUiElement} from "../Base/FixedUiElement"; |  | ||||||
| import {OH} from "./OpeningHours"; |  | ||||||
| import Translations from "../i18n/Translations"; |  | ||||||
| import Constants from "../../Models/Constants"; |  | ||||||
| import opening_hours from "opening_hours"; |  | ||||||
| import BaseUIElement from "../BaseUIElement"; |  | ||||||
| 
 |  | ||||||
| export default class OpeningHoursVisualization extends UIElement { |  | ||||||
|     private static readonly weekdays = [ |  | ||||||
|         Translations.t.general.weekdays.abbreviations.monday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.tuesday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.wednesday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.thursday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.friday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.saturday, |  | ||||||
|         Translations.t.general.weekdays.abbreviations.sunday, |  | ||||||
|     ] |  | ||||||
|     private readonly _key: string; |  | ||||||
| 
 |  | ||||||
|     constructor(tags: UIEventSource<any>, key: string) { |  | ||||||
|         super(tags); |  | ||||||
|         this._key = key; |  | ||||||
|         this.ListenTo(UIEventSource.Chronic(60 * 1000)); // Automatically reload every minute
 |  | ||||||
|         this.ListenTo(UIEventSource.Chronic(500, () => { |  | ||||||
|             return tags.data._country === undefined; |  | ||||||
|         })); |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static GetRanges(oh: any, from: Date, to: Date): ({ |  | ||||||
|         isOpen: boolean, |  | ||||||
|         isSpecial: boolean, |  | ||||||
|         comment: string, |  | ||||||
|         startDate: Date, |  | ||||||
|         endDate: Date |  | ||||||
|     }[])[] { |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const values = [[], [], [], [], [], [], []]; |  | ||||||
| 
 |  | ||||||
|         const start = new Date(from); |  | ||||||
|         // We go one day more into the past, in order to force rendering of holidays in the start of the period
 |  | ||||||
|         start.setDate(from.getDate() - 1); |  | ||||||
| 
 |  | ||||||
|         const iterator = oh.getIterator(start); |  | ||||||
| 
 |  | ||||||
|         let prevValue = undefined; |  | ||||||
|         while (iterator.advance(to)) { |  | ||||||
| 
 |  | ||||||
|             if (prevValue) { |  | ||||||
|                 prevValue.endDate = iterator.getDate() as Date |  | ||||||
|             } |  | ||||||
|             const endDate = new Date(iterator.getDate()) as Date; |  | ||||||
|             endDate.setHours(0, 0, 0, 0) |  | ||||||
|             endDate.setDate(endDate.getDate() + 1); |  | ||||||
|             const value = { |  | ||||||
|                 isSpecial: iterator.getUnknown(), |  | ||||||
|                 isOpen: iterator.getState(), |  | ||||||
|                 comment: iterator.getComment(), |  | ||||||
|                 startDate: iterator.getDate() as Date, |  | ||||||
|                 endDate: endDate // Should be overwritten by the next iteration
 |  | ||||||
|             } |  | ||||||
|             prevValue = value; |  | ||||||
| 
 |  | ||||||
|             if (value.comment === undefined && !value.isOpen && !value.isSpecial) { |  | ||||||
|                 // simply closed, nothing special here
 |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (value.startDate < from) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             // Get day: sunday is 0, monday is 1. We move everything so that monday == 0
 |  | ||||||
|             values[(value.startDate.getDay() + 6) % 7].push(value); |  | ||||||
|         } |  | ||||||
|         return values; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static getMonday(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)); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     InnerRender(): string | BaseUIElement { |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const today = new Date(); |  | ||||||
|         today.setHours(0, 0, 0, 0); |  | ||||||
| 
 |  | ||||||
|         const lastMonday = OpeningHoursVisualization.getMonday(today); |  | ||||||
|         const nextSunday = new Date(lastMonday); |  | ||||||
|         nextSunday.setDate(nextSunday.getDate() + 7); |  | ||||||
| 
 |  | ||||||
|         const tags = this._source.data; |  | ||||||
|         if (tags._country === undefined) { |  | ||||||
|             return "Loading country information..."; |  | ||||||
|         } |  | ||||||
|         let oh = null; |  | ||||||
| 
 |  | ||||||
|         try { |  | ||||||
|             // noinspection JSPotentiallyInvalidConstructorUsage
 |  | ||||||
|             oh = new opening_hours(tags[this._key], { |  | ||||||
|                 lat: tags._lat, |  | ||||||
|                 lon: tags._lon, |  | ||||||
|                 address: { |  | ||||||
|                     country_code: tags._country |  | ||||||
|                 } |  | ||||||
|             }, {tag_key: this._key}); |  | ||||||
|         } catch (e) { |  | ||||||
|             console.log(e); |  | ||||||
|             return new Combine([Translations.t.general.opening_hours.error_loading, |  | ||||||
|                 State.state?.osmConnection?.userDetails?.data?.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked ? |  | ||||||
|                     `<span class='subtle'>${e}</span>` |  | ||||||
|                     : "" |  | ||||||
|             ]); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         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 mover further along
 |  | ||||||
|                 lastMonday.setDate(lastMonday.getDate() + 7); |  | ||||||
|                 nextSunday.setDate(nextSunday.getDate() + 7); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // ranges[0] are all ranges for monday
 |  | ||||||
|         const ranges = OpeningHoursVisualization.GetRanges(oh, lastMonday, nextSunday); |  | ||||||
|         if (ranges.map(r => r.length).reduce((a, b) => a + b, 0) == 0) { |  | ||||||
|             // Closed!
 |  | ||||||
|             const opensAtDate = oh.getNextChange(); |  | ||||||
|             if (opensAtDate === undefined) { |  | ||||||
|                 const comm = oh.getComment() ?? oh.getUnknown(); |  | ||||||
|                 if (!!comm) { |  | ||||||
|                     return new FixedUiElement(comm).SetClass("ohviz-closed"); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (oh.getState()) { |  | ||||||
|                     return Translations.t.general.opening_hours.open_24_7.SetClass("ohviz-closed") |  | ||||||
|                 } |  | ||||||
|                 return Translations.t.general.opening_hours.closed_permanently.SetClass("ohviz-closed") |  | ||||||
|             } |  | ||||||
|             const moment = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` |  | ||||||
|             return Translations.t.general.opening_hours.closed_until.Subs({date: moment}).SetClass("ohviz-closed") |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const isWeekstable = oh.isWeekStable(); |  | ||||||
| 
 |  | ||||||
|         let [changeHours, changeHourText] = OpeningHoursVisualization.allChangeMoments(ranges); |  | ||||||
| 
 |  | ||||||
|         // By default, we always show the range between 8 - 19h, in order to give a stable impression
 |  | ||||||
|         // Ofc, a bigger range is used if needed
 |  | ||||||
|         const earliestOpen = Math.min(8 * 60 * 60, ...changeHours); |  | ||||||
|         let latestclose = Math.max(...changeHours); |  | ||||||
|         // We always make sure there is 30m of leeway in order to give enough room for the closing entry
 |  | ||||||
|         latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const rows: BaseUIElement[] = []; |  | ||||||
|         const availableArea = latestclose - earliestOpen; |  | ||||||
|         // @ts-ignore
 |  | ||||||
|         const now = (100 * (((new Date() - today) / 1000) - earliestOpen)) / availableArea; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         let header: BaseUIElement[] = []; |  | ||||||
| 
 |  | ||||||
|         if (now >= 0 && now <= 100) { |  | ||||||
|             header.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now")) |  | ||||||
|         } |  | ||||||
|         for (const changeMoment of changeHours) { |  | ||||||
|             const offset = 100 * (changeMoment - earliestOpen) / availableArea; |  | ||||||
|             if (offset < 0 || offset > 100) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line"); |  | ||||||
|             header.push(el); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (let i = 0; i < changeHours.length; i++) { |  | ||||||
|             let changeMoment = changeHours[i]; |  | ||||||
|             const offset = 100 * (changeMoment - earliestOpen) / availableArea; |  | ||||||
|             if (offset < 0 || offset > 100) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|             const el = new FixedUiElement( |  | ||||||
|                 `<div style='margin-top: ${i % 2 == 0 ? '1.5em;' : "1%"}'>${changeHourText[i]}</div>` |  | ||||||
|             ) |  | ||||||
|                 .SetStyle(`left:${offset}%`) |  | ||||||
|                 .SetClass("ohviz-time-indication"); |  | ||||||
|             header.push(el); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         rows.push(new Combine([`<td width="5%"> </td>`, |  | ||||||
|             `<td style="position:relative;height:2.5em;">`, |  | ||||||
|             new Combine(header), `</td>`])); |  | ||||||
| 
 |  | ||||||
|         for (let i = 0; i < 7; i++) { |  | ||||||
|             const dayRanges = ranges[i]; |  | ||||||
|             const isToday = (new Date().getDay() + 6) % 7 === i; |  | ||||||
|             let weekday = OpeningHoursVisualization.weekdays[i]; |  | ||||||
| 
 |  | ||||||
|             let dateToShow = "" |  | ||||||
|             if (!isWeekstable) { |  | ||||||
|                 const day = new Date(lastMonday) |  | ||||||
|                 day.setDate(day.getDate() + i); |  | ||||||
|                 dateToShow = "" + day.getDate() + "/" + (day.getMonth() + 1); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let innerContent: (string | BaseUIElement)[] = []; |  | ||||||
| 
 |  | ||||||
|             // Add the lines
 |  | ||||||
|             for (const changeMoment of changeHours) { |  | ||||||
|                 const offset = 100 * (changeMoment - earliestOpen) / availableArea; |  | ||||||
|                 innerContent.push(new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line")) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Add the actual ranges
 |  | ||||||
|             for (const range of dayRanges) { |  | ||||||
|                 if (!range.isOpen && !range.isSpecial) { |  | ||||||
|                     innerContent.push( |  | ||||||
|                         new FixedUiElement(range.comment ?? dateToShow).SetClass("ohviz-day-off")) |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 const startOfDay: Date = new Date(range.startDate); |  | ||||||
|                 startOfDay.setHours(0, 0, 0, 0); |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen; |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen); |  | ||||||
|                 const startPercentage = (100 * startpoint / availableArea); |  | ||||||
|                 innerContent.push( |  | ||||||
|                     new FixedUiElement(range.comment ?? dateToShow).SetStyle(`left:${startPercentage}%; width:${width}%`).SetClass("ohviz-range")) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Add line for 'now'
 |  | ||||||
|             if (now >= 0 && now <= 100) { |  | ||||||
|                 innerContent.push(new FixedUiElement("").SetStyle(`left:${now}%;`).SetClass("ohviz-now")) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let clss = "" |  | ||||||
|             if (isToday) { |  | ||||||
|                 clss = "ohviz-today" |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             rows.push(new Combine( |  | ||||||
|                 [`<td class="ohviz-weekday ${clss}">${weekday}</td>`, |  | ||||||
|                     `<td style="position:relative;" class="${clss}">`, |  | ||||||
|                     ...innerContent, |  | ||||||
|                     `</td>`])) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         return new Combine([ |  | ||||||
|             "<table class='ohviz' style='width:100%; word-break: normal; word-wrap: normal'>", |  | ||||||
|             ...rows.map(el => new Combine(["<tr>" ,el , "</tr>"])), |  | ||||||
|             "</table>" |  | ||||||
|         ]).SetClass("ohviz-container"); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private static allChangeMoments(ranges: { |  | ||||||
|         isOpen: boolean, |  | ||||||
|         isSpecial: boolean, |  | ||||||
|         comment: string, |  | ||||||
|         startDate: Date, |  | ||||||
|         endDate: Date |  | ||||||
|     }[][]): [number[], string[]] { |  | ||||||
|         const changeHours: number[] = [] |  | ||||||
|         const changeHourText: string[] = []; |  | ||||||
|         const extrachangeHours: number[] = [] |  | ||||||
|         const extrachangeHourText: string[] = []; |  | ||||||
| 
 |  | ||||||
|         for (const weekday of ranges) { |  | ||||||
|             for (const range of weekday) { |  | ||||||
|                 if (!range.isOpen && !range.isSpecial) { |  | ||||||
|                     continue; |  | ||||||
|                 } |  | ||||||
|                 const startOfDay: Date = new Date(range.startDate); |  | ||||||
|                 startOfDay.setHours(0, 0, 0, 0); |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 const changeMoment: number = (range.startDate - startOfDay) / 1000; |  | ||||||
|                 if (changeHours.indexOf(changeMoment) < 0) { |  | ||||||
|                     changeHours.push(changeMoment); |  | ||||||
|                     changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes())) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // @ts-ignore
 |  | ||||||
|                 let changeMomentEnd: number = (range.endDate - startOfDay) / 1000; |  | ||||||
|                 if (changeMomentEnd >= 24 * 60 * 60) { |  | ||||||
|                     if (extrachangeHours.indexOf(changeMomentEnd) < 0) { |  | ||||||
|                         extrachangeHours.push(changeMomentEnd); |  | ||||||
|                         extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) |  | ||||||
|                     } |  | ||||||
|                 } else if (changeHours.indexOf(changeMomentEnd) < 0) { |  | ||||||
|                     changeHours.push(changeMomentEnd); |  | ||||||
|                     changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         changeHourText.sort(); |  | ||||||
|         changeHours.sort(); |  | ||||||
|         extrachangeHourText.sort(); |  | ||||||
|         extrachangeHours.sort(); |  | ||||||
|         changeHourText.push(...extrachangeHourText); |  | ||||||
|         changeHours.push(...extrachangeHours); |  | ||||||
| 
 |  | ||||||
|         return [changeHours, changeHourText] |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
|  | @ -8,6 +8,9 @@ export interface OpeningHour { | ||||||
|     endMinutes: number |     endMinutes: number | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Various utilities manipulating opening hours | ||||||
|  |  */ | ||||||
| export class OH { | export class OH { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -163,6 +166,12 @@ export class OH { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Gives the number of hours since the start of day. | ||||||
|  |      * E.g. | ||||||
|  |      * startTime({startHour: 9, startMinuts: 15}) == 9.25 | ||||||
|  |      * @param oh | ||||||
|  |      */ | ||||||
|     public static startTime(oh: OpeningHour): number { |     public static startTime(oh: OpeningHour): number { | ||||||
|         return oh.startHour + oh.startMinutes / 60; |         return oh.startHour + oh.startMinutes / 60; | ||||||
|     } |     } | ||||||
|  | @ -348,5 +357,125 @@ export class OH { | ||||||
| 
 | 
 | ||||||
|         return ohs; |         return ohs; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |  This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs. | ||||||
|  |  E.g. | ||||||
|  |  Monday, some business is opended from 9:00 till 17:00 | ||||||
|  |  Tuesday from 9:30 till 18:00 | ||||||
|  |  Wednesday from 9:30 till 12:30 | ||||||
|  |  This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00 | ||||||
|  |  This list will be sorted | ||||||
|  |  */ | ||||||
|  |     public static allChangeMoments(ranges: { | ||||||
|  |         isOpen: boolean, | ||||||
|  |         isSpecial: boolean, | ||||||
|  |         comment: string, | ||||||
|  |         startDate: Date, | ||||||
|  |         endDate: Date | ||||||
|  |     }[][]): [number[], string[]] { | ||||||
|  |         const changeHours: number[] = [] | ||||||
|  |         const changeHourText: string[] = []; | ||||||
|  |          | ||||||
|  |         const extrachangeHours: number[] = [] | ||||||
|  |         const extrachangeHourText: string[] = []; | ||||||
|  | 
 | ||||||
|  |         for (const weekday of ranges) { | ||||||
|  |             for (const range of weekday) { | ||||||
|  |                 if (!range.isOpen && !range.isSpecial) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 const startOfDay: Date = new Date(range.startDate); | ||||||
|  |                 startOfDay.setHours(0, 0, 0, 0); | ||||||
|  |                  | ||||||
|  |                 // The number of seconds since the start of the day
 | ||||||
|  |                 // @ts-ignore
 | ||||||
|  |                 const changeMoment: number = (range.startDate - startOfDay) / 1000; | ||||||
|  |                 if (changeHours.indexOf(changeMoment) < 0) { | ||||||
|  |                     changeHours.push(changeMoment); | ||||||
|  |                     changeHourText.push(OH.hhmm(range.startDate.getHours(), range.startDate.getMinutes())) | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // The number of seconds till between the start of the day and closing
 | ||||||
|  |                 // @ts-ignore
 | ||||||
|  |                 let changeMomentEnd: number = (range.endDate - startOfDay) / 1000; | ||||||
|  |                 if (changeMomentEnd >= 24 * 60 * 60) { | ||||||
|  |                     if (extrachangeHours.indexOf(changeMomentEnd) < 0) { | ||||||
|  |                         extrachangeHours.push(changeMomentEnd); | ||||||
|  |                         extrachangeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) | ||||||
|  |                     } | ||||||
|  |                 } else if (changeHours.indexOf(changeMomentEnd) < 0) { | ||||||
|  |                     changeHours.push(changeMomentEnd); | ||||||
|  |                     changeHourText.push(OH.hhmm(range.endDate.getHours(), range.endDate.getMinutes())) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Note that 'changeHours' and 'changeHourText' will be more or less in sync - one is in numbers, the other in 'HH:MM' format.
 | ||||||
|  |         // But both can be sorted without problem; they'll stay in sync
 | ||||||
|  |         changeHourText.sort(); | ||||||
|  |         changeHours.sort(); | ||||||
|  |         extrachangeHourText.sort(); | ||||||
|  |         extrachangeHours.sort(); | ||||||
|  |          | ||||||
|  |         changeHourText.push(...extrachangeHourText); | ||||||
|  |         changeHours.push(...extrachangeHours); | ||||||
|  | 
 | ||||||
|  |         return [changeHours, changeHourText] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |  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 | ||||||
|  |     }[])[] { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const values = [[], [], [], [], [], [], []]; | ||||||
|  | 
 | ||||||
|  |         const start = new Date(from); | ||||||
|  |         // We go one day more into the past, in order to force rendering of holidays in the start of the period
 | ||||||
|  |         start.setDate(from.getDate() - 1); | ||||||
|  | 
 | ||||||
|  |         const iterator = oh.getIterator(start); | ||||||
|  | 
 | ||||||
|  |         let prevValue = undefined; | ||||||
|  |         while (iterator.advance(to)) { | ||||||
|  | 
 | ||||||
|  |             if (prevValue) { | ||||||
|  |                 prevValue.endDate = iterator.getDate() as Date | ||||||
|  |             } | ||||||
|  |             const endDate = new Date(iterator.getDate()) as Date; | ||||||
|  |             endDate.setHours(0, 0, 0, 0) | ||||||
|  |             endDate.setDate(endDate.getDate() + 1); | ||||||
|  |             const value = { | ||||||
|  |                 isSpecial: iterator.getUnknown(), | ||||||
|  |                 isOpen: iterator.getState(), | ||||||
|  |                 comment: iterator.getComment(), | ||||||
|  |                 startDate: iterator.getDate() as Date, | ||||||
|  |                 endDate: endDate // Should be overwritten by the next iteration
 | ||||||
|  |             } | ||||||
|  |             prevValue = value; | ||||||
|  | 
 | ||||||
|  |             if (value.comment === undefined && !value.isOpen && !value.isSpecial) { | ||||||
|  |                 // simply closed, nothing special here
 | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (value.startDate < from) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             // Get day: sunday is 0, monday is 1. We move everything so that monday == 0
 | ||||||
|  |             values[(value.startDate.getDay() + 6) % 7].push(value); | ||||||
|  |         } | ||||||
|  |         return values; | ||||||
|  |     } | ||||||
|  |      | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {UIElement} from "../UIElement"; |  | ||||||
| import OpeningHoursRange from "./OpeningHoursRange"; | import OpeningHoursRange from "./OpeningHoursRange"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; | import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; | ||||||
|  | @ -8,63 +7,39 @@ import {InputElement} from "../Input/InputElement"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| 
 | 
 | ||||||
| export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { | export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { | ||||||
|     private readonly _ohs: UIEventSource<OpeningHour[]>;     |  | ||||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); |     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); | ||||||
| 
 |     private readonly _ohs: UIEventSource<OpeningHour[]>; | ||||||
|     private readonly _backgroundTable: OpeningHoursPickerTable; |     private readonly _backgroundTable: OpeningHoursPickerTable; | ||||||
| 
 | 
 | ||||||
|     private readonly _weekdays: UIEventSource<BaseUIElement[]> = new UIEventSource<BaseUIElement[]>([]); |  | ||||||
| 
 | 
 | ||||||
|     constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) { |     constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) { | ||||||
|         super(); |         super(); | ||||||
|         this._ohs = ohs; |         this._ohs = ohs; | ||||||
|         this._backgroundTable = new OpeningHoursPickerTable(this._weekdays, this._ohs); |  | ||||||
|         const self = this; |  | ||||||
| 
 | 
 | ||||||
|          |         ohs.addCallback(oh => { | ||||||
|         this._ohs.addCallback(ohs => { |             ohs.setData(OH.MergeTimes(oh)); | ||||||
|             self._ohs.setData(OH.MergeTimes(ohs)); |  | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         ohs.addCallbackAndRun(ohs => { |         this._backgroundTable = new OpeningHoursPickerTable(this._ohs); | ||||||
|             const perWeekday: UIElement[][] = []; |         this._backgroundTable.ConstructElement() | ||||||
|             for (let i = 0; i < 7; i++) { |  | ||||||
|                 perWeekday[i] = []; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (const oh of ohs) { |  | ||||||
|                 const source = new UIEventSource<OpeningHour>(oh) |  | ||||||
|                 source.addCallback(_ => { |  | ||||||
|                     self._ohs.setData(OH.MergeTimes(self._ohs.data)) |  | ||||||
|                 }) |  | ||||||
|                 const r = new OpeningHoursRange(source, this._backgroundTable); |  | ||||||
|                 perWeekday[oh.weekday].push(r);  |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (let i = 0; i < 7; i++) { |  | ||||||
|                 self._weekdays.data[i] = new Combine(perWeekday[i]); |  | ||||||
|             } |  | ||||||
|             self._weekdays.ping(); |  | ||||||
| 
 |  | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|  |         ohs.ping(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     InnerRender(): BaseUIElement { |     InnerRender(): BaseUIElement { | ||||||
|         return this._backgroundTable; |         return this._backgroundTable; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._backgroundTable.ConstructElement(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<OpeningHour[]> { |     GetValue(): UIEventSource<OpeningHour[]> { | ||||||
|         return this._ohs |         return this._ohs | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     IsValid(t: OpeningHour[]): boolean { |     IsValid(t: OpeningHour[]): boolean { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     protected InnerConstructElement(): HTMLElement { | ||||||
|  |         return this._backgroundTable.ConstructElement(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -3,19 +3,18 @@ | ||||||
|  * It will genarate the currently selected opening hour. |  * It will genarate the currently selected opening hour. | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {UIElement} from "../UIElement"; |  | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import {OpeningHour} from "./OpeningHours"; | import {OpeningHour} from "./OpeningHours"; | ||||||
| import {InputElement} from "../Input/InputElement"; | import {InputElement} from "../Input/InputElement"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import {Translation} from "../i18n/Translation"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
|  | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import Combine from "../Base/Combine"; | ||||||
|  | import OpeningHoursRange from "./OpeningHoursRange"; | ||||||
| 
 | 
 | ||||||
| export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> { | export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> { | ||||||
|     public readonly IsSelected: UIEventSource<boolean>; |     public static readonly days: Translation[] = | ||||||
|     private readonly weekdays: UIEventSource<BaseUIElement[]>; |  | ||||||
|     private readonly _element: HTMLTableElement |  | ||||||
| 
 |  | ||||||
|     public static readonly days: BaseUIElement[] = |  | ||||||
|         [ |         [ | ||||||
|             Translations.t.general.weekdays.abbreviations.monday, |             Translations.t.general.weekdays.abbreviations.monday, | ||||||
|             Translations.t.general.weekdays.abbreviations.tuesday, |             Translations.t.general.weekdays.abbreviations.tuesday, | ||||||
|  | @ -25,59 +24,105 @@ export default class OpeningHoursPickerTable extends InputElement<OpeningHour[]> | ||||||
|             Translations.t.general.weekdays.abbreviations.saturday, |             Translations.t.general.weekdays.abbreviations.saturday, | ||||||
|             Translations.t.general.weekdays.abbreviations.sunday |             Translations.t.general.weekdays.abbreviations.sunday | ||||||
|         ] |         ] | ||||||
| 
 |     public readonly IsSelected: UIEventSource<boolean>; | ||||||
| 
 |  | ||||||
|     private readonly source: UIEventSource<OpeningHour[]>; |     private readonly source: UIEventSource<OpeningHour[]>; | ||||||
|      |      | ||||||
|     private static _nextId = 0; |     /* | ||||||
|  |     These html-elements are an overlay over the table columns and act as a host for the ranges in the weekdays | ||||||
|  |      */ | ||||||
|  |     public readonly weekdayElements : HTMLElement[] = Utils.TimesT(7, () => document.createElement("div")) | ||||||
| 
 | 
 | ||||||
|     constructor(weekdays: UIEventSource<BaseUIElement[]>, source?: UIEventSource<OpeningHour[]>) { |     constructor(source?: UIEventSource<OpeningHour[]>) { | ||||||
|         super(); |         super(); | ||||||
|         this.weekdays = weekdays; |  | ||||||
|         this.source = source ?? new UIEventSource<OpeningHour[]>([]); |         this.source = source ?? new UIEventSource<OpeningHour[]>([]); | ||||||
|         this.IsSelected = new UIEventSource<boolean>(false); |         this.IsSelected = new UIEventSource<boolean>(false); | ||||||
|         this.SetStyle("width:100%;height:100%;display:block;"); |         this.SetStyle("width:100%;height:100%;display:block;"); | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const id = OpeningHoursPickerTable._nextId; |  | ||||||
| OpeningHoursPickerTable._nextId ++ ; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         let rows = ""; |  | ||||||
|         const self = this; |  | ||||||
|         for (let h = 0; h < 24; h++) { |  | ||||||
|             let hs = "" + h; |  | ||||||
|             if (hs.length == 1) { |  | ||||||
|                 hs = "0" + hs; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |     IsValid(t: OpeningHour[]): boolean { | ||||||
|             rows += `<tr><td rowspan="2" class="oh-left-col oh-timecell-full">${hs}:00</td>` + |         return true; | ||||||
|                 Utils.Times(weekday => `<td id="${id}-timecell-${weekday}-${h}" class="oh-timecell oh-timecell-full oh-timecell-${weekday}"></td>`, 7) + |  | ||||||
|                 '</tr><tr>' + |  | ||||||
|                 Utils.Times(id => `<td id="${id}-timecell-${id}-${h}-30" class="oh-timecell oh-timecell-half oh-timecell-${id}"></td>`, 7) + |  | ||||||
|                 '</tr>'; |  | ||||||
|     } |     } | ||||||
|         let days = OpeningHoursPickerTable.days.map((day, i) => { |  | ||||||
|             const innerContent  =  self.weekdays.data[i]?.ConstructElement()?.innerHTML ?? ""; |  | ||||||
|             return day.ConstructElement().innerHTML + "<span style='width:100%; display:block; position: relative;'>"+innerContent+"</span>"; |  | ||||||
|         }).join("</th><th width='14%'>"); |  | ||||||
| 
 | 
 | ||||||
|         this._element = document.createElement("table") |     GetValue(): UIEventSource<OpeningHour[]> { | ||||||
|         const el = this._element; |         return this.source; | ||||||
|         this.SetClass("oh-table") |  | ||||||
|         el.innerHTML =`<tr><th></th><th width='14%'>${days}</th></tr>${rows}`; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected InnerConstructElement(): HTMLElement { |     protected InnerConstructElement(): HTMLElement { | ||||||
|         return this._element | 
 | ||||||
|  |         const table = document.createElement("table") | ||||||
|  |         table.classList.add("oh-table") | ||||||
|  | 
 | ||||||
|  |         const headerRow = document.createElement("tr") | ||||||
|  |         headerRow.appendChild(document.createElement("th")) | ||||||
|  |         for (let i = 0; i < OpeningHoursPickerTable.days.length; i++) { | ||||||
|  |             let weekday = OpeningHoursPickerTable.days[i].Clone(); | ||||||
|  |             const cell = document.createElement("th") | ||||||
|  |             cell.style.width = "14%" | ||||||
|  |             cell.appendChild(weekday.ConstructElement()) | ||||||
|  |             const fullColumnSpan = this.weekdayElements[i] | ||||||
|  |             fullColumnSpan.classList.add("w-full","h-full","relative") | ||||||
|  |             fullColumnSpan.style.height = "42rem"   | ||||||
|  |              | ||||||
|  |              | ||||||
|  |             const ranges = new VariableUiElement( | ||||||
|  |                 this.source.map(ohs => ohs.filter((oh : OpeningHour) => oh.weekday === i)) | ||||||
|  |                     .map(ohsForToday => { | ||||||
|  |                         return new Combine(ohsForToday.map(oh => new OpeningHoursRange(oh, () =>{ | ||||||
|  |                             this.source.data.splice(this.source.data.indexOf(oh), 1) | ||||||
|  |                             this.source.ping() | ||||||
|  |                         }))) | ||||||
|  |                     }) | ||||||
|  |             ) | ||||||
|  |             fullColumnSpan.appendChild(ranges.ConstructElement()) | ||||||
|  |              | ||||||
|  |              | ||||||
|  |              | ||||||
|  |              | ||||||
|  |             const fullColumnSpanWrapper = document.createElement("div") | ||||||
|  |             fullColumnSpanWrapper.classList.add("absolute") | ||||||
|  |             fullColumnSpanWrapper.style.zIndex = "10" | ||||||
|  |             fullColumnSpanWrapper.style.width = "13.5%" | ||||||
|  |             fullColumnSpanWrapper.style.pointerEvents = "none" | ||||||
|  | 
 | ||||||
|  |             fullColumnSpanWrapper.appendChild(fullColumnSpan) | ||||||
|  |              | ||||||
|  |             cell.appendChild(fullColumnSpanWrapper) | ||||||
|  |             headerRow.appendChild(cell) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     private InnerUpdate(table: HTMLTableElement) { |         table.appendChild(headerRow) | ||||||
|  | 
 | ||||||
|         const self = this; |         const self = this; | ||||||
|         if (table === undefined || table === null) { |         for (let h = 0; h < 24; h++) { | ||||||
|             return; | 
 | ||||||
|  |             const hs = Utils.TwoDigits(h); | ||||||
|  |             const firstCell = document.createElement("td") | ||||||
|  |             firstCell.rowSpan = 2 | ||||||
|  |             firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box","h-2") | ||||||
|  |             firstCell.appendChild(new FixedUiElement(hs + ":00").ConstructElement()) | ||||||
|  | 
 | ||||||
|  |             const evenRow = document.createElement("tr") | ||||||
|  |             evenRow.appendChild(firstCell); | ||||||
|  | 
 | ||||||
|  |             for (let weekday = 0; weekday < 7; weekday++) { | ||||||
|  |                 const cell = document.createElement("td") | ||||||
|  |                 cell.classList.add("oh-timecell", "oh-timecell-full", `oh-timecell-${weekday}`) | ||||||
|  |                 evenRow.appendChild(cell) | ||||||
|             } |             } | ||||||
|  |             table.appendChild(evenRow) | ||||||
|  | 
 | ||||||
|  |             const oddRow = document.createElement("tr") | ||||||
|  | 
 | ||||||
|  |             for (let weekday = 0; weekday < 7; weekday++) { | ||||||
|  |                 const cell = document.createElement("td") | ||||||
|  |                 cell.classList.add("oh-timecell", "oh-timecell-half", `oh-timecell-${weekday}`) | ||||||
|  |                 oddRow.appendChild(cell) | ||||||
|  |             } | ||||||
|  |             table.appendChild(oddRow) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |          | ||||||
|  |         /**** Event handling below ***/ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         let mouseIsDown = false; |         let mouseIsDown = false; | ||||||
|  | @ -123,6 +168,7 @@ OpeningHoursPickerTable._nextId ++ ; | ||||||
|                     oh.endMinutes = 0; |                     oh.endMinutes = 0; | ||||||
|                 } |                 } | ||||||
|                 self.source.data.push(oh); |                 self.source.data.push(oh); | ||||||
|  |                 console.log("Created ", oh) | ||||||
|             } |             } | ||||||
|             self.source.ping(); |             self.source.ping(); | ||||||
| 
 | 
 | ||||||
|  | @ -149,6 +195,7 @@ OpeningHoursPickerTable._nextId ++ ; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         let lastSelectionIend, lastSelectionJEnd; |         let lastSelectionIend, lastSelectionJEnd; | ||||||
|  | 
 | ||||||
|         function selectAllBetween(iEnd, jEnd) { |         function selectAllBetween(iEnd, jEnd) { | ||||||
| 
 | 
 | ||||||
|             if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { |             if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { | ||||||
|  | @ -287,15 +334,7 @@ OpeningHoursPickerTable._nextId ++ ; | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |         return table | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(t: OpeningHour[]): boolean { |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<OpeningHour[]> { |  | ||||||
|         return this.source; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -1,73 +1,57 @@ | ||||||
| /** | /** | ||||||
|  * A single opening hours range, shown on top of the OH-picker table |  * A single opening hours range, shown on top of the OH-picker table | ||||||
|  */ |  */ | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; |  | ||||||
| import {UIElement} from "../UIElement"; |  | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; |  | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import Combine from "../Base/Combine"; | import Combine from "../Base/Combine"; | ||||||
| import {OH, OpeningHour} from "./OpeningHours"; | import {OH, OpeningHour} from "./OpeningHours"; | ||||||
| import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; |  | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
| 
 | 
 | ||||||
| export default class OpeningHoursRange extends UIElement { | export default class OpeningHoursRange extends BaseUIElement { | ||||||
|     private _oh: UIEventSource<OpeningHour>; |     private _oh: OpeningHour; | ||||||
| 
 | 
 | ||||||
|     private readonly _startTime: BaseUIElement; |     private readonly _onDelete: () => void; | ||||||
|     private readonly _endTime: BaseUIElement; |  | ||||||
|     private readonly _deleteRange: BaseUIElement; |  | ||||||
|     private readonly _tableId: OpeningHoursPickerTable; |  | ||||||
| 
 | 
 | ||||||
|     constructor(oh: UIEventSource<OpeningHour>, tableId: OpeningHoursPickerTable) { |     constructor(oh: OpeningHour, onDelete: () => void) { | ||||||
|         super(oh); |         super(); | ||||||
|         this._tableId = tableId; |  | ||||||
|         const self = this; |  | ||||||
|         this._oh = oh; |         this._oh = oh; | ||||||
|  |         this._onDelete = onDelete; | ||||||
|         this.SetClass("oh-timerange"); |         this.SetClass("oh-timerange"); | ||||||
|         oh.addCallbackAndRun(() => { |  | ||||||
|             const el = document.getElementById(this.id) as HTMLElement; |  | ||||||
|             self.InnerUpdate(el); |  | ||||||
|         }) |  | ||||||
| 
 | 
 | ||||||
|         this._deleteRange =  |     } | ||||||
|  | 
 | ||||||
|  |     InnerConstructElement(): HTMLElement { | ||||||
|  |         const height = this.getHeight(); | ||||||
|  |         const oh = this._oh; | ||||||
|  |         const startTime = new FixedUiElement(Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes)).SetClass("oh-timerange-label") | ||||||
|  |         const endTime = new FixedUiElement(Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes)).SetClass("oh-timerange-label") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const deleteRange = | ||||||
|             Svg.delete_icon_ui() |             Svg.delete_icon_ui() | ||||||
|                 .SetClass("oh-delete-range") |                 .SetClass("oh-delete-range") | ||||||
|                 .onClick(() => { |                 .onClick(() => { | ||||||
|                 oh.data.weekday = undefined; |                     this._onDelete() | ||||||
|                 oh.ping(); |  | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         this._startTime = new VariableUiElement(oh.map(oh => { |         let content = [deleteRange] | ||||||
|             return Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes); |  | ||||||
|         })).SetClass("oh-timerange-label") |  | ||||||
| 
 |  | ||||||
|         this._endTime = new VariableUiElement(oh.map(oh => { |  | ||||||
|             return Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes); |  | ||||||
|         })).SetClass("oh-timerange-label") |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     InnerRender(): BaseUIElement { |  | ||||||
|         const oh = this._oh.data; |  | ||||||
|         if (oh === undefined) { |  | ||||||
|             return undefined; |  | ||||||
|         } |  | ||||||
|         const height = this.getHeight(); |  | ||||||
| 
 |  | ||||||
|         let content = [this._deleteRange] |  | ||||||
|         if (height > 2) { |         if (height > 2) { | ||||||
|             content = [this._startTime, this._deleteRange, this._endTime]; |             content = [startTime, deleteRange, endTime]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return new Combine(content) |         const el = new Combine(content) | ||||||
|             .SetClass("oh-timerange-inner") |             .SetClass("oh-timerange-inner").ConstructElement(); | ||||||
|  | 
 | ||||||
|  |         el.style.top = (100 * OH.startTime(oh) / 24) + "%" | ||||||
|  |         el.style.height = (100 * (OH.endTime(oh) - OH.startTime(oh)) / 24) + "%" | ||||||
|  |         return el; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|     private getHeight(): number { |     private getHeight(): number { | ||||||
|         const oh = this._oh.data; |         const oh = this._oh; | ||||||
| 
 | 
 | ||||||
|         let endhour = oh.endHour; |         let endhour = oh.endHour; | ||||||
|         if (oh.endHour == 0 && oh.endMinutes == 0) { |         if (oh.endHour == 0 && oh.endMinutes == 0) { | ||||||
|  | @ -76,28 +60,5 @@ export default class OpeningHoursRange extends UIElement { | ||||||
|         return (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); |         return (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected InnerUpdate(el: HTMLElement) { |  | ||||||
|         if (el == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         const oh = this._oh.data; |  | ||||||
|         if (oh === undefined) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // The header cell containing monday, tuesday, ...
 |  | ||||||
|         const table = this._tableId.ConstructElement() as HTMLTableElement; |  | ||||||
| 
 |  | ||||||
|         const bodyRect = document.body.getBoundingClientRect(); |  | ||||||
|         const rangeStart = table.rows[1].cells[1].getBoundingClientRect().top - bodyRect.top; |  | ||||||
|         const rangeEnd = table.rows[table.rows.length - 1].cells[1].getBoundingClientRect().bottom - bodyRect.top; |  | ||||||
| 
 |  | ||||||
|         const pixelsPerHour = (rangeEnd - rangeStart) / 24; |  | ||||||
| 
 |  | ||||||
|         el.style.top = (pixelsPerHour * OH.startTime(oh)) + "px"; |  | ||||||
|         el.style.height = (pixelsPerHour * (OH.endTime(oh) - OH.startTime(oh))) + "px"; |  | ||||||
| 
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
							
								
								
									
										292
									
								
								UI/OpeningHours/OpeningHoursVisualization.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								UI/OpeningHours/OpeningHoursVisualization.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,292 @@ | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import Combine from "../Base/Combine"; | ||||||
|  | import State from "../../State"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
|  | import {OH} from "./OpeningHours"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import Constants from "../../Models/Constants"; | ||||||
|  | import opening_hours from "opening_hours"; | ||||||
|  | import BaseUIElement from "../BaseUIElement"; | ||||||
|  | import Toggle from "../Input/Toggle"; | ||||||
|  | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import Table from "../Base/Table"; | ||||||
|  | import {Translation} from "../i18n/Translation"; | ||||||
|  | import {UIElement} from "../UIElement"; | ||||||
|  | 
 | ||||||
|  | export default class OpeningHoursVisualization extends UIElement { | ||||||
|  |     private static readonly weekdays: Translation[] = [ | ||||||
|  |         Translations.t.general.weekdays.abbreviations.monday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.tuesday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.wednesday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.thursday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.friday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.saturday, | ||||||
|  |         Translations.t.general.weekdays.abbreviations.sunday, | ||||||
|  |     ] | ||||||
|  |     private readonly _tags: UIEventSource<any>; | ||||||
|  |     private readonly _key: string; | ||||||
|  | 
 | ||||||
|  |     constructor(tags: UIEventSource<any>, key: string) { | ||||||
|  |         super() | ||||||
|  |         this._tags = tags; | ||||||
|  |         this._key = key; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     InnerRender(): BaseUIElement { | ||||||
|  |         const tags = this._tags; | ||||||
|  |         const key = this._key; | ||||||
|  |         const tagsDirect = tags.data; | ||||||
|  |         const ohTable = new VariableUiElement(tags | ||||||
|  |             .map(tags => tags[key]) // This mapping will absorb all other changes to tags in order to prevent regeneration
 | ||||||
|  |             .map(ohtext => { | ||||||
|  |                     try { | ||||||
|  |                         // noinspection JSPotentiallyInvalidConstructorUsage
 | ||||||
|  |                         const oh = new opening_hours(ohtext, { | ||||||
|  |                             lat: tagsDirect._lat, | ||||||
|  |                             lon: tagsDirect._lon, | ||||||
|  |                             address: { | ||||||
|  |                                 country_code: tagsDirect._country | ||||||
|  |                             } | ||||||
|  |                         }, {tag_key: this._key}); | ||||||
|  | 
 | ||||||
|  |                         return OpeningHoursVisualization.CreateFullVisualisation(oh) | ||||||
|  |                     } catch (e) { | ||||||
|  |                         console.log(e); | ||||||
|  |                         return new Combine([Translations.t.general.opening_hours.error_loading, | ||||||
|  |                             new Toggle( | ||||||
|  |                                 new FixedUiElement(e).SetClass("subtle"), | ||||||
|  |                                 undefined, | ||||||
|  |                                 State.state?.osmConnection?.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAndWikiLinked) | ||||||
|  |                             ) | ||||||
|  |                         ]); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |             )) | ||||||
|  | 
 | ||||||
|  |         return new Toggle( | ||||||
|  |             ohTable, | ||||||
|  |             Translations.t.general.loadingCountry.Clone(), | ||||||
|  |             tags.map(tgs => tgs._country !== undefined) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 = OpeningHoursVisualization.getMonday(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); | ||||||
|  | 
 | ||||||
|  |         /* 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) { | ||||||
|  |             return OpeningHoursVisualization.ShowSpecialCase(oh).SetClass("p-4 rounded-full block bg-gray-200") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** With all the edge cases handled, we can actually construct the table! **/ | ||||||
|  | 
 | ||||||
|  |         return OpeningHoursVisualization.ConstructVizTable(oh, ranges, lastMonday) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static ConstructVizTable(oh: any, ranges: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }[][], | ||||||
|  |                                      rangeStart: Date): BaseUIElement { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const isWeekstable: boolean = oh.isWeekStable(); | ||||||
|  |         let [changeHours, changeHourText] = OH.allChangeMoments(ranges); | ||||||
|  |         const today = new Date(); | ||||||
|  |         today.setHours(0, 0, 0, 0); | ||||||
|  | 
 | ||||||
|  |         // @ts-ignore
 | ||||||
|  |         const todayIndex = Math.ceil((today - rangeStart) / (1000 * 60 * 60 * 24)) | ||||||
|  |         // By default, we always show the range between 8 - 19h, in order to give a stable impression
 | ||||||
|  |         // Ofc, a bigger range is used if needed
 | ||||||
|  |         const earliestOpen = Math.min(8 * 60 * 60, ...changeHours); | ||||||
|  |         let latestclose = Math.max(...changeHours); | ||||||
|  |         // We always make sure there is 30m of leeway in order to give enough room for the closing entry
 | ||||||
|  |         latestclose = Math.max(19 * 60 * 60, latestclose + 30 * 60) | ||||||
|  |         const availableArea = latestclose - earliestOpen; | ||||||
|  | 
 | ||||||
|  |         /* | ||||||
|  |         * The OH-visualisation is a table, consisting of 8 rows and 2 columns: | ||||||
|  |         * The first row is a header row (which is NOT passed as header but just as a normal row!) containing empty for the first column and one object giving all the end times | ||||||
|  |         * The other rows are one for each weekday: the first element showing 'mo', 'tu', ..., the second element containing the bars. | ||||||
|  |         * Note that the bars are actually an embedded <div> spanning the full width, containing multiple sub-elements | ||||||
|  |         * */ | ||||||
|  | 
 | ||||||
|  |         const [header, headerHeight] = OpeningHoursVisualization.ConstructHeaderElement(availableArea, changeHours, changeHourText, earliestOpen) | ||||||
|  | 
 | ||||||
|  |         const weekdays = [] | ||||||
|  |         const weekdayStyles = [] | ||||||
|  |         for (let i = 0; i < 7; i++) { | ||||||
|  | 
 | ||||||
|  |             const day = OpeningHoursVisualization.weekdays[i].Clone(); | ||||||
|  |             day.SetClass("w-full h-full block") | ||||||
|  | 
 | ||||||
|  |             const rangesForDay = ranges[i].map(range => | ||||||
|  |                 OpeningHoursVisualization.CreateRangeElem(availableArea, earliestOpen, latestclose, range, isWeekstable) | ||||||
|  |             ) | ||||||
|  |             const allRanges = new Combine([ | ||||||
|  |                 ...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen) , | ||||||
|  |                 ...rangesForDay]).SetClass("w-full block"); | ||||||
|  | 
 | ||||||
|  |             let extraStyle = "" | ||||||
|  |             if (todayIndex == i) { | ||||||
|  |                 extraStyle = "background-color: var(--subtle-detail-color);" | ||||||
|  |                 allRanges.SetClass("ohviz-today") | ||||||
|  |             } else if (i >= 5) { | ||||||
|  |                 extraStyle = "background-color: rgba(230, 231, 235, 1);" | ||||||
|  |             } | ||||||
|  |             weekdays.push([day, allRanges]) | ||||||
|  |             weekdayStyles.push(["padding-left: 0.5em;" + extraStyle, `position: relative;` + extraStyle]) | ||||||
|  |         } | ||||||
|  |         return new Table(undefined, | ||||||
|  |             [[" ", header], ...weekdays], | ||||||
|  |             [["width: 5%", `position: relative; height: ${headerHeight}`], ...weekdayStyles] | ||||||
|  |         ).SetClass("w-full") | ||||||
|  |             .SetStyle("border-collapse: collapse; word-break; word-break: normal; word-wrap: normal") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static CreateRangeElem(availableArea: number, earliestOpen: number, latestclose: number, | ||||||
|  |                                    range: { isOpen: boolean; isSpecial: boolean; comment: string; startDate: Date; endDate: Date }, | ||||||
|  |                                    isWeekstable: boolean): BaseUIElement { | ||||||
|  | 
 | ||||||
|  |         const textToShow = range.comment ?? (isWeekstable ? "" : range.startDate.toLocaleDateString()); | ||||||
|  | 
 | ||||||
|  |         if (!range.isOpen && !range.isSpecial) { | ||||||
|  |             return new FixedUiElement(textToShow).SetClass("ohviz-day-off") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const startOfDay: Date = new Date(range.startDate); | ||||||
|  |         startOfDay.setHours(0, 0, 0, 0); | ||||||
|  |         // @ts-ignore
 | ||||||
|  |         const startpoint = (range.startDate - startOfDay) / 1000 - earliestOpen; | ||||||
|  |         // @ts-ignore
 | ||||||
|  |         const width = (100 * (range.endDate - range.startDate) / 1000) / (latestclose - earliestOpen); | ||||||
|  |         const startPercentage = (100 * startpoint / availableArea); | ||||||
|  |         return new FixedUiElement(textToShow).SetStyle(`left:${startPercentage}%; width:${width}%`) | ||||||
|  |             .SetClass("ohviz-range"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static CreateLinesAtChangeHours(changeHours: number[], availableArea: number, earliestOpen: number): | ||||||
|  |         BaseUIElement[] { | ||||||
|  | 
 | ||||||
|  |         const allLines: BaseUIElement[] = [] | ||||||
|  |         for (const changeMoment of changeHours) { | ||||||
|  |             const offset = 100 * (changeMoment - earliestOpen) / availableArea; | ||||||
|  |             if (offset < 0 || offset > 100) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             const el = new FixedUiElement("").SetStyle(`left:${offset}%;`).SetClass("ohviz-line"); | ||||||
|  |             allLines.push(el); | ||||||
|  |         } | ||||||
|  |         return allLines; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The OH-Visualization header element, a single bar with hours | ||||||
|  |      * @param availableArea | ||||||
|  |      * @param changeHours | ||||||
|  |      * @param changeHourText | ||||||
|  |      * @param earliestOpen | ||||||
|  |      * @constructor | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|  |     private static ConstructHeaderElement(availableArea: number, changeHours: number[], changeHourText: string[], earliestOpen: number) | ||||||
|  |         : [BaseUIElement, string] { | ||||||
|  |         let header: BaseUIElement[] = []; | ||||||
|  | 
 | ||||||
|  |         header.push(...OpeningHoursVisualization.CreateLinesAtChangeHours(changeHours, availableArea, earliestOpen)) | ||||||
|  | 
 | ||||||
|  |         let showHigher = false; | ||||||
|  |         let showHigherUsed = false; | ||||||
|  |         for (let i = 0; i < changeHours.length; i++) { | ||||||
|  |             let changeMoment = changeHours[i]; | ||||||
|  |             const offset = 100 * (changeMoment - earliestOpen) / availableArea; | ||||||
|  |             if (offset < 0 || offset > 100) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (i > 0 && ((changeMoment - changeHours[i - 1]) / (60*60)) < 2) { | ||||||
|  |                 // Quite close to the previous value
 | ||||||
|  |                 // We alternate the heights
 | ||||||
|  |                 showHigherUsed = true; | ||||||
|  |                 showHigher = !showHigher; | ||||||
|  |             } else { | ||||||
|  |                 showHigher = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const el = new Combine([ | ||||||
|  | 
 | ||||||
|  |                 new FixedUiElement(changeHourText[i]) | ||||||
|  |                     .SetClass("relative bg-white pl-1 pr-1 h-3 font-sm rounded-xl border-2 border-black  border-opacity-50") | ||||||
|  |                     .SetStyle("left: -50%; word-break:initial") | ||||||
|  | 
 | ||||||
|  |             ]) | ||||||
|  |                 .SetStyle(`left:${offset}%;margin-top: ${showHigher ? '1.4rem;' : "0.1rem"}`) | ||||||
|  |                 .SetClass("block absolute top-0 m-0 h-full box-border ohviz-time-indication"); | ||||||
|  |             header.push(el); | ||||||
|  |         } | ||||||
|  |         const headerElem = new Combine(header).SetClass(`w-full absolute block ${showHigherUsed ? "h-16" : "h-8"}`) | ||||||
|  |             .SetStyle("margin-top: -1rem") | ||||||
|  |         const headerHeight = showHigherUsed ? "4rem" : "2rem"; | ||||||
|  |         return [headerElem, headerHeight] | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     * Visualizes any special case: e.g. not open for a long time, 24/7 open, ... | ||||||
|  |     * */ | ||||||
|  |     private static ShowSpecialCase(oh: any) { | ||||||
|  |         const opensAtDate = oh.getNextChange(); | ||||||
|  |         if (opensAtDate === undefined) { | ||||||
|  |             const comm = oh.getComment() ?? oh.getUnknown(); | ||||||
|  |             if (!!comm) { | ||||||
|  |                 return new FixedUiElement(comm) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (oh.getState()) { | ||||||
|  |                 return Translations.t.general.opening_hours.open_24_7.Clone() | ||||||
|  |             } | ||||||
|  |             return Translations.t.general.opening_hours.closed_permanently.Clone() | ||||||
|  |         } | ||||||
|  |         const willOpenAt = `${opensAtDate.getDate()}/${opensAtDate.getMonth() + 1} ${OH.hhmm(opensAtDate.getHours(), opensAtDate.getMinutes())}` | ||||||
|  |         return Translations.t.general.opening_hours.closed_until.Subs({date: willOpenAt}) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static getMonday(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)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -18,6 +18,7 @@ export default class EditableTagRendering extends Toggle { | ||||||
|         const editMode = new UIEventSource<boolean>(false); |         const editMode = new UIEventSource<boolean>(false); | ||||||
| 
 | 
 | ||||||
|         const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) |         const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration) | ||||||
|  |         answer.SetClass("w-full") | ||||||
|         let rendering = answer; |         let rendering = answer; | ||||||
| 
 | 
 | ||||||
|         if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { |         if (configuration.question !== undefined && State.state?.featureSwitchUserbadge?.data) { | ||||||
|  |  | ||||||
|  | @ -109,7 +109,6 @@ export default class ShowDataLayer { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     private postProcessFeature(feature, leafletLayer: L.Layer) { |     private postProcessFeature(feature, leafletLayer: L.Layer) { | ||||||
|         const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; |         const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; | ||||||
|         if (layer === undefined) { |         if (layer === undefined) { | ||||||
|  | @ -156,7 +155,7 @@ export default class ShowDataLayer { | ||||||
|             if (selected === undefined || self._leafletMap.data === undefined) { |             if (selected === undefined || self._leafletMap.data === undefined) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (popup.isOpen()) { |             if (leafletLayer.getPopup().isOpen()) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (selected.properties.id === feature.properties.id) { |             if (selected.properties.id === feature.properties.id) { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ import ReviewElement from "./Reviews/ReviewElement"; | ||||||
| import MangroveReviews from "../Logic/Web/MangroveReviews"; | import MangroveReviews from "../Logic/Web/MangroveReviews"; | ||||||
| import Translations from "./i18n/Translations"; | import Translations from "./i18n/Translations"; | ||||||
| import ReviewForm from "./Reviews/ReviewForm"; | import ReviewForm from "./Reviews/ReviewForm"; | ||||||
| import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; | import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; | ||||||
| 
 | 
 | ||||||
| import State from "../State"; | import State from "../State"; | ||||||
| import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; | import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; | ||||||
|  | @ -120,11 +120,7 @@ export default class SpecialVisualizations { | ||||||
|                     doc: "The tagkey from which the table is constructed." |                     doc: "The tagkey from which the table is constructed." | ||||||
|                 }], |                 }], | ||||||
|                 constr: (state: State, tagSource: UIEventSource<any>, args) => { |                 constr: (state: State, tagSource: UIEventSource<any>, args) => { | ||||||
|                     let keyname = args[0]; |                     return new OpeningHoursVisualization(tagSource, args[0]) | ||||||
|                     if (keyname === undefined || keyname === "") { |  | ||||||
|                         keyname = keyname ?? "opening_hours" |  | ||||||
|                     } |  | ||||||
|                     return new OpeningHoursVisualization(tagSource, keyname) |  | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -80,8 +80,6 @@ export abstract class UIElement extends BaseUIElement{ | ||||||
|          |          | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|    |  | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * @deprecated The method should not be used |      * @deprecated The method should not be used | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										8
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -73,6 +73,14 @@ export class Utils { | ||||||
|         return res; |         return res; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static TimesT<T>(count : number, f: ((i: number) => T)): T[] { | ||||||
|  |         let res : T[] = []; | ||||||
|  |         for (let i = 0; i < count; i++) { | ||||||
|  |             res .push(f(i)); | ||||||
|  |         } | ||||||
|  |         return res; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     static DoEvery(millis: number, f: (() => void)) { |     static DoEvery(millis: number, f: (() => void)) { | ||||||
|         if (Utils.runningFromConsole) { |         if (Utils.runningFromConsole) { | ||||||
|             return; |             return; | ||||||
|  |  | ||||||
|  | @ -103,38 +103,29 @@ | ||||||
|     border-right: 10px solid var(--catch-detail-color) !important; |     border-right: 10px solid var(--catch-detail-color) !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| .oh-draggable-header { |  | ||||||
|     background-color: blue; |  | ||||||
|     height: 0.5em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .oh-timerange { | .oh-timerange { | ||||||
|  |     color: var(--catch-detail-color-contrast); | ||||||
|     border-radius: 0.5em; |     border-radius: 0.5em; | ||||||
|     margin-left: 2px; |  | ||||||
|     display: block; |     display: block; | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     top: 0; |     top: 0; | ||||||
|     left: 0; |     left: 0; | ||||||
|     width: calc(100% - 4px); |     margin-left: calc(5% - 1px); | ||||||
|  |     width: 90%; | ||||||
|     background: var(--catch-detail-color); |     background: var(--catch-detail-color); | ||||||
|     z-index: 1; |     z-index: 1; | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
|  |     border: 2px solid var(--catch-detail-color); | ||||||
|  |     overflow: unset; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .oh-timerange-inner { | .oh-timerange-inner { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-direction: column; |     flex-direction: column; | ||||||
|     overflow-x: hidden; |     justify-content: center; | ||||||
|     justify-content: space-between; |  | ||||||
|     align-content: center; |     align-content: center; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     overflow-y: hidden; |     overflow-x: unset; | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .oh-timerange-inner input { |  | ||||||
|      width: 100%; |  | ||||||
|      box-sizing: border-box; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .oh-timerange-inner-small { | .oh-timerange-inner-small { | ||||||
|  | @ -144,12 +135,6 @@ | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     width:100%; |     width:100%; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .oh-timerange-inner-small input { |  | ||||||
|     width: min-content; |  | ||||||
|     box-sizing: border-box; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .oh-delete-range{ | .oh-delete-range{ | ||||||
|     width: 1.5em; |     width: 1.5em; | ||||||
|     height: 1.5em; |     height: 1.5em; | ||||||
|  | @ -162,10 +147,6 @@ | ||||||
|     max-width: 2em; |     max-width: 2em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .oh-timerange-label { |  | ||||||
|     color: white; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /**** Opening hours visualization table ****/ | /**** Opening hours visualization table ****/ | ||||||
| 
 | 
 | ||||||
|  | @ -190,7 +171,6 @@ | ||||||
| 
 | 
 | ||||||
| .ohviz-today .ohviz-range { | .ohviz-today .ohviz-range { | ||||||
|     border: 1.5px solid black; |     border: 1.5px solid black; | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ohviz-day-off { | .ohviz-day-off { | ||||||
|  | @ -235,70 +215,12 @@ | ||||||
|     border-radius: 1em; |     border-radius: 1em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .ohviz-now { |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     margin: 0; |  | ||||||
|     height: 100%; |  | ||||||
|     border: 1px solid black; |  | ||||||
|     box-sizing: border-box |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .ohviz-line { | .ohviz-line { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     top: 0; |     top: 0; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     height: 100%; |     height: 100%; | ||||||
|     border-left: 1px solid #ccc; |     border-left: 1px solid #999; | ||||||
|     box-sizing: border-box |     box-sizing: border-box | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .ohviz-time-indication > div { |  | ||||||
|     position: relative; |  | ||||||
|     background-color: white; |  | ||||||
|     left: -50%; |  | ||||||
|     padding-left: 0.3em; |  | ||||||
|     padding-right: 0.3em; |  | ||||||
|     font-size: smaller; |  | ||||||
|     border-radius: 0.3em; |  | ||||||
|     border: 1px solid #ccc; |  | ||||||
|     word-break: initial; |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ohviz-time-indication { |  | ||||||
|     position: absolute; |  | ||||||
|     top: 0; |  | ||||||
|     margin: 0; |  | ||||||
|     height: 100%; |  | ||||||
|     box-sizing: border-box; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .ohviz-today { |  | ||||||
|     background-color: var(--subtle-detail-color); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ohviz-weekday { |  | ||||||
|     padding-left: 0.5em; |  | ||||||
|     word-break: normal; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .ohviz { |  | ||||||
|     border-collapse: collapse; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ohviz-container { |  | ||||||
|     border: 0.5em solid var(--subtle-detail-color); |  | ||||||
|     border-radius: 1em; |  | ||||||
|     display: block; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .ohviz-closed { |  | ||||||
|     padding: 1em; |  | ||||||
|     background-color: #eee; |  | ||||||
|     border-radius: 1em; |  | ||||||
|     display: block; |  | ||||||
| } |  | ||||||
|  | @ -121,6 +121,7 @@ | ||||||
|             "zoomInToSeeThisLayer": "Zoom in to see this layer", |             "zoomInToSeeThisLayer": "Zoom in to see this layer", | ||||||
|             "title": "Select layers" |             "title": "Select layers" | ||||||
|         }, |         }, | ||||||
|  |         "loadingCountry": "Determining country...", | ||||||
|         "weekdays": { |         "weekdays": { | ||||||
|             "abbreviations": { |             "abbreviations": { | ||||||
|                 "monday": "Mon", |                 "monday": "Mon", | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -3,6 +3,11 @@ import SpecialVisualizations from "./UI/SpecialVisualizations"; | ||||||
| import State from "./State"; | import State from "./State"; | ||||||
| import Combine from "./UI/Base/Combine"; | import Combine from "./UI/Base/Combine"; | ||||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; | import {FixedUiElement} from "./UI/Base/FixedUiElement"; | ||||||
|  | import OpeningHoursVisualization from "./UI/OpeningHours/OpeningHoursVisualization"; | ||||||
|  | import OpeningHoursPickerTable from "./UI/OpeningHours/OpeningHoursPickerTable"; | ||||||
|  | import OpeningHoursPicker from "./UI/OpeningHours/OpeningHoursPicker"; | ||||||
|  | import {OH, OpeningHour} from "./UI/OpeningHours/OpeningHours"; | ||||||
|  | import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const tagsSource = new UIEventSource({ | const tagsSource = new UIEventSource({ | ||||||
|  | @ -11,13 +16,23 @@ const tagsSource = new UIEventSource({ | ||||||
|     surface: 'asphalt', |     surface: 'asphalt', | ||||||
|     image: "https://i.imgur.com/kX3rl3v.jpg", |     image: "https://i.imgur.com/kX3rl3v.jpg", | ||||||
|     "image:1": "https://i.imgur.com/oHAJqMB.jpg", |     "image:1": "https://i.imgur.com/oHAJqMB.jpg", | ||||||
|    // "opening_hours":"mo-fr 09:00-18:00",
 |     "opening_hours": "mo-fr 09:00-18:00", | ||||||
|     _country: "be", |     _country: "be", | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| const state = new State(undefined) | const state = new State(undefined) | ||||||
| State.state = state | State.state = state | ||||||
| 
 | 
 | ||||||
|  | const ohData = new UIEventSource<OpeningHour[]>([{ | ||||||
|  |     weekday: 1, | ||||||
|  |     startHour: 10, | ||||||
|  |     startMinutes: 0 | ||||||
|  |     , endHour: 12, | ||||||
|  |     endMinutes: 0 | ||||||
|  | }]) | ||||||
|  | new OpeningHoursPicker(ohData).AttachTo("maindiv") | ||||||
|  | new VariableUiElement(ohData.map(OH.ToString)).AttachTo("extradiv") | ||||||
|  | /* | ||||||
| const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { | const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { | ||||||
|     try{ |     try{ | ||||||
| 
 | 
 | ||||||
|  | @ -28,4 +43,4 @@ const allSpecials = SpecialVisualizations.specialVisualizations.map(spec => { | ||||||
|         return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") |         return new FixedUiElement("Could not construct "+spec.funcName+" due to "+e).SetClass("alert") | ||||||
|     } |     } | ||||||
| }) | }) | ||||||
| new Combine(allSpecials).AttachTo("maindiv") | new Combine(allSpecials).AttachTo("maindiv")*/ | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue