From 6eed7b5a0e6656f604b00ba54797e77e9f3214a5 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Thu, 14 Jul 2022 17:32:45 +0200 Subject: [PATCH] WIP converting avatar widget to webcomponent --- api/js/etemplate/Et2Avatar/Et2Avatar.ts | 207 ++++++++++++- api/js/etemplate/Et2Avatar/Et2LAvatar.ts | 27 +- api/js/etemplate/Et2Avatar/cropperStyles.ts | 310 ++++++++++++++++++++ api/templates/default/avatar_edit.xet | 10 +- 4 files changed, 533 insertions(+), 21 deletions(-) create mode 100644 api/js/etemplate/Et2Avatar/cropperStyles.ts diff --git a/api/js/etemplate/Et2Avatar/Et2Avatar.ts b/api/js/etemplate/Et2Avatar/Et2Avatar.ts index d4682efc0d..4bbd367e36 100644 --- a/api/js/etemplate/Et2Avatar/Et2Avatar.ts +++ b/api/js/etemplate/Et2Avatar/Et2Avatar.ts @@ -14,15 +14,19 @@ import {SlAvatar} from "@shoelace-style/shoelace"; import {et2_IDetachedDOM} from "../et2_core_interfaces"; import {egw} from "../../jsapi/egw_global"; import shoelace from "../Styles/shoelace"; +import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; +import "../../../../vendor/bower-asset/cropper/dist/cropper.min.js"; +import {cropperStyles} from "./cropperStyles"; export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDetachedDOM { - + private _contact_id; static get styles() { return [ ...super.styles, shoelace, + cropperStyles, css` ` @@ -67,18 +71,66 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe image: { type: String, reflect: true - } + }, + + crop: {type: Boolean}, + + size: {type: String} } } - - constructor() { super(); - this.contact_id = ""; this.src = ""; this.label = ""; + this.contact_id = ""; + this.editable = false; + this.crop = false; + this.size = "3em"; + } + + /** + * Handle changes that have to happen based on changes to properties + * + */ + updated(changedProperties) + { + super.updated(changedProperties); + + if (changedProperties.has("crop")) { + if (this.crop && !this.readonly && this._imageNode) + { + jQuery(this._imageNode).cropper({aspectRatio: 1/1}); + } + } + if (changedProperties.has("size")) + { + this.getDOMNode().setAttribute('style', `--size:${this.size}`); + } + } + + firstUpdated() + { + let self = this; + if (this.contact_id && this.editable) + { + egw(window).json( + 'addressbook.addressbook_ui.ajax_noPhotoExists', + [this.contact_id], + function(noPhotoExists) + { + if (noPhotoExists) self.image=""; + self._buildEditableLayer(noPhotoExists); + } + ).sendRequest(true); + } + } + + + get contact_id() + { + return this._contact_id; } /** @@ -110,13 +162,15 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe id = 'contact_id'; _contact_id = _contact_id.replace('contact:', ''); } - + let oldContactId = this._contact_id; + this._contact_id = _contact_id; // if our src (incl. cache-buster) already includes the correct id, use that one - if (!this.options.src || !this.options.src.match("(&|\\?)contact_id="+_contact_id+"(&|\\$)")) + if (!this.src || !this.src.match("(&|\\?)contact_id="+_contact_id+"(&|\\$)")) { params[id] = _contact_id; this.src = egw.link('/api/avatar.php',params); } + this.requestUpdate("contact_id", oldContactId); } set value(_value) @@ -129,8 +183,138 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe this.image = _value; } + get _baseNode() + { + return this.shadowRoot.querySelector("[part='base']"); + } + + get _imageNode() + { + return this.shadowRoot.querySelector("[part='image']"); + } + /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * Build Editable Mask Layer (EML) in order to show edit/delete actions + * on top of profile picture. + * @param {boolean} _noDelete disable delete button in initialization + */ + private _buildEditableLayer(_noDelete : boolean) + { + let self = this; + let editBtn = document.createElement('sl-icon-button'); + editBtn.setAttribute('name', 'pencil'); + let delBtn = document.createElement('sl-icon-button'); + delBtn.setAttribute('name', 'trash'); + this._baseNode.append(editBtn); + this._baseNode.append(delBtn); + + delBtn.disabled = _noDelete; + + editBtn.addEventListener('click', function(){ + let buttons = [ + {"button_id": 1, label: self.egw().lang('save'), id: 'save', image: 'check', "default": true}, + {"button_id": 0, label: self.egw().lang('cancel'), id: 'cancel', image: 'cancelled'} + ]; + let dialog = function(_title, _value, _buttons, _egw_or_appname) + { + let dialog = new Et2Dialog(self.egw()); + dialog.transformAttributes({ + callback: function(_buttons, _value) + { + let widget = document.getElementById('_cropper_image'); + switch(_buttons) + { + case 1: + let canvas = jQuery(widget._imageNode).cropper('getCroppedCanvas'); + self.image = canvas.toDataURL("image/jpeg", 1.0) + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', + [self.getInstanceManager().etemplate_exec_id, canvas.toDataURL("image/jpeg", 1.0)], + function(res) + { + if(res) + { + delBtn.style.visibility = 'visible'; + } + }).sendRequest(); + break; + case '_rotate_reset': + jQuery(widget._imageNode).cropper('reset'); + return false; + case '_rotate_l': + jQuery(widget._imageNode).cropper('rotate', -90); + return false; + case '_rotate_r': + jQuery(widget._imageNode).cropper('rotate', 90); + return false; + } + }, + title: _title || egw.lang('Input required'), + buttons: _buttons || Et2Dialog.BUTTONS_OK_CANCEL, + value: { + content: _value + }, + width: "90%", + height: "450", + resizable: false, + position: "top+10", + template: egw.webserverUrl + '/api/templates/default/avatar_edit.xet' + }); + document.body.appendChild(dialog); + return dialog; + }; + + dialog(egw.lang('Edit avatar'),self.options, buttons, null); + }); + + + + delBtn.addEventListener('click', function() + { + Et2Dialog.show_dialog(function(_btn) + { + if(_btn == Et2Dialog.YES_BUTTON) + { + self.egw().json('addressbook.addressbook_ui.ajax_update_photo', + [self.getInstanceManager().etemplate_exec_id, null], + function(res) + { + if(res) + { + self.image = ''; + delBtn.style.visibility = 'none'; + egw.refresh('Avatar Deleted.', egw.app_name()); + } + }).sendRequest(); + } + }, egw.lang('Delete this photo?'), egw.lang('Delete'), null, Et2Dialog.BUTTONS_YES_NO); + }); + } + + /** + * Function runs after uplaod in avatar dialog is finished and it tries to + * update image and cropper container. + * @param {type} e + */ + static uploadAvatar_onFinish(e) + { + let file = e.data.resumable.files[0].file; + let reader = new FileReader(); + reader.onload = function (e) + { + let widget = document.getElementById('_cropper_image'); + widget.image = e.target.result; + // Wait for everything to complete + widget.getUpdateComplete().then(() => + { + jQuery(widget._imageNode).cropper('replace',e.target.result) + }); + + }; + reader.readAsDataURL(file); + } + + /** + * */ getDetachedAttributes(_attrs : string[]) { @@ -150,4 +334,9 @@ export class Et2Avatar extends Et2Widget(SlotMixin(SlAvatar)) implements et2_IDe } } } -customElements.define("et2-avatar", Et2Avatar as any); \ No newline at end of file +customElements.define("et2-avatar", Et2Avatar as any); +// make et2_avatar publicly available as we need to call it from templates +{ + window['et2_avatar'] = Et2Avatar; + window['Et2Avatar'] = Et2Avatar; +} \ No newline at end of file diff --git a/api/js/etemplate/Et2Avatar/Et2LAvatar.ts b/api/js/etemplate/Et2Avatar/Et2LAvatar.ts index cf83969230..d6999a3fba 100644 --- a/api/js/etemplate/Et2Avatar/Et2LAvatar.ts +++ b/api/js/etemplate/Et2Avatar/Et2LAvatar.ts @@ -55,19 +55,32 @@ export class Et2LAvatar extends Et2Avatar super(); this.lname = ""; this.fname = ""; - this.initials = Et2LAvatar.lavatar(this.fname, this.lname, this.contact_id).initials; } - set src(_url) + /** + * Handle changes that have to happen based on changes to properties + * + */ + updated(changedProperties) { - if (_url && decodeURIComponent(_url).match("lavatar=1") && (this.options.fname || this.options.lname) && this.options.contact_id) - { - this.initials = Et2LAvatar.lavatar(this.options.fname, this.options.lname, this.options.contact_id).initials; - return; + super.updated(changedProperties); + + if (changedProperties.has("lname") || changedProperties.has("fname") || changedProperties.has("contact_id")) { + if (!this.src || !decodeURIComponent(this.src).match("lavatar=1") && (this.fname || this.lname) && this.contact_id) + { + this.initials = Et2LAvatar.lavatar(this.fname, this.lname, this.contact_id).initials; + } } - super.src= _url; } + /** + * + */ + getDetachedAttributes(_attrs : string[]) + { + super.getDetachedAttributes(_attrs); + _attrs.push("lname", "fname"); + } /** * Generate letter avatar with given data diff --git a/api/js/etemplate/Et2Avatar/cropperStyles.ts b/api/js/etemplate/Et2Avatar/cropperStyles.ts new file mode 100644 index 0000000000..db4ba7de36 --- /dev/null +++ b/api/js/etemplate/Et2Avatar/cropperStyles.ts @@ -0,0 +1,310 @@ +/** + * Cropper styles constant + */ +import {css} from "@lion/core"; + +/*! + * Cropper.js v1.5.12 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2021-06-12T08:00:11.623Z + */ +export const cropperStyles = css` +.cropper-container { + direction: ltr; + font-size: 0; + line-height: 0; + position: relative; + -ms-touch-action: none; + touch-action: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.cropper-container img { + display: block; + height: 100%; + image-orientation: 0deg; + max-height: none !important; + max-width: none !important; + min-height: 0 !important; + min-width: 0 !important; + width: 100%; +} + +.cropper-wrap-box, +.cropper-canvas, +.cropper-drag-box, +.cropper-crop-box, +.cropper-modal { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} + +.cropper-wrap-box, +.cropper-canvas { + overflow: hidden; +} + +.cropper-drag-box { + background-color: #fff; + opacity: 0; +} + +.cropper-modal { + background-color: #000; + opacity: 0.5; +} + +.cropper-view-box { + display: block; + height: 100%; + outline: 1px solid #39f; + outline-color: rgba(51, 153, 255, 0.75); + overflow: hidden; + width: 100%; +} + +.cropper-dashed { + border: 0 dashed #eee; + display: block; + opacity: 0.5; + position: absolute; +} + +.cropper-dashed.dashed-h { + border-bottom-width: 1px; + border-top-width: 1px; + height: calc(100% / 3); + left: 0; + top: calc(100% / 3); + width: 100%; +} + +.cropper-dashed.dashed-v { + border-left-width: 1px; + border-right-width: 1px; + height: 100%; + left: calc(100% / 3); + top: 0; + width: calc(100% / 3); +} + +.cropper-center { + display: block; + height: 0; + left: 50%; + opacity: 0.75; + position: absolute; + top: 50%; + width: 0; +} + +.cropper-center::before, +.cropper-center::after { + background-color: #eee; + content: ' '; + display: block; + position: absolute; +} + +.cropper-center::before { + height: 1px; + left: -3px; + top: 0; + width: 7px; +} + +.cropper-center::after { + height: 7px; + left: 0; + top: -3px; + width: 1px; +} + +.cropper-face, +.cropper-line, +.cropper-point { + display: block; + height: 100%; + opacity: 0.1; + position: absolute; + width: 100%; +} + +.cropper-face { + background-color: #fff; + left: 0; + top: 0; +} + +.cropper-line { + background-color: #39f; +} + +.cropper-line.line-e { + cursor: ew-resize; + right: -3px; + top: 0; + width: 5px; +} + +.cropper-line.line-n { + cursor: ns-resize; + height: 5px; + left: 0; + top: -3px; +} + +.cropper-line.line-w { + cursor: ew-resize; + left: -3px; + top: 0; + width: 5px; +} + +.cropper-line.line-s { + bottom: -3px; + cursor: ns-resize; + height: 5px; + left: 0; +} + +.cropper-point { + background-color: #39f; + height: 5px; + opacity: 0.75; + width: 5px; +} + +.cropper-point.point-e { + cursor: ew-resize; + margin-top: -3px; + right: -3px; + top: 50%; +} + +.cropper-point.point-n { + cursor: ns-resize; + left: 50%; + margin-left: -3px; + top: -3px; +} + +.cropper-point.point-w { + cursor: ew-resize; + left: -3px; + margin-top: -3px; + top: 50%; +} + +.cropper-point.point-s { + bottom: -3px; + cursor: s-resize; + left: 50%; + margin-left: -3px; +} + +.cropper-point.point-ne { + cursor: nesw-resize; + right: -3px; + top: -3px; +} + +.cropper-point.point-nw { + cursor: nwse-resize; + left: -3px; + top: -3px; +} + +.cropper-point.point-sw { + bottom: -3px; + cursor: nesw-resize; + left: -3px; +} + +.cropper-point.point-se { + bottom: -3px; + cursor: nwse-resize; + height: 20px; + opacity: 1; + right: -3px; + width: 20px; +} + +@media (min-width: 768px) { +.cropper-point.point-se { + height: 15px; + width: 15px; + } +} + +@media (min-width: 992px) { +.cropper-point.point-se { + height: 10px; + width: 10px; + } +} + +@media (min-width: 1200px) { +.cropper-point.point-se { + height: 5px; + opacity: 0.75; + width: 5px; + } +} + +.cropper-point.point-se::before { + background-color: #39f; + bottom: -50%; + content: ' '; + display: block; + height: 200%; + opacity: 0; + position: absolute; + right: -50%; + width: 200%; +} + +.cropper-invisible { + opacity: 0; +} + +.cropper-bg { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); +} + +.cropper-hide { + display: block; + height: 0; + position: absolute; + width: 0; +} + +.cropper-hidden { + display: none !important; +} + +.cropper-move { + cursor: move; +} + +.cropper-crop { + cursor: crosshair; +} + +.cropper-disabled .cropper-drag-box, +.cropper-disabled .cropper-face, +.cropper-disabled .cropper-line, +.cropper-disabled .cropper-point { + cursor: not-allowed; +} +`; \ No newline at end of file diff --git a/api/templates/default/avatar_edit.xet b/api/templates/default/avatar_edit.xet index e279a48299..07e02f776b 100644 --- a/api/templates/default/avatar_edit.xet +++ b/api/templates/default/avatar_edit.xet @@ -4,13 +4,13 @@