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 {!Element} el The element to start searching from.
28 * @return {cr.ui.TreeItem} The found tree item or null if not found.
30 function findTreeItem(el) {
31 while (el && !(el instanceof TreeItem)) {
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(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(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 cr.dispatchSimpleEvent(this, 'change');
212 * @return {!ClientRect} The rect to use for the context menu.
214 getRectForContextMenu: function() {
215 // TODO(arv): Add trait support so we can share more code between trees
217 if (this.selectedItem)
218 return this.selectedItem.rowElement.getBoundingClientRect();
219 return this.getBoundingClientRect();
224 * Determines the visibility of icons next to the treeItem labels. If set to
225 * 'hidden', no space is reserved for icons and no icons are displayed next
226 * to treeItem labels. If set to 'parent', folder icons will be displayed
227 * next to expandable parent nodes. If set to 'all' folder icons will be
228 * displayed next to all nodes. Icons can be set using the treeItem's icon
231 cr.defineProperty(Tree, 'iconVisibility', cr.PropertyKind.ATTR);
234 * This is used as a blueprint for new tree item elements.
235 * @type {!HTMLElement}
237 var treeItemProto = (function() {
238 var treeItem = cr.doc.createElement('div');
239 treeItem.className = 'tree-item';
240 treeItem.innerHTML = '<div class=tree-row>' +
241 '<span class=expand-icon></span>' +
242 '<span class=tree-label></span>' +
244 '<div class=tree-children></div>';
245 treeItem.setAttribute('role', 'treeitem');
250 * Creates a new tree item.
251 * @param {Object=} opt_propertyBag Optional properties.
253 * @extends {HTMLElement}
255 var TreeItem = cr.ui.define(function() {
256 return treeItemProto.cloneNode(true);
259 TreeItem.prototype = {
260 __proto__: HTMLElement.prototype,
263 * Initializes the element.
265 decorate: function() {
270 * The tree items children.
273 return this.lastElementChild.children;
277 * The depth of the tree item.
287 * @param {number} depth The new depth.
290 setDepth_: function(depth) {
291 if (depth != this.depth_) {
292 this.rowElement.style.WebkitPaddingStart = Math.max(0, depth - 1) *
295 var items = this.items;
296 for (var i = 0, item; item = items[i]; i++) {
297 item.setDepth_(depth + 1);
303 * Adds a tree item as a child.
304 * @param {!cr.ui.TreeItem} child The child to add.
306 add: function(child) {
307 this.addAt(child, 0xffffffff);
311 * Adds a tree item as a child at a given index.
312 * @param {!cr.ui.TreeItem} child The child to add.
313 * @param {number} index The index where to add the child.
315 addAt: function(child, index) {
316 this.lastElementChild.insertBefore(child, this.items[index]);
317 if (this.items.length == 1)
318 this.hasChildren = true;
319 child.setDepth_(this.depth + 1);
324 * @param {!cr.ui.TreeItem} child The tree item child to remove.
326 remove: function(child) {
327 // If we removed the selected item we should become selected.
328 var tree = this.tree;
329 var selectedItem = tree.selectedItem;
330 if (selectedItem && child.contains(selectedItem))
331 this.selected = true;
333 this.lastElementChild.removeChild(child);
334 if (this.items.length == 0)
335 this.hasChildren = false;
339 * The parent tree item.
340 * @type {!cr.ui.Tree|cr.ui.TreeItem}
343 var p = this.parentNode;
344 while (p && !(p instanceof TreeItem) && !(p instanceof Tree)) {
351 * The tree that the tree item belongs to or null of no added to a tree.
355 var t = this.parentItem;
356 while (t && !(t instanceof Tree)) {
363 * Whether the tree item is expanded or not.
367 return this.hasAttribute('expanded');
370 if (this.expanded == b)
373 var treeChildren = this.lastElementChild;
376 if (this.mayHaveChildren_) {
377 this.setAttribute('expanded', '');
378 treeChildren.setAttribute('expanded', '');
379 cr.dispatchSimpleEvent(this, 'expand', true);
380 this.scrollIntoViewIfNeeded(false);
383 var tree = this.tree;
384 if (tree && !this.selected) {
385 var oldSelected = tree.selectedItem;
386 if (oldSelected && this.contains(oldSelected))
387 this.selected = true;
389 this.removeAttribute('expanded');
390 treeChildren.removeAttribute('expanded');
391 cr.dispatchSimpleEvent(this, 'collapse', true);
396 * Expands all parent items.
399 var pi = this.parentItem;
400 while (pi && !(pi instanceof Tree)) {
407 * The element representing the row that gets highlighted.
408 * @type {!HTMLElement}
411 return this.firstElementChild;
415 * The element containing the label text and the icon.
416 * @type {!HTMLElement}
419 return this.firstElementChild.lastElementChild;
427 return this.labelElement.textContent;
430 this.labelElement.textContent = s;
434 * The URL for the icon.
438 return getComputedStyle(this.labelElement).backgroundImage.slice(4, -1);
441 return this.labelElement.style.backgroundImage = url(icon);
445 * Whether the tree item is selected or not.
449 return this.hasAttribute('selected');
452 if (this.selected == b)
454 var rowItem = this.firstElementChild;
455 var tree = this.tree;
457 this.setAttribute('selected', '');
458 rowItem.setAttribute('selected', '');
460 this.labelElement.scrollIntoViewIfNeeded(false);
462 tree.selectedItem = this;
464 this.removeAttribute('selected');
465 rowItem.removeAttribute('selected');
466 if (tree && tree.selectedItem == this)
467 tree.selectedItem = null;
472 * Whether the tree item has children.
475 get mayHaveChildren_() {
476 return this.hasAttribute('may-have-children');
478 set mayHaveChildren_(b) {
479 var rowItem = this.firstElementChild;
481 this.setAttribute('may-have-children', '');
482 rowItem.setAttribute('may-have-children', '');
484 this.removeAttribute('may-have-children');
485 rowItem.removeAttribute('may-have-children');
490 * Whether the tree item has children.
494 return !!this.items[0];
498 * Whether the tree item has children.
502 var rowItem = this.firstElementChild;
503 this.setAttribute('has-children', b);
504 rowItem.setAttribute('has-children', b);
506 this.mayHaveChildren_ = true;
510 * Called when the user clicks on a tree item. This is forwarded from the
512 * @param {Event} e The click event.
514 handleClick: function(e) {
515 if (e.target.className == 'expand-icon')
516 this.expanded = !this.expanded;
518 this.selected = true;
522 * Makes the tree item user editable. If the user renamed the item a
523 * bubbling {@code rename} event is fired.
526 set editing(editing) {
527 var oldEditing = this.editing;
528 if (editing == oldEditing)
532 var labelEl = this.labelElement;
533 var text = this.label;
536 // Handles enter and escape which trigger reset and commit respectively.
537 function handleKeydown(e) {
538 // Make sure that the tree does not handle the key.
541 // Calling tree.focus blurs the input which will make the tree item
543 switch (e.keyIdentifier) {
544 case 'U+001B': // Esc
552 function stopPropagation(e) {
557 this.selected = true;
558 this.setAttribute('editing', '');
559 this.draggable = false;
561 // We create an input[type=text] and copy over the label value. When
562 // the input loses focus we set editing to false again.
563 input = this.ownerDocument.createElement('input');
565 if (labelEl.firstChild)
566 labelEl.replaceChild(input, labelEl.firstChild);
568 labelEl.appendChild(input);
570 input.addEventListener('keydown', handleKeydown);
571 input.addEventListener('blur', (function() {
572 this.editing = false;
575 // Make sure that double clicks do not expand and collapse the tree
577 var eventsToStop = ['mousedown', 'mouseup', 'contextmenu', 'dblclick'];
578 eventsToStop.forEach(function(type) {
579 input.addEventListener(type, stopPropagation);
582 // Wait for the input element to recieve focus before sizing it.
583 var rowElement = this.rowElement;
585 input.removeEventListener('focus', onFocus);
586 // 20 = the padding and border of the tree-row
587 cr.ui.limitInputWidth(input, rowElement, 100);
589 input.addEventListener('focus', onFocus);
593 this.oldLabel_ = text;
595 this.removeAttribute('editing');
596 this.draggable = true;
597 input = labelEl.firstChild;
598 var value = input.value;
599 if (/^\s*$/.test(value)) {
600 labelEl.textContent = this.oldLabel_;
602 labelEl.textContent = value;
603 if (value != this.oldLabel_) {
604 cr.dispatchSimpleEvent(this, 'rename', true);
607 delete this.oldLabel_;
612 return this.hasAttribute('editing');
617 * Helper function that returns the next visible tree item.
618 * @param {cr.ui.TreeItem} item The tree item.
619 * @return {cr.ui.TreeItem} The found item or null.
621 function getNext(item) {
623 var firstChild = item.items[0];
629 return getNextHelper(item);
633 * Another helper function that returns the next visible tree item.
634 * @param {cr.ui.TreeItem} item The tree item.
635 * @return {cr.ui.TreeItem} The found item or null.
637 function getNextHelper(item) {
641 var nextSibling = item.nextElementSibling;
645 return getNextHelper(item.parentItem);
649 * Helper function that returns the previous visible tree item.
650 * @param {cr.ui.TreeItem} item The tree item.
651 * @return {cr.ui.TreeItem} The found item or null.
653 function getPrevious(item) {
654 var previousSibling = item.previousElementSibling;
655 return previousSibling ? getLastHelper(previousSibling) : item.parentItem;
659 * Helper function that returns the last visible tree item in the subtree.
660 * @param {cr.ui.TreeItem} item The item to find the last visible item for.
661 * @return {cr.ui.TreeItem} The found item or null.
663 function getLastHelper(item) {
666 if (item.expanded && item.hasChildren) {
667 var lastChild = item.items[item.items.length - 1];
668 return getLastHelper(lastChild);