diff --git a/packages/provider/rollup.config.js b/packages/provider/rollup.config.js index 37765ed907261741713b723342c2f351f73c7ff3..d847e3a450584c42d9de9f9e353a76615843e338 100644 --- a/packages/provider/rollup.config.js +++ b/packages/provider/rollup.config.js @@ -40,7 +40,7 @@ function getBuildInfo() { export default (async () => { return { - input: (build != 'test') ? ['src/dbp-provider.js', 'src/dbp-provider-demo.js'] : glob.sync('test/**/*.js'), + input: (build != 'test') ? ['src/dbp-provider.js', 'src/dbp-adapter.js','src/dbp-provider-demo.js'] : glob.sync('test/**/*.js'), output: { dir: 'dist', entryFileNames: '[name].js', diff --git a/packages/provider/src/adapter.js b/packages/provider/src/adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..d264ef6705c5db8934e1975921fd053fadee33e0 --- /dev/null +++ b/packages/provider/src/adapter.js @@ -0,0 +1,264 @@ +export class Adapter extends HTMLElement { + 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; + + 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 = {}; + + console.log('Adapter constructor()'); + } + + getProperty(name) { + return this.propertyStore[name]; + } + + getPropertyByAttributeName(name) { + return this[this.findPropertyName(name)]; + } + + + setProperty(name, value) { + if (typeof value === 'object' && value !== null) { + // console.log("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() { + + 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; + + // 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)) { + console.log('AdapterProvider (' + 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); + console.log('AdapterProvider (' + 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) { + console.log('AdapterProvider(' + 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('subscribe', + { + bubbles: true, + composed: true, + detail: { + name: global, + callback: (value) => { + console.log('AdapterProvider(' + that.tagName + ') sub/Callback ' + global + ' -> ' + local + ' = ' + value); + that.setPropertiesToChildNodes(local, value); + + // If value is an object set it directly as property + if (typeof value === 'object' && value !== null) { + // console.log("value is object", value); + that.setPropertyByAttributeName(local, value); + } else { + // console.log("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) { + console.log('AdapterProvider(' + this.tagName + ') unSubscribeProviderFor( ' + element + ' )'); + const pair = element.trim().split(':'); + const global = pair[1] || pair[0]; + const event = new CustomEvent('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; + // console.log("properties", properties); + + for (const propertyName in properties) { + // console.log("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': + console.log('AdapterProvider() 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: + break; + //super.attributeChangedCallback(name, oldValue, newValue); + } + + // console.log("this.lang", this.tagName, name, this.lang); + // console.log("this.entryPointUrl", this.tagName, name, this.entryPointUrl); + // console.trace(); + } + + /** + * Send a set-property event to the provider components + * + * @param name + * @param value + * @returns {boolean} + */ + sendSetPropertyEvent(name, value) { + const event = new CustomEvent("set-property", { + bubbles: true, + composed: true, + detail: {'name': name, 'value': value} + }); + + return this.dispatchEvent(event); + } + + setPropertiesToChildNodes(local, value) + { + let children = this.children; + Array.from(children).forEach(child => child.setAttribute(local, value)); + } + +} diff --git a/packages/provider/src/dbp-adapter.js b/packages/provider/src/dbp-adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..96f80b717f4a47bba0ab9e71c491bb5159da3a4e --- /dev/null +++ b/packages/provider/src/dbp-adapter.js @@ -0,0 +1,4 @@ +import * as commonUtils from '@dbp-toolkit/common/utils'; +import {Adapter} from './adapter.js'; + +commonUtils.defineCustomElement('dbp-provider-adapter', Adapter); diff --git a/packages/provider/src/dbp-provider-demo.js b/packages/provider/src/dbp-provider-demo.js index 60c7f0a7e5ec7bdfad273e1b51e34ab31c6e7626..035a992991f75b56e09526eb897743297b406c8e 100644 --- a/packages/provider/src/dbp-provider-demo.js +++ b/packages/provider/src/dbp-provider-demo.js @@ -5,6 +5,7 @@ import {AuthKeycloak, LoginButton} from '@dbp-toolkit/auth'; import * as commonUtils from '@dbp-toolkit/common/utils'; import * as commonStyles from '@dbp-toolkit/common/styles'; import {Provider} from '@dbp-toolkit/provider'; +import {Adapter} from '@dbp-toolkit/provider'; import {LanguageSelect} from '@dbp-toolkit/language-select'; import DBPLitElement from "@dbp-toolkit/common/dbp-lit-element"; @@ -22,6 +23,7 @@ class ProviderDemo extends ScopedElementsMixin(DBPLitElement) { 'dbp-login-button': LoginButton, 'dbp-language-select': LanguageSelect, 'dbp-provider': Provider, + 'dbp-provider-adapter': Adapter, 'dbp-consumer': DemoConsumer, }; } @@ -110,6 +112,23 @@ class ProviderDemo extends ScopedElementsMixin(DBPLitElement) { </div> </dbp-provider> </dbp-provider> + + <h2> DBP Provider </h2> + <p> The dbp-provider is for third party webcomponents, which we want to configure with a provider.</p> + <pre><dbp-provider id="demoadapter" dbp-style-red="color:red;" dbp-style-green="color:green;" ></dbp-provider></pre> + <dbp-provider id="demoadapter" + dbp-style-red="color:red;" dbp-style-green="color:green;"> + <pre><dbp-provider-adapter id="a1" subscribe="style:dbp-style-red" ></dbp-provider-adapter></pre> + <dbp-provider-adapter id="a1" subscribe="style:dbp-style-red"> + <p> I'm a normal p tag without attributes and without style. </p> + <p> I'm a normal p tag without attributes and without style. </p> + <p> I'm a normal p tag without attributes and without style. </p> + </dbp-provider-adapter> + <pre><dbp-provider-adapter id="a2" subscribe="style:dbp-style-green" ></dbp-provider-adapter></pre> + <dbp-provider-adapter id="a2" subscribe="style:dbp-style-green"> + <p style="background-color:green;"> I'm a normal p tag without attributes and without style. <span style="color:blue;"> I'm blue dabedidabedei...</span> </p> + </dbp-provider-adapter> + </dbp-provider> </section> `; } diff --git a/packages/provider/src/index.js b/packages/provider/src/index.js index 34bdc9a60df3726f8fecfb1a35283fbfc2e42c6c..6f6fd2b3599dd14e85c844a5c7a4de8ac5fd3b9c 100644 --- a/packages/provider/src/index.js +++ b/packages/provider/src/index.js @@ -1,3 +1,5 @@ import {Provider} from './provider.js'; +import {Adapter} from './adapter.js'; -export {Provider}; \ No newline at end of file +export {Provider}; +export {Adapter}; \ No newline at end of file