atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
770 lines (670 loc) • 25.8 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _atom2;
function _atom() {
return _atom2 = require('atom');
}
var _LazyTreeNode2;
function _LazyTreeNode() {
return _LazyTreeNode2 = require('./LazyTreeNode');
}
var _TreeNodeComponent2;
function _TreeNodeComponent() {
return _TreeNodeComponent2 = require('./TreeNodeComponent');
}
var _treeNodeTraversals2;
function _treeNodeTraversals() {
return _treeNodeTraversals2 = require('./tree-node-traversals');
}
var _reactForAtom2;
function _reactForAtom() {
return _reactForAtom2 = require('react-for-atom');
}
/**
* Toggles the existence of a value in a set. If the value exists, deletes it.
* If the value does not exist, adds it.
*
* @param set The set whose value to toggle.
* @param value The value to toggle in the set.
* @param [forceHas] If defined, forces the existence of the value in the set
* regardless of its current existence. If truthy, adds `value`, if falsy
* deletes `value`.
* @returns `true` if the value was added to the set, otherwise `false`. If
* `forceHas` is defined, the return value will be equal to `forceHas`.
*/
function toggleSetHas(set, value, forceHas) {
var added = undefined;
if (forceHas || forceHas === undefined && !set.has(value)) {
set.add(value);
added = true;
} else {
set.delete(value);
added = false;
}
return added;
}
var FIRST_SELECTED_DESCENDANT_REF = 'firstSelectedDescendant';
/**
* Generic tree component that operates on LazyTreeNodes.
*/
var TreeRootComponent = (function (_React$Component) {
_inherits(TreeRootComponent, _React$Component);
_createClass(TreeRootComponent, null, [{
key: 'defaultProps',
value: {
elementToRenderWhenEmpty: null,
onConfirmSelection: function onConfirmSelection(node) {},
rowClassNameForNode: function rowClassNameForNode(node) {
return '';
}
},
enumerable: true
}]);
function TreeRootComponent(props) {
_classCallCheck(this, TreeRootComponent);
_get(Object.getPrototypeOf(TreeRootComponent.prototype), 'constructor', this).call(this, props);
this._allKeys = null;
this._emitter = null;
this._isMounted = false;
this._keyToNode = null;
this._rejectDidUpdateListenerPromise = null;
this._subscriptions = null;
var rootKeys = this.props.initialRoots.map(function (root) {
return root.getKey();
});
this.state = {
roots: this.props.initialRoots,
// This is maintained as a set of strings for two reasons:
// (1) It is straightforward to serialize.
// (2) If the LazyFileTreeNode for a path is re-created, this will still work.
expandedKeys: new Set(this.props.initialExpandedNodeKeys || rootKeys),
selectedKeys: this.props.initialSelectedNodeKeys ? new Set(this.props.initialSelectedNodeKeys) : new Set(rootKeys.length === 0 ? [] : [rootKeys[0]])
};
this._onClickNodeArrow = this._onClickNodeArrow.bind(this);
this._onClickNode = this._onClickNode.bind(this);
this._onDoubleClickNode = this._onDoubleClickNode.bind(this);
this._onMouseDown = this._onMouseDown.bind(this);
}
_createClass(TreeRootComponent, [{
key: 'componentDidMount',
value: function componentDidMount() {
this._isMounted = true;
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
// If the Set of selected items is new, like when navigating the tree with
// the arrow keys, scroll the first item into view. This addresses the
// following scenario:
// (1) Select a node in the tree
// (2) Scroll the selected node out of the viewport
// (3) Press the up or down arrow key to change the selected node
// (4) The new node should scroll into view
if (!prevState || this.state.selectedKeys !== prevState.selectedKeys) {
var firstSelectedDescendant = this.refs[FIRST_SELECTED_DESCENDANT_REF];
if (firstSelectedDescendant !== undefined) {
(_reactForAtom2 || _reactForAtom()).ReactDOM.findDOMNode(firstSelectedDescendant).scrollIntoViewIfNeeded(false);
}
}
(0, (_assert2 || _assert()).default)(this._emitter);
this._emitter.emit('did-update');
}
}, {
key: '_deselectDescendants',
value: function _deselectDescendants(root) {
var selectedKeys = this.state.selectedKeys;
(0, (_treeNodeTraversals2 || _treeNodeTraversals()).forEachCachedNode)(root, function (node) {
// `forEachCachedNode` iterates over the root, but it should remain
// selected. Skip it.
if (node === root) {
return;
}
selectedKeys.delete(node.getKey());
});
this.setState({ selectedKeys: selectedKeys });
}
}, {
key: '_isNodeExpanded',
value: function _isNodeExpanded(node) {
return this.state.expandedKeys.has(node.getKey());
}
}, {
key: '_isNodeSelected',
value: function _isNodeSelected(node) {
return this.state.selectedKeys.has(node.getKey());
}
}, {
key: '_toggleNodeExpanded',
value: function _toggleNodeExpanded(node, forceExpanded) {
var expandedKeys = this.state.expandedKeys;
var keyAdded = toggleSetHas(expandedKeys, node.getKey(), forceExpanded);
// If the node was collapsed, deselect its descendants so only nodes visible
// in the tree remain selected.
if (!keyAdded) {
this._deselectDescendants(node);
}
this.setState({ expandedKeys: expandedKeys });
}
}, {
key: '_toggleNodeSelected',
value: function _toggleNodeSelected(node, forceSelected) {
var selectedKeys = this.state.selectedKeys;
toggleSetHas(selectedKeys, node.getKey(), forceSelected);
this.setState({ selectedKeys: selectedKeys });
}
}, {
key: '_onClickNode',
value: function _onClickNode(event, node) {
if (event.metaKey) {
this._toggleNodeSelected(node);
return;
}
this.setState({
selectedKeys: new Set([node.getKey()])
});
if (!this._isNodeSelected(node) && node.isContainer()) {
// User clicked on a new directory or the user isn't using the "Preview Tabs" feature of the
// `tabs` package, so don't toggle the node's state any further yet.
return;
}
this._confirmNode(node);
}
}, {
key: '_onClickNodeArrow',
value: function _onClickNodeArrow(event, node) {
this._toggleNodeExpanded(node);
}
}, {
key: '_onDoubleClickNode',
value: function _onDoubleClickNode(event, node) {
// Double clicking a non-directory will keep the created tab open.
if (!node.isContainer()) {
this.props.onKeepSelection();
}
}
}, {
key: '_onMouseDown',
value: function _onMouseDown(event, node) {
// Select the node on right-click.
if (event.button === 2 || event.button === 0 && event.ctrlKey === true) {
if (!this._isNodeSelected(node)) {
this.setState({ selectedKeys: new Set([node.getKey()]) });
}
}
}
}, {
key: 'addContextMenuItemGroup',
value: function addContextMenuItemGroup(menuItemDefinitions) {
var _this = this;
var items = menuItemDefinitions.slice();
items = items.map(function (definition) {
definition.shouldDisplay = function () {
if (_this.state.roots.length === 0 && !definition.shouldDisplayIfTreeIsEmpty) {
return false;
}
var shouldDisplayForSelectedNodes = definition.shouldDisplayForSelectedNodes;
if (shouldDisplayForSelectedNodes) {
return shouldDisplayForSelectedNodes.call(definition, _this.getSelectedNodes());
}
return true;
};
return definition;
});
// Atom is smart about only displaying a separator when there are items to
// separate, so there will never be a dangling separator at the end.
items.push({ type: 'separator' });
// TODO: Use a computed property when supported by Flow.
var contextMenuObj = {};
contextMenuObj[this.props.eventHandlerSelector] = items;
atom.contextMenu.add(contextMenuObj);
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
if (this.state.roots.length === 0) {
return this.props.elementToRenderWhenEmpty;
}
var children = [];
var expandedKeys = this.state.expandedKeys;
var foundFirstSelectedDescendant = false;
var promises = [];
var allKeys = [];
var keyToNode = {};
this.state.roots.forEach(function (root) {
var stack = [{ node: root, depth: 0 }];
while (stack.length !== 0) {
// Pop off the top of the stack and add it to the list of nodes to display.
var item = stack.pop();
var _node = item.node;
// Keep a reference the first selected descendant with
// `this.refs[FIRST_SELECTED_DESCENDANT_REF]`.
var isNodeSelected = _this2._isNodeSelected(_node);
var ref = null;
if (!foundFirstSelectedDescendant && isNodeSelected) {
foundFirstSelectedDescendant = true;
ref = FIRST_SELECTED_DESCENDANT_REF;
}
var child = (_reactForAtom2 || _reactForAtom()).React.createElement((_TreeNodeComponent2 || _TreeNodeComponent()).TreeNodeComponent, _extends({}, item, {
isContainer: _node.isContainer(),
isExpanded: _this2._isNodeExpanded(_node),
isLoading: !_node.isCacheValid(),
isSelected: isNodeSelected,
label: _node.getLabel(),
labelElement: _node.getLabelElement(),
labelClassName: _this2.props.labelClassNameForNode(_node),
rowClassName: _this2.props.rowClassNameForNode(_node),
onClickArrow: _this2._onClickNodeArrow,
onClick: _this2._onClickNode,
onDoubleClick: _this2._onDoubleClickNode,
onMouseDown: _this2._onMouseDown,
path: _node.getKey(),
key: _node.getKey(),
ref: ref
}));
children.push(child);
allKeys.push(_node.getKey());
keyToNode[_node.getKey()] = _node;
// Check whether the node has any children that should be displayed.
if (!_node.isContainer() || !expandedKeys.has(_node.getKey())) {
continue;
}
var cachedChildren = _node.getCachedChildren();
if (!cachedChildren || !_node.isCacheValid()) {
promises.push(_node.fetchChildren());
}
// Prevent flickering by always rendering cached children -- if they're invalid,
// then the fetch will happen soon.
if (cachedChildren) {
(function () {
var depth = item.depth + 1;
// Push the node's children on the stack in reverse order so that when
// they are popped off the stack, they are iterated in the original
// order.
cachedChildren.reverse().forEach(function (childNode) {
stack.push({ node: childNode, depth: depth });
});
})();
}
}
});
if (promises.length) {
Promise.all(promises).then(function () {
// The component could have been unmounted by the time the promises are resolved.
if (_this2._isMounted) {
_this2.forceUpdate();
}
});
}
this._allKeys = allKeys;
this._keyToNode = keyToNode;
return (_reactForAtom2 || _reactForAtom()).React.createElement(
'div',
{ className: 'nuclide-tree-root' },
children
);
}
}, {
key: 'componentWillMount',
value: function componentWillMount() {
var _this3 = this;
var allKeys = [];
var keyToNode = {};
this.state.roots.forEach(function (root) {
var rootKey = root.getKey();
allKeys.push(rootKey);
keyToNode[rootKey] = root;
});
var subscriptions = new (_atom2 || _atom()).CompositeDisposable();
subscriptions.add(atom.commands.add(this.props.eventHandlerSelector, {
// Expand and collapse.
'core:move-right': function coreMoveRight() {
return _this3._expandSelection();
},
'core:move-left': function coreMoveLeft() {
return _this3._collapseSelection();
},
// Move selection up and down.
'core:move-up': function coreMoveUp() {
return _this3._moveSelectionUp();
},
'core:move-down': function coreMoveDown() {
return _this3._moveSelectionDown();
},
'core:confirm': function coreConfirm() {
return _this3._confirmSelection();
}
}));
this._allKeys = allKeys;
this._emitter = new (_atom2 || _atom()).Emitter();
this._keyToNode = keyToNode;
this._subscriptions = subscriptions;
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this._subscriptions) {
this._subscriptions.dispose();
}
if (this._emitter) {
this._emitter.dispose();
}
this._isMounted = false;
}
}, {
key: 'serialize',
value: function serialize() {
return {
expandedNodeKeys: Array.from(this.state.expandedKeys),
selectedNodeKeys: Array.from(this.state.selectedKeys)
};
}
}, {
key: 'invalidateCachedNodes',
value: function invalidateCachedNodes() {
this.state.roots.forEach(function (root) {
(0, (_treeNodeTraversals2 || _treeNodeTraversals()).forEachCachedNode)(root, function (node) {
node.invalidateCache();
});
});
}
/**
* Returns a Promise that's resolved when the roots are rendered.
*/
}, {
key: 'setRoots',
value: function setRoots(roots) {
var _this4 = this;
this.state.roots.forEach(function (root) {
_this4.removeStateForSubtree(root);
});
var expandedKeys = this.state.expandedKeys;
roots.forEach(function (root) {
return expandedKeys.add(root.getKey());
});
// We have to create the listener before setting the state so it can pick
// up the changes from `setState`.
var promise = this._createDidUpdateListener( /* shouldResolve */function () {
var rootsReady = _this4.state.roots === roots;
var childrenReady = _this4.state.roots.every(function (root) {
return root.isCacheValid();
});
return rootsReady && childrenReady;
});
this.setState({
roots: roots,
expandedKeys: expandedKeys
});
return promise;
}
}, {
key: '_createDidUpdateListener',
value: function _createDidUpdateListener(shouldResolve) {
var _this5 = this;
return new Promise(function (resolve, reject) {
(0, (_assert2 || _assert()).default)(_this5._emitter);
var didUpdateDisposable = _this5._emitter.on('did-update', function () {
if (shouldResolve()) {
resolve(undefined);
// Set this to null so this promise can't be rejected anymore.
_this5._rejectDidUpdateListenerPromise = null;
didUpdateDisposable.dispose();
}
});
// We need to reject the previous promise, so it doesn't get leaked.
if (_this5._rejectDidUpdateListenerPromise) {
_this5._rejectDidUpdateListenerPromise();
_this5._rejectDidUpdateListenerPromise = null;
}
_this5._rejectDidUpdateListenerPromise = function () {
reject(undefined);
didUpdateDisposable.dispose();
};
});
}
}, {
key: 'removeStateForSubtree',
value: function removeStateForSubtree(root) {
var expandedKeys = this.state.expandedKeys;
var selectedKeys = this.state.selectedKeys;
(0, (_treeNodeTraversals2 || _treeNodeTraversals()).forEachCachedNode)(root, function (node) {
var cachedKey = node.getKey();
expandedKeys.delete(cachedKey);
selectedKeys.delete(cachedKey);
});
this.setState({
expandedKeys: expandedKeys,
selectedKeys: selectedKeys
});
}
}, {
key: 'getRootNodes',
value: function getRootNodes() {
return this.state.roots;
}
}, {
key: 'getExpandedNodes',
value: function getExpandedNodes() {
var _this6 = this;
var expandedNodes = [];
this.state.expandedKeys.forEach(function (key) {
var node = _this6.getNodeForKey(key);
if (node != null) {
expandedNodes.push(node);
}
});
return expandedNodes;
}
}, {
key: 'getSelectedNodes',
value: function getSelectedNodes() {
var _this7 = this;
var selectedNodes = [];
this.state.selectedKeys.forEach(function (key) {
var node = _this7.getNodeForKey(key);
if (node != null) {
selectedNodes.push(node);
}
});
return selectedNodes;
}
// Return the key for the first node that is selected, or null if there are none.
}, {
key: '_getFirstSelectedKey',
value: function _getFirstSelectedKey() {
var _this8 = this;
if (this.state.selectedKeys.size === 0) {
return null;
}
var selectedKey = undefined;
if (this._allKeys != null) {
this._allKeys.every(function (key) {
if (_this8.state.selectedKeys.has(key)) {
selectedKey = key;
return false;
}
return true;
});
}
return selectedKey;
}
}, {
key: '_expandSelection',
value: function _expandSelection() {
var key = this._getFirstSelectedKey();
if (key) {
this.expandNodeKey(key);
}
}
/**
* Selects a node by key if it's in the file tree; otherwise, do nothing.
*/
}, {
key: 'selectNodeKey',
value: function selectNodeKey(nodeKey) {
var _this9 = this;
if (!this.getNodeForKey(nodeKey)) {
return Promise.reject();
}
// We have to create the listener before setting the state so it can pick
// up the changes from `setState`.
var promise = this._createDidUpdateListener( /* shouldResolve */function () {
return _this9.state.selectedKeys.has(nodeKey);
});
this.setState({ selectedKeys: new Set([nodeKey]) });
return promise;
}
}, {
key: 'getNodeForKey',
value: function getNodeForKey(nodeKey) {
if (this._keyToNode != null) {
return this._keyToNode[nodeKey];
}
}
/**
* If this function is called multiple times in parallel, the later calls will
* cause the previous promises to reject even if they end up expanding the
* node key successfully.
*
* If we don't reject, then we might leak promises if a node key is expanded
* and collapsed in succession (the collapse could succeed first, causing
* the expand to never resolve).
*/
}, {
key: 'expandNodeKey',
value: function expandNodeKey(nodeKey) {
var _this10 = this;
var node = this.getNodeForKey(nodeKey);
if (node && node.isContainer()) {
var promise = this._createDidUpdateListener( /* shouldResolve */function () {
var isExpanded = _this10.state.expandedKeys.has(nodeKey);
var nodeNow = _this10.getNodeForKey(nodeKey);
var isDoneFetching = nodeNow && nodeNow.isContainer() && nodeNow.isCacheValid();
return Boolean(isExpanded && isDoneFetching);
});
this._toggleNodeExpanded(node, true /* forceExpanded */);
return promise;
}
return Promise.resolve();
}
}, {
key: 'collapseNodeKey',
value: function collapseNodeKey(nodeKey) {
var _this11 = this;
var node = this.getNodeForKey(nodeKey);
if (node && node.isContainer()) {
var promise = this._createDidUpdateListener(
/* shouldResolve */function () {
return !_this11.state.expandedKeys.has(nodeKey);
});
this._toggleNodeExpanded(node, false /* forceExpanded */);
return promise;
}
return Promise.resolve();
}
}, {
key: 'isNodeKeyExpanded',
value: function isNodeKeyExpanded(nodeKey) {
return this.state.expandedKeys.has(nodeKey);
}
}, {
key: '_collapseSelection',
value: function _collapseSelection() {
var key = this._getFirstSelectedKey();
if (!key) {
return;
}
var expandedKeys = this.state.expandedKeys;
var node = this.getNodeForKey(key);
if (node != null && (!expandedKeys.has(key) || !node.isContainer())) {
// If the selection is already collapsed or it's not a container, select its parent.
var _parent = node.getParent();
if (_parent) {
this.selectNodeKey(_parent.getKey());
}
}
this.collapseNodeKey(key);
}
}, {
key: '_moveSelectionUp',
value: function _moveSelectionUp() {
var allKeys = this._allKeys;
if (!allKeys) {
return;
}
var keyIndexToSelect = allKeys.length - 1;
var key = this._getFirstSelectedKey();
if (key) {
keyIndexToSelect = allKeys.indexOf(key);
if (keyIndexToSelect > 0) {
--keyIndexToSelect;
}
}
this.setState({ selectedKeys: new Set([allKeys[keyIndexToSelect]]) });
}
}, {
key: '_moveSelectionDown',
value: function _moveSelectionDown() {
var allKeys = this._allKeys;
if (!allKeys) {
return;
}
var keyIndexToSelect = 0;
var key = this._getFirstSelectedKey();
if (key) {
keyIndexToSelect = allKeys.indexOf(key);
if (keyIndexToSelect !== -1 && keyIndexToSelect < allKeys.length - 1) {
++keyIndexToSelect;
}
}
this.setState({ selectedKeys: new Set([allKeys[keyIndexToSelect]]) });
}
}, {
key: '_confirmSelection',
value: function _confirmSelection() {
var key = this._getFirstSelectedKey();
if (key) {
var _node2 = this.getNodeForKey(key);
if (_node2) {
this._confirmNode(_node2);
}
}
}
}, {
key: '_confirmNode',
value: function _confirmNode(node) {
if (node.isContainer()) {
this._toggleNodeExpanded(node);
} else {
this.props.onConfirmSelection(node);
}
}
}]);
return TreeRootComponent;
})((_reactForAtom2 || _reactForAtom()).React.Component);
exports.TreeRootComponent = TreeRootComponent;
// By default, no context menu item will be displayed if the tree is empty.
// Set this to true to override that behavior.
// Render will return this component if there are no root nodes.
// A node can be confirmed if it is a selected non-container node and the user is clicks on it
// or presses <enter>.
// A node can be "kept" (opened permanently) by double clicking it. This only has an effect
// when the `usePreviewTabs` setting is enabled in the "tabs" package.