diff --git a/Logic/OpeningHours.ts b/Logic/OpeningHours.ts index 21dcc19..82dfef0 100644 --- a/Logic/OpeningHours.ts +++ b/Logic/OpeningHours.ts @@ -1,9 +1,111 @@ export interface OpeningHour { - weekdayStart: number, // 0 is monday, 1 is tuesday, ... - weekdayEnd: number, + weekday: number, // 0 is monday, 1 is tuesday, ... startHour: number, startMinutes: number, endHour: number, endMinutes: number } +export class OpeningHourUtils { + /** + * Merge duplicate opening-hour element in place. + * Returns true if something changed + * @param ohs + * @constructor + */ + public static MergeTimes(ohs: OpeningHour[]): OpeningHour[] { + const queue = [...ohs]; + const newList = []; + while (queue.length > 0) { + let maybeAdd = queue.pop(); + + let doAddEntry = true; + if(maybeAdd.weekday == undefined){ + doAddEntry = false; + } + + for (let i = newList.length - 1; i >= 0 && doAddEntry; i--) { + let guard = newList[i]; + if (maybeAdd.weekday != guard.weekday) { + // Not the same day + continue + } + + if (OpeningHourUtils.startTimeLiesInRange(maybeAdd, guard) && OpeningHourUtils.endTimeLiesInRange(maybeAdd, guard)) { + // Guard fully covers 'maybeAdd': we can safely ignore maybeAdd + doAddEntry = false; + break; + } + + if (OpeningHourUtils.startTimeLiesInRange(guard, maybeAdd) && OpeningHourUtils.endTimeLiesInRange(guard, maybeAdd)) { + // 'maybeAdd' fully covers Guard - the guard is killed + newList.splice(i, 1); + break; + } + + if (OpeningHourUtils.startTimeLiesInRange(maybeAdd, guard) || OpeningHourUtils.endTimeLiesInRange(maybeAdd, guard) + || OpeningHourUtils.startTimeLiesInRange(guard, maybeAdd) || OpeningHourUtils.endTimeLiesInRange(guard, maybeAdd)) { + // At this point, the maybeAdd overlaps the guard: we should extend the guard and retest it + newList.splice(i, 1); + let startHour = guard.startHour; + let startMinutes = guard.startMinutes; + if(OpeningHourUtils.startTime(maybeAdd)OpeningHourUtils.endTime(guard)){ + endHour = maybeAdd.endHour; + endMinutes = maybeAdd.endMinutes; + } + + queue.push({ + startHour: startHour, + startMinutes: startMinutes, + endHour:endHour, + endMinutes:endMinutes, + weekday: guard.weekday + }); + + doAddEntry = false; + break; + } + + } + if (doAddEntry) { + newList.push(maybeAdd); + } + } + + // New list can only differ from the old list by merging entries + // This means that the list is changed only if the lengths are different. + // If the lengths are the same, we might just as well return the old list and be a bit more stable + if (newList.length !== ohs.length) { + return newList; + } else { + return ohs; + } + + } + + private static startTime(oh: OpeningHour): number { + return oh.startHour + oh.startMinutes / 60; + } + + private static endTime(oh: OpeningHour): number { + return oh.endHour + oh.endMinutes / 60; + } + + public static startTimeLiesInRange(checked: OpeningHour, mightLieIn: OpeningHour) { + return OpeningHourUtils.startTime(mightLieIn) <= OpeningHourUtils.startTime(checked) && + OpeningHourUtils.startTime(checked) <= OpeningHourUtils.endTime(mightLieIn) + } + + public static endTimeLiesInRange(checked: OpeningHour, mightLieIn: OpeningHour) { + return OpeningHourUtils.startTime(mightLieIn) <= OpeningHourUtils.endTime(checked) && + OpeningHourUtils.endTime(checked) <= OpeningHourUtils.endTime(mightLieIn) + } +} + diff --git a/UI/Input/NumberField.ts b/UI/Input/NumberField.ts new file mode 100644 index 0000000..d825bdf --- /dev/null +++ b/UI/Input/NumberField.ts @@ -0,0 +1,123 @@ +import {UIElement} from "../UIElement"; +import {InputElement} from "./InputElement"; +import Translations from "../i18n/Translations"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export class NumberField extends InputElement { + private readonly value: UIEventSource; + public readonly enterPressed = new UIEventSource(undefined); + private readonly _placeholder: UIElement; + private options?: { + placeholder?: string | UIElement, + value?: UIEventSource, + isValid?: ((i: number) => boolean), + min?: number, + max?: number + }; + public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly _isValid: (i:number) => boolean; + + constructor(options?: { + placeholder?: string | UIElement, + value?: UIEventSource, + isValid?: ((i:number) => boolean), + min?: number, + max?:number + }) { + super(undefined); + this.options = options; + const self = this; + this.value = new UIEventSource(undefined); + this.value = options?.value ?? new UIEventSource(undefined); + + this._isValid = options.isValid ?? ((i) => true); + + this._placeholder = Translations.W(options.placeholder ?? ""); + this.ListenTo(this._placeholder._source); + + this.onClick(() => { + self.IsSelected.setData(true) + }); + this.value.addCallback((t) => { + const field = document.getElementById("txt-"+this.id); + if (field === undefined || field === null) { + return; + } + field.className = self.IsValid(t) ? "" : "invalid"; + + if (t === undefined || t === null) { + return; + } + // @ts-ignore + field.value = t; + }); + this.dumbMode = false; + } + + GetValue(): UIEventSource { + return this.value; + } + + InnerRender(): string { + + const placeholder = this._placeholder.InnerRender().replace("'", "'"); + + let min = ""; + if(this.options.min){ + min = `min='${this.options.min}'`; + } + + let max = ""; + if(this.options.min){ + max = `max='${this.options.max}'`; + } + + return `
` + + `` + + `
`; + } + + InnerUpdate() { + const field = document.getElementById("txt-" + this.id); + const self = this; + field.oninput = () => { + + // How much characters are on the right, not including spaces? + // @ts-ignore + const endDistance = field.value.substring(field.selectionEnd).replace(/ /g,'').length; + // @ts-ignore + let val: number = Number(field.value); + if (!self.IsValid(val)) { + self.value.setData(undefined); + } else { + self.value.setData(val); + } + + }; + + if (this.value.data !== undefined && this.value.data !== null) { + // @ts-ignore + field.value = this.value.data; + } + + field.addEventListener("focusin", () => self.IsSelected.setData(true)); + field.addEventListener("focusout", () => self.IsSelected.setData(false)); + + + field.addEventListener("keyup", function (event) { + if (event.key === "Enter") { + // @ts-ignore + self.enterPressed.setData(field.value); + } + }); + + } + + IsValid(t: number): boolean { + if (t === undefined || t === null) { + return false + } + return this._isValid(t); + } + +} \ No newline at end of file diff --git a/UI/Input/OpeningHours/OpeningHoursPicker.ts b/UI/Input/OpeningHours/OpeningHoursPicker.ts index 521dcd8..1ae9663 100644 --- a/UI/Input/OpeningHours/OpeningHoursPicker.ts +++ b/UI/Input/OpeningHours/OpeningHoursPicker.ts @@ -1,215 +1,73 @@ -/** - * 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 "../../../Logic/OpeningHours"; +import {UIElement} from "../../UIElement"; import {InputElement} from "../InputElement"; +import {OpeningHour, OpeningHourUtils} from "../../../Logic/OpeningHours"; +import {UIEventSource} from "../../../Logic/UIEventSource"; +import OpeningHoursPickerTable from "./OpeningHoursPickerTable"; +import OpeningHoursRange from "./OpeningHoursRange"; +import Combine from "../../Base/Combine"; -export default class OpeningHoursPicker extends InputElement { - public readonly IsSelected: UIEventSource; +export default class OpeningHoursPicker extends InputElement { + private readonly _ohs: UIEventSource; + public readonly IsSelected: UIEventSource = new UIEventSource(false); - public static readonly days = ["Maan", "Din", "Woe", "Don", "Vrij", "Zat", "Zon"]; + private readonly _backgroundTable: OpeningHoursPickerTable; - private readonly source: UIEventSource; + private readonly _weekdays: UIEventSource = new UIEventSource([]); - constructor(source: UIEventSource = undefined) { + constructor(ohs: UIEventSource) { super(); - this.source = source ?? new UIEventSource(undefined); - this.IsSelected = new UIEventSource(false); - this.SetStyle("width:100%;height:100%;display:block;") + this._ohs = ohs; + this._backgroundTable = new OpeningHoursPickerTable(this._weekdays); + const self = this; + + this._backgroundTable.GetValue().addCallback(oh => { + if (oh) { + ohs.data.push(oh); + ohs.ping(); + } + }); + + this._ohs.addCallback(ohs => { + self._ohs.setData(OpeningHourUtils.MergeTimes(ohs)); + }) + + ohs.addCallback(ohs => { + const perWeekday: UIElement[][] = []; + + for (let i = 0; i < 7; i++) { + perWeekday[i] = []; + } + + for (const oh of ohs) { + const source = new UIEventSource(oh) + source.addCallback(_ => { + self._ohs.setData(OpeningHourUtils.MergeTimes(self._ohs.data)) + }) + const r = new OpeningHoursRange(source); + perWeekday[oh.weekday].push(r); + } + + for (let i = 0; i < 7; i++) { + self._weekdays.data[i] = new Combine(perWeekday[i]); + } + self._weekdays.ping(); + + + }); + } InnerRender(): string { - let rows = ""; - for (let h = 0; h < 24; h++) { - let hs = "" + h; - if (hs.length == 1) { - hs = "0" + hs; - } - rows += `${hs}:00` + - Utils.Times('', 7) + - '' + - // Utils.Times('', 7) + - // '' + - Utils.Times('', 7) + - // '' + - // Utils.Times('', 7) + - ''; - } - let days = OpeningHoursPicker.days.join(""); - return `${rows}
${days}
`; + return this._backgroundTable.Render(); } - protected InnerUpdate() { - const self = this; - const table = (document.getElementById(`oh-table-${this.id}`) as HTMLTableElement); - if (table === undefined || table === null) { - return; - } - - let mouseIsDown = false; - let selectionStart: [number, number] = undefined; - let selectionEnd: [number, number] = undefined; - - function h(timeSegment: number) { - return Math.floor(timeSegment / 4); - } - - function m(timeSegment: number) { - return (timeSegment % 2) * 30; - } - - function startSelection(i: number, j: number, cell: HTMLElement) { - mouseIsDown = true; - selectionStart = [i, j]; - selectionEnd = [i, j]; - cell.classList.add("oh-timecell-selected") - } - - function endSelection() { - if (selectionStart === undefined) { - 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; - const oh: OpeningHour = { - weekdayStart: dStart, - weekdayEnd: dEnd, - startHour: h(timeStart), - startMinutes: m(timeStart), - endHour: h(timeEnd + 1), - endMinutes: m(timeEnd + 1) - } - self.source.setData(oh); - } - - table.onmouseup = () => { - endSelection(); - }; - table.onmouseleave = () => { - endSelection(); - }; - - function selectAllBetween(iEnd, 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; - } - - 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; - } - 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, cell) - selectAllBetween(i, j + offset); - } - cell.ontouchstart = (ev) => { - ev.preventDefault(); - startSelection(i, j + offset, cell); - 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]; - const elUnderTouch = document.elementFromPoint( - touch.screenX, - touch.screenY - ); - // @ts-ignore - const f = elUnderTouch.onmouseenter; - if (f) { - f(); - } - } - - } - - cell.ontouchend = (ev) => { - ev.preventDefault(); - for (const k in ev.targetTouches) { - const touch = ev.targetTouches[k]; - const elUnderTouch = document.elementFromPoint( - touch.pageX, - touch.pageY - ); - // @ts-ignore - const f = elUnderTouch.onmouseup; - if (f) { - f(); - } - } - } - } - - } - - + GetValue(): UIEventSource { + return this._ohs } - IsValid(t: OpeningHour): boolean { + + IsValid(t: OpeningHour[]): boolean { return true; } - GetValue(): UIEventSource { - return this.source; - } - } \ No newline at end of file diff --git a/UI/Input/OpeningHours/OpeningHoursPickerTable.ts b/UI/Input/OpeningHours/OpeningHoursPickerTable.ts new file mode 100644 index 0000000..00f9f0f --- /dev/null +++ b/UI/Input/OpeningHours/OpeningHoursPickerTable.ts @@ -0,0 +1,248 @@ +import {InputElement} from "../InputElement"; +import {OpeningHour} from "../../../Logic/OpeningHours"; +import {UIEventSource} from "../../../Logic/UIEventSource"; +import {Utils} from "../../../Utils"; +import {UIElement} from "../../UIElement"; + +/** + * This is the base-table which is selectable by hovering over it. + * It will genarate the currently selected opening hour. + */ +export default class OpeningHoursPickerTable extends InputElement { + public readonly IsSelected: UIEventSource; + private readonly weekdays: UIEventSource; + + public static readonly days = ["Maan", "Din", "Woe", "Don", "Vrij", "Zat", "Zon"]; + + private readonly source: UIEventSource; + + + constructor(weekdays: UIEventSource, source?: UIEventSource) { + super(weekdays); + this.weekdays = weekdays; + this.source = source ?? new UIEventSource(undefined); + this.IsSelected = new UIEventSource(false); + this.SetStyle("width:100%;height:100%;display:block;"); + + } + + InnerRender(): string { + let rows = ""; + const self = this; + for (let h = 0; h < 24; h++) { + let hs = "" + h; + if (hs.length == 1) { + hs = "0" + hs; + } + + + rows += `${hs}:00` + + Utils.Times(weekday => { + let innerContent = ""; + if (h == 0) { + innerContent = self.weekdays.data[weekday]?.Render() ?? ""; + } + return `
${innerContent}`; + }, 7) + + '' + + Utils.Times(id => `
`, 7) + + ''; + } + let days = OpeningHoursPickerTable.days.join(""); + return `${rows}
${days}
`; + } + + protected InnerUpdate() { + const self = this; + const table = (document.getElementById(`oh-table-${this.id}`) as HTMLTableElement); + if (table === undefined || table === null) { + return; + } + + for (const uielement of this.weekdays.data) { + uielement.Update(); + } + + 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.setData(oh); + } + + // Clear the highlighting + 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") + } + } + } + + table.onmouseup = () => { + endSelection(); + }; + table.onmouseleave = () => { + endSelection(); + }; + + function selectAllBetween(iEnd, 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; + } + + 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] + if (cell === undefined) { + continue; + } + let offset = 0; + if (i % 2 == 1) { + if (j == 0) { + 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].getElementsByClassName("oh-timecell-inner")[0] as HTMLElement + 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]; + const elUnderTouch = document.elementFromPoint( + touch.screenX, + touch.screenY + ); + // @ts-ignore + const f = elUnderTouch.onmouseenter; + if (f) { + f(); + } + } + + } + + cell.ontouchend = (ev) => { + ev.preventDefault(); + for (const k in ev.targetTouches) { + const touch = ev.targetTouches[k]; + const elUnderTouch = document.elementFromPoint( + touch.pageX, + touch.pageY + ); + // @ts-ignore + const f = elUnderTouch.onmouseup; + if (f) { + f(); + } + } + } + } + + } + + + } + + IsValid(t: OpeningHour): boolean { + return true; + } + + GetValue(): UIEventSource { + return this.source; + } + +} \ No newline at end of file diff --git a/UI/Input/OpeningHours/OpeningHoursRange.ts b/UI/Input/OpeningHours/OpeningHoursRange.ts index 66fbe30..9f1c4ac 100644 --- a/UI/Input/OpeningHours/OpeningHoursRange.ts +++ b/UI/Input/OpeningHours/OpeningHoursRange.ts @@ -1,19 +1,178 @@ import {UIElement} from "../../UIElement"; +import {UIEventSource} from "../../../Logic/UIEventSource"; +import {OpeningHour} from "../../../Logic/OpeningHours"; +import {TextField} from "../TextField"; +import Combine from "../../Base/Combine"; +import {Utils} from "../../../Utils"; +import {FixedUiElement} from "../../Base/FixedUiElement"; /** * A single opening hours range, shown on top of the OH-picker table */ -export default class OpeningHoursRange extends UIElement{ - private _parentCell: HTMLElement; - constructor(parentCell : HTMLElement) { - super(); - this._parentCell = parentCell; +export default class OpeningHoursRange extends UIElement { + private _oh: UIEventSource; + + private _startTime: TextField; + private _endTime: TextField; + private _deleteRange: UIElement; + + constructor(oh: UIEventSource) { + super(oh); + const self = this; + this._oh = oh; + this.SetClass("oh-timerange"); + oh.addCallbackAndRun(oh => { + const el = document.getElementById(this.id) as HTMLElement; + self.InnerUpdate(el); + }) + + + this._deleteRange = new FixedUiElement("") + .SetClass("oh-delete-range") + .onClick(() => { + oh.data.weekday = undefined; + oh.ping(); + }); + + this._startTime = new TextField({ + value: oh.map(oh => { + if (oh) { + return Utils.TwoDigits(oh.startHour) + ":" + Utils.TwoDigits(oh.startMinutes); + } + }), + htmlType: "time" + }); + + this._endTime = new TextField({ + value: oh.map(oh => { + if (oh) { + if (oh.endHour == 24) { + return "00:00"; + } + return Utils.TwoDigits(oh.endHour) + ":" + Utils.TwoDigits(oh.endMinutes); + } + }), + htmlType: "time" + }); + + + function applyStartTime() { + if (self._startTime.GetValue().data === undefined) { + return; + } + const spl = self._startTime.GetValue().data.split(":"); + oh.data.startHour = Number(spl[0]); + oh.data.startMinutes = Number(spl[1]); + + if (oh.data.startHour >= oh.data.endHour) { + if (oh.data.startMinutes + 10 >= oh.data.endMinutes) { + oh.data.endHour = oh.data.startHour + 1; + oh.data.endMinutes = oh.data.startMinutes; + if (oh.data.endHour > 23) { + oh.data.endHour = 24; + oh.data.endMinutes = 0; + oh.data.startHour = Math.min(oh.data.startHour, 23); + oh.data.startMinutes = Math.min(oh.data.startMinutes, 45); + } + } + } + + oh.ping(); + } + + function applyEndTime() { + if (self._endTime.GetValue().data === undefined) { + return; + } + const spl = self._endTime.GetValue().data.split(":"); + let newEndHour = Number(spl[0]); + const newEndMinutes = Number(spl[1]); + if (newEndHour == 0 && newEndMinutes == 0) { + newEndHour = 24; + } + + if (newEndHour == oh.data.endMinutes && newEndMinutes == oh.data.endMinutes) { + // NOthing to change + return; + } + + oh.data.endHour = newEndHour; + oh.data.endMinutes = newEndMinutes; + + oh.ping(); + } + + this._startTime.GetValue().addCallbackAndRun(startTime => { + const spl = startTime.split(":"); + if (spl[0].startsWith('0') || spl[1].startsWith('0')) { + return; + } + applyStartTime(); + }); + + this._endTime.GetValue().addCallbackAndRun(endTime => { + const spl = endTime.split(":"); + if (spl[0].startsWith('0') || spl[1].startsWith('0')) { + return; + } + applyEndTime() + }); + this._startTime.enterPressed.addCallback(() => { + applyStartTime(); + }); + this._endTime.enterPressed.addCallbackAndRun(() => { + applyEndTime(); + }) + + this._startTime.IsSelected.addCallback(isSelected => { + if (!isSelected) { + applyStartTime(); + } + }); + + this._endTime.IsSelected.addCallback(isSelected => { + if (!isSelected) { + applyEndTime(); + } + }) - } + InnerRender(): string { - this.SetStyle(`display:block;position:absolute;top:0;left:0;width:100%;background:blue;height:${this._parentCell.offsetHeight*2}px`) - return "Hi"; + const oh = this._oh.data; + if (oh === undefined) { + return ""; + } + const height = this.getHeight(); + return new Combine([this._startTime, this._deleteRange, this._endTime]) + .SetClass(height < 2 ? "oh-timerange-inner-small" : "oh-timerange-inner") + .Render(); } + private getHeight(): number { + const oh = this._oh.data; + + let endhour = oh.endHour; + if (oh.endHour == 0 && oh.endMinutes == 0) { + endhour = 24; + } + const height = (endhour - oh.startHour + ((oh.endMinutes - oh.startMinutes) / 60)); + return height; + } + + protected InnerUpdate(el: HTMLElement) { + if (el == null) { + return; + } + const oh = this._oh.data; + if (oh === undefined) { + return; + } + const height = this.getHeight(); + el.style.height = `${height * 200}%` + const upperDiff = (oh.startHour + oh.startMinutes / 60); + el.style.marginTop = `${2 * upperDiff * el.parentElement.offsetHeight - upperDiff*0.75}px`; + } + + } \ No newline at end of file diff --git a/UI/Input/TextField.ts b/UI/Input/TextField.ts index d1d2524..4aa96b5 100644 --- a/UI/Input/TextField.ts +++ b/UI/Input/TextField.ts @@ -8,7 +8,7 @@ export class TextField extends InputElement { public readonly enterPressed = new UIEventSource(undefined); private readonly _placeholder: UIElement; public readonly IsSelected: UIEventSource = new UIEventSource(false); - private readonly _isArea: boolean; + private readonly _htmlType: string; private readonly _textAreaRows: number; private readonly _isValid: (string, country) => boolean; @@ -17,6 +17,7 @@ export class TextField extends InputElement { placeholder?: string | UIElement, value?: UIEventSource, textArea?: boolean, + htmlType?: string, textAreaRows?: number, isValid?: ((s: string, country?: string) => boolean) }) { @@ -24,7 +25,7 @@ export class TextField extends InputElement { const self = this; this.value = new UIEventSource(""); options = options ?? {}; - this._isArea = options.textArea ?? false; + this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text"); this.value = options?.value ?? new UIEventSource(undefined); this._textAreaRows = options.textAreaRows; @@ -58,15 +59,15 @@ export class TextField extends InputElement { InnerRender(): string { - if (this._isArea) { + if (this._htmlType === "area") { return `` } const placeholder = this._placeholder.InnerRender().replace("'", "'"); - return `
` + - `` + - `
`; + return `
` + + `` + + `
`; } InnerUpdate() { @@ -121,6 +122,9 @@ export class TextField extends InputElement { } public SetCursorPosition(i: number) { + if(this._htmlType !== "text" && this._htmlType !== "area"){ + return; + } const field = document.getElementById('txt-' + this.id); if(field === undefined || field === null){ return; diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 5d534ab..8586084 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -14,7 +14,7 @@ interface TextFieldDef { explanation: string, isValid: ((s: string, country?: string) => boolean), reformat?: ((s: string, country?: string) => string), - inputHelper?: (value:UIEventSource) => InputElement + inputHelper?: (value:UIEventSource) => InputElement, } export default class ValidatedTextField { diff --git a/Utils.ts b/Utils.ts index 21b7dd2..3a85418 100644 --- a/Utils.ts +++ b/Utils.ts @@ -32,10 +32,17 @@ export class Utils { return str.substr(0, 1).toUpperCase() + str.substr(1); } - public static Times(str: string, count: number): string { + public static TwoDigits(i: number) { + if (i < 10) { + return "0" + i; + } + return "" + i; + } + + public static Times(f: ((i: number) => string), count: number): string { let res = ""; for (let i = 0; i < count; i++) { - res += str; + res += f(i); } return res; } @@ -194,4 +201,5 @@ This is around ${secsPerCS} seconds/changeset.
The next million (still ${st } ); } + } diff --git a/css/openinghourstable.css b/css/openinghourstable.css index 73d0872..a54c700 100644 --- a/css/openinghourstable.css +++ b/css/openinghourstable.css @@ -15,33 +15,35 @@ vertical-align: top; } -.oh-timecell:hover { - background-color: lightsalmon !important; +.oh-timecell-inner:hover { + background-color: #ffd1be; } .oh-timecell { background-color: white; border-left: 1px solid #eee; border-right: 1px solid #eee; + position: relative; + box-sizing: border-box; } .oh-timecell-selected { - background-color: red; + background-color: orange; } -.oh-timecell-half { +.oh-timecell-half .oh-timecell-inner{ border-top: 0.5px solid #eee } -.oh-timecell-half.oh-timecell-selected { +.oh-timecell-half.oh-timecell-selected .oh-timecell-inner { border-top: 0.5px solid lightsalmon; } -.oh-timecell-full { - border-top: 1px solid #aaa +.oh-timecell-full .oh-timecell-inner{ + border-top: 1px solid #ccc } -.oh-timecell-full.oh-timecell-selected { +.oh-timecell-full.oh-timecell-selected .oh-timecell-inner { border-top: 1px solid lightsalmon; } @@ -55,3 +57,70 @@ background: #ddd; } +.oh-draggable-header { + background-color: blue; + height: 0.5em; +} + +.oh-timecell-inner { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: absolute; +} + +.oh-timerange { + border-radius: 0.5em; + margin: 2px; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + background: orange; + z-index: 1; + box-sizing: border-box +} + +.oh-timerange-inner { + display: flex; + flex-direction: column; + justify-content: space-between; + align-content: center; + height: 100%; +} + +.oh-timerange-inner input { + width: calc(100% - 2em); + box-sizing: border-box; + margin-left: 1em; + margin-right:1em; + } + +.oh-timerange-inner-small { + display: flex; + flex-direction: row; + justify-content: space-between; + height: 100%; + width:100%; +} + +.oh-timerange-inner-small input { + width: min-content; + box-sizing: border-box; + margin-left: 1em; + margin-right:1em; +} + +.oh-delete-range{ + width: 1.5em; + height: 1.5em; + background:black; + border-radius:0.75em; +} + +.oh-delete-range img { + height: 100%; + max-width: 2em; +} \ No newline at end of file diff --git a/test.ts b/test.ts index 9d0b37c..f7208b2 100644 --- a/test.ts +++ b/test.ts @@ -1,8 +1,11 @@ -/* +//* import {VariableUiElement} from "./UI/Base/VariableUIElement"; -import OpeningHoursPicker from "./UI/Input/OpeningHoursPicker"; +import OpeningHoursRange from "./UI/Input/OpeningHours/OpeningHoursRange"; +import {UIEventSource} from "./Logic/UIEventSource"; +import OpeningHoursPicker from "./UI/Input/OpeningHours/OpeningHoursPicker"; +import {OpeningHour} from "./Logic/OpeningHours"; -let oh = new OpeningHoursPicker(); +let oh = new OpeningHoursPicker(new UIEventSource([])); oh.SetStyle("height:100vh;display:block;").AttachTo('maindiv'); oh.GetValue().addCallback(data => console.log(data)) @@ -15,6 +18,7 @@ new VariableUiElement(oh.GetValue().map(oh => { oh.weekdayEnd + " " + oh.endHour + ":" + oh.endMinutes })).AttachTo("extradiv"); + /*/ diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index e8e0bea..4c2944d 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -13,6 +13,7 @@ import {TagRenderingOptions} from "../Customizations/TagRenderingOptions"; import {UIEventSource} from "../Logic/UIEventSource"; import {TagRendering} from "../UI/TagRendering"; import {Basemap} from "../Logic/Leaflet/Basemap"; +import {OpeningHour, OpeningHourUtils} from "../Logic/OpeningHours"; new T([ @@ -121,5 +122,55 @@ new T([ equal(true, rendered.indexOf("Niet toegankelijk") > 0) } - ], + ], [ + "Merge touching opening hours", + () => { + const oh1: OpeningHour = { + weekday: 0, + startHour: 10, + startMinutes: 0, + endHour: 11, + endMinutes: 0 + }; + const oh0: OpeningHour = { + weekday: 0, + startHour: 11, + startMinutes: 0, + endHour: 12, + endMinutes: 0 + }; + + const merged = OpeningHourUtils.MergeTimes([oh0, oh1]); + const r = merged[0]; + equal( merged.length, 1); + equal(r.startHour,10 ); + equal(r.endHour, 12) + + } + ], [ + "Merge overlapping opening hours", + () => { + const oh1: OpeningHour = { + weekday: 0, + startHour: 10, + startMinutes: 0, + endHour: 11, + endMinutes: 0 + }; + const oh0: OpeningHour = { + weekday: 0, + startHour: 10, + startMinutes: 30, + endHour: 12, + endMinutes: 0 + }; + + const merged = OpeningHourUtils.MergeTimes([oh0, oh1]); + const r = merged[0]; + equal( merged.length, 1); + equal(r.startHour,10 ); + equal(r.endHour, 12) + + } + ] ]);