/** * EGroupware eTemplate2 - JS Link list object * * @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 * @copyright 2022 Nathan Gray */ import {css, html, LitElement, PropertyValues, render, TemplateResult} from "lit"; import {until} from "lit/directives/until.js"; import {Et2Widget} from "../Et2Widget/Et2Widget"; import {LinkInfo} from "./Et2Link"; import {et2_IDetachedDOM} from "../et2_core_interfaces"; import {property} from "lit/decorators/property.js"; import {customElement} from "lit/decorators/custom-element.js"; /** * Display a list of entries in a comma separated list * * Given an application & entry ID, will query the list of links and display * * @see Et2Link */ // @ts-ignore TypeScript says there's something wrong with types @customElement('et2-link-string') export class Et2LinkString extends Et2Widget(LitElement) implements et2_IDetachedDOM { static get styles() { return [ ...super.styles, css` :host { list-style-type: none; display: inline; padding: 0px; } ::slotted(*) { display: inline; } ::slotted(*):hover { text-decoration: underline; } /* CSS for child elements */ ::slotted(*):after { content: ", " } ::slotted(*:last-child):after { content: initial; } ` ]; } /** * Specify the application for all the entries, so you only need to specify the entry ID */ @property({ type: String, reflect: true }) application; /** * Application entry ID */ @property({type: String, reflect: true}) entryId; /** * Application filter * Set to an appname or comma separated list of applications to show only linked entries from those * applications */ @property({type: String}) onlyApp; /** * Type filter * Sub-type key to list only entries of that type */ @property({type: String}) linkType; /** * Show links that are marked as deleted, being held for purge */ @property({type: Boolean}) showDeleted = false; /** * Pass value as an object, will be parsed to set application & entryId */ @property({type: Object}) value; /** * Number of application-links to load (file-links are always fully loaded currently) * * If number is exceeded, a "Load more links ..." button is displayed, which will load the double amount of links each time clicked */ @property({type: Number}) limit = 20; protected _link_list : LinkInfo[]; protected _loadingPromise : Promise; constructor() { super(); this._link_list = [] } async getUpdateComplete() { const result = await super.getUpdateComplete(); if(this._loadingPromise) { // Wait for the values to arrive before we say we're done await this._loadingPromise; } return result; } /** * Set the value of the list * * Value can be: * - String: CSV list of entries in either app:ID or just ID if application is set. * - Object: {to_app: , to_id: } List of linked entries will be fetched from the server * - Array: {app: , id: }[] * @param _value */ public set_value(_value : string | { to_app : string, to_id : string } | LinkInfo[]) { this._link_list = []; if(typeof _value == "object" && !Array.isArray(_value) && !_value.to_app && this.application) { _value.to_app = this.application; } // We have app & ID - fetch list if(typeof _value == 'object' && !Array.isArray(_value) && _value.to_app && _value.to_id && ( typeof _value.to_id === "string" || typeof _value.to_id == "number")) { this.application = _value.to_app; this.entryId = _value.to_id; this.get_links(); return; } // CSV list of IDs for one app if(typeof _value === "string") { let ids = _value.split(","); ids.forEach((id) => (this._link_list).push({app: this.application, id: id})); } // List of LinkInfo else if(Array.isArray(_value)) { this._link_list = _value; } // List of LinkInfo stuffed into to_id - entry is not yet saved else if(_value.to_id && typeof _value.to_id !== "string") { this.entryId = _value.to_id; Object.keys(_value.to_id).forEach((key) => { this._link_list.push(_value.to_id[key]); }); } this._addLinks(this._link_list); super.requestUpdate(); } public updated(changedProperties : PropertyValues) { super.updated(changedProperties); if((changedProperties.has("application") || changedProperties.has("entryId") || changedProperties.has("onlyApp") || changedProperties.has("linkType")) && this.application && this.entryId ) { // Something changed, and we have the information needed to get the matching links this.get_links(); } } public render() : TemplateResult { // This shows loading template until loadingPromise resolves, then shows _listTemplate return html` ${this._loadingPromise ? until( this._loadingPromise?.then(res => { this._listTemplate(); }), this._loadingTemplate() ) : this._listTemplate()} `; } protected _listTemplate() { return html` `; } /** * Render one link * * @param link * @returns {TemplateResult} * @protected */ protected _linkTemplate(link : LinkInfo) : TemplateResult { const id = typeof link.id === "string" ? link.id : link.link_id; return html` `; } /** * Render "more links available" * * @param link * @returns {TemplateResult} * @protected */ protected _moreAvailableTemplate(link : LinkInfo) : TemplateResult { return html` `; } /** * Render that we're waiting for data * @returns {TemplateResult} * @protected */ protected _loadingTemplate() : TemplateResult { return html`loading...`; } /** * Render a list of links inside the list * These get slotted, rather than put inside the shadow dom * * @param links * @protected */ protected _addLinks(links : LinkInfo[]) { // Remove anything there right now while(this.lastChild) { this.removeChild(this.lastChild); } links.forEach((link) => { let temp = document.createElement("div"); render(link.app === 'exceeded' ? this._moreAvailableTemplate(link) : this._linkTemplate(link), temp); temp.childNodes.forEach((node) => this.appendChild(node)); }) /* This should work, and it does, but only once. It fails if you try and update then run it again - none of the children get added Something about how lit renders render(html`${repeat(links, (link) => link.app + ":" + link.id, (link) => this._linkTemplate(link))}`, this ); */ this.dispatchEvent(new Event("change", {bubbles: true})); } /** * Starts the request for link list to the server * * Called internally to fetch the list. May be called externally to trigger a refresh if a link is added. * */ public get_links(not_saved_links? : LinkInfo[]) { if(typeof not_saved_links === "undefined") { not_saved_links = []; } let _value = { to_app: this.application, to_id: this.entryId, only_app: this.onlyApp, show_deleted: this.showDeleted, limit: this.limit }; this.limit *= 2; // double number of loaded links on next call if(this._loadingPromise) { // Already waiting return; } this._loadingPromise = >(this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value])) .then(_value => { if(_value && Array.isArray(_value)) { for(let link of _value) { if(!not_saved_links.some(l => l.app == link.app && l.id == link.id)) { not_saved_links.push(link); } } } this._addLinks(not_saved_links); this._loadingPromise = null; }) } getDetachedAttributes(_attrs : string[]) { _attrs.push("application", "entryId", "statustext"); } getDetachedNodes() : HTMLElement[] { return [this]; } setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?) { for(let k in _values) { this[k] = _values[k]; } } }