react-pivot
Version:
React-Pivot is a data-grid component with pivot-table-like functionality for data display, filtering, and exploration.
427 lines (398 loc) • 12.4 kB
JavaScript
/**
* Copyright 2013-2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactMultiChild
* @typechecks static-only
*/
"use strict";
var ReactComponent = require("./ReactComponent");
var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes");
var flattenChildren = require("./flattenChildren");
var instantiateReactComponent = require("./instantiateReactComponent");
var shouldUpdateReactComponent = require("./shouldUpdateReactComponent");
/**
* Updating children of a component may trigger recursive updates. The depth is
* used to batch recursive updates to render markup more efficiently.
*
* @type {number}
* @private
*/
var updateDepth = 0;
/**
* Queue of update configuration objects.
*
* Each object has a `type` property that is in `ReactMultiChildUpdateTypes`.
*
* @type {array<object>}
* @private
*/
var updateQueue = [];
/**
* Queue of markup to be rendered.
*
* @type {array<string>}
* @private
*/
var markupQueue = [];
/**
* Enqueues markup to be rendered and inserted at a supplied index.
*
* @param {string} parentID ID of the parent component.
* @param {string} markup Markup that renders into an element.
* @param {number} toIndex Destination index.
* @private
*/
function enqueueMarkup(parentID, markup, toIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
markupIndex: markupQueue.push(markup) - 1,
textContent: null,
fromIndex: null,
toIndex: toIndex
});
}
/**
* Enqueues moving an existing element to another index.
*
* @param {string} parentID ID of the parent component.
* @param {number} fromIndex Source index of the existing element.
* @param {number} toIndex Destination index of the element.
* @private
*/
function enqueueMove(parentID, fromIndex, toIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
markupIndex: null,
textContent: null,
fromIndex: fromIndex,
toIndex: toIndex
});
}
/**
* Enqueues removing an element at an index.
*
* @param {string} parentID ID of the parent component.
* @param {number} fromIndex Index of the element to remove.
* @private
*/
function enqueueRemove(parentID, fromIndex) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.REMOVE_NODE,
markupIndex: null,
textContent: null,
fromIndex: fromIndex,
toIndex: null
});
}
/**
* Enqueues setting the text content.
*
* @param {string} parentID ID of the parent component.
* @param {string} textContent Text content to set.
* @private
*/
function enqueueTextContent(parentID, textContent) {
// NOTE: Null values reduce hidden classes.
updateQueue.push({
parentID: parentID,
parentNode: null,
type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
markupIndex: null,
textContent: textContent,
fromIndex: null,
toIndex: null
});
}
/**
* Processes any enqueued updates.
*
* @private
*/
function processQueue() {
if (updateQueue.length) {
ReactComponent.BackendIDOperations.dangerouslyProcessChildrenUpdates(
updateQueue,
markupQueue
);
clearQueue();
}
}
/**
* Clears any enqueued updates.
*
* @private
*/
function clearQueue() {
updateQueue.length = 0;
markupQueue.length = 0;
}
/**
* ReactMultiChild are capable of reconciling multiple children.
*
* @class ReactMultiChild
* @internal
*/
var ReactMultiChild = {
/**
* Provides common functionality for components that must reconcile multiple
* children. This is used by `ReactDOMComponent` to mount, update, and
* unmount child components.
*
* @lends {ReactMultiChild.prototype}
*/
Mixin: {
/**
* Generates a "mount image" for each of the supplied children. In the case
* of `ReactDOMComponent`, a mount image is a string of markup.
*
* @param {?object} nestedChildren Nested child maps.
* @return {array} An array of mounted representations.
* @internal
*/
mountChildren: function(nestedChildren, transaction) {
var children = flattenChildren(nestedChildren);
var mountImages = [];
var index = 0;
this._renderedChildren = children;
for (var name in children) {
var child = children[name];
if (children.hasOwnProperty(name)) {
// The rendered children must be turned into instances as they're
// mounted.
var childInstance = instantiateReactComponent(child, null);
children[name] = childInstance;
// Inlined for performance, see `ReactInstanceHandles.createReactID`.
var rootID = this._rootNodeID + name;
var mountImage = childInstance.mountComponent(
rootID,
transaction,
this._mountDepth + 1
);
childInstance._mountIndex = index;
mountImages.push(mountImage);
index++;
}
}
return mountImages;
},
/**
* Replaces any rendered children with a text content string.
*
* @param {string} nextContent String of content.
* @internal
*/
updateTextContent: function(nextContent) {
updateDepth++;
var errorThrown = true;
try {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
this._unmountChildByName(prevChildren[name], name);
}
}
// Set new text content.
this.setTextContent(nextContent);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
errorThrown ? clearQueue() : processQueue();
}
}
},
/**
* Updates the rendered children with new children.
*
* @param {?object} nextNestedChildren Nested child maps.
* @param {ReactReconcileTransaction} transaction
* @internal
*/
updateChildren: function(nextNestedChildren, transaction) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildren, transaction);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
errorThrown ? clearQueue() : processQueue();
}
}
},
/**
* Improve performance by isolating this hot code path from the try/catch
* block in `updateChildren`.
*
* @param {?object} nextNestedChildren Nested child maps.
* @param {ReactReconcileTransaction} transaction
* @final
* @protected
*/
_updateChildren: function(nextNestedChildren, transaction) {
var nextChildren = flattenChildren(nextNestedChildren);
var prevChildren = this._renderedChildren;
if (!nextChildren && !prevChildren) {
return;
}
var name;
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var lastIndex = 0;
var nextIndex = 0;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var prevElement = prevChild && prevChild._currentElement;
var nextElement = nextChildren[name];
if (shouldUpdateReactComponent(prevElement, nextElement)) {
this.moveChild(prevChild, nextIndex, lastIndex);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild.receiveComponent(nextElement, transaction);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
this._unmountChildByName(prevChild, name);
}
// The child must be instantiated before it's mounted.
var nextChildInstance = instantiateReactComponent(
nextElement,
null
);
this._mountChildByNameAtIndex(
nextChildInstance, name, nextIndex, transaction
);
}
nextIndex++;
}
// Remove children that are no longer present.
for (name in prevChildren) {
if (prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren[name])) {
this._unmountChildByName(prevChildren[name], name);
}
}
},
/**
* Unmounts all rendered children. This should be used to clean up children
* when this component is unmounted.
*
* @internal
*/
unmountChildren: function() {
var renderedChildren = this._renderedChildren;
for (var name in renderedChildren) {
var renderedChild = renderedChildren[name];
// TODO: When is this not true?
if (renderedChild.unmountComponent) {
renderedChild.unmountComponent();
}
}
this._renderedChildren = null;
},
/**
* Moves a child component to the supplied index.
*
* @param {ReactComponent} child Component to move.
* @param {number} toIndex Destination index of the element.
* @param {number} lastIndex Last index visited of the siblings of `child`.
* @protected
*/
moveChild: function(child, toIndex, lastIndex) {
// If the index of `child` is less than `lastIndex`, then it needs to
// be moved. Otherwise, we do not need to move it because a child will be
// inserted or moved before `child`.
if (child._mountIndex < lastIndex) {
enqueueMove(this._rootNodeID, child._mountIndex, toIndex);
}
},
/**
* Creates a child component.
*
* @param {ReactComponent} child Component to create.
* @param {string} mountImage Markup to insert.
* @protected
*/
createChild: function(child, mountImage) {
enqueueMarkup(this._rootNodeID, mountImage, child._mountIndex);
},
/**
* Removes a child component.
*
* @param {ReactComponent} child Child to remove.
* @protected
*/
removeChild: function(child) {
enqueueRemove(this._rootNodeID, child._mountIndex);
},
/**
* Sets this text content string.
*
* @param {string} textContent Text content to set.
* @protected
*/
setTextContent: function(textContent) {
enqueueTextContent(this._rootNodeID, textContent);
},
/**
* Mounts a child with the supplied name.
*
* NOTE: This is part of `updateChildren` and is here for readability.
*
* @param {ReactComponent} child Component to mount.
* @param {string} name Name of the child.
* @param {number} index Index at which to insert the child.
* @param {ReactReconcileTransaction} transaction
* @private
*/
_mountChildByNameAtIndex: function(child, name, index, transaction) {
// Inlined for performance, see `ReactInstanceHandles.createReactID`.
var rootID = this._rootNodeID + name;
var mountImage = child.mountComponent(
rootID,
transaction,
this._mountDepth + 1
);
child._mountIndex = index;
this.createChild(child, mountImage);
this._renderedChildren = this._renderedChildren || {};
this._renderedChildren[name] = child;
},
/**
* Unmounts a rendered child by name.
*
* NOTE: This is part of `updateChildren` and is here for readability.
*
* @param {ReactComponent} child Component to unmount.
* @param {string} name Name of the child in `this._renderedChildren`.
* @private
*/
_unmountChildByName: function(child, name) {
this.removeChild(child);
child._mountIndex = null;
child.unmountComponent();
delete this._renderedChildren[name];
}
}
};
module.exports = ReactMultiChild;