mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
Merge pull request #8521 from bvaughn/react-art-fiber
New fiber-based ReactART renderer
This commit is contained in:
@@ -15,15 +15,6 @@ src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
|
||||
src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
|
||||
* should give context for PropType errors in nested components.
|
||||
|
||||
src/renderers/art/__tests__/ReactART-test.js
|
||||
* should have the correct lifecycle state
|
||||
* should render a reasonable SVG structure in SVG mode
|
||||
* should be able to reorder components
|
||||
* should be able to reorder many components
|
||||
* renders composite with lifecycle inside group
|
||||
* resolves refs before componentDidMount
|
||||
* resolves refs before componentDidUpdate
|
||||
|
||||
src/renderers/dom/__tests__/ReactDOMProduction-test.js
|
||||
* should throw with an error code in production
|
||||
|
||||
|
||||
@@ -485,6 +485,16 @@ src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
|
||||
* should warn on invalid context types
|
||||
* should warn if getDefaultProps is specificed on the class
|
||||
|
||||
src/renderers/art/__tests__/ReactART-test.js
|
||||
* should have the correct lifecycle state
|
||||
* should render a reasonable SVG structure in SVG mode
|
||||
* should be able to reorder components
|
||||
* should be able to reorder many components
|
||||
* renders composite with lifecycle inside group
|
||||
* resolves refs before componentDidMount
|
||||
* resolves refs before componentDidUpdate
|
||||
* adds and updates event handlers
|
||||
|
||||
src/renderers/dom/__tests__/ReactDOMProduction-test.js
|
||||
* should use prod fbjs
|
||||
* should use prod React
|
||||
|
||||
@@ -11,631 +11,8 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
require('art/modes/current').setCurrent(
|
||||
require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
|
||||
);
|
||||
const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
|
||||
|
||||
const Transform = require('art/core/transform');
|
||||
const Mode = require('art/modes/current');
|
||||
|
||||
const React = require('React');
|
||||
const ReactDOM = require('ReactDOM');
|
||||
const ReactInstanceMap = require('ReactInstanceMap');
|
||||
const ReactMultiChild = require('ReactMultiChild');
|
||||
const ReactUpdates = require('ReactUpdates');
|
||||
|
||||
const emptyObject = require('emptyObject');
|
||||
const invariant = require('invariant');
|
||||
|
||||
const assign = require('object-assign');
|
||||
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++) {
|
||||
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 = 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 transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this.updateChildren,
|
||||
this,
|
||||
this.props.children,
|
||||
transaction,
|
||||
ReactInstanceMap.get(this)._context
|
||||
);
|
||||
ReactUpdates.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
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
|
||||
className={props.className}
|
||||
draggable={props.draggable}
|
||||
role={props.role}
|
||||
style={props.style}
|
||||
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, listener, 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 = 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,
|
||||
};
|
||||
module.exports = ReactDOMFeatureFlags.useFiber
|
||||
? require('ReactARTFiber')
|
||||
: require('ReactARTStack');
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* 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 ReactARTFiber
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
require('art/modes/current').setCurrent(
|
||||
// Change to 'art/modes/dom' for easier debugging via SVG
|
||||
require('art/modes/fast-noSideEffects'),
|
||||
);
|
||||
|
||||
const Mode = require('art/modes/current');
|
||||
const Transform = require('art/core/transform');
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const React = require('React');
|
||||
const ReactFiberReconciler = require('ReactFiberReconciler');
|
||||
|
||||
const { Component } = React;
|
||||
|
||||
const pooledTransform = new Transform();
|
||||
|
||||
const EVENT_TYPES = {
|
||||
onClick: 'click',
|
||||
onMouseMove: 'mousemove',
|
||||
onMouseOver: 'mouseover',
|
||||
onMouseOut: 'mouseout',
|
||||
onMouseUp: 'mouseup',
|
||||
onMouseDown: 'mousedown',
|
||||
};
|
||||
|
||||
const TYPES = {
|
||||
CLIPPING_RECTANGLE: 'ClippingRectangle',
|
||||
GROUP: 'Group',
|
||||
SHAPE: 'Shape',
|
||||
TEXT: 'Text',
|
||||
};
|
||||
|
||||
/** Helper Methods */
|
||||
|
||||
function addEventListeners(instance, type, listener) {
|
||||
// We need to explicitly unregister before unmount.
|
||||
// For this reason we need to track subscriptions.
|
||||
if (!instance._listeners) {
|
||||
instance._listeners = {};
|
||||
instance._subscriptions = {};
|
||||
}
|
||||
|
||||
instance._listeners[type] = listener;
|
||||
|
||||
if (listener) {
|
||||
if (!instance._subscriptions[type]) {
|
||||
instance._subscriptions[type] = instance.subscribe(
|
||||
type,
|
||||
createEventHandler(instance),
|
||||
instance,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (instance._subscriptions[type]) {
|
||||
instance._subscriptions[type]();
|
||||
delete instance._subscriptions[type];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function childrenAsString(children) {
|
||||
if (!children) {
|
||||
return '';
|
||||
} else if (typeof children === 'string') {
|
||||
return children;
|
||||
} else if (children.length) {
|
||||
return children.join('');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function createEventHandler(instance) {
|
||||
return function handleEvent(event) {
|
||||
const listener = instance._listeners[event.type];
|
||||
|
||||
if (!listener) {
|
||||
// Noop
|
||||
} else if (typeof listener === 'function') {
|
||||
listener.call(instance, event);
|
||||
} else if (listener.handleEvent) {
|
||||
listener.handleEvent(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function destroyEventListeners(instance) {
|
||||
if (instance._subscriptions) {
|
||||
for (let type in instance._subscriptions) {
|
||||
instance._subscriptions[type]();
|
||||
}
|
||||
}
|
||||
|
||||
instance._subscriptions = null;
|
||||
instance._listeners = null;
|
||||
}
|
||||
|
||||
function getScaleX(props) {
|
||||
if (props.scaleX != null) {
|
||||
return props.scaleX;
|
||||
} else if (props.scale != null) {
|
||||
return props.scale;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function getScaleY(props) {
|
||||
if (props.scaleY != null) {
|
||||
return props.scaleY;
|
||||
} else if (props.scale != null) {
|
||||
return props.scale;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function isSameFont(oldFont, newFont) {
|
||||
if (oldFont === newFont) {
|
||||
return true;
|
||||
} else if (
|
||||
typeof newFont === 'string' ||
|
||||
typeof oldFont === 'string'
|
||||
) {
|
||||
return false;
|
||||
} else {
|
||||
return (
|
||||
newFont.fontSize === oldFont.fontSize &&
|
||||
newFont.fontStyle === oldFont.fontStyle &&
|
||||
newFont.fontVariant === oldFont.fontVariant &&
|
||||
newFont.fontWeight === oldFont.fontWeight &&
|
||||
newFont.fontFamily === oldFont.fontFamily
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Render Methods */
|
||||
|
||||
function applyClippingRectangleProps(instance, props, prevProps = {}) {
|
||||
applyNodeProps(instance, props, prevProps);
|
||||
|
||||
instance.width = props.width;
|
||||
instance.height = props.height;
|
||||
}
|
||||
|
||||
function applyGroupProps(instance, props, prevProps = {}) {
|
||||
applyNodeProps(instance, props, prevProps);
|
||||
|
||||
instance.width = props.width;
|
||||
instance.height = props.height;
|
||||
}
|
||||
|
||||
function applyNodeProps(instance, props, prevProps = {}) {
|
||||
const scaleX = getScaleX(props);
|
||||
const scaleY = getScaleY(props);
|
||||
|
||||
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 (
|
||||
instance.xx !== pooledTransform.xx || instance.yx !== pooledTransform.yx ||
|
||||
instance.xy !== pooledTransform.xy || instance.yy !== pooledTransform.yy ||
|
||||
instance.x !== pooledTransform.x || instance.y !== pooledTransform.y
|
||||
) {
|
||||
instance.transformTo(pooledTransform);
|
||||
}
|
||||
|
||||
if (
|
||||
props.cursor !== prevProps.cursor ||
|
||||
props.title !== prevProps.title
|
||||
) {
|
||||
instance.indicate(props.cursor, props.title);
|
||||
}
|
||||
|
||||
if (
|
||||
instance.blend &&
|
||||
props.opacity !== prevProps.opacity
|
||||
) {
|
||||
instance.blend(props.opacity == null ? 1 : props.opacity);
|
||||
}
|
||||
|
||||
if (props.visible !== prevProps.visible) {
|
||||
if (props.visible == null || props.visible) {
|
||||
instance.show();
|
||||
} else {
|
||||
instance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
for (let type in EVENT_TYPES) {
|
||||
addEventListeners(instance, EVENT_TYPES[type], props[type]);
|
||||
}
|
||||
}
|
||||
|
||||
function applyRenderableNodeProps(instance, props, prevProps = {}) {
|
||||
applyNodeProps(instance, props, prevProps);
|
||||
|
||||
if (prevProps.fill !== props.fill) {
|
||||
if (props.fill && props.fill.applyFill) {
|
||||
props.fill.applyFill(instance);
|
||||
} else {
|
||||
instance.fill(props.fill);
|
||||
}
|
||||
}
|
||||
if (
|
||||
prevProps.stroke !== props.stroke ||
|
||||
prevProps.strokeWidth !== props.strokeWidth ||
|
||||
prevProps.strokeCap !== props.strokeCap ||
|
||||
prevProps.strokeJoin !== props.strokeJoin ||
|
||||
// TODO: Consider deep check of stokeDash; may benefit VML in IE.
|
||||
prevProps.strokeDash !== props.strokeDash
|
||||
) {
|
||||
instance.stroke(
|
||||
props.stroke,
|
||||
props.strokeWidth,
|
||||
props.strokeCap,
|
||||
props.strokeJoin,
|
||||
props.strokeDash,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function applyShapeProps(instance, props, prevProps = {}) {
|
||||
applyRenderableNodeProps(instance, props, prevProps);
|
||||
|
||||
const path = props.d || childrenAsString(props.children);
|
||||
|
||||
const prevDelta = instance._prevDelta;
|
||||
const prevPath = instance._prevPath;
|
||||
|
||||
if (
|
||||
path !== prevPath ||
|
||||
path.delta !== prevDelta ||
|
||||
prevProps.height !== props.height ||
|
||||
prevProps.width !== props.width
|
||||
) {
|
||||
instance.draw(
|
||||
path,
|
||||
props.width,
|
||||
props.height,
|
||||
);
|
||||
|
||||
instance._prevDelta = path.delta;
|
||||
instance._prevPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTextProps(instance, props, prevProps = {}) {
|
||||
applyRenderableNodeProps(instance, props, prevProps);
|
||||
|
||||
const string = childrenAsString(props.children);
|
||||
|
||||
if (
|
||||
instance._currentString !== string ||
|
||||
!isSameFont(props.font, prevProps.font) ||
|
||||
props.alignment !== prevProps.alignment ||
|
||||
props.path !== prevProps.path
|
||||
) {
|
||||
instance.draw(
|
||||
string,
|
||||
props.font,
|
||||
props.alignment,
|
||||
props.path,
|
||||
);
|
||||
|
||||
instance._currentString = string;
|
||||
}
|
||||
}
|
||||
|
||||
/** Declarative fill-type objects; API design not finalized */
|
||||
|
||||
const slice = Array.prototype.slice;
|
||||
|
||||
class LinearGradient {
|
||||
constructor(stops, x1, y1, x2, y2) {
|
||||
this._args = slice.call(arguments);
|
||||
}
|
||||
|
||||
applyFill(node) {
|
||||
node.fillLinear.apply(node, this._args);
|
||||
}
|
||||
}
|
||||
|
||||
class RadialGradient {
|
||||
constructor(stops, fx, fy, rx, ry, cx, cy) {
|
||||
this._args = slice.call(arguments);
|
||||
}
|
||||
|
||||
applyFill(node) {
|
||||
node.fillRadial.apply(node, this.args);
|
||||
}
|
||||
}
|
||||
|
||||
class Pattern {
|
||||
constructor(url, width, height, left, top) {
|
||||
this._args = slice.call(arguments);
|
||||
}
|
||||
|
||||
applyFill(node) {
|
||||
node.fillImage.apply(node, this.args);
|
||||
}
|
||||
}
|
||||
|
||||
/** React Components */
|
||||
|
||||
class Surface extends Component {
|
||||
componentDidMount() {
|
||||
const { height, width } = this.props;
|
||||
|
||||
this._surface = Mode.Surface(+width, +height, this._tagRef);
|
||||
|
||||
this._mountNode = ARTRenderer.mountContainer(
|
||||
this.props.children,
|
||||
this._surface,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const props = this.props;
|
||||
|
||||
if (
|
||||
props.height !== prevProps.height ||
|
||||
props.width !== prevProps.width
|
||||
) {
|
||||
this._surface.resize(+props.width, +props.height);
|
||||
}
|
||||
|
||||
ARTRenderer.updateContainer(
|
||||
this.props.children,
|
||||
this._mountNode,
|
||||
this,
|
||||
);
|
||||
|
||||
if (this._surface.render) {
|
||||
this._surface.render();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ARTRenderer.unmountContainer(this._mountNode);
|
||||
}
|
||||
|
||||
render() {
|
||||
// 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
|
||||
ref={ref => this._tagRef = ref}
|
||||
accessKey={props.accessKey}
|
||||
className={props.className}
|
||||
draggable={props.draggable}
|
||||
role={props.role}
|
||||
style={props.style}
|
||||
tabIndex={props.tabIndex}
|
||||
title={props.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** ART Renderer */
|
||||
|
||||
const ARTRenderer = ReactFiberReconciler({
|
||||
appendChild(parentInstance, child) {
|
||||
if (child.parentNode === parentInstance) {
|
||||
child.eject();
|
||||
}
|
||||
|
||||
child.inject(parentInstance);
|
||||
},
|
||||
|
||||
appendInitialChild(parentInstance, child) {
|
||||
if (typeof child === 'string') {
|
||||
// Noop for string children of Text (eg <Text>{'foo'}{'bar'}</Text>)
|
||||
return;
|
||||
}
|
||||
|
||||
child.inject(parentInstance);
|
||||
},
|
||||
|
||||
commitTextUpdate(textInstance, oldText, newText) {
|
||||
// Noop
|
||||
},
|
||||
|
||||
commitUpdate(instance, oldProps, newProps) {
|
||||
instance._applyProps(instance, newProps, oldProps);
|
||||
},
|
||||
|
||||
createInstance(type, props, internalInstanceHandle) {
|
||||
let instance;
|
||||
|
||||
switch (type) {
|
||||
case TYPES.CLIPPING_RECTANGLE:
|
||||
instance = Mode.ClippingRectangle();
|
||||
instance._applyProps = applyClippingRectangleProps;
|
||||
break;
|
||||
case TYPES.GROUP:
|
||||
instance = Mode.Group();
|
||||
instance._applyProps = applyGroupProps;
|
||||
break;
|
||||
case TYPES.SHAPE:
|
||||
instance = Mode.Shape();
|
||||
instance._applyProps = applyShapeProps;
|
||||
break;
|
||||
case TYPES.TEXT:
|
||||
instance = Mode.Text(
|
||||
childrenAsString(props.children),
|
||||
props.font,
|
||||
props.alignment,
|
||||
props.path,
|
||||
);
|
||||
instance._applyProps = applyTextProps;
|
||||
break;
|
||||
}
|
||||
|
||||
invariant(instance, 'ReactART does not support the type "%s"', type);
|
||||
|
||||
instance._applyProps(instance, props);
|
||||
|
||||
return instance;
|
||||
},
|
||||
|
||||
createTextInstance(text, internalInstanceHandle) {
|
||||
return text;
|
||||
},
|
||||
|
||||
finalizeInitialChildren(domElement, type, props) {
|
||||
// Noop
|
||||
},
|
||||
|
||||
insertBefore(parentInstance, child, beforeChild) {
|
||||
invariant(
|
||||
child !== beforeChild,
|
||||
'ReactART: Can not insert node before itself'
|
||||
);
|
||||
|
||||
child.injectBefore(beforeChild);
|
||||
},
|
||||
|
||||
prepareForCommit() {
|
||||
// Noop
|
||||
},
|
||||
|
||||
prepareUpdate(domElement, oldProps, newProps) {
|
||||
return true;
|
||||
},
|
||||
|
||||
removeChild(parentInstance, child) {
|
||||
destroyEventListeners(child);
|
||||
|
||||
child.eject();
|
||||
},
|
||||
|
||||
resetAfterCommit() {
|
||||
// Noop
|
||||
},
|
||||
|
||||
resetTextContent(domElement) {
|
||||
// Noop
|
||||
},
|
||||
|
||||
scheduleAnimationCallback: window.requestAnimationFrame,
|
||||
|
||||
scheduleDeferredCallback: window.requestIdleCallback,
|
||||
|
||||
shouldSetTextContent(props) {
|
||||
return (
|
||||
typeof props.children === 'string' ||
|
||||
typeof props.children === 'number'
|
||||
);
|
||||
},
|
||||
|
||||
useSyncScheduling: true,
|
||||
});
|
||||
|
||||
/** API */
|
||||
|
||||
module.exports = {
|
||||
ClippingRectangle: TYPES.CLIPPING_RECTANGLE,
|
||||
Group: TYPES.GROUP,
|
||||
LinearGradient,
|
||||
Path: Mode.Path,
|
||||
Pattern,
|
||||
RadialGradient,
|
||||
Shape: TYPES.SHAPE,
|
||||
Surface,
|
||||
Text: TYPES.TEXT,
|
||||
Transform,
|
||||
};
|
||||
@@ -0,0 +1,643 @@
|
||||
/**
|
||||
* 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 ReactDOM = require('ReactDOM');
|
||||
const ReactInstanceMap = require('ReactInstanceMap');
|
||||
const ReactMultiChild = require('ReactMultiChild');
|
||||
const ReactUpdates = require('ReactUpdates');
|
||||
|
||||
const emptyObject = require('emptyObject');
|
||||
const invariant = require('invariant');
|
||||
|
||||
const assign = require('object-assign');
|
||||
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++) {
|
||||
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 = 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 transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this.updateChildren,
|
||||
this,
|
||||
this.props.children,
|
||||
transaction,
|
||||
ReactInstanceMap.get(this)._context
|
||||
);
|
||||
ReactUpdates.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
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 = 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,
|
||||
};
|
||||
@@ -295,4 +295,33 @@ describe('ReactART', () => {
|
||||
expect(ref.constructor).toBe(CustomShape);
|
||||
});
|
||||
|
||||
it('adds and updates event handlers', () => {
|
||||
const container = document.createElement('div');
|
||||
|
||||
function render(onClick) {
|
||||
return ReactDOM.render(
|
||||
<Surface>
|
||||
<Shape onClick={onClick} />
|
||||
</Surface>,
|
||||
container,
|
||||
);
|
||||
}
|
||||
|
||||
function doClick(instance) {
|
||||
const path = ReactDOM.findDOMNode(instance).querySelector('path');
|
||||
|
||||
// ReactTestUtils.Simulate.click doesn't work with SVG elements
|
||||
path.click();
|
||||
}
|
||||
|
||||
const onClick1 = jest.fn();
|
||||
let instance = render(onClick1);
|
||||
doClick(instance);
|
||||
expect(onClick1).toBeCalled();
|
||||
|
||||
const onClick2 = jest.fn();
|
||||
instance = render(onClick2);
|
||||
doClick(instance);
|
||||
expect(onClick2).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user