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