More work on opening hours

This commit is contained in:
Pieter Vander Vennet 2020-10-04 01:04:46 +02:00
parent 9970c4b8bb
commit d1f286f466
11 changed files with 855 additions and 229 deletions

View file

@ -1,9 +1,111 @@
export interface OpeningHour { export interface OpeningHour {
weekdayStart: number, // 0 is monday, 1 is tuesday, ... weekday: number, // 0 is monday, 1 is tuesday, ...
weekdayEnd: number,
startHour: number, startHour: number,
startMinutes: number, startMinutes: number,
endHour: number, endHour: number,
endMinutes: 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.startTime(guard)){
startHour = maybeAdd.startHour;
startMinutes = maybeAdd.startMinutes;
}
let endHour = guard.endHour;
let endMinutes = guard.endMinutes;
if(OpeningHourUtils.endTime(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)
}
}

123
UI/Input/NumberField.ts Normal file
View file

@ -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<number> {
private readonly value: UIEventSource<number>;
public readonly enterPressed = new UIEventSource<string>(undefined);
private readonly _placeholder: UIElement;
private options?: {
placeholder?: string | UIElement,
value?: UIEventSource<number>,
isValid?: ((i: number) => boolean),
min?: number,
max?: number
};
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _isValid: (i:number) => boolean;
constructor(options?: {
placeholder?: string | UIElement,
value?: UIEventSource<number>,
isValid?: ((i:number) => boolean),
min?: number,
max?:number
}) {
super(undefined);
this.options = options;
const self = this;
this.value = new UIEventSource<number>(undefined);
this.value = options?.value ?? new UIEventSource<number>(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<number> {
return this.value;
}
InnerRender(): string {
const placeholder = this._placeholder.InnerRender().replace("'", "&#39");
let min = "";
if(this.options.min){
min = `min='${this.options.min}'`;
}
let max = "";
if(this.options.min){
max = `max='${this.options.max}'`;
}
return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
`<input type='number' ${min} ${max} placeholder='${placeholder}' id='txt-${this.id}'>` +
`</form></span>`;
}
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);
}
}

View file

@ -1,215 +1,73 @@
/** import {UIElement} from "../../UIElement";
* 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 {InputElement} from "../InputElement"; 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<OpeningHour> { export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
public readonly IsSelected: UIEventSource<boolean>; private readonly _ohs: UIEventSource<OpeningHour[]>;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public static readonly days = ["Maan", "Din", "Woe", "Don", "Vrij", "Zat", "Zon"]; private readonly _backgroundTable: OpeningHoursPickerTable;
private readonly source: UIEventSource<OpeningHour>; private readonly _weekdays: UIEventSource<UIElement[]> = new UIEventSource<UIElement[]>([]);
constructor(source: UIEventSource<OpeningHour> = undefined) { constructor(ohs: UIEventSource<OpeningHour[]>) {
super(); super();
this.source = source ?? new UIEventSource<OpeningHour>(undefined); this._ohs = ohs;
this.IsSelected = new UIEventSource<boolean>(false); this._backgroundTable = new OpeningHoursPickerTable(this._weekdays);
this.SetStyle("width:100%;height:100%;display:block;") 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<OpeningHour>(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 { InnerRender(): string {
let rows = ""; return this._backgroundTable.Render();
for (let h = 0; h < 24; h++) {
let hs = "" + h;
if (hs.length == 1) {
hs = "0" + hs;
}
rows += `<tr><td rowspan="2" class="oh-left-col oh-timecell-full">${hs}:00</td>` +
Utils.Times('<td class="oh-timecell oh-timecell-full"></td>', 7) +
'</tr><tr>' +
// Utils.Times('<td class="oh-timecell"></td>', 7) +
// '</tr><tr>' +
Utils.Times('<td class="oh-timecell oh-timecell-half"></td>', 7) +
// '</tr><tr>' +
// Utils.Times('<td class="oh-timecell"></td>', 7) +
'</tr>';
}
let days = OpeningHoursPicker.days.join("</th><th>");
return `<table id="oh-table-${this.id}" class="oh-table"><tr><th></th><th>${days}</tr>${rows}</table>`;
} }
protected InnerUpdate() { GetValue(): UIEventSource<OpeningHour[]> {
const self = this; return this._ohs
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) => { IsValid(t: OpeningHour[]): boolean {
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();
}
}
}
}
}
}
IsValid(t: OpeningHour): boolean {
return true; return true;
} }
GetValue(): UIEventSource<OpeningHour> {
return this.source;
}
} }

View file

@ -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<OpeningHour> {
public readonly IsSelected: UIEventSource<boolean>;
private readonly weekdays: UIEventSource<UIElement[]>;
public static readonly days = ["Maan", "Din", "Woe", "Don", "Vrij", "Zat", "Zon"];
private readonly source: UIEventSource<OpeningHour>;
constructor(weekdays: UIEventSource<UIElement[]>, source?: UIEventSource<OpeningHour>) {
super(weekdays);
this.weekdays = weekdays;
this.source = source ?? new UIEventSource<OpeningHour>(undefined);
this.IsSelected = new UIEventSource<boolean>(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 += `<tr><td rowspan="2" class="oh-left-col oh-timecell-full">${hs}:00</td>` +
Utils.Times(weekday => {
let innerContent = "";
if (h == 0) {
innerContent = self.weekdays.data[weekday]?.Render() ?? "";
}
return `<td id="${this.id}-timecell-${weekday}-${h}" class="oh-timecell oh-timecell-full"><div class="oh-timecell-inner"></div>${innerContent}</td>`;
}, 7) +
'</tr><tr>' +
Utils.Times(id => `<td id="${this.id}-timecell-${id}-${h}-30" class="oh-timecell oh-timecell-half"><div class="oh-timecell-inner"></div></td>`, 7) +
'</tr>';
}
let days = OpeningHoursPickerTable.days.join("</th><th>");
return `<table id="oh-table-${this.id}" class="oh-table"><tr><th></th><th>${days}</tr>${rows}</table>`;
}
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<OpeningHour> {
return this.source;
}
}

View file

@ -1,19 +1,178 @@
import {UIElement} from "../../UIElement"; 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 * A single opening hours range, shown on top of the OH-picker table
*/ */
export default class OpeningHoursRange extends UIElement{ export default class OpeningHoursRange extends UIElement {
private _parentCell: HTMLElement; private _oh: UIEventSource<OpeningHour>;
constructor(parentCell : HTMLElement) {
super();
this._parentCell = parentCell;
private _startTime: TextField;
private _endTime: TextField;
private _deleteRange: UIElement;
constructor(oh: UIEventSource<OpeningHour>) {
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("<img src='./assets/delete.svg'>")
.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 { InnerRender(): string {
this.SetStyle(`display:block;position:absolute;top:0;left:0;width:100%;background:blue;height:${this._parentCell.offsetHeight*2}px`) const oh = this._oh.data;
return "Hi"; 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`;
}
} }

View file

@ -8,7 +8,7 @@ export class TextField extends InputElement<string> {
public readonly enterPressed = new UIEventSource<string>(undefined); public readonly enterPressed = new UIEventSource<string>(undefined);
private readonly _placeholder: UIElement; private readonly _placeholder: UIElement;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _isArea: boolean; private readonly _htmlType: string;
private readonly _textAreaRows: number; private readonly _textAreaRows: number;
private readonly _isValid: (string, country) => boolean; private readonly _isValid: (string, country) => boolean;
@ -17,6 +17,7 @@ export class TextField extends InputElement<string> {
placeholder?: string | UIElement, placeholder?: string | UIElement,
value?: UIEventSource<string>, value?: UIEventSource<string>,
textArea?: boolean, textArea?: boolean,
htmlType?: string,
textAreaRows?: number, textAreaRows?: number,
isValid?: ((s: string, country?: string) => boolean) isValid?: ((s: string, country?: string) => boolean)
}) { }) {
@ -24,7 +25,7 @@ export class TextField extends InputElement<string> {
const self = this; const self = this;
this.value = new UIEventSource<string>(""); this.value = new UIEventSource<string>("");
options = options ?? {}; options = options ?? {};
this._isArea = options.textArea ?? false; this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text");
this.value = options?.value ?? new UIEventSource<string>(undefined); this.value = options?.value ?? new UIEventSource<string>(undefined);
this._textAreaRows = options.textAreaRows; this._textAreaRows = options.textAreaRows;
@ -58,15 +59,15 @@ export class TextField extends InputElement<string> {
InnerRender(): string { InnerRender(): string {
if (this._isArea) { if (this._htmlType === "area") {
return `<span id="${this.id}"><textarea id="txt-${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>` return `<span id="${this.id}"><textarea id="txt-${this.id}" class="form-text-field" rows="${this._textAreaRows}" cols="50" style="max-width: 100%; width: 100%; box-sizing: border-box"></textarea></span>`
} }
const placeholder = this._placeholder.InnerRender().replace("'", "&#39"); const placeholder = this._placeholder.InnerRender().replace("'", "&#39");
return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` + return `<div id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
`<input type='text' placeholder='${placeholder}' id='txt-${this.id}'>` + `<input type='${this._htmlType}' placeholder='${placeholder}' id='txt-${this.id}'/>` +
`</form></span>`; `</form></div>`;
} }
InnerUpdate() { InnerUpdate() {
@ -121,6 +122,9 @@ export class TextField extends InputElement<string> {
} }
public SetCursorPosition(i: number) { public SetCursorPosition(i: number) {
if(this._htmlType !== "text" && this._htmlType !== "area"){
return;
}
const field = document.getElementById('txt-' + this.id); const field = document.getElementById('txt-' + this.id);
if(field === undefined || field === null){ if(field === undefined || field === null){
return; return;

View file

@ -14,7 +14,7 @@ interface TextFieldDef {
explanation: string, explanation: string,
isValid: ((s: string, country?: string) => boolean), isValid: ((s: string, country?: string) => boolean),
reformat?: ((s: string, country?: string) => string), reformat?: ((s: string, country?: string) => string),
inputHelper?: (value:UIEventSource<string>) => InputElement<string> inputHelper?: (value:UIEventSource<string>) => InputElement<string>,
} }
export default class ValidatedTextField { export default class ValidatedTextField {

View file

@ -32,10 +32,17 @@ export class Utils {
return str.substr(0, 1).toUpperCase() + str.substr(1); 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 = ""; let res = "";
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
res += str; res += f(i);
} }
return res; return res;
} }
@ -194,4 +201,5 @@ This is around ${secsPerCS} seconds/changeset.<br/> The next million (still ${st
} }
); );
} }
} }

View file

@ -15,33 +15,35 @@
vertical-align: top; vertical-align: top;
} }
.oh-timecell:hover { .oh-timecell-inner:hover {
background-color: lightsalmon !important; background-color: #ffd1be;
} }
.oh-timecell { .oh-timecell {
background-color: white; background-color: white;
border-left: 1px solid #eee; border-left: 1px solid #eee;
border-right: 1px solid #eee; border-right: 1px solid #eee;
position: relative;
box-sizing: border-box;
} }
.oh-timecell-selected { .oh-timecell-selected {
background-color: red; background-color: orange;
} }
.oh-timecell-half { .oh-timecell-half .oh-timecell-inner{
border-top: 0.5px solid #eee 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; border-top: 0.5px solid lightsalmon;
} }
.oh-timecell-full { .oh-timecell-full .oh-timecell-inner{
border-top: 1px solid #aaa 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; border-top: 1px solid lightsalmon;
} }
@ -55,3 +57,70 @@
background: #ddd; 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;
}

10
test.ts
View file

@ -1,8 +1,11 @@
/* //*
import {VariableUiElement} from "./UI/Base/VariableUIElement"; 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<OpeningHour[]>([]));
oh.SetStyle("height:100vh;display:block;").AttachTo('maindiv'); oh.SetStyle("height:100vh;display:block;").AttachTo('maindiv');
oh.GetValue().addCallback(data => console.log(data)) oh.GetValue().addCallback(data => console.log(data))
@ -15,6 +18,7 @@ new VariableUiElement(oh.GetValue().map(oh => {
oh.weekdayEnd + " " + oh.endHour + ":" + oh.endMinutes oh.weekdayEnd + " " + oh.endHour + ":" + oh.endMinutes
})).AttachTo("extradiv"); })).AttachTo("extradiv");
/*/ /*/

View file

@ -13,6 +13,7 @@ import {TagRenderingOptions} from "../Customizations/TagRenderingOptions";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {TagRendering} from "../UI/TagRendering"; import {TagRendering} from "../UI/TagRendering";
import {Basemap} from "../Logic/Leaflet/Basemap"; import {Basemap} from "../Logic/Leaflet/Basemap";
import {OpeningHour, OpeningHourUtils} from "../Logic/OpeningHours";
new T([ new T([
@ -121,5 +122,55 @@ new T([
equal(true, rendered.indexOf("Niet toegankelijk") > 0) 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)
}
]
]); ]);