| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 /////////////////////////////////////////////////////////////////////////////// |
| 6 // Globals: |
| 7 var RESULTS_PER_PAGE = 150; |
| 8 var MAX_SEARCH_DEPTH_MONTHS = 18; |
| 9 |
| 10 // Amount of time between pageviews that we consider a 'break' in browsing, |
| 11 // measured in milliseconds. |
| 12 var BROWSING_GAP_TIME = 15 * 60 * 1000; |
| 13 |
| 14 function $(o) {return document.getElementById(o);} |
| 15 |
| 16 function createElementWithClassName(type, className) { |
| 17 var elm = document.createElement(type); |
| 18 elm.className = className; |
| 19 return elm; |
| 20 } |
| 21 |
| 22 // Escapes a URI as appropriate for CSS. |
| 23 function encodeURIForCSS(uri) { |
| 24 // CSS uris need to have '(' and ')' escaped. |
| 25 return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)"); |
| 26 } |
| 27 |
| 28 // TODO(glen): Get rid of these global references, replace with a controller |
| 29 // or just make the classes own more of the page. |
| 30 var historyModel; |
| 31 var historyView; |
| 32 var localStrings; |
| 33 var pageState; |
| 34 var deleteQueue = []; |
| 35 var deleteInFlight = false; |
| 36 var selectionAnchor = -1; |
| 37 var idToCheckbox = []; |
| 38 |
| 39 |
| 40 /////////////////////////////////////////////////////////////////////////////// |
| 41 // Page: |
| 42 /** |
| 43 * Class to hold all the information about an entry in our model. |
| 44 * @param {Object} result An object containing the page's data. |
| 45 * @param {boolean} continued Whether this page is on the same day as the |
| 46 * page before it |
| 47 */ |
| 48 function Page(result, continued, model, id) { |
| 49 this.model_ = model; |
| 50 this.title_ = result.title; |
| 51 this.url_ = result.url; |
| 52 this.domain_ = this.getDomainFromURL_(this.url_); |
| 53 this.starred_ = result.starred; |
| 54 this.snippet_ = result.snippet || ""; |
| 55 this.id_ = id; |
| 56 |
| 57 this.changed = false; |
| 58 |
| 59 this.isRendered = false; |
| 60 |
| 61 // All the date information is public so that owners can compare properties of |
| 62 // two items easily. |
| 63 |
| 64 // We get the time in seconds, but we want it in milliseconds. |
| 65 this.time = new Date(result.time * 1000); |
| 66 |
| 67 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always |
| 68 // get all of these. |
| 69 this.dateRelativeDay = result.dateRelativeDay || ""; |
| 70 this.dateTimeOfDay = result.dateTimeOfDay || ""; |
| 71 this.dateShort = result.dateShort || ""; |
| 72 |
| 73 // Whether this is the continuation of a previous day. |
| 74 this.continued = continued; |
| 75 } |
| 76 |
| 77 // Page, Public: -------------------------------------------------------------- |
| 78 /** |
| 79 * Returns a dom structure for a browse page result or a search page result. |
| 80 * @param {boolean} Flag to indicate if result is a search result. |
| 81 * @return {Element} The dom structure. |
| 82 */ |
| 83 Page.prototype.getResultDOM = function(searchResultFlag) { |
| 84 var node = createElementWithClassName('li', 'entry'); |
| 85 var time = createElementWithClassName('div', 'time'); |
| 86 var domain = createElementWithClassName('span', 'domain'); |
| 87 domain.style.backgroundImage = |
| 88 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; |
| 89 domain.textContent = this.domain_; |
| 90 node.appendChild(time); |
| 91 node.appendChild(domain); |
| 92 node.appendChild(this.getTitleDOM_()); |
| 93 if (searchResultFlag) { |
| 94 time.textContent = this.dateShort; |
| 95 var snippet = createElementWithClassName('div', 'snippet'); |
| 96 this.addHighlightedText_(snippet, |
| 97 this.snippet_, |
| 98 this.model_.getSearchText()); |
| 99 node.appendChild(snippet); |
| 100 } else { |
| 101 if (this.model_.getEditMode()) { |
| 102 var checkbox = document.createElement('input'); |
| 103 checkbox.type = 'checkbox'; |
| 104 checkbox.name = this.id_; |
| 105 checkbox.time = this.time.toString(); |
| 106 checkbox.addEventListener("click", checkboxClicked); |
| 107 idToCheckbox[this.id_] = checkbox; |
| 108 time.appendChild(checkbox); |
| 109 } |
| 110 time.appendChild(document.createTextNode(this.dateTimeOfDay)); |
| 111 } |
| 112 return node; |
| 113 }; |
| 114 |
| 115 // Page, private: ------------------------------------------------------------- |
| 116 /** |
| 117 * Extracts and returns the domain (and subdomains) from a URL. |
| 118 * @param {string} The url |
| 119 * @return (string) The domain. An empty string is returned if no domain can |
| 120 * be found. |
| 121 */ |
| 122 Page.prototype.getDomainFromURL_ = function(url) { |
| 123 var domain = url.replace(/^.+:\/\//, '').match(/[^/]+/); |
| 124 return domain ? domain[0] : ''; |
| 125 }; |
| 126 |
| 127 /** |
| 128 * Truncates a string to a maximum lenth (including ... if truncated) |
| 129 * @param {string} The string to be truncated |
| 130 * @param {number} The length to truncate the string to |
| 131 * @return (string) The truncated string |
| 132 */ |
| 133 Page.prototype.truncateString_ = function(str, maxLength) { |
| 134 if (str.length > maxLength) { |
| 135 return str.substr(0, maxLength - 3) + '...'; |
| 136 } else { |
| 137 return str; |
| 138 } |
| 139 }; |
| 140 |
| 141 /** |
| 142 * Add child text nodes to a node such that occurrences of the spcified text is |
| 143 * highligted. |
| 144 * @param {Node} node The node under which new text nodes will be made as |
| 145 * children. |
| 146 * @param {string} content Text to be added beneath |node| as one or more |
| 147 * text nodes. |
| 148 * @param {string} highlightText Occurences of this text inside |content| will |
| 149 * be highlighted. |
| 150 */ |
| 151 Page.prototype.addHighlightedText_ = function(node, content, highlightText) { |
| 152 var i = 0; |
| 153 if (highlightText) { |
| 154 var re = new RegExp(Page.pregQuote_(highlightText), 'gim'); |
| 155 var match; |
| 156 while (match = re.exec(content)) { |
| 157 if (match.index > i) |
| 158 node.appendChild(document.createTextNode(content.slice(i, |
| 159 match.index))); |
| 160 i = re.lastIndex; |
| 161 // Mark the highlighted text in bold. |
| 162 var b = document.createElement('b'); |
| 163 b.textContent = content.substring(match.index, i); |
| 164 node.appendChild(b); |
| 165 } |
| 166 } |
| 167 if (i < content.length) |
| 168 node.appendChild(document.createTextNode(content.slice(i))); |
| 169 }; |
| 170 |
| 171 /** |
| 172 * @return {DOMObject} DOM representation for the title block. |
| 173 */ |
| 174 Page.prototype.getTitleDOM_ = function() { |
| 175 var node = document.createElement('span'); |
| 176 node.className = 'title'; |
| 177 var link = document.createElement('a'); |
| 178 link.href = this.url_; |
| 179 link.id = "id-" + this.id_; |
| 180 |
| 181 var content = this.truncateString_(this.title_, 80); |
| 182 |
| 183 // If we have truncated the title, add a tooltip. |
| 184 if (content.length != this.title_.length) { |
| 185 link.title = this.title_; |
| 186 } |
| 187 this.addHighlightedText_(link, content, this.model_.getSearchText()); |
| 188 node.appendChild(link); |
| 189 |
| 190 if (this.starred_) { |
| 191 node.className += ' starred'; |
| 192 node.appendChild(createElementWithClassName('div', 'starred')); |
| 193 } |
| 194 |
| 195 return node; |
| 196 }; |
| 197 |
| 198 // Page, private, static: ----------------------------------------------------- |
| 199 |
| 200 /** |
| 201 * Quote a string so it can be used in a regular expression. |
| 202 * @param {string} str The source string |
| 203 * @return {string} The escaped string |
| 204 */ |
| 205 Page.pregQuote_ = function(str) { |
| 206 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); |
| 207 }; |
| 208 |
| 209 /////////////////////////////////////////////////////////////////////////////// |
| 210 // HistoryModel: |
| 211 /** |
| 212 * Global container for history data. Future optimizations might include |
| 213 * allowing the creation of a HistoryModel for each search string, allowing |
| 214 * quick flips back and forth between results. |
| 215 * |
| 216 * The history model is based around pages, and only fetching the data to |
| 217 * fill the currently requested page. This is somewhat dependent on the view, |
| 218 * and so future work may wish to change history model to operate on |
| 219 * timeframe (day or week) based containers. |
| 220 */ |
| 221 function HistoryModel() { |
| 222 this.clearModel_(); |
| 223 this.setEditMode(false); |
| 224 this.view_; |
| 225 } |
| 226 |
| 227 // HistoryModel, Public: ------------------------------------------------------ |
| 228 /** |
| 229 * Sets our current view that is called when the history model changes. |
| 230 * @param {HistoryView} view The view to set our current view to. |
| 231 */ |
| 232 HistoryModel.prototype.setView = function(view) { |
| 233 this.view_ = view; |
| 234 }; |
| 235 |
| 236 /** |
| 237 * Start a new search - this will clear out our model. |
| 238 * @param {String} searchText The text to search for |
| 239 * @param {Number} opt_page The page to view - this is mostly used when setting |
| 240 * up an initial view, use #requestPage otherwise. |
| 241 */ |
| 242 HistoryModel.prototype.setSearchText = function(searchText, opt_page) { |
| 243 this.clearModel_(); |
| 244 this.searchText_ = searchText; |
| 245 this.requestedPage_ = opt_page ? opt_page : 0; |
| 246 this.getSearchResults_(); |
| 247 }; |
| 248 |
| 249 /** |
| 250 * Reload our model with the current parameters. |
| 251 */ |
| 252 HistoryModel.prototype.reload = function() { |
| 253 var search = this.searchText_; |
| 254 var page = this.requestedPage_; |
| 255 this.clearModel_(); |
| 256 this.searchText_ = search; |
| 257 this.requestedPage_ = page; |
| 258 this.getSearchResults_(); |
| 259 }; |
| 260 |
| 261 /** |
| 262 * @return {String} The current search text. |
| 263 */ |
| 264 HistoryModel.prototype.getSearchText = function() { |
| 265 return this.searchText_; |
| 266 }; |
| 267 |
| 268 /** |
| 269 * Tell the model that the view will want to see the current page. When |
| 270 * the data becomes available, the model will call the view back. |
| 271 * @page {Number} page The page we want to view. |
| 272 */ |
| 273 HistoryModel.prototype.requestPage = function(page) { |
| 274 this.requestedPage_ = page; |
| 275 this.changed = true; |
| 276 this.updateSearch_(false); |
| 277 }; |
| 278 |
| 279 /** |
| 280 * Receiver for history query. |
| 281 * @param {String} term The search term that the results are for. |
| 282 * @param {Array} results A list of results |
| 283 */ |
| 284 HistoryModel.prototype.addResults = function(info, results) { |
| 285 this.inFlight_ = false; |
| 286 if (info.term != this.searchText_) { |
| 287 // If our results aren't for our current search term, they're rubbish. |
| 288 return; |
| 289 } |
| 290 |
| 291 // Currently we assume we're getting things in date order. This needs to |
| 292 // be updated if that ever changes. |
| 293 if (results) { |
| 294 var lastURL, lastDay; |
| 295 var oldLength = this.pages_.length; |
| 296 if (oldLength) { |
| 297 var oldPage = this.pages_[oldLength - 1]; |
| 298 lastURL = oldPage.url; |
| 299 lastDay = oldPage.dateRelativeDay; |
| 300 } |
| 301 |
| 302 for (var i = 0, thisResult; thisResult = results[i]; i++) { |
| 303 var thisURL = thisResult.url; |
| 304 var thisDay = thisResult.dateRelativeDay; |
| 305 |
| 306 // Remove adjacent duplicates. |
| 307 if (!lastURL || lastURL != thisURL) { |
| 308 // Figure out if this page is in the same day as the previous page, |
| 309 // this is used to determine how day headers should be drawn. |
| 310 this.pages_.push(new Page(thisResult, thisDay == lastDay, this, |
| 311 this.last_id_++)); |
| 312 lastDay = thisDay; |
| 313 lastURL = thisURL; |
| 314 } |
| 315 } |
| 316 if (results.length) |
| 317 this.changed = true; |
| 318 } |
| 319 |
| 320 this.updateSearch_(info.finished); |
| 321 }; |
| 322 |
| 323 /** |
| 324 * @return {Number} The number of pages in the model. |
| 325 */ |
| 326 HistoryModel.prototype.getSize = function() { |
| 327 return this.pages_.length; |
| 328 }; |
| 329 |
| 330 /** |
| 331 * @return {boolean} Whether our history query has covered all of |
| 332 * the user's history |
| 333 */ |
| 334 HistoryModel.prototype.isComplete = function() { |
| 335 return this.complete_; |
| 336 }; |
| 337 |
| 338 /** |
| 339 * Get a list of pages between specified index positions. |
| 340 * @param {Number} start The start index |
| 341 * @param {Number} end The end index |
| 342 * @return {Array} A list of pages |
| 343 */ |
| 344 HistoryModel.prototype.getNumberedRange = function(start, end) { |
| 345 if (start >= this.getSize()) |
| 346 return []; |
| 347 |
| 348 var end = end > this.getSize() ? this.getSize() : end; |
| 349 return this.pages_.slice(start, end); |
| 350 }; |
| 351 |
| 352 /** |
| 353 * @return {boolean} Whether we are in edit mode where history items can be |
| 354 * deleted |
| 355 */ |
| 356 HistoryModel.prototype.getEditMode = function() { |
| 357 return this.editMode_; |
| 358 }; |
| 359 |
| 360 /** |
| 361 * @param {boolean} edit_mode Control whether we are in edit mode. |
| 362 */ |
| 363 HistoryModel.prototype.setEditMode = function(edit_mode) { |
| 364 this.editMode_ = edit_mode; |
| 365 }; |
| 366 |
| 367 // HistoryModel, Private: ----------------------------------------------------- |
| 368 HistoryModel.prototype.clearModel_ = function() { |
| 369 this.inFlight_ = false; // Whether a query is inflight. |
| 370 this.searchText_ = ''; |
| 371 this.searchDepth_ = 0; |
| 372 this.pages_ = []; // Date-sorted list of pages. |
| 373 this.last_id_ = 0; |
| 374 selectionAnchor = -1; |
| 375 idToCheckbox = []; |
| 376 |
| 377 // The page that the view wants to see - we only fetch slightly past this |
| 378 // point. If the view requests a page that we don't have data for, we try |
| 379 // to fetch it and call back when we're done. |
| 380 this.requestedPage_ = 0; |
| 381 |
| 382 this.complete_ = false; |
| 383 |
| 384 if (this.view_) { |
| 385 this.view_.clear_(); |
| 386 } |
| 387 }; |
| 388 |
| 389 /** |
| 390 * Figure out if we need to do more searches to fill the currently requested |
| 391 * page. If we think we can fill the page, call the view and let it know |
| 392 * we're ready to show something. |
| 393 */ |
| 394 HistoryModel.prototype.updateSearch_ = function(finished) { |
| 395 if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || |
| 396 finished) { |
| 397 // We have maxed out. There will be no more data. |
| 398 this.complete_ = true; |
| 399 this.view_.onModelReady(); |
| 400 this.changed = false; |
| 401 } else { |
| 402 // If we can't fill the requested page, ask for more data unless a request |
| 403 // is still in-flight. |
| 404 if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) { |
| 405 this.getSearchResults_(this.searchDepth_ + 1); |
| 406 } |
| 407 |
| 408 // If we have any data for the requested page, show it. |
| 409 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { |
| 410 this.view_.onModelReady(); |
| 411 this.changed = false; |
| 412 } |
| 413 } |
| 414 }; |
| 415 |
| 416 /** |
| 417 * Get search results for a selected depth. Our history system is optimized |
| 418 * for queries that don't cross month boundaries, but an entire month's |
| 419 * worth of data is huge. When we're in browse mode (searchText is empty) |
| 420 * we request the data a day at a time. When we're searching, a month is |
| 421 * used. |
| 422 * |
| 423 * TODO: Fix this for when the user's clock goes across month boundaries. |
| 424 * @param {number} opt_day How many days back to do the search. |
| 425 */ |
| 426 HistoryModel.prototype.getSearchResults_ = function(depth) { |
| 427 this.searchDepth_ = depth || 0; |
| 428 |
| 429 if (this.searchText_ == "") { |
| 430 chrome.send('getHistory', |
| 431 [String(this.searchDepth_)]); |
| 432 } else { |
| 433 chrome.send('searchHistory', |
| 434 [this.searchText_, String(this.searchDepth_)]); |
| 435 } |
| 436 |
| 437 this.inFlight_ = true; |
| 438 }; |
| 439 |
| 440 /** |
| 441 * Check to see if we have data for a given page. |
| 442 * @param {number} page The page number |
| 443 * @return {boolean} Whether we have any data for the given page. |
| 444 */ |
| 445 HistoryModel.prototype.haveDataForPage_ = function(page) { |
| 446 return (page * RESULTS_PER_PAGE < this.getSize()); |
| 447 }; |
| 448 |
| 449 /** |
| 450 * Check to see if we have data to fill a page. |
| 451 * @param {number} page The page number. |
| 452 * @return {boolean} Whether we have data to fill the page. |
| 453 */ |
| 454 HistoryModel.prototype.canFillPage_ = function(page) { |
| 455 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); |
| 456 }; |
| 457 |
| 458 /////////////////////////////////////////////////////////////////////////////// |
| 459 // HistoryView: |
| 460 /** |
| 461 * Functions and state for populating the page with HTML. This should one-day |
| 462 * contain the view and use event handlers, rather than pushing HTML out and |
| 463 * getting called externally. |
| 464 * @param {HistoryModel} model The model backing this view. |
| 465 */ |
| 466 function HistoryView(model) { |
| 467 this.summaryTd_ = $('results-summary'); |
| 468 this.summaryTd_.textContent = localStrings.getString('loading'); |
| 469 this.editButtonTd_ = $('edit-button'); |
| 470 this.editingControlsDiv_ = $('editing-controls'); |
| 471 this.resultDiv_ = $('results-display'); |
| 472 this.pageDiv_ = $('results-pagination'); |
| 473 this.model_ = model |
| 474 this.pageIndex_ = 0; |
| 475 this.lastDisplayed_ = []; |
| 476 |
| 477 this.model_.setView(this); |
| 478 |
| 479 this.currentPages_ = []; |
| 480 |
| 481 var self = this; |
| 482 window.onresize = function() { |
| 483 self.updateEntryAnchorWidth_(); |
| 484 }; |
| 485 self.updateEditControls_(); |
| 486 |
| 487 this.boundUpdateRemoveButton_ = function(e) { |
| 488 return self.updateRemoveButton_(e); |
| 489 }; |
| 490 } |
| 491 |
| 492 // HistoryView, public: ------------------------------------------------------- |
| 493 /** |
| 494 * Do a search and optionally view a certain page. |
| 495 * @param {string} term The string to search for. |
| 496 * @param {number} opt_page The page we wish to view, only use this for |
| 497 * setting up initial views, as this triggers a search. |
| 498 */ |
| 499 HistoryView.prototype.setSearch = function(term, opt_page) { |
| 500 this.pageIndex_ = parseInt(opt_page || 0, 10); |
| 501 window.scrollTo(0, 0); |
| 502 this.model_.setSearchText(term, this.pageIndex_); |
| 503 if (term) { |
| 504 this.setEditMode(false); |
| 505 } |
| 506 this.updateEditControls_(); |
| 507 pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_); |
| 508 }; |
| 509 |
| 510 /** |
| 511 * Controls edit mode where history can be deleted. |
| 512 * @param {boolean} edit_mode Whether to enable edit mode. |
| 513 */ |
| 514 HistoryView.prototype.setEditMode = function(edit_mode) { |
| 515 this.model_.setEditMode(edit_mode); |
| 516 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), |
| 517 this.pageIndex_); |
| 518 }; |
| 519 |
| 520 /** |
| 521 * Toggles the edit mode and triggers UI update. |
| 522 */ |
| 523 HistoryView.prototype.toggleEditMode = function() { |
| 524 var editMode = !this.model_.getEditMode(); |
| 525 this.setEditMode(editMode); |
| 526 this.updateEditControls_(); |
| 527 }; |
| 528 |
| 529 /** |
| 530 * @return {boolean} Whether we are in edit mode where history items can be |
| 531 * deleted |
| 532 */ |
| 533 HistoryView.prototype.getEditMode = function() { |
| 534 return this.model_.getEditMode(); |
| 535 }; |
| 536 |
| 537 /** |
| 538 * Reload the current view. |
| 539 */ |
| 540 HistoryView.prototype.reload = function() { |
| 541 this.model_.reload(); |
| 542 }; |
| 543 |
| 544 /** |
| 545 * Switch to a specified page. |
| 546 * @param {number} page The page we wish to view. |
| 547 */ |
| 548 HistoryView.prototype.setPage = function(page) { |
| 549 this.clear_(); |
| 550 this.pageIndex_ = parseInt(page, 10); |
| 551 window.scrollTo(0, 0); |
| 552 this.model_.requestPage(page); |
| 553 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), |
| 554 this.pageIndex_); |
| 555 }; |
| 556 |
| 557 /** |
| 558 * @return {number} The page number being viewed. |
| 559 */ |
| 560 HistoryView.prototype.getPage = function() { |
| 561 return this.pageIndex_; |
| 562 }; |
| 563 |
| 564 /** |
| 565 * Callback for the history model to let it know that it has data ready for us |
| 566 * to view. |
| 567 */ |
| 568 HistoryView.prototype.onModelReady = function() { |
| 569 this.displayResults_(); |
| 570 }; |
| 571 |
| 572 // HistoryView, private: ------------------------------------------------------ |
| 573 /** |
| 574 * Clear the results in the view. Since we add results piecemeal, we need |
| 575 * to clear them out when we switch to a new page or reload. |
| 576 */ |
| 577 HistoryView.prototype.clear_ = function() { |
| 578 this.resultDiv_.textContent = ''; |
| 579 |
| 580 var pages = this.currentPages_; |
| 581 for (var i = 0; i < pages.length; i++) { |
| 582 pages[i].isRendered = false; |
| 583 } |
| 584 this.currentPages_ = []; |
| 585 }; |
| 586 |
| 587 HistoryView.prototype.setPageRendered_ = function(page) { |
| 588 page.isRendered = true; |
| 589 this.currentPages_.push(page); |
| 590 }; |
| 591 |
| 592 /** |
| 593 * Update the page with results. |
| 594 */ |
| 595 HistoryView.prototype.displayResults_ = function() { |
| 596 var results = this.model_.getNumberedRange( |
| 597 this.pageIndex_ * RESULTS_PER_PAGE, |
| 598 this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); |
| 599 |
| 600 if (this.model_.getSearchText()) { |
| 601 var searchResults = createElementWithClassName('ol', 'search-results'); |
| 602 for (var i = 0, page; page = results[i]; i++) { |
| 603 if (!page.isRendered) { |
| 604 searchResults.appendChild(page.getResultDOM(true)); |
| 605 this.setPageRendered_(page); |
| 606 } |
| 607 } |
| 608 this.resultDiv_.appendChild(searchResults); |
| 609 } else { |
| 610 var resultsFragment = document.createDocumentFragment(); |
| 611 var lastTime = Math.infinity; |
| 612 var dayResults; |
| 613 for (var i = 0, page; page = results[i]; i++) { |
| 614 if (page.isRendered) { |
| 615 continue; |
| 616 } |
| 617 // Break across day boundaries and insert gaps for browsing pauses. |
| 618 // Create a dayResults element to contain results for each day |
| 619 var thisTime = page.time.getTime(); |
| 620 |
| 621 if ((i == 0 && page.continued) || !page.continued) { |
| 622 var day = createElementWithClassName('h2', 'day'); |
| 623 day.appendChild(document.createTextNode(page.dateRelativeDay)); |
| 624 if (i == 0 && page.continued) { |
| 625 day.appendChild(document.createTextNode(' ' + |
| 626 localStrings.getString('cont'))); |
| 627 } |
| 628 |
| 629 // If there is an existing dayResults element, append it. |
| 630 if (dayResults) { |
| 631 resultsFragment.appendChild(dayResults); |
| 632 } |
| 633 resultsFragment.appendChild(day); |
| 634 dayResults = createElementWithClassName('ol', 'day-results'); |
| 635 } else if (lastTime - thisTime > BROWSING_GAP_TIME) { |
| 636 if (dayResults) { |
| 637 dayResults.appendChild(createElementWithClassName('li', 'gap')); |
| 638 } |
| 639 } |
| 640 lastTime = thisTime; |
| 641 // Add entry. |
| 642 if (dayResults) { |
| 643 dayResults.appendChild(page.getResultDOM(false)); |
| 644 this.setPageRendered_(page); |
| 645 } |
| 646 } |
| 647 // Add final dayResults element. |
| 648 if (dayResults) { |
| 649 resultsFragment.appendChild(dayResults); |
| 650 } |
| 651 this.resultDiv_.appendChild(resultsFragment); |
| 652 } |
| 653 |
| 654 this.displaySummaryBar_(); |
| 655 this.displayNavBar_(); |
| 656 this.updateEntryAnchorWidth_(); |
| 657 }; |
| 658 |
| 659 /** |
| 660 * Update the summary bar with descriptive text. |
| 661 */ |
| 662 HistoryView.prototype.displaySummaryBar_ = function() { |
| 663 var searchText = this.model_.getSearchText(); |
| 664 if (searchText != '') { |
| 665 this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', |
| 666 searchText); |
| 667 } else { |
| 668 this.summaryTd_.textContent = localStrings.getString('history'); |
| 669 } |
| 670 }; |
| 671 |
| 672 /** |
| 673 * Update the widgets related to edit mode. |
| 674 */ |
| 675 HistoryView.prototype.updateEditControls_ = function() { |
| 676 // Display a button (looking like a link) to enable/disable edit mode. |
| 677 var oldButton = this.editButtonTd_.firstChild; |
| 678 if (this.model_.getSearchText()) { |
| 679 this.editButtonTd_.replaceChild(document.createElement('p'), oldButton); |
| 680 this.editingControlsDiv_.textContent = ''; |
| 681 return; |
| 682 } |
| 683 |
| 684 var editMode = this.model_.getEditMode(); |
| 685 var button = createElementWithClassName('button', 'edit-button'); |
| 686 button.onclick = toggleEditMode; |
| 687 button.textContent = localStrings.getString(editMode ? |
| 688 'doneediting' : 'edithistory'); |
| 689 this.editButtonTd_.replaceChild(button, oldButton); |
| 690 |
| 691 this.editingControlsDiv_.textContent = ''; |
| 692 |
| 693 if (editMode) { |
| 694 // Button to delete the selected items. |
| 695 button = document.createElement('button'); |
| 696 button.onclick = removeItems; |
| 697 button.textContent = localStrings.getString('removeselected'); |
| 698 button.disabled = true; |
| 699 this.editingControlsDiv_.appendChild(button); |
| 700 this.removeButton_ = button; |
| 701 |
| 702 // Button that opens up the clear browsing data dialog. |
| 703 button = document.createElement('button'); |
| 704 button.onclick = openClearBrowsingData; |
| 705 button.textContent = localStrings.getString('clearallhistory'); |
| 706 this.editingControlsDiv_.appendChild(button); |
| 707 |
| 708 // Listen for clicks in the page to sync the disabled state. |
| 709 document.addEventListener('click', this.boundUpdateRemoveButton_); |
| 710 } else { |
| 711 this.removeButton_ = null; |
| 712 document.removeEventListener('click', this.boundUpdateRemoveButton_); |
| 713 } |
| 714 }; |
| 715 |
| 716 /** |
| 717 * Updates the disabled state of the remove button when in editing mode. |
| 718 * @param {!Event} e The click event object. |
| 719 * @private |
| 720 */ |
| 721 HistoryView.prototype.updateRemoveButton_ = function(e) { |
| 722 if (e.target.tagName != 'INPUT') |
| 723 return; |
| 724 |
| 725 var anyChecked = document.querySelector('.entry input:checked') != null; |
| 726 if (this.removeButton_) |
| 727 this.removeButton_.disabled = !anyChecked; |
| 728 }; |
| 729 |
| 730 /** |
| 731 * Update the pagination tools. |
| 732 */ |
| 733 HistoryView.prototype.displayNavBar_ = function() { |
| 734 this.pageDiv_.textContent = ''; |
| 735 |
| 736 if (this.pageIndex_ > 0) { |
| 737 this.pageDiv_.appendChild( |
| 738 this.createPageNav_(0, localStrings.getString('newest'))); |
| 739 this.pageDiv_.appendChild( |
| 740 this.createPageNav_(this.pageIndex_ - 1, |
| 741 localStrings.getString('newer'))); |
| 742 } |
| 743 |
| 744 // TODO(feldstein): this causes the navbar to not show up when your first |
| 745 // page has the exact amount of results as RESULTS_PER_PAGE. |
| 746 if (this.model_.getSize() > (this.pageIndex_ + 1) * RESULTS_PER_PAGE) { |
| 747 this.pageDiv_.appendChild( |
| 748 this.createPageNav_(this.pageIndex_ + 1, |
| 749 localStrings.getString('older'))); |
| 750 } |
| 751 }; |
| 752 |
| 753 /** |
| 754 * Make a DOM object representation of a page navigation link. |
| 755 * @param {number} page The page index the navigation element should link to |
| 756 * @param {string} name The text content of the link |
| 757 * @return {HTMLAnchorElement} the pagination link |
| 758 */ |
| 759 HistoryView.prototype.createPageNav_ = function(page, name) { |
| 760 anchor = document.createElement('a'); |
| 761 anchor.className = 'page-navigation'; |
| 762 anchor.textContent = name; |
| 763 var hashString = PageState.getHashString(this.model_.getEditMode(), |
| 764 this.model_.getSearchText(), page); |
| 765 var link = 'chrome://history2/' + (hashString ? '#' + hashString : ''); |
| 766 anchor.href = link; |
| 767 anchor.onclick = function() { |
| 768 setPage(page); |
| 769 return false; |
| 770 }; |
| 771 return anchor; |
| 772 }; |
| 773 |
| 774 /** |
| 775 * Updates the CSS rule for the entry anchor. |
| 776 * @private |
| 777 */ |
| 778 HistoryView.prototype.updateEntryAnchorWidth_ = function() { |
| 779 // We need to have at least on .title div to be able to calculate the |
| 780 // desired width of the anchor. |
| 781 var titleElement = document.querySelector('.entry .title'); |
| 782 if (!titleElement) |
| 783 return; |
| 784 |
| 785 // Create new CSS rules and add them last to the last stylesheet. |
| 786 // TODO(jochen): The following code does not work due to WebKit bug #32309 |
| 787 // if (!this.entryAnchorRule_) { |
| 788 // var styleSheets = document.styleSheets; |
| 789 // var styleSheet = styleSheets[styleSheets.length - 1]; |
| 790 // var rules = styleSheet.cssRules; |
| 791 // var createRule = function(selector) { |
| 792 // styleSheet.insertRule(selector + '{}', rules.length); |
| 793 // return rules[rules.length - 1]; |
| 794 // }; |
| 795 // this.entryAnchorRule_ = createRule('.entry .title > a'); |
| 796 // // The following rule needs to be more specific to have higher priority. |
| 797 // this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a'); |
| 798 // } |
| 799 // |
| 800 // var anchorMaxWith = titleElement.offsetWidth; |
| 801 // this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px'; |
| 802 // // Adjust by the width of star plus its margin. |
| 803 // this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px'; |
| 804 }; |
| 805 |
| 806 /////////////////////////////////////////////////////////////////////////////// |
| 807 // State object: |
| 808 /** |
| 809 * An 'AJAX-history' implementation. |
| 810 * @param {HistoryModel} model The model we're representing |
| 811 * @param {HistoryView} view The view we're representing |
| 812 */ |
| 813 function PageState(model, view) { |
| 814 // Enforce a singleton. |
| 815 if (PageState.instance) { |
| 816 return PageState.instance; |
| 817 } |
| 818 |
| 819 this.model = model; |
| 820 this.view = view; |
| 821 |
| 822 if (typeof this.checker_ != 'undefined' && this.checker_) { |
| 823 clearInterval(this.checker_); |
| 824 } |
| 825 |
| 826 // TODO(glen): Replace this with a bound method so we don't need |
| 827 // public model and view. |
| 828 this.checker_ = setInterval((function(state_obj) { |
| 829 var hashData = state_obj.getHashData(); |
| 830 |
| 831 if (hashData.q != state_obj.model.getSearchText(term)) { |
| 832 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); |
| 833 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { |
| 834 state_obj.view.setPage(hashData.p); |
| 835 } |
| 836 }), 50, this); |
| 837 } |
| 838 |
| 839 PageState.instance = null; |
| 840 |
| 841 /** |
| 842 * @return {Object} An object containing parameters from our window hash. |
| 843 */ |
| 844 PageState.prototype.getHashData = function() { |
| 845 var result = { |
| 846 e : 0, |
| 847 q : '', |
| 848 p : 0 |
| 849 }; |
| 850 |
| 851 if (!window.location.hash) { |
| 852 return result; |
| 853 } |
| 854 |
| 855 var hashSplit = window.location.hash.substr(1).split('&'); |
| 856 for (var i = 0; i < hashSplit.length; i++) { |
| 857 var pair = hashSplit[i].split('='); |
| 858 if (pair.length > 1) { |
| 859 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); |
| 860 } |
| 861 } |
| 862 |
| 863 return result; |
| 864 }; |
| 865 |
| 866 /** |
| 867 * Set the hash to a specified state, this will create an entry in the |
| 868 * session history so the back button cycles through hash states, which |
| 869 * are then picked up by our listener. |
| 870 * @param {string} term The current search string. |
| 871 * @param {string} page The page currently being viewed. |
| 872 */ |
| 873 PageState.prototype.setUIState = function(editMode, term, page) { |
| 874 // Make sure the form looks pretty. |
| 875 document.forms[0].term.value = term; |
| 876 var currentHash = this.getHashData(); |
| 877 if (Boolean(currentHash.e) != editMode || currentHash.q != term || |
| 878 currentHash.p != page) { |
| 879 window.location.hash = PageState.getHashString(editMode, term, page); |
| 880 } |
| 881 }; |
| 882 |
| 883 /** |
| 884 * Static method to get the hash string for a specified state |
| 885 * @param {string} term The current search string. |
| 886 * @param {string} page The page currently being viewed. |
| 887 * @return {string} The string to be used in a hash. |
| 888 */ |
| 889 PageState.getHashString = function(editMode, term, page) { |
| 890 var newHash = []; |
| 891 if (editMode) { |
| 892 newHash.push('e=1'); |
| 893 } |
| 894 if (term) { |
| 895 newHash.push('q=' + encodeURIComponent(term)); |
| 896 } |
| 897 if (page != undefined) { |
| 898 newHash.push('p=' + page); |
| 899 } |
| 900 |
| 901 return newHash.join('&'); |
| 902 }; |
| 903 |
| 904 /////////////////////////////////////////////////////////////////////////////// |
| 905 // Document Functions: |
| 906 /** |
| 907 * Window onload handler, sets up the page. |
| 908 */ |
| 909 function load() { |
| 910 $('term').focus(); |
| 911 |
| 912 localStrings = new LocalStrings(); |
| 913 historyModel = new HistoryModel(); |
| 914 historyView = new HistoryView(historyModel); |
| 915 pageState = new PageState(historyModel, historyView); |
| 916 |
| 917 // Create default view. |
| 918 var hashData = pageState.getHashData(); |
| 919 if (Boolean(hashData.e)) { |
| 920 historyView.toggleEditMode(); |
| 921 } |
| 922 historyView.setSearch(hashData.q, hashData.p); |
| 923 |
| 924 // Setup click handlers. |
| 925 $('history-section').onclick = function () { |
| 926 setSearch(''); |
| 927 return false; |
| 928 }; |
| 929 $('search-form').onsubmit = function () { |
| 930 setSearch(this.term.value); |
| 931 return false; |
| 932 }; |
| 933 } |
| 934 |
| 935 /** |
| 936 * TODO(glen): Get rid of this function. |
| 937 * Set the history view to a specified page. |
| 938 * @param {String} term The string to search for |
| 939 */ |
| 940 function setSearch(term) { |
| 941 if (historyView) { |
| 942 historyView.setSearch(term); |
| 943 } |
| 944 } |
| 945 |
| 946 /** |
| 947 * TODO(glen): Get rid of this function. |
| 948 * Set the history view to a specified page. |
| 949 * @param {number} page The page to set the view to. |
| 950 */ |
| 951 function setPage(page) { |
| 952 if (historyView) { |
| 953 historyView.setPage(page); |
| 954 } |
| 955 } |
| 956 |
| 957 /** |
| 958 * TODO(glen): Get rid of this function. |
| 959 * Toggles edit mode. |
| 960 */ |
| 961 function toggleEditMode() { |
| 962 if (historyView) { |
| 963 historyView.toggleEditMode(); |
| 964 historyView.reload(); |
| 965 } |
| 966 } |
| 967 |
| 968 /** |
| 969 * Delete the next item in our deletion queue. |
| 970 */ |
| 971 function deleteNextInQueue() { |
| 972 if (!deleteInFlight && deleteQueue.length) { |
| 973 deleteInFlight = true; |
| 974 chrome.send('removeURLsOnOneDay', |
| 975 [String(deleteQueue[0])].concat(deleteQueue[1])); |
| 976 } |
| 977 } |
| 978 |
| 979 /** |
| 980 * Open the clear browsing data dialog. |
| 981 */ |
| 982 function openClearBrowsingData() { |
| 983 chrome.send('clearBrowsingData', []); |
| 984 return false; |
| 985 } |
| 986 |
| 987 /** |
| 988 * Collect IDs from checked checkboxes and send to Chrome for deletion. |
| 989 */ |
| 990 function removeItems() { |
| 991 var checkboxes = document.getElementsByTagName('input'); |
| 992 var ids = []; |
| 993 var disabledItems = []; |
| 994 var queue = []; |
| 995 var date = new Date(); |
| 996 for (var i = 0; i < checkboxes.length; i++) { |
| 997 if (checkboxes[i].type == 'checkbox' && checkboxes[i].checked && |
| 998 !checkboxes[i].disabled) { |
| 999 var cbDate = new Date(checkboxes[i].time); |
| 1000 if (date.getFullYear() != cbDate.getFullYear() || |
| 1001 date.getMonth() != cbDate.getMonth() || |
| 1002 date.getDate() != cbDate.getDate()) { |
| 1003 if (ids.length > 0) { |
| 1004 queue.push(date.valueOf() / 1000); |
| 1005 queue.push(ids); |
| 1006 } |
| 1007 ids = []; |
| 1008 date = cbDate; |
| 1009 } |
| 1010 var link = $('id-' + checkboxes[i].name); |
| 1011 checkboxes[i].disabled = true; |
| 1012 link.style.textDecoration = 'line-through'; |
| 1013 disabledItems.push(checkboxes[i]); |
| 1014 ids.push(link.href); |
| 1015 } |
| 1016 } |
| 1017 if (ids.length > 0) { |
| 1018 queue.push(date.valueOf() / 1000); |
| 1019 queue.push(ids); |
| 1020 } |
| 1021 if (queue.length > 0) { |
| 1022 if (confirm(localStrings.getString('deletewarning'))) { |
| 1023 deleteQueue = deleteQueue.concat(queue); |
| 1024 deleteNextInQueue(); |
| 1025 } else { |
| 1026 // If the remove is cancelled, return the checkboxes to their |
| 1027 // enabled, non-line-through state. |
| 1028 for (var i = 0; i < disabledItems.length; i++) { |
| 1029 var link = $('id-' + disabledItems[i].name); |
| 1030 disabledItems[i].disabled = false; |
| 1031 link.style.textDecoration = ''; |
| 1032 } |
| 1033 } |
| 1034 } |
| 1035 return false; |
| 1036 } |
| 1037 |
| 1038 /** |
| 1039 * Toggle state of checkbox and handle Shift modifier. |
| 1040 */ |
| 1041 function checkboxClicked(event) { |
| 1042 if (event.shiftKey && (selectionAnchor != -1)) { |
| 1043 var checked = this.checked; |
| 1044 // Set all checkboxes from the anchor up to the clicked checkbox to the |
| 1045 // state of the clicked one. |
| 1046 var begin = Math.min(this.name, selectionAnchor); |
| 1047 var end = Math.max(this.name, selectionAnchor); |
| 1048 for (var i = begin; i <= end; i++) { |
| 1049 idToCheckbox[i].checked = checked; |
| 1050 } |
| 1051 } |
| 1052 selectionAnchor = this.name; |
| 1053 this.focus(); |
| 1054 } |
| 1055 |
| 1056 /////////////////////////////////////////////////////////////////////////////// |
| 1057 // Chrome callbacks: |
| 1058 /** |
| 1059 * Our history system calls this function with results from searches. |
| 1060 */ |
| 1061 function historyResult(info, results) { |
| 1062 historyModel.addResults(info, results); |
| 1063 } |
| 1064 |
| 1065 /** |
| 1066 * Our history system calls this function when a deletion has finished. |
| 1067 */ |
| 1068 function deleteComplete() { |
| 1069 window.console.log('Delete complete'); |
| 1070 deleteInFlight = false; |
| 1071 if (deleteQueue.length > 2) { |
| 1072 deleteQueue = deleteQueue.slice(2); |
| 1073 deleteNextInQueue(); |
| 1074 } else { |
| 1075 deleteQueue = []; |
| 1076 } |
| 1077 } |
| 1078 |
| 1079 /** |
| 1080 * Our history system calls this function if a delete is not ready (e.g. |
| 1081 * another delete is in-progress). |
| 1082 */ |
| 1083 function deleteFailed() { |
| 1084 window.console.log('Delete failed'); |
| 1085 // The deletion failed - try again later. |
| 1086 deleteInFlight = false; |
| 1087 setTimeout(deleteNextInQueue, 500); |
| 1088 } |
| 1089 |
| 1090 /** |
| 1091 * We're called when something is deleted (either by us or by someone |
| 1092 * else). |
| 1093 */ |
| 1094 function historyDeleted() { |
| 1095 window.console.log('History deleted'); |
| 1096 var anyChecked = document.querySelector('.entry input:checked') != null; |
| 1097 if (!(historyView.getEditMode() && anyChecked)) |
| 1098 historyView.reload(); |
| 1099 } |
| 1100 |
| 1101 // Add handlers to HTML elements. |
| 1102 document.addEventListener('DOMContentLoaded', load); |
| OLD | NEW |