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

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

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

Powered by Google App Engine
This is Rietveld 408576698