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