<!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>