1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('cr.ui', function() {
6 // require cr.ui.define
7 // require cr.ui.limitInputWidth
10 * The number of pixels to indent per level.
17 * Returns the computed style for an element.
18 * @param {!Element} el The element to get the computed style for.
19 * @return {!CSSStyleDeclaration} The computed style.
21 function getComputedStyle(el) {
22 return el.ownerDocument.defaultView.getComputedStyle(el);
26 * Helper function that finds the first ancestor tree item.
27 * @param {Node} node The node to start searching from.
28 * @return {cr.ui.TreeItem} The found tree item or null if not found.
30 function findTreeItem(node) {
31 while (node && !(node instanceof TreeItem)) {
32 node = node.parentNode;
38 * Creates a new tree element.
39 * @param {Object=} opt_propertyBag Optional properties.
41 * @extends {HTMLElement}
43 var Tree = cr.ui.define('tree');
46 __proto__: HTMLElement.prototype,
49 * Initializes the element.
51 decorate: function() {
52 // Make list focusable
53 if (!this.hasAttribute('tabindex'))
56 this.addEventListener('click', this.handleClick);
57 this.addEventListener('mousedown', this.handleMouseDown);
58 this.addEventListener('dblclick', this.handleDblClick);
59 this.addEventListener('keydown', this.handleKeyDown);
63 * Returns the tree item that are children of this tree.
70 * Adds a tree item to the tree.
71 * @param {!cr.ui.TreeItem} treeItem The item to add.
73 add: function(treeItem) {
74 this.addAt(treeItem, 0xffffffff);
78 * Adds a tree item at the given index.
79 * @param {!cr.ui.TreeItem} treeItem The item to add.
80 * @param {number} index The index where we want to add the item.
82 addAt: function(treeItem, index) {
83 this.insertBefore(treeItem, this.children[index]);
84 treeItem.setDepth_(this.depth + 1);
88 * Removes a tree item child.
89 * @param {!cr.ui.TreeItem} treeItem The tree item to remove.
91 remove: function(treeItem) {
92 this.removeChild(treeItem);
96 * The depth of the node. This is 0 for the tree itself.
104 * Handles click events on the tree and forwards the event to the relevant
105 * tree items as necesary.
106 * @param {Event} e The click event object.
108 handleClick: function(e) {
109 var treeItem = findTreeItem(/** @type {!Node} */(e.target));
111 treeItem.handleClick(e);
114 handleMouseDown: function(e) {
115 if (e.button == 2) // right
120 * Handles double click events on the tree.
121 * @param {Event} e The dblclick event object.
123 handleDblClick: function(e) {
124 var treeItem = findTreeItem(/** @type {!Node} */(e.target));
126 treeItem.expanded = !treeItem.expanded;
130 * Handles keydown events on the tree and updates selection and exanding
132 * @param {Event} e The click event object.
134 handleKeyDown: function(e) {
139 var item = this.selectedItem;
143 var rtl = getComputedStyle(item).direction == 'rtl';
145 switch (e.keyIdentifier) {
147 itemToSelect = item ? getPrevious(item) :
148 this.items[this.items.length - 1];
151 itemToSelect = item ? getNext(item) :
156 // Don't let back/forward keyboard shortcuts be used.
157 if (!cr.isMac && e.altKey || cr.isMac && e.metaKey)
160 if (e.keyIdentifier == 'Left' && !rtl ||
161 e.keyIdentifier == 'Right' && rtl) {
163 item.expanded = false;
165 itemToSelect = findTreeItem(item.parentNode);
168 item.expanded = true;
170 itemToSelect = item.items[0];
174 itemToSelect = this.items[0];
177 itemToSelect = this.items[this.items.length - 1];
182 itemToSelect.selected = true;
188 * The selected tree item or null if none.
189 * @type {cr.ui.TreeItem}
192 return this.selectedItem_ || null;
194 set selectedItem(item) {
195 var oldSelectedItem = this.selectedItem_;
196 if (oldSelectedItem != item) {
197 // Set the selectedItem_ before deselecting the old item since we only
198 // want one change when moving between items.
199 this.selectedItem_ = item;
202 oldSelectedItem.selected = false;
205 item.selected = true;
207 this.setAttribute('aria-activedescendant', item.id);
209 this.removeAttribute('aria-activedescendant');
211 cr.dispatchSimpleEvent(this, 'change');
216 * @return {!ClientRect} The rect to use for the context menu.
218 getRectForContextMenu: function() {
219 // TODO(arv): Add trait support so we can share more code between trees
221 if (this.selectedItem)
222 return this.selectedItem.rowElement.getBoundingClientRect();
223 return this.getBoundingClientRect();
228 * Determines the visibility of icons next to the treeItem labels. If set to
229 * 'hidden', no space is reserved for icons and no icons are displayed next
230 * to treeItem labels. If set to 'parent', folder icons will be displayed
231 * next to expandable parent nodes. If set to 'all' folder icons will be
232 * displayed next to all nodes. Icons can be set using the treeItem's icon
235 cr.defineProperty(Tree, 'iconVisibility', cr.PropertyKind.ATTR);
238 * Incremental counter for an auto generated ID of the tree item. This will
239 * be incremented per element, so each element never share same ID.
243 var treeItemAutoGeneratedIdCounter = 0;
246 * This is used as a blueprint for new tree item elements.
247 * @type {!HTMLElement}
249 var treeItemProto = (function() {
250 var treeItem = cr.doc.createElement('div');
251 treeItem.className = 'tree-item';
252 treeItem.innerHTML = '<div class=tree-row>' +
253 '<span class=expand-icon></span>' +
254 '<span class=tree-label></span>' +
256 '<div class=tree-children></div>';
257 treeItem.setAttribute('role', 'treeitem');
262 * Creates a new tree item.
263 * @param {Object=} opt_propertyBag Optional properties.
265 * @extends {HTMLElement}
267 var TreeItem = cr.ui.define(function() {
268 var treeItem = treeItemProto.cloneNode(true);
269 treeItem.id = 'tree-item-autogen-id-' + treeItemAutoGeneratedIdCounter++;
273 TreeItem.prototype = {
274 __proto__: HTMLElement.prototype,
277 * Initializes the element.
279 decorate: function() {
284 * The tree items children.
287 return this.lastElementChild.children;
291 * The depth of the tree item.
301 * @param {number} depth The new depth.
304 setDepth_: function(depth) {
305 if (depth != this.depth_) {
306 this.rowElement.style.WebkitPaddingStart = Math.max(0, depth - 1) *
309 var items = this.items;
310 for (var i = 0, item; item = items[i]; i++) {
311 item.setDepth_(depth + 1);
317 * Adds a tree item as a child.
318 * @param {!cr.ui.TreeItem} child The child to add.
320 add: function(child) {
321 this.addAt(child, 0xffffffff);
325 * Adds a tree item as a child at a given index.
326 * @param {!cr.ui.TreeItem} child The child to add.
327 * @param {number} index The index where to add the child.
329 addAt: function(child, index) {
330 this.lastElementChild.insertBefore(child, this.items[index]);
331 if (this.items.length == 1)
332 this.hasChildren = true;
333 child.setDepth_(this.depth + 1);
338 * @param {!cr.ui.TreeItem} child The tree item child to remove.
340 remove: function(child) {
341 // If we removed the selected item we should become selected.
342 var tree = this.tree;
343 var selectedItem = tree.selectedItem;
344 if (selectedItem && child.contains(selectedItem))
345 this.selected = true;
347 this.lastElementChild.removeChild(child);
348 if (this.items.length == 0)
349 this.hasChildren = false;
353 * The parent tree item.
354 * @type {!cr.ui.Tree|cr.ui.TreeItem}
357 var p = this.parentNode;
358 while (p && !(p instanceof TreeItem) && !(p instanceof Tree)) {
365 * The tree that the tree item belongs to or null of no added to a tree.
369 var t = this.parentItem;
370 while (t && !(t instanceof Tree)) {
377 * Whether the tree item is expanded or not.
381 return this.hasAttribute('expanded');
384 if (this.expanded == b)
387 var treeChildren = this.lastElementChild;
390 if (this.mayHaveChildren_) {
391 this.setAttribute('expanded', '');
392 treeChildren.setAttribute('expanded', '');
393 cr.dispatchSimpleEvent(this, 'expand', true);
394 this.scrollIntoViewIfNeeded(false);
397 var tree = this.tree;
398 if (tree && !this.selected) {
399 var oldSelected = tree.selectedItem;
400 if (oldSelected && this.contains(oldSelected))
401 this.selected = true;
403 this.removeAttribute('expanded');
404 treeChildren.removeAttribute('expanded');
405 cr.dispatchSimpleEvent(this, 'collapse', true);
410 * Expands all parent items.
413 var pi = this.parentItem;
414 while (pi && !(pi instanceof Tree)) {
421 * The element representing the row that gets highlighted.
422 * @type {!HTMLElement}
425 return this.firstElementChild;
429 * The element containing the label text and the icon.
430 * @type {!HTMLElement}
433 return this.firstElementChild.lastElementChild;
441 return this.labelElement.textContent;
444 this.labelElement.textContent = s;
448 * The URL for the icon.
452 return getComputedStyle(this.labelElement).backgroundImage.slice(4, -1);
455 return this.labelElement.style.backgroundImage = url(icon);
459 * Whether the tree item is selected or not.
463 return this.hasAttribute('selected');
466 if (this.selected == b)
468 var rowItem = this.firstElementChild;
469 var tree = this.tree;
471 this.setAttribute('selected', '');
472 rowItem.setAttribute('selected', '');
474 this.labelElement.scrollIntoViewIfNeeded(false);
476 tree.selectedItem = this;
478 this.removeAttribute('selected');
479 rowItem.removeAttribute('selected');
480 if (tree && tree.selectedItem == this)
481 tree.selectedItem = null;
486 * Whether the tree item has children.
489 get mayHaveChildren_() {
490 return this.hasAttribute('may-have-children');
492 set mayHaveChildren_(b) {
493 var rowItem = this.firstElementChild;
495 this.setAttribute('may-have-children', '');
496 rowItem.setAttribute('may-have-children', '');
498 this.removeAttribute('may-have-children');
499 rowItem.removeAttribute('may-have-children');
504 * Whether the tree item has children.
508 return !!this.items[0];
512 * Whether the tree item has children.
516 var rowItem = this.firstElementChild;
517 this.setAttribute('has-children', b);
518 rowItem.setAttribute('has-children', b);
520 this.mayHaveChildren_ = true;
524 * Called when the user clicks on a tree item. This is forwarded from the
526 * @param {Event} e The click event.
528 handleClick: function(e) {
529 if (e.target.className == 'expand-icon')
530 this.expanded = !this.expanded;
532 this.selected = true;
536 * Makes the tree item user editable. If the user renamed the item a
537 * bubbling {@code rename} event is fired.
540 set editing(editing) {
541 var oldEditing = this.editing;
542 if (editing == oldEditing)
546 var labelEl = this.labelElement;
547 var text = this.label;
550 // Handles enter and escape which trigger reset and commit respectively.
551 function handleKeydown(e) {
552 // Make sure that the tree does not handle the key.
555 // Calling tree.focus blurs the input which will make the tree item
557 switch (e.keyIdentifier) {
558 case 'U+001B': // Esc
566 function stopPropagation(e) {
571 this.selected = true;
572 this.setAttribute('editing', '');
573 this.draggable = false;
575 // We create an input[type=text] and copy over the label value. When
576 // the input loses focus we set editing to false again.
577 input = this.ownerDocument.createElement('input');
579 if (labelEl.firstChild)
580 labelEl.replaceChild(input, labelEl.firstChild);
582 labelEl.appendChild(input);
584 input.addEventListener('keydown', handleKeydown);
585 input.addEventListener('blur', (function() {
586 this.editing = false;
589 // Make sure that double clicks do not expand and collapse the tree
591 var eventsToStop = ['mousedown', 'mouseup', 'contextmenu', 'dblclick'];
592 eventsToStop.forEach(function(type) {
593 input.addEventListener(type, stopPropagation);
596 // Wait for the input element to recieve focus before sizing it.
597 var rowElement = this.rowElement;
598 var onFocus = function() {
599 input.removeEventListener('focus', onFocus);
600 // 20 = the padding and border of the tree-row
601 cr.ui.limitInputWidth(input, rowElement, 100);
603 input.addEventListener('focus', onFocus);
607 this.oldLabel_ = text;
609 this.removeAttribute('editing');
610 this.draggable = true;
611 input = labelEl.firstChild;
612 var value = input.value;
613 if (/^\s*$/.test(value)) {
614 labelEl.textContent = this.oldLabel_;
616 labelEl.textContent = value;
617 if (value != this.oldLabel_) {
618 cr.dispatchSimpleEvent(this, 'rename', true);
621 delete this.oldLabel_;
626 return this.hasAttribute('editing');
631 * Helper function that returns the next visible tree item.
632 * @param {cr.ui.TreeItem} item The tree item.
633 * @return {cr.ui.TreeItem} The found item or null.
635 function getNext(item) {
637 var firstChild = item.items[0];
643 return getNextHelper(item);
647 * Another helper function that returns the next visible tree item.
648 * @param {cr.ui.TreeItem} item The tree item.
649 * @return {cr.ui.TreeItem} The found item or null.
651 function getNextHelper(item) {
655 var nextSibling = item.nextElementSibling;
657 return assertInstanceof(nextSibling, cr.ui.TreeItem);
658 return getNextHelper(item.parentItem);
662 * Helper function that returns the previous visible tree item.
663 * @param {cr.ui.TreeItem} item The tree item.
664 * @return {cr.ui.TreeItem} The found item or null.
666 function getPrevious(item) {
667 var previousSibling = assertInstanceof(item.previousElementSibling,
669 return previousSibling ? getLastHelper(previousSibling) : item.parentItem;
673 * Helper function that returns the last visible tree item in the subtree.
674 * @param {cr.ui.TreeItem} item The item to find the last visible item for.
675 * @return {cr.ui.TreeItem} The found item or null.
677 function getLastHelper(item) {
680 if (item.expanded && item.hasChildren) {
681 var lastChild = item.items[item.items.length - 1];
682 return getLastHelper(lastChild);