awv3
Version:
⚡ AWV3 embedded CAD
275 lines (245 loc) • 10.3 kB
JavaScript
import omit from 'lodash/omit'
import flatten from 'lodash/flatten'
import isEqual from 'lodash/isEqual'
import Element from './element'
import shallowEqual from 'shallow-equal/objects'
import hash from 'string-hash'
let queue = Promise.resolve(),
currentId,
frames = {}
export class Component {
constructor(props) {
this.id = currentId
this.plugin = frames[this.id].plugin
this.node = undefined
this.props = props || {}
this.refs = {}
//console.log(" constructor", this.constructor.name, this.plugin.type, this.plugin.id)
}
dispatch(action) {
this.plugin.store.dispatch(action)
}
setState(state) {
const frame = frames[this.id]
const newState = { ...this.state, ...state }
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.reusable)
frame.reusable = frame.reusable.sort((a, b) => a.order - b.order)
this.node.children = prepare(this.render(this.props, this.state, this.setState.bind(this)), this.node.depth)
frame.reusable = []
if (this.node.children.isArray) {
let parent = this.node.children
while (parent && parent.isArray && parent.parent) parent = parent.parent
renderNodes(this.plugin, parent, parent.parent, true)
} else {
renderNodes(this.plugin, this.node.children, this.node.parent, true)
}
}
}
}
export function connect(selector) {
return DecoratedComponent =>
class ConnectedComponent 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 render(plugin, cb) {
queue = queue.then(() => {
currentId = plugin.id
//console.log("render plugin", plugin.type, plugin.id)
frames[currentId] && destroy(currentId)
frames[currentId] = {
stack: {},
reusable: [],
order: 0,
plugin: plugin,
}
plugin.addElement(renderNodes(plugin, prepare(cb())))
//console.log("finished rendering plugin", plugin.type, plugin.id)
})
}
export function destroy(id) {
let frame = frames[id]
if (frame) {
Object.keys(frame.stack).forEach(key => destroyNode(frame, frame.stack[key]))
frame.plugin.destroyElements()
}
}
function destroyNode(frame, node, traverse = true) {
if (Array.isArray(node)) return node.forEach(node => destroyNode(frame, node))
else if (node) {
node.unsubscribes && node.unsubscribes.forEach(unsub => unsub())
if (node.component) {
node.component.componentWillUnmount && node.component.componentWillUnmount()
node.component = undefined
}
if (node.element) {
const elements = Array.isArray(node.element) ? node.element : [node.element]
elements.forEach(element => {
let parent = node.parent
while (parent && !parent.element) parent = parent.parent
if (parent && parent.element)
parent.element.children = parent.element.children.filter(id => id != element.id)
element.destroy()
})
node.element = undefined
}
traverse && destroyNode(frame, node.children, traverse)
delete frame.stack[node.tag]
}
}
export function buildStack(tree, target) {
if (Array.isArray(tree)) return tree.forEach(node => buildStack(node, 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 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]
tree.tag = tree.hash + '.' + tree.depth.join('.')
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))
}
if (node && node.el) {
const found = frame.stack[node.tag]
const target = found ? found : node
const element = target.element
if (found && Array.isArray(node.children)) {
// Remove missing array items or items without key
const nodeArrays = node.children.filter(Array.isArray)
target.children.filter(Array.isArray).forEach((targetArray, index) => {
const nodeArray = nodeArrays[index]
targetArray.forEach((targetChild, index) => {
let test =
targetChild.props.key !== undefined &&
nodeArray.find(nodeChild => targetChild.props.key === nodeChild.props.key)
if (test === undefined) {
destroyNode(frame, frame.stack[targetChild.tag])
delete targetArray[index]
}
})
})
}
let flattenedChildren = Array.isArray(node.children) ? flatten(node.children) : node.children
parent = parent || target.parent
let parentElem = (parent || {}).element
if (!found) {
frame.stack[node.tag] = target
}
target.parent = parent
if (target.el.prototype instanceof Element) {
// Render children into Elements and remove missing items
// Items become undefined when they are toggled by ternary
const children = flatten(renderNodes(plugin, flattenedChildren, target))
for (let i = 0; i < children.length; i++) {
children[i] === undefined &&
target.children[i] &&
frame.stack[target.children[i].tag] &&
destroyNode(frame, frame.stack[target.children[i].tag])
}
const update = {
...node.props,
children: children.filter(e => e).map(e => e.id),
}
if (found) {
let newProps = Object.keys(update)
.filter(key => !isEqual(update[key], target.element[key]))
.reduce((acc, key) => ({ ...acc, [key]: update[key] }), {})
Object.keys(newProps).length > 0 && target.element.update(newProps)
} else {
target.element = new target.el(frame.plugin, update)
}
target.children = node.children
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 {
return renderNodes(plugin, flattenedChildren, parent)
}
return target.element
}
}
const React = {
createElement(el, props, ...children) {
const frame = frames[currentId]
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,
hash: el.name,
props: omit(props, handlers),
handlers,
events,
children: children,
}
} else {
let trace = frame.reusable.shift()
let node = { el, hash: hash(el.toString()), props: props || {}, order: frame.order++ }
if (
trace &&
trace.el === node.el &&
trace.hash === node.hash &&
trace.component.props.key === node.props.key
) {
node = trace
const newProps = { ...node.component.props, ...props, children }
node.component.componentWillReceiveProps && node.component.componentWillReceiveProps(newProps)
node.component.props = newProps
} else {
node.component = new el({ ...props, children })
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),
)
node.isArray = Array.isArray(node.children)
return node
}
},
}
export default React