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

Side by Side Diff: chrome/browser/resources/options/options_page.js

Issue 410293004: Split OptionsPage into Page and PageManager (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: s/PageTree/PageManager/, s/pageHelper/pageManager/ Created 6 years, 5 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
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 cr.define('options', function() { 5 cr.define('options', function() {
6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager; 6 /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
7 /** @const */ var PageManager = cr.ui.pageManager.PageManager;
7 8
8 ///////////////////////////////////////////////////////////////////////////// 9 var OptionsPage = {
9 // OptionsPage class: 10 /**
11 * This is the absolute difference maintained between standard and
12 * fixed-width font sizes. Refer http://crbug.com/91922.
13 * @const
14 */
15 SIZE_DIFFERENCE_FIXED_STANDARD: 3,
10 16
11 /** 17 /**
12 * Base class for options page. 18 * Gets the default page (to be shown on initial load).
Dan Beam 2014/07/28 20:57:58 @return
Dan Beam 2014/07/28 20:57:58 @override
michaelpg 2014/07/29 01:08:29 Getting rid of this by making the default page a p
13 * @constructor 19 */
14 * @param {string} name Options page name. 20 getDefaultPage: function() {
15 * @param {string} title Options page title, used for history. 21 return BrowserOptions.getInstance();
16 * @extends {EventTarget} 22 },
17 */
18 function OptionsPage(name, title, pageDivName) {
19 this.name = name;
20 this.title = title;
21 this.pageDivName = pageDivName;
22 this.pageDiv = $(this.pageDivName);
23 // |pageDiv.page| is set to the page object (this) when the page is visible
24 // to track which page is being shown when multiple pages can share the same
25 // underlying div.
26 this.pageDiv.page = null;
27 this.tab = null;
28 this.lastFocusedElement = null;
29 }
30 23
31 /** 24 /**
32 * This is the absolute difference maintained between standard and 25 * Shows the tab contents for the given navigation tab.
33 * fixed-width font sizes. Refer http://crbug.com/91922. 26 * @param {!Element} tab The tab that the user clicked.
34 * @const 27 */
35 */ 28 showTab: function(tab) {
36 OptionsPage.SIZE_DIFFERENCE_FIXED_STANDARD = 3; 29 // Search parents until we find a tab, or the nav bar itself. This allows
30 // tabs to have child nodes, e.g. labels in separately-styled spans.
31 while (tab && !tab.classList.contains('subpages-nav-tabs') &&
32 !tab.classList.contains('tab')) {
33 tab = tab.parentNode;
34 }
35 if (!tab || !tab.classList.contains('tab'))
36 return;
37 37
38 /** 38 // Find tab bar of the tab.
39 * Offset of page container in pixels, to allow room for side menu. 39 var tabBar = tab;
40 * Simplified settings pages can override this if they don't use the menu. 40 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) {
41 * The default (155) comes from -webkit-margin-start in uber_shared.css 41 tabBar = tabBar.parentNode;
42 * @private 42 }
43 */ 43 if (!tabBar)
44 OptionsPage.horizontalOffset = 155; 44 return;
45 45
46 /** 46 if (tabBar.activeNavTab != null) {
47 * Main level option pages. Maps lower-case page names to the respective page 47 tabBar.activeNavTab.classList.remove('active-tab');
48 * object. 48 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList.
49 * @protected 49 remove('active-tab-contents');
50 */
51 OptionsPage.registeredPages = {};
52
53 /**
54 * Pages which are meant to behave like modal dialogs. Maps lower-case overlay
55 * names to the respective overlay object.
56 * @protected
57 */
58 OptionsPage.registeredOverlayPages = {};
59
60 /**
61 * True if options page is served from a dialog.
62 */
63 OptionsPage.isDialog = false;
64
65 /**
66 * Gets the default page (to be shown on initial load).
67 */
68 OptionsPage.getDefaultPage = function() {
69 return BrowserOptions.getInstance();
70 };
71
72 /**
73 * Shows the default page.
74 */
75 OptionsPage.showDefaultPage = function() {
76 this.navigateToPage(this.getDefaultPage().name);
77 };
78
79 /**
80 * "Navigates" to a page, meaning that the page will be shown and the
81 * appropriate entry is placed in the history.
82 * @param {string} pageName Page name.
83 */
84 OptionsPage.navigateToPage = function(pageName) {
85 this.showPageByName(pageName, true);
86 };
87
88 /**
89 * Shows a registered page. This handles both top-level and overlay pages.
90 * @param {string} pageName Page name.
91 * @param {boolean} updateHistory True if we should update the history after
92 * showing the page.
93 * @param {Object=} opt_propertyBag An optional bag of properties including
94 * replaceState (if history state should be replaced instead of pushed).
95 * @private
96 */
97 OptionsPage.showPageByName = function(pageName,
98 updateHistory,
99 opt_propertyBag) {
100 // If |opt_propertyBag| is non-truthy, homogenize to object.
101 opt_propertyBag = opt_propertyBag || {};
102
103 // If a bubble is currently being shown, hide it.
104 this.hideBubble();
105
106 // Find the currently visible root-level page.
107 var rootPage = null;
108 for (var name in this.registeredPages) {
109 var page = this.registeredPages[name];
110 if (page.visible && !page.parentPage) {
111 rootPage = page;
112 break;
113 }
114 }
115
116 // Find the target page.
117 var targetPage = this.registeredPages[pageName.toLowerCase()];
118 if (!targetPage || !targetPage.canShowPage()) {
119 // If it's not a page, try it as an overlay.
120 if (!targetPage && this.showOverlay_(pageName, rootPage)) {
121 if (updateHistory)
122 this.updateHistoryState_(!!opt_propertyBag.replaceState);
123 this.updateTitle_();
124 return;
125 } else {
126 targetPage = this.getDefaultPage();
127 }
128 }
129
130 pageName = targetPage.name.toLowerCase();
131 var targetPageWasVisible = targetPage.visible;
132
133 // Determine if the root page is 'sticky', meaning that it
134 // shouldn't change when showing an overlay. This can happen for special
135 // pages like Search.
136 var isRootPageLocked =
137 rootPage && rootPage.sticky && targetPage.parentPage;
138
139 var allPageNames = Array.prototype.concat.call(
140 Object.keys(this.registeredPages),
141 Object.keys(this.registeredOverlayPages));
142
143 // Notify pages if they will be hidden.
144 for (var i = 0; i < allPageNames.length; ++i) {
145 var name = allPageNames[i];
146 var page = this.registeredPages[name] ||
147 this.registeredOverlayPages[name];
148 if (!page.parentPage && isRootPageLocked)
149 continue;
150 if (page.willHidePage && name != pageName &&
151 !page.isAncestorOfPage(targetPage)) {
152 page.willHidePage();
153 }
154 }
155
156 // Update visibilities to show only the hierarchy of the target page.
157 for (var i = 0; i < allPageNames.length; ++i) {
158 var name = allPageNames[i];
159 var page = this.registeredPages[name] ||
160 this.registeredOverlayPages[name];
161 if (!page.parentPage && isRootPageLocked)
162 continue;
163 page.visible = name == pageName || page.isAncestorOfPage(targetPage);
164 }
165
166 // Update the history and current location.
167 if (updateHistory)
168 this.updateHistoryState_(!!opt_propertyBag.replaceState);
169
170 // Update focus if any other control was focused on the previous page,
171 // or the previous page is not known.
172 if (document.activeElement != document.body &&
173 (!rootPage || rootPage.pageDiv.contains(document.activeElement))) {
174 targetPage.focus();
175 }
176
177 // Notify pages if they were shown.
178 for (var i = 0; i < allPageNames.length; ++i) {
179 var name = allPageNames[i];
180 var page = this.registeredPages[name] ||
181 this.registeredOverlayPages[name];
182 if (!page.parentPage && isRootPageLocked)
183 continue;
184 if (!targetPageWasVisible && page.didShowPage &&
185 (name == pageName || page.isAncestorOfPage(targetPage))) {
186 page.didShowPage();
187 }
188 }
189
190 // Update the document title. Do this after didShowPage was called, in case
191 // a page decides to change its title.
192 this.updateTitle_();
193 };
194
195 /**
196 * Scrolls the page to the correct position (the top when opening an overlay,
197 * or the old scroll position a previously hidden overlay becomes visible).
198 * @private
199 */
200 OptionsPage.updateScrollPosition_ = function() {
201 var container = $('page-container');
202 var scrollTop = container.oldScrollTop || 0;
203 container.oldScrollTop = undefined;
204 window.scroll(scrollLeftForDocument(document), scrollTop);
205 };
206
207 /**
208 * Updates the title to title of the current page.
209 * @private
210 */
211 OptionsPage.updateTitle_ = function() {
212 var page = this.getTopmostVisiblePage();
213 uber.setTitle(page.title);
214 };
215
216 /**
217 * Pushes the current page onto the history stack, replacing the current entry
218 * if appropriate.
219 * @param {boolean} replace If true, allow no history events to be created.
220 * @param {object=} opt_params A bag of optional params, including:
221 * {boolean} ignoreHash Whether to include the hash or not.
222 * @private
223 */
224 OptionsPage.updateHistoryState_ = function(replace, opt_params) {
225 if (OptionsPage.isDialog)
226 return;
227
228 var page = this.getTopmostVisiblePage();
229 var path = window.location.pathname + window.location.hash;
230 if (path)
231 path = path.slice(1).replace(/\/(?:#|$)/, ''); // Remove trailing slash.
232
233 // If the page is already in history (the user may have clicked the same
234 // link twice, or this is the initial load), do nothing.
235 var hash = opt_params && opt_params.ignoreHash ? '' : window.location.hash;
236 var newPath = (page == this.getDefaultPage() ? '' : page.name) + hash;
237 if (path == newPath)
238 return;
239
240 var historyFunction = replace ? uber.replaceState : uber.pushState;
241 historyFunction.call(uber, {}, newPath);
242 };
243
244 /**
245 * Shows a registered Overlay page. Does not update history.
246 * @param {string} overlayName Page name.
247 * @param {OptionPage} rootPage The currently visible root-level page.
248 * @return {boolean} whether we showed an overlay.
249 */
250 OptionsPage.showOverlay_ = function(overlayName, rootPage) {
251 var overlay = this.registeredOverlayPages[overlayName.toLowerCase()];
252 if (!overlay || !overlay.canShowPage())
253 return false;
254
255 // Save the currently focused element in the page for restoration later.
256 var currentPage = this.getTopmostVisiblePage();
257 if (currentPage)
258 currentPage.lastFocusedElement = document.activeElement;
259
260 if ((!rootPage || !rootPage.sticky) &&
261 overlay.parentPage &&
262 !overlay.parentPage.visible) {
263 this.showPageByName(overlay.parentPage.name, false);
264 }
265
266 if (!overlay.visible) {
267 overlay.visible = true;
268 if (overlay.didShowPage) overlay.didShowPage();
269 }
270
271 // Change focus to the overlay if any other control was focused by keyboard
272 // before. Otherwise, no one should have focus.
273 if (document.activeElement != document.body) {
274 if (FocusOutlineManager.forDocument(document).visible) {
275 overlay.focus();
276 } else if (!overlay.pageDiv.contains(document.activeElement)) {
277 document.activeElement.blur();
278 }
279 }
280
281 if ($('search-field') && $('search-field').value == '') {
282 var section = overlay.associatedSection;
283 if (section)
284 options.BrowserOptions.scrollToSection(section);
285 }
286
287 return true;
288 };
289
290 /**
291 * Returns whether or not an overlay is visible.
292 * @return {boolean} True if an overlay is visible.
293 * @private
294 */
295 OptionsPage.isOverlayVisible_ = function() {
296 return this.getVisibleOverlay_() != null;
297 };
298
299 /**
300 * Returns the currently visible overlay, or null if no page is visible.
301 * @return {OptionPage} The visible overlay.
302 */
303 OptionsPage.getVisibleOverlay_ = function() {
304 var topmostPage = null;
305 for (var name in this.registeredOverlayPages) {
306 var page = this.registeredOverlayPages[name];
307 if (page.visible &&
308 (!topmostPage || page.nestingLevel > topmostPage.nestingLevel)) {
309 topmostPage = page;
310 }
311 }
312 return topmostPage;
313 };
314
315 /**
316 * Restores the last focused element on a given page.
317 */
318 OptionsPage.restoreLastFocusedElement_ = function() {
319 var currentPage = this.getTopmostVisiblePage();
320 if (currentPage.lastFocusedElement)
321 currentPage.lastFocusedElement.focus();
322 };
323
324 /**
325 * Closes the visible overlay. Updates the history state after closing the
326 * overlay.
327 */
328 OptionsPage.closeOverlay = function() {
329 var overlay = this.getVisibleOverlay_();
330 if (!overlay)
331 return;
332
333 overlay.visible = false;
334
335 if (overlay.didClosePage) overlay.didClosePage();
336 this.updateHistoryState_(false, {ignoreHash: true});
337 this.updateTitle_();
338
339 this.restoreLastFocusedElement_();
340 };
341
342 /**
343 * Closes all overlays and updates the history after each closed overlay.
344 */
345 OptionsPage.closeAllOverlays = function() {
346 while (this.isOverlayVisible_()) {
347 this.closeOverlay();
348 }
349 };
350
351 /**
352 * Cancels (closes) the overlay, due to the user pressing <Esc>.
353 */
354 OptionsPage.cancelOverlay = function() {
355 // Blur the active element to ensure any changed pref value is saved.
356 document.activeElement.blur();
357 var overlay = this.getVisibleOverlay_();
358 // Let the overlay handle the <Esc> if it wants to.
359 if (overlay.handleCancel) {
360 overlay.handleCancel();
361 this.restoreLastFocusedElement_();
362 } else {
363 this.closeOverlay();
364 }
365 };
366
367 /**
368 * Hides the visible overlay. Does not affect the history state.
369 * @private
370 */
371 OptionsPage.hideOverlay_ = function() {
372 var overlay = this.getVisibleOverlay_();
373 if (overlay)
374 overlay.visible = false;
375 };
376
377 /**
378 * Returns the pages which are currently visible, ordered by nesting level
379 * (ascending).
380 * @return {Array.OptionPage} The pages which are currently visible, ordered
381 * by nesting level (ascending).
382 */
383 OptionsPage.getVisiblePages_ = function() {
384 var visiblePages = [];
385 for (var name in this.registeredPages) {
386 var page = this.registeredPages[name];
387 if (page.visible)
388 visiblePages[page.nestingLevel] = page;
389 }
390 return visiblePages;
391 };
392
393 /**
394 * Returns the topmost visible page (overlays excluded).
395 * @return {OptionPage} The topmost visible page aside any overlay.
396 * @private
397 */
398 OptionsPage.getTopmostVisibleNonOverlayPage_ = function() {
399 var topPage = null;
400 for (var name in this.registeredPages) {
401 var page = this.registeredPages[name];
402 if (page.visible &&
403 (!topPage || page.nestingLevel > topPage.nestingLevel))
404 topPage = page;
405 }
406
407 return topPage;
408 };
409
410 /**
411 * Returns the topmost visible page, or null if no page is visible.
412 * @return {OptionPage} The topmost visible page.
413 */
414 OptionsPage.getTopmostVisiblePage = function() {
415 // Check overlays first since they're top-most if visible.
416 return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_();
417 };
418
419 /**
420 * Returns the currently visible bubble, or null if no bubble is visible.
421 * @return {AutoCloseBubble} The bubble currently being shown.
422 */
423 OptionsPage.getVisibleBubble = function() {
424 var bubble = OptionsPage.bubble_;
425 return bubble && !bubble.hidden ? bubble : null;
426 };
427
428 /**
429 * Shows an informational bubble displaying |content| and pointing at the
430 * |target| element. If |content| has focusable elements, they join the
431 * current page's tab order as siblings of |domSibling|.
432 * @param {HTMLDivElement} content The content of the bubble.
433 * @param {HTMLElement} target The element at which the bubble points.
434 * @param {HTMLElement} domSibling The element after which the bubble is added
435 * to the DOM.
436 * @param {cr.ui.ArrowLocation} location The arrow location.
437 */
438 OptionsPage.showBubble = function(content, target, domSibling, location) {
439 OptionsPage.hideBubble();
440
441 var bubble = new cr.ui.AutoCloseBubble;
442 bubble.anchorNode = target;
443 bubble.domSibling = domSibling;
444 bubble.arrowLocation = location;
445 bubble.content = content;
446 bubble.show();
447 OptionsPage.bubble_ = bubble;
448 };
449
450 /**
451 * Hides the currently visible bubble, if any.
452 */
453 OptionsPage.hideBubble = function() {
454 if (OptionsPage.bubble_)
455 OptionsPage.bubble_.hide();
456 };
457
458 /**
459 * Shows the tab contents for the given navigation tab.
460 * @param {!Element} tab The tab that the user clicked.
461 */
462 OptionsPage.showTab = function(tab) {
463 // Search parents until we find a tab, or the nav bar itself. This allows
464 // tabs to have child nodes, e.g. labels in separately-styled spans.
465 while (tab && !tab.classList.contains('subpages-nav-tabs') &&
466 !tab.classList.contains('tab')) {
467 tab = tab.parentNode;
468 }
469 if (!tab || !tab.classList.contains('tab'))
470 return;
471
472 // Find tab bar of the tab.
473 var tabBar = tab;
474 while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) {
475 tabBar = tabBar.parentNode;
476 }
477 if (!tabBar)
478 return;
479
480 if (tabBar.activeNavTab != null) {
481 tabBar.activeNavTab.classList.remove('active-tab');
482 $(tabBar.activeNavTab.getAttribute('tab-contents')).classList.
483 remove('active-tab-contents');
484 }
485
486 tab.classList.add('active-tab');
487 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
488 tabBar.activeNavTab = tab;
489 };
490
491 /**
492 * Registers new options page.
493 * @param {OptionsPage} page Page to register.
494 */
495 OptionsPage.register = function(page) {
496 this.registeredPages[page.name.toLowerCase()] = page;
497 page.initializePage();
498 };
499
500 /**
501 * Find an enclosing section for an element if it exists.
502 * @param {Element} element Element to search.
503 * @return {OptionPage} The section element, or null.
504 * @private
505 */
506 OptionsPage.findSectionForNode_ = function(node) {
507 while (node = node.parentNode) {
508 if (node.nodeName == 'SECTION')
509 return node;
510 }
511 return null;
512 };
513
514 /**
515 * Registers a new Overlay page.
516 * @param {OptionsPage} overlay Overlay to register.
517 * @param {OptionsPage} parentPage Associated parent page for this overlay.
518 * @param {Array} associatedControls Array of control elements associated with
519 * this page.
520 */
521 OptionsPage.registerOverlay = function(overlay,
522 parentPage,
523 associatedControls) {
524 this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay;
525 overlay.parentPage = parentPage;
526 if (associatedControls) {
527 overlay.associatedControls = associatedControls;
528 if (associatedControls.length) {
529 overlay.associatedSection =
530 this.findSectionForNode_(associatedControls[0]);
531 } 50 }
532 51
533 // Sanity check. 52 tab.classList.add('active-tab');
534 for (var i = 0; i < associatedControls.length; ++i) { 53 $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents');
535 assert(associatedControls[i], 'Invalid element passed.'); 54 tabBar.activeNavTab = tab;
536 } 55 },
537 }
538
539 // Reverse the button strip for Windows and CrOS. See the documentation of
540 // reverseButtonStripIfNecessary_() for an explanation of why this is done.
541 if (cr.isWindows || cr.isChromeOS)
542 this.reverseButtonStripIfNecessary_(overlay);
543
544 overlay.tab = undefined;
545 overlay.isOverlay = true;
546 overlay.initializePage();
547 };
548
549 /**
550 * Reverses the child elements of a button strip if it hasn't already been
551 * reversed. This is necessary because WebKit does not alter the tab order for
552 * elements that are visually reversed using -webkit-box-direction: reverse,
553 * and the button order is reversed for views. See http://webk.it/62664 for
554 * more information.
555 * @param {Object} overlay The overlay containing the button strip to reverse.
556 * @private
557 */
558 OptionsPage.reverseButtonStripIfNecessary_ = function(overlay) {
559 var buttonStrips =
560 overlay.pageDiv.querySelectorAll('.button-strip:not([reversed])');
561
562 // Reverse all button-strips in the overlay.
563 for (var j = 0; j < buttonStrips.length; j++) {
564 var buttonStrip = buttonStrips[j];
565
566 var childNodes = buttonStrip.childNodes;
567 for (var i = childNodes.length - 1; i >= 0; i--)
568 buttonStrip.appendChild(childNodes[i]);
569
570 buttonStrip.setAttribute('reversed', '');
571 }
572 };
573
574 /**
575 * Returns the name of the page from the current path.
576 */
577 OptionsPage.getPageNameFromPath = function() {
578 var path = location.pathname;
579 if (path.length <= 1)
580 return this.getDefaultPage().name;
581
582 // Skip starting slash and remove trailing slash (if any).
583 return path.slice(1).replace(/\/$/, '');
584 };
585
586 /**
587 * Callback for window.onpopstate to handle back/forward navigations.
588 * @param {string} pageName The current page name.
589 * @param {Object} data State data pushed into history.
590 */
591 OptionsPage.setState = function(pageName, data) {
592 var currentOverlay = this.getVisibleOverlay_();
593 var lowercaseName = pageName.toLowerCase();
594 var newPage = this.registeredPages[lowercaseName] ||
595 this.registeredOverlayPages[lowercaseName] ||
596 this.getDefaultPage();
597 if (currentOverlay && !currentOverlay.isAncestorOfPage(newPage)) {
598 currentOverlay.visible = false;
599 if (currentOverlay.didClosePage) currentOverlay.didClosePage();
600 }
601 this.showPageByName(pageName, false);
602 };
603
604 /**
605 * Callback for window.onbeforeunload. Used to notify overlays that they will
606 * be closed.
607 */
608 OptionsPage.willClose = function() {
609 var overlay = this.getVisibleOverlay_();
610 if (overlay && overlay.didClosePage)
611 overlay.didClosePage();
612 };
613
614 /**
615 * Freezes/unfreezes the scroll position of the root page container.
616 * @param {boolean} freeze Whether the page should be frozen.
617 * @private
618 */
619 OptionsPage.setRootPageFrozen_ = function(freeze) {
620 var container = $('page-container');
621 if (container.classList.contains('frozen') == freeze)
622 return;
623
624 if (freeze) {
625 // Lock the width, since auto width computation may change.
626 container.style.width = window.getComputedStyle(container).width;
627 container.oldScrollTop = scrollTopForDocument(document);
628 container.classList.add('frozen');
629 var verticalPosition =
630 container.getBoundingClientRect().top - container.oldScrollTop;
631 container.style.top = verticalPosition + 'px';
632 this.updateFrozenElementHorizontalPosition_(container);
633 } else {
634 container.classList.remove('frozen');
635 container.style.top = '';
636 container.style.left = '';
637 container.style.right = '';
638 container.style.width = '';
639 }
640 };
641
642 /**
643 * Freezes/unfreezes the scroll position of the root page based on the current
644 * page stack.
645 */
646 OptionsPage.updateRootPageFreezeState = function() {
647 var topPage = OptionsPage.getTopmostVisiblePage();
648 if (topPage)
649 this.setRootPageFrozen_(topPage.isOverlay);
650 };
651
652 /**
653 * Initializes the complete options page. This will cause all C++ handlers to
654 * be invoked to do final setup.
655 */
656 OptionsPage.initialize = function() {
657 chrome.send('coreOptionsInitialize');
658 uber.onContentFrameLoaded();
659 FocusOutlineManager.forDocument(document);
660 document.addEventListener('scroll', this.handleScroll_.bind(this));
661
662 // Trigger the scroll handler manually to set the initial state.
663 this.handleScroll_();
664
665 // Shake the dialog if the user clicks outside the dialog bounds.
666 var containers = [$('overlay-container-1'), $('overlay-container-2')];
667 for (var i = 0; i < containers.length; i++) {
668 var overlay = containers[i];
669 cr.ui.overlay.setupOverlay(overlay);
670 overlay.addEventListener('cancelOverlay',
671 OptionsPage.cancelOverlay.bind(OptionsPage));
672 }
673
674 cr.ui.overlay.globalInitialization();
675 };
676
677 /**
678 * Does a bounds check for the element on the given x, y client coordinates.
679 * @param {Element} e The DOM element.
680 * @param {number} x The client X to check.
681 * @param {number} y The client Y to check.
682 * @return {boolean} True if the point falls within the element's bounds.
683 * @private
684 */
685 OptionsPage.elementContainsPoint_ = function(e, x, y) {
686 var clientRect = e.getBoundingClientRect();
687 return x >= clientRect.left && x <= clientRect.right &&
688 y >= clientRect.top && y <= clientRect.bottom;
689 };
690
691 /**
692 * Called when the page is scrolled; moves elements that are position:fixed
693 * but should only behave as if they are fixed for vertical scrolling.
694 * @private
695 */
696 OptionsPage.handleScroll_ = function() {
697 this.updateAllFrozenElementPositions_();
698 };
699
700 /**
701 * Updates all frozen pages to match the horizontal scroll position.
702 * @private
703 */
704 OptionsPage.updateAllFrozenElementPositions_ = function() {
705 var frozenElements = document.querySelectorAll('.frozen');
706 for (var i = 0; i < frozenElements.length; i++)
707 this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
708 };
709
710 /**
711 * Updates the given frozen element to match the horizontal scroll position.
712 * @param {HTMLElement} e The frozen element to update.
713 * @private
714 */
715 OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) {
716 if (isRTL()) {
717 e.style.right = OptionsPage.horizontalOffset + 'px';
718 } else {
719 var scrollLeft = scrollLeftForDocument(document);
720 e.style.left = OptionsPage.horizontalOffset - scrollLeft + 'px';
721 }
722 };
723
724 /**
725 * Change the horizontal offset used to reposition elements while showing an
726 * overlay from the default.
727 */
728 OptionsPage.setHorizontalOffset = function(value) {
729 OptionsPage.horizontalOffset = value;
730 };
731
732 OptionsPage.setClearPluginLSODataEnabled = function(enabled) {
733 if (enabled) {
734 document.documentElement.setAttribute(
735 'flashPluginSupportsClearSiteData', '');
736 } else {
737 document.documentElement.removeAttribute(
738 'flashPluginSupportsClearSiteData');
739 }
740 if (navigator.plugins['Shockwave Flash'])
741 document.documentElement.setAttribute('hasFlashPlugin', '');
742 };
743
744 OptionsPage.setPepperFlashSettingsEnabled = function(enabled) {
745 if (enabled) {
746 document.documentElement.setAttribute(
747 'enablePepperFlashSettings', '');
748 } else {
749 document.documentElement.removeAttribute(
750 'enablePepperFlashSettings');
751 }
752 };
753
754 OptionsPage.setIsSettingsApp = function() {
755 document.documentElement.classList.add('settings-app');
756 };
757
758 OptionsPage.isSettingsApp = function() {
759 return document.documentElement.classList.contains('settings-app');
760 };
761
762 /**
763 * Whether the page is still loading (i.e. onload hasn't finished running).
764 * @return {boolean} Whether the page is still loading.
765 */
766 OptionsPage.isLoading = function() {
767 return document.documentElement.classList.contains('loading');
768 };
769
770 OptionsPage.prototype = {
771 __proto__: cr.EventTarget.prototype,
772 56
773 /** 57 /**
774 * The parent page of this option page, or null for top-level pages. 58 * Initializes the complete options page. This will cause all C++ handlers
Dan Beam 2014/07/28 20:57:58 nit: \s\s => \s
michaelpg 2014/07/29 01:08:28 Done.
775 * @type {OptionsPage} 59 * to be invoked to do final setup.
776 */ 60 */
777 parentPage: null, 61 initialize: function() {
62 chrome.send('coreOptionsInitialize');
63 uber.onContentFrameLoaded();
64 PageManager.initialize.call(PageManager);
Dan Beam 2014/07/28 20:57:58 can we call the super first?
michaelpg 2014/07/29 01:08:29 It's not a super. I'm not sure. The original Optio
65 },
778 66
Dan Beam 2014/07/28 20:57:58 doc on all these static methods
michaelpg 2014/07/29 01:08:29 Done.
779 /** 67 setClearPluginLSODataEnabled: function(enabled) {
780 * The section on the parent page that is associated with this page. 68 if (enabled) {
781 * Can be null. 69 document.documentElement.setAttribute(
782 * @type {Element} 70 'flashPluginSupportsClearSiteData', '');
783 */ 71 } else {
784 associatedSection: null, 72 document.documentElement.removeAttribute(
73 'flashPluginSupportsClearSiteData');
74 }
75 if (navigator.plugins['Shockwave Flash'])
76 document.documentElement.setAttribute('hasFlashPlugin', '');
77 },
785 78
786 /** 79 setPepperFlashSettingsEnabled: function(enabled) {
787 * An array of controls that are associated with this page. The first 80 if (enabled) {
788 * control should be located on a top-level page. 81 document.documentElement.setAttribute(
789 * @type {OptionsPage} 82 'enablePepperFlashSettings', '');
790 */ 83 } else {
791 associatedControls: null, 84 document.documentElement.removeAttribute(
792 85 'enablePepperFlashSettings');
793 /**
794 * Initializes page content.
795 */
796 initializePage: function() {},
797
798 /**
799 * Sets focus on the first focusable element. Override for a custom focus
800 * strategy.
801 */
802 focus: function() {
803 // Do not change focus if any control on this page is already focused.
804 if (this.pageDiv.contains(document.activeElement))
805 return;
806
807 var elements = this.pageDiv.querySelectorAll(
808 'input, list, select, textarea, button');
809 for (var i = 0; i < elements.length; i++) {
810 var element = elements[i];
811 // Try to focus. If fails, then continue.
812 element.focus();
813 if (document.activeElement == element)
814 return;
815 } 86 }
816 }, 87 },
817 88
818 /** 89 setIsSettingsApp: function() {
Dan Beam 2014/07/28 20:57:58 nit: it makes very little sense to me for a set* m
michaelpg 2014/07/29 01:08:29 Done.
819 * Gets the container div for this page if it is an overlay. 90 document.documentElement.classList.add('settings-app');
820 * @type {HTMLElement}
821 */
822 get container() {
823 assert(this.isOverlay);
824 return this.pageDiv.parentNode;
825 }, 91 },
826 92
827 /** 93 isSettingsApp: function() {
828 * Gets page visibility state. 94 return document.documentElement.classList.contains('settings-app');
829 * @type {boolean}
830 */
831 get visible() {
832 // If this is an overlay dialog it is no longer considered visible while
833 // the overlay is fading out. See http://crbug.com/118629.
834 if (this.isOverlay &&
835 this.container.classList.contains('transparent')) {
836 return false;
837 }
838 if (this.pageDiv.hidden)
839 return false;
840 return this.pageDiv.page == this;
841 },
842
843 /**
844 * Sets page visibility.
845 * @type {boolean}
846 */
847 set visible(visible) {
848 if ((this.visible && visible) || (!this.visible && !visible))
849 return;
850
851 // If using an overlay, the visibility of the dialog is toggled at the
852 // same time as the overlay to show the dialog's out transition. This
853 // is handled in setOverlayVisible.
854 if (this.isOverlay) {
855 this.setOverlayVisible_(visible);
856 } else {
857 this.pageDiv.page = this;
858 this.pageDiv.hidden = !visible;
859 this.onVisibilityChanged_();
860 }
861
862 cr.dispatchPropertyChange(this, 'visible', visible, !visible);
863 },
864
865 /**
866 * Shows or hides an overlay (including any visible dialog).
867 * @param {boolean} visible Whether the overlay should be visible or not.
868 * @private
869 */
870 setOverlayVisible_: function(visible) {
871 assert(this.isOverlay);
872 var pageDiv = this.pageDiv;
873 var container = this.container;
874
875 if (visible)
876 uber.invokeMethodOnParent('beginInterceptingEvents');
877
878 if (container.hidden != visible) {
879 if (visible) {
880 // If the container is set hidden and then immediately set visible
881 // again, the fadeCompleted_ callback would cause it to be erroneously
882 // hidden again. Removing the transparent tag avoids that.
883 container.classList.remove('transparent');
884
885 // Hide all dialogs in this container since a different one may have
886 // been previously visible before fading out.
887 var pages = container.querySelectorAll('.page');
888 for (var i = 0; i < pages.length; i++)
889 pages[i].hidden = true;
890 // Show the new dialog.
891 pageDiv.hidden = false;
892 pageDiv.page = this;
893 }
894 return;
895 }
896
897 var self = this;
898 var loading = OptionsPage.isLoading();
899 if (!loading) {
900 // TODO(flackr): Use an event delegate to avoid having to subscribe and
901 // unsubscribe for webkitTransitionEnd events.
902 container.addEventListener('webkitTransitionEnd', function f(e) {
903 var propName = e.propertyName;
904 if (e.target != e.currentTarget ||
905 (propName && propName != 'opacity')) {
906 return;
907 }
908 container.removeEventListener('webkitTransitionEnd', f);
909 self.fadeCompleted_();
910 });
911 // -webkit-transition is 200ms. Let's wait for 400ms.
912 ensureTransitionEndEvent(container, 400);
913 }
914
915 if (visible) {
916 container.hidden = false;
917 pageDiv.hidden = false;
918 pageDiv.page = this;
919 // NOTE: This is a hacky way to force the container to layout which
920 // will allow us to trigger the webkit transition.
921 container.scrollTop;
922
923 this.pageDiv.removeAttribute('aria-hidden');
924 if (this.parentPage) {
925 this.parentPage.pageDiv.parentElement.setAttribute('aria-hidden',
926 true);
927 }
928 container.classList.remove('transparent');
929 this.onVisibilityChanged_();
930 } else {
931 // Kick change events for text fields.
932 if (pageDiv.contains(document.activeElement))
933 document.activeElement.blur();
934 container.classList.add('transparent');
935 }
936
937 if (loading)
938 this.fadeCompleted_();
939 },
940
941 /**
942 * Called when a container opacity transition finishes.
943 * @private
944 */
945 fadeCompleted_: function() {
946 if (this.container.classList.contains('transparent')) {
947 this.pageDiv.hidden = true;
948 this.container.hidden = true;
949
950 if (this.parentPage)
951 this.parentPage.pageDiv.parentElement.removeAttribute('aria-hidden');
952
953 if (this.nestingLevel == 1)
954 uber.invokeMethodOnParent('stopInterceptingEvents');
955
956 this.onVisibilityChanged_();
957 }
958 },
959
960 /**
961 * Called when a page is shown or hidden to update the root options page
962 * based on this page's visibility.
963 * @private
964 */
965 onVisibilityChanged_: function() {
966 OptionsPage.updateRootPageFreezeState();
967
968 if (this.isOverlay && !this.visible)
969 OptionsPage.updateScrollPosition_();
970 },
971
972 /**
973 * The nesting level of this page.
974 * @type {number} The nesting level of this page (0 for top-level page)
975 */
976 get nestingLevel() {
977 var level = 0;
978 var parent = this.parentPage;
979 while (parent) {
980 level++;
981 parent = parent.parentPage;
982 }
983 return level;
984 },
985
986 /**
987 * Whether the page is considered 'sticky', such that it will
988 * remain a top-level page even if sub-pages change.
989 * @type {boolean} True if this page is sticky.
990 */
991 get sticky() {
992 return false;
993 },
994
995 /**
996 * Checks whether this page is an ancestor of the given page in terms of
997 * subpage nesting.
998 * @param {OptionsPage} page The potential descendent of this page.
999 * @return {boolean} True if |page| is nested under this page.
1000 */
1001 isAncestorOfPage: function(page) {
1002 var parent = page.parentPage;
1003 while (parent) {
1004 if (parent == this)
1005 return true;
1006 parent = parent.parentPage;
1007 }
1008 return false;
1009 },
1010
1011 /**
1012 * Whether it should be possible to show the page.
1013 * @return {boolean} True if the page should be shown.
1014 */
1015 canShowPage: function() {
1016 return true;
1017 }, 95 },
1018 }; 96 };
1019 97
1020 // Export 98 // Export
1021 return { 99 return {
1022 OptionsPage: OptionsPage 100 OptionsPage: OptionsPage
1023 }; 101 };
1024 }); 102 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698