frontend-dali/source/widgets/weekview/logic.ts

1055 lines
24 KiB
TypeScript

/*
This file is part of »dali«.
Copyright 2025 'kcf' <fenris@folksprak.org>
»dali« is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
»dali« is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with »dali«. If not, see <http://www.gnu.org/licenses/>.
*/
namespace _dali.widgets.weekview
{
/**
*/
type type_get_entries = (
(
from_pit : lib_plankton.pit.type_pit,
to_pit : lib_plankton.pit.type_pit,
calendar_ids : Array<_dali.type_calendar_id>
)
=>
Promise<Array<_dali.type_event_object_extended>>
);
/**
*/
export class class_widget_weekview implements lib_plankton.zoo_widget.interface_widget
{
/**
* [dependency]
*/
private get_entries : type_get_entries;
/**
* [hook]
*/
private action_select_event : (
(
event_key : _dali.type_event_key
)
=>
void
);
/**
* [hook]
*/
private action_select_day : (
(
date : lib_plankton.pit.type_date
)
=>
void
);
/**
* [state]
*/
private vertical : boolean;
/**
* [state]
*/
private year : int;
/**
* [state]
*/
private week : int;
/**
* [state]
*/
private count : int;
/**
* [state]
*/
private event_map : lib_plankton.map.type_map<
_dali.type_event_key,
{
element : HTMLElement;
hash : string;
}
>;
/**
* [state]
*/
private container : (null | Element);
/**
*/
public constructor(
get_entries : type_get_entries,
{
"action_select_day": action_select_day = ((date) => {}),
"action_select_event": action_select_event = ((event_key) => {}),
"vertical": vertical = false,
"initial_year": initial_year = null,
"initial_week": initial_week = null,
"initial_count": initial_count = 5,
}
:
{
action_select_event ?: (
(
event_key : _dali.type_event_key
)
=>
void
);
action_select_day ?: (
(
date : lib_plankton.pit.type_date
)
=>
void
);
vertical ?: boolean;
initial_year ?: (null | int);
initial_week ?: (null | int);
initial_count ?: int;
}
=
{}
)
{
// dependencies
this.get_entries = get_entries;
// hooks
this.action_select_day = action_select_day;
this.action_select_event = action_select_event;
// state
const ywd_now : lib_plankton.pit.type_ywd = lib_plankton.pit.to_ywd(lib_plankton.pit.now());
this.vertical = vertical;
this.year = (
initial_year
??
ywd_now.year
);
this.week = (
initial_week
??
Math.max(0, (ywd_now.week - 1))
);
this.count = initial_count;
this.event_map = lib_plankton.map.hashmap.implementation_map(
lib_plankton.map.hashmap.make<
_dali.type_event_key,
{
element : HTMLElement;
hash : string;
}
>(
event_key => event_key
)
);
this.container = null;
}
/**
* some kind of checksum for comparing entries
* @todo base64 encode?
* @todo sha256 hash?
*/
private static entry_hash(
entry : _dali.type_event_object_extended
) : string
{
return lib_plankton.call.convey(
{
"calendar_id": entry.calendar_id,
"calendar_name": entry.calendar_name,
"hue": Math.floor(entry.hue * 0xFFFF),
"access_level": entry.access_level,
"event_object": entry.event_object,
},
[
x => lib_plankton.json.encode(x),
]
);
}
/**
*/
private static event_generate_tooltip(
calendar_name : string,
event_object : _dali.type_event_object
) : string
{
return (
lib_plankton.string.coin(
"[{{calendar_name}}] {{event_name}}\n",
{
"calendar_name": calendar_name,
"event_name": event_object.name,
}
)
+
"--\n"
+
(
(event_object.begin.time !== null)
?
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": lib_plankton.translate.get("event.when"),
"value": lib_plankton.pit.timespan_format(
event_object.begin,
event_object.end,
{
"timezone_indicator": lib_plankton.translate.get("common.timezone_indicator"),
"adjust_to_ce": true,
"show_timezone": true,
"omit_date": true,
}
),
}
)
:
""
)
+
(
(event_object.location !== null)
?
(
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": lib_plankton.translate.get("event.location"),
"value": event_object.location,
}
)
)
:
""
)
+
(
(event_object.link !== null)
?
(
lib_plankton.string.coin(
"{{label}}: {{value}}\n",
{
"label": lib_plankton.translate.get("event.link"),
"value": event_object.link,
}
)
)
:
""
)
/*
+
(
(event_object.description !== null)
?
(
"--\n"
+
lib_plankton.string.coin(
"{{description}}\n",
{
"description": event_object.description,
}
)
)
:
""
)
*/
);
}
/**
*/
private async get_entries_wrapped(
{
"calendar_ids": calendar_ids = null,
"timezone_shift": timezone_shift = 0,
}
:
{
calendar_ids ?: (null | Array<_dali.type_calendar_id>);
timezone_shift ?: int;
}
=
{
}
)
: Promise<Array<_dali.type_event_object_extended>>
{
const entries = await this.get_entries(
lib_plankton.pit.from_ywd(
{
"year": this.year,
"week": this.week,
"day": 1,
},
{
"timezone_shift": timezone_shift,
}
),
lib_plankton.pit.from_ywd(
{
"year": this.year,
"week": (this.week + this.count),
"day": 1,
},
{
"timezone_shift": timezone_shift,
}
),
calendar_ids
);
entries.sort(
(entry1, entry2) => {
const b1 : string = lib_plankton.pit.datetime_format(entry1.event_object.begin);
const b2 : string = lib_plankton.pit.datetime_format(entry2.event_object.begin);
return ((b1 <= b2) ? -1 : +1);
}
);
return entries;
}
/**
*/
private async entry_insert(
entry : _dali.type_event_object_extended
) : Promise<(null | HTMLElement)>
{
const selector : string = lib_plankton.string.coin(
".weekview-cell[rel=\"{{rel}}\"] > .weekview-events",
{
"rel": lib_plankton.pit.date_format(entry.event_object.begin.date),
}
);
const dom_cell = this.container.querySelector(selector);
if (dom_cell === null)
{
lib_plankton.log.debug(
"dali.widget.weekview.entry_insert.out_of_scope",
{
"entry": entry,
}
);
return null;
}
else
{
let dom_dummy : HTMLElement = document.createElement("div");
dom_dummy.innerHTML = await _dali.helpers.template_coin(
"widget-weekview",
"tableview-cell-entry",
{
"color": _dali.helpers.event_color(entry.hue),
"name": entry.event_object.name,
"rel": entry.key,
"additional_classes": lib_plankton.string.coin(
" access_level-{{access_level}}",
{
"access_level": _dali.access_level_encode(entry.access_level),
}
),
}
);
const dom_entry : HTMLElement = dom_dummy.querySelector(".weekview-event_entry");
// listener
dom_entry.addEventListener(
"click",
(event) => {
const rel : string = dom_entry.getAttribute("rel");
const event_key : _dali.type_event_key = rel;
this.action_select_event(
event_key
);
}
);
// emplace
dom_cell.appendChild(dom_entry);
return dom_entry;
}
}
/**
*/
private async entry_add(
entry : _dali.type_event_object_extended
) : Promise<void>
{
const dom_entry : (null | HTMLElement) = await this.entry_insert(entry);
if (dom_entry === null)
{
// do nothing
}
else
{
this.event_map.set(
entry.key,
{
"element": dom_entry,
"hash": class_widget_weekview.entry_hash(entry),
}
);
}
}
/**
*/
private async entry_update(
key : _dali.type_event_key,
entry : _dali.type_event_object_extended
) : Promise<void>
{
if (! this.event_map.has(key))
{
lib_plankton.log.warning(
"dali.widget.weekview.entry_update.event_missing",
{
"key": key,
}
);
}
else
{
const value = this.event_map.get(key);
const hash_old : string = value.hash;
const hash_new : string = class_widget_weekview.entry_hash(entry);
if (hash_old === hash_new)
{
// do nothing
lib_plankton.log.debug(
"dali.widget.weekview.entry_update.nothing_to_update",
{
"key": key,
"entry": entry,
"element": value.element,
}
);
}
else
{
const dom_entry_old : HTMLElement = value.element;
dom_entry_old.remove();
const dom_entry_new : (null | HTMLElement) = await this.entry_insert(entry);
if (dom_entry_new === null)
{
// do nothing
}
else
{
this.event_map.set(
entry.key,
{
"element": dom_entry_new,
"hash": hash_new,
}
);
}
}
}
}
/**
*/
private async entry_remove(
key : _dali.type_event_key
) : Promise<void>
{
if (! this.event_map.has(key))
{
// do nothing
lib_plankton.log.warning(
"dali.widget.weekview.entry_remove.not_in_map",
{
"key": key,
"pairs": lib_plankton.map.dump(this.event_map),
}
);
}
else
{
const value = this.event_map.get(
key
);
this.event_map.delete(
key
);
value.element.remove();
}
}
/**
*/
public async update_entries(
) : Promise<void>
{
const entries : Array<_dali.type_event_object_extended> = await this.get_entries_wrapped(
);
const contrast = lib_plankton.list.contrast<
any,
_dali.type_event_object_extended
>(
lib_plankton.map.dump(this.event_map),
pair => pair.key,
entries,
event => event.key
);
await Promise.all(
[]
// remove
.concat(
contrast.only_left.map(
({"key": key, "left": left}) => this.entry_remove(key)
)
)
// update
.concat(
contrast.both.map(
({"key": key, "left": left, "right": right}) => this.entry_update(key, right)
)
)
// add
.concat(
contrast.only_right.map(
({"key": key, "right": right}) => this.entry_add(right)
)
)
);
}
/**
*/
private async update_controls(
) : Promise<void>
{
const context : Element = this.container;
(context.querySelector(".weekview-control-year input") as HTMLInputElement).value = this.year.toFixed(0);
(context.querySelector(".weekview-control-week input") as HTMLInputElement).value = this.week.toFixed(0);
(context.querySelector(".weekview-control-count input") as HTMLInputElement).value = this.count.toFixed(0);
(context.querySelector(".weekview-control-vertical input") as HTMLInputElement).checked = this.vertical;
}
/**
*/
private async update_table(
) : Promise<void>
{
/**
* @todo avoid?
*/
lib_plankton.map.clear(this.event_map);
const context : Element = this.container;
// structure
{
type type_row_data = Array<
{
week : int;
day_pits : Array<lib_plankton.pit.type_pit>;
}
>;
/**
* @todo als Variable?
*/
const timezone_shift : int = 0;
const now_pit : lib_plankton.pit.type_pit = lib_plankton.pit.now();
const today_begin_pit : lib_plankton.pit.type_pit = lib_plankton.pit.trunc_day(now_pit);
const today_end_pit : lib_plankton.pit.type_pit = lib_plankton.pit.shift_day(today_begin_pit, 1);
const day_names : Array<string> = [
lib_plankton.translate.get("common.weekday.monday"),
lib_plankton.translate.get("common.weekday.tuesday"),
lib_plankton.translate.get("common.weekday.wednesday"),
lib_plankton.translate.get("common.weekday.thursday"),
lib_plankton.translate.get("common.weekday.friday"),
lib_plankton.translate.get("common.weekday.saturday"),
lib_plankton.translate.get("common.weekday.sunday"),
];
const row_data_original : type_row_data = (
lib_plankton.list.sequence(this.count)
.map(
offset => {
const week : int = (this.week + offset);
return {
"week": week,
"day_pits": (
lib_plankton.list.sequence(7)
.map(
day => lib_plankton.pit.from_ywd(
{
"year": this.year,
"week": week,
"day": (1 + day),
},
{
"timezone_shift": timezone_shift,
}
)
)
),
};
}
)
);
const row_data_alternative : type_row_data = (
lib_plankton.list.sequence(7)
.map(
day_of_week => {
return {
/*"day_of_week"*/"week": day_of_week,
/*"week_pits"*/"day_pits": (
lib_plankton.list.sequence(this.count)
.map(
offset => {
const week : int = (this.week + offset);
return lib_plankton.pit.from_ywd(
{
"year": this.year,
"week": week,
"day": (1 + day_of_week),
},
{
"timezone_shift": timezone_shift,
}
);
}
)
)
};
}
)
);
const row_data : type_row_data = (
(! this.vertical)
?
row_data_original
:
row_data_alternative
);
// head
{
const dom_tr = document.createElement("tr");
{
if (! this.vertical)
{
// anchor
{
const dom_th = document.createElement("th");
dom_th.classList.add("weekview-cell");
dom_tr.appendChild(dom_th);
}
// days
{
day_names.forEach(
(day_name) => {
const dom_th = document.createElement("th");
dom_th.classList.add("weekview-cell");
dom_th.classList.add("weekview-cell-week");
dom_th.textContent = day_name;
dom_tr.appendChild(dom_th);
}
);
}
}
else
{
// anchor
{
const dom_th = document.createElement("th");
dom_th.classList.add("weekview-cell");
dom_tr.appendChild(dom_th);
}
// days
{
lib_plankton.list.sequence(this.count).forEach(
(offset) => {
const dom_th = document.createElement("th");
dom_th.classList.add("weekview-cell");
dom_th.classList.add("weekview-cell-day");
dom_th.textContent = (this.week + offset).toFixed(0).padStart(2, "0");
dom_tr.appendChild(dom_th);
}
);
}
}
}
context.querySelector(".weekview-table thead").innerHTML = "";
context.querySelector(".weekview-table thead").appendChild(dom_tr);
}
// body
{
context.querySelector(".weekview-table tbody").innerHTML = (
await _dali.helpers.promise_row<string>(
row_data
.map(
(row, index) => async () => _dali.helpers.template_coin(
"widget-weekview",
"tableview-row",
{
"week": (
(! this.vertical)
?
row.week.toFixed(0).padStart(2, "0")
:
day_names[index]
),
"cells": (
await _dali.helpers.promise_row<string>(
row.day_pits
.map(
(day_pit) => async () => {
const is_today : boolean = lib_plankton.pit.is_between(
day_pit,
today_begin_pit,
today_end_pit
);
const day_as_date : lib_plankton.pit.type_date = lib_plankton.pit.to_datetime_ce(day_pit).date;
const day_as_ywd : lib_plankton.pit.type_ywd = lib_plankton.pit.to_ywd(day_pit);
return _dali.helpers.template_coin(
"widget-weekview",
"tableview-cell",
{
"extra_classes": (
[""]
.concat(is_today ? ["weekview-cell-today"] : [])
.join(" ")
),
"day": (
_dali.conf.get().misc.weekview_cell_day_format
.replace(new RegExp("Y", "g"), day_as_date.year.toFixed(0).padStart(4, "0"))
.replace(new RegExp("m", "g"), day_as_date.month.toFixed(0).padStart(2, "0"))
.replace(new RegExp("b", "g"), _dali.helpers.month_name(day_as_date.month))
.replace(new RegExp("d", "g"), day_as_date.day.toFixed(0).padStart(2, "0"))
.replace(new RegExp("W", "g"), day_as_ywd.week.toFixed(0).padStart(2, "0"))
.replace(new RegExp("w", "g"), day_as_ywd.day.toFixed(0).padStart(1, "0"))
),
"rel": lib_plankton.string.coin(
"{{year}}-{{month}}-{{day}}",
{
"year": day_as_date.year.toFixed(0).padStart(4, "0"),
"month": day_as_date.month.toFixed(0).padStart(2, "0"),
"day": day_as_date.day.toFixed(0).padStart(2, "0"),
}
),
"entries": ""
}
);
}
)
)
).join(""),
}
)
)
)
).join("");
}
}
// listeners
{
context.querySelectorAll(".weekview-cell-regular").forEach(
(element) => {
element.addEventListener(
"click",
(event) => {
if (
(! (element === event.target))
&&
(! (event.target as HTMLElement).classList.contains("weekview-day"))
)
{
// do nothing
}
else
{
const rel : string = element.getAttribute("rel");
const parts : Array<string> = rel.split("-");
const date : lib_plankton.pit.type_date = {
"year": parseInt(parts[0]),
"month": parseInt(parts[1]),
"day": parseInt(parts[2]),
};
this.action_select_day(date);
}
}
);
}
);
}
}
/**
*/
public toggle_visibility(
calendar_id : _dali.type_calendar_id,
{
"mode": mode = null,
}
:
{
mode ?: (null | boolean);
}
=
{
}
) : void
{
this.container.querySelectorAll(".weekview-event_entry").forEach(
(element) => {
const rel : string = element.getAttribute("rel");
const parts : Array<string> = rel.split("/");
const calendar_id_ : _dali.type_calendar_id = parseInt(parts[0]);
if (! (calendar_id === calendar_id_))
{
// do nothing
}
else
{
element.classList.toggle(
"weekview-cell-hidden",
((mode !== null) ? (! mode) : undefined)
);
}
}
);
}
/**
* [implementation]
*/
public async load(
target_element : Element
) : Promise<void>
{
target_element.innerHTML = await _dali.helpers.template_coin(
"widget-weekview",
"main",
{
"label_control_year": lib_plankton.translate.get("widget.weekview.controls.year"),
"label_control_week": lib_plankton.translate.get("widget.weekview.controls.week"),
"label_control_count": lib_plankton.translate.get("widget.weekview.controls.count"),
"label_control_vertical": lib_plankton.translate.get("widget.weekview.controls.vertical"),
"label_control_apply": lib_plankton.translate.get("widget.weekview.controls.apply"),
}
);
this.container = target_element.querySelector(".weekview");
// controls
{
// direct inputs
{
[
{
"name": "year",
"selector": ".weekview-control-year input",
"retrieve": element => parseInt(element.value),
"write": x => {this.year = x;}
},
{
"name": "week",
"selector": ".weekview-control-week input",
"retrieve": element => parseInt(element.value),
"write": x => {this.week = x;}
},
{
"name": "count",
"selector": ".weekview-control-count input",
"retrieve": element => parseInt(element.value),
"write": x => {this.count = x;}
},
{
"name": "vertical",
"selector": ".weekview-control-vertical input",
"retrieve": element => element.checked,
"write": x => {this.vertical = x;}
},
].forEach(
(entry) => {
const element : HTMLInputElement = (target_element.querySelector(entry.selector) as HTMLInputElement);
element.addEventListener(
"change",
async (event) => {
event.preventDefault();
const value : unknown = entry.retrieve(element);
entry.write(value);
await this.update_table();
await this.update_entries();
}
);
}
);
}
// buttons
{
// year
{
/**
* @todo limit
*/
target_element.querySelector(".weekview-control-year-prev").addEventListener(
"click",
async () => {
this.year -= 1;
await this.update_controls();
await this.update_table();
await this.update_entries();
}
);
/**
* @todo limit
*/
target_element.querySelector(".weekview-control-year-next").addEventListener(
"click",
async () => {
this.year += 1;
await this.update_controls();
await this.update_table();
await this.update_entries();
}
);
}
// week
{
target_element.querySelector(".weekview-control-week-prev").addEventListener(
"click",
async () => {
if (this.week >= 1)
{
this.week -= 1;
}
else
{
this.year -= 1;
/**
* @todo get correct week
*/
this.week = 51;
}
await this.update_controls();
await this.update_table();
await this.update_entries();
}
);
target_element.querySelector(".weekview-control-week-next").addEventListener(
"click",
async () => {
/**
* @todo correct limit
*/
if (this.week <= 51)
{
this.week += 1;
}
else
{
this.year += 1;
this.week = 1;
}
await this.update_controls();
await this.update_table();
await this.update_entries();
}
);
}
// count
{
target_element.querySelector(".weekview-control-count-prev").addEventListener(
"click",
async () => {
if (this.count >= 2)
{
this.count -= 1;
await this.update_controls();
await this.update_table();
await this.update_entries();
}
else
{
// do nothing
}
}
);
target_element.querySelector(".weekview-control-count-next").addEventListener(
"click",
async () => {
if (this.count <= 6)
{
this.count += 1;
await this.update_controls();
await this.update_table();
await this.update_entries();
}
else
{
// do nothing
}
}
);
}
}
}
await this.update_controls();
await this.update_table();
await this.update_entries();
return Promise.resolve<void>(undefined);
}
}
}