OLD | NEW |
| (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 }); | |
OLD | NEW |