diff --git a/api/js/etemplate/Et2Date/Et2DateDuration.ts b/api/js/etemplate/Et2Date/Et2DateDuration.ts new file mode 100644 index 0000000000..0bb1b2dcc6 --- /dev/null +++ b/api/js/etemplate/Et2Date/Et2DateDuration.ts @@ -0,0 +1,243 @@ +/** + * EGroupware eTemplate2 - Duration date widget (WebComponent) + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link https://www.egroupware.org + * @author Nathan Gray + */ + + +import {css, LitElement} from "@lion/core"; +import {Unparseable} from "@lion/form-core"; +import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; + +export interface formatOptions +{ + select_unit : string; + display_format : string; + data_format : string; + hours_per_day : number; + empty_not_0 : boolean; + number_format? : string; +}; + +/** + * Format a number as a time duration + * + * @param {number} value + * @param {object} options + * set 'timeFormat': "12" to specify a particular format + * @returns {value: string, unit: string} + */ +export function formatDuration(value : number | string, options : formatOptions) : { value : string, unit : string } +{ + // Handle empty / 0 / no value + if(value === "" || value == "0" || !value) + { + return {value: options.empty_not_0 ? "0" : "", unit: ""}; + } + // Make sure it's a number now + value = typeof value == "string" ? parseInt(value) : value; + + if(!options.select_unit) + { + let vals = []; + for(let i = 0; i < options.display_format.length; ++i) + { + let unit = options.display_format[i]; + let val = this._unit_from_value(value, unit, i === 0); + if(unit === 's' || unit === 'm' || unit === 'h' && options.display_format[0] === 'd') + { + vals.push(sprintf('%02d', val)); + } + else + { + vals.push(val); + } + } + return {value: vals.join(':'), unit: ''}; + } + + // Put value into minutes for further processing + switch(options.data_format) + { + case 'd': + value *= options.hours_per_day; + // fall-through + case 'h': + value *= 60; + break; + case 's': + value /= 60.0; + break; + } + + // Figure out the best unit for display + let _unit = options.display_format == "d" ? "d" : "h"; + if(options.display_format.indexOf('m') > -1 && value < 60) + { + _unit = 'm'; + } + else if(options.display_format.indexOf('d') > -1 && value >= (60 * options.hours_per_day)) + { + _unit = 'd'; + } + let out_value = "" + (_unit == 'm' ? value : (Math.round((value / 60.0 / (_unit == 'd' ? options.hours_per_day : 1)) * 100) / 100)); + + if(out_value === '') + { + _unit = ''; + } + + // use decimal separator from user prefs + var format = options.number_format || this.egw().preference('number_format'); + var sep = format ? format[0] : '.'; + if(format && sep && sep != '.') + { + out_value = out_value.replace('.', sep); + } + + return {value: out_value, unit: _unit}; +} + +/** + * Display a time duration (eg: 3 days, 6 hours) + * + * If not specified, the time is in assumed to be minutes and will be displayed with a calculated unit + * but this can be specified with the properties. + */ +export class Et2DateDuration extends Et2InputWidget(LitElement) +{ + static get styles() + { + return [ + ...super.styles, + css` + :host([focused]) ::slotted(button), :host(:hover) ::slotted(button) { + display: inline-block; + } + `, + ]; + } + + static get properties() + { + return { + ...super.properties, + + /** + * Data format + * + * Units to read/store the data. 'd' = days (float), 'h' = hours (float), 'm' = minutes (int), 's' = seconds (int). + */ + data_format: { + type: String + }, + /** + * Display format + * + * Permitted units for displaying the data. + * 'd' = days, 'h' = hours, 'm' = minutes, 's' = seconds. Use combinations to give a choice. + * Default is 'dh' = days or hours with selectbox. + */ + display_format: { + type: String + }, + + /** + * Select unit or input per unit + * + * Display a unit-selection for multiple units, or an input field per unit. + * Default is true + */ + select_unit: { + type: Boolean + }, + + /** + * Percent allowed + * + * Allows to enter a percentage instead of numbers + */ + percent_allowed: { + type: Boolean + }, + + /** + * Hours per day + * + * Number of hours in a day, used for converting between hours and (working) days. + * Default 8 + */ + hours_per_day: {type: Number}, + + /** + * 0 or empty + * + * Should the widget differ between 0 and empty, which get then returned as NULL + * Default false, empty is considered as 0 + */ + empty_not_0: {type: Boolean}, + + /** + * Short labels + * + * use d/h/m instead of day/hour/minute + */ + short_labels: { + type: Boolean + }, + + /** + * Step limit + * + * Works with the min and max attributes to limit the increments at which a numeric or date-time value can be set. + */ + step: { + type: String + } + } + } + + constructor() + { + super(); + + // Property defaults + this.data_format = "m"; + this.display_format = "dhm"; + this.select_unit = true; + this.percent_allowed = false; + this.hours_per_day = 8; + this.empty_not_0 = false; + this.short_labels = false; + + this.formatter = formatDuration; + } + + getValue() + { + if(this.readOnly) + { + return null; + } + + // The supplied value was not understandable, return null + if(this.modelValue instanceof Unparseable || !this.modelValue) + { + return null; + } + + return this.modelValue.toJSON(); + } + + render() + { + // TODO + } +} + +// @ts-ignore TypeScript is not recognizing that this is a LitElement +customElements.define("et2-date-duration", Et2DateDuration); diff --git a/api/js/etemplate/Et2Date/Et2DateDurationReadonly.ts b/api/js/etemplate/Et2Date/Et2DateDurationReadonly.ts new file mode 100644 index 0000000000..7ef1338b1e --- /dev/null +++ b/api/js/etemplate/Et2Date/Et2DateDurationReadonly.ts @@ -0,0 +1,58 @@ +/** + * EGroupware eTemplate2 - Readonly duration WebComponent + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @link https://www.egroupware.org + * @author Nathan Gray + */ + + +import {html} from "@lion/core"; +import {Et2DateDuration, formatOptions} from "./Et2DateDuration"; + + +/** + * This is a stripped-down read-only widget used in nextmatch + */ +export class Et2DateDurationReadonly extends Et2DateDuration +{ + render() + { + let parsed = this.value; + + const format_options = { + select_unit: this.select_unit, + display_format: this.display_format, + data_format: this.data_format, + number_format: this.egw().preference("number_format"), + hours_per_day: this.hours_per_day, + empty_not_0: this.empty_not_0 + }; + + const display = this.formatter(parsed, format_options); + return html` + + ${display.value}${display.unit} + + `; + } + + getDetachedAttributes(attrs) + { + attrs.push("id", "value", "class"); + } + + getDetachedNodes() : HTMLElement[] + { + return [this]; + } + + setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data? : any) : void + { + // Do nothing, since we can't actually stop being a DOM node... + } +} + +// @ts-ignore TypeScript is not recognizing that this widget is a LitElement +customElements.define("et2-date-duration_ro", Et2DateDurationReadonly); \ No newline at end of file diff --git a/api/js/etemplate/Et2Date/Et2DateSinceReadonly.ts b/api/js/etemplate/Et2Date/Et2DateSinceReadonly.ts new file mode 100644 index 0000000000..1037f73d60 --- /dev/null +++ b/api/js/etemplate/Et2Date/Et2DateSinceReadonly.ts @@ -0,0 +1,133 @@ +/** + * EGroupware eTemplate2 - Readonly time since WebComponent + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @link https://www.egroupware.org + * @author Nathan Gray + */ + + +import {html} from "@lion/core"; +import {parseDate, parseDateTime} from "./Et2Date"; +import {Et2DateReadonly} from "./Et2DateReadonly"; + +/** + * Formatter for time since widget. + * + * @param {Date} date + * @returns {string} + */ +const formatDate = function(date : Date, options = {units: "YmdHis"}) +{ + const unit2label = { + 'Y': 'years', + 'm': 'month', + 'd': 'days', + 'H': 'hours', + 'i': 'minutes', + 's': 'seconds' + }; + let unit2s : Object = { + 'Y': 31536000, + 'm': 2628000, + 'd': 86400, + 'H': 3600, + 'i': 60, + 's': 1 + }; + var d = new Date(); + var diff = Math.round(d.valueOf() / 1000) - Math.round(date.valueOf() / 1000); + let display = ''; + + // limit units used to display + let smallest = 's'; + if(options.units) + { + const valid = Object.entries(unit2s).filter((e) => (options.units).includes(e[0])); + unit2s = Object.fromEntries(valid); + smallest = (valid.pop() || [])[0]; + } + + for(var unit in unit2s) + { + var unit_s = unit2s[unit]; + if(diff >= unit_s || unit === smallest) + { + display = Math.round(diff / unit_s) + ' ' + this.egw().lang(unit2label[unit]); + break; + } + } + return display; +} + +/** + * Displays the elapsed time since the given date + * + * The time units (years, months, days, etc) will be calculated automatically to best match the + * time scale being dealt with, unless the units property is set. + * + * This is a stripped-down read-only widget used in nextmatch + */ +export class Et2DateSinceReadonly extends Et2DateReadonly +{ + static get properties() + { + return { + ...super.properties, + /** + * Allowed display units, default 'YmdHis', e.g. 'd' to display a value only in days" + */ + units: {type: String, reflect: true}, + } + } + + constructor() + { + super(); + + this.parser = parseDateTime; + this.formatter = formatDate; + } + + set_value(value) + { + this.value = value; + } + + render() + { + let parsed : Date | Boolean = this.value ? this.parser(this.value) : false + + // Be more forgiving if time is missing + if(!parsed && this.value) + { + parsed = parseDate(this.value) || false; + } + + return html` + + ${this.value ? this.formatter(parsed, {units: this.units}) : ''} + + `; + } + + getDetachedAttributes(attrs) + { + attrs.push("id", "value", "class"); + } + + getDetachedNodes() : HTMLElement[] + { + return [this]; + } + + setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data? : any) : void + { + // Do nothing, since we can't actually stop being a DOM node... + } +} + +// @ts-ignore TypeScript is not recognizing that this widget is a LitElement +customElements.define("et2-date-since_ro", Et2DateSinceReadonly); \ No newline at end of file diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 96d5c8e5d9..8b157b0654 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -26,7 +26,10 @@ import {egwIsMobile} from "../egw_action/egw_action_common.js"; import './Et2Box/Et2Box'; import './Et2Button/Et2Button'; import './Et2Date/Et2Date'; -import './Et2Date/Et2DateReadonly' +import './Et2Date/Et2DateDuration'; +import './Et2Date/Et2DateDurationReadonly'; +import './Et2Date/Et2DateReadonly'; +import './Et2Date/Et2DateSinceReadonly'; import './Et2Date/Et2DateTime'; import './Et2Date/Et2DateTimeReadonly'; import './Et2Description/Et2Description';