diff --git a/api/js/etemplate/Et2Link/Et2Link.ts b/api/js/etemplate/Et2Link/Et2Link.ts new file mode 100644 index 0000000000..f8b608e5d1 --- /dev/null +++ b/api/js/etemplate/Et2Link/Et2Link.ts @@ -0,0 +1,288 @@ +/** + * EGroupware eTemplate2 - JS Link 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 {ExposeMixin} from "../Expose/ExposeMixin"; +import {css, html, LitElement} from "@lion/core"; +import {Et2Widget} from "../Et2Widget/Et2Widget"; +import {et2_IDetachedDOM} from "../et2_core_interfaces"; + +/** + * Display a specific, single entry from an application + * + * The entry is specified with the application name, and the app's ID for that entry. + * You can set it directly in the properties (application, entry_id) or use set_value() to + * pass an object {app: string, id: string, [title: string]} or string in the form ::. + * If title is not specified, it will be fetched using framework's egw.link_title() + */ + +// @ts-ignore TypeScript says there's something wrong with types +export class Et2Link extends ExposeMixin(Et2Widget(LitElement)) implements et2_IDetachedDOM +{ + static get styles() + { + return [ + ...super.styles, + css` + :host { + display: block; + } + /** Style based on parent **/ + :host-context(et2-link-string) { + display: inline; + } + :host-context(et2-link-list) { + background-color: green; + } + ` + ]; + } + + + static get properties() + { + return { + ...super.properties, + /** + * Specify the application for the entry + */ + app: { + type: String, + reflect: true, + }, + /** + * Application entry ID + */ + entry_id: { + type: String, + reflect: true + }, + /** + * Pass value as an object, will be parsed to set application & entry_id + */ + value: { + type: Object, + reflect: false + }, + /** + * View link type + * Used for displaying the linked entry + * [view|edit|add] + * default "view" + */ + link_hook: { + type: String + }, + /** + * Target application + * + * Passed to egw.open() to open entry in specified application + */ + target_app: { + type: String + }, + /** + * Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank + */ + extra_link_target: { + type: String + }, + + /** + * Breaks title into multiple lines based on this delimiter by replacing it with '\r\n'" + */ + break_title: { + type: String + } + + } + } + + private static MISSING_TITLE = "??"; + + // Title is read-only inside + private _title : string; + private _titlePromise : Promise; + + constructor() + { + super(); + this._title = ""; + this.__link_hook = "view"; + } + + connectedCallback() + { + super.connectedCallback(); + + this.classList.add(...["et2_clickable", "et2_link"]); + } + + render() + { + let title = this.title; + + if(this.break_title) + { + // Set up title to optionally break on the provided character - replace all space with nbsp, add a + // zero-width space after the break string + title = title + .replace(this.break_title, this.break_title.trimEnd() + "\u200B") + .replace(/ /g, '\u00a0'); + } + return html`${title}`; + } + + public set title(_title) + { + this._title = _title; + } + + public get title() + { + return this._title; + } + + set value(_value : LinkInfo | string) + { + if(!_value) + { + this.app = ""; + this.id = ""; + this.title = ""; + return; + } + if(typeof _value != 'object' && _value) + { + if(_value.indexOf(':') >= 0) + { + // application_name:ID + let app = _value.split(':', 1); + let id = _value.substr(app[0].length + 1); + _value = {app: app[0], id: id}; + } + else if(this.app) + { + // Application set, just passed ID + _value = {app: this.app, id: _value}; + } + else + { + console.warn("Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'", _value); + return; + } + } + if(typeof _value !== "string") + { + this.app = _value.app; + this.entry_id = _value.id; + this._title = Et2Link.MISSING_TITLE; + + if(_value.title) + { + this._title = _value.title; + } + Object.keys(_value).forEach(key => + { + if(["app", "entry_id", "title", "id"].indexOf(key) != -1) + { + return; + } + this.dataset[key] = _value[key]; + }) + } + } + + + set_value(_value : LinkInfo | string) + { + this.value = _value; + } + + /** + * If app or entry_id has changed, we'll update the title + * + * @param changedProperties + */ + willUpdate(changedProperties) + { + super.willUpdate(changedProperties); + + super.requestUpdate(); + if(changedProperties.has("app") || changedProperties.has("entry_id")) + { + if(!this.app || !this.entry_id || (this.app && this.entry_id && !this._title)) + { + this._title = Et2Link.MISSING_TITLE; + } + if(this.app && this.entry_id && this._title == Et2Link.MISSING_TITLE) + { + // Title will be fetched from server and then set + this._titlePromise = this.egw()?.link_title(this.app, this.entry_id, true).then(title => + { + this._title = title; + // It's probably already been rendered + this.requestUpdate(); + }); + } + } + } + + _handleClick(_ev : MouseEvent) : boolean + { + this.egw().open(Object.assign({ + app: this.app, + id: this.entry_id + }, this.dataset), "", this.link_hook, this.dataset.extra_args, this.target_app || this.app, this.target_app); + + _ev.stopImmediatePropagation(); + return true; + } + + getDetachedAttributes(_attrs : string[]) + { + _attrs.push("app", "entry_id"); + } + + getDetachedNodes() : HTMLElement[] + { + return [this]; + } + + setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?) + { + for(let k in _values) + { + this[k] = _values[k]; + } + } +} + +// @ts-ignore TypeScript says there's something wrong with types +customElements.define("et2-link", Et2Link); + +/** + * Interface to describe needed information about a link + */ +export interface LinkInfo +{ + app : string, + id : string, + title? : string, + + comment? : string + icon? : string, + + // Extra information for things like files + download_url? : string, + target? : string, + mode? : number +} diff --git a/api/js/etemplate/Et2Link/Et2LinkString.ts b/api/js/etemplate/Et2Link/Et2LinkString.ts new file mode 100644 index 0000000000..4e7b61d233 --- /dev/null +++ b/api/js/etemplate/Et2Link/Et2LinkString.ts @@ -0,0 +1,234 @@ +/** + * 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, render, repeat, TemplateResult, until} from "@lion/core"; +import {Et2Widget} from "../Et2Widget/Et2Widget"; +import {Et2Link, LinkInfo} from "./Et2Link"; +import {et2_IDetachedDOM} from "../et2_core_interfaces"; + +/** + * 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 +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; + } + /* CSS for child elements */ + ::slotted(*):after { + content: ", " + } + ::slotted(*:last-child):after { + content:initial; + } + ` + ]; + } + + + static get properties() + { + return { + ...super.properties, + /** + * Specify the application for all the entries, so you only need to specify the entry ID + */ + application: { + type: String, + reflect: true, + }, + /** + * Application entry ID + */ + entry_id: { + type: String, + reflect: true + }, + /** + * Application filter + * Set to an appname or comma separated list of applications to show only linked entries from those + * applications + */ + only_app: { + type: String + }, + /** + * Type filter + * Sub-type key to list only entries of that type + */ + link_type: { + type: String + }, + + /** + * Pass value as an object, will be parsed to set application & entry_id + */ + value: { + type: Object, + reflect: false + } + } + } + + protected _link_list : LinkInfo[]; + protected _loadingPromise : Promise; + + constructor() + { + super(); + this._link_list = [] + } + + + /** + * 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; + } + if(typeof _value == 'object' && !Array.isArray(_value) && _value.to_app && _value.to_id) + { + this.application = _value.to_app; + this.entry_id = _value.to_id; + this._get_links(); + return; + } + if(typeof _value === "string") + { + let ids = _value.split(","); + ids.forEach((id) => (this._link_list).push({app: this.application, id: id})); + } + else if(Array.isArray(_value)) + { + this._link_list = _value; + } + this._add_links(this._link_list); + super.requestUpdate(); + } + + 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) : 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 + * @param links + * @protected + */ + protected _add_links(links : LinkInfo[]) + { + render(html`${repeat(this._link_list, (link) => link.app + ":" + link.id, (link) => this._linkTemplate(link))}`, this); + } + + /** + * Starts the request for link list to the server + * @protected + */ + protected _get_links() + { + let _value = { + to_app: this.application, + to_id: this.entry_id, + only_app: this.only_app + }; + + this.egw().jsonq('EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list', [_value]).then(_value => + { + this._addLinks(_value); + }); + return; + } + + getDetachedAttributes(_attrs : string[]) + { + _attrs.push("application", "entry_id"); + } + + getDetachedNodes() : HTMLElement[] + { + return [this]; + } + + setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data?) + { + for(let k in _values) + { + this[k] = _values[k]; + } + } +}; + +// @ts-ignore TypeScript says there's something wrong with types +customElements.define("et2-link-string", Et2LinkString, {extends: 'ul'}); \ No newline at end of file