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
});