diff options
author | Ben Murdoch <benm@google.com> | 2010-07-29 17:14:53 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-08-04 14:29:45 +0100 |
commit | c407dc5cd9bdc5668497f21b26b09d988ab439de (patch) | |
tree | 7eaf8707c0309516bdb042ad976feedaf72b0bb1 /chrome/browser/resources/history2.html | |
parent | 0998b1cdac5733f299c12d88bc31ef9c8035b8fa (diff) | |
download | external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.zip external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.gz external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.bz2 |
Merge Chromium src@r53293
Change-Id: Ia79acf8670f385cee48c45b0a75371d8e950af34
Diffstat (limited to 'chrome/browser/resources/history2.html')
-rw-r--r-- | chrome/browser/resources/history2.html | 1199 |
1 files changed, 1199 insertions, 0 deletions
diff --git a/chrome/browser/resources/history2.html b/chrome/browser/resources/history2.html new file mode 100644 index 0000000..a2a2ca3 --- /dev/null +++ b/chrome/browser/resources/history2.html @@ -0,0 +1,1199 @@ +<!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 id2checkbox = []; + + +/////////////////////////////////////////////////////////////////////////////// +// 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.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: -------------------------------------------------------------- +/** + * @return {DOMObject} Gets the DOM representation of the page + * for use in browse results. + */ +Page.prototype.getBrowseResultDOM = function() { + var node = createElementWithClassName('div', 'entry'); + var time = createElementWithClassName('div', 'time'); + 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, false); + id2checkbox[this.id_] = checkbox; + time.appendChild(checkbox); + } + time.appendChild(document.createTextNode(this.dateTimeOfDay)); + node.appendChild(time); + node.appendChild(this.getTitleDOM_()); + return node; +}; + +/** + * @return {DOMObject} Gets the DOM representation of the page for + * use in search results. + */ +Page.prototype.getSearchResultDOM = function() { + var row = createElementWithClassName('tr', 'entry'); + var datecell = createElementWithClassName('td', 'time'); + datecell.appendChild(document.createTextNode(this.dateShort)); + row.appendChild(datecell); + + var titleCell = document.createElement('td'); + titleCell.valign = 'top'; + titleCell.appendChild(this.getTitleDOM_()); + var snippet = createElementWithClassName('div', 'snippet'); + this.addHighlightedText_(snippet, + this.snippet_, + this.model_.getSearchText()); + titleCell.appendChild(snippet); + row.appendChild(titleCell); + + return row; +}; + +// Page, private: ------------------------------------------------------------- +/** + * 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('div'); + node.className = 'title'; + var link = document.createElement('a'); + link.href = this.url_; + link.style.backgroundImage = + 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; + link.id = "id-" + this.id_; + this.addHighlightedText_(link, this.title_, 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; + id2checkbox = []; + + // 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 resultTable = createElementWithClassName('table', 'results'); + resultTable.cellSpacing = 0; + resultTable.cellPadding = 0; + resultTable.border = 0; + + for (var i = 0, page; page = results[i]; i++) { + if (!page.isRendered) { + resultTable.appendChild(page.getSearchResultDOM()); + this.setPageRendered_(page); + } + } + this.resultDiv_.appendChild(resultTable); + } else { + var lastTime = Math.infinity; + for (var i = 0, page; page = results[i]; i++) { + if (page.isRendered) { + continue; + } + // Break across day boundaries and insert gaps for browsing pauses. + var thisTime = page.time.getTime(); + + if ((i == 0 && page.continued) || !page.continued) { + var day = createElementWithClassName('div', 'day'); + day.appendChild(document.createTextNode(page.dateRelativeDay)); + + if (i == 0 && page.continued) { + day.appendChild(document.createTextNode(' ' + + localStrings.getString('cont'))); + } + + this.resultDiv_.appendChild(day); + } else if (lastTime - thisTime > BROWSING_GAP_TIME) { + this.resultDiv_.appendChild(createElementWithClassName('div', 'gap')); + } + lastTime = thisTime; + + // Add entry. + this.resultDiv_.appendChild(page.getBrowseResultDOM()); + this.setPageRendered_(page); + } + } + + 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++) { + id2checkbox[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 > 1) { + deleteQueue = deleteQueue.slice(2); + deleteNextInQueue(); + } else { + deleteQueue = []; + } +} + +/** + * 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); +} + +/** + * We're called when something is deleted (either by us or by someone + * else). + */ +function historyDeleted() { + window.console.log('History deleted'); + historyView.reload(); +} +</script> +<link rel="stylesheet" href="dom_ui2.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; +} +.day { + margin-top:18px; + padding:0px 3px; + display:inline-block; +} +.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 { + margin-left:18px; + width:15px; + border-right:1px solid #ddd; + height:14px; +} +.entry { + margin-left:18px; + margin-top:6px; + overflow:auto; +} +table.results { + margin-left:4px; +} +.entry .time { + color:#888; + float:left; + min-width:56px; + margin-right:5px; + padding-top:1px; + white-space:nowrap; +} +html[dir='rtl'] .time { + margin-right:0px; + margin-left:5px; + float:right; +} +.entry .title { + max-width:600px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.results .time, .results .title { + margin-top:18px; +} +.title > .starred { + background:url('shared/images/star_small.png'); + background-repeat:no-repeat; + display:inline-block; + margin-left:12px; + margin-right:0; + width:11px; + height:11px; +} +html[dir='rtl'] .title > .starred { + margin-left:0; + margin-right:12px; +} +.entry .title > a { + -webkit-box-sizing: border-box; + background-repeat:no-repeat; + background-size:16px; + background-position:0px 1px; + padding:1px 0px 4px 22px; + display:inline-block; + overflow:hidden; + text-overflow:ellipsis; +} +html[dir='rtl'] .entry .title > a { + background-position-x:right; + padding-left:0px; + padding-right:22px; +} +#results-pagination { + padding-top:24px; + margin-left:18px; +} + +</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> + Debug: this is History 2 + <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> |