import Component from '@glimmer/component'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import { assert } from '@ember/debug'; const ATTRIBUTE_CHANGE = 'custom-element.attributeChange'; const elements = new Map(); const proxies = new WeakMap(); const typeCast = (attributeInfo, value) => { let type = attributeInfo.type; const d = attributeInfo.default; value = value == null ? attributeInfo.default : value; if (type.indexOf('|') !== -1) { assert( `"${value} is not of type '${type}'"`, type .split('|') .map((item) => item.replaceAll('"', '').trim()) .includes(value) ); type = 'string'; } switch (type) { case '': case '': case '': case 'number': { const num = parseFloat(value); if (isNaN(num)) { return typeof d === 'undefined' ? 0 : d; } else { return num; } } case '': return parseInt(value); case '': case 'string': return (value || '').toString(); } }; const attributeChangingElement = (name, Cls = HTMLElement, attributes = {}, cssprops = {}) => { const attrs = Object.keys(attributes); const customClass = class extends Cls { static get observedAttributes() { return attrs; } attributeChangedCallback(name, oldValue, newValue) { const prev = typeCast(attributes[name], oldValue); const value = typeCast(attributes[name], newValue); const cssProp = cssprops[`--${name}`]; if (typeof cssProp !== 'undefined' && cssProp.track === `[${name}]`) { this.style.setProperty(`--${name}`, value); } if (typeof super.attributeChangedCallback === 'function') { super.attributeChangedCallback(name, prev, value); } this.dispatchEvent( new CustomEvent(ATTRIBUTE_CHANGE, { detail: { name: name, previousValue: prev, value: value, }, }) ); } }; customElements.define(name, customClass); return () => {}; }; const infoFromArray = (arr, keys) => { return (arr || []).reduce((prev, info) => { let key; const obj = {}; keys.forEach((item, i) => { if (item === '_') { key = i; return; } obj[item] = info[i]; }); prev[info[key]] = obj; return prev; }, {}); }; const debounceRAF = (cb, prev) => { if (typeof prev !== 'undefined') { cancelAnimationFrame(prev); } return requestAnimationFrame(cb); }; const createElementProxy = ($element, component) => { return new Proxy($element, { get: (target, prop, receiver) => { switch (prop) { case 'attrs': return component.attributes; default: if (typeof target[prop] === 'function') { // need to ensure we use a MultiWeakMap here // if(this.methods.has(prop)) { // return this.methods.get(prop); // } const method = target[prop].bind(target); // this.methods.set(prop, method); return method; } } }, }); }; export default class CustomElementComponent extends Component { @tracked $element; @tracked _attributes = {}; __attributes; _attchange; constructor(owner, args) { super(...arguments); if (!elements.has(args.element)) { const cb = attributeChangingElement( args.element, args.class, infoFromArray(args.attrs, ['_', 'type', 'default', 'description']), infoFromArray(args.cssprops, ['_', 'type', 'track', 'description']) ); elements.set(args.element, cb); } } get attributes() { return this._attributes; } get element() { if (this.$element) { if (proxies.has(this.$element)) { return proxies.get(this.$element); } const proxy = createElementProxy(this.$element, this); proxies.set(this.$element, proxy); return proxy; } return undefined; } @action setHost(attachShadow, $element) { attachShadow($element); this.$element = $element; this.$element.addEventListener(ATTRIBUTE_CHANGE, this.attributeChange); (this.args.attrs || []).forEach((entry) => { const value = $element.getAttribute(entry[0]); $element.attributeChangedCallback(entry[0], value, value); }); } @action disconnect() { this.$element.removeEventListener(ATTRIBUTE_CHANGE, this.attributeChange); } @action attributeChange(e) { e.stopImmediatePropagation(); // currently if one single attribute changes // they all change this.__attributes = { ...this.__attributes, [e.detail.name]: e.detail.value, }; this._attchange = debounceRAF(() => { // tell glimmer we changed the attrs this._attributes = this.__attributes; }, this._attchange); } }