ignite-html/ignite-template.js

2566 lines
95 KiB
JavaScript

import { IgniteProperty } from './ignite-html.js';
import { IgniteElement } from './ignite-element.js';
/**
* The outline of a ignite template. Templates are a blueprint that specify's
* how to construct an element and then can be used to construct the element. Everything
* starts with a template.
*
* @example
* //You can easily create a template to construct any html element. See the following:
* class div extends IgniteTemplate {
* constructor(...items) {
* super("div", items);
* }
* }
*/
class IgniteTemplate {
/**
* Creates a new IgniteTemplate with the tag name of the element that will be constructed and an
* array of child elements.
* @param {String} tagName The tag name of the element this template will construct.
* @param {String|Number|IgniteProperty|IgniteTemplate} children An array of child elements to be added to this template.
*/
constructor(tagName = null, children = null) {
this.children = [];
this.tagName = tagName;
this.tagNamespace = null;
this.element = null;
this._attributes = {};
this._classes = [];
this._properties = {};
this._variables = {};
this._reflecting = {};
this._refs = [];
this._callbacks = [];
this._events = {};
this._styles = {};
this._elementValue = null;
this._elementInnerHTML = null;
this._resizeObserverCallback = null;
this._resizeObserver = null;
if (children) {
for (var i = 0; i < children.length; i++) {
if (children[i] === undefined || children[i] === null) {
continue; //Skip undefined or null children.
} else if (children[i] instanceof IgniteProperty) {
this.children.push(new html(children[i]));
} else if (children[i] instanceof String || typeof children[i] === 'string') {
this.children.push(new html(children[i]));
} else if (children[i] instanceof Number || typeof children[i] === 'number') {
this.children.push(new html(children[i]));
} else if (children[i] instanceof IgniteTemplate || children[i].prototype instanceof IgniteTemplate) {
this.children.push(children[i]);
} else {
throw `Attempted to add a child for template: ${this.tagName} which is not supported. Child: ${children[i]}`;
}
}
}
}
/**
* Adds a CSS class to be added once this template is constructed.
* @param {String|IgniteProperty} name Name of the CSS class to add. Multiple CSS classes are supported if they are separated by a space.
* @param {Function} converter Optional function that can convert the class name into a different one.
* @example
* .class("row justify-content-center")
* @returns This ignite template so function calls can be chained.
*/
class(name, converter = null) {
IgniteRenderingContext.push();
if (name instanceof IgniteProperty) {
this._callbacks.push(name.attachOnChange((oldValue, newValue) => this.onClassChanged((converter != null ? converter(oldValue) : oldValue), (converter != null ? converter(newValue) : newValue))));
this._callbacks.push(name.attachOnPush((list, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(name.attachOnUnshift((list, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(name.attachOnPop((list) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(name.attachOnShift((list) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(name.attachOnSplice((list, start, deleteCount, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
var value = (converter != null ? converter(name.value) : name.value);
(value != null ? value.toString().split(" ") : []).forEach(cl => {
if (cl.length > 0) {
this._classes.push(cl);
}
});
} else if (Array.isArray(name) && name.length > 0 && name[0] instanceof IgniteProperty) {
//There must be a converter for this to work correctly
if (!converter) {
throw "Cannot pass an array of properties without using a converter!";
}
//Attack a callback for all the properties
name.forEach(prop => {
if (prop instanceof IgniteProperty) {
this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, oldValue)), converter(...name.getPropertyValues()))));
this._callbacks.push(prop.attachOnPush((list, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
this._callbacks.push(prop.attachOnUnshift((list, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
this._callbacks.push(prop.attachOnPop((list) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
this._callbacks.push(prop.attachOnShift((list) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
}
});
var value = converter(...name.getPropertyValues());
(value != null ? value.toString().split(" ") : []).forEach(cl => {
if (cl.length > 0) {
this._classes.push(cl);
}
});
} else {
var value = (converter != null ? converter(name) : name);
(value != null ? value.toString().split(" ") : []).forEach(cl => {
if (cl.length > 0) {
this._classes.push(cl);
}
});
}
IgniteRenderingContext.pop();
return this;
}
/**
* Adds a html element attribute to this template to be added once this template is constructed.
* @param {String} name The name of the attribute to add
* @param {String|IgniteProperty} value The value of the attribute to set, can be anything. If Property is passed it will auto update.
* @param {Function} converter Optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
attribute(name, value, converter = null) {
IgniteRenderingContext.push();
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name, converter)));
this._attributes[name] = converter != null ? converter(value.value) : value.value;
} else {
this._attributes[name] = converter != null ? converter(value) : value;
}
IgniteRenderingContext.pop();
return this;
}
/**
* Sets the value of the element this template is constructing with the option to reflect changes
* to the value.
* @param {String|IgniteProperty} value The value to set on the element.
* @param {Boolean|Function} reflect Whether or not to reflect changes to the value of the element back to the property if one was used.
* @param {Function} converter Optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
value(value, reflect = false, converter = null) {
IgniteRenderingContext.push();
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onValueChanged((converter != null ? converter(newValue) : newValue))));
this._callbacks.push(value.attachOnPush((list, items) => this.onValueChanged((converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnUnshift((list, items) => this.onValueChanged((converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnPop((list) => this.onValueChanged((converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnShift((list) => this.onValueChanged((converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onValueChanged((converter != null ? converter(list) : null))));
if (reflect != null && ((typeof (reflect) == "boolean" && reflect == true) || reflect instanceof Function)) {
var valueChanged = e => {
var newValue = null;
var type = this.element.hasAttribute("type") ? this.element.getAttribute("type").toLowerCase().trim() : null;
if (type == "checkbox") {
newValue = this.element.checked;
} else if (type == "radio") {
newValue = this.element.checked;
} else if (type == "number") {
newValue = Number(this.element.value);
} else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
newValue = this.element.textContent;
} else {
newValue = this.element.value;
}
if (reflect instanceof Function) {
reflect(newValue);
} else {
value.setValue(newValue, true);
}
};
this.on("change", valueChanged);
this.on("keyup", valueChanged);
}
this._elementValue = (converter != null ? converter(value.value) : value.value);
} else {
this._elementValue = (converter != null ? converter(value) : value);
}
IgniteRenderingContext.pop();
return this;
}
/**
* Sets a property on the element this template will construct.
* @param {String} name Name of the property to set.
* @param {Any|IgniteProperty} value Value of the property to use. If a Property is passed the value will auto update.
* @param {Boolean} reflect If true whenever this property is changed it's value will be passed back to the Property that was passed as value if one was passed.
* @param {Function} converter Optional function that can be used to convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
property(name, value, reflect = false, converter = null) {
IgniteRenderingContext.push();
if (this._properties[name]) {
throw `Attempted to set a property twice on a IgniteTemplate: ${this.tagName}. This is not allowed and should be avoided.`;
}
if (reflect && converter != null) {
throw `Cannot add a property to an IgniteTemplate: ${this.tagName} with reflect and a converter used at the same time.`;
}
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onPropertyChanged(name, (converter != null ? converter(newValue) : newValue))));
this._callbacks.push(value.attachOnPush((list, items) => this.onPropertyChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnUnshift((list, items) => this.onPropertyChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnPop((list) => this.onPropertyChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnShift((list) => this.onPropertyChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onPropertyChanged(name, (converter != null ? converter(list) : list))));
this._properties[name] = {
value: (converter != null ? converter(value.value) : value.value),
reflect: (reflect == true ? value : null)
};
} else {
this._properties[name] = {
value: (converter != null ? converter(value) : value),
reflect: null
};
}
IgniteRenderingContext.pop();
return this;
}
/**
* Sets a variable on the element this template will construct.
* @param {String} name Name of the variable to set
* @param {Any|IgniteProperty} value Value of the variable to set
* @param {Function} converter Optional function that can be used to convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
variable(name, value, converter = null) {
IgniteRenderingContext.push();
if (this._variables[name]) {
throw `Attempted to set a variable twice on a IgniteTemplate: ${this.tagName}. This is not allowed and should be avoided.`;
}
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onVariableChanged(name, (converter != null ? converter(newValue) : newValue))));
this._callbacks.push(value.attachOnPush((list, items) => this.onVariableChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnUnshift((list, items) => this.onVariableChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnPop((list) => this.onVariableChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnShift((list) => this.onVariableChanged(name, (converter != null ? converter(list) : list))));
this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onVariableChanged(name, (converter != null ? converter(list) : list))));
this._variables[name] = {
value: (converter != null ? converter(value.value) : value.value)
};
} else {
this._variables[name] = {
value: (converter != null ? converter(value) : value)
};
}
IgniteRenderingContext.pop();
return this;
}
/**
* Makes a property on this template reflect its value back to the given target.
* (You can reflect a property more than once if it's needed.)
* @param {String} name Name of the property to reflect.
* @param {IgniteProperty|Function} target The target for the value to be reflected to.
* @returns This ignite template so function calls can be chained.
*/
reflect(name, target) {
IgniteRenderingContext.push();
if (this._reflecting[name] == undefined || this._reflecting[name] == null) {
this._reflecting[name] = [];
}
if (target instanceof IgniteProperty) {
this._reflecting[name].push(target);
} else if (target instanceof Function) {
this._reflecting[name].push(target);
}
IgniteRenderingContext.pop();
return this;
}
/**
* Adds a set of properties from an object to be added to this template once it's constructed.
* @param {Object} props The object value that property names/values will be pulled from.
* @returns This ignite template so function calls can be chained.
*/
properties(props) {
IgniteRenderingContext.push();
//Make sure we have a valid props.
if (props == null || props == undefined) {
return;
}
if (!(typeof props === 'object')) {
throw `Cannot set properties with a non object set of properties: ${props}`;
}
var propNames = Object.keys(props);
propNames.forEach(name => {
this.property(name, props[name], false, null);
});
IgniteRenderingContext.pop();
return this;
}
/**
* Sets the inner html of the element to be constructed by this template.
* @param {String|IgniteProperty} value InnerHTML to set for element. If a property is passed the html will auto update.
* @param {Function} converter Optional function that can be used to convert the value if needed.
* @returns This ignite template so funciton calls can be chained.
*/
innerHTML(value, converter = null) {
IgniteRenderingContext.push();
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onInnerHTMLChanged((converter != null ? converter(oldValue) : oldValue), (converter != null ? converter(newValue) : newValue))));
this._callbacks.push(value.attachOnPush((list, items) => this.onInnerHTMLChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnUnshift((list, items) => this.onInnerHTMLChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnPop((list) => this.onInnerHTMLChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnShift((list) => this.onInnerHTMLChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onInnerHTMLChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
this._elementInnerHTML = (converter != null ? converter(value.value) : value.value);
} else {
this._elementInnerHTML = (converter != null ? converter(value) : value);
}
IgniteRenderingContext.pop();
return this;
}
/**
* Adds a single or series of children to be added once this template
* is constructed. Numbers, Strings, and Properties passed will be added as HTML child elements.
* @param {...Number|String|IgniteProperty|IgniteTemplate} items A series of children to be added to this template.
* @returns This ignite template so function calls can be chained.
*/
child(...items) {
if (items) {
for (var i = 0; i < items.length; i++) {
if (items[i] === undefined || items[i] === null) {
continue; //Skip undefined or null items.
} else if (items[i] instanceof IgniteProperty) {
this.children.push(new html(items[i]));
} else if (items[i] instanceof String || typeof items[i] === 'string') {
this.children.push(new html(items[i]));
} else if (items[i] instanceof Number || typeof items[i] === 'number') {
this.children.push(new html(items[i]));
} else if (items[i] instanceof IgniteTemplate || items[i].prototype instanceof IgniteTemplate) {
this.children.push(items[i]);
} else {
throw `Attempted to add a child for template: ${this.tagName} which is not supported. Child: ${items[i]}`;
}
}
}
return this;
}
/**
* Adds a reference callback function to be invoked once this template is constructed. The function will be invoked
* with the constructed HTMLElement. If an IgniteProperty is passed it's value will be set to the constructed HTMLElement once
* the template is constructed.
* @param {Function|IgniteProperty} refCallback The callback to be invoked with the HTMLElement of the element this template constructed.
* @returns This ignite template so function calls can be chained.
*/
ref(refCallback) {
if (refCallback instanceof IgniteProperty) {
this._callbacks.push(refCallback.attachOnChange((oldValue, newValue) => this.onRefChanged(oldValue, newValue, refCallback)));
this._refs.push(element => refCallback.value = element);
} else {
this._refs.push(refCallback);
}
return this;
}
/**
* Adds an event by its name and the function to invoke once the event fires. Properties may be used
* for the function, but their value must be a valid function in order to get a proper event callback.
* @param {String} eventName The name of the event to add.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
on(eventName, eventCallback) {
IgniteRenderingContext.push();
if (!this._events[eventName]) {
this._events[eventName] = [];
}
if (eventCallback instanceof IgniteProperty) {
this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => this.onEventChanged(oldValue, newValue, eventName)));
this._events[eventName].push(eventCallback.value);
} else {
this._events[eventName].push(eventCallback);
}
IgniteRenderingContext.pop();
return this;
}
/**
* Adds a click event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onClick(eventCallback) {
return this.on("click", eventCallback);
}
/**
* Adds a touch event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onTouch(eventCallback) {
return this.on("touch", eventCallback);
}
/**
* Adds a onblur event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onBlur(eventCallback) {
return this.on("blur", eventCallback);
}
/**
* Adds a onfocus event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onFocus(eventCallback) {
return this.on("focus", eventCallback);
}
/**
* Adds a onchange event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onChange(eventCallback) {
return this.on("change", eventCallback);
}
/**
* Adds a on enter key press event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onEnter(eventCallback) {
var eventName = "keydown";
if (!this._events[eventName]) {
this._events[eventName] = [];
}
if (eventCallback instanceof IgniteProperty) {
this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => {
//Create a new wrapped function to check for the enter key being pressed.
var wrapped = (e) => {
if (e.key === 'Enter') {
newValue(e);
}
};
eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
}));
//Create the initial wrapper
var target = eventCallback._value;
var wrapped = (e) => {
if (e.key === 'Enter') {
target(e);
}
};
//Store the wrapped so that it's the old value next time around.
eventCallback._value = wrapped;
this._events[eventName].push(wrapped);
} else {
this.on(eventName, (e) => {
if (e.key === 'Enter') {
eventCallback(e);
}
});
}
return this;
}
/**
* Adds a on backspace key press event handler to this template.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onBackspace(eventCallback) {
var eventName = "keydown";
if (!this._events[eventName]) {
this._events[eventName] = [];
}
if (eventCallback instanceof IgniteProperty) {
this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => {
//Create a new wrapped function to check for the enter key being pressed.
var wrapped = (e) => {
if (e.key === 'Backspace') {
newValue(e);
}
};
eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
}));
//Create the initial wrapper
var target = eventCallback._value;
var wrapped = (e) => {
if (e.key === 'Backspace') {
target(e);
}
};
//Store the wrapped so that it's the old value next time around.
eventCallback._value = wrapped;
this._events[eventName].push(wrapped);
} else {
this.on(eventName, (e) => {
if (e.key === 'Backspace') {
eventCallback(e);
}
});
}
return this;
}
/**
* Adds a special on resize event handler to this template that will
* fire anytime the element is resized by using a resize observer.
* @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
* @returns This ignite template so function calls can be chained.
*/
onResize(eventCallback) {
IgniteRenderingContext.push();
if (eventCallback instanceof IgniteProperty) {
this._resizeObserverCallback = eventCallback.value;
} else if (eventCallback instanceof Function) {
this._resizeObserverCallback = eventCallback;
}
IgniteRenderingContext.pop();
return this;
}
/**
* Adds a CSS property to this template with a value and priority.
* @param {String} name The name of the CSS property to set.
* @param {String|IgniteProperty} value The value to set for the property. If an IgniteProperty is used it will auto update this style.
* @param {String} priority If set to "important" then the style will be marked with !important. Acceptable values: important, !important, true, false, null
* @param {Function} converter Optional function to convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
style(name, value, priority = null, converter = null) {
IgniteRenderingContext.push();
//If the name has a : remove it.
if (name && typeof name === "string" && name.includes(":")) {
name = name.replace(":", "");
}
//If the priority starts with a ! remove it.
if (priority && typeof priority === "string" && priority.trim().startsWith("!")) {
priority = priority.split("!")[1].trim();
} else if (priority && typeof priority === "boolean") {
priority = "important"; //If priority is true, set it to important
} else if (!priority && typeof priority === "boolean") {
priority = null; //If priority is false, set it to null.
}
//If the value has a ; remove it.
if (value && typeof value === "string" && value.includes(";")) {
value = value.replace(";", "");
}
if (value instanceof IgniteProperty) {
this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, (converter ? converter(newValue) : newValue))));
this._styles[name] = { name: name, value: (converter ? converter(value.value) : value.value), priority: priority };
} else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
//There must be a converter for this to work correctly
if (!converter) {
throw "Cannot pass an array of properties without using a converter!";
}
//Attack a callback for all the properties
value.forEach(prop => {
if (prop instanceof IgniteProperty) {
this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this._callbacks.push(prop.attachOnPush((list, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this._callbacks.push(prop.attachOnUnshift((list, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this._callbacks.push(prop.attachOnPop((list) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this._callbacks.push(prop.attachOnShift((list) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
}
});
this._styles[name] = { name: name, value: converter(...value.getPropertyValues()), priority: priority };
} else {
this._styles[name] = { name: name, value: (converter != null ? converter(value) : value), priority: priority };
}
IgniteRenderingContext.pop();
return this;
}
/**
* Hides the element this template is constructing if the value is true.
* @param {Boolean|IgniteProperty} value If true hides the element this template is constructing. If an IgniteProperty is passed it's value will auto update this.
* @param {Function} converter An optional function to convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
hide(value, converter = null) {
return this.style("display", value, true, (...params) => {
return ((converter != null && converter(...params)) || (converter == null && params[0])) ? "none" : null;
});
}
/**
* Shows the element this template is constructing if the value is true.
* @param {Boolean|IgniteProperty} value
* @param {Function} converter
* @returns This ignite template so function calls can be chained.
*/
show(value, converter = null) {
return this.style("display", value, true, (...params) => {
return ((converter != null && converter(...params)) || (converter == null && params[0])) ? null : "none";
});
}
/**
* Sets the id attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the id attribute of the element this template will construct.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
id(value, converter = null) {
return this.attribute("id", value, converter);
}
/**
* Sets the title attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the title attribute of the element this template will construct.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
title(value, converter = null) {
return this.attribute("title", value, converter);
}
/**
* Sets the for attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the for attribute of the element this template will construct.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
for(value, converter = null) {
return this.attribute("for", value, converter);
}
/**
* Adds a checked attribute to this template.
* @param {Boolean|IgniteProperty} value The value to set for the checked attribute.
* @param {*} converter Optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
checked(value, converter = null) {
return this.attribute("checked", value, converter);
}
/**
* Adds a disabled attribute and class to this template.
* @param {Boolean|IgniteProperty} value A value to determine whether or not the element should be marked as disable dor not.
* @param {*} converter Optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
disabled(value, converter = null) {
if (value instanceof IgniteProperty) {
this.attribute("disabled", value, convert => {
convert = (converter != null ? converter(convert) : convert);
if (convert) {
return "disabled";
} else {
return null;
}
});
this.class(value, convert => {
convert = (converter != null ? converter(convert) : convert);
if (convert) {
return "disabled";
} else {
return null;
}
});
} else if (value) {
this.attribute("disabled", "disabled");
this.class("disabled");
}
return this;
}
/**
* Sets the type attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the type attribute of the element this template will construct.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
type(value, converter = null) {
return this.attribute("type", value, converter);
}
/**
* Sets a data attribute on the element to be constructed by this template.
* @param {String} name The name of the data attribute to set on the element this template will construct.
* @param {String|IgniteProperty} value The value to set for the data attribute of the element this template will construct.
* @param {*} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
data(name, value, converter = null) {
return this.attribute(`data-${name}`, value, converter);
}
/**
* Sets the value attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the src attribute of the element to be constructed by this template.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
src(value, converter = null) {
return this.attribute("src", value, converter);
}
/**
* Sets the value attribute of the element to be constructed by this template.
* @param {String|IgniteProeprty} value The value to set for the href attribute of the element to be constructed by this template.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
href(value, converter = null) {
return this.attribute("href", value, converter);
}
/**
* Sets the name attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the name attribute of the element to be constructed by this template.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
name(value, converter = null) {
return this.attribute("name", value, converter);
}
/**
* Sets the placeholder attribute of the element to be constructed by this template.
* @param {String|IgniteProperty} value The value to set for the placeholder attribute of the element.
* @param {Function} converter An optional function that can convert the value if needed.
* @returns This ignite template so function calls can be chained.
*/
placeholder(value, converter = null) {
return this.attribute("placeholder", value, converter);
}
/**
* Constructs this template and adds it to the DOM if this template
* has not already been constructed.
* @param {HTMLElement} parent Parent element that will contain the constructed element.
* @param {HTMLElement} sibling Optional sibling element that can be used to add the element adjacantly.
*/
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
//Construct this element if we haven't already
if (!this.element) {
//If we have a tag namespace use that to construct this element. This is mainly
//for svg elements since they have to be created this way.
if (this.tagNamespace) {
this.element = window.document.createElementNS(this.tagNamespace, this.tagName);
} else {
this.element = window.document.createElement(this.tagName);
}
//Pass back our template to the element we are creating if it's not a ignite element, since
//it will have it's own template automatically.
if (!(this.element instanceof IgniteElement)) {
this.element.template = this;
}
//Invoke any refs we have and pass back the element reference.
this._refs.forEach((ref) => ref(this.element));
}
//Set the classes on this element
for (var i = 0; i < this._classes.length; i++) {
if (this._classes[i] !== null && this._classes[i] !== undefined && this._classes[i] !== "") {
this.element.classList.add(this._classes[i]);
}
}
//Set the attributes on this element
var keys = Object.keys(this._attributes);
for (var i = 0; i < keys.length; i++) {
if (this._attributes[keys[i]] !== null && this._attributes[keys[i]] !== undefined) {
this.element.setAttribute(keys[i], this._attributes[keys[i]]);
}
}
//Set the events on this element
var keys = Object.keys(this._events);
for (var i = 0; i < keys.length; i++) {
this._events[keys[i]].forEach((event) => {
if (event !== null && event !== undefined) {
this.element.addEventListener(keys[i], event);
}
});
}
//Set the styles on this element.
var keys = Object.keys(this._styles);
for (var i = 0; i < keys.length; i++) {
var style = this._styles[keys[i]];
this.element.style.setProperty(style.name, style.value, style.priority);
}
//Set the properties on this element
var keys = Object.keys(this._properties);
for (var i = 0; i < keys.length; i++) {
this.element[keys[i]] = this._properties[keys[i]].value;
if (this._properties[keys[i]].reflect != null) {
this.element[keys[i]].reflected.push(this._properties[keys[i]].reflect);
}
}
//Set the variables on this element.
var keys = Object.keys(this._variables);
for (var i = 0; i < keys.length; i++) {
this.element[keys[i]] = this._variables[keys[i]].value;
}
//Setup any reflecting properties on this element
var keys = Object.keys(this._reflecting);
for (var i = 0; i < keys.length; i++) {
this._reflecting[keys[i]].forEach(value => this.element[keys[i]].reflected.push(value));
}
//Set the elements inner html if it was set
if (this._elementInnerHTML != null) {
this.element.innerHTML = this._elementInnerHTML;
}
//Construct the children under this element
for (var i = 0; i < this.children.length; i++) {
this.children[i].construct(this.element);
}
//Set the elements value if there is one.
if (this._elementValue != null) {
if (this.element.hasAttribute("type") && (this.element.getAttribute("type").toLowerCase().trim() == "checkbox" || this.element.getAttribute("type").toLowerCase().trim() == "radio")) {
this.element.checked = this._elementValue;
} else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
this.element.textContent = this._elementValue.toString();
} else {
this.element.value = this._elementValue;
}
}
//Setup a resize observer if needed
if (this._resizeObserverCallback !== null) {
this._resizeObserver = new ResizeObserver(e => this._resizeObserverCallback(e));
this._resizeObserver.observe(this.element);
}
//If our element has not been added to the dom yet, then add it.
if (this.element.isConnected == false && this.element.parentElement == null) {
if (sibling) {
parent.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
}
}
/**
* Deconstructs this template and cleans up all resources to make sure
* there are no memory leaks.
*/
deconstruct() {
//Remove and disconnect all events
if (this.element && this._events) {
var keys = Object.keys(this._events);
for (var i = 0; i < keys.length; i++) {
this.element.removeEventListener(keys[i], this._events[keys[i]]);
}
this._events = null;
}
//Remove our element if we have one.
if (this.element) {
this.element.remove();
this.element = null;
}
//Deconstruct all children elements.
if (this.children) {
for (var i = 0; i < this.children.length; i++) {
if (this.children[i] instanceof IgniteTemplate || this.children[i].prototype instanceof IgniteTemplate) {
this.children[i].deconstruct();
}
}
this.children = null;
}
//Disconnect all callbacks
if (this._callbacks) {
this._callbacks.forEach((item) => item.disconnect());
this._callbacks = null;
}
//Stop observing resize events if we need to.
if (this._resizeObserver != null) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
this._resizeObserverCallback = null;
}
//Remove any refs
if (this._refs) {
//Pass null to our refs so that the reference is updated.
this._refs.forEach(ref => ref(null));
this._refs = null;
}
}
/**
* Called when a class on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {Function} converter Optional converter for the value if needed.
* @ignore
*/
onClassChanged(oldValue, newValue) {
var oldClasses = (oldValue != null && oldValue != "" ? oldValue.toString().split(" ") : []);
var newClasses = (newValue != null && newValue != "" ? newValue.toString().split(" ") : []);
if (this.element) {
oldClasses.forEach((cl) => this.element.classList.remove(cl));
newClasses.forEach((cl) => this.element.classList.add(cl));
}
//Remove the old values from the template, but only remove one copy.
oldClasses.forEach((cl) => this._classes.splice(this._classes.indexOf(cl), 1));
//Add the new classes to the template.
newClasses.forEach((cl) => this._classes.push(cl));
//For any classes that are missing on the element, add them. If we have duplicates this
//can happen.
this._classes.forEach((cl) => {
if (!this.element.classList.contains(cl)) {
this.element.classList.add(cl);
}
});
}
/**
* Called when a attribute on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {string} attributeName
* @ignore
*/
onAttributeChanged(oldValue, newValue, attributeName, converter) {
if (converter !== null) {
IgniteRenderingContext.push();
newValue = converter(newValue);
IgniteRenderingContext.pop();
}
if (this.element) {
if (newValue == null || newValue == undefined) {
this.element.removeAttribute(attributeName);
} else {
this.element.setAttribute(attributeName, newValue);
}
}
this._attributes[attributeName] = newValue;
}
/**
* Called when a value for this template was changed and needs to be updated on the template's element.
* @param {any} newValue
* @ignore
*/
onValueChanged(newValue) {
//Only update the elements value if it actually changed.
//This is to prevent endless looping potentially.
if (this.element) {
if (this.element.hasAttribute("type") && (this.element.getAttribute("type").toLowerCase().trim() == "checkbox" || this.element.getAttribute("type").toLowerCase().trim() == "radio")) {
if (this.element.checked != newValue) {
this.element.checked = newValue;
}
} else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
if (this.element.textContent != newValue.toString()) {
this.element.textContent = newValue.toString();
}
} else {
if (this.element.value != newValue) {
this.element.value = newValue;
}
}
}
this._elementValue = newValue;
}
/**
* Called when a property on this template was changed and needs to be updated
* on the template's element.
* @param {string} propertyName
* @param {any} newValue
* @ignore
*/
onPropertyChanged(propertyName, newValue) {
if (this.element) {
//Use the set value function and don't reflect the change because it came from above which
//would be the reflected property, this reduces some functions being called twice that don't need to be.
this.element[`_${propertyName}`].setValue(newValue, false);
}
this._properties[propertyName].value = newValue;
}
/**
* Called when a variable on this template was changed and needs to be updated.
* @param {string} variableName
* @param {any} newValue
* @ignore
*/
onVariableChanged(variableName, newValue) {
if (this.element) {
this.element[variableName] = newValue;
}
this._variables[variableName].value = newValue;
}
/**
* Called when the inner html for this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @ignore
*/
onInnerHTMLChanged(oldValue, newValue) {
if (this.element) {
this.element.innerHTML = newValue;
}
this._elementInnerHTML = newValue;
}
/**
* Called when a ref was changed and we need to update the refs
* value to match this elements reference.
* @param {any} oldValue
* @param {any} newValue
* @param {any} ref
* @ignore
*/
onRefChanged(oldValue, newValue, ref) {
//Only set the reference value to ourself if it's not our element.
//Otherwise we will get a never ending loop.
if (this.element != newValue) {
ref.value = this.element;
}
}
/**
* Called when a event was changed and we need to update it.
* @param {any} oldValue
* @param {any} newValue
* @param {any} eventName
* @ignore
*/
onEventChanged(oldValue, newValue, eventName) {
if (this.element) {
if (oldValue !== null && oldValue !== undefined) {
this.element.removeEventListener(eventName, oldValue);
}
if (newValue !== null && newValue !== undefined) {
this.element.addEventListener(eventName, newValue);
}
//Remove the old value from the events
this._events[eventName] = this._events[eventName].filter(ev => ev != oldValue);
//Add the new value if it's needed
if (newValue !== null && newValue !== undefined) {
this._events[eventName].push(newValue);
}
}
}
/**
* Called when a css value was changed and we need to update the styling.
* @param {String} name
* @param {any} newValue
* @ignore
*/
onStyleChanged(name, newValue) {
//Remove the ; from the value if there is one.
if (newValue && typeof newValue === "string" && newValue.includes(";")) {
newValue = newValue.replace(";", "");
}
if (this.element) {
this.element.style.setProperty(name, newValue, this._styles[name].priority);
}
this._styles[name].value = newValue;
}
}
/**
* An ignite template that can be used to construct a div element.
*/
class div extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("div", children);
}
}
/**
* An ignite template that can be used to construct a hyperlink element.
*/
class a extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("a", children);
}
}
/**
* An ignite template that can be used to construct a input element.
*/
class input extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("input", children);
}
}
/**
* An ignite template that can be used to construct a textarea element.
*/
class textarea extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("textarea", children);
}
}
/**
* An ignite template that can be used to construct a button element.
*/
class button extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("button", children);
this.type("button");
}
}
/**
* An ignite template that can be used to construct a h1 element.
*/
class h1 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h1", children);
}
}
/**
* An ignite template that can be used to construct a h2 element.
*/
class h2 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h2", children);
}
}
/**
* An ignite template that can be used to construct a h3 element.
*/
class h3 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h3", children);
}
}
/**
* An ignite template that can be used to construct a h4 element.
*/
class h4 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h4", children);
}
}
/**
* An ignite template that can be used to construct a h5 element.
*/
class h5 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h5", children);
}
}
/**
* An ignite template that can be used to construct a h6 element.
*/
class h6 extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("h6", children);
}
}
/**
* An ignite template that can be used to construct a hr element.
*/
class hr extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("hr", children);
}
}
/**
* An ignite template that can be used to construct a p element.
*/
class p extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("p", children);
}
}
/**
* An ignite template that can be used to construct a ul element.
*/
class ul extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("ul", children);
}
}
/**
* An ignite template that can be used to construct a li element.
*/
class li extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("li", children);
}
}
/**
* An ignite template that can be used to construct a span element.
*/
class span extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("span", children);
}
}
/**
* An ignite template that can be used to construct a strong element.
*/
class strong extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("strong", children);
}
}
/**
* An ignite template that can be used to construct an i element.
*/
class i extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("i", children);
}
}
/**
* An ignite template that can be used to construct a table element.
*/
class table extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("table", children);
}
}
/**
* An ignite template that can be used to construct a td element.
*/
class td extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("td", children);
}
}
/**
* An ignite template that can be used to construct a th element.
*/
class th extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("th", children);
}
}
/**
* An ignite template that can be used to construct a tr element.
*/
class tr extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("tr", children);
}
}
/**
* An ignite template that can be used to construct a thead element.
*/
class thead extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("thead", children);
}
}
/**
* An ignite template that can be used to construct a tbody element.
*/
class tbody extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("tbody", children);
}
}
/**
* An ignite template that can be used to construct a br element.
*/
class br extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("br", children);
}
}
/**
* An ignite template that can be used to construct a img element.
*/
class img extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("img", children);
}
}
/**
* An ignite template that can be used to construct a label element.
*/
class label extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("label", children);
}
}
/**
* An ignite template that can be used to construct a select element.
*/
class select extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("select", children);
}
}
/**
* An ignite template that can be used to construct a option element.
*/
class option extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("option", children);
}
}
/**
* An ignite template that can be used to construct a script element.
*/
class script extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("script", children);
}
}
/**
* An ignite template that can be used to construct a form element.
*/
class form extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("form", children);
}
}
/**
* An ignite template that can be used to construct a progress element.
*/
class progress extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("progress", children);
}
}
/**
* An ignite template that can be used to construct a svg element.
*/
class svg extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("svg", children);
this.tagNamespace = "http://www.w3.org/2000/svg";
}
}
/**
* An ignite template that can be used to construct a g element.
*/
class g extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("g", children);
this.tagNamespace = "http://www.w3.org/2000/svg";
}
}
/**
* An ignite template that can be used to construct a path element.
*/
class path extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("path", children);
this.tagNamespace = "http://www.w3.org/2000/svg";
}
}
/**
* An ignite template that can be used to construct a circle element.
*/
class circle extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("circle", children);
this.tagNamespace = "http://www.w3.org/2000/svg";
}
}
/**
* An ignite template that can be used to construct a line element.
*/
class line extends IgniteTemplate {
/**
* @param {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
*/
constructor(...children) {
super("line", children);
this.tagNamespace = "http://www.w3.org/2000/svg";
}
}
/**
* Html is a special template that can construct raw html or properties into the dom and automatically
* update the dom if the property changes.
*
* @example
* new html(`<h1>Hello world!</h1>`)
*/
class html extends IgniteTemplate {
/**
* @param {String|IgniteProperty} code HTML code to be constructed within this template. If an IgniteProperty is passed it's value will be used.
*/
constructor(code) {
super();
if (code instanceof IgniteProperty) {
this._callbacks.push(code.attachOnChange((oldValue, newValue) => this.onPropertyChanged(oldValue, newValue)));
this.code = code.value;
} else {
this.code = code;
}
this.tagName = "shadow html";
this.elements = [];
}
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
if (!this.element) {
this.element = window.document.createTextNode("");
if (sibling) {
sibling.parentElement.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
}
//Create a template to hold the elements that will be created from the
//properties value and then add them to the DOM and store their pointer.
if (this.elements.length == 0 && this.code !== null && this.code !== undefined) {
//If the code is an ignite template then reder that template.
if (this.code instanceof IgniteTemplate) {
this.code.construct(parent, this.element);
this.elements.push(this.code.element);
} else {
var template = window.document.createElement("template");
template.innerHTML = this.code.toString();
while (template.content.childNodes.length > 0) {
var item = template.content.childNodes[0];
this.element.parentElement.insertBefore(item, this.element);
this.elements.push(item);
}
template.remove();
}
}
}
onPropertyChanged(oldValue, newValue) {
//Update our code to the new value from the property.
this.code = newValue;
//Remove any elements that already exist.
this.elements.forEach((item) => item.remove());
this.elements = [];
//Reconstruct the html
this.construct(null, null);
}
}
/**
* A special ignite template that constructs a list of items using a template
* that is dynamically created for each item.
*
* @example
* new list(["1", "2", "3"], (item) => {
* return new h1(item);
* })
*/
class list extends IgniteTemplate {
/**
* @param {Array|IgniteProperty} list The list of items to construct within this template.
* @param {Function} forEach A function that construct a template for an item from the list that is passed to it.
* @param {Boolean} reflect If true any items removed from the DOM will be removed from the list if they exist. By default this is false.
*/
constructor(list, forEach, reflect = false) {
super();
if (list instanceof IgniteProperty) {
this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(newValue)));
this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(items)));
this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(items)));
this._callbacks.push(list.attachOnPop(list => this.onListPop()));
this._callbacks.push(list.attachOnShift(list => this.onListShift()));
this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(start, deleteCount, items)));
this.list = list.value;
} else {
this.list = list;
}
this.reflecting = reflect;
this.reflectCallbacks = [];
this.forEach = forEach;
this.elements = [];
this.tagName = "shadow list";
}
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
if (!this.element) {
this.element = window.document.createTextNode(""); //Use a textnode as our placeholder
if (sibling) {
sibling.parentElement.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
} else {
parent = this.element.parentElement;
}
//If we have any reflect callbacks, remove them.
//(If we dont do this, we will accidentally remove items from the list potentially)
if (this.reflectCallbacks.length > 0) {
for (var i = 0; i < this.reflectCallbacks.length; i++) {
this.reflectCallbacks[i].disconnect();
}
this.reflectCallbacks = [];
}
//If we already have elements we created, destroy them.
if (this.elements.length > 0) {
for (var i = 0; i < this.elements.length; i++) {
this.elements[i].remove();
}
this.elements = [];
}
//If we already have children elements, deconstruct them.
//(Because we are about to recreate them)
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].deconstruct();
}
this.children = [];
}
//Construct all the items in our list and use the container
if (this.list) {
for (var i = 0; i < this.list.length; i++) {
var template = this.forEach(this.list[i]);
template.construct(parent, this.element);
//If we are reflecting, attach to the elements disconnect event.
if (this.reflecting) {
this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(this.list[i])));
}
this.children.push(template);
this.elements.push(template.element);
}
}
}
onListChanged(newValue) {
this.list = newValue;
IgniteRenderingContext.enter();
try {
this.construct(null); //The list changed, reconstruct this template.
} catch (error) {
console.error("An error occurred during onListChanged:", error);
}
IgniteRenderingContext.leave();
}
onListPush(items) {
IgniteRenderingContext.enter();
try {
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
} else {
template.construct(this.element.parentElement, this.element);
}
//If we are reflecting, attach to the elements disconnect event.
if (this.reflecting) {
this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
}
this.children.push(template);
this.elements.push(template.element);
});
} catch (error) {
console.error("An error occurred during onListPush:", error);
}
IgniteRenderingContext.leave();
}
onListUnshift(items) {
IgniteRenderingContext.enter();
try {
items.reverse();
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[0]);
} else {
template.construct(this.element.parentElement, this.element);
}
if (this.reflecting) {
this.reflectCallbacks.unshift(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
}
this.children.unshift(template);
this.elements.unshift(template.element);
});
} catch (error) {
console.error("An error occurred during onListUnshift:", error);
}
IgniteRenderingContext.leave();
}
onListPop() {
if (this.children.length > 0) {
this.children[this.children.length - 1].deconstruct();
this.children.pop();
this.elements.pop();
if (this.reflecting) {
this.reflectCallbacks[this.reflectCallbacks.length - 1].disconnect();
this.reflectCallbacks.pop();
}
}
}
onListShift() {
if (this.children.length > 0) {
this.children[0].deconstruct();
this.children.shift();
this.elements.shift();
if (this.reflecting) {
this.reflectCallbacks[0].disconnect();
this.reflectCallbacks.shift();
}
}
}
onListSplice(start, deleteCount, items) {
IgniteRenderingContext.enter();
//Remove any items that will no longer exist.
if (deleteCount > 0 && this.children.length > 0) {
for (var i = start; i < Math.min(this.children.length, start + deleteCount); i++) {
this.children[i].deconstruct();
if (this.reflecting) {
this.reflectCallbacks[i].disconnect();
}
}
this.children.splice(start, deleteCount);
this.elements.splice(start, deleteCount);
if (this.reflecting) {
this.reflectCallbacks.splice(start, deleteCount);
}
}
//If the start is greater than the length of the items adjust it.
if (start > this.children.length) {
start = Math.max(this.children.length - 1, 0);
}
//Append any new items if there are any.
if (items) {
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[start]);
} else {
template.construct(this.element.parentElement, this.element);
}
if (this.reflecting) {
this.reflectCallbacks.splice(start, 0, template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
}
this.children.splice(start, 0, template);
this.elements.splice(start, 0, template.element);
start += 1;
});
}
IgniteRenderingContext.leave();
}
onItemRemove(item) {
var index = this.list.indexOf(item);
if (index >= 0) {
this.list.splice(index, 1);
}
}
onStyleChanged(name, newValue) {
this.elements.forEach((element) => {
element.style.setProperty(name, newValue, this._styles[name].priority);
});
this._styles[name].value = newValue;
}
}
/**
* A slot template that mimicks the functionality of a slot element in Web Components. This can
* be used to place children of a IgniteElement anywhere in the DOM. Slots don't actually construct an element,
* they simply just place children in place where the slot was used. If classes, styles, or attributes are applied
* to the slot they will be applied to the children of the slot.
*
* @example
* //You must pass the ignite element who owns the slot of the first param.
* new slot(this)
*
* @example
* //Slots can apply classes, attributes, and styles to children within the slot
* new slot(this).class("active") //< Would apply .active to all children
*
* @example
* //You can also use properties to have dynamic classes, styles, or attributes on slot children
* new slot(this).class(this.someClass)
*/
class slot extends IgniteTemplate {
/**
* @param {IgniteElement} element The parent IgniteElement that this slot is for.
*/
constructor(element) {
super();
this.parent = element;
}
/**
* Constructs this slot from this template.
* @param {HTMLElement} parent
* @param {HTMLElement} sibling
* @ignore
*/
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
if (!this.element) {
this.element = window.document.createTextNode("");
if (sibling) {
sibling.parentElement.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
//Add any slot elements after this element.
this.parent.elements.forEach((item) => {
this.element.parentElement.insertBefore(item, this.element);
//Set the classes on the item
if (item.classList) {
for (var i = 0; i < this._classes.length; i++) {
if (this._classes[i] !== null && this._classes[i] !== undefined && this._classes[i] !== "") {
item.classList.add(this._classes[i]);
}
}
}
//Set the attributes on the item
var keys = Object.keys(this._attributes);
for (var i = 0; i < keys.length; i++) {
if (this._attributes[keys[i]] !== null && this._attributes[keys[i]] !== undefined) {
item.setAttribute(keys[i], this._attributes[keys[i]]);
}
}
//Set the styles on the item
if (item.style) {
var keys = Object.keys(this._styles);
for (var i = 0; i < keys.length; i++) {
var style = this._styles[keys[i]];
item.style.setProperty(style.name, style.value, style.priority);
}
}
});
}
}
/**
* Called when a class on this slot changes and needs to be updated on the
* elements within this slot.
* @param {Any} oldValue
* @param {Any} newValue
* @param {Function} converter
* @ignore
*/
onClassChanged(oldValue, newValue) {
var oldClasses = (oldValue != null && oldValue != "" ? oldValue.toString().split(" ") : []);
var newClasses = (newValue != null && newValue != "" ? newValue.toString().split(" ") : []);
//Remove the old values from the template, but only remove one copy.
oldClasses.forEach((cl) => this._classes.splice(this._classes.indexOf(cl), 1));
//Add the new classes to the template.
newClasses.forEach((cl) => this._classes.push(cl));
//Add the classes to the elements
this.parent.elements.forEach((element) => {
//Only do this if the element has a class list.
if (!element.classList) { return; }
//Remove the old ones first.
oldClasses.forEach((cl) => element.classList.remove(cl));
//Add the new ones.
newClasses.forEach((cl) => element.classList.add(cl));
//Add any missing ones
this._classes.forEach((cl) => {
if (!element.classList.contains(cl)) {
element.classList.add(cl);
}
});
});
}
/**
* Called when a attribute on this template was changed and needs to be updated
* on the template's element.
* @param {any} oldValue
* @param {any} newValue
* @param {string} attributeName
* @param {Function} converter Optional converter function for the value if needed.
* @ignore
*/
onAttributeChanged(oldValue, newValue, attributeName, converter) {
if (converter !== null) {
newValue = converter(newValue);
}
this.parent.elements.forEach((element) => {
if (newValue == null || newValue == undefined) {
element.removeAttribute(attributeName);
} else {
element.setAttribute(attributeName, newValue);
}
});
this._attributes[attributeName] = newValue;
}
/**
* Called when a css value was changed and we need to update the styling.
* @param {String} name
* @param {any} newValue
* @ignore
*/
onStyleChanged(name, newValue) {
this.parent.elements.forEach((element) => {
element.style.setProperty(name, newValue, this._styles[name].priority);
});
this._styles[name].value = newValue;
}
}
/**
* A pagination is a template that segments a list of items into pages based
* on the items, page size, current page.
*/
class pagination extends IgniteTemplate {
constructor(list, pageSize, currentPage, forEach) {
super();
if (list instanceof IgniteProperty) {
this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(oldValue, newValue)));
this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(list, items)));
this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(list, items)));
this._callbacks.push(list.attachOnPop(list => this.onListPop(list)));
this._callbacks.push(list.attachOnShift(list => this.onListShift(list)));
this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(list, start, deleteCount, items)));
this.list = list.value;
} else {
this.list = list;
}
if (pageSize instanceof IgniteProperty) {
this._callbacks.push(pageSize.attachOnChange((oldValue, newValue) => this.onPageSizeChanged(newValue)));
this.pageSize = pageSize.value;
} else {
this.pageSize = pageSize;
}
if (currentPage instanceof IgniteProperty) {
this._callbacks.push(currentPage.attachOnChange((oldValue, newValue) => this.onCurrentPageChanged(newValue)));
this.currentPage = currentPage.value;
} else {
this.currentPage = currentPage;
}
this.forEach = forEach;
this.elements = [];
this.tagName = "shadow pagination";
this.pages = [];
}
recalculate() {
//Hide the elements in the current page.
this.pages[this.currentPage].forEach(item => item.style.setProperty("display", "none", "important"));
//Recreate the pages.
var pages = parseInt((this.list.length / this.pageSize)) + (this.list.length % this.pageSize > 0 ? 1 : 0);
this.pages = [];
for (var i = 0; i < pages; i++) {
this.pages.push([]);
}
//Add the elements to the correct pages.
for (var i = 0; i < this.elements.length; i++) {
var page = parseInt(i / this.pageSize);
this.pages[page].push(this.elements[i]);
}
//Adjust the current page if it's now incorrect.
if (this.currentPage >= pages) {
this.currentPage = pages - 1;
}
//Show the elements in the current page
this.pages[this.currentPage].forEach(item => item.style.removeProperty("display"));
}
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
if (!this.element) {
this.element = window.document.createTextNode(""); //Use a textnode as our placeholder
if (sibling) {
sibling.parentElement.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
} else {
parent = this.element.parentElement;
}
//If we already have elements we created, destroy them.
if (this.elements.length > 0) {
for (var i = 0; i < this.elements.length; i++) {
this.elements[i].remove();
}
this.elements = [];
}
//If we already have children elements, deconstruct them.
//(Because we are about to recreate them)
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; i++) {
this.children[i].deconstruct();
}
this.children = [];
}
//Init our pages to put elements into.
var pages = parseInt((this.list.length / this.pageSize)) + (this.list.length % this.pageSize > 0 ? 1 : 0);
this.pages = [];
for (var i = 0; i < pages; i++) {
this.pages.push([]);
}
//Construct all the items in our list and use the container
if (this.list) {
for (var i = 0; i < this.list.length; i++) {
var template = this.forEach(this.list[i]);
template.construct(parent, this.element);
var page = parseInt(i / this.pageSize);
if (page != this.currentPage) {
template.element.style.setProperty("display", "none", "important");
} else {
template.element.style.removeProperty("display");
}
this.pages[page].push(template.element);
this.children.push(template);
this.elements.push(template.element);
}
}
}
onPageSizeChanged(newValue) {
//Set the new page size
this.pageSize = newValue;
//Recalculate the pagination.
this.recalculate();
}
onCurrentPageChanged(newValue) {
//If the new page is invalid don't do this.
if (newValue >= this.pages.length) {
return;
} else if (newValue < 0) {
return;
}
//Hide all the elements in the current page
this.pages[this.currentPage].forEach(item => item.style.setProperty("display", "none", "important"));
//Set the new current page.
this.currentPage = newValue;
//Show the elements in the next page
this.pages[this.currentPage].forEach(item => item.style.removeProperty("display"));
}
onListChanged(oldValue, newValue) {
this.list = newValue;
IgniteRenderingContext.enter();
try {
this.construct(null); //The list changed, reconstruct this template.
} catch (error) {
console.error("An error occurred during onListChanged:", error);
}
IgniteRenderingContext.leave();
}
onListPush(list, items) {
IgniteRenderingContext.enter();
try {
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
} else {
template.construct(this.element.parentElement, this.element);
}
this.children.push(template);
this.elements.push(template.element);
});
//Recalculate the pagination.
this.recalculate();
} catch (error) {
console.error("An error occurred during onListPush:", error);
}
IgniteRenderingContext.leave();
}
onListUnshift(list, items) {
IgniteRenderingContext.enter();
try {
items.reverse();
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[0]);
} else {
template.construct(this.element.parentElement, this.element);
}
this.children.unshift(template);
this.elements.unshift(template.element);
});
//Recalculate the pagination.
this.recalculate();
} catch (error) {
console.error("An error occurred during onListUnshift:", error);
}
IgniteRenderingContext.leave();
}
onListPop(list) {
if (this.children.length > 0) {
this.children[this.children.length - 1].deconstruct();
this.children.pop();
this.elements.pop();
}
//Recalculate the pagination.
this.recalculate();
}
onListShift(list) {
if (this.children.length > 0) {
this.children[0].deconstruct();
this.children.shift();
this.elements.shift();
}
//Recalculate the pagination.
this.recalculate();
}
onListSplice(list, start, deleteCount, items) {
IgniteRenderingContext.enter();
//Remove any items that are needed.
if (deleteCount > 0 && this.children.length > 0) {
for (var i = start; i < Math.min(this.children.length, start + deleteCount); i++) {
this.children[i].deconstruct();
}
this.children.splice(start, deleteCount);
this.elements.splice(start, deleteCount);
}
//If the start is greater than the length of the items adjust it.
if (start > this.children.length) {
start = Math.max(this.children.length - 1, 0);
}
//Append any new items if there are any.
if (items) {
items.forEach(item => {
var template = this.forEach(item);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[start]);
} else {
template.construct(this.element.parentElement, this.element);
}
this.children.splice(start, 0, template);
this.elements.splice(start, 0, template.element);
start += 1;
});
}
//Recalculate the pagination.
this.recalculate();
IgniteRenderingContext.leave();
}
}
/**
* An ignite template that can construct a population of items
* based on a count.
*/
class population extends IgniteTemplate {
/**
* Creates a new population with the number of items in it, a converter if needed, and a foreach function.
* @param {Number|IgniteProperty} count The number of items in this population.
* @param {Function} forEach A function to generate items in the population.
* @param {Function?} converter A converter to be used to convert the count if needed.
*/
constructor(count, forEach, converter = null) {
super();
if (count instanceof IgniteProperty) {
this._callbacks.push(count.attachOnChange((oldValue, newValue) => this.onCountChange(converter != null ? converter(newValue) : newValue)));
this.count = count.value;
} else if (Array.isArray(count) && count.length > 0 && count[0] instanceof IgniteProperty) {
//There must be a converter for this to work correctly
if (!converter) {
throw "Cannot pass an array of properties without using a converter!";
}
//Attack a callback for all the properties
count.forEach(prop => {
if (prop instanceof IgniteProperty) {
this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onCountChange(converter(...count.getPropertyValues()))));
this._callbacks.push(prop.attachOnPush((list, items) => this.onCountChange(converter(...count.getPropertyValues()))));
this._callbacks.push(prop.attachOnUnshift((list, items) => this.onCountChange(converter(...count.getPropertyValues()))));
this._callbacks.push(prop.attachOnPop((list) => this.onCountChange(converter(...count.getPropertyValues()))));
this._callbacks.push(prop.attachOnShift((list) => this.onCountChange(converter(...count.getPropertyValues()))));
this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onCountChange(converter(...count.getPropertyValues()))));
}
});
this.count = converter(...count.getPropertyValues());
} else {
this.count = (converter != null ? converter(count) : count);
}
this.forEach = forEach;
this.elements = [];
this.tagName = "shadow population";
}
construct(parent, sibling) {
//Don't construct if we have no parent, no sibling and no element.
if (!parent && !sibling && !this.element) {
return;
}
if (!this.element) {
this.element = window.document.createTextNode(""); //Use a textnode as our placeholder
if (sibling) {
sibling.parentElement.insertBefore(this.element, sibling);
} else {
parent.appendChild(this.element);
}
} else {
parent = this.element.parentElement;
}
//If we already have elements we created, destroy them.
if (this.elements.length > 0) {
this.elements.forEach(item => item.remove());
this.elements = [];
}
//Construct all the elements for this population.
for (var i = 0; i < this.count; i++) {
var template = this.forEach(i);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
} else {
template.construct(this.element.parentElement, this.element);
}
this.elements.push(template.element);
}
}
onCountChange(newValue) {
IgniteRenderingContext.enter();
if (newValue != this.elements.length) {
//Remove all our existing elements.
this.elements.forEach(item => item.remove());
this.elements = [];
//Create new elements.
for (var i = 0; i < newValue; i++) {
var template = this.forEach(i);
if (this.elements.length > 0) {
template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
} else {
template.construct(this.element.parentElement, this.element);
}
this.elements.push(template.element);
}
//Set the new count
this.count = newValue;
}
IgniteRenderingContext.leave();
}
}
export {
IgniteTemplate,
div,
html,
list,
a,
input,
textarea,
button,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
span,
strong,
i,
ul,
li,
br,
img,
label,
select,
option,
script,
slot,
pagination,
population,
table,
tr,
th,
td,
tbody,
thead,
progress,
svg,
g,
path,
circle,
line,
form
};