Files
blockly/core/components/tree/basenode.js
Sam El-Husseini eb07793ba8 Cleanup Blockly UI components. (#3723)
* Cleanup Blockly UI components. Remove unnecessary getters
2020-03-11 10:40:16 -07:00

911 lines
22 KiB
JavaScript

/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Definition of the Blockly.tree.BaseNode class.
* This class is similar to Closure's goog.ui.tree.BaseNode class.
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.tree.BaseNode');
goog.require('Blockly.Component');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.KeyCodes');
goog.require('Blockly.utils.style');
/**
* An abstract base class for a node in the tree.
* Similar to goog.ui.tree.BaseNode
*
* @param {string} content The content of the node label treated as
* plain-text and will be HTML escaped.
* @param {!Blockly.tree.BaseNode.Config} config The configuration for the tree.
* @constructor
* @extends {Blockly.Component}
*/
Blockly.tree.BaseNode = function(content, config) {
Blockly.Component.call(this);
/**
* Text content of the node label.
* @type {string}
* @package
*/
this.content = content;
/**
* @type {string}
* @package
*/
this.iconClass;
/**
* @type {string}
* @package
*/
this.expandedIconClass;
/**
* The configuration for the tree.
* @type {!Blockly.tree.BaseNode.Config}
* @protected
*/
this.config_ = config;
/**
* @type {Blockly.tree.TreeControl}
* @protected
*/
this.tree;
/**
* @type {Blockly.tree.BaseNode}
* @private
*/
this.previousSibling_;
/**
* @type {Blockly.tree.BaseNode}
* @private
*/
this.nextSibling_;
/**
* Whether the tree item is selected.
* @type {boolean}
* @protected
*/
this.selected_ = false;
/**
* Whether the tree node is expanded.
* @type {boolean}
* @protected
*/
this.expanded_ = false;
/**
* Whether to allow user to collapse this node.
* @type {boolean}
* @protected
*/
this.isUserCollapsible_ = true;
/**
* Nesting depth of this node; cached result of getDepth.
* -1 if value has not been cached.
* @type {number}
* @private
*/
this.depth_ = -1;
};
Blockly.utils.object.inherits(Blockly.tree.BaseNode, Blockly.Component);
/**
* The config type for the tree.
* @typedef {{
* indentWidth:number,
* cssRoot:string,
* cssHideRoot:string,
* cssTreeRow:string,
* cssItemLabel:string,
* cssTreeIcon:string,
* cssExpandedFolderIcon:string,
* cssCollapsedFolderIcon:string,
* cssFileIcon:string,
* cssSelectedRow:string
* }}
*/
Blockly.tree.BaseNode.Config;
/**
* Map of nodes in existence. Needed to route events to the appropriate nodes.
* Nodes are added to the map at {@link #enterDocument} time and removed at
* {@link #exitDocument} time.
* @type {Object}
* @protected
*/
Blockly.tree.BaseNode.allNodes = {};
/** @override */
Blockly.tree.BaseNode.prototype.disposeInternal = function() {
Blockly.tree.BaseNode.superClass_.disposeInternal.call(this);
if (this.tree) {
this.tree = null;
}
this.setElementInternal(null);
};
/**
* Adds roles and states.
* @protected
*/
Blockly.tree.BaseNode.prototype.initAccessibility = function() {
var el = this.getElement();
if (el) {
// Set an id for the label
var label = this.getLabelElement();
if (label && !label.id) {
label.id = this.getId() + '.label';
}
Blockly.utils.aria.setRole(el, Blockly.utils.aria.Role.TREEITEM);
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.SELECTED, false);
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.LEVEL, this.getDepth());
if (label) {
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.LABELLEDBY, label.id);
}
var img = this.getIconElement();
if (img) {
Blockly.utils.aria.setRole(img, Blockly.utils.aria.Role.PRESENTATION);
}
var ce = this.getChildrenElement();
if (ce) {
Blockly.utils.aria.setRole(ce,
Blockly.utils.aria.Role.GROUP);
// In case the children will be created lazily.
if (ce.hasChildNodes()) {
// Only set aria-expanded if the node has children (can be expanded).
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.EXPANDED, false);
// do setsize for each child
var count = this.getChildCount();
for (var i = 1; i <= count; i++) {
var child = /** @type {!Element} */ (this.getChildAt(i - 1).getElement());
Blockly.utils.aria.setState(child,
Blockly.utils.aria.State.SETSIZE, count);
Blockly.utils.aria.setState(child,
Blockly.utils.aria.State.POSINSET, i);
}
}
}
}
};
/** @override */
Blockly.tree.BaseNode.prototype.createDom = function() {
var element = document.createElement('div');
element.appendChild(this.toDom());
this.setElementInternal(/** @type {!Element} */ (element));
};
/** @override */
Blockly.tree.BaseNode.prototype.enterDocument = function() {
Blockly.tree.BaseNode.superClass_.enterDocument.call(this);
Blockly.tree.BaseNode.allNodes[this.getId()] = this;
this.initAccessibility();
};
/** @override */
Blockly.tree.BaseNode.prototype.exitDocument = function() {
Blockly.tree.BaseNode.superClass_.exitDocument.call(this);
delete Blockly.tree.BaseNode.allNodes[this.getId()];
};
/**
* The method assumes that the child doesn't have parent node yet.
* @override
*/
Blockly.tree.BaseNode.prototype.addChildAt = function(child, index) {
child = /** @type {Blockly.tree.BaseNode} */ (child);
var prevNode = this.getChildAt(index - 1);
var nextNode = this.getChildAt(index);
Blockly.tree.BaseNode.superClass_.addChildAt.call(this, child, index);
child.previousSibling_ = prevNode;
child.nextSibling_ = nextNode;
if (prevNode) {
prevNode.nextSibling_ = child;
}
if (nextNode) {
nextNode.previousSibling_ = child;
}
var tree = this.getTree();
if (tree) {
child.setTreeInternal(tree);
}
child.setDepth_(this.getDepth() + 1);
var el = this.getElement();
if (el) {
this.updateExpandIcon();
Blockly.utils.aria.setState(
el, Blockly.utils.aria.State.EXPANDED, this.expanded_);
if (this.expanded_) {
var childrenEl = this.getChildrenElement();
if (!child.getElement()) {
child.createDom();
}
var childElement = child.getElement();
var nextElement = nextNode && nextNode.getElement();
childrenEl.insertBefore(childElement, nextElement);
if (this.isInDocument()) {
child.enterDocument();
}
if (!nextNode) {
if (prevNode) {
prevNode.updateExpandIcon();
} else {
Blockly.utils.style.setElementShown(childrenEl, true);
this.setExpanded(this.expanded_);
}
}
}
}
};
/**
* Appends a node as a child to the current node.
* @param {Blockly.tree.BaseNode} child The child to add.
* @package
*/
Blockly.tree.BaseNode.prototype.add = function(child) {
if (child.getParent()) {
throw Error(Blockly.Component.Error.PARENT_UNABLE_TO_BE_SET);
}
this.addChildAt(child, this.getChildCount());
};
/**
* Returns the tree.
* @return {?Blockly.tree.TreeControl} tree
* @protected
*/
Blockly.tree.BaseNode.prototype.getTree = function() {
return null;
};
/**
* Returns the depth of the node in the tree. Should not be overridden.
* @return {number} The non-negative depth of this node (the root is zero).
* @protected
*/
Blockly.tree.BaseNode.prototype.getDepth = function() {
var depth = this.depth_;
if (depth < 0) {
var parent = this.getParent();
if (parent) {
depth = parent.getDepth() + 1;
} else {
depth = 0;
}
this.setDepth_(depth);
}
return depth;
};
/**
* Changes the depth of a node (and all its descendants).
* @param {number} depth The new nesting depth; must be non-negative.
* @private
*/
Blockly.tree.BaseNode.prototype.setDepth_ = function(depth) {
if (depth != this.depth_) {
this.depth_ = depth;
var row = this.getRowElement();
if (row) {
var indent = this.getPixelIndent_() + 'px';
if (this.rightToLeft_) {
row.style.paddingRight = indent;
} else {
row.style.paddingLeft = indent;
}
}
this.forEachChild(function(child) { child.setDepth_(depth + 1); });
}
};
/**
* Returns true if the node is a descendant of this node
* @param {Blockly.tree.BaseNode} node The node to check.
* @return {boolean} True if the node is a descendant of this node, false
* otherwise.
* @protected
*/
Blockly.tree.BaseNode.prototype.contains = function(node) {
var current = node;
while (current) {
if (current == this) {
return true;
}
current = current.getParent();
}
return false;
};
/**
* This is re-defined here to indicate to the closure compiler the correct
* child return type.
* @param {number} index 0-based index.
* @return {Blockly.tree.BaseNode} The child at the given index; null if none.
* @protected
*/
Blockly.tree.BaseNode.prototype.getChildAt;
/**
* Returns the children of this node.
* @return {!Array.<!Blockly.tree.BaseNode>} The children.
* @package
*/
Blockly.tree.BaseNode.prototype.getChildren = function() {
var children = [];
this.forEachChild(function(child) { children.push(child); });
return children;
};
/**
* @return {Blockly.tree.BaseNode} The previous sibling of this node.
* @protected
*/
Blockly.tree.BaseNode.prototype.getPreviousSibling = function() {
return this.previousSibling_;
};
/**
* @return {Blockly.tree.BaseNode} The next sibling of this node.
* @protected
*/
Blockly.tree.BaseNode.prototype.getNextSibling = function() {
return this.nextSibling_;
};
/**
* @return {boolean} Whether the node is the last sibling.
* @protected
*/
Blockly.tree.BaseNode.prototype.isLastSibling = function() {
return !this.nextSibling_;
};
/**
* @return {boolean} Whether the node is selected.
* @protected
*/
Blockly.tree.BaseNode.prototype.isSelected = function() {
return this.selected_;
};
/**
* Selects the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.select = function() {
var tree = this.getTree();
if (tree) {
tree.setSelectedItem(this);
}
};
/**
* Called from the tree to instruct the node change its selection state.
* @param {boolean} selected The new selection state.
* @protected
*/
Blockly.tree.BaseNode.prototype.setSelected = function(selected) {
if (this.selected_ == selected) {
return;
}
this.selected_ = selected;
this.updateRow();
var el = this.getElement();
if (el) {
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.SELECTED, selected);
if (selected) {
var treeElement = /** @type {!Element} */ (this.getTree().getElement());
Blockly.utils.aria.setState(treeElement,
Blockly.utils.aria.State.ACTIVEDESCENDANT, this.getId());
}
}
};
/**
* Sets the node to be expanded.
* @param {boolean} expanded Whether to expand or close the node.
* @package
*/
Blockly.tree.BaseNode.prototype.setExpanded = function(expanded) {
var isStateChange = expanded != this.expanded_;
var ce;
this.expanded_ = expanded;
var tree = this.getTree();
var el = this.getElement();
if (this.hasChildren()) {
if (!expanded && tree && this.contains(tree.getSelectedItem())) {
this.select();
}
if (el) {
ce = this.getChildrenElement();
if (ce) {
Blockly.utils.style.setElementShown(ce, expanded);
Blockly.utils.aria.setState(el, Blockly.utils.aria.State.EXPANDED, expanded);
// Make sure we have the HTML for the children here.
if (expanded && this.isInDocument() && !ce.hasChildNodes()) {
this.forEachChild(function(child) {
ce.appendChild(child.toDom());
});
this.forEachChild(function(child) { child.enterDocument(); });
}
}
this.updateExpandIcon();
}
} else {
ce = this.getChildrenElement();
if (ce) {
Blockly.utils.style.setElementShown(ce, false);
}
}
if (el) {
this.updateIcon_();
}
if (isStateChange) {
if (expanded) {
this.doNodeExpanded();
} else {
this.doNodeCollapsed();
}
}
};
/**
* Used to notify a node of that we have expanded it.
* Can be overidden by subclasses, see Blockly.tree.TreeNode.
* @protected
*/
Blockly.tree.BaseNode.prototype.doNodeExpanded = function() {
// NOP
};
/**
* Used to notify a node that we have collapsed it.
* Can be overidden by subclasses, see Blockly.tree.TreeNode.
* @protected
*/
Blockly.tree.BaseNode.prototype.doNodeCollapsed = function() {
// NOP
};
/**
* Toggles the expanded state of the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.toggle = function() {
this.setExpanded(!this.expanded_);
};
/**
* Creates HTML Element for the node.
* @return {!Element} HTML element
* @protected
*/
Blockly.tree.BaseNode.prototype.toDom = function() {
var nonEmptyAndExpanded = this.expanded_ && this.hasChildren();
var children = document.createElement('div');
children.style.backgroundPosition = this.getBackgroundPosition();
if (!nonEmptyAndExpanded) {
children.style.display = 'none';
}
if (nonEmptyAndExpanded) {
// children
this.forEachChild(function(child) { children.appendChild(child.toDom()); });
}
var node = document.createElement('div');
node.id = this.getId();
node.appendChild(this.getRowDom());
node.appendChild(children);
return node;
};
/**
* @return {number} The pixel indent of the row.
* @private
*/
Blockly.tree.BaseNode.prototype.getPixelIndent_ = function() {
return Math.max(0, (this.getDepth() - 1) * this.config_.indentWidth);
};
/**
* @return {!Element} The HTML element for the row.
* @protected
*/
Blockly.tree.BaseNode.prototype.getRowDom = function() {
var row = document.createElement('div');
row.className = this.getRowClassName();
row.style['padding-' + (this.rightToLeft_ ? 'right' : 'left')] =
this.getPixelIndent_() + 'px';
row.appendChild(this.getIconDom());
row.appendChild(this.getLabelDom());
return row;
};
/**
* @return {string} The class name for the row.
* @protected
*/
Blockly.tree.BaseNode.prototype.getRowClassName = function() {
var selectedClass = '';
if (this.isSelected()) {
selectedClass = ' ' + (this.config_.cssSelectedRow || '');
}
return this.config_.cssTreeRow + selectedClass;
};
/**
* @return {!Element} The HTML element for the label.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLabelDom = function() {
var label = document.createElement('span');
label.className = this.config_.cssItemLabel || '';
label.textContent = this.content;
return label;
};
/**
* @return {!Element} The HTML for the icon.
* @protected
*/
Blockly.tree.BaseNode.prototype.getIconDom = function() {
var icon = document.createElement('span');
icon.style.display = 'inline-block';
icon.className = this.getCalculatedIconClass();
return icon;
};
/**
* Gets the calculated icon class.
* @protected
*/
Blockly.tree.BaseNode.prototype.getCalculatedIconClass = function() {
throw Error('unimplemented abstract method');
};
/**
* @return {string} The background position style value.
* @protected
*/
Blockly.tree.BaseNode.prototype.getBackgroundPosition = function() {
return (this.isLastSibling() ? '-100' : (this.getDepth() - 1) *
this.config_.indentWidth) + 'px 0';
};
/**
* @return {Element} The element for the tree node.
* @override
*/
Blockly.tree.BaseNode.prototype.getElement = function() {
var el = Blockly.tree.BaseNode.superClass_.getElement.call(this);
if (!el) {
el = document.getElementById(this.getId());
this.setElementInternal(el);
}
return el;
};
/**
* @return {Element} The row is the div that is used to draw the node without
* the children.
* @package
*/
Blockly.tree.BaseNode.prototype.getRowElement = function() {
var el = this.getElement();
return el ? /** @type {Element} */ (el.firstChild) : null;
};
/**
* @return {Element} The icon element.
* @protected
*/
Blockly.tree.BaseNode.prototype.getIconElement = function() {
var el = this.getRowElement();
return el ? /** @type {Element} */ (el.firstChild) : null;
};
/**
* @return {Element} The label element.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLabelElement = function() {
var el = this.getRowElement();
return el && el.lastChild ?
/** @type {Element} */ (el.lastChild.previousSibling) :
null;
};
/**
* @return {Element} The div containing the children.
* @protected
*/
Blockly.tree.BaseNode.prototype.getChildrenElement = function() {
var el = this.getElement();
return el ? /** @type {Element} */ (el.lastChild) : null;
};
/**
* Updates the row styles.
* @protected
*/
Blockly.tree.BaseNode.prototype.updateRow = function() {
var rowEl = this.getRowElement();
if (rowEl) {
rowEl.className = this.getRowClassName();
}
};
/**
* Updates the expand icon of the node.
* @protected
*/
Blockly.tree.BaseNode.prototype.updateExpandIcon = function() {
var cel = this.getChildrenElement();
if (cel) {
cel.style.backgroundPosition = this.getBackgroundPosition();
}
};
/**
* Updates the icon of the node. Assumes that this.getElement() is created.
* @private
*/
Blockly.tree.BaseNode.prototype.updateIcon_ = function() {
this.getIconElement().className = this.getCalculatedIconClass();
};
/**
* Handles mouse down event.
* @param {!Event} e The browser event.
* @protected
*/
Blockly.tree.BaseNode.prototype.onMouseDown = function(e) {
var el = e.target;
// expand icon
var type = el.getAttribute('type');
if (type == 'expand' && this.hasChildren()) {
if (this.isUserCollapsible_) {
this.toggle();
}
return;
}
this.select();
this.updateRow();
};
/**
* Handles a click event.
* @param {!Event} e The browser event.
* @protected
*/
Blockly.tree.BaseNode.prototype.onClick_ = function(e) {
e.preventDefault();
};
/**
* Handles a key down event.
* @param {!Event} e The browser event.
* @return {boolean} The handled value.
* @protected
*/
Blockly.tree.BaseNode.prototype.onKeyDown = function(e) {
var handled = true;
switch (e.keyCode) {
case Blockly.utils.KeyCodes.RIGHT:
if (e.altKey) {
break;
}
handled = this.selectChild();
break;
case Blockly.utils.KeyCodes.LEFT:
if (e.altKey) {
break;
}
handled = this.selectParent();
break;
case Blockly.utils.KeyCodes.DOWN:
handled = this.selectNext();
break;
case Blockly.utils.KeyCodes.UP:
handled = this.selectPrevious();
break;
default:
handled = false;
}
if (handled) {
e.preventDefault();
}
return handled;
};
/**
* Select the next node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectNext = function() {
var nextNode = this.getNextShownNode();
if (nextNode) {
nextNode.select();
}
return true;
};
/**
* Select the previous node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectPrevious = function() {
var previousNode = this.getPreviousShownNode();
if (previousNode) {
previousNode.select();
}
return true;
};
/**
* Select the parent node or collapse the current node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectParent = function() {
if (this.hasChildren() && this.expanded_ && this.isUserCollapsible_) {
this.setExpanded(false);
} else {
var parent = this.getParent();
var tree = this.getTree();
// don't go to root if hidden
if (parent && (parent != tree)) {
parent.select();
}
}
return true;
};
/**
* Expand the current node if it's not already expanded, or select the
* child node.
* @return {boolean} True if the action has been handled, false otherwise.
* @package
*/
Blockly.tree.BaseNode.prototype.selectChild = function() {
if (this.hasChildren()) {
if (!this.expanded_) {
this.setExpanded(true);
} else {
this.getChildAt(0).select();
}
return true;
}
return false;
};
/**
* @return {Blockly.tree.BaseNode} The last shown descendant.
* @protected
*/
Blockly.tree.BaseNode.prototype.getLastShownDescendant = function() {
if (!this.expanded_ || !this.hasChildren()) {
return this;
}
// we know there is at least 1 child
return this.getChildAt(this.getChildCount() - 1).getLastShownDescendant();
};
/**
* @return {Blockly.tree.BaseNode} The next node to show or null if there isn't
* a next node to show.
* @protected
*/
Blockly.tree.BaseNode.prototype.getNextShownNode = function() {
if (this.hasChildren() && this.expanded_) {
return this.getChildAt(0);
} else {
var parent = this;
var next;
while (parent != this.getTree()) {
next = parent.getNextSibling();
if (next != null) {
return next;
}
parent = parent.getParent();
}
return null;
}
};
/**
* @return {Blockly.tree.BaseNode} The previous node to show.
* @protected
*/
Blockly.tree.BaseNode.prototype.getPreviousShownNode = function() {
var ps = this.getPreviousSibling();
if (ps != null) {
return ps.getLastShownDescendant();
}
var parent = this.getParent();
var tree = this.getTree();
if (parent == tree) {
return null;
}
// The root is the first node.
if (this == tree) {
return null;
}
return /** @type {Blockly.tree.BaseNode} */ (parent);
};
/**
* Internal method that is used to set the tree control on the node.
* @param {Blockly.tree.TreeControl} tree The tree control.
* @protected
*/
Blockly.tree.BaseNode.prototype.setTreeInternal = function(tree) {
if (this.tree != tree) {
this.tree = tree;
this.forEachChild(function(child) { child.setTreeInternal(tree); });
}
};