import {LitElement} from 'lit'; import {Logger} from './logger'; export class AdapterLitElement extends LitElement { constructor() { super(); this.connected = false; this.deferSubscribe = false; this.deferUnSubscribe = false; // attributes (if they exist) will be updated if a property is changed by "subscribe" this.reflectAttribute = true; // default values this.subscribe = ''; this.unsubscribe = ''; this.callbackStore = []; // Previously we used direct properties like this["lang"] (instead of this.propertyStore["lang"]) for storing the // properties, but the "lang" property seems to be updated before the event from the MutationObserver, so we // cannot observe a value change directly (as workaround we use another property (e.g. "langValue") instead of "lang") this.propertyStore = {}; // We need to store our own "last values" because we cannot be sure what the MutationObserver detects this.lastProperties = {}; Logger.debug('AdapterLitElement(' + this.tagName + ') constructor()'); } getProperty(name) { return this.propertyStore[name]; } getPropertyByAttributeName(name) { return this[this.findPropertyName(name)]; } setPropertyByAttributeName(name, value) { this[this.findPropertyName(name)] = value; } setProperty(name, value) { // TODO: check if lit attribute really present? if (typeof value === 'object' && value !== null) { // Logger.debug("value is object", value); this.setPropertyByAttributeName(name, value); } else { this.attributeChangedCallback(name, this.getPropertyByAttributeName(name), value); } this.lastProperties[name] = value; this.propertyStore[name] = value; } hasPropertyChanged(name, value) { return this.lastProperties[name] !== value; } hasProperty(name) { // return this.hasAttribute("name") return Object.hasOwnProperty.call(this.propertyStore, name); } connectedCallback() { super.connectedCallback(); if (this.deferUnSubscribe) { const attrs = this.unsubscribe.split(','); attrs.forEach((element) => this.unSubscribeProviderFor(element)); this.deferSubscribe = false; this.unsubscribe = ''; } if (this.deferSubscribe) { const attrs = this.subscribe.split(','); attrs.forEach((element) => this.subscribeProviderFor(element)); this.deferSubscribe = false; } this.connected = true; const that = this; this.addEventListener( 'dbp-subscribe', function (e) { const name = e.detail.name; if (that.hasProperty(name) || that.providerRoot) { Logger.debug( 'AdapterLitElementProvider(' + that.tagName + ') eventListener("dbp-subscribe",..) name "' + name + '" found.' ); that.callbackStore.push({ name: name, callback: e.detail.callback, sender: e.detail.sender, }); e.detail.callback(that.getProperty(name)); e.stopPropagation(); } }, false ); this.addEventListener( 'dbp-unsubscribe', function (e) { const name = e.detail.name; const sender = e.detail.sender; if (that.hasProperty(name) || that.providerRoot) { Logger.debug( 'AdapterLitElementProvider(' + that.tagName + ') eventListener("dbp-unsubscribe",..) name "' + name + '" found.' ); that.callbackStore.forEach((item) => { if (item.sender === sender && item.name === name) { const index = that.callbackStore.indexOf(item); that.callbackStore.splice(index, 1); Logger.debug( 'AdapterLitElementProvider(' + that.tagName + ') eventListener for name "' + name + '" removed.' ); } }); e.stopPropagation(); } }, false ); // listen to property changes this.addEventListener( 'dbp-set-property', function (e) { const name = e.detail.name; const value = e.detail.value; if (that.hasProperty(name) || that.providerRoot) { Logger.debug( 'AdapterLitElementProvider(' + that.tagName + ') eventListener("dbp-set-property",..) name "' + name + '" found.' ); that.setProperty(name, value); that.callbackStore.forEach((item) => { if (item.name === name) { item.callback(value); } }); e.stopPropagation(); } }, false ); // Options for the observer (which mutations to observe) const config = {attributes: true, childList: false, subtree: false}; // Callback function to execute when mutations are observed const callback = function (mutationsList, observer) { // Use traditional 'for loops' for IE 11 for (const mutation of mutationsList) { if (mutation.type === 'attributes') { const name = mutation.attributeName; const value = that.getAttribute(name); if (that.hasPropertyChanged(name, value)) { Logger.debug( 'AdapterLitElementProvider (' + that.tagName + ') observed attribute "' + name + '" changed' ); that.setProperty(name, value); that.callbackStore.forEach((item) => { if (item.name === name) { item.callback(value); } }); } } } }; // Create an observer instance linked to the callback function const observer = new MutationObserver(callback); // Start observing the target node for configured mutations observer.observe(this, config); // get all *not observed* attributes if (this.hasAttributes()) { const attrs = this.attributes; for (let i = attrs.length - 1; i >= 0; i--) { if (['id', 'class', 'style', 'data-tag-name'].includes(attrs[i].name)) { continue; } this.setProperty(attrs[i].name, attrs[i].value); Logger.debug( 'AdapterLitElementProvider (' + that.tagName + ') found attribute "' + attrs[i].name + '" = "' + attrs[i].value + '"' ); } } } disconnectedCallback() { const attrs = this.subscribe.split(','); attrs.forEach((element) => this.unSubscribeProviderFor(element)); super.disconnectedCallback(); } subscribeProviderFor(element) { Logger.debug( 'AdapterLitElement(' + this.tagName + ') subscribeProviderFor( ' + element + ' )' ); const pair = element.trim().split(':'); const local = pair[0]; const global = pair[1] || local; const that = this; const event = new CustomEvent('dbp-subscribe', { bubbles: true, composed: true, detail: { name: global, callback: (value) => { // Don't send back "undefined" if the attribute wasn't found (for example if the providerRoot // is used and the attribute was subscribed but not set anywhere), because that will be // interpreted as "true" for Boolean lit-element attributes! if (value === undefined) { return; } Logger.debug( 'AdapterLitElement(' + that.tagName + ') sub/Callback ' + global + ' -> ' + local + ' = ' + value ); // If value is an object set it directly as property if (typeof value === 'object' && value !== null) { // Logger.debug("value is object", value); that.setPropertyByAttributeName(local, value); } else { // Logger.debug("local, that.getPropertyByAttributeName(local), value", local, that.getPropertyByAttributeName(local), value); that.attributeChangedCallback( local, that.getPropertyByAttributeName(local), value ); // check if an attribute also exists in the tag if (that.getAttribute(local) !== null) { // we don't support attributes and provider values at the same time console.warn( 'Provider callback: "' + local + '" is also an attribute in tag "' + that.tagName + '", this is not supported!' ); // update attribute if reflectAttribute is enabled if (that.reflectAttribute) { that.setAttribute(local, value); } } } }, sender: this, }, }); this.dispatchEvent(event); } unSubscribeProviderFor(element) { Logger.debug( 'AdapterLitElement(' + this.tagName + ') unSubscribeProviderFor( ' + element + ' )' ); const pair = element.trim().split(':'); const global = pair[1] || pair[0]; const event = new CustomEvent('dbp-unsubscribe', { bubbles: true, composed: true, detail: { name: global, sender: this, }, }); this.dispatchEvent(event); } static get properties() { return { ...super.properties, subscribe: {type: String}, unsubscribe: {type: String}, providerRoot: {type: Boolean, attribute: 'provider-root'}, }; } findPropertyName(attributeName) { let resultName = attributeName; const properties = this.constructor.properties; // Logger.debug("properties", properties); for (const propertyName in properties) { // Logger.debug("findPropertyName", `${propertyName}: ${properties[propertyName]}`); const attribute = properties[propertyName].attribute; if (attribute === attributeName) { resultName = propertyName; break; } } return resultName; } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case 'subscribe': Logger.debug( 'AdapterLitElement() attributeChangesCallback( ' + name + ', ' + oldValue + ', ' + newValue + ')' ); if (this.subscribe && this.subscribe.length > 0) { if (this.connected) { const attrs = this.subscribe.split(','); attrs.forEach((element) => this.unSubscribeProviderFor(element)); } else { this.deferUnSubscribe = this.subscribe.length > 0; this.unsubscribe = this.subscribe; } } if (newValue !== null) { this.subscribe = newValue; if (this.connected) { const attrs = newValue.split(','); attrs.forEach((element) => this.subscribeProviderFor(element)); } else { this.deferSubscribe = newValue && newValue.length > 0; } } break; default: // The function should not be called if oldValue is an object, oldValue and newValue are empty // or if newValue is empty but name and oldValue are set // This should prevent 'Uncaught SyntaxError: JSON.parse unexpected end of data at line 1 column 1 of the JSON data' if ( (typeof oldValue === 'object' && !oldValue && !newValue) || (!newValue && oldValue && name) ) { // Logger.debug("attributeChangedCallback ignored", name, oldValue, newValue); break; } super.attributeChangedCallback(name, oldValue, newValue); } // Logger.debug("this.lang", this.tagName, name, this.lang); // Logger.debug("this.entryPointUrl", this.tagName, name, this.entryPointUrl); // console.trace(); } /** * Send a dbp-set-property event to the provider components * * @param name * @param value * @param sendToSelf Set this to "true" if the event should be sent to oneself instead of the parent (e.g. in the app shell if there isn't a provider around it) * @returns {boolean} */ sendSetPropertyEvent(name, value, sendToSelf = false) { // Logger.debug("dbp-set-property", name, value); const event = new CustomEvent('dbp-set-property', { bubbles: true, composed: true, detail: {name: name, value: value}, }); // dispatch the dbp-set-property event to the parent (if there is any) so that the current element // doesn't terminate the event if it has the attribute set itself const element = this.parentElement && !sendToSelf ? this.parentElement : this; return element.dispatchEvent(event); } // update(changedProperties) { // changedProperties.forEach((oldValue, propName) => { // switch(propName) { // case 'subscribe': // if (this.subscribe && this.subscribe.length > 0) { // if (this.connected) { // const attrs = this.subscribe.split(','); // attrs.forEach(element => this.unSubscribeProviderFor(element)); // } else { // this.deferUnSubscribe = this.subscribe.length > 0; // this.unsubscribe = this.subscribe; // } // } // if (this.subscribe !== null) { // if (this.connected) { // const attrs = this.subscribe.split(','); // attrs.forEach(element => this.subscribeProviderFor(element)); // } else { // this.deferSubscribe = this.subscribe && this.subscribe.length > 0; // } // } // break; // } // }); // // super.update(changedProperties); // } }