OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2010 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. |
| 4 |
| 5 cr.define('cr.ui', function() { |
| 6 // require cr.ui.define |
| 7 // require cr.ui.limitInputWidth |
| 8 |
| 9 /** |
| 10 * Helper function that finds the first ancestor tree item. |
| 11 * @param {!Element} el The element to start searching from. |
| 12 * @return {cr.ui.TreeItem} The found tree item or null if not found. |
| 13 */ |
| 14 function findTreeItem(el) { |
| 15 while (el && !(el instanceof TreeItem)) { |
| 16 el = el.parentNode; |
| 17 } |
| 18 return el; |
| 19 } |
| 20 |
| 21 /** |
| 22 * Creates a new tree element. |
| 23 * @param {Object=} opt_propertyBag Optional properties. |
| 24 * @constructor |
| 25 * @extends {HTMLElement} |
| 26 */ |
| 27 var Tree = cr.ui.define('tree'); |
| 28 |
| 29 Tree.prototype = { |
| 30 __proto__: HTMLElement.prototype, |
| 31 |
| 32 /** |
| 33 * Initializes the element. |
| 34 */ |
| 35 decorate: function() { |
| 36 // Make list focusable |
| 37 if (!this.hasAttribute('tabindex')) |
| 38 this.tabIndex = 0; |
| 39 |
| 40 this.addEventListener('click', this.handleClick); |
| 41 this.addEventListener('mousedown', this.handleMouseDown); |
| 42 this.addEventListener('dblclick', this.handleDblClick); |
| 43 this.addEventListener('keydown', this.handleKeyDown); |
| 44 }, |
| 45 |
| 46 /** |
| 47 * Returns the tree item that are children of this tree. |
| 48 */ |
| 49 get items() { |
| 50 return this.children; |
| 51 }, |
| 52 |
| 53 /** |
| 54 * Adds a tree item to the tree. |
| 55 * @param {!cr.ui.TreeItem} treeItem The item to add. |
| 56 */ |
| 57 add: function(treeItem) { |
| 58 this.appendChild(treeItem); |
| 59 }, |
| 60 |
| 61 /** |
| 62 * Adds a tree item at the given index. |
| 63 * @param {!cr.ui.TreeItem} treeItem The item to add. |
| 64 * @param {number} index The index where we want to add the item. |
| 65 */ |
| 66 addAt: function(treeItem, index) { |
| 67 this.insertBefore(treeItem, this.children[index]); |
| 68 }, |
| 69 |
| 70 /** |
| 71 * Removes a tree item child. |
| 72 * @param {!cr.ui.TreeItem} treeItem The tree item to remove. |
| 73 */ |
| 74 remove: function(treeItem) { |
| 75 this.removeChild(treeItem); |
| 76 }, |
| 77 |
| 78 /** |
| 79 * Handles click events on the tree and forwards the event to the relevant |
| 80 * tree items as necesary. |
| 81 * @param {Event} e The click event object. |
| 82 */ |
| 83 handleClick: function(e) { |
| 84 var treeItem = findTreeItem(e.target); |
| 85 if (treeItem) |
| 86 treeItem.handleClick(e); |
| 87 }, |
| 88 |
| 89 handleMouseDown: function(e) { |
| 90 if (e.button == 2) // right |
| 91 this.handleClick(e); |
| 92 }, |
| 93 |
| 94 /** |
| 95 * Handles double click events on the tree. |
| 96 * @param {Event} e The dblclick event object. |
| 97 */ |
| 98 handleDblClick: function(e) { |
| 99 var treeItem = findTreeItem(e.target); |
| 100 if (treeItem) |
| 101 treeItem.expanded = !treeItem.expanded; |
| 102 }, |
| 103 |
| 104 /** |
| 105 * Handles keydown events on the tree and updates selection and exanding |
| 106 * of tree items. |
| 107 * @param {Event} e The click event object. |
| 108 */ |
| 109 handleKeyDown: function(e) { |
| 110 var itemToSelect; |
| 111 if (e.ctrlKey) |
| 112 return; |
| 113 |
| 114 var item = this.selectedItem; |
| 115 |
| 116 var rtl = window.getComputedStyle(item).direction == 'rtl'; |
| 117 |
| 118 switch (e.keyIdentifier) { |
| 119 case 'Up': |
| 120 itemToSelect = item ? getPrevious(item) : |
| 121 this.items[this.items.length - 1]; |
| 122 break; |
| 123 case 'Down': |
| 124 itemToSelect = item ? getNext(item) : |
| 125 this.items[0]; |
| 126 break; |
| 127 case 'Left': |
| 128 case 'Right': |
| 129 // Don't let back/forward keyboard shortcuts be used. |
| 130 if (!cr.isMac && e.altKey || cr.isMac && e.metaKey) |
| 131 break; |
| 132 |
| 133 if (e.keyIdentifier == 'Left' && !rtl || |
| 134 e.keyIdentifier == 'Right' && rtl) { |
| 135 if (item.expanded) |
| 136 item.expanded = false; |
| 137 else |
| 138 itemToSelect = findTreeItem(item.parentNode); |
| 139 } else { |
| 140 if (!item.expanded) |
| 141 item.expanded = true; |
| 142 else |
| 143 itemToSelect = item.items[0]; |
| 144 } |
| 145 break; |
| 146 case 'Home': |
| 147 itemToSelect = this.items[0]; |
| 148 break; |
| 149 case 'End': |
| 150 itemToSelect = this.items[this.items.length - 1]; |
| 151 break; |
| 152 } |
| 153 |
| 154 if (itemToSelect) { |
| 155 itemToSelect.selected = true; |
| 156 e.preventDefault(); |
| 157 } |
| 158 }, |
| 159 |
| 160 /** |
| 161 * The selected tree item or null if none. |
| 162 * @type {cr.ui.TreeItem} |
| 163 */ |
| 164 get selectedItem() { |
| 165 return this.selectedItem_ || null; |
| 166 }, |
| 167 set selectedItem(item) { |
| 168 var oldSelectedItem = this.selectedItem_; |
| 169 if (oldSelectedItem != item) { |
| 170 // Set the selectedItem_ before deselecting the old item since we only |
| 171 // want one change when moving between items. |
| 172 this.selectedItem_ = item; |
| 173 |
| 174 if (oldSelectedItem) |
| 175 oldSelectedItem.selected = false; |
| 176 |
| 177 if (item) |
| 178 item.selected = true; |
| 179 |
| 180 cr.dispatchSimpleEvent(this, 'change'); |
| 181 } |
| 182 } |
| 183 }; |
| 184 |
| 185 /** |
| 186 * This is used as a blueprint for new tree item elements. |
| 187 * @type {!HTMLElement} |
| 188 */ |
| 189 var treeItemProto = (function() { |
| 190 var treeItem = cr.doc.createElement('div'); |
| 191 treeItem.className = 'tree-item'; |
| 192 treeItem.innerHTML = '<div class=tree-row>' + |
| 193 '<span class=expand-icon></span>' + |
| 194 '<span class=tree-label></span>' + |
| 195 '</div>' + |
| 196 '<div class=tree-children></div>'; |
| 197 return treeItem; |
| 198 })(); |
| 199 |
| 200 /** |
| 201 * Creates a new tree item. |
| 202 * @param {Object=} opt_propertyBag Optional properties. |
| 203 * @constructor |
| 204 * @extends {HTMLElement} |
| 205 */ |
| 206 var TreeItem = cr.ui.define(function() { |
| 207 return treeItemProto.cloneNode(true); |
| 208 }); |
| 209 |
| 210 TreeItem.prototype = { |
| 211 __proto__: HTMLElement.prototype, |
| 212 |
| 213 /** |
| 214 * Initializes the element. |
| 215 */ |
| 216 decorate: function() { |
| 217 |
| 218 }, |
| 219 |
| 220 /** |
| 221 * The tree items children. |
| 222 */ |
| 223 get items() { |
| 224 return this.lastElementChild.children; |
| 225 }, |
| 226 |
| 227 /** |
| 228 * Adds a tree item as a child. |
| 229 * @param {!cr.ui.TreeItem} child The child to add. |
| 230 */ |
| 231 add: function(child) { |
| 232 this.addAt(child, 0xffffffff); |
| 233 }, |
| 234 |
| 235 /** |
| 236 * Adds a tree item as a child at a given index. |
| 237 * @param {!cr.ui.TreeItem} child The child to add. |
| 238 * @param {number} index The index where to add the child. |
| 239 */ |
| 240 addAt: function(child, index) { |
| 241 this.lastElementChild.insertBefore(child, this.items[index]); |
| 242 if (this.items.length == 1) |
| 243 this.hasChildren_ = true; |
| 244 }, |
| 245 |
| 246 /** |
| 247 * Removes a child. |
| 248 * @param {!cr.ui.TreeItem} child The tree item child to remove. |
| 249 */ |
| 250 remove: function(child) { |
| 251 // If we removed the selected item we should become selected. |
| 252 var tree = this.tree; |
| 253 var selectedItem = tree.selectedItem; |
| 254 if (selectedItem && child.contains(selectedItem)) |
| 255 this.selected = true; |
| 256 |
| 257 this.lastElementChild.removeChild(child); |
| 258 if (this.items.length == 0) |
| 259 this.hasChildren_ = false; |
| 260 }, |
| 261 |
| 262 /** |
| 263 * The parent tree item. |
| 264 * @type {!cr.ui.Tree|cr.ui.TreeItem} |
| 265 */ |
| 266 get parentItem() { |
| 267 var p = this.parentNode; |
| 268 while (p && !(p instanceof TreeItem) && !(p instanceof Tree)) { |
| 269 p = p.parentNode; |
| 270 } |
| 271 return p; |
| 272 }, |
| 273 |
| 274 /** |
| 275 * The tree that the tree item belongs to or null of no added to a tree. |
| 276 * @type {cr.ui.Tree} |
| 277 */ |
| 278 get tree() { |
| 279 var t = this.parentItem; |
| 280 while (t && !(t instanceof Tree)) { |
| 281 t = t.parentItem; |
| 282 } |
| 283 return t; |
| 284 }, |
| 285 |
| 286 /** |
| 287 * Whether the tree item is expanded or not. |
| 288 * @type {boolean} |
| 289 */ |
| 290 get expanded() { |
| 291 return this.hasAttribute('expanded'); |
| 292 }, |
| 293 set expanded(b) { |
| 294 if (this.expanded == b) |
| 295 return; |
| 296 |
| 297 var treeChildren = this.lastElementChild; |
| 298 |
| 299 if (b) { |
| 300 if (this.mayHaveChildren_) { |
| 301 this.setAttribute('expanded', ''); |
| 302 treeChildren.setAttribute('expanded', ''); |
| 303 cr.dispatchSimpleEvent(this, 'expand', true); |
| 304 this.scrollIntoViewIfNeeded(false); |
| 305 } |
| 306 } else { |
| 307 var tree = this.tree; |
| 308 if (tree && !this.selected) { |
| 309 var oldSelected = tree.selectedItem; |
| 310 if (oldSelected && this.contains(oldSelected)) |
| 311 this.selected = true; |
| 312 } |
| 313 this.removeAttribute('expanded'); |
| 314 treeChildren.removeAttribute('expanded'); |
| 315 cr.dispatchSimpleEvent(this, 'collapse', true); |
| 316 } |
| 317 }, |
| 318 |
| 319 /** |
| 320 * Expands all parent items. |
| 321 */ |
| 322 reveal: function() { |
| 323 var pi = this.parentItem; |
| 324 while (pi && !(pi instanceof Tree)) { |
| 325 pi.expanded = true; |
| 326 pi = pi.parentItem; |
| 327 } |
| 328 }, |
| 329 |
| 330 /** |
| 331 * The element representing the row that gets highlighted. |
| 332 * @type {!HTMLElement} |
| 333 */ |
| 334 get rowElement() { |
| 335 return this.firstElementChild; |
| 336 }, |
| 337 |
| 338 /** |
| 339 * The element containing the label text and the icon. |
| 340 * @type {!HTMLElement} |
| 341 */ |
| 342 get labelElement() { |
| 343 return this.firstElementChild.lastElementChild; |
| 344 }, |
| 345 |
| 346 /** |
| 347 * The label text. |
| 348 * @type {string} |
| 349 */ |
| 350 get label() { |
| 351 return this.labelElement.textContent; |
| 352 }, |
| 353 set label(s) { |
| 354 this.labelElement.textContent = s; |
| 355 }, |
| 356 |
| 357 /** |
| 358 * The URL for the icon. |
| 359 * @type {string} |
| 360 */ |
| 361 get icon() { |
| 362 return window.getComputedStyle(this.labelElement). |
| 363 backgroundImage.slice(4, -1); |
| 364 }, |
| 365 set icon(icon) { |
| 366 return this.labelElement.style.backgroundImage = url(icon); |
| 367 }, |
| 368 |
| 369 /** |
| 370 * Whether the tree item is selected or not. |
| 371 * @type {boolean} |
| 372 */ |
| 373 get selected() { |
| 374 return this.hasAttribute('selected'); |
| 375 }, |
| 376 set selected(b) { |
| 377 if (this.selected == b) |
| 378 return; |
| 379 var rowItem = this.firstElementChild; |
| 380 var tree = this.tree; |
| 381 if (b) { |
| 382 this.setAttribute('selected', ''); |
| 383 rowItem.setAttribute('selected', ''); |
| 384 this.labelElement.scrollIntoViewIfNeeded(false); |
| 385 if (tree) |
| 386 tree.selectedItem = this; |
| 387 } else { |
| 388 this.removeAttribute('selected'); |
| 389 rowItem.removeAttribute('selected'); |
| 390 if (tree && tree.selectedItem == this) |
| 391 tree.selectedItem = null; |
| 392 } |
| 393 }, |
| 394 |
| 395 /** |
| 396 * Whether the tree item has children. |
| 397 * @type {boolean} |
| 398 */ |
| 399 get mayHaveChildren_() { |
| 400 return this.hasAttribute('may-have-children'); |
| 401 }, |
| 402 set mayHaveChildren_(b) { |
| 403 var rowItem = this.firstElementChild; |
| 404 if (b) { |
| 405 this.setAttribute('may-have-children', ''); |
| 406 rowItem.setAttribute('may-have-children', ''); |
| 407 } else { |
| 408 this.removeAttribute('may-have-children'); |
| 409 rowItem.removeAttribute('may-have-children'); |
| 410 } |
| 411 }, |
| 412 |
| 413 /** |
| 414 * Whether the tree item has children. |
| 415 * @type {boolean} |
| 416 */ |
| 417 get hasChildren() { |
| 418 return !!this.items[0]; |
| 419 }, |
| 420 |
| 421 /** |
| 422 * Whether the tree item has children. |
| 423 * @type {boolean} |
| 424 * @private |
| 425 */ |
| 426 set hasChildren_(b) { |
| 427 var rowItem = this.firstElementChild; |
| 428 this.setAttribute('has-children', b); |
| 429 rowItem.setAttribute('has-children', b); |
| 430 if (b) |
| 431 this.mayHaveChildren_ = true; |
| 432 }, |
| 433 |
| 434 /** |
| 435 * Called when the user clicks on a tree item. This is forwarded from the |
| 436 * cr.ui.Tree. |
| 437 * @param {Event} e The click event. |
| 438 */ |
| 439 handleClick: function(e) { |
| 440 if (e.target.className == 'expand-icon') |
| 441 this.expanded = !this.expanded; |
| 442 else |
| 443 this.selected = true; |
| 444 }, |
| 445 |
| 446 /** |
| 447 * Makes the tree item user editable. If the user renamed the item a |
| 448 * bubbling {@code rename} event is fired. |
| 449 * @type {boolean} |
| 450 */ |
| 451 set editing(editing) { |
| 452 var oldEditing = this.editing; |
| 453 if (editing == oldEditing) |
| 454 return; |
| 455 |
| 456 var self = this; |
| 457 var labelEl = this.labelElement; |
| 458 var text = this.label; |
| 459 var input; |
| 460 |
| 461 // Handles enter and escape which trigger reset and commit respectively. |
| 462 function handleKeydown(e) { |
| 463 // Make sure that the tree does not handle the key. |
| 464 e.stopPropagation(); |
| 465 |
| 466 // Calling tree.focus blurs the input which will make the tree item |
| 467 // non editable. |
| 468 switch (e.keyIdentifier) { |
| 469 case 'U+001B': // Esc |
| 470 input.value = text; |
| 471 // fall through |
| 472 case 'Enter': |
| 473 self.tree.focus(); |
| 474 } |
| 475 } |
| 476 |
| 477 function stopPropagation(e) { |
| 478 e.stopPropagation(); |
| 479 } |
| 480 |
| 481 if (editing) { |
| 482 this.selected = true; |
| 483 this.setAttribute('editing', ''); |
| 484 this.draggable = false; |
| 485 |
| 486 // We create an input[type=text] and copy over the label value. When |
| 487 // the input loses focus we set editing to false again. |
| 488 input = this.ownerDocument.createElement('input'); |
| 489 input.value = text; |
| 490 if (labelEl.firstChild) |
| 491 labelEl.replaceChild(input, labelEl.firstChild); |
| 492 else |
| 493 labelEl.appendChild(input); |
| 494 |
| 495 input.addEventListener('keydown', handleKeydown); |
| 496 input.addEventListener('blur', cr.bind(function() { |
| 497 this.editing = false; |
| 498 }, this)); |
| 499 |
| 500 // Make sure that double clicks do not expand and collapse the tree |
| 501 // item. |
| 502 var eventsToStop = ['mousedown', 'mouseup', 'contextmenu', 'dblclick']; |
| 503 eventsToStop.forEach(function(type) { |
| 504 input.addEventListener(type, stopPropagation); |
| 505 }); |
| 506 |
| 507 input.focus(); |
| 508 input.select(); |
| 509 cr.ui.limitInputWidth(input, this.rowElement, 20); |
| 510 // the padding and border of the tree-row |
| 511 |
| 512 this.oldLabel_ = text; |
| 513 } else { |
| 514 this.removeAttribute('editing'); |
| 515 this.draggable = true; |
| 516 input = labelEl.firstChild; |
| 517 var value = input.value; |
| 518 if (/^\s*$/.test(value)) { |
| 519 labelEl.textContent = this.oldLabel_; |
| 520 } else { |
| 521 labelEl.textContent = value; |
| 522 if (value != this.oldLabel_) { |
| 523 cr.dispatchSimpleEvent(this, 'rename', true); |
| 524 } |
| 525 } |
| 526 delete this.oldLabel_; |
| 527 } |
| 528 }, |
| 529 |
| 530 get editing() { |
| 531 return this.hasAttribute('editing'); |
| 532 } |
| 533 }; |
| 534 |
| 535 /** |
| 536 * Helper function that returns the next visible tree item. |
| 537 * @param {cr.ui.TreeItem} item The tree item. |
| 538 * @retrun {cr.ui.TreeItem} The found item or null. |
| 539 */ |
| 540 function getNext(item) { |
| 541 if (item.expanded) { |
| 542 var firstChild = item.items[0]; |
| 543 if (firstChild) { |
| 544 return firstChild; |
| 545 } |
| 546 } |
| 547 |
| 548 return getNextHelper(item); |
| 549 } |
| 550 |
| 551 /** |
| 552 * Another helper function that returns the next visible tree item. |
| 553 * @param {cr.ui.TreeItem} item The tree item. |
| 554 * @retrun {cr.ui.TreeItem} The found item or null. |
| 555 */ |
| 556 function getNextHelper(item) { |
| 557 if (!item) |
| 558 return null; |
| 559 |
| 560 var nextSibling = item.nextElementSibling; |
| 561 if (nextSibling) { |
| 562 return nextSibling; |
| 563 } |
| 564 return getNextHelper(item.parentItem); |
| 565 } |
| 566 |
| 567 /** |
| 568 * Helper function that returns the previous visible tree item. |
| 569 * @param {cr.ui.TreeItem} item The tree item. |
| 570 * @retrun {cr.ui.TreeItem} The found item or null. |
| 571 */ |
| 572 function getPrevious(item) { |
| 573 var previousSibling = item.previousElementSibling; |
| 574 return previousSibling ? getLastHelper(previousSibling) : item.parentItem; |
| 575 } |
| 576 |
| 577 /** |
| 578 * Helper function that returns the last visible tree item in the subtree. |
| 579 * @param {cr.ui.TreeItem} item The item to find the last visible item for. |
| 580 * @return {cr.ui.TreeItem} The found item or null. |
| 581 */ |
| 582 function getLastHelper(item) { |
| 583 if (!item) |
| 584 return null; |
| 585 if (item.expanded && item.hasChildren) { |
| 586 var lastChild = item.items[item.items.length - 1]; |
| 587 return getLastHelper(lastChild); |
| 588 } |
| 589 return item; |
| 590 } |
| 591 |
| 592 // Export |
| 593 return { |
| 594 Tree: Tree, |
| 595 TreeItem: TreeItem |
| 596 }; |
| 597 }); |
OLD | NEW |