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