home *** CD-ROM | disk | FTP | other *** search
Text File | 2013-04-03 | 314.4 KB | 10,278 lines |
- <!DOCTYPE html>
- <html i18n-values="
- dir:textdirection;
- hasattribution:hasattribution;
- themegravity:themegravity;
- bookmarkbarattached:bookmarkbarattached;"
- class="starting-up">
- <head>
- <meta charset="utf-8">
- <title i18n-content="title"></title>
- <!-- Don't scale the viewport in either portrait or landscape mode.
- Note that this means apps will be reflowed when rotated (like iPad).
- If we wanted to maintain position we could remove 'maximum-scale' so
- that we'd zoom out in portrait mode, but then there would be a bunch
- of unusable space at the bottom.
- -->
- <meta name="viewport"
- content="user-scalable=no, width=device-width, maximum-scale=1.0">
-
- <!-- It's important that this be the first script loaded. -->
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview
- * Logging info for benchmarking purposes. Should be the first js file included.
- */
-
- /* Stack of events that has been logged. */
- var eventLog = [];
-
- /**
- * Logs an event.
- * @param {String} name The name of the event (can be any string).
- * @param {boolean} shouldLogTime If true, the event is used for benchmarking
- * and the time is logged. Otherwise, just push the event on the event
- * stack.
- */
- function logEvent(name, shouldLogTime) {
- if (shouldLogTime)
- chrome.send('metricsHandler:logEventTime', [name]);
- eventLog.push([name, Date.now()]);
- }
-
- logEvent('Tab.NewTabScriptStart', true);
- window.addEventListener('load', function(e) {
- logEvent('Tab.NewTabOnload', true);
- });
- document.addEventListener('DOMContentLoaded', function(e) {
- logEvent('Tab.NewTabDOMContentLoaded', true);
- });
- </script>
-
- <!-- TODO(jeremycho): This is only used for notifications. Discuss with UX. -->
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- .bubble {
- position: absolute;
- white-space: normal;
- /* Height is dynamic, width fixed. */
- width: 300px;
- z-index: 9999;
- }
-
- .bubble-content {
- color: black;
- left: 1px;
- line-height: 150%;
- padding: 8px 11px 12px;
- position: relative;
- right: 1px;
- top: 1px;
- width: 298px;
- z-index: 3;
- }
-
- /* When the close button is there, we need more padding on the right of the
- * bubble. */
- .bubble-close:not([hidden]) ~ .bubble-content {
- -webkit-padding-end: 22px;
- }
-
- .bubble-close {
- background-image: no-repeat 50% 50%;
- height: 16px;
- position: absolute;
- right: 6px;
- top: 6px;
- width: 16px;
- z-index: 4;
- }
-
- html[dir='rtl'] .bubble-close {
- left: 6px;
- right: auto;
- }
-
- .bubble-close {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==') 1x);
- }
-
- .bubble-close:hover {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==')
- 2x);
- }
-
- .bubble-close:active {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAARElEQVQoz2P4z4AfMlBLAYMdwxkghgEwD1XBGTC0g0sDIaYJECVwFqoChBK4WegKkJWArSJZAQErCDqSKG/iCyhaRhYA9LDIbULDzlIAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC')
- 2x);
- }
-
- .bubble-shadow {
- bottom: -2px;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
- left: 0;
- position: absolute;
- right: 0;
- top: 0;
- z-index: 1;
- }
-
- .bubble-arrow {
- -webkit-transform: rotate(45deg);
- box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.15);
- height: 15px;
- position: absolute;
- width: 15px;
- z-index: 2;
- }
-
- .bubble-content,
- .bubble-arrow {
- background: white;
- }
-
- .bubble-shadow,
- .bubble-arrow {
- border: 1px solid rgba(0, 0, 0, 0.3);
- }
-
- .bubble-shadow,
- .bubble-content {
- border-radius: 6px;
- box-sizing: border-box;
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- .expandable-bubble {
- -webkit-border-image: url('chrome://theme/IDR_APP_NOTIFICATION_SMALL_BUBBLE')
- 5 5 7 6 stretch;
- -webkit-box-sizing: border-box;
- -webkit-user-select: none;
- border-width: 5px 5px 7px 6px;
- color: #444;
- cursor: pointer;
- display: inline-block;
- font-size: 12px;
- position: absolute;
- z-index: 1;
- }
-
- .expandable-bubble::after {
- bottom: -1px;
- content: url('chrome://theme/IDR_APP_NOTIFICATION_NUB');
- display: block;
- height: 7px;
- position: absolute;
- right: 5px; /* TODO(finnur): Need to handle RTL properly. */
- width: 9px;
- }
-
- .expandable-bubble > .expandable-bubble-contents > .expandable-bubble-title {
- display: inline-block;
- margin-left: 1px;
- margin-top : -3px;
- overflow: hidden;
- white-space: nowrap;
- }
-
- .expandable-bubble[masked] > .expandable-bubble-contents >
- .expandable-bubble-title::after {
- content: url('chrome://theme/IDR_APP_NOTIFICATION_NUB_MASK');
- display: block;
- height: 15px;
- overflow: hidden;
- position: absolute;
- right: 0;
- top: 0;
- width: 12px;
- }
-
- .expandable-bubble[expanded] > .expandable-bubble-contents >
- .expandable-bubble-title {
- font-size: 13px;
- margin-bottom: 3px;
- margin-left: 0;
- }
-
- .expandable-bubble-close {
- background-image: no-repeat 50% 50%;
- height: 16px;
- position: absolute;
- right: 0;
- top: 0;
- width: 16px;
- z-index: 2;
- }
-
- .expandable-bubble[expanded] {
- padding: 3px;
- z-index: 3; /* One higher then the close button on an unexpanded bubble. */
- }
-
- .expandable-bubble[expanded] > .expandable-bubble-close {
- z-index: 4;
- }
-
- .expandable-bubble-close {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==') 1x);
- }
-
- .expandable-bubble-close:hover {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==')
- 2x);
- }
-
- .expandable-bubble-close:active {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAARElEQVQoz2P4z4AfMlBLAYMdwxkghgEwD1XBGTC0g0sDIaYJECVwFqoChBK4WegKkJWArSJZAQErCDqSKG/iCyhaRhYA9LDIbULDzlIAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC')
- 2x);
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- menu {
- -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, .50);
- background: white;
- color: black;
- cursor: default;
- left: 0;
- margin: 0;
- outline: 1px solid rgba(0, 0, 0, 0.2);
- padding: 8px 0;
- position: fixed;
- white-space: nowrap;
- z-index: 3;
- }
-
- menu:not(.decorated) {
- display: none;
- }
-
- menu > * {
- box-sizing: border-box;
- display: block;
- margin: 0;
- text-align: start;
- width: 100%;
- }
-
- menu > :not(hr) {
- -webkit-appearance: none;
- background: transparent;
- border: 0;
- font: inherit;
- line-height: 18px;
- overflow: hidden;
- padding: 0 19px;
- text-overflow: ellipsis;
- }
-
- menu > hr {
- background: -webkit-linear-gradient(left,
- rgba(0, 0, 0, .10),
- rgba(0, 0, 0, .02) 96%);
- border: 0;
- height: 1px;
- margin: 8px 0;
- }
-
- menu > [disabled] {
- color: rgba(0, 0, 0, .3);
- }
-
- menu > [hidden] {
- display: none;
- }
-
- menu > :not(hr)[selected] {
- background-color: rgb(220, 229, 250);
- }
-
- menu > :not(hr)[selected]:active {
- background-color: rgb(66, 109, 201);
- color: #fff;
- }
-
- menu > [checked]::before {
- content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAARklEQVQY02NgwA+MgViQkIJ3QKzEAFVpjEPBf5giJaiAMRYF72DWKSEJlKMpgNsgiCTxH5sCBhxWGOPzzV2sCv7//08QAwAUfjKK4sDXvQAAAABJRU5ErkJggg==');
- display: inline-block;
- height: 9px;
- margin: 0 5px;
- vertical-align: 50%;
- width: 9px;
- }
-
- menu > [checked] {
- -webkit-padding-start: 0;
- }
-
- menu > [selected][checked]:active::before {
- content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAATUlEQVQYV2P4//8/Ax5sDMSChBS8A2IlEEcQKoBNwX+YIiWoAEwhsgIQLQhTBBMoR1MA1gizDiYBA8gmM2BzA4oCZEUwhXfRFaArwokBBR4JwRMmHQoAAAAASUVORK5CYII=');
- }
-
- /* TODO(zvorygin) menu > [shortcutText]::after - this selector is much better,
- * but it's buggy in current webkit revision, so I have to use [showShortcuts].
- */
- menu[showShortcuts] > ::after {
- -webkit-padding-start: 30px;
- color: #999;
- content: attr(shortcutText);
- float: right;
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- /* NOTE: If you are using the drop-down style, you must first call
- * MenuButton.createDropDownArrows() to initialize the CSS canvases that
- * contain the arrow images. */
-
- button.menu-button.drop-down {
- background: white -webkit-canvas(drop-down-arrow) no-repeat center 4px;
- border: 1px solid rgb(192, 195, 198);
- border-radius: 2px;
- height: 12px;
- margin: 0 5px;
- padding: 0;
- position: relative;
- top: 1px;
- width: 12px;
- }
-
- button.menu-button.drop-down:hover {
- background-image: -webkit-canvas(drop-down-arrow-hover);
- border-color: rgb(48, 57, 66);
- }
-
- button.menu-button.drop-down[menu-shown],
- button.menu-button.drop-down:focus {
- background-color: rgb(48, 57, 66);
- background-image: -webkit-canvas(drop-down-arrow-active);
- border-color: rgb(48, 57, 66);
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- /* This file defines styles for form controls. The order of rule blocks is
- * important as there are some rules with equal specificity that rely on order
- * as a tiebreaker. These are marked with OVERRIDE. */
-
- /* Default state **************************************************************/
-
- :-webkit-any(button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button),
- select,
- input[type='checkbox'],
- input[type='radio'] {
- -webkit-appearance: none;
- -webkit-user-select: none;
- background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
- border: 1px solid rgba(0, 0, 0, 0.25);
- border-radius: 2px;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08),
- inset 0 1px 2px rgba(255, 255, 255, 0.75);
- color: #444;
- font: inherit;
- margin: 0 1px 0 0;
- text-shadow: 0 1px 0 rgb(240, 240, 240);
- }
-
- :-webkit-any(button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button),
- select {
- min-height: 2em;
- min-width: 4em;
- /* The following platform-specific rule is necessary to get adjacent
- * buttons, text inputs, and so forth to align on their borders while also
- * aligning on the text's baselines. */
- padding-bottom: 1px;
- }
-
- :-webkit-any(button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button) {
- -webkit-padding-end: 10px;
- -webkit-padding-start: 10px;
- }
-
- select {
- -webkit-appearance: none;
- -webkit-padding-end: 20px;
- -webkit-padding-start: 6px;
- /* OVERRIDE */
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'),
- -webkit-linear-gradient(#ededed, #ededed 38%, #dedede);
- background-position: right center;
- background-repeat: no-repeat;
- }
-
- html[dir='rtl'] select {
- background-position: center left;
- }
-
- input[type='checkbox'] {
- bottom: 2px;
- height: 13px;
- position: relative;
- vertical-align: middle;
- width: 13px;
- }
-
- input[type='radio'] {
- /* OVERRIDE */
- border-radius: 100%;
- bottom: 3px;
- height: 15px;
- position: relative;
- vertical-align: middle;
- width: 15px;
- }
-
- /* TODO(estade): add more types here? */
- input[type='password'],
- input[type='search'],
- input[type='text'],
- input[type='url'],
- input:not([type]),
- textarea {
- border: 1px solid #bfbfbf;
- border-radius: 2px;
- box-sizing: border-box;
- color: #444;
- font: inherit;
- margin: 0;
- /* Use min-height to accommodate addditional padding for touch as needed. */
- min-height: 2em;
- padding: 3px;
- /* For better alignment between adjacent buttons and inputs. */
- padding-bottom: 4px;
- }
-
- input[type='search'] {
- -webkit-appearance: textfield;
- /* NOTE: Keep a relatively high min-width for this so we don't obscure the end
- * of the default text in relatively spacious languages (i.e. German). */
- min-width: 160px;
- }
-
- /* Remove when https://bugs.webkit.org/show_bug.cgi?id=51499 is fixed.
- * TODO(dbeam): are there more types that would benefit from this? */
- input[type='search']::-webkit-textfield-decoration-container {
- direction: inherit;
- }
-
- /* Checked ********************************************************************/
-
- input[type='checkbox']:checked::before {
- -webkit-user-select: none;
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAcklEQVQY02NgwA/YoJgoEA/Es4DYgJBCJSBeD8SboRinBiYg7kZS2IosyQ/Eakh8LySFq4FYHFlxGRBvBOJYqMRqJMU+yApNkSRAeC0Sux3dfSCTetE0wKyXxOWhMKhTYIr9CAUXyJMzgLgBagBBgDPGAI2LGdNt0T1AAAAAAElFTkSuQmCC');
- background-size: 100% 100%;
- content: '';
- display: block;
- height: 100%;
- width: 100%;
- }
-
- html[dir='rtl'] input[type='checkbox']:checked::before {
- -webkit-transform: scaleX(-1);
- }
-
- input[type='radio']:checked::before {
- background-color: #666;
- border-radius: 100%;
- bottom: 3px;
- content: '';
- display: block;
- left: 3px;
- position: absolute;
- right: 3px;
- top: 3px;
- }
-
- /* Hover **********************************************************************/
-
- :enabled:hover:-webkit-any(
- select,
- input[type='checkbox'],
- input[type='radio'],
- :-webkit-any(
- button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button)) {
- background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- border-color: rgba(0, 0, 0, 0.3);
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12),
- inset 0 1px 2px rgba(255, 255, 255, 0.95);
- color: black;
- }
-
- :enabled:hover:-webkit-any(select) {
- /* OVERRIDE */
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'),
- -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0);
- }
-
- /* Active *********************************************************************/
-
- :enabled:active:-webkit-any(
- select,
- input[type='checkbox'],
- input[type='radio'],
- :-webkit-any(
- button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button)) {
- background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- box-shadow: none;
- text-shadow: none;
- }
-
- :enabled:active:-webkit-any(select) {
- /* OVERRIDE */
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'),
- -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7);
- }
-
- /* Disabled *******************************************************************/
-
- :disabled:-webkit-any(
- button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button),
- select:disabled {
- background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
- border-color: rgba(80, 80, 80, 0.2);
- box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08),
- inset 0 1px 2px rgba(255, 255, 255, 0.75);
- color: #aaa;
- }
-
- select:disabled {
- /* OVERRIDE */
- background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEIGhoa/uPCIHmiDQNihQULFizEZhBIHCRPijexGggzCCpPvGHoBiIbRJZhMAPfvn3bhGwQ2YZBNbJiEcPAAIgGZrTRc1ZLAAAAAElFTkSuQmCC'),
- -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6);
- }
-
- input:disabled:-webkit-any([type='checkbox'],
- [type='radio']) {
- opacity: .75;
- }
-
- input:disabled:-webkit-any([type='password'],
- [type='search'],
- [type='text'],
- [type='url'],
- :not([type])) {
- color: #999;
- }
-
- /* Focus **********************************************************************/
-
- :enabled:focus:-webkit-any(
- select,
- input[type='checkbox'],
- input[type='password'],
- input[type='radio'],
- input[type='search'],
- input[type='text'],
- input[type='url'],
- input:not([type]),
- :-webkit-any(
- button,
- input[type='button'],
- input[type='submit']):not(.custom-appearance):not(.link-button)) {
- /* OVERRIDE */
- -webkit-transition: border-color 200ms;
- /* We use border color because it follows the border radius (unlike outline).
- * This is particularly noticeable on mac. */
- border-color: rgb(77, 144, 254);
- outline: none;
- }
-
- /* Link buttons ***************************************************************/
-
- .link-button {
- -webkit-box-shadow: none;
- background: transparent none;
- border: none;
- color: rgb(17, 85, 204);
- cursor: pointer;
- /* Input elements have -webkit-small-control which can override the body font.
- * Resolve this by using 'inherit'. */
- font: inherit;
- margin: 0;
- padding: 0 4px;
- }
-
- .link-button:hover {
- text-decoration: underline;
- }
-
- .link-button:active {
- color: rgb(5, 37, 119);
- text-decoration: underline;
- }
-
- .link-button[disabled] {
- color: #999;
- cursor: default;
- text-decoration: none;
- }
-
- /* Checkbox/radio helpers ******************************************************
- *
- * .checkbox and .radio classes wrap labels. Checkboxes and radios should use
- * these classes with the markup structure:
- *
- * <div class="checkbox">
- * <label>
- * <input type="checkbox"></input>
- * <span>
- * </label>
- * </div>
- */
-
- :-webkit-any(.checkbox, .radio) label {
- /* Don't expand horizontally: <http://crbug.com/112091>. */
- display: -webkit-inline-box;
- padding-bottom: 7px;
- padding-top: 7px;
- }
-
- :-webkit-any(.checkbox, .radio) label input ~ span {
- -webkit-margin-start: 0.6em;
- /* Make sure long spans wrap at the same horizontal position they start. */
- display: block;
- }
-
- :-webkit-any(.checkbox, .radio) label:hover {
- color: black;
- }
-
- label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span {
- color: #999;
- }
- </style>
-
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- .close-button {
- background: no-repeat;
- background-color: transparent;
- /* TODO(estade): this should animate between states. */
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==') 1x);
- border: 0;
- cursor: default;
- display: inline-block;
- height: 16px;
- padding: 0;
- width: 16px;
- z-index: 999;
- }
-
- .close-button:hover,
- .close-button:focus {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAB4UlEQVR42u2VsWoCQRBAh+MUFP0C1V9QD4NEOxs9xBQHQVCwSJFWVBAtBNXCxk6wTkBJYUTwEwQLC61E8QP0NzZzt5g5726DkC7EYWHZ8T3WndkV2C/jLwn4hwVYBIdLn9vkLp79QcBCTDMiy3w2gQ9XeTYkEHA8vqj2rworXu3HF1YFfSWgp5QFnKVLvYvzDEKEZ5hW70oXOCtcEbQLIkx7+IQtfMBSOjU6XEF4oyOdYInZbXyOuajjDlpNeQgleIUJKUz4BDMledhqOu/AzVSmzZ49CUjCC0yvim98iqtJT2L2jKsqczsdok9XrHNexaww415lnTNwn6CM/KxJIR8bnUZHPhLO6yMoIyk2pNjLewFuE5AiY1KMMQx8Q7hQYFek4AkjxXFe1rsF84I/BTFQMGL+1Lxwl4DwdtM1gjwKohgxyLtG7SYpxALqugOMcfOKN+bFXeBsLB1uulNcRqq7/tt36k41zoL6QlxGjtd6lrahiqCi1iOFYyvXuxY8yzK33VnvUivbLlOlj/jktm0s3YnXrNIXXufHNxuOGasi8S68zkwrlnV8ZcJJsTIUxbLgQcFZWE8N0gau2p40VVcM0gYeFpSRK6445UhBuKiRgiyKw+34rLt59nb1/7+RwReVkaFtqvNBuwAAAABJRU5ErkJggg==')
- 2x);
- }
-
- .close-button:active {
- background-image: -webkit-image-set(
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAARElEQVQoz2P4z4AfMlBLAYMdwxkghgEwD1XBGTC0g0sDIaYJECVwFqoChBK4WegKkJWArSJZAQErCDqSKG/iCyhaRhYA9LDIbULDzlIAAAAASUVORK5CYII=')
- 1x,
- url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAA/ElEQVR4Xu3UsWrCUBiG4efGlIBoIMFbcnYolYJ3pg4iKGrGYFTRwaUFhYAekiDt0EG++X2W83N8/3J/DbwBMJJSsdQItcDY1VlCOImzq3Ed8OmicHASB3ns5KBw8VUNpDJrW7uAiJ3sbK1l0mqArpmFTUlQ5jYWZrrUAUSmT0SZm4qoA56JvVhs/5g3A7RLolA85A1ASOTye65NMxASK6syfxGITMzvMxG9CvRkliWwlOm9AsSOcitzU1NzK7mjuBkQvHtLK7iLBiB5PhttJSGpB8I8vM6kDuiHeUjoVwMfYR4SRtUAw1veIZzOjRhSBzCoyKFjgH/3K7+BHzg+Cgw0eSW3AAAAAElFTkSuQmCC')
- 2x);
- }
-
- /* TODO(pedrosimonetti): Organize these rules. */
- .thumbnail .close-button {
- -webkit-transition: opacity 500ms ease-in-out;
- opacity: 0;
- position: absolute;
- right: 2px;
- top: 2px;
- }
-
- html[dir=rtl] .thumbnail .close-button {
- left: 0;
- right: auto;
- }
-
- .thumbnail:hover .close-button {
- -webkit-transition-delay: 500ms;
- opacity: 1;
- }
-
- .thumbnail .close-button:hover {
- -webkit-transition: none;
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- #dot-list {
- height: 15px;
- margin: 0;
- overflow: hidden;
- padding: 0;
- }
-
- .dot {
- -webkit-transition: color 200ms;
- color: #666;
- cursor: pointer;
- display: inline-block;
- font-size: 1.0833em;
- margin: 0 1em;
- min-width: 55px;
- outline: none;
- text-shadow: 0 1px 0 rgba(255, 255, 255, .7);
- white-space: nowrap;
- }
-
- .dot.selected {
- color: rgb(221, 75, 57);
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- /* -----------------------------------------------------------------------------
- General Styles
- ----------------------------------------------------------------------------- */
-
- html {
- /* It's necessary to put this here instead of in body in order to get the
- background-size of 100% to work properly */
- height: 100%;
- overflow: hidden;
- }
-
- body {
- /* Don't highlight links when they're tapped. Safari has bugs here that
- show up as flicker when dragging in some situations */
- -webkit-tap-highlight-color: transparent;
- /* Don't allow selecting text - can occur when dragging */
- -webkit-user-select: none;
- background-size: auto 100%;
- /* TODO(pedrosimonetti): Confirm with designers/engineers what do we want
- * to do regarding font family, once AFAIK we want to use Arial always. */
- font-family: Arial;
- margin: 0;
- overflow: hidden;
- padding: 0;
- }
-
- /* [hidden] does display:none, but its priority is too low in some cases. */
- [hidden] {
- display: none !important;
- }
-
- #notification-container {
- height: 18px;
- padding: 10px 0;
- }
-
- #notification {
- display: inline-block;
- font-weight: bold;
- white-space: nowrap;
- }
-
- #notification > div > div,
- #notification > div {
- display: inline-block;
- }
-
- /* NOTE: This is in the probable case that we start stuffing 16x16 data URI'd
- * icons in the promo notification responses. */
- #notification > span > img {
- margin-bottom: -3px;
- }
-
- #notification .close-button {
- -webkit-margin-start: 8px;
- vertical-align: top;
- }
-
- .link-button {
- -webkit-margin-start: 0.5em;
- }
-
- #bottom-panel {
- -webkit-transition: opacity 200ms;
- position: absolute;
- top: 300px;
- visibility: hidden;
- width: 100%;
- }
-
- .hide-bottom-panel {
- opacity: 0;
- }
-
- #card-slider-frame {
- height: 100px;
- overflow: hidden;
- }
-
- #page-list {
- display: -webkit-box;
- height: 100%;
- position: static;
- }
-
- #bottom-panel-header,
- #bottom-panel-footer,
- #bottom-panel-toolbar {
- margin: 0 auto;
- text-align: center;
- }
-
- #bottom-panel-header {
- padding: 10px 0 20px;
- }
-
- #bookmark-bar-spacer {
- height: 48px;
- }
-
- #promo-bubble-anchor {
- bottom: 50px;
- height: 10px;
- left: 0;
- position: absolute;
- width: 90px;
- }
-
- .bubble .bubble-arrow {
- display: none;
- }
-
- /* TODO(jeremycho): Figure out if we need this. */
- #attribution {
- bottom: 0;
- left: auto;
- margin-left: 8px;
- /* Leave room for the scrollbar. */
- margin-right: 13px;
- position: absolute;
- right: 0;
- text-align: left;
- z-index: -5;
- }
-
- /* For themes that right-align their images, we flip the attribution to the
- * left to avoid conflicts. We also do this for bare-minimum mode since there
- * can be conflicts with the recently closed menu. */
- html[themegravity='right'] #attribution,
- body.bare-minimum #attribution,
- html[dir='rtl'] #attribution {
- left: 0;
- right: auto;
- text-align: right;
- }
-
- #attribution > span {
- display: block;
- }
-
- .starting-up * {
- -webkit-transition: none !important;
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- /* -----------------------------------------------------------------------------
- Thumbnail Cell and Tile
- ----------------------------------------------------------------------------- */
-
- .thumbnail-page .tile-cell {
- -webkit-margin-start: 12px;
- height: 68px;
- margin-bottom: 12px;
- width: 110px;
- }
-
- .thumbnail-page .tile {
- background: #fff;
- border: 1px solid rgb(192, 192, 192);
- border-radius: 2px;
- box-shadow: 0 1px 0 rgba(255, 255, 255, .7);
- display: block;
- height: 66px;
- outline: none; /* Avoids outline glitch when blacklisting a thumbnail. */
- position: absolute;
- width: 108px;
- }
-
- .thumbnail-page .tile:hover,
- .thumbnail-page .tile:hover .thumbnail-card {
- /* TODO(pedrosimonetti): Confirm value with Marcin. */
- border-color: rgb(127, 127, 127);
- }
-
- .thumbnail-page .tile-cell.filler .tile {
- background: -webkit-linear-gradient(rgb(242, 242, 242), rgb(232, 232, 232));
- border-color: rgb(224, 224, 224);
- border-radius: 3px;
- box-shadow: inset 0 2px 3px rgba(0, 0, 0, .09);
- }
-
- /* -----------------------------------------------------------------------------
- Thumbnail
- ----------------------------------------------------------------------------- */
-
- .thumbnail .thumbnail-image {
- /* These values are equivalent to background-size: 100%.
- TODO(jeremycho): Resolve the discrepancy with the cell dimensions above. */
- background-size: 110px 68px;
- }
-
- .thumbnail .title,
- .thumbnail-banner {
- color: #777;
- font-size: 0.9167em;
- overflow: hidden;
- position: absolute;
- text-align: center;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 100%;
- }
-
- .thumbnail .title {
- bottom: -26px;
- }
-
- .thumbnail-banner {
- bottom: 25px;
- display: block;
- font-size: 1.14em;
- margin: 0 7px;
- width: 88%;
- }
-
- .thumbnail,
- .thumbnail-wrapper,
- .thumbnail-card {
- -webkit-background-clip: padding-box;
- }
-
- .thumbnail-wrapper {
- display: block;
- height: 100%;
- overflow: hidden;
- width: 100%;
- }
-
- .thumbnail-favicon {
- height: 16px;
- margin: 0 auto;
- position: relative;
- top: -8px;
- width: 16px;
- }
-
- .thumbnail-card {
- /* This gives a 3px offset between consecutive thumbnails on the stack and
- should be kept in sync with RecentlyClosed's STACK_OFFSET. */
- -webkit-margin-start: -129px;
- border: 1px solid silver;
- border-radius: 2px;
- display: inline-block;
- height: 100%;
- margin-top: -1px;
- position: relative;
- width: 100%;
- }
-
- .thumbnail-card:first-child {
- -webkit-margin-start: -1px;
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- /* -----------------------------------------------------------------------------
- Tile Page
- ----------------------------------------------------------------------------- */
-
- .tile-page {
- overflow: hidden;
- position: relative;
- }
-
- .tile-page-frame {
- margin: 0 auto;
- overflow: hidden;
- position: relative;
- width: 748px;
- }
-
- .tile-page-content {
- overflow: hidden;
- }
-
- .tile-grid {
- display: block;
- margin: 0 auto;
- width: 732px;
- }
-
- .tile-grid-content {
- -webkit-transform: translate3d(0, 0, 0);
- -webkit-transition: -webkit-transform 200ms;
- }
-
- .tile-row {
- -webkit-transition: opacity 200ms;
- height: 100px;
- text-align: start;
- white-space: nowrap;
- }
-
- /* -----------------------------------------------------------------------------
- Tile Grid Animation
- ----------------------------------------------------------------------------- */
-
- .animate-grid-width {
- -webkit-transform: translate3d(0, 0, 0);
- -webkit-transition: width 200ms;
- }
-
- /* -----------------------------------------------------------------------------
- Tile Cell
- ----------------------------------------------------------------------------- */
-
- .tile-cell {
- -webkit-transform: translate3d(0, 0, 0);
- display: inline-block;
- position: relative;
- }
-
- .tile-cell:first-child {
- -webkit-margin-start: 0;
- }
-
- /* -----------------------------------------------------------------------------
- Tile Cell Animation
- ----------------------------------------------------------------------------- */
-
- .animate-grid-width .tile-cell {
- -webkit-transition: margin 200ms;
- -webkit-transition-property: margin, opacity, width;
- }
-
- /* Animates entire columns of Tiles at once.*/
- .hide-col-0 .tile-cell:nth-child(1),
- .hide-col-1 .tile-cell:nth-child(2),
- .hide-col-2 .tile-cell:nth-child(3),
- .hide-col-3 .tile-cell:nth-child(4),
- .hide-col-4 .tile-cell:nth-child(5),
- .hide-col-5 .tile-cell:nth-child(6),
- .hide-col-6 .tile-cell:nth-child(7),
- .hide-col-7 .tile-cell:nth-child(8),
- .hide-col-8 .tile-cell:nth-child(9),
- .hide-col-9 .tile-cell:nth-child(10) {
- -webkit-margin-end: -10px;
- opacity: 0;
- width: 10px !important;
- }
-
- /* -----------------------------------------------------------------------------
- Tile Position Animation
- ----------------------------------------------------------------------------- */
-
- .animate-tile-repositioning .tile {
- -webkit-transition-duration: 200ms;
- -webkit-transition-property: -webkit-transform, left, opacity, top;
- position: absolute;
- }
-
- .animate-tile-repositioning .tile:not(.target-tile) {
- -webkit-transition-duration: 400ms;
- }
-
- .animate-tile-repositioning.undo-removal .target-tile {
- -webkit-transition-delay: 200ms;
- }
-
- .animate-tile-repositioning .animate-hide-tile {
- opacity: 0;
- }
-
- .animate-tile-repositioning .animate-hide-tile.target-tile {
- -webkit-transform: scale(0.5);
- }
-
- /* -----------------------------------------------------------------------------
- Scroll Bars
- ----------------------------------------------------------------------------- */
-
- .scrollable {
- overflow-y: auto;
- }
-
- .scrollable .shadow-top {
- -webkit-margin-end: 0;
- -webkit-mask-box-image: -webkit-linear-gradient(left,
- rgba(0, 0, 0, 0.1),
- rgba(0, 0, 0, 0.8),
- rgba(0, 0, 0, 0.1));
- background: -webkit-linear-gradient(top,
- rgba(0, 0, 0, 0.2),
- transparent);
- height: 6px;
- left: 0;
- opacity: 0;
- position: absolute;
- top: 0;
- width: 100%;
- z-index: 1000;
- }
-
- .scrollable .shadow-top::after {
- border-top: 1px solid rgba(0, 0, 0, 0.3);
- content: '';
- display: block;
- height: 0;
- left: 0;
- position: absolute;
- top: 0;
- width: 100%;
- }
-
- .scrollable .shadow-bottom {
- -webkit-margin-end: 0;
- -webkit-mask-box-image: -webkit-linear-gradient(left,
- rgba(0, 0, 0, 0.1),
- rgba(0, 0, 0, 0.8),
- rgba(0, 0, 0, 0.1));
- background: -webkit-linear-gradient(bottom,
- rgba(0, 0, 0, 0.2),
- transparent);
- bottom: 0;
- height: 4px;
- left: 0;
- opacity: 1;
- position: absolute;
- width: 100%;
- z-index: 1000;
- }
-
- .scrollable .shadow-bottom::after {
- border-bottom: 1px solid rgba(0, 0, 0, 0.3);
- bottom: 0;
- content: '';
- display: block;
- height: 0;
- left: 0;
- position: absolute;
- width: 100%;
- }
-
- ::-webkit-scrollbar {
- height: 12px;
- width: 12px;
- }
-
- ::-webkit-scrollbar-button {
- height: 0;
- width: 0;
- }
-
- ::-webkit-scrollbar-button:start:decrement,
- ::-webkit-scrollbar-button:end:increment {
- display: block;
- }
-
- ::-webkit-scrollbar-button:vertical:start:increment,
- ::-webkit-scrollbar-button:vertical:end:decrement {
- display: none;
- }
-
- ::-webkit-scrollbar-track:vertical {
- -webkit-border-end: none;
- -webkit-border-start: 5px solid transparent;
- background-clip: padding-box;
- background-color: white;
- }
-
- ::-webkit-scrollbar-track:horizontal {
- background-clip: padding-box;
- background-color: white;
- border-bottom: none;
- border-top: 5px solid transparent;
- }
-
- ::-webkit-scrollbar-thumb {
- -webkit-box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.10),
- inset 0 -1px 0 rgba(0, 0, 0, 0.07);
- background-clip: padding-box;
- background-color: rgba(0, 0, 0, 0.2);
- min-height: 28px;
- padding-top: 100px;
- }
-
- ::-webkit-scrollbar-thumb:hover {
- -webkit-box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
- background-color: rgba(0, 0, 0, 0.4);
- }
-
- ::-webkit-scrollbar-thumb:active {
- -webkit-box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
- background-color: rgba(0, 0, 0, 0.5);
- }
-
- ::-webkit-scrollbar-thumb:vertical {
- -webkit-border-end: none;
- -webkit-border-start: 5px solid transparent;
- border-bottom: none;
- border-top: none;
- }
-
- ::-webkit-scrollbar-thumb:horizontal {
- border: none;
- border-top: 5px solid transparent;
- }
-
- ::-webkit-scrollbar-track:hover {
- -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.10);
- background-color: rgba(0, 0, 0, 0.05);
- }
-
- ::-webkit-scrollbar-track:active {
- -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.14),
- inset -1px -1px 0 rgba(0, 0, 0, 0.07);
- background-color: rgba(0, 0, 0, 0.05);
- }
- </style>
- <style>/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file. */
-
- .apps-page .tile-cell {
- -webkit-margin-start: 20px;
- height: 70px;
- margin-bottom: 12px;
- width: 70px;
- }
-
- .apps-page .tile-cell:first-child {
- -webkit-margin-start: 0;
- }
-
- .apps-page .app {
- display: block;
- height: 70px;
- margin-top: 8px;
- outline: none;
- text-align: center;
- width: 70px;
- }
-
- .apps-page .app .title {
- color: #777;
- font-size: 0.9166em;
- left: -7px;
- margin-top: 4px;
- position: relative;
- width: 84px;
- }
-
- .app-contents {
- -webkit-transition: -webkit-transform 100ms;
- }
-
- .app-contents:active:not(.suppress-active),
- .app:not(.click-focus):focus .app-contents:not(.suppress-active),
- .drag-representation:not(.placing) .app-contents {
- -webkit-transform: scale(1.1);
- }
-
- /* Don't animate the initial scaling. */
- .app-contents:active:not(.suppress-active),
- /* Active gets applied right before .suppress-active, so to avoid flicker
- * we need to make the scale go back to normal without an animation. */
- .app-contents.suppress-active {
- -webkit-transition-duration: 0;
- }
-
- .app-contents > span {
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .app-img-container {
- /* -webkit-mask-image set by JavaScript to the image source. */
- -webkit-mask-size: 100% 100%;
- margin-left: auto;
- margin-right: auto;
- }
-
- .app-img-container > * {
- height: 100%;
- width: 100%;
- }
-
- .app-icon-div {
- -webkit-box-align: center;
- -webkit-box-pack: center;
- background-color: white;
- border: 1px solid #d5d5d5;
- border-radius: 5px;
- display: -webkit-box;
- margin-left: auto;
- margin-right: auto;
- position: relative;
- vertical-align: middle;
- z-index: 0;
- }
-
- .animate-tile-repositioning .app.small-icon {
- margin-top: 0;
- }
-
- .small-icon .app-icon-div {
- height: 50px;
- margin: 18px 9px 12px;
- width: 50px;
- }
-
- .small-icon .app-img-container {
- bottom: 7px;
- height: 16px;
- left: 5px;
- position: absolute;
- width: 16px;
- }
-
- .small-icon .color-stripe {
- border-bottom-left-radius: 5px 5px;
- border-bottom-right-radius: 5px 5px;
- bottom: 0;
- height: 3px;
- opacity: 1.0;
- position: absolute;
- width: 100%;
- z-index: 100;
- }
-
- .app-context-menu > button:first-child {
- font-weight: bold;
- }
-
- .app-context-menu {
- z-index: 1000;
- }
-
- .app-context-menu > [checked]::before {
- height: 5px;
- }
-
- .launch-click-target {
- cursor: pointer;
- }
-
- /* Notifications */
-
- .app-notification {
- -webkit-transition: color 150ms linear;
- color: #999;
- display: block;
- font-size: 0.9em;
- white-space: nowrap;
- }
-
- .app-notification:hover {
- text-decoration: underline;
- }
-
- .app-img-container > img:first-child {
- display: block;
- }
-
- .app .invisible {
- visibility: hidden;
- }
-
- /* Move the notification lower on apps pages to account for the 16px of
- * transparency each app icon should have. */
- .apps-page #notification-container {
- bottom: 15px;
- }
- </style>
-
- <link id="themecss" rel="stylesheet">
-
- <script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /** @fileoverview EventTracker is a simple class that manages the addition and
- * removal of DOM event listeners. In particular, it keeps track of all
- * listeners that have been added and makes it easy to remove some or all of
- * them without requiring all the information again. This is particularly
- * handy when the listener is a generated function such as a lambda or the
- * result of calling Function.bind.
- */
-
- // Use an anonymous function to enable strict mode just for this file (which
- // will be concatenated with other files when embedded in Chrome)
- var EventTracker = (function() {
- 'use strict';
-
- /**
- * Create an EventTracker to track a set of events.
- * EventTracker instances are typically tied 1:1 with other objects or
- * DOM elements whose listeners should be removed when the object is disposed
- * or the corresponding elements are removed from the DOM.
- * @constructor
- */
- function EventTracker() {
- /**
- * @type {Array.<EventTracker.Entry>}
- * @private
- */
- this.listeners_ = [];
- }
-
- /**
- * The type of the internal tracking entry.
- * @typedef {{node: !Node,
- * eventType: string,
- * listener: Function,
- * capture: boolean}}
- */
- EventTracker.Entry;
-
- EventTracker.prototype = {
- /**
- * Add an event listener - replacement for Node.addEventListener.
- * @param {!Node} node The DOM node to add a listener to.
- * @param {string} eventType The type of event to subscribe to.
- * @param {Function} listener The listener to add.
- * @param {boolean} capture Whether to invoke during the capture phase.
- */
- add: function(node, eventType, listener, capture) {
- var h = {
- node: node,
- eventType: eventType,
- listener: listener,
- capture: capture
- };
- this.listeners_.push(h);
- node.addEventListener(eventType, listener, capture);
- },
-
- /**
- * Remove any specified event listeners added with this EventTracker.
- * @param {!Node} node The DOM node to remove a listener from.
- * @param {?string} eventType The type of event to remove.
- */
- remove: function(node, eventType) {
- this.listeners_ = this.listeners_.filter(function(h) {
- if (h.node == node && (!eventType || (h.eventType == eventType))) {
- EventTracker.removeEventListener_(h);
- return false;
- }
- return true;
- });
- },
-
- /**
- * Remove all event listeners added with this EventTracker.
- */
- removeAll: function() {
- this.listeners_.forEach(EventTracker.removeEventListener_);
- this.listeners_ = [];
- }
- };
-
- /**
- * Remove a single event listener given it's tracker entry. It's up to the
- * caller to ensure the entry is removed from listeners_.
- * @param {EventTracker.Entry} h The entry describing the listener to remove.
- * @private
- */
- EventTracker.removeEventListener_ = function(h) {
- h.node.removeEventListener(h.eventType, h.listener, h.capture);
- };
-
- return EventTracker;
- })();
-
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview This file defines a singleton which provides access to all data
- * that is available as soon as the page's resources are loaded (before DOM
- * content has finished loading). This data includes both localized strings and
- * any data that is important to have ready from a very early stage (e.g. things
- * that must be displayed right away).
- */
-
- var loadTimeData;
-
- (function() {
- 'use strict';
-
- function LoadTimeData() {
- }
-
- LoadTimeData.prototype = {
- /**
- * Sets the backing object.
- * @param {Object} value The de-serialized page data.
- */
- set data(value) {
- expect(!this.data_, 'Re-setting data.');
- this.data_ = value;
- },
-
- /**
- * @return {boolean} True if |id| is a key in the dictionary.
- */
- valueExists: function(id) {
- return id in this.data_;
- },
-
- /**
- * Fetches a value, expecting that it exists.
- * @param {string} id The key that identifies the desired value.
- * @return {*} The corresponding value.
- */
- getValue: function(id) {
- expect(this.data_, 'No data. Did you remember to include strings.js?');
- var value = this.data_[id];
- expect(typeof value != 'undefined', 'Could not find value for ' + id);
- return value;
- },
-
- /**
- * As above, but also makes sure that the value is a string.
- * @param {string} id The key that identifies the desired string.
- * @return {string} The corresponding string value.
- */
- getString: function(id) {
- var value = this.getValue(id);
- expectIsType(id, value, 'string');
- return value;
- },
-
- /**
- * Returns a formatted localized string where $1 to $9 are replaced by the
- * second to the tenth argument.
- * @param {string} id The ID of the string we want.
- * @param {...string} The extra values to include in the formatted output.
- * @return {string} The formatted string.
- */
- getStringF: function(id) {
- var value = this.getString(id);
- if (!value)
- return;
-
- var varArgs = arguments;
- return value.replace(/\$[$1-9]/g, function(m) {
- return m == '$$' ? '$' : varArgs[m[1]];
- });
- },
-
- /**
- * As above, but also makes sure that the value is a boolean.
- * @param {string} id The key that identifies the desired boolean.
- * @return {boolean} The corresponding boolean value.
- */
- getBoolean: function(id) {
- var value = this.getValue(id);
- expectIsType(id, value, 'boolean');
- return value;
- },
-
- /**
- * As above, but also makes sure that the value is an integer.
- * @param {string} id The key that identifies the desired number.
- * @return {number} The corresponding number value.
- */
- getInteger: function(id) {
- var value = this.getValue(id);
- expectIsType(id, value, 'number');
- expect(value == Math.floor(value), 'Number isn\'t integer: ' + value);
- return value;
- },
-
- /**
- * Override values in loadTimeData with the values found in |replacements|.
- * @param {Object} replacements The dictionary object of keys to replace.
- */
- overrideValues: function(replacements) {
- expect(typeof replacements == 'object',
- 'Replacements must be a dictionary object.');
- for (var key in replacements) {
- this.data_[key] = replacements[key];
- }
- }
- };
-
- /**
- * Checks condition, displays error message if expectation fails.
- * @param {*} condition The condition to check for truthiness.
- * @param {string} message The message to display if the check fails.
- */
- function expect(condition, message) {
- if (!condition)
- console.error(message);
- }
-
- /**
- * Checks that the given value has the given type.
- * @param {string} id The id of the value (only used for error message).
- * @param {*} value The value to check the type on.
- * @param {string} type The type we expect |value| to be.
- */
- function expectIsType(id, value, type) {
- expect(typeof value == type, '[' + value + '] (' + id +
- ') is not a ' + type);
- }
-
- expect(!loadTimeData, 'should only include this file once');
- loadTimeData = new LoadTimeData;
- })();
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * Parse a very small subset of HTML. This ensures that insecure HTML /
- * javascript cannot be injected into the new tab page.
- * @param {string} s The string to parse.
- * @param {Array.<string>=} opt_extraTags Optional extra allowed tags.
- * @param {Object.<string, function(Node, string):boolean>=} opt_extraAttrs
- * Optional extra allowed attributes (all tags are run through these).
- * @throws {Error} In case of non supported markup.
- * @return {DocumentFragment} A document fragment containing the DOM tree.
- */
- var parseHtmlSubset = (function() {
- 'use strict';
-
- var allowedAttributes = {
- 'href': function(node, value) {
- // Only allow a[href] starting with chrome:// and https://
- return node.tagName == 'A' && (value.indexOf('chrome://') == 0 ||
- value.indexOf('https://') == 0);
- },
- 'target': function(node, value) {
- // Allow a[target] but reset the value to "".
- if (node.tagName != 'A')
- return false;
- node.setAttribute('target', '');
- return true;
- }
- };
-
- /**
- * Whitelist of tag names allowed in parseHtmlSubset.
- * @type {!Array.<string>}
- * @const
- */
- var allowedTags = ['A', 'B', 'STRONG'];
-
- function merge() {
- var clone = {};
- for (var i = 0; i < arguments.length; ++i) {
- if (typeof arguments[i] == 'object') {
- for (var key in arguments[i]) {
- if (arguments[i].hasOwnProperty(key))
- clone[key] = arguments[i][key];
- }
- }
- }
- return clone;
- }
-
- function walk(n, f) {
- f(n);
- for (var i = 0; i < n.childNodes.length; i++) {
- walk(n.childNodes[i], f);
- }
- }
-
- function assertElement(tags, node) {
- if (tags.indexOf(node.tagName) == -1)
- throw Error(node.tagName + ' is not supported');
- }
-
- function assertAttribute(attrs, attrNode, node) {
- var n = attrNode.nodeName;
- var v = attrNode.nodeValue;
- if (!attrs.hasOwnProperty(n) || !attrs[n](node, v))
- throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
- }
-
- return function(s, opt_extraTags, opt_extraAttrs) {
- var extraTags =
- (opt_extraTags || []).map(function(str) { return str.toUpperCase(); });
- var tags = allowedTags.concat(extraTags);
- var attrs = merge(allowedAttributes, opt_extraAttrs || {});
-
- var r = document.createRange();
- r.selectNode(document.body);
- // This does not execute any scripts.
- var df = r.createContextualFragment(s);
- walk(df, function(node) {
- switch (node.nodeType) {
- case Node.ELEMENT_NODE:
- assertElement(tags, node);
- var nodeAttrs = node.attributes;
- for (var i = 0; i < nodeAttrs.length; ++i) {
- assertAttribute(attrs, nodeAttrs[i], node);
- }
- break;
-
- case Node.COMMENT_NODE:
- case Node.DOCUMENT_FRAGMENT_NODE:
- case Node.TEXT_NODE:
- break;
-
- default:
- throw Error('Node type ' + node.nodeType + ' is not supported');
- }
- });
- return df;
- };
- })();
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * The global object.
- * @type {!Object}
- * @const
- */
- var global = this;
-
- /**
- * Alias for document.getElementById.
- * @param {string} id The ID of the element to find.
- * @return {HTMLElement} The found element or null if not found.
- */
- function $(id) {
- return document.getElementById(id);
- }
-
- /**
- * Calls chrome.send with a callback and restores the original afterwards.
- * @param {string} name The name of the message to send.
- * @param {!Array} params The parameters to send.
- * @param {string} callbackName The name of the function that the backend calls.
- * @param {!Function} callback The function to call.
- */
- function chromeSend(name, params, callbackName, callback) {
- var old = global[callbackName];
- global[callbackName] = function() {
- // restore
- global[callbackName] = old;
-
- var args = Array.prototype.slice.call(arguments);
- return callback.apply(global, args);
- };
- chrome.send(name, params);
- }
-
- /**
- * Generates a CSS url string.
- * @param {string} s The URL to generate the CSS url for.
- * @return {string} The CSS url string.
- */
- function url(s) {
- // http://www.w3.org/TR/css3-values/#uris
- // Parentheses, commas, whitespace characters, single quotes (') and double
- // quotes (") appearing in a URI must be escaped with a backslash
- var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
- // WebKit has a bug when it comes to URLs that end with \
- // https://bugs.webkit.org/show_bug.cgi?id=28885
- if (/\\\\$/.test(s2)) {
- // Add a space to work around the WebKit bug.
- s2 += ' ';
- }
- return 'url("' + s2 + '")';
- }
-
- /**
- * Parses query parameters from Location.
- * @param {string} location The URL to generate the CSS url for.
- * @return {object} Dictionary containing name value pairs for URL
- */
- function parseQueryParams(location) {
- var params = {};
- var query = unescape(location.search.substring(1));
- var vars = query.split('&');
- for (var i = 0; i < vars.length; i++) {
- var pair = vars[i].split('=');
- params[pair[0]] = pair[1];
- }
- return params;
- }
-
- function findAncestorByClass(el, className) {
- return findAncestor(el, function(el) {
- if (el.classList)
- return el.classList.contains(className);
- return null;
- });
- }
-
- /**
- * Return the first ancestor for which the {@code predicate} returns true.
- * @param {Node} node The node to check.
- * @param {function(Node) : boolean} predicate The function that tests the
- * nodes.
- * @return {Node} The found ancestor or null if not found.
- */
- function findAncestor(node, predicate) {
- var last = false;
- while (node != null && !(last = predicate(node))) {
- node = node.parentNode;
- }
- return last ? node : null;
- }
-
- function swapDomNodes(a, b) {
- var afterA = a.nextSibling;
- if (afterA == b) {
- swapDomNodes(b, a);
- return;
- }
- var aParent = a.parentNode;
- b.parentNode.replaceChild(a, b);
- aParent.insertBefore(b, afterA);
- }
-
- /**
- * Disables text selection and dragging, with optional whitelist callbacks.
- * @param {function(Event):boolean=} opt_allowSelectStart Unless this function
- * is defined and returns true, the onselectionstart event will be
- * surpressed.
- * @param {function(Event):boolean=} opt_allowDragStart Unless this function
- * is defined and returns true, the ondragstart event will be surpressed.
- */
- function disableTextSelectAndDrag(opt_allowSelectStart, opt_allowDragStart) {
- // Disable text selection.
- document.onselectstart = function(e) {
- if (!(opt_allowSelectStart && opt_allowSelectStart.call(this, e)))
- e.preventDefault();
- };
-
- // Disable dragging.
- document.ondragstart = function(e) {
- if (!(opt_allowDragStart && opt_allowDragStart.call(this, e)))
- e.preventDefault();
- };
- }
-
- /**
- * Call this to stop clicks on <a href="#"> links from scrolling to the top of
- * the page (and possibly showing a # in the link).
- */
- function preventDefaultOnPoundLinkClicks() {
- document.addEventListener('click', function(e) {
- var anchor = findAncestor(e.target, function(el) {
- return el.tagName == 'A';
- });
- // Use getAttribute() to prevent URL normalization.
- if (anchor && anchor.getAttribute('href') == '#')
- e.preventDefault();
- });
- }
-
- /**
- * Check the directionality of the page.
- * @return {boolean} True if Chrome is running an RTL UI.
- */
- function isRTL() {
- return document.documentElement.dir == 'rtl';
- }
-
- /**
- * Simple common assertion API
- * @param {*} condition The condition to test. Note that this may be used to
- * test whether a value is defined or not, and we don't want to force a
- * cast to Boolean.
- * @param {string=} opt_message A message to use in any error.
- */
- function assert(condition, opt_message) {
- 'use strict';
- if (!condition) {
- var msg = 'Assertion failed';
- if (opt_message)
- msg = msg + ': ' + opt_message;
- throw new Error(msg);
- }
- }
-
- /**
- * Get an element that's known to exist by its ID. We use this instead of just
- * calling getElementById and not checking the result because this lets us
- * satisfy the JSCompiler type system.
- * @param {string} id The identifier name.
- * @return {!Element} the Element.
- */
- function getRequiredElement(id) {
- var element = $(id);
- assert(element, 'Missing required element: ' + id);
- return element;
- }
-
- // Handle click on a link. If the link points to a chrome: or file: url, then
- // call into the browser to do the navigation.
- document.addEventListener('click', function(e) {
- // Allow preventDefault to work.
- if (!e.returnValue)
- return;
-
- var el = e.target;
- if (el.nodeType == Node.ELEMENT_NODE &&
- el.webkitMatchesSelector('A, A *')) {
- while (el.tagName != 'A') {
- el = el.parentElement;
- }
-
- if ((el.protocol == 'file:' || el.protocol == 'about:') &&
- (e.button == 0 || e.button == 1)) {
- chrome.send('navigateToUrl', [
- el.href,
- el.target,
- e.button,
- e.altKey,
- e.ctrlKey,
- e.metaKey,
- e.shiftKey
- ]);
- e.preventDefault();
- }
- }
- });
-
- /**
- * Creates a new URL which is the old URL with a GET param of key=value.
- * @param {string} url The base URL. There is not sanity checking on the URL so
- * it must be passed in a proper format.
- * @param {string} key The key of the param.
- * @param {string} value The value of the param.
- * @return {string} The new URL.
- */
- function appendParam(url, key, value) {
- var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
-
- if (url.indexOf('?') == -1)
- return url + '?' + param;
- return url + '&' + param;
- }
-
- /**
- * Creates a new URL for a favicon request.
- * @param {string} url The url for the favicon.
- * @param {number=} opt_size Optional preferred size of the favicon.
- * @return {string} Updated URL for the favicon.
- */
- function getFaviconURL(url, opt_size) {
- var size = opt_size || 16;
- return 'chrome://favicon/size/' + size + '@' +
- window.devicePixelRatio + 'x/' + url;
- }
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * The global object.
- * @type {!Object}
- * @const
- */
- var global = this;
-
- /** Platform, package, object property, and Event support. **/
- this.cr = (function() {
- 'use strict';
-
- /**
- * Builds an object structure for the provided namespace path,
- * ensuring that names that already exist are not overwritten. For
- * example:
- * "a.b.c" -> a = {};a.b={};a.b.c={};
- * @param {string} name Name of the object that this file defines.
- * @param {*=} opt_object The object to expose at the end of the path.
- * @param {Object=} opt_objectToExportTo The object to add the path to;
- * default is {@code global}.
- * @private
- */
- function exportPath(name, opt_object, opt_objectToExportTo) {
- var parts = name.split('.');
- var cur = opt_objectToExportTo || global;
-
- for (var part; parts.length && (part = parts.shift());) {
- if (!parts.length && opt_object !== undefined) {
- // last part and we have an object; use it
- cur[part] = opt_object;
- } else if (part in cur) {
- cur = cur[part];
- } else {
- cur = cur[part] = {};
- }
- }
- return cur;
- };
-
- /**
- * Fires a property change event on the target.
- * @param {EventTarget} target The target to dispatch the event on.
- * @param {string} propertyName The name of the property that changed.
- * @param {*} newValue The new value for the property.
- * @param {*} oldValue The old value for the property.
- */
- function dispatchPropertyChange(target, propertyName, newValue, oldValue) {
- var e = new cr.Event(propertyName + 'Change');
- e.propertyName = propertyName;
- e.newValue = newValue;
- e.oldValue = oldValue;
- target.dispatchEvent(e);
- }
-
- /**
- * Converts a camelCase javascript property name to a hyphenated-lower-case
- * attribute name.
- * @param {string} jsName The javascript camelCase property name.
- * @return {string} The equivalent hyphenated-lower-case attribute name.
- */
- function getAttributeName(jsName) {
- return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
- }
-
- /**
- * The kind of property to define in {@code defineProperty}.
- * @enum {number}
- * @const
- */
- var PropertyKind = {
- /**
- * Plain old JS property where the backing data is stored as a "private"
- * field on the object.
- */
- JS: 'js',
-
- /**
- * The property backing data is stored as an attribute on an element.
- */
- ATTR: 'attr',
-
- /**
- * The property backing data is stored as an attribute on an element. If the
- * element has the attribute then the value is true.
- */
- BOOL_ATTR: 'boolAttr'
- };
-
- /**
- * Helper function for defineProperty that returns the getter to use for the
- * property.
- * @param {string} name The name of the property.
- * @param {cr.PropertyKind} kind The kind of the property.
- * @return {function():*} The getter for the property.
- */
- function getGetter(name, kind) {
- switch (kind) {
- case PropertyKind.JS:
- var privateName = name + '_';
- return function() {
- return this[privateName];
- };
- case PropertyKind.ATTR:
- var attributeName = getAttributeName(name);
- return function() {
- return this.getAttribute(attributeName);
- };
- case PropertyKind.BOOL_ATTR:
- var attributeName = getAttributeName(name);
- return function() {
- return this.hasAttribute(attributeName);
- };
- }
- }
-
- /**
- * Helper function for defineProperty that returns the setter of the right
- * kind.
- * @param {string} name The name of the property we are defining the setter
- * for.
- * @param {cr.PropertyKind} kind The kind of property we are getting the
- * setter for.
- * @param {function(*):void} opt_setHook A function to run after the property
- * is set, but before the propertyChange event is fired.
- * @return {function(*):void} The function to use as a setter.
- */
- function getSetter(name, kind, opt_setHook) {
- switch (kind) {
- case PropertyKind.JS:
- var privateName = name + '_';
- return function(value) {
- var oldValue = this[name];
- if (value !== oldValue) {
- this[privateName] = value;
- if (opt_setHook)
- opt_setHook.call(this, value, oldValue);
- dispatchPropertyChange(this, name, value, oldValue);
- }
- };
-
- case PropertyKind.ATTR:
- var attributeName = getAttributeName(name);
- return function(value) {
- var oldValue = this[name];
- if (value !== oldValue) {
- if (value == undefined)
- this.removeAttribute(attributeName);
- else
- this.setAttribute(attributeName, value);
- if (opt_setHook)
- opt_setHook.call(this, value, oldValue);
- dispatchPropertyChange(this, name, value, oldValue);
- }
- };
-
- case PropertyKind.BOOL_ATTR:
- var attributeName = getAttributeName(name);
- return function(value) {
- var oldValue = this[name];
- if (value !== oldValue) {
- if (value)
- this.setAttribute(attributeName, name);
- else
- this.removeAttribute(attributeName);
- if (opt_setHook)
- opt_setHook.call(this, value, oldValue);
- dispatchPropertyChange(this, name, value, oldValue);
- }
- };
- }
- }
-
- /**
- * Defines a property on an object. When the setter changes the value a
- * property change event with the type {@code name + 'Change'} is fired.
- * @param {!Object} obj The object to define the property for.
- * @param {string} name The name of the property.
- * @param {cr.PropertyKind=} opt_kind What kind of underlying storage to use.
- * @param {function(*):void} opt_setHook A function to run after the
- * property is set, but before the propertyChange event is fired.
- */
- function defineProperty(obj, name, opt_kind, opt_setHook) {
- if (typeof obj == 'function')
- obj = obj.prototype;
-
- var kind = opt_kind || PropertyKind.JS;
-
- if (!obj.__lookupGetter__(name))
- obj.__defineGetter__(name, getGetter(name, kind));
-
- if (!obj.__lookupSetter__(name))
- obj.__defineSetter__(name, getSetter(name, kind, opt_setHook));
- }
-
- /**
- * Counter for use with createUid
- */
- var uidCounter = 1;
-
- /**
- * @return {number} A new unique ID.
- */
- function createUid() {
- return uidCounter++;
- }
-
- /**
- * Returns a unique ID for the item. This mutates the item so it needs to be
- * an object
- * @param {!Object} item The item to get the unique ID for.
- * @return {number} The unique ID for the item.
- */
- function getUid(item) {
- if (item.hasOwnProperty('uid'))
- return item.uid;
- return item.uid = createUid();
- }
-
- /**
- * Dispatches a simple event on an event target.
- * @param {!EventTarget} target The event target to dispatch the event on.
- * @param {string} type The type of the event.
- * @param {boolean=} opt_bubbles Whether the event bubbles or not.
- * @param {boolean=} opt_cancelable Whether the default action of the event
- * can be prevented.
- * @return {boolean} If any of the listeners called {@code preventDefault}
- * during the dispatch this will return false.
- */
- function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
- var e = new cr.Event(type, opt_bubbles, opt_cancelable);
- return target.dispatchEvent(e);
- }
-
- /**
- * Calls |fun| and adds all the fields of the returned object to the object
- * named by |name|. For example, cr.define('cr.ui', function() {
- * function List() {
- * ...
- * }
- * function ListItem() {
- * ...
- * }
- * return {
- * List: List,
- * ListItem: ListItem,
- * };
- * });
- * defines the functions cr.ui.List and cr.ui.ListItem.
- * @param {string} name The name of the object that we are adding fields to.
- * @param {!Function} fun The function that will return an object containing
- * the names and values of the new fields.
- */
- function define(name, fun) {
- var obj = exportPath(name);
- var exports = fun();
- for (var propertyName in exports) {
- // Maybe we should check the prototype chain here? The current usage
- // pattern is always using an object literal so we only care about own
- // properties.
- var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
- propertyName);
- if (propertyDescriptor)
- Object.defineProperty(obj, propertyName, propertyDescriptor);
- }
- }
-
- /**
- * Adds a {@code getInstance} static method that always return the same
- * instance object.
- * @param {!Function} ctor The constructor for the class to add the static
- * method to.
- */
- function addSingletonGetter(ctor) {
- ctor.getInstance = function() {
- return ctor.instance_ || (ctor.instance_ = new ctor());
- };
- }
-
- /**
- * Creates a new event to be used with cr.EventTarget or DOM EventTarget
- * objects.
- * @param {string} type The name of the event.
- * @param {boolean=} opt_bubbles Whether the event bubbles.
- * Default is false.
- * @param {boolean=} opt_preventable Whether the default action of the event
- * can be prevented.
- * @constructor
- * @extends {Event}
- */
- function Event(type, opt_bubbles, opt_preventable) {
- var e = cr.doc.createEvent('Event');
- e.initEvent(type, !!opt_bubbles, !!opt_preventable);
- e.__proto__ = global.Event.prototype;
- return e;
- };
-
- /**
- * Initialization which must be deferred until run-time.
- */
- function initialize() {
- // If 'document' isn't defined, then we must be being pre-compiled,
- // so set a trap so that we're initialized on first access at run-time.
- if (!global.document) {
- var originalCr = cr;
-
- Object.defineProperty(global, 'cr', {
- get: function() {
- Object.defineProperty(global, 'cr', {value: originalCr});
- originalCr.initialize();
- return originalCr;
- },
- configurable: true
- });
-
- return;
- }
-
- Event.prototype = {__proto__: global.Event.prototype};
-
- cr.doc = document;
-
- /**
- * Whether we are using a Mac or not.
- */
- cr.isMac = /Mac/.test(navigator.platform);
-
- /**
- * Whether this is on the Windows platform or not.
- */
- cr.isWindows = /Win/.test(navigator.platform);
-
- /**
- * Whether this is on chromeOS or not.
- */
- cr.isChromeOS = /CrOS/.test(navigator.userAgent);
-
- /**
- * Whether this is on vanilla Linux (not chromeOS).
- */
- cr.isLinux = /Linux/.test(navigator.userAgent);
-
- /**
- * Whether this uses GTK or not.
- */
- cr.isGTK = typeof chrome.getVariableValue == 'function' &&
- /GTK/.test(chrome.getVariableValue('toolkit'));
-
- /**
- * Whether this uses the views toolkit or not.
- */
- cr.isViews = typeof chrome.getVariableValue == 'function' &&
- /views/.test(chrome.getVariableValue('toolkit'));
- }
-
- return {
- addSingletonGetter: addSingletonGetter,
- createUid: createUid,
- define: define,
- defineProperty: defineProperty,
- dispatchPropertyChange: dispatchPropertyChange,
- dispatchSimpleEvent: dispatchSimpleEvent,
- Event: Event,
- getUid: getUid,
- initialize: initialize,
- PropertyKind: PropertyKind
- };
- })();
-
-
- /**
- * TODO(kgr): Move this to another file which is to be loaded last.
- * This will be done as part of future work to make this code pre-compilable.
- */
- cr.initialize();
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui', function() {
-
- /**
- * Decorates elements as an instance of a class.
- * @param {string|!Element} source The way to find the element(s) to decorate.
- * If this is a string then {@code querySeletorAll} is used to find the
- * elements to decorate.
- * @param {!Function} constr The constructor to decorate with. The constr
- * needs to have a {@code decorate} function.
- */
- function decorate(source, constr) {
- var elements;
- if (typeof source == 'string')
- elements = cr.doc.querySelectorAll(source);
- else
- elements = [source];
-
- for (var i = 0, el; el = elements[i]; i++) {
- if (!(el instanceof constr))
- constr.decorate(el);
- }
- }
-
- /**
- * Helper function for creating new element for define.
- */
- function createElementHelper(tagName, opt_bag) {
- // Allow passing in ownerDocument to create in a different document.
- var doc;
- if (opt_bag && opt_bag.ownerDocument)
- doc = opt_bag.ownerDocument;
- else
- doc = cr.doc;
- return doc.createElement(tagName);
- }
-
- /**
- * Creates the constructor for a UI element class.
- *
- * Usage:
- * <pre>
- * var List = cr.ui.define('list');
- * List.prototype = {
- * __proto__: HTMLUListElement.prototype,
- * decorate: function() {
- * ...
- * },
- * ...
- * };
- * </pre>
- *
- * @param {string|Function} tagNameOrFunction The tagName or
- * function to use for newly created elements. If this is a function it
- * needs to return a new element when called.
- * @return {function(Object=):Element} The constructor function which takes
- * an optional property bag. The function also has a static
- * {@code decorate} method added to it.
- */
- function define(tagNameOrFunction) {
- var createFunction, tagName;
- if (typeof tagNameOrFunction == 'function') {
- createFunction = tagNameOrFunction;
- tagName = '';
- } else {
- createFunction = createElementHelper;
- tagName = tagNameOrFunction;
- }
-
- /**
- * Creates a new UI element constructor.
- * @param {Object=} opt_propertyBag Optional bag of properties to set on the
- * object after created. The property {@code ownerDocument} is special
- * cased and it allows you to create the element in a different
- * document than the default.
- * @constructor
- */
- function f(opt_propertyBag) {
- var el = createFunction(tagName, opt_propertyBag);
- f.decorate(el);
- for (var propertyName in opt_propertyBag) {
- el[propertyName] = opt_propertyBag[propertyName];
- }
- return el;
- }
-
- /**
- * Decorates an element as a UI element class.
- * @param {!Element} el The element to decorate.
- */
- f.decorate = function(el) {
- el.__proto__ = f.prototype;
- el.decorate();
- };
-
- return f;
- }
-
- /**
- * Input elements do not grow and shrink with their content. This is a simple
- * (and not very efficient) way of handling shrinking to content with support
- * for min width and limited by the width of the parent element.
- * @param {HTMLElement} el The element to limit the width for.
- * @param {number} parentEl The parent element that should limit the size.
- * @param {number} min The minimum width.
- * @param {number} opt_scale Optional scale factor to apply to the width.
- */
- function limitInputWidth(el, parentEl, min, opt_scale) {
- // Needs a size larger than borders
- el.style.width = '10px';
- var doc = el.ownerDocument;
- var win = doc.defaultView;
- var computedStyle = win.getComputedStyle(el);
- var parentComputedStyle = win.getComputedStyle(parentEl);
- var rtl = computedStyle.direction == 'rtl';
-
- // To get the max width we get the width of the treeItem minus the position
- // of the input.
- var inputRect = el.getBoundingClientRect(); // box-sizing
- var parentRect = parentEl.getBoundingClientRect();
- var startPos = rtl ? parentRect.right - inputRect.right :
- inputRect.left - parentRect.left;
-
- // Add up border and padding of the input.
- var inner = parseInt(computedStyle.borderLeftWidth, 10) +
- parseInt(computedStyle.paddingLeft, 10) +
- parseInt(computedStyle.paddingRight, 10) +
- parseInt(computedStyle.borderRightWidth, 10);
-
- // We also need to subtract the padding of parent to prevent it to overflow.
- var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
- parseInt(parentComputedStyle.paddingRight, 10);
-
- var max = parentEl.clientWidth - startPos - inner - parentPadding;
- if (opt_scale)
- max *= opt_scale;
-
- function limit() {
- if (el.scrollWidth > max) {
- el.style.width = max + 'px';
- } else {
- el.style.width = 0;
- var sw = el.scrollWidth;
- if (sw < min) {
- el.style.width = min + 'px';
- } else {
- el.style.width = sw + 'px';
- }
- }
- }
-
- el.addEventListener('input', limit);
- limit();
- }
-
- /**
- * Takes a number and spits out a value CSS will be happy with. To avoid
- * subpixel layout issues, the value is rounded to the nearest integral value.
- * @param {number} pixels The number of pixels.
- * @return {string} e.g. '16px'.
- */
- function toCssPx(pixels) {
- if (!window.isFinite(pixels))
- console.error('Pixel value is not a number: ' + pixels);
- return Math.round(pixels) + 'px';
- }
-
- /**
- * Users complain they occasionaly use doubleclicks instead of clicks
- * (http://crbug.com/140364). To fix it we freeze click handling for
- * the doubleclick time interval.
- * @param {MouseEvent} e Initial click event.
- */
- function swallowDoubleClick(e) {
- var doc = e.target.ownerDocument;
- var counter = e.type == 'click' ? e.detail : 0;
- function swallow(e) {
- e.stopPropagation();
- e.preventDefault();
- }
- function onclick(e) {
- if (e.detail > counter) {
- counter = e.detail;
- // Swallow the click since it's a click inside the doubleclick timeout.
- swallow(e);
- } else {
- // Stop tracking clicks and let regular handling.
- doc.removeEventListener('dblclick', swallow, true);
- doc.removeEventListener('click', onclick, true);
- }
- }
- doc.addEventListener('click', onclick, true);
- doc.addEventListener('dblclick', swallow, true);
- }
-
- return {
- decorate: decorate,
- define: define,
- limitInputWidth: limitInputWidth,
- toCssPx: toCssPx,
- swallowDoubleClick: swallowDoubleClick
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- // require: event_tracker.js
-
- cr.define('cr.ui', function() {
-
- /**
- * The arrow location specifies how the arrow and bubble are positioned in
- * relation to the anchor node.
- * @enum
- */
- var ArrowLocation = {
- // The arrow is positioned at the top and the start of the bubble. In left
- // to right mode this is the top left. The entire bubble is positioned below
- // the anchor node.
- TOP_START: 'top-start',
- // The arrow is positioned at the top and the end of the bubble. In left to
- // right mode this is the top right. The entire bubble is positioned below
- // the anchor node.
- TOP_END: 'top-end',
- // The arrow is positioned at the bottom and the start of the bubble. In
- // left to right mode this is the bottom left. The entire bubble is
- // positioned above the anchor node.
- BOTTOM_START: 'bottom-start',
- // The arrow is positioned at the bottom and the end of the bubble. In
- // left to right mode this is the bottom right. The entire bubble is
- // positioned above the anchor node.
- BOTTOM_END: 'bottom-end'
- };
-
- /**
- * The bubble alignment specifies the position of the bubble in relation to
- * the anchor node.
- * @enum
- */
- var BubbleAlignment = {
- // The bubble is positioned just above or below the anchor node (as
- // specified by the arrow location) so that the arrow points at the midpoint
- // of the anchor.
- ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
- // The bubble is positioned just above or below the anchor node (as
- // specified by the arrow location) so that its reference edge lines up with
- // the edge of the anchor.
- BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge',
- // The bubble is positioned so that it is entirely within view and does not
- // obstruct the anchor element, if possible. The specified arrow location is
- // taken into account as the preferred alignment but may be overruled if
- // there is insufficient space (see BubbleBase.reposition for the exact
- // placement algorithm).
- ENTIRELY_VISIBLE: 'entirely-visible'
- };
-
- /**
- * Abstract base class that provides common functionality for implementing
- * free-floating informational bubbles with a triangular arrow pointing at an
- * anchor node.
- */
- var BubbleBase = cr.ui.define('div');
-
- /**
- * The horizontal distance between the tip of the arrow and the reference edge
- * of the bubble (as specified by the arrow location). In pixels.
- * @type {number}
- * @const
- */
- BubbleBase.ARROW_OFFSET = 30;
-
- /**
- * Minimum horizontal spacing between edge of bubble and edge of viewport
- * (when using the ENTIRELY_VISIBLE alignment). In pixels.
- * @type {number}
- * @const
- */
- BubbleBase.MIN_VIEWPORT_EDGE_MARGIN = 2;
-
- BubbleBase.prototype = {
- // Set up the prototype chain.
- __proto__: HTMLDivElement.prototype,
-
- /**
- * Initialization function for the cr.ui framework.
- */
- decorate: function() {
- this.className = 'bubble';
- this.innerHTML =
- '<div class="bubble-content"></div>' +
- '<div class="bubble-shadow"></div>' +
- '<div class="bubble-arrow"></div>';
- this.hidden = true;
- this.bubbleAlignment = BubbleAlignment.ENTIRELY_VISIBLE;
- },
-
- /**
- * Set the anchor node, i.e. the node that this bubble points at. Only
- * available when the bubble is not being shown.
- * @param {HTMLElement} node The new anchor node.
- */
- set anchorNode(node) {
- if (!this.hidden)
- return;
-
- this.anchorNode_ = node;
- },
-
- /**
- * Set the conent of the bubble. Only available when the bubble is not being
- * shown.
- * @param {HTMLElement} node The root node of the new content.
- */
- set content(node) {
- if (!this.hidden)
- return;
-
- var bubbleContent = this.querySelector('.bubble-content');
- bubbleContent.innerHTML = '';
- bubbleContent.appendChild(node);
- },
-
- /**
- * Set the arrow location. Only available when the bubble is not being
- * shown.
- * @param {cr.ui.ArrowLocation} location The new arrow location.
- */
- set arrowLocation(location) {
- if (!this.hidden)
- return;
-
- this.arrowAtRight_ = location == ArrowLocation.TOP_END ||
- location == ArrowLocation.BOTTOM_END;
- if (document.documentElement.dir == 'rtl')
- this.arrowAtRight_ = !this.arrowAtRight_;
- this.arrowAtTop_ = location == ArrowLocation.TOP_START ||
- location == ArrowLocation.TOP_END;
- },
-
- /**
- * Set the bubble alignment. Only available when the bubble is not being
- * shown.
- * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
- */
- set bubbleAlignment(alignment) {
- if (!this.hidden)
- return;
-
- this.bubbleAlignment_ = alignment;
- },
-
- /**
- * Update the position of the bubble. Whenever the layout may have changed,
- * the bubble should either be repositioned by calling this function or
- * hidden so that it does not point to a nonsensical location on the page.
- */
- reposition: function() {
- var documentWidth = document.documentElement.clientWidth;
- var documentHeight = document.documentElement.clientHeight;
- var anchor = this.anchorNode_.getBoundingClientRect();
- var anchorMid = (anchor.left + anchor.right) / 2;
- var bubble = this.getBoundingClientRect();
- var arrow = this.querySelector('.bubble-arrow').getBoundingClientRect();
-
- if (this.bubbleAlignment_ == BubbleAlignment.ENTIRELY_VISIBLE) {
- // Work out horizontal placement. The bubble is initially positioned so
- // that the arrow tip points toward the midpoint of the anchor and is
- // BubbleBase.ARROW_OFFSET pixels from the reference edge and (as
- // specified by the arrow location). If the bubble is not entirely
- // within view, it is then shifted, preserving the arrow tip position.
- var left = this.arrowAtRight_ ?
- anchorMid + BubbleBase.ARROW_OFFSET - bubble.width :
- anchorMid - BubbleBase.ARROW_OFFSET;
- var max_left_pos =
- documentWidth - bubble.width - BubbleBase.MIN_VIEWPORT_EDGE_MARGIN;
- var min_left_pos = BubbleBase.MIN_VIEWPORT_EDGE_MARGIN;
- if (document.documentElement.dir == 'rtl')
- left = Math.min(Math.max(left, min_left_pos), max_left_pos);
- else
- left = Math.max(Math.min(left, max_left_pos), min_left_pos);
- var arrowTip = Math.min(
- Math.max(arrow.width / 2,
- this.arrowAtRight_ ? left + bubble.width - anchorMid :
- anchorMid - left),
- bubble.width - arrow.width / 2);
-
- // Work out the vertical placement, attempting to fit the bubble
- // entirely into view. The following placements are considered in
- // decreasing order of preference:
- // * Outside the anchor, arrow tip touching the anchor (arrow at
- // top/bottom as specified by the arrow location).
- // * Outside the anchor, arrow tip touching the anchor (arrow at
- // bottom/top, opposite the specified arrow location).
- // * Outside the anchor, arrow tip overlapping the anchor (arrow at
- // top/bottom as specified by the arrow location).
- // * Outside the anchor, arrow tip overlapping the anchor (arrow at
- // bottom/top, opposite the specified arrow location).
- // * Overlapping the anchor.
- var offsetTop = Math.min(documentHeight - anchor.bottom - bubble.height,
- arrow.height / 2);
- var offsetBottom = Math.min(anchor.top - bubble.height,
- arrow.height / 2);
- if (offsetTop < 0 && offsetBottom < 0) {
- var top = 0;
- this.updateArrowPosition_(false, false, arrowTip);
- } else if (offsetTop > offsetBottom ||
- offsetTop == offsetBottom && this.arrowAtTop_) {
- var top = anchor.bottom + offsetTop;
- this.updateArrowPosition_(true, true, arrowTip);
- } else {
- var top = anchor.top - bubble.height - offsetBottom;
- this.updateArrowPosition_(true, false, arrowTip);
- }
- } else {
- if (this.bubbleAlignment_ ==
- BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
- var left = this.arrowAtRight_ ? anchor.right - bubble.width :
- anchor.left;
- } else {
- var left = this.arrowAtRight_ ?
- anchorMid - this.clientWidth + BubbleBase.ARROW_OFFSET :
- anchorMid - BubbleBase.ARROW_OFFSET;
- }
- var top = this.arrowAtTop_ ? anchor.bottom + arrow.height / 2 :
- anchor.top - this.clientHeight - arrow.height / 2;
- this.updateArrowPosition_(true, this.arrowAtTop_,
- BubbleBase.ARROW_OFFSET);
- }
-
- this.style.left = left + 'px';
- this.style.top = top + 'px';
- },
-
- /**
- * Show the bubble.
- */
- show: function() {
- if (!this.hidden)
- return;
-
- this.attachToDOM_();
- this.hidden = false;
- this.reposition();
-
- var doc = this.ownerDocument;
- this.eventTracker_ = new EventTracker;
- this.eventTracker_.add(doc, 'keydown', this, true);
- this.eventTracker_.add(doc, 'mousedown', this, true);
- },
-
- /**
- * Hide the bubble.
- */
- hide: function() {
- if (this.hidden)
- return;
-
- this.eventTracker_.removeAll();
- this.hidden = true;
- this.parentNode.removeChild(this);
- },
-
- /**
- * Handle keyboard events, dismissing the bubble if necessary.
- * @param {Event} event The event.
- */
- handleEvent: function(event) {
- // Close the bubble when the user presses <Esc>.
- if (event.type == 'keydown' && event.keyCode == 27) {
- this.hide();
- event.preventDefault();
- event.stopPropagation();
- }
- },
-
- /**
- * Attach the bubble to the document's DOM.
- * @private
- */
- attachToDOM_: function() {
- document.body.appendChild(this);
- },
-
- /**
- * Update the arrow so that it appears at the correct position.
- * @param {Boolean} visible Whether the arrow should be visible.
- * @param {Boolean} atTop Whether the arrow should be at the top of the
- * bubble.
- * @param {number} tipOffset The horizontal distance between the tip of the
- * arrow and the reference edge of the bubble (as specified by the arrow
- * location).
- * @private
- */
- updateArrowPosition_: function(visible, atTop, tipOffset) {
- var bubbleArrow = this.querySelector('.bubble-arrow');
- bubbleArrow.hidden = !visible;
- if (!visible)
- return;
-
- var edgeOffset = (-bubbleArrow.clientHeight / 2) + 'px';
- bubbleArrow.style.top = atTop ? edgeOffset : 'auto';
- bubbleArrow.style.bottom = atTop ? 'auto' : edgeOffset;
-
- edgeOffset = (tipOffset - bubbleArrow.offsetWidth / 2) + 'px';
- bubbleArrow.style.left = this.arrowAtRight_ ? 'auto' : edgeOffset;
- bubbleArrow.style.right = this.arrowAtRight_ ? edgeOffset : 'auto';
- },
- };
-
- /**
- * A bubble that remains open until the user explicitly dismisses it or clicks
- * outside the bubble after it has been shown for at least the specified
- * amount of time (making it less likely that the user will unintentionally
- * dismiss the bubble). The bubble repositions itself on layout changes.
- */
- var Bubble = cr.ui.define('div');
-
- Bubble.prototype = {
- // Set up the prototype chain
- __proto__: BubbleBase.prototype,
-
- /**
- * Initialization function for the cr.ui framework.
- */
- decorate: function() {
- BubbleBase.prototype.decorate.call(this);
-
- var close = document.createElement('div');
- close.className = 'bubble-close';
- this.insertBefore(close, this.querySelector('.bubble-content'));
-
- this.handleCloseEvent = this.hide;
- this.deactivateToDismissDelay_ = 0;
- this.bubbleAlignment = BubbleAlignment.ARROW_TO_MID_ANCHOR;
- },
-
- /**
- * Handler for close events triggered when the close button is clicked. By
- * default, set to this.hide. Only available when the bubble is not being
- * shown.
- * @param {function} handler The new handler, a function with no parameters.
- */
- set handleCloseEvent(handler) {
- if (!this.hidden)
- return;
-
- this.handleCloseEvent_ = handler;
- },
-
- /**
- * Set the delay before the user is allowed to click outside the bubble to
- * dismiss it. Using a delay makes it less likely that the user will
- * unintentionally dismiss the bubble.
- * @param {number} delay The delay in milliseconds.
- */
- set deactivateToDismissDelay(delay) {
- this.deactivateToDismissDelay_ = delay;
- },
-
- /**
- * Hide or show the close button.
- * @param {Boolean} isVisible True if the close button should be visible.
- */
- set closeButtonVisible(isVisible) {
- this.querySelector('.bubble-close').hidden = !isVisible;
- },
-
- /**
- * Show the bubble.
- */
- show: function() {
- if (!this.hidden)
- return;
-
- BubbleBase.prototype.show.call(this);
-
- this.showTime_ = Date.now();
- this.eventTracker_.add(window, 'resize', this.reposition.bind(this));
- },
-
- /**
- * Handle keyboard and mouse events, dismissing the bubble if necessary.
- * @param {Event} event The event.
- */
- handleEvent: function(event) {
- BubbleBase.prototype.handleEvent.call(this, event);
-
- if (event.type == 'mousedown') {
- // Dismiss the bubble when the user clicks on the close button.
- if (event.target == this.querySelector('.bubble-close')) {
- this.handleCloseEvent_();
- // Dismiss the bubble when the user clicks outside it after the
- // specified delay has passed.
- } else if (!this.contains(event.target) &&
- Date.now() - this.showTime_ >= this.deactivateToDismissDelay_) {
- this.hide();
- }
- }
- },
- };
-
- return {
- ArrowLocation: ArrowLocation,
- BubbleAlignment: BubbleAlignment,
- BubbleBase: BubbleBase,
- Bubble: Bubble
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview Card slider implementation. Allows you to create interactions
- * that have items that can slide left to right to reveal additional items.
- * Works by adding the necessary event handlers to a specific DOM structure
- * including a frame, container and cards.
- * - The frame defines the boundary of one item. Each card will be expanded to
- * fill the width of the frame. This element is also overflow hidden so that
- * the additional items left / right do not trigger horizontal scrolling.
- * - The container is what all the touch events are attached to. This element
- * will be expanded to be the width of all cards.
- * - The cards are the individual viewable items. There should be one card for
- * each item in the list. Only one card will be visible at a time. Two cards
- * will be visible while you are transitioning between cards.
- *
- * This class is designed to work well on any hardware-accelerated touch device.
- * It should still work on pre-hardware accelerated devices it just won't feel
- * very good. It should also work well with a mouse.
- */
-
- // Use an anonymous function to enable strict mode just for this file (which
- // will be concatenated with other files when embedded in Chrome
- cr.define('cr.ui', function() {
- 'use strict';
-
- /**
- * @constructor
- * @param {!Element} frame The bounding rectangle that cards are visible in.
- * @param {!Element} container The surrounding element that will have event
- * listeners attached to it.
- * @param {number} cardWidth The width of each card should have.
- */
- function CardSlider(frame, container, cardWidth) {
- /**
- * @type {!Element}
- * @private
- */
- this.frame_ = frame;
-
- /**
- * @type {!Element}
- * @private
- */
- this.container_ = container;
-
- /**
- * Array of card elements.
- * @type {!Array.<!Element>}
- * @private
- */
- this.cards_ = [];
-
- /**
- * Index of currently shown card.
- * @type {number}
- * @private
- */
- this.currentCard_ = -1;
-
- /**
- * @type {number}
- * @private
- */
- this.cardWidth_ = cardWidth;
-
- /**
- * @type {!cr.ui.TouchHandler}
- * @private
- */
- this.touchHandler_ = new cr.ui.TouchHandler(this.container_);
- }
-
-
- /**
- * The time to transition between cards when animating. Measured in ms.
- * @type {number}
- * @private
- * @const
- */
- CardSlider.TRANSITION_TIME_ = 200;
-
-
- /**
- * The minimum velocity required to transition cards if they did not drag past
- * the halfway point between cards. Measured in pixels / ms.
- * @type {number}
- * @private
- * @const
- */
- CardSlider.TRANSITION_VELOCITY_THRESHOLD_ = 0.2;
-
-
- CardSlider.prototype = {
- /**
- * The current left offset of the container relative to the frame. This
- * position does not include deltas from active drag operations, and
- * always aligns with a frame boundary.
- * @type {number}
- * @private
- */
- currentLeft_: 0,
-
- /**
- * Current offset relative to |currentLeft_| due to an active drag
- * operation.
- * @type {number}
- * @private
- */
- deltaX_: 0,
-
- /**
- * Initialize all elements and event handlers. Must call after construction
- * and before usage.
- * @param {boolean} ignoreMouseWheelEvents If true, horizontal mouse wheel
- * events will be ignored, rather than flipping between pages.
- */
- initialize: function(ignoreMouseWheelEvents) {
- var view = this.container_.ownerDocument.defaultView;
- assert(view.getComputedStyle(this.container_).display == '-webkit-box',
- 'Container should be display -webkit-box.');
- assert(view.getComputedStyle(this.frame_).overflow == 'hidden',
- 'Frame should be overflow hidden.');
- assert(view.getComputedStyle(this.container_).position == 'static',
- 'Container should be position static.');
-
- this.updateCardWidths_();
-
- this.mouseWheelScrollAmount_ = 0;
- this.mouseWheelCardSelected_ = false;
- this.mouseWheelIsContinuous_ = false;
- this.scrollClearTimeout_ = null;
- if (!ignoreMouseWheelEvents) {
- this.frame_.addEventListener('mousewheel',
- this.onMouseWheel_.bind(this));
- }
- this.container_.addEventListener(
- 'webkitTransitionEnd', this.onWebkitTransitionEnd_.bind(this));
-
- // Also support touch events in case a touch screen happens to be
- // available. Note that this has minimal impact in the common case of
- // no touch events (eg. we're mainly just adding listeners for events that
- // will never trigger).
- var TouchHandler = cr.ui.TouchHandler;
- this.container_.addEventListener(TouchHandler.EventType.TOUCH_START,
- this.onTouchStart_.bind(this));
- this.container_.addEventListener(TouchHandler.EventType.DRAG_START,
- this.onDragStart_.bind(this));
- this.container_.addEventListener(TouchHandler.EventType.DRAG_MOVE,
- this.onDragMove_.bind(this));
- this.container_.addEventListener(TouchHandler.EventType.DRAG_END,
- this.onDragEnd_.bind(this));
-
- this.touchHandler_.enable(/* opt_capture */ false);
- },
-
- /**
- * Use in cases where the width of the frame has changed in order to update
- * the width of cards. For example should be used when orientation changes
- * in full width sliders.
- * @param {number} newCardWidth Width all cards should have, in pixels.
- */
- resize: function(newCardWidth) {
- if (newCardWidth != this.cardWidth_) {
- this.cardWidth_ = newCardWidth;
-
- this.updateCardWidths_();
-
- // Must upate the transform on the container to show the correct card.
- this.transformToCurrentCard_();
- }
- },
-
- /**
- * Sets the cards used. Can be called more than once to switch card sets.
- * @param {!Array.<!Element>} cards The individual viewable cards.
- * @param {number} index Index of the card to in the new set of cards to
- * navigate to.
- */
- setCards: function(cards, index) {
- assert(index >= 0 && index < cards.length,
- 'Invalid index in CardSlider#setCards');
- this.cards_ = cards;
-
- this.updateCardWidths_();
- this.updateSelectedCardAttributes_();
-
- // Jump to the given card index.
- this.selectCard(index, false, false, true);
- },
-
- /**
- * Ensures that for all cards:
- * - if the card is the current card, then it has 'selected-card' in its
- * classList, and is visible for accessibility
- * - if the card is not the selected card, then it does not have
- * 'selected-card' in its classList, and is invisible for accessibility.
- * @private
- */
- updateSelectedCardAttributes_: function() {
- for (var i = 0; i < this.cards_.length; i++) {
- if (i == this.currentCard_) {
- this.cards_[i].classList.add('selected-card');
- this.cards_[i].removeAttribute('aria-hidden');
- } else {
- this.cards_[i].classList.remove('selected-card');
- this.cards_[i].setAttribute('aria-hidden', true);
- }
- }
- },
-
- /**
- * Updates the width of each card.
- * @private
- */
- updateCardWidths_: function() {
- for (var i = 0, card; card = this.cards_[i]; i++)
- card.style.width = this.cardWidth_ + 'px';
- },
-
- /**
- * Returns the index of the current card.
- * @return {number} index of the current card.
- */
- get currentCard() {
- return this.currentCard_;
- },
-
- /**
- * Allows setting the current card index.
- * @param {number} index A new index to set the current index to.
- * @return {number} The new index after having been set.
- */
- set currentCard(index) {
- return (this.currentCard_ = index);
- },
-
- /**
- * Returns the number of cards.
- * @return {number} number of cards.
- */
- get cardCount() {
- return this.cards_.length;
- },
-
- /**
- * Returns the current card itself.
- * @return {!Element} the currently shown card.
- */
- get currentCardValue() {
- return this.cards_[this.currentCard_];
- },
-
- /**
- * Returns the frame holding the cards.
- * @return {Element} The frame used to position the cards.
- */
- get frame() {
- return this.frame_;
- },
-
- /**
- * Handle horizontal scrolls to flip between pages.
- * @private
- */
- onMouseWheel_: function(e) {
- if (e.wheelDeltaX == 0)
- return;
-
- // Continuous devices such as an Apple Touchpad or Apple MagicMouse will
- // send arbitrary delta values. Conversly, standard mousewheels will
- // send delta values in increments of 120. (There is of course a small
- // chance we mistake a continuous device for a non-continuous device.
- // Unfortunately there isn't a better way to do this until real touch
- // events are available to desktop clients.)
- var DISCRETE_DELTA = 120;
- if (e.wheelDeltaX % DISCRETE_DELTA)
- this.mouseWheelIsContinuous_ = true;
-
- if (this.mouseWheelIsContinuous_) {
- // For continuous devices, detect a page swipe when the accumulated
- // delta matches a pre-defined threshhold. After changing the page,
- // ignore wheel events for a short time before repeating this process.
- if (this.mouseWheelCardSelected_) return;
- this.mouseWheelScrollAmount_ += e.wheelDeltaX;
- if (Math.abs(this.mouseWheelScrollAmount_) >= 600) {
- var pagesToScroll = this.mouseWheelScrollAmount_ > 0 ? 1 : -1;
- if (!isRTL())
- pagesToScroll *= -1;
- var newCardIndex = this.currentCard + pagesToScroll;
- newCardIndex = Math.min(this.cards_.length - 1,
- Math.max(0, newCardIndex));
- this.selectCard(newCardIndex, true);
- this.mouseWheelCardSelected_ = true;
- }
- } else {
- // For discrete devices, consider each wheel tick a page change.
- var pagesToScroll = e.wheelDeltaX / DISCRETE_DELTA;
- if (!isRTL())
- pagesToScroll *= -1;
- var newCardIndex = this.currentCard + pagesToScroll;
- newCardIndex = Math.min(this.cards_.length - 1,
- Math.max(0, newCardIndex));
- this.selectCard(newCardIndex, true);
- }
-
- // We got a mouse wheel event, so cancel any pending scroll wheel timeout.
- if (this.scrollClearTimeout_ != null)
- clearTimeout(this.scrollClearTimeout_);
- // If we didn't use up all the scroll, hold onto it for a little bit, but
- // drop it after a delay.
- if (this.mouseWheelScrollAmount_ != 0) {
- this.scrollClearTimeout_ =
- setTimeout(this.clearMouseWheelScroll_.bind(this), 500);
- }
- },
-
- /**
- * Resets the amount of horizontal scroll we've seen to 0. See
- * onMouseWheel_.
- * @private
- */
- clearMouseWheelScroll_: function() {
- this.mouseWheelScrollAmount_ = 0;
- this.mouseWheelCardSelected_ = false;
- },
-
- /**
- * Handles the ends of -webkit-transitions on -webkit-transform (animated
- * card switches).
- * @param {Event} e The webkitTransitionEnd event.
- * @private
- */
- onWebkitTransitionEnd_: function(e) {
- // Ignore irrelevant transitions that might bubble up.
- if (e.target !== this.container_ ||
- e.propertyName != '-webkit-transform') {
- return;
- }
- this.fireChangeEndedEvent_(true);
- },
-
- /**
- * Dispatches a simple event to tell subscribers we're done moving to the
- * newly selected card.
- * @param {boolean} wasAnimated whether or not the change was animated.
- * @private
- */
- fireChangeEndedEvent_: function(wasAnimated) {
- var e = document.createEvent('Event');
- e.initEvent('cardSlider:card_change_ended', true, true);
- e.cardSlider = this;
- e.changedTo = this.currentCard_;
- e.wasAnimated = wasAnimated;
- this.container_.dispatchEvent(e);
- },
-
- /**
- * Add a card to the card slider at a particular index. If the card being
- * added is inserted in front of the current card, cardSlider.currentCard
- * will be adjusted accordingly (to current card + 1).
- * @param {!Node} card A card that will be added to the card slider.
- * @param {number} index An index at which the given |card| should be
- * inserted. Must be positive and less than the number of cards.
- */
- addCardAtIndex: function(card, index) {
- assert(card instanceof Node, '|card| isn\'t a Node');
- this.assertValidIndex_(index);
- this.cards_ = Array.prototype.concat.call(
- this.cards_.slice(0, index), card, this.cards_.slice(index));
-
- this.updateSelectedCardAttributes_();
-
- if (this.currentCard_ == -1)
- this.currentCard_ = 0;
- else if (index <= this.currentCard_)
- this.selectCard(this.currentCard_ + 1, false, true, true);
-
- this.fireAddedEvent_(card, index);
- },
-
- /**
- * Append a card to the end of the list.
- * @param {!Node} card A card to add at the end of the card slider.
- */
- appendCard: function(card) {
- assert(card instanceof Node, '|card| isn\'t a Node');
- this.cards_.push(card);
- this.fireAddedEvent_(card, this.cards_.length - 1);
- },
-
- /**
- * Dispatches a simple event to tell interested subscribers that a card was
- * added to this card slider.
- * @param {Node} card The recently added card.
- * @param {number} index The position of the newly added card.
- * @private
- */
- fireAddedEvent_: function(card, index) {
- this.assertValidIndex_(index);
- var e = document.createEvent('Event');
- e.initEvent('cardSlider:card_added', true, true);
- e.addedIndex = index;
- e.addedCard = card;
- this.container_.dispatchEvent(e);
- },
-
- /**
- * Returns the card at a particular index.
- * @param {number} index The index of the card to return.
- * @return {!Element} The card at the given index.
- */
- getCardAtIndex: function(index) {
- this.assertValidIndex_(index);
- return this.cards_[index];
- },
-
- /**
- * Removes a card by index from the card slider. If the card to be removed
- * is the current card or in front of the current card, the current card
- * will be updated (to current card - 1).
- * @param {!Node} card A card to be removed.
- */
- removeCard: function(card) {
- assert(card instanceof Node, '|card| isn\'t a Node');
- this.removeCardAtIndex(this.cards_.indexOf(card));
- },
-
- /**
- * Removes a card by index from the card slider. If the card to be removed
- * is the current card or in front of the current card, the current card
- * will be updated (to current card - 1).
- * @param {number} index The index of the tile that should be removed.
- */
- removeCardAtIndex: function(index) {
- this.assertValidIndex_(index);
- var removed = this.cards_.splice(index, 1).pop();
-
- if (this.cards_.length == 0)
- this.currentCard_ = -1;
- else if (index < this.currentCard_)
- this.selectCard(this.currentCard_ - 1, false, true);
-
- this.fireRemovedEvent_(removed, index);
- },
-
- /**
- * Dispatches a cardSlider:card_removed event so interested subscribers know
- * when a card was removed from this card slider.
- * @param {Node} card The recently removed card.
- * @param {number} index The index of the card before it was removed.
- * @private
- */
- fireRemovedEvent_: function(card, index) {
- var e = document.createEvent('Event');
- e.initEvent('cardSlider:card_removed', true, true);
- e.removedCard = card;
- e.removedIndex = index;
- this.container_.dispatchEvent(e);
- },
-
- /**
- * This re-syncs the -webkit-transform that's used to position the frame in
- * the likely event it needs to be updated by a card being inserted or
- * removed in the flow.
- */
- repositionFrame: function() {
- this.transformToCurrentCard_();
- },
-
- /**
- * Checks the the given |index| exists in this.cards_.
- * @param {number} index An index to check.
- * @private
- */
- assertValidIndex_: function(index) {
- assert(index >= 0 && index < this.cards_.length);
- },
-
- /**
- * Selects a new card, ensuring that it is a valid index, transforming the
- * view and possibly calling the change card callback.
- * @param {number} newCardIndex Index of card to show.
- * @param {boolean=} opt_animate If true will animate transition from
- * current position to new position.
- * @param {boolean=} opt_dontNotify If true, don't tell subscribers that
- * we've changed cards.
- * @param {boolean=} opt_forceChange If true, ignore if the card already
- * selected.
- */
- selectCard: function(newCardIndex,
- opt_animate,
- opt_dontNotify,
- opt_forceChange) {
- this.assertValidIndex_(newCardIndex);
-
- var previousCard = this.currentCardValue;
- var isChangingCard =
- !this.cards_[newCardIndex].classList.contains('selected-card');
-
- if (typeof opt_forceChange != 'undefined' && opt_forceChange)
- isChangingCard = true;
-
- if (isChangingCard) {
- this.currentCard_ = newCardIndex;
- this.updateSelectedCardAttributes_();
- }
-
- var willTransitionHappen = this.transformToCurrentCard_(opt_animate);
-
- if (isChangingCard && !opt_dontNotify) {
- var event = document.createEvent('Event');
- event.initEvent('cardSlider:card_changed', true, true);
- event.cardSlider = this;
- event.wasAnimated = !!opt_animate;
- this.container_.dispatchEvent(event);
-
- // We also dispatch an event on the cards themselves.
- if (previousCard) {
- cr.dispatchSimpleEvent(previousCard, 'carddeselected',
- true, true);
- }
- cr.dispatchSimpleEvent(this.currentCardValue, 'cardselected',
- true, true);
- }
-
- // If we're not changing, animated, or transitioning, fire a
- // cardSlider:card_change_ended event right away.
- if ((!isChangingCard || !opt_animate || !willTransitionHappen) &&
- !opt_dontNotify) {
- this.fireChangeEndedEvent_(false);
- }
- },
-
- /**
- * Selects a card from the stack. Passes through to selectCard.
- * @param {Node} newCard The card that should be selected.
- * @param {boolean=} opt_animate Whether to animate.
- */
- selectCardByValue: function(newCard, opt_animate) {
- var i = this.cards_.indexOf(newCard);
- assert(i != -1);
- this.selectCard(i, opt_animate);
- },
-
- /**
- * Centers the view on the card denoted by this.currentCard. Can either
- * animate to that card or snap to it.
- * @param {boolean=} opt_animate If true will animate transition from
- * current position to new position.
- * @return {boolean} Whether or not a transformation was necessary.
- * @private
- */
- transformToCurrentCard_: function(opt_animate) {
- var prevLeft = this.currentLeft_;
- this.currentLeft_ = -this.cardWidth_ *
- (isRTL() ? this.cards_.length - this.currentCard - 1 :
- this.currentCard);
-
- // If there's no change, return something to let the caller know there
- // won't be a transition occuring.
- if (prevLeft == this.currentLeft_ && this.deltaX_ == 0)
- return false;
-
- // Animate to the current card, which will either transition if the
- // current card is new, or reset the existing card if we didn't drag
- // enough to change cards.
- var transition = '';
- if (opt_animate) {
- transition = '-webkit-transform ' + CardSlider.TRANSITION_TIME_ +
- 'ms ease-in-out';
- }
- this.container_.style.WebkitTransition = transition;
- this.translateTo_(this.currentLeft_);
-
- return true;
- },
-
- /**
- * Moves the view to the specified position.
- * @param {number} x Horizontal position to move to.
- * @private
- */
- translateTo_: function(x) {
- // We use a webkitTransform to slide because this is GPU accelerated on
- // Chrome and iOS. Once Chrome does GPU acceleration on the position
- // fixed-layout elements we could simply set the element's position to
- // fixed and modify 'left' instead.
- this.deltaX_ = x - this.currentLeft_;
- this.container_.style.WebkitTransform = 'translate3d(' + x + 'px, 0, 0)';
- },
-
- /* Touch ******************************************************************/
-
- /**
- * Clear any transition that is in progress and enable dragging for the
- * touch.
- * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
- * @private
- */
- onTouchStart_: function(e) {
- this.container_.style.WebkitTransition = '';
- e.enableDrag = true;
- },
-
- /**
- * Tell the TouchHandler that dragging is acceptable when the user begins by
- * scrolling horizontally and there is more than one card to slide.
- * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
- * @private
- */
- onDragStart_: function(e) {
- e.enableDrag = this.cardCount > 1 && Math.abs(e.dragDeltaX) >
- Math.abs(e.dragDeltaY);
- },
-
- /**
- * On each drag move event reposition the container appropriately so the
- * cards look like they are sliding.
- * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
- * @private
- */
- onDragMove_: function(e) {
- var deltaX = e.dragDeltaX;
- // If dragging beyond the first or last card then apply a backoff so the
- // dragging feels stickier than usual.
- if (!this.currentCard && deltaX > 0 ||
- this.currentCard == (this.cards_.length - 1) && deltaX < 0) {
- deltaX /= 2;
- }
- this.translateTo_(this.currentLeft_ + deltaX);
- },
-
- /**
- * On drag end events we may want to transition to another card, depending
- * on the ending position of the drag and the velocity of the drag.
- * @param {!cr.ui.TouchHandler.Event} e The TouchHandler event.
- * @private
- */
- onDragEnd_: function(e) {
- var deltaX = e.dragDeltaX;
- var velocity = this.touchHandler_.getEndVelocity().x;
- var newX = this.currentLeft_ + deltaX;
- var newCardIndex = Math.round(-newX / this.cardWidth_);
-
- if (newCardIndex == this.currentCard && Math.abs(velocity) >
- CardSlider.TRANSITION_VELOCITY_THRESHOLD_) {
- // The drag wasn't far enough to change cards but the velocity was
- // high enough to transition anyways. If the velocity is to the left
- // (negative) then the user wishes to go right (card + 1).
- newCardIndex += velocity > 0 ? -1 : 1;
- }
- // Ensure that the new card index is valid. The new card index could be
- // invalid if a swipe suggests scrolling off the end of the list of
- // cards.
- if (newCardIndex < 0)
- newCardIndex = 0;
- else if (newCardIndex >= this.cardCount)
- newCardIndex = this.cardCount - 1;
- this.selectCard(newCardIndex, /* animate */ true);
- },
-
- /**
- * Cancel any current touch/slide as if we saw a touch end
- */
- cancelTouch: function() {
- // Stop listening to any current touch
- this.touchHandler_.cancelTouch();
-
- // Ensure we're at a card bounary
- this.transformToCurrentCard_(true);
- },
- };
-
- return {
- CardSlider: CardSlider
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui', function() {
-
- /** @const */ var Menu = cr.ui.Menu;
-
- /**
- * Handles context menus.
- * @constructor
- */
- function ContextMenuHandler() {}
-
- ContextMenuHandler.prototype = {
-
- /**
- * The menu that we are currently showing.
- * @type {cr.ui.Menu}
- */
- menu_: null,
- get menu() {
- return this.menu_;
- },
-
- /**
- * Shows a menu as a context menu.
- * @param {!Event} e The event triggering the show (usually a contextmenu
- * event).
- * @param {!cr.ui.Menu} menu The menu to show.
- */
- showMenu: function(e, menu) {
- this.menu_ = menu;
- menu.updateCommands(e.currentTarget);
- menu.hidden = false;
- menu.contextElement = e.currentTarget;
-
- // when the menu is shown we steal all keyboard events.
- var doc = menu.ownerDocument;
- doc.addEventListener('keydown', this, true);
- doc.addEventListener('mousedown', this, true);
- // Note: this should be listening for focus, as in menu_button.js, but
- // as per crbug.com/162190 this indirectly causes odd behaviour.
- // Since the context menu is currently not keyboard-accessible, blur
- // is sufficient for now.
- doc.addEventListener('blur', this, true);
- doc.defaultView.addEventListener('resize', this);
- menu.addEventListener('contextmenu', this);
- menu.addEventListener('activate', this);
- this.positionMenu_(e, menu);
- },
-
- /**
- * Hide the currently shown menu.
- */
- hideMenu: function() {
- var menu = this.menu;
- if (!menu)
- return;
-
- menu.hidden = true;
- menu.contextElement = null;
- var doc = menu.ownerDocument;
- doc.removeEventListener('keydown', this, true);
- doc.removeEventListener('mousedown', this, true);
- doc.removeEventListener('blur', this, true);
- doc.defaultView.removeEventListener('resize', this);
- menu.removeEventListener('contextmenu', this);
- menu.removeEventListener('activate', this);
- menu.selectedIndex = -1;
- this.menu_ = null;
-
- // On windows we might hide the menu in a right mouse button up and if
- // that is the case we wait some short period before we allow the menu
- // to be shown again.
- this.hideTimestamp_ = cr.isWindows ? Date.now() : 0;
- },
-
- /**
- * Positions the menu
- * @param {!Event} e The event object triggering the showing.
- * @param {!cr.ui.Menu} menu The menu to position.
- * @private
- */
- positionMenu_: function(e, menu) {
- // TODO(arv): Handle scrolled documents when needed.
-
- var element = e.currentTarget;
- var x, y;
- // When the user presses the context menu key (on the keyboard) we need
- // to detect this.
- if (this.keyIsDown_) {
- var rect = element.getRectForContextMenu ?
- element.getRectForContextMenu() :
- element.getBoundingClientRect();
- var offset = Math.min(rect.width, rect.height) / 2;
- x = rect.left + offset;
- y = rect.top + offset;
- } else {
- x = e.clientX;
- y = e.clientY;
- }
-
- cr.ui.positionPopupAtPoint(x, y, menu);
- },
-
- /**
- * Handles event callbacks.
- * @param {!Event} e The event object.
- */
- handleEvent: function(e) {
- // Keep track of keydown state so that we can use that to determine the
- // reason for the contextmenu event.
- switch (e.type) {
- case 'keydown':
- this.keyIsDown_ = !e.ctrlKey && !e.altKey &&
- // context menu key or Shift-F10
- (e.keyCode == 93 && !e.shiftKey ||
- e.keyIdentifier == 'F10' && e.shiftKey);
- break;
-
- case 'keyup':
- this.keyIsDown_ = false;
- break;
- }
-
- // Context menu is handled even when we have no menu.
- if (e.type != 'contextmenu' && !this.menu)
- return;
-
- switch (e.type) {
- case 'mousedown':
- if (!this.menu.contains(e.target))
- this.hideMenu();
- else
- e.preventDefault();
- break;
- case 'keydown':
- // keyIdentifier does not report 'Esc' correctly
- if (e.keyCode == 27 /* Esc */) {
- this.hideMenu();
- e.stopPropagation();
- e.preventDefault();
-
- // If the menu is visible we let it handle all the keyboard events.
- } else if (this.menu) {
- this.menu.handleKeyDown(e);
- e.preventDefault();
- e.stopPropagation();
- }
- break;
-
- case 'activate':
- case 'blur':
- case 'resize':
- this.hideMenu();
- break;
-
- case 'contextmenu':
- if ((!this.menu || !this.menu.contains(e.target)) &&
- (!this.hideTimestamp_ || Date.now() - this.hideTimestamp_ > 50))
- this.showMenu(e, e.currentTarget.contextMenu);
- e.preventDefault();
- // Don't allow elements further up in the DOM to show their menus.
- e.stopPropagation();
- break;
- }
- },
-
- /**
- * Adds a contextMenu property to an element or element class.
- * @param {!Element|!Function} element The element or class to add the
- * contextMenu property to.
- */
- addContextMenuProperty: function(element) {
- if (typeof element == 'function')
- element = element.prototype;
-
- element.__defineGetter__('contextMenu', function() {
- return this.contextMenu_;
- });
- element.__defineSetter__('contextMenu', function(menu) {
- var oldContextMenu = this.contextMenu;
-
- if (typeof menu == 'string' && menu[0] == '#') {
- menu = this.ownerDocument.getElementById(menu.slice(1));
- cr.ui.decorate(menu, Menu);
- }
-
- if (menu === oldContextMenu)
- return;
-
- if (oldContextMenu && !menu) {
- this.removeEventListener('contextmenu', contextMenuHandler);
- this.removeEventListener('keydown', contextMenuHandler);
- this.removeEventListener('keyup', contextMenuHandler);
- }
- if (menu && !oldContextMenu) {
- this.addEventListener('contextmenu', contextMenuHandler);
- this.addEventListener('keydown', contextMenuHandler);
- this.addEventListener('keyup', contextMenuHandler);
- }
-
- this.contextMenu_ = menu;
-
- if (menu && menu.id)
- this.setAttribute('contextmenu', '#' + menu.id);
-
- cr.dispatchPropertyChange(this, 'contextMenu', menu, oldContextMenu);
- });
-
- if (!element.getRectForContextMenu) {
- /**
- * @return {!ClientRect} The rect to use for positioning the context
- * menu when the context menu is not opened using a mouse position.
- */
- element.getRectForContextMenu = function() {
- return this.getBoundingClientRect();
- };
- }
- },
-
- /**
- * Sets the given contextMenu to the given element. A contextMenu property
- * would be added if necessary.
- * @param {!Element} element The element or class to set the contextMenu to.
- * @param {!cr.ui.Menu} contextMenu The contextMenu property to be set.
- */
- setContextMenu: function(element, contextMenu) {
- if (!element.contextMenu)
- this.addContextMenuProperty(element);
- element.contextMenu = contextMenu;
- }
- };
-
- /**
- * The singleton context menu handler.
- * @type {!ContextMenuHandler}
- */
- var contextMenuHandler = new ContextMenuHandler;
-
- // Export
- return {
- contextMenuHandler: contextMenuHandler
- };
- });
- </script>
- <script>// Copyright (c) 2011 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview DragWrapper
- * A class for simplifying HTML5 drag and drop. Classes should use this to
- * handle the nitty gritty of nested drag enters and leaves.
- */
- cr.define('cr.ui', function() {
- /**
- * Creates a DragWrapper which listens for drag target events on |target| and
- * delegates event handling to |handler|. The |handler| must implement:
- * shouldAcceptDrag
- * doDragEnter
- * doDragLeave
- * doDragOver
- * doDrop
- */
- function DragWrapper(target, handler) {
- this.initialize(target, handler);
- }
-
- DragWrapper.prototype = {
- initialize: function(target, handler) {
- target.addEventListener('dragenter',
- this.onDragEnter_.bind(this));
- target.addEventListener('dragover', this.onDragOver_.bind(this));
- target.addEventListener('drop', this.onDrop_.bind(this));
- target.addEventListener('dragleave', this.onDragLeave_.bind(this));
-
- this.target_ = target;
- this.handler_ = handler;
- },
-
- /**
- * The number of un-paired dragenter events that have fired on |this|. This
- * is incremented by |onDragEnter_| and decremented by |onDragLeave_|. This
- * is necessary because dragging over child widgets will fire additional
- * enter and leave events on |this|. A non-zero value does not necessarily
- * indicate that |isCurrentDragTarget()| is true.
- * @type {number}
- * @private
- */
- dragEnters_: 0,
-
- /**
- * Whether the tile page is currently being dragged over with data it can
- * accept.
- * @type {boolean}
- */
- get isCurrentDragTarget() {
- return this.target_.classList.contains('drag-target');
- },
-
- /**
- * Handler for dragenter events fired on |target_|.
- * @param {Event} e A MouseEvent for the drag.
- * @private
- */
- onDragEnter_: function(e) {
- if (++this.dragEnters_ == 1) {
- if (this.handler_.shouldAcceptDrag(e)) {
- this.target_.classList.add('drag-target');
- this.handler_.doDragEnter(e);
- }
- } else {
- // Sometimes we'll get an enter event over a child element without an
- // over event following it. In this case we have to still call the
- // drag over handler so that we make the necessary updates (one visible
- // symptom of not doing this is that the cursor's drag state will
- // flicker during drags).
- this.onDragOver_(e);
- }
- },
-
- /**
- * Thunk for dragover events fired on |target_|.
- * @param {Event} e A MouseEvent for the drag.
- * @private
- */
- onDragOver_: function(e) {
- if (!this.target_.classList.contains('drag-target'))
- return;
- this.handler_.doDragOver(e);
- },
-
- /**
- * Thunk for drop events fired on |target_|.
- * @param {Event} e A MouseEvent for the drag.
- * @private
- */
- onDrop_: function(e) {
- this.dragEnters_ = 0;
- if (!this.target_.classList.contains('drag-target'))
- return;
- this.target_.classList.remove('drag-target');
- this.handler_.doDrop(e);
- },
-
- /**
- * Thunk for dragleave events fired on |target_|.
- * @param {Event} e A MouseEvent for the drag.
- * @private
- */
- onDragLeave_: function(e) {
- if (--this.dragEnters_ > 0)
- return;
-
- this.target_.classList.remove('drag-target');
- this.handler_.doDragLeave(e);
- },
- };
-
- return {
- DragWrapper: DragWrapper
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- // require: event_tracker.js
-
- cr.define('cr.ui', function() {
- 'use strict';
-
- /**
- * ExpandableBubble is a free-floating compact informational bubble with an
- * arrow that points at a place of interest on the page. When clicked, the
- * bubble expands to show more of its content. Width of the bubble is the
- * width of the node it is overlapping when unexpanded. Expanded, it is of a
- * fixed width, but variable height. Currently the arrow is always positioned
- * at the bottom right and points down.
- * @constructor
- * @extends {cr.ui.div}
- */
- var ExpandableBubble = cr.ui.define('div');
-
- ExpandableBubble.prototype = {
- __proto__: HTMLDivElement.prototype,
-
- /** @override */
- decorate: function() {
- this.className = 'expandable-bubble';
- this.innerHTML =
- '<div class="expandable-bubble-contents">' +
- '<div class="expandable-bubble-title"></div>' +
- '<div class="expandable-bubble-main" hidden></div>' +
- '</div>' +
- '<div class="expandable-bubble-close" hidden></div>';
-
- this.hidden = true;
- this.bubbleSuppressed = false;
- this.handleCloseEvent = this.hide;
- },
-
- /**
- * Sets the title of the bubble. The title is always visible when the
- * bubble is visible.
- * @type {Node} An HTML element to set as the title.
- */
- set contentTitle(node) {
- var bubbleTitle = this.querySelector('.expandable-bubble-title');
- bubbleTitle.textContent = '';
- bubbleTitle.appendChild(node);
- },
-
- /**
- * Sets the content node of the bubble. The content node is only visible
- * when the bubble is expanded.
- * @param {Node} An HTML element.
- */
- set content(node) {
- var bubbleMain = this.querySelector('.expandable-bubble-main');
- bubbleMain.textContent = '';
- bubbleMain.appendChild(node);
- },
-
- /**
- * Sets the anchor node, i.e. the node that this bubble points at and
- * partially overlaps.
- * @param {HTMLElement} node The new anchor node.
- */
- set anchorNode(node) {
- this.anchorNode_ = node;
-
- if (!this.hidden)
- this.resizeAndReposition();
- },
-
- /**
- * Handles the close event which is triggered when the close button
- * is clicked. By default is set to this.hide.
- * @param {function} A function with no parameters
- */
- set handleCloseEvent(func) {
- this.handleCloseEvent_ = func;
- },
-
- /**
- * Temporarily suppresses the bubble from view (and toggles it back).
- * 'Suppressed' and 'hidden' are two bubble states that both indicate that
- * the bubble should not be visible, but when you 'un-suppress' a bubble,
- * only a suppressed bubble becomes visible. This can be handy, for example,
- * if the user switches away from the app card (then we need to know which
- * bubbles to show (only the suppressed ones, not the hidden ones). Hiding
- * and un-hiding a bubble overrides the suppressed state (a bubble cannot
- * be suppressed but not hidden).
- */
- set suppressed(suppress) {
- if (suppress) {
- // If the bubble is already hidden, then we don't need to suppress it.
- if (this.hidden)
- return;
-
- this.hidden = true;
- } else if (this.bubbleSuppressed) {
- this.hidden = false;
- }
- this.bubbleSuppressed = suppress;
- this.resizeAndReposition(this);
- },
-
- /**
- * Updates the position of the bubble.
- * @private
- */
- reposition_: function() {
- var clientRect = this.anchorNode_.getBoundingClientRect();
-
- // Center bubble in collapsed mode (if it doesn't take up all the room we
- // have).
- var offset = 0;
- if (!this.expanded)
- offset = (clientRect.width - parseInt(this.style.width)) / 2;
- this.style.left = this.style.right = clientRect.left + offset + 'px';
-
- var top = Math.max(0, clientRect.top - 4);
- this.style.top = this.expanded ?
- (top - this.offsetHeight + this.unexpandedHeight) + 'px' :
- top + 'px';
- },
-
- /**
- * Resizes the bubble and then repositions it.
- * @private
- */
- resizeAndReposition: function() {
- var clientRect = this.anchorNode_.getBoundingClientRect();
- var width = clientRect.width;
-
- var bubbleTitle = this.querySelector('.expandable-bubble-title');
- var closeElement = this.querySelector('.expandable-bubble-close');
- var closeWidth = this.expanded ? closeElement.clientWidth : 0;
- var margin = 15;
-
- // Suppress the width style so we can get it to calculate its width.
- // We'll set the right width again when we are done.
- bubbleTitle.style.width = '';
-
- if (this.expanded) {
- // We always show the full title but never show less width than 250
- // pixels.
- var expandedWidth =
- Math.max(250, bubbleTitle.scrollWidth + closeWidth + margin);
- this.style.marginLeft = (width - expandedWidth) + 'px';
- width = expandedWidth;
- } else {
- var newWidth = Math.min(bubbleTitle.scrollWidth + margin, width);
- // If we've maxed out in width then apply the mask.
- this.masked = newWidth == width;
- width = newWidth;
- this.style.marginLeft = '0';
- }
-
- // Width is determined by the width of the title (when not expanded) but
- // capped to the width of the anchor node.
- this.style.width = width + 'px';
- bubbleTitle.style.width = Math.max(0, width - margin - closeWidth) + 'px';
-
- // Also reposition the bubble -- dimensions have potentially changed.
- this.reposition_();
- },
-
- /*
- * Expand the bubble (bringing the full content into view).
- * @private
- */
- expandBubble_: function() {
- this.querySelector('.expandable-bubble-main').hidden = false;
- this.querySelector('.expandable-bubble-close').hidden = false;
- this.expanded = true;
- this.resizeAndReposition();
- },
-
- /**
- * Collapse the bubble, hiding the main content and the close button.
- * This is automatically called when the window is resized.
- * @private
- */
- collapseBubble_: function() {
- this.querySelector('.expandable-bubble-main').hidden = true;
- this.querySelector('.expandable-bubble-close').hidden = true;
- this.expanded = false;
- this.resizeAndReposition();
- },
-
- /**
- * The onclick handler for the notification (expands the bubble).
- * @param {Event} e The event.
- * @private
- */
- onNotificationClick_: function(e) {
- if (!this.contains(e.target))
- return;
-
- if (!this.expanded) {
- // Save the height of the unexpanded bubble, so we can make sure to
- // position it correctly (arrow points in the same location) after
- // we expand it.
- this.unexpandedHeight = this.offsetHeight;
- }
-
- this.expandBubble_();
- },
-
- /**
- * Shows the bubble. The bubble will start collapsed and expand when
- * clicked.
- */
- show: function() {
- if (!this.hidden)
- return;
-
- document.body.appendChild(this);
- this.hidden = false;
- this.resizeAndReposition();
-
- this.eventTracker_ = new EventTracker;
- this.eventTracker_.add(window,
- 'load', this.resizeAndReposition.bind(this));
- this.eventTracker_.add(window,
- 'resize', this.resizeAndReposition.bind(this));
- this.eventTracker_.add(this, 'click', this.onNotificationClick_);
-
- var doc = this.ownerDocument;
- this.eventTracker_.add(doc, 'keydown', this, true);
- this.eventTracker_.add(doc, 'mousedown', this, true);
- },
-
- /**
- * Hides the bubble from view.
- */
- hide: function() {
- this.hidden = true;
- this.bubbleSuppressed = false;
- this.eventTracker_.removeAll();
- this.parentNode.removeChild(this);
- },
-
- /**
- * Handles keydown and mousedown events, dismissing the bubble if
- * necessary.
- * @param {Event} e The event.
- * @private
- */
- handleEvent: function(e) {
- var handled = false;
- switch (e.type) {
- case 'keydown':
- if (e.keyCode == 27) { // Esc.
- if (this.expanded) {
- this.collapseBubble_();
- handled = true;
- }
- }
- break;
-
- case 'mousedown':
- if (e.target == this.querySelector('.expandable-bubble-close')) {
- this.handleCloseEvent_();
- handled = true;
- } else if (!this.contains(e.target)) {
- if (this.expanded) {
- this.collapseBubble_();
- handled = true;
- }
- }
- break;
- }
-
- if (handled) {
- // The bubble emulates a focus grab when expanded, so when we've
- // collapsed/hide the bubble we consider the event handles and don't
- // need to propagate it further.
- e.stopPropagation();
- e.preventDefault();
- }
- },
- };
-
- /**
- * Whether the bubble is expanded or not.
- * @type {boolean}
- */
- cr.defineProperty(ExpandableBubble, 'expanded', cr.PropertyKind.BOOL_ATTR);
-
- /**
- * Whether the title needs to be masked out towards the right, which indicates
- * to the user that part of the text is clipped. This is only used when the
- * bubble is collapsed and the title doesn't fit because it is maxed out in
- * width within the anchored node.
- * @type {boolean}
- */
- cr.defineProperty(ExpandableBubble, 'masked', cr.PropertyKind.BOOL_ATTR);
-
- return {
- ExpandableBubble: ExpandableBubble
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui', function() {
-
- /** @const */ var MenuItem = cr.ui.MenuItem;
-
- /**
- * Creates a new menu element. Menu dispatches all commands on the element it
- * was shown for.
- *
- * @param {Object=} opt_propertyBag Optional properties.
- * @constructor
- * @extends {HTMLMenuElement}
- */
- var Menu = cr.ui.define('menu');
-
- Menu.prototype = {
- __proto__: HTMLMenuElement.prototype,
-
- selectedIndex_: -1,
-
- /**
- * Element for which menu is being shown.
- */
- contextElement: null,
-
- /**
- * Initializes the menu element.
- */
- decorate: function() {
- this.addEventListener('mouseover', this.handleMouseOver_);
- this.addEventListener('mouseout', this.handleMouseOut_);
-
- this.classList.add('decorated');
- this.setAttribute('role', 'menu');
- this.hidden = true; // Hide the menu by default.
-
- // Decorate the children as menu items.
- var children = this.children;
- for (var i = 0, child; child = children[i]; i++) {
- cr.ui.decorate(child, MenuItem);
- }
- },
-
- /**
- * Adds menu item at the end of the list.
- * @param {Object} item Menu item properties.
- * @return {cr.ui.MenuItem} The created menu item.
- */
- addMenuItem: function(item) {
- var menuItem = this.ownerDocument.createElement('menuitem');
- this.appendChild(menuItem);
-
- cr.ui.decorate(menuItem, MenuItem);
-
- if (item.label)
- menuItem.label = item.label;
-
- if (item.iconUrl)
- menuItem.iconUrl = item.iconUrl;
-
- return menuItem;
- },
-
- /**
- * Adds separator at the end of the list.
- */
- addSeparator: function() {
- var separator = this.ownerDocument.createElement('hr');
- this.appendChild(separator);
- },
-
- /**
- * Clears menu.
- */
- clear: function() {
- this.textContent = '';
- },
-
- /**
- * Walks up the ancestors of |el| until a menu item belonging to this menu
- * is found.
- * @param {Element} el The element to start searching from.
- * @return {cr.ui.MenuItem} The found menu item or null.
- * @private
- */
- findMenuItem_: function(el) {
- while (el && el.parentNode != this) {
- el = el.parentNode;
- }
- return el;
- },
-
- /**
- * Handles mouseover events and selects the hovered item.
- * @param {Event} e The mouseover event.
- * @private
- */
- handleMouseOver_: function(e) {
- var overItem = this.findMenuItem_(e.target);
- this.selectedItem = overItem;
- },
-
- /**
- * Handles mouseout events and deselects any selected item.
- * @param {Event} e The mouseout event.
- * @private
- */
- handleMouseOut_: function(e) {
- this.selectedItem = null;
- },
-
- /**
- * The selected menu item or null if none.
- * @type {cr.ui.MenuItem}
- */
- get selectedItem() {
- return this.children[this.selectedIndex];
- },
- set selectedItem(item) {
- var index = Array.prototype.indexOf.call(this.children, item);
- this.selectedIndex = index;
- },
-
- /**
- * Focuses the selected item. If selectedIndex is invalid, set it to 0
- * first.
- */
- focusSelectedItem: function() {
- if (this.selectedIndex < 0 ||
- this.selectedIndex > this.children.length) {
- this.selectedIndex = 0;
- }
-
- if (this.selectedItem)
- this.selectedItem.focus();
- },
-
- /**
- * Menu length
- */
- get length() {
- return this.children.length;
- },
-
- /**
- * This is the function that handles keyboard navigation. This is usually
- * called by the element responsible for managing the menu.
- * @param {Event} e The keydown event object.
- * @return {boolean} Whether the event was handled be the menu.
- */
- handleKeyDown: function(e) {
- var item = this.selectedItem;
-
- var self = this;
- function selectNextAvailable(m) {
- var children = self.children;
- var len = children.length;
- var i = self.selectedIndex;
- if (i == -1 && m == -1) {
- // Edge case when needed to go the last item first.
- i = 0;
- }
-
- // "i" may be negative(-1), so modulus operation and cycle below
- // wouldn't work as assumed. This trick makes startPosition positive
- // without altering it's modulo.
- var startPosition = (i + len) % len;
-
- while (true) {
- i = (i + m + len) % len;
-
- // Check not to enter into infinite loop if all items are hidden or
- // disabled.
- if (i == startPosition)
- break;
-
- item = children[i];
- if (item && !item.isSeparator() && !item.hidden && !item.disabled)
- break;
- }
- if (item && !item.disabled)
- self.selectedIndex = i;
- }
-
- switch (e.keyIdentifier) {
- case 'Down':
- selectNextAvailable(1);
- this.focusSelectedItem();
- return true;
- case 'Up':
- selectNextAvailable(-1);
- this.focusSelectedItem();
- return true;
- case 'Enter':
- case 'U+0020': // Space
- if (item) {
- var activationEvent = cr.doc.createEvent('Event');
- activationEvent.initEvent('activate', true, true);
- activationEvent.originalEvent = e;
- if (item.dispatchEvent(activationEvent)) {
- if (item.command)
- item.command.execute();
- }
- }
- return true;
- }
-
- return false;
- },
-
- /**
- * Updates menu items command according to context.
- * @param {Node=} node Node for which to actuate commands state.
- */
- updateCommands: function(node) {
- var children = this.children;
-
- for (var i = 0, child; child = children[i]; i++)
- child.updateCommand(node);
- }
- };
-
- function selectedIndexChanged(selectedIndex, oldSelectedIndex) {
- var oldSelectedItem = this.children[oldSelectedIndex];
- if (oldSelectedItem) {
- oldSelectedItem.selected = false;
- oldSelectedItem.blur();
- }
- var item = this.selectedItem;
- if (item)
- item.selected = true;
- }
-
- /**
- * The selected menu item.
- * @type {number}
- */
- cr.defineProperty(Menu, 'selectedIndex', cr.PropertyKind.JS,
- selectedIndexChanged);
-
- // Export
- return {
- Menu: Menu
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui', function() {
- /** @const */ var Command = cr.ui.Command;
-
- /**
- * Creates a new menu item element.
- * @param {Object=} opt_propertyBag Optional properties.
- * @constructor
- * @extends {HTMLDivElement}
- */
- var MenuItem = cr.ui.define('div');
-
- /**
- * Creates a new menu separator element.
- * @return {cr.ui.MenuItem} The new separator element.
- */
- MenuItem.createSeparator = function() {
- var el = cr.doc.createElement('hr');
- MenuItem.decorate(el);
- return el;
- };
-
- MenuItem.prototype = {
- __proto__: HTMLButtonElement.prototype,
-
- /**
- * Initializes the menu item.
- */
- decorate: function() {
- var commandId;
- if ((commandId = this.getAttribute('command')))
- this.command = commandId;
-
- this.addEventListener('mouseup', this.handleMouseUp_);
-
- // Adding the 'custom-appearance' class prevents widgets.css from changing
- // the appearance of this element.
- this.classList.add('custom-appearance');
-
- this.setAttribute('role', 'menuitem');
-
- var iconUrl;
- if ((iconUrl = this.getAttribute('icon')))
- this.iconUrl = iconUrl;
- },
-
- /**
- * The command associated with this menu item. If this is set to a string
- * of the form "#element-id" then the element is looked up in the document
- * of the command.
- * @type {cr.ui.Command}
- */
- command_: null,
- get command() {
- return this.command_;
- },
- set command(command) {
- if (this.command_) {
- this.command_.removeEventListener('labelChange', this);
- this.command_.removeEventListener('disabledChange', this);
- this.command_.removeEventListener('hiddenChange', this);
- this.command_.removeEventListener('checkedChange', this);
- }
-
- if (typeof command == 'string' && command[0] == '#') {
- command = this.ownerDocument.getElementById(command.slice(1));
- cr.ui.decorate(command, Command);
- }
-
- this.command_ = command;
- if (command) {
- if (command.id)
- this.setAttribute('command', '#' + command.id);
-
- this.label = command.label;
- this.disabled = command.disabled;
- this.hidden = command.hidden;
-
- this.command_.addEventListener('labelChange', this);
- this.command_.addEventListener('disabledChange', this);
- this.command_.addEventListener('hiddenChange', this);
- this.command_.addEventListener('checkedChange', this);
- }
-
- this.updateShortcut_();
- },
-
- /**
- * The text label.
- * @type {string}
- */
- get label() {
- return this.textContent;
- },
- set label(label) {
- this.textContent = label;
- },
-
- /**
- * Menu icon.
- * @type {string}
- */
- get iconUrl() {
- return this.style.backgroundImage;
- },
- set iconUrl(url) {
- this.style.backgroundImage = 'url(' + url + ')';
- },
-
- /**
- * @return {boolean} Whether the menu item is a separator.
- */
- isSeparator: function() {
- return this.tagName == 'HR';
- },
-
- /**
- * Updates shortcut text according to associated command. If command has
- * multiple shortcuts, only first one is displayed.
- */
- updateShortcut_: function() {
- this.removeAttribute('shortcutText');
-
- if (!(this.command_ && this.command_.shortcut))
- return;
-
- var shortcuts = this.command_.shortcut.split(/\s+/);
-
- if (shortcuts.length == 0)
- return;
-
- var shortcut = shortcuts[0];
- var mods = {};
- var ident = '';
- shortcut.split('-').forEach(function(part) {
- var partUc = part.toUpperCase();
- switch (partUc) {
- case 'CTRL':
- case 'ALT':
- case 'SHIFT':
- case 'META':
- mods[partUc] = true;
- break;
- default:
- console.assert(!ident, 'Shortcut has two non-modifier keys');
- ident = part;
- }
- });
-
- var shortcutText = '';
-
- // TODO(zvorygin): if more cornercases appear - optimize following
- // code. Currently 'Enter' keystroke is passed as 'Enter', and 'Space'
- // is passed as 'U+0020'
- if (ident == 'U+0020')
- ident = 'Space';
-
- ['CTRL', 'ALT', 'SHIFT', 'META'].forEach(function(mod) {
- if (mods[mod])
- shortcutText += loadTimeData.getString('SHORTCUT_' + mod) + '+';
- });
-
- if (ident.indexOf('U+') != 0) {
- shortcutText +=
- loadTimeData.getString('SHORTCUT_' + ident.toUpperCase());
- } else {
- shortcutText +=
- String.fromCharCode(parseInt(ident.substring(2), 16));
- }
-
- this.setAttribute('shortcutText', shortcutText);
- },
-
- /**
- * Handles mouseup events. This dispatches an activate event; if there is an
- * associated command, that command is executed.
- * @param {Event} e The mouseup event object.
- * @private
- */
- handleMouseUp_: function(e) {
- if (!this.disabled && !this.isSeparator() && this.selected) {
- // Store |contextElement| since it'll be removed by {Menu} on handling
- // 'activate' event.
- var contextElement = this.parentNode.contextElement;
- var activationEvent = cr.doc.createEvent('Event');
- activationEvent.initEvent('activate', true, true);
- activationEvent.originalEvent = e;
- // Dispatch command event followed by executing the command object.
- if (this.dispatchEvent(activationEvent)) {
- var command = this.command;
- if (command) {
- command.execute(contextElement);
- cr.ui.swallowDoubleClick(e);
- }
- }
- }
- },
-
- /**
- * Updates command according to the node on which this menu was invoked.
- * @param {Node=} opt_node Node on which menu was opened.
- */
- updateCommand: function(opt_node) {
- if (this.command_) {
- this.command_.canExecuteChange(opt_node);
- }
- },
-
- /**
- * Handles changes to the associated command.
- * @param {Event} e The event object.
- */
- handleEvent: function(e) {
- switch (e.type) {
- case 'disabledChange':
- this.disabled = this.command.disabled;
- break;
- case 'hiddenChange':
- this.hidden = this.command.hidden;
- break;
- case 'labelChange':
- this.label = this.command.label;
- break;
- case 'checkedChange':
- this.checked = this.command.checked;
- break;
- }
- }
- };
-
- /**
- * Whether the menu item is disabled or not.
- * @type {boolean}
- */
- cr.defineProperty(MenuItem, 'disabled', cr.PropertyKind.BOOL_ATTR);
-
- /**
- * Whether the menu item is hidden or not.
- * @type {boolean}
- */
- cr.defineProperty(MenuItem, 'hidden', cr.PropertyKind.BOOL_ATTR);
-
- /**
- * Whether the menu item is selected or not.
- * @type {boolean}
- */
- cr.defineProperty(MenuItem, 'selected', cr.PropertyKind.BOOL_ATTR);
-
- /**
- * Whether the menu item is checked or not.
- * @type {boolean}
- */
- cr.defineProperty(MenuItem, 'checked', cr.PropertyKind.BOOL_ATTR);
-
- // Export
- return {
- MenuItem: MenuItem
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview This file provides utility functions for position popups.
- */
-
- cr.define('cr.ui', function() {
-
- /**
- * Type def for rects as returned by getBoundingClientRect.
- * @typedef { {left: number, top: number, width: number, height: number,
- * right: number, bottom: number}}
- */
- var Rect;
-
- /**
- * Enum for defining how to anchor a popup to an anchor element.
- * @enum {number}
- */
- var AnchorType = {
- /**
- * The popup's right edge is aligned with the left edge of the anchor.
- * The popup's top edge is aligned with the top edge of the anchor.
- */
- BEFORE: 1, // p: right, a: left, p: top, a: top
-
- /**
- * The popop's left edge is aligned with the right edge of the anchor.
- * The popup's top edge is aligned with the top edge of the anchor.
- */
- AFTER: 2, // p: left a: right, p: top, a: top
-
- /**
- * The popop's bottom edge is aligned with the top edge of the anchor.
- * The popup's left edge is aligned with the left edge of the anchor.
- */
- ABOVE: 3, // p: bottom, a: top, p: left, a: left
-
- /**
- * The popop's top edge is aligned with the bottom edge of the anchor.
- * The popup's left edge is aligned with the left edge of the anchor.
- */
- BELOW: 4 // p: top, a: bottom, p: left, a: left
- };
-
- /**
- * Helper function for positionPopupAroundElement and positionPopupAroundRect.
- * @param {!Rect} anchorRect The rect for the anchor.
- * @param {!HTMLElement} popupElement The element used for the popup.
- * @param {AnchorType} type The type of anchoring to do.
- * @param {boolean} invertLeftRight Whether to invert the right/left
- * alignment.
- */
- function positionPopupAroundRect(anchorRect, popupElement, type,
- invertLeftRight) {
- var popupRect = popupElement.getBoundingClientRect();
- var availRect;
- var ownerDoc = popupElement.ownerDocument;
- var cs = ownerDoc.defaultView.getComputedStyle(popupElement);
- var docElement = ownerDoc.documentElement;
-
- if (cs.position == 'fixed') {
- // For 'fixed' positioned popups, the available rectangle should be based
- // on the viewport rather than the document.
- availRect = {
- height: docElement.clientHeight,
- width: docElement.clientWidth,
- top: 0,
- bottom: docElement.clientHeight,
- left: 0,
- right: docElement.clientWidth
- };
- } else {
- availRect = popupElement.offsetParent.getBoundingClientRect();
- }
-
- if (cs.direction == 'rtl')
- invertLeftRight = !invertLeftRight;
-
- // Flip BEFORE, AFTER based on alignment.
- if (invertLeftRight) {
- if (type == AnchorType.BEFORE)
- type = AnchorType.AFTER;
- else if (type == AnchorType.AFTER)
- type = AnchorType.BEFORE;
- }
-
- // Flip type based on available size
- switch (type) {
- case AnchorType.BELOW:
- if (anchorRect.bottom + popupRect.height > availRect.height &&
- popupRect.height <= anchorRect.top) {
- type = AnchorType.ABOVE;
- }
- break;
- case AnchorType.ABOVE:
- if (popupRect.height > anchorRect.top &&
- anchorRect.bottom + popupRect.height <= availRect.height) {
- type = AnchorType.BELOW;
- }
- break;
- case AnchorType.AFTER:
- if (anchorRect.right + popupRect.width > availRect.width &&
- popupRect.width <= anchorRect.left) {
- type = AnchorType.BEFORE;
- }
- break;
- case AnchorType.BEFORE:
- if (popupRect.width > anchorRect.left &&
- anchorRect.right + popupRect.width <= availRect.width) {
- type = AnchorType.AFTER;
- }
- break;
- }
- // flipping done
-
- var style = popupElement.style;
- // Reset all directions.
- style.left = style.right = style.top = style.bottom = 'auto';
-
- // Primary direction
- switch (type) {
- case AnchorType.BELOW:
- if (anchorRect.bottom + popupRect.height <= availRect.height)
- style.top = anchorRect.bottom + 'px';
- else
- style.bottom = '0';
- break;
- case AnchorType.ABOVE:
- if (availRect.height - anchorRect.top >= 0)
- style.bottom = availRect.height - anchorRect.top + 'px';
- else
- style.top = '0';
- break;
- case AnchorType.AFTER:
- if (anchorRect.right + popupRect.width <= availRect.width)
- style.left = anchorRect.right + 'px';
- else
- style.right = '0';
- break;
- case AnchorType.BEFORE:
- if (availRect.width - anchorRect.left >= 0)
- style.right = availRect.width - anchorRect.left + 'px';
- else
- style.left = '0';
- break;
- }
-
- // Secondary direction
- switch (type) {
- case AnchorType.BELOW:
- case AnchorType.ABOVE:
- if (invertLeftRight) {
- // align right edges
- if (anchorRect.right - popupRect.width >= 0) {
- style.right = availRect.width - anchorRect.right + 'px';
-
- // align left edges
- } else if (anchorRect.left + popupRect.width <= availRect.width) {
- style.left = anchorRect.left + 'px';
-
- // not enough room on either side
- } else {
- style.right = '0';
- }
- } else {
- // align left edges
- if (anchorRect.left + popupRect.width <= availRect.width) {
- style.left = anchorRect.left + 'px';
-
- // align right edges
- } else if (anchorRect.right - popupRect.width >= 0) {
- style.right = availRect.width - anchorRect.right + 'px';
-
- // not enough room on either side
- } else {
- style.left = '0';
- }
- }
- break;
-
- case AnchorType.AFTER:
- case AnchorType.BEFORE:
- // align top edges
- if (anchorRect.top + popupRect.height <= availRect.height) {
- style.top = anchorRect.top + 'px';
-
- // align bottom edges
- } else if (anchorRect.bottom - popupRect.height >= 0) {
- style.bottom = availRect.height - anchorRect.bottom + 'px';
-
- // not enough room on either side
- } else {
- style.top = '0';
- }
- break;
- }
- }
-
- /**
- * Positions a popup element relative to an anchor element. The popup element
- * should have position set to absolute and it should be a child of the body
- * element.
- * @param {!HTMLElement} anchorElement The element that the popup is anchored
- * to.
- * @param {!HTMLElement} popupElement The popup element we are positioning.
- * @param {AnchorType} type The type of anchoring we want.
- * @param {boolean} invertLeftRight Whether to invert the right/left
- * alignment.
- */
- function positionPopupAroundElement(anchorElement, popupElement, type,
- invertLeftRight) {
- var anchorRect = anchorElement.getBoundingClientRect();
- positionPopupAroundRect(anchorRect, popupElement, type, invertLeftRight);
- }
-
- /**
- * Positions a popup around a point.
- * @param {number} x The client x position.
- * @param {number} y The client y position.
- * @param {!HTMLElement} popupElement The popup element we are positioning.
- */
- function positionPopupAtPoint(x, y, popupElement) {
- var rect = {
- left: x,
- top: y,
- width: 0,
- height: 0,
- right: x,
- bottom: y
- };
- positionPopupAroundRect(rect, popupElement, AnchorType.BELOW);
- }
-
- // Export
- return {
- AnchorType: AnchorType,
- positionPopupAroundElement: positionPopupAroundElement,
- positionPopupAtPoint: positionPopupAtPoint
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui', function() {
- /** @const */
- var Menu = cr.ui.Menu;
- /** @const */
- var positionPopupAroundElement = cr.ui.positionPopupAroundElement;
-
- /**
- * Creates a new menu button element.
- * @param {Object=} opt_propertyBag Optional properties.
- * @constructor
- * @extends {HTMLButtonElement}
- */
- var MenuButton = cr.ui.define('button');
-
- MenuButton.prototype = {
- __proto__: HTMLButtonElement.prototype,
-
- /**
- * Initializes the menu button.
- */
- decorate: function() {
- this.addEventListener('mousedown', this);
- this.addEventListener('keydown', this);
-
- // Adding the 'custom-appearance' class prevents widgets.css from changing
- // the appearance of this element.
- this.classList.add('custom-appearance');
- this.classList.add('menu-button'); // For styles in menu_button.css.
-
- var menu;
- if ((menu = this.getAttribute('menu')))
- this.menu = menu;
-
- // An event tracker for events we only connect to while the menu is
- // displayed.
- this.showingEvents_ = new EventTracker();
-
- this.anchorType = cr.ui.AnchorType.BELOW;
- this.invertLeftRight = false;
- },
-
- /**
- * The menu associated with the menu button.
- * @type {cr.ui.Menu}
- */
- get menu() {
- return this.menu_;
- },
- set menu(menu) {
- if (typeof menu == 'string' && menu[0] == '#') {
- menu = this.ownerDocument.getElementById(menu.slice(1));
- cr.ui.decorate(menu, Menu);
- }
-
- this.menu_ = menu;
- if (menu) {
- if (menu.id)
- this.setAttribute('menu', '#' + menu.id);
- }
- },
-
- /**
- * Handles event callbacks.
- * @param {Event} e The event object.
- */
- handleEvent: function(e) {
- if (!this.menu)
- return;
-
- switch (e.type) {
- case 'mousedown':
- if (e.currentTarget == this.ownerDocument) {
- if (!this.contains(e.target) && !this.menu.contains(e.target))
- this.hideMenu();
- else
- e.preventDefault();
- } else {
- if (this.isMenuShown()) {
- this.hideMenu();
- } else if (e.button == 0) { // Only show the menu when using left
- // mouse button.
- this.showMenu(false);
- // Prevent the button from stealing focus on mousedown.
- e.preventDefault();
- }
- }
- break;
- case 'keydown':
- this.handleKeyDown(e);
- // If the menu is visible we let it handle all the keyboard events.
- if (this.isMenuShown() && e.currentTarget == this.ownerDocument) {
- if (this.menu.handleKeyDown(e)) {
- e.preventDefault();
- e.stopPropagation();
- }
- }
- break;
-
- case 'focus':
- if (!this.contains(e.target) && !this.menu.contains(e.target))
- this.hideMenu();
- break;
-
- case 'activate':
- case 'resize':
- this.hideMenu();
- break;
- }
- },
-
- /**
- * Shows the menu.
- * @param {boolean} shouldSetFocus Whether to set focus on the
- * selected menu item.
- */
- showMenu: function(shouldSetFocus) {
- this.hideMenu();
-
- var event = document.createEvent('UIEvents');
- event.initUIEvent('menushow', true, true, window, null);
-
- if (this.dispatchEvent(event)) {
- this.menu.hidden = false;
-
- this.setAttribute('menu-shown', '');
- if (shouldSetFocus)
- this.menu.focusSelectedItem();
-
- // when the menu is shown we steal all keyboard events.
- var doc = this.ownerDocument;
- var win = doc.defaultView;
- this.showingEvents_.add(doc, 'keydown', this, true);
- this.showingEvents_.add(doc, 'mousedown', this, true);
- this.showingEvents_.add(doc, 'focus', this, true);
- this.showingEvents_.add(win, 'resize', this);
- this.showingEvents_.add(this.menu, 'activate', this);
- this.positionMenu_();
- }
- },
-
- /**
- * Hides the menu. If your menu can go out of scope, make sure to call this
- * first.
- */
- hideMenu: function() {
- if (!this.isMenuShown())
- return;
-
- this.removeAttribute('menu-shown');
- this.menu.hidden = true;
-
- this.showingEvents_.removeAll();
- this.focus();
- },
-
- /**
- * Whether the menu is shown.
- */
- isMenuShown: function() {
- return this.hasAttribute('menu-shown');
- },
-
- /**
- * Positions the menu below the menu button. At this point we do not use any
- * advanced positioning logic to ensure the menu fits in the viewport.
- * @private
- */
- positionMenu_: function() {
- positionPopupAroundElement(this, this.menu, this.anchorType,
- this.invertLeftRight);
- },
-
- /**
- * Handles the keydown event for the menu button.
- */
- handleKeyDown: function(e) {
- switch (e.keyIdentifier) {
- case 'Down':
- case 'Up':
- case 'Enter':
- case 'U+0020': // Space
- if (!this.isMenuShown())
- this.showMenu(true);
- e.preventDefault();
- break;
- case 'Esc':
- case 'U+001B': // Maybe this is remote desktop playing a prank?
- case 'U+0009': // Tab
- this.hideMenu();
- break;
- }
- }
- };
-
- /**
- * Helper for styling a menu button with a drop-down arrow indicator.
- * Creates a new 2D canvas context and draws a downward-facing arrow into it.
- * @param {string} canvasName The name of the canvas. The canvas can be
- * addressed from CSS using -webkit-canvas(<canvasName>).
- * @param {number} width The width of the canvas and the arrow.
- * @param {number} height The height of the canvas and the arrow.
- * @param {string} colorSpec The CSS color to use when drawing the arrow.
- */
- function createDropDownArrowCanvas(canvasName, width, height, colorSpec) {
- var ctx = document.getCSSCanvasContext('2d', canvasName, width, height);
- ctx.fillStyle = ctx.strokeStyle = colorSpec;
- ctx.beginPath();
- ctx.moveTo(0, 0);
- ctx.lineTo(width, 0);
- ctx.lineTo(height, height);
- ctx.closePath();
- ctx.fill();
- ctx.stroke();
- };
-
- /** @const */ var ARROW_WIDTH = 6;
- /** @const */ var ARROW_HEIGHT = 3;
-
- /**
- * Create the images used to style drop-down-style MenuButtons.
- * This should be called before creating any MenuButtons that will have the
- * CSS class 'drop-down'. If no colors are specified, defaults will be used.
- * @param {=string} normalColor CSS color for the default button state.
- * @param {=string} hoverColor CSS color for the hover button state.
- * @param {=string} activeColor CSS color for the active button state.
- */
- MenuButton.createDropDownArrows = function(
- normalColor, hoverColor, activeColor) {
- normalColor = normalColor || 'rgb(192, 195, 198)';
- hoverColor = hoverColor || 'rgb(48, 57, 66)';
- activeColor = activeColor || 'white';
-
- createDropDownArrowCanvas(
- 'drop-down-arrow', ARROW_WIDTH, ARROW_HEIGHT, normalColor);
- createDropDownArrowCanvas(
- 'drop-down-arrow-hover', ARROW_WIDTH, ARROW_HEIGHT, hoverColor);
- createDropDownArrowCanvas(
- 'drop-down-arrow-active', ARROW_WIDTH, ARROW_HEIGHT, activeColor);
- };
-
- // Export
- return {
- MenuButton: MenuButton
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview This implements a special button that is useful for showing a
- * context menu.
- */
-
- cr.define('cr.ui', function() {
- /** @const */ var MenuButton = cr.ui.MenuButton;
-
- /**
- * Helper function for ContextMenuButton to find the first ancestor of the
- * button that has a context menu.
- * @param {!MenuButton} el The button to start the search from.
- * @return {HTMLElement} The found element or null if not found.
- */
- function getContextMenuTarget(el) {
- do {
- el = el.parentNode;
- } while (el && !('contextMenu' in el));
- return el;
- }
-
- /**
- * Creates a new menu button which is used to show the context menu for an
- * ancestor that has a {@code contextMenu} property.
- * @param {Object=} opt_propertyBag Optional properties.
- * @constructor
- * @extends {MenuButton}
- */
- var ContextMenuButton = cr.ui.define('button');
-
- ContextMenuButton.prototype = {
- __proto__: MenuButton.prototype,
-
- /**
- * Override to return the contextMenu for the ancestor.
- * @override
- * @type {cr.ui.Menu}
- */
- get menu() {
- var target = getContextMenuTarget(this);
- return target && target.contextMenu;
- },
-
- /** @override */
- decorate: function() {
- this.tabIndex = -1;
- this.addEventListener('mouseup', this);
- MenuButton.prototype.decorate.call(this);
- },
-
- /** @override */
- handleEvent: function(e) {
- switch (e.type) {
- case 'mousedown':
- // Menu buttons prevent focus changes.
- var target = getContextMenuTarget(this);
- if (target)
- target.focus();
- break;
- case 'mouseup':
- // Stop mouseup to prevent selection changes.
- e.stopPropagation();
- break;
- }
- MenuButton.prototype.handleEvent.call(this, e);
- }
- };
-
- // Export
- return {
- ContextMenuButton: ContextMenuButton
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview Touch Handler. Class that handles all touch events and
- * uses them to interpret higher level gestures and behaviors. TouchEvent is a
- * built in mobile safari type:
- * http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html.
- * This class is intended to work with all webkit browsers, tested on Chrome and
- * iOS.
- *
- * The following types of gestures are currently supported. See the definition
- * of TouchHandler.EventType for details.
- *
- * Single Touch:
- * This provides simple single-touch events. Any secondary touch is
- * ignored.
- *
- * Drag:
- * A single touch followed by some movement. This behavior will handle all
- * of the required events and report the properties of the drag to you
- * while the touch is happening and at the end of the drag sequence. This
- * behavior will NOT perform the actual dragging (redrawing the element)
- * for you, this responsibility is left to the client code.
- *
- * Long press:
- * When your element is touched and held without any drag occuring, the
- * LONG_PRESS event will fire.
- */
-
- // Use an anonymous function to enable strict mode just for this file (which
- // will be concatenated with other files when embedded in Chrome)
- cr.define('cr.ui', function() {
- 'use strict';
-
- /**
- * A TouchHandler attaches to an Element, listents for low-level touch (or
- * mouse) events and dispatching higher-level events on the element.
- * @param {!Element} element The element to listen on and fire events
- * for.
- * @constructor
- */
- function TouchHandler(element) {
- /**
- * @type {!Element}
- * @private
- */
- this.element_ = element;
-
- /**
- * The absolute sum of all touch y deltas.
- * @type {number}
- * @private
- */
- this.totalMoveY_ = 0;
-
- /**
- * The absolute sum of all touch x deltas.
- * @type {number}
- * @private
- */
- this.totalMoveX_ = 0;
-
- /**
- * An array of tuples where the first item is the horizontal component of a
- * recent relevant touch and the second item is the touch's time stamp. Old
- * touches are removed based on the max tracking time and when direction
- * changes.
- * @type {!Array.<number>}
- * @private
- */
- this.recentTouchesX_ = [];
-
- /**
- * An array of tuples where the first item is the vertical component of a
- * recent relevant touch and the second item is the touch's time stamp. Old
- * touches are removed based on the max tracking time and when direction
- * changes.
- * @type {!Array.<number>}
- * @private
- */
- this.recentTouchesY_ = [];
-
- /**
- * Used to keep track of all events we subscribe to so we can easily clean
- * up
- * @type {EventTracker}
- * @private
- */
- this.events_ = new EventTracker();
- }
-
-
- /**
- * DOM Events that may be fired by the TouchHandler at the element
- */
- TouchHandler.EventType = {
- // Fired whenever the element is touched as the only touch to the device.
- // enableDrag defaults to false, set to true to permit dragging.
- TOUCH_START: 'touchHandler:touch_start',
-
- // Fired when an element is held for a period of time. Prevents dragging
- // from occuring (even if enableDrag was set to true).
- LONG_PRESS: 'touchHandler:long_press',
-
- // If enableDrag was set to true at TOUCH_START, DRAG_START will fire when
- // the touch first moves sufficient distance. enableDrag is set to true but
- // can be reset to false to cancel the drag.
- DRAG_START: 'touchHandler:drag_start',
-
- // If enableDrag was true after DRAG_START, DRAG_MOVE will fire whenever the
- // touch is moved.
- DRAG_MOVE: 'touchHandler:drag_move',
-
- // Fired just before TOUCH_END when a drag is released. Correlates 1:1 with
- // a DRAG_START.
- DRAG_END: 'touchHandler:drag_end',
-
- // Fired whenever a touch that is being tracked has been released.
- // Correlates 1:1 with a TOUCH_START.
- TOUCH_END: 'touchHandler:touch_end',
-
- // Fired whenever the element is tapped in a short time and no dragging is
- // detected.
- TAP: 'touchHandler:tap'
- };
-
-
- /**
- * The type of event sent by TouchHandler
- * @constructor
- * @param {string} type The type of event (one of cr.ui.Grabber.EventType).
- * @param {boolean} bubbles Whether or not the event should bubble.
- * @param {number} clientX The X location of the touch.
- * @param {number} clientY The Y location of the touch.
- * @param {!Element} touchedElement The element at the current location of the
- * touch.
- */
- TouchHandler.Event = function(type, bubbles, clientX, clientY,
- touchedElement) {
- var event = document.createEvent('Event');
- event.initEvent(type, bubbles, true);
- event.__proto__ = TouchHandler.Event.prototype;
-
- /**
- * The X location of the touch affected
- * @type {number}
- */
- event.clientX = clientX;
-
- /**
- * The Y location of the touch affected
- * @type {number}
- */
- event.clientY = clientY;
-
- /**
- * The element at the current location of the touch.
- * @type {!Element}
- */
- event.touchedElement = touchedElement;
-
- return event;
- };
-
- TouchHandler.Event.prototype = {
- __proto__: Event.prototype,
-
- /**
- * For TOUCH_START and DRAG START events, set to true to enable dragging or
- * false to disable dragging.
- * @type {boolean|undefined}
- */
- enableDrag: undefined,
-
- /**
- * For DRAG events, provides the horizontal component of the
- * drag delta. Drag delta is defined as the delta of the start touch
- * position and the current drag position.
- * @type {number|undefined}
- */
- dragDeltaX: undefined,
-
- /**
- * For DRAG events, provides the vertical component of the
- * drag delta.
- * @type {number|undefined}
- */
- dragDeltaY: undefined
- };
-
- /**
- * Maximum movement of touch required to be considered a tap.
- * @type {number}
- * @private
- */
- TouchHandler.MAX_TRACKING_FOR_TAP_ = 8;
-
-
- /**
- * The maximum number of ms to track a touch event. After an event is older
- * than this value, it will be ignored in velocity calculations.
- * @type {number}
- * @private
- */
- TouchHandler.MAX_TRACKING_TIME_ = 250;
-
-
- /**
- * The maximum number of touches to track.
- * @type {number}
- * @private
- */
- TouchHandler.MAX_TRACKING_TOUCHES_ = 5;
-
-
- /**
- * The maximum velocity to return, in pixels per millisecond, that is used
- * to guard against errors in calculating end velocity of a drag. This is a
- * very fast drag velocity.
- * @type {number}
- * @private
- */
- TouchHandler.MAXIMUM_VELOCITY_ = 5;
-
-
- /**
- * The velocity to return, in pixel per millisecond, when the time stamps on
- * the events are erroneous. The browser can return bad time stamps if the
- * thread is blocked for the duration of the drag. This is a low velocity to
- * prevent the content from moving quickly after a slow drag. It is less
- * jarring if the content moves slowly after a fast drag.
- * @type {number}
- * @private
- */
- TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ = 1;
-
- /**
- * The time, in milliseconds, that a touch must be held to be considered
- * 'long'.
- * @type {number}
- * @private
- */
- TouchHandler.TIME_FOR_LONG_PRESS_ = 500;
-
- TouchHandler.prototype = {
- /**
- * If defined, the identifer of the single touch that is active. Note that
- * 0 is a valid touch identifier - it should not be treated equivalently to
- * undefined.
- * @type {number|undefined}
- * @private
- */
- activeTouch_: undefined,
-
- /**
- * @type {boolean|undefined}
- * @private
- */
- tracking_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- startTouchX_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- startTouchY_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- endTouchX_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- endTouchY_: undefined,
-
- /**
- * Time of the touchstart event.
- * @type {number|undefined}
- * @private
- */
- startTime_: undefined,
-
- /**
- * The time of the touchend event.
- * @type {number|undefined}
- * @private
- */
- endTime_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- lastTouchX_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- lastTouchY_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- lastMoveX_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- lastMoveY_: undefined,
-
- /**
- * @type {number|undefined}
- * @private
- */
- longPressTimeout_: undefined,
-
- /**
- * If defined and true, the next click event should be swallowed
- * @type {boolean|undefined}
- * @private
- */
- swallowNextClick_: undefined,
-
- /**
- * @type {boolean}
- * @private
- */
- draggingEnabled_: false,
-
- /**
- * Start listenting for events.
- * @param {boolean=} opt_capture True if the TouchHandler should listen to
- * during the capture phase.
- * @param {boolean=} opt_mouse True if the TouchHandler should generate
- * events for mouse input (in addition to touch input).
- */
- enable: function(opt_capture, opt_mouse) {
- var capture = !!opt_capture;
-
- // Just listen to start events for now. When a touch is occuring we'll
- // want to be subscribed to move and end events on the document, but we
- // don't want to incur the cost of lots of no-op handlers on the document.
- this.events_.add(this.element_, 'touchstart', this.onStart_.bind(this),
- capture);
- if (opt_mouse) {
- this.events_.add(this.element_, 'mousedown',
- this.mouseToTouchCallback_(this.onStart_.bind(this)),
- capture);
- }
-
- // If the element is long-pressed, we may need to swallow a click
- this.events_.add(this.element_, 'click', this.onClick_.bind(this), true);
- },
-
- /**
- * Stop listening to all events.
- */
- disable: function() {
- this.stopTouching_();
- this.events_.removeAll();
- },
-
- /**
- * Wraps a callback with translations of mouse events to touch events.
- * NOTE: These types really should be function(Event) but then we couldn't
- * use this with bind (which operates on any type of function). Doesn't
- * JSDoc support some sort of polymorphic types?
- * @param {Function} callback The event callback.
- * @return {Function} The wrapping callback.
- * @private
- */
- mouseToTouchCallback_: function(callback) {
- return function(e) {
- // Note that there may be synthesizes mouse events caused by touch
- // events (a mouseDown after a touch-click). We leave it up to the
- // client to worry about this if it matters to them (typically a short
- // mouseDown/mouseUp without a click is no big problem and it's not
- // obvious how we identify such synthesized events in a general way).
- var touch = {
- // any fixed value will do for the identifier - there will only
- // ever be a single active 'touch' when using the mouse.
- identifier: 0,
- clientX: e.clientX,
- clientY: e.clientY,
- target: e.target
- };
- e.touches = [];
- e.targetTouches = [];
- e.changedTouches = [touch];
- if (e.type != 'mouseup') {
- e.touches[0] = touch;
- e.targetTouches[0] = touch;
- }
- callback(e);
- };
- },
-
- /**
- * Begin tracking the touchable element, it is eligible for dragging.
- * @private
- */
- beginTracking_: function() {
- this.tracking_ = true;
- },
-
- /**
- * Stop tracking the touchable element, it is no longer dragging.
- * @private
- */
- endTracking_: function() {
- this.tracking_ = false;
- this.dragging_ = false;
- this.totalMoveY_ = 0;
- this.totalMoveX_ = 0;
- },
-
- /**
- * Reset the touchable element as if we never saw the touchStart
- * Doesn't dispatch any end events - be careful of existing listeners.
- */
- cancelTouch: function() {
- this.stopTouching_();
- this.endTracking_();
- // If clients needed to be aware of this, we could fire a cancel event
- // here.
- },
-
- /**
- * Record that touching has stopped
- * @private
- */
- stopTouching_: function() {
- // Mark as no longer being touched
- this.activeTouch_ = undefined;
-
- // If we're waiting for a long press, stop
- window.clearTimeout(this.longPressTimeout_);
-
- // Stop listening for move/end events until there's another touch.
- // We don't want to leave handlers piled up on the document.
- // Note that there's no harm in removing handlers that weren't added, so
- // rather than track whether we're using mouse or touch we do both.
- this.events_.remove(document, 'touchmove');
- this.events_.remove(document, 'touchend');
- this.events_.remove(document, 'touchcancel');
- this.events_.remove(document, 'mousemove');
- this.events_.remove(document, 'mouseup');
- },
-
- /**
- * Touch start handler.
- * @param {!TouchEvent} e The touchstart event.
- * @private
- */
- onStart_: function(e) {
- // Only process single touches. If there is already a touch happening, or
- // two simultaneous touches then just ignore them.
- if (e.touches.length > 1)
- // Note that we could cancel an active touch here. That would make
- // simultaneous touch behave similar to near-simultaneous. However, if
- // the user is dragging something, an accidental second touch could be
- // quite disruptive if it cancelled their drag. Better to just ignore
- // it.
- return;
-
- // It's still possible there could be an active "touch" if the user is
- // simultaneously using a mouse and a touch input.
- if (this.activeTouch_ !== undefined)
- return;
-
- var touch = e.targetTouches[0];
- this.activeTouch_ = touch.identifier;
-
- // We've just started touching so shouldn't swallow any upcoming click
- if (this.swallowNextClick_)
- this.swallowNextClick_ = false;
-
- this.disableTap_ = false;
-
- // Sign up for end/cancel notifications for this touch.
- // Note that we do this on the document so that even if the user drags
- // their finger off the element, we'll still know what they're doing.
- if (e.type == 'mousedown') {
- this.events_.add(document, 'mouseup',
- this.mouseToTouchCallback_(this.onEnd_.bind(this)), false);
- } else {
- this.events_.add(document, 'touchend', this.onEnd_.bind(this), false);
- this.events_.add(document, 'touchcancel', this.onEnd_.bind(this),
- false);
- }
-
- // This timeout is cleared on touchEnd and onDrag
- // If we invoke the function then we have a real long press
- window.clearTimeout(this.longPressTimeout_);
- this.longPressTimeout_ = window.setTimeout(
- this.onLongPress_.bind(this),
- TouchHandler.TIME_FOR_LONG_PRESS_);
-
- // Dispatch the TOUCH_START event
- this.draggingEnabled_ =
- !!this.dispatchEvent_(TouchHandler.EventType.TOUCH_START, touch);
-
- // We want dragging notifications
- if (e.type == 'mousedown') {
- this.events_.add(document, 'mousemove',
- this.mouseToTouchCallback_(this.onMove_.bind(this)), false);
- } else {
- this.events_.add(document, 'touchmove', this.onMove_.bind(this), false);
- }
-
- this.startTouchX_ = this.lastTouchX_ = touch.clientX;
- this.startTouchY_ = this.lastTouchY_ = touch.clientY;
- this.startTime_ = e.timeStamp;
-
- this.recentTouchesX_ = [];
- this.recentTouchesY_ = [];
- this.recentTouchesX_.push(touch.clientX, e.timeStamp);
- this.recentTouchesY_.push(touch.clientY, e.timeStamp);
-
- this.beginTracking_();
- },
-
- /**
- * Given a list of Touches, find the one matching our activeTouch
- * identifier. Note that Chrome currently always uses 0 as the identifier.
- * In that case we'll end up always choosing the first element in the list.
- * @param {TouchList} touches The list of Touch objects to search.
- * @return {!Touch|undefined} The touch matching our active ID if any.
- * @private
- */
- findActiveTouch_: function(touches) {
- assert(this.activeTouch_ !== undefined, 'Expecting an active touch');
- // A TouchList isn't actually an array, so we shouldn't use
- // Array.prototype.filter/some, etc.
- for (var i = 0; i < touches.length; i++) {
- if (touches[i].identifier == this.activeTouch_)
- return touches[i];
- }
- return undefined;
- },
-
- /**
- * Touch move handler.
- * @param {!TouchEvent} e The touchmove event.
- * @private
- */
- onMove_: function(e) {
- if (!this.tracking_)
- return;
-
- // Our active touch should always be in the list of touches still active
- assert(this.findActiveTouch_(e.touches), 'Missing touchEnd');
-
- var that = this;
- var touch = this.findActiveTouch_(e.changedTouches);
- if (!touch)
- return;
-
- var clientX = touch.clientX;
- var clientY = touch.clientY;
-
- var moveX = this.lastTouchX_ - clientX;
- var moveY = this.lastTouchY_ - clientY;
- this.totalMoveX_ += Math.abs(moveX);
- this.totalMoveY_ += Math.abs(moveY);
- this.lastTouchX_ = clientX;
- this.lastTouchY_ = clientY;
-
- var couldBeTap =
- this.totalMoveY_ <= TouchHandler.MAX_TRACKING_FOR_TAP_ ||
- this.totalMoveX_ <= TouchHandler.MAX_TRACKING_FOR_TAP_;
-
- if (!couldBeTap)
- this.disableTap_ = true;
-
- if (this.draggingEnabled_ && !this.dragging_ && !couldBeTap) {
- // If we're waiting for a long press, stop
- window.clearTimeout(this.longPressTimeout_);
-
- // Dispatch the DRAG_START event and record whether dragging should be
- // allowed or not. Note that this relies on the current value of
- // startTouchX/Y - handlers may use the initial drag delta to determine
- // if dragging should be permitted.
- this.dragging_ = this.dispatchEvent_(
- TouchHandler.EventType.DRAG_START, touch);
-
- if (this.dragging_) {
- // Update the start position here so that drag deltas have better
- // values but don't touch the recent positions so that velocity
- // calculations can still use touchstart position in the time and
- // distance delta.
- this.startTouchX_ = clientX;
- this.startTouchY_ = clientY;
- this.startTime_ = e.timeStamp;
- } else {
- this.endTracking_();
- }
- }
-
- if (this.dragging_) {
- this.dispatchEvent_(TouchHandler.EventType.DRAG_MOVE, touch);
-
- this.removeTouchesInWrongDirection_(this.recentTouchesX_,
- this.lastMoveX_, moveX);
- this.removeTouchesInWrongDirection_(this.recentTouchesY_,
- this.lastMoveY_, moveY);
- this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
- this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
- this.recentTouchesX_.push(clientX, e.timeStamp);
- this.recentTouchesY_.push(clientY, e.timeStamp);
- }
-
- this.lastMoveX_ = moveX;
- this.lastMoveY_ = moveY;
- },
-
- /**
- * Filters the provided recent touches array to remove all touches except
- * the last if the move direction has changed.
- * @param {!Array.<number>} recentTouches An array of tuples where the first
- * item is the x or y component of the recent touch and the second item
- * is the touch time stamp.
- * @param {number|undefined} lastMove The x or y component of the previous
- * move.
- * @param {number} recentMove The x or y component of the most recent move.
- * @private
- */
- removeTouchesInWrongDirection_: function(recentTouches, lastMove,
- recentMove) {
- if (lastMove && recentMove && recentTouches.length > 2 &&
- (lastMove > 0 ^ recentMove > 0)) {
- recentTouches.splice(0, recentTouches.length - 2);
- }
- },
-
- /**
- * Filters the provided recent touches array to remove all touches older
- * than the max tracking time or the 5th most recent touch.
- * @param {!Array.<number>} recentTouches An array of tuples where the first
- * item is the x or y component of the recent touch and the second item
- * is the touch time stamp.
- * @param {number} recentTime The time of the most recent event.
- * @private
- */
- removeOldTouches_: function(recentTouches, recentTime) {
- while (recentTouches.length && recentTime - recentTouches[1] >
- TouchHandler.MAX_TRACKING_TIME_ ||
- recentTouches.length >
- TouchHandler.MAX_TRACKING_TOUCHES_ * 2) {
- recentTouches.splice(0, 2);
- }
- },
-
- /**
- * Touch end handler.
- * @param {!TouchEvent} e The touchend event.
- * @private
- */
- onEnd_: function(e) {
- var that = this;
- assert(this.activeTouch_ !== undefined, 'Expect to already be touching');
-
- // If the touch we're tracking isn't changing here, ignore this touch end.
- var touch = this.findActiveTouch_(e.changedTouches);
- if (!touch) {
- // In most cases, our active touch will be in the 'touches' collection,
- // but we can't assert that because occasionally two touchend events can
- // occur at almost the same time with both having empty 'touches' lists.
- // I.e., 'touches' seems like it can be a bit more up-to-date than the
- // current event.
- return;
- }
-
- // This is touchEnd for the touch we're monitoring
- assert(!this.findActiveTouch_(e.touches),
- 'Touch ended also still active');
-
- // Indicate that touching has finished
- this.stopTouching_();
-
- if (this.tracking_) {
- var clientX = touch.clientX;
- var clientY = touch.clientY;
-
- if (this.dragging_) {
- this.endTime_ = e.timeStamp;
- this.endTouchX_ = clientX;
- this.endTouchY_ = clientY;
-
- this.removeOldTouches_(this.recentTouchesX_, e.timeStamp);
- this.removeOldTouches_(this.recentTouchesY_, e.timeStamp);
-
- this.dispatchEvent_(TouchHandler.EventType.DRAG_END, touch);
-
- // Note that in some situations we can get a click event here as well.
- // For now this isn't a problem, but we may want to consider having
- // some logic that hides clicks that appear to be caused by a touchEnd
- // used for dragging.
- }
-
- this.endTracking_();
- }
- this.draggingEnabled_ = false;
-
- // Note that we dispatch the touchEnd event last so that events at
- // different levels of semantics nest nicely (similar to how DOM
- // drag-and-drop events are nested inside of the mouse events that trigger
- // them).
- this.dispatchEvent_(TouchHandler.EventType.TOUCH_END, touch);
- if (!this.disableTap_)
- this.dispatchEvent_(TouchHandler.EventType.TAP, touch);
- },
-
- /**
- * Get end velocity of the drag. This method is specific to drag behavior,
- * so if touch behavior and drag behavior is split then this should go with
- * drag behavior. End velocity is defined as deltaXY / deltaTime where
- * deltaXY is the difference between endPosition and the oldest recent
- * position, and deltaTime is the difference between endTime and the oldest
- * recent time stamp.
- * @return {Object} The x and y velocity.
- */
- getEndVelocity: function() {
- // Note that we could move velocity to just be an end-event parameter.
- var velocityX = this.recentTouchesX_.length ?
- (this.endTouchX_ - this.recentTouchesX_[0]) /
- (this.endTime_ - this.recentTouchesX_[1]) : 0;
- var velocityY = this.recentTouchesY_.length ?
- (this.endTouchY_ - this.recentTouchesY_[0]) /
- (this.endTime_ - this.recentTouchesY_[1]) : 0;
-
- velocityX = this.correctVelocity_(velocityX);
- velocityY = this.correctVelocity_(velocityY);
-
- return {
- x: velocityX,
- y: velocityY
- };
- },
-
- /**
- * Correct erroneous velocities by capping the velocity if we think it's too
- * high, or setting it to a default velocity if know that the event data is
- * bad.
- * @param {number} velocity The x or y velocity component.
- * @return {number} The corrected velocity.
- * @private
- */
- correctVelocity_: function(velocity) {
- var absVelocity = Math.abs(velocity);
-
- // We add to recent touches for each touchstart and touchmove. If we have
- // fewer than 3 touches (6 entries), we assume that the thread was blocked
- // for the duration of the drag and we received events in quick succession
- // with the wrong time stamps.
- if (absVelocity > TouchHandler.MAXIMUM_VELOCITY_) {
- absVelocity = this.recentTouchesY_.length < 3 ?
- TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ :
- TouchHandler.MAXIMUM_VELOCITY_;
- }
- return absVelocity * (velocity < 0 ? -1 : 1);
- },
-
- /**
- * Handler when an element has been pressed for a long time
- * @private
- */
- onLongPress_: function() {
- // Swallow any click that occurs on this element without an intervening
- // touch start event. This simple click-busting technique should be
- // sufficient here since a real click should have a touchstart first.
- this.swallowNextClick_ = true;
- this.disableTap_ = true;
-
- // Dispatch to the LONG_PRESS
- this.dispatchEventXY_(TouchHandler.EventType.LONG_PRESS, this.element_,
- this.startTouchX_, this.startTouchY_);
- },
-
- /**
- * Click handler - used to swallow clicks after a long-press
- * @param {!Event} e The click event.
- * @private
- */
- onClick_: function(e) {
- if (this.swallowNextClick_) {
- e.preventDefault();
- e.stopPropagation();
- this.swallowNextClick_ = false;
- }
- },
-
- /**
- * Dispatch a TouchHandler event to the element
- * @param {string} eventType The event to dispatch.
- * @param {Touch} touch The touch triggering this event.
- * @return {boolean|undefined} The value of enableDrag after dispatching
- * the event.
- * @private
- */
- dispatchEvent_: function(eventType, touch) {
-
- // Determine which element was touched. For mouse events, this is always
- // the event/touch target. But for touch events, the target is always the
- // target of the touchstart (and it's unlikely we can change this
- // since the common implementation of touch dragging relies on it). Since
- // touch is our primary scenario (which we want to emulate with mouse),
- // we'll treat both cases the same and not depend on the target.
- var touchedElement;
- if (eventType == TouchHandler.EventType.TOUCH_START) {
- touchedElement = touch.target;
- } else {
- touchedElement = this.element_.ownerDocument.
- elementFromPoint(touch.clientX, touch.clientY);
- }
-
- return this.dispatchEventXY_(eventType, touchedElement, touch.clientX,
- touch.clientY);
- },
-
- /**
- * Dispatch a TouchHandler event to the element
- * @param {string} eventType The event to dispatch.
- @param {number} clientX The X location for the event.
- @param {number} clientY The Y location for the event.
- * @return {boolean|undefined} The value of enableDrag after dispatching
- * the event.
- * @private
- */
- dispatchEventXY_: function(eventType, touchedElement, clientX, clientY) {
- var isDrag = (eventType == TouchHandler.EventType.DRAG_START ||
- eventType == TouchHandler.EventType.DRAG_MOVE ||
- eventType == TouchHandler.EventType.DRAG_END);
-
- // Drag events don't bubble - we're really just dragging the element,
- // not affecting its parent at all.
- var bubbles = !isDrag;
-
- var event = new TouchHandler.Event(eventType, bubbles, clientX, clientY,
- touchedElement);
-
- // Set enableDrag when it can be overridden
- if (eventType == TouchHandler.EventType.TOUCH_START)
- event.enableDrag = false;
- else if (eventType == TouchHandler.EventType.DRAG_START)
- event.enableDrag = true;
-
- if (isDrag) {
- event.dragDeltaX = clientX - this.startTouchX_;
- event.dragDeltaY = clientY - this.startTouchY_;
- }
-
- this.element_.dispatchEvent(event);
- return event.enableDrag;
- }
- };
-
- return {
- TouchHandler: TouchHandler
- };
- });
- </script>
-
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview This file provides utility functions for position popups.
- */
-
- cr.define('cr.ui', function() {
-
- /**
- * Type def for rects as returned by getBoundingClientRect.
- * @typedef { {left: number, top: number, width: number, height: number,
- * right: number, bottom: number}}
- */
- var Rect;
-
- /** @const */
- var AnchorType = cr.ui.AnchorType;
-
- /**
- * @type {Number}
- * @const
- */
- var BOOKMARK_BAR_HEIGHT = 48;
-
- /**
- * Helper function for positionPopupAroundElement and positionPopupAroundRect.
- * @param {!Rect} anchorRect The rect for the anchor.
- * @param {!HTMLElement} popupElement The element used for the popup.
- * @param {AnchorType} type The type of anchoring to do.
- * @param {boolean} invertLeftRight Whether to invert the right/left
- * alignment.
- */
- function positionPopupAroundRect(anchorRect, popupElement, type,
- invertLeftRight) {
- var popupRect = popupElement.getBoundingClientRect();
- var availRect;
- var ownerDoc = popupElement.ownerDocument;
- var cs = ownerDoc.defaultView.getComputedStyle(popupElement);
- var docElement = ownerDoc.documentElement;
-
- if (cs.position == 'fixed') {
- // For 'fixed' positioned popups, the available rectangle should be based
- // on the viewport rather than the document.
- availRect = {
- height: docElement.clientHeight,
- width: docElement.clientWidth,
- top: 0,
- bottom: docElement.clientHeight,
- left: 0,
- right: docElement.clientWidth
- };
- } else {
- availRect = popupElement.offsetParent.getBoundingClientRect();
- }
-
- if (cs.direction == 'rtl')
- invertLeftRight = !invertLeftRight;
-
- // Flip BEFORE, AFTER based on alignment.
- if (invertLeftRight) {
- if (type == AnchorType.BEFORE)
- type = AnchorType.AFTER;
- else if (type == AnchorType.AFTER)
- type = AnchorType.BEFORE;
- }
-
- // Flip type based on available size
- switch (type) {
- case AnchorType.BELOW:
- // Do not flip when the type is below to avoid crbug.com/164113.
- break;
- case AnchorType.ABOVE:
- if (popupRect.height > anchorRect.top &&
- anchorRect.bottom + popupRect.height <= availRect.height) {
- type = AnchorType.BELOW;
- }
- break;
- case AnchorType.AFTER:
- if (anchorRect.right + popupRect.width > availRect.width &&
- popupRect.width <= anchorRect.left) {
- type = AnchorType.BEFORE;
- }
- break;
- case AnchorType.BEFORE:
- if (popupRect.width > anchorRect.left &&
- anchorRect.right + popupRect.width <= availRect.width) {
- type = AnchorType.AFTER;
- }
- break;
- default:
- assert(false, 'unknown type');
- }
- // flipping done
-
- var style = popupElement.style;
- // Reset all directions.
- style.left = style.right = style.top = style.bottom = 'auto';
-
- // Primary direction
- switch (type) {
- case AnchorType.BELOW:
- if (anchorRect.bottom + popupRect.height <= availRect.height -
- BOOKMARK_BAR_HEIGHT) {
- style.top = anchorRect.bottom + 'px';
- } else {
- style.bottom = BOOKMARK_BAR_HEIGHT + 'px';
- }
- break;
- case AnchorType.ABOVE:
- if (availRect.height - anchorRect.top >= 0)
- style.bottom = availRect.height - anchorRect.top + 'px';
- else
- style.top = '0';
- break;
- case AnchorType.AFTER:
- if (anchorRect.right + popupRect.width <= availRect.width)
- style.left = anchorRect.right + 'px';
- else
- style.right = '0';
- break;
- case AnchorType.BEFORE:
- if (availRect.width - anchorRect.left >= 0)
- style.right = availRect.width - anchorRect.left + 'px';
- else
- style.left = '0';
- break;
- }
-
- // Secondary direction
- switch (type) {
- case AnchorType.BELOW:
- case AnchorType.ABOVE:
- if (invertLeftRight) {
- // align right edges
- if (anchorRect.right - popupRect.width >= 0) {
- style.right = availRect.width - anchorRect.right + 'px';
-
- // align left edges
- } else if (anchorRect.left + popupRect.width <= availRect.width) {
- style.left = anchorRect.left + 'px';
-
- // not enough room on either side
- } else {
- style.right = '0';
- }
- } else {
- // align left edges
- if (anchorRect.left + popupRect.width <= availRect.width) {
- style.left = anchorRect.left + 'px';
-
- // align right edges
- } else if (anchorRect.right - popupRect.width >= 0) {
- style.right = availRect.width - anchorRect.right + 'px';
-
- // not enough room on either side
- } else {
- style.left = '0';
- }
- }
- break;
-
- case AnchorType.AFTER:
- case AnchorType.BEFORE:
- // align top edges
- if (anchorRect.top + popupRect.height <= availRect.height) {
- style.top = anchorRect.top + 'px';
-
- // align bottom edges
- } else if (anchorRect.bottom - popupRect.height >= 0) {
- style.bottom = availRect.height - anchorRect.bottom + 'px';
-
- // not enough room on either side
- } else {
- style.top = '0';
- }
- break;
- }
- }
-
- /**
- * Positions a popup element relative to an anchor element. The popup element
- * should have position set to absolute and it should be a child of the body
- * element.
- * @param {!HTMLElement} anchorElement The element that the popup is anchored
- * to.
- * @param {!HTMLElement} popupElement The popup element we are positioning.
- * @param {AnchorType} type The type of anchoring we want.
- * @param {boolean} invertLeftRight Whether to invert the right/left
- * alignment.
- */
- function positionPopupAroundElement(anchorElement, popupElement, type,
- invertLeftRight) {
- var anchorRect = anchorElement.getBoundingClientRect();
- positionPopupAroundRect(anchorRect, popupElement, type, invertLeftRight);
- }
-
- /**
- * Positions a popup around a point.
- * @param {number} x The client x position.
- * @param {number} y The client y position.
- * @param {!HTMLElement} popupElement The popup element we are positioning.
- */
- function positionPopupAtPoint(x, y, popupElement) {
- var rect = {
- left: x,
- top: y,
- width: 0,
- height: 0,
- right: x,
- bottom: y
- };
- positionPopupAroundRect(rect, popupElement, AnchorType.BELOW);
- }
-
- // Monkey patch popup positioning methods to avoid the popup being hidden
- // by the top overlay. This is a hacky solution, but temporarily necessary
- // until we get rid of the overlay madness.
- // TODO(pedrosimonetti): Remove this code when deprecating the NTP5 Apps.
- cr.ui.positionPopupAroundElement = positionPopupAroundElement;
- cr.ui.positionPopupAtPoint = positionPopupAtPoint;
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview New tab page
- * This is the main code for the new tab page. NewTabView manages page list,
- * dot list and handles apps pages callbacks from backend. It also handles
- * the layout of the Bottom Panel and the global UI states of the New Tab Page.
- */
-
- // Use an anonymous function to enable strict mode just for this file (which
- // will be concatenated with other files when embedded in Chrome
- cr.define('ntp', function() {
- 'use strict';
-
- var APP_LAUNCH = {
- // The histogram buckets (keep in sync with extension_constants.h).
- NTP_APPS_MAXIMIZED: 0,
- NTP_APPS_COLLAPSED: 1,
- NTP_APPS_MENU: 2,
- NTP_MOST_VISITED: 3,
- NTP_RECENTLY_CLOSED: 4,
- NTP_APP_RE_ENABLE: 16,
- NTP_WEBSTORE_FOOTER: 18,
- NTP_WEBSTORE_PLUS_ICON: 19,
- };
-
- /**
- * @type {number}
- * @const
- */
- var BOTTOM_PANEL_HORIZONTAL_MARGIN = 100;
-
- /**
- * The height required to show the Bottom Panel.
- * @type {number}
- * @const
- */
- var HEIGHT_FOR_BOTTOM_PANEL = 531;
-
- /**
- * The Bottom Panel width required to show 6 cols of Tiles, which is used
- * in the width computation.
- * @type {number}
- * @const
- */
- var MAX_BOTTOM_PANEL_WIDTH = 920;
-
- /**
- * The minimum width of the Bottom Panel's content.
- * @type {number}
- * @const
- */
- var MIN_BOTTOM_PANEL_CONTENT_WIDTH = 200;
-
- /**
- * The minimum Bottom Panel width. If the available width is smaller than
- * this value, then the width of the Bottom Panel's content will be fixed to
- * MIN_BOTTOM_PANEL_CONTENT_WIDTH.
- * @type {number}
- * @const
- */
- var MIN_BOTTOM_PANEL_WIDTH = 300;
-
- /**
- * The normal Bottom Panel width. If the window width is greater than or
- * equal to this value, then the width of the Bottom Panel's content will be
- * the available width minus side margin. If the available width is smaller
- * than this value, then the width of the Bottom Panel's content will be an
- * interpolation between the normal width, and the minimum width defined by
- * the constant MIN_BOTTOM_PANEL_CONTENT_WIDTH.
- * @type {number}
- * @const
- */
- var NORMAL_BOTTOM_PANEL_WIDTH = 500;
-
- /**
- * @type {number}
- * @const
- */
- var TILE_ROW_HEIGHT = 100;
-
- //----------------------------------------------------------------------------
-
- /**
- * NewTabView instance.
- * @type {!Object|undefined}
- */
- var newTabView;
-
- /**
- * The 'notification-container' element.
- * @type {!Element|undefined}
- */
- var notificationContainer;
-
- /**
- * If non-null, an info bubble for showing messages to the user. It points at
- * the Most Visited label, and is used to draw more attention to the
- * navigation dot UI.
- * @type {!Element|undefined}
- */
- var promoBubble;
-
- /**
- * The total number of thumbnails that were hovered over.
- * @type {number}
- * @private
- */
- var hoveredThumbnailCount = 0;
-
- /**
- * The time when all sections are ready.
- * @type {number|undefined}
- * @private
- */
- var startTime;
-
- /**
- * The top position of the Bottom Panel.
- * @type {number|undefined}
- * @private
- */
- var bottomPanelOffsetTop;
-
- /**
- * The height of the Bottom Panel Header, in pixels.
- * @type {number|undefined}
- * @private
- */
- var headerHeight;
-
- /**
- * The time in milliseconds for most transitions. This should match what's
- * in new_tab.css. Unfortunately there's no better way to try to time
- * something to occur until after a transition has completed.
- * @type {number}
- * @const
- */
- var DEFAULT_TRANSITION_TIME = 500;
-
- /**
- * See description for these values in ntp_stats.h.
- * @enum {number}
- */
- var NtpFollowAction = {
- CLICKED_TILE: 11,
- CLICKED_OTHER_NTP_PANE: 12,
- OTHER: 13,
- };
-
- /**
- * Creates a NewTabView object.
- * @constructor
- */
- function NewTabView() {
- this.initialize(getRequiredElement('page-list'),
- getRequiredElement('dot-list'),
- getRequiredElement('card-slider-frame'));
- }
-
- NewTabView.prototype = {
- /**
- * The CardSlider object to use for changing app pages.
- * @type {CardSlider|undefined}
- */
- cardSlider: undefined,
-
- /**
- * The frame div for this.cardSlider.
- * @type {!Element|undefined}
- */
- sliderFrame: undefined,
-
- /**
- * The 'page-list' element.
- * @type {!Element|undefined}
- */
- pageList: undefined,
-
- /**
- * A list of all 'tile-page' elements.
- * @type {!NodeList|undefined}
- */
- tilePages: undefined,
-
- /**
- * The Apps page.
- * @type {!Element|undefined}
- */
- appsPage: undefined,
-
- /**
- * The Most Visited page.
- * @type {!Element|undefined}
- */
- mostVisitedPage: undefined,
-
- /**
- * The Recently Closed page.
- * @type {!Element|undefined}
- */
- recentlyClosedPage: undefined,
-
- /**
- * The Devices page.
- * @type {!Element|undefined}
- */
- otherDevicesPage: undefined,
-
- /**
- * The 'dots-list' element.
- * @type {!Element|undefined}
- */
- dotList: undefined,
-
- /**
- * The type of page that is currently shown. The value is a numerical ID.
- * @type {number}
- */
- shownPage: 0,
-
- /**
- * The index of the page that is currently shown, within the page type.
- * For example if the third Apps page is showing, this will be 2.
- * @type {number}
- */
- shownPageIndex: 0,
-
- /**
- * If non-null, this is the ID of the app to highlight to the user the next
- * time getAppsCallback runs. "Highlight" in this case means to switch to
- * the page and run the new tile animation.
- * @type {?string}
- */
- highlightAppId: null,
-
- /**
- * Initializes new tab view.
- * @param {!Element} pageList A DIV element to host all pages.
- * @param {!Element} dotList An UL element to host nav dots. Each dot
- * represents a page.
- * @param {!Element} cardSliderFrame The card slider frame that hosts
- * pageList.
- */
- initialize: function(pageList, dotList, cardSliderFrame) {
- this.pageList = pageList;
-
- this.dotList = dotList;
- cr.ui.decorate(this.dotList, ntp.DotList);
-
- this.shownPage = loadTimeData.getInteger('shown_page_type');
- this.shownPageIndex = loadTimeData.getInteger('shown_page_index');
-
- if (loadTimeData.getBoolean('showApps')) {
- // When the Apps Page is available, then the dot list should be visible.
- this.dotList.removeAttribute('hidden');
- // Request data on the apps so we can fill them in.
- // Note that this is kicked off asynchronously. 'getAppsCallback' will
- // be invoked at some point after this function returns.
- chrome.send('getApps');
- } else if (this.shownPage == loadTimeData.getInteger('apps_page_id')) {
- // No apps page.
- this.setShownPage_(
- loadTimeData.getInteger('most_visited_page_id'), 0);
- }
-
- this.tilePages = this.pageList.getElementsByClassName('tile-page');
-
- // Initialize the cardSlider without any cards at the moment.
- this.sliderFrame = cardSliderFrame;
- this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
- this.sliderFrame.offsetWidth);
-
- var cardSlider = this.cardSlider;
- this.cardSlider.initialize(
- loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));
-
- // Prevent touch events from triggering any sort of native scrolling.
- document.addEventListener('touchmove', function(e) {
- e.preventDefault();
- }, true);
-
- // Handle events from the card slider.
- this.pageList.addEventListener('cardSlider:card_changed',
- this.onCardChanged_.bind(this));
- this.pageList.addEventListener('cardSlider:card_added',
- this.onCardAdded_.bind(this));
- this.pageList.addEventListener('cardSlider:card_removed',
- this.onCardRemoved_.bind(this));
-
- // Update apps when online state changes.
- window.addEventListener('online',
- this.updateOfflineEnabledApps_.bind(this));
- window.addEventListener('offline',
- this.updateOfflineEnabledApps_.bind(this));
- },
-
- /**
- * Starts listening to user input events. The resize and keydown events
- * must be added only when all NTP have finished loading because they
- * will act in the current selected page.
- */
- onReady: function() {
- window.addEventListener('resize', this.onWindowResize_.bind(this));
- document.addEventListener('keydown', this.onDocKeyDown_.bind(this));
- },
-
- /**
- * Appends a tile page.
- *
- * @param {TilePage} page The page element.
- * @param {string} title The title of the tile page.
- * @param {TilePage=} opt_refNode Optional reference node to insert in front
- * of.
- * When opt_refNode is falsey, |page| will just be appended to the end of
- * the page list.
- */
- appendTilePage: function(page, title, opt_refNode) {
- if (opt_refNode) {
- var refIndex = this.getTilePageIndex(opt_refNode);
- this.cardSlider.addCardAtIndex(page, refIndex);
- } else {
- this.cardSlider.appendCard(page);
- }
-
- // Remember special MostVisitedPage.
- if (typeof ntp.MostVisitedPage != 'undefined' &&
- page instanceof ntp.MostVisitedPage) {
- assert(this.tilePages.length == 1,
- 'MostVisitedPage should be added as first tile page');
- this.mostVisitedPage = page;
- }
-
- if (typeof ntp.AppsPage != 'undefined' &&
- page instanceof ntp.AppsPage) {
- this.appsPage = page;
- }
-
- if (typeof ntp.RecentlyClosedPage != 'undefined' &&
- page instanceof ntp.RecentlyClosedPage) {
- this.recentlyClosedPage = page;
- }
-
- // Remember special OtherDevicesPage.
- if (typeof ntp.OtherDevicesPage != 'undefined' &&
- page instanceof ntp.OtherDevicesPage) {
- this.otherDevicesPage = page;
- }
-
- // Make a deep copy of the dot template to add a new one.
- var newDot = new ntp.NavDot(page, title);
- page.navigationDot = newDot;
- this.dotList.insertBefore(newDot,
- opt_refNode ? opt_refNode.navigationDot : null);
- // Set a tab index on the first dot.
- if (this.dotList.dots.length == 1)
- newDot.tabIndex = 3;
- },
-
- /**
- * Called by chrome when an app has changed positions.
- * @param {Object} data The data for the app. This contains page and
- * position indices.
- */
- appMoved: function(data) {
- assert(loadTimeData.getBoolean('showApps'));
-
- var app = $(data.id);
- assert(app, 'trying to move an app that doesn\'t exist');
- app.remove(false);
-
- this.appsPage.insertApp(data, false);
- },
-
- /**
- * Called by chrome when an existing app has been disabled or
- * removed/uninstalled from chrome.
- * @param {Object} data A data structure full of relevant information for
- * the app.
- * @param {boolean} isUninstall True if the app is being uninstalled;
- * false if the app is being disabled.
- * @param {boolean} fromPage True if the removal was from the current page.
- */
- appRemoved: function(data, isUninstall, fromPage) {
- assert(loadTimeData.getBoolean('showApps'));
-
- var app = $(data.id);
- assert(app, 'trying to remove an app that doesn\'t exist');
-
- if (!isUninstall)
- app.replaceAppData(data);
- else
- app.remove(!!fromPage);
- },
-
- /**
- * @return {boolean} If the page is still starting up.
- * @private
- */
- isStartingUp_: function() {
- return document.documentElement.classList.contains('starting-up');
- },
-
- /**
- * Tracks whether apps have been loaded at least once.
- * @type {boolean}
- * @private
- */
- appsLoaded_: false,
-
- /**
- * Callback invoked by chrome with the apps available.
- *
- * Note that calls to this function can occur at any time, not just in
- * response to a getApps request. For example, when a user
- * installs/uninstalls an app on another synchronized devices.
- * @param {Object} data An object with all the data on available
- * applications.
- */
- getAppsCallback: function(data) {
- assert(loadTimeData.getBoolean('showApps'));
-
- var startTime = Date.now();
-
- // Get the array of apps and add any special synthesized entries.
- var apps = data.apps;
-
- // Sort alphabetically.
- apps.sort(function(a, b) {
- return a.title.toLocaleLowerCase() > b.title.toLocaleLowerCase() ? 1 :
- a.title.toLocaleLowerCase() < b.title.toLocaleLowerCase() ? -1 : 0;
- });
-
- // An app to animate (in case it was just installed).
- var highlightApp;
-
- if (this.appsPage) {
- this.appsPage.removeAllTiles();
- } else {
- var page = new ntp.AppsPage();
- page.setDataList(apps);
- this.appendTilePage(page, loadTimeData.getString('appDefaultPageName'));
- }
-
- for (var i = 0; i < apps.length; i++) {
- var app = apps[i];
- if (app.id == this.highlightAppId)
- highlightApp = app;
- else
- this.appsPage.insertApp(app, false);
- }
-
- if (highlightApp)
- this.appAdded(highlightApp, true);
-
- logEvent('apps.layout: ' + (Date.now() - startTime));
-
- // Tell the slider about the pages and mark the current page.
- this.updateSliderCards();
-
- if (!this.appsLoaded_) {
- this.appsLoaded_ = true;
- cr.dispatchSimpleEvent(document, 'sectionready', true, true);
- }
- },
-
- /**
- * Called by chrome when a new app has been added to chrome or has been
- * enabled if previously disabled.
- * @param {Object} data A data structure full of relevant information for
- * the app.
- * @param {boolean=} opt_highlight Whether the app about to be added should
- * be highlighted.
- */
- appAdded: function(data, opt_highlight) {
- assert(loadTimeData.getBoolean('showApps'));
-
- if (data.id == this.highlightAppId) {
- opt_highlight = true;
- this.highlightAppId = null;
- }
-
- if (!this.appsLoaded_)
- opt_highlight = false;
-
- var app = $(data.id);
- if (app) {
- app.replaceAppData(data);
- } else if (opt_highlight) {
- this.appsPage.insertAndHighlightApp(data);
- this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
- data.page_index);
- } else {
- this.appsPage.insertApp(data, false);
- }
- },
-
- /**
- * Callback invoked by chrome whenever an app preference changes.
- * @param {Object} data An object with all the data on available
- * applications.
- */
- appsPrefChangedCallback: function(data) {
- assert(loadTimeData.getBoolean('showApps'));
-
- for (var i = 0; i < data.apps.length; ++i) {
- var element = $(data.apps[i].id);
- if (element)
- element.data = data.apps[i];
- }
- },
-
- /**
- * Invoked whenever the pages in page-list have changed so that the
- * CardSlider knows about the new elements.
- */
- updateSliderCards: function() {
- var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard,
- this.tilePages.length - 1));
- this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
- pageNo);
- switch (this.shownPage) {
- case loadTimeData.getInteger('apps_page_id'):
- this.cardSlider.selectCardByValue(this.appsPage);
- break;
- case loadTimeData.getInteger('most_visited_page_id'):
- if (this.mostVisitedPage)
- this.cardSlider.selectCardByValue(this.mostVisitedPage);
- break;
- }
- },
-
- /**
- * Handler for cardSlider:card_changed events from this.cardSlider.
- * @param {Event} e The cardSlider:card_changed event.
- * @private
- */
- onCardChanged_: function(e) {
- var page = e.cardSlider.currentCardValue;
-
- // Don't change shownPage until startup is done (and page changes actually
- // reflect user actions).
- if (!this.isStartingUp_()) {
- if (page.classList.contains('apps-page')) {
- this.setShownPage_(loadTimeData.getInteger('apps_page_id'), 0);
- } else if (page.classList.contains('most-visited-page')) {
- this.setShownPage_(
- loadTimeData.getInteger('most_visited_page_id'), 0);
- } else if (page.classList.contains('recently-closed-page')) {
- this.setShownPage_(
- loadTimeData.getInteger('recently_closed_page_id'), 0);
- } else if (page.classList.contains('other-devices-page')) {
- this.setShownPage_(
- loadTimeData.getInteger('other_devices_page_id'), 0);
- } else {
- console.error('unknown page selected');
- }
- }
-
- // Update the active dot
- var curDot = this.dotList.getElementsByClassName('selected')[0];
- if (curDot)
- curDot.classList.remove('selected');
- page.navigationDot.classList.add('selected');
- },
-
- /**
- * Saves/updates the newly selected page to open when first loading the NTP.
- * @type {number} shownPage The new shown page type.
- * @type {number} shownPageIndex The new shown page index.
- * @private
- */
- setShownPage_: function(shownPage, shownPageIndex) {
- assert(shownPageIndex >= 0);
- this.shownPage = shownPage;
- this.shownPageIndex = shownPageIndex;
- chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
- },
-
- /**
- * Listen for card additions to update the current card accordingly.
- * @param {Event} e A card removed or added event.
- */
- onCardAdded_: function(e) {
- var page = e.addedCard;
- // When the second arg passed to insertBefore is falsey, it acts just like
- // appendChild.
- this.pageList.insertBefore(page, this.tilePages[e.addedIndex]);
- this.layout(false, page);
- this.onCardAddedOrRemoved_();
- },
-
- /**
- * Listen for card removals to update the current card accordingly.
- * @param {Event} e A card removed or added event.
- */
- onCardRemoved_: function(e) {
- e.removedCard.remove();
- this.onCardAddedOrRemoved_();
- },
-
- /**
- * Called when a card is removed or added.
- * @private
- */
- onCardAddedOrRemoved_: function() {
- if (this.isStartingUp_())
- return;
-
- // Without repositioning there were issues - http://crbug.com/133457.
- this.cardSlider.repositionFrame();
- },
-
- /**
- * Window resize handler.
- * @private
- */
- onWindowResize_: function(e) {
- this.cardSlider.resize(this.sliderFrame.offsetWidth);
- this.layout(true);
- },
-
- /**
- * Handler for key events on the page. Ctrl-Arrow will switch the visible
- * page.
- * @param {Event} e The KeyboardEvent.
- * @private
- */
- onDocKeyDown_: function(e) {
- if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
- return;
-
- var direction = 0;
- if (e.keyIdentifier == 'Left')
- direction = -1;
- else if (e.keyIdentifier == 'Right')
- direction = 1;
- else
- return;
-
- var cardIndex =
- (this.cardSlider.currentCard + direction +
- this.cardSlider.cardCount) % this.cardSlider.cardCount;
- this.cardSlider.selectCard(cardIndex, true);
-
- e.stopPropagation();
- },
-
- /**
- * Listener for offline status change events. Updates apps that are
- * not offline-enabled to be grayscale if the browser is offline.
- * @private
- */
- updateOfflineEnabledApps_: function() {
- var apps = document.querySelectorAll('.app');
- for (var i = 0; i < apps.length; ++i) {
- if (apps[i].data.enabled && !apps[i].data.offline_enabled) {
- apps[i].setIcon();
- apps[i].loadIcon();
- }
- }
- },
-
- /**
- * Returns the index of a given tile page.
- * @param {TilePage} page The TilePage we wish to find.
- * @return {number} The index of |page| or -1 if it is not in the
- * collection.
- */
- getTilePageIndex: function(page) {
- return Array.prototype.indexOf.call(this.tilePages, page);
- },
-
- /**
- * Removes a page and navigation dot (if the navdot exists).
- * @param {TilePage} page The page to be removed.
- */
- removeTilePageAndDot_: function(page) {
- if (page.navigationDot)
- page.navigationDot.remove();
- this.cardSlider.removeCard(page);
- },
-
- /**
- * The width of the Bottom Panel's content.
- * @type {number}
- */
- contentWidth_: 0,
-
- /**
- * Calculates the layout of the NTP's Bottom Panel. This method will resize
- * and position all container elements in the Bottom Panel. At the end of
- * the layout process it will dispatch the layout method to the current
- * selected TilePage. Alternatively, you can pass a specific TilePage in
- * the |opt_page| parameter, which is useful for initializing the layout
- * of a recently created TilePage.
- *
- * The |NewTabView.layout| deals with the global layout state while the
- * |TilePage.layout| deals with the per-page layout state. A general rule
- * would be: if you need to resize any element which is outside the
- * card-slider-frame, it should be handled here in NewTabView. Otherwise,
- * it should be handled in TilePage.
- *
- * @param {boolean=} opt_animate Whether the layout should be animated.
- * @param {ntp.TilePage=} opt_page Alternative TilePage to calculate layout.
- */
- layout: function(opt_animate, opt_page) {
- opt_animate = typeof opt_animate == 'undefined' ? false : opt_animate;
-
- var viewHeight = cr.doc.documentElement.clientHeight;
- var isBottomPanelVisible = viewHeight >= HEIGHT_FOR_BOTTOM_PANEL;
- // Toggles the visibility of the Bottom Panel when there is (or there
- // is not) space to show the entire panel.
- this.showBottomPanel_(isBottomPanelVisible);
-
- // The layout calculation can be skipped if Bottom Panel is not visible.
- if (!isBottomPanelVisible && !opt_page)
- return;
-
- // Calculates the width of the Bottom Panel's Content.
- var width = this.calculateContentWidth_();
- if (width != this.contentWidth_) {
- this.contentWidth_ = width;
- $('bottom-panel-footer').style.width = width + 'px';
- }
-
- // Finally, dispatch the layout method to the current page.
- var currentPage = opt_page || this.cardSlider.currentCardValue;
-
- var contentHeight = TILE_ROW_HEIGHT;
- if (!opt_page && currentPage.config.scrollable) {
- contentHeight = viewHeight - bottomPanelOffsetTop -
- headerHeight - $('bottom-panel-footer').offsetHeight;
- contentHeight = Math.max(TILE_ROW_HEIGHT, contentHeight);
- }
- this.contentHeight_ = contentHeight;
-
- $('card-slider-frame').style.height = contentHeight + 'px';
-
- currentPage.layout(opt_animate);
- },
-
- /**
- * @return {number} The height of the Bottom Panel's content.
- */
- get contentHeight() {
- return this.contentHeight_;
- },
-
- /**
- * @return {number} The width of the Bottom Panel's content.
- */
- get contentWidth() {
- return this.contentWidth_;
- },
-
- /**
- * @return {number} The width of the Bottom Panel's content.
- * @private
- */
- calculateContentWidth_: function() {
- var windowWidth = cr.doc.documentElement.clientWidth;
- var margin = 2 * BOTTOM_PANEL_HORIZONTAL_MARGIN;
-
- var width;
- if (windowWidth >= MAX_BOTTOM_PANEL_WIDTH) {
- width = MAX_BOTTOM_PANEL_WIDTH - margin;
- } else if (windowWidth >= NORMAL_BOTTOM_PANEL_WIDTH) {
- width = windowWidth - margin;
- } else if (windowWidth >= MIN_BOTTOM_PANEL_WIDTH) {
- // Interpolation between the previous and next states.
- var minMargin = MIN_BOTTOM_PANEL_WIDTH - MIN_BOTTOM_PANEL_CONTENT_WIDTH;
- var factor = (windowWidth - MIN_BOTTOM_PANEL_WIDTH) /
- (NORMAL_BOTTOM_PANEL_WIDTH - MIN_BOTTOM_PANEL_WIDTH);
- var interpolatedMargin = minMargin + factor * (margin - minMargin);
- width = windowWidth - interpolatedMargin;
- } else {
- width = MIN_BOTTOM_PANEL_CONTENT_WIDTH;
- }
-
- return width;
- },
-
- /**
- * Animates the display of the Bottom Panel.
- * @param {boolean} show Whether or not to show the Bottom Panel.
- */
- showBottomPanel_: function(show) {
- $('bottom-panel').classList.toggle('hide-bottom-panel', !show);
- },
- };
-
- /**
- * Invoked at startup once the DOM is available to initialize the app.
- */
- function onLoad() {
-
- if (!loadTimeData.getBoolean('showApps'))
- cr.dispatchSimpleEvent(document, 'sectionready', true, true);
-
- // Load the current theme colors.
- themeChanged();
-
- newTabView = new NewTabView();
-
- bottomPanelOffsetTop = $('bottom-panel').offsetTop;
- headerHeight = $('bottom-panel-header').offsetHeight;
-
- notificationContainer = getRequiredElement('notification-container');
-
- var mostVisited = new ntp.MostVisitedPage();
- newTabView.appendTilePage(mostVisited,
- loadTimeData.getString('mostvisited'));
- chrome.send('getMostVisited');
-
- if (loadTimeData.valueExists('bubblePromoText')) {
- promoBubble = new cr.ui.Bubble;
- promoBubble.anchorNode = getRequiredElement('promo-bubble-anchor');
- promoBubble.arrowLocation = cr.ui.ArrowLocation.BOTTOM_START;
- promoBubble.bubbleAlignment = cr.ui.BubbleAlignment.ENTIRELY_VISIBLE;
- promoBubble.deactivateToDismissDelay = 2000;
- promoBubble.content = parseHtmlSubset(
- loadTimeData.getString('bubblePromoText'), ['BR']);
-
- var bubbleLink = promoBubble.querySelector('a');
- if (bubbleLink) {
- bubbleLink.addEventListener('click', function(e) {
- chrome.send('bubblePromoLinkClicked');
- });
- }
-
- promoBubble.handleCloseEvent = function() {
- promoBubble.hide();
- chrome.send('bubblePromoClosed');
- };
- promoBubble.show();
- chrome.send('bubblePromoViewed');
- }
-
- doWhenAllSectionsReady(function() {
- // Tell the slider about the pages.
- newTabView.updateSliderCards();
- newTabView.onReady();
-
- // Restore the visibility only after calling updateSliderCards to avoid
- // flickering, otherwise for a small fraction of a second the Page List is
- // partially rendered.
- $('bottom-panel').style.visibility = 'visible';
-
- if (loadTimeData.valueExists('notificationPromoText')) {
- var promoText = loadTimeData.getString('notificationPromoText');
- var tags = ['IMG'];
- var attrs = {
- src: function(node, value) {
- return node.tagName == 'IMG' &&
- /^data\:image\/(?:png|gif|jpe?g)/.test(value);
- },
- };
-
- var promo = parseHtmlSubset(promoText, tags, attrs);
- var promoLink = promo.querySelector('a');
- if (promoLink) {
- promoLink.addEventListener('click', function(e) {
- chrome.send('notificationPromoLinkClicked');
- });
- }
-
- showNotification(promo, [], function() {
- chrome.send('notificationPromoClosed');
- }, 60000);
- chrome.send('notificationPromoViewed');
- }
-
- cr.dispatchSimpleEvent(document, 'ntpLoaded', true, true);
- document.documentElement.classList.remove('starting-up');
-
- startTime = Date.now();
- });
- }
-
- /*
- * The number of sections to wait on.
- * @type {number}
- */
- var sectionsToWaitFor = 2;
-
- /**
- * Queued callbacks which lie in wait for all sections to be ready.
- * @type {!Array}
- */
- var readyCallbacks = [];
-
- /**
- * Fired as each section of pages becomes ready.
- * @param {Event} e Each page's synthetic DOM event.
- */
- document.addEventListener('sectionready', function(e) {
- if (--sectionsToWaitFor <= 0) {
- while (readyCallbacks.length) {
- readyCallbacks.shift()();
- }
- }
- });
-
- /**
- * This is used to simulate a fire-once event (i.e. $(document).ready() in
- * jQuery or Y.on('domready') in YUI. If all sections are ready, the callback
- * is fired right away. If all pages are not ready yet, the function is queued
- * for later execution.
- * @param {function} callback The work to be done when ready.
- */
- function doWhenAllSectionsReady(callback) {
- assert(typeof callback == 'function');
- if (sectionsToWaitFor > 0)
- readyCallbacks.push(callback);
- else
- window.setTimeout(callback, 0); // Do soon after, but asynchronously.
- }
-
- function themeChanged(opt_hasAttribution) {
- $('themecss').href = 'chrome://theme/css/new_tab_theme.css?' + Date.now();
-
- if (typeof opt_hasAttribution != 'undefined') {
- document.documentElement.setAttribute('hasattribution',
- opt_hasAttribution);
- }
-
- updateAttribution();
- }
-
- function setBookmarkBarAttached(attached) {
- document.documentElement.setAttribute('bookmarkbarattached', attached);
- }
-
- /**
- * Attributes the attribution image at the bottom left.
- */
- function updateAttribution() {
- var attribution = $('attribution');
- if (document.documentElement.getAttribute('hasattribution') == 'true') {
- $('attribution-img').src =
- 'chrome://theme/IDR_THEME_NTP_ATTRIBUTION?' + Date.now();
- attribution.hidden = false;
- } else {
- attribution.hidden = true;
- }
- }
-
- /**
- * Timeout ID.
- * @type {number}
- */
- var notificationTimeout = 0;
-
- /**
- * Shows the notification bubble.
- * @param {string|Node} message The notification message or node to use as
- * message.
- * @param {Array.<{text: string, action: function()}>} links An array of
- * records describing the links in the notification. Each record should
- * have a 'text' attribute (the display string) and an 'action' attribute
- * (a function to run when the link is activated).
- * @param {Function=} opt_closeHandler The callback invoked if the user
- * manually dismisses the notification.
- */
- function showNotification(message, links, opt_closeHandler, opt_timeout) {
- window.clearTimeout(notificationTimeout);
-
- var span = document.querySelector('#notification > span');
- if (typeof message == 'string') {
- span.textContent = message;
- } else {
- span.textContent = ''; // Remove all children.
- span.appendChild(message);
- }
-
- var linksBin = $('notificationLinks');
- linksBin.textContent = '';
- for (var i = 0; i < links.length; i++) {
- var link = linksBin.ownerDocument.createElement('div');
- link.textContent = links[i].text;
- link.action = links[i].action;
- link.onclick = function() {
- this.action();
- hideNotification();
- };
- link.setAttribute('role', 'button');
- link.setAttribute('tabindex', 0);
- link.className = 'link-button';
- linksBin.appendChild(link);
- }
-
- function closeFunc(e) {
- if (opt_closeHandler)
- opt_closeHandler();
- hideNotification();
- }
-
- document.querySelector('#notification button').onclick = closeFunc;
- document.addEventListener('dragstart', closeFunc);
-
- notificationContainer.hidden = false;
-
- var timeout = opt_timeout || 10000;
- notificationTimeout = window.setTimeout(hideNotification, timeout);
-
- layout();
- }
-
- /**
- * Hide the notification bubble.
- */
- function hideNotification() {
- notificationContainer.hidden = true;
-
- layout();
- }
-
- function setMostVisitedPages(dataList, hasBlacklistedUrls) {
- var page = newTabView.mostVisitedPage;
- var state = page.getTileRepositioningState();
- if (state) {
- if (state.isRemoving)
- page.animateTileRemoval(state.index, dataList);
- else
- page.animateTileRestoration(state.index, dataList);
-
- page.resetTileRepositioningState();
- } else {
- page.setDataList(dataList);
- cr.dispatchSimpleEvent(document, 'sectionready', true, true);
- }
- }
-
- /**
- * Set the dominant color for a node. This will be called in response to
- * getFaviconDominantColor. The node represented by |id| better have a setter
- * for stripeColor.
- * @param {string} id The ID of a node.
- * @param {string} color The color represented as a CSS string.
- */
- function setFaviconDominantColor(id, color) {
- var node = $(id);
- var prop = Object.getOwnPropertyDescriptor(node.__proto__, 'stripeColor');
- assert(prop && prop.set, 'Node doesn\'t have a stripeColor setter');
- if (node)
- node.stripeColor = color;
- }
-
- function getThumbnailUrl(url) {
- return 'chrome://thumb/' + url;
- }
-
- /**
- * Increments the parameter used to log the total number of thumbnail hovered
- * over.
- */
- function incrementHoveredThumbnailCount() {
- hoveredThumbnailCount++;
- }
-
- /**
- * Logs the time to click for the specified item and the total number of
- * thumbnails hovered over.
- * @param {string} item The item to log the time-to-click.
- */
- function logTimeToClickAndHoverCount(item) {
- var timeToClick = Date.now() - startTime;
- chrome.send('logTimeToClick',
- ['ExtendedNewTabPage.TimeToClick' + item, timeToClick]);
- chrome.send('metricsHandler:recordInHistogram',
- ['ExtendedNewTabPage.hoveredThumbnailCount',
- hoveredThumbnailCount, 40]);
- }
-
- /**
- * Wrappers to forward the callback to corresponding NewTabView member.
- */
- function appAdded() {
- return newTabView.appAdded.apply(newTabView, arguments);
- }
-
- function appMoved() {
- return newTabView.appMoved.apply(newTabView, arguments);
- }
-
- function appRemoved() {
- return newTabView.appRemoved.apply(newTabView, arguments);
- }
-
- function appsPrefChangeCallback() {
- return newTabView.appsPrefChangedCallback.apply(newTabView, arguments);
- }
-
- function getAppsCallback() {
- return newTabView.getAppsCallback.apply(newTabView, arguments);
- }
-
- function getCardSlider() {
- return newTabView.cardSlider;
- }
-
- function setAppToBeHighlighted(appId) {
- newTabView.highlightAppId = appId;
- }
-
- function layout() {
- newTabView.layout.apply(newTabView, arguments);
- }
-
- function getContentHeight() {
- return newTabView.contentHeight;
- }
-
- function getContentWidth() {
- return newTabView.contentWidth;
- }
-
- function noop() {
- // Ignore some NTP4 callbacks for backwards compatibility purposes.
- }
-
- // Return an object with all the exports
- return {
- APP_LAUNCH: APP_LAUNCH,
- TILE_ROW_HEIGHT: TILE_ROW_HEIGHT,
- appAdded: appAdded,
- appMoved: appMoved,
- appRemoved: appRemoved,
- appsPrefChangeCallback: appsPrefChangeCallback,
- getAppsCallback: getAppsCallback,
- getCardSlider: getCardSlider,
- getContentHeight: getContentHeight,
- getContentWidth: getContentWidth,
- getThumbnailUrl: getThumbnailUrl,
- incrementHoveredThumbnailCount: incrementHoveredThumbnailCount,
- layout: layout,
- logTimeToClickAndHoverCount: logTimeToClickAndHoverCount,
- NtpFollowAction: NtpFollowAction,
- onLoad: onLoad,
- setAppToBeHighlighted: setAppToBeHighlighted,
- setBookmarkBarAttached: setBookmarkBarAttached,
- setFaviconDominantColor: setFaviconDominantColor,
- setForeignSessions: noop,
- setMostVisitedPages: setMostVisitedPages,
- setRecentlyClosedTabs: noop,
- showNotification: showNotification,
- themeChanged: themeChanged,
- updateLogin: noop,
- };
- });
-
- document.addEventListener('DOMContentLoaded', ntp.onLoad);
-
- var toCssPx = cr.ui.toCssPx;
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview DotList implementation
- */
-
- cr.define('ntp', function() {
- 'use strict';
-
- /**
- * Live list of the navigation dots.
- * @type {!NodeList|undefined}
- */
- var navDots;
-
- /**
- * Creates a new DotList object.
- * @constructor
- * @extends {HTMLUListElement}
- */
- var DotList = cr.ui.define('ul');
-
- DotList.prototype = {
- __proto__: HTMLUListElement.prototype,
-
- /** @override */
- decorate: function() {
- this.addEventListener('keydown', this.onKeyDown_.bind(this));
- navDots = this.getElementsByClassName('dot');
- },
-
- /**
- * Live list of the navigation dots.
- * @type {!NodeList|undefined}
- */
- get dots() {
- return navDots;
- },
-
- /**
- * Handler for key events on the dot list. These keys will change the focus
- * element.
- * @param {Event} e The KeyboardEvent.
- */
- onKeyDown_: function(e) {
- if (e.metaKey || e.shiftKey || e.altKey || e.ctrlKey)
- return;
-
- var direction = 0;
- if (e.keyIdentifier == 'Left')
- direction = -1;
- else if (e.keyIdentifier == 'Right')
- direction = 1;
- else
- return;
-
- var focusDot = this.querySelector('.dot:focus');
- if (!focusDot)
- return;
- var focusIndex = Array.prototype.indexOf.call(navDots, focusDot);
- var newFocusIndex = focusIndex + direction;
- if (focusIndex == newFocusIndex)
- return;
-
- newFocusIndex = (newFocusIndex + navDots.length) % navDots.length;
- navDots[newFocusIndex].tabIndex = 3;
- navDots[newFocusIndex].focus();
- focusDot.tabIndex = -1;
-
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
- return {
- DotList: DotList
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- /**
- * @fileoverview Nav dot
- * This is the class for the navigation controls that appear along the bottom
- * of the NTP.
- */
-
- cr.define('ntp', function() {
- 'use strict';
-
- /**
- * Creates a new navigation dot.
- * @param {TilePage} page The associated TilePage.
- * @param {string} title The title of the navigation dot.
- * @constructor
- * @extends {HTMLLIElement}
- */
- function NavDot(page, title) {
- var dot = cr.doc.createElement('li');
- dot.__proto__ = NavDot.prototype;
- dot.initialize(page, title);
-
- return dot;
- }
-
- NavDot.prototype = {
- __proto__: HTMLLIElement.prototype,
-
- initialize: function(page, title) {
- this.className = 'dot';
- this.page_ = page;
- this.textContent = title;
-
- this.addEventListener('keydown', this.onKeyDown_);
- this.addEventListener('click', this.onClick_);
- },
-
- /**
- * @return {TilePage} The associated TilePage.
- */
- get page() {
- return this.page_;
- },
-
- /**
- * Removes the dot from the page.
- */
- remove: function() {
- this.parentNode.removeChild(this);
- },
-
- /**
- * Navigates the card slider to the page for this dot.
- */
- switchToPage: function() {
- ntp.getCardSlider().selectCardByValue(this.page_, true);
- },
-
- /**
- * Handler for keydown event on the dot.
- * @param {Event} e The KeyboardEvent.
- */
- onKeyDown_: function(e) {
- if (e.keyIdentifier == 'Enter') {
- this.onClick_(e);
- e.stopPropagation();
- }
- },
-
- /**
- * Clicking causes the associated page to show.
- * @param {Event} e The click event.
- * @private
- */
- onClick_: function(e) {
- this.switchToPage();
- e.stopPropagation();
- },
-
- };
-
- return {
- NavDot: NavDot,
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('ntp', function() {
- 'use strict';
-
- /**
- * The maximum gap from the edge of the scrolling area which will display
- * the shadow with transparency. After this point the shadow will become
- * 100% opaque.
- * @type {number}
- * @const
- */
- var MAX_SCROLL_SHADOW_GAP = 16;
-
- /**
- * @type {number}
- * @const
- */
- var SCROLL_BAR_WIDTH = 12;
-
- //----------------------------------------------------------------------------
- // Tile
- //----------------------------------------------------------------------------
-
- /**
- * A virtual Tile class. Each TilePage subclass should have its own Tile
- * subclass implemented too (e.g. MostVisitedPage contains MostVisited
- * tiles, and MostVisited is a Tile subclass).
- * @constructor
- */
- function Tile() {
- console.error('Tile is a virtual class and is not supposed to be ' +
- 'instantiated');
- }
-
- /**
- * Creates a Tile subclass. We need to use this function to create a Tile
- * subclass because a Tile must also subclass a HTMLElement (which can be
- * any HTMLElement), so we need to individually add methods and getters here.
- * @param {Object} Subclass The prototype object of the class we want to be
- * a Tile subclass.
- * @param {Object} The extended Subclass object.
- */
- Tile.subclass = function(Subclass) {
- var Base = Tile.prototype;
- for (var name in Base) {
- if (!Subclass.hasOwnProperty(name))
- Subclass[name] = Base[name];
- }
- for (var name in TileGetters) {
- if (!Subclass.hasOwnProperty(name))
- Subclass.__defineGetter__(name, TileGetters[name]);
- }
- return Subclass;
- };
-
- Tile.prototype = {
- // Tile data object.
- data_: null,
-
- /**
- * Initializes a Tile.
- */
- initialize: function() {
- this.classList.add('tile');
- this.reset();
- },
-
- /**
- * Resets the tile DOM.
- */
- reset: function() {
- },
-
- /**
- * The data for this Tile.
- * @param {Object} data A dictionary of relevant data for the page.
- */
- set data(data) {
- // TODO(pedrosimonetti): Remove data.filler usage everywhere.
- if (!data || data.filler) {
- if (this.data_)
- this.reset();
- return;
- }
-
- this.data_ = data;
- },
- };
-
- var TileGetters = {
- /**
- * The TileCell associated to this Tile.
- * @type {TileCell}
- */
- 'tileCell': function() {
- return findAncestorByClass(this, 'tile-cell');
- },
-
- /**
- * The index of the Tile.
- * @type {number}
- */
- 'index': function() {
- assert(this.tileCell);
- return this.tileCell.index;
- },
- };
-
- //----------------------------------------------------------------------------
- // TileCell
- //----------------------------------------------------------------------------
-
- /**
- * Creates a new TileCell object. A TileCell represents a cell in the
- * TilePage's grid. A TilePage uses TileCells to position Tiles in the proper
- * place and to animate them individually. Each TileCell is associated to
- * one Tile at a time (or none if it is a filler object), and that association
- * might change when the grid is resized. When that happens, the grid is
- * updated and the Tiles are moved to the proper TileCell. We cannot move the
- * the TileCell itself during the resize because this transition is animated
- * with CSS and there's no way to stop CSS animations, and we really want to
- * animate with CSS to take advantage of hardware acceleration.
- * @constructor
- * @extends {HTMLDivElement}
- * @param {HTMLElement} tile Tile element that will be associated to the cell.
- */
- function TileCell(tile) {
- var tileCell = cr.doc.createElement('div');
- tileCell.__proto__ = TileCell.prototype;
- tileCell.initialize(tile);
-
- return tileCell;
- }
-
- TileCell.prototype = {
- __proto__: HTMLDivElement.prototype,
-
- /**
- * Initializes a TileCell.
- * @param {Tile} tile The Tile that will be assigned to this TileCell.
- */
- initialize: function(tile) {
- this.className = 'tile-cell';
- this.assign(tile);
- },
-
- /**
- * The index of the TileCell.
- * @type {number}
- */
- get index() {
- return Array.prototype.indexOf.call(this.tilePage.tiles_,
- this.tile);
- },
-
- /**
- * The Tile associated to this TileCell.
- * @type {Tile}
- */
- get tile() {
- return this.firstElementChild;
- },
-
- /**
- * The TilePage associated to this TileCell.
- * @type {TilePage}
- */
- get tilePage() {
- return findAncestorByClass(this, 'tile-page');
- },
-
- /**
- * Assigns a Tile to the this TileCell.
- * @type {TilePage}
- */
- assign: function(tile) {
- if (this.tile)
- this.replaceChild(tile, this.tile);
- else
- this.appendChild(tile);
- },
-
- /**
- * Called when an app is removed from Chrome. Animates its disappearance.
- * @param {boolean=} opt_animate Whether the animation should be animated.
- */
- doRemove: function(opt_animate) {
- this.tilePage.removeTile(this.tile, false);
- },
- };
-
- //----------------------------------------------------------------------------
- // TilePage
- //----------------------------------------------------------------------------
-
- /**
- * Creates a new TilePage object. This object contains tiles and controls
- * their layout.
- * @constructor
- * @extends {HTMLDivElement}
- */
- function TilePage() {
- var el = cr.doc.createElement('div');
- el.__proto__ = TilePage.prototype;
-
- return el;
- }
-
- TilePage.prototype = {
- __proto__: HTMLDivElement.prototype,
-
- /**
- * Reference to the Tile subclass that will be used to create the tiles.
- * @constructor
- * @extends {Tile}
- */
- TileClass: Tile,
-
- // The config object should be defined by a TilePage subclass if it
- // wants the non-default behavior.
- config: {
- // The width of a cell.
- cellWidth: 110,
- // The start margin of a cell (left or right according to text direction).
- cellMarginStart: 12,
- // The maximum number of Tiles to be displayed.
- maxTileCount: 6,
- // Whether the TilePage content will be scrollable.
- scrollable: false,
- },
-
- /**
- * Initializes a TilePage.
- */
- initialize: function() {
- this.className = 'tile-page';
-
- // The div that wraps the scrollable element.
- this.frame_ = this.ownerDocument.createElement('div');
- this.frame_.className = 'tile-page-frame';
- this.appendChild(this.frame_);
-
- // The content/scrollable element.
- this.content_ = this.ownerDocument.createElement('div');
- this.content_.className = 'tile-page-content';
- this.frame_.appendChild(this.content_);
-
- if (this.config.scrollable) {
- this.content_.classList.add('scrollable');
-
- // The scrollable shadow top.
- this.shadowTop_ = this.ownerDocument.createElement('div');
- this.shadowTop_.className = 'shadow-top';
- this.content_.appendChild(this.shadowTop_);
-
- // The scrollable shadow bottom.
- this.shadowBottom_ = this.ownerDocument.createElement('div');
- this.shadowBottom_.className = 'shadow-bottom';
- this.content_.appendChild(this.shadowBottom_);
- }
-
- // The div that defines the tile grid viewport.
- this.tileGrid_ = this.ownerDocument.createElement('div');
- this.tileGrid_.className = 'tile-grid';
- this.content_.appendChild(this.tileGrid_);
-
- // The tile grid contents, which can be scrolled.
- this.tileGridContent_ = this.ownerDocument.createElement('div');
- this.tileGridContent_.className = 'tile-grid-content';
- this.tileGrid_.appendChild(this.tileGridContent_);
-
- // The list of Tile elements which is used to fill the TileGrid cells.
- this.tiles_ = [];
-
- // TODO(pedrosimonetti): Check duplication of these methods.
- this.addEventListener('cardselected', this.handleCardSelection_);
- this.addEventListener('carddeselected', this.handleCardDeselection_);
-
- this.tileGrid_.addEventListener('webkitTransitionEnd',
- this.onTileGridTransitionEnd_.bind(this));
-
- this.content_.addEventListener('scroll', this.onScroll.bind(this));
- },
-
- /**
- * The list of Tile elements.
- * @type {Array<Tile>}
- */
- get tiles() {
- return this.tiles_;
- },
-
- /**
- * The number of Tiles in this TilePage.
- * @type {number}
- */
- get tileCount() {
- return this.tiles_.length;
- },
-
- /**
- * Whether or not this TilePage is selected.
- * @type {boolean}
- */
- get selected() {
- return Array.prototype.indexOf.call(this.parentNode.children, this) ==
- ntp.getCardSlider().currentCard;
- },
-
- /**
- * Removes the tilePage from the DOM and cleans up event handlers.
- */
- remove: function() {
- // This checks arguments.length as most remove functions have a boolean
- // |opt_animate| argument, but that's not necesarilly applicable to
- // removing a tilePage. Selecting a different card in an animated way and
- // deleting the card afterward is probably a better choice.
- assert(typeof arguments[0] != 'boolean',
- 'This function takes no |opt_animate| argument.');
- this.parentNode.removeChild(this);
- },
-
- /**
- * Notify interested subscribers that a tile has been removed from this
- * page.
- * @param {Tile} tile The newly added tile.
- * @param {number} index The index of the tile that was added.
- * @param {boolean} wasAnimated Whether the removal was animated.
- */
- fireAddedEvent: function(tile, index, wasAnimated) {
- var e = document.createEvent('Event');
- e.initEvent('tilePage:tile_added', true, true);
- e.addedIndex = index;
- e.addedTile = tile;
- e.wasAnimated = wasAnimated;
- this.dispatchEvent(e);
- },
-
- /**
- * Removes the given tile and animates the repositioning of the other tiles.
- * @param {boolean=} opt_animate Whether the removal should be animated.
- * @param {boolean=} opt_dontNotify Whether a page should be removed if the
- * last tile is removed from it.
- */
- removeTile: function(tile, opt_animate, opt_dontNotify) {
- var tiles = this.tiles;
- var index = tiles.indexOf(tile);
- tile.parentNode.removeChild(tile);
- tiles.splice(index, 1);
- this.renderGrid();
-
- if (!opt_dontNotify)
- this.fireRemovedEvent(tile, index, !!opt_animate);
- },
-
- /**
- * Notify interested subscribers that a tile has been removed from this
- * page.
- * @param {TileCell} tile The tile that was removed.
- * @param {number} oldIndex Where the tile was positioned before removal.
- * @param {boolean} wasAnimated Whether the removal was animated.
- */
- fireRemovedEvent: function(tile, oldIndex, wasAnimated) {
- var e = document.createEvent('Event');
- e.initEvent('tilePage:tile_removed', true, true);
- e.removedIndex = oldIndex;
- e.removedTile = tile;
- e.wasAnimated = wasAnimated;
- this.dispatchEvent(e);
- },
-
- /**
- * Removes all tiles from the page.
- */
- removeAllTiles: function() {
- while (this.tiles.length > 0) {
- this.removeTile(this.tiles[this.tiles.length - 1]);
- }
- },
-
- /**
- * Called when the page is selected (in the card selector).
- * @param {Event} e A custom cardselected event.
- * @private
- */
- handleCardSelection_: function(e) {
- ntp.layout();
- },
-
- /**
- * Called when the page loses selection (in the card selector).
- * @param {Event} e A custom carddeselected event.
- * @private
- */
- handleCardDeselection_: function(e) {
- },
-
- // #########################################################################
- // Extended Chrome Instant
- // #########################################################################
-
-
- // properties
- // -------------------------------------------------------------------------
-
- // The number of columns.
- colCount_: 0,
- // The number of rows.
- rowCount_: 0,
- // The number of visible rows. We initialize this value with zero so
- // we can detect when the first time the page is rendered.
- numOfVisibleRows_: 1,
- // The number of the last column being animated. We initialize this value
- // with zero so we can detect when the first time the page is rendered.
- animatingColCount_: 0,
- // The index of the topmost row visible.
- pageOffset_: 0,
- // Data object representing the tiles.
- dataList_: null,
-
- /**
- * Appends a tile to the end of the tile grid.
- * @param {Tile} tile The tile to be added.
- * @param {number} index The location in the tile grid to insert it at.
- * @protected
- */
- appendTile: function(tile) {
- var index = this.tiles_.length;
- this.addTileAt(tile, index);
- },
-
- /**
- * Adds the given element to the tile grid.
- * @param {Tile} tile The tile to be added.
- * @param {number} index The location in the tile grid to insert it at.
- * @protected
- */
- addTileAt: function(tile, index) {
- this.tiles_.splice(index, 0, tile);
- this.fireAddedEvent(tile, index, false);
- this.renderGrid();
- },
-
- /**
- * Create a blank tile.
- * @protected
- */
- createTile_: function() {
- return new this.TileClass();
- },
-
- /**
- * Create blank tiles.
- * @param {number} count The desired number of Tiles to be created. If this
- * value the maximum value defined in |config.maxTileCount|, the maximum
- * value will be used instead.
- * @protected
- */
- createTiles_: function(count) {
- count = Math.min(count, this.config.maxTileCount);
- for (var i = 0; i < count; i++) {
- this.appendTile(this.createTile_());
- }
- },
-
- /**
- * This method will create/remove necessary/unnecessary tiles, render the
- * grid when the number of tiles has changed, and finally will call
- * |updateTiles_| which will in turn render the individual tiles.
- * @protected
- */
- updateGrid: function() {
- var dataListLength = this.dataList_.length;
- var tileCount = this.tileCount;
- // Create or remove tiles if necessary.
- if (tileCount < dataListLength) {
- this.createTiles_(dataListLength - tileCount);
- } else if (tileCount > dataListLength) {
- var tiles = this.tiles_;
- while (tiles.length > dataListLength) {
- var previousLength = tiles.length;
- // It doesn't matter which tiles are being removed here because
- // they're going to be reconstructed below when calling updateTiles_
- // method, so the first tiles are being removed here.
- this.removeTile(tiles[0]);
- assert(tiles.length < previousLength);
- }
- }
-
- this.updateTiles_();
- },
-
- /**
- * Update the tiles after a change to |dataList_|.
- */
- updateTiles_: function() {
- var maxTileCount = this.config.maxTileCount;
- var dataList = this.dataList_;
- var tiles = this.tiles;
- for (var i = 0; i < maxTileCount; i++) {
- var data = dataList[i];
- var tile = tiles[i];
-
- // TODO(pedrosimonetti): What do we do when there's no tile here?
- if (!tile)
- return;
-
- if (i >= dataList.length)
- tile.reset();
- else
- tile.data = data;
- }
- },
-
- /**
- * Sets the dataList that will be used to create Tiles.
- * TODO(pedrosimonetti): Use setters and getters instead.
- */
- setDataList: function(dataList) {
- this.dataList_ = dataList.slice(0, this.config.maxTileCount);
- },
-
- // internal helpers
- // -------------------------------------------------------------------------
-
- /**
- * Gets the required width for a Tile.
- * @private
- */
- getTileRequiredWidth_: function() {
- var config = this.config;
- return config.cellWidth + config.cellMarginStart;
- },
-
- /**
- * Gets the the maximum number of columns that can fit in a given width.
- * @param {number} width The width in pixels.
- * @private
- */
- getColCountForWidth_: function(width) {
- var scrollBarIsVisible = this.config.scrollable &&
- this.content_.scrollHeight > this.content_.clientHeight;
- var scrollBarWidth = scrollBarIsVisible ? SCROLL_BAR_WIDTH : 0;
- var availableWidth = width + this.config.cellMarginStart - scrollBarWidth;
-
- var requiredWidth = this.getTileRequiredWidth_();
- var colCount = Math.floor(availableWidth / requiredWidth);
- return colCount;
- },
-
- /**
- * Gets the width for a given number of columns.
- * @param {number} colCount The number of columns.
- * @private
- */
- getWidthForColCount_: function(colCount) {
- var requiredWidth = this.getTileRequiredWidth_();
- var width = colCount * requiredWidth - this.config.cellMarginStart;
- return width;
- },
-
- /**
- * Returns the position of the tile at |index|.
- * @param {number} index Tile index.
- * @private
- * @return {!{top: number, left: number}} Position.
- */
- getTilePosition_: function(index) {
- var colCount = this.colCount_;
- var row = Math.floor(index / colCount);
- var col = index % colCount;
- if (isRTL())
- col = colCount - col - 1;
- var config = this.config;
- var top = ntp.TILE_ROW_HEIGHT * row;
- var left = col * (config.cellWidth + config.cellMarginStart);
- return {top: top, left: left};
- },
-
- // rendering
- // -------------------------------------------------------------------------
-
- /**
- * Renders the tile grid, and the individual tiles. Rendering the grid
- * consists of adding/removing tile rows and tile cells according to the
- * specified size (defined by the number of columns in the grid). While
- * rendering the grid, the tiles are rendered in order in their respective
- * cells and tile fillers are rendered when needed. This method sets the
- * private properties colCount_ and rowCount_.
- *
- * This method should be called every time the contents of the grid changes,
- * that is, when the number, contents or order of the tiles has changed.
- * @param {number=} opt_colCount The number of columns.
- * @param {number=} opt_tileCount Forces a particular number of tiles to
- * be drawn. This is useful for cases like the restoration/insertion
- * of tiles when you need to place a tile in a place of the grid that
- * is not rendered at the moment.
- * @protected
- */
- renderGrid: function(opt_colCount, opt_tileCount) {
- var colCount = opt_colCount || this.colCount_;
-
- var tileGridContent = this.tileGridContent_;
- var tiles = this.tiles_;
- var tileCount = opt_tileCount || tiles.length;
-
- var rowCount = Math.ceil(tileCount / colCount);
- var tileRows = tileGridContent.getElementsByClassName('tile-row');
-
- for (var tile = 0, row = 0; row < rowCount; row++) {
- var tileRow = tileRows[row];
-
- // Create tile row if there's no one yet.
- if (!tileRow) {
- tileRow = cr.doc.createElement('div');
- tileRow.className = 'tile-row';
- tileGridContent.appendChild(tileRow);
- }
-
- // The tiles inside the current row.
- var tileRowTiles = tileRow.childNodes;
-
- // Remove excessive columns from a particular tile row.
- var maxColCount = Math.min(colCount, tileCount - tile);
- maxColCount = Math.max(0, maxColCount);
- while (tileRowTiles.length > maxColCount) {
- tileRow.removeChild(tileRow.lastElementChild);
- }
-
- // For each column in the current row.
- for (var col = 0; col < colCount; col++, tile++) {
- var tileCell;
- var tileElement;
- if (tileRowTiles[col]) {
- tileCell = tileRowTiles[col];
- } else {
- var span = cr.doc.createElement('span');
- tileCell = new TileCell(span);
- }
-
- // Render Tiles.
- tileElement = tiles[tile];
- if (tile < tileCount && tileElement) {
- tileCell.classList.remove('filler');
- if (!tileCell.tile)
- tileCell.appendChild(tileElement);
- else if (tileElement != tileCell.tile)
- tileCell.replaceChild(tileElement, tileCell.tile);
- } else if (!tileCell.classList.contains('filler')) {
- tileCell.classList.add('filler');
- tileElement = cr.doc.createElement('span');
- tileElement.className = 'tile';
- if (tileCell.tile)
- tileCell.replaceChild(tileElement, tileCell.tile);
- else
- tileCell.appendChild(tileElement);
- }
-
- if (!tileRowTiles[col])
- tileRow.appendChild(tileCell);
- }
- }
-
- // Remove excessive tile rows from the tile grid.
- while (tileRows.length > rowCount) {
- tileGridContent.removeChild(tileGridContent.lastElementChild);
- }
-
- this.colCount_ = colCount;
- this.rowCount_ = rowCount;
-
- // If we are manually changing the tile count (which can happen during
- // the restoration/insertion animation) we should not fire the scroll
- // event once some cells might contain dummy tiles which will cause
- // an error.
- if (!opt_tileCount)
- this.onScroll();
- },
-
- // layout
- // -------------------------------------------------------------------------
-
- /**
- * Calculates the layout of the tile page according to the current Bottom
- * Panel's size. This method will resize the containers of the tile page,
- * and re-render the grid when its dimension changes (number of columns or
- * visible rows changes). This method also sets the private properties
- * |numOfVisibleRows_| and |animatingColCount_|.
- *
- * This method should be called every time the dimension of the grid changes
- * or when you need to reinforce its dimension.
- * @param {boolean=} opt_animate Whether the layout be animated.
- */
- layout: function(opt_animate) {
- var contentHeight = ntp.getContentHeight();
- this.content_.style.height = contentHeight + 'px';
-
- var contentWidth = ntp.getContentWidth();
- var colCount = this.getColCountForWidth_(contentWidth);
- var lastColCount = this.colCount_;
- var animatingColCount = this.animatingColCount_;
- if (colCount != animatingColCount) {
- if (opt_animate)
- this.tileGrid_.classList.add('animate-grid-width');
-
- if (colCount > animatingColCount) {
- // If the grid is expanding, it needs to be rendered first so the
- // revealing tiles are visible as soon as the animation starts.
- if (colCount != lastColCount)
- this.renderGrid(colCount);
-
- // Hides affected columns and forces the reflow.
- this.showTileCols_(animatingColCount, false);
- // Trigger reflow, making the tiles completely hidden.
- this.tileGrid_.offsetTop;
- // Fades in the affected columns.
- this.showTileCols_(animatingColCount, true);
- } else {
- // Fades out the affected columns.
- this.showTileCols_(colCount, false);
- }
-
- var newWidth = this.getWidthForColCount_(colCount);
- this.tileGrid_.style.width = newWidth + 'px';
-
- // TODO(pedrosimonetti): move to handler below.
- var self = this;
- this.onTileGridTransitionEndHandler_ = function() {
- if (colCount < lastColCount)
- self.renderGrid(colCount);
- else
- self.showTileCols_(0, true);
- };
- }
-
- this.animatingColCount_ = colCount;
-
- this.frame_.style.width = contentWidth + 'px';
-
- this.onScroll();
- },
-
- // tile repositioning animation
- // -------------------------------------------------------------------------
-
- /**
- * Tile repositioning state.
- * @type {{index: number, isRemoving: number}}
- */
- tileRepositioningState_: null,
-
- /**
- * Gets the repositioning state.
- * @return {{index: number, isRemoving: number}} The repositioning data.
- */
- getTileRepositioningState: function() {
- return this.tileRepositioningState_;
- },
-
- /**
- * Sets the repositioning state that will be used to animate the tiles.
- * @param {number} index The tile's index.
- * @param {boolean} isRemoving Whether the tile is being removed.
- */
- setTileRepositioningState: function(index, isRemoving) {
- this.tileRepositioningState_ = {
- index: index,
- isRemoving: isRemoving
- };
- },
-
- /**
- * Resets the repositioning state.
- */
- resetTileRepositioningState: function() {
- this.tileRepositioningState_ = null;
- },
-
- /**
- * Animates a tile removal.
- * @param {number} index The index of the tile to be removed.
- * @param {Object} newDataList The new data list.
- */
- animateTileRemoval: function(index, newDataList) {
- var tiles = this.tiles_;
- var tileCount = tiles.length;
- assert(tileCount > 0);
-
- var tileCells = this.querySelectorAll('.tile-cell');
- var extraTileIndex = tileCount - 1;
- var extraCell = tileCells[extraTileIndex];
- var extraTileData = newDataList[extraTileIndex];
-
- var repositioningStartIndex = index + 1;
- var repositioningEndIndex = tileCount;
-
- this.initializeRepositioningAnimation_(index, repositioningEndIndex,
- true);
-
- var tileBeingRemoved = tiles[index];
- tileBeingRemoved.scrollTop;
-
- // The extra tile is the new one that will appear. It can be a normal
- // tile (when there's extra data for it), or a filler tile.
- var extraTile = createTile(this, extraTileData);
- if (!extraTileData)
- extraCell.classList.add('filler');
- // The extra tile is being assigned in order to put it in the right spot.
- extraCell.assign(extraTile);
-
- this.executeRepositioningAnimation_(tileBeingRemoved, extraTile,
- repositioningStartIndex, repositioningEndIndex, true);
-
- // Cleans up the animation.
- var onPositioningTransitionEnd = function(e) {
- var propertyName = e.propertyName;
- if (!(propertyName == '-webkit-transform' ||
- propertyName == 'opacity')) {
- return;
- }
-
- lastAnimatingTile.removeEventListener('webkitTransitionEnd',
- onPositioningTransitionEnd);
-
- this.finalizeRepositioningAnimation_(tileBeingRemoved,
- repositioningStartIndex, repositioningEndIndex, true);
-
- this.removeTile(tileBeingRemoved);
-
- // If the extra tile is a real one (not a filler), then it needs to be
- // added to the tile list. The tile has been placed in the right spot
- // but the tile page still doesn't know about this new tile.
- if (extraTileData)
- this.appendTile(extraTile);
-
- }.bind(this);
-
- // Listens to the animation end.
- var lastAnimatingTile = extraTile;
- lastAnimatingTile.addEventListener('webkitTransitionEnd',
- onPositioningTransitionEnd);
- },
-
- /**
- * Animates a tile restoration.
- * @param {number} index The index of the tile to be restored.
- * @param {Object} newDataList The new data list.
- */
- animateTileRestoration: function(index, newDataList) {
- var tiles = this.tiles_;
- var tileCount = tiles.length;
-
- var tileCells = this.getElementsByClassName('tile-cell');
-
- // If the desired position is outside the grid, then the grid must be
- // expanded so there will be a cell in the desired position.
- if (index >= tileCells.length)
- this.renderGrid(null, index + 1);
-
- var extraTileIndex = Math.min(tileCount, this.config.maxTileCount - 1);
- var extraCell = tileCells[extraTileIndex];
- var extraTileData = newDataList[extraTileIndex + 1];
-
- var repositioningStartIndex = index;
- var repositioningEndIndex = tileCount - (extraTileData ? 1 : 0);
-
- this.initializeRepositioningAnimation_(index, repositioningEndIndex);
-
- var restoredData = newDataList[index];
- var tileBeingRestored = createTile(this, restoredData);
-
- // Temporarily assume the |index| cell so the tile can be animated in
- // the right spot.
- tileCells[index].appendChild(tileBeingRestored);
-
- if (this.config.scrollable)
- this.content_.scrollTop = tileCells[index].offsetTop;
-
- var extraTile;
- if (extraCell)
- extraTile = extraCell.tile;
-
- this.executeRepositioningAnimation_(tileBeingRestored, extraTile,
- repositioningStartIndex, repositioningEndIndex, false);
-
- // Cleans up the animation.
- var onPositioningTransitionEnd = function(e) {
- var propertyName = e.propertyName;
- if (!(propertyName == '-webkit-transform' ||
- propertyName == 'opacity')) {
- return;
- }
-
- lastAnimatingTile.removeEventListener('webkitTransitionEnd',
- onPositioningTransitionEnd);
-
- // When there's an extra data, it means the tile is a real one (not a
- // filler), and therefore it needs to be removed from the tile list.
- if (extraTileData)
- this.removeTile(extraTile);
-
- this.finalizeRepositioningAnimation_(tileBeingRestored,
- repositioningStartIndex, repositioningEndIndex, false);
-
- this.addTileAt(tileBeingRestored, index);
-
- }.bind(this);
-
- // Listens to the animation end.
- var lastAnimatingTile = tileBeingRestored;
- lastAnimatingTile.addEventListener('webkitTransitionEnd',
- onPositioningTransitionEnd);
- },
-
- // animation helpers
- // -------------------------------------------------------------------------
-
- /**
- * Moves a tile to a new position.
- * @param {Tile} tile A tile.
- * @param {number} left Left coordinate.
- * @param {number} top Top coordinate.
- * @private
- */
- moveTileTo_: function(tile, left, top) {
- tile.style.left = left + 'px';
- tile.style.top = top + 'px';
- },
-
- /**
- * Resets a tile's position.
- * @param {Tile} tile A tile.
- * @private
- */
- resetTilePosition_: function(tile) {
- tile.style.left = '';
- tile.style.top = '';
- },
-
- /**
- * Initializes the repositioning animation.
- * @param {number} startIndex Index of the first tile to be repositioned.
- * @param {number} endIndex Index of the last tile to be repositioned.
- * @param {boolean} isRemoving Whether the tile is being removed.
- * @private
- */
- initializeRepositioningAnimation_: function(startIndex, endIndex,
- isRemoving) {
- // Move tiles from relative to absolute position.
- var tiles = this.tiles_;
- var tileGridContent = this.tileGridContent_;
- for (var i = startIndex; i < endIndex; i++) {
- var tile = tiles[i];
- var position = this.getTilePosition_(i);
- this.moveTileTo_(tile, position.left, position.top);
- tile.style.zIndex = endIndex - i;
- tileGridContent.appendChild(tile);
- }
-
- tileGridContent.classList.add('animate-tile-repositioning');
-
- if (!isRemoving)
- tileGridContent.classList.add('undo-removal');
- },
-
- /**
- * Executes the repositioning animation.
- * @param {Tile} targetTile The tile that is being removed/restored.
- * @param {Tile} extraTile The extra tile that is going to appear/disappear.
- * @param {number} startIndex Index of the first tile to be repositioned.
- * @param {number} endIndex Index of the last tile to be repositioned.
- * @param {boolean} isRemoving Whether the tile is being removed.
- * @private
- */
- executeRepositioningAnimation_: function(targetTile, extraTile, startIndex,
- endIndex, isRemoving) {
- targetTile.classList.add('target-tile');
-
- // Alternate the visualization of the target and extra tiles.
- fadeTile(targetTile, !isRemoving);
- if (extraTile)
- fadeTile(extraTile, isRemoving);
-
- // Move tiles to the new position.
- var tiles = this.tiles_;
- var positionDiff = isRemoving ? -1 : 1;
- for (var i = startIndex; i < endIndex; i++) {
- var position = this.getTilePosition_(i + positionDiff);
- this.moveTileTo_(tiles[i], position.left, position.top);
- }
- },
-
- /**
- * Finalizes the repositioning animation.
- * @param {Tile} targetTile The tile that is being removed/restored.
- * @param {number} startIndex Index of the first tile to be repositioned.
- * @param {number} endIndex Index of the last tile to be repositioned.
- * @param {boolean} isRemoving Whether the tile is being removed.
- * @private
- */
- finalizeRepositioningAnimation_: function(targetTile, startIndex, endIndex,
- isRemoving) {
- // Remove temporary class names.
- var tileGridContent = this.tileGridContent_;
- tileGridContent.classList.remove('animate-tile-repositioning');
- tileGridContent.classList.remove('undo-removal');
- targetTile.classList.remove('target-tile');
-
- // Move tiles back to relative position.
- var tiles = this.tiles_;
- var tileCells = this.querySelectorAll('.tile-cell');
- var positionDiff = isRemoving ? -1 : 1;
- for (var i = startIndex; i < endIndex; i++) {
- var tile = tiles[i];
- this.resetTilePosition_(tile);
- tile.style.zIndex = '';
- var tileCell = tileCells[i + positionDiff];
- if (tileCell)
- tileCell.assign(tile);
- }
- },
-
- /**
- * Animates the display of columns.
- * @param {number} col The column number.
- * @param {boolean} show Whether or not to show the row.
- */
- showTileCols_: function(col, show) {
- var prop = show ? 'remove' : 'add';
- var max = 10; // TODO(pedrosimonetti): Add const?
- var tileGridContent = this.tileGridContent_;
- for (var i = col; i < max; i++) {
- tileGridContent.classList[prop]('hide-col-' + i);
- }
- },
-
- // event handlers
- // -------------------------------------------------------------------------
-
- /**
- * Handles the scroll event.
- * @protected
- */
- onScroll: function() {
- // If the TilePage is scrollable, then the opacity of shadow top and
- // bottom must adjusted, indicating when there's an overflow content.
- if (this.config.scrollable) {
- var content = this.content_;
- var topGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollTop);
- var bottomGap = Math.min(MAX_SCROLL_SHADOW_GAP, content.scrollHeight -
- content.scrollTop - content.clientHeight);
-
- this.shadowTop_.style.opacity = topGap / MAX_SCROLL_SHADOW_GAP;
- this.shadowBottom_.style.opacity = bottomGap / MAX_SCROLL_SHADOW_GAP;
- }
- },
-
- /**
- * Handles the end of the horizontal tile grid transition.
- * @param {Event} e The tile grid webkitTransitionEnd event.
- */
- onTileGridTransitionEnd_: function(e) {
- if (!this.selected)
- return;
-
- // We should remove the classes that control transitions when the
- // transition ends so when the text is resized (Ctrl + '+'), no other
- // transition should happen except those defined in the specification.
- // For example, the tile has a transition for its 'width' property which
- // is used when the tile is being hidden. But when you resize the text,
- // and therefore the tile changes its 'width', this change should not be
- // animated.
-
- // When the tile grid width transition ends, we need to remove the class
- // 'animate-grid-width' which handles the tile grid width transition, and
- // individual tile transitions. TODO(pedrosimonetti): Investigate if we
- // can improve the performance here by using a more efficient selector.
- var tileGrid = this.tileGrid_;
- if (e.target == tileGrid &&
- tileGrid.classList.contains('animate-grid-width')) {
- tileGrid.classList.remove('animate-grid-width');
-
- if (this.onTileGridTransitionEndHandler_)
- this.onTileGridTransitionEndHandler_();
- }
- },
- };
-
- /**
- * Creates a new tile given a particular data. If there's no data, then
- * a tile filler will be created.
- * @param {TilePage} tilePage A TilePage.
- * @param {Object=} opt_data The data that will be used to create the tile.
- * @return {Tile} The new tile.
- */
- function createTile(tilePage, opt_data) {
- var tile;
- if (opt_data) {
- // If there's data, the new tile will be a real one (not a filler).
- tile = new tilePage.TileClass(opt_data);
- } else {
- // Otherwise, it will be a fake filler tile.
- tile = cr.doc.createElement('span');
- tile.className = 'tile';
- }
- return tile;
- }
-
- /**
- * Fades a tile.
- * @param {Tile} tile A Tile.
- * @param {boolean} isFadeIn Whether to fade-in the tile. If |isFadeIn| is
- * false, then the tile is going to fade-out.
- */
- function fadeTile(tile, isFadeIn) {
- var className = 'animate-hide-tile';
- tile.classList.add(className);
- if (isFadeIn) {
- // Forces a reflow to ensure that the fade-out animation will work.
- tile.scrollTop;
- tile.classList.remove(className);
- }
- }
-
- return {
- Tile: Tile,
- TilePage: TilePage,
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('ntp', function() {
- 'use strict';
-
- var Tile = ntp.Tile;
- var TilePage = ntp.TilePage;
-
- /**
- * Creates a new Thumbnail object for tiling.
- * @param {Object=} opt_data The data representing the thumbnail.
- * @constructor
- * @extends {Tile}
- * @extends {HTMLAnchorElement}
- */
- function Thumbnail(opt_data) {
- var el = cr.doc.createElement('a');
- el.__proto__ = Thumbnail.prototype;
- el.initialize();
-
- if (opt_data)
- el.data = opt_data;
-
- return el;
- }
-
- Thumbnail.prototype = Tile.subclass({
- __proto__: HTMLAnchorElement.prototype,
-
- /**
- * Initializes a Thumbnail.
- */
- initialize: function() {
- Tile.prototype.initialize.apply(this, arguments);
- this.classList.add('thumbnail');
- this.addEventListener('mouseover', this.handleMouseOver_);
- },
-
- /**
- * Clears the DOM hierarchy for this node, setting it back to the default
- * for a blank thumbnail.
- */
- reset: function() {
- this.innerHTML =
- '<span class="thumbnail-wrapper">' +
- '<span class="thumbnail-image thumbnail-card"></span>' +
- '</span>' +
- '<span class="title"></span>';
-
- this.tabIndex = -1;
- this.data_ = null;
- this.title = '';
- },
-
- /**
- * Update the appearance of this tile according to |data|.
- * @param {Object} data A dictionary of relevant data for the page.
- */
- set data(data) {
- Object.getOwnPropertyDescriptor(Tile.prototype, 'data').set.apply(this,
- arguments);
-
- this.formatThumbnail_(data);
- },
-
- /**
- * Formats this tile according to |data|.
- * @param {Object} data A dictionary of relevant data for the page.
- * @private
- */
- formatThumbnail_: function(data) {
- var title = this.querySelector('.title');
- title.textContent = data.title;
- title.dir = data.direction;
-
- // Sets the tooltip.
- this.title = data.title;
-
- var dataUrl = data.url;
- // Allow an empty string href (e.g. for a multiple tab thumbnail).
- this.href = typeof data.href != 'undefined' ? data.href : dataUrl;
-
- var thumbnailImage = this.querySelector('.thumbnail-image');
-
- var banner = thumbnailImage.querySelector('.thumbnail-banner');
- if (banner)
- thumbnailImage.removeChild(banner);
-
- var favicon = this.querySelector('.thumbnail-favicon') ||
- this.ownerDocument.createElement('div');
- favicon.className = 'thumbnail-favicon';
- favicon.style.backgroundImage =
- url('chrome://favicon/size/16/' + dataUrl);
- this.appendChild(favicon);
-
- var self = this;
- var image = new Image();
-
- // If the thumbnail image fails to load, show the favicon and URL instead.
- // TODO(jeremycho): Move to a separate function?
- image.onerror = function() {
- banner = thumbnailImage.querySelector('.thumbnail-banner') ||
- self.ownerDocument.createElement('div');
- banner.className = 'thumbnail-banner';
-
- // For now, just strip leading http://www and trailing backslash.
- // TODO(jeremycho): Consult with UX on URL truncation.
- banner.textContent = dataUrl.replace(/^(http:\/\/)?(www\.)?|\/$/gi, '');
- thumbnailImage.appendChild(banner);
- };
-
- var thumbnailUrl = ntp.getThumbnailUrl(dataUrl);
- thumbnailImage.style.backgroundImage = url(thumbnailUrl);
- image.src = thumbnailUrl;
- },
-
- /**
- * Returns true if this is a thumbnail or descendant thereof. Used to
- * detect when the mouse has transitioned into this thumbnail from a
- * strictly non-thumbnail element.
- * @param {Object} element The element to test.
- * @return {boolean} True if this is a thumbnail or a descendant thereof.
- * @private
- */
- isInThumbnail_: function(element) {
- while (element) {
- if (element == this)
- return true;
- element = element.parentNode;
- }
- return false;
- },
-
- /**
- * Increment the hover count whenever the mouse enters a thumbnail.
- * @param {Event} e The mouse over event.
- * @private
- */
- handleMouseOver_: function(e) {
- if (this.isInThumbnail_(e.target) &&
- !this.isInThumbnail_(e.relatedTarget)) {
- ntp.incrementHoveredThumbnailCount();
- }
- },
- });
-
- /**
- * Creates a new ThumbnailPage object.
- * @constructor
- * @extends {TilePage}
- */
- function ThumbnailPage() {
- var el = new TilePage();
- el.__proto__ = ThumbnailPage.prototype;
-
- return el;
- }
-
- ThumbnailPage.prototype = {
- __proto__: TilePage.prototype,
-
- /**
- * Initializes a ThumbnailPage.
- */
- initialize: function() {
- TilePage.prototype.initialize.apply(this, arguments);
-
- this.classList.add('thumbnail-page');
- },
-
- /** @override */
- shouldAcceptDrag: function(e) {
- return false;
- },
- };
-
- return {
- Thumbnail: Thumbnail,
- ThumbnailPage: ThumbnailPage,
- };
- });
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('ntp', function() {
- 'use strict';
-
- var Thumbnail = ntp.Thumbnail;
- var ThumbnailPage = ntp.ThumbnailPage;
-
- /**
- * Creates a new Most Visited object for tiling.
- * @param {Object=} opt_data The data representing the most visited page.
- * @constructor
- * @extends {Thumbnail}
- * @extends {HTMLAnchorElement}
- */
- function MostVisited(opt_data) {
- var el = cr.doc.createElement('a');
- el.__proto__ = MostVisited.prototype;
- el.initialize();
-
- if (opt_data)
- el.data = opt_data;
-
- return el;
- }
-
- MostVisited.prototype = {
- __proto__: Thumbnail.prototype,
-
- /**
- * Initializes a MostVisited Thumbnail.
- */
- initialize: function() {
- Thumbnail.prototype.initialize.apply(this, arguments);
-
- this.addEventListener('click', this.handleClick_);
- this.addEventListener('keydown', this.handleKeyDown_);
- this.addEventListener('carddeselected', this.handleCardDeselected_);
- this.addEventListener('cardselected', this.handleCardSelected_);
- },
-
- /**
- * Clears the DOM hierarchy for this node, setting it back to the default
- * for a blank thumbnail.
- */
- reset: function() {
- Thumbnail.prototype.reset.apply(this, arguments);
-
- var closeButton = cr.doc.createElement('div');
- closeButton.className = 'close-button';
- closeButton.title = loadTimeData.getString('removethumbnailtooltip');
- this.appendChild(closeButton);
- },
-
- /**
- * Update the appearance of this tile according to |data|.
- * @param {Object} data A dictionary of relevant data for the page.
- */
- set data(data) {
- Object.getOwnPropertyDescriptor(Thumbnail.prototype, 'data').set.apply(
- this, arguments);
-
- if (this.classList.contains('blacklisted') && data) {
- // Animate appearance of new tile.
- this.classList.add('new-tile-contents');
- }
- this.classList.remove('blacklisted');
- },
- get data() {
- return this.data_;
- },
-
- /**
- * Handles a click on the tile.
- * @param {Event} e The click event.
- * @private
- */
- handleClick_: function(e) {
- if (e.target.classList.contains('close-button')) {
- this.blacklist_();
- e.preventDefault();
- } else {
- ntp.logTimeToClickAndHoverCount('MostVisited');
- // Records an app launch from the most visited page (Chrome will decide
- // whether the url is an app). TODO(estade): this only works for clicks;
- // other actions like "open in new tab" from the context menu won't be
- // recorded. Can this be fixed?
- chrome.send('recordAppLaunchByURL',
- [encodeURIComponent(this.href),
- ntp.APP_LAUNCH.NTP_MOST_VISITED]);
- // Records the index of this tile.
- chrome.send('metricsHandler:recordInHistogram',
- ['NewTabPage.MostVisited', this.index, 8]);
- chrome.send('mostVisitedAction',
- [ntp.NtpFollowAction.CLICKED_TILE]);
- }
- },
-
- /**
- * Allow blacklisting most visited site using the keyboard.
- * @private
- */
- handleKeyDown_: function(e) {
- if (!cr.isMac && e.keyCode == 46 || // Del
- cr.isMac && e.metaKey && e.keyCode == 8) { // Cmd + Backspace
- this.blacklist_();
- }
- },
-
- /**
- * Permanently removes a page from Most Visited.
- * @private
- */
- blacklist_: function() {
- this.tileCell.tilePage.setTileRepositioningState(this.index, true);
- this.showUndoNotification_();
- chrome.send('blacklistURLFromMostVisited', [this.data_.url]);
- this.classList.add('blacklisted');
- },
-
- /**
- * Shows the undo notification when blacklisting a most visited site.
- * @private
- */
- showUndoNotification_: function() {
- var data = this.data_;
- var tilePage = this.tileCell.tilePage;
- var index = this.index;
- var doUndo = function() {
- tilePage.setTileRepositioningState(index, false);
- chrome.send('removeURLsFromMostVisitedBlacklist', [data.url]);
- };
-
- var undo = {
- action: doUndo,
- text: loadTimeData.getString('undothumbnailremove'),
- };
-
- var undoAll = {
- action: function() {
- chrome.send('clearMostVisitedURLsBlacklist');
- },
- text: loadTimeData.getString('restoreThumbnailsShort'),
- };
-
- ntp.showNotification(
- loadTimeData.getString('thumbnailremovednotification'),
- [undo, undoAll]);
- },
-
- /**
- * Returns whether this element can be 'removed' from chrome.
- * @return {boolean} True, since most visited pages can always be
- * blacklisted.
- */
- canBeRemoved: function() {
- return true;
- },
- };
-
- /**
- * Creates a new MostVisitedPage object.
- * @constructor
- * @extends {ThumbnailPage}
- */
- function MostVisitedPage() {
- var el = new ThumbnailPage();
- el.__proto__ = MostVisitedPage.prototype;
- el.initialize();
-
- return el;
- }
-
- MostVisitedPage.prototype = {
- __proto__: ThumbnailPage.prototype,
-
- TileClass: MostVisited,
-
- /**
- * Initializes a MostVisitedPage.
- */
- initialize: function() {
- ThumbnailPage.prototype.initialize.apply(this, arguments);
-
- this.classList.add('most-visited-page');
- },
-
- /**
- * Handles the 'card deselected' event (i.e. the user clicked to another
- * pane).
- * @private
- * @param {Event} e The CardChanged event.
- */
- handleCardDeselected_: function(e) {
- if (!document.documentElement.classList.contains('starting-up')) {
- chrome.send('mostVisitedAction',
- [ntp.NtpFollowAction.CLICKED_OTHER_NTP_PANE]);
- }
- },
-
- /**
- * Handles the 'card selected' event (i.e. the user clicked to select the
- * this page's pane).
- * @private
- * @param {Event} e The CardChanged event.
- */
- handleCardSelected_: function(e) {
- if (!document.documentElement.classList.contains('starting-up'))
- chrome.send('mostVisitedSelected');
- },
-
- /** @override */
- setDataList: function(dataList) {
- var startTime = Date.now();
- ThumbnailPage.prototype.setDataList.apply(this, arguments);
- this.updateGrid();
- logEvent('mostVisited.layout: ' + (Date.now() - startTime));
- },
- };
-
- /**
- * Executed once the NTP has loaded. Checks if the Most Visited pane is
- * shown or not. If it is shown, the 'mostVisitedSelected' message is sent
- * to the C++ code, to record the fact that the user has seen this pane.
- */
- MostVisitedPage.onLoaded = function() {
- if (ntp.getCardSlider() &&
- ntp.getCardSlider().currentCardValue &&
- ntp.getCardSlider().currentCardValue.classList
- .contains('most-visited-page')) {
- chrome.send('mostVisitedSelected');
- }
- };
-
- return {
- MostVisitedPage: MostVisitedPage,
- };
- });
-
- document.addEventListener('ntpLoaded', ntp.MostVisitedPage.onLoaded);
- </script>
- <script>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('ntp', function() {
- 'use strict';
-
- var Tile = ntp.Tile;
- var TilePage = ntp.TilePage;
- var APP_LAUNCH = ntp.APP_LAUNCH;
-
- // Histogram buckets for UMA tracking of where a DnD drop came from.
- var DRAG_SOURCE = {
- SAME_APPS_PANE: 0,
- OTHER_APPS_PANE: 1,
- MOST_VISITED_PANE: 2,
- BOOKMARKS_PANE: 3,
- OUTSIDE_NTP: 4
- };
- var DRAG_SOURCE_LIMIT = DRAG_SOURCE.OUTSIDE_NTP + 1;
-
- /**
- * App context menu. The class is designed to be used as a singleton with
- * the app that is currently showing a context menu stored in this.app_.
- * @constructor
- */
- function AppContextMenu() {
- this.__proto__ = AppContextMenu.prototype;
- this.initialize();
- }
- cr.addSingletonGetter(AppContextMenu);
-
- AppContextMenu.prototype = {
- initialize: function() {
- var menu = new cr.ui.Menu;
- cr.ui.decorate(menu, cr.ui.Menu);
- menu.classList.add('app-context-menu');
- this.menu = menu;
-
- this.launch_ = this.appendMenuItem_();
- this.launch_.addEventListener('activate', this.onLaunch_.bind(this));
-
- menu.appendChild(cr.ui.MenuItem.createSeparator());
- this.launchRegularTab_ = this.appendMenuItem_('applaunchtyperegular');
- this.launchPinnedTab_ = this.appendMenuItem_('applaunchtypepinned');
- if (!cr.isMac)
- this.launchNewWindow_ = this.appendMenuItem_('applaunchtypewindow');
- this.launchFullscreen_ = this.appendMenuItem_('applaunchtypefullscreen');
-
- var self = this;
- this.forAllLaunchTypes_(function(launchTypeButton, id) {
- launchTypeButton.addEventListener('activate',
- self.onLaunchTypeChanged_.bind(self));
- });
-
- menu.appendChild(cr.ui.MenuItem.createSeparator());
- this.options_ = this.appendMenuItem_('appoptions');
- this.details_ = this.appendMenuItem_('appdetails');
- this.disableNotifications_ =
- this.appendMenuItem_('appdisablenotifications');
- this.uninstall_ = this.appendMenuItem_('appuninstall');
- this.options_.addEventListener('activate',
- this.onShowOptions_.bind(this));
- this.details_.addEventListener('activate',
- this.onShowDetails_.bind(this));
- this.disableNotifications_.addEventListener(
- 'activate', this.onDisableNotifications_.bind(this));
- this.uninstall_.addEventListener('activate',
- this.onUninstall_.bind(this));
-
- if (!cr.isMac && !cr.isChromeOS) {
- menu.appendChild(cr.ui.MenuItem.createSeparator());
- this.createShortcut_ = this.appendMenuItem_('appcreateshortcut');
- this.createShortcut_.addEventListener(
- 'activate', this.onCreateShortcut_.bind(this));
- }
-
- document.body.appendChild(menu);
- },
-
- /**
- * Appends a menu item to |this.menu|.
- * @param {?String} textId If non-null, the ID for the localized string
- * that acts as the item's label.
- */
- appendMenuItem_: function(textId) {
- var button = cr.doc.createElement('button');
- this.menu.appendChild(button);
- cr.ui.decorate(button, cr.ui.MenuItem);
- if (textId)
- button.textContent = loadTimeData.getString(textId);
- return button;
- },
-
- /**
- * Iterates over all the launch type menu items.
- * @param {function(cr.ui.MenuItem, number)} f The function to call for each
- * menu item. The parameters to the function include the menu item and
- * the associated launch ID.
- */
- forAllLaunchTypes_: function(f) {
- // Order matters: index matches launchType id.
- var launchTypes = [this.launchPinnedTab_,
- this.launchRegularTab_,
- this.launchFullscreen_,
- this.launchNewWindow_];
-
- for (var i = 0; i < launchTypes.length; ++i) {
- if (!launchTypes[i])
- continue;
-
- f(launchTypes[i], i);
- }
- },
-
- /**
- * Does all the necessary setup to show the menu for the given app.
- * @param {App} app The App object that will be showing a context menu.
- */
- setupForApp: function(app) {
- this.app_ = app;
-
- this.launch_.textContent = app.data.title;
-
- this.forAllLaunchTypes_(function(launchTypeButton, id) {
- launchTypeButton.disabled = false;
- launchTypeButton.checked = app.data.launch_type == id;
- });
-
- this.options_.disabled = !app.data.optionsUrl || !app.data.enabled;
- this.details_.disabled = !app.data.detailsUrl;
- this.uninstall_.disabled = !app.data.mayDisable;
-
- this.disableNotifications_.hidden = true;
- var notificationsDisabled = app.data.notifications_disabled;
- if (typeof notificationsDisabled != 'undefined') {
- this.disableNotifications_.hidden = false;
- this.disableNotifications_.checked = notificationsDisabled;
- }
- },
-
- /**
- * Handlers for menu item activation.
- * @param {Event} e The activation event.
- * @private
- */
- onLaunch_: function(e) {
- chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
- },
- onLaunchTypeChanged_: function(e) {
- var pressed = e.currentTarget;
- var app = this.app_;
- this.forAllLaunchTypes_(function(launchTypeButton, id) {
- if (launchTypeButton == pressed) {
- chrome.send('setLaunchType', [app.appId, id]);
- // Manually update the launch type. We will only get
- // appsPrefChangeCallback calls after changes to other NTP instances.
- app.data.launch_type = id;
- }
- });
- },
- onShowOptions_: function(e) {
- window.location = this.app_.data.optionsUrl;
- },
- onShowDetails_: function(e) {
- var url = this.app_.data.detailsUrl;
- url = appendParam(url, 'utm_source', 'chrome-ntp-launcher');
- window.location = url;
- },
- onDisableNotifications_: function(e) {
- var app = this.app_;
- app.removeBubble();
- // Toggle the current disable setting.
- var newSetting = !this.disableNotifications_.checked;
- app.data.notifications_disabled = newSetting;
- chrome.send('setNotificationsDisabled', [app.data.id, newSetting]);
- },
- onUninstall_: function(e) {
- chrome.send('uninstallApp', [this.app_.data.id]);
- },
- onCreateShortcut_: function(e) {
- chrome.send('createAppShortcut', [this.app_.data.id]);
- },
- };
-
- /**
- * Creates a new App object.
- * @param {Object=} opt_data The data representing the app.
- * @constructor
- * @extends {HTMLDivElement}
- */
- function App(opt_data) {
- var el = cr.doc.createElement('div');
- el.__proto__ = App.prototype;
- el.initialize_();
-
- if (opt_data)
- el.data = opt_data;
-
- return el;
- }
-
- App.prototype = Tile.subclass({
- __proto__: HTMLDivElement.prototype,
-
- /**
- * Initialize the app object.
- * @private
- */
- initialize_: function() {
- Tile.prototype.initialize.apply(this, arguments);
-
- this.classList.add('app');
- this.classList.add('focusable');
- },
-
- /**
- * Formats this app according to |data|.
- * @param {Object} data The data object that describes the app.
- * @private
- */
- formatApp_: function(data) {
- assert(this.data_.id, 'Got an app without an ID');
- this.id = this.data_.id;
- this.setAttribute('role', 'menuitem');
-
- if (!this.data_.icon_big_exists && this.data_.icon_small_exists)
- this.useSmallIcon_ = true;
-
- // TODO(pedrosimonetti): Fix crbug.com/165612
- if (!this.appContents_) {
- this.appContents_ = this.useSmallIcon_ ?
- $('app-small-icon-template').cloneNode(true) :
- $('app-large-icon-template').cloneNode(true);
- this.appContents_.id = '';
- this.appendChild(this.appContents_);
- }
-
- this.appImgContainer_ = this.querySelector('.app-img-container');
- this.appImg_ = this.appImgContainer_.querySelector('img');
- this.setIcon();
-
- var appTitle;
- if (this.useSmallIcon_) {
- this.classList.add('small-icon');
- this.imgDiv_ = this.querySelector('.app-icon-div');
- this.addLaunchClickTarget_(this.imgDiv_);
- this.imgDiv_.title = this.data_.title;
- appTitle = formatTitle(this.data_.title);
- chrome.send('getAppIconDominantColor', [this.id]);
- } else {
- this.classList.remove('small-icon');
- this.addLaunchClickTarget_(this.appImgContainer_);
- this.appImgContainer_.title = this.data_.title;
- appTitle = this.data_.title;
- }
-
- var appSpan = this.appContents_.querySelector('.title');
- appSpan.textContent = appTitle;
- appSpan.title = this.data_.title;
- this.addLaunchClickTarget_(appSpan);
-
- var notification = this.data_.notification;
- var hasNotification = typeof notification != 'undefined' &&
- typeof notification['title'] != 'undefined' &&
- typeof notification['body'] != 'undefined' &&
- !this.data_.notifications_disabled;
- if (hasNotification)
- this.setupNotification_(notification);
-
- this.addEventListener('keydown', cr.ui.contextMenuHandler);
- this.addEventListener('keyup', cr.ui.contextMenuHandler);
-
- // This hack is here so that appContents.contextMenu will be the same as
- // this.contextMenu.
- var self = this;
- this.appContents_.__defineGetter__('contextMenu', function() {
- return self.contextMenu;
- });
- this.appContents_.addEventListener('contextmenu',
- cr.ui.contextMenuHandler);
-
- this.addEventListener('mousedown', this.onMousedown_, true);
- this.addEventListener('keydown', this.onKeydown_);
- this.addEventListener('keyup', this.onKeyup_);
- },
-
- /**
- * Sets the color of the favicon dominant color bar.
- * @param {string} color The css-parsable value for the color.
- */
- set stripeColor(color) {
- this.querySelector('.color-stripe').style.backgroundColor = color;
- },
-
- /**
- * Removes the app tile from the page. Should be called after the app has
- * been uninstalled.
- */
- remove: function(opt_animate) {
- // Unset the ID immediately, because the app is already gone. But leave
- // the tile on the page as it animates out.
- this.id = '';
-
- if (opt_animate) {
- var cell = this.tileCell;
- var tilePage = cell.tilePage;
- tilePage.dataList_.splice(cell.index, 1);
- tilePage.animateTileRemoval(cell.index, tilePage.dataList_);
- } else {
- this.tileCell.doRemove(opt_animate);
- }
- },
-
- /**
- * Set the URL of the icon from |this.data_|. This won't actually show the
- * icon until loadIcon() is called (for performance reasons; we don't want
- * to load icons until we have to).
- */
- setIcon: function() {
- var src = this.useSmallIcon_ ? this.data_.icon_small :
- this.data_.icon_big;
- if (!this.data_.enabled ||
- (!this.data_.offlineEnabled && !navigator.onLine)) {
- src += '?grayscale=true';
- }
-
- this.appImgSrc_ = src;
- this.classList.add('icon-loading');
- },
-
- /**
- * Shows the icon for the app. That is, it causes chrome to load the app
- * icon resource.
- */
- loadIcon: function() {
- if (this.appImgSrc_) {
- this.appImg_.src = this.appImgSrc_;
- this.appImg_.classList.remove('invisible');
- this.appImgSrc_ = null;
- }
-
- this.classList.remove('icon-loading');
- },
-
- /**
- * Creates a bubble node.
- * @param {Object} notification The notification to show in the bubble.
- * @param {boolean} full Whether we want the headline or just the content.
- * @private
- */
- createBubbleNode_: function(notification, full) {
- if (!full) {
- var titleItem = this.ownerDocument.createElement('span');
- titleItem.textContent = notification['title'];
- return titleItem;
- } else {
- var container = this.ownerDocument.createElement('div');
-
- var messageItem = this.ownerDocument.createElement('div');
- messageItem.textContent = notification['body'];
- container.appendChild(messageItem);
-
- if (notification['linkUrl'] && notification['linkText']) {
- var anchor = this.ownerDocument.createElement('a');
- anchor.href = notification['linkUrl'];
- anchor.textContent = notification['linkText'];
- container.appendChild(anchor);
- }
-
- return container;
- }
- },
-
- /**
- * Sets up a notification for the app icon.
- * @param {Object} notification The notification to show in the bubble.
- * @private
- */
- setupNotification_: function(notification) {
- if (notification) {
- var infoBubble;
- if (!this.currentBubbleShowing_) {
- // Create a new bubble.
- infoBubble = new cr.ui.ExpandableBubble;
- infoBubble.anchorNode = this;
- infoBubble.appId = this.data_.id;
- infoBubble.handleCloseEvent = function() {
- chrome.send('closeNotification', [this.appId]);
- infoBubble.hide();
- };
- } else {
- // Reuse the old bubble instead of popping up a new bubble over
- // the old one.
- infoBubble = this.currentBubbleShowing_;
- infoBubble.collapseBubble_();
- }
- infoBubble.contentTitle = this.createBubbleNode_(notification, false);
- infoBubble.content = this.createBubbleNode_(notification, true);
- infoBubble.show();
- infoBubble.resizeAndReposition();
-
- this.currentBubbleShowing_ = infoBubble;
- }
- },
-
- /**
- * Removes the info bubble if there is one.
- */
- removeBubble: function() {
- if (this.currentBubbleShowing_) {
- this.currentBubbleShowing_.hide();
- this.currentBubbleShowing_ = null;
- }
- },
-
- /**
- * Invoked when an app is clicked.
- * @param {Event} e The click event.
- * @private
- */
- onClick_: function(e) {
- var url = !this.data_.is_webstore ? '' :
- appendParam(this.data_.url,
- 'utm_source',
- 'chrome-ntp-icon');
-
- chrome.send('launchApp',
- [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
- e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
-
- // Don't allow the click to trigger a link or anything
- e.preventDefault();
- },
-
- /**
- * Invoked when the user presses a key while the app is focused.
- * @param {Event} e The key event.
- * @private
- */
- onKeydown_: function(e) {
- if (e.keyIdentifier == 'Enter') {
- chrome.send('launchApp',
- [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, '',
- 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
- e.preventDefault();
- e.stopPropagation();
- }
- this.onKeyboardUsed_(e.keyCode);
- },
-
- /**
- * Invoked when the user releases a key while the app is focused.
- * @param {Event} e The key event.
- * @private
- */
- onKeyup_: function(e) {
- this.onKeyboardUsed_(e.keyCode);
- },
-
- /**
- * Called when the keyboard has been used (key down or up). The .click-focus
- * hack is removed if the user presses a key that can change focus.
- * @param {number} keyCode The key code of the keyboard event.
- * @private
- */
- onKeyboardUsed_: function(keyCode) {
- switch (keyCode) {
- case 9: // Tab.
- case 37: // Left arrow.
- case 38: // Up arrow.
- case 39: // Right arrow.
- case 40: // Down arrow.
- this.classList.remove('click-focus');
- }
- },
-
- /**
- * Adds a node to the list of targets that will launch the app. This list
- * is also used in onMousedown to determine whether the app contents should
- * be shown as active (if we don't do this, then clicking anywhere in
- * appContents, even a part that is outside the ideally clickable region,
- * will cause the app icon to look active).
- * @param {HTMLElement} node The node that should be clickable.
- */
- addLaunchClickTarget_: function(node) {
- node.classList.add('launch-click-target');
- node.addEventListener('click', this.onClick_.bind(this));
- },
-
- /**
- * Handler for mousedown on the App. Adds a class that allows us to
- * not display as :active for right clicks and clicks on app notifications
- * (specifically, don't pulse on these occasions). Also, we don't pulse
- * for clicks that aren't within the clickable regions.
- * @param {Event} e The mousedown event.
- */
- onMousedown_: function(e) {
- if (e.button == 2 ||
- !findAncestorByClass(e.target, 'launch-click-target')) {
- this.appContents_.classList.add('suppress-active');
- } else {
- this.appContents_.classList.remove('suppress-active');
- }
-
- // This class is here so we don't show the focus state for apps that
- // gain keyboard focus via mouse clicking.
- this.classList.add('click-focus');
- },
-
- /**
- * Change the data and update the appearance of the app.
- * @param {Object} data The new data object that describes the app.
- */
- replaceAppData: function(data) {
- assert(data);
- this.data = data;
- this.setIcon();
- this.loadIcon();
- },
-
- /**
- * The data and preferences for this app.
- * @type {Object}
- */
- set data(data) {
- Object.getOwnPropertyDescriptor(Tile.prototype, 'data').set.apply(this,
- arguments);
-
- this.formatApp_(data);
- },
- get data() {
- return this.data_;
- },
-
- get appId() {
- return this.data_.id;
- },
-
- /**
- * Returns a pointer to the context menu for this app. All apps share the
- * singleton AppContextMenu. This function is called by the
- * ContextMenuHandler in response to the 'contextmenu' event.
- * @type {cr.ui.Menu}
- */
- get contextMenu() {
- var menu = AppContextMenu.getInstance();
- menu.setupForApp(this);
- return menu.menu;
- },
-
- /**
- * Returns whether this element can be 'removed' from chrome (i.e. whether
- * the user can drag it onto the trash and expect something to happen).
- * @return {boolean} True if the app can be uninstalled.
- */
- canBeRemoved: function() {
- return this.data_.mayDisable;
- },
-
- /**
- * Uninstalls the app after it's been dropped on the trash.
- */
- removeFromChrome: function() {
- chrome.send('uninstallApp', [this.data_.id, true]);
- this.tile.tilePage.removeTile(this.tile, true);
- if (this.currentBubbleShowing_)
- this.currentBubbleShowing_.hide();
- },
- });
-
- /**
- * Creates a new AppsPage object.
- * @constructor
- * @extends {TilePage}
- */
- function AppsPage() {
- var el = new TilePage();
- el.__proto__ = AppsPage.prototype;
- el.initialize();
-
- return el;
- }
-
- AppsPage.prototype = {
- __proto__: TilePage.prototype,
-
- /**
- * Reference to the Tile subclass that will be used to create the tiles.
- * @constructor
- * @extends {Tile}
- */
- TileClass: App,
-
- // The config object should be defined by a TilePage subclass if it
- // wants the non-default behavior.
- config: {
- // The width of a cell.
- cellWidth: 70,
- // The start margin of a cell (left or right according to text direction).
- cellMarginStart: 20,
- // The maximum number of Tiles to be displayed.
- maxTileCount: 512,
- // Whether the TilePage content will be scrollable.
- scrollable: true,
- },
-
- initialize: function() {
- TilePage.prototype.initialize.apply(this, arguments);
-
- this.classList.add('apps-page');
-
- this.addEventListener('cardselected', this.onCardSelected_);
- // Add event listeners for two events, so we can temporarily suppress
- // the app notification bubbles when the app card slides in and out of
- // view.
- this.addEventListener('carddeselected', this.onCardDeselected_);
- this.addEventListener('cardSlider:card_change_ended',
- this.onCardChangeEnded_);
-
- this.addEventListener('tilePage:tile_added', this.onTileAdded_);
- },
-
- /**
- * Highlight a newly installed app as it's added to the NTP.
- * @param {Object} data The data object that describes the app.
- */
- insertAndHighlightApp: function(data) {
- ntp.getCardSlider().selectCardByValue(this);
- this.insertApp(data, true);
- },
-
- /**
- * Inserts an App into the TilePage, preserving the alphabetical order.
- * @param {Object} data The data that describes the app.
- * @param {boolean} animate Whether to animate the insertion.
- */
- insertApp: function(data, animate) {
- var index = this.tiles_.length;
- for (var i = 0; i < this.tiles_.length; i++) {
- if (data.title.toLocaleLowerCase() <
- this.tiles_[i].data.title.toLocaleLowerCase()) {
- index = i;
- break;
- }
- }
-
- if (animate) {
- this.dataList_.splice(index, 0, data);
- this.animateTileRestoration(index, this.dataList_);
- } else {
- var app = new App(data);
- this.addTileAt(app, index);
- }
- },
-
- /**
- * Handler for 'cardselected' event, fired when |this| is selected. The
- * first time this is called, we load all the app icons.
- * @private
- */
- onCardSelected_: function(e) {
- var apps = this.querySelectorAll('.app.icon-loading');
- for (var i = 0; i < apps.length; i++) {
- apps[i].loadIcon();
- if (apps[i].currentBubbleShowing_)
- apps[i].currentBubbleShowing_.suppressed = false;
- }
- },
-
- /**
- * Handler for tile additions to this page.
- * @param {Event} e The tilePage:tile_added event.
- */
- onTileAdded_: function(e) {
- assert(e.currentTarget == this);
- assert(e.addedTile instanceof App);
- if (this.classList.contains('selected-card'))
- e.addedTile.loadIcon();
- },
-
- /**
- * Handler for the when this.cardSlider ends change its card. If animated,
- * this happens when the -webkit-transition is done, otherwise happens
- * immediately (but after cardSlider:card_changed).
- * @private
- */
- onCardChangeEnded_: function(e) {
- for (var i = 0; i < this.tiles_.length; i++) {
- var app = this.tiles_[i];
- assert(app instanceof App);
- if (app.currentBubbleShowing_)
- app.currentBubbleShowing_.suppressed = false;
- }
- },
-
- /**
- * Handler for the 'carddeselected' event, fired when the user switches
- * to another 'card' than the App 'card' on the NTP (|this| gets
- * deselected).
- * @private
- */
- onCardDeselected_: function(e) {
- for (var i = 0; i < this.tiles_.length; i++) {
- var app = this.tiles_[i];
- assert(app instanceof App);
- if (app.currentBubbleShowing_)
- app.currentBubbleShowing_.suppressed = true;
- }
- },
-
- /** @override */
- onScroll: function() {
- TilePage.prototype.onScroll.apply(this, arguments);
-
- for (var i = 0; i < this.tiles_.length; i++) {
- var app = this.tiles_[i];
- assert(app instanceof App);
- if (app.currentBubbleShowing_)
- app.currentBubbleShowing_.resizeAndReposition();
- }
- },
-
- /**
- * Creates a new crx-less app manifest and installs it.
- * @param {Object} data The data object describing the link. Must have |url|
- * and |title| members.
- */
- generateAppForLink: function(data) {
- assert(data.url != undefined);
- assert(data.title != undefined);
- chrome.send('generateAppForLink', [data.url, data.title, 0]);
- },
- };
-
- /**
- * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
- * histogram. This should only be invoked from the AppLauncherHandler.
- * @param {String} appID The ID of the app.
- */
- function launchAppAfterEnable(appId) {
- chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
- }
-
- function appNotificationChanged(id, notification) {
- var app = $(id);
- // The app might have been uninstalled, or notifications might be disabled.
- if (app && !app.data.notifications_disabled)
- app.setupNotification_(notification);
- }
-
- /**
- * Formats titles by removing the leading 'http://www.' part of the URL,
- * and the last slash, so 'http://www.test.com/' becomes 'test.com'.
- * @param {string} title Page's title.
- * @return {string} The formatted title.
- */
- function formatTitle(title) {
- return title.replace(/^(https?\:\/\/)?(www\.)?|\/$/gi, '');
- }
-
- return {
- appNotificationChanged: appNotificationChanged,
- AppsPage: AppsPage,
- launchAppAfterEnable: launchAppAfterEnable,
- };
- });
- </script>
- </head>
-
- <body id="ntp5" i18n-values=".style.fontSize:fontsize">
- <div id="bottom-panel">
- <div id="bottom-panel-header">
- <ul id="dot-list" hidden></ul>
- </div>
- <div id="card-slider-frame">
- <div id="page-list"></div>
- <div id="attribution">
- <span i18n-content="attributionintro"></span>
- <img id="attribution-img">
- </div>
- </div>
- <div id="bottom-panel-footer">
- <div id="bottom-panel-toolbar">
- <div id="notification-container" hidden>
- <div id="notification">
- <span></span>
- <div id="notificationLinks"></div>
- <button class="close-button custom-appearance"></button>
- </div>
- </div>
- </div>
- <div id="bookmark-bar-spacer"></div>
- </div>
- </div>
- <div id="promo-bubble-anchor"></div>
- </body>
-
- <!-- A div to hold all the templates, and in the darkness bind them. -->
- <div hidden>
-
- <!-- NTP4 intro bubble -->
- <div id="ntp4-intro-bubble-contents">
- <div></div>
- <a i18n-content="learn_more" target="_blank"></a>
- </div>
-
- <!-- App Contents w/ Large Icon -->
- <div id="app-large-icon-template" class="app-contents">
- <div class="app-img-container">
- <img class="invisible">
- </div>
- <span class="title"></span>
- </div>
-
- <!-- App Contents w/ Small Icon -->
- <div id="app-small-icon-template" class="app-contents">
- <div class="app-icon-div" aria-hidden="true">
- <div class="app-img-container">
- <img class="invisible" alt="">
- </div>
- <div class="color-stripe"></div>
- </div>
- <span class="title"></span>
- </div>
-
- </div>
-
- </html>
-