hele
Version:
A front-end UI lib.
558 lines (546 loc) • 18.6 kB
JavaScript
class Reference {
constructor() {
this.current = undefined;
}
}
function isEqual(a, b) {
if (a === b || a !== a && b !== b) {
return true;
}
if ((!(a instanceof Object && b instanceof Object)) ||
String(a) !== String(b)) {
return false;
}
for (const key in a) {
if (!((key in b) && isEqual(a[key], b[key]))) {
return false;
}
}
for (const key in b) {
if (!(key in a)) {
return false;
}
}
return true;
}
function _clrChd(node, deep = false) {
[...node.childNodes].forEach(childNode => {
if (deep) {
_clrChd(childNode, true);
}
node.removeChild(childNode);
});
}
function _flatten(array) {
const ans = new Array();
array.forEach(ele => {
if (ele instanceof Array) {
ele.forEach(e => {
ans.push(e);
});
}
else {
ans.push(ele);
}
});
return ans;
}
function _copy(original) {
if (original instanceof Object) {
// @ts-ignore
return Object.create(original);
}
else {
return original;
}
}
const _isNorObj = (obj) => obj instanceof Object && String(obj) === '[object Object]';
const _eleMap = new Map();
const namespaces = new Map([
['svg', 'http://www.w3.org/2000/svg']
]);
const _nsCtxName = '_xmlns';
function _toEle(element) {
if (element instanceof HElement) {
return element;
}
else {
const type = typeof element;
if (type === 'string') {
return new HElement(null, { children: [element] });
}
else if (type === 'number') {
return new HElement(null, { children: [element.toString()] });
}
else if (element instanceof Array) {
return element.map(_toEle);
}
else {
return null;
}
}
}
function _toNode(element) {
if (element instanceof HElement) {
return element.toNode();
}
else if (element instanceof Array) {
return _flatten(element.map(_toNode));
}
else {
return document.createTextNode('');
}
}
class HElement {
constructor(type, props) {
this.type = type;
this.parent = undefined;
this.node = undefined;
this.context = undefined;
this.props = (type && typeof type !== 'string' && type.prototype instanceof Component) ?
Object.assign({}, type.defaultProps, props) :
props;
}
toNode() {
const { type, props } = this;
let node;
if (type === null) {
node = document.createTextNode(props.children[0]);
}
else if (typeof type === 'string') {
const context = _copy(this.context), xmlns = !props['no-xmlns'] &&
(props.xmlns || namespaces.get(type) || context[_nsCtxName]);
node = xmlns ?
document.createElementNS(xmlns, type) :
document.createElement(type);
if (xmlns) {
context[_nsCtxName] = xmlns;
}
_crtNode(props, node, context);
}
else {
const { element, component } = _getCom(type, props, _copy(this.context)), parsedElement = _toEle(element);
// @ts-ignore
if (type === Context) {
this.context = component.state;
}
if (parsedElement) {
const { context } = this;
_flatten([parsedElement]).forEach(ele => {
if (ele) {
ele.parent = this;
ele.context = _copy(context);
}
});
}
if (component) {
_eleMap.set(component, this);
}
try {
node = parsedElement instanceof Array ?
_flatten(parsedElement.map(_toNode)) :
_toNode(parsedElement);
if (component) {
component.onDidMount();
}
}
catch (error) {
node = document.createTextNode('');
if (component) {
component.onCaughtError(error);
}
else {
throw error;
}
}
}
return this.node = node;
}
}
function _upCom(component) {
const oldElement = _eleMap.get(component);
if (oldElement) {
const { node } = oldElement;
if (node) {
const { parent } = oldElement, nodes = _flatten([node]);
nodes.forEach((n, i) => {
const { parentNode } = n;
_clrChd(n, true);
if (parentNode) {
if (i === 0) {
const newElement = component.toElement();
if (newElement instanceof HElement) {
newElement.parent = parent;
newElement.context = oldElement.context;
_eleMap.set(component, newElement);
const newNode = newElement.toNode(), newNodes = _flatten([newNode]), fragment = document.createDocumentFragment();
if (parent) {
if (parent.node instanceof Array) {
nodes.forEach((n, i) => {
const index = parent.node.indexOf(n);
if (i === 0) {
parent.node.splice(index, 1, ...newNodes);
}
else {
parent.node.splice(index, 1);
}
});
}
else {
parent.node = newNode;
}
}
newNodes.forEach(child => {
fragment.appendChild(child);
});
parentNode.replaceChild(fragment, n);
}
else {
_eleMap.delete(component);
parentNode.removeChild(n);
}
}
else {
parentNode.removeChild(n);
}
}
});
}
}
}
const _expired = new Set();
const defaultTickMethod = callback => {
requestAnimationFrame(callback);
};
const Ticker = {
tickMethod: defaultTickMethod,
maxUpdateTime: 12,
maxClearTime: 3,
_willTick: false,
_tick() {
if (!Ticker._willTick) {
Ticker.tickMethod(() => {
Ticker._willTick = false;
const { maxUpdateTime, maxClearTime } = Ticker;
let startTime = Date.now();
_expired.forEach(component => {
if (Date.now() - startTime < maxUpdateTime) {
_expired.delete(component);
const { state, updateRequestCallbacks, _forceUp } = component, callbacks = updateRequestCallbacks.slice(0), callbackCount = callbacks.length;
let newState = _copy(state), t;
callbacks.forEach(callback => {
t = callback(newState);
if (t !== undefined) {
newState = t;
}
});
component.updateRequestCallbacks = updateRequestCallbacks.slice(callbackCount);
try {
if (_forceUp || component.shouldUpdate(state, newState)) {
component._forceUp = false;
const snapshot = component.onWillUpdate(state);
component.state = newState;
_upCom(component);
component.onDidUpdate(snapshot);
}
}
catch (error) {
component.onCaughtError(error);
}
}
});
if (_expired.size) {
Ticker._tick();
}
startTime = Date.now();
let hasElementDeleted = true;
while (hasElementDeleted && Date.now() - startTime < maxClearTime) {
hasElementDeleted = false;
_eleMap.forEach((element, component) => {
const { node } = element;
if (node) {
const tempNode = (node instanceof Array ? node[0] : node);
if (!tempNode || !tempNode.parentNode) {
hasElementDeleted = true;
try {
component.onWillUnmount();
_eleMap.delete(component);
component.onDidUnmount();
}
catch (error) {
component.onCaughtError(error);
}
}
}
});
}
});
Ticker._willTick = true;
}
},
_mark(component) {
_expired.add(component);
Ticker._tick();
}
};
class Component {
constructor(props, context) {
this.context = context;
this.state = {};
this.refs = new Map();
this.updateRequestCallbacks = new Array();
this._forceUp = false;
this.props = props;
}
toElement() {
this.refs.forEach(ref => {
ref.current = undefined;
});
try {
const result = this.render(), type = typeof result;
if (type === 'string') {
return new HElement(null, { children: [result] });
}
else if (type === 'number') {
return new HElement(null, { children: [result.toString()] });
}
else {
return result;
}
}
catch (error) {
this.onCaughtError(error);
return null;
}
}
onWillMount() { }
onDidMount() { }
shouldUpdate(oldState, newState) {
return !isEqual(oldState, newState);
}
// @ts-ignore
onWillUpdate(newState) { }
onDidUpdate(snapShot) { }
onWillUnmount() { }
onDidUnmount() { }
onCaughtError(error) {
throw error;
}
createRef(name) {
const { refs } = this;
if (refs.has(name)) {
return refs.get(name);
}
else {
const ref = new Reference();
refs.set(name, ref);
return ref;
}
}
requestUpdate(callback) {
this.updateRequestCallbacks.push(callback);
Ticker._mark(this);
return this;
}
update(newState) {
return this.requestUpdate(state => {
if (_isNorObj(newState) && _isNorObj(state)) {
Object.assign(state, newState);
}
else {
return newState;
}
});
}
forceUpdate(newState) {
this._forceUp = true;
if (arguments.length) {
// @ts-ignore
this.update(newState);
}
else {
Ticker._mark(this);
}
return this;
}
}
Component.defaultProps = {};
const Fragment = props => _flatten(props.children);
class Context extends Component {
constructor(props, context) {
super(props, context);
this.state = Object.assign({}, context, props.value);
}
render() {
return _flatten(this.props.children);
}
}
function render(node, root, deepClear = true) {
_clrChd(root, deepClear);
Ticker._tick();
// @ts-ignore
const { context } = root;
_flatten([node]).forEach(element => {
if (element instanceof HElement) {
element.context = context;
_flatten([element.toNode()]).forEach(child => {
root.appendChild(child);
});
}
else {
const type = typeof element;
if (type === 'string') {
root.appendChild(document.createTextNode(element));
}
else if (type === 'number') {
root.appendChild(document.createTextNode(element.toString()));
}
}
});
}
const specialNodePropProcessors = new Map([
['children', (children, node) => {
render(children, node, false);
}],
['style', (style, node) => {
if (!(node instanceof HTMLElement)) {
return;
}
if (style instanceof Object) {
for (const key in style) {
// @ts-ignore
node.style[key] = style[key];
}
}
else {
// @ts-ignore
node.style = style;
}
}],
['ref', (ref, node) => {
ref.current = node;
}],
['class', (classNames, node) => {
if ('setAttribute' in node) {
node.setAttribute('class', typeof classNames === 'string' ?
classNames :
classNames.filter(name => typeof name === 'string').join(' '));
}
}],
['no-xmlns', () => { }]
]);
const specialComponentPropProcessors = new Map([
['ref', (ref, component) => {
ref.current = component;
}]
]);
const specialFactoryPropProcessors = new Map([
['ref', (ref) => {
ref.current = undefined;
}]
]);
const eventPattern = /^on(\w+)$/i, captruePattern = /capture/i, nonpassivePattern = /nonpassive/i, oncePattern = /once/i;
function _getEName(rawEvent, useCapture, nonpassive, once) {
let t = 0;
if (useCapture) {
t += 7;
}
if (nonpassive) {
t += 7;
}
if (once) {
t += 4;
}
return t > 0 ? rawEvent.slice(0, -t) : rawEvent;
}
function _getEOpt(capture, nonpassive, once) {
return (!nonpassive && !capture && !once) ?
false :
{ capture, passive: !nonpassive, once };
}
function _crtNode(props, node, context) {
// @ts-ignore
node.context = context;
for (const key in props) {
const value = props[key], processor = specialNodePropProcessors.get(key);
if (processor) {
processor(value, node);
}
else if (key.match(eventPattern)) {
const rawEvent = RegExp.$1, capture = captruePattern.test(rawEvent), nonpassive = nonpassivePattern.test(rawEvent), once = oncePattern.test(rawEvent), event = _getEName(rawEvent, capture, nonpassive, once);
node.addEventListener(event, value, _getEOpt(capture, nonpassive, once));
}
else if (!(key in node) && ('setAttribute' in node)) {
node.setAttribute(key, value);
}
else {
try {
// @ts-ignore
node[key] = value;
}
catch (err) {
node.setAttribute(key, value);
}
}
}
}
function _getCom(componentGetter, props, context) {
const result = { element: null, component: null };
if (componentGetter.prototype instanceof Component) {
const component = result.component = new componentGetter(props, context);
for (const key in props) {
const processor = specialComponentPropProcessors.get(key);
if (processor) {
processor(props[key], component);
}
}
try {
component.onWillMount();
}
catch (error) {
component.onCaughtError(error);
}
result.element = component.toElement();
}
else {
for (const key in props) {
const processor = specialFactoryPropProcessors.get(key);
if (processor) {
processor(props[key], undefined);
}
}
const element = componentGetter(props, context);
result.element = element;
}
return result;
}
function createElement(type, props, ...children) {
return new HElement(type, {
...(props || {}),
children: _flatten(children)
});
}
class Portal extends Component {
constructor(props, context) {
super(props, context);
if (props.container) {
this.container = props.container;
}
else {
this.container = document.createElement('div');
document.body.appendChild(this.container);
}
}
render() {
return null;
}
onWillMount() {
render(createElement(Context, { value: this.context }, this.props.children), this.container, this.props.deepClear);
}
}
function createFactory(type) {
return function ComponentFactory(props, ...children) {
return createElement(type, props, ...children);
};
}
export { specialNodePropProcessors, specialComponentPropProcessors, specialFactoryPropProcessors, _getEName, _getEOpt, _crtNode, _getCom, isEqual, _clrChd, _flatten, _copy, _isNorObj, _upCom, _expired, defaultTickMethod, Ticker, Component, Fragment, Context, _eleMap, namespaces, _nsCtxName, _toEle, _toNode, HElement, Reference, render, Portal, createElement, createFactory };