OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 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 <include src="../uber/uber_utils.js"> | 5 <include src="../uber/uber_utils.js"> |
6 | 6 |
7 /////////////////////////////////////////////////////////////////////////////// | 7 /////////////////////////////////////////////////////////////////////////////// |
8 // Globals: | 8 // Globals: |
9 /** @const */ var RESULTS_PER_PAGE = 150; | 9 /** @const */ var RESULTS_PER_PAGE = 150; |
10 | 10 |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
63 this.dateShort = result.dateShort || ''; | 63 this.dateShort = result.dateShort || ''; |
64 | 64 |
65 // Whether this is the continuation of a previous day. | 65 // Whether this is the continuation of a previous day. |
66 this.continued = continued; | 66 this.continued = continued; |
67 } | 67 } |
68 | 68 |
69 // Visit, public: ------------------------------------------------------------- | 69 // Visit, public: ------------------------------------------------------------- |
70 | 70 |
71 /** | 71 /** |
72 * Returns a dom structure for a browse page result or a search page result. | 72 * Returns a dom structure for a browse page result or a search page result. |
73 * @param {boolean} searchResultFlag Indicates whether the result is a search | 73 * @param {Object} propertyBag A bag of configuration properties, false by |
74 * result or not. | 74 * default: |
| 75 * <ul> |
| 76 * <li>isSearchResult: Whether or not the result is a search result.</li> |
| 77 * <li>addTitleFavicon: Whether or not the favicon should be added.</li> |
| 78 * </ul> |
75 * @return {Node} A DOM node to represent the history entry or search result. | 79 * @return {Node} A DOM node to represent the history entry or search result. |
76 */ | 80 */ |
77 Visit.prototype.getResultDOM = function(searchResultFlag) { | 81 Visit.prototype.getResultDOM = function(propertyBag) { |
| 82 var isSearchResult = propertyBag.isSearchResult || false; |
| 83 var addTitleFavicon = propertyBag.addTitleFavicon || false; |
78 var node = createElementWithClassName('li', 'entry'); | 84 var node = createElementWithClassName('li', 'entry'); |
79 var time = createElementWithClassName('div', 'time'); | 85 var time = createElementWithClassName('div', 'time'); |
80 var entryBox = createElementWithClassName('label', 'entry-box'); | 86 var entryBox = createElementWithClassName('label', 'entry-box'); |
81 var domain = createElementWithClassName('div', 'domain'); | 87 var domain = createElementWithClassName('div', 'domain'); |
82 | 88 |
83 var dropDown = createElementWithClassName('button', 'drop-down'); | 89 var dropDown = createElementWithClassName('button', 'drop-down'); |
84 dropDown.value = 'Open action menu'; | 90 dropDown.value = 'Open action menu'; |
85 dropDown.title = loadTimeData.getString('actionMenuDescription'); | 91 dropDown.title = loadTimeData.getString('actionMenuDescription'); |
86 dropDown.setAttribute('menu', '#action-menu'); | 92 dropDown.setAttribute('menu', '#action-menu'); |
87 cr.ui.decorate(dropDown, MenuButton); | 93 cr.ui.decorate(dropDown, MenuButton); |
(...skipping 18 matching lines...) Expand all Loading... |
106 | 112 |
107 domain.textContent = this.getDomainFromURL_(this.url_); | 113 domain.textContent = this.getDomainFromURL_(this.url_); |
108 | 114 |
109 // Clicking anywhere in the entryBox will check/uncheck the checkbox. | 115 // Clicking anywhere in the entryBox will check/uncheck the checkbox. |
110 entryBox.setAttribute('for', checkbox.id); | 116 entryBox.setAttribute('for', checkbox.id); |
111 entryBox.addEventListener('mousedown', entryBoxMousedown); | 117 entryBox.addEventListener('mousedown', entryBoxMousedown); |
112 | 118 |
113 // Prevent clicks on the drop down from affecting the checkbox. | 119 // Prevent clicks on the drop down from affecting the checkbox. |
114 dropDown.addEventListener('click', function(e) { e.preventDefault(); }); | 120 dropDown.addEventListener('click', function(e) { e.preventDefault(); }); |
115 | 121 |
116 // We use a wrapper div so that the entry contents will be shinkwrapped. | 122 // We use a wrapper div so that the entry contents will be shrinkwrapped. |
117 entryBox.appendChild(time); | 123 entryBox.appendChild(time); |
118 entryBox.appendChild(this.getTitleDOM_()); | 124 entryBox.appendChild(this.getTitleDOM_(addTitleFavicon)); |
119 entryBox.appendChild(domain); | 125 entryBox.appendChild(domain); |
120 entryBox.appendChild(dropDown); | 126 entryBox.appendChild(dropDown); |
121 | 127 |
122 // Let the entryBox be styled appropriately when it contains keyboard focus. | 128 // Let the entryBox be styled appropriately when it contains keyboard focus. |
123 entryBox.addEventListener('focus', function() { | 129 entryBox.addEventListener('focus', function() { |
124 this.classList.add('contains-focus'); | 130 this.classList.add('contains-focus'); |
125 }, true); | 131 }, true); |
126 entryBox.addEventListener('blur', function() { | 132 entryBox.addEventListener('blur', function() { |
127 this.classList.remove('contains-focus'); | 133 this.classList.remove('contains-focus'); |
128 }, true); | 134 }, true); |
129 | 135 |
130 node.appendChild(entryBox); | 136 node.appendChild(entryBox); |
131 | 137 |
132 if (searchResultFlag) { | 138 if (isSearchResult) { |
133 time.appendChild(document.createTextNode(this.dateShort)); | 139 time.appendChild(document.createTextNode(this.dateShort)); |
134 var snippet = createElementWithClassName('div', 'snippet'); | 140 var snippet = createElementWithClassName('div', 'snippet'); |
135 this.addHighlightedText_(snippet, | 141 this.addHighlightedText_(snippet, |
136 this.snippet_, | 142 this.snippet_, |
137 this.model_.getSearchText()); | 143 this.model_.getSearchText()); |
138 node.appendChild(snippet); | 144 node.appendChild(snippet); |
139 } else { | 145 } else { |
140 time.appendChild(document.createTextNode(this.dateTimeOfDay)); | 146 time.appendChild(document.createTextNode(this.dateTimeOfDay)); |
141 } | 147 } |
142 | 148 |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
184 var b = document.createElement('b'); | 190 var b = document.createElement('b'); |
185 b.textContent = content.substring(match.index, i); | 191 b.textContent = content.substring(match.index, i); |
186 node.appendChild(b); | 192 node.appendChild(b); |
187 } | 193 } |
188 } | 194 } |
189 if (i < content.length) | 195 if (i < content.length) |
190 node.appendChild(document.createTextNode(content.slice(i))); | 196 node.appendChild(document.createTextNode(content.slice(i))); |
191 }; | 197 }; |
192 | 198 |
193 /** | 199 /** |
194 * @return {DOMObject} DOM representation for the title block. | 200 * Returns the DOM element containing a link on the title of the URL for the |
| 201 * current visit. Optionally sets the favicon as well. |
| 202 * @param {boolean} addFavicon Whether to add a favicon or not. |
| 203 * @return {Element} DOM representation for the title block. |
195 * @private | 204 * @private |
196 */ | 205 */ |
197 Visit.prototype.getTitleDOM_ = function() { | 206 Visit.prototype.getTitleDOM_ = function(addFavicon) { |
198 var node = createElementWithClassName('div', 'title'); | 207 var node = createElementWithClassName('div', 'title'); |
199 node.style.backgroundImage = getFaviconImageSet(this.url_); | 208 if (addFavicon) { |
200 node.style.backgroundSize = '16px'; | 209 node.style.backgroundImage = getFaviconImageSet(this.url_); |
| 210 node.style.backgroundSize = '16px'; |
| 211 } |
201 | 212 |
202 var link = document.createElement('a'); | 213 var link = document.createElement('a'); |
203 link.href = this.url_; | 214 link.href = this.url_; |
204 link.id = 'id-' + this.id_; | 215 link.id = 'id-' + this.id_; |
205 link.target = '_top'; | 216 link.target = '_top'; |
206 | 217 |
207 // Add a tooltip, since it might be ellipsized. | 218 // Add a tooltip, since it might be ellipsized. |
208 // TODO(dubroy): Find a way to show the tooltip only when necessary. | 219 // TODO(dubroy): Find a way to show the tooltip only when necessary. |
209 link.title = this.title_; | 220 link.title = this.title_; |
210 | 221 |
211 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); | 222 this.addHighlightedText_(link, this.title_, this.model_.getSearchText()); |
212 node.appendChild(link); | 223 node.appendChild(link); |
213 | 224 |
214 if (this.starred_) { | 225 if (this.starred_) { |
215 var star = createElementWithClassName('div', 'starred'); | 226 var star = createElementWithClassName('div', 'starred'); |
216 node.appendChild(star); | 227 node.appendChild(star); |
217 star.addEventListener('click', this.starClicked_.bind(this)); | 228 star.addEventListener('click', this.starClicked_.bind(this)); |
218 } | 229 } |
219 | 230 |
220 return node; | 231 return node; |
221 }; | 232 }; |
222 | 233 |
223 /** | 234 /** |
| 235 * Set the favicon for an element. |
| 236 * @param {Element} el The DOM element to which to add the icon. |
| 237 * @private |
| 238 */ |
| 239 Visit.prototype.addFaviconToElement_ = function(el) { |
| 240 el.style.backgroundImage = getFaviconImageSet(this.url_); |
| 241 }; |
| 242 |
| 243 /** |
224 * Launch a search for more history entries from the same domain. | 244 * Launch a search for more history entries from the same domain. |
225 * @private | 245 * @private |
226 */ | 246 */ |
227 Visit.prototype.showMoreFromSite_ = function() { | 247 Visit.prototype.showMoreFromSite_ = function() { |
228 setSearch(this.getDomainFromURL_(this.url_)); | 248 setSearch(this.getDomainFromURL_(this.url_)); |
229 }; | 249 }; |
230 | 250 |
231 /** | 251 /** |
232 * Remove a single entry from the history. | 252 * Remove a single entry from the history. |
233 * @private | 253 * @private |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
301 * up an initial view, use #requestPage otherwise. | 321 * up an initial view, use #requestPage otherwise. |
302 */ | 322 */ |
303 HistoryModel.prototype.setSearchText = function(searchText, opt_page) { | 323 HistoryModel.prototype.setSearchText = function(searchText, opt_page) { |
304 this.clearModel_(); | 324 this.clearModel_(); |
305 this.searchText_ = searchText; | 325 this.searchText_ = searchText; |
306 this.requestedPage_ = opt_page ? opt_page : 0; | 326 this.requestedPage_ = opt_page ? opt_page : 0; |
307 this.queryHistory_(); | 327 this.queryHistory_(); |
308 }; | 328 }; |
309 | 329 |
310 /** | 330 /** |
| 331 * Clear the search text. |
| 332 */ |
| 333 HistoryModel.prototype.clearSearchText = function() { |
| 334 this.searchText_ = ''; |
| 335 }; |
| 336 |
| 337 /** |
311 * Reload our model with the current parameters. | 338 * Reload our model with the current parameters. |
312 */ | 339 */ |
313 HistoryModel.prototype.reload = function() { | 340 HistoryModel.prototype.reload = function() { |
| 341 // Save user-visible state, clear the model, and restore the state. |
314 var search = this.searchText_; | 342 var search = this.searchText_; |
315 var page = this.requestedPage_; | 343 var page = this.requestedPage_; |
| 344 var groupByDomain = this.groupByDomain_; |
| 345 |
316 this.clearModel_(); | 346 this.clearModel_(); |
317 this.searchText_ = search; | 347 this.searchText_ = search; |
318 this.requestedPage_ = page; | 348 this.requestedPage_ = page; |
| 349 this.groupByDomain_ = groupByDomain; |
319 this.queryHistory_(); | 350 this.queryHistory_(); |
320 }; | 351 }; |
321 | 352 |
322 /** | 353 /** |
323 * @return {string} The current search text. | 354 * @return {string} The current search text. |
324 */ | 355 */ |
325 HistoryModel.prototype.getSearchText = function() { | 356 HistoryModel.prototype.getSearchText = function() { |
326 return this.searchText_; | 357 return this.searchText_; |
327 }; | 358 }; |
328 | 359 |
(...skipping 12 matching lines...) Expand all Loading... |
341 * Receiver for history query. | 372 * Receiver for history query. |
342 * @param {Object} info An object containing information about the query. | 373 * @param {Object} info An object containing information about the query. |
343 * @param {Array} results A list of results. | 374 * @param {Array} results A list of results. |
344 */ | 375 */ |
345 HistoryModel.prototype.addResults = function(info, results) { | 376 HistoryModel.prototype.addResults = function(info, results) { |
346 $('loading-spinner').hidden = true; | 377 $('loading-spinner').hidden = true; |
347 this.inFlight_ = false; | 378 this.inFlight_ = false; |
348 this.isQueryFinished_ = info.finished; | 379 this.isQueryFinished_ = info.finished; |
349 this.queryCursor_ = info.cursor; | 380 this.queryCursor_ = info.cursor; |
350 | 381 |
351 // If there are no results, or they're not for the current search term, | 382 // If the results are not for the current search term there's nothing more |
352 // there's nothing more to do. | 383 // to do. |
353 if (!results || !results.length || info.term != this.searchText_) | 384 if (info.term != this.searchText_) |
354 return; | 385 return; |
355 | 386 |
356 // If necessary, sort the results from newest to oldest. | 387 // If necessary, sort the results from newest to oldest. |
357 if (!results.sorted) | 388 if (!results.sorted) |
358 results.sort(function(a, b) { return b.time - a.time; }); | 389 results.sort(function(a, b) { return b.time - a.time; }); |
359 | 390 |
360 var lastVisit = this.visits_.slice(-1)[0]; | 391 var lastVisit = this.visits_.slice(-1)[0]; |
361 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null; | 392 var lastDay = lastVisit ? lastVisit.dateRelativeDay : null; |
362 | 393 |
363 for (var i = 0, thisResult; thisResult = results[i]; i++) { | 394 for (var i = 0, thisResult; thisResult = results[i]; i++) { |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
408 | 439 |
409 // HistoryModel, Private: ----------------------------------------------------- | 440 // HistoryModel, Private: ----------------------------------------------------- |
410 | 441 |
411 /** | 442 /** |
412 * Clear the history model. | 443 * Clear the history model. |
413 * @private | 444 * @private |
414 */ | 445 */ |
415 HistoryModel.prototype.clearModel_ = function() { | 446 HistoryModel.prototype.clearModel_ = function() { |
416 this.inFlight_ = false; // Whether a query is inflight. | 447 this.inFlight_ = false; // Whether a query is inflight. |
417 this.searchText_ = ''; | 448 this.searchText_ = ''; |
| 449 // Flag to show that the results are grouped by domain or not. |
| 450 this.groupByDomain_ = false; |
418 | 451 |
419 this.visits_ = []; // Date-sorted list of visits (most recent first). | 452 this.visits_ = []; // Date-sorted list of visits (most recent first). |
420 this.last_id_ = 0; | 453 this.last_id_ = 0; |
421 selectionAnchor = -1; | 454 selectionAnchor = -1; |
422 | 455 |
423 // The page that the view wants to see - we only fetch slightly past this | 456 // The page that the view wants to see - we only fetch slightly past this |
424 // point. If the view requests a page that we don't have data for, we try | 457 // point. If the view requests a page that we don't have data for, we try |
425 // to fetch it and call back when we're done. | 458 // to fetch it and call back when we're done. |
426 this.requestedPage_ = 0; | 459 this.requestedPage_ = 0; |
427 | 460 |
(...skipping 10 matching lines...) Expand all Loading... |
438 // visit to a URL on any day. | 471 // visit to a URL on any day. |
439 this.urlsFromLastSeenDay_ = {}; | 472 this.urlsFromLastSeenDay_ = {}; |
440 | 473 |
441 if (this.view_) | 474 if (this.view_) |
442 this.view_.clear_(); | 475 this.view_.clear_(); |
443 }; | 476 }; |
444 | 477 |
445 /** | 478 /** |
446 * Figure out if we need to do more queries to fill the currently requested | 479 * Figure out if we need to do more queries to fill the currently requested |
447 * page. If we think we can fill the page, call the view and let it know | 480 * page. If we think we can fill the page, call the view and let it know |
448 * we're ready to show something. | 481 * we're ready to show something. This only applies to the daily time-based |
| 482 * view. |
449 * @private | 483 * @private |
450 */ | 484 */ |
451 HistoryModel.prototype.updateSearch_ = function() { | 485 HistoryModel.prototype.updateSearch_ = function() { |
452 var doneLoading = | 486 var doneLoading = this.isQueryFinished_ || |
453 this.canFillPage_(this.requestedPage_) || this.isQueryFinished_; | 487 this.canFillPage_(this.requestedPage_); |
454 | 488 |
455 // Try to fetch more results if the current page isn't full. | 489 // Try to fetch more results if the results are not grouped by domain and |
456 if (!doneLoading && !this.inFlight_) | 490 // the current page isn't full. |
| 491 if (!this.groupByDomain_ && !doneLoading && !this.inFlight_) |
457 this.queryHistory_(); | 492 this.queryHistory_(); |
458 | 493 |
459 // If we have any data for the requested page, show it. | 494 // Show the result or a message if no results were returned. |
460 if (this.changed && this.haveDataForPage_(this.requestedPage_)) { | 495 this.view_.onModelReady(); |
461 this.view_.onModelReady(); | |
462 this.changed = false; | |
463 } | |
464 }; | 496 }; |
465 | 497 |
466 /** | 498 /** |
467 * Query for history, either for a search or time-based browsing. | 499 * Query for history, either for a search or time-based browsing. |
468 * @private | 500 * @private |
469 */ | 501 */ |
470 HistoryModel.prototype.queryHistory_ = function() { | 502 HistoryModel.prototype.queryHistory_ = function() { |
471 var endTime = 0; | 503 var endTime = 0; |
472 | 504 if (!this.getGroupByDomain()) { |
473 // If there are already some visits, pick up the previous query where it | 505 // Do the time-based search. |
474 // left off. | 506 // If there are already some visits, pick up the previous query where it |
475 if (this.visits_.length > 0) { | 507 // left off. |
476 var lastVisit = this.visits_.slice(-1)[0]; | 508 if (this.visits_.length > 0) { |
477 endTime = lastVisit.date.getTime(); | 509 var lastVisit = this.visits_.slice(-1)[0]; |
478 cursor = this.queryCursor_; | 510 endTime = lastVisit.date.getTime(); |
| 511 cursor = this.queryCursor_; |
| 512 } |
479 } | 513 } |
480 | |
481 $('loading-spinner').hidden = false; | 514 $('loading-spinner').hidden = false; |
482 this.inFlight_ = true; | 515 this.inFlight_ = true; |
483 chrome.send('queryHistory', | 516 chrome.send('queryHistory', |
484 [this.searchText_, endTime, this.queryCursor_, RESULTS_PER_PAGE]); | 517 [this.searchText_, endTime, this.queryCursor_, |
| 518 RESULTS_PER_PAGE]); |
485 }; | 519 }; |
486 | 520 |
487 /** | 521 /** |
488 * Check to see if we have data for the given page. | 522 * Check to see if we have data for the given page. |
489 * @param {number} page The page number. | 523 * @param {number} page The page number. |
490 * @return {boolean} Whether we have any data for the given page. | 524 * @return {boolean} Whether we have any data for the given page. |
491 * @private | 525 * @private |
492 */ | 526 */ |
493 HistoryModel.prototype.haveDataForPage_ = function(page) { | 527 HistoryModel.prototype.haveDataForPage_ = function(page) { |
494 return (page * RESULTS_PER_PAGE < this.getSize()); | 528 return (page * RESULTS_PER_PAGE < this.getSize()); |
495 }; | 529 }; |
496 | 530 |
497 /** | 531 /** |
498 * Check to see if we have data to fill the given page. | 532 * Check to see if we have data to fill the given page. |
499 * @param {number} page The page number. | 533 * @param {number} page The page number. |
500 * @return {boolean} Whether we have data to fill the page. | 534 * @return {boolean} Whether we have data to fill the page. |
501 * @private | 535 * @private |
502 */ | 536 */ |
503 HistoryModel.prototype.canFillPage_ = function(page) { | 537 HistoryModel.prototype.canFillPage_ = function(page) { |
504 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); | 538 return ((page + 1) * RESULTS_PER_PAGE <= this.getSize()); |
505 }; | 539 }; |
506 | 540 |
| 541 /** |
| 542 * Enables or disables grouping by domain. |
| 543 * @param {boolean} groupByDomain New groupByDomain_ value. |
| 544 */ |
| 545 HistoryModel.prototype.setGroupByDomain = function(groupByDomain) { |
| 546 this.groupByDomain_ = groupByDomain; |
| 547 }; |
| 548 |
| 549 /** |
| 550 * Gets whether we are grouped by domain. |
| 551 * @return {boolean} Whether the results are grouped by domain. |
| 552 */ |
| 553 HistoryModel.prototype.getGroupByDomain = function() { |
| 554 return this.groupByDomain_; |
| 555 }; |
| 556 |
507 /////////////////////////////////////////////////////////////////////////////// | 557 /////////////////////////////////////////////////////////////////////////////// |
508 // HistoryView: | 558 // HistoryView: |
509 | 559 |
510 /** | 560 /** |
511 * Functions and state for populating the page with HTML. This should one-day | 561 * Functions and state for populating the page with HTML. This should one-day |
512 * contain the view and use event handlers, rather than pushing HTML out and | 562 * contain the view and use event handlers, rather than pushing HTML out and |
513 * getting called externally. | 563 * getting called externally. |
514 * @param {HistoryModel} model The model backing this view. | 564 * @param {HistoryModel} model The model backing this view. |
515 * @constructor | 565 * @constructor |
516 */ | 566 */ |
(...skipping 18 matching lines...) Expand all Loading... |
535 // Add handlers for the page navigation buttons at the bottom. | 585 // Add handlers for the page navigation buttons at the bottom. |
536 $('newest-button').addEventListener('click', function() { | 586 $('newest-button').addEventListener('click', function() { |
537 self.setPage(0); | 587 self.setPage(0); |
538 }); | 588 }); |
539 $('newer-button').addEventListener('click', function() { | 589 $('newer-button').addEventListener('click', function() { |
540 self.setPage(self.pageIndex_ - 1); | 590 self.setPage(self.pageIndex_ - 1); |
541 }); | 591 }); |
542 $('older-button').addEventListener('click', function() { | 592 $('older-button').addEventListener('click', function() { |
543 self.setPage(self.pageIndex_ + 1); | 593 self.setPage(self.pageIndex_ + 1); |
544 }); | 594 }); |
| 595 |
| 596 $('display-filter-sites').addEventListener('click', function(e) { |
| 597 self.setGroupByDomain($('display-filter-sites').checked); |
| 598 }); |
545 } | 599 } |
546 | 600 |
547 // HistoryView, public: ------------------------------------------------------- | 601 // HistoryView, public: ------------------------------------------------------- |
548 /** | 602 /** |
549 * Do a search and optionally view a certain page. | 603 * Do a search and optionally view a certain page. |
550 * @param {string} term The string to search for. | 604 * @param {string} term The string to search for. |
551 * @param {number} opt_page The page we wish to view, only use this for | 605 * @param {number} opt_page The page we wish to view, only use this for |
552 * setting up initial views, as this triggers a search. | 606 * setting up initial views, as this triggers a search. |
553 */ | 607 */ |
554 HistoryView.prototype.setSearch = function(term, opt_page) { | 608 HistoryView.prototype.setSearch = function(term, opt_page) { |
555 this.pageIndex_ = parseInt(opt_page || 0, 10); | 609 this.pageIndex_ = parseInt(opt_page || 0, 10); |
556 window.scrollTo(0, 0); | 610 window.scrollTo(0, 0); |
557 this.model_.setSearchText(term, this.pageIndex_); | 611 this.model_.setSearchText(term, this.pageIndex_); |
558 pageState.setUIState(term, this.pageIndex_); | 612 pageState.setUIState(term, this.pageIndex_, this.model_.getGroupByDomain()); |
559 }; | 613 }; |
560 | 614 |
561 /** | 615 /** |
| 616 * Enable or disable results as being grouped by domain. |
| 617 * @param {boolean} groupedByDomain Whether to group by domain or not. |
| 618 */ |
| 619 HistoryView.prototype.setGroupByDomain = function(groupedByDomain) { |
| 620 // Group by domain is not currently supported for search results, so reset |
| 621 // the search term if there was one. |
| 622 this.model_.clearSearchText(); |
| 623 this.model_.setGroupByDomain(groupedByDomain); |
| 624 this.model_.reload(); |
| 625 pageState.setUIState(this.model_.getSearchText(), |
| 626 this.pageIndex_, |
| 627 this.model_.getGroupByDomain()); |
| 628 }; |
| 629 |
| 630 /** |
562 * Reload the current view. | 631 * Reload the current view. |
563 */ | 632 */ |
564 HistoryView.prototype.reload = function() { | 633 HistoryView.prototype.reload = function() { |
565 this.model_.reload(); | 634 this.model_.reload(); |
566 this.updateRemoveButton(); | 635 this.updateRemoveButton(); |
567 }; | 636 }; |
568 | 637 |
569 /** | 638 /** |
570 * Switch to a specified page. | 639 * Switch to a specified page. |
571 * @param {number} page The page we wish to view. | 640 * @param {number} page The page we wish to view. |
572 */ | 641 */ |
573 HistoryView.prototype.setPage = function(page) { | 642 HistoryView.prototype.setPage = function(page) { |
574 this.clear_(); | 643 this.clear_(); |
575 this.pageIndex_ = parseInt(page, 10); | 644 this.pageIndex_ = parseInt(page, 10); |
576 window.scrollTo(0, 0); | 645 window.scrollTo(0, 0); |
577 this.model_.requestPage(page); | 646 this.model_.requestPage(page); |
578 pageState.setUIState(this.model_.getSearchText(), this.pageIndex_); | 647 pageState.setUIState(this.model_.getSearchText(), |
| 648 this.pageIndex_, |
| 649 this.model_.getGroupByDomain()); |
579 }; | 650 }; |
580 | 651 |
581 /** | 652 /** |
582 * @return {number} The page number being viewed. | 653 * @return {number} The page number being viewed. |
583 */ | 654 */ |
584 HistoryView.prototype.getPage = function() { | 655 HistoryView.prototype.getPage = function() { |
585 return this.pageIndex_; | 656 return this.pageIndex_; |
586 }; | 657 }; |
587 | 658 |
588 /** | 659 /** |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
622 * Record that the given visit has been rendered. | 693 * Record that the given visit has been rendered. |
623 * @param {Visit} visit The visit that was rendered. | 694 * @param {Visit} visit The visit that was rendered. |
624 * @private | 695 * @private |
625 */ | 696 */ |
626 HistoryView.prototype.setVisitRendered_ = function(visit) { | 697 HistoryView.prototype.setVisitRendered_ = function(visit) { |
627 visit.isRendered = true; | 698 visit.isRendered = true; |
628 this.currentVisits_.push(visit); | 699 this.currentVisits_.push(visit); |
629 }; | 700 }; |
630 | 701 |
631 /** | 702 /** |
| 703 * This function generates and adds the grouped visits DOM for a certain |
| 704 * domain. This includes the clickable arrow and domain name and the visit |
| 705 * entries for that domain. |
| 706 * @param {Element} results DOM object to which to add the elements. |
| 707 * @param {string} domain Current domain name. |
| 708 * @param {Array} domainVisits Array of visits for this domain. |
| 709 * @private |
| 710 */ |
| 711 HistoryView.prototype.getGroupedVisitsDOM_ = function( |
| 712 results, domain, domainVisits) { |
| 713 // Add a new domain entry. |
| 714 var siteResults = results.appendChild( |
| 715 createElementWithClassName('li', 'site-name')); |
| 716 // Make a wrapper that will contain the arrow, the favicon and the domain. |
| 717 var siteDomainWrapper = siteResults.appendChild( |
| 718 createElementWithClassName('div', 'site-domain-wrapper')); |
| 719 var siteArrow = siteDomainWrapper.appendChild( |
| 720 createElementWithClassName('div', 'site-domain-arrow collapse')); |
| 721 var siteDomain = siteDomainWrapper.appendChild( |
| 722 createElementWithClassName('div', 'site-domain')); |
| 723 var numberOfVisits = createElementWithClassName('span', 'number-visits'); |
| 724 numberOfVisits.textContent = loadTimeData.getStringF('numbervisits', |
| 725 domainVisits.length); |
| 726 siteDomain.textContent = domain; |
| 727 siteDomain.appendChild(numberOfVisits); |
| 728 siteResults.appendChild(siteDomainWrapper); |
| 729 var resultsList = siteResults.appendChild( |
| 730 createElementWithClassName('ol', 'site-results')); |
| 731 |
| 732 domainVisits[0].addFaviconToElement_(siteDomain); |
| 733 |
| 734 var toggleHandler = function(e) { |
| 735 // |this| is the parent of the element which was clicked on. |
| 736 var innerResultList = this.querySelector('.site-results'); |
| 737 var innerArrow = this.querySelector('.site-domain-arrow'); |
| 738 if (innerArrow.classList.contains('collapse')) { |
| 739 innerResultList.style.height = 'auto'; |
| 740 // -webkit-transition does not work on height:auto elements so first set |
| 741 // the height to auto so that it is computed and then set it to the |
| 742 // computed value in pixels so the transition works properly. |
| 743 var height = innerResultList.clientHeight; |
| 744 innerResultList.style.height = height + 'px'; |
| 745 innerArrow.className = 'site-domain-arrow expand'; |
| 746 } else { |
| 747 innerResultList.style.height = 0; |
| 748 innerArrow.className = 'site-domain-arrow collapse'; |
| 749 } |
| 750 }; |
| 751 // |siteResults| is the parent of the arrow and the results so use bind to |
| 752 // make it easily accessible from the handler. |
| 753 siteDomainWrapper.addEventListener('click', toggleHandler.bind(siteResults)); |
| 754 // Collapse until it gets toggled. |
| 755 resultsList.style.height = 0; |
| 756 |
| 757 // Add the results for each of the domain. |
| 758 for (var j = 0, visit; visit = domainVisits[j]; j++) { |
| 759 resultsList.appendChild(visit.getResultDOM({})); |
| 760 this.setVisitRendered_(visit); |
| 761 } |
| 762 }; |
| 763 |
| 764 /** |
| 765 * Groups visits by domain, sorting them by the number of visits. |
| 766 * @param {Array} visits Visits received from the query results. |
| 767 * @param {Element} results Object where the results are added to. |
| 768 * @private |
| 769 */ |
| 770 HistoryView.prototype.groupVisitsByDomain_ = function(visits, results) { |
| 771 var visitsByDomain = {}; |
| 772 var domains = []; |
| 773 |
| 774 // Group the visits into a dictionary and generate a list of domains. |
| 775 for (var i = 0, visit; visit = visits[i]; i++) { |
| 776 var domain = visit.getDomainFromURL_(visit.url_); |
| 777 if (!visitsByDomain[domain]) { |
| 778 visitsByDomain[domain] = []; |
| 779 domains.push(domain); |
| 780 } |
| 781 visitsByDomain[domain].push(visit); |
| 782 } |
| 783 var sortByVisits = function(a, b) { |
| 784 return visitsByDomain[b].length - visitsByDomain[a].length; |
| 785 }; |
| 786 domains.sort(sortByVisits); |
| 787 |
| 788 for (var i = 0, domain; domain = domains[i]; i++) { |
| 789 this.getGroupedVisitsDOM_(results, domain, visitsByDomain[domain]); |
| 790 } |
| 791 }; |
| 792 |
| 793 /** |
| 794 * Adds the results grouped by days, grouping them if needed. |
| 795 * @param {Array} visits Visits returned by the query. |
| 796 * @param {Element} parentElement Element to which to add the results to. |
| 797 * @private |
| 798 */ |
| 799 HistoryView.prototype.addDayResults_ = function(visits, parentElement) { |
| 800 if (visits.length == 0) |
| 801 return; |
| 802 |
| 803 var firstVisit = visits[0]; |
| 804 var day = parentElement.appendChild(createElementWithClassName('h3', 'day')); |
| 805 day.appendChild(document.createTextNode(firstVisit.dateRelativeDay)); |
| 806 if (firstVisit.continued) { |
| 807 day.appendChild(document.createTextNode(' ' + |
| 808 loadTimeData.getString('cont'))); |
| 809 } |
| 810 var dayResults = parentElement.appendChild( |
| 811 createElementWithClassName('ol', 'day-results')); |
| 812 |
| 813 if (this.model_.getGroupByDomain()) { |
| 814 this.groupVisitsByDomain_(visits, dayResults); |
| 815 } else { |
| 816 var lastTime; |
| 817 |
| 818 for (var i = 0, visit; visit = visits[i]; i++) { |
| 819 // If enough time has passed between visits, indicate a gap in browsing. |
| 820 var thisTime = visit.date.getTime(); |
| 821 if (lastTime && lastTime - thisTime > BROWSING_GAP_TIME) |
| 822 dayResults.appendChild(createElementWithClassName('li', 'gap')); |
| 823 |
| 824 // Insert the visit into the DOM. |
| 825 dayResults.appendChild(visit.getResultDOM({ |
| 826 addTitleFavicon: true |
| 827 })); |
| 828 this.setVisitRendered_(visit); |
| 829 |
| 830 lastTime = thisTime; |
| 831 } |
| 832 } |
| 833 }; |
| 834 |
| 835 /** |
632 * Update the page with results. | 836 * Update the page with results. |
633 * @private | 837 * @private |
634 */ | 838 */ |
635 HistoryView.prototype.displayResults_ = function() { | 839 HistoryView.prototype.displayResults_ = function() { |
636 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE; | 840 var rangeStart = this.pageIndex_ * RESULTS_PER_PAGE; |
637 var rangeEnd = rangeStart + RESULTS_PER_PAGE; | 841 var rangeEnd = rangeStart + RESULTS_PER_PAGE; |
638 var results = this.model_.getNumberedRange(rangeStart, rangeEnd); | 842 var results = this.model_.getNumberedRange(rangeStart, rangeEnd); |
639 | 843 |
640 var searchText = this.model_.getSearchText(); | 844 var searchText = this.model_.getSearchText(); |
| 845 var groupByDomain = this.model_.getGroupByDomain(); |
| 846 |
641 if (searchText) { | 847 if (searchText) { |
642 // Add a header for the search results, if there isn't already one. | 848 // Add a header for the search results, if there isn't already one. |
643 if (!this.resultDiv_.querySelector('h3')) { | 849 if (!this.resultDiv_.querySelector('h3')) { |
644 var header = document.createElement('h3'); | 850 var header = document.createElement('h3'); |
645 header.textContent = loadTimeData.getStringF('searchresultsfor', | 851 header.textContent = loadTimeData.getStringF('searchresultsfor', |
646 searchText); | 852 searchText); |
647 this.resultDiv_.appendChild(header); | 853 this.resultDiv_.appendChild(header); |
648 } | 854 } |
649 | 855 |
650 var searchResults = createElementWithClassName('ol', 'search-results'); | 856 var searchResults = createElementWithClassName('ol', 'search-results'); |
651 if (results.length == 0) { | 857 if (results.length == 0) { |
652 var noResults = document.createElement('div'); | 858 var noResults = document.createElement('div'); |
653 noResults.textContent = loadTimeData.getString('noresults'); | 859 noResults.textContent = loadTimeData.getString('noresults'); |
654 searchResults.appendChild(noResults); | 860 searchResults.appendChild(noResults); |
655 } else { | 861 } else { |
656 for (var i = 0, visit; visit = results[i]; i++) { | 862 for (var i = 0, visit; visit = results[i]; i++) { |
657 if (!visit.isRendered) { | 863 if (!visit.isRendered) { |
658 searchResults.appendChild(visit.getResultDOM(true)); | 864 searchResults.appendChild(visit.getResultDOM({ |
| 865 isSearchResult: true, |
| 866 addTitleFavicon: true |
| 867 })); |
659 this.setVisitRendered_(visit); | 868 this.setVisitRendered_(visit); |
660 } | 869 } |
661 } | 870 } |
662 } | 871 } |
663 this.resultDiv_.appendChild(searchResults); | 872 this.resultDiv_.appendChild(searchResults); |
664 } else { | 873 } else { |
665 var resultsFragment = document.createDocumentFragment(); | 874 var resultsFragment = document.createDocumentFragment(); |
666 var lastTime = Math.infinity; | |
667 var dayResults; | |
668 | 875 |
| 876 if (this.model_.getGroupByDomain()) { |
| 877 if (results.length == 0) { |
| 878 var noResults = document.createElement('div'); |
| 879 noResults.textContent = loadTimeData.getString('noresultsinterval'); |
| 880 resultsFragment.appendChild(noResults); |
| 881 } |
| 882 } |
| 883 |
| 884 var dayStartIndex = 0; |
| 885 |
| 886 // Go through all of the visits and process them in chunks of one day. |
669 for (var i = 0, visit; visit = results[i]; i++) { | 887 for (var i = 0, visit; visit = results[i]; i++) { |
670 if (visit.isRendered) | 888 if (visit.isRendered) { |
| 889 dayStartIndex = i; |
671 continue; | 890 continue; |
672 | 891 } |
673 var thisTime = visit.date.getTime(); | |
674 | 892 |
675 // Break across day boundaries and insert gaps for browsing pauses. | 893 // Break across day boundaries and insert gaps for browsing pauses. |
676 // Create a dayResults element to contain results for each day. | 894 // Create a dayResults element to contain results for each day. |
677 if ((i == 0 && visit.continued) || !visit.continued) { | 895 if ((i == 0 && visit.continued) || (i != 0 && !visit.continued)) { |
678 // It's the first visit of the day, or the day is continued from | 896 // Process the visits from the previous day. |
679 // the previous page. Create a header for the day on the current page. | 897 this.addDayResults_( |
680 var day = createElementWithClassName('h3', 'day'); | 898 results.slice(dayStartIndex, i), resultsFragment, groupByDomain); |
681 day.appendChild(document.createTextNode(visit.dateRelativeDay)); | 899 dayStartIndex = i; |
682 if (visit.continued) { | 900 } |
683 day.appendChild(document.createTextNode(' ' + | 901 } |
684 loadTimeData.getString('cont'))); | 902 // Process the final day. |
685 } | 903 this.addDayResults_(results.slice(dayStartIndex), resultsFragment); |
686 | 904 |
687 resultsFragment.appendChild(day); | 905 // Add all the days and their visits to the page. |
688 dayResults = createElementWithClassName('ol', 'day-results'); | |
689 resultsFragment.appendChild(dayResults); | |
690 } else if (dayResults && lastTime - thisTime > BROWSING_GAP_TIME) { | |
691 dayResults.appendChild(createElementWithClassName('li', 'gap')); | |
692 } | |
693 lastTime = thisTime; | |
694 | |
695 // Add the entry to the appropriate day. | |
696 dayResults.appendChild(visit.getResultDOM(false)); | |
697 this.setVisitRendered_(visit); | |
698 } | |
699 this.resultDiv_.appendChild(resultsFragment); | 906 this.resultDiv_.appendChild(resultsFragment); |
700 } | 907 } |
| 908 this.updateNavBar_(); |
701 }; | 909 }; |
702 | 910 |
703 /** | 911 /** |
704 * Update the visibility of the page navigation buttons. | 912 * Update the visibility of the page navigation buttons. |
705 * @private | 913 * @private |
706 */ | 914 */ |
707 HistoryView.prototype.updateNavBar_ = function() { | 915 HistoryView.prototype.updateNavBar_ = function() { |
708 $('newest-button').hidden = this.pageIndex_ == 0; | 916 $('newest-button').hidden = this.pageIndex_ == 0; |
709 $('newer-button').hidden = this.pageIndex_ == 0; | 917 $('newer-button').hidden = this.pageIndex_ == 0; |
710 $('older-button').hidden = !this.model_.hasMoreResults(); | 918 $('older-button').hidden = !this.model_.hasMoreResults(); |
(...skipping 21 matching lines...) Expand all Loading... |
732 } | 940 } |
733 | 941 |
734 // TODO(glen): Replace this with a bound method so we don't need | 942 // TODO(glen): Replace this with a bound method so we don't need |
735 // public model and view. | 943 // public model and view. |
736 this.checker_ = setInterval((function(state_obj) { | 944 this.checker_ = setInterval((function(state_obj) { |
737 var hashData = state_obj.getHashData(); | 945 var hashData = state_obj.getHashData(); |
738 if (hashData.q != state_obj.model.getSearchText()) { | 946 if (hashData.q != state_obj.model.getSearchText()) { |
739 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); | 947 state_obj.view.setSearch(hashData.q, parseInt(hashData.p, 10)); |
740 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { | 948 } else if (parseInt(hashData.p, 10) != state_obj.view.getPage()) { |
741 state_obj.view.setPage(hashData.p); | 949 state_obj.view.setPage(hashData.p); |
| 950 } else if ((hashData.g == 'true') != |
| 951 state_obj.view.model_.getGroupByDomain()) { |
| 952 state_obj.view.setGroupByDomain(hashData.g); |
742 } | 953 } |
743 }), 50, this); | 954 }), 50, this); |
744 } | 955 } |
745 | 956 |
746 /** | 957 /** |
747 * Holds the singleton instance. | 958 * Holds the singleton instance. |
748 */ | 959 */ |
749 PageState.instance = null; | 960 PageState.instance = null; |
750 | 961 |
751 /** | 962 /** |
752 * @return {Object} An object containing parameters from our window hash. | 963 * @return {Object} An object containing parameters from our window hash. |
753 */ | 964 */ |
754 PageState.prototype.getHashData = function() { | 965 PageState.prototype.getHashData = function() { |
755 var result = { | 966 var result = { |
756 e: 0, | 967 e: 0, |
757 q: '', | 968 q: '', |
758 p: 0 | 969 p: 0, |
| 970 g: false |
759 }; | 971 }; |
760 | 972 |
761 if (!window.location.hash) { | 973 if (!window.location.hash) |
762 return result; | 974 return result; |
763 } | |
764 | 975 |
765 var hashSplit = window.location.hash.substr(1).split('&'); | 976 var hashSplit = window.location.hash.substr(1).split('&'); |
766 for (var i = 0; i < hashSplit.length; i++) { | 977 for (var i = 0; i < hashSplit.length; i++) { |
767 var pair = hashSplit[i].split('='); | 978 var pair = hashSplit[i].split('='); |
768 if (pair.length > 1) { | 979 if (pair.length > 1) { |
769 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); | 980 result[pair[0]] = decodeURIComponent(pair[1].replace(/\+/g, ' ')); |
770 } | 981 } |
771 } | 982 } |
772 | 983 |
773 return result; | 984 return result; |
774 }; | 985 }; |
775 | 986 |
776 /** | 987 /** |
777 * Set the hash to a specified state, this will create an entry in the | 988 * Set the hash to a specified state, this will create an entry in the |
778 * session history so the back button cycles through hash states, which | 989 * session history so the back button cycles through hash states, which |
779 * are then picked up by our listener. | 990 * are then picked up by our listener. |
780 * @param {string} term The current search string. | 991 * @param {string} term The current search string. |
781 * @param {string} page The page currently being viewed. | 992 * @param {number} page The page currently being viewed. |
| 993 * @param {boolean} grouped Whether the results are grouped or not. |
782 */ | 994 */ |
783 PageState.prototype.setUIState = function(term, page) { | 995 PageState.prototype.setUIState = function(term, page, grouped) { |
784 // Make sure the form looks pretty. | 996 // Make sure the form looks pretty. |
785 $('search-field').value = term; | 997 $('search-field').value = term; |
786 var currentHash = this.getHashData(); | 998 if (grouped) { |
787 if (currentHash.q != term || currentHash.p != page) { | 999 $('display-filter-sites').checked = true; |
788 window.location.hash = PageState.getHashString(term, page); | 1000 } else { |
| 1001 $('display-filter-sites').checked = false; |
| 1002 } |
| 1003 var hash = this.getHashData(); |
| 1004 if (hash.q != term || hash.p != page || hash.g != grouped) { |
| 1005 window.location.hash = PageState.getHashString( |
| 1006 term, page, grouped); |
789 } | 1007 } |
790 }; | 1008 }; |
791 | 1009 |
792 /** | 1010 /** |
793 * Static method to get the hash string for a specified state | 1011 * Static method to get the hash string for a specified state |
794 * @param {string} term The current search string. | 1012 * @param {string} term The current search string. |
795 * @param {string} page The page currently being viewed. | 1013 * @param {number} page The page currently being viewed. |
| 1014 * @param {boolean} grouped Whether the results are grouped or not. |
796 * @return {string} The string to be used in a hash. | 1015 * @return {string} The string to be used in a hash. |
797 */ | 1016 */ |
798 PageState.getHashString = function(term, page) { | 1017 PageState.getHashString = function(term, page, grouped) { |
| 1018 // Omit elements that are empty. |
799 var newHash = []; | 1019 var newHash = []; |
800 if (term) { | 1020 |
| 1021 if (term) |
801 newHash.push('q=' + encodeURIComponent(term)); | 1022 newHash.push('q=' + encodeURIComponent(term)); |
802 } | 1023 |
803 if (page != undefined) { | 1024 if (page) |
804 newHash.push('p=' + page); | 1025 newHash.push('p=' + page); |
805 } | 1026 |
| 1027 if (grouped) |
| 1028 newHash.push('g=' + grouped); |
806 | 1029 |
807 return newHash.join('&'); | 1030 return newHash.join('&'); |
808 }; | 1031 }; |
809 | 1032 |
810 /////////////////////////////////////////////////////////////////////////////// | 1033 /////////////////////////////////////////////////////////////////////////////// |
811 // Document Functions: | 1034 // Document Functions: |
812 /** | 1035 /** |
813 * Window onload handler, sets up the page. | 1036 * Window onload handler, sets up the page. |
814 */ | 1037 */ |
815 function load() { | 1038 function load() { |
(...skipping 17 matching lines...) Expand all Loading... |
833 | 1056 |
834 $('remove-visit').addEventListener('activate', function(e) { | 1057 $('remove-visit').addEventListener('activate', function(e) { |
835 activeVisit.removeFromHistory_(); | 1058 activeVisit.removeFromHistory_(); |
836 activeVisit = null; | 1059 activeVisit = null; |
837 }); | 1060 }); |
838 $('more-from-site').addEventListener('activate', function(e) { | 1061 $('more-from-site').addEventListener('activate', function(e) { |
839 activeVisit.showMoreFromSite_(); | 1062 activeVisit.showMoreFromSite_(); |
840 activeVisit = null; | 1063 activeVisit = null; |
841 }); | 1064 }); |
842 | 1065 |
| 1066 // Only show the controls if the command line switch is activated. |
| 1067 if (loadTimeData.getBoolean('historyGroupEnabled')) { |
| 1068 $('filter-controls').hidden = false; |
| 1069 } |
| 1070 |
843 var title = loadTimeData.getString('title'); | 1071 var title = loadTimeData.getString('title'); |
844 uber.invokeMethodOnParent('setTitle', {title: title}); | 1072 uber.invokeMethodOnParent('setTitle', {title: title}); |
845 | 1073 |
846 window.addEventListener('message', function(e) { | 1074 window.addEventListener('message', function(e) { |
847 if (e.data.method == 'frameSelected') | 1075 if (e.data.method == 'frameSelected') |
848 searchField.focus(); | 1076 searchField.focus(); |
849 }); | 1077 }); |
850 } | 1078 } |
851 | 1079 |
852 /** | 1080 /** |
(...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1079 historyView.reload(); | 1307 historyView.reload(); |
1080 } | 1308 } |
1081 | 1309 |
1082 // Add handlers to HTML elements. | 1310 // Add handlers to HTML elements. |
1083 document.addEventListener('DOMContentLoaded', load); | 1311 document.addEventListener('DOMContentLoaded', load); |
1084 | 1312 |
1085 // This event lets us enable and disable menu items before the menu is shown. | 1313 // This event lets us enable and disable menu items before the menu is shown. |
1086 document.addEventListener('canExecute', function(e) { | 1314 document.addEventListener('canExecute', function(e) { |
1087 e.canExecute = true; | 1315 e.canExecute = true; |
1088 }); | 1316 }); |
OLD | NEW |