forked from MapComplete/MapComplete
		
	
							parent
							
								
									379f472adf
								
							
						
					
					
						commit
						44acfd2562
					
				
					 14 changed files with 421 additions and 584 deletions
				
			
		|  | @ -146,12 +146,10 @@ name | doc | ||||||
| ------ | ----- | ------ | ----- | ||||||
| options | A JSON-object of type `{ prefix: string, postfix: string }`.   | options | A JSON-object of type `{ prefix: string, postfix: string }`.   | ||||||
| 
 | 
 | ||||||
| subarg \| doc | | subarg  | doc                                                                                                                                                   | | ||||||
| -------- \| ----- | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||||
| prefix \| Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse. | | prefix  | Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse. | | ||||||
| postfix \| Piece of text that will always be added to the end of the generated opening hours | | postfix | Piece of text that will always be added to the end of the generated opening hours                                                                     | | ||||||
| 
 |  | ||||||
|   |  | ||||||
| 
 | 
 | ||||||
| ### Example usage  | ### Example usage  | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -78,13 +78,6 @@ | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .oh-timecell-0 { |  | ||||||
|     border-left: 10px solid rgba(0, 0, 0, 0); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .oh-timecell-6 { |  | ||||||
|     border-right: 10px solid rgba(0, 0, 0, 0); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| .oh-timecol-selected { | .oh-timecol-selected { | ||||||
|     border-right: var(--catch-detail-color); |     border-right: var(--catch-detail-color); | ||||||
|  |  | ||||||
|  | @ -4,39 +4,11 @@ export class ThemeMetaTagging { | ||||||
|    public static readonly themeName = "usersettings" |    public static readonly themeName = "usersettings" | ||||||
| 
 | 
 | ||||||
|    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { |    public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) { | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )  | ||||||
|             feat.properties._description |       Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )  | ||||||
|                 .match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href   }) (feat)  )  | ||||||
|                 ?.at(1) |       Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat)  )  | ||||||
|         ) |       Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )  | ||||||
|         Utils.AddLazyProperty( |       feat.properties['__current_backgroun'] = 'initial_value' | ||||||
|             feat.properties, |  | ||||||
|             "_d", |  | ||||||
|             () => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? "" |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.href.match(/mastodon|en.osm.town/) !== null |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty(feat.properties, "_mastodon_link", () => |  | ||||||
|             ((feat) => { |  | ||||||
|                 const e = document.createElement("div") |  | ||||||
|                 e.innerHTML = feat.properties._d |  | ||||||
|                 return Array.from(e.getElementsByTagName("a")).filter( |  | ||||||
|                     (a) => a.getAttribute("rel")?.indexOf("me") >= 0 |  | ||||||
|                 )[0]?.href |  | ||||||
|             })(feat) |  | ||||||
|         ) |  | ||||||
|         Utils.AddLazyProperty( |  | ||||||
|             feat.properties, |  | ||||||
|             "_mastodon_candidate", |  | ||||||
|             () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a |  | ||||||
|         ) |  | ||||||
|         feat.properties["__current_backgroun"] = "initial_value" |  | ||||||
|    } |    } | ||||||
| } | } | ||||||
|  | @ -1,104 +0,0 @@ | ||||||
| import { InputElement } from "./InputElement" |  | ||||||
| import Translations from "../i18n/Translations" |  | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" |  | ||||||
| import BaseUIElement from "../BaseUIElement" |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * @deprecated |  | ||||||
|  */ |  | ||||||
| export class DropDown<T> extends InputElement<T> { |  | ||||||
|     private static _nextDropdownId = 0 |  | ||||||
| 
 |  | ||||||
|     private readonly _element: HTMLElement |  | ||||||
| 
 |  | ||||||
|     private readonly _value: UIEventSource<T> |  | ||||||
|     private readonly _values: { value: T; shown: string | BaseUIElement }[] |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * |  | ||||||
|      * const dropdown = new DropDown<number>("test",[{value: 42, shown: "the answer"}]) |  | ||||||
|      * dropdown.GetValue().data // => 42
 |  | ||||||
|      */ |  | ||||||
|     constructor( |  | ||||||
|         label: string | BaseUIElement, |  | ||||||
|         values: { value: T; shown: string | BaseUIElement }[], |  | ||||||
|         value: UIEventSource<T> = undefined, |  | ||||||
|         options?: { |  | ||||||
|             select_class?: string |  | ||||||
|         } |  | ||||||
|     ) { |  | ||||||
|         super() |  | ||||||
|         value = value ?? new UIEventSource<T>(values[0].value) |  | ||||||
|         this._value = value |  | ||||||
|         this._values = values |  | ||||||
|         if (values.length <= 1) { |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         const id = DropDown._nextDropdownId |  | ||||||
|         DropDown._nextDropdownId++ |  | ||||||
| 
 |  | ||||||
|         const el = document.createElement("form") |  | ||||||
|         this._element = el |  | ||||||
|         el.id = "dropdown" + id |  | ||||||
| 
 |  | ||||||
|         { |  | ||||||
|             const labelEl = Translations.W(label)?.ConstructElement() |  | ||||||
|             if (labelEl !== undefined) { |  | ||||||
|                 const labelHtml = document.createElement("label") |  | ||||||
|                 labelHtml.appendChild(labelEl) |  | ||||||
|                 labelHtml.htmlFor = el.id |  | ||||||
|                 el.appendChild(labelHtml) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         options = options ?? {} |  | ||||||
|         options.select_class = |  | ||||||
|             options.select_class ?? "w-full bg-indigo-100 p-1 rounded hover:bg-indigo-200" |  | ||||||
| 
 |  | ||||||
|         { |  | ||||||
|             const select = document.createElement("select") |  | ||||||
|             select.classList.add(...(options.select_class.split(" ") ?? [])) |  | ||||||
|             for (let i = 0; i < values.length; i++) { |  | ||||||
|                 const option = document.createElement("option") |  | ||||||
|                 option.value = "" + i |  | ||||||
|                 option.appendChild(Translations.W(values[i].shown).ConstructElement()) |  | ||||||
|                 select.appendChild(option) |  | ||||||
|             } |  | ||||||
|             el.appendChild(select) |  | ||||||
| 
 |  | ||||||
|             select.onchange = () => { |  | ||||||
|                 const index = select.selectedIndex |  | ||||||
|                 value.setData(values[index].value) |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             value.addCallbackAndRun((selected) => { |  | ||||||
|                 for (let i = 0; i < values.length; i++) { |  | ||||||
|                     const value = values[i].value |  | ||||||
|                     if (value === selected) { |  | ||||||
|                         select.selectedIndex = i |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         this.onClick(() => {}) // by registering a click, the click event is consumed and doesn't bubble further to other elements, e.g. checkboxes
 |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<T> { |  | ||||||
|         return this._value |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(t: T): boolean { |  | ||||||
|         for (const value of this._values) { |  | ||||||
|             if (value.value === t) { |  | ||||||
|                 return true |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._element |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										64
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHCell.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  |   import { createEventDispatcher, onMount } from "svelte" | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * This is a pile of hacks to get the events working on mobile too | ||||||
|  |    */ | ||||||
|  |   export let wd: number | ||||||
|  |   export let h: number | ||||||
|  |   export let type: "full" | "half" | ||||||
|  |   let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>() | ||||||
|  |   let element: HTMLElement | ||||||
|  | 
 | ||||||
|  |   function send(signal: "start" | "end" | "move", ev: Event) { | ||||||
|  |     ev?.preventDefault() | ||||||
|  |     dispatch(signal) | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let lastElement: HTMLElement | ||||||
|  | 
 | ||||||
|  |   function elementUnderTouch(ev: TouchEvent): HTMLElement { | ||||||
|  |     for (const k in ev.targetTouches) { | ||||||
|  |       const touch = ev.targetTouches[k] | ||||||
|  |       if (touch.clientX === undefined || touch.clientY === undefined) { | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  |       const el = document.elementFromPoint(touch.clientX, touch.clientY) | ||||||
|  |       if (!el) { | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  |       lastElement = <any>el | ||||||
|  |       return <any>el | ||||||
|  |     } | ||||||
|  |     return lastElement | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   onMount(() => { | ||||||
|  |     element.addEventListener("mousedown", (ev) => send("start", ev)) | ||||||
|  |     element.onmouseenter = (ev) => send("move", ev) | ||||||
|  |     element.onmouseup = (ev) => send("end", ev) | ||||||
|  | 
 | ||||||
|  |     element.addEventListener("touchstart", ev => dispatch("start", ev)) | ||||||
|  |     element.addEventListener("touchend", ev => { | ||||||
|  | 
 | ||||||
|  |       const el = elementUnderTouch(ev) | ||||||
|  |       if (el?.onmouseup) { | ||||||
|  |         el?.onmouseup(<any>ev) | ||||||
|  |       }else{ | ||||||
|  |         dispatch("clear") | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     }) | ||||||
|  |     element.addEventListener("touchmove", ev => { | ||||||
|  |       elementUnderTouch(ev)?.onmouseenter(<any>ev) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd} | ||||||
|  |     class:border-black={(h + 1) % 6 === 0} | ||||||
|  |     class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `} | ||||||
|  | /> | ||||||
							
								
								
									
										238
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/UI/InputElement/Helpers/OpeningHours/OHTable.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,238 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 
 | ||||||
|  |   import { UIEventSource } from "../../../../Logic/UIEventSource" | ||||||
|  |   import type { OpeningHour } from "../../../OpeningHours/OpeningHours" | ||||||
|  |   import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours" | ||||||
|  |   import { Translation } from "../../../i18n/Translation" | ||||||
|  |   import Translations from "../../../i18n/Translations" | ||||||
|  |   import Tr from "../../../Base/Tr.svelte" | ||||||
|  |   import { Utils } from "../../../../Utils" | ||||||
|  |   import { onMount } from "svelte" | ||||||
|  |   import { TrashIcon } from "@babeard/svelte-heroicons/mini" | ||||||
|  |   import OHCell from "./OHCell.svelte" | ||||||
|  | 
 | ||||||
|  |   export let value: UIEventSource<OpeningHour[]> | ||||||
|  | 
 | ||||||
|  |   const wd = Translations.t.general.weekdays.abbreviations | ||||||
|  |   const days: Translation[] = [ | ||||||
|  |     wd.monday, | ||||||
|  |     wd.tuesday, | ||||||
|  |     wd.wednesday, | ||||||
|  |     wd.thursday, | ||||||
|  |     wd.friday, | ||||||
|  |     wd.saturday, | ||||||
|  |     wd.sunday, | ||||||
|  |   ] | ||||||
|  | 
 | ||||||
|  |   function range(n: number) { | ||||||
|  |     return Utils.TimesT(n, n => n) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function clearSelection() { | ||||||
|  |     const allCells = Array.from(document.getElementsByClassName("oh-timecell")) | ||||||
|  |     for (const timecell of allCells) { | ||||||
|  |       timecell.classList.remove("oh-timecell-selected") | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) { | ||||||
|  |     for (let wd = weekdayStart; wd <= weekdayEnd; wd++) { | ||||||
|  |       for (let h = (hourStart); h < (hourEnd); h++) { | ||||||
|  |         h = Math.floor(h) | ||||||
|  |         if (h >= hourStart && h < hourEnd) { | ||||||
|  |           const elFull = document.getElementById("oh-full-" + h + "-" + wd) | ||||||
|  |           elFull?.classList?.add("oh-timecell-selected") | ||||||
|  |         } | ||||||
|  |         if (h + 0.5 < hourEnd) { | ||||||
|  | 
 | ||||||
|  |           const elHalf = document.getElementById("oh-half-" + h + "-" + wd) | ||||||
|  |           elHalf?.classList?.add("oh-timecell-selected") | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) { | ||||||
|  |     let hourA = hourStart | ||||||
|  |     let hourB = hourEnd | ||||||
|  |     if (hourA > hourB) { | ||||||
|  |       hourA = hourEnd - 0.5 | ||||||
|  |       hourB = hourStart + 0.5 | ||||||
|  |     } | ||||||
|  |     if (hourA == hourB) { | ||||||
|  |       hourA -= 0.5 | ||||||
|  |       hourB += 0.5 | ||||||
|  |     } | ||||||
|  |     setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd), | ||||||
|  |       hourA, hourB) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let selectionStart: [number, number] = undefined | ||||||
|  | 
 | ||||||
|  |   function startSelection(weekday: number, hour: number) { | ||||||
|  |     selectionStart = [weekday, hour] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function endSelection(weekday: number, hour: number) { | ||||||
|  |     if (!selectionStart) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5) | ||||||
|  | 
 | ||||||
|  |     hour += 0.5 | ||||||
|  |     let start = Math.min(selectionStart[1], hour) | ||||||
|  |     let end = Math.max(selectionStart[1], hour) | ||||||
|  |     if (selectionStart[1] > hour) { | ||||||
|  |       end += 0.5 | ||||||
|  |       start -= 0.5 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (end === start) { | ||||||
|  |       end += 0.5 | ||||||
|  |       start -= 0.5 | ||||||
|  |     } | ||||||
|  |     let startMinutes = Math.round((start * 60) % 60) | ||||||
|  |     let endMinutes = Math.round((end * 60) % 60) | ||||||
|  |     let newOhs = [...value.data] | ||||||
|  |     for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) { | ||||||
|  | 
 | ||||||
|  |       const oh: OpeningHour = { | ||||||
|  |         startHour: Math.floor(start), | ||||||
|  |         endHour: Math.floor(end), | ||||||
|  |         startMinutes, | ||||||
|  |         endMinutes, | ||||||
|  |         weekday: wd, | ||||||
|  |       } | ||||||
|  |       newOhs.push(oh) | ||||||
|  |     } | ||||||
|  |     value.set(OpeningHours.MergeTimes(newOhs)) | ||||||
|  |     selectionStart = undefined | ||||||
|  |     clearSelection() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function moved(weekday: number, hour: number) { | ||||||
|  |     if (selectionStart) { | ||||||
|  |       clearSelection() | ||||||
|  |       setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let totalHeight = 0 | ||||||
|  | 
 | ||||||
|  |   onMount(() => { | ||||||
|  |     requestAnimationFrame(() => { | ||||||
|  |       const mondayMorning = document.getElementById("oh-full-" + 0 + "-" + 0) | ||||||
|  |       const sundayEvening = document.getElementById("oh-half-" + 23 + "-" + 6) | ||||||
|  |       const top = mondayMorning.getBoundingClientRect().top | ||||||
|  |       const bottom = sundayEvening.getBoundingClientRect().bottom | ||||||
|  |       totalHeight = bottom - top | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Determines 'top' and 'height-attributes, returns a CSS-string' | ||||||
|  |    * @param oh | ||||||
|  |    */ | ||||||
|  |   function rangeStyle(oh: OpeningHour, totalHeight: number): string { | ||||||
|  |     const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24 | ||||||
|  |     const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24 | ||||||
|  |     return `top: ${top}px; height: ${height}px; z-index: 20` | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <table class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0"> | ||||||
|  |   <tr> | ||||||
|  | 
 | ||||||
|  |     <th style="width: 9%"> | ||||||
|  |       <!-- Top-left cell --> | ||||||
|  |       <button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10"> | ||||||
|  |         <TrashIcon class="w-5 h-5" /> | ||||||
|  |       </button> | ||||||
|  |     </th> | ||||||
|  |     {#each days as wd} | ||||||
|  |       <th style="width: 13%"> | ||||||
|  |         <Tr cls="w-full" t={wd} /> | ||||||
|  |       </th> | ||||||
|  |     {/each} | ||||||
|  |   </tr> | ||||||
|  | 
 | ||||||
|  |   <tr class="h-0"> | ||||||
|  |     <!-- Virtual row to add the ranges to--> | ||||||
|  |     <td style="width: 9%" /> | ||||||
|  |     {#each range(7) as wd} | ||||||
|  |       <td style="width: 13%; position: relative;"> | ||||||
|  | 
 | ||||||
|  |         <div class="h-0 pointer-events-none" style="z-index: 10"> | ||||||
|  |           {#each $value.filter(oh => oh.weekday === wd) as range } | ||||||
|  |             <div class="absolute pointer-events-none px-1 md:px-2 w-full " | ||||||
|  |                  style={rangeStyle(range, totalHeight)} | ||||||
|  |             > | ||||||
|  |               <div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between"> | ||||||
|  |                 <div class:hidden={range.endHour - range.startHour < 3}> | ||||||
|  |                   {OpeningHours.hhmm(range.startHour, range.startMinutes)} | ||||||
|  |                 </div> | ||||||
|  |                 <button class="w-fit rounded-full p-1 self-center pointer-events-auto" | ||||||
|  |                         on:click={() => { | ||||||
|  |                           const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range)) | ||||||
|  |                           console.log("Cleaned", cleaned, value.data) | ||||||
|  |                   value.set(cleaned) | ||||||
|  |                 }}> | ||||||
|  |                   <TrashIcon class="w-6 h-6" /> | ||||||
|  |                 </button> | ||||||
|  |                 <div class:hidden={range.endHour - range.startHour < 3}> | ||||||
|  |                   {OpeningHours.hhmm(range.endHour, range.endMinutes)} | ||||||
|  | 
 | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  | 
 | ||||||
|  |             </div> | ||||||
|  |           {/each} | ||||||
|  |         </div> | ||||||
|  |       </td> | ||||||
|  | 
 | ||||||
|  |     {/each} | ||||||
|  | 
 | ||||||
|  |   </tr> | ||||||
|  | 
 | ||||||
|  |   {#each range(24) as h} | ||||||
|  |     <tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour --> | ||||||
|  |       <td rowspan={ h < 23 ? 2: 1 } | ||||||
|  |           class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive" | ||||||
|  |           style={ h < 23 ? "top: 0.75rem" : "height:0; top: 0.75rem"}> | ||||||
|  |         {#if h < 23} | ||||||
|  |           {h + 1}:00 | ||||||
|  |         {/if} | ||||||
|  |       </td> | ||||||
|  |       {#each range(7) as wd} | ||||||
|  |         <OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)} | ||||||
|  |                 on:move={() => moved(wd, h)} on:clear={() => clearSelection()} /> | ||||||
|  |       {/each} | ||||||
|  |     </tr> | ||||||
|  | 
 | ||||||
|  |     <tr style="height: 0.75rem"> <!-- odd row, for the half hour --> | ||||||
|  |       {#if h === 23} | ||||||
|  |         <td/> | ||||||
|  |         {/if} | ||||||
|  |       {#each range(7) as wd} | ||||||
|  |         <OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)} | ||||||
|  |                 on:move={() => moved(wd, h)} on:clear={() => clearSelection()} /> | ||||||
|  |       {/each} | ||||||
|  |     </tr> | ||||||
|  | 
 | ||||||
|  |   {/each} | ||||||
|  | 
 | ||||||
|  | </table> | ||||||
|  | <style> | ||||||
|  | 
 | ||||||
|  |     th { | ||||||
|  |         top: 0; | ||||||
|  |         position: sticky; | ||||||
|  |         z-index: 10; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -4,12 +4,39 @@ | ||||||
|    */ |    */ | ||||||
|   import { UIEventSource } from "../../../Logic/UIEventSource" |   import { UIEventSource } from "../../../Logic/UIEventSource" | ||||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" |   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||||
|   import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput" |   import OpeningHoursInput from "../../OpeningHours/OpeningHoursState" | ||||||
|   import PublicHolidaySelector from "../../OpeningHours/PublicHolidaySelector.svelte" |   import PublicHolidaySelector from "../../OpeningHours/PublicHolidaySelector.svelte" | ||||||
|  |   import OHTable from "./OpeningHours/OHTable.svelte" | ||||||
|  |   import OpeningHoursState from "../../OpeningHours/OpeningHoursState" | ||||||
|  |   import Popup from "../../Base/Popup.svelte" | ||||||
| 
 | 
 | ||||||
|   export let value: UIEventSource<string> |   export let value: UIEventSource<string> | ||||||
|   export let phSelectorValue = new UIEventSource("") |   export let args: string | ||||||
| </script> |   let prefix = "" | ||||||
|  |   let postfix = "" | ||||||
|  |   if (args) { | ||||||
|  |     try { | ||||||
| 
 | 
 | ||||||
| <ToSvelte construct={new OpeningHoursInput(value, phSelectorValue)} /> |       const data = JSON.stringify(args) | ||||||
| <PublicHolidaySelector value={phSelectorValue}/> |       if (data["prefix"]) { | ||||||
|  |         prefix = data["prefix"] | ||||||
|  |       } | ||||||
|  |       if (data["postfix"]) { | ||||||
|  |         postfix = data["postfix"] | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error("Could not parse arguments") | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const state = new OpeningHoursState(value) | ||||||
|  |   let expanded = new UIEventSource(false) | ||||||
|  | </script> | ||||||
|  | <Popup bodyPadding="p-0" shown={expanded}> | ||||||
|  |   <OHTable value={state.normalOhs} /> | ||||||
|  |   <div class="absolute w-full pointer-events-none bottom-0 flex justify-end"> | ||||||
|  |     <button on:click={() => expanded.set(false)} class="primary pointer-events-auto">Done</button> | ||||||
|  |   </div> | ||||||
|  | </Popup> | ||||||
|  | <button on:click={() => expanded.set(true)}>Pick opening hours</button> | ||||||
|  | <PublicHolidaySelector value={state.phSelectorValue} /> | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|   import type { ValidatorType } from "./Validators" |   import type { ValidatorType } from "./Validators" | ||||||
|   import InputHelpers from "./InputHelpers" |   import InputHelpers from "./InputHelpers" | ||||||
|   import ToSvelte from "../Base/ToSvelte.svelte" |  | ||||||
|   import type { Feature } from "geojson" |   import type { Feature } from "geojson" | ||||||
|   import ImageHelper from "./Helpers/ImageHelper.svelte" |   import ImageHelper from "./Helpers/ImageHelper.svelte" | ||||||
|   import TranslationInput from "./Helpers/TranslationInput.svelte" |   import TranslationInput from "./Helpers/TranslationInput.svelte" | ||||||
|  | @ -19,7 +18,6 @@ | ||||||
|   import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte" |   import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte" | ||||||
|   import SlopeInput from "./Helpers/SlopeInput.svelte" |   import SlopeInput from "./Helpers/SlopeInput.svelte" | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" |   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||||
|   import WikidataInput from "./Helpers/WikidataInput.svelte" |  | ||||||
|   import WikidataInputHelper from "./WikidataInputHelper.svelte" |   import WikidataInputHelper from "./WikidataInputHelper.svelte" | ||||||
| 
 | 
 | ||||||
|   export let type: ValidatorType |   export let type: ValidatorType | ||||||
|  | @ -48,7 +46,7 @@ | ||||||
| {:else if type === "simple_tag"} | {:else if type === "simple_tag"} | ||||||
|   <SimpleTagInput {value} {args} on:submit /> |   <SimpleTagInput {value} {args} on:submit /> | ||||||
| {:else if type === "opening_hours"} | {:else if type === "opening_hours"} | ||||||
|   <OpeningHoursInput {value} /> |   <OpeningHoursInput {value} {args} /> | ||||||
| {:else if type === "slope"} | {:else if type === "slope"} | ||||||
|   <SlopeInput {value} {feature} {state} /> |   <SlopeInput {value} {feature} {state} /> | ||||||
| {:else if type === "wikidata"} | {:else if type === "wikidata"} | ||||||
|  |  | ||||||
|  | @ -859,6 +859,9 @@ This list will be sorted | ||||||
|         return ranges |         return ranges | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public static isSame(a: OpeningHour, b: OpeningHour){ | ||||||
|  |         return a.weekday === b.weekday && a.startHour === b.startHour && a.startMinutes === b.startMinutes && a.endHour === b.endHour && a.endMinutes === b.endMinutes | ||||||
|  |     } | ||||||
|     private static multiply( |     private static multiply( | ||||||
|         weekdays: number[], |         weekdays: number[], | ||||||
|         timeranges: { |         timeranges: { | ||||||
|  |  | ||||||
|  | @ -1,33 +0,0 @@ | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" |  | ||||||
| import OpeningHoursPickerTable from "./OpeningHoursPickerTable" |  | ||||||
| import { OH, OpeningHour } from "./OpeningHours" |  | ||||||
| import { InputElement } from "../Input/InputElement" |  | ||||||
| 
 |  | ||||||
| export default class OpeningHoursPicker extends InputElement<OpeningHour[]> { |  | ||||||
|     private readonly _ohs: UIEventSource<OpeningHour[]> |  | ||||||
|     private readonly _backgroundTable: OpeningHoursPickerTable |  | ||||||
| 
 |  | ||||||
|     constructor(ohs: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([])) { |  | ||||||
|         super() |  | ||||||
|         this._ohs = ohs |  | ||||||
| 
 |  | ||||||
|         ohs.addCallback((oh) => { |  | ||||||
|             ohs.setData(OH.MergeTimes(oh)) |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
|         this._backgroundTable = new OpeningHoursPickerTable(this._ohs) |  | ||||||
|         this._backgroundTable.ConstructElement() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<OpeningHour[]> { |  | ||||||
|         return this._ohs |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(_: OpeningHour[]): boolean { |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._backgroundTable.ConstructElement() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,332 +0,0 @@ | ||||||
| /** |  | ||||||
|  * This is the base-table which is selectable by hovering over it. |  | ||||||
|  * It will genarate the currently selected opening hour. |  | ||||||
|  */ |  | ||||||
| import { UIEventSource } from "../../Logic/UIEventSource" |  | ||||||
| import { Utils } from "../../Utils" |  | ||||||
| import { OpeningHour } from "./OpeningHours" |  | ||||||
| import { InputElement } from "../Input/InputElement" |  | ||||||
| import Translations from "../i18n/Translations" |  | ||||||
| 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[]> { |  | ||||||
|     public static readonly days: 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, |  | ||||||
|     ] |  | ||||||
|     /* |  | ||||||
|     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") |  | ||||||
|     ) |  | ||||||
|     private readonly source: UIEventSource<OpeningHour[]> |  | ||||||
| 
 |  | ||||||
|     constructor(source?: UIEventSource<OpeningHour[]>) { |  | ||||||
|         super() |  | ||||||
|         this.source = source ?? new UIEventSource<OpeningHour[]>([]) |  | ||||||
|         this.SetClass("w-full block") |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(_: OpeningHour[]): boolean { |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     GetValue(): UIEventSource<OpeningHour[]> { |  | ||||||
|         return this.source |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         const table = document.createElement("table") |  | ||||||
|         table.classList.add("oh-table") |  | ||||||
|         table.classList.add("no-weblate") |  | ||||||
|         table.classList.add("relative") // Workaround for webkit-based viewers, see #1019
 |  | ||||||
| 
 |  | ||||||
|         const cellHeightInPx = 14 |  | ||||||
| 
 |  | ||||||
|         const headerRow = document.createElement("tr") |  | ||||||
|         headerRow.appendChild(document.createElement("th")) |  | ||||||
|         headerRow.classList.add("relative") |  | ||||||
|         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", "relative") |  | ||||||
| 
 |  | ||||||
|             // We need to round! The table height is rounded as following, we use this to calculate the actual number of pixels afterwards
 |  | ||||||
|             fullColumnSpan.style.height = cellHeightInPx * 48 + "px" |  | ||||||
| 
 |  | ||||||
|             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) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         table.appendChild(headerRow) |  | ||||||
| 
 |  | ||||||
|         const self = this |  | ||||||
|         for (let h = 0; h < 24; h++) { |  | ||||||
|             const hs = Utils.TwoDigits(h) |  | ||||||
|             const firstCell = document.createElement("td") |  | ||||||
|             firstCell.rowSpan = 2 |  | ||||||
|             firstCell.classList.add("oh-left-col", "oh-timecell-full", "border-box") |  | ||||||
|             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) |  | ||||||
|             } |  | ||||||
|             evenRow.style.height = cellHeightInPx + "px" |  | ||||||
|             evenRow.style.maxHeight = evenRow.style.height |  | ||||||
|             evenRow.style.minHeight = evenRow.style.height |  | ||||||
|             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) |  | ||||||
|             } |  | ||||||
|             oddRow.style.minHeight = evenRow.style.height |  | ||||||
|             oddRow.style.maxHeight = evenRow.style.height |  | ||||||
| 
 |  | ||||||
|             table.appendChild(oddRow) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /**** Event handling below ***/ |  | ||||||
| 
 |  | ||||||
|         let mouseIsDown = false |  | ||||||
|         let selectionStart: [number, number] = undefined |  | ||||||
|         let selectionEnd: [number, number] = undefined |  | ||||||
| 
 |  | ||||||
|         function h(timeSegment: number) { |  | ||||||
|             return Math.floor(timeSegment / 2) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function m(timeSegment: number) { |  | ||||||
|             return (timeSegment % 2) * 30 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function startSelection(i: number, j: number) { |  | ||||||
|             mouseIsDown = true |  | ||||||
|             selectionStart = [i, j] |  | ||||||
|             selectionEnd = [i, j] |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function endSelection() { |  | ||||||
|             if (selectionStart === undefined) { |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             if (!mouseIsDown) { |  | ||||||
|                 return |  | ||||||
|             } |  | ||||||
|             mouseIsDown = false |  | ||||||
|             const dStart = Math.min(selectionStart[1], selectionEnd[1]) |  | ||||||
|             const dEnd = Math.max(selectionStart[1], selectionEnd[1]) |  | ||||||
|             const timeStart = Math.min(selectionStart[0], selectionEnd[0]) - 1 |  | ||||||
|             const timeEnd = Math.max(selectionStart[0], selectionEnd[0]) - 1 |  | ||||||
|             for (let weekday = dStart; weekday <= dEnd; weekday++) { |  | ||||||
|                 const oh: OpeningHour = { |  | ||||||
|                     weekday: weekday, |  | ||||||
|                     startHour: h(timeStart), |  | ||||||
|                     startMinutes: m(timeStart), |  | ||||||
|                     endHour: h(timeEnd + 1), |  | ||||||
|                     endMinutes: m(timeEnd + 1), |  | ||||||
|                 } |  | ||||||
|                 if (oh.endHour > 23) { |  | ||||||
|                     oh.endHour = 24 |  | ||||||
|                     oh.endMinutes = 0 |  | ||||||
|                 } |  | ||||||
|                 self.source.data.push(oh) |  | ||||||
|             } |  | ||||||
|             self.source.ping() |  | ||||||
| 
 |  | ||||||
|             // Clear the highlighting
 |  | ||||||
|             let header = table.rows[0] |  | ||||||
|             for (let j = 1; j < header.cells.length; j++) { |  | ||||||
|                 header.cells[j].classList?.remove("oh-timecol-selected") |  | ||||||
|             } |  | ||||||
|             for (let i = 1; i < table.rows.length; i++) { |  | ||||||
|                 let row = table.rows[i] |  | ||||||
|                 for (let j = 0; j < row.cells.length; j++) { |  | ||||||
|                     let cell = row.cells[j] |  | ||||||
|                     cell?.classList?.remove("oh-timecell-selected") |  | ||||||
|                     row.classList?.remove("oh-timerow-selected") |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         table.onmouseup = () => { |  | ||||||
|             endSelection() |  | ||||||
|         } |  | ||||||
|         table.onmouseleave = () => { |  | ||||||
|             endSelection() |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         let lastSelectionIend, lastSelectionJEnd |  | ||||||
| 
 |  | ||||||
|         function selectAllBetween(iEnd, jEnd) { |  | ||||||
|             if (lastSelectionIend === iEnd && lastSelectionJEnd === jEnd) { |  | ||||||
|                 return // We already did this
 |  | ||||||
|             } |  | ||||||
|             lastSelectionIend = iEnd |  | ||||||
|             lastSelectionJEnd = jEnd |  | ||||||
| 
 |  | ||||||
|             let iStart = selectionStart[0] |  | ||||||
|             let jStart = selectionStart[1] |  | ||||||
| 
 |  | ||||||
|             if (iStart > iEnd) { |  | ||||||
|                 const h = iStart |  | ||||||
|                 iStart = iEnd |  | ||||||
|                 iEnd = h |  | ||||||
|             } |  | ||||||
|             if (jStart > jEnd) { |  | ||||||
|                 const h = jStart |  | ||||||
|                 jStart = jEnd |  | ||||||
|                 jEnd = h |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let header = table.rows[0] |  | ||||||
|             for (let j = 1; j < header.cells.length; j++) { |  | ||||||
|                 let cell = header.cells[j] |  | ||||||
|                 cell.classList?.remove("oh-timecol-selected-round-left") |  | ||||||
|                 cell.classList?.remove("oh-timecol-selected-round-right") |  | ||||||
| 
 |  | ||||||
|                 if (jStart + 1 <= j && j <= jEnd + 1) { |  | ||||||
|                     cell.classList?.add("oh-timecol-selected") |  | ||||||
|                     if (jStart + 1 == j) { |  | ||||||
|                         cell.classList?.add("oh-timecol-selected-round-left") |  | ||||||
|                     } |  | ||||||
|                     if (jEnd + 1 == j) { |  | ||||||
|                         cell.classList?.add("oh-timecol-selected-round-right") |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     cell.classList?.remove("oh-timecol-selected") |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (let i = 1; i < table.rows.length; i++) { |  | ||||||
|                 let row = table.rows[i] |  | ||||||
|                 if (iStart <= i && i <= iEnd) { |  | ||||||
|                     row.classList?.add("oh-timerow-selected") |  | ||||||
|                 } else { |  | ||||||
|                     row.classList?.remove("oh-timerow-selected") |  | ||||||
|                 } |  | ||||||
|                 for (let j = 0; j < row.cells.length; j++) { |  | ||||||
|                     let cell = row.cells[j] |  | ||||||
|                     if (cell === undefined) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     let offset = 0 |  | ||||||
|                     if (i % 2 == 1) { |  | ||||||
|                         if (j == 0) { |  | ||||||
|                             // This is the first column of a full hour -> This is the time indication (skip)
 |  | ||||||
|                             continue |  | ||||||
|                         } |  | ||||||
|                         offset = -1 |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     if (iStart <= i && i <= iEnd && jStart <= j + offset && j + offset <= jEnd) { |  | ||||||
|                         cell?.classList?.add("oh-timecell-selected") |  | ||||||
|                     } else { |  | ||||||
|                         cell?.classList?.remove("oh-timecell-selected") |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (let i = 1; i < table.rows.length; i++) { |  | ||||||
|             let row = table.rows[i] |  | ||||||
|             for (let j = 0; j < row.cells.length; j++) { |  | ||||||
|                 let cell = row.cells[j] |  | ||||||
|                 let offset = 0 |  | ||||||
|                 if (i % 2 == 1) { |  | ||||||
|                     if (j == 0) { |  | ||||||
|                         continue |  | ||||||
|                     } |  | ||||||
|                     offset = -1 |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 cell.onmousedown = (ev) => { |  | ||||||
|                     ev.preventDefault() |  | ||||||
|                     startSelection(i, j + offset) |  | ||||||
|                     selectAllBetween(i, j + offset) |  | ||||||
|                 } |  | ||||||
|                 cell.ontouchstart = (ev) => { |  | ||||||
|                     ev.preventDefault() |  | ||||||
|                     startSelection(i, j + offset) |  | ||||||
|                     selectAllBetween(i, j + offset) |  | ||||||
|                 } |  | ||||||
|                 cell.onmouseenter = () => { |  | ||||||
|                     if (mouseIsDown) { |  | ||||||
|                         selectionEnd = [i, j + offset] |  | ||||||
|                         selectAllBetween(i, j + offset) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 cell.ontouchmove = (ev: TouchEvent) => { |  | ||||||
|                     ev.preventDefault() |  | ||||||
|                     for (const k in ev.targetTouches) { |  | ||||||
|                         const touch = ev.targetTouches[k] |  | ||||||
|                         if (touch.clientX === undefined || touch.clientY === undefined) { |  | ||||||
|                             continue |  | ||||||
|                         } |  | ||||||
|                         const elUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY) |  | ||||||
|                         // @ts-ignore
 |  | ||||||
|                         const f = elUnderTouch.onmouseenter |  | ||||||
|                         if (f) { |  | ||||||
|                             f() |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 cell.ontouchend = (ev) => { |  | ||||||
|                     ev.preventDefault() |  | ||||||
|                     endSelection() |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return table |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -38,7 +38,7 @@ export default class OpeningHoursRange extends BaseUIElement { | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|         let content: BaseUIElement |         let content: BaseUIElement | ||||||
|         if (height > 2) { |         if (height > 3) { | ||||||
|             content = new Combine([startTime, deleteRange, endTime]).SetClass( |             content = new Combine([startTime, deleteRange, endTime]).SetClass( | ||||||
|                 "flex flex-col h-full justify-between" |                 "flex flex-col h-full justify-between" | ||||||
|             ) |             ) | ||||||
|  | @ -55,6 +55,10 @@ export default class OpeningHoursRange extends BaseUIElement { | ||||||
|         return el |         return el | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Gets the relative height, in number of hours to display | ||||||
|  |      * Range: ]0 - 24] | ||||||
|  |      */ | ||||||
|     private getHeight(): number { |     private getHeight(): number { | ||||||
|         const oh = this._oh |         const oh = this._oh | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,30 +3,19 @@ | ||||||
|  * Keeps track of unparsed rules |  * Keeps track of unparsed rules | ||||||
|  * Exports everything conveniently as a string, for direct use |  * Exports everything conveniently as a string, for direct use | ||||||
|  */ |  */ | ||||||
| import OpeningHoursPicker from "./OpeningHoursPicker" |  | ||||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||||
| import { VariableUiElement } from "../Base/VariableUIElement" |  | ||||||
| import Combine from "../Base/Combine" |  | ||||||
| import { FixedUiElement } from "../Base/FixedUiElement" |  | ||||||
| import { OH, OpeningHour } from "./OpeningHours" | import { OH, OpeningHour } from "./OpeningHours" | ||||||
| import { InputElement } from "../Input/InputElement" |  | ||||||
| import Translations from "../i18n/Translations" |  | ||||||
| import BaseUIElement from "../BaseUIElement" |  | ||||||
| import SvelteUIElement from "../Base/SvelteUIElement" |  | ||||||
| import PublicHolidaySelector from "./PublicHolidaySelector.svelte" |  | ||||||
| 
 | 
 | ||||||
| export default class OpeningHoursInput extends InputElement<string> { | export default class OpeningHoursState { | ||||||
|     private readonly _value: UIEventSource<string> |     public readonly normalOhs: UIEventSource<OpeningHour[]> | ||||||
|     private readonly _element: BaseUIElement |     public readonly leftoverRules: Store<string[]> | ||||||
|  |     public readonly phSelectorValue: UIEventSource<string> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         value: UIEventSource<string> = new UIEventSource<string>(""), |         value: UIEventSource<string> = new UIEventSource<string>(""), | ||||||
|         phSelectorValue: UIEventSource<string> = new UIEventSource<string>(undefined), |  | ||||||
|         prefix = "", |         prefix = "", | ||||||
|         postfix = "" |         postfix = "", | ||||||
|     ) { |     ) { | ||||||
|         super() |  | ||||||
|         this._value = value |  | ||||||
|         let valueWithoutPrefix = value |         let valueWithoutPrefix = value | ||||||
|         if (prefix !== "" && postfix !== "") { |         if (prefix !== "" && postfix !== "") { | ||||||
|             valueWithoutPrefix = value.sync( |             valueWithoutPrefix = value.sync( | ||||||
|  | @ -55,11 +44,11 @@ export default class OpeningHoursInput extends InputElement<string> { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                     return prefix + noPrefix + postfix |                     return prefix + noPrefix + postfix | ||||||
|                 } |                 }, | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const leftoverRules: Store<string[]> = valueWithoutPrefix.map((str) => { |         this.leftoverRules = valueWithoutPrefix.map((str) => { | ||||||
|             if (str === undefined) { |             if (str === undefined) { | ||||||
|                 return [] |                 return [] | ||||||
|             } |             } | ||||||
|  | @ -89,25 +78,25 @@ export default class OpeningHoursInput extends InputElement<string> { | ||||||
|                 break |                 break | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         this.phSelectorValue = new UIEventSource<string>(ph ?? "") | ||||||
| 
 | 
 | ||||||
|         phSelectorValue.set(ph ?? "") |  | ||||||
| 
 | 
 | ||||||
|         // Note: MUST be bound AFTER the leftover rules!
 |         // Note: MUST be bound AFTER the leftover rules!
 | ||||||
|         const rulesFromOhPicker: UIEventSource<OpeningHour[]> = valueWithoutPrefix.sync( |         this.normalOhs = valueWithoutPrefix.sync( | ||||||
|             (str) => { |             (str) => { | ||||||
|                 return OH.Parse(str) |                 return OH.Parse(str) | ||||||
|             }, |             }, | ||||||
|             [leftoverRules, phSelectorValue], |             [this.leftoverRules, this.phSelectorValue], | ||||||
|             (rules, oldString) => { |             (rules, oldString) => { | ||||||
|                 // We always add a ';', to easily add new rules. We remove the ';' again at the end of the function
 |                 // We always add a ';', to easily add new rules. We remove the ';' again at the end of the function
 | ||||||
|                 // Important: spaces are _not_ allowed after a ';' as it'll destabilize the parsing!
 |                 // Important: spaces are _not_ allowed after a ';' as it'll destabilize the parsing!
 | ||||||
|                 let str = OH.ToString(rules) + ";" |                 let str = OH.ToString(rules) + ";" | ||||||
|                 const ph = phSelectorValue.data |                 const ph = this.phSelectorValue.data | ||||||
|                 if (ph) { |                 if (ph) { | ||||||
|                     str += ph + ";" |                     str += ph + ";" | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 str += leftoverRules.data.join(";") + ";" |                 str += this.leftoverRules.data.join(";") + ";" | ||||||
| 
 | 
 | ||||||
|                 str = str.trim() |                 str = str.trim() | ||||||
|                 while (str.endsWith(";")) { |                 while (str.endsWith(";")) { | ||||||
|  | @ -122,9 +111,9 @@ export default class OpeningHoursInput extends InputElement<string> { | ||||||
|                     return oldString // We pass a reference to the old string to stabilize the EventSource
 |                     return oldString // We pass a reference to the old string to stabilize the EventSource
 | ||||||
|                 } |                 } | ||||||
|                 return str |                 return str | ||||||
|             } |             }, | ||||||
|         ) |         ) | ||||||
| 
 |         /* | ||||||
|                 const leftoverWarning = new VariableUiElement( |                 const leftoverWarning = new VariableUiElement( | ||||||
|                     leftoverRules.map((leftovers: string[]) => { |                     leftoverRules.map((leftovers: string[]) => { | ||||||
|                         if (leftovers.length == 0) { |                         if (leftovers.length == 0) { | ||||||
|  | @ -137,25 +126,9 @@ export default class OpeningHoursInput extends InputElement<string> { | ||||||
|                             ), |                             ), | ||||||
|                         ]) |                         ]) | ||||||
|                     }) |                     }) | ||||||
|         ) |                 )*/ | ||||||
| 
 | 
 | ||||||
|         const ohPicker = new OpeningHoursPicker(rulesFromOhPicker) |  | ||||||
| 
 | 
 | ||||||
|         this._element = new Combine([ |  | ||||||
|             leftoverWarning, |  | ||||||
|             ohPicker, |  | ||||||
|         ]) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     GetValue(): UIEventSource<string> { |  | ||||||
|         return this._value |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     IsValid(_: string): boolean { |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected InnerConstructElement(): HTMLElement { |  | ||||||
|         return this._element.ConstructElement() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | @ -1,4 +1,40 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | import OHTable from "./InputElement/Helpers/OpeningHours/OHTable.svelte" | ||||||
|  | import { UIEventSource } from "../Logic/UIEventSource" | ||||||
|  | import type { OpeningHour } from "./OpeningHours/OpeningHours" | ||||||
|  | export let value: UIEventSource<OpeningHour[]> = new UIEventSource<OpeningHour[]>([ | ||||||
|  |   { | ||||||
|  |     weekday: 3, | ||||||
|  |     startMinutes: 0, | ||||||
|  |     endMinutes: 0, | ||||||
|  |     startHour: 12, | ||||||
|  |     endHour: 16 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     weekday: 0, | ||||||
|  |     startMinutes: 0, | ||||||
|  |     endMinutes: 0, | ||||||
|  |     startHour: 0, | ||||||
|  |     endHour: 24 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     weekday: 1, | ||||||
|  |     startMinutes: 0, | ||||||
|  |     endMinutes: 0, | ||||||
|  |     startHour: 1, | ||||||
|  |     endHour: 24 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     weekday: 2, | ||||||
|  |     startMinutes: 0, | ||||||
|  |     endMinutes: 0, | ||||||
|  |     startHour: 12, | ||||||
|  |     endHour: 24 | ||||||
|  |   } | ||||||
|  | ]) | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <main /> | <main > | ||||||
|  |   <OHTable {value}/> | ||||||
|  | </main> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue