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

Side by Side Diff: chrome/browser/resources/history/history.js

Issue 2830983005: Remove old webui History page on desktop and mobile (Closed)
Patch Set: merge Created 3 years, 7 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
OLDNEW
(Empty)
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.
4
5 // <include src="../uber/uber_utils.js">
6 // <include src="history_focus_manager.js">
7
8 ///////////////////////////////////////////////////////////////////////////////
9 // Globals:
10 /** @const */ var RESULTS_PER_PAGE = 150;
11
12 // Amount of time between pageviews that we consider a 'break' in browsing,
13 // measured in milliseconds.
14 /** @const */ var BROWSING_GAP_TIME = 15 * 60 * 1000;
15
16 // The largest bucket value for UMA histogram, based on entry ID. All entries
17 // with IDs greater than this will be included in this bucket.
18 /** @const */ var UMA_MAX_BUCKET_VALUE = 1000;
19
20 // The largest bucket value for a UMA histogram that is a subset of above.
21 /** @const */ var UMA_MAX_SUBSET_BUCKET_VALUE = 100;
22
23 // TODO(glen): Get rid of these global references, replace with a controller
24 // or just make the classes own more of the page.
25 var historyModel;
26 var historyView;
27 var pageState;
28 var selectionAnchor = -1;
29 var activeVisit = null;
30
31 /** @const */ var Command = cr.ui.Command;
32 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
33 /** @const */ var Menu = cr.ui.Menu;
34 /** @const */ var MenuItem = cr.ui.MenuItem;
35
36 /**
37 * Enum that shows the filtering behavior for a host or URL to a supervised
38 * user. Must behave like the FilteringBehavior enum from
39 * supervised_user_url_filter.h.
40 * @enum {number}
41 */
42 var SupervisedUserFilteringBehavior = {
43 ALLOW: 0,
44 WARN: 1,
45 BLOCK: 2
46 };
47
48 /**
49 * Returns true if the mobile (non-desktop) version is being shown.
50 * @return {boolean} true if the mobile version is being shown.
51 */
52 function isMobileVersion() {
53 return !document.body.classList.contains('uber-frame');
54 }
55
56 /**
57 * Record an action in UMA.
58 * @param {string} actionDesc The name of the action to be logged.
59 */
60 function recordUmaAction(actionDesc) {
61 chrome.send('metricsHandler:recordAction', [actionDesc]);
62 }
63
64 /**
65 * Record a histogram value in UMA. If specified value is larger than the max
66 * bucket value, record the value in the largest bucket.
67 * @param {string} histogram The name of the histogram to be recorded in.
68 * @param {number} maxBucketValue The max value for the last histogram bucket.
69 * @param {number} value The value to record in the histogram.
70 */
71 function recordUmaHistogram(histogram, maxBucketValue, value) {
72 chrome.send('metricsHandler:recordInHistogram',
73 [histogram,
74 ((value > maxBucketValue) ? maxBucketValue : value),
75 maxBucketValue]);
76 }
77
78 ///////////////////////////////////////////////////////////////////////////////
79 // Visit:
80
81 /**
82 * Class to hold all the information about an entry in our model.
83 * @param {HistoryEntry} result An object containing the visit's data.
84 * @param {boolean} continued Whether this visit is on the same day as the
85 * visit before it.
86 * @param {HistoryModel} model The model object this entry belongs to.
87 * @constructor
88 */
89 function Visit(result, continued, model) {
90 this.model_ = model;
91 this.title_ = result.title;
92 this.url_ = result.url;
93 this.domain_ = result.domain;
94 this.starred_ = result.starred;
95 this.fallbackFaviconText_ = result.fallbackFaviconText;
96
97 // These identify the name and type of the device on which this visit
98 // occurred. They will be empty if the visit occurred on the current device.
99 this.deviceName = result.deviceName;
100 this.deviceType = result.deviceType;
101
102 // The ID will be set according to when the visit was displayed, not
103 // received. Set to -1 to show that it has not been set yet.
104 this.id_ = -1;
105
106 this.isRendered = false; // Has the visit already been rendered on the page?
107
108 // All the date information is public so that owners can compare properties of
109 // two items easily.
110
111 this.date = new Date(result.time);
112
113 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always
114 // get all of these.
115 this.dateRelativeDay = result.dateRelativeDay;
116 this.dateTimeOfDay = result.dateTimeOfDay;
117 this.dateShort = result.dateShort;
118
119 // Shows the filtering behavior for that host (only used for supervised
120 // users).
121 // A value of |SupervisedUserFilteringBehavior.ALLOW| is not displayed so it
122 // is used as the default value.
123 this.hostFilteringBehavior = SupervisedUserFilteringBehavior.ALLOW;
124 if (result.hostFilteringBehavior)
125 this.hostFilteringBehavior = result.hostFilteringBehavior;
126
127 this.blockedVisit = result.blockedVisit;
128
129 // Whether this is the continuation of a previous day.
130 this.continued = continued;
131
132 this.allTimestamps = result.allTimestamps;
133 }
134
135 // Visit, public: -------------------------------------------------------------
136
137 /**
138 * Returns a dom structure for a browse page result or a search page result.
139 * @param {Object} propertyBag A bag of configuration properties, false by
140 * default:
141 * - isSearchResult: Whether or not the result is a search result.
142 * - addTitleFavicon: Whether or not the favicon should be added.
143 * - useMonthDate: Whether or not the full date should be inserted (used for
144 * monthly view).
145 * @return {Node} A DOM node to represent the history entry or search result.
146 */
147 Visit.prototype.getResultDOM = function(propertyBag) {
148 var isSearchResult = propertyBag.isSearchResult || false;
149 var addTitleFavicon = propertyBag.addTitleFavicon || false;
150 var useMonthDate = propertyBag.useMonthDate || false;
151 var focusless = propertyBag.focusless || false;
152 var node = createElementWithClassName('li', 'entry');
153 var time = createElementWithClassName('span', 'time');
154 var entryBox = createElementWithClassName('div', 'entry-box');
155 var domain = createElementWithClassName('div', 'domain');
156
157 this.id_ = this.model_.getNextVisitId();
158 var self = this;
159
160 // Only create the checkbox if it can be used to delete an entry.
161 if (this.model_.editingEntriesAllowed) {
162 var checkbox = document.createElement('input');
163 checkbox.type = 'checkbox';
164 checkbox.id = 'checkbox-' + this.id_;
165 checkbox.time = this.date.getTime();
166 checkbox.setAttribute('aria-label', loadTimeData.getStringF(
167 'entrySummary',
168 this.dateTimeOfDay,
169 this.starred_ ? loadTimeData.getString('bookmarked') : '',
170 this.title_,
171 this.domain_));
172 checkbox.addEventListener('click', checkboxClicked);
173 entryBox.appendChild(checkbox);
174
175 if (focusless)
176 checkbox.tabIndex = -1;
177
178 if (!isMobileVersion()) {
179 // Clicking anywhere in the entryBox will check/uncheck the checkbox.
180 entryBox.setAttribute('for', checkbox.id);
181 entryBox.addEventListener('mousedown', this.handleMousedown_.bind(this));
182 entryBox.addEventListener('click', entryBoxClick);
183 entryBox.addEventListener('keydown', this.handleKeydown_.bind(this));
184 }
185 }
186
187 // Keep track of the drop down that triggered the menu, so we know
188 // which element to apply the command to.
189 // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it.
190 var setActiveVisit = function(e) {
191 activeVisit = self;
192 var menu = $('action-menu');
193 menu.dataset.devicename = self.deviceName;
194 menu.dataset.devicetype = self.deviceType;
195 };
196 domain.textContent = this.domain_;
197
198 entryBox.appendChild(time);
199
200 var bookmarkSection = createElementWithClassName(
201 'button', 'bookmark-section custom-appearance');
202 if (this.starred_) {
203 bookmarkSection.title = loadTimeData.getString('removeBookmark');
204 bookmarkSection.classList.add('starred');
205 bookmarkSection.addEventListener('click', function f(e) {
206 recordUmaAction('HistoryPage_BookmarkStarClicked');
207 chrome.send('removeBookmark', [self.url_]);
208
209 this.model_.getView().onBeforeUnstarred(this);
210 bookmarkSection.classList.remove('starred');
211 this.model_.getView().onAfterUnstarred(this);
212
213 bookmarkSection.removeEventListener('click', f);
214 e.preventDefault();
215 }.bind(this));
216 }
217
218 if (focusless)
219 bookmarkSection.tabIndex = -1;
220
221 entryBox.appendChild(bookmarkSection);
222
223 if (addTitleFavicon || this.blockedVisit) {
224 var faviconSection = createElementWithClassName('div', 'favicon');
225 if (this.blockedVisit)
226 faviconSection.classList.add('blocked-icon');
227 else
228 this.loadFavicon_(faviconSection);
229 entryBox.appendChild(faviconSection);
230 }
231
232 var visitEntryWrapper = /** @type {HTMLElement} */(
233 entryBox.appendChild(document.createElement('div')));
234 if (addTitleFavicon || this.blockedVisit)
235 visitEntryWrapper.classList.add('visit-entry');
236 if (this.blockedVisit) {
237 visitEntryWrapper.classList.add('blocked-indicator');
238 visitEntryWrapper.appendChild(this.getVisitAttemptDOM_());
239 } else {
240 var title = visitEntryWrapper.appendChild(
241 this.getTitleDOM_(isSearchResult));
242
243 if (focusless)
244 title.querySelector('a').tabIndex = -1;
245
246 visitEntryWrapper.appendChild(domain);
247 }
248
249 if (isMobileVersion()) {
250 if (this.model_.editingEntriesAllowed) {
251 var removeButton = createElementWithClassName('button', 'remove-entry');
252 removeButton.setAttribute('aria-label',
253 loadTimeData.getString('removeFromHistory'));
254 removeButton.classList.add('custom-appearance');
255 removeButton.addEventListener(
256 'click', this.removeEntryFromHistory_.bind(this));
257 entryBox.appendChild(removeButton);
258
259 // Support clicking anywhere inside the entry box.
260 entryBox.addEventListener('click', function(e) {
261 if (!e.defaultPrevented) {
262 self.titleLink.focus();
263 self.titleLink.click();
264 }
265 });
266 }
267 } else {
268 var dropDown = createElementWithClassName('button', 'drop-down');
269 dropDown.value = 'Open action menu';
270 dropDown.title = loadTimeData.getString('actionMenuDescription');
271 dropDown.setAttribute('menu', '#action-menu');
272 dropDown.setAttribute('aria-haspopup', 'true');
273
274 if (focusless)
275 dropDown.tabIndex = -1;
276
277 cr.ui.decorate(dropDown, cr.ui.MenuButton);
278 dropDown.respondToArrowKeys = false;
279
280 dropDown.addEventListener('mousedown', setActiveVisit);
281 dropDown.addEventListener('focus', setActiveVisit);
282
283 // Prevent clicks on the drop down from affecting the checkbox. We need to
284 // call blur() explicitly because preventDefault() cancels any focus
285 // handling.
286 dropDown.addEventListener('click', function(e) {
287 e.preventDefault();
288 document.activeElement.blur();
289 });
290 entryBox.appendChild(dropDown);
291 }
292
293 // Let the entryBox be styled appropriately when it contains keyboard focus.
294 entryBox.addEventListener('focus', function() {
295 this.classList.add('contains-focus');
296 }, true);
297 entryBox.addEventListener('blur', function() {
298 this.classList.remove('contains-focus');
299 }, true);
300
301 var entryBoxContainer =
302 createElementWithClassName('div', 'entry-box-container');
303 node.appendChild(entryBoxContainer);
304 entryBoxContainer.appendChild(entryBox);
305
306 if (isSearchResult || useMonthDate) {
307 // Show the day instead of the time.
308 time.appendChild(document.createTextNode(this.dateShort));
309 } else {
310 time.appendChild(document.createTextNode(this.dateTimeOfDay));
311 }
312
313 this.domNode_ = node;
314 node.visit = this;
315
316 return node;
317 };
318
319 /**
320 * Remove this visit from the history.
321 */
322 Visit.prototype.removeFromHistory = function() {
323 recordUmaAction('HistoryPage_EntryMenuRemoveFromHistory');
324 this.model_.removeVisitsFromHistory([this], function() {
325 this.model_.getView().removeVisit(this);
326 }.bind(this));
327 };
328
329 // Closure Compiler doesn't support Object.defineProperty().
330 // https://github.com/google/closure-compiler/issues/302
331 Object.defineProperty(Visit.prototype, 'checkBox', {
332 get: /** @this {Visit} */function() {
333 return this.domNode_.querySelector('input[type=checkbox]');
334 },
335 });
336
337 Object.defineProperty(Visit.prototype, 'bookmarkStar', {
338 get: /** @this {Visit} */function() {
339 return this.domNode_.querySelector('.bookmark-section.starred');
340 },
341 });
342
343 Object.defineProperty(Visit.prototype, 'titleLink', {
344 get: /** @this {Visit} */function() {
345 return this.domNode_.querySelector('.title a');
346 },
347 });
348
349 Object.defineProperty(Visit.prototype, 'dropDown', {
350 get: /** @this {Visit} */function() {
351 return this.domNode_.querySelector('button.drop-down');
352 },
353 });
354
355 // Visit, private: ------------------------------------------------------------
356
357 /**
358 * Add child text nodes to a node such that occurrences of the specified text is
359 * highlighted.
360 * @param {Node} node The node under which new text nodes will be made as
361 * children.
362 * @param {string} content Text to be added beneath |node| as one or more
363 * text nodes.
364 * @param {string} highlightText Occurences of this text inside |content| will
365 * be highlighted.
366 * @private
367 */
368 Visit.prototype.addHighlightedText_ = function(node, content, highlightText) {
369 var i = 0;
370 if (highlightText) {
371 var re = new RegExp(quoteString(highlightText), 'gim');
372 var match;
373 while (match = re.exec(content)) {
374 if (match.index > i)
375 node.appendChild(document.createTextNode(content.slice(i,
376 match.index)));
377 i = re.lastIndex;
378 // Mark the highlighted text in bold.
379 var b = document.createElement('b');
380 b.textContent = content.substring(match.index, i);
381 node.appendChild(b);
382 }
383 }
384 if (i < content.length)
385 node.appendChild(document.createTextNode(content.slice(i)));
386 };
387
388 /**
389 * Returns the DOM element containing a link on the title of the URL for the
390 * current visit.
391 * @param {boolean} isSearchResult Whether or not the entry is a search result.
392 * @return {Element} DOM representation for the title block.
393 * @private
394 */
395 Visit.prototype.getTitleDOM_ = function(isSearchResult) {
396 var node = createElementWithClassName('div', 'title');
397 var link = document.createElement('a');
398 link.href = this.url_;
399 link.id = 'id-' + this.id_;
400 link.target = '_top';
401 var integerId = parseInt(this.id_, 10);
402 link.addEventListener('click', function() {
403 recordUmaAction('HistoryPage_EntryLinkClick');
404 // Record the ID of the entry to signify how many entries are above this
405 // link on the page.
406 recordUmaHistogram('HistoryPage.ClickPosition',
407 UMA_MAX_BUCKET_VALUE,
408 integerId);
409 if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
410 recordUmaHistogram('HistoryPage.ClickPositionSubset',
411 UMA_MAX_SUBSET_BUCKET_VALUE,
412 integerId);
413 }
414 });
415 link.addEventListener('contextmenu', function() {
416 recordUmaAction('HistoryPage_EntryLinkRightClick');
417 });
418
419 if (isSearchResult) {
420 link.addEventListener('click', function() {
421 recordUmaAction('HistoryPage_SearchResultClick');
422 });
423 }
424
425 // Add a tooltip, since it might be ellipsized.
426 // TODO(dubroy): Find a way to show the tooltip only when necessary.
427 link.title = this.title_;
428
429 this.addHighlightedText_(link, this.title_, this.model_.getSearchText());
430 node.appendChild(link);
431
432 return node;
433 };
434
435 /**
436 * Returns the DOM element containing the text for a blocked visit attempt.
437 * @return {Element} DOM representation of the visit attempt.
438 * @private
439 */
440 Visit.prototype.getVisitAttemptDOM_ = function() {
441 var node = createElementWithClassName('div', 'title');
442 node.innerHTML = loadTimeData.getStringF('blockedVisitText',
443 this.url_,
444 this.id_,
445 this.domain_);
446 return node;
447 };
448
449 /**
450 * Load the favicon for an element.
451 * @param {Element} faviconDiv The DOM element for which to load the icon.
452 * @private
453 */
454 Visit.prototype.loadFavicon_ = function(faviconDiv) {
455 if (cr.isAndroid) {
456 // On Android, if a large icon is unavailable, an HTML/CSS fallback favicon
457 // is generated because Android does not yet support text drawing in native.
458
459 // Check whether a fallback favicon needs to be generated.
460 var desiredPixelSize = 32 * window.devicePixelRatio;
461 var img = new Image();
462 img.onload = this.onLargeFaviconLoadedAndroid_.bind(this, faviconDiv);
463 img.src = 'chrome://large-icon/' + desiredPixelSize + '/' + this.url_;
464 } else {
465 faviconDiv.style.backgroundImage = cr.icon.getFavicon(this.url_);
466 }
467 };
468
469 /**
470 * Called when the chrome://large-icon image has finished loading.
471 * @param {Element} faviconDiv The DOM element to add the favicon to.
472 * @param {Event} event The onload event.
473 * @private
474 */
475 Visit.prototype.onLargeFaviconLoadedAndroid_ = function(faviconDiv, event) {
476 // The loaded image should either:
477 // - Have the desired size.
478 // OR
479 // - Be 1x1 px with the background color for the fallback icon.
480 var loadedImg = event.target;
481 if (loadedImg.width == 1) {
482 faviconDiv.classList.add('fallback-favicon');
483 faviconDiv.textContent = this.fallbackFaviconText_;
484 }
485 faviconDiv.style.backgroundImage = url(loadedImg.src);
486 };
487
488 /**
489 * Launch a search for more history entries from the same domain.
490 * @private
491 */
492 Visit.prototype.showMoreFromSite_ = function() {
493 recordUmaAction('HistoryPage_EntryMenuShowMoreFromSite');
494 historyView.setSearch(this.domain_);
495 $('search-field').focus();
496 };
497
498 /**
499 * @param {Event} e A keydown event to handle.
500 * @private
501 */
502 Visit.prototype.handleKeydown_ = function(e) {
503 // Delete or Backspace should delete the entry if allowed.
504 if (e.key == 'Backspace' || e.key == 'Delete')
505 this.removeEntryFromHistory_(e);
506 };
507
508 /**
509 * @param {Event} event A mousedown event.
510 * @private
511 */
512 Visit.prototype.handleMousedown_ = function(event) {
513 // Prevent text selection when shift-clicking to select multiple entries.
514 if (event.shiftKey) {
515 event.preventDefault();
516
517 var target = assertInstanceof(event.target, HTMLElement);
518 if (this.model_.getView().isInFocusGrid(target))
519 target.focus();
520 }
521 };
522
523 /**
524 * Removes a history entry on click or keydown and finds a new entry to focus.
525 * @param {Event} e A click or keydown event.
526 * @private
527 */
528 Visit.prototype.removeEntryFromHistory_ = function(e) {
529 if (!this.model_.deletingHistoryAllowed || this.model_.isDeletingVisits() ||
530 this.domNode_.classList.contains('fade-out')) {
531 return;
532 }
533
534 this.model_.getView().onBeforeRemove(this);
535 this.removeFromHistory();
536 e.preventDefault();
537 };
538
539 ///////////////////////////////////////////////////////////////////////////////
540 // HistoryModel:
541
542 /**
543 * Global container for history data. Future optimizations might include
544 * allowing the creation of a HistoryModel for each search string, allowing
545 * quick flips back and forth between results.
546 *
547 * The history model is based around pages, and only fetching the data to
548 * fill the currently requested page. This is somewhat dependent on the view,
549 * and so future work may wish to change history model to operate on
550 * timeframe (day or week) based containers.
551 *
552 * @constructor
553 */
554 function HistoryModel() {
555 this.clearModel_();
556 }
557
558 // HistoryModel, Public: ------------------------------------------------------
559
560 /** @enum {number} */
561 HistoryModel.Range = {
562 ALL_TIME: 0,
563 WEEK: 1,
564 MONTH: 2
565 };
566
567 /**
568 * Sets our current view that is called when the history model changes.
569 * @param {HistoryView} view The view to set our current view to.
570 */
571 HistoryModel.prototype.setView = function(view) {
572 this.view_ = view;
573 };
574
575
576 /**
577 * @return {HistoryView|undefined} Returns the view for this model (if set).
578 */
579 HistoryModel.prototype.getView = function() {
580 return this.view_;
581 };
582
583 /**
584 * Reload our model with the current parameters.
585 */
586 HistoryModel.prototype.reload = function() {
587 // Save user-visible state, clear the model, and restore the state.
588 var search = this.searchText_;
589 var page = this.requestedPage_;
590 var range = this.rangeInDays_;
591 var offset = this.offset_;
592 var groupByDomain = this.groupByDomain_;
593
594 this.clearModel_();
595 this.searchText_ = search;
596 this.requestedPage_ = page;
597 this.rangeInDays_ = range;
598 this.offset_ = offset;
599 this.groupByDomain_ = groupByDomain;
600 this.queryHistory_();
601 };
602
603 /**
604 * @return {string} The current search text.
605 */
606 HistoryModel.prototype.getSearchText = function() {
607 return this.searchText_;
608 };
609
610 /**
611 * Tell the model that the view will want to see the current page. When
612 * the data becomes available, the model will call the view back.
613 * @param {number} page The page we want to view.
614 */
615 HistoryModel.prototype.requestPage = function(page) {
616 this.requestedPage_ = page;
617 this.updateSearch_();
618 };
619
620 /**
621 * Receiver for history query.
622 * @param {HistoryQuery} info An object containing information about the query.
623 * @param {Array<HistoryEntry>} results A list of results.
624 */
625 HistoryModel.prototype.addResults = function(info, results) {
626 // If no requests are in flight then this was an old request so we drop the
627 // results. Double check the search term as well.
628 if (!this.inFlight_ || info.term != this.searchText_)
629 return;
630
631 $('loading-spinner').hidden = true;
632 this.inFlight_ = false;
633 this.isQueryFinished_ = info.finished;
634 this.queryInterval = info.queryInterval;
635
636 var lastVisit = this.visits_.slice(-1)[0];
637 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null;
638
639 for (var i = 0, result; result = results[i]; i++) {
640 var thisDay = result.dateRelativeDay;
641 var isSameDay = lastDay == thisDay;
642 this.visits_.push(new Visit(result, isSameDay, this));
643 lastDay = thisDay;
644 }
645
646 this.updateSearch_();
647 };
648
649 /**
650 * @return {number} The number of visits in the model.
651 */
652 HistoryModel.prototype.getSize = function() {
653 return this.visits_.length;
654 };
655
656 /**
657 * Get a list of visits between specified index positions.
658 * @param {number} start The start index.
659 * @param {number} end The end index.
660 * @return {Array<Visit>} A list of visits.
661 */
662 HistoryModel.prototype.getNumberedRange = function(start, end) {
663 return this.visits_.slice(start, end);
664 };
665
666 /**
667 * Return true if there are more results beyond the current page.
668 * @return {boolean} true if the there are more results, otherwise false.
669 */
670 HistoryModel.prototype.hasMoreResults = function() {
671 return this.haveDataForPage_(this.requestedPage_ + 1) ||
672 !this.isQueryFinished_;
673 };
674
675 /**
676 * Removes a list of visits from the history, and calls |callback| when the
677 * removal has successfully completed.
678 * @param {Array<Visit>} visits The visits to remove.
679 * @param {Function} callback The function to call after removal succeeds.
680 */
681 HistoryModel.prototype.removeVisitsFromHistory = function(visits, callback) {
682 assert(this.deletingHistoryAllowed);
683
684 var toBeRemoved = [];
685 for (var i = 0; i < visits.length; i++) {
686 toBeRemoved.push({
687 url: visits[i].url_,
688 timestamps: visits[i].allTimestamps
689 });
690 }
691
692 this.deleteCompleteCallback_ = callback;
693 chrome.send('removeVisits', toBeRemoved);
694 };
695
696 /** @return {boolean} Whether the model is currently deleting a visit. */
697 HistoryModel.prototype.isDeletingVisits = function() {
698 return !!this.deleteCompleteCallback_;
699 };
700
701 /**
702 * Called when visits have been succesfully removed from the history.
703 */
704 HistoryModel.prototype.deleteComplete = function() {
705 // Call the callback, with 'this' undefined inside the callback.
706 this.deleteCompleteCallback_.call();
707 this.deleteCompleteCallback_ = null;
708 };
709
710 // Getter and setter for HistoryModel.rangeInDays_.
711 Object.defineProperty(HistoryModel.prototype, 'rangeInDays', {
712 get: /** @this {HistoryModel} */function() {
713 return this.rangeInDays_;
714 },
715 set: /** @this {HistoryModel} */function(range) {
716 this.rangeInDays_ = range;
717 }
718 });
719
720 /**
721 * Getter and setter for HistoryModel.offset_. The offset moves the current
722 * query 'window' |range| days behind. As such for range set to WEEK an offset
723 * of 0 refers to the last 7 days, an offset of 1 refers to the 7 day period
724 * that ended 7 days ago, etc. For MONTH an offset of 0 refers to the current
725 * calendar month, 1 to the previous one, etc.
726 */
727 Object.defineProperty(HistoryModel.prototype, 'offset', {
728 get: /** @this {HistoryModel} */function() {
729 return this.offset_;
730 },
731 set: /** @this {HistoryModel} */function(offset) {
732 this.offset_ = offset;
733 }
734 });
735
736 // Setter for HistoryModel.requestedPage_.
737 Object.defineProperty(HistoryModel.prototype, 'requestedPage', {
738 set: /** @this {HistoryModel} */function(page) {
739 this.requestedPage_ = page;
740 }
741 });
742
743 /**
744 * Removes |visit| from this model.
745 * @param {Visit} visit A visit to remove.
746 */
747 HistoryModel.prototype.removeVisit = function(visit) {
748 var index = this.visits_.indexOf(visit);
749 if (index >= 0)
750 this.visits_.splice(index, 1);
751 };
752
753 /**
754 * Automatically generates a new visit ID.
755 * @return {number} The next visit ID.
756 */
757 HistoryModel.prototype.getNextVisitId = function() {
758 return this.nextVisitId_++;
759 };
760
761 // HistoryModel, Private: -----------------------------------------------------
762
763 /**
764 * Clear the history model.
765 * @private
766 */
767 HistoryModel.prototype.clearModel_ = function() {
768 this.inFlight_ = false; // Whether a query is inflight.
769 this.searchText_ = '';
770 // Whether this user is a supervised user.
771 this.isSupervisedProfile = loadTimeData.getBoolean('isSupervisedProfile');
772 this.deletingHistoryAllowed = loadTimeData.getBoolean('allowDeletingHistory');
773
774 // Only create checkboxes for editing entries if they can be used either to
775 // delete an entry or to block/allow it.
776 this.editingEntriesAllowed = this.deletingHistoryAllowed;
777
778 // Flag to show that the results are grouped by domain or not.
779 this.groupByDomain_ = false;
780
781 this.visits_ = []; // Date-sorted list of visits (most recent first).
782 this.nextVisitId_ = 0;
783 selectionAnchor = -1;
784
785 // The page that the view wants to see - we only fetch slightly past this
786 // point. If the view requests a page that we don't have data for, we try
787 // to fetch it and call back when we're done.
788 this.requestedPage_ = 0;
789
790 // The range of history to view or search over.
791 this.rangeInDays_ = HistoryModel.Range.ALL_TIME;
792
793 // Skip |offset_| * weeks/months from the begining.
794 this.offset_ = 0;
795
796 // Keeps track of whether or not there are more results available than are
797 // currently held in |this.visits_|.
798 this.isQueryFinished_ = false;
799
800 if (this.view_)
801 this.view_.clear_();
802 };
803
804 /**
805 * Figure out if we need to do more queries to fill the currently requested
806 * page. If we think we can fill the page, call the view and let it know
807 * we're ready to show something. This only applies to the daily time-based
808 * view.
809 * @private
810 */
811 HistoryModel.prototype.updateSearch_ = function() {
812 var doneLoading = this.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
813 this.isQueryFinished_ ||
814 this.canFillPage_(this.requestedPage_);
815
816 // Try to fetch more results if more results can arrive and the page is not
817 // full.
818 if (!doneLoading && !this.inFlight_)
819 this.queryHistory_();
820
821 // Show the result or a message if no results were returned.
822 this.view_.onModelReady(doneLoading);
823 };
824
825 /**
826 * Query for history, either for a search or time-based browsing.
827 * @private
828 */
829 HistoryModel.prototype.queryHistory_ = function() {
830 var maxResults =
831 (this.rangeInDays_ == HistoryModel.Range.ALL_TIME) ? RESULTS_PER_PAGE : 0;
832
833 // If there are already some visits, pick up the previous query where it
834 // left off.
835 var lastVisit = this.visits_.slice(-1)[0];
836 var endTime = lastVisit ? lastVisit.date.getTime() : 0;
837
838 $('loading-spinner').hidden = false;
839 this.inFlight_ = true;
840 chrome.send('queryHistory',
841 [this.searchText_, this.offset_, this.rangeInDays_, endTime, maxResults]);
842 };
843
844 /**
845 * Check to see if we have data for the given page.
846 * @param {number} page The page number.
847 * @return {boolean} Whether we have any data for the given page.
848 * @private
849 */
850 HistoryModel.prototype.haveDataForPage_ = function(page) {
851 return page * RESULTS_PER_PAGE < this.getSize();
852 };
853
854 /**
855 * Check to see if we have data to fill the given page.
856 * @param {number} page The page number.
857 * @return {boolean} Whether we have data to fill the page.
858 * @private
859 */
860 HistoryModel.prototype.canFillPage_ = function(page) {
861 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize());
862 };
863
864 /**
865 * Gets whether we are grouped by domain.
866 * @return {boolean} Whether the results are grouped by domain.
867 */
868 HistoryModel.prototype.getGroupByDomain = function() {
869 return this.groupByDomain_;
870 };
871
872 ///////////////////////////////////////////////////////////////////////////////
873 // HistoryFocusRow:
874
875 /**
876 * Provides an implementation for a single column grid.
877 * @param {!Element} root
878 * @param {?Element} boundary
879 * @constructor
880 * @extends {cr.ui.FocusRow}
881 */
882 function HistoryFocusRow(root, boundary) {
883 cr.ui.FocusRow.call(this, root, boundary);
884
885 // None of these are guaranteed to exist in all versions of the UI.
886 this.addItem('checkbox', '.entry-box input');
887 this.addItem('checkbox', '.domain-checkbox');
888 this.addItem('star', '.bookmark-section.starred');
889 this.addItem('domain', '[is="action-link"]');
890 this.addItem('title', '.title a');
891 this.addItem('menu', '.drop-down');
892 }
893
894 HistoryFocusRow.prototype = {
895 __proto__: cr.ui.FocusRow.prototype,
896
897 /** @override */
898 getCustomEquivalent: function(sampleElement) {
899 var equivalent;
900
901 switch (this.getTypeForElement(sampleElement)) {
902 case 'star':
903 equivalent = this.getFirstFocusable('title') ||
904 this.getFirstFocusable('domain');
905 break;
906 case 'domain':
907 equivalent = this.getFirstFocusable('title');
908 break;
909 case 'title':
910 equivalent = this.getFirstFocusable('domain');
911 break;
912 case 'menu':
913 equivalent = this.getFocusableElements().slice(-1)[0];
914 break;
915 }
916
917 return equivalent ||
918 cr.ui.FocusRow.prototype.getCustomEquivalent.call(this, sampleElement);
919 },
920 };
921
922 ///////////////////////////////////////////////////////////////////////////////
923 // HistoryView:
924
925 /**
926 * Functions and state for populating the page with HTML. This should one-day
927 * contain the view and use event handlers, rather than pushing HTML out and
928 * getting called externally.
929 * @param {HistoryModel} model The model backing this view.
930 * @constructor
931 */
932 function HistoryView(model) {
933 this.editButtonTd_ = $('edit-button');
934 this.editingControlsDiv_ = $('editing-controls');
935 this.resultDiv_ = $('results-display');
936 this.focusGrid_ = new cr.ui.FocusGrid();
937 this.pageDiv_ = $('results-pagination');
938 this.model_ = model;
939 this.pageIndex_ = 0;
940 this.lastDisplayed_ = [];
941 this.hasRenderedResults_ = false;
942
943 this.model_.setView(this);
944
945 this.currentVisits_ = [];
946
947 // If there is no search button, use the search button label as placeholder
948 // text in the search field.
949 if ($('search-button').offsetWidth == 0)
950 $('search-field').placeholder = $('search-button').value;
951
952 var self = this;
953
954 $('clear-browsing-data').addEventListener('click', openClearBrowsingData);
955 $('remove-selected').addEventListener('click', removeItems);
956
957 // Add handlers for the page navigation buttons at the bottom.
958 $('newest-button').addEventListener('click', function() {
959 recordUmaAction('HistoryPage_NewestHistoryClick');
960 self.setPage(0);
961 });
962 $('newer-button').addEventListener('click', function() {
963 recordUmaAction('HistoryPage_NewerHistoryClick');
964 self.setPage(self.pageIndex_ - 1);
965 });
966 $('older-button').addEventListener('click', function() {
967 recordUmaAction('HistoryPage_OlderHistoryClick');
968 self.setPage(self.pageIndex_ + 1);
969 });
970
971 $('timeframe-controls').onchange = function(e) {
972 var value = parseInt(e.target.value, 10);
973 self.setRangeInDays(/** @type {HistoryModel.Range<number>} */(value));
974 };
975
976 $('range-previous').addEventListener('click', function(e) {
977 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
978 self.setPage(self.pageIndex_ + 1);
979 else
980 self.setOffset(self.getOffset() + 1);
981 });
982 $('range-next').addEventListener('click', function(e) {
983 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
984 self.setPage(self.pageIndex_ - 1);
985 else
986 self.setOffset(self.getOffset() - 1);
987 });
988 $('range-today').addEventListener('click', function(e) {
989 if (self.getRangeInDays() == HistoryModel.Range.ALL_TIME)
990 self.setPage(0);
991 else
992 self.setOffset(0);
993 });
994 }
995
996 // HistoryView, public: -------------------------------------------------------
997 /**
998 * Do a search on a specific term.
999 * @param {string} term The string to search for.
1000 */
1001 HistoryView.prototype.setSearch = function(term) {
1002 window.scrollTo(0, 0);
1003 this.setPageState(term, 0, this.getRangeInDays(), this.getOffset());
1004 };
1005
1006 /**
1007 * Reload the current view.
1008 */
1009 HistoryView.prototype.reload = function() {
1010 this.model_.reload();
1011 this.updateSelectionEditButtons();
1012 this.updateRangeButtons_();
1013 };
1014
1015 /**
1016 * Sets all the parameters for the history page and then reloads the view to
1017 * update the results.
1018 * @param {string} searchText The search string to set.
1019 * @param {number} page The page to be viewed.
1020 * @param {HistoryModel.Range} range The range to view or search over.
1021 * @param {number} offset Set the begining of the query to the specific offset.
1022 */
1023 HistoryView.prototype.setPageState = function(searchText, page, range, offset) {
1024 this.clear_();
1025 this.model_.searchText_ = searchText;
1026 this.pageIndex_ = page;
1027 this.model_.requestedPage_ = page;
1028 this.model_.rangeInDays_ = range;
1029 this.model_.groupByDomain_ = false;
1030 if (range != HistoryModel.Range.ALL_TIME)
1031 this.model_.groupByDomain_ = true;
1032 this.model_.offset_ = offset;
1033 this.reload();
1034 pageState.setUIState(this.model_.getSearchText(),
1035 this.pageIndex_,
1036 this.getRangeInDays(),
1037 this.getOffset());
1038 };
1039
1040 /**
1041 * Switch to a specified page.
1042 * @param {number} page The page we wish to view.
1043 */
1044 HistoryView.prototype.setPage = function(page) {
1045 // TODO(sergiu): Move this function to setPageState as well and see why one
1046 // of the tests fails when using setPageState.
1047 this.clear_();
1048 this.pageIndex_ = parseInt(page, 10);
1049 window.scrollTo(0, 0);
1050 this.model_.requestPage(page);
1051 pageState.setUIState(this.model_.getSearchText(),
1052 this.pageIndex_,
1053 this.getRangeInDays(),
1054 this.getOffset());
1055 };
1056
1057 /**
1058 * @return {number} The page number being viewed.
1059 */
1060 HistoryView.prototype.getPage = function() {
1061 return this.pageIndex_;
1062 };
1063
1064 /**
1065 * Set the current range for grouped results.
1066 * @param {HistoryModel.Range} range The number of days to which the range
1067 * should be set.
1068 */
1069 HistoryView.prototype.setRangeInDays = function(range) {
1070 // Set the range, offset and reset the page.
1071 this.setPageState(this.model_.getSearchText(), 0, range, 0);
1072 };
1073
1074 /**
1075 * Get the current range in days.
1076 * @return {HistoryModel.Range} Current range in days from the model.
1077 */
1078 HistoryView.prototype.getRangeInDays = function() {
1079 return this.model_.rangeInDays;
1080 };
1081
1082 /**
1083 * Set the current offset for grouped results.
1084 * @param {number} offset Offset to set.
1085 */
1086 HistoryView.prototype.setOffset = function(offset) {
1087 // If there is another query already in flight wait for that to complete.
1088 if (this.model_.inFlight_)
1089 return;
1090 this.setPageState(this.model_.getSearchText(),
1091 this.pageIndex_,
1092 this.getRangeInDays(),
1093 offset);
1094 };
1095
1096 /**
1097 * Get the current offset.
1098 * @return {number} Current offset from the model.
1099 */
1100 HistoryView.prototype.getOffset = function() {
1101 return this.model_.offset;
1102 };
1103
1104 /**
1105 * Callback for the history model to let it know that it has data ready for us
1106 * to view.
1107 * @param {boolean} doneLoading Whether the current request is complete.
1108 */
1109 HistoryView.prototype.onModelReady = function(doneLoading) {
1110 this.displayResults_(doneLoading);
1111
1112 // Allow custom styling based on whether there are any results on the page.
1113 // To make this easier, add a class to the body if there are any results.
1114 var hasResults = this.model_.visits_.length > 0;
1115 document.body.classList.toggle('has-results', hasResults);
1116
1117 this.updateFocusGrid_();
1118 this.updateNavBar_();
1119
1120 if (isMobileVersion()) {
1121 // Hide the search field if it is empty and there are no results.
1122 var isSearch = this.model_.getSearchText().length > 0;
1123 $('search-field').hidden = !(hasResults || isSearch);
1124 }
1125
1126 if (!this.hasRenderedResults_) {
1127 this.hasRenderedResults_ = true;
1128 setTimeout(function() {
1129 chrome.send(
1130 'metricsHandler:recordTime',
1131 ['History.ResultsRenderedTime', window.performance.now()]);
1132 });
1133 }
1134 };
1135
1136 /**
1137 * Enables or disables the buttons that control editing entries depending on
1138 * whether there are any checked boxes.
1139 */
1140 HistoryView.prototype.updateSelectionEditButtons = function() {
1141 if (loadTimeData.getBoolean('allowDeletingHistory')) {
1142 var anyChecked = document.querySelector('.entry input:checked') != null;
1143 $('remove-selected').disabled = !anyChecked;
1144 } else {
1145 $('remove-selected').disabled = true;
1146 }
1147 };
1148
1149 /**
1150 * Shows the notification bar at the top of the page with |innerHTML| as its
1151 * content.
1152 * @param {string} innerHTML The HTML content of the warning.
1153 * @param {boolean} isWarning If true, style the notification as a warning.
1154 */
1155 HistoryView.prototype.showNotification = function(innerHTML, isWarning) {
1156 var bar = $('notification-bar');
1157 bar.innerHTML = innerHTML;
1158 bar.hidden = false;
1159 if (isWarning)
1160 bar.classList.add('warning');
1161 else
1162 bar.classList.remove('warning');
1163
1164 // Make sure that any links in the HTML are targeting the top level.
1165 var links = bar.querySelectorAll('a');
1166 for (var i = 0; i < links.length; i++)
1167 links[i].target = '_top';
1168
1169 this.positionNotificationBar();
1170 };
1171
1172 /**
1173 * Shows a notification about whether there are any synced results, and whether
1174 * there are other forms of browsing history on the server.
1175 * @param {boolean} hasSyncedResults Whether there are synced results.
1176 * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
1177 * a sentence about the existence of other forms of browsing history.
1178 */
1179 HistoryView.prototype.showWebHistoryNotification = function(
1180 hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
1181 var message = '';
1182
1183 if (loadTimeData.getBoolean('isUserSignedIn')) {
1184 message += '<span>' + loadTimeData.getString(
1185 hasSyncedResults ? 'hasSyncedResults' : 'noSyncedResults') + '</span>';
1186 }
1187
1188 if (includeOtherFormsOfBrowsingHistory) {
1189 message += ' ' /* A whitespace to separate <span>s. */ + '<span>' +
1190 loadTimeData.getString('otherFormsOfBrowsingHistory') + '</span>';
1191 }
1192
1193 if (message)
1194 this.showNotification(message, false /* isWarning */);
1195 };
1196
1197 /**
1198 * @param {Visit} visit The visit about to be removed from this view.
1199 */
1200 HistoryView.prototype.onBeforeRemove = function(visit) {
1201 assert(this.currentVisits_.indexOf(visit) >= 0);
1202
1203 var rowIndex = this.focusGrid_.getRowIndexForTarget(document.activeElement);
1204 if (rowIndex == -1)
1205 return;
1206
1207 var rowToFocus = this.focusGrid_.rows[rowIndex + 1] ||
1208 this.focusGrid_.rows[rowIndex - 1];
1209 if (rowToFocus)
1210 rowToFocus.getEquivalentElement(document.activeElement).focus();
1211 };
1212
1213 /** @param {Visit} visit The visit about to be unstarred. */
1214 HistoryView.prototype.onBeforeUnstarred = function(visit) {
1215 assert(this.currentVisits_.indexOf(visit) >= 0);
1216 assert(visit.bookmarkStar == document.activeElement);
1217
1218 var rowIndex = this.focusGrid_.getRowIndexForTarget(document.activeElement);
1219 var row = this.focusGrid_.rows[rowIndex];
1220
1221 // Focus the title or domain when the bookmarked star is removed because the
1222 // star will no longer be focusable.
1223 row.root.querySelector('[focus-type=title], [focus-type=domain]').focus();
1224 };
1225
1226 /** @param {Visit} visit The visit that was just unstarred. */
1227 HistoryView.prototype.onAfterUnstarred = function(visit) {
1228 this.updateFocusGrid_();
1229 };
1230
1231 /**
1232 * Removes a single entry from the view. Also removes gaps before and after
1233 * entry if necessary.
1234 * @param {Visit} visit The visit to be removed.
1235 */
1236 HistoryView.prototype.removeVisit = function(visit) {
1237 var entry = visit.domNode_;
1238 var previousEntry = entry.previousSibling;
1239 var nextEntry = entry.nextSibling;
1240 var toRemove = [entry];
1241
1242 // If there is no previous entry, and the next entry is a gap, remove it.
1243 if (!previousEntry && nextEntry && nextEntry.classList.contains('gap'))
1244 toRemove.push(nextEntry);
1245
1246 // If there is no next entry, and the previous entry is a gap, remove it.
1247 if (!nextEntry && previousEntry && previousEntry.classList.contains('gap'))
1248 toRemove.push(previousEntry);
1249
1250 // If both the next and previous entries are gaps, remove the next one.
1251 if (nextEntry && nextEntry.classList.contains('gap') &&
1252 previousEntry && previousEntry.classList.contains('gap')) {
1253 toRemove.push(nextEntry);
1254 }
1255
1256 // If removing the last entry on a day, remove the entire day.
1257 var dayResults = findAncestorByClass(entry, 'day-results');
1258 if (dayResults && dayResults.querySelectorAll('.entry').length <= 1) {
1259 toRemove.push(dayResults.previousSibling); // Remove the 'h3'.
1260 toRemove.push(dayResults);
1261 }
1262
1263 // Callback to be called when each node has finished animating. It detects
1264 // when all the animations have completed.
1265 function onRemove() {
1266 for (var i = 0; i < toRemove.length; ++i) {
1267 if (toRemove[i].parentNode)
1268 return;
1269 }
1270 onEntryRemoved();
1271 }
1272
1273 // Kick off the removal process.
1274 for (var i = 0; i < toRemove.length; ++i) {
1275 removeNode(toRemove[i], onRemove, this);
1276 }
1277 this.updateFocusGrid_();
1278
1279 var index = this.currentVisits_.indexOf(visit);
1280 if (index >= 0)
1281 this.currentVisits_.splice(index, 1);
1282
1283 this.model_.removeVisit(visit);
1284 };
1285
1286 /**
1287 * Called when an individual history entry has been removed from the page.
1288 * This will only be called when all the elements affected by the deletion
1289 * have been removed from the DOM and the animations have completed.
1290 */
1291 HistoryView.prototype.onEntryRemoved = function() {
1292 this.updateSelectionEditButtons();
1293
1294 if (this.model_.getSize() == 0) {
1295 this.clear_();
1296 this.onModelReady(true); // Shows "No entries" message.
1297 }
1298 };
1299
1300 /**
1301 * Adjusts the position of the notification bar based on the size of the page.
1302 */
1303 HistoryView.prototype.positionNotificationBar = function() {
1304 var bar = $('notification-bar');
1305 var container = $('top-container');
1306
1307 // If the bar does not fit beside the editing controls, or if it contains
1308 // more than one message, put it into the overflow state.
1309 var shouldOverflow =
1310 (bar.getBoundingClientRect().top >=
1311 $('editing-controls').getBoundingClientRect().bottom) ||
1312 bar.childElementCount > 1;
1313 container.classList.toggle('overflow', shouldOverflow);
1314 };
1315
1316 /**
1317 * @param {!Element} el An element to look for.
1318 * @return {boolean} Whether |el| is in |this.focusGrid_|.
1319 */
1320 HistoryView.prototype.isInFocusGrid = function(el) {
1321 return this.focusGrid_.getRowIndexForTarget(el) != -1;
1322 };
1323
1324 // HistoryView, private: ------------------------------------------------------
1325
1326 /**
1327 * Clear the results in the view. Since we add results piecemeal, we need
1328 * to clear them out when we switch to a new page or reload.
1329 * @private
1330 */
1331 HistoryView.prototype.clear_ = function() {
1332 var alertOverlay = $('alertOverlay');
1333 if (alertOverlay && alertOverlay.classList.contains('showing'))
1334 hideConfirmationOverlay();
1335
1336 // Remove everything but <h3 id="results-header"> (the first child).
1337 while (this.resultDiv_.children.length > 1) {
1338 this.resultDiv_.removeChild(this.resultDiv_.lastElementChild);
1339 }
1340 $('results-header').textContent = '';
1341
1342 this.currentVisits_.forEach(function(visit) {
1343 visit.isRendered = false;
1344 });
1345 this.currentVisits_ = [];
1346
1347 document.body.classList.remove('has-results');
1348 };
1349
1350 /**
1351 * Record that the given visit has been rendered.
1352 * @param {Visit} visit The visit that was rendered.
1353 * @private
1354 */
1355 HistoryView.prototype.setVisitRendered_ = function(visit) {
1356 visit.isRendered = true;
1357 this.currentVisits_.push(visit);
1358 };
1359
1360 /**
1361 * Generates and adds the grouped visits DOM for a certain domain. This
1362 * includes the clickable arrow and domain name and the visit entries for
1363 * that domain.
1364 * @param {Element} results DOM object to which to add the elements.
1365 * @param {string} domain Current domain name.
1366 * @param {Array} domainVisits Array of visits for this domain.
1367 * @private
1368 */
1369 HistoryView.prototype.getGroupedVisitsDOM_ = function(
1370 results, domain, domainVisits) {
1371 // Add a new domain entry.
1372 var siteResults = results.appendChild(
1373 createElementWithClassName('li', 'site-entry'));
1374
1375 var siteDomainWrapper = siteResults.appendChild(
1376 createElementWithClassName('div', 'site-domain-wrapper'));
1377 // Make a row that will contain the arrow, the favicon and the domain.
1378 var siteDomainRow = siteDomainWrapper.appendChild(
1379 createElementWithClassName('div', 'site-domain-row'));
1380
1381 if (this.model_.editingEntriesAllowed) {
1382 var siteDomainCheckbox =
1383 createElementWithClassName('input', 'domain-checkbox');
1384
1385 siteDomainCheckbox.type = 'checkbox';
1386 siteDomainCheckbox.addEventListener('click', domainCheckboxClicked);
1387 siteDomainCheckbox.domain_ = domain;
1388 siteDomainCheckbox.setAttribute('aria-label', domain);
1389 siteDomainRow.appendChild(siteDomainCheckbox);
1390 }
1391
1392 var siteArrow = siteDomainRow.appendChild(
1393 createElementWithClassName('div', 'site-domain-arrow'));
1394 var siteFavicon = siteDomainRow.appendChild(
1395 createElementWithClassName('div', 'favicon'));
1396 var siteDomain = siteDomainRow.appendChild(
1397 createElementWithClassName('div', 'site-domain'));
1398 var siteDomainLink = siteDomain.appendChild(new ActionLink);
1399 siteDomainLink.textContent = domain;
1400 var numberOfVisits = createElementWithClassName('span', 'number-visits');
1401 var domainElement = document.createElement('span');
1402
1403 numberOfVisits.textContent =
1404 loadTimeData.getStringF('numberVisits',
1405 domainVisits.length.toLocaleString());
1406 siteDomain.appendChild(numberOfVisits);
1407
1408 domainVisits[0].loadFavicon_(siteFavicon);
1409
1410 siteDomainWrapper.addEventListener(
1411 'click', this.toggleGroupedVisits_.bind(this));
1412
1413 if (this.model_.isSupervisedProfile) {
1414 siteDomainRow.appendChild(
1415 getFilteringStatusDOM(domainVisits[0].hostFilteringBehavior));
1416 }
1417
1418 siteResults.appendChild(siteDomainWrapper);
1419 var resultsList = siteResults.appendChild(
1420 createElementWithClassName('ol', 'site-results'));
1421 resultsList.classList.add('grouped');
1422
1423 // Collapse until it gets toggled.
1424 resultsList.style.height = 0;
1425 resultsList.setAttribute('aria-hidden', 'true');
1426
1427 // Add the results for each of the domain.
1428 var isMonthGroupedResult = this.getRangeInDays() == HistoryModel.Range.MONTH;
1429 for (var j = 0, visit; visit = domainVisits[j]; j++) {
1430 resultsList.appendChild(visit.getResultDOM({
1431 focusless: true,
1432 useMonthDate: isMonthGroupedResult,
1433 }));
1434 this.setVisitRendered_(visit);
1435 }
1436 };
1437
1438 /**
1439 * Enables or disables the time range buttons.
1440 * @private
1441 */
1442 HistoryView.prototype.updateRangeButtons_ = function() {
1443 // The enabled state for the previous, today and next buttons.
1444 var previousState = false;
1445 var todayState = false;
1446 var nextState = false;
1447 var usePage = (this.getRangeInDays() == HistoryModel.Range.ALL_TIME);
1448
1449 // Use pagination for most recent visits, offset otherwise.
1450 // TODO(sergiu): Maybe send just one variable in the future.
1451 if (usePage) {
1452 if (this.getPage() != 0) {
1453 nextState = true;
1454 todayState = true;
1455 }
1456 previousState = this.model_.hasMoreResults();
1457 } else {
1458 if (this.getOffset() != 0) {
1459 nextState = true;
1460 todayState = true;
1461 }
1462 previousState = !this.model_.isQueryFinished_;
1463 }
1464
1465 $('range-previous').disabled = !previousState;
1466 $('range-today').disabled = !todayState;
1467 $('range-next').disabled = !nextState;
1468 };
1469
1470 /**
1471 * Groups visits by domain, sorting them by the number of visits.
1472 * @param {Array} visits Visits received from the query results.
1473 * @param {Element} results Object where the results are added to.
1474 * @private
1475 */
1476 HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) {
1477 var visitsByDomain = {};
1478 var domains = [];
1479
1480 // Group the visits into a dictionary and generate a list of domains.
1481 for (var i = 0, visit; visit = visits[i]; i++) {
1482 var domain = visit.domain_;
1483 if (!visitsByDomain[domain]) {
1484 visitsByDomain[domain] = [];
1485 domains.push(domain);
1486 }
1487 visitsByDomain[domain].push(visit);
1488 }
1489 var sortByVisits = function(a, b) {
1490 return visitsByDomain[b].length - visitsByDomain[a].length;
1491 };
1492 domains.sort(sortByVisits);
1493
1494 for (var i = 0; i < domains.length; ++i) {
1495 var domain = domains[i];
1496 this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]);
1497 }
1498 };
1499
1500 /**
1501 * Adds the results for a month.
1502 * @param {Array} visits Visits returned by the query.
1503 * @param {Node} parentNode Node to which to add the results to.
1504 * @private
1505 */
1506 HistoryView.prototype.addMonthResults_ = function(visits, parentNode) {
1507 if (visits.length == 0)
1508 return;
1509
1510 var monthResults = /** @type {HTMLOListElement} */(parentNode.appendChild(
1511 createElementWithClassName('ol', 'month-results')));
1512 // Don't add checkboxes if entries can not be edited.
1513 if (!this.model_.editingEntriesAllowed)
1514 monthResults.classList.add('no-checkboxes');
1515
1516 this.groupVisitsByDomain_(visits, monthResults);
1517 };
1518
1519 /**
1520 * Adds the results for a certain day. This includes a title with the day of
1521 * the results and the results themselves, grouped or not.
1522 * @param {Array} visits Visits returned by the query.
1523 * @param {Node} parentNode Node to which to add the results to.
1524 * @private
1525 */
1526 HistoryView.prototype.addDayResults_ = function(visits, parentNode) {
1527 if (visits.length == 0)
1528 return;
1529
1530 var firstVisit = visits[0];
1531 var day = parentNode.appendChild(createElementWithClassName('h3', 'day'));
1532 day.appendChild(document.createTextNode(firstVisit.dateRelativeDay));
1533 if (firstVisit.continued) {
1534 day.appendChild(document.createTextNode(' ' +
1535 loadTimeData.getString('cont')));
1536 }
1537 var dayResults = /** @type {HTMLElement} */(parentNode.appendChild(
1538 createElementWithClassName('ol', 'day-results')));
1539
1540 // Don't add checkboxes if entries can not be edited.
1541 if (!this.model_.editingEntriesAllowed)
1542 dayResults.classList.add('no-checkboxes');
1543
1544 if (this.model_.getGroupByDomain()) {
1545 this.groupVisitsByDomain_(visits, dayResults);
1546 } else {
1547 var lastTime;
1548
1549 for (var i = 0, visit; visit = visits[i]; i++) {
1550 // If enough time has passed between visits, indicate a gap in browsing.
1551 var thisTime = visit.date.getTime();
1552 if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME)
1553 dayResults.appendChild(createElementWithClassName('li', 'gap'));
1554
1555 // Insert the visit into the DOM.
1556 dayResults.appendChild(visit.getResultDOM({ addTitleFavicon: true }));
1557 this.setVisitRendered_(visit);
1558
1559 lastTime = thisTime;
1560 }
1561 }
1562 };
1563
1564 /**
1565 * Adds the text that shows the current interval, used for week and month
1566 * results.
1567 * @param {Node} resultsFragment The element to which the interval will be
1568 * added to.
1569 * @private
1570 */
1571 HistoryView.prototype.addTimeframeInterval_ = function(resultsFragment) {
1572 if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME)
1573 return;
1574
1575 // If this is a time range result add some text that shows what is the
1576 // time range for the results the user is viewing.
1577 var timeFrame = resultsFragment.appendChild(
1578 createElementWithClassName('h2', 'timeframe'));
1579 // TODO(sergiu): Figure the best way to show this for the first day of
1580 // the month.
1581 timeFrame.appendChild(
1582 document.createTextNode(this.model_.queryInterval));
1583 };
1584
1585 /**
1586 * Update the page with results.
1587 * @param {boolean} doneLoading Whether the current request is complete.
1588 * @private
1589 */
1590 HistoryView.prototype.displayResults_ = function(doneLoading) {
1591 // Either show a page of results received for the all time results or all the
1592 // received results for the weekly and monthly view.
1593 var results = this.model_.visits_;
1594 if (this.getRangeInDays() == HistoryModel.Range.ALL_TIME) {
1595 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE;
1596 var rangeEnd = rangeStart + RESULTS_PER_PAGE;
1597 results = this.model_.getNumberedRange(rangeStart, rangeEnd);
1598 }
1599 var searchText = this.model_.getSearchText();
1600 var groupByDomain = this.model_.getGroupByDomain();
1601
1602 if (searchText) {
1603 var headerText;
1604 if (!doneLoading) {
1605 headerText = loadTimeData.getStringF('searchResultsFor', searchText);
1606 } else if (results.length == 0) {
1607 headerText = loadTimeData.getString('noSearchResults');
1608 } else {
1609 var resultId = results.length == 1 ? 'searchResult' : 'searchResults';
1610 headerText = loadTimeData.getStringF('foundSearchResults',
1611 results.length,
1612 loadTimeData.getString(resultId),
1613 searchText);
1614 }
1615 $('results-header').textContent = headerText;
1616
1617 this.addTimeframeInterval_(this.resultDiv_);
1618
1619 var searchResults = createElementWithClassName('ol', 'search-results');
1620
1621 // Don't add checkboxes if entries can not be edited.
1622 if (!this.model_.editingEntriesAllowed)
1623 searchResults.classList.add('no-checkboxes');
1624
1625 if (doneLoading) {
1626 for (var i = 0, visit; visit = results[i]; i++) {
1627 if (!visit.isRendered) {
1628 searchResults.appendChild(visit.getResultDOM({
1629 isSearchResult: true,
1630 addTitleFavicon: true
1631 }));
1632 this.setVisitRendered_(visit);
1633 }
1634 }
1635 }
1636 this.resultDiv_.appendChild(searchResults);
1637 } else {
1638 var resultsFragment = document.createDocumentFragment();
1639
1640 this.addTimeframeInterval_(resultsFragment);
1641
1642 var noResults = results.length == 0 && doneLoading;
1643 $('results-header').textContent = noResults ?
1644 loadTimeData.getString('noResults') : '';
1645
1646 if (noResults)
1647 return;
1648
1649 if (this.getRangeInDays() == HistoryModel.Range.MONTH &&
1650 groupByDomain) {
1651 // Group everything together in the month view.
1652 this.addMonthResults_(results, resultsFragment);
1653 } else {
1654 var dayStart = 0;
1655 var dayEnd = 0;
1656 // Go through all of the visits and process them in chunks of one day.
1657 while (dayEnd < results.length) {
1658 // Skip over the ones that are already rendered.
1659 while (dayStart < results.length && results[dayStart].isRendered)
1660 ++dayStart;
1661 var dayEnd = dayStart + 1;
1662 while (dayEnd < results.length && results[dayEnd].continued)
1663 ++dayEnd;
1664
1665 this.addDayResults_(
1666 results.slice(dayStart, dayEnd), resultsFragment);
1667 }
1668 }
1669
1670 // Add all the days and their visits to the page.
1671 this.resultDiv_.appendChild(resultsFragment);
1672 }
1673 // After the results have been added to the DOM, determine the size of the
1674 // time column.
1675 this.setTimeColumnWidth_();
1676 };
1677
1678 var focusGridRowSelector = [
1679 ':-webkit-any(.day-results, .search-results) > .entry:not(.fade-out)',
1680 '.expand .grouped .entry:not(.fade-out)',
1681 '.site-domain-wrapper'
1682 ].join(', ');
1683
1684 /** @private */
1685 HistoryView.prototype.updateFocusGrid_ = function() {
1686 var rows = this.resultDiv_.querySelectorAll(focusGridRowSelector);
1687 this.focusGrid_.destroy();
1688
1689 for (var i = 0; i < rows.length; ++i) {
1690 assert(rows[i].parentNode);
1691 this.focusGrid_.addRow(new HistoryFocusRow(rows[i], this.resultDiv_));
1692 }
1693 this.focusGrid_.ensureRowActive();
1694 };
1695
1696 /**
1697 * Update the visibility of the page navigation buttons.
1698 * @private
1699 */
1700 HistoryView.prototype.updateNavBar_ = function() {
1701 this.updateRangeButtons_();
1702
1703 // If grouping by domain is enabled, there's a control bar on top, don't show
1704 // the one on the bottom as well.
1705 if (!loadTimeData.getBoolean('groupByDomain')) {
1706 $('newest-button').hidden = this.pageIndex_ == 0;
1707 $('newer-button').hidden = this.pageIndex_ == 0;
1708 $('older-button').hidden =
1709 this.model_.rangeInDays_ != HistoryModel.Range.ALL_TIME ||
1710 !this.model_.hasMoreResults();
1711 }
1712 };
1713
1714 /**
1715 * Updates the visibility of the 'Clear browsing data' button.
1716 * Only used on mobile platforms.
1717 * @private
1718 */
1719 HistoryView.prototype.updateClearBrowsingDataButton_ = function() {
1720 // Ideally, we should hide the 'Clear browsing data' button whenever the
1721 // soft keyboard is visible. This is not possible, so instead, hide the
1722 // button whenever the search field has focus.
1723 $('clear-browsing-data').hidden =
1724 (document.activeElement === $('search-field'));
1725 };
1726
1727 /**
1728 * Dynamically sets the min-width of the time column for history entries.
1729 * This ensures that all entry times will have the same width, without
1730 * imposing a fixed width that may not be appropriate for some locales.
1731 * @private
1732 */
1733 HistoryView.prototype.setTimeColumnWidth_ = function() {
1734 // Find the maximum width of all the time elements on the page.
1735 var times = this.resultDiv_.querySelectorAll('.entry .time');
1736 Array.prototype.forEach.call(times, function(el) {
1737 el.style.minWidth = '-webkit-min-content';
1738 });
1739 var widths = Array.prototype.map.call(times, function(el) {
1740 // Add an extra pixel to prevent rounding errors from causing the text to
1741 // be ellipsized at certain zoom levels (see crbug.com/329779).
1742 return el.clientWidth + 1;
1743 });
1744 Array.prototype.forEach.call(times, function(el) {
1745 el.style.minWidth = '';
1746 });
1747 var maxWidth = widths.length ? Math.max.apply(null, widths) : 0;
1748
1749 // Add a dynamic stylesheet to the page (or replace the existing one), to
1750 // ensure that all entry times have the same width.
1751 var styleEl = $('timeColumnStyle');
1752 if (!styleEl) {
1753 styleEl = document.head.appendChild(document.createElement('style'));
1754 styleEl.id = 'timeColumnStyle';
1755 }
1756 styleEl.textContent = '.entry .time { min-width: ' + maxWidth + 'px; }';
1757 };
1758
1759 /**
1760 * Toggles an element in the grouped history.
1761 * @param {Event} e The event with element |e.target| which was clicked on.
1762 * @private
1763 */
1764 HistoryView.prototype.toggleGroupedVisits_ = function(e) {
1765 var entry = findAncestorByClass(/** @type {Element} */(e.target),
1766 'site-entry');
1767 var innerResultList = entry.querySelector('.site-results');
1768
1769 if (entry.classList.contains('expand')) {
1770 innerResultList.style.height = 0;
1771 innerResultList.setAttribute('aria-hidden', 'true');
1772 } else {
1773 innerResultList.setAttribute('aria-hidden', 'false');
1774 innerResultList.style.height = 'auto';
1775 // transition does not work on height:auto elements so first set
1776 // the height to auto so that it is computed and then set it to the
1777 // computed value in pixels so the transition works properly.
1778 var height = innerResultList.clientHeight;
1779 innerResultList.style.height = 0;
1780 setTimeout(function() {
1781 innerResultList.style.height = height + 'px';
1782 }, 0);
1783 }
1784
1785 entry.classList.toggle('expand');
1786
1787 var root = entry.querySelector('.site-domain-wrapper');
1788
1789 this.focusGrid_.rows.forEach(function(row) {
1790 row.makeActive(row.root == root);
1791 });
1792
1793 this.updateFocusGrid_();
1794 };
1795
1796 ///////////////////////////////////////////////////////////////////////////////
1797 // State object:
1798 /**
1799 * An 'AJAX-history' implementation.
1800 * @param {HistoryModel} model The model we're representing.
1801 * @param {HistoryView} view The view we're representing.
1802 * @constructor
1803 */
1804 function PageState(model, view) {
1805 // Enforce a singleton.
1806 if (PageState.instance) {
1807 return PageState.instance;
1808 }
1809
1810 this.model = model;
1811 this.view = view;
1812
1813 if (typeof this.checker_ != 'undefined' && this.checker_) {
1814 clearInterval(this.checker_);
1815 }
1816
1817 // TODO(glen): Replace this with a bound method so we don't need
1818 // public model and view.
1819 this.checker_ = window.setInterval(function() {
1820 var hashData = this.getHashData();
1821 var page = parseInt(hashData.page, 10);
1822 var range = parseInt(hashData.range, 10);
1823 var offset = parseInt(hashData.offset, 10);
1824 if (hashData.q != this.model.getSearchText() ||
1825 page != this.view.getPage() ||
1826 range != this.model.rangeInDays ||
1827 offset != this.model.offset) {
1828 this.view.setPageState(hashData.q, page, range, offset);
1829 }
1830 }.bind(this), 50);
1831 }
1832
1833 /**
1834 * Holds the singleton instance.
1835 */
1836 PageState.instance = null;
1837
1838 /**
1839 * @return {Object} An object containing parameters from our window hash.
1840 */
1841 PageState.prototype.getHashData = function() {
1842 var result = {
1843 q: '',
1844 page: 0,
1845 range: 0,
1846 offset: 0
1847 };
1848
1849 if (!window.location.hash)
1850 return result;
1851
1852 var hashSplit = window.location.hash.substr(1).split('&');
1853 for (var i = 0; i < hashSplit.length; i++) {
1854 var pair = hashSplit[i].split('=');
1855 if (pair.length > 1)
1856 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' '));
1857 }
1858
1859 return result;
1860 };
1861
1862 /**
1863 * Set the hash to a specified state, this will create an entry in the
1864 * session history so the back button cycles through hash states, which
1865 * are then picked up by our listener.
1866 * @param {string} term The current search string.
1867 * @param {number} page The page currently being viewed.
1868 * @param {HistoryModel.Range} range The range to view or search over.
1869 * @param {number} offset Set the begining of the query to the specific offset.
1870 */
1871 PageState.prototype.setUIState = function(term, page, range, offset) {
1872 // Make sure the form looks pretty.
1873 $('search-field').value = term;
1874 var hash = this.getHashData();
1875 if (hash.q != term || hash.page != page || hash.range != range ||
1876 hash.offset != offset) {
1877 window.location.hash = PageState.getHashString(term, page, range, offset);
1878 }
1879 };
1880
1881 /**
1882 * Static method to get the hash string for a specified state
1883 * @param {string} term The current search string.
1884 * @param {number} page The page currently being viewed.
1885 * @param {HistoryModel.Range} range The range to view or search over.
1886 * @param {number} offset Set the begining of the query to the specific offset.
1887 * @return {string} The string to be used in a hash.
1888 */
1889 PageState.getHashString = function(term, page, range, offset) {
1890 // Omit elements that are empty.
1891 var newHash = [];
1892
1893 if (term)
1894 newHash.push('q=' + encodeURIComponent(term));
1895
1896 if (page)
1897 newHash.push('page=' + page);
1898
1899 if (range)
1900 newHash.push('range=' + range);
1901
1902 if (offset)
1903 newHash.push('offset=' + offset);
1904
1905 return newHash.join('&');
1906 };
1907
1908 ///////////////////////////////////////////////////////////////////////////////
1909 // Document Functions:
1910 /**
1911 * Window onload handler, sets up the page.
1912 */
1913 function load() {
1914 uber.onContentFrameLoaded();
1915 FocusOutlineManager.forDocument(document);
1916
1917 var searchField = $('search-field');
1918
1919 historyModel = new HistoryModel();
1920 historyView = new HistoryView(historyModel);
1921 pageState = new PageState(historyModel, historyView);
1922
1923 // Create default view.
1924 var hashData = pageState.getHashData();
1925 var page = parseInt(hashData.page, 10) || historyView.getPage();
1926 var range = /** @type {HistoryModel.Range} */(parseInt(hashData.range, 10)) ||
1927 historyView.getRangeInDays();
1928 var offset = parseInt(hashData.offset, 10) || historyView.getOffset();
1929 historyView.setPageState(hashData.q, page, range, offset);
1930
1931 if ($('overlay')) {
1932 cr.ui.overlay.setupOverlay($('overlay'));
1933 cr.ui.overlay.globalInitialization();
1934 }
1935 HistoryFocusManager.getInstance().initialize();
1936
1937 var doSearch = function(e) {
1938 recordUmaAction('HistoryPage_Search');
1939 historyView.setSearch(searchField.value);
1940
1941 if (isMobileVersion())
1942 searchField.blur(); // Dismiss the keyboard.
1943 };
1944
1945 var removeMenu = getRequiredElement('remove-visit');
1946 // Decorate remove-visit before disabling/hiding because the values are
1947 // overwritten when decorating a MenuItem that has a Command.
1948 cr.ui.decorate(removeMenu, MenuItem);
1949 removeMenu.disabled = !loadTimeData.getBoolean('allowDeletingHistory');
1950 removeMenu.hidden = loadTimeData.getBoolean('hideDeleteVisitUI');
1951
1952 document.addEventListener('command', handleCommand);
1953
1954 searchField.addEventListener('search', doSearch);
1955 $('search-button').addEventListener('click', doSearch);
1956
1957 $('more-from-site').addEventListener('activate', function(e) {
1958 activeVisit.showMoreFromSite_();
1959 activeVisit = null;
1960 });
1961
1962 // Only show the controls if the command line switch is activated or the user
1963 // is supervised.
1964 if (loadTimeData.getBoolean('groupByDomain')) {
1965 $('history-page').classList.add('big-topbar-page');
1966 $('filter-controls').hidden = false;
1967 }
1968 // Hide the top container which has the "Clear browsing data" and "Remove
1969 // selected entries" buttons if deleting history is not allowed.
1970 if (!loadTimeData.getBoolean('allowDeletingHistory'))
1971 $('top-container').hidden = true;
1972
1973 uber.setTitle(loadTimeData.getString('title'));
1974
1975 // Adjust the position of the notification bar when the window size changes.
1976 window.addEventListener('resize',
1977 historyView.positionNotificationBar.bind(historyView));
1978
1979 if (isMobileVersion()) {
1980 // Move the search box out of the header.
1981 var resultsDisplay = $('results-display');
1982 resultsDisplay.parentNode.insertBefore($('search-field'), resultsDisplay);
1983
1984 window.addEventListener(
1985 'resize', historyView.updateClearBrowsingDataButton_);
1986
1987 // <if expr="is_ios">
1988 // Trigger window resize event when search field is focused to force update
1989 // of the clear browsing button, which should disappear when search field
1990 // is active. The window is not resized when the virtual keyboard is shown
1991 // on iOS.
1992 searchField.addEventListener('focus', function() {
1993 cr.dispatchSimpleEvent(window, 'resize');
1994 });
1995 // </if> /* is_ios */
1996
1997 // When the search field loses focus, add a delay before updating the
1998 // visibility, otherwise the button will flash on the screen before the
1999 // keyboard animates away.
2000 searchField.addEventListener('blur', function() {
2001 setTimeout(historyView.updateClearBrowsingDataButton_, 250);
2002 });
2003
2004 // Move the button to the bottom of the page.
2005 $('history-page').appendChild($('clear-browsing-data'));
2006 } else {
2007 window.addEventListener('message', function(e) {
2008 e = /** @type {!MessageEvent<!{method: string}>} */(e);
2009 if (e.data.method == 'frameSelected')
2010 searchField.focus();
2011 });
2012 searchField.focus();
2013 }
2014
2015 // <if expr="is_ios">
2016 function checkKeyboardVisibility() {
2017 // Figure out the real height based on the orientation, becauase
2018 // screen.width and screen.height don't update after rotation.
2019 var screenHeight = window.orientation % 180 ? screen.width : screen.height;
2020
2021 // Assume that the keyboard is visible if more than 30% of the screen is
2022 // taken up by window chrome.
2023 var isKeyboardVisible = (window.innerHeight / screenHeight) < 0.7;
2024
2025 document.body.classList.toggle('ios-keyboard-visible', isKeyboardVisible);
2026 }
2027 window.addEventListener('orientationchange', checkKeyboardVisibility);
2028 window.addEventListener('resize', checkKeyboardVisibility);
2029 // </if> /* is_ios */
2030 }
2031
2032 /**
2033 * Handle all commands in the history page.
2034 * @param {!Event} e is a command event.
2035 */
2036 function handleCommand(e) {
2037 switch (e.command.id) {
2038 case 'remove-visit-command':
2039 // Removing visited items needs to be done with a command in order to have
2040 // proper focus. This is because the command event is handled after the
2041 // menu dialog is no longer visible and focus has returned to the history
2042 // items. The activate event is handled when the menu dialog is still
2043 // visible and focus is lost.
2044 // removeEntryFromHistory_ will update activeVisit to the newly focused
2045 // history item.
2046 assert(!$('remove-visit').disabled);
2047 activeVisit.removeEntryFromHistory_(e);
2048 break;
2049 }
2050 }
2051
2052 /**
2053 * Updates the filter status labels of a host/URL entry to the current value.
2054 * @param {Element} statusElement The div which contains the status labels.
2055 * @param {SupervisedUserFilteringBehavior} newStatus The filter status of the
2056 * current domain/URL.
2057 */
2058 function updateHostStatus(statusElement, newStatus) {
2059 var filteringBehaviorDiv =
2060 statusElement.querySelector('.filtering-behavior');
2061 // Reset to the base class first, then add modifier classes if needed.
2062 filteringBehaviorDiv.className = 'filtering-behavior';
2063 if (newStatus == SupervisedUserFilteringBehavior.BLOCK) {
2064 filteringBehaviorDiv.textContent =
2065 loadTimeData.getString('filterBlocked');
2066 filteringBehaviorDiv.classList.add('filter-blocked');
2067 } else {
2068 filteringBehaviorDiv.textContent = '';
2069 }
2070 }
2071
2072 /**
2073 * Click handler for the 'Clear browsing data' dialog.
2074 * @param {Event} e The click event.
2075 */
2076 function openClearBrowsingData(e) {
2077 recordUmaAction('HistoryPage_InitClearBrowsingData');
2078 chrome.send('clearBrowsingData');
2079 }
2080
2081 /**
2082 * Shows the dialog for the user to confirm removal of selected history entries.
2083 */
2084 function showConfirmationOverlay() {
2085 $('alertOverlay').classList.add('showing');
2086 $('overlay').hidden = false;
2087 $('history-page').setAttribute('aria-hidden', 'true');
2088 uber.invokeMethodOnParent('beginInterceptingEvents');
2089
2090 // Change focus to the overlay if any other control was focused by keyboard
2091 // before. Otherwise, no one should have focus.
2092 var focusOverlay = FocusOutlineManager.forDocument(document).visible &&
2093 document.activeElement != document.body;
2094 if ($('history-page').contains(document.activeElement))
2095 document.activeElement.blur();
2096
2097 if (focusOverlay) {
2098 // Wait until the browser knows the button has had a chance to become
2099 // visible.
2100 window.requestAnimationFrame(function() {
2101 var button = cr.ui.overlay.getDefaultButton($('overlay'));
2102 if (button)
2103 button.focus();
2104 });
2105 }
2106 $('alertOverlay').classList.toggle('focus-on-hide', focusOverlay);
2107 }
2108
2109 /**
2110 * Hides the confirmation overlay used to confirm selected history entries.
2111 */
2112 function hideConfirmationOverlay() {
2113 $('alertOverlay').classList.remove('showing');
2114 $('overlay').hidden = true;
2115 $('history-page').removeAttribute('aria-hidden');
2116 uber.invokeMethodOnParent('stopInterceptingEvents');
2117 }
2118
2119 /**
2120 * Shows the confirmation alert for history deletions and permits browser tests
2121 * to override the dialog.
2122 * @param {function()=} okCallback A function to be called when the user presses
2123 * the ok button.
2124 * @param {function()=} cancelCallback A function to be called when the user
2125 * presses the cancel button.
2126 */
2127 function confirmDeletion(okCallback, cancelCallback) {
2128 alertOverlay.setValues(
2129 loadTimeData.getString('removeSelected'),
2130 loadTimeData.getString('deleteWarning'),
2131 loadTimeData.getString('deleteConfirm'),
2132 loadTimeData.getString('cancel'),
2133 okCallback,
2134 cancelCallback);
2135 showConfirmationOverlay();
2136 }
2137
2138 /**
2139 * Click handler for the 'Remove selected items' button.
2140 * Confirms the deletion with the user, and then deletes the selected visits.
2141 */
2142 function removeItems() {
2143 recordUmaAction('HistoryPage_RemoveSelected');
2144 if (!loadTimeData.getBoolean('allowDeletingHistory'))
2145 return;
2146
2147 var checked = $('results-display').querySelectorAll(
2148 '.entry-box input[type=checkbox]:checked:not([disabled])');
2149 var disabledItems = [];
2150 var toBeRemoved = [];
2151
2152 for (var i = 0; i < checked.length; i++) {
2153 var checkbox = checked[i];
2154 var entry = findAncestorByClass(checkbox, 'entry');
2155 toBeRemoved.push(entry.visit);
2156
2157 // Disable the checkbox and put a strikethrough style on the link, so the
2158 // user can see what will be deleted.
2159 checkbox.disabled = true;
2160 entry.visit.titleLink.classList.add('to-be-removed');
2161 disabledItems.push(checkbox);
2162 var integerId = parseInt(entry.visit.id_, 10);
2163 // Record the ID of the entry to signify how many entries are above this
2164 // link on the page.
2165 recordUmaHistogram('HistoryPage.RemoveEntryPosition',
2166 UMA_MAX_BUCKET_VALUE,
2167 integerId);
2168 if (integerId <= UMA_MAX_SUBSET_BUCKET_VALUE) {
2169 recordUmaHistogram('HistoryPage.RemoveEntryPositionSubset',
2170 UMA_MAX_SUBSET_BUCKET_VALUE,
2171 integerId);
2172 }
2173 if (entry.parentNode.className == 'search-results')
2174 recordUmaAction('HistoryPage_SearchResultRemove');
2175 }
2176
2177 function onConfirmRemove() {
2178 recordUmaAction('HistoryPage_ConfirmRemoveSelected');
2179 historyModel.removeVisitsFromHistory(toBeRemoved,
2180 historyView.reload.bind(historyView));
2181 $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
2182 hideConfirmationOverlay();
2183 if ($('alertOverlay').classList.contains('focus-on-hide') &&
2184 FocusOutlineManager.forDocument(document).visible) {
2185 $('search-field').focus();
2186 }
2187 }
2188
2189 function onCancelRemove() {
2190 recordUmaAction('HistoryPage_CancelRemoveSelected');
2191 // Return everything to its previous state.
2192 for (var i = 0; i < disabledItems.length; i++) {
2193 var checkbox = disabledItems[i];
2194 checkbox.disabled = false;
2195
2196 var entry = findAncestorByClass(checkbox, 'entry');
2197 entry.visit.titleLink.classList.remove('to-be-removed');
2198 }
2199 $('overlay').removeEventListener('cancelOverlay', onCancelRemove);
2200 hideConfirmationOverlay();
2201 if ($('alertOverlay').classList.contains('focus-on-hide') &&
2202 FocusOutlineManager.forDocument(document).visible) {
2203 $('remove-selected').focus();
2204 }
2205 }
2206
2207 if (checked.length) {
2208 confirmDeletion(onConfirmRemove, onCancelRemove);
2209 $('overlay').addEventListener('cancelOverlay', onCancelRemove);
2210 }
2211 }
2212
2213 /**
2214 * Handler for the 'click' event on a checkbox.
2215 * @param {Event} e The click event.
2216 */
2217 function checkboxClicked(e) {
2218 handleCheckboxStateChange(/** @type {!HTMLInputElement} */(e.currentTarget),
2219 e.shiftKey);
2220 }
2221
2222 /**
2223 * Post-process of checkbox state change. This handles range selection and
2224 * updates internal state.
2225 * @param {!HTMLInputElement} checkbox Clicked checkbox.
2226 * @param {boolean} shiftKey true if shift key is pressed.
2227 */
2228 function handleCheckboxStateChange(checkbox, shiftKey) {
2229 updateParentCheckbox(checkbox);
2230 var id = Number(checkbox.id.slice('checkbox-'.length));
2231 // Handle multi-select if shift was pressed.
2232 if (shiftKey && (selectionAnchor != -1)) {
2233 var checked = checkbox.checked;
2234 // Set all checkboxes from the anchor up to the clicked checkbox to the
2235 // state of the clicked one.
2236 var begin = Math.min(id, selectionAnchor);
2237 var end = Math.max(id, selectionAnchor);
2238 for (var i = begin; i <= end; i++) {
2239 var ithCheckbox = document.querySelector('#checkbox-' + i);
2240 if (ithCheckbox) {
2241 ithCheckbox.checked = checked;
2242 updateParentCheckbox(ithCheckbox);
2243 }
2244 }
2245 }
2246 selectionAnchor = id;
2247
2248 historyView.updateSelectionEditButtons();
2249 }
2250
2251 /**
2252 * Handler for the 'click' event on a domain checkbox. Checkes or unchecks the
2253 * checkboxes of the visits to this domain in the respective group.
2254 * @param {Event} e The click event.
2255 */
2256 function domainCheckboxClicked(e) {
2257 var siteEntry = findAncestorByClass(/** @type {Element} */(e.currentTarget),
2258 'site-entry');
2259 var checkboxes =
2260 siteEntry.querySelectorAll('.site-results input[type=checkbox]');
2261 for (var i = 0; i < checkboxes.length; i++)
2262 checkboxes[i].checked = e.currentTarget.checked;
2263 historyView.updateSelectionEditButtons();
2264 // Stop propagation as clicking the checkbox would otherwise trigger the
2265 // group to collapse/expand.
2266 e.stopPropagation();
2267 }
2268
2269 /**
2270 * Updates the domain checkbox for this visit checkbox if it has been
2271 * unchecked.
2272 * @param {Element} checkbox The checkbox that has been clicked.
2273 */
2274 function updateParentCheckbox(checkbox) {
2275 if (checkbox.checked)
2276 return;
2277
2278 var entry = findAncestorByClass(checkbox, 'site-entry');
2279 if (!entry)
2280 return;
2281
2282 var groupCheckbox = entry.querySelector('.site-domain-wrapper input');
2283 if (groupCheckbox)
2284 groupCheckbox.checked = false;
2285 }
2286
2287 /**
2288 * Handle click event for entryBoxes.
2289 * @param {!Event} event A click event.
2290 */
2291 function entryBoxClick(event) {
2292 event = /** @type {!MouseEvent} */(event);
2293 // Do nothing if a bookmark star is clicked.
2294 if (event.defaultPrevented)
2295 return;
2296 var element = event.target;
2297 // Do nothing if the event happened in an interactive element.
2298 for (; element != event.currentTarget; element = element.parentNode) {
2299 switch (element.tagName) {
2300 case 'A':
2301 case 'BUTTON':
2302 case 'INPUT':
2303 return;
2304 }
2305 }
2306 var checkbox = assertInstanceof($(event.currentTarget.getAttribute('for')),
2307 HTMLInputElement);
2308 checkbox.checked = !checkbox.checked;
2309 handleCheckboxStateChange(checkbox, event.shiftKey);
2310 // We don't want to focus on the checkbox.
2311 event.preventDefault();
2312 }
2313
2314 /**
2315 * Called when an individual history entry has been removed from the page.
2316 * This will only be called when all the elements affected by the deletion
2317 * have been removed from the DOM and the animations have completed.
2318 */
2319 function onEntryRemoved() {
2320 historyView.onEntryRemoved();
2321 }
2322
2323 /**
2324 * Triggers a fade-out animation, and then removes |node| from the DOM.
2325 * @param {Node} node The node to be removed.
2326 * @param {Function?} onRemove A function to be called after the node
2327 * has been removed from the DOM.
2328 * @param {*=} opt_scope An optional scope object to call |onRemove| with.
2329 */
2330 function removeNode(node, onRemove, opt_scope) {
2331 node.classList.add('fade-out'); // Trigger CSS fade out animation.
2332
2333 // Delete the node when the animation is complete.
2334 node.addEventListener('transitionend', function(e) {
2335 node.parentNode.removeChild(node);
2336
2337 // In case there is nested deletion happening, prevent this event from
2338 // being handled by listeners on ancestor nodes.
2339 e.stopPropagation();
2340
2341 if (onRemove)
2342 onRemove.call(opt_scope);
2343 });
2344 }
2345
2346 /**
2347 * Builds the DOM elements to show the filtering status of a domain/URL.
2348 * @param {SupervisedUserFilteringBehavior} filteringBehavior The filter
2349 * behavior for this item.
2350 * @return {Element} Returns the DOM elements which show the status.
2351 */
2352 function getFilteringStatusDOM(filteringBehavior) {
2353 var filterStatusDiv = createElementWithClassName('div', 'filter-status');
2354 var filteringBehaviorDiv =
2355 createElementWithClassName('div', 'filtering-behavior');
2356 filterStatusDiv.appendChild(filteringBehaviorDiv);
2357
2358 updateHostStatus(filterStatusDiv, filteringBehavior);
2359 return filterStatusDiv;
2360 }
2361
2362
2363 ///////////////////////////////////////////////////////////////////////////////
2364 // Chrome callbacks:
2365
2366 /**
2367 * Our history system calls this function with results from searches.
2368 * @param {HistoryQuery} info An object containing information about the query.
2369 * @param {Array<HistoryEntry>} results A list of results.
2370 */
2371 function historyResult(info, results) {
2372 historyModel.addResults(info, results);
2373 }
2374
2375 /**
2376 * Called by the history backend after receiving results and after discovering
2377 * the existence of other forms of browsing history.
2378 * @param {boolean} hasSyncedResults Whether there are synced results.
2379 * @param {boolean} includeOtherFormsOfBrowsingHistory Whether to include
2380 * a sentence about the existence of other forms of browsing history.
2381 */
2382 function showNotification(
2383 hasSyncedResults, includeOtherFormsOfBrowsingHistory) {
2384 historyView.showWebHistoryNotification(
2385 hasSyncedResults, includeOtherFormsOfBrowsingHistory);
2386 }
2387
2388 /**
2389 * Called by the history backend when history removal is successful.
2390 */
2391 function deleteComplete() {
2392 historyModel.deleteComplete();
2393 }
2394
2395 /**
2396 * Called by the history backend when history removal is unsuccessful.
2397 */
2398 function deleteFailed() {
2399 window.console.log('Delete failed');
2400 }
2401
2402 /**
2403 * Called when the history is deleted by someone else.
2404 */
2405 function historyDeleted() {
2406 var anyChecked = document.querySelector('.entry input:checked') != null;
2407 // Reload the page, unless the user has any items checked.
2408 // TODO(dubroy): We should just reload the page & restore the checked items.
2409 if (!anyChecked)
2410 historyView.reload();
2411 }
2412
2413 // Add handlers to HTML elements.
2414 document.addEventListener('DOMContentLoaded', load);
2415
2416 // This event lets us enable and disable menu items before the menu is shown.
2417 document.addEventListener('canExecute', function(e) {
2418 e.canExecute = true;
2419 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/history/history.html ('k') | chrome/browser/resources/history/history_focus_manager.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698