awv3
Version:
⚡ AWV3 embedded CAD
211 lines (184 loc) • 7.26 kB
JavaScript
import omit from 'lodash/omit';
import flatten from 'lodash/flatten';
import Element from './element';
import shallowEqual from 'shallow-equal/objects';
import hash from 'string-hash';
let queue = Promise.resolve(), currentId, frames = window.f = { };
export function render(plugin, cb) {
requestAnimationFrame(() => {
queue = queue.then(() => {
currentId = plugin.id;
frames[currentId] && destroy(currentId);
frames[currentId] = {
stack: {},
stateStack: [],
order: 0,
plugin: plugin
}
plugin.addElement(renderNodes(plugin, prepare(cb())));
});
});
}
export function destroy(id) {
let frame = frames[id];
if (frame) {
Object.values(frame.stack).forEach(node => {
node.unsubscribes && node.unsubscribes.forEach(unsub => unsub());
node.component && node.component.componentWillUnmount && node.component.componentWillUnmount();
node.component = undefined;
node.element.destroy();
node.element = undefined;
node.children = undefined;
});
delete frames[id];
frame.plugin.destroyElements();
}
}
export function connect(selector) {
return DecoratedComponent =>
class extends Component {
state = {};
componentWillMount() {
let oldState = {};
let store = this.plugin.store;
this.unsubscribe = store.subscribe(() => {
let selectedState = selector(store.getState(), this.props);
let changedKeys;
Object.entries(selectedState).map(([key, value]) => {
if (oldState[key] !== value) {
changedKeys = { ...changedKeys, [key]: value };
oldState[key] = value;
}
});
changedKeys && this.setState(changedKeys);
});
this.state = { ...this.state, ...selector(store.getState(), this.props) };
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = { ...this.props, ...this.state };
return <DecoratedComponent {...props} />;
}
};
}
export function buildStack(tree, target) {
(Array.isArray(tree) ? tree : [tree]).forEach(child => {
child.el && !(child.el.prototype instanceof Element) && target.push(child);
child.children && buildStack(child.children, target);
});
}
export class Component {
constructor(props) {
this.id = currentId;
this.plugin = frames[this.id].plugin;
this.node;
this.props = props || {};
this.refs = {};
}
dispatch(action) {
this.plugin.store.dispatch(action);
}
setState(props) {
const frame = frames[this.id];
const newState = { ...this.state, ...props };
if (!shallowEqual(newState, this.state)) {
this.state = newState;
// Build component stack & construct virtual dom
currentId = this.id;
frame.order = this.node.order;
buildStack(this.node.children, frame.stateStack);
frame.stateStack = frame.stateStack.sort((a, b) => a.order - b.order);
this.node.children = prepare(this.render(this.node.props, this.state, this.setState.bind(this)), this.node.depth);
frame.stateStack = [];
renderNodes(this.plugin, this.node.children, this.node.parent, true);
}
}
}
export function prepare(tree, depth = [0]) {
if (Array.isArray(tree)) {
for (let i in tree) tree[i] = prepare(tree[i] || {}, [...depth, parseInt(i)]);
} else {
tree.depth = [...depth];
if (tree.children) tree.children = prepare(tree.children, depth);
}
return tree;
}
function renderNodes(plugin, node, parent, reconcile = false) {
const frame = frames[plugin.id];
if (Array.isArray(node)) {
return node.map(node => renderNodes(plugin, node, parent));
} else if (node.el) {
let tag = node.tag + '.' + node.depth.join('.');
let found = frame.stack[tag];
let target = found ? found : node;
let element = target.element;
parent = parent || target.parent;
let parentElem = (parent || {}).element;
//console.log(tag, node.props, ', found: ', !!found, ', frame: ', currentId);
if (!found) {
frame.stack[tag] = target;
}
target.parent = parent;
if (target.el.prototype instanceof Element) {
const update = {
...node.props,
children: renderNodes(plugin, node.children, target).filter(e => e).map(e => e.id)
};
if (found) {
target.element.update(update)
} else {
target.element = new target.el(frame.plugin, update);
}
target.unsubscribes && target.unsubscribes.forEach(unsub => unsub());
target.unsubscribes = target.handlers.map(handler => {
const name = handler.charAt(2).toLowerCase() + handler.substr(3);
return target.element.observe(state => state[name], (state, old) => node.events[handler](state, old));
});
} else {
target.element = renderNodes(plugin, node.children, parent);
}
return target.element;
}
}
const React = {
createElement(el, props, ...children) {
const frame = frames[currentId];
children = flatten(children);
if (el.prototype instanceof Element) {
const handlers = Object.keys(props || {}).filter(
key => typeof props[key] === 'function' && key.startsWith('on'),
);
const events = handlers.reduce((acc, val) => ({ ...acc, [val]: props[val] }), {});
return {
el,
tag: el.name,
props: omit(props, handlers),
handlers,
events,
children: children,
};
} else {
let state;
let trace = frame.stateStack.shift();
let node = { el, tag: hash(el.toString()), props: props || {}, order: frame.order++ };
//if (trace) console.log(' diff', trace.tag, node.tag);
if (trace && trace.el === node.el && trace.tag === node.tag) {
state = trace.component.state;
node.order = trace.order;
}
node.component = new el({ ...props, children });
node.component.state = { ...node.component.state, ...state };
node.component.node = node;
node.component.componentWillMount && node.component.componentWillMount();
node.children = node.component.render(
node.component.props,
node.component.state,
node.component.setState.bind(node.component),
);
return node;
}
},
};
export default React;