Files
react/src/renderers/art/ReactARTStack.js
T
2017-03-31 16:53:23 +01:00

636 lines
17 KiB
JavaScript

/**
* Copyright (c) 2013-present Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactARTStack
*/
'use strict';
require('art/modes/current').setCurrent(
require('art/modes/fast-noSideEffects'), // Flip this to DOM mode for debugging
);
const Transform = require('art/core/transform');
const Mode = require('art/modes/current');
const React = require('react');
const ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
const ReactDOM = require('react-dom');
const ReactInstanceMap = require('ReactInstanceMap');
const ReactMultiChild = require('ReactMultiChild');
const ReactUpdates = require('ReactUpdates');
const emptyObject = require('fbjs/lib/emptyObject');
const invariant = require('fbjs/lib/invariant');
const pooledTransform = new Transform();
// Utilities
function childrenAsString(children) {
if (!children) {
return '';
}
if (typeof children === 'string') {
return children;
}
if (children.length) {
return children.join('\n');
}
return '';
}
function createComponent(name) {
const ReactARTComponent = function(element) {
this.node = null;
this.subscriptions = null;
this.listeners = null;
this._mountImage = null;
this._renderedChildren = null;
this.construct(element);
};
ReactARTComponent.displayName = name;
for (let i = 1, l = arguments.length; i < l; i++) {
Object.assign(ReactARTComponent.prototype, arguments[i]);
}
return ReactARTComponent;
}
/**
* Insert `node` into `parentNode` after `referenceNode`.
*/
function injectAfter(parentNode, referenceNode, node) {
let beforeNode;
if (
node.parentNode === parentNode && node.previousSibling === referenceNode
) {
return;
}
if (referenceNode == null) {
// node is supposed to be first.
beforeNode = parentNode.firstChild;
} else {
// node is supposed to be after referenceNode.
beforeNode = referenceNode.nextSibling;
}
if (beforeNode && beforeNode.previousSibling !== node) {
// Cases where `node === beforeNode` should get filtered out by earlier
// checks and the behavior isn't well-defined.
invariant(
node !== beforeNode,
'ReactART: Can not insert node before itself',
);
node.injectBefore(beforeNode);
} else if (node.parentNode !== parentNode) {
node.inject(parentNode);
}
}
// ContainerMixin for components that can hold ART nodes
const ContainerMixin = Object.assign({}, ReactMultiChild, {
/**
* Moves a child component to the supplied index.
*
* @param {ReactComponent} child Component to move.
* @param {number} toIndex Destination index of the element.
* @protected
*/
moveChild: function(child, afterNode, toIndex, lastIndex) {
const childNode = child._mountImage;
injectAfter(this.node, afterNode, childNode);
},
/**
* Creates a child component.
*
* @param {ReactComponent} child Component to create.
* @param {object} childNode ART node to insert.
* @protected
*/
createChild: function(child, afterNode, childNode) {
child._mountImage = childNode;
injectAfter(this.node, afterNode, childNode);
},
/**
* Removes a child component.
*
* @param {ReactComponent} child Child to remove.
* @protected
*/
removeChild: function(child) {
child._mountImage.eject();
child._mountImage = null;
},
updateChildrenAtRoot: function(nextChildren, transaction) {
this.updateChildren(nextChildren, transaction, emptyObject);
},
mountAndInjectChildrenAtRoot: function(children, transaction) {
this.mountAndInjectChildren(children, transaction, emptyObject);
},
/**
* Override to bypass batch updating because it is not necessary.
*
* @param {?object} nextChildren.
* @param {ReactReconcileTransaction} transaction
* @internal
* @override {ReactMultiChild.updateChildren}
*/
updateChildren: function(nextChildren, transaction, context) {
this._updateChildren(nextChildren, transaction, context);
},
// Shorthands
mountAndInjectChildren: function(children, transaction, context) {
const mountedImages = this.mountChildren(children, transaction, context);
// Each mount image corresponds to one of the flattened children
let i = 0;
for (let key in this._renderedChildren) {
if (this._renderedChildren.hasOwnProperty(key)) {
const child = this._renderedChildren[key];
child._mountImage = mountedImages[i];
mountedImages[i].inject(this.node);
i++;
}
}
},
});
// Surface is a React DOM Component, not an ART component. It serves as the
// entry point into the ART reconciler.
const Surface = React.createClass({
displayName: 'Surface',
mixins: [ContainerMixin],
componentDidMount: function() {
const domNode = ReactDOM.findDOMNode(this);
this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(
this.mountAndInjectChildren,
this,
this.props.children,
transaction,
ReactInstanceMap.get(this)._context,
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
},
componentDidUpdate: function(oldProps) {
const node = this.node;
if (
this.props.width != oldProps.width || this.props.height != oldProps.height
) {
node.resize(+this.props.width, +this.props.height);
}
const doUpdate = () => {
const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(
this.updateChildren,
this,
this.props.children,
transaction,
ReactInstanceMap.get(this)._context,
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
};
if (ReactDefaultBatchingStrategy.isBatchingUpdates) {
doUpdate();
} else {
ReactUpdates.batchedUpdates(doUpdate);
}
if (node.render) {
node.render();
}
},
componentWillUnmount: function() {
this.unmountChildren();
},
render: function() {
// This is going to be a placeholder because we don't know what it will
// actually resolve to because ART may render canvas, vml or svg tags here.
// We only allow a subset of properties since others might conflict with
// ART's properties.
const props = this.props;
// TODO: ART's Canvas Mode overrides surface title and cursor
const Tag = Mode.Surface.tagName;
return (
<Tag
accessKey={props.accessKey}
className={props.className}
draggable={props.draggable}
role={props.role}
style={props.style}
tabIndex={props.tabIndex}
title={props.title}
/>
);
},
});
// Various nodes that can go into a surface
const EventTypes = {
onMouseMove: 'mousemove',
onMouseOver: 'mouseover',
onMouseOut: 'mouseout',
onMouseUp: 'mouseup',
onMouseDown: 'mousedown',
onClick: 'click',
};
const NodeMixin = {
construct: function(element) {
this._currentElement = element;
},
getNativeNode: function() {
return this.node;
},
getPublicInstance: function() {
return this.node;
},
putEventListener: function(type, listener) {
const subscriptions = this.subscriptions || (this.subscriptions = {});
const listeners = this.listeners || (this.listeners = {});
listeners[type] = listener;
if (listener) {
if (!subscriptions[type]) {
subscriptions[type] = this.node.subscribe(type, this.handleEvent, this);
}
} else {
if (subscriptions[type]) {
subscriptions[type]();
delete subscriptions[type];
}
}
},
handleEvent: function(event) {
const listener = this.listeners[event.type];
if (!listener) {
return;
}
if (typeof listener === 'function') {
listener.call(this, event);
} else if (listener.handleEvent) {
listener.handleEvent(event);
}
},
destroyEventListeners: function() {
const subscriptions = this.subscriptions;
if (subscriptions) {
for (let type in subscriptions) {
subscriptions[type]();
}
}
this.subscriptions = null;
this.listeners = null;
},
applyNodeProps: function(oldProps, props) {
const node = this.node;
const scaleX = props.scaleX != null
? props.scaleX
: props.scale != null ? props.scale : 1;
const scaleY = props.scaleY != null
? props.scaleY
: props.scale != null ? props.scale : 1;
pooledTransform
.transformTo(1, 0, 0, 1, 0, 0)
.move(props.x || 0, props.y || 0)
.rotate(props.rotation || 0, props.originX, props.originY)
.scale(scaleX, scaleY, props.originX, props.originY);
if (props.transform != null) {
pooledTransform.transform(props.transform);
}
if (
node.xx !== pooledTransform.xx ||
node.yx !== pooledTransform.yx ||
node.xy !== pooledTransform.xy ||
node.yy !== pooledTransform.yy ||
node.x !== pooledTransform.x ||
node.y !== pooledTransform.y
) {
node.transformTo(pooledTransform);
}
if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
node.indicate(props.cursor, props.title);
}
if (node.blend && props.opacity !== oldProps.opacity) {
node.blend(props.opacity == null ? 1 : props.opacity);
}
if (props.visible !== oldProps.visible) {
if (props.visible == null || props.visible) {
node.show();
} else {
node.hide();
}
}
for (let type in EventTypes) {
this.putEventListener(EventTypes[type], props[type]);
}
},
mountComponentIntoNode: function(rootID, container) {
throw new Error(
'You cannot render an ART component standalone. ' +
'You need to wrap it in a Surface.',
);
},
};
// Group
const Group = createComponent('Group', NodeMixin, ContainerMixin, {
mountComponent: function(
transaction,
nativeParent,
nativeContainerInfo,
context,
) {
this.node = Mode.Group();
const props = this._currentElement.props;
this.applyGroupProps(emptyObject, props);
this.mountAndInjectChildren(props.children, transaction, context);
return this.node;
},
receiveComponent: function(nextComponent, transaction, context) {
const props = nextComponent.props;
const oldProps = this._currentElement.props;
this.applyGroupProps(oldProps, props);
this.updateChildren(props.children, transaction, context);
this._currentElement = nextComponent;
},
applyGroupProps: function(oldProps, props) {
this.node.width = props.width;
this.node.height = props.height;
this.applyNodeProps(oldProps, props);
},
unmountComponent: function() {
this.destroyEventListeners();
this.unmountChildren();
},
});
// ClippingRectangle
const ClippingRectangle = createComponent(
'ClippingRectangle',
NodeMixin,
ContainerMixin,
{
mountComponent: function(
transaction,
nativeParent,
nativeContainerInfo,
context,
) {
this.node = Mode.ClippingRectangle();
const props = this._currentElement.props;
this.applyClippingProps(emptyObject, props);
this.mountAndInjectChildren(props.children, transaction, context);
return this.node;
},
receiveComponent: function(nextComponent, transaction, context) {
const props = nextComponent.props;
const oldProps = this._currentElement.props;
this.applyClippingProps(oldProps, props);
this.updateChildren(props.children, transaction, context);
this._currentElement = nextComponent;
},
applyClippingProps: function(oldProps, props) {
this.node.width = props.width;
this.node.height = props.height;
this.node.x = props.x;
this.node.y = props.y;
this.applyNodeProps(oldProps, props);
},
unmountComponent: function() {
this.destroyEventListeners();
this.unmountChildren();
},
},
);
// Renderables
const RenderableMixin = Object.assign({}, NodeMixin, {
applyRenderableProps: function(oldProps, props) {
if (oldProps.fill !== props.fill) {
if (props.fill && props.fill.applyFill) {
props.fill.applyFill(this.node);
} else {
this.node.fill(props.fill);
}
}
if (
oldProps.stroke !== props.stroke ||
oldProps.strokeWidth !== props.strokeWidth ||
oldProps.strokeCap !== props.strokeCap ||
oldProps.strokeJoin !== props.strokeJoin ||
// TODO: Consider a deep check of stokeDash.
// This may benefit the VML version in IE.
oldProps.strokeDash !== props.strokeDash
) {
this.node.stroke(
props.stroke,
props.strokeWidth,
props.strokeCap,
props.strokeJoin,
props.strokeDash,
);
}
this.applyNodeProps(oldProps, props);
},
unmountComponent: function() {
this.destroyEventListeners();
},
});
// Shape
const Shape = createComponent('Shape', RenderableMixin, {
construct: function(element) {
this._currentElement = element;
this._oldDelta = null;
this._oldPath = null;
},
mountComponent: function(
transaction,
nativeParent,
nativeContainerInfo,
context,
) {
this.node = Mode.Shape();
const props = this._currentElement.props;
this.applyShapeProps(emptyObject, props);
return this.node;
},
receiveComponent: function(nextComponent, transaction, context) {
const props = nextComponent.props;
const oldProps = this._currentElement.props;
this.applyShapeProps(oldProps, props);
this._currentElement = nextComponent;
},
applyShapeProps: function(oldProps, props) {
const oldDelta = this._oldDelta;
const oldPath = this._oldPath;
const path = props.d || childrenAsString(props.children);
if (
path.delta !== oldDelta ||
path !== oldPath ||
oldProps.width !== props.width ||
oldProps.height !== props.height
) {
this.node.draw(path, props.width, props.height);
this._oldPath = path;
this._oldDelta = path.delta;
}
this.applyRenderableProps(oldProps, props);
},
});
// Text
const Text = createComponent('Text', RenderableMixin, {
construct: function(element) {
this._currentElement = element;
this._oldString = null;
},
mountComponent: function(
transaction,
nativeParent,
nativeContainerInfo,
context,
) {
const props = this._currentElement.props;
const newString = childrenAsString(props.children);
this.node = Mode.Text(newString, props.font, props.alignment, props.path);
this._oldString = newString;
this.applyRenderableProps(emptyObject, props);
return this.node;
},
isSameFont: function(oldFont, newFont) {
if (oldFont === newFont) {
return true;
}
if (typeof newFont === 'string' || typeof oldFont === 'string') {
return false;
}
return newFont.fontSize === oldFont.fontSize &&
newFont.fontStyle === oldFont.fontStyle &&
newFont.fontVariant === oldFont.fontVariant &&
newFont.fontWeight === oldFont.fontWeight &&
newFont.fontFamily === oldFont.fontFamily;
},
receiveComponent: function(nextComponent, transaction, context) {
const props = nextComponent.props;
const oldProps = this._currentElement.props;
const oldString = this._oldString;
const newString = childrenAsString(props.children);
if (
oldString !== newString ||
!this.isSameFont(oldProps.font, props.font) ||
oldProps.alignment !== props.alignment ||
oldProps.path !== props.path
) {
this.node.draw(newString, props.font, props.alignment, props.path);
this._oldString = newString;
}
this.applyRenderableProps(oldProps, props);
this._currentElement = nextComponent;
},
});
// Declarative fill type objects - API design not finalized
const slice = Array.prototype.slice;
function LinearGradient(stops, x1, y1, x2, y2) {
this.args = slice.call(arguments);
}
LinearGradient.prototype.applyFill = function(node) {
node.fillLinear.apply(node, this.args);
};
function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
this.args = slice.call(arguments);
}
RadialGradient.prototype.applyFill = function(node) {
node.fillRadial.apply(node, this.args);
};
function Pattern(url, width, height, left, top) {
this.args = slice.call(arguments);
}
Pattern.prototype.applyFill = function(node) {
node.fillImage.apply(node, this.args);
};
module.exports = {
ClippingRectangle,
Group,
LinearGradient,
Path: Mode.Path,
Pattern,
RadialGradient,
Shape,
Surface,
Text,
Transform,
};