history.html revision dc0f95d653279beabeb9817299e2902918ba123e
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: 12var RESULTS_PER_PAGE = 150; 13var MAX_SEARCH_DEPTH_MONTHS = 18; 14 15// Amount of time between pageviews that we consider a 'break' in browsing, 16// measured in milliseconds. 17var BROWSING_GAP_TIME = 15 * 60 * 1000; 18 19function $(o) {return document.getElementById(o);} 20 21function 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. 28function 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. 35var historyModel; 36var historyView; 37var localStrings; 38var pageState; 39var deleteQueue = []; 40var deleteInFlight = false; 41var selectionAnchor = -1; 42var 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 */ 53function 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 */ 86Page.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 */ 108Page.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 114 var titleCell = document.createElement('td'); 115 titleCell.valign = 'top'; 116 titleCell.appendChild(this.getTitleDOM_()); 117 var snippet = createElementWithClassName('div', 'snippet'); 118 this.addHighlightedText_(snippet, 119 this.snippet_, 120 this.model_.getSearchText()); 121 titleCell.appendChild(snippet); 122 row.appendChild(titleCell); 123 124 return row; 125}; 126 127// Page, private: ------------------------------------------------------------- 128/** 129 * Add child text nodes to a node such that occurrences of the spcified text is 130 * highligted. 131 * @param {Node} node The node under which new text nodes will be made as 132 * children. 133 * @param {string} content Text to be added beneath |node| as one or more 134 * text nodes. 135 * @param {string} highlightText Occurences of this text inside |content| will 136 * be highlighted. 137 */ 138Page.prototype.addHighlightedText_ = function(node, content, highlightText) { 139 var i = 0; 140 if (highlightText) { 141 var re = new RegExp(Page.pregQuote_(highlightText), 'gim'); 142 var match; 143 while (match = re.exec(content)) { 144 if (match.index > i) 145 node.appendChild(document.createTextNode(content.slice(i, 146 match.index))); 147 i = re.lastIndex; 148 // Mark the highlighted text in bold. 149 var b = document.createElement('b'); 150 b.textContent = content.substring(match.index, i); 151 node.appendChild(b); 152 } 153 } 154 if (i < content.length) 155 node.appendChild(document.createTextNode(content.slice(i))); 156}; 157 158/** 159 * @return {DOMObject} DOM representation for the title block. 160 */ 161Page.prototype.getTitleDOM_ = function() { 162 var node = document.createElement('div'); 163 node.className = 'title'; 164 var link = document.createElement('a'); 165 link.href = this.url_; 166 167 link.style.backgroundImage = 168 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; 169 link.id = "id-" + this.id_; 170 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); 171 172 node.appendChild(link); 173 174 if (this.starred_) { 175 node.className += ' starred'; 176 node.appendChild(createElementWithClassName('div', 'starred')); 177 } 178 179 return node; 180}; 181 182// Page, private, static: ----------------------------------------------------- 183 184/** 185 * Quote a string so it can be used in a regular expression. 186 * @param {string} str The source string 187 * @return {string} The escaped string 188 */ 189Page.pregQuote_ = function(str) { 190 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); 191}; 192 193/////////////////////////////////////////////////////////////////////////////// 194// HistoryModel: 195/** 196 * Global container for history data. Future optimizations might include 197 * allowing the creation of a HistoryModel for each search string, allowing 198 * quick flips back and forth between results. 199 * 200 * The history model is based around pages, and only fetching the data to 201 * fill the currently requested page. This is somewhat dependent on the view, 202 * and so future work may wish to change history model to operate on 203 * timeframe (day or week) based containers. 204 */ 205function HistoryModel() { 206 this.clearModel_(); 207 this.setEditMode(false); 208 this.view_; 209} 210 211// HistoryModel, Public: ------------------------------------------------------ 212/** 213 * Sets our current view that is called when the history model changes. 214 * @param {HistoryView} view The view to set our current view to. 215 */ 216HistoryModel.prototype.setView = function(view) { 217 this.view_ = view; 218}; 219 220/** 221 * Start a new search - this will clear out our model. 222 * @param {String} searchText The text to search for 223 * @param {Number} opt_page The page to view - this is mostly used when setting 224 * up an initial view, use #requestPage otherwise. 225 */ 226HistoryModel.prototype.setSearchText = function(searchText, opt_page) { 227 this.clearModel_(); 228 this.searchText_ = searchText; 229 this.requestedPage_ = opt_page ? opt_page : 0; 230 this.getSearchResults_(); 231}; 232 233/** 234 * Reload our model with the current parameters. 235 */ 236HistoryModel.prototype.reload = function() { 237 var search = this.searchText_; 238 var page = this.requestedPage_; 239 this.clearModel_(); 240 this.searchText_ = search; 241 this.requestedPage_ = page; 242 this.getSearchResults_(); 243}; 244 245/** 246 * @return {String} The current search text. 247 */ 248HistoryModel.prototype.getSearchText = function() { 249 return this.searchText_; 250}; 251 252/** 253 * Tell the model that the view will want to see the current page. When 254 * the data becomes available, the model will call the view back. 255 * @page {Number} page The page we want to view. 256 */ 257HistoryModel.prototype.requestPage = function(page) { 258 this.requestedPage_ = page; 259 this.changed = true; 260 this.updateSearch_(false); 261}; 262 263/** 264 * Receiver for history query. 265 * @param {String} term The search term that the results are for. 266 * @param {Array} results A list of results 267 */ 268HistoryModel.prototype.addResults = function(info, results) { 269 this.inFlight_ = false; 270 if (info.term != this.searchText_) { 271 // If our results aren't for our current search term, they're rubbish. 272 return; 273 } 274 275 // Currently we assume we're getting things in date order. This needs to 276 // be updated if that ever changes. 277 if (results) { 278 var lastURL, lastDay; 279 var oldLength = this.pages_.length; 280 if (oldLength) { 281 var oldPage = this.pages_[oldLength - 1]; 282 lastURL = oldPage.url; 283 lastDay = oldPage.dateRelativeDay; 284 } 285 286 for (var i = 0, thisResult; thisResult = results[i]; i++) { 287 var thisURL = thisResult.url; 288 var thisDay = thisResult.dateRelativeDay; 289 290 // Remove adjacent duplicates. 291 if (!lastURL || lastURL != thisURL) { 292 // Figure out if this page is in the same day as the previous page, 293 // this is used to determine how day headers should be drawn. 294 this.pages_.push(new Page(thisResult, thisDay == lastDay, this, 295 this.last_id_++)); 296 lastDay = thisDay; 297 lastURL = thisURL; 298 } 299 } 300 if (results.length) 301 this.changed = true; 302 } 303 304 this.updateSearch_(info.finished); 305}; 306 307/** 308 * @return {Number} The number of pages in the model. 309 */ 310HistoryModel.prototype.getSize = function() { 311 return this.pages_.length; 312}; 313 314/** 315 * @return {boolean} Whether our history query has covered all of 316 * the user's history 317 */ 318HistoryModel.prototype.isComplete = function() { 319 return this.complete_; 320}; 321 322/** 323 * Get a list of pages between specified index positions. 324 * @param {Number} start The start index 325 * @param {Number} end The end index 326 * @return {Array} A list of pages 327 */ 328HistoryModel.prototype.getNumberedRange = function(start, end) { 329 if (start >= this.getSize()) 330 return []; 331 332 var end = end > this.getSize() ? this.getSize() : end; 333 return this.pages_.slice(start, end); 334}; 335 336/** 337 * @return {boolean} Whether we are in edit mode where history items can be 338 * deleted 339 */ 340HistoryModel.prototype.getEditMode = function() { 341 return this.editMode_; 342}; 343 344/** 345 * @param {boolean} edit_mode Control whether we are in edit mode. 346 */ 347HistoryModel.prototype.setEditMode = function(edit_mode) { 348 this.editMode_ = edit_mode; 349}; 350 351// HistoryModel, Private: ----------------------------------------------------- 352HistoryModel.prototype.clearModel_ = function() { 353 this.inFlight_ = false; // Whether a query is inflight. 354 this.searchText_ = ''; 355 this.searchDepth_ = 0; 356 this.pages_ = []; // Date-sorted list of pages. 357 this.last_id_ = 0; 358 selectionAnchor = -1; 359 id2checkbox = []; 360 361 // The page that the view wants to see - we only fetch slightly past this 362 // point. If the view requests a page that we don't have data for, we try 363 // to fetch it and call back when we're done. 364 this.requestedPage_ = 0; 365 366 this.complete_ = false; 367 368 if (this.view_) { 369 this.view_.clear_(); 370 } 371}; 372 373/** 374 * Figure out if we need to do more searches to fill the currently requested 375 * page. If we think we can fill the page, call the view and let it know 376 * we're ready to show something. 377 */ 378HistoryModel.prototype.updateSearch_ = function(finished) { 379 if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || 380 finished) { 381 // We have maxed out. There will be no more data. 382 this.complete_ = true; 383 this.view_.onModelReady(); 384 this.changed = false; 385 } else { 386 // If we can't fill the requested page, ask for more data unless a request 387 // is still in-flight. 388 if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) { 389 this.getSearchResults_(this.searchDepth_ + 1); 390 } 391 392 // If we have any data for the requested page, show it. 393 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { 394 this.view_.onModelReady(); 395 this.changed = false; 396 } 397 } 398}; 399 400/** 401 * Get search results for a selected depth. Our history system is optimized 402 * for queries that don't cross month boundaries, but an entire month's 403 * worth of data is huge. When we're in browse mode (searchText is empty) 404 * we request the data a day at a time. When we're searching, a month is 405 * used. 406 * 407 * TODO: Fix this for when the user's clock goes across month boundaries. 408 * @param {number} opt_day How many days back to do the search. 409 */ 410HistoryModel.prototype.getSearchResults_ = function(depth) { 411 this.searchDepth_ = depth || 0; 412 413 if (this.searchText_ == "") { 414 chrome.send('getHistory', 415 [String(this.searchDepth_)]); 416 } else { 417 chrome.send('searchHistory', 418 [this.searchText_, String(this.searchDepth_)]); 419 } 420 421 this.inFlight_ = true; 422}; 423 424/** 425 * Check to see if we have data for a given page. 426 * @param {number} page The page number 427 * @return {boolean} Whether we have any data for the given page. 428 */ 429HistoryModel.prototype.haveDataForPage_ = function(page) { 430 return (page * RESULTS_PER_PAGE < this.getSize()); 431}; 432 433/** 434 * Check to see if we have data to fill a page. 435 * @param {number} page The page number. 436 * @return {boolean} Whether we have data to fill the page. 437 */ 438HistoryModel.prototype.canFillPage_ = function(page) { 439 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); 440}; 441 442/////////////////////////////////////////////////////////////////////////////// 443// HistoryView: 444/** 445 * Functions and state for populating the page with HTML. This should one-day 446 * contain the view and use event handlers, rather than pushing HTML out and 447 * getting called externally. 448 * @param {HistoryModel} model The model backing this view. 449 */ 450function HistoryView(model) { 451 this.summaryTd_ = $('results-summary'); 452 this.summaryTd_.textContent = localStrings.getString('loading'); 453 this.editButtonTd_ = $('edit-button'); 454 this.editingControlsDiv_ = $('editing-controls'); 455 this.resultDiv_ = $('results-display'); 456 this.pageDiv_ = $('results-pagination'); 457 this.model_ = model 458 this.pageIndex_ = 0; 459 this.lastDisplayed_ = []; 460 461 this.model_.setView(this); 462 463 this.currentPages_ = []; 464 465 var self = this; 466 window.onresize = function() { 467 self.updateEntryAnchorWidth_(); 468 }; 469 self.updateEditControls_(); 470 471 this.boundUpdateRemoveButton_ = function(e) { 472 return self.updateRemoveButton_(e); 473 }; 474} 475 476// HistoryView, public: ------------------------------------------------------- 477/** 478 * Do a search and optionally view a certain page. 479 * @param {string} term The string to search for. 480 * @param {number} opt_page The page we wish to view, only use this for 481 * setting up initial views, as this triggers a search. 482 */ 483HistoryView.prototype.setSearch = function(term, opt_page) { 484 this.pageIndex_ = parseInt(opt_page || 0, 10); 485 window.scrollTo(0, 0); 486 this.model_.setSearchText(term, this.pageIndex_); 487 if (term) { 488 this.setEditMode(false); 489 } 490 this.updateEditControls_(); 491 pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_); 492}; 493 494/** 495 * Controls edit mode where history can be deleted. 496 * @param {boolean} edit_mode Whether to enable edit mode. 497 */ 498HistoryView.prototype.setEditMode = function(edit_mode) { 499 this.model_.setEditMode(edit_mode); 500 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), 501 this.pageIndex_); 502}; 503 504/** 505 * Toggles the edit mode and triggers UI update. 506 */ 507HistoryView.prototype.toggleEditMode = function() { 508 var editMode = !this.model_.getEditMode(); 509 this.setEditMode(editMode); 510 this.updateEditControls_(); 511}; 512 513/** 514 * Reload the current view. 515 */ 516HistoryView.prototype.reload = function() { 517 this.model_.reload(); 518}; 519 520/** 521 * Switch to a specified page. 522 * @param {number} page The page we wish to view. 523 */ 524HistoryView.prototype.setPage = function(page) { 525 this.clear_(); 526 this.pageIndex_ = parseInt(page, 10); 527 window.scrollTo(0, 0); 528 this.model_.requestPage(page); 529 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), 530 this.pageIndex_); 531}; 532 533/** 534 * @return {number} The page number being viewed. 535 */ 536HistoryView.prototype.getPage = function() { 537 return this.pageIndex_; 538}; 539 540/** 541 * Callback for the history model to let it know that it has data ready for us 542 * to view. 543 */ 544HistoryView.prototype.onModelReady = function() { 545 this.displayResults_(); 546}; 547 548// HistoryView, private: ------------------------------------------------------ 549/** 550 * Clear the results in the view. Since we add results piecemeal, we need 551 * to clear them out when we switch to a new page or reload. 552 */ 553HistoryView.prototype.clear_ = function() { 554 this.resultDiv_.textContent = ''; 555 556 var pages = this.currentPages_; 557 for (var i = 0; i < pages.length; i++) { 558 pages[i].isRendered = false; 559 } 560 this.currentPages_ = []; 561}; 562 563HistoryView.prototype.setPageRendered_ = function(page) { 564 page.isRendered = true; 565 this.currentPages_.push(page); 566}; 567 568/** 569 * Update the page with results. 570 */ 571HistoryView.prototype.displayResults_ = function() { 572 var results = this.model_.getNumberedRange( 573 this.pageIndex_ * RESULTS_PER_PAGE, 574 this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); 575 576 if (this.model_.getSearchText()) { 577 var resultTable = createElementWithClassName('table', 'results'); 578 resultTable.cellSpacing = 0; 579 resultTable.cellPadding = 0; 580 resultTable.border = 0; 581 582 for (var i = 0, page; page = results[i]; i++) { 583 if (!page.isRendered) { 584 resultTable.appendChild(page.getSearchResultDOM()); 585 this.setPageRendered_(page); 586 } 587 } 588 this.resultDiv_.appendChild(resultTable); 589 } else { 590 var lastTime = Math.infinity; 591 for (var i = 0, page; page = results[i]; i++) { 592 if (page.isRendered) { 593 continue; 594 } 595 // Break across day boundaries and insert gaps for browsing pauses. 596 var thisTime = page.time.getTime(); 597 598 if ((i == 0 && page.continued) || !page.continued) { 599 var day = createElementWithClassName('div', 'day'); 600 day.appendChild(document.createTextNode(page.dateRelativeDay)); 601 602 if (i == 0 && page.continued) { 603 day.appendChild(document.createTextNode(' ' + 604 localStrings.getString('cont'))); 605 } 606 607 this.resultDiv_.appendChild(day); 608 } else if (lastTime - thisTime > BROWSING_GAP_TIME) { 609 this.resultDiv_.appendChild(createElementWithClassName('div', 'gap')); 610 } 611 lastTime = thisTime; 612 613 // Add entry. 614 this.resultDiv_.appendChild(page.getBrowseResultDOM()); 615 this.setPageRendered_(page); 616 } 617 } 618 619 this.displaySummaryBar_(); 620 this.displayNavBar_(); 621 this.updateEntryAnchorWidth_(); 622}; 623 624/** 625 * Update the summary bar with descriptive text. 626 */ 627HistoryView.prototype.displaySummaryBar_ = function() { 628 var searchText = this.model_.getSearchText(); 629 if (searchText != '') { 630 this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', 631 searchText); 632 } else { 633 this.summaryTd_.textContent = localStrings.getString('history'); 634 } 635}; 636 637/** 638 * Update the widgets related to edit mode. 639 */ 640HistoryView.prototype.updateEditControls_ = function() { 641 // Display a button (looking like a link) to enable/disable edit mode. 642 var oldButton = this.editButtonTd_.firstChild; 643 if (this.model_.getSearchText()) { 644 this.editButtonTd_.replaceChild(document.createElement('p'), oldButton); 645 this.editingControlsDiv_.textContent = ''; 646 return; 647 } 648 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 */ 686HistoryView.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 */ 698HistoryView.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 */ 724HistoryView.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 */ 743HistoryView.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 */ 777function 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 803PageState.instance = null; 804 805/** 806 * @return {Object} An object containing parameters from our window hash. 807 */ 808PageState.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 */ 837PageState.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 */ 853PageState.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 */ 873function 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 */ 894function 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 */ 905function 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 */ 915function toggleEditMode() { 916 if (historyView) { 917 historyView.toggleEditMode(); 918 historyView.reload(); 919 } 920} 921 922/** 923 * Delete the next item in our deletion queue. 924 */ 925function 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 */ 936function openClearBrowsingData() { 937 chrome.send('clearBrowsingData', []); 938 return false; 939} 940 941/** 942 * Collect IDs from checked checkboxes and send to Chrome for deletion. 943 */ 944function 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 */ 995function 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 */ 1015function historyResult(info, results) { 1016 historyModel.addResults(info, results); 1017} 1018 1019/** 1020 * Our history system calls this function when a deletion has finished. 1021 */ 1022function deleteComplete() { 1023 window.console.log('Delete complete'); 1024 deleteInFlight = false; 1025 if (deleteQueue.length > 1) { 1026 deleteQueue = deleteQueue.slice(2); 1027 deleteNextInQueue(); 1028 } else { 1029 deleteQueue = []; 1030 } 1031} 1032 1033/** 1034 * Our history system calls this function if a delete is not ready (e.g. 1035 * another delete is in-progress). 1036 */ 1037function deleteFailed() { 1038 window.console.log('Delete failed'); 1039 // The deletion failed - try again later. 1040 deleteInFlight = false; 1041 setTimeout(deleteNextInQueue, 500); 1042} 1043 1044/** 1045 * We're called when something is deleted (either by us or by someone 1046 * else). 1047 */ 1048function historyDeleted() { 1049 window.console.log('History deleted'); 1050 historyView.reload(); 1051} 1052</script> 1053<link rel="stylesheet" href="webui.css"> 1054<style> 1055#results-separator { 1056 margin-top:12px; 1057 border-top:1px solid #9cc2ef; 1058 background-color:#ebeff9; 1059 font-weight:bold; 1060 padding:3px; 1061 margin-bottom:-8px; 1062} 1063#results-separator table { 1064 width: 100%; 1065} 1066#results-summary { 1067 overflow: hidden; 1068 white-space: nowrap; 1069 text-overflow: ellipsis; 1070 width: 50%; 1071} 1072#edit-button { 1073 text-align: end; 1074 overflow: hidden; 1075 white-space: nowrap; 1076 text-overflow: ellipsis; 1077 width: 50%; 1078} 1079#editing-controls button { 1080 margin-top: 18px; 1081 margin-bottom: -8px; 1082} 1083#results-display { 1084 max-width:740px; 1085} 1086.day { 1087 margin-top:18px; 1088 padding:0px 3px; 1089 display:inline-block; 1090} 1091.edit-button { 1092 display: inline; 1093 -webkit-appearance: none; 1094 background: none; 1095 border: 0; 1096 color: blue; /* -webkit-link makes it purple :'( */ 1097 cursor: pointer; 1098 text-decoration: underline; 1099 padding:0px 9px; 1100 display:inline-block; 1101 font:inherit; 1102} 1103.gap { 1104 margin-left:18px; 1105 width:15px; 1106 border-right:1px solid #ddd; 1107 height:14px; 1108} 1109.entry { 1110 margin-left:18px; 1111 margin-top:6px; 1112 overflow:auto; 1113} 1114table.results { 1115 margin-left:4px; 1116} 1117.entry .time { 1118 color:#888; 1119 float:left; 1120 min-width:56px; 1121 -webkit-margin-end:5px; 1122 padding-top:1px; 1123 white-space:nowrap; 1124} 1125html[dir='rtl'] .time { 1126 float:right; 1127} 1128.entry .title { 1129 max-width:600px; 1130 overflow: hidden; 1131 white-space: nowrap; 1132 text-overflow: ellipsis; 1133} 1134.results .time, .results .title { 1135 margin-top:18px; 1136} 1137.title > .starred { 1138 background:url('shared/images/star_small.png'); 1139 background-repeat:no-repeat; 1140 display:inline-block; 1141 -webkit-margin-start:12px; 1142 -webkit-margin-end:0; 1143 width:11px; 1144 height:11px; 1145} 1146.entry .title > a { 1147 box-sizing: border-box; 1148 background-repeat:no-repeat; 1149 background-size:16px; 1150 background-position:0px 1px; 1151 padding:1px 0px 4px 22px; 1152 display:inline-block; 1153 overflow:hidden; 1154 text-overflow:ellipsis; 1155} 1156html[dir='rtl'] .entry .title > a { 1157 background-position-x:right; 1158 padding-left:0px; 1159 padding-right:22px; 1160} 1161#results-pagination { 1162 padding-top:24px; 1163 -webkit-margin-start:18px; 1164} 1165 1166</style> 1167</head> 1168<body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> 1169<div class="header"> 1170 <a href="" onclick="setSearch(''); return false;"> 1171 <img src="shared/images/history_section.png" 1172 width="67" height="67" class="logo" border="0"></a> 1173 <form method="post" action="" 1174 onsubmit="setSearch(this.term.value); return false;" 1175 class="form"> 1176 <input type="text" name="term" id="term"> 1177 <input type="submit" name="submit" i18n-values="value:searchbutton"> 1178 </form> 1179</div> 1180<div class="main"> 1181 <div id="results-separator"> 1182 <table border="0" cellPadding="0" cellSpacing="0"> 1183 <tr><td id="results-summary"></td><td id="edit-button"><p></p></td></tr> 1184 </table> 1185 </div> 1186 <div id="editing-controls"></div> 1187 <div id="results-display"></div> 1188 <div id="results-pagination"></div> 1189</div> 1190<div class="footer"> 1191</div> 1192</body> 1193</html> 1194