blob: c288fef60930ceddf94c2e5f86639862c5a92c65 [file] [log] [blame]
class ComponentBase {
constructor(name)
{
this._componentName = name || ComponentBase._componentByClass.get(new.target);
const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
let element = currentlyConstructed.get(new.target);
if (!element) {
currentlyConstructed.set(new.target, this);
element = document.createElement(this._componentName);
currentlyConstructed.delete(new.target);
}
element.component = () => { return this };
this._element = element;
this._shadow = null;
this._actionCallbacks = new Map;
this._oldSizeToCheckForResize = null;
if (!ComponentBase.useNativeCustomElements)
element.addEventListener('DOMNodeInsertedIntoDocument', () => this.enqueueToRender());
if (!ComponentBase.useNativeCustomElements && new.target.enqueueToRenderOnResize)
ComponentBase._connectedComponentToRenderOnResize(this);
}
element() { return this._element; }
content(id = null)
{
this._ensureShadowTree();
if (this._shadow && id != null)
return this._shadow.getElementById(id);
return this._shadow;
}
part(id)
{
this._ensureShadowTree();
if (!this._shadow)
return null;
const part = this._shadow.getElementById(id);
if (!part)
return null;
return part.component();
}
dispatchAction(actionName, ...args)
{
const callback = this._actionCallbacks.get(actionName);
if (callback)
return callback.apply(this, args);
}
listenToAction(actionName, callback)
{
this._actionCallbacks.set(actionName, callback);
}
render() { this._ensureShadowTree(); }
enqueueToRender()
{
Instrumentation.startMeasuringTime('ComponentBase', 'updateRendering');
if (!ComponentBase._componentsToRender) {
ComponentBase._componentsToRender = new Set;
requestAnimationFrame(() => ComponentBase.renderingTimerDidFire());
}
ComponentBase._componentsToRender.add(this);
Instrumentation.endMeasuringTime('ComponentBase', 'updateRendering');
}
static renderingTimerDidFire()
{
Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire');
const componentsToRender = ComponentBase._componentsToRender;
this._renderLoop();
if (ComponentBase._componentsToRenderOnResize) {
const resizedComponents = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
if (resizedComponents.length) {
ComponentBase._componentsToRender = new Set(resizedComponents);
this._renderLoop();
}
}
Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire');
}
static _renderLoop()
{
const componentsToRender = ComponentBase._componentsToRender;
do {
const currentSet = [...componentsToRender];
componentsToRender.clear();
const resizeSet = ComponentBase._componentsToRenderOnResize;
for (let component of currentSet) {
Instrumentation.startMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
component.render();
if (resizeSet && resizeSet.has(component)) {
const element = component.element();
component._oldSizeToCheckForResize = {width: element.offsetWidth, height: element.offsetHeight};
}
Instrumentation.endMeasuringTime('ComponentBase', 'renderingTimerDidFire.render');
}
} while (componentsToRender.size);
ComponentBase._componentsToRender = null;
}
static _resizedComponents(componentSet)
{
if (!componentSet)
return [];
const resizedList = [];
for (let component of componentSet) {
const element = component.element();
const width = element.offsetWidth;
const height = element.offsetHeight;
const oldSize = component._oldSizeToCheckForResize;
if (oldSize && oldSize.width == width && oldSize.height == height)
continue;
resizedList.push(component);
}
return resizedList;
}
static _connectedComponentToRenderOnResize(component)
{
if (!ComponentBase._componentsToRenderOnResize) {
ComponentBase._componentsToRenderOnResize = new Set;
window.addEventListener('resize', () => {
const resized = this._resizedComponents(ComponentBase._componentsToRenderOnResize);
for (const component of resized)
component.enqueueToRender();
});
}
ComponentBase._componentsToRenderOnResize.add(component);
}
static _disconnectedComponentToRenderOnResize(component)
{
ComponentBase._componentsToRenderOnResize.delete(component);
}
renderReplace(element, content) { ComponentBase.renderReplace(element, content); }
static renderReplace(element, content)
{
element.innerHTML = '';
if (content)
ComponentBase._addContentToElement(element, content);
}
_ensureShadowTree()
{
if (this._shadow)
return;
const newTarget = this.__proto__.constructor;
const htmlTemplate = newTarget['htmlTemplate'];
const cssTemplate = newTarget['cssTemplate'];
if (!htmlTemplate && !cssTemplate)
return;
const shadow = this._element.attachShadow({mode: 'closed'});
if (htmlTemplate) {
const template = document.createElement('template');
template.innerHTML = newTarget.htmlTemplate();
shadow.appendChild(document.importNode(template.content, true));
this._recursivelyReplaceUnknownElementsByComponents(shadow);
}
if (cssTemplate) {
const style = document.createElement('style');
style.textContent = newTarget.cssTemplate();
shadow.appendChild(style);
}
this._shadow = shadow;
this.didConstructShadowTree();
}
didConstructShadowTree() { }
_recursivelyReplaceUnknownElementsByComponents(parent)
{
let nextSibling;
for (let child = parent.firstChild; child; child = child.nextSibling) {
if (child instanceof HTMLElement && !child.component) {
const elementInterface = ComponentBase._componentByName.get(child.localName);
if (elementInterface) {
const component = new elementInterface();
const newChild = component.element();
for (let i = 0; i < child.attributes.length; i++) {
const attr = child.attributes[i];
newChild.setAttribute(attr.name, attr.value);
}
parent.replaceChild(newChild, child);
child = newChild;
}
}
this._recursivelyReplaceUnknownElementsByComponents(child);
}
}
static defineElement(name, elementInterface)
{
ComponentBase._componentByName.set(name, elementInterface);
ComponentBase._componentByClass.set(elementInterface, name);
const enqueueToRenderOnResize = elementInterface.enqueueToRenderOnResize;
if (!ComponentBase.useNativeCustomElements)
return;
class elementClass extends HTMLElement {
constructor()
{
super();
const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
const component = currentlyConstructed.get(elementInterface);
if (component)
return; // ComponentBase's constructor will take care of the rest.
currentlyConstructed.set(elementInterface, this);
new elementInterface();
currentlyConstructed.delete(elementInterface);
}
connectedCallback()
{
this.component().enqueueToRender();
if (enqueueToRenderOnResize)
ComponentBase._connectedComponentToRenderOnResize(this.component());
}
disconnectedCallback()
{
if (enqueueToRenderOnResize)
ComponentBase._disconnectedComponentToRenderOnResize(this.component());
}
}
const nameDescriptor = Object.getOwnPropertyDescriptor(elementClass, 'name');
nameDescriptor.value = `${elementInterface.name}Element`;
Object.defineProperty(elementClass, 'name', nameDescriptor);
customElements.define(name, elementClass);
}
static createElement(name, attributes, content)
{
var element = document.createElement(name);
if (!content && (Array.isArray(attributes) || attributes instanceof Node
|| attributes instanceof ComponentBase || typeof(attributes) != 'object')) {
content = attributes;
attributes = {};
}
if (attributes) {
for (let name in attributes) {
if (name.startsWith('on'))
element.addEventListener(name.substring(2), attributes[name]);
else if (attributes[name] === true)
element.setAttribute(name, name);
else if (attributes[name] !== false)
element.setAttribute(name, attributes[name]);
}
}
if (content)
ComponentBase._addContentToElement(element, content);
return element;
}
static _addContentToElement(element, content)
{
if (Array.isArray(content)) {
for (var nestedChild of content)
this._addContentToElement(element, nestedChild);
} else if (content instanceof Node)
element.appendChild(content);
else if (content instanceof ComponentBase)
element.appendChild(content.element());
else
element.appendChild(document.createTextNode(content));
}
static createLink(content, titleOrCallback, callback, isExternal)
{
var title = titleOrCallback;
if (callback === undefined) {
title = content;
callback = titleOrCallback;
}
var attributes = {
href: '#',
title: title,
};
if (typeof(callback) === 'string')
attributes['href'] = callback;
else
attributes['onclick'] = ComponentBase.createEventHandler(callback);
if (isExternal)
attributes['target'] = '_blank';
return ComponentBase.createElement('a', attributes, content);
}
createEventHandler(callback) { return ComponentBase.createEventHandler(callback); }
static createEventHandler(callback)
{
return function (event) {
event.preventDefault();
event.stopPropagation();
callback.call(this, event);
};
}
}
ComponentBase.useNativeCustomElements = !!window.customElements;
ComponentBase._componentByName = new Map;
ComponentBase._componentByClass = new Map;
ComponentBase._currentlyConstructedByInterface = new Map;
ComponentBase._componentsToRender = null;
ComponentBase._componentsToRenderOnResize = null;