UNPKG

heresy

Version:

lighterhtml based custom elements builtins

347 lines (303 loc) 7.55 kB
import Event from '@ungap/event'; import WeakMap from '@ungap/weakmap'; import WeakSet from '@ungap/weakset'; import {Hole, custom} from 'lighterhtml'; import { augmentor, useCallback, useContext, useEffect, dropEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'augmentor'; import {registry, replace} from './utils.js'; const hooks = { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState }; const { render: lighterRender, html: lighterHTML, svg: lighterSVG } = custom({ transform: () => markup => replace(markup, registry) }); const secret = '_\uD83D\uDD25'; const {defineProperties} = Object; const $html = new WeakMap; const $svg = new WeakMap; const $mappedAttributes = new WeakMap; const ws = new WeakSet; const configurable = true; const attributeChangedCallback = 'attributeChangedCallback'; const connectedCallback = 'connectedCallback'; const disconnectedCallback = `dis${connectedCallback}`; const addInit = (prototype, properties, method) => { if (method in prototype) { const original = prototype[method]; properties[method] = { configurable, value() { init.call(this); return original.apply(this, arguments); } }; } else properties[method] = { configurable, value: init }; }; const augmented = Class => { const {prototype} = Class; const events = []; const properties = { html: { configurable, get: getHTML }, svg: { configurable, get: getSVG } }; properties[secret] = { value: { events, info: null } }; if (!('handleEvent' in prototype)) properties.handleEvent = { configurable, value: handleEvent }; if ('render' in prototype && prototype.render.length) { const {oninit} = prototype; defineProperties(prototype, { oninit: { configurable, value() { const hook = augmentor(this.render.bind(this, hooks)); defineProperties(this, { render: { configurable, value: hook } }); this.addEventListener( 'disconnected', dropEffect.bind(null, hook), false ); if (oninit) oninit.apply(this, arguments); } } }); } // setup the init dispatch only if needed // ensure render with an init is triggered after if ('oninit' in prototype) { events.push('init'); addInit(prototype, properties, 'render'); } // ensure all other callbacks are dispatched too addInit(prototype, properties, attributeChangedCallback); addInit(prototype, properties, connectedCallback); addInit(prototype, properties, disconnectedCallback); [ [ attributeChangedCallback, 'onattributechanged', onattributechanged ], [ connectedCallback, 'onconnected', onconnected ], [ disconnectedCallback, 'ondisconnected', ondisconnected ], [ connectedCallback, 'render', onconnectedrender ] ].forEach(([ce, he, value]) => { if (!(ce in prototype) && he in prototype) { if (he !== 'render') events.push(he.slice(2)); if (ce in properties) { const original = properties[ce].value; properties[ce] = { configurable, value() { original.apply(this, arguments); return value.apply(this, arguments); } }; } else properties[ce] = {configurable, value}; } }); const booleanAttributes = Class.booleanAttributes || []; booleanAttributes.forEach(name => { if (!(name in prototype)) properties[name] = { configurable, get() { return this.hasAttribute(name); }, set(value) { if (!value || value === 'false') this.removeAttribute(name); else this.setAttribute(name, value); } }; }); const observedAttributes = Class.observedAttributes || []; observedAttributes.forEach(name => { if (!(name in prototype)) properties[name] = { configurable, get() { return this.getAttribute(name); }, set(value) { if (value == null) this.removeAttribute(name); else this.setAttribute(name, value); } }; }); const mappedAttributes = Class.mappedAttributes || []; mappedAttributes.forEach(name => { const _ = new WeakMap; const listening = ('on' + name) in prototype; if (listening) events.push(name); properties[name] = { configurable, get() { return _.get(this); }, set(detail) { _.set(this, detail); if (listening) { const e = evt(name); e.detail = detail; if (ws.has(this)) this.dispatchEvent(e); else { const dispatch = $mappedAttributes.get(this); if (dispatch) dispatch.push(e); else $mappedAttributes.set(this, [e]); } } } }; }); defineProperties(prototype, properties); const attributes = booleanAttributes.concat(observedAttributes); return attributes.length ? defineProperties(Class, { observedAttributes: { configurable, get: () => attributes } }) : Class; }; const defineHook = (name, hook) => { if (name in hooks) throw new Error('duplicated hook ' + name); hooks[name] = hook(hooks); }; const evt = type => new Event(type); const html = (...args) => new Hole('html', args); html.for = lighterHTML.for; const svg = (...args) => new Hole('svg', args); svg.for = lighterSVG.for; const setParsed = (wm, template, {info}) => { const value = ( info ? replace(template.join(secret), info).split(secret) : template ); wm.set(template, value); return value; }; const setWrap = (self, type, wm) => { const fn = wrap(self, type, new WeakMap); wm.set(self, fn); return fn; }; const wrap = (self, type, wm) => (template, ...values) => { const local = wm.get(template) || setParsed(wm, template, self[secret]); return lighterRender(self, () => type(local, ...values)); }; export { Hole, secret, augmented, defineHook, lighterRender, html, svg }; function addListener(type) { this.addEventListener(type, this); } function dispatchEvent(event) { this.dispatchEvent(event); } function getHTML() { return $html.get(this) || setWrap(this, html, $html); } function getSVG() { return $svg.get(this) || setWrap(this, svg, $svg); } function handleEvent(event) { this[`on${event.type}`](event); } function init() { if (!ws.has(this)) { ws.add(this); this[secret].events.forEach(addListener, this); this.dispatchEvent(evt('init')); const events = $mappedAttributes.get(this); if (events) { $mappedAttributes.delete(this); events.forEach(dispatchEvent, this); } } } function onattributechanged(attributeName, oldValue, newValue) { const event = evt('attributechanged'); event.attributeName = attributeName; event.oldValue = oldValue; event.newValue = newValue; this.dispatchEvent(event); } function onconnected() { this.dispatchEvent(evt('connected')); } function onconnectedrender() { this.render(); } function ondisconnected() { this.dispatchEvent(evt('disconnected')); }