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