/**
* EGroupware eTemplate2 - Image widget
*
* @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 {html, LitElement} from "lit";
import {Et2Widget} from "../Et2Widget/Et2Widget";
import {et2_IDetachedDOM} from "../et2_core_interfaces";
import {property} from "lit/decorators/property.js";
import {customElement} from "lit/decorators/custom-element.js";
import {until} from "lit/directives/until.js";
import {unsafeHTML} from "lit/directives/unsafe-html.js";
@customElement("et2-image")
export class Et2Image extends Et2Widget(LitElement) implements et2_IDetachedDOM
{
/** Et2Image has no shadow DOM, styles in etemplate2.css
static get styles()
{
return [
...super.styles,
css`
:host {
display: inline-block;
}
::slotted(img) {
max-height: 100%;
max-width: 100%;
}
:host([icon]) {
height: 1.3rem;
font-size: 1.3rem !important;
}
`];
}
*/
/**
* The label of the image
* Actually not used as label, but we put it as title
*/
@property({type: String})
label = "";
/**
* Image
* Displayed image
*/
@property({type: String})
set src(_src)
{
this.classList.forEach(_class =>
{
if(_class.startsWith('bi-'))
{
this.classList.remove(_class);
}
});
this.__src = _src;
let url = this.parse_href(_src) || this.parse_href(this.defaultSrc);
if(!url)
{
// Hide if no valid image
if (this._img) this._img.src = '';
return;
}
const bootstrap = url.match(/\/node_modules\/bootstrap-icons\/icons\/([^.]+)\.svg/);
if (bootstrap && !this._img)
{
this.classList.add('bi-' + bootstrap[1]);
return;
}
// change between bootstrap and regular img
this.requestUpdate();
}
get src()
{
return this.__src;
}
private __src: string;
/**
* Default image
* Image to use if src is not found
*/
@property({type: String})
defaultSrc = "";
/**
* Link Target
* Link URL, empty if you don't wan't to display a link.
*/
@property({type: String})
href = "";
/**
* Link target
* Link target descriptor
*/
@property({type: String})
extraLinkTarget = "_self";
/**
* Popup
* widthxheight, if popup should be used, eg. 640x480
*/
@property({type: String})
extraLinkPopup = "";
/**
* Width of image:
* - either number of px (e.g. 32) or
* - string incl. CSS unit (e.g. "32px") or
* - even CSS functions like e.g. "calc(1rem + 2px)"
*/
@property({type: String})
set width(_width : string)
{
if (this.style)
{
this.style.width = isNaN(_width) ? _width : _width+'px';
}
}
get width()
{
return this.style?.width;
}
/**
* Height of image:
* - either number of px (e.g. 32) or
* - string incl. CSS unit (e.g. "32px") or
* - even CSS functions like e.g. "calc(1rem + 2px)"
*/
@property({type: String})
set height(_height)
{
if (this.style)
{
this.style.height = isNaN(_height) ? _height : _height+'px';
}
}
get height()
{
return this.style.height;
}
constructor()
{
super();
this._handleClick = this._handleClick.bind(this);
}
connectedCallback()
{
super.connectedCallback();
}
render()
{
let url = this.parse_href(this.src) || this.parse_href(this.defaultSrc);
if(!url)
{
// Hide if no valid image
return html``;
}
// set title on et2-image for both bootstrap-image via css-class and embedded img tag
this.title = this.statustext || this.label || "";
const bootstrap = url.match(/\/node_modules\/bootstrap-icons\/icons\/([^.]+)\.svg/);
if (bootstrap)
{
this.classList.add('bi-'+bootstrap[1]);
return html``;
}
// our own svg images
// We have svg images prefixed "bi-". These are used like bootstrap font icons.
// We inline them to be able to control there color etc. directly via css
//only call unsafeHtml when we are inside /egroupware/
const ourSvg = url.startsWith(this.egw().webserverUrl + '/') //checks if source is trusted
if (ourSvg && url.match(/\/bi-.*\.svg/))
{
const svg = fetch(url)
.then(res => res.text()
.then(text => unsafeHTML(text)));
return html`
${until(svg, html`...`)}
`
}
// fallback case (no svg, web source)
return html`
`;
}
/**
* Puts the rendered content / img-tag in light DOM
* @link https://lit.dev/docs/components/shadow-dom/#implementing-createrenderroot
*/
protected createRenderRoot()
{
return this;
}
protected parse_href(img_href : string) : string
{
img_href = img_href || '';
// allow url's too
if(img_href[0] == '/' || img_href.substr(0, 4) == 'http' || img_href.substr(0, 5) == 'data:')
{
return img_href;
}
let src = this.egw() && typeof this.egw().image == "function" ? this.egw()?.image(img_href) : "";
if(src)
{
return src;
}
return "";
}
_handleClick(_ev : MouseEvent) : boolean
{
if(this.href)
{
this.egw().open_link(this.href, this.extraLinkTarget, this.extraLinkPopup);
}
else
{
return super._handleClick(_ev);
}
}
get _img()
{
return this.querySelector('img');
}
/**
* Handle changes that have to happen based on changes to properties
*
*/
updated(changedProperties)
{
super.updated(changedProperties);
// if there's an href or onclick, make it look clickable
if(changedProperties.has("href") || typeof this.onclick !== "undefined")
{
this.classList.toggle("et2_clickable", this.href || typeof this.onclick !== "undefined")
}
for(const changedPropertiesKey in changedProperties)
{
if(Et2Image.getPropertyOptions()[changedPropertiesKey] &&
!(changedPropertiesKey === 'label' || changedPropertiesKey === 'statustext'))
{
this._img[changedPropertiesKey] = this[changedPropertiesKey];
}
}
}
transformAttributes(_attrs : any)
{
super.transformAttributes(_attrs);
// Expand src with additional stuff
// This should go away, since we're not checking for $ or @
if(typeof _attrs["src"] != "undefined")
{
let manager = this.getArrayMgr("content");
if(manager && _attrs["src"])
{
let src = manager.getEntry(_attrs["src"], false, true);
if(typeof src != "undefined" && src !== null)
{
if(typeof src == "object")
{
this.src = this.egw().link('/index.php', src);
}
else
{
this.src = src;
}
}
}
}
}
/**
* Code for implementing et2_IDetachedDOM
*
* Individual widgets are detected and handled by the grid, but the interface is needed for this to happen
*
* @param {array} _attrs array to add further attributes to
*/
getDetachedAttributes(_attrs)
{
_attrs.push("src", "label", "href", "statustext");
}
getDetachedNodes()
{
return [this];
}
setDetachedAttributes(_nodes, _values)
{
for(let attr in _values)
{
this[attr] = _values[attr];
}
}
}