mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-28 17:48:56 +01:00
WIP on Et2Link & Et2LinkString
This commit is contained in:
parent
1fb46bb74c
commit
07ced8046d
288
api/js/etemplate/Et2Link/Et2Link.ts
Normal file
288
api/js/etemplate/Et2Link/Et2Link.ts
Normal file
@ -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 <application>::<ID>.
|
||||
* 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>(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<string>;
|
||||
|
||||
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 [<HTMLElement><unknown>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
|
||||
}
|
234
api/js/etemplate/Et2Link/Et2LinkString.ts
Normal file
234
api/js/etemplate/Et2Link/Et2LinkString.ts
Normal file
@ -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<LinkInfo[]>;
|
||||
|
||||
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: <appname>, to_id: <Entry ID>} List of linked entries will be fetched from the server
|
||||
* - Array: {app: <appname>, id: <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) => (<LinkInfo[]>this._link_list).push(<LinkInfo>{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`
|
||||
<slot></slot>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one link
|
||||
*
|
||||
* @param link
|
||||
* @returns {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
protected _linkTemplate(link) : TemplateResult
|
||||
{
|
||||
return html`
|
||||
<et2-link app="${link.app}" entry_id="${link.id}" .value=${link}></et2-link>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 [<HTMLElement><unknown>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'});
|
Loading…
Reference in New Issue
Block a user