2022-05-10 00:06:16 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2022-05-12 18:08:59 +02:00
|
|
|
import {ExposeMixin, ExposeValue} from "../Expose/ExposeMixin";
|
2023-05-18 00:55:05 +02:00
|
|
|
import {css, html, LitElement, TemplateResult} from "@lion/core";
|
2022-05-10 00:06:16 +02:00
|
|
|
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.
|
2022-07-21 17:57:50 +02:00
|
|
|
* You can set it directly in the properties (application, entryId) or use set_value() to
|
2022-05-10 00:06:16 +02:00
|
|
|
* 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()
|
2023-05-18 00:55:05 +02:00
|
|
|
*
|
2022-05-10 00:06:16 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
// @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`
|
2023-05-18 00:55:05 +02:00
|
|
|
:host {
|
2022-05-10 00:06:16 +02:00
|
|
|
display: block;
|
2022-05-11 00:02:33 +02:00
|
|
|
cursor: pointer;
|
2023-05-18 00:55:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
.link {
|
|
|
|
display: flex;
|
|
|
|
gap: 0.5rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
.link__title {
|
|
|
|
flex: 2 1 50%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.link__remark {
|
|
|
|
flex: 1 1 50%
|
|
|
|
}
|
|
|
|
|
|
|
|
:host:hover {
|
2022-05-11 23:10:09 +02:00
|
|
|
text-decoration: underline
|
2023-05-18 00:55:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Style based on parent **/
|
|
|
|
|
|
|
|
:host(et2-link-string) div {
|
2022-05-10 00:06:16 +02:00
|
|
|
display: inline;
|
2023-05-18 00:55:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
:host-context(et2-link-list):hover {
|
2022-05-11 23:10:09 +02:00
|
|
|
text-decoration: none;
|
2023-05-18 00:55:05 +02:00
|
|
|
}
|
2022-05-10 00:06:16 +02:00
|
|
|
`
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static get properties()
|
|
|
|
{
|
|
|
|
return {
|
|
|
|
...super.properties,
|
|
|
|
/**
|
|
|
|
* Specify the application for the entry
|
|
|
|
*/
|
|
|
|
app: {
|
|
|
|
type: String,
|
|
|
|
reflect: true,
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Application entry ID
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
entryId: {
|
2022-05-10 00:06:16 +02:00
|
|
|
type: String,
|
|
|
|
reflect: true
|
|
|
|
},
|
|
|
|
/**
|
2022-07-21 17:57:50 +02:00
|
|
|
* Pass value as an object, will be parsed to set application & entryId
|
2022-05-10 00:06:16 +02:00
|
|
|
*/
|
|
|
|
value: {
|
|
|
|
type: Object,
|
|
|
|
reflect: false
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* View link type
|
|
|
|
* Used for displaying the linked entry
|
|
|
|
* [view|edit|add]
|
|
|
|
* default "view"
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
linkHook: {
|
2022-05-10 00:06:16 +02:00
|
|
|
type: String
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Target application
|
|
|
|
*
|
|
|
|
* Passed to egw.open() to open entry in specified application
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
targetApp: {
|
2022-05-10 00:06:16 +02:00
|
|
|
type: String
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
extraLinkTarget: {
|
2022-05-10 00:06:16 +02:00
|
|
|
type: String
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Breaks title into multiple lines based on this delimiter by replacing it with '\r\n'"
|
|
|
|
*/
|
2022-07-21 17:57:50 +02:00
|
|
|
breakTitle: {
|
2022-05-10 00:06:16 +02:00
|
|
|
type: String
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-01 21:30:31 +02:00
|
|
|
static MISSING_TITLE = "??";
|
2022-05-10 00:06:16 +02:00
|
|
|
|
|
|
|
// Title is read-only inside
|
|
|
|
private _title : string;
|
|
|
|
private _titlePromise : Promise<string>;
|
|
|
|
|
|
|
|
constructor()
|
|
|
|
{
|
|
|
|
super();
|
2023-05-18 00:55:05 +02:00
|
|
|
this._title = Et2Link.MISSING_TITLE;
|
2022-07-21 17:57:50 +02:00
|
|
|
this.__linkHook = "view";
|
2022-05-10 00:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback()
|
|
|
|
{
|
|
|
|
super.connectedCallback();
|
|
|
|
}
|
|
|
|
|
2023-05-18 00:55:05 +02:00
|
|
|
/**
|
|
|
|
* Build a thumbnail for the link
|
|
|
|
* @param link
|
|
|
|
* @returns {TemplateResult}
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _thumbnailTemplate(link : LinkInfo) : TemplateResult
|
2022-05-11 23:10:09 +02:00
|
|
|
{
|
2023-05-18 00:55:05 +02:00
|
|
|
// If we have a mimetype, use a Et2VfsMime
|
|
|
|
// Files have path set in 'icon' property, and mime in 'type'
|
|
|
|
if(link.type && link.icon)
|
|
|
|
{
|
|
|
|
return html`
|
|
|
|
<et2-vfs-mime part="icon" class="link__icon" ._parent=${this} .value=${Object.assign({
|
|
|
|
name: link.title,
|
|
|
|
mime: link.type,
|
|
|
|
path: link.icon
|
|
|
|
}, link)}
|
|
|
|
></et2-vfs-mime>`;
|
|
|
|
}
|
|
|
|
return html`
|
|
|
|
<et2-image-expose
|
|
|
|
part="icon"
|
|
|
|
class="link__icon"
|
|
|
|
._parent=${this}
|
|
|
|
href="${link.href}"
|
|
|
|
src=${this.egw().image("" + link.icon)}
|
|
|
|
></et2-image-expose>`;
|
2022-05-11 23:10:09 +02:00
|
|
|
}
|
|
|
|
|
2022-05-10 00:06:16 +02:00
|
|
|
render()
|
|
|
|
{
|
|
|
|
let title = this.title;
|
|
|
|
|
2022-07-21 17:57:50 +02:00
|
|
|
if(this.breakTitle)
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
|
|
|
// 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
|
2022-07-21 17:57:50 +02:00
|
|
|
.replace(this.breakTitle, this.breakTitle.trimEnd() + "\u200B")
|
2022-05-10 00:06:16 +02:00
|
|
|
.replace(/ /g, '\u00a0');
|
|
|
|
}
|
2023-05-18 00:55:05 +02:00
|
|
|
return html`
|
2023-05-22 11:45:56 +02:00
|
|
|
<div part="base" class="link et2_link" draggable="${this.app == 'file'}" @dragstart=${this._handleDragStart.bind(this, this.dataset)}>
|
2023-05-18 00:55:05 +02:00
|
|
|
${this._thumbnailTemplate({id: this.entryId, app: this.app, ...this.dataset})}
|
|
|
|
<span part="title" class="link__title">${title}</span>
|
|
|
|
<span part="remark" class="link__remark">${this.dataset.remark}</span>
|
|
|
|
</div>`;
|
2022-05-10 00:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public set title(_title)
|
|
|
|
{
|
|
|
|
this._title = _title;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get title()
|
|
|
|
{
|
|
|
|
return this._title;
|
|
|
|
}
|
|
|
|
|
2022-05-12 18:08:59 +02:00
|
|
|
/**
|
|
|
|
* Get a value representation of the link.
|
|
|
|
*
|
|
|
|
* @returns {LinkInfo | string}
|
|
|
|
*/
|
|
|
|
get value() : LinkInfo | string
|
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
return this.app && this.entryId ? this.app + ":" + this.entryId : "";
|
2022-05-12 18:08:59 +02:00
|
|
|
}
|
|
|
|
|
2022-05-10 00:06:16 +02:00
|
|
|
set value(_value : LinkInfo | string)
|
|
|
|
{
|
|
|
|
if(!_value)
|
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
this.entryId = "";
|
2022-05-10 00:06:16 +02:00
|
|
|
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;
|
2022-07-21 17:57:50 +02:00
|
|
|
this.entryId = _value.id;
|
2022-05-10 00:06:16 +02:00
|
|
|
|
|
|
|
if(_value.title)
|
|
|
|
{
|
|
|
|
this._title = _value.title;
|
|
|
|
}
|
|
|
|
Object.keys(_value).forEach(key =>
|
|
|
|
{
|
2022-05-11 23:10:09 +02:00
|
|
|
// Skip these, they're either handled explicitly, or ID which we don't want to mess with
|
2022-07-21 17:57:50 +02:00
|
|
|
if(["app", "entryId", "title", "id"].indexOf(key) != -1)
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2023-05-24 13:38:13 +02:00
|
|
|
// we should not let null value being stored into dataset as 'null'
|
|
|
|
if (_value[key] === null)
|
|
|
|
{
|
|
|
|
this.dataset[key] = "";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this.dataset[key] = _value[key];
|
|
|
|
}
|
2022-05-10 00:06:16 +02:00
|
|
|
})
|
|
|
|
}
|
2023-05-18 00:55:05 +02:00
|
|
|
this.requestUpdate("value");
|
2022-05-10 00:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
set_value(_value : LinkInfo | string)
|
|
|
|
{
|
|
|
|
this.value = _value;
|
|
|
|
}
|
|
|
|
|
2022-05-12 18:08:59 +02:00
|
|
|
get exposeValue() : ExposeValue
|
|
|
|
{
|
|
|
|
let info = <ExposeValue><unknown>{
|
|
|
|
app: this.app,
|
2022-07-21 17:57:50 +02:00
|
|
|
id: this.entryId,
|
2022-05-12 18:08:59 +02:00
|
|
|
path: this.dataset['icon']
|
|
|
|
};
|
|
|
|
info['label'] = this.title;
|
|
|
|
info = Object.assign(info, this.dataset);
|
|
|
|
|
|
|
|
if(info['remark'])
|
|
|
|
{
|
|
|
|
info['label'] += " - " + info['remark'];
|
|
|
|
}
|
|
|
|
if(!info.path && this.app == "file")
|
|
|
|
{
|
|
|
|
// Fallback to check the "normal" place if path wasn't available
|
2022-07-21 17:57:50 +02:00
|
|
|
info.path = "/webdav.php/apps/" + this.dataset.app2 + "/" + this.dataset.id2 + "/" + this.entryId;
|
2022-05-12 18:08:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if(typeof info["type"] !== "undefined")
|
|
|
|
{
|
|
|
|
// Links use "type" for mimetype.
|
|
|
|
info.mime = info["type"];
|
|
|
|
}
|
|
|
|
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
2022-05-10 00:06:16 +02:00
|
|
|
/**
|
2022-07-21 17:57:50 +02:00
|
|
|
* If app or entryId has changed, we'll update the title
|
2022-05-10 00:06:16 +02:00
|
|
|
*
|
|
|
|
* @param changedProperties
|
|
|
|
*/
|
|
|
|
willUpdate(changedProperties)
|
|
|
|
{
|
|
|
|
super.willUpdate(changedProperties);
|
|
|
|
|
|
|
|
super.requestUpdate();
|
2022-07-21 17:57:50 +02:00
|
|
|
if(changedProperties.has("app") || changedProperties.has("entryId"))
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
if(this.app && this.entryId && !this._title)
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
|
|
|
this._title = Et2Link.MISSING_TITLE;
|
|
|
|
}
|
2022-07-21 17:57:50 +02:00
|
|
|
if(this.app && this.entryId && this._title == Et2Link.MISSING_TITLE)
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
|
|
|
// Title will be fetched from server and then set
|
2022-07-21 17:57:50 +02:00
|
|
|
this._titlePromise = this.egw()?.link_title(this.app, this.entryId, true).then(title =>
|
2022-05-10 00:06:16 +02:00
|
|
|
{
|
|
|
|
this._title = title;
|
|
|
|
// It's probably already been rendered
|
|
|
|
this.requestUpdate();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-22 11:45:56 +02:00
|
|
|
/**
|
|
|
|
* Handle dragstart event for dragging out a file
|
|
|
|
*
|
|
|
|
* @param _data
|
|
|
|
* @param _ev
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected _handleDragStart (_data, _ev)
|
|
|
|
{
|
|
|
|
// // Unfortunately, dragging files is currently only supported by Chrome
|
|
|
|
if(navigator && navigator.userAgent.indexOf('Chrome') >= 0) {
|
|
|
|
|
|
|
|
if (_ev.dataTransfer == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_data && _data.type && _data.download_url) {
|
|
|
|
_ev.dataTransfer.dropEffect = "copy";
|
|
|
|
_ev.dataTransfer.effectAllowed = "copy";
|
|
|
|
|
|
|
|
let url = _data.download_url;
|
|
|
|
|
|
|
|
// NEED an absolute URL
|
|
|
|
if (url[0] == '/') url = egw.link(url);
|
|
|
|
// egw.link adds the webserver, but that might not be an absolute URL - try again
|
|
|
|
if (url[0] == '/') url = window.location.origin + url;
|
|
|
|
|
|
|
|
// Unfortunately, dragging files is currently only supported by Chrome
|
|
|
|
if (navigator && navigator.userAgent.indexOf('Chrome')) {
|
2023-05-22 15:22:11 +02:00
|
|
|
_ev.dataTransfer.setData("DownloadURL", _data.type + ':' + this.title + ':' + url);
|
2023-05-22 11:45:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Include URL as a fallback
|
|
|
|
_ev.dataTransfer.setData("text/uri-list", url);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_ev.dataTransfer.types.length == 0) {
|
|
|
|
// No file data? Abort: drag does nothing
|
|
|
|
_ev.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-10 00:06:16 +02:00
|
|
|
_handleClick(_ev : MouseEvent) : boolean
|
|
|
|
{
|
2022-07-21 17:57:50 +02:00
|
|
|
// If we don't have app & entryId, nothing we can do
|
|
|
|
if(!this.app || !this.entryId || typeof this.entryId !== "string")
|
2022-05-12 18:54:29 +02:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2022-05-12 18:08:59 +02:00
|
|
|
// If super didn't handle it (returns false), just use egw.open()
|
|
|
|
if(super._handleClick(_ev))
|
|
|
|
{
|
|
|
|
this.egw().open(Object.assign({
|
|
|
|
app: this.app,
|
2022-07-21 17:57:50 +02:00
|
|
|
id: this.entryId
|
2023-05-01 18:28:36 +02:00
|
|
|
}, this.dataset), "", this.linkHook, this.dataset.extra_args, this.extraLinkTarget || this.app, this.targetApp || this.app);
|
2022-05-12 18:08:59 +02:00
|
|
|
}
|
2022-05-10 00:06:16 +02:00
|
|
|
|
|
|
|
_ev.stopImmediatePropagation();
|
2022-05-12 18:08:59 +02:00
|
|
|
return false;
|
2022-05-10 00:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
getDetachedAttributes(_attrs : string[])
|
|
|
|
{
|
2023-04-25 21:53:16 +02:00
|
|
|
_attrs.push("app", "entryId", "statustext");
|
2022-05-10 00:06:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
2022-05-11 00:02:33 +02:00
|
|
|
link_id? : string;
|
2022-05-10 00:06:16 +02:00
|
|
|
comment? : string
|
|
|
|
icon? : string,
|
2022-05-12 18:08:59 +02:00
|
|
|
help? : string,
|
2022-05-10 00:06:16 +02:00
|
|
|
|
|
|
|
// Extra information for things like files
|
|
|
|
download_url? : string,
|
|
|
|
target? : string,
|
|
|
|
mode? : number
|
2022-06-09 23:05:55 +02:00
|
|
|
}
|