Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(56)

Side by Side Diff: chrome/browser/resources/options2/intents_list.js

Issue 8937011: Revert "Options2: Pull the trigger." (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2011 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 // TODO(gbillock): refactor this together with CookiesList once we have
6 // a better sense from UX design what it'll look like and so what'll be shared.
7 cr.define('options', function() {
8 const DeletableItemList = options.DeletableItemList;
9 const DeletableItem = options.DeletableItem;
10 const ArrayDataModel = cr.ui.ArrayDataModel;
11 const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
12 const localStrings = new LocalStrings();
13
14 /**
15 * Returns the item's height, like offsetHeight but such that it works better
16 * when the page is zoomed. See the similar calculation in @{code cr.ui.List}.
17 * This version also accounts for the animation done in this file.
18 * @param {Element} item The item to get the height of.
19 * @return {number} The height of the item, calculated with zooming in mind.
20 */
21 function getItemHeight(item) {
22 var height = item.style.height;
23 // Use the fixed animation target height if set, in case the element is
24 // currently being animated and we'd get an intermediate height below.
25 if (height && height.substr(-2) == 'px')
26 return parseInt(height.substr(0, height.length - 2));
27 return item.getBoundingClientRect().height;
28 }
29
30 // Map of parent pathIDs to node objects.
31 var parentLookup = {};
32
33 // Pending requests for child information.
34 var lookupRequests = {};
35
36 /**
37 * Creates a new list item for intent service data. Note that these are
38 * created and destroyed lazily as they scroll into and out of view,
39 * so they must be stateless. We cache the expanded item in
40 * @{code IntentsList} though, so it can keep state.
41 * (Mostly just which item is selected.)
42 *
43 * @param {Object} origin Data used to create an intents list item.
44 * @param {IntentsList} list The list that will contain this item.
45 * @constructor
46 * @extends {DeletableItem}
47 */
48 function IntentsListItem(origin, list) {
49 var listItem = new DeletableItem(null);
50 listItem.__proto__ = IntentsListItem.prototype;
51
52 listItem.origin = origin;
53 listItem.list = list;
54 listItem.decorate();
55
56 // This hooks up updateOrigin() to the list item, makes the top-level
57 // tree nodes (i.e., origins) register their IDs in parentLookup, and
58 // causes them to request their children if they have none. Note that we
59 // have special logic in the setter for the parent property to make sure
60 // that we can still garbage collect list items when they scroll out of
61 // view, even though it appears that we keep a direct reference.
62 if (origin) {
63 origin.parent = listItem;
64 origin.updateOrigin();
65 }
66
67 return listItem;
68 }
69
70 IntentsListItem.prototype = {
71 __proto__: DeletableItem.prototype,
72
73 /** @inheritDoc */
74 decorate: function() {
75 this.siteChild = this.ownerDocument.createElement('div');
76 this.siteChild.className = 'intents-site';
77 this.dataChild = this.ownerDocument.createElement('div');
78 this.dataChild.className = 'intents-data';
79 this.itemsChild = this.ownerDocument.createElement('div');
80 this.itemsChild.className = 'intents-items';
81 this.infoChild = this.ownerDocument.createElement('div');
82 this.infoChild.className = 'intents-details';
83 this.infoChild.hidden = true;
84 var remove = this.ownerDocument.createElement('button');
85 remove.textContent = localStrings.getString('removeIntent');
86 remove.onclick = this.removeIntent_.bind(this);
87 this.infoChild.appendChild(remove);
88 var content = this.contentElement;
89 content.appendChild(this.siteChild);
90 content.appendChild(this.dataChild);
91 content.appendChild(this.itemsChild);
92 this.itemsChild.appendChild(this.infoChild);
93 if (this.origin && this.origin.data) {
94 this.siteChild.textContent = this.origin.data.site;
95 this.siteChild.setAttribute('title', this.origin.data.site);
96 }
97 this.itemList_ = [];
98 },
99
100 /** @type {boolean} */
101 get expanded() {
102 return this.expanded_;
103 },
104 set expanded(expanded) {
105 if (this.expanded_ == expanded)
106 return;
107 this.expanded_ = expanded;
108 if (expanded) {
109 var oldExpanded = this.list.expandedItem;
110 this.list.expandedItem = this;
111 this.updateItems_();
112 if (oldExpanded)
113 oldExpanded.expanded = false;
114 this.classList.add('show-items');
115 this.dataChild.hidden = true;
116 } else {
117 if (this.list.expandedItem == this) {
118 this.list.expandedItem = null;
119 }
120 this.style.height = '';
121 this.itemsChild.style.height = '';
122 this.classList.remove('show-items');
123 this.dataChild.hidden = false;
124 }
125 },
126
127 /**
128 * The callback for the "remove" button shown when an item is selected.
129 * Requests that the currently selected intent service be removed.
130 * @private
131 */
132 removeIntent_: function() {
133 if (this.selectedIndex_ >= 0) {
134 var item = this.itemList_[this.selectedIndex_];
135 if (item && item.node)
136 chrome.send('removeIntent', [item.node.pathId]);
137 }
138 },
139
140 /**
141 * Disable animation within this intents list item, in preparation for
142 * making changes that will need to be animated. Makes it possible to
143 * measure the contents without displaying them, to set animation targets.
144 * @private
145 */
146 disableAnimation_: function() {
147 this.itemsHeight_ = getItemHeight(this.itemsChild);
148 this.classList.add('measure-items');
149 },
150
151 /**
152 * Enable animation after changing the contents of this intents list item.
153 * See @{code disableAnimation_}.
154 * @private
155 */
156 enableAnimation_: function() {
157 if (!this.classList.contains('measure-items'))
158 this.disableAnimation_();
159 this.itemsChild.style.height = '';
160 // This will force relayout in order to calculate the new heights.
161 var itemsHeight = getItemHeight(this.itemsChild);
162 var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_;
163 this.itemsChild.style.height = this.itemsHeight_ + 'px';
164 // Force relayout before enabling animation, so that if we have
165 // changed things since the last layout, they will not be animated
166 // during subsequent layouts.
167 this.itemsChild.offsetHeight;
168 this.classList.remove('measure-items');
169 this.itemsChild.style.height = itemsHeight + 'px';
170 this.style.height = fixedHeight + 'px';
171 },
172
173 /**
174 * Updates the origin summary to reflect changes in its items.
175 * Both IntentsListItem and IntentsTreeNode implement this API.
176 * This implementation scans the descendants to update the text.
177 */
178 updateOrigin: function() {
179 var text = '';
180 for (var i = 0; i < this.origin.children.length; ++i) {
181 if (text.length > 0)
182 text += ', ' + this.origin.children[i].data.action;
183 else
184 text = this.origin.children[i].data.action;
185 }
186 this.dataChild.textContent = text;
187
188 if (this.expanded)
189 this.updateItems_();
190 },
191
192 /**
193 * Updates the items section to reflect changes, animating to the new state.
194 * Removes existing contents and calls @{code IntentsTreeNode.createItems}.
195 * @private
196 */
197 updateItems_: function() {
198 this.disableAnimation_();
199 this.itemsChild.textContent = '';
200 this.infoChild.hidden = true;
201 this.selectedIndex_ = -1;
202 this.itemList_ = [];
203 if (this.origin)
204 this.origin.createItems(this);
205 this.itemsChild.appendChild(this.infoChild);
206 this.enableAnimation_();
207 },
208
209 /**
210 * Append a new intents node "bubble" to this list item.
211 * @param {IntentsTreeNode} node The intents node to add a bubble for.
212 * @param {Element} div The DOM element for the bubble itself.
213 * @return {number} The index the bubble was added at.
214 */
215 appendItem: function(node, div) {
216 this.itemList_.push({node: node, div: div});
217 this.itemsChild.appendChild(div);
218 return this.itemList_.length - 1;
219 },
220
221 /**
222 * The currently selected intents node ("intents bubble") index.
223 * @type {number}
224 * @private
225 */
226 selectedIndex_: -1,
227
228 /**
229 * Get the currently selected intents node ("intents bubble") index.
230 * @type {number}
231 */
232 get selectedIndex() {
233 return this.selectedIndex_;
234 },
235
236 /**
237 * Set the currently selected intents node ("intents bubble") index to
238 * @{code itemIndex}, unselecting any previously selected node first.
239 * @param {number} itemIndex The index to set as the selected index.
240 * TODO: KILL THIS
241 */
242 set selectedIndex(itemIndex) {
243 // Get the list index up front before we change anything.
244 var index = this.list.getIndexOfListItem(this);
245 // Unselect any previously selected item.
246 if (this.selectedIndex_ >= 0) {
247 var item = this.itemList_[this.selectedIndex_];
248 if (item && item.div)
249 item.div.removeAttribute('selected');
250 }
251 // Special case: decrementing -1 wraps around to the end of the list.
252 if (itemIndex == -2)
253 itemIndex = this.itemList_.length - 1;
254 // Check if we're going out of bounds and hide the item details.
255 if (itemIndex < 0 || itemIndex >= this.itemList_.length) {
256 this.selectedIndex_ = -1;
257 this.disableAnimation_();
258 this.infoChild.hidden = true;
259 this.enableAnimation_();
260 return;
261 }
262 // Set the new selected item and show the item details for it.
263 this.selectedIndex_ = itemIndex;
264 this.itemList_[itemIndex].div.setAttribute('selected', '');
265 this.disableAnimation_();
266 this.infoChild.hidden = false;
267 this.enableAnimation_();
268 // If we're near the bottom of the list this may cause the list item to go
269 // beyond the end of the visible area. Fix it after the animation is done.
270 var list = this.list;
271 window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150);
272 },
273 };
274
275 /**
276 * {@code IntentsTreeNode}s mirror the structure of the intents tree lazily,
277 * and contain all the actual data used to generate the
278 * {@code IntentsListItem}s.
279 * @param {Object} data The data object for this node.
280 * @constructor
281 */
282 function IntentsTreeNode(data) {
283 this.data = data;
284 this.children = [];
285 }
286
287 IntentsTreeNode.prototype = {
288 /**
289 * Insert an intents tree node at the given index.
290 * Both IntentsList and IntentsTreeNode implement this API.
291 * @param {Object} data The data object for the node to add.
292 * @param {number} index The index at which to insert the node.
293 */
294 insertAt: function(data, index) {
295 var child = new IntentsTreeNode(data);
296 this.children.splice(index, 0, child);
297 child.parent = this;
298 this.updateOrigin();
299 },
300
301 /**
302 * Remove an intents tree node from the given index.
303 * Both IntentsList and IntentsTreeNode implement this API.
304 * @param {number} index The index of the tree node to remove.
305 */
306 remove: function(index) {
307 if (index < this.children.length) {
308 this.children.splice(index, 1);
309 this.updateOrigin();
310 }
311 },
312
313 /**
314 * Clears all children.
315 * Both IntentsList and IntentsTreeNode implement this API.
316 * It is used by IntentsList.loadChildren().
317 */
318 clear: function() {
319 // We might leave some garbage in parentLookup for removed children.
320 // But that should be OK because parentLookup is cleared when we
321 // reload the tree.
322 this.children = [];
323 this.updateOrigin();
324 },
325
326 /**
327 * The counter used by startBatchUpdates() and endBatchUpdates().
328 * @type {number}
329 */
330 batchCount_: 0,
331
332 /**
333 * See cr.ui.List.startBatchUpdates().
334 * Both IntentsList (via List) and IntentsTreeNode implement this API.
335 */
336 startBatchUpdates: function() {
337 this.batchCount_++;
338 },
339
340 /**
341 * See cr.ui.List.endBatchUpdates().
342 * Both IntentsList (via List) and IntentsTreeNode implement this API.
343 */
344 endBatchUpdates: function() {
345 if (!--this.batchCount_)
346 this.updateOrigin();
347 },
348
349 /**
350 * Requests updating the origin summary to reflect changes in this item.
351 * Both IntentsListItem and IntentsTreeNode implement this API.
352 */
353 updateOrigin: function() {
354 if (!this.batchCount_ && this.parent)
355 this.parent.updateOrigin();
356 },
357
358 /**
359 * Create the intents services rows for this node.
360 * Append the rows to @{code item}.
361 * @param {IntentsListItem} item The intents list item to create items in.
362 */
363 createItems: function(item) {
364 if (this.children.length > 0) {
365 for (var i = 0; i < this.children.length; ++i)
366 this.children[i].createItems(item);
367 } else if (this.data && !this.data.hasChildren) {
368 var div = item.ownerDocument.createElement('div');
369 div.className = 'intents-item';
370 // Help out screen readers and such: this is a clickable thing.
371 div.setAttribute('role', 'button');
372
373 var divAction = item.ownerDocument.createElement('div');
374 divAction.className = 'intents-item-action';
375 divAction.textContent = this.data.action;
376 div.appendChild(divAction);
377
378 var divTypes = item.ownerDocument.createElement('div');
379 divTypes.className = 'intents-item-types';
380 var text = "";
381 for (var i = 0; i < this.data.types.length; ++i) {
382 if (text != "")
383 text += ", ";
384 text += this.data.types[i];
385 }
386 divTypes.textContent = text;
387 div.appendChild(divTypes);
388
389 var divUrl = item.ownerDocument.createElement('div');
390 divUrl.className = 'intents-item-url';
391 divUrl.textContent = this.data.url;
392 div.appendChild(divUrl);
393
394 var index = item.appendItem(this, div);
395 div.onclick = function() {
396 if (item.selectedIndex == index)
397 item.selectedIndex = -1;
398 else
399 item.selectedIndex = index;
400 };
401 }
402 },
403
404 /**
405 * The parent of this intents tree node.
406 * @type {?IntentsTreeNode|IntentsListItem}
407 */
408 get parent(parent) {
409 // See below for an explanation of this special case.
410 if (typeof this.parent_ == 'number')
411 return this.list_.getListItemByIndex(this.parent_);
412 return this.parent_;
413 },
414 set parent(parent) {
415 if (parent == this.parent)
416 return;
417
418 if (parent instanceof IntentsListItem) {
419 // If the parent is to be a IntentsListItem, then we keep the reference
420 // to it by its containing list and list index, rather than directly.
421 // This allows the list items to be garbage collected when they scroll
422 // out of view (except the expanded item, which we cache). This is
423 // transparent except in the setter and getter, where we handle it.
424 this.parent_ = parent.listIndex;
425 this.list_ = parent.list;
426 parent.addEventListener('listIndexChange',
427 this.parentIndexChanged_.bind(this));
428 } else {
429 this.parent_ = parent;
430 }
431
432
433 if (parent)
434 parentLookup[this.pathId] = this;
435 else
436 delete parentLookup[this.pathId];
437
438 if (this.data && this.data.hasChildren &&
439 !this.children.length && !lookupRequests[this.pathId]) {
440 lookupRequests[this.pathId] = true;
441 chrome.send('loadIntents', [this.pathId]);
442 }
443 },
444
445 /**
446 * Called when the parent is a IntentsListItem whose index has changed.
447 * See the code above that avoids keeping a direct reference to
448 * IntentsListItem parents, to allow them to be garbage collected.
449 * @private
450 */
451 parentIndexChanged_: function(event) {
452 if (typeof this.parent_ == 'number') {
453 this.parent_ = event.newValue;
454 // We set a timeout to update the origin, rather than doing it right
455 // away, because this callback may occur while the list items are
456 // being repopulated following a scroll event. Calling updateOrigin()
457 // immediately could trigger relayout that would reset the scroll
458 // position within the list, among other things.
459 window.setTimeout(this.updateOrigin.bind(this), 0);
460 }
461 },
462
463 /**
464 * The intents tree path id.
465 * @type {string}
466 */
467 get pathId() {
468 var parent = this.parent;
469 if (parent && parent instanceof IntentsTreeNode)
470 return parent.pathId + ',' + this.data.action;
471 return this.data.site;
472 },
473 };
474
475 /**
476 * Creates a new intents list.
477 * @param {Object=} opt_propertyBag Optional properties.
478 * @constructor
479 * @extends {DeletableItemList}
480 */
481 var IntentsList = cr.ui.define('list');
482
483 IntentsList.prototype = {
484 __proto__: DeletableItemList.prototype,
485
486 /** @inheritDoc */
487 decorate: function() {
488 DeletableItemList.prototype.decorate.call(this);
489 this.classList.add('intents-list');
490 this.data_ = [];
491 this.dataModel = new ArrayDataModel(this.data_);
492 this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this));
493 var sm = new ListSingleSelectionModel();
494 sm.addEventListener('change', this.cookieSelectionChange_.bind(this));
495 sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this));
496 this.selectionModel = sm;
497 this.fixedHeight = false;
498 },
499
500 /**
501 * Handles key down events and looks for left and right arrows, then
502 * dispatches to the currently expanded item, if any.
503 * @param {Event} e The keydown event.
504 * @private
505 */
506 handleKeyLeftRight_: function(e) {
507 var id = e.keyIdentifier;
508 if ((id == 'Left' || id == 'Right') && this.expandedItem) {
509 var cs = this.ownerDocument.defaultView.getComputedStyle(this);
510 var rtl = cs.direction == 'rtl';
511 if ((!rtl && id == 'Left') || (rtl && id == 'Right'))
512 this.expandedItem.selectedIndex--;
513 else
514 this.expandedItem.selectedIndex++;
515 this.scrollIndexIntoView(this.expandedItem.listIndex);
516 // Prevent the page itself from scrolling.
517 e.preventDefault();
518 }
519 },
520
521 /**
522 * Called on selection model selection changes.
523 * @param {Event} ce The selection change event.
524 * @private
525 */
526 cookieSelectionChange_: function(ce) {
527 ce.changes.forEach(function(change) {
528 var listItem = this.getListItemByIndex(change.index);
529 if (listItem) {
530 if (!change.selected) {
531 // We set a timeout here, rather than setting the item unexpanded
532 // immediately, so that if another item gets set expanded right
533 // away, it will be expanded before this item is unexpanded. It
534 // will notice that, and unexpand this item in sync with its own
535 // expansion. Later, this callback will end up having no effect.
536 window.setTimeout(function() {
537 if (!listItem.selected || !listItem.lead)
538 listItem.expanded = false;
539 }, 0);
540 } else if (listItem.lead) {
541 listItem.expanded = true;
542 }
543 }
544 }, this);
545 },
546
547 /**
548 * Called on selection model lead changes.
549 * @param {Event} pe The lead change event.
550 * @private
551 */
552 cookieLeadChange_: function(pe) {
553 if (pe.oldValue != -1) {
554 var listItem = this.getListItemByIndex(pe.oldValue);
555 if (listItem) {
556 // See cookieSelectionChange_ above for why we use a timeout here.
557 window.setTimeout(function() {
558 if (!listItem.lead || !listItem.selected)
559 listItem.expanded = false;
560 }, 0);
561 }
562 }
563 if (pe.newValue != -1) {
564 var listItem = this.getListItemByIndex(pe.newValue);
565 if (listItem && listItem.selected)
566 listItem.expanded = true;
567 }
568 },
569
570 /**
571 * The currently expanded item. Used by IntentsListItem above.
572 * @type {?IntentsListItem}
573 */
574 expandedItem: null,
575
576 // from cr.ui.List
577 /** @inheritDoc */
578 createItem: function(data) {
579 // We use the cached expanded item in order to allow it to maintain some
580 // state (like its fixed height, and which bubble is selected).
581 if (this.expandedItem && this.expandedItem.origin == data)
582 return this.expandedItem;
583 return new IntentsListItem(data, this);
584 },
585
586 // from options.DeletableItemList
587 /** @inheritDoc */
588 deleteItemAtIndex: function(index) {
589 var item = this.data_[index];
590 if (item) {
591 var pathId = item.pathId;
592 if (pathId)
593 chrome.send('removeIntent', [pathId]);
594 }
595 },
596
597 /**
598 * Insert an intents tree node at the given index.
599 * Both IntentsList and IntentsTreeNode implement this API.
600 * @param {Object} data The data object for the node to add.
601 * @param {number} index The index at which to insert the node.
602 */
603 insertAt: function(data, index) {
604 this.dataModel.splice(index, 0, new IntentsTreeNode(data));
605 },
606
607 /**
608 * Remove an intents tree node from the given index.
609 * Both IntentsList and IntentsTreeNode implement this API.
610 * @param {number} index The index of the tree node to remove.
611 */
612 remove: function(index) {
613 if (index < this.data_.length)
614 this.dataModel.splice(index, 1);
615 },
616
617 /**
618 * Clears the list.
619 * Both IntentsList and IntentsTreeNode implement this API.
620 * It is used by IntentsList.loadChildren().
621 */
622 clear: function() {
623 parentLookup = {};
624 this.data_ = [];
625 this.dataModel = new ArrayDataModel(this.data_);
626 this.redraw();
627 },
628
629 /**
630 * Add tree nodes by given parent.
631 * Note: this method will be O(n^2) in the general case. Use it only to
632 * populate an empty parent or to insert single nodes to avoid this.
633 * @param {Object} parent The parent node.
634 * @param {number} start Start index of where to insert nodes.
635 * @param {Array} nodesData Nodes data array.
636 * @private
637 */
638 addByParent_: function(parent, start, nodesData) {
639 if (!parent)
640 return;
641
642 parent.startBatchUpdates();
643 for (var i = 0; i < nodesData.length; ++i)
644 parent.insertAt(nodesData[i], start + i);
645 parent.endBatchUpdates();
646
647 cr.dispatchSimpleEvent(this, 'change');
648 },
649
650 /**
651 * Add tree nodes by parent id.
652 * This is used by intents_view.js.
653 * Note: this method will be O(n^2) in the general case. Use it only to
654 * populate an empty parent or to insert single nodes to avoid this.
655 * @param {string} parentId Id of the parent node.
656 * @param {number} start Start index of where to insert nodes.
657 * @param {Array} nodesData Nodes data array.
658 */
659 addByParentId: function(parentId, start, nodesData) {
660 var parent = parentId ? parentLookup[parentId] : this;
661 this.addByParent_(parent, start, nodesData);
662 },
663
664 /**
665 * Removes tree nodes by parent id.
666 * This is used by intents_view.js.
667 * @param {string} parentId Id of the parent node.
668 * @param {number} start Start index of nodes to remove.
669 * @param {number} count Number of nodes to remove.
670 */
671 removeByParentId: function(parentId, start, count) {
672 var parent = parentId ? parentLookup[parentId] : this;
673 if (!parent)
674 return;
675
676 parent.startBatchUpdates();
677 while (count-- > 0)
678 parent.remove(start);
679 parent.endBatchUpdates();
680
681 cr.dispatchSimpleEvent(this, 'change');
682 },
683
684 /**
685 * Loads the immediate children of given parent node.
686 * This is used by intents_view.js.
687 * @param {string} parentId Id of the parent node.
688 * @param {Array} children The immediate children of parent node.
689 */
690 loadChildren: function(parentId, children) {
691 if (parentId)
692 delete lookupRequests[parentId];
693 var parent = parentId ? parentLookup[parentId] : this;
694 if (!parent)
695 return;
696
697 parent.startBatchUpdates();
698 parent.clear();
699 this.addByParent_(parent, 0, children);
700 parent.endBatchUpdates();
701 },
702 };
703
704 return {
705 IntentsList: IntentsList
706 };
707 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/options2/instant_confirm_overlay.js ('k') | chrome/browser/resources/options2/intents_view.css » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698