| <!DOCTYPE HTML> |
| <html i18n-values="dir:textdirection;"> |
| <head> |
| <meta charset="utf-8"> |
| <title i18n-content="title"></title> |
| <link rel="icon" href="../../app/theme/history_favicon.png"> |
| <script src="shared/js/local_strings.js"></script> |
| <script> |
| /////////////////////////////////////////////////////////////////////////////// |
| // Globals: |
| var RESULTS_PER_PAGE = 150; |
| var MAX_SEARCH_DEPTH_MONTHS = 18; |
| |
| // Amount of time between pageviews that we consider a 'break' in browsing, |
| // measured in milliseconds. |
| var BROWSING_GAP_TIME = 15 * 60 * 1000; |
| |
| function $(o) {return document.getElementById(o);} |
| |
| function createElementWithClassName(type, className) { |
| var elm = document.createElement(type); |
| elm.className = className; |
| return elm; |
| } |
| |
| // Escapes a URI as appropriate for CSS. |
| function encodeURIForCSS(uri) { |
| // CSS uris need to have '(' and ')' escaped. |
| return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)"); |
| } |
| |
| // TODO(glen): Get rid of these global references, replace with a controller |
| // or just make the classes own more of the page. |
| var historyModel; |
| var historyView; |
| var localStrings; |
| var pageState; |
| var deleteQueue = []; |
| var deleteInFlight = false; |
| var selectionAnchor = -1; |
| var idToCheckbox = []; |
| |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Page: |
| /** |
| * Class to hold all the information about an entry in our model. |
| * @param {Object} result An object containing the page's data. |
| * @param {boolean} continued Whether this page is on the same day as the |
| * page before it |
| */ |
| function Page(result, continued, model, id) { |
| this.model_ = model; |
| this.title_ = result.title; |
| this.url_ = result.url; |
| this.domain_ = this.getDomainFromURL_(this.url_); |
| this.starred_ = result.starred; |
| this.snippet_ = result.snippet || ""; |
| this.id_ = id; |
| |
| this.changed = false; |
| |
| this.isRendered = false; |
| |
| // All the date information is public so that owners can compare properties of |
| // two items easily. |
| |
| // We get the time in seconds, but we want it in milliseconds. |
| this.time = new Date(result.time * 1000); |
| |
| // See comment in BrowsingHistoryHandler::QueryComplete - we won't always |
| // get all of these. |
| this.dateRelativeDay = result.dateRelativeDay || ""; |
| this.dateTimeOfDay = result.dateTimeOfDay || ""; |
| this.dateShort = result.dateShort || ""; |
| |
| // Whether this is the continuation of a previous day. |
| this.continued = continued; |
| } |
| |
| // Page, Public: -------------------------------------------------------------- |
| /** |
| * Returns a dom structure for a browse page result or a search page result. |
| * @param {boolean} Flag to indicate if result is a search result. |
| * @return {Element} The dom structure. |
| */ |
| Page.prototype.getResultDOM = function(searchResultFlag) { |
| var node = createElementWithClassName('li', 'entry'); |
| var time = createElementWithClassName('div', 'time'); |
| var domain = createElementWithClassName('span', 'domain'); |
| domain.style.backgroundImage = |
| 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; |
| domain.textContent = this.domain_; |
| node.appendChild(time); |
| node.appendChild(domain); |
| node.appendChild(this.getTitleDOM_()); |
| if (searchResultFlag) { |
| time.textContent = this.dateShort; |
| var snippet = createElementWithClassName('div', 'snippet'); |
| this.addHighlightedText_(snippet, |
| this.snippet_, |
| this.model_.getSearchText()); |
| node.appendChild(snippet); |
| } else { |
| if (this.model_.getEditMode()) { |
| var checkbox = document.createElement('input'); |
| checkbox.type = 'checkbox'; |
| checkbox.name = this.id_; |
| checkbox.time = this.time.toString(); |
| checkbox.addEventListener("click", checkboxClicked); |
| idToCheckbox[this.id_] = checkbox; |
| time.appendChild(checkbox); |
| } |
| time.appendChild(document.createTextNode(this.dateTimeOfDay)); |
| } |
| return node; |
| }; |
| |
| // Page, private: ------------------------------------------------------------- |
| /** |
| * Extracts and returns the domain (and subdomains) from a URL. |
| * @param {string} The url |
| * @return (string) The domain. An empty string is returned if no domain can |
| * be found. |
| */ |
| Page.prototype.getDomainFromURL_ = function(url) { |
| var domain = url.replace(/^.+:\/\//, '').match(/[^/]+/); |
| return domain ? domain[0] : ''; |
| }; |
| |
| /** |
| * Truncates a string to a maximum lenth (including ... if truncated) |
| * @param {string} The string to be truncated |
| * @param {number} The length to truncate the string to |
| * @return (string) The truncated string |
| */ |
| Page.prototype.truncateString_ = function(str, maxLength) { |
| if (str.length > maxLength) { |
| return str.substr(0, maxLength - 3) + '...'; |
| } else { |
| return str; |
| } |
| }; |
| |
| /** |
| * Add child text nodes to a node such that occurrences of the spcified text is |
| * highligted. |
| * @param {Node} node The node under which new text nodes will be made as |
| * children. |
| * @param {string} content Text to be added beneath |node| as one or more |
| * text nodes. |
| * @param {string} highlightText Occurences of this text inside |content| will |
| * be highlighted. |
| */ |
| Page.prototype.addHighlightedText_ = function(node, content, highlightText) { |
| var i = 0; |
| if (highlightText) { |
| var re = new RegExp(Page.pregQuote_(highlightText), 'gim'); |
| var match; |
| while (match = re.exec(content)) { |
| if (match.index > i) |
| node.appendChild(document.createTextNode(content.slice(i, |
| match.index))); |
| i = re.lastIndex; |
| // Mark the highlighted text in bold. |
| var b = document.createElement('b'); |
| b.textContent = content.substring(match.index, i); |
| node.appendChild(b); |
| } |
| } |
| if (i < content.length) |
| node.appendChild(document.createTextNode(content.slice(i))); |
| }; |
| |
| /** |
| * @return {DOMObject} DOM representation for the title block. |
| */ |
| Page.prototype.getTitleDOM_ = function() { |
| var node = document.createElement('span'); |
| node.className = 'title'; |
| var link = document.createElement('a'); |
| link.href = this.url_; |
| link.id = "id-" + this.id_; |
| |
| var content = this.truncateString_(this.title_, 80); |
| |
| // If we have truncated the title, add a tooltip. |
| if (content.length != this.title_.length) { |
| link.title = this.title_; |
| } |
| this.addHighlightedText_(link, content, this.model_.getSearchText()); |
| node.appendChild(link); |
| |
| if (this.starred_) { |
| node.className += ' starred'; |
| node.appendChild(createElementWithClassName('div', 'starred')); |
| } |
| |
| return node; |
| }; |
| |
| // Page, private, static: ----------------------------------------------------- |
| |
| /** |
| * Quote a string so it can be used in a regular expression. |
| * @param {string} str The source string |
| * @return {string} The escaped string |
| */ |
| Page.pregQuote_ = function(str) { |
| return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // HistoryModel: |
| /** |
| * Global container for history data. Future optimizations might include |
| * allowing the creation of a HistoryModel for each search string, allowing |
| * quick flips back and forth between results. |
| * |
| * The history model is based around pages, and only fetching the data to |
| * fill the currently requested page. This is somewhat dependent on the view, |
| * and so future work may wish to change history model to operate on |
| * timeframe (day or week) based containers. |
| */ |
| function HistoryModel() { |
| this.clearModel_(); |
| this.setEditMode(false); |
| this.view_; |
| } |
| |
| // HistoryModel, Public: ------------------------------------------------------ |
| /** |
| * Sets our current view that is called when the history model changes. |
| * @param {HistoryView} view The view to set our current view to. |
| */ |
| HistoryModel.prototype.setView = function(view) { |
| this.view_ = view; |
| }; |
| |
| /** |
| * Start a new search - this will clear out our model. |
| * @param {String} searchText The text to search for |
| * @param {Number} opt_page The page to view - this is mostly used when setting |
| * up an initial view, use #requestPage otherwise. |
| */ |
| HistoryModel.prototype.setSearchText = function(searchText, opt_page) { |
| this.clearModel_(); |
| this.searchText_ = searchText; |
| this.requestedPage_ = opt_page ? opt_page : 0; |
| this.getSearchResults_(); |
| }; |
| |
| /** |
| * Reload our model with the current parameters. |
| */ |
| HistoryModel.prototype.reload = function() { |
| var search = this.searchText_; |
| var page = this.requestedPage_; |
| this.clearModel_(); |
| this.searchText_ = search; |
| this.requestedPage_ = page; |
| this.getSearchResults_(); |
| }; |
| |
| /** |
| * @return {String} The current search text. |
| */ |
| HistoryModel.prototype.getSearchText = function() { |
| return this.searchText_; |
| }; |
| |
| /** |
| * Tell the model that the view will want to see the current page. When |
| * the data becomes available, the model will call the view back. |
| * @page {Number} page The page we want to view. |
| */ |
| HistoryModel.prototype.requestPage = function(page) { |
| this.requestedPage_ = page; |
| this.changed = true; |
| this.updateSearch_(false); |
| }; |
| |
| /** |
| * Receiver for history query. |
| * @param {String} term The search term that the results are for. |
| * @param {Array} results A list of results |
| */ |
| HistoryModel.prototype.addResults = function(info, results) { |
| this.inFlight_ = false; |
| if (info.term != this.searchText_) { |
| // If our results aren't for our current search term, they're rubbish. |
| return; |
| } |
| |
| // Currently we assume we're getting things in date order. This needs to |
| // be updated if that ever changes. |
| if (results) { |
| var lastURL, lastDay; |
| var oldLength = this.pages_.length; |
| if (oldLength) { |
| var oldPage = this.pages_[oldLength - 1]; |
| lastURL = oldPage.url; |
| lastDay = oldPage.dateRelativeDay; |
| } |
| |
| for (var i = 0, thisResult; thisResult = results[i]; i++) { |
| var thisURL = thisResult.url; |
| var thisDay = thisResult.dateRelativeDay; |
| |
| // Remove adjacent duplicates. |
| if (!lastURL || lastURL != thisURL) { |
| // Figure out if this page is in the same day as the previous page, |
| // this is used to determine how day headers should be drawn. |
| this.pages_.push(new Page(thisResult, thisDay == lastDay, this, |
| this.last_id_++)); |
| lastDay = thisDay; |
| lastURL = thisURL; |
| } |
| } |
| if (results.length) |
| this.changed = true; |
| } |
| |
| this.updateSearch_(info.finished); |
| }; |
| |
| /** |
| * @return {Number} The number of pages in the model. |
| */ |
| HistoryModel.prototype.getSize = function() { |
| return this.pages_.length; |
| }; |
| |
| /** |
| * @return {boolean} Whether our history query has covered all of |
| * the user's history |
| */ |
| HistoryModel.prototype.isComplete = function() { |
| return this.complete_; |
| }; |
| |
| /** |
| * Get a list of pages between specified index positions. |
| * @param {Number} start The start index |
| * @param {Number} end The end index |
| * @return {Array} A list of pages |
| */ |
| HistoryModel.prototype.getNumberedRange = function(start, end) { |
| if (start >= this.getSize()) |
| return []; |
| |
| var end = end > this.getSize() ? this.getSize() : end; |
| return this.pages_.slice(start, end); |
| }; |
| |
| /** |
| * @return {boolean} Whether we are in edit mode where history items can be |
| * deleted |
| */ |
| HistoryModel.prototype.getEditMode = function() { |
| return this.editMode_; |
| }; |
| |
| /** |
| * @param {boolean} edit_mode Control whether we are in edit mode. |
| */ |
| HistoryModel.prototype.setEditMode = function(edit_mode) { |
| this.editMode_ = edit_mode; |
| }; |
| |
| // HistoryModel, Private: ----------------------------------------------------- |
| HistoryModel.prototype.clearModel_ = function() { |
| this.inFlight_ = false; // Whether a query is inflight. |
| this.searchText_ = ''; |
| this.searchDepth_ = 0; |
| this.pages_ = []; // Date-sorted list of pages. |
| this.last_id_ = 0; |
| selectionAnchor = -1; |
| idToCheckbox = []; |
| |
| // The page that the view wants to see - we only fetch slightly past this |
| // point. If the view requests a page that we don't have data for, we try |
| // to fetch it and call back when we're done. |
| this.requestedPage_ = 0; |
| |
| this.complete_ = false; |
| |
| if (this.view_) { |
| this.view_.clear_(); |
| } |
| }; |
| |
| /** |
| * Figure out if we need to do more searches to fill the currently requested |
| * page. If we think we can fill the page, call the view and let it know |
| * we're ready to show something. |
| */ |
| HistoryModel.prototype.updateSearch_ = function(finished) { |
| if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || |
| finished) { |
| // We have maxed out. There will be no more data. |
| this.complete_ = true; |
| this.view_.onModelReady(); |
| this.changed = false; |
| } else { |
| // If we can't fill the requested page, ask for more data unless a request |
| // is still in-flight. |
| if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) { |
| this.getSearchResults_(this.searchDepth_ + 1); |
| } |
| |
| // If we have any data for the requested page, show it. |
| if (this.changed && this.haveDataForPage_(this.requestedPage_)) { |
| this.view_.onModelReady(); |
| this.changed = false; |
| } |
| } |
| }; |
| |
| /** |
| * Get search results for a selected depth. Our history system is optimized |
| * for queries that don't cross month boundaries, but an entire month's |
| * worth of data is huge. When we're in browse mode (searchText is empty) |
| * we request the data a day at a time. When we're searching, a month is |
| * used. |
| * |
| * TODO: Fix this for when the user's clock goes across month boundaries. |
| * @param {number} opt_day How many days back to do the search. |
| */ |
| HistoryModel.prototype.getSearchResults_ = function(depth) { |
| this.searchDepth_ = depth || 0; |
| |
| if (this.searchText_ == "") { |
| chrome.send('getHistory', |
| [String(this.searchDepth_)]); |
| } else { |
| chrome.send('searchHistory', |
| [this.searchText_, String(this.searchDepth_)]); |
| } |
| |
| this.inFlight_ = true; |
| }; |
| |
| /** |
| * Check to see if we have data for a given page. |
| * @param {number} page The page number |
| * @return {boolean} Whether we have any data for the given page. |
| */ |
| HistoryModel.prototype.haveDataForPage_ = function(page) { |
| return (page * RESULTS_PER_PAGE < this.getSize()); |
| }; |
| |
| /** |
| * Check to see if we have data to fill a page. |
| * @param {number} page The page number. |
| * @return {boolean} Whether we have data to fill the page. |
| */ |
| HistoryModel.prototype.canFillPage_ = function(page) { |
| return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // HistoryView: |
| /** |
| * Functions and state for populating the page with HTML. This should one-day |
| * contain the view and use event handlers, rather than pushing HTML out and |
| * getting called externally. |
| * @param {HistoryModel} model The model backing this view. |
| */ |
| function HistoryView(model) { |
| this.summaryTd_ = $('results-summary'); |
| this.summaryTd_.textContent = localStrings.getString('loading'); |
| this.editButtonTd_ = $('edit-button'); |
| this.editingControlsDiv_ = $('editing-controls'); |
| this.resultDiv_ = $('results-display'); |
| this.pageDiv_ = $('results-pagination'); |
| this.model_ = model |
| this.pageIndex_ = 0; |
| this.lastDisplayed_ = []; |
| |
| this.model_.setView(this); |
| |
| this.currentPages_ = []; |
| |
| var self = this; |
| window.onresize = function() { |
| self.updateEntryAnchorWidth_(); |
| }; |
| self.updateEditControls_(); |
| |
| this.boundUpdateRemoveButton_ = function(e) { |
| return self.updateRemoveButton_(e); |
| }; |
| } |
| |
| // HistoryView, public: ------------------------------------------------------- |
| /** |
| * Do a search and optionally view a certain page. |
| * @param {string} term The string to search for. |
| * @param {number} opt_page The page we wish to view, only use this for |
| * setting up initial views, as this triggers a search. |
| */ |
| HistoryView.prototype.setSearch = function(term, opt_page) { |
| this.pageIndex_ = parseInt(opt_page || 0, 10); |
| window.scrollTo(0, 0); |
| this.model_.setSearchText(term, this.pageIndex_); |
| if (term) { |
| this.setEditMode(false); |
| } |
| this.updateEditControls_(); |
| pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_); |
| }; |
| |
| /** |
| * Controls edit mode where history can be deleted. |
| * @param {boolean} edit_mode Whether to enable edit mode. |
| */ |
| HistoryView.prototype.setEditMode = function(edit_mode) { |
| this.model_.setEditMode(edit_mode); |
| pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), |
| this.pageIndex_); |
| }; |
| |
| /** |
| * Toggles the edit mode and triggers UI update. |
| */ |
| HistoryView.prototype.toggleEditMode = function() { |
| var editMode = !this.model_.getEditMode(); |
| this.setEditMode(editMode); |
| this.updateEditControls_(); |
| }; |
| |
| /** |
| * Reload the current view. |
| */ |
| HistoryView.prototype.reload = function() { |
| this.model_.reload(); |
| }; |
| |
| /** |
| * Switch to a specified page. |
| * @param {number} page The page we wish to view. |
| */ |
| HistoryView.prototype.setPage = function(page) { |
| this.clear_(); |
| this.pageIndex_ = parseInt(page, 10); |
| window.scrollTo(0, 0); |
| this.model_.requestPage(page); |
| pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), |
| this.pageIndex_); |
| }; |
| |
| /** |
| * @return {number} The page number being viewed. |
| */ |
| HistoryView.prototype.getPage = function() { |
| return this.pageIndex_; |
| }; |
| |
| /** |
| * Callback for the history model to let it know that it has data ready for us |
| * to view. |
| */ |
| HistoryView.prototype.onModelReady = function() { |
| this.displayResults_(); |
| }; |
| |
| // HistoryView, private: ------------------------------------------------------ |
| /** |
| * Clear the results in the view. Since we add results piecemeal, we need |
| * to clear them out when we switch to a new page or reload. |
| */ |
| HistoryView.prototype.clear_ = function() { |
| this.resultDiv_.textContent = ''; |
| |
| var pages = this.currentPages_; |
| for (var i = 0; i < pages.length; i++) { |
| pages[i].isRendered = false; |
| } |
| this.currentPages_ = []; |
| }; |
| |
| HistoryView.prototype.setPageRendered_ = function(page) { |
| page.isRendered = true; |
| this.currentPages_.push(page); |
| }; |
| |
| /** |
| * Update the page with results. |
| */ |
| HistoryView.prototype.displayResults_ = function() { |
| var results = this.model_.getNumberedRange( |
| this.pageIndex_ * RESULTS_PER_PAGE, |
| this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); |
| |
| if (this.model_.getSearchText()) { |
| var searchResults = createElementWithClassName('ol', 'search-results'); |
| for (var i = 0, page; page = results[i]; i++) { |
| if (!page.isRendered) { |
| searchResults.appendChild(page.getResultDOM(true)); |
| this.setPageRendered_(page); |
| } |
| } |
| this.resultDiv_.appendChild(searchResults); |
| } else { |
| var resultsFragment = document.createDocumentFragment(); |
| var lastTime = Math.infinity; |
| var dayResults; |
| for (var i = 0, page; page = results[i]; i++) { |
| if (page.isRendered) { |
| continue; |
| } |
| // Break across day boundaries and insert gaps for browsing pauses. |
| // Create a dayResults element to contain results for each day |
| var thisTime = page.time.getTime(); |
| |
| if ((i == 0 && page.continued) || !page.continued) { |
| var day = createElementWithClassName('h2', 'day'); |
| day.appendChild(document.createTextNode(page.dateRelativeDay)); |
| if (i == 0 && page.continued) { |
| day.appendChild(document.createTextNode(' ' + |
| localStrings.getString('cont'))); |
| } |
| |
| // If there is an existing dayResults element, append it. |
| if (dayResults) { |
| resultsFragment.appendChild(dayResults); |
| } |
| resultsFragment.appendChild(day); |
| dayResults = createElementWithClassName('ol', 'day-results'); |
| } else if (lastTime - thisTime > BROWSING_GAP_TIME) { |
| if (dayResults) { |
| dayResults.appendChild(createElementWithClassName('li', 'gap')); |
| } |
| } |
| lastTime = thisTime; |
| // Add entry. |
| if (dayResults) { |
| dayResults.appendChild(page.getResultDOM(false)); |
| this.setPageRendered_(page); |
| } |
| } |
| // Add final dayResults element. |
| if (dayResults) { |
| resultsFragment.appendChild(dayResults); |
| } |
| this.resultDiv_.appendChild(resultsFragment); |
| } |
| |
| this.displaySummaryBar_(); |
| this.displayNavBar_(); |
| this.updateEntryAnchorWidth_(); |
| }; |
| |
| /** |
| * Update the summary bar with descriptive text. |
| */ |
| HistoryView.prototype.displaySummaryBar_ = function() { |
| var searchText = this.model_.getSearchText(); |
| if (searchText != '') { |
| this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', |
| searchText); |
| } else { |
| this.summaryTd_.textContent = localStrings.getString('history'); |
| } |
| }; |
| |
| /** |
| * Update the widgets related to edit mode. |
| */ |
| HistoryView.prototype.updateEditControls_ = function() { |
| // Display a button (looking like a link) to enable/disable edit mode. |
| var oldButton = this.editButtonTd_.firstChild; |
| if (this.model_.getSearchText()) { |
| this.editButtonTd_.replaceChild(document.createElement('p'), oldButton); |
| this.editingControlsDiv_.textContent = ''; |
| return; |
| } |
| |
| var editMode = this.model_.getEditMode(); |
| var button = createElementWithClassName('button', 'edit-button'); |
| button.onclick = toggleEditMode; |
| button.textContent = localStrings.getString(editMode ? |
| 'doneediting' : 'edithistory'); |
| this.editButtonTd_.replaceChild(button, oldButton); |
| |
| this.editingControlsDiv_.textContent = ''; |
| |
| if (editMode) { |
| // Button to delete the selected items. |
| button = document.createElement('button'); |
| button.onclick = removeItems; |
| button.textContent = localStrings.getString('removeselected'); |
| button.disabled = true; |
| this.editingControlsDiv_.appendChild(button); |
| this.removeButton_ = button; |
| |
| // Button that opens up the clear browsing data dialog. |
| button = document.createElement('button'); |
| button.onclick = openClearBrowsingData; |
| button.textContent = localStrings.getString('clearallhistory'); |
| this.editingControlsDiv_.appendChild(button); |
| |
| // Listen for clicks in the page to sync the disabled state. |
| document.addEventListener('click', this.boundUpdateRemoveButton_); |
| } else { |
| this.removeButton_ = null; |
| document.removeEventListener('click', this.boundUpdateRemoveButton_); |
| } |
| }; |
| |
| /** |
| * Updates the disabled state of the remove button when in editing mode. |
| * @param {!Event} e The click event object. |
| * @private |
| */ |
| HistoryView.prototype.updateRemoveButton_ = function(e) { |
| if (e.target.tagName != 'INPUT') |
| return; |
| |
| var anyChecked = document.querySelector('.entry input:checked') != null; |
| if (this.removeButton_) |
| this.removeButton_.disabled = !anyChecked; |
| }; |
| |
| /** |
| * Update the pagination tools. |
| */ |
| HistoryView.prototype.displayNavBar_ = function() { |
| this.pageDiv_.textContent = ''; |
| |
| if (this.pageIndex_ > 0) { |
| this.pageDiv_.appendChild( |
| this.createPageNav_(0, localStrings.getString('newest'))); |
| this.pageDiv_.appendChild( |
| this.createPageNav_(this.pageIndex_ - 1, |
| localStrings.getString('newer'))); |
| } |
| |
| // TODO(feldstein): this causes the navbar to not show up when your first |
| // page has the exact amount of results as RESULTS_PER_PAGE. |
| if (this.model_.getSize() > (this.pageIndex_ + 1) * RESULTS_PER_PAGE) { |
| this.pageDiv_.appendChild( |
| this.createPageNav_(this.pageIndex_ + 1, |
| localStrings.getString('older'))); |
| } |
| }; |
| |
| /** |
| * Make a DOM object representation of a page navigation link. |
| * @param {number} page The page index the navigation element should link to |
| * @param {string} name The text content of the link |
| * @return {HTMLAnchorElement} the pagination link |
| */ |
| HistoryView.prototype.createPageNav_ = function(page, name) { |
| anchor = document.createElement('a'); |
| anchor.className = 'page-navigation'; |
| anchor.textContent = name; |
| var hashString = PageState.getHashString(this.model_.getEditMode(), |
| this.model_.getSearchText(), page); |
| var link = 'chrome://history2/' + (hashString ? '#' + hashString : ''); |
| anchor.href = link; |
| anchor.onclick = function() { |
| setPage(page); |
| return false; |
| }; |
| return anchor; |
| }; |
| |
| /** |
| * Updates the CSS rule for the entry anchor. |
| * @private |
| */ |
| HistoryView.prototype.updateEntryAnchorWidth_ = function() { |
| // We need to have at least on .title div to be able to calculate the |
| // desired width of the anchor. |
| var titleElement = document.querySelector('.entry .title'); |
| if (!titleElement) |
| return; |
| |
| // Create new CSS rules and add them last to the last stylesheet. |
| // TODO(jochen): The following code does not work due to WebKit bug #32309 |
| // if (!this.entryAnchorRule_) { |
| // var styleSheets = document.styleSheets; |
| // var styleSheet = styleSheets[styleSheets.length - 1]; |
| // var rules = styleSheet.cssRules; |
| // var createRule = function(selector) { |
| // styleSheet.insertRule(selector + '{}', rules.length); |
| // return rules[rules.length - 1]; |
| // }; |
| // this.entryAnchorRule_ = createRule('.entry .title > a'); |
| // // The following rule needs to be more specific to have higher priority. |
| // this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a'); |
| // } |
| // |
| // var anchorMaxWith = titleElement.offsetWidth; |
| // this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px'; |
| // // Adjust by the width of star plus its margin. |
| // this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px'; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // State object: |
| /** |
| * An 'AJAX-history' implementation. |
| * @param {HistoryModel} model The model we're representing |
| * @param {HistoryView} view The view we're representing |
| */ |
| function PageState(model, view) { |
| // Enforce a singleton. |
| if (PageState.instance) { |
| return PageState.instance; |
| } |
| |
| this.model = model; |
| this.view = view; |
| |
| if (typeof this.checker_ != 'undefined' && this.checker_) { |
| clearInterval(this.checker_); |
| } |
| |
| // TODO(glen): Replace this with a bound method so we don't need |
| // public model and view. |
| this.checker_ = setInterval((function(state_obj) { |
| var hashData = state_obj.getHashData(); |
| |
| if (hashData.q != state_obj.model.getSearchText(term)) { |
| state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); |
| } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { |
| state_obj.view.setPage(hashData.p); |
| } |
| }), 50, this); |
| } |
| |
| PageState.instance = null; |
| |
| /** |
| * @return {Object} An object containing parameters from our window hash. |
| */ |
| PageState.prototype.getHashData = function() { |
| var result = { |
| e : 0, |
| q : '', |
| p : 0 |
| }; |
| |
| if (!window.location.hash) { |
| return result; |
| } |
| |
| var hashSplit = window.location.hash.substr(1).split('&'); |
| for (var i = 0; i < hashSplit.length; i++) { |
| var pair = hashSplit[i].split('='); |
| if (pair.length > 1) { |
| result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); |
| } |
| } |
| |
| return result; |
| }; |
| |
| /** |
| * Set the hash to a specified state, this will create an entry in the |
| * session history so the back button cycles through hash states, which |
| * are then picked up by our listener. |
| * @param {string} term The current search string. |
| * @param {string} page The page currently being viewed. |
| */ |
| PageState.prototype.setUIState = function(editMode, term, page) { |
| // Make sure the form looks pretty. |
| document.forms[0].term.value = term; |
| var currentHash = this.getHashData(); |
| if (Boolean(currentHash.e) != editMode || currentHash.q != term || |
| currentHash.p != page) { |
| window.location.hash = PageState.getHashString(editMode, term, page); |
| } |
| }; |
| |
| /** |
| * Static method to get the hash string for a specified state |
| * @param {string} term The current search string. |
| * @param {string} page The page currently being viewed. |
| * @return {string} The string to be used in a hash. |
| */ |
| PageState.getHashString = function(editMode, term, page) { |
| var newHash = []; |
| if (editMode) { |
| newHash.push('e=1'); |
| } |
| if (term) { |
| newHash.push('q=' + encodeURIComponent(term)); |
| } |
| if (page != undefined) { |
| newHash.push('p=' + page); |
| } |
| |
| return newHash.join('&'); |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Document Functions: |
| /** |
| * Window onload handler, sets up the page. |
| */ |
| function load() { |
| $('term').focus(); |
| |
| localStrings = new LocalStrings(); |
| historyModel = new HistoryModel(); |
| historyView = new HistoryView(historyModel); |
| pageState = new PageState(historyModel, historyView); |
| |
| // Create default view. |
| var hashData = pageState.getHashData(); |
| if (Boolean(hashData.e)) { |
| historyView.toggleEditMode(); |
| } |
| historyView.setSearch(hashData.q, hashData.p); |
| } |
| |
| /** |
| * TODO(glen): Get rid of this function. |
| * Set the history view to a specified page. |
| * @param {String} term The string to search for |
| */ |
| function setSearch(term) { |
| if (historyView) { |
| historyView.setSearch(term); |
| } |
| } |
| |
| /** |
| * TODO(glen): Get rid of this function. |
| * Set the history view to a specified page. |
| * @param {number} page The page to set the view to. |
| */ |
| function setPage(page) { |
| if (historyView) { |
| historyView.setPage(page); |
| } |
| } |
| |
| /** |
| * TODO(glen): Get rid of this function. |
| * Toggles edit mode. |
| */ |
| function toggleEditMode() { |
| if (historyView) { |
| historyView.toggleEditMode(); |
| historyView.reload(); |
| } |
| } |
| |
| /** |
| * Delete the next item in our deletion queue. |
| */ |
| function deleteNextInQueue() { |
| if (!deleteInFlight && deleteQueue.length) { |
| deleteInFlight = true; |
| chrome.send('removeURLsOnOneDay', |
| [String(deleteQueue[0])].concat(deleteQueue[1])); |
| } |
| } |
| |
| /** |
| * Open the clear browsing data dialog. |
| */ |
| function openClearBrowsingData() { |
| chrome.send('clearBrowsingData', []); |
| return false; |
| } |
| |
| /** |
| * Collect IDs from checked checkboxes and send to Chrome for deletion. |
| */ |
| function removeItems() { |
| var checkboxes = document.getElementsByTagName('input'); |
| var ids = []; |
| var disabledItems = []; |
| var queue = []; |
| var date = new Date(); |
| for (var i = 0; i < checkboxes.length; i++) { |
| if (checkboxes[i].type == 'checkbox' && checkboxes[i].checked && |
| !checkboxes[i].disabled) { |
| var cbDate = new Date(checkboxes[i].time); |
| if (date.getFullYear() != cbDate.getFullYear() || |
| date.getMonth() != cbDate.getMonth() || |
| date.getDate() != cbDate.getDate()) { |
| if (ids.length > 0) { |
| queue.push(date.valueOf() / 1000); |
| queue.push(ids); |
| } |
| ids = []; |
| date = cbDate; |
| } |
| var link = $('id-' + checkboxes[i].name); |
| checkboxes[i].disabled = true; |
| link.style.textDecoration = 'line-through'; |
| disabledItems.push(checkboxes[i]); |
| ids.push(link.href); |
| } |
| } |
| if (ids.length > 0) { |
| queue.push(date.valueOf() / 1000); |
| queue.push(ids); |
| } |
| if (queue.length > 0) { |
| if (confirm(localStrings.getString('deletewarning'))) { |
| deleteQueue = deleteQueue.concat(queue); |
| deleteNextInQueue(); |
| } else { |
| // If the remove is cancelled, return the checkboxes to their |
| // enabled, non-line-through state. |
| for (var i = 0; i < disabledItems.length; i++) { |
| var link = $('id-' + disabledItems[i].name); |
| disabledItems[i].disabled = false; |
| link.style.textDecoration = ''; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Toggle state of checkbox and handle Shift modifier. |
| */ |
| function checkboxClicked(event) { |
| if (event.shiftKey && (selectionAnchor != -1)) { |
| var checked = this.checked; |
| // Set all checkboxes from the anchor up to the clicked checkbox to the |
| // state of the clicked one. |
| var begin = Math.min(this.name, selectionAnchor); |
| var end = Math.max(this.name, selectionAnchor); |
| for (var i = begin; i <= end; i++) { |
| idToCheckbox[i].checked = checked; |
| } |
| } |
| selectionAnchor = this.name; |
| this.focus(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Chrome callbacks: |
| /** |
| * Our history system calls this function with results from searches. |
| */ |
| function historyResult(info, results) { |
| historyModel.addResults(info, results); |
| } |
| |
| /** |
| * Our history system calls this function when a deletion has finished. |
| */ |
| function deleteComplete() { |
| window.console.log('Delete complete'); |
| deleteInFlight = false; |
| if (deleteQueue.length > 2) { |
| deleteQueue = deleteQueue.slice(2); |
| deleteNextInQueue(); |
| } else { |
| deleteQueue = []; |
| historyView.reload(); |
| } |
| } |
| |
| /** |
| * Our history system calls this function if a delete is not ready (e.g. |
| * another delete is in-progress). |
| */ |
| function deleteFailed() { |
| window.console.log('Delete failed'); |
| // The deletion failed - try again later. |
| deleteInFlight = false; |
| setTimeout(deleteNextInQueue, 500); |
| } |
| </script> |
| <link rel="stylesheet" href="webui2.css"> |
| <style> |
| #results-separator { |
| margin-top:12px; |
| border-top:1px solid #9cc2ef; |
| background-color:#ebeff9; |
| font-weight:bold; |
| padding:3px; |
| margin-bottom:-8px; |
| } |
| #results-separator table { |
| width: 100%; |
| } |
| #results-summary { |
| overflow: hidden; |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| width: 50%; |
| } |
| #edit-button { |
| text-align: right; |
| overflow: hidden; |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| width: 50%; |
| } |
| #editing-controls button { |
| margin-top: 18px; |
| margin-bottom: -8px; |
| } |
| #results-display { |
| max-width:740px; |
| overflow: hidden; |
| margin: 16px 4px 0 4px; |
| } |
| .day { |
| color: #6a6a6a; |
| font-weight: bold; |
| margin: 0 0 4px 0; |
| text-transform: uppercase; |
| font-size: 13px; |
| } |
| .edit-button { |
| display: inline; |
| -webkit-appearance: none; |
| background: none; |
| border: 0; |
| color: blue; /* -webkit-link makes it purple :'( */ |
| cursor: pointer; |
| text-decoration: underline; |
| padding:0px 9px; |
| display:inline-block; |
| font:inherit; |
| } |
| .gap { |
| padding: 0; |
| margin: 0; |
| list-style: none; |
| width: 15px; |
| -webkit-border-end: 1px solid #ddd; |
| height: 14px; |
| } |
| .entry { |
| margin: 0; |
| -webkit-margin-start: 90px; |
| list-style: none; |
| padding: 0; |
| position: relative; |
| line-height: 1.6em; |
| } |
| .search-results, .day-results { |
| margin: 0 0 24px 0; |
| padding: 0; |
| } |
| .snippet { |
| font-size: 11px; |
| line-height: 1.6em; |
| margin-bottom: 12px; |
| } |
| .entry .domain { |
| color: #282; |
| -webkit-padding-start: 20px; |
| -webkit-padding-end: 8px; |
| background-repeat: no-repeat; |
| background-position-y: center; |
| display: inline-block; /* Fixes RTL wrapping issue */ |
| } |
| html[dir='rtl'] .entry .domain { |
| background-position-x: right; |
| } |
| .entry .time { |
| color:#9a9a9a; |
| left: -90px; |
| width: 90px; |
| position: absolute; |
| top: 0; |
| white-space:nowrap; |
| } |
| html[dir='rtl'] .time { |
| left: auto; |
| right: -90px; |
| } |
| .title > .starred { |
| background:url('shared/images/star_small.png'); |
| background-repeat:no-repeat; |
| display:inline-block; |
| -webkit-margin-start: 4px; |
| width:11px; |
| height:11px; |
| } |
| /* Fixes RTL wrapping */ |
| html[dir='rtl'] .title { |
| display: inline-block; |
| } |
| .entry .title > a { |
| color: #11c; |
| text-decoration: none; |
| } |
| .entry .title > a:hover { |
| text-decoration: underline; |
| } |
| /* Since all history links are visited, we can make them blue. */ |
| .entry .title > a:visted { |
| color: #11c; |
| } |
| |
| </style> |
| </head> |
| <body onload="load();" i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize"> |
| <div class="header"> |
| <a href="" onclick="setSearch(''); return false;"> |
| <img src="shared/images/history_section.png" |
| width="67" height="67" class="logo" border="0"></a> |
| <form method="post" action="" |
| onsubmit="setSearch(this.term.value); return false;" |
| class="form"> |
| <input type="text" name="term" id="term"> |
| <input type="submit" name="submit" i18n-values="value:searchbutton"> |
| </form> |
| </div> |
| <div class="main"> |
| <div id="results-separator"> |
| <table border="0" cellPadding="0" cellSpacing="0"> |
| <tr><td id="results-summary"></td><td id="edit-button"><p></p></td></tr> |
| </table> |
| </div> |
| <div id="editing-controls"></div> |
| <div id="results-display"></div> |
| <div id="results-pagination"></div> |
| </div> |
| <div class="footer"> |
| </div> |
| </body> |
| </html> |