UNPKG

@pictogrammers/element

Version:

Web Component Element for TypeScript

595 lines 23.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.index = void 0; exports.getProxyValue = getProxyValue; exports.Component = Component; exports.TransmutePart = TransmutePart; exports.Prop = Prop; exports.Part = Part; exports.Local = Local; exports.node = node; exports.normalizeInt = normalizeInt; exports.normalizeFloat = normalizeFloat; exports.normalizeBoolean = normalizeBoolean; exports.normalizeString = normalizeString; exports.forEach = forEach; exports.selectComponent = selectComponent; exports.selectPart = selectPart; exports.getProps = getProps; require("./global"); const proxy_1 = require("./proxy"); class PropError extends Error { constructor(message, ignore) { super(message); this.name = 'PropError'; // @ts-ignore if (Error.captureStackTrace) { // @ts-ignore Error.captureStackTrace(this, ignore); } } } exports.index = Symbol('index'); const init = Symbol('init'); const template = Symbol('template'); const style = Symbol('style'); const parent = Symbol('parent'); function getProxyValue(obj) { return obj[proxy_1.isProxy] && obj[proxy_1.getTarget]; } function extendTemplate(base, append) { if (append && append.match(/<parent\/>/)) { return append.replace(/<parent\/>/, base); } else { return `${base}${append || ''}`; } } function camelToDash(str) { return str.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase(); } function dashToCamel(str) { return str.replace(/-([a-z])/g, m => m[1].toUpperCase()); } function Component(config = {}) { return function (cls, context) { if (context.kind !== 'class') { throw new Error('@Component() can only decorate a class'); } // webpack removes class names, override name Reflect.defineProperty(cls, 'name', { value: config.selector, writable: false, configurable: false, }); if (cls.prototype[parent] && !(cls.prototype[parent][cls.prototype[parent].length - 1] instanceof Object.getPrototypeOf(cls))) { cls.prototype[parent].push(cls.prototype); cls.prototype[style].push(config.style); cls.prototype[template] = extendTemplate(cls.prototype[template], config.template || null); } else if (cls.prototype instanceof HTMLElement) { cls.prototype[parent] = [cls.prototype]; cls.prototype[style] = config.style ? [config.style] : []; cls.prototype[template] = config.template || ''; } if (!cls.symbols) { cls.symbols = {}; } const connectedCallback = cls.prototype.connectedCallback || (() => { }); const disconnectedCallback = cls.prototype.disconnectedCallback || (() => { }); cls.prototype.connectedCallback = function () { if (!this[init] && config.template === undefined && config.style === undefined) { if (config.useShadow === false) { // Base class with no template } else { this.attachShadow({ mode: 'open' }); } } else if (!this[init]) { if (config.useShadow === false) { throw new Error('unsupported'); } else { const $template = document.createElement('template'); $template.innerHTML = cls.prototype[template] || ''; const $node = document.importNode($template.content, true); const shadowRoot = this.attachShadow({ mode: 'open' }); shadowRoot.adoptedStyleSheets = cls.prototype[style]; shadowRoot.appendChild($node); } } else if (this[init] && config.style) { /*if (this.shadowRoot) { const style = document.createElement('style'); style.appendChild(document.createTextNode(config.style)); this.appendChild(style); }*/ // } else if (this[init] && config.template) { // This is allowed now via <parent/> // throw new Error('template from base class cannot be overriden. Fix: remove template from @Component'); } else if (this[init] && config.selector && !config.template) { throw new Error('You need to pass a template for an extended element.'); } const tags = new Set(); for (const ele of this.shadowRoot.querySelectorAll('*')) { if (ele.localName.indexOf('-') !== -1) { tags.add(ele.localName); } } const promises = Array.from(tags.values()).map(tag => { return customElements.get(tag) ? Promise.resolve() : customElements.whenDefined(tag); }); if (promises.length === 0) { this[init] = true; connectedCallback.call(this); } else { Promise.all(promises).then(() => { this[init] = true; connectedCallback.call(this); // dispatch slotchange for (const slot of this.shadowRoot.querySelectorAll('slot')) { slot.dispatchEvent(new CustomEvent('slotchange')); } }); } this[parent].map((p) => { if (p.render) { p.render.call(this, cls.observedAttributes ? cls.observedAttributes.reduce((a, c) => { const n = dashToCamel(c); a[n] = true; return a; }, {}) : {}); } }); }; cls.prototype.disconnectedCallback = function () { disconnectedCallback.call(this); }; cls.prototype.attributeChangedCallback = function (name, oldValue, newValue) { const normalizedName = dashToCamel(name); this[normalizedName] = newValue; }; // Base components may not define a selector context.addInitializer(function () { if (config.selector) { if (window.customElements.get(config.selector)) { throw new Error(`@Component() ${context.name} duplicate selector '${config.selector}'`); } window.customElements.define(config.selector, cls); } }); }; } const transmute = Symbol('transmute'); function TransmutePart(part, selector) { return function (cls) { if (cls.prototype[transmute]) { cls.prototype[transmute][part] = selector; } else { cls.prototype[transmute] = { [part]: selector }; } }; } function isArray(a) { return (!!a) && (a.constructor === Array); } function isObject(a) { return (!!a) && (a.constructor === Object); } function render(self, propertyKey) { if (self[init]) { self[parent].map((p) => { if (p.render) { p.render.call(self, { [propertyKey]: true }); } }); } } function getSymbolType(value) { if (value === null) { return 'null'; } return isArray(value) ? 'array' : typeof value; } function Prop(normalize) { return function (_, context) { const propertyKey = context.name; const symbol = Symbol(propertyKey); const symbolType = Symbol(`${propertyKey}:type`); const symbolMeta = Symbol(`${propertyKey}:meta`); context.addInitializer(function () { Reflect.defineProperty(this, propertyKey, { get: () => { if (this[symbolType] === 'object') { if (this[symbol][proxy_1.isProxy]) { return this[symbol]; } else { return (0, proxy_1.createProxy)(this[symbol]); } } if (this[symbolType] === 'array') { if (this[symbol][proxy_1.isProxy]) { return this[symbol]; } else { return (0, proxy_1.createProxy)(this[symbol]); } } return this[symbol]; }, set: (value) => { var _a; // ToDo: cleanup const newSymbolType = getSymbolType(normalize ? normalize(value) : value); if (propertyKey !== 'index' && this[symbolType] !== newSymbolType && this[symbolType] !== 'null' && newSymbolType !== 'null') { throw new Error(`@Prop() ${propertyKey} with type '${this[symbolType]}' cannot be set to ${newSymbolType}.`); } if (this[symbolType] === 'array') { if (!isArray(value)) { throw new PropError(`Array "${propertyKey}" (Prop) initialized already. Reassignments must be array type.`, (_a = Object.getOwnPropertyDescriptor(this, propertyKey)) === null || _a === void 0 ? void 0 : _a.set); } if (this[symbol] === value) { throw new Error('Setting an array to itself is not allowed.'); } const proxified = (0, proxy_1.createProxy)(this[symbol]); if (proxified[proxy_1.hasObserver]) { proxified.splice(0, this[symbol].length, ...value); } else { this[symbol] = value; } } else { this[symbol] = normalize ? normalize(value) : value; render(this, propertyKey); } } }); }); return function (initialValue) { var _a, _b, _c; if (initialValue === undefined && propertyKey !== 'index') { throw new Error(`@Prop() ${propertyKey} must have an initial value defined.`); } else if (initialValue !== undefined && propertyKey === 'index') { throw new Error(`@Prop() index must not have an initial value defined.`); } if (initialValue === true) { throw new Error(`@Prop() ${propertyKey} boolean must initialize to false.`); } // Web Component, todo: refactor to only be called once if (!context.private) { const { constructor } = this; (_a = constructor.observedAttributes) !== null && _a !== void 0 ? _a : (constructor.observedAttributes = []); if (!constructor.symbols) { constructor.symbols = {}; } const { symbols } = constructor; const normalizedPropertyKey = camelToDash(propertyKey); if (!symbols[propertyKey]) { constructor.observedAttributes.push(normalizedPropertyKey); symbols[propertyKey] = symbol; } } // Rest this[symbolType] = getSymbolType(initialValue); if (this[symbolType] === 'array') { this[symbol] = initialValue; return new Proxy(initialValue, { get: (target, key) => { if (key === meta) { return this[symbolMeta]; } console.log('errr???'); return Reflect.get(this[symbol], key); }, set: (target, key, v) => { if (key === meta) { this[symbolMeta] = v; return true; } const x = Reflect.set(target, key, v); if (!(key === 'length' && this[symbol].length === v)) { render(this, propertyKey); } this[symbol] = v; return x; } }); } // todo watch objects??? this[symbol] = normalize ? normalize((_b = this.getAttribute(propertyKey)) !== null && _b !== void 0 ? _b : initialValue) : (_c = this.getAttribute(propertyKey)) !== null && _c !== void 0 ? _c : initialValue; return this[symbol]; }; }; } function Part() { return function (_, context) { const propertyKey = context.name; const key = propertyKey.replace(/^\$/, ''); context.addInitializer(function () { let cache = null; Reflect.defineProperty(this, propertyKey, { get() { var _a; return cache !== null && cache !== void 0 ? cache : (cache = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`[part~=${key}]`)); } }); }); }; } /** * Store data via a Map into LocalStorage * @param key String * @returns Value */ function Local(key) { return function (_, context) { const propertyKey = context.name; return function (initialValue) { if (!(initialValue instanceof Map)) { throw new Error('@Local(key) property must be type Map'); } return new Proxy(initialValue, { get(target, prop) { switch (prop) { case 'get': return (k) => { var _a; if (!initialValue.has(k)) { throw new Error(`@Local(key) missing key ${k}`); } const storeKey = `${key}:${k}`; if (window.localStorage.getItem(storeKey) === null) { return initialValue.get(k); } else { return JSON.parse((_a = window.localStorage.getItem(storeKey)) !== null && _a !== void 0 ? _a : 'null'); } }; case 'set': return (k, v) => { if (!initialValue.has(k)) { throw new Error(`@Local(key) missing key ${k}`); } const storeKey = `${key}:${k}`; if (v === null || JSON.stringify(v) === JSON.stringify(initialValue.get(k))) { // todo? Reset to initial value window.localStorage.removeItem(storeKey); } else { window.localStorage.setItem(storeKey, JSON.stringify(v)); } }; default: throw new Error(`@Local(key) supported method ${prop}`); } } }); }; }; } function node(template, init) { const $template = document.createElement('template'); $template.innerHTML = template; const $node = document.importNode($template.content, true); for (const [part, attributes] of Object.entries(init)) { const $part = $node.querySelector(`[part~="${part}"]`); if ($part) { for (const [prop, value] of Object.entries(attributes)) { if (value instanceof Function) { const val = value(); if (val === null) { $part.removeAttribute(prop); } else { $part.setAttribute(prop, value()); } } else { $part[prop] = value; } } } } return $node; } function normalizeInt(value) { return parseInt(`${value}`, 10); } function normalizeFloat(value) { return parseFloat(`${value}`); } function normalizeBoolean(value) { return value === '' || value === true ? true : value === null || value === false ? false : value || true; } function normalizeString(value) { return `${value}`; } const trackProxy = Symbol('hasProxy'); function hasProxy(obj) { if (obj === null || typeof obj !== "object") return false; return obj[trackProxy]; } const meta = Symbol('meta'); function intersect(arr1, arr2) { const set1 = new Set(arr1); return arr2.filter(item => set1.has(item)); } function difference(arr1, arr2) { return arr1.filter(str => !arr2.includes(str)); } function forEach({ container, items, type, create, connect, disconnect, update }) { function newItem(item, itemIndex) { const comp = type(item); const $new = document.createElement(camelToDash(comp.name), comp); const { observedAttributes } = comp; const props = intersect(Object.keys(item), observedAttributes); if (observedAttributes.includes('index')) { //@ts-ignore $new['index'] = itemIndex; } let idx = props.indexOf('index'); if (idx !== -1) { props.splice(props.indexOf('index'), 1); } props.forEach((attr) => { //@ts-ignore $new[attr] = item[attr]; }); create && create($new, item); items[itemIndex][proxy_1.addObserver]($new, (prop, value) => { // @ts-ignore $new[prop] = value; }); return $new; } // Add initial items items.forEach((item, i) => { const $new = newItem(item, i); container.appendChild($new); connect && connect($new, item); }); // Handle each mutation items[proxy_1.addObserver](container, (target, prop, args) => { switch (prop) { case proxy_1.Mutation.fill: // this could be optimized more, but would need the previous items keys const [value, start, end] = args; for (let i = start || 0; i < (end || items.length); i++) { Object.keys(value).forEach((key) => { // @ts-ignore container.children[i][key] = value[key]; }); } break; case proxy_1.Mutation.pop: const count = container.children.length; if (count > 0) { container.children[count - 1].remove(); } break; case proxy_1.Mutation.push: const last = container.children.length; [...args].forEach((item, i) => { const $new = newItem(item, last + i); container.appendChild($new); connect && connect($new, item); }); break; case proxy_1.Mutation.reverse: for (var i = 1; i < container.children.length; i++) { container.insertBefore(container.children[i], container.children[0]); } break; case proxy_1.Mutation.shift: if (container.children.length) { container.children[0].remove(); } break; case proxy_1.Mutation.sort: throw new Error('ToDo... write sort.'); break; case proxy_1.Mutation.splice: const [startIndex, deleteCount, ...newItems] = args; if (deleteCount > 0) { for (let i = startIndex; i < deleteCount + startIndex; i++) { container.children[i].remove(); } } let newCount = newItems.length || 0; if (newCount > 0) { const nItems = newItems.map((item, i) => { return newItem(item, startIndex + i); }); if (startIndex === 0) { container.append(...nItems); } else { container.children[startIndex].after(...nItems); } nItems.forEach(($new) => { connect && connect($new, newItems[i]); }); } const shift = deleteCount - newCount; if (shift > 0 && startIndex + shift - 1 > 0) { // update index values after for (let i = startIndex + shift - 1; i < container.children.length; i++) { // @ts-ignore container.children[i].index = i; } } break; case proxy_1.Mutation.unshift: const first = container.children.length && container.children[0]; [...args].forEach((item, i) => { if (first) { first.before(newItem(item, i)); } else { container.appendChild(newItem(item, i)); } }); break; } }); } // JEST function selectComponent(tagName) { const component = document.querySelector(tagName); const tags = new Set(); let shadowRoot; try { shadowRoot = component.shadowRoot; } catch (error) { throw new Error('Add the component via document.body.appendChild(...) before selectComponent.'); } for (const ele of shadowRoot.querySelectorAll('*')) { if (ele.localName.indexOf('-') !== -1) { tags.add(ele.localName); } } for (var it = tags.values(), tag = null; tag = it.next().value;) { if (!customElements.get(tag)) { const namespaceMatch = tag.match(/^([^-]+)/); if (namespaceMatch === null || namespaceMatch.length > 1) { throw new Error('Failed to parse namespace.'); } const namespace = namespaceMatch[1]; const componentMatch = tag.match(/^[^-]+-(.+)/); if (componentMatch === null || componentMatch.length > 1) { throw new Error('Failed to parse component name.'); } const componentName = dashToCamel(componentMatch[1]); throw new Error(`Missing \`import '../${componentName}/${componentName}';\` in spec.ts file.`); } } return component; } function selectPart(component, name) { return component.shadowRoot.querySelector(`[part=${name}]`); } function getProps(tag) { const { symbols } = customElements.get(tag); return Object.keys(symbols); } //# sourceMappingURL=element.js.map