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

Side by Side Diff: ios/chrome/app/resources/history/history.js

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

Powered by Google App Engine
This is Rietveld 408576698