diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 5be728bdf9e..8d705c30e83 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -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 diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 91873c59e11..f1c91f35171 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -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 diff --git a/src/renderers/art/ReactART.js b/src/renderers/art/ReactART.js index 0f102926e31..c8d3bebf979 100644 --- a/src/renderers/art/ReactART.js +++ b/src/renderers/art/ReactART.js @@ -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 ( - - ); - } - -}); - -// 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'); diff --git a/src/renderers/art/ReactARTFiber.js b/src/renderers/art/ReactARTFiber.js new file mode 100644 index 00000000000..467f9764634 --- /dev/null +++ b/src/renderers/art/ReactARTFiber.js @@ -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 ( + 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 {'foo'}{'bar'}) + 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, +}; diff --git a/src/renderers/art/ReactARTStack.js b/src/renderers/art/ReactARTStack.js new file mode 100644 index 00000000000..0c17c714739 --- /dev/null +++ b/src/renderers/art/ReactARTStack.js @@ -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 ( + + ); + } + +}); + +// 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, +}; diff --git a/src/renderers/art/__tests__/ReactART-test.js b/src/renderers/art/__tests__/ReactART-test.js index b8cfea1c24b..f628627adbb 100644 --- a/src/renderers/art/__tests__/ReactART-test.js +++ b/src/renderers/art/__tests__/ReactART-test.js @@ -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( + + + , + 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(); + }); });