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