| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /////////////////////////////////////////////////////////////////////////////// | 5 /////////////////////////////////////////////////////////////////////////////// |
| 6 // Globals: | 6 // Globals: |
| 7 var RESULTS_PER_PAGE = 150; | 7 var RESULTS_PER_PAGE = 150; |
| 8 var MAX_SEARCH_DEPTH_MONTHS = 18; | 8 var MAX_SEARCH_DEPTH_MONTHS = 18; |
| 9 | 9 |
| 10 // Amount of time between pageviews that we consider a 'break' in browsing, | 10 // Amount of time between pageviews that we consider a 'break' in browsing, |
| (...skipping 14 matching lines...) Expand all Loading... |
| 25 return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)"); | 25 return uri.replace(/\(/g, "\\(").replace(/\)/g, "\\)"); |
| 26 } | 26 } |
| 27 | 27 |
| 28 // TODO(glen): Get rid of these global references, replace with a controller | 28 // TODO(glen): Get rid of these global references, replace with a controller |
| 29 // or just make the classes own more of the page. | 29 // or just make the classes own more of the page. |
| 30 var historyModel; | 30 var historyModel; |
| 31 var historyView; | 31 var historyView; |
| 32 var localStrings; | 32 var localStrings; |
| 33 var pageState; | 33 var pageState; |
| 34 var deleteQueue = []; | 34 var deleteQueue = []; |
| 35 var deleteInFlight = false; | |
| 36 var selectionAnchor = -1; | 35 var selectionAnchor = -1; |
| 37 var id2checkbox = []; | 36 var activePage = null; |
| 38 | 37 |
| 38 const MenuButton = cr.ui.MenuButton; |
| 39 const Command = cr.ui.Command; |
| 40 const Menu = cr.ui.Menu; |
| 41 |
| 42 function createDropDownBgImage(canvasName, colorSpec) { |
| 43 var ctx = document.getCSSCanvasContext('2d', canvasName, 6, 4); |
| 44 ctx.fillStyle = ctx.strokeStyle = colorSpec; |
| 45 ctx.beginPath(); |
| 46 ctx.moveTo(0, 0); |
| 47 ctx.lineTo(6, 0); |
| 48 ctx.lineTo(3, 3); |
| 49 ctx.closePath(); |
| 50 ctx.fill(); |
| 51 ctx.stroke(); |
| 52 return ctx; |
| 53 } |
| 54 |
| 55 // Create the canvases to be used as the drop down button background images. |
| 56 var arrow = createDropDownBgImage('drop-down-arrow', 'hsl(214, 91%, 85%)'); |
| 57 var hoverArrow = createDropDownBgImage('drop-down-arrow-hover', '#6A86DE'); |
| 58 var activeArrow = createDropDownBgImage('drop-down-arrow-active', 'white'); |
| 39 | 59 |
| 40 /////////////////////////////////////////////////////////////////////////////// | 60 /////////////////////////////////////////////////////////////////////////////// |
| 41 // Page: | 61 // Page: |
| 42 /** | 62 /** |
| 43 * Class to hold all the information about an entry in our model. | 63 * Class to hold all the information about an entry in our model. |
| 44 * @param {Object} result An object containing the page's data. | 64 * @param {Object} result An object containing the page's data. |
| 45 * @param {boolean} continued Whether this page is on the same day as the | 65 * @param {boolean} continued Whether this page is on the same day as the |
| 46 * page before it | 66 * page before it |
| 47 */ | 67 */ |
| 48 function Page(result, continued, model, id) { | 68 function Page(result, continued, model, id) { |
| 49 this.model_ = model; | 69 this.model_ = model; |
| 50 this.title_ = result.title; | 70 this.title_ = result.title; |
| 51 this.url_ = result.url; | 71 this.url_ = result.url; |
| 72 this.domain_ = this.getDomainFromURL_(this.url_); |
| 52 this.starred_ = result.starred; | 73 this.starred_ = result.starred; |
| 53 this.snippet_ = result.snippet || ""; | 74 this.snippet_ = result.snippet || ""; |
| 54 this.id_ = id; | 75 this.id_ = id; |
| 55 | 76 |
| 56 this.changed = false; | 77 this.changed = false; |
| 57 | 78 |
| 58 this.isRendered = false; | 79 this.isRendered = false; |
| 59 | 80 |
| 60 // All the date information is public so that owners can compare properties of | 81 // All the date information is public so that owners can compare properties of |
| 61 // two items easily. | 82 // two items easily. |
| 62 | 83 |
| 63 // We get the time in seconds, but we want it in milliseconds. | 84 // We get the time in seconds, but we want it in milliseconds. |
| 64 this.time = new Date(result.time * 1000); | 85 this.time = new Date(result.time * 1000); |
| 65 | 86 |
| 66 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always | 87 // See comment in BrowsingHistoryHandler::QueryComplete - we won't always |
| 67 // get all of these. | 88 // get all of these. |
| 68 this.dateRelativeDay = result.dateRelativeDay || ""; | 89 this.dateRelativeDay = result.dateRelativeDay || ""; |
| 69 this.dateTimeOfDay = result.dateTimeOfDay || ""; | 90 this.dateTimeOfDay = result.dateTimeOfDay || ""; |
| 70 this.dateShort = result.dateShort || ""; | 91 this.dateShort = result.dateShort || ""; |
| 71 | 92 |
| 72 // Whether this is the continuation of a previous day. | 93 // Whether this is the continuation of a previous day. |
| 73 this.continued = continued; | 94 this.continued = continued; |
| 74 } | 95 } |
| 75 | 96 |
| 76 // Page, Public: -------------------------------------------------------------- | 97 // Page, Public: -------------------------------------------------------------- |
| 77 /** | 98 /** |
| 78 * @return {DOMObject} Gets the DOM representation of the page | 99 * Returns a dom structure for a browse page result or a search page result. |
| 79 * for use in browse results. | 100 * @param {boolean} Flag to indicate if result is a search result. |
| 101 * @return {Element} The dom structure. |
| 80 */ | 102 */ |
| 81 Page.prototype.getBrowseResultDOM = function() { | 103 Page.prototype.getResultDOM = function(searchResultFlag) { |
| 82 var node = createElementWithClassName('div', 'entry'); | 104 var node = createElementWithClassName('li', 'entry'); |
| 83 var time = createElementWithClassName('div', 'time'); | 105 var time = createElementWithClassName('div', 'time'); |
| 84 if (this.model_.getEditMode()) { | 106 var entryBox = createElementWithClassName('label', 'entry-box'); |
| 85 var checkbox = document.createElement('input'); | 107 var domain = createElementWithClassName('div', 'domain'); |
| 86 checkbox.type = "checkbox"; | |
| 87 checkbox.name = this.id_; | |
| 88 checkbox.time = this.time.toString(); | |
| 89 checkbox.addEventListener("click", checkboxClicked, false); | |
| 90 id2checkbox[this.id_] = checkbox; | |
| 91 time.appendChild(checkbox); | |
| 92 } | |
| 93 time.appendChild(document.createTextNode(this.dateTimeOfDay)); | |
| 94 node.appendChild(time); | |
| 95 node.appendChild(this.getTitleDOM_()); | |
| 96 return node; | |
| 97 }; | |
| 98 | 108 |
| 99 /** | 109 var dropDown = createElementWithClassName('button', 'drop-down'); |
| 100 * @return {DOMObject} Gets the DOM representation of the page for | 110 dropDown.value = 'Open action menu'; |
| 101 * use in search results. | 111 dropDown.title = localStrings.getString('actionMenuDescription'); |
| 102 */ | 112 dropDown.setAttribute('menu', '#action-menu'); |
| 103 Page.prototype.getSearchResultDOM = function() { | 113 cr.ui.decorate(dropDown, MenuButton); |
| 104 var row = createElementWithClassName('tr', 'entry'); | 114 |
| 105 var datecell = createElementWithClassName('td', 'time'); | 115 // Checkbox is always created, but only visible on hover & when checked. |
| 106 datecell.appendChild(document.createTextNode(this.dateShort)); | 116 var checkbox = document.createElement('input'); |
| 107 row.appendChild(datecell); | 117 checkbox.type = 'checkbox'; |
| 108 if (this.model_.getEditMode()) { | 118 checkbox.id = 'checkbox-' + this.id_; |
| 109 var checkbox = document.createElement('input'); | 119 checkbox.time = this.time.getTime(); |
| 110 checkbox.type = "checkbox"; | 120 checkbox.addEventListener('click', checkboxClicked); |
| 111 checkbox.name = this.id_; | 121 time.appendChild(checkbox); |
| 112 checkbox.time = this.time.toString(); | 122 |
| 113 checkbox.addEventListener("click", checkboxClicked, false); | 123 // Keep track of the drop down that triggered the menu, so we know |
| 114 id2checkbox[this.id_] = checkbox; | 124 // which element to apply the command to. |
| 115 datecell.appendChild(checkbox); | 125 // TODO(dubroy): Ideally we'd use 'activate', but MenuButton swallows it. |
| 126 var self = this; |
| 127 var setActivePage = function(e) { |
| 128 activePage = self; |
| 129 }; |
| 130 dropDown.addEventListener('mousedown', setActivePage); |
| 131 dropDown.addEventListener('focus', setActivePage); |
| 132 |
| 133 domain.style.backgroundImage = |
| 134 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; |
| 135 domain.textContent = this.domain_; |
| 136 |
| 137 // Clicking anywhere in the entryBox will check/uncheck the checkbox. |
| 138 entryBox.setAttribute('for', checkbox.id); |
| 139 entryBox.addEventListener('mousedown', entryBoxMousedown, false); |
| 140 |
| 141 // Prevent clicks on the drop down from affecting the checkbox. |
| 142 dropDown.addEventListener('click', function(e) { e.preventDefault(); }); |
| 143 |
| 144 // We use a wrapper div so that the entry contents will be shinkwrapped. |
| 145 entryBox.appendChild(time); |
| 146 entryBox.appendChild(domain); |
| 147 entryBox.appendChild(this.getTitleDOM_()); |
| 148 entryBox.appendChild(dropDown); |
| 149 |
| 150 // Let the entryBox be styled appropriately when it contains keyboard focus. |
| 151 entryBox.addEventListener('focus', function() { |
| 152 this.classList.add('contains-focus'); |
| 153 }, true); |
| 154 entryBox.addEventListener('blur', function() { |
| 155 this.classList.remove('contains-focus'); |
| 156 }, true); |
| 157 |
| 158 node.appendChild(entryBox); |
| 159 |
| 160 if (searchResultFlag) { |
| 161 time.textContent = this.dateShort; |
| 162 var snippet = createElementWithClassName('div', 'snippet'); |
| 163 this.addHighlightedText_(snippet, |
| 164 this.snippet_, |
| 165 this.model_.getSearchText()); |
| 166 node.appendChild(snippet); |
| 167 } else { |
| 168 time.appendChild(document.createTextNode(this.dateTimeOfDay)); |
| 116 } | 169 } |
| 117 | 170 |
| 118 var titleCell = document.createElement('td'); | 171 if (typeof this.domNode_ != 'undefined') { |
| 119 titleCell.valign = 'top'; | 172 console.error('Already generated node for page.'); |
| 120 titleCell.appendChild(this.getTitleDOM_()); | 173 } |
| 121 var snippet = createElementWithClassName('div', 'snippet'); | 174 this.domNode_ = node; |
| 122 this.addHighlightedText_(snippet, | |
| 123 this.snippet_, | |
| 124 this.model_.getSearchText()); | |
| 125 titleCell.appendChild(snippet); | |
| 126 row.appendChild(titleCell); | |
| 127 | 175 |
| 128 return row; | 176 return node; |
| 129 }; | 177 }; |
| 130 | 178 |
| 131 // Page, private: ------------------------------------------------------------- | 179 // Page, private: ------------------------------------------------------------- |
| 132 /** | 180 /** |
| 133 * Add child text nodes to a node such that occurrences of the spcified text is | 181 * Extracts and returns the domain (and subdomains) from a URL. |
| 134 * highligted. | 182 * @param {string} The url |
| 183 * @return (string) The domain. An empty string is returned if no domain can |
| 184 * be found. |
| 185 */ |
| 186 Page.prototype.getDomainFromURL_ = function(url) { |
| 187 var domain = url.replace(/^.+:\/\//, '').match(/[^/]+/); |
| 188 return domain ? domain[0] : ''; |
| 189 }; |
| 190 |
| 191 /** |
| 192 * Add child text nodes to a node such that occurrences of the specified text is |
| 193 * highlighted. |
| 135 * @param {Node} node The node under which new text nodes will be made as | 194 * @param {Node} node The node under which new text nodes will be made as |
| 136 * children. | 195 * children. |
| 137 * @param {string} content Text to be added beneath |node| as one or more | 196 * @param {string} content Text to be added beneath |node| as one or more |
| 138 * text nodes. | 197 * text nodes. |
| 139 * @param {string} highlightText Occurences of this text inside |content| will | 198 * @param {string} highlightText Occurences of this text inside |content| will |
| 140 * be highlighted. | 199 * be highlighted. |
| 141 */ | 200 */ |
| 142 Page.prototype.addHighlightedText_ = function(node, content, highlightText) { | 201 Page.prototype.addHighlightedText_ = function(node, content, highlightText) { |
| 143 var i = 0; | 202 var i = 0; |
| 144 if (highlightText) { | 203 if (highlightText) { |
| (...skipping 11 matching lines...) Expand all Loading... |
| 156 } | 215 } |
| 157 } | 216 } |
| 158 if (i < content.length) | 217 if (i < content.length) |
| 159 node.appendChild(document.createTextNode(content.slice(i))); | 218 node.appendChild(document.createTextNode(content.slice(i))); |
| 160 }; | 219 }; |
| 161 | 220 |
| 162 /** | 221 /** |
| 163 * @return {DOMObject} DOM representation for the title block. | 222 * @return {DOMObject} DOM representation for the title block. |
| 164 */ | 223 */ |
| 165 Page.prototype.getTitleDOM_ = function() { | 224 Page.prototype.getTitleDOM_ = function() { |
| 166 var node = document.createElement('div'); | 225 var node = createElementWithClassName('div', 'title'); |
| 167 node.className = 'title'; | |
| 168 var link = document.createElement('a'); | 226 var link = document.createElement('a'); |
| 169 link.href = this.url_; | 227 link.href = this.url_; |
| 228 link.id = "id-" + this.id_; |
| 170 | 229 |
| 171 link.style.backgroundImage = | 230 // Add a tooltip, since it might be ellipsized. |
| 172 'url(chrome://favicon/' + encodeURIForCSS(this.url_) + ')'; | 231 // TODO(dubroy): Find a way to show the tooltip only when necessary. |
| 173 link.id = "id-" + this.id_; | 232 link.title = this.title_; |
| 233 |
| 174 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); | 234 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); |
| 175 | |
| 176 node.appendChild(link); | 235 node.appendChild(link); |
| 177 | 236 |
| 178 if (this.starred_) { | 237 if (this.starred_) { |
| 179 node.className += ' starred'; | 238 node.className += ' starred'; |
| 180 node.appendChild(createElementWithClassName('div', 'starred')); | 239 node.appendChild(createElementWithClassName('div', 'starred')); |
| 181 } | 240 } |
| 182 | 241 |
| 183 return node; | 242 return node; |
| 184 }; | 243 }; |
| 185 | 244 |
| 245 /** |
| 246 * Launch a search for more history entries from the same domain. |
| 247 */ |
| 248 Page.prototype.showMoreFromSite_ = function() { |
| 249 setSearch(this.domain_); |
| 250 }; |
| 251 |
| 252 /** |
| 253 * Remove a single entry from the history. |
| 254 */ |
| 255 Page.prototype.removeFromHistory_ = function() { |
| 256 var self = this; |
| 257 var onSuccessCallback = function() { |
| 258 removeEntryFromView(self.domNode_); |
| 259 }; |
| 260 queueURLsForDeletion(this.time, [this.url_], onSuccessCallback); |
| 261 deleteNextInQueue(); |
| 262 }; |
| 263 |
| 264 |
| 186 // Page, private, static: ----------------------------------------------------- | 265 // Page, private, static: ----------------------------------------------------- |
| 187 | 266 |
| 188 /** | 267 /** |
| 189 * Quote a string so it can be used in a regular expression. | 268 * Quote a string so it can be used in a regular expression. |
| 190 * @param {string} str The source string | 269 * @param {string} str The source string |
| 191 * @return {string} The escaped string | 270 * @return {string} The escaped string |
| 192 */ | 271 */ |
| 193 Page.pregQuote_ = function(str) { | 272 Page.pregQuote_ = function(str) { |
| 194 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); | 273 return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); |
| 195 }; | 274 }; |
| 196 | 275 |
| 197 /////////////////////////////////////////////////////////////////////////////// | 276 /////////////////////////////////////////////////////////////////////////////// |
| 198 // HistoryModel: | 277 // HistoryModel: |
| 199 /** | 278 /** |
| 200 * Global container for history data. Future optimizations might include | 279 * Global container for history data. Future optimizations might include |
| 201 * allowing the creation of a HistoryModel for each search string, allowing | 280 * allowing the creation of a HistoryModel for each search string, allowing |
| 202 * quick flips back and forth between results. | 281 * quick flips back and forth between results. |
| 203 * | 282 * |
| 204 * The history model is based around pages, and only fetching the data to | 283 * The history model is based around pages, and only fetching the data to |
| 205 * fill the currently requested page. This is somewhat dependent on the view, | 284 * fill the currently requested page. This is somewhat dependent on the view, |
| 206 * and so future work may wish to change history model to operate on | 285 * and so future work may wish to change history model to operate on |
| 207 * timeframe (day or week) based containers. | 286 * timeframe (day or week) based containers. |
| 208 */ | 287 */ |
| 209 function HistoryModel() { | 288 function HistoryModel() { |
| 210 this.clearModel_(); | 289 this.clearModel_(); |
| 211 this.setEditMode(false); | |
| 212 this.view_; | |
| 213 } | 290 } |
| 214 | 291 |
| 215 // HistoryModel, Public: ------------------------------------------------------ | 292 // HistoryModel, Public: ------------------------------------------------------ |
| 216 /** | 293 /** |
| 217 * Sets our current view that is called when the history model changes. | 294 * Sets our current view that is called when the history model changes. |
| 218 * @param {HistoryView} view The view to set our current view to. | 295 * @param {HistoryView} view The view to set our current view to. |
| 219 */ | 296 */ |
| 220 HistoryModel.prototype.setView = function(view) { | 297 HistoryModel.prototype.setView = function(view) { |
| 221 this.view_ = view; | 298 this.view_ = view; |
| 222 }; | 299 }; |
| (...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 330 * @return {Array} A list of pages | 407 * @return {Array} A list of pages |
| 331 */ | 408 */ |
| 332 HistoryModel.prototype.getNumberedRange = function(start, end) { | 409 HistoryModel.prototype.getNumberedRange = function(start, end) { |
| 333 if (start >= this.getSize()) | 410 if (start >= this.getSize()) |
| 334 return []; | 411 return []; |
| 335 | 412 |
| 336 var end = end > this.getSize() ? this.getSize() : end; | 413 var end = end > this.getSize() ? this.getSize() : end; |
| 337 return this.pages_.slice(start, end); | 414 return this.pages_.slice(start, end); |
| 338 }; | 415 }; |
| 339 | 416 |
| 340 /** | |
| 341 * @return {boolean} Whether we are in edit mode where history items can be | |
| 342 * deleted | |
| 343 */ | |
| 344 HistoryModel.prototype.getEditMode = function() { | |
| 345 return this.editMode_; | |
| 346 }; | |
| 347 | |
| 348 /** | |
| 349 * @param {boolean} edit_mode Control whether we are in edit mode. | |
| 350 */ | |
| 351 HistoryModel.prototype.setEditMode = function(edit_mode) { | |
| 352 this.editMode_ = edit_mode; | |
| 353 }; | |
| 354 | |
| 355 // HistoryModel, Private: ----------------------------------------------------- | 417 // HistoryModel, Private: ----------------------------------------------------- |
| 356 HistoryModel.prototype.clearModel_ = function() { | 418 HistoryModel.prototype.clearModel_ = function() { |
| 357 this.inFlight_ = false; // Whether a query is inflight. | 419 this.inFlight_ = false; // Whether a query is inflight. |
| 358 this.searchText_ = ''; | 420 this.searchText_ = ''; |
| 359 this.searchDepth_ = 0; | 421 this.searchDepth_ = 0; |
| 360 this.pages_ = []; // Date-sorted list of pages. | 422 this.pages_ = []; // Date-sorted list of pages. |
| 361 this.last_id_ = 0; | 423 this.last_id_ = 0; |
| 362 selectionAnchor = -1; | 424 selectionAnchor = -1; |
| 363 id2checkbox = []; | |
| 364 | 425 |
| 365 // The page that the view wants to see - we only fetch slightly past this | 426 // The page that the view wants to see - we only fetch slightly past this |
| 366 // point. If the view requests a page that we don't have data for, we try | 427 // point. If the view requests a page that we don't have data for, we try |
| 367 // to fetch it and call back when we're done. | 428 // to fetch it and call back when we're done. |
| 368 this.requestedPage_ = 0; | 429 this.requestedPage_ = 0; |
| 369 | 430 |
| 370 this.complete_ = false; | 431 this.complete_ = false; |
| 371 | 432 |
| 372 if (this.view_) { | 433 if (this.view_) { |
| 373 this.view_.clear_(); | 434 this.view_.clear_(); |
| 374 } | 435 } |
| 375 }; | 436 }; |
| 376 | 437 |
| 377 /** | 438 /** |
| 378 * Figure out if we need to do more searches to fill the currently requested | 439 * Figure out if we need to do more searches to fill the currently requested |
| 379 * page. If we think we can fill the page, call the view and let it know | 440 * page. If we think we can fill the page, call the view and let it know |
| 380 * we're ready to show something. | 441 * we're ready to show something. |
| 381 */ | 442 */ |
| 382 HistoryModel.prototype.updateSearch_ = function(finished) { | 443 HistoryModel.prototype.updateSearch_ = function(finished) { |
| 383 if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || | 444 if ((this.searchText_ && this.searchDepth_ >= MAX_SEARCH_DEPTH_MONTHS) || |
| 384 finished) { | 445 finished) { |
| 385 // We have maxed out. There will be no more data. | 446 // We have maxed out. There will be no more data. |
| 386 this.complete_ = true; | 447 this.complete_ = true; |
| 387 this.view_.onModelReady(); | 448 this.view_.onModelReady(); |
| 388 this.changed = false; | 449 this.changed = false; |
| 389 } else { | 450 } else { |
| 390 // If we can't fill the requested page, ask for more data unless a request | 451 // If we can't fill the requested page, ask for more data unless a request |
| 391 // is still in-flight. | 452 // is still in-flight. |
| 392 if (!this.inFlight_ && !this.canFillPage_(this.requestedPage_)) { | 453 if (!this.canFillPage_(this.requestedPage_) && !this.inFlight_) { |
| 393 this.getSearchResults_(this.searchDepth_ + 1); | 454 this.getSearchResults_(this.searchDepth_ + 1); |
| 394 } | 455 } |
| 395 | 456 |
| 396 // If we have any data for the requested page, show it. | 457 // If we have any data for the requested page, show it. |
| 397 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { | 458 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { |
| 398 this.view_.onModelReady(); | 459 this.view_.onModelReady(); |
| 399 this.changed = false; | 460 this.changed = false; |
| 400 } | 461 } |
| 401 } | 462 } |
| 402 }; | 463 }; |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 463 this.lastDisplayed_ = []; | 524 this.lastDisplayed_ = []; |
| 464 | 525 |
| 465 this.model_.setView(this); | 526 this.model_.setView(this); |
| 466 | 527 |
| 467 this.currentPages_ = []; | 528 this.currentPages_ = []; |
| 468 | 529 |
| 469 var self = this; | 530 var self = this; |
| 470 window.onresize = function() { | 531 window.onresize = function() { |
| 471 self.updateEntryAnchorWidth_(); | 532 self.updateEntryAnchorWidth_(); |
| 472 }; | 533 }; |
| 473 this.updateEditControls_(); | |
| 474 this.editButtonTd_.hidden = true; | |
| 475 | 534 |
| 476 this.boundUpdateRemoveButton_ = function(e) { | 535 $('clear-browsing-data').addEventListener('click', openClearBrowsingData); |
| 477 return self.updateRemoveButton_(e); | 536 $('remove-selected').addEventListener('click', removeItems); |
| 478 }; | |
| 479 } | 537 } |
| 480 | 538 |
| 481 // HistoryView, public: ------------------------------------------------------- | 539 // HistoryView, public: ------------------------------------------------------- |
| 482 /** | 540 /** |
| 483 * Do a search and optionally view a certain page. | 541 * Do a search and optionally view a certain page. |
| 484 * @param {string} term The string to search for. | 542 * @param {string} term The string to search for. |
| 485 * @param {number} opt_page The page we wish to view, only use this for | 543 * @param {number} opt_page The page we wish to view, only use this for |
| 486 * setting up initial views, as this triggers a search. | 544 * setting up initial views, as this triggers a search. |
| 487 */ | 545 */ |
| 488 HistoryView.prototype.setSearch = function(term, opt_page) { | 546 HistoryView.prototype.setSearch = function(term, opt_page) { |
| 489 this.pageIndex_ = parseInt(opt_page || 0, 10); | 547 this.pageIndex_ = parseInt(opt_page || 0, 10); |
| 490 window.scrollTo(0, 0); | 548 window.scrollTo(0, 0); |
| 491 this.model_.setSearchText(term, this.pageIndex_); | 549 this.model_.setSearchText(term, this.pageIndex_); |
| 492 this.updateEditControls_(); | 550 pageState.setUIState(term, this.pageIndex_); |
| 493 pageState.setUIState(this.model_.getEditMode(), term, this.pageIndex_); | |
| 494 }; | 551 }; |
| 495 | 552 |
| 496 /** | 553 /** |
| 497 * Controls edit mode where history can be deleted. | |
| 498 * @param {boolean} edit_mode Whether to enable edit mode. | |
| 499 */ | |
| 500 HistoryView.prototype.setEditMode = function(edit_mode) { | |
| 501 this.model_.setEditMode(edit_mode); | |
| 502 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), | |
| 503 this.pageIndex_); | |
| 504 }; | |
| 505 | |
| 506 /** | |
| 507 * Toggles the edit mode and triggers UI update. | |
| 508 */ | |
| 509 HistoryView.prototype.toggleEditMode = function() { | |
| 510 var editMode = !this.model_.getEditMode(); | |
| 511 this.setEditMode(editMode); | |
| 512 this.updateEditControls_(); | |
| 513 }; | |
| 514 | |
| 515 /** | |
| 516 * @return {boolean} Whether we are in edit mode where history items can be | |
| 517 * deleted | |
| 518 */ | |
| 519 HistoryView.prototype.getEditMode = function() { | |
| 520 return this.model_.getEditMode(); | |
| 521 }; | |
| 522 | |
| 523 /** | |
| 524 * Reload the current view. | 554 * Reload the current view. |
| 525 */ | 555 */ |
| 526 HistoryView.prototype.reload = function() { | 556 HistoryView.prototype.reload = function() { |
| 527 this.model_.reload(); | 557 this.model_.reload(); |
| 558 this.updateRemoveButton(); |
| 528 }; | 559 }; |
| 529 | 560 |
| 530 /** | 561 /** |
| 531 * Switch to a specified page. | 562 * Switch to a specified page. |
| 532 * @param {number} page The page we wish to view. | 563 * @param {number} page The page we wish to view. |
| 533 */ | 564 */ |
| 534 HistoryView.prototype.setPage = function(page) { | 565 HistoryView.prototype.setPage = function(page) { |
| 535 this.clear_(); | 566 this.clear_(); |
| 536 this.pageIndex_ = parseInt(page, 10); | 567 this.pageIndex_ = parseInt(page, 10); |
| 537 window.scrollTo(0, 0); | 568 window.scrollTo(0, 0); |
| 538 this.model_.requestPage(page); | 569 this.model_.requestPage(page); |
| 539 pageState.setUIState(this.model_.getEditMode(), this.model_.getSearchText(), | 570 pageState.setUIState(this.model_.getSearchText(), this.pageIndex_); |
| 540 this.pageIndex_); | |
| 541 }; | 571 }; |
| 542 | 572 |
| 543 /** | 573 /** |
| 544 * @return {number} The page number being viewed. | 574 * @return {number} The page number being viewed. |
| 545 */ | 575 */ |
| 546 HistoryView.prototype.getPage = function() { | 576 HistoryView.prototype.getPage = function() { |
| 547 return this.pageIndex_; | 577 return this.pageIndex_; |
| 548 }; | 578 }; |
| 549 | 579 |
| 550 /** | 580 /** |
| 551 * Callback for the history model to let it know that it has data ready for us | 581 * Callback for the history model to let it know that it has data ready for us |
| 552 * to view. | 582 * to view. |
| 553 */ | 583 */ |
| 554 HistoryView.prototype.onModelReady = function() { | 584 HistoryView.prototype.onModelReady = function() { |
| 555 this.displayResults_(); | 585 this.displayResults_(); |
| 556 }; | 586 }; |
| 557 | 587 |
| 588 /** |
| 589 * Enables or disables the 'Remove selected items' button as appropriate. |
| 590 */ |
| 591 HistoryView.prototype.updateRemoveButton = function() { |
| 592 var anyChecked = document.querySelector('.entry input:checked') != null; |
| 593 $('remove-selected').disabled = !anyChecked; |
| 594 } |
| 595 |
| 558 // HistoryView, private: ------------------------------------------------------ | 596 // HistoryView, private: ------------------------------------------------------ |
| 559 /** | 597 /** |
| 560 * Clear the results in the view. Since we add results piecemeal, we need | 598 * Clear the results in the view. Since we add results piecemeal, we need |
| 561 * to clear them out when we switch to a new page or reload. | 599 * to clear them out when we switch to a new page or reload. |
| 562 */ | 600 */ |
| 563 HistoryView.prototype.clear_ = function() { | 601 HistoryView.prototype.clear_ = function() { |
| 564 this.resultDiv_.textContent = ''; | 602 this.resultDiv_.textContent = ''; |
| 565 | 603 |
| 566 var pages = this.currentPages_; | 604 var pages = this.currentPages_; |
| 567 for (var i = 0; i < pages.length; i++) { | 605 for (var i = 0; i < pages.length; i++) { |
| 568 pages[i].isRendered = false; | 606 pages[i].isRendered = false; |
| 569 } | 607 } |
| 570 this.currentPages_ = []; | 608 this.currentPages_ = []; |
| 571 }; | 609 }; |
| 572 | 610 |
| 573 HistoryView.prototype.setPageRendered_ = function(page) { | 611 HistoryView.prototype.setPageRendered_ = function(page) { |
| 574 page.isRendered = true; | 612 page.isRendered = true; |
| 575 this.currentPages_.push(page); | 613 this.currentPages_.push(page); |
| 576 }; | 614 }; |
| 577 | 615 |
| 578 /** | 616 /** |
| 579 * Update the page with results. | 617 * Update the page with results. |
| 580 */ | 618 */ |
| 581 HistoryView.prototype.displayResults_ = function() { | 619 HistoryView.prototype.displayResults_ = function() { |
| 582 // Hide the Edit Button if there are no history results to display. | |
| 583 this.editButtonTd_.hidden = !this.model_.getSize(); | |
| 584 | |
| 585 var results = this.model_.getNumberedRange( | 620 var results = this.model_.getNumberedRange( |
| 586 this.pageIndex_ * RESULTS_PER_PAGE, | 621 this.pageIndex_ * RESULTS_PER_PAGE, |
| 587 this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); | 622 this.pageIndex_ * RESULTS_PER_PAGE + RESULTS_PER_PAGE); |
| 588 | 623 |
| 589 if (this.model_.getSearchText()) { | 624 if (this.model_.getSearchText()) { |
| 590 var resultTable = createElementWithClassName('table', 'results'); | 625 var searchResults = createElementWithClassName('ol', 'search-results'); |
| 591 resultTable.cellSpacing = 0; | |
| 592 resultTable.cellPadding = 0; | |
| 593 resultTable.border = 0; | |
| 594 | |
| 595 for (var i = 0, page; page = results[i]; i++) { | 626 for (var i = 0, page; page = results[i]; i++) { |
| 596 if (!page.isRendered) { | 627 if (!page.isRendered) { |
| 597 resultTable.appendChild(page.getSearchResultDOM()); | 628 searchResults.appendChild(page.getResultDOM(true)); |
| 598 this.setPageRendered_(page); | 629 this.setPageRendered_(page); |
| 599 } | 630 } |
| 600 } | 631 } |
| 601 this.resultDiv_.appendChild(resultTable); | 632 this.resultDiv_.appendChild(searchResults); |
| 602 } else { | 633 } else { |
| 634 var resultsFragment = document.createDocumentFragment(); |
| 603 var lastTime = Math.infinity; | 635 var lastTime = Math.infinity; |
| 636 var dayResults; |
| 604 for (var i = 0, page; page = results[i]; i++) { | 637 for (var i = 0, page; page = results[i]; i++) { |
| 605 if (page.isRendered) { | 638 if (page.isRendered) { |
| 606 continue; | 639 continue; |
| 607 } | 640 } |
| 608 // Break across day boundaries and insert gaps for browsing pauses. | 641 // Break across day boundaries and insert gaps for browsing pauses. |
| 642 // Create a dayResults element to contain results for each day |
| 609 var thisTime = page.time.getTime(); | 643 var thisTime = page.time.getTime(); |
| 610 | 644 |
| 611 if ((i == 0 && page.continued) || !page.continued) { | 645 if ((i == 0 && page.continued) || !page.continued) { |
| 612 var day = createElementWithClassName('div', 'day'); | 646 var day = createElementWithClassName('h2', 'day'); |
| 613 day.appendChild(document.createTextNode(page.dateRelativeDay)); | 647 day.appendChild(document.createTextNode(page.dateRelativeDay)); |
| 614 | |
| 615 if (i == 0 && page.continued) { | 648 if (i == 0 && page.continued) { |
| 616 day.appendChild(document.createTextNode(' ' + | 649 day.appendChild(document.createTextNode(' ' + |
| 617 localStrings.getString('cont'))); | 650 localStrings.getString('cont'))); |
| 618 } | 651 } |
| 619 | 652 |
| 620 this.resultDiv_.appendChild(day); | 653 // If there is an existing dayResults element, append it. |
| 654 if (dayResults) { |
| 655 resultsFragment.appendChild(dayResults); |
| 656 } |
| 657 resultsFragment.appendChild(day); |
| 658 dayResults = createElementWithClassName('ol', 'day-results'); |
| 621 } else if (lastTime - thisTime > BROWSING_GAP_TIME) { | 659 } else if (lastTime - thisTime > BROWSING_GAP_TIME) { |
| 622 this.resultDiv_.appendChild(createElementWithClassName('div', 'gap')); | 660 if (dayResults) { |
| 661 dayResults.appendChild(createElementWithClassName('li', 'gap')); |
| 662 } |
| 623 } | 663 } |
| 624 lastTime = thisTime; | 664 lastTime = thisTime; |
| 625 | |
| 626 // Add entry. | 665 // Add entry. |
| 627 this.resultDiv_.appendChild(page.getBrowseResultDOM()); | 666 if (dayResults) { |
| 628 this.setPageRendered_(page); | 667 dayResults.appendChild(page.getResultDOM(false)); |
| 668 this.setPageRendered_(page); |
| 669 } |
| 629 } | 670 } |
| 671 // Add final dayResults element. |
| 672 if (dayResults) { |
| 673 resultsFragment.appendChild(dayResults); |
| 674 } |
| 675 this.resultDiv_.appendChild(resultsFragment); |
| 630 } | 676 } |
| 631 | 677 |
| 632 this.displaySummaryBar_(); | 678 this.displaySummaryBar_(); |
| 633 this.displayNavBar_(); | 679 this.displayNavBar_(); |
| 634 this.updateEntryAnchorWidth_(); | 680 this.updateEntryAnchorWidth_(); |
| 635 }; | 681 }; |
| 636 | 682 |
| 637 /** | 683 /** |
| 638 * Update the summary bar with descriptive text. | 684 * Update the summary bar with descriptive text. |
| 639 */ | 685 */ |
| 640 HistoryView.prototype.displaySummaryBar_ = function() { | 686 HistoryView.prototype.displaySummaryBar_ = function() { |
| 641 var searchText = this.model_.getSearchText(); | 687 var searchText = this.model_.getSearchText(); |
| 642 if (searchText != '') { | 688 if (searchText != '') { |
| 643 this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', | 689 this.summaryTd_.textContent = localStrings.getStringF('searchresultsfor', |
| 644 searchText); | 690 searchText); |
| 645 } else { | 691 } else { |
| 646 this.summaryTd_.textContent = localStrings.getString('history'); | 692 this.summaryTd_.textContent = localStrings.getString('history'); |
| 647 } | 693 } |
| 648 }; | 694 }; |
| 649 | 695 |
| 650 /** | 696 /** |
| 651 * Update the widgets related to edit mode. | |
| 652 */ | |
| 653 HistoryView.prototype.updateEditControls_ = function() { | |
| 654 // Display a button (looking like a link) to enable/disable edit mode. | |
| 655 var oldButton = this.editButtonTd_.firstChild; | |
| 656 var editMode = this.model_.getEditMode(); | |
| 657 var button = createElementWithClassName('button', 'edit-button'); | |
| 658 button.onclick = toggleEditMode; | |
| 659 button.textContent = localStrings.getString(editMode ? | |
| 660 'doneediting' : 'edithistory'); | |
| 661 this.editButtonTd_.replaceChild(button, oldButton); | |
| 662 | |
| 663 this.editingControlsDiv_.textContent = ''; | |
| 664 | |
| 665 if (editMode) { | |
| 666 // Button to delete the selected items. | |
| 667 button = document.createElement('button'); | |
| 668 button.onclick = removeItems; | |
| 669 button.textContent = localStrings.getString('removeselected'); | |
| 670 button.disabled = true; | |
| 671 this.editingControlsDiv_.appendChild(button); | |
| 672 this.removeButton_ = button; | |
| 673 | |
| 674 // Button that opens up the clear browsing data dialog. | |
| 675 button = document.createElement('button'); | |
| 676 button.onclick = openClearBrowsingData; | |
| 677 button.textContent = localStrings.getString('clearallhistory'); | |
| 678 this.editingControlsDiv_.appendChild(button); | |
| 679 | |
| 680 // Listen for clicks in the page to sync the disabled state. | |
| 681 document.addEventListener('click', this.boundUpdateRemoveButton_); | |
| 682 } else { | |
| 683 this.removeButton_ = null; | |
| 684 document.removeEventListener('click', this.boundUpdateRemoveButton_); | |
| 685 } | |
| 686 }; | |
| 687 | |
| 688 /** | |
| 689 * Updates the disabled state of the remove button when in editing mode. | |
| 690 * @param {!Event} e The click event object. | |
| 691 * @private | |
| 692 */ | |
| 693 HistoryView.prototype.updateRemoveButton_ = function(e) { | |
| 694 if (e.target.tagName != 'INPUT') | |
| 695 return; | |
| 696 | |
| 697 var anyChecked = document.querySelector('.entry input:checked') != null; | |
| 698 if (this.removeButton_) | |
| 699 this.removeButton_.disabled = !anyChecked; | |
| 700 }; | |
| 701 | |
| 702 /** | |
| 703 * Update the pagination tools. | 697 * Update the pagination tools. |
| 704 */ | 698 */ |
| 705 HistoryView.prototype.displayNavBar_ = function() { | 699 HistoryView.prototype.displayNavBar_ = function() { |
| 706 this.pageDiv_.textContent = ''; | 700 this.pageDiv_.textContent = ''; |
| 707 | 701 |
| 708 if (this.pageIndex_ > 0) { | 702 if (this.pageIndex_ > 0) { |
| 709 this.pageDiv_.appendChild( | 703 this.pageDiv_.appendChild( |
| 710 this.createPageNav_(0, localStrings.getString('newest'))); | 704 this.createPageNav_(0, localStrings.getString('newest'))); |
| 711 this.pageDiv_.appendChild( | 705 this.pageDiv_.appendChild( |
| 712 this.createPageNav_(this.pageIndex_ - 1, | 706 this.createPageNav_(this.pageIndex_ - 1, |
| (...skipping 12 matching lines...) Expand all Loading... |
| 725 /** | 719 /** |
| 726 * Make a DOM object representation of a page navigation link. | 720 * Make a DOM object representation of a page navigation link. |
| 727 * @param {number} page The page index the navigation element should link to | 721 * @param {number} page The page index the navigation element should link to |
| 728 * @param {string} name The text content of the link | 722 * @param {string} name The text content of the link |
| 729 * @return {HTMLAnchorElement} the pagination link | 723 * @return {HTMLAnchorElement} the pagination link |
| 730 */ | 724 */ |
| 731 HistoryView.prototype.createPageNav_ = function(page, name) { | 725 HistoryView.prototype.createPageNav_ = function(page, name) { |
| 732 anchor = document.createElement('a'); | 726 anchor = document.createElement('a'); |
| 733 anchor.className = 'page-navigation'; | 727 anchor.className = 'page-navigation'; |
| 734 anchor.textContent = name; | 728 anchor.textContent = name; |
| 735 var hashString = PageState.getHashString(this.model_.getEditMode(), | 729 var hashString = PageState.getHashString(this.model_.getSearchText(), page); |
| 736 this.model_.getSearchText(), page); | |
| 737 var link = 'chrome://history/' + (hashString ? '#' + hashString : ''); | 730 var link = 'chrome://history/' + (hashString ? '#' + hashString : ''); |
| 738 anchor.href = link; | 731 anchor.href = link; |
| 739 anchor.onclick = function() { | 732 anchor.onclick = function() { |
| 740 setPage(page); | 733 setPage(page); |
| 741 return false; | 734 return false; |
| 742 }; | 735 }; |
| 743 return anchor; | 736 return anchor; |
| 744 }; | 737 }; |
| 745 | 738 |
| 746 /** | 739 /** |
| 747 * Updates the CSS rule for the entry anchor. | 740 * Updates the CSS rule for the entry anchor. |
| 748 * @private | 741 * @private |
| 749 */ | 742 */ |
| 750 HistoryView.prototype.updateEntryAnchorWidth_ = function() { | 743 HistoryView.prototype.updateEntryAnchorWidth_ = function() { |
| 751 // We need to have at least on .title div to be able to calculate the | 744 // We need to have at least on .title div to be able to calculate the |
| 752 // desired width of the anchor. | 745 // desired width of the anchor. |
| 753 var titleElement = document.querySelector('.entry .title'); | 746 var titleElement = document.querySelector('.entry .title'); |
| 754 if (!titleElement) | 747 if (!titleElement) |
| 755 return; | 748 return; |
| 756 | 749 |
| 757 // Create new CSS rules and add them last to the last stylesheet. | 750 // Create new CSS rules and add them last to the last stylesheet. |
| 758 if (!this.entryAnchorRule_) { | 751 // TODO(jochen): The following code does not work due to WebKit bug #32309 |
| 759 var styleSheets = document.styleSheets; | 752 // if (!this.entryAnchorRule_) { |
| 760 var styleSheet = styleSheets[styleSheets.length - 1]; | 753 // var styleSheets = document.styleSheets; |
| 761 var rules = styleSheet.cssRules; | 754 // var styleSheet = styleSheets[styleSheets.length - 1]; |
| 762 var createRule = function(selector) { | 755 // var rules = styleSheet.cssRules; |
| 763 styleSheet.insertRule(selector + '{}', rules.length); | 756 // var createRule = function(selector) { |
| 764 return rules[rules.length - 1]; | 757 // styleSheet.insertRule(selector + '{}', rules.length); |
| 765 }; | 758 // return rules[rules.length - 1]; |
| 766 this.entryAnchorRule_ = createRule('.entry .title > a'); | 759 // }; |
| 767 // The following rule needs to be more specific to have higher priority. | 760 // this.entryAnchorRule_ = createRule('.entry .title > a'); |
| 768 this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a'); | 761 // // The following rule needs to be more specific to have higher priority. |
| 769 } | 762 // this.entryAnchorStarredRule_ = createRule('.entry .title.starred > a'); |
| 770 | 763 // } |
| 771 var anchorMaxWith = titleElement.offsetWidth; | 764 // |
| 772 this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px'; | 765 // var anchorMaxWith = titleElement.offsetWidth; |
| 773 // Adjust by the width of star plus its margin. | 766 // this.entryAnchorRule_.style.maxWidth = anchorMaxWith + 'px'; |
| 774 this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px'; | 767 // // Adjust by the width of star plus its margin. |
| 768 // this.entryAnchorStarredRule_.style.maxWidth = anchorMaxWith - 23 + 'px'; |
| 775 }; | 769 }; |
| 776 | 770 |
| 777 /////////////////////////////////////////////////////////////////////////////// | 771 /////////////////////////////////////////////////////////////////////////////// |
| 778 // State object: | 772 // State object: |
| 779 /** | 773 /** |
| 780 * An 'AJAX-history' implementation. | 774 * An 'AJAX-history' implementation. |
| 781 * @param {HistoryModel} model The model we're representing | 775 * @param {HistoryModel} model The model we're representing |
| 782 * @param {HistoryView} view The view we're representing | 776 * @param {HistoryView} view The view we're representing |
| 783 */ | 777 */ |
| 784 function PageState(model, view) { | 778 function PageState(model, view) { |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 834 return result; | 828 return result; |
| 835 }; | 829 }; |
| 836 | 830 |
| 837 /** | 831 /** |
| 838 * Set the hash to a specified state, this will create an entry in the | 832 * Set the hash to a specified state, this will create an entry in the |
| 839 * session history so the back button cycles through hash states, which | 833 * session history so the back button cycles through hash states, which |
| 840 * are then picked up by our listener. | 834 * are then picked up by our listener. |
| 841 * @param {string} term The current search string. | 835 * @param {string} term The current search string. |
| 842 * @param {string} page The page currently being viewed. | 836 * @param {string} page The page currently being viewed. |
| 843 */ | 837 */ |
| 844 PageState.prototype.setUIState = function(editMode, term, page) { | 838 PageState.prototype.setUIState = function(term, page) { |
| 845 // Make sure the form looks pretty. | 839 // Make sure the form looks pretty. |
| 846 document.forms[0].term.value = term; | 840 document.forms[0].term.value = term; |
| 847 var currentHash = this.getHashData(); | 841 var currentHash = this.getHashData(); |
| 848 if (Boolean(currentHash.e) != editMode || currentHash.q != term || | 842 if (currentHash.q != term || currentHash.p != page) { |
| 849 currentHash.p != page) { | 843 window.location.hash = PageState.getHashString(term, page); |
| 850 window.location.hash = PageState.getHashString(editMode, term, page); | |
| 851 } | 844 } |
| 852 }; | 845 }; |
| 853 | 846 |
| 854 /** | 847 /** |
| 855 * Static method to get the hash string for a specified state | 848 * Static method to get the hash string for a specified state |
| 856 * @param {string} term The current search string. | 849 * @param {string} term The current search string. |
| 857 * @param {string} page The page currently being viewed. | 850 * @param {string} page The page currently being viewed. |
| 858 * @return {string} The string to be used in a hash. | 851 * @return {string} The string to be used in a hash. |
| 859 */ | 852 */ |
| 860 PageState.getHashString = function(editMode, term, page) { | 853 PageState.getHashString = function(term, page) { |
| 861 var newHash = []; | 854 var newHash = []; |
| 862 if (editMode) { | |
| 863 newHash.push('e=1'); | |
| 864 } | |
| 865 if (term) { | 855 if (term) { |
| 866 newHash.push('q=' + encodeURIComponent(term)); | 856 newHash.push('q=' + encodeURIComponent(term)); |
| 867 } | 857 } |
| 868 if (page != undefined) { | 858 if (page != undefined) { |
| 869 newHash.push('p=' + page); | 859 newHash.push('p=' + page); |
| 870 } | 860 } |
| 871 | 861 |
| 872 return newHash.join('&'); | 862 return newHash.join('&'); |
| 873 }; | 863 }; |
| 874 | 864 |
| 875 /////////////////////////////////////////////////////////////////////////////// | 865 /////////////////////////////////////////////////////////////////////////////// |
| 876 // Document Functions: | 866 // Document Functions: |
| 877 /** | 867 /** |
| 878 * Window onload handler, sets up the page. | 868 * Window onload handler, sets up the page. |
| 879 */ | 869 */ |
| 880 function load() { | 870 function load() { |
| 881 $('term').focus(); | 871 $('term').focus(); |
| 882 | 872 |
| 883 localStrings = new LocalStrings(); | 873 localStrings = new LocalStrings(); |
| 884 historyModel = new HistoryModel(); | 874 historyModel = new HistoryModel(); |
| 885 historyView = new HistoryView(historyModel); | 875 historyView = new HistoryView(historyModel); |
| 886 pageState = new PageState(historyModel, historyView); | 876 pageState = new PageState(historyModel, historyView); |
| 887 | 877 |
| 888 // Create default view. | 878 // Create default view. |
| 889 var hashData = pageState.getHashData(); | 879 var hashData = pageState.getHashData(); |
| 890 if (Boolean(hashData.e)) { | |
| 891 historyView.toggleEditMode(); | |
| 892 } | |
| 893 historyView.setSearch(hashData.q, hashData.p); | 880 historyView.setSearch(hashData.q, hashData.p); |
| 894 | 881 |
| 895 // Add handlers to HTML elements. | 882 // Setup click handlers. |
| 896 $('history-section').onclick = function () { | 883 $('history-section').onclick = function () { |
| 897 setSearch(''); | 884 setSearch(''); |
| 898 return false; | 885 return false; |
| 899 }; | 886 }; |
| 900 $('search-form').onsubmit = function () { | 887 $('search-form').onsubmit = function () { |
| 901 setSearch(this.term.value); | 888 setSearch(this.term.value); |
| 902 return false; | 889 return false; |
| 903 }; | 890 }; |
| 891 |
| 892 $('remove-page').addEventListener('activate', function(e) { |
| 893 activePage.removeFromHistory_(); |
| 894 activePage = null; |
| 895 }); |
| 896 $('more-from-site').addEventListener('activate', function(e) { |
| 897 activePage.showMoreFromSite_(); |
| 898 activePage = null; |
| 899 }); |
| 904 } | 900 } |
| 905 | 901 |
| 906 /** | 902 /** |
| 907 * TODO(glen): Get rid of this function. | 903 * TODO(glen): Get rid of this function. |
| 908 * Set the history view to a specified page. | 904 * Set the history view to a specified page. |
| 909 * @param {String} term The string to search for | 905 * @param {String} term The string to search for |
| 910 */ | 906 */ |
| 911 function setSearch(term) { | 907 function setSearch(term) { |
| 912 if (historyView) { | 908 if (historyView) { |
| 913 historyView.setSearch(term); | 909 historyView.setSearch(term); |
| 914 } | 910 } |
| 915 } | 911 } |
| 916 | 912 |
| 917 /** | 913 /** |
| 918 * TODO(glen): Get rid of this function. | 914 * TODO(glen): Get rid of this function. |
| 919 * Set the history view to a specified page. | 915 * Set the history view to a specified page. |
| 920 * @param {number} page The page to set the view to. | 916 * @param {number} page The page to set the view to. |
| 921 */ | 917 */ |
| 922 function setPage(page) { | 918 function setPage(page) { |
| 923 if (historyView) { | 919 if (historyView) { |
| 924 historyView.setPage(page); | 920 historyView.setPage(page); |
| 925 } | 921 } |
| 926 } | 922 } |
| 927 | 923 |
| 928 /** | 924 /** |
| 929 * TODO(glen): Get rid of this function. | |
| 930 * Toggles edit mode. | |
| 931 */ | |
| 932 function toggleEditMode() { | |
| 933 if (historyView) { | |
| 934 historyView.toggleEditMode(); | |
| 935 historyView.reload(); | |
| 936 } | |
| 937 } | |
| 938 | |
| 939 /** | |
| 940 * Delete the next item in our deletion queue. | 925 * Delete the next item in our deletion queue. |
| 941 */ | 926 */ |
| 942 function deleteNextInQueue() { | 927 function deleteNextInQueue() { |
| 943 if (!deleteInFlight && deleteQueue.length) { | 928 if (deleteQueue.length > 0) { |
| 944 deleteInFlight = true; | 929 // Call the native function to remove history entries. |
| 930 // First arg is a time in seconds (passed as String) identifying the day. |
| 931 // Remaining args are URLs of history entries from that day to delete. |
| 932 var timeInSeconds = Math.floor(deleteQueue[0].date.getTime() / 1000); |
| 945 chrome.send('removeURLsOnOneDay', | 933 chrome.send('removeURLsOnOneDay', |
| 946 [String(deleteQueue[0])].concat(deleteQueue[1])); | 934 [String(timeInSeconds)].concat(deleteQueue[0].urls)); |
| 947 } | 935 } |
| 948 } | 936 } |
| 949 | 937 |
| 950 /** | 938 /** |
| 951 * Open the clear browsing data dialog. | 939 * Open the clear browsing data dialog. |
| 952 */ | 940 */ |
| 953 function openClearBrowsingData() { | 941 function openClearBrowsingData() { |
| 954 chrome.send('clearBrowsingData', []); | 942 chrome.send('clearBrowsingData', []); |
| 955 return false; | 943 return false; |
| 956 } | 944 } |
| 957 | 945 |
| 958 /** | 946 /** |
| 947 * Queue a set of URLs from the same day for deletion. |
| 948 * @param {Date} date A date indicating the day the URLs were visited. |
| 949 * @param {Array} urls Array of URLs from the same day to be deleted. |
| 950 * @param {Function} opt_callback An optional callback to be executed when |
| 951 * the deletion is complete. |
| 952 */ |
| 953 function queueURLsForDeletion(date, urls, opt_callback) { |
| 954 deleteQueue.push({ 'date': date, 'urls': urls, 'callback': opt_callback }); |
| 955 } |
| 956 |
| 957 function reloadHistory() { |
| 958 historyView.reload(); |
| 959 } |
| 960 |
| 961 /** |
| 959 * Collect IDs from checked checkboxes and send to Chrome for deletion. | 962 * Collect IDs from checked checkboxes and send to Chrome for deletion. |
| 960 */ | 963 */ |
| 961 function removeItems() { | 964 function removeItems() { |
| 962 var checkboxes = document.getElementsByTagName('input'); | 965 var checked = document.querySelectorAll( |
| 963 var ids = []; | 966 'input[type=checkbox]:checked:not([disabled])'); |
| 967 var urls = []; |
| 964 var disabledItems = []; | 968 var disabledItems = []; |
| 965 var queue = []; | 969 var queue = []; |
| 966 var date = new Date(); | 970 var date = new Date(); |
| 967 for (var i = 0; i < checkboxes.length; i++) { | 971 |
| 968 if (checkboxes[i].type == 'checkbox' && checkboxes[i].checked && | 972 for (var i = 0; i < checked.length; i++) { |
| 969 !checkboxes[i].disabled) { | 973 var checkbox = checked[i]; |
| 970 var cbDate = new Date(checkboxes[i].time); | 974 var cbDate = new Date(checkbox.time); |
| 971 if (date.getFullYear() != cbDate.getFullYear() || | 975 if (date.getFullYear() != cbDate.getFullYear() || |
| 972 date.getMonth() != cbDate.getMonth() || | 976 date.getMonth() != cbDate.getMonth() || |
| 973 date.getDate() != cbDate.getDate()) { | 977 date.getDate() != cbDate.getDate()) { |
| 974 if (ids.length > 0) { | 978 if (urls.length > 0) { |
| 975 queue.push(date.valueOf() / 1000); | 979 queue.push([date, urls]); |
| 976 queue.push(ids); | |
| 977 } | |
| 978 ids = []; | |
| 979 date = cbDate; | |
| 980 } | 980 } |
| 981 var link = $('id-' + checkboxes[i].name); | 981 urls = []; |
| 982 checkboxes[i].disabled = true; | 982 date = cbDate; |
| 983 link.style.textDecoration = 'line-through'; | |
| 984 disabledItems.push(checkboxes[i]); | |
| 985 ids.push(link.href); | |
| 986 } | 983 } |
| 984 var link = checkbox.parentNode.parentNode.querySelector('a'); |
| 985 checkbox.disabled = true; |
| 986 link.classList.add('to-be-removed'); |
| 987 disabledItems.push(checkbox); |
| 988 urls.push(link.href); |
| 987 } | 989 } |
| 988 if (ids.length > 0) { | 990 if (urls.length > 0) { |
| 989 queue.push(date.valueOf() / 1000); | 991 queue.push([date, urls]); |
| 990 queue.push(ids); | |
| 991 } | 992 } |
| 992 if (queue.length > 0) { | 993 if (checked.length > 0 && confirm(localStrings.getString('deletewarning'))) { |
| 993 if (confirm(localStrings.getString('deletewarning'))) { | 994 for (var i = 0; i < queue.length; i++) { |
| 994 deleteQueue = deleteQueue.concat(queue); | 995 // Reload the page when the final entry has been deleted. |
| 995 deleteNextInQueue(); | 996 var callback = i == 0 ? reloadHistory : null; |
| 996 historyView.removeButton_.disabled = true; | 997 |
| 997 } else { | 998 queueURLsForDeletion(queue[i][0], queue[i][1], callback); |
| 998 // If the remove is cancelled, return the checkboxes to their | 999 } |
| 999 // enabled, non-line-through state. | 1000 deleteNextInQueue(); |
| 1000 for (var i = 0; i < disabledItems.length; i++) { | 1001 } else { |
| 1001 var link = $('id-' + disabledItems[i].name); | 1002 // If the remove is cancelled, return the checkboxes to their |
| 1002 disabledItems[i].disabled = false; | 1003 // enabled, non-line-through state. |
| 1003 link.style.textDecoration = ''; | 1004 for (var i = 0; i < disabledItems.length; i++) { |
| 1004 } | 1005 var checkbox = disabledItems[i]; |
| 1006 var link = checkbox.parentNode.parentNode.querySelector('a'); |
| 1007 checkbox.disabled = false; |
| 1008 link.classList.remove('to-be-removed'); |
| 1005 } | 1009 } |
| 1006 } | 1010 } |
| 1007 return false; | 1011 return false; |
| 1008 } | 1012 } |
| 1009 | 1013 |
| 1010 /** | 1014 /** |
| 1011 * Toggle state of checkbox and handle Shift modifier. | 1015 * Toggle state of checkbox and handle Shift modifier. |
| 1012 */ | 1016 */ |
| 1013 function checkboxClicked(event) { | 1017 function checkboxClicked(event) { |
| 1018 var id = Number(this.id.slice("checkbox-".length)); |
| 1014 if (event.shiftKey && (selectionAnchor != -1)) { | 1019 if (event.shiftKey && (selectionAnchor != -1)) { |
| 1015 var checked = this.checked; | 1020 var checked = this.checked; |
| 1016 // Set all checkboxes from the anchor up to the clicked checkbox to the | 1021 // Set all checkboxes from the anchor up to the clicked checkbox to the |
| 1017 // state of the clicked one. | 1022 // state of the clicked one. |
| 1018 var begin = Math.min(this.name, selectionAnchor); | 1023 var begin = Math.min(id, selectionAnchor); |
| 1019 var end = Math.max(this.name, selectionAnchor); | 1024 var end = Math.max(id, selectionAnchor); |
| 1020 for (var i = begin; i <= end; i++) { | 1025 for (var i = begin; i <= end; i++) { |
| 1021 id2checkbox[i].checked = checked; | 1026 var checkbox = document.querySelector('#checkbox-' + i); |
| 1027 if (checkbox) |
| 1028 checkbox.checked = checked; |
| 1022 } | 1029 } |
| 1023 } | 1030 } |
| 1024 selectionAnchor = this.name; | 1031 selectionAnchor = id; |
| 1025 this.focus(); | 1032 |
| 1033 historyView.updateRemoveButton(); |
| 1034 } |
| 1035 |
| 1036 function entryBoxMousedown(event) { |
| 1037 // Prevent text selection when shift-clicking to select multiple entries. |
| 1038 if (event.shiftKey) { |
| 1039 event.preventDefault(); |
| 1040 } |
| 1041 } |
| 1042 |
| 1043 function removeNode(node) { |
| 1044 node.classList.add('fade-out'); // Trigger CSS fade out animation. |
| 1045 |
| 1046 // Delete the node when the animation is complete. |
| 1047 node.addEventListener('webkitTransitionEnd', function() { |
| 1048 node.parentNode.removeChild(node); |
| 1049 }); |
| 1050 } |
| 1051 |
| 1052 /** |
| 1053 * Removes a single entry from the view. Also removes gaps before and after |
| 1054 * entry if necessary. |
| 1055 */ |
| 1056 function removeEntryFromView(entry) { |
| 1057 var nextEntry = entry.nextSibling; |
| 1058 var previousEntry = entry.previousSibling; |
| 1059 |
| 1060 removeNode(entry); |
| 1061 |
| 1062 // if there is no previous entry, and the next entry is a gap, remove it |
| 1063 if (!previousEntry && nextEntry && nextEntry.className == 'gap') { |
| 1064 removeNode(nextEntry); |
| 1065 } |
| 1066 |
| 1067 // if there is no next entry, and the previous entry is a gap, remove it |
| 1068 if (!nextEntry && previousEntry && previousEntry.className == 'gap') { |
| 1069 removeNode(previousEntry); |
| 1070 } |
| 1071 |
| 1072 // if both the next and previous entries are gaps, remove one |
| 1073 if (nextEntry && nextEntry.className == 'gap' && |
| 1074 previousEntry && previousEntry.className == 'gap') { |
| 1075 removeNode(nextEntry); |
| 1076 } |
| 1026 } | 1077 } |
| 1027 | 1078 |
| 1028 /////////////////////////////////////////////////////////////////////////////// | 1079 /////////////////////////////////////////////////////////////////////////////// |
| 1029 // Chrome callbacks: | 1080 // Chrome callbacks: |
| 1030 /** | 1081 /** |
| 1031 * Our history system calls this function with results from searches. | 1082 * Our history system calls this function with results from searches. |
| 1032 */ | 1083 */ |
| 1033 function historyResult(info, results) { | 1084 function historyResult(info, results) { |
| 1034 historyModel.addResults(info, results); | 1085 historyModel.addResults(info, results); |
| 1035 } | 1086 } |
| 1036 | 1087 |
| 1037 /** | 1088 /** |
| 1038 * Our history system calls this function when a deletion has finished. | 1089 * Our history system calls this function when a deletion has finished. |
| 1039 */ | 1090 */ |
| 1040 function deleteComplete() { | 1091 function deleteComplete() { |
| 1041 window.console.log('Delete complete'); | 1092 if (deleteQueue.length > 0) { |
| 1042 deleteInFlight = false; | 1093 // Remove the successfully deleted entry from the queue. |
| 1043 if (deleteQueue.length > 2) { | 1094 if (deleteQueue[0].callback) |
| 1044 deleteQueue = deleteQueue.slice(2); | 1095 deleteQueue[0].callback.apply(); |
| 1096 deleteQueue.splice(0, 1); |
| 1045 deleteNextInQueue(); | 1097 deleteNextInQueue(); |
| 1046 } else { | 1098 } else { |
| 1047 deleteQueue = []; | 1099 console.error('Received deleteComplete but queue is empty.'); |
| 1048 } | 1100 } |
| 1049 } | 1101 } |
| 1050 | 1102 |
| 1051 /** | 1103 /** |
| 1052 * Our history system calls this function if a delete is not ready (e.g. | 1104 * Our history system calls this function if a delete is not ready (e.g. |
| 1053 * another delete is in-progress). | 1105 * another delete is in-progress). |
| 1054 */ | 1106 */ |
| 1055 function deleteFailed() { | 1107 function deleteFailed() { |
| 1056 window.console.log('Delete failed'); | 1108 window.console.log('Delete failed'); |
| 1109 |
| 1057 // The deletion failed - try again later. | 1110 // The deletion failed - try again later. |
| 1058 deleteInFlight = false; | 1111 // TODO(dubroy): We should probably give up at some point. |
| 1059 setTimeout(deleteNextInQueue, 500); | 1112 setTimeout(deleteNextInQueue, 500); |
| 1060 } | 1113 } |
| 1061 | 1114 |
| 1062 /** | 1115 /** |
| 1063 * We're called when something is deleted (either by us or by someone | 1116 * Called when the history is deleted by someone else. |
| 1064 * else). | |
| 1065 */ | 1117 */ |
| 1066 function historyDeleted() { | 1118 function historyDeleted() { |
| 1067 window.console.log('History deleted'); | 1119 window.console.log('History deleted'); |
| 1068 var anyChecked = document.querySelector('.entry input:checked') != null; | 1120 var anyChecked = document.querySelector('.entry input:checked') != null; |
| 1069 if (!(historyView.getEditMode() && anyChecked)) | 1121 // Reload the page, unless the user has any items checked. |
| 1122 // TODO(dubroy): We should just reload the page & restore the checked items. |
| 1123 if (!anyChecked) |
| 1070 historyView.reload(); | 1124 historyView.reload(); |
| 1071 } | 1125 } |
| 1072 | 1126 |
| 1127 // Add handlers to HTML elements. |
| 1073 document.addEventListener('DOMContentLoaded', load); | 1128 document.addEventListener('DOMContentLoaded', load); |
| 1129 |
| 1130 // This event lets us enable and disable menu items before the menu is shown. |
| 1131 document.addEventListener('canExecute', function(e) { |
| 1132 e.canExecute = true; |
| 1133 }); |
| OLD | NEW |