From ed92bea5e24a3f52ce5582723cf0c16d303d221b Mon Sep 17 00:00:00 2001 From: Christoph Reiter <reiter.christoph@gmail.com> Date: Tue, 23 Mar 2021 14:11:51 +0100 Subject: [PATCH] Move AdapterLitElement from provider to common to break a dep cycle --- packages/auth/src/login-button.js | 2 +- packages/common/dbp-lit-element.js | 2 +- packages/common/index.js | 3 +- packages/common/src/adapter-lit-element.js | 355 ++++++++++++++++++ packages/provider/src/adapter-lit-element.js | 356 +------------------ 5 files changed, 360 insertions(+), 358 deletions(-) create mode 100644 packages/common/src/adapter-lit-element.js diff --git a/packages/auth/src/login-button.js b/packages/auth/src/login-button.js index 27dc27cd..be721d2c 100644 --- a/packages/auth/src/login-button.js +++ b/packages/auth/src/login-button.js @@ -4,7 +4,7 @@ import {unsafeHTML} from 'lit-html/directives/unsafe-html.js'; import {ScopedElementsMixin} from '@open-wc/scoped-elements'; import * as commonStyles from '@dbp-toolkit/common/styles'; import {LoginStatus} from './util.js'; -import {AdapterLitElement} from "../../provider/src/adapter-lit-element"; +import {AdapterLitElement} from '@dbp-toolkit/common'; let logoutSVG = ` <svg diff --git a/packages/common/dbp-lit-element.js b/packages/common/dbp-lit-element.js index 0a1f687c..9e9b3192 100644 --- a/packages/common/dbp-lit-element.js +++ b/packages/common/dbp-lit-element.js @@ -1,4 +1,4 @@ -import {AdapterLitElement} from "@dbp-toolkit/provider/src/adapter-lit-element"; +import {AdapterLitElement} from "./src/adapter-lit-element"; export default class DBPLitElement extends AdapterLitElement { _(selector) { diff --git a/packages/common/index.js b/packages/common/index.js index 68b571b4..424cbc9f 100644 --- a/packages/common/index.js +++ b/packages/common/index.js @@ -12,4 +12,5 @@ export {MiniSpinner}; export {Button, LoadingButton}; export {Spinner}; export {InlineNotification}; -export * from './src/logger.js'; \ No newline at end of file +export * from './src/logger.js'; +export {AdapterLitElement} from './src/adapter-lit-element.js'; \ No newline at end of file diff --git a/packages/common/src/adapter-lit-element.js b/packages/common/src/adapter-lit-element.js new file mode 100644 index 00000000..c0d001e3 --- /dev/null +++ b/packages/common/src/adapter-lit-element.js @@ -0,0 +1,355 @@ +import {LitElement} from "lit-element"; +import {Logger} from "@dbp-toolkit/common"; + +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.root) { + 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.root) { + 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.root) { + 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) => { + 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 { + subscribe: { type: String }, + unsubscribe: { type: String }, + }; + } + + 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 + * @returns {boolean} + */ + sendSetPropertyEvent(name, value) { + // 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 ? 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); + // } +} diff --git a/packages/provider/src/adapter-lit-element.js b/packages/provider/src/adapter-lit-element.js index c0d001e3..573b152b 100644 --- a/packages/provider/src/adapter-lit-element.js +++ b/packages/provider/src/adapter-lit-element.js @@ -1,355 +1 @@ -import {LitElement} from "lit-element"; -import {Logger} from "@dbp-toolkit/common"; - -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.root) { - 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.root) { - 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.root) { - 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) => { - 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 { - subscribe: { type: String }, - unsubscribe: { type: String }, - }; - } - - 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 - * @returns {boolean} - */ - sendSetPropertyEvent(name, value) { - // 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 ? 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); - // } -} +export {AdapterLitElement} from "@dbp-toolkit/common"; -- GitLab