/* This file is part of »dali«. Copyright 2025 'kcf' »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 . */ 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> ); /** */ 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 = 4, } : { 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> { 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), /* "title": class_widget_weekview.event_generate_tooltip( entry.calendar_name, entry.event_object ), */ "title": "", "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 { 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 { 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 { 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 { 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 { 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 { /** * @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; } >; /** * @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 = [ 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( 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( row.day_pits .map( (day_pit) => async () => { const is_today : boolean = lib_plankton.pit.is_between( day_pit, today_begin_pit, today_end_pit ); return _dali.helpers.template_coin( "widget-weekview", "tableview-cell", { "extra_classes": ( [""] .concat(is_today ? ["weekview-cell-today"] : []) .join(" ") ), "title": lib_plankton.call.convey( day_pit, [ lib_plankton.pit.to_datetime_ce, (x : lib_plankton.pit.type_datetime) => lib_plankton.string.coin( "{{year}}-{{month}}-{{day}}", { "year": x.date.year.toFixed(0).padStart(4, "0"), "month": x.date.month.toFixed(0).padStart(2, "0"), "day": x.date.day.toFixed(0).padStart(2, "0"), } ), ] ), "day": lib_plankton.call.convey( day_pit, [ lib_plankton.pit.to_datetime_ce, (x : lib_plankton.pit.type_datetime) => lib_plankton.string.coin( "{{day}}", { "year": x.date.year.toFixed(0).padStart(4, "0"), "month": x.date.month.toFixed(0).padStart(2, "0"), "day": x.date.day.toFixed(0).padStart(2, "0"), } ), ] ), "rel": lib_plankton.call.convey( day_pit, [ lib_plankton.pit.to_datetime_ce, (x : lib_plankton.pit.type_datetime) => lib_plankton.string.coin( "{{year}}-{{month}}-{{day}}", { "year": x.date.year.toFixed(0).padStart(4, "0"), "month": x.date.month.toFixed(0).padStart(2, "0"), "day": x.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)) { // do nothing } else { const rel : string = element.getAttribute("rel"); const parts : Array = 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 = 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 { 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(undefined); } } }