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