Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(482)

Side by Side Diff: chrome/browser/resources/history.js

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

Powered by Google App Engine
This is Rietveld 408576698