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

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

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

Powered by Google App Engine
This is Rietveld 408576698