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

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

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

Powered by Google App Engine
This is Rietveld 408576698