@samitouri / QOS-React-2 / commits / 81cb216288

Imported new ReactART fiber renderer

Split ReactART into stack and fiber targets. Added new ReactART test case for recently-fixed bug.

Brian Vaughn committed Dec 7, 2016 at 12:26 UTC 81cb216288030cbdf1c0054255d636464bf43fb4
6 files changed +1201 -636
scripts/fiber/tests-failing.txt
-9
@@ -15,15 +15,6 @@ src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
15 src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
16 * should give context for PropType errors in nested components.
17
18 -src/renderers/art/__tests__/ReactART-test.js
19 -* should have the correct lifecycle state
20 -* should render a reasonable SVG structure in SVG mode
21 -* should be able to reorder components
22 -* should be able to reorder many components
23 -* renders composite with lifecycle inside group
24 -* resolves refs before componentDidMount
25 -* resolves refs before componentDidUpdate
26 -
18 src/renderers/dom/__tests__/ReactDOMProduction-test.js
19 * should throw with an error code in production
20
scripts/fiber/tests-passing.txt
+10
@@ -485,6 +485,16 @@ src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js
485 * should warn on invalid context types
486 * should warn if getDefaultProps is specificed on the class
487
488 +src/renderers/art/__tests__/ReactART-test.js
489 +* should have the correct lifecycle state
490 +* should render a reasonable SVG structure in SVG mode
491 +* should be able to reorder components
492 +* should be able to reorder many components
493 +* renders composite with lifecycle inside group
494 +* resolves refs before componentDidMount
495 +* resolves refs before componentDidUpdate
496 +* adds and updates event handlers
497 +
498 src/renderers/dom/__tests__/ReactDOMProduction-test.js
499 * should use prod fbjs
500 * should use prod React
src/renderers/art/ReactART.js
+4 -627
@@ -11,631 +11,8 @@
11
12 'use strict';
13
14 -require('art/modes/current').setCurrent(
15 - require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
16 -);
14 +const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15
18 -const Transform = require('art/core/transform');
19 -const Mode = require('art/modes/current');
20 -
21 -const React = require('React');
22 -const ReactDOM = require('ReactDOM');
23 -const ReactInstanceMap = require('ReactInstanceMap');
24 -const ReactMultiChild = require('ReactMultiChild');
25 -const ReactUpdates = require('ReactUpdates');
26 -
27 -const emptyObject = require('emptyObject');
28 -const invariant = require('invariant');
29 -
30 -const assign = require('object-assign');
31 -const pooledTransform = new Transform();
32 -
33 -// Utilities
34 -
35 -function childrenAsString(children) {
36 - if (!children) {
37 - return '';
38 - }
39 - if (typeof children === 'string') {
40 - return children;
41 - }
42 - if (children.length) {
43 - return children.join('\n');
44 - }
45 - return '';
46 -}
47 -
48 -function createComponent(name) {
49 - const ReactARTComponent = function(element) {
50 - this.node = null;
51 - this.subscriptions = null;
52 - this.listeners = null;
53 - this._mountImage = null;
54 - this._renderedChildren = null;
55 - this.construct(element);
56 - };
57 - ReactARTComponent.displayName = name;
58 - for (let i = 1, l = arguments.length; i < l; i++) {
59 - assign(ReactARTComponent.prototype, arguments[i]);
60 - }
61 -
62 - return ReactARTComponent;
63 -}
64 -
65 -/**
66 - * Insert `node` into `parentNode` after `referenceNode`.
67 - */
68 -function injectAfter(parentNode, referenceNode, node) {
69 - let beforeNode;
70 - if (node.parentNode === parentNode &&
71 - node.previousSibling === referenceNode) {
72 - return;
73 - }
74 - if (referenceNode == null) {
75 - // node is supposed to be first.
76 - beforeNode = parentNode.firstChild;
77 - } else {
78 - // node is supposed to be after referenceNode.
79 - beforeNode = referenceNode.nextSibling;
80 - }
81 - if (beforeNode && beforeNode.previousSibling !== node) {
82 - // Cases where `node === beforeNode` should get filtered out by earlier
83 - // checks and the behavior isn't well-defined.
84 - invariant(
85 - node !== beforeNode,
86 - 'ReactART: Can not insert node before itself'
87 - );
88 - node.injectBefore(beforeNode);
89 - } else if (node.parentNode !== parentNode) {
90 - node.inject(parentNode);
91 - }
92 -}
93 -
94 -// ContainerMixin for components that can hold ART nodes
95 -
96 -const ContainerMixin = assign({}, ReactMultiChild, {
97 -
98 - /**
99 - * Moves a child component to the supplied index.
100 - *
101 - * @param {ReactComponent} child Component to move.
102 - * @param {number} toIndex Destination index of the element.
103 - * @protected
104 - */
105 - moveChild: function(child, afterNode, toIndex, lastIndex) {
106 - const childNode = child._mountImage;
107 - injectAfter(this.node, afterNode, childNode);
108 - },
109 -
110 - /**
111 - * Creates a child component.
112 - *
113 - * @param {ReactComponent} child Component to create.
114 - * @param {object} childNode ART node to insert.
115 - * @protected
116 - */
117 - createChild: function(child, afterNode, childNode) {
118 - child._mountImage = childNode;
119 - injectAfter(this.node, afterNode, childNode);
120 - },
121 -
122 - /**
123 - * Removes a child component.
124 - *
125 - * @param {ReactComponent} child Child to remove.
126 - * @protected
127 - */
128 - removeChild: function(child) {
129 - child._mountImage.eject();
130 - child._mountImage = null;
131 - },
132 -
133 - updateChildrenAtRoot: function(nextChildren, transaction) {
134 - this.updateChildren(nextChildren, transaction, emptyObject);
135 - },
136 -
137 - mountAndInjectChildrenAtRoot: function(children, transaction) {
138 - this.mountAndInjectChildren(children, transaction, emptyObject);
139 - },
140 -
141 - /**
142 - * Override to bypass batch updating because it is not necessary.
143 - *
144 - * @param {?object} nextChildren.
145 - * @param {ReactReconcileTransaction} transaction
146 - * @internal
147 - * @override {ReactMultiChild.updateChildren}
148 - */
149 - updateChildren: function(nextChildren, transaction, context) {
150 - this._updateChildren(nextChildren, transaction, context);
151 - },
152 -
153 - // Shorthands
154 -
155 - mountAndInjectChildren: function(children, transaction, context) {
156 - const mountedImages = this.mountChildren(
157 - children,
158 - transaction,
159 - context
160 - );
161 - // Each mount image corresponds to one of the flattened children
162 - let i = 0;
163 - for (let key in this._renderedChildren) {
164 - if (this._renderedChildren.hasOwnProperty(key)) {
165 - const child = this._renderedChildren[key];
166 - child._mountImage = mountedImages[i];
167 - mountedImages[i].inject(this.node);
168 - i++;
169 - }
170 - }
171 - }
172 -
173 -});
174 -
175 -// Surface is a React DOM Component, not an ART component. It serves as the
176 -// entry point into the ART reconciler.
177 -
178 -const Surface = React.createClass({
179 -
180 - displayName: 'Surface',
181 -
182 - mixins: [ContainerMixin],
183 -
184 - componentDidMount: function() {
185 - const domNode = ReactDOM.findDOMNode(this);
186 -
187 - this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
188 -
189 - const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
190 - transaction.perform(
191 - this.mountAndInjectChildren,
192 - this,
193 - this.props.children,
194 - transaction,
195 - ReactInstanceMap.get(this)._context
196 - );
197 - ReactUpdates.ReactReconcileTransaction.release(transaction);
198 - },
199 -
200 - componentDidUpdate: function(oldProps) {
201 - const node = this.node;
202 - if (this.props.width != oldProps.width ||
203 - this.props.height != oldProps.height) {
204 - node.resize(+this.props.width, +this.props.height);
205 - }
206 -
207 - const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
208 - transaction.perform(
209 - this.updateChildren,
210 - this,
211 - this.props.children,
212 - transaction,
213 - ReactInstanceMap.get(this)._context
214 - );
215 - ReactUpdates.ReactReconcileTransaction.release(transaction);
216 -
217 - if (node.render) {
218 - node.render();
219 - }
220 - },
221 -
222 - componentWillUnmount: function() {
223 - this.unmountChildren();
224 - },
225 -
226 - render: function() {
227 - // This is going to be a placeholder because we don't know what it will
228 - // actually resolve to because ART may render canvas, vml or svg tags here.
229 - // We only allow a subset of properties since others might conflict with
230 - // ART's properties.
231 - const props = this.props;
232 -
233 - // TODO: ART's Canvas Mode overrides surface title and cursor
234 - const Tag = Mode.Surface.tagName;
235 - return (
236 - <Tag
237 - className={props.className}
238 - draggable={props.draggable}
239 - role={props.role}
240 - style={props.style}
241 - title={props.title}
242 - />
243 - );
244 - }
245 -
246 -});
247 -
248 -// Various nodes that can go into a surface
249 -
250 -const EventTypes = {
251 - onMouseMove: 'mousemove',
252 - onMouseOver: 'mouseover',
253 - onMouseOut: 'mouseout',
254 - onMouseUp: 'mouseup',
255 - onMouseDown: 'mousedown',
256 - onClick: 'click'
257 -};
258 -
259 -const NodeMixin = {
260 -
261 - construct: function(element) {
262 - this._currentElement = element;
263 - },
264 -
265 - getNativeNode: function() {
266 - return this.node;
267 - },
268 -
269 - getPublicInstance: function() {
270 - return this.node;
271 - },
272 -
273 - putEventListener: function(type, listener) {
274 - const subscriptions = this.subscriptions || (this.subscriptions = {});
275 - const listeners = this.listeners || (this.listeners = {});
276 - listeners[type] = listener;
277 - if (listener) {
278 - if (!subscriptions[type]) {
279 - subscriptions[type] = this.node.subscribe(type, listener, this);
280 - }
281 - } else {
282 - if (subscriptions[type]) {
283 - subscriptions[type]();
284 - delete subscriptions[type];
285 - }
286 - }
287 - },
288 -
289 - handleEvent: function(event) {
290 - const listener = this.listeners[event.type];
291 - if (!listener) {
292 - return;
293 - }
294 - if (typeof listener === 'function') {
295 - listener.call(this, event);
296 - } else if (listener.handleEvent) {
297 - listener.handleEvent(event);
298 - }
299 - },
300 -
301 - destroyEventListeners: function() {
302 - const subscriptions = this.subscriptions;
303 - if (subscriptions) {
304 - for (let type in subscriptions) {
305 - subscriptions[type]();
306 - }
307 - }
308 - this.subscriptions = null;
309 - this.listeners = null;
310 - },
311 -
312 - applyNodeProps: function(oldProps, props) {
313 - const node = this.node;
314 -
315 - const scaleX = props.scaleX != null ? props.scaleX :
316 - props.scale != null ? props.scale : 1;
317 - const scaleY = props.scaleY != null ? props.scaleY :
318 - props.scale != null ? props.scale : 1;
319 -
320 - pooledTransform
321 - .transformTo(1, 0, 0, 1, 0, 0)
322 - .move(props.x || 0, props.y || 0)
323 - .rotate(props.rotation || 0, props.originX, props.originY)
324 - .scale(scaleX, scaleY, props.originX, props.originY);
325 -
326 - if (props.transform != null) {
327 - pooledTransform.transform(props.transform);
328 - }
329 -
330 - if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx ||
331 - node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy ||
332 - node.x !== pooledTransform.x || node.y !== pooledTransform.y) {
333 - node.transformTo(pooledTransform);
334 - }
335 -
336 - if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
337 - node.indicate(props.cursor, props.title);
338 - }
339 -
340 - if (node.blend && props.opacity !== oldProps.opacity) {
341 - node.blend(props.opacity == null ? 1 : props.opacity);
342 - }
343 -
344 - if (props.visible !== oldProps.visible) {
345 - if (props.visible == null || props.visible) {
346 - node.show();
347 - } else {
348 - node.hide();
349 - }
350 - }
351 -
352 - for (let type in EventTypes) {
353 - this.putEventListener(EventTypes[type], props[type]);
354 - }
355 - },
356 -
357 - mountComponentIntoNode: function(rootID, container) {
358 - throw new Error(
359 - 'You cannot render an ART component standalone. ' +
360 - 'You need to wrap it in a Surface.'
361 - );
362 - }
363 -
364 -};
365 -
366 -// Group
367 -
368 -const Group = createComponent('Group', NodeMixin, ContainerMixin, {
369 -
370 - mountComponent: function(
371 - transaction,
372 - nativeParent,
373 - nativeContainerInfo,
374 - context
375 - ) {
376 - this.node = Mode.Group();
377 - const props = this._currentElement.props;
378 - this.applyGroupProps(emptyObject, props);
379 - this.mountAndInjectChildren(props.children, transaction, context);
380 - return this.node;
381 - },
382 -
383 - receiveComponent: function(nextComponent, transaction, context) {
384 - const props = nextComponent.props;
385 - const oldProps = this._currentElement.props;
386 - this.applyGroupProps(oldProps, props);
387 - this.updateChildren(props.children, transaction, context);
388 - this._currentElement = nextComponent;
389 - },
390 -
391 - applyGroupProps: function(oldProps, props) {
392 - this.node.width = props.width;
393 - this.node.height = props.height;
394 - this.applyNodeProps(oldProps, props);
395 - },
396 -
397 - unmountComponent: function() {
398 - this.destroyEventListeners();
399 - this.unmountChildren();
400 - }
401 -
402 -});
403 -
404 -// ClippingRectangle
405 -const ClippingRectangle = createComponent(
406 - 'ClippingRectangle', NodeMixin, ContainerMixin, {
407 -
408 - mountComponent: function(
409 - transaction,
410 - nativeParent,
411 - nativeContainerInfo,
412 - context
413 - ) {
414 - this.node = Mode.ClippingRectangle();
415 - const props = this._currentElement.props;
416 - this.applyClippingProps(emptyObject, props);
417 - this.mountAndInjectChildren(props.children, transaction, context);
418 - return this.node;
419 - },
420 -
421 - receiveComponent: function(nextComponent, transaction, context) {
422 - const props = nextComponent.props;
423 - const oldProps = this._currentElement.props;
424 - this.applyClippingProps(oldProps, props);
425 - this.updateChildren(props.children, transaction, context);
426 - this._currentElement = nextComponent;
427 - },
428 -
429 - applyClippingProps: function(oldProps, props) {
430 - this.node.width = props.width;
431 - this.node.height = props.height;
432 - this.node.x = props.x;
433 - this.node.y = props.y;
434 - this.applyNodeProps(oldProps, props);
435 - },
436 -
437 - unmountComponent: function() {
438 - this.destroyEventListeners();
439 - this.unmountChildren();
440 - }
441 -
442 -});
443 -
444 -
445 -// Renderables
446 -
447 -const RenderableMixin = assign({}, NodeMixin, {
448 -
449 - applyRenderableProps: function(oldProps, props) {
450 - if (oldProps.fill !== props.fill) {
451 - if (props.fill && props.fill.applyFill) {
452 - props.fill.applyFill(this.node);
453 - } else {
454 - this.node.fill(props.fill);
455 - }
456 - }
457 - if (
458 - oldProps.stroke !== props.stroke ||
459 - oldProps.strokeWidth !== props.strokeWidth ||
460 - oldProps.strokeCap !== props.strokeCap ||
461 - oldProps.strokeJoin !== props.strokeJoin ||
462 - // TODO: Consider a deep check of stokeDash.
463 - // This may benefit the VML version in IE.
464 - oldProps.strokeDash !== props.strokeDash
465 - ) {
466 - this.node.stroke(
467 - props.stroke,
468 - props.strokeWidth,
469 - props.strokeCap,
470 - props.strokeJoin,
471 - props.strokeDash
472 - );
473 - }
474 - this.applyNodeProps(oldProps, props);
475 - },
476 -
477 - unmountComponent: function() {
478 - this.destroyEventListeners();
479 - }
480 -
481 -});
482 -
483 -// Shape
484 -
485 -const Shape = createComponent('Shape', RenderableMixin, {
486 -
487 - construct: function(element) {
488 - this._currentElement = element;
489 - this._oldDelta = null;
490 - this._oldPath = null;
491 - },
492 -
493 - mountComponent: function(
494 - transaction,
495 - nativeParent,
496 - nativeContainerInfo,
497 - context
498 - ) {
499 - this.node = Mode.Shape();
500 - const props = this._currentElement.props;
501 - this.applyShapeProps(emptyObject, props);
502 - return this.node;
503 - },
504 -
505 - receiveComponent: function(nextComponent, transaction, context) {
506 - const props = nextComponent.props;
507 - const oldProps = this._currentElement.props;
508 - this.applyShapeProps(oldProps, props);
509 - this._currentElement = nextComponent;
510 - },
511 -
512 - applyShapeProps: function(oldProps, props) {
513 - const oldDelta = this._oldDelta;
514 - const oldPath = this._oldPath;
515 - const path = props.d || childrenAsString(props.children);
516 -
517 - if (path.delta !== oldDelta ||
518 - path !== oldPath ||
519 - oldProps.width !== props.width ||
520 - oldProps.height !== props.height) {
521 -
522 - this.node.draw(
523 - path,
524 - props.width,
525 - props.height
526 - );
527 -
528 - this._oldPath = path;
529 - this._oldDelta = path.delta;
530 - }
531 -
532 - this.applyRenderableProps(oldProps, props);
533 - }
534 -
535 -});
536 -
537 -// Text
538 -
539 -const Text = createComponent('Text', RenderableMixin, {
540 -
541 - construct: function(element) {
542 - this._currentElement = element;
543 - this._oldString = null;
544 - },
545 -
546 - mountComponent: function(
547 - transaction,
548 - nativeParent,
549 - nativeContainerInfo,
550 - context
551 - ) {
552 - const props = this._currentElement.props;
553 - const newString = childrenAsString(props.children);
554 - this.node = Mode.Text(newString, props.font, props.alignment, props.path);
555 - this._oldString = newString;
556 - this.applyRenderableProps(emptyObject, props);
557 - return this.node;
558 - },
559 -
560 - isSameFont: function(oldFont, newFont) {
561 - if (oldFont === newFont) {
562 - return true;
563 - }
564 - if (typeof newFont === 'string' || typeof oldFont === 'string') {
565 - return false;
566 - }
567 - return (
568 - newFont.fontSize === oldFont.fontSize &&
569 - newFont.fontStyle === oldFont.fontStyle &&
570 - newFont.fontVariant === oldFont.fontVariant &&
571 - newFont.fontWeight === oldFont.fontWeight &&
572 - newFont.fontFamily === oldFont.fontFamily
573 - );
574 - },
575 -
576 - receiveComponent: function(nextComponent, transaction, context) {
577 - const props = nextComponent.props;
578 - const oldProps = this._currentElement.props;
579 -
580 - const oldString = this._oldString;
581 - const newString = childrenAsString(props.children);
582 -
583 - if (oldString !== newString ||
584 - !this.isSameFont(oldProps.font, props.font) ||
585 - oldProps.alignment !== props.alignment ||
586 - oldProps.path !== props.path) {
587 - this.node.draw(
588 - newString,
589 - props.font,
590 - props.alignment,
591 - props.path
592 - );
593 - this._oldString = newString;
594 - }
595 -
596 - this.applyRenderableProps(oldProps, props);
597 - this._currentElement = nextComponent;
598 - }
599 -
600 -});
601 -
602 -// Declarative fill type objects - API design not finalized
603 -
604 -const slice = Array.prototype.slice;
605 -
606 -function LinearGradient(stops, x1, y1, x2, y2) {
607 - this.args = slice.call(arguments);
608 -}
609 -
610 -LinearGradient.prototype.applyFill = function(node) {
611 - node.fillLinear.apply(node, this.args);
612 -};
613 -
614 -function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
615 - this.args = slice.call(arguments);
616 -}
617 -
618 -RadialGradient.prototype.applyFill = function(node) {
619 - node.fillRadial.apply(node, this.args);
620 -};
621 -
622 -function Pattern(url, width, height, left, top) {
623 - this.args = slice.call(arguments);
624 -}
625 -
626 -Pattern.prototype.applyFill = function(node) {
627 - node.fillImage.apply(node, this.args);
628 -};
629 -
630 -module.exports = {
631 - ClippingRectangle,
632 - Group,
633 - LinearGradient,
634 - Path: Mode.Path,
635 - Pattern,
636 - RadialGradient,
637 - Shape,
638 - Surface,
639 - Text,
640 - Transform,
641 -};
16 +module.exports = ReactDOMFeatureFlags.useFiber
17 + ? require('ReactARTFiber')
18 + : require('ReactARTStack');
src/renderers/art/ReactARTFiber.js new
+515
@@ -0,0 +1,515 @@
1 +/**
2 + * Copyright (c) 2013-present Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactARTFiber
10 + */
11 +'use strict';
12 +
13 +require('art/modes/current').setCurrent(
14 + // Change to 'art/modes/dom' for easier debugging via SVG
15 + require('art/modes/fast-noSideEffects'),
16 +);
17 +
18 +const Mode = require('art/modes/current');
19 +const Transform = require('art/core/transform');
20 +const invariant = require('fbjs/lib/invariant');
21 +const React = require('React');
22 +const ReactFiberReconciler = require('ReactFiberReconciler');
23 +
24 +const { Component } = React;
25 +
26 +const pooledTransform = new Transform();
27 +
28 +const EVENT_TYPES = {
29 + onClick: 'click',
30 + onMouseMove: 'mousemove',
31 + onMouseOver: 'mouseover',
32 + onMouseOut: 'mouseout',
33 + onMouseUp: 'mouseup',
34 + onMouseDown: 'mousedown',
35 +};
36 +
37 +const TYPES = {
38 + CLIPPING_RECTANGLE: 'ClippingRectangle',
39 + GROUP: 'Group',
40 + SHAPE: 'Shape',
41 + TEXT: 'Text',
42 +};
43 +
44 +/** Helper Methods */
45 +
46 +function addEventListeners(instance, type, listener) {
47 + // We need to explicitly unregister before unmount.
48 + // For this reason we need to track subscriptions.
49 + if (!instance._listeners) {
50 + instance._listeners = {};
51 + instance._subscriptions = {};
52 + }
53 +
54 + instance._listeners[type] = listener;
55 +
56 + if (listener) {
57 + if (!instance._subscriptions[type]) {
58 + instance._subscriptions[type] = instance.subscribe(
59 + type,
60 + createEventHandler(instance),
61 + instance,
62 + );
63 + }
64 + } else {
65 + if (instance._subscriptions[type]) {
66 + instance._subscriptions[type]();
67 + delete instance._subscriptions[type];
68 + }
69 + }
70 +}
71 +
72 +function childrenAsString(children) {
73 + if (!children) {
74 + return '';
75 + } else if (typeof children === 'string') {
76 + return children;
77 + } else if (children.length) {
78 + return children.join('');
79 + } else {
80 + return '';
81 + }
82 +}
83 +
84 +function createEventHandler(instance) {
85 + return function handleEvent(event) {
86 + const listener = instance._listeners[event.type];
87 +
88 + if (!listener) {
89 + // Noop
90 + } else if (typeof listener === 'function') {
91 + listener.call(instance, event);
92 + } else if (listener.handleEvent) {
93 + listener.handleEvent(event);
94 + }
95 + };
96 +}
97 +
98 +function destroyEventListeners(instance) {
99 + if (instance._subscriptions) {
100 + for (let type in instance._subscriptions) {
101 + instance._subscriptions[type]();
102 + }
103 + }
104 +
105 + instance._subscriptions = null;
106 + instance._listeners = null;
107 +}
108 +
109 +function getScaleX(props) {
110 + if (props.scaleX != null) {
111 + return props.scaleX;
112 + } else if (props.scale != null) {
113 + return props.scale;
114 + } else {
115 + return 1;
116 + }
117 +}
118 +
119 +function getScaleY(props) {
120 + if (props.scaleY != null) {
121 + return props.scaleY;
122 + } else if (props.scale != null) {
123 + return props.scale;
124 + } else {
125 + return 1;
126 + }
127 +}
128 +
129 +function isSameFont(oldFont, newFont) {
130 + if (oldFont === newFont) {
131 + return true;
132 + } else if (
133 + typeof newFont === 'string' ||
134 + typeof oldFont === 'string'
135 + ) {
136 + return false;
137 + } else {
138 + return (
139 + newFont.fontSize === oldFont.fontSize &&
140 + newFont.fontStyle === oldFont.fontStyle &&
141 + newFont.fontVariant === oldFont.fontVariant &&
142 + newFont.fontWeight === oldFont.fontWeight &&
143 + newFont.fontFamily === oldFont.fontFamily
144 + );
145 + }
146 +}
147 +
148 +/** Render Methods */
149 +
150 +function applyClippingRectangleProps(instance, props, prevProps = {}) {
151 + applyNodeProps(instance, props, prevProps);
152 +
153 + instance.width = props.width;
154 + instance.height = props.height;
155 +}
156 +
157 +function applyGroupProps(instance, props, prevProps = {}) {
158 + applyNodeProps(instance, props, prevProps);
159 +
160 + instance.width = props.width;
161 + instance.height = props.height;
162 +}
163 +
164 +function applyNodeProps(instance, props, prevProps = {}) {
165 + const scaleX = getScaleX(props);
166 + const scaleY = getScaleY(props);
167 +
168 + pooledTransform
169 + .transformTo(1, 0, 0, 1, 0, 0)
170 + .move(props.x || 0, props.y || 0)
171 + .rotate(props.rotation || 0, props.originX, props.originY)
172 + .scale(scaleX, scaleY, props.originX, props.originY);
173 +
174 + if (props.transform != null) {
175 + pooledTransform.transform(props.transform);
176 + }
177 +
178 + if (
179 + instance.xx !== pooledTransform.xx || instance.yx !== pooledTransform.yx ||
180 + instance.xy !== pooledTransform.xy || instance.yy !== pooledTransform.yy ||
181 + instance.x !== pooledTransform.x || instance.y !== pooledTransform.y
182 + ) {
183 + instance.transformTo(pooledTransform);
184 + }
185 +
186 + if (
187 + props.cursor !== prevProps.cursor ||
188 + props.title !== prevProps.title
189 + ) {
190 + instance.indicate(props.cursor, props.title);
191 + }
192 +
193 + if (
194 + instance.blend &&
195 + props.opacity !== prevProps.opacity
196 + ) {
197 + instance.blend(props.opacity == null ? 1 : props.opacity);
198 + }
199 +
200 + if (props.visible !== prevProps.visible) {
201 + if (props.visible == null || props.visible) {
202 + instance.show();
203 + } else {
204 + instance.hide();
205 + }
206 + }
207 +
208 + for (let type in EVENT_TYPES) {
209 + addEventListeners(instance, EVENT_TYPES[type], props[type]);
210 + }
211 +}
212 +
213 +function applyRenderableNodeProps(instance, props, prevProps = {}) {
214 + applyNodeProps(instance, props, prevProps);
215 +
216 + if (prevProps.fill !== props.fill) {
217 + if (props.fill && props.fill.applyFill) {
218 + props.fill.applyFill(instance);
219 + } else {
220 + instance.fill(props.fill);
221 + }
222 + }
223 + if (
224 + prevProps.stroke !== props.stroke ||
225 + prevProps.strokeWidth !== props.strokeWidth ||
226 + prevProps.strokeCap !== props.strokeCap ||
227 + prevProps.strokeJoin !== props.strokeJoin ||
228 + // TODO: Consider deep check of stokeDash; may benefit VML in IE.
229 + prevProps.strokeDash !== props.strokeDash
230 + ) {
231 + instance.stroke(
232 + props.stroke,
233 + props.strokeWidth,
234 + props.strokeCap,
235 + props.strokeJoin,
236 + props.strokeDash,
237 + );
238 + }
239 +}
240 +
241 +function applyShapeProps(instance, props, prevProps = {}) {
242 + applyRenderableNodeProps(instance, props, prevProps);
243 +
244 + const path = props.d || childrenAsString(props.children);
245 +
246 + const prevDelta = instance._prevDelta;
247 + const prevPath = instance._prevPath;
248 +
249 + if (
250 + path !== prevPath ||
251 + path.delta !== prevDelta ||
252 + prevProps.height !== props.height ||
253 + prevProps.width !== props.width
254 + ) {
255 + instance.draw(
256 + path,
257 + props.width,
258 + props.height,
259 + );
260 +
261 + instance._prevDelta = path.delta;
262 + instance._prevPath = path;
263 + }
264 +}
265 +
266 +function applyTextProps(instance, props, prevProps = {}) {
267 + applyRenderableNodeProps(instance, props, prevProps);
268 +
269 + const string = childrenAsString(props.children);
270 +
271 + if (
272 + instance._currentString !== string ||
273 + !isSameFont(props.font, prevProps.font) ||
274 + props.alignment !== prevProps.alignment ||
275 + props.path !== prevProps.path
276 + ) {
277 + instance.draw(
278 + string,
279 + props.font,
280 + props.alignment,
281 + props.path,
282 + );
283 +
284 + instance._currentString = string;
285 + }
286 +}
287 +
288 +/** Declarative fill-type objects; API design not finalized */
289 +
290 +const slice = Array.prototype.slice;
291 +
292 +class LinearGradient {
293 + constructor(stops, x1, y1, x2, y2) {
294 + this._args = slice.call(arguments);
295 + }
296 +
297 + applyFill(node) {
298 + node.fillLinear.apply(node, this._args);
299 + }
300 +}
301 +
302 +class RadialGradient {
303 + constructor(stops, fx, fy, rx, ry, cx, cy) {
304 + this._args = slice.call(arguments);
305 + }
306 +
307 + applyFill(node) {
308 + node.fillRadial.apply(node, this.args);
309 + }
310 +}
311 +
312 +class Pattern {
313 + constructor(url, width, height, left, top) {
314 + this._args = slice.call(arguments);
315 + }
316 +
317 + applyFill(node) {
318 + node.fillImage.apply(node, this.args);
319 + }
320 +}
321 +
322 +/** React Components */
323 +
324 +class Surface extends Component {
325 + componentDidMount() {
326 + const { height, width } = this.props;
327 +
328 + this._surface = Mode.Surface(+width, +height, this._tagRef);
329 +
330 + this._mountNode = ARTRenderer.mountContainer(
331 + this.props.children,
332 + this._surface,
333 + this,
334 + );
335 + }
336 +
337 + componentDidUpdate(prevProps, prevState) {
338 + const props = this.props;
339 +
340 + if (
341 + props.height !== prevProps.height ||
342 + props.width !== prevProps.width
343 + ) {
344 + this._surface.resize(+props.width, +props.height);
345 + }
346 +
347 + ARTRenderer.updateContainer(
348 + this.props.children,
349 + this._mountNode,
350 + this,
351 + );
352 +
353 + if (this._surface.render) {
354 + this._surface.render();
355 + }
356 + }
357 +
358 + componentWillUnmount() {
359 + ARTRenderer.unmountContainer(this._mountNode);
360 + }
361 +
362 + render() {
363 + // This is going to be a placeholder because we don't know what it will
364 + // actually resolve to because ART may render canvas, vml or svg tags here.
365 + // We only allow a subset of properties since others might conflict with
366 + // ART's properties.
367 + const props = this.props;
368 +
369 + // TODO: ART's Canvas Mode overrides surface title and cursor
370 + const Tag = Mode.Surface.tagName;
371 +
372 + return (
373 + <Tag
374 + ref={ref => this._tagRef = ref}
375 + accessKey={props.accessKey}
376 + className={props.className}
377 + draggable={props.draggable}
378 + role={props.role}
379 + style={props.style}
380 + tabIndex={props.tabIndex}
381 + title={props.title}
382 + />
383 + );
384 + }
385 +}
386 +
387 +/** ART Renderer */
388 +
389 +const ARTRenderer = ReactFiberReconciler({
390 + appendChild(parentInstance, child) {
391 + if (child.parentNode === parentInstance) {
392 + child.eject();
393 + }
394 +
395 + child.inject(parentInstance);
396 + },
397 +
398 + appendInitialChild(parentInstance, child) {
399 + if (typeof child === 'string') {
400 + // Noop for string children of Text (eg <Text>{'foo'}{'bar'}</Text>)
401 + return;
402 + }
403 +
404 + child.inject(parentInstance);
405 + },
406 +
407 + commitTextUpdate(textInstance, oldText, newText) {
408 + // Noop
409 + },
410 +
411 + commitUpdate(instance, oldProps, newProps) {
412 + instance._applyProps(instance, newProps, oldProps);
413 + },
414 +
415 + createInstance(type, props, internalInstanceHandle) {
416 + let instance;
417 +
418 + switch (type) {
419 + case TYPES.CLIPPING_RECTANGLE:
420 + instance = Mode.ClippingRectangle();
421 + instance._applyProps = applyClippingRectangleProps;
422 + break;
423 + case TYPES.GROUP:
424 + instance = Mode.Group();
425 + instance._applyProps = applyGroupProps;
426 + break;
427 + case TYPES.SHAPE:
428 + instance = Mode.Shape();
429 + instance._applyProps = applyShapeProps;
430 + break;
431 + case TYPES.TEXT:
432 + instance = Mode.Text(
433 + childrenAsString(props.children),
434 + props.font,
435 + props.alignment,
436 + props.path,
437 + );
438 + instance._applyProps = applyTextProps;
439 + break;
440 + }
441 +
442 + invariant(instance, 'ReactART does not support the type "%s"', type);
443 +
444 + instance._applyProps(instance, props);
445 +
446 + return instance;
447 + },
448 +
449 + createTextInstance(text, internalInstanceHandle) {
450 + return text;
451 + },
452 +
453 + finalizeInitialChildren(domElement, type, props) {
454 + // Noop
455 + },
456 +
457 + insertBefore(parentInstance, child, beforeChild) {
458 + invariant(
459 + child !== beforeChild,
460 + 'ReactART: Can not insert node before itself'
461 + );
462 +
463 + child.injectBefore(beforeChild);
464 + },
465 +
466 + prepareForCommit() {
467 + // Noop
468 + },
469 +
470 + prepareUpdate(domElement, oldProps, newProps) {
471 + return true;
472 + },
473 +
474 + removeChild(parentInstance, child) {
475 + destroyEventListeners(child);
476 +
477 + child.eject();
478 + },
479 +
480 + resetAfterCommit() {
481 + // Noop
482 + },
483 +
484 + resetTextContent(domElement) {
485 + // Noop
486 + },
487 +
488 + scheduleAnimationCallback: window.requestAnimationFrame,
489 +
490 + scheduleDeferredCallback: window.requestIdleCallback,
491 +
492 + shouldSetTextContent(props) {
493 + return (
494 + typeof props.children === 'string' ||
495 + typeof props.children === 'number'
496 + );
497 + },
498 +
499 + useSyncScheduling: true,
500 +});
501 +
502 +/** API */
503 +
504 +module.exports = {
505 + ClippingRectangle: TYPES.CLIPPING_RECTANGLE,
506 + Group: TYPES.GROUP,
507 + LinearGradient,
508 + Path: Mode.Path,
509 + Pattern,
510 + RadialGradient,
511 + Shape: TYPES.SHAPE,
512 + Surface,
513 + Text: TYPES.TEXT,
514 + Transform,
515 +};
src/renderers/art/ReactARTStack.js new
+643
@@ -0,0 +1,643 @@
1 +/**
2 + * Copyright (c) 2013-present Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactARTStack
10 + */
11 +
12 +'use strict';
13 +
14 +require('art/modes/current').setCurrent(
15 + require('art/modes/fast-noSideEffects') // Flip this to DOM mode for debugging
16 +);
17 +
18 +const Transform = require('art/core/transform');
19 +const Mode = require('art/modes/current');
20 +
21 +const React = require('React');
22 +const ReactDOM = require('ReactDOM');
23 +const ReactInstanceMap = require('ReactInstanceMap');
24 +const ReactMultiChild = require('ReactMultiChild');
25 +const ReactUpdates = require('ReactUpdates');
26 +
27 +const emptyObject = require('emptyObject');
28 +const invariant = require('invariant');
29 +
30 +const assign = require('object-assign');
31 +const pooledTransform = new Transform();
32 +
33 +// Utilities
34 +
35 +function childrenAsString(children) {
36 + if (!children) {
37 + return '';
38 + }
39 + if (typeof children === 'string') {
40 + return children;
41 + }
42 + if (children.length) {
43 + return children.join('\n');
44 + }
45 + return '';
46 +}
47 +
48 +function createComponent(name) {
49 + const ReactARTComponent = function(element) {
50 + this.node = null;
51 + this.subscriptions = null;
52 + this.listeners = null;
53 + this._mountImage = null;
54 + this._renderedChildren = null;
55 + this.construct(element);
56 + };
57 + ReactARTComponent.displayName = name;
58 + for (let i = 1, l = arguments.length; i < l; i++) {
59 + assign(ReactARTComponent.prototype, arguments[i]);
60 + }
61 +
62 + return ReactARTComponent;
63 +}
64 +
65 +/**
66 + * Insert `node` into `parentNode` after `referenceNode`.
67 + */
68 +function injectAfter(parentNode, referenceNode, node) {
69 + let beforeNode;
70 + if (node.parentNode === parentNode &&
71 + node.previousSibling === referenceNode) {
72 + return;
73 + }
74 + if (referenceNode == null) {
75 + // node is supposed to be first.
76 + beforeNode = parentNode.firstChild;
77 + } else {
78 + // node is supposed to be after referenceNode.
79 + beforeNode = referenceNode.nextSibling;
80 + }
81 + if (beforeNode && beforeNode.previousSibling !== node) {
82 + // Cases where `node === beforeNode` should get filtered out by earlier
83 + // checks and the behavior isn't well-defined.
84 + invariant(
85 + node !== beforeNode,
86 + 'ReactART: Can not insert node before itself'
87 + );
88 + node.injectBefore(beforeNode);
89 + } else if (node.parentNode !== parentNode) {
90 + node.inject(parentNode);
91 + }
92 +}
93 +
94 +// ContainerMixin for components that can hold ART nodes
95 +
96 +const ContainerMixin = assign({}, ReactMultiChild, {
97 +
98 + /**
99 + * Moves a child component to the supplied index.
100 + *
101 + * @param {ReactComponent} child Component to move.
102 + * @param {number} toIndex Destination index of the element.
103 + * @protected
104 + */
105 + moveChild: function(child, afterNode, toIndex, lastIndex) {
106 + const childNode = child._mountImage;
107 + injectAfter(this.node, afterNode, childNode);
108 + },
109 +
110 + /**
111 + * Creates a child component.
112 + *
113 + * @param {ReactComponent} child Component to create.
114 + * @param {object} childNode ART node to insert.
115 + * @protected
116 + */
117 + createChild: function(child, afterNode, childNode) {
118 + child._mountImage = childNode;
119 + injectAfter(this.node, afterNode, childNode);
120 + },
121 +
122 + /**
123 + * Removes a child component.
124 + *
125 + * @param {ReactComponent} child Child to remove.
126 + * @protected
127 + */
128 + removeChild: function(child) {
129 + child._mountImage.eject();
130 + child._mountImage = null;
131 + },
132 +
133 + updateChildrenAtRoot: function(nextChildren, transaction) {
134 + this.updateChildren(nextChildren, transaction, emptyObject);
135 + },
136 +
137 + mountAndInjectChildrenAtRoot: function(children, transaction) {
138 + this.mountAndInjectChildren(children, transaction, emptyObject);
139 + },
140 +
141 + /**
142 + * Override to bypass batch updating because it is not necessary.
143 + *
144 + * @param {?object} nextChildren.
145 + * @param {ReactReconcileTransaction} transaction
146 + * @internal
147 + * @override {ReactMultiChild.updateChildren}
148 + */
149 + updateChildren: function(nextChildren, transaction, context) {
150 + this._updateChildren(nextChildren, transaction, context);
151 + },
152 +
153 + // Shorthands
154 +
155 + mountAndInjectChildren: function(children, transaction, context) {
156 + const mountedImages = this.mountChildren(
157 + children,
158 + transaction,
159 + context
160 + );
161 + // Each mount image corresponds to one of the flattened children
162 + let i = 0;
163 + for (let key in this._renderedChildren) {
164 + if (this._renderedChildren.hasOwnProperty(key)) {
165 + const child = this._renderedChildren[key];
166 + child._mountImage = mountedImages[i];
167 + mountedImages[i].inject(this.node);
168 + i++;
169 + }
170 + }
171 + }
172 +
173 +});
174 +
175 +// Surface is a React DOM Component, not an ART component. It serves as the
176 +// entry point into the ART reconciler.
177 +
178 +const Surface = React.createClass({
179 +
180 + displayName: 'Surface',
181 +
182 + mixins: [ContainerMixin],
183 +
184 + componentDidMount: function() {
185 + const domNode = ReactDOM.findDOMNode(this);
186 +
187 + this.node = Mode.Surface(+this.props.width, +this.props.height, domNode);
188 +
189 + const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
190 + transaction.perform(
191 + this.mountAndInjectChildren,
192 + this,
193 + this.props.children,
194 + transaction,
195 + ReactInstanceMap.get(this)._context
196 + );
197 + ReactUpdates.ReactReconcileTransaction.release(transaction);
198 + },
199 +
200 + componentDidUpdate: function(oldProps) {
201 + const node = this.node;
202 + if (this.props.width != oldProps.width ||
203 + this.props.height != oldProps.height) {
204 + node.resize(+this.props.width, +this.props.height);
205 + }
206 +
207 + const transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
208 + transaction.perform(
209 + this.updateChildren,
210 + this,
211 + this.props.children,
212 + transaction,
213 + ReactInstanceMap.get(this)._context
214 + );
215 + ReactUpdates.ReactReconcileTransaction.release(transaction);
216 +
217 + if (node.render) {
218 + node.render();
219 + }
220 + },
221 +
222 + componentWillUnmount: function() {
223 + this.unmountChildren();
224 + },
225 +
226 + render: function() {
227 + // This is going to be a placeholder because we don't know what it will
228 + // actually resolve to because ART may render canvas, vml or svg tags here.
229 + // We only allow a subset of properties since others might conflict with
230 + // ART's properties.
231 + const props = this.props;
232 +
233 + // TODO: ART's Canvas Mode overrides surface title and cursor
234 + const Tag = Mode.Surface.tagName;
235 + return (
236 + <Tag
237 + accessKey={props.accessKey}
238 + className={props.className}
239 + draggable={props.draggable}
240 + role={props.role}
241 + style={props.style}
242 + tabIndex={props.tabIndex}
243 + title={props.title}
244 + />
245 + );
246 + }
247 +
248 +});
249 +
250 +// Various nodes that can go into a surface
251 +
252 +const EventTypes = {
253 + onMouseMove: 'mousemove',
254 + onMouseOver: 'mouseover',
255 + onMouseOut: 'mouseout',
256 + onMouseUp: 'mouseup',
257 + onMouseDown: 'mousedown',
258 + onClick: 'click'
259 +};
260 +
261 +const NodeMixin = {
262 +
263 + construct: function(element) {
264 + this._currentElement = element;
265 + },
266 +
267 + getNativeNode: function() {
268 + return this.node;
269 + },
270 +
271 + getPublicInstance: function() {
272 + return this.node;
273 + },
274 +
275 + putEventListener: function(type, listener) {
276 + const subscriptions = this.subscriptions || (this.subscriptions = {});
277 + const listeners = this.listeners || (this.listeners = {});
278 + listeners[type] = listener;
279 + if (listener) {
280 + if (!subscriptions[type]) {
281 + subscriptions[type] = this.node.subscribe(type, this.handleEvent, this);
282 + }
283 + } else {
284 + if (subscriptions[type]) {
285 + subscriptions[type]();
286 + delete subscriptions[type];
287 + }
288 + }
289 + },
290 +
291 + handleEvent: function(event) {
292 + const listener = this.listeners[event.type];
293 + if (!listener) {
294 + return;
295 + }
296 + if (typeof listener === 'function') {
297 + listener.call(this, event);
298 + } else if (listener.handleEvent) {
299 + listener.handleEvent(event);
300 + }
301 + },
302 +
303 + destroyEventListeners: function() {
304 + const subscriptions = this.subscriptions;
305 + if (subscriptions) {
306 + for (let type in subscriptions) {
307 + subscriptions[type]();
308 + }
309 + }
310 + this.subscriptions = null;
311 + this.listeners = null;
312 + },
313 +
314 + applyNodeProps: function(oldProps, props) {
315 + const node = this.node;
316 +
317 + const scaleX = props.scaleX != null ? props.scaleX :
318 + props.scale != null ? props.scale : 1;
319 + const scaleY = props.scaleY != null ? props.scaleY :
320 + props.scale != null ? props.scale : 1;
321 +
322 + pooledTransform
323 + .transformTo(1, 0, 0, 1, 0, 0)
324 + .move(props.x || 0, props.y || 0)
325 + .rotate(props.rotation || 0, props.originX, props.originY)
326 + .scale(scaleX, scaleY, props.originX, props.originY);
327 +
328 + if (props.transform != null) {
329 + pooledTransform.transform(props.transform);
330 + }
331 +
332 + if (node.xx !== pooledTransform.xx || node.yx !== pooledTransform.yx ||
333 + node.xy !== pooledTransform.xy || node.yy !== pooledTransform.yy ||
334 + node.x !== pooledTransform.x || node.y !== pooledTransform.y) {
335 + node.transformTo(pooledTransform);
336 + }
337 +
338 + if (props.cursor !== oldProps.cursor || props.title !== oldProps.title) {
339 + node.indicate(props.cursor, props.title);
340 + }
341 +
342 + if (node.blend && props.opacity !== oldProps.opacity) {
343 + node.blend(props.opacity == null ? 1 : props.opacity);
344 + }
345 +
346 + if (props.visible !== oldProps.visible) {
347 + if (props.visible == null || props.visible) {
348 + node.show();
349 + } else {
350 + node.hide();
351 + }
352 + }
353 +
354 + for (let type in EventTypes) {
355 + this.putEventListener(EventTypes[type], props[type]);
356 + }
357 + },
358 +
359 + mountComponentIntoNode: function(rootID, container) {
360 + throw new Error(
361 + 'You cannot render an ART component standalone. ' +
362 + 'You need to wrap it in a Surface.'
363 + );
364 + }
365 +
366 +};
367 +
368 +// Group
369 +
370 +const Group = createComponent('Group', NodeMixin, ContainerMixin, {
371 +
372 + mountComponent: function(
373 + transaction,
374 + nativeParent,
375 + nativeContainerInfo,
376 + context
377 + ) {
378 + this.node = Mode.Group();
379 + const props = this._currentElement.props;
380 + this.applyGroupProps(emptyObject, props);
381 + this.mountAndInjectChildren(props.children, transaction, context);
382 + return this.node;
383 + },
384 +
385 + receiveComponent: function(nextComponent, transaction, context) {
386 + const props = nextComponent.props;
387 + const oldProps = this._currentElement.props;
388 + this.applyGroupProps(oldProps, props);
389 + this.updateChildren(props.children, transaction, context);
390 + this._currentElement = nextComponent;
391 + },
392 +
393 + applyGroupProps: function(oldProps, props) {
394 + this.node.width = props.width;
395 + this.node.height = props.height;
396 + this.applyNodeProps(oldProps, props);
397 + },
398 +
399 + unmountComponent: function() {
400 + this.destroyEventListeners();
401 + this.unmountChildren();
402 + }
403 +
404 +});
405 +
406 +// ClippingRectangle
407 +const ClippingRectangle = createComponent(
408 + 'ClippingRectangle', NodeMixin, ContainerMixin, {
409 +
410 + mountComponent: function(
411 + transaction,
412 + nativeParent,
413 + nativeContainerInfo,
414 + context
415 + ) {
416 + this.node = Mode.ClippingRectangle();
417 + const props = this._currentElement.props;
418 + this.applyClippingProps(emptyObject, props);
419 + this.mountAndInjectChildren(props.children, transaction, context);
420 + return this.node;
421 + },
422 +
423 + receiveComponent: function(nextComponent, transaction, context) {
424 + const props = nextComponent.props;
425 + const oldProps = this._currentElement.props;
426 + this.applyClippingProps(oldProps, props);
427 + this.updateChildren(props.children, transaction, context);
428 + this._currentElement = nextComponent;
429 + },
430 +
431 + applyClippingProps: function(oldProps, props) {
432 + this.node.width = props.width;
433 + this.node.height = props.height;
434 + this.node.x = props.x;
435 + this.node.y = props.y;
436 + this.applyNodeProps(oldProps, props);
437 + },
438 +
439 + unmountComponent: function() {
440 + this.destroyEventListeners();
441 + this.unmountChildren();
442 + }
443 +
444 +});
445 +
446 +
447 +// Renderables
448 +
449 +const RenderableMixin = assign({}, NodeMixin, {
450 +
451 + applyRenderableProps: function(oldProps, props) {
452 + if (oldProps.fill !== props.fill) {
453 + if (props.fill && props.fill.applyFill) {
454 + props.fill.applyFill(this.node);
455 + } else {
456 + this.node.fill(props.fill);
457 + }
458 + }
459 + if (
460 + oldProps.stroke !== props.stroke ||
461 + oldProps.strokeWidth !== props.strokeWidth ||
462 + oldProps.strokeCap !== props.strokeCap ||
463 + oldProps.strokeJoin !== props.strokeJoin ||
464 + // TODO: Consider a deep check of stokeDash.
465 + // This may benefit the VML version in IE.
466 + oldProps.strokeDash !== props.strokeDash
467 + ) {
468 + this.node.stroke(
469 + props.stroke,
470 + props.strokeWidth,
471 + props.strokeCap,
472 + props.strokeJoin,
473 + props.strokeDash
474 + );
475 + }
476 + this.applyNodeProps(oldProps, props);
477 + },
478 +
479 + unmountComponent: function() {
480 + this.destroyEventListeners();
481 + }
482 +
483 +});
484 +
485 +// Shape
486 +
487 +const Shape = createComponent('Shape', RenderableMixin, {
488 +
489 + construct: function(element) {
490 + this._currentElement = element;
491 + this._oldDelta = null;
492 + this._oldPath = null;
493 + },
494 +
495 + mountComponent: function(
496 + transaction,
497 + nativeParent,
498 + nativeContainerInfo,
499 + context
500 + ) {
501 + this.node = Mode.Shape();
502 + const props = this._currentElement.props;
503 + this.applyShapeProps(emptyObject, props);
504 + return this.node;
505 + },
506 +
507 + receiveComponent: function(nextComponent, transaction, context) {
508 + const props = nextComponent.props;
509 + const oldProps = this._currentElement.props;
510 + this.applyShapeProps(oldProps, props);
511 + this._currentElement = nextComponent;
512 + },
513 +
514 + applyShapeProps: function(oldProps, props) {
515 + const oldDelta = this._oldDelta;
516 + const oldPath = this._oldPath;
517 + const path = props.d || childrenAsString(props.children);
518 +
519 + if (path.delta !== oldDelta ||
520 + path !== oldPath ||
521 + oldProps.width !== props.width ||
522 + oldProps.height !== props.height) {
523 +
524 + this.node.draw(
525 + path,
526 + props.width,
527 + props.height
528 + );
529 +
530 + this._oldPath = path;
531 + this._oldDelta = path.delta;
532 + }
533 +
534 + this.applyRenderableProps(oldProps, props);
535 + }
536 +
537 +});
538 +
539 +// Text
540 +
541 +const Text = createComponent('Text', RenderableMixin, {
542 +
543 + construct: function(element) {
544 + this._currentElement = element;
545 + this._oldString = null;
546 + },
547 +
548 + mountComponent: function(
549 + transaction,
550 + nativeParent,
551 + nativeContainerInfo,
552 + context
553 + ) {
554 + const props = this._currentElement.props;
555 + const newString = childrenAsString(props.children);
556 + this.node = Mode.Text(newString, props.font, props.alignment, props.path);
557 + this._oldString = newString;
558 + this.applyRenderableProps(emptyObject, props);
559 + return this.node;
560 + },
561 +
562 + isSameFont: function(oldFont, newFont) {
563 + if (oldFont === newFont) {
564 + return true;
565 + }
566 + if (typeof newFont === 'string' || typeof oldFont === 'string') {
567 + return false;
568 + }
569 + return (
570 + newFont.fontSize === oldFont.fontSize &&
571 + newFont.fontStyle === oldFont.fontStyle &&
572 + newFont.fontVariant === oldFont.fontVariant &&
573 + newFont.fontWeight === oldFont.fontWeight &&
574 + newFont.fontFamily === oldFont.fontFamily
575 + );
576 + },
577 +
578 + receiveComponent: function(nextComponent, transaction, context) {
579 + const props = nextComponent.props;
580 + const oldProps = this._currentElement.props;
581 +
582 + const oldString = this._oldString;
583 + const newString = childrenAsString(props.children);
584 +
585 + if (oldString !== newString ||
586 + !this.isSameFont(oldProps.font, props.font) ||
587 + oldProps.alignment !== props.alignment ||
588 + oldProps.path !== props.path) {
589 + this.node.draw(
590 + newString,
591 + props.font,
592 + props.alignment,
593 + props.path
594 + );
595 + this._oldString = newString;
596 + }
597 +
598 + this.applyRenderableProps(oldProps, props);
599 + this._currentElement = nextComponent;
600 + }
601 +
602 +});
603 +
604 +// Declarative fill type objects - API design not finalized
605 +
606 +const slice = Array.prototype.slice;
607 +
608 +function LinearGradient(stops, x1, y1, x2, y2) {
609 + this.args = slice.call(arguments);
610 +}
611 +
612 +LinearGradient.prototype.applyFill = function(node) {
613 + node.fillLinear.apply(node, this.args);
614 +};
615 +
616 +function RadialGradient(stops, fx, fy, rx, ry, cx, cy) {
617 + this.args = slice.call(arguments);
618 +}
619 +
620 +RadialGradient.prototype.applyFill = function(node) {
621 + node.fillRadial.apply(node, this.args);
622 +};
623 +
624 +function Pattern(url, width, height, left, top) {
625 + this.args = slice.call(arguments);
626 +}
627 +
628 +Pattern.prototype.applyFill = function(node) {
629 + node.fillImage.apply(node, this.args);
630 +};
631 +
632 +module.exports = {
633 + ClippingRectangle,
634 + Group,
635 + LinearGradient,
636 + Path: Mode.Path,
637 + Pattern,
638 + RadialGradient,
639 + Shape,
640 + Surface,
641 + Text,
642 + Transform,
643 +};
src/renderers/art/__tests__/ReactART-test.js
+29
@@ -295,4 +295,33 @@ describe('ReactART', () => {
295 expect(ref.constructor).toBe(CustomShape);
296 });
297
298 + it('adds and updates event handlers', () => {
299 + const container = document.createElement('div');
300 +
301 + function render(onClick) {
302 + return ReactDOM.render(
303 + <Surface>
304 + <Shape onClick={onClick} />
305 + </Surface>,
306 + container,
307 + );
308 + }
309 +
310 + function doClick(instance) {
311 + const path = ReactDOM.findDOMNode(instance).querySelector('path');
312 +
313 + // ReactTestUtils.Simulate.click doesn't work with SVG elements
314 + path.click();
315 + }
316 +
317 + const onClick1 = jest.fn();
318 + let instance = render(onClick1);
319 + doClick(instance);
320 + expect(onClick1).toBeCalled();
321 +
322 + const onClick2 = jest.fn();
323 + instance = render(onClick2);
324 + doClick(instance);
325 + expect(onClick2).toBeCalled();
326 + });
327 });