More work on opening hours
This commit is contained in:
parent
9970c4b8bb
commit
d1f286f466
11 changed files with 855 additions and 229 deletions
|
@ -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.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
123
UI/Input/NumberField.ts
Normal 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("'", "'");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OpeningHour> {
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
export default class OpeningHoursPicker extends InputElement<OpeningHour[]> {
|
||||
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();
|
||||
this.source = source ?? new UIEventSource<OpeningHour>(undefined);
|
||||
this.IsSelected = new UIEventSource<boolean>(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<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 {
|
||||
let rows = "";
|
||||
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>`;
|
||||
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<OpeningHour[]> {
|
||||
return this._ohs
|
||||
}
|
||||
|
||||
IsValid(t: OpeningHour): boolean {
|
||||
|
||||
IsValid(t: OpeningHour[]): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<OpeningHour> {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
}
|
248
UI/Input/OpeningHours/OpeningHoursPickerTable.ts
Normal file
248
UI/Input/OpeningHours/OpeningHoursPickerTable.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OpeningHour>;
|
||||
|
||||
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 {
|
||||
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`;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ export class TextField extends InputElement<string> {
|
|||
public readonly enterPressed = new UIEventSource<string>(undefined);
|
||||
private readonly _placeholder: UIElement;
|
||||
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(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<string> {
|
|||
placeholder?: string | UIElement,
|
||||
value?: UIEventSource<string>,
|
||||
textArea?: boolean,
|
||||
htmlType?: string,
|
||||
textAreaRows?: number,
|
||||
isValid?: ((s: string, country?: string) => boolean)
|
||||
}) {
|
||||
|
@ -24,7 +25,7 @@ export class TextField extends InputElement<string> {
|
|||
const self = this;
|
||||
this.value = new UIEventSource<string>("");
|
||||
options = options ?? {};
|
||||
this._isArea = options.textArea ?? false;
|
||||
this._htmlType = options.textArea ? "area" : (options.htmlType ?? "text");
|
||||
this.value = options?.value ?? new UIEventSource<string>(undefined);
|
||||
|
||||
this._textAreaRows = options.textAreaRows;
|
||||
|
@ -58,15 +59,15 @@ export class TextField extends InputElement<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>`
|
||||
}
|
||||
|
||||
const placeholder = this._placeholder.InnerRender().replace("'", "'");
|
||||
|
||||
return `<span id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
|
||||
`<input type='text' placeholder='${placeholder}' id='txt-${this.id}'>` +
|
||||
`</form></span>`;
|
||||
return `<div id="${this.id}"><form onSubmit='return false' class='form-text-field'>` +
|
||||
`<input type='${this._htmlType}' placeholder='${placeholder}' id='txt-${this.id}'/>` +
|
||||
`</form></div>`;
|
||||
}
|
||||
|
||||
InnerUpdate() {
|
||||
|
@ -121,6 +122,9 @@ export class TextField extends InputElement<string> {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -14,7 +14,7 @@ interface TextFieldDef {
|
|||
explanation: string,
|
||||
isValid: ((s: string, country?: string) => boolean),
|
||||
reformat?: ((s: string, country?: string) => string),
|
||||
inputHelper?: (value:UIEventSource<string>) => InputElement<string>
|
||||
inputHelper?: (value:UIEventSource<string>) => InputElement<string>,
|
||||
}
|
||||
|
||||
export default class ValidatedTextField {
|
||||
|
|
12
Utils.ts
12
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.<br/> The next million (still ${st
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
10
test.ts
10
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<OpeningHour[]>([]));
|
||||
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");
|
||||
|
||||
|
||||
/*/
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
|
Loading…
Reference in a new issue