rahisi
Version:
UI library for prototyping ideas for reactive programming.
371 lines • 12.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRef = (() => {
let id = 0; // possible collision
return () => `id_${id++}`;
})();
exports.mounted = "mounted";
exports.unmounted = "unmounted";
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((n) => n.dispatchEvent(new Event(exports.mounted)));
mutation.removedNodes.forEach((n) => n.dispatchEvent(new Event(exports.unmounted)));
}
});
});
document.addEventListener("DOMContentLoaded", () => observer.observe(document.body, {
attributes: false,
characterData: false,
childList: true,
subtree: true,
}), false);
class Notifier {
constructor() {
this.nextId = 0;
this.subscribers = new Map();
}
start() {
const notify = () => {
this.subscribers.forEach((v) => v());
window.requestAnimationFrame(notify);
};
window.requestAnimationFrame(notify);
}
subscribe(onNext, dependency) {
const currentId = this.nextId;
this.nextId++;
this.subscribers.set(currentId, onNext);
dependency.addEventListener(exports.unmounted, () => this.subscribers.delete(currentId));
}
}
class VersionedList {
constructor(items = new Array()) {
this.items = items;
this.nextKey = 0;
// tslint:disable-next-line:no-empty
this.addListener = () => { };
// tslint:disable-next-line:no-empty
this.removeListener = () => { };
}
getItems() {
return this.items.map((a) => a.value);
}
getItem(index) {
return this.items[index].value;
}
count() { return this.items.length; }
add(item) {
const val = { key: this.nextKey, value: item };
this.items.push(val);
this.nextKey++;
this.addListener([val]);
}
delete(itemIndex) {
const val = this.items[itemIndex];
this.items.splice(itemIndex, 1);
this.nextKey++;
this.removeListener([val]);
}
remove(item) {
this.delete(this.indexOf(item));
}
clear() {
const cleared = this.items.splice(0);
this.items.length = 0;
this.nextKey++;
this.removeListener(cleared);
}
indexOf(obj, fromIndex = 0) {
if (fromIndex < 0) {
fromIndex = Math.max(0, this.items.length + fromIndex);
}
for (let i = fromIndex, j = this.items.length; i < j; i++) {
if (this.items[i].value === obj) {
return i;
}
}
return -1;
}
forEach(action) {
this.getItems().forEach(action);
}
filter(filter) {
return this.getItems().filter(filter);
}
setListeners(addListener, removeListener) {
this.addListener = addListener;
this.removeListener = removeListener;
this.addListener(this.items);
}
}
exports.VersionedList = VersionedList;
class BaseElement {
constructor(elementName, attributes = new Array(), children = new Array()) {
this.elementName = elementName;
this.attributes = attributes;
this.children = children;
}
// factor out
mount(parent) {
const notifier = new Notifier();
const v = this.render(parent, notifier, false);
notifier.start();
return v;
}
render(parent, watch, isSvg) {
const useSvg = isSvg || this.elementName === "svg";
if (this.elementName == null) { // it's a fragment
const view = document.createDocumentFragment();
this.children.forEach((a) => a.render(view, watch, useSvg));
parent.appendChild(view);
return parent;
}
const view = useSvg ? document.createElementNS("http://www.w3.org/2000/svg", this.elementName) :
document.createElement(this.elementName);
this.attributes.forEach((a) => a.set(view, watch, useSvg));
this.children.forEach((a) => a.render(view, watch, useSvg));
parent.appendChild(view);
return view;
}
}
exports.BaseElement = BaseElement;
class ConditionalRenderElement {
constructor(source, def) {
this.source = source;
this.def = def;
this.currentNode = document.createTextNode("");
this.fallback = { test: () => true, renderable: def };
this.currentSource = source.find((a) => a.test()) || this.fallback;
}
mount(parent) {
const notifier = new Notifier();
const v = this.render(parent, notifier, false);
notifier.start();
return v;
}
render(parent, watch, isSvg) {
this.currentNode =
this.currentSource
.renderable()
.render(parent, watch, isSvg);
const gen = this.source;
watch.subscribe(() => {
const s = gen.find((a) => a.test());
if (this.currentSource !== s) {
this.currentSource = s || this.fallback;
const replacement = this.currentSource
.renderable()
.render(document.createDocumentFragment(), watch, isSvg);
parent.replaceChild(replacement, this.currentNode);
this.currentNode = replacement;
}
}, parent);
return this.currentNode;
}
}
exports.ConditionalRenderElement = ConditionalRenderElement;
class TemplateElement {
constructor(source, template, placeholder) {
this.source = source;
this.template = template;
this.placeholder = placeholder;
this.nodes = new Map();
this.currentValue = new VersionedList();
}
mount(parent) {
const notifier = new Notifier();
const v = this.render(parent, notifier, false);
notifier.start();
return v;
}
render(o, watch, isSvg) {
const placeholderNode = this.placeholder ? this.placeholder.render(document.createDocumentFragment(), watch, isSvg) : null;
const showPlaceHolder = () => {
if (!placeholderNode) {
return;
}
if (this.nodes.size === 0) {
const _ = placeholderNode.parentElement === o || o.appendChild(placeholderNode);
}
else {
const _ = placeholderNode.parentElement === o && o.removeChild(placeholderNode);
}
};
const subscribe = () => {
this.nodes.forEach((child, _) => o.removeChild(child));
this.nodes.clear();
this.currentValue.setListeners((items) => {
const fragment = document.createDocumentFragment();
items.forEach((i) => {
const child = this.template(i.value).render(fragment, watch, isSvg);
this.nodes.set(i.key, child);
});
o.appendChild(fragment);
showPlaceHolder();
}, (items) => {
items.forEach((i) => {
o.removeChild(this.nodes.get(i.key));
this.nodes.delete(i.key);
});
showPlaceHolder();
});
showPlaceHolder();
};
if (this.source instanceof VersionedList) {
this.currentValue = this.source;
subscribe();
}
else {
this.currentValue = this.source();
subscribe();
const gen = this.source;
watch.subscribe(() => {
const s = gen();
if (this.currentValue !== s) {
this.currentValue = s;
subscribe();
}
}, o);
}
return o;
}
}
exports.TemplateElement = TemplateElement;
class TextElement {
constructor(textContent) {
this.textContent = textContent;
this.currentValue = "";
}
mount(parent) {
const notifier = new Notifier();
const v = this.render(parent, notifier, false);
notifier.start();
return v;
}
render(parent, watch, _) {
const o = document.createTextNode("");
if (typeof this.textContent !== "function") {
this.currentValue = typeof this.textContent === "boolean" ? "" : this.textContent;
o.textContent = this.currentValue;
}
else {
const gen = this.textContent;
const getValue = () => typeof gen() === "boolean" ? "" : gen();
this.currentValue = getValue();
o.textContent = this.currentValue;
watch.subscribe(() => {
const s = getValue();
if (this.currentValue !== s) {
this.currentValue = s;
o.textContent = this.currentValue;
}
}, o);
}
parent.appendChild(o);
return o;
}
}
exports.TextElement = TextElement;
// xss via href
class NativeAttribute {
constructor(attribute, value) {
this.attribute = attribute;
this.value = value;
this.currentValue = "";
}
set(o, watch, isSvg) {
if (typeof this.value !== "function") {
this.currentValue = this.value;
NativeAttribute.setAttribute(this.attribute, o, this.currentValue, isSvg);
}
else {
this.currentValue = this.value();
NativeAttribute.setAttribute(this.attribute, o, this.currentValue, isSvg);
const gen = this.value;
watch.subscribe(() => {
const s = gen();
if (this.currentValue !== s) {
this.currentValue = s;
NativeAttribute.setAttribute(this.attribute, o, this.currentValue, isSvg);
}
}, o);
}
}
}
NativeAttribute.setAttribute = (attribute, element, value, isSvg) => {
if (attribute === "style") {
for (const key of Object.keys(value)) {
const style = value == null || value[key] == null ? "" : value[key];
if (key[0] === "-") {
element[attribute].setProperty(key, style);
}
else {
element[attribute][key] = style;
}
}
}
else if (attribute in element &&
attribute !== "list" &&
attribute !== "type" &&
attribute !== "draggable" &&
attribute !== "spellcheck" &&
attribute !== "translate" &&
!isSvg) {
element[attribute] = value == null ? "" : value;
}
else if (value != null && value !== false) {
element.setAttribute(attribute, value);
}
if (value == null || value === false) {
element.removeAttribute(attribute);
}
};
exports.NativeAttribute = NativeAttribute;
// lose focus when body is clicked
class FocusA {
constructor(focus) {
this.focus = focus;
this.currentValue = false;
}
set(o, watch) {
if (typeof this.focus !== "function") {
this.currentValue = this.focus;
if (this.currentValue) {
o.focus();
}
}
else {
this.currentValue = this.focus();
if (this.currentValue) {
o.focus();
}
const gen = this.focus;
watch.subscribe(() => {
const s = gen();
if (this.currentValue !== s) {
this.currentValue = s;
}
if (this.currentValue && document.activeElement !== o) {
o.focus();
}
}, o);
}
}
}
exports.FocusA = FocusA;
class OnHandlerA {
constructor(eventName, handler) {
this.eventName = eventName;
this.handler = handler;
}
set(o) {
o.addEventListener(this.eventName, this.handler);
}
}
exports.OnHandlerA = OnHandlerA;
exports.Template = (props) => {
const { source, template, placeholder } = props;
return new TemplateElement(source, template, placeholder || null); // no props
};
//# sourceMappingURL=index.js.map