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

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

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

Powered by Google App Engine
This is Rietveld 408576698