import { Cookie } from '../../../classes/cookies.js';
import { EMPTY_STRING } from '../../../constants/constants.js';
import { REG_NUMBERS } from '../../../constants/regexp.js';
import { BnumEvent, MelConditionnalEvent } from '../../../mel_events.js';
import { MelObject } from '../../../mel_object.js';
import { Mel_Promise } from '../../../mel_promise.js';
import {
EWebComponentMode,
HtmlCustomTag,
} from './js_html_base_web_elements.js';
export { AvatarElement };
//#region JsDoc
/**
* Contient la classe lié aux éléments custom "bnum-avatar" ainsi que toutes les classes et fonctions utile.
*
* Le chargement de se module implique le chargement des images après le chargement de la page.
* @module WebComponents/Avatar
* @tutorial webcomponent-avatar
* @local OnImageLoadCallback
* @local OnImageNotLoadCallback
* @local AvatarElement
* @local onLoaded
* @local AvatarEvent
* @local AvatarLoadEvent
* @local AvatarNotLoadEvent
*/
/**
* @callback OnImageLoadCallback
* @param {HTMLImageElement} img Node de l'image chargée
* @param {AvatarElement} avatarElement Node qui contient l'image
* @returns {void}
*/
/**
* @callback OnImageNotLoadCallback
* @param {AvatarElement} avatarElement Element qui contient l'image qui n'a pas été chargé
* @returns {null | undefined | {stop:boolean}}
*/
//#endregion
//#region Constantes
/**
* Style dans le shadow dom de l'image
* @type {string}
* @constant
* @package
*/
const STYLE_BASE = `
img {
filter: blur(0.2em);
transition: filter 0.5s;
object-fit: cover;
border: var(--avatar-border);
border-radius: 100%;
width:100%;
height:100%;
box-sizing: var(--avatar-box-sizing);
}
`;
/**
* Style dans le shadow dom de l'image lorsqu'elle est chargée
* @type {string}
* @constant
* @package
*/
const STYLE_LOADED = `
img {
filter: blur(0)!important;
--avatar-border: var(--avatar-border-loaded)!important;
--avatar-box-sizing: var(--avatar-box-sizing-loaded)!important;
}
`;
/**
* Style du host
* @type {string}
* @constant
* @package
*/
const STYLE_HOST = `
:host {
width:%0%1;
height:%0%1;
}
`;
/**
* Style dans le shadow dom lorsque l'image n'est pas chargé
* @type {string}
* @constant
* @package
*/
const STYLE_ERROR = `
.absolute-center {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translateY(-50%) translateX(-50%);
transform: translateY(-50%) translateX(-50%);
font-size: xx-large;
color: var(--mel-button-text-color);
}
.no-picture {
position:relative;
display: block;
width: 100%;
height: 100%;
}`;
/**
* Nom de l'évènement lorsque l'image est chargée
* @type {string}
* @constant
* @package
* @default 'api:imgload'
*/
const EVENT_IMAGE_LOAD = 'api:imgload';
/**
* Nom de l'évènement lorsque l'image n'est pas chargée
* @type {string}
* @constant
* @package
* @default 'api:imgloaderror'
*/
const EVENT_IMAGE_NOT_LOAD = 'api:imgloaderror';
/**
* Nom du cookie servant à contenir les adresse email des avatars à charger en erreur instantanément.
* @type {string}
* @constant
* @package
* @default 'avatars_in_memory'
*/
const COOKIE_NAME = 'avatars_in_memory';
/**
* Expiration du cookie servant à contenir les adresse email des avatars à charger en erreur instantanément.
* @type {number}
* @constant
* @package
* @default 7
*/
const COOKIE_EXPIRE = 7;
/**
* Nombre d'adresse email max dans le cookie servant à contenir les adresse email des avatars à charger en erreur instantanément.
* @type {number}
* @constant
* @package
* @default 20
*/
const COOKIE_LENGTH_MAX = 20;
/**
* Si on active le cookie d'erreurs ou non.
* @type {boolean}
* @constant
* @package
* @default true
*/
const ENABLE_COOKIE = false;
/**
* Url de l'avatar. Remplacez %0 par l'email.
* @constant
* @package
*/
const AVATAR_URL = MelObject.Empty().url('mel_metapage', {
action: 'avatar',
params: { _email: '%0' },
});
//#endregion
//#region Classes d'évènements
/**
* @class
* @classdesc Evènement de base pour l'avatar.
* @abstract
* @extends CustomEvent
* @package
*/
class AvatarEvent extends CustomEvent {
/**
* Le principe est que cette classe ainsi que les classes filles auront une valeur de retour en mémoire.
*
* Cette valeur de retour pourra ensuite être utiliser par les `BnumEvent` ou d'autres fonctions.
* @param {string} type Type d'évènement
* @param {Object<string, *>} config Configuration de l'évènement
*/
constructor(type, config = {}) {
super(type, config);
/**
* Données de retour.
* @private
* @type {?any}
*/
this._return_data = null;
}
/**
* Permet de données une valeur qui sera mise en mémoire pour plus tard.
* @param {*} value Valeur à mettre en mémoire.
*/
setReturnData(value) {
this._return_data = value;
}
/**
* Récupère la valeur mise en mémoire.
* @returns {?*}
*/
getReturnData() {
return this._return_data;
}
}
/**
* @class
* @classdesc Evènement de type `api:imgload`.
* @extends AvatarEvent
* @package
*/
class AvatarLoadEvent extends AvatarEvent {
/**
* Evènement reçu lorsque l'on fait un listener sur `api:imgload`.
* @param {HTMLImageElement} imageNode Image qui a été changée
* @param {AvatarElement} avatarNode Node parente
*/
constructor(imageNode, avatarNode) {
super(EVENT_IMAGE_LOAD);
/**
* @private
*/
this._imageNode = imageNode;
/**
* @private
*/
this._avatarNode = avatarNode;
}
/**
* Récupère l'image
* @returns {HTMLImageElement}
*/
image() {
return this._imageNode;
}
/**
* Récupère l'avatar
* @returns {AvatarElement}
*/
avatar() {
return this._avatarNode;
}
}
/**
* @class
* @classdesc Evènement de type `api:imgloaderror`.
* @extends AvatarEvent
* @package
*/
class AvatarNotLoadEvent extends AvatarEvent {
/**
* Evènement reçu lorsque l'on fait un listener sur `api:imgloaderror`.
* @param {AvatarElement} avatarNode Node parente
*/
constructor(avatarNode) {
super(EVENT_IMAGE_NOT_LOAD);
/**
* @private
*/
this._avatarNode = avatarNode;
}
/**
* Récupère l'avatar
* @returns {AvatarElement}
*/
avatar() {
return this._avatarNode;
}
/**
* Empêche la fonction _on_error de s'éxécuter normalement.
* @returns {void}
*/
stop() {
return this.setReturnData({ stop: true });
}
}
//#endregion
//#region Element Html
/**
* @class
* @classdesc Gestion de la balise bnum-avatar.
* @extends HtmlCustomTag
* @tutorial webcomponent-avatar
* @frommodule WebComponents/Base
*/
class AvatarElement extends HtmlCustomTag {
/**
* Contient ou non, un timeout lié au chargement des avatars.
*
* Les avatars se chargent au bout de 5 secondes.
* @type {null | undefined |number }
* @private
*/
#timeout;
/**
* La balise bnum-avatar permet de charger l'avatar de l'utilisateur en cours ou d'un utilisateur du bnum.
*
* Le chargement des avatars se fait après le chargement de la page. On peut néanmoins le forcer avec le data `data-forceload`.
*
* Les évènements sont api:imgload et api:imgloaderror.
*
* Liste des data :
*
* data-email => email de l'utilisateur dont on souhaite l'avatar. Si indéfini, se sera l'utilisateur en cours. (Optionnel)
*
* data-force-size => taille de l'objet, en pourcentage. (Optionnel)
*
* data-f100 => Equivalent de `data-force-size=100`
*
* data-forceload => Force le chargement de l'image
*
*/
constructor() {
super({ mode: EWebComponentMode.div });
/**
* Email qui permettra de retrouver l'avatar de l'utilisateur
* @package
* @type {string}
*/
this._email = null;
this._id = null;
this._errorBackgroundColor = null;
/**
* Taille (de 0 à 100) de la balise.
*
* Si null, ça sera le css qui s'en chargera.
* @package
* @type {string | number | null}
*/
this._force = null;
this.#timeout = null;
/**
* Action à faire lorsque l'image est chargée.
* @type {BnumEvent<OnImageLoadCallback>}
* @frommodule WebComponents/Avatar {@linkto OnImageLoadCallback}
*/
this.onimgload = new BnumEvent();
/**
* Action à faire lorsque l'image n'a pas réussie à être chargée.
* @type {BnumEvent<OnImageNotLoadCallback>}
* @frommodule WebComponents/Avatar {@linkto OnImageNotLoadCallback}
*/
this.onimgloaderror = new BnumEvent();
//Ajoute les évènements liés aux listeners javascript/jquery.
this.onimgload.push((...args) => {
const [imageNode, avatarElement] = args;
this.dispatchEvent(new AvatarLoadEvent(imageNode, avatarElement));
});
this.onimgloaderror.push((...args) => {
const [avatarElement] = args;
let customEvent = new AvatarNotLoadEvent(avatarElement);
this.dispatchEvent(customEvent);
return customEvent.getReturnData();
});
this.state = false;
}
/**
* Est appelé par le navigateur.
* @protected
*/
_p_main() {
super._p_main();
if (!['false', false, true, 'true'].includes(this.data('shadow')))
this.data('shadow', true);
//Init
Object.defineProperty(this, '_email', {
value:
this.dataset.email || rcmail?.env?.mel_metapage_user_emails?.[0] || '?',
});
Object.defineProperty(this, '_errorBackgroundColor', {
value: this.data('error-background-color') ?? null,
writable: false,
configurable: false,
});
this.removeAttribute('data-error-background-color');
this.removeAttribute('data-email');
this.removeAttribute('data-id');
if (this.dataset.f100) {
this.setAttribute('data-force-size', '100');
this.removeAttribute('data-f100');
}
let imgInMemory = null; /*this._canBeSaveInMemory()
? MelObject.Empty().load(`avatar_${this._errorBackgroundColor}`)
: null;*/
if (imgInMemory) this.saved = true;
if (AvatarElement.IsLoaded || imgInMemory) {
this.setAttribute('data-forceload', true);
}
this.setAttribute('data-needcreation', true);
let shadow = this._p_start_construct();
let img = document.createElement('img');
img.src = imgInMemory ?? 'skins/elastic/images/contactpic.svg';
if (this.shadowEnabled()) {
let style = document.createElement('style');
let style_ex = EMPTY_STRING;
if (this.dataset.forceSize) {
this._force = this.dataset.forceSize;
style_ex = this._get_style_force();
this.removeAttribute('data-force-size');
}
style.append(document.createTextNode(STYLE_BASE + style_ex));
shadow.append(style);
style = null;
}
shadow.append(img);
//end
img = null;
if (ENABLE_COOKIE && this._cookie_exist(this._email)) {
this.removeAttribute('data-forceload');
this._on_error();
} else {
if (this.dataset.forceload) {
setTimeout(() => {
// this.removeAttribute('data-needcreation');
this.removeAttribute('data-forceload');
this.update_img();
}, 10);
} else {
this.#timeout = setTimeout(() => {
this.update_img();
}, 5 * 1000);
}
}
}
/**
* Met la bonne url à l'image.
*/
update_img() {
this.setAttribute('data-loading', true);
if (this.saved) return this._on_load();
let url = AVATAR_URL.replace('%0', this._email);
if (this._errorBackgroundColor)
url += `&_background=${this._errorBackgroundColor.replaceAll('#', EMPTY_STRING)}`;
this.setAttribute('data-state', 'loading');
let img = this.navigator.querySelector('img');
img.onload = this._on_load.bind(this);
img.onerror = this._on_error.bind(this);
img.src = url.replaceAll('_is_from=iframe', EMPTY_STRING);
img = null;
}
/**
* Récupère le block de style lié au forcage de la taille
* @package
* @returns {string}
*/
_get_style_force() {
const unit = this._force.replace(REG_NUMBERS, EMPTY_STRING) || '%';
return STYLE_HOST.replaceAll(
'%0',
this._force.replaceAll(unit, EMPTY_STRING),
).replaceAll('%1', unit);
}
/**
* Vérifie si le cookie éxiste
* @private
* @param {string} mail Mail à tester
* @returns {boolean}
*/
_cookie_exist(mail) {
let cookie_avatars = Cookie.get_cookie(COOKIE_NAME)?.value?.split?.(',');
return cookie_avatars && cookie_avatars.includes(mail);
}
/**
* Ajoute un mail au cookie.
*
* Les mails dans se cookie seront charger instantanément.
* @param {string} mail Mail à ajouter au cookie
* @private
*/
_add_cookie(mail) {
let cookie_avatars =
Cookie.get_cookie(COOKIE_NAME)?.value?.split?.(',') ?? [];
if (
(cookie_avatars.length <= COOKIE_LENGTH_MAX ||
mail === rcmail?.env?.mel_metapage_user_emails?.[0]) &&
!cookie_avatars.includes(mail)
) {
cookie_avatars.push(mail);
Cookie.set_cookie(
COOKIE_NAME,
cookie_avatars.join(','),
moment().add(COOKIE_EXPIRE, 'd').toDate(),
);
}
}
/**
* Appelé lorsque l'image est chargée.
* @package
* @returns {AvatarElement} Chaîne
*/
_on_load() {
if (this.#timeout) {
clearTimeout(this.#timeout);
this.#timeout = null;
}
this.removeAttribute('data-loading');
this.removeAttribute('data-needcreation');
this.setAttribute('data-state', 'loaded');
if (this.shadowEnabled()) {
let style = document.createElement('style');
style.append(document.createTextNode(STYLE_LOADED));
this.shadowRoot.append(style);
style = null;
}
let img = this.navigator.querySelector('img');
img.onload = null;
img.onerror = null;
this.onimgload.call(img, this);
//save
if (this._canBeSaveInMemory()) {
const data = this._toData(img);
MelObject.Empty().save(`avatar_${this._errorBackgroundColor}`, data);
}
this.state = true;
return this;
}
_canBeSaveInMemory() {
return (
this._email === rcmail?.env?.mel_metapage_user_emails?.[0] &&
this._email &&
!this.saved
);
}
_toData(img) {
return AvatarElement.ImgToData(img);
}
getData() {
return this._toData(this.navigator.querySelector('img'));
}
static ImgToData(img) {
img = img.cloneNode();
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
const data = canvas.toDataURL();
canvas.remove();
canvas = null;
img.remove();
img = null;
return data;
}
/**
* Est appelé lorsque l'image ne se charge pas
* @returns {AvatarElement} Chaîne
* @package
*/
_on_error() {
if (this.#timeout) {
clearTimeout(this.#timeout);
this.#timeout = null;
}
this.removeAttribute('data-loading');
this.removeAttribute('data-needcreation');
this.setAttribute('data-state', 'error');
if (!this._cookie_exist(this._email)) this._add_cookie(this._email);
let error_data = this.onimgloaderror.call(this);
if (Array.isArray(error_data)) {
for (const element of error_data) {
if (element.stop === true) return this;
}
} else if (error_data && error_data.stop === true) return this;
const txt = this._email;
this.navigator.querySelector('img').remove();
if (this.shadowEnabled()) this.shadowRoot.querySelector('style').remove();
let element = document.createElement('span');
element.classList.add('no-picture');
let span = document.createElement('span');
span.appendChild(
document.createTextNode(txt.substring(0, 1).toUpperCase()),
);
span.classList.add('absolute-center');
element.appendChild(span);
if (this.shadowEnabled()) {
let style = document.createElement('style');
style.append(
document.createTextNode(
STYLE_ERROR + (this._force ? this._get_style_force() : EMPTY_STRING),
),
);
this.shadowRoot.append(style);
style = null;
}
this.navigator.append(element);
element = null;
span = null;
this.state = true;
return this;
}
async waitLoading() {
return Mel_Promise.wait(() => this.state);
}
/**
*
* @param {*} param0
* @returns {AvatarElement}
*/
static Create({
id = null,
email = null,
force = null,
error_background_color = null,
} = {}) {
let node = document.createElement('bnum-avatar');
if (email) node.setAttribute('data-email', email);
if (id) node.setAttribute('data-id', id);
if (force) node.setAttribute('data-forceload', true);
if (error_background_color)
node.setAttribute('data-error-background-color', error_background_color);
return node;
}
}
/**
* Si la page a été chargé et les avatars aussi.
* @static
* @readonly
* @type {boolean}
*/
AvatarElement.IsLoaded;
Object.defineProperty(AvatarElement, 'IsLoaded', {
get() {
return !!window.avatarPageLoaded;
},
});
//#endregion
//#region Definition
{
const TAG = 'bnum-avatar';
if (!customElements.get(TAG)) customElements.define(TAG, AvatarElement);
}
//#endregion
if (!window.avatarloadedthings) {
window.addEventListener('load', function () {
onLoaded();
});
window.avatarloadedthings = true;
}
//#region Chargement
/**
* Charge tout les avatars qui ont besoin d'être chargés.
* @package
*/
function onLoaded(timeout = true) {
let imagesToLoad = document.querySelectorAll(
'bnum-avatar[data-needcreation]:not([data-loading])',
);
for (const image of imagesToLoad) {
image.update_img();
}
window.avatarPageLoaded = true;
if (timeout) {
setTimeout(() => {
onLoaded(false);
}, 1000);
}
}
//#endregion