diff options
author | jhawkins@chromium.org <jhawkins@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-14 18:47:26 +0000 |
---|---|---|
committer | jhawkins@chromium.org <jhawkins@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-12-14 18:47:26 +0000 |
commit | 846c3eceac6eb556f77218c50ab16244ebbb9146 (patch) | |
tree | 259ee74cab54e263c15b06eec22dcca2d9abf2a8 /chrome/browser/resources | |
parent | 34608cfed25d4ed2275af1ceb7ff978d0a48ef17 (diff) | |
download | chromium_src-846c3eceac6eb556f77218c50ab16244ebbb9146.zip chromium_src-846c3eceac6eb556f77218c50ab16244ebbb9146.tar.gz chromium_src-846c3eceac6eb556f77218c50ab16244ebbb9146.tar.bz2 |
Options2: Pull the trigger.
Options2 is a copy of the resources for and implementation of chrome://settings that will be pared down significantly for UberPage (see bug).
BUG=100885
TEST=none
R=csilv
Review URL: http://codereview.chromium.org/8895023
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@114462 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/resources')
146 files changed, 25092 insertions, 1 deletions
diff --git a/chrome/browser/resources/options2/OWNERS b/chrome/browser/resources/options2/OWNERS new file mode 100644 index 0000000..67257e6 --- /dev/null +++ b/chrome/browser/resources/options2/OWNERS @@ -0,0 +1,4 @@ +csilv@chromium.org +estade@chromium.org +jhawkins@chromium.org +stuartmorgan@chromium.org diff --git a/chrome/browser/resources/options2/about_page.css b/chrome/browser/resources/options2/about_page.css new file mode 100644 index 0000000..a64f9b8 --- /dev/null +++ b/chrome/browser/resources/options2/about_page.css @@ -0,0 +1,34 @@ +#aboutPage { + -webkit-user-select: text; +} + +.loading { + font-style: italic; +} + +#channelSelect { + margin-bottom: 5px; +} + +#channelWarning { + color: red; + font-weight: bold; +} + +.update-icon { + width: 17px; + height: 17px; + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; +} + +.update-icon.fail { + background-image: url('../../../app/theme/update_fail.png'); +} +.update-icon.available { + background-image: url('../../../app/theme/update_available.png'); +} +.update-icon.up-to-date { + background-image: url('../../../app/theme/update_uptodate.png'); +} diff --git a/chrome/browser/resources/options2/about_page.html b/chrome/browser/resources/options2/about_page.html new file mode 100644 index 0000000..3ed27e1 --- /dev/null +++ b/chrome/browser/resources/options2/about_page.html @@ -0,0 +1,119 @@ +<div id="aboutPage" class="page" hidden> + <h1 i18n-content="product"></h1> + <div id="aboutPageLessInfo"> + <section> + <div> + <!-- White space is significant between spans. --> + <div> + <span i18n-content="browser"></span> + <span i18n-content="browser_version"></span> + </div> + <div> +<if expr="not pp_ifdef('chromeos')"> + <span i18n-content="os"></span> <span id="osVersion0"> +</if> +<if expr="pp_ifdef('chromeos')"> + <span i18n-content="platform"></span> <span id="osVersion0"> +</if> + <span class="loading" i18n-content="loading"></span></span></div> + <div><span i18n-content="firmware"></span> <span id="osFirmware0"> + <span class="loading" i18n-content="loading"></span></span></div> + <div> + <button id="moreInfoButton" class="link-button" + i18n-content="more_info"></button> + </div> + </div> + </section> + </div> + <div id="aboutPageMoreInfo" hidden> + <section> + <h3 i18n-content="channel"></h3> + <div> + <select id="channelSelect"> + <option value="stable-channel" i18n-content="stable"></option> + <option value="beta-channel" i18n-content="beta"></option> + <option value="dev-channel" i18n-content="dev"></option> + </select> + <div id="channelWarningBlock" hidden> + <div id="channelWarning" i18n-content="channel_warning_header"></div> + <div i18n-content="channel_warning_text"></div> + </div> + </div> + </section> + <section> + <h3 i18n-content="browser"></h3> + <div i18n-content="browser_version"></div> + </section> + <section> +<if expr="not pp_ifdef('chromeos')"> + <h3 i18n-content="os"></h3> +</if> +<if expr="pp_ifdef('chromeos')"> + <h3 i18n-content="platform"></h3> +</if> + <div id="osVersion1"> + <span class="loading" i18n-content="loading"></span> + </div> + </section> + <section> + <h3 i18n-content="firmware"></h3> + <div id="osFirmware1"> + <span class="loading" i18n-content="loading"></span> + </section> + <section> + <h3>WebKit</h3> + <div i18n-content="webkit_version"></div> + </section> + <section> + <h3 i18n-content="js_engine"></h3> + <div i18n-content="js_engine_version"></div> + </section> + <section> + <h3 i18n-content="user_agent"></h3> + <div i18n-content="user_agent_info"></div> + </section> + <section> + <h3 i18n-content="command_line"></h3> + <div i18n-content="command_line_info"></div> + </section> + </div> + <section> + <div> + <div i18n-content="copyright"></div> + <div> + <!-- Odd formatting to avoid unwanted spaces between elements. --> + <span i18n-content="license_content_0"> + </span><a target="_blank" + i18n-values="href:license_link_0" + i18n-content="license_link_content_0"> + </a><span i18n-content="license_content_1"> + </span><a target="_blank" + i18n-values="href:license_link_1" + i18n-content="license_link_content_1"> + </a><span i18n-content="license_content_2"> + </span> + </div> + <div> + <span i18n-content="cros_license_content_0"> + </span><a target="_blank" + i18n-values="href:cros_license_link_0" + i18n-content="cros_license_link_content_0"> + </a><span i18n-content="cros_license_content_1"> + </span> + </div> + </div> + </section> + <section> + <div> + <div> + <div id="updateIcon" class="update-icon up-to-date"></div> + <span id="updateStatus" i18n-content="update_status"></span> + </div> + <div> + <!-- TODO seanparent: fill in last checked. --> + <!-- <span i18n-content="last_check"></span> --> + <button id="checkNow" i18n-content="check_now" disabled></button> + </div> + </div> + </section> +</div> diff --git a/chrome/browser/resources/options2/about_page.js b/chrome/browser/resources/options2/about_page.js new file mode 100644 index 0000000..6f608f0 --- /dev/null +++ b/chrome/browser/resources/options2/about_page.js @@ -0,0 +1,220 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * The number of milliseconds used for showing a message. + * @type {number} + */ + const MESSAGE_DELAY_MS = 1000; // 1 sec. + + /** + * Encapsulated handling of about page. + */ + function AboutPage() { + OptionsPage.call(this, 'about', templateData.aboutPageTabTitle, + 'aboutPage'); + } + + cr.addSingletonGetter(AboutPage); + + AboutPage.prototype = { + // Inherit AboutPage from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * The queue is used for updating the status message with delay, like: + * [["Check for update...", 1000], ["Chrome OS is up to date", 0]] + * @type {!Array.<!Array>} + */ + statusMessageQueue_: [], + + /** + * True if the status message queue flush started. + * @type {boolean} + */ + statusMessageQueueFlushStarted_: false, + + /** + * The selected release channel. + * @type {string} + */ + selectedChannel_: '', + + // Initialize AboutPage. + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + $('checkNow').onclick = function(event) { + chrome.send('CheckNow'); + }; + + $('moreInfoButton').onclick = function(event) { + $('aboutPageLessInfo').hidden = true; + $('aboutPageMoreInfo').hidden = false; + }; + + if (!AccountsOptions.currentUserIsOwner()) { + $('channelSelect').disabled = true; + } else { + var self = this; + $('channelSelect').onchange = function(event) { + self.selectedOptionOnChange_(event.target.value); + }; + } + + // Notify the handler that the page is ready. + chrome.send('PageReady'); + }, + + // Update the Default Browsers section based on the current state. + updateOSVersion_: function(versionString) { + $('osVersion0').textContent = versionString; + $('osVersion1').textContent = versionString; + }, + + updateOSFirmware_: function(firmwareString) { + $('osFirmware0').textContent = firmwareString; + $('osFirmware1').textContent = firmwareString; + }, + + /** + * Updates the status message like "Checking for update...". + * @param {string} message The message to be shown. + * @param {boolean} insertDelay show the message for a while. + * @private + */ + updateStatus_: function(message, insertDelay) { + // Add the message to the queue with delay if needed. + // The delay is inserted so users can read the message. + var delayMs = insertDelay ? MESSAGE_DELAY_MS : 0; + this.statusMessageQueue_.push([message, delayMs]); + // Start the periodic flusher if not started. + if (this.statusMessageQueueFlushStarted_ == false) { + this.flushStatusMessageQueuePeriodically_(); + } + }, + + /** + * Flushes the status message queue periodically using a timer. + * @private + */ + flushStatusMessageQueuePeriodically_: function() { + // Stop the periodic flusher if the queue becomes empty. + if (this.statusMessageQueue_.length == 0) { + this.statusMessageQueueFlushStarted_ = false; + return; + } + this.statusMessageQueueFlushStarted_ = true; + + // Update the status message. + var pair = this.statusMessageQueue_.shift(); + var message = pair[0]; + var delayMs = pair[1]; + $('updateStatus').textContent = message; + + // Schedule the next flush with delay as needed. + var self = this; + window.setTimeout( + function() { self.flushStatusMessageQueuePeriodically_() }, + delayMs); + }, + + updateEnable_: function(enable) { + $('checkNow').disabled = !enable; + }, + + setReleaseChannel_: function(channel) { + // Write the value into the pref which will end up in the policy. + // Eventually, the update engine will use the policy value as the + // source truth for the update channel (see http://crosbug/17015). + Preferences.setStringPref("cros.system.releaseChannel", channel); + this.selectedChannel_ = channel; + chrome.send('SetReleaseTrack', [channel]); + }, + + selectedOptionOnChange_: function(value) { + if (value == 'dev-channel') { + // Open confirm dialog. + var self = this; + AlertOverlay.show( + localStrings.getString('channel_warning_header'), + localStrings.getString('channel_warning_text'), + localStrings.getString('ok'), + localStrings.getString('cancel'), + function() { + // Ok, so set release track and update selected channel. + $('channelWarningBlock').hidden = false; + self.setReleaseChannel_(value); }, + function() { + // Cancel, so switch back to previous selected channel. + self.updateSelectedOption_(self.selectedChannel_); } + ); + } else { + $('channelWarningBlock').hidden = true; + this.setReleaseChannel_(value); + } + }, + + // Updates the selected option in 'channelSelect' <select> element. + updateSelectedOption_: function(value) { + var options = $('channelSelect').querySelectorAll('option'); + for (var i = 0; i < options.length; i++) { + var option = options[i]; + if (option.value == value) { + option.selected = true; + this.selectedChannel_ = value; + } + } + if (value == 'dev-channel') + $('channelWarningBlock').hidden = false; + }, + + // Changes the "check now" button to "restart now" button. + changeToRestartButton_: function() { + $('checkNow').textContent = localStrings.getString('restart_now'); + $('checkNow').disabled = false; + $('checkNow').onclick = function(event) { + chrome.send('RestartNow'); + }; + }, + }; + + AboutPage.updateOSVersionCallback = function(versionString) { + AboutPage.getInstance().updateOSVersion_(versionString); + }; + + AboutPage.updateOSFirmwareCallback = function(firmwareString) { + AboutPage.getInstance().updateOSFirmware_(firmwareString); + }; + + AboutPage.updateStatusCallback = function(message, insertDelay) { + AboutPage.getInstance().updateStatus_(message, insertDelay); + }; + + AboutPage.updateEnableCallback = function(enable) { + AboutPage.getInstance().updateEnable_(enable); + }; + + AboutPage.updateSelectedOptionCallback = function(value) { + AboutPage.getInstance().updateSelectedOption_(value); + }; + + AboutPage.setUpdateImage = function(state) { + $('updateIcon').className= 'update-icon ' + state; + }; + + AboutPage.changeToRestartButton = function() { + AboutPage.getInstance().changeToRestartButton_(); + }; + + // Export + return { + AboutPage: AboutPage + }; + +}); diff --git a/chrome/browser/resources/options2/advanced_options.css b/chrome/browser/resources/options2/advanced_options.css new file mode 100644 index 0000000..bf53be2 --- /dev/null +++ b/chrome/browser/resources/options2/advanced_options.css @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#advancedPage .section-group:not(:first-child) { + margin-top: 10px; +} + +#advancedPage .section-group:not(:last-child) { + margin-bottom: 10px; +} + +#advancedPage select, +#advancedPage .web-content-select-label { + min-width: 145px; +} + +#advancedPage .web-content-select-label > span:only-of-type { + display: inline-block; + min-width: 100px; +} + +#download-location-group { + margin: 10px 0 5px; + min-width: 470px; +} + +#fontSettingsCustomizeFontsButton, +#privacyClearDataButton { + margin-left: 10px; +} diff --git a/chrome/browser/resources/options2/advanced_options.html b/chrome/browser/resources/options2/advanced_options.html new file mode 100644 index 0000000..525eb99 --- /dev/null +++ b/chrome/browser/resources/options2/advanced_options.html @@ -0,0 +1,232 @@ +<div id="advancedPage" class="page" hidden> + <h1 i18n-content="advancedPage"></h1> + <div class="displaytable"> + <section> + <h3 i18n-content="advancedSectionTitlePrivacy"></h3> + <div> + <div class="section-group"> + <button id="privacyContentSettingsButton" + i18n-content="privacyContentSettingsButton"></button> + <button id="privacyClearDataButton" + i18n-content="privacyClearDataButton"></button> + </div> + <div i18n-content="improveBrowsingExperience" + class="informational-text"> + </div> + <div> + <span i18n-content="disableWebServices" + class="informational-text"> + </span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:privacyLearnMoreURL"></a> + </div> + <div class="checkbox"> + <label> + <input id="alternateErrorPagesEnabled" + pref="alternate_error_pages.enabled" + metric="Options_LinkDoctorCheckbox" type="checkbox"> + <span i18n-content="linkDoctorPref"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="searchSuggestEnabled" pref="search.suggest_enabled" + metric="Options_UseSuggestCheckbox" type="checkbox"> + <span i18n-content="suggestPref"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="dnsPrefetchingEnabled" pref="dns_prefetching.enabled" + metric="Options_DnsPrefetchCheckbox" type="checkbox"> + <span i18n-content="networkPredictionEnabledDescription"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="safeBrowsingEnabled" pref="safebrowsing.enabled" + metric="Options_SafeBrowsingCheckbox" type="checkbox"> + <span i18n-content="safeBrowsingEnableProtection"></span> + </label> + </div> +<if expr="pp_ifdef('_google_chrome') and pp_ifdef('chromeos')"> + <div id="metricsReportingSetting" class="checkbox"> + <label> + <input id="metricsReportingEnabled" + pref="cros.metrics.reportingEnabled" type="checkbox"> + <span id="metricsReportingEnabledText" + i18n-content="enableLogging"> + </span> + </label> + </div> +</if> +<if expr="pp_ifdef('_google_chrome') and not pp_ifdef('chromeos')"> + <div id="metricsReportingSetting" class="checkbox"> + <label> + <input id="metricsReportingEnabled" type="checkbox"> + <span i18n-content="enableLogging"></span> + </label> + </div> +</if> + </div> + </section> + <section> + <h3 i18n-content="advancedSectionTitleContent"></h3> + <div> + <div class="section-group"> + <label class="web-content-select-label"> + <span i18n-content="defaultFontSizeLabel"></span> + <select id="defaultFontSize"> + <option value="9" i18n-content="fontSizeLabelVerySmall"> + </option> + <option value="12" i18n-content="fontSizeLabelSmall"></option> + <option value="16" i18n-content="fontSizeLabelMedium"></option> + <option value="20" i18n-content="fontSizeLabelLarge"></option> + <option value="24" i18n-content="fontSizeLabelVeryLarge"> + </option> + </select> + </label> + <button id="fontSettingsCustomizeFontsButton" + i18n-content="fontSettingsCustomizeFontsButton"></button> + </div> + <div class="section-group"> + <label class="web-content-select-label"> + <span i18n-content="defaultZoomFactorLabel"></span> + <select id="defaultZoomFactor" dataType="double"></select> + </label> + </div> +<if expr="not pp_ifdef('chromeos') or os == 'win32'"> + <div class="section-group"> +</if> +<if expr="not pp_ifdef('chromeos')"> + <button id="language-button" + i18n-content="languageAndSpellCheckSettingsButton"></button> +</if> +<if expr="not pp_ifdef('chromeos') or os == 'win32'"> + </div> +</if> +<if expr="os == 'darwin'"> + <div class="checkbox"> + <label> + <input id="tabsToLinksPref" pref="webkit.webprefs.tabs_to_links" + metric="Options_TabsToLinks" type="checkbox"> + <span i18n-content="tabsToLinksPref"></span> + </label> + </div> +</if> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="advancedSectionTitleNetwork"></h3> + <div> + <div id="proxiesLabel"></div> + <div class="section-group"> + <button id="proxiesConfigureButton" + i18n-content="proxiesConfigureButton"></button> + </div> + </div> + </section> +</if> + <section> + <h3 i18n-content="advancedSectionTitleTranslate"></h3> + <div class="checkbox"> + <label> + <input id="enableTranslate" pref="translate.enabled" + metric="Options_Translate" type="checkbox"> + <span i18n-content="translateEnableTranslate"></span> + </label> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="downloadLocationGroupName"></h3> + <div> + <div id="download-location-group"> + <label> + <span i18n-content="downloadLocationBrowseTitle"></span> + <input id="downloadLocationPath" class="weakrtl" type="text" + pref="download.default_directory" size="36"> + </label> + <button id="downloadLocationChangeButton" + pref="download.prompt_for_download" + i18n-content="downloadLocationChangeButton"></button> + </div> + <div class="checkbox"> + <label> + <input type="checkbox" + pref="download.prompt_for_download" + metric="Options_AskForSaveLocation"> + <span i18n-content="downloadLocationAskForSaveLocation"></span> + </label> + </div> + <div id="auto-open-file-types-label" + i18n-content="autoOpenFileTypesInfo"></div> + <div class="section-group"> + <button id="autoOpenFileTypesResetToDefault" + i18n-content="autoOpenFileTypesResetToDefault"></button> + </div> + </div> + </section> +</if> + <section> + <h3 i18n-content="advancedSectionTitleSecurity"></h3> + <div> + <div class="section-group"> + <button id="certificatesManageButton" + i18n-content="certificatesManageButton"></button> + </div> + <div class="checkbox"> + <label> + <input id="sslCheckRevocation" type="checkbox"> + <span i18n-content="sslCheckRevocation"></span> + </label> + </div> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section id="cloud-print-proxy-section"> + <h3 i18n-content="advancedSectionTitleCloudPrint"></h3> + <div> + <div id="cloudPrintProxyLabel" + i18n-content="cloudPrintProxyDisabledLabel"></div> + <div class="section-group"> + <button id="cloudPrintProxySetupButton" + i18n-content="cloudPrintProxyDisabledButton"></button> + <button id="cloudPrintProxyManageButton" + i18n-content="cloudPrintProxyEnabledManageButton"></button> + </div> + </div> + </section> +</if> +<if expr="pp_ifdef('chromeos')"> + <section id="cloud-print-proxy-section"> + <h3 i18n-content="advancedSectionTitleCloudPrint"></h3> + <div> + <div> + <span i18n-content="cloudPrintChromeosOptionLabel" + class="informational-text"> + </span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:cloudPrintLearnMoreURL"></a> + </div> + <div class="section-group"> + <button id="cloudPrintProxyManageButton" + i18n-content="cloudPrintChromeosOptionButton"></button> + </div> + </div> + </section> +</if> +<if expr="os != 'darwin' and not pp_ifdef('chromeos')"> + <section id="background-section"> + <h3 i18n-content="advancedSectionTitleBackground"></h3> + <div class="checkbox"> + <label> + <input id="backgroundModeCheckbox" type="checkbox"> + <span i18n-content="backgroundModeCheckbox"></span> + </label> + </div> + </section> +</if> + </div> +</div> diff --git a/chrome/browser/resources/options2/advanced_options.js b/chrome/browser/resources/options2/advanced_options.js new file mode 100644 index 0000000..fb1a809 --- /dev/null +++ b/chrome/browser/resources/options2/advanced_options.js @@ -0,0 +1,266 @@ +// 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. + +cr.define('options', function() { + +var OptionsPage = options.OptionsPage; + + // + // AdvancedOptions class + // Encapsulated handling of advanced options page. + // + function AdvancedOptions() { + OptionsPage.call(this, 'advanced', templateData.advancedPageTabTitle, + 'advancedPage'); + } + + cr.addSingletonGetter(AdvancedOptions); + + AdvancedOptions.prototype = { + // Inherit AdvancedOptions from OptionsPage. + __proto__: options.OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up click handlers for buttons. + $('privacyContentSettingsButton').onclick = function(event) { + OptionsPage.navigateToPage('content'); + OptionsPage.showTab($('cookies-nav-tab')); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ContentSettings']); + }; + $('privacyClearDataButton').onclick = function(event) { + OptionsPage.navigateToPage('clearBrowserData'); + chrome.send('coreOptionsUserMetricsAction', ['Options_ClearData']); + }; + + // 'metricsReportingEnabled' element is only present on Chrome branded + // builds. + if ($('metricsReportingEnabled')) { + $('metricsReportingEnabled').onclick = function(event) { + chrome.send('metricsReportingCheckboxAction', + [String(event.target.checked)]); + }; + } + + if (!cr.isChromeOS) { + $('autoOpenFileTypesResetToDefault').onclick = function(event) { + chrome.send('autoOpenFileTypesAction'); + }; + } + + $('fontSettingsCustomizeFontsButton').onclick = function(event) { + OptionsPage.navigateToPage('fonts'); + chrome.send('coreOptionsUserMetricsAction', ['Options_FontSettings']); + }; + $('defaultFontSize').onchange = function(event) { + chrome.send('defaultFontSizeAction', + [String(event.target.options[event.target.selectedIndex].value)]); + }; + $('defaultZoomFactor').onchange = function(event) { + chrome.send('defaultZoomFactorAction', + [String(event.target.options[event.target.selectedIndex].value)]); + }; + + $('language-button').onclick = function(event) { + OptionsPage.navigateToPage('languages'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_LanuageAndSpellCheckSettings']); + }; + + if (cr.isWindows || cr.isMac) { + $('certificatesManageButton').onclick = function(event) { + chrome.send('showManageSSLCertificates'); + }; + } else { + $('certificatesManageButton').onclick = function(event) { + OptionsPage.navigateToPage('certificates'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ManageSSLCertificates']); + }; + } + + if (!cr.isChromeOS) { + $('proxiesConfigureButton').onclick = function(event) { + chrome.send('showNetworkProxySettings'); + }; + $('downloadLocationChangeButton').onclick = function(event) { + chrome.send('selectDownloadLocation'); + }; + // This text field is always disabled. Setting ".disabled = true" isn't + // enough, since a policy can disable it but shouldn't re-enable when + // it is removed. + $('downloadLocationPath').setDisabled('readonly', true); + } + + $('sslCheckRevocation').onclick = function(event) { + chrome.send('checkRevocationCheckboxAction', + [String($('sslCheckRevocation').checked)]); + }; + + if ($('backgroundModeCheckbox')) { + $('backgroundModeCheckbox').onclick = function(event) { + chrome.send('backgroundModeAction', + [String($('backgroundModeCheckbox').checked)]); + }; + } + + // 'cloudPrintProxyEnabled' is true for Chrome branded builds on + // certain platforms, or could be enabled by a lab. + if (!cr.isChromeOS) { + $('cloudPrintProxySetupButton').onclick = function(event) { + if ($('cloudPrintProxyManageButton').style.display == 'none') { + // Disable the button, set it's text to the intermediate state. + $('cloudPrintProxySetupButton').textContent = + localStrings.getString('cloudPrintProxyEnablingButton'); + $('cloudPrintProxySetupButton').disabled = true; + chrome.send('showCloudPrintSetupDialog'); + } else { + chrome.send('disableCloudPrintProxy'); + } + }; + } + $('cloudPrintProxyManageButton').onclick = function(event) { + chrome.send('showCloudPrintManagePage'); + }; + + } + }; + + // + // Chrome callbacks + // + + // Set the checked state of the metrics reporting checkbox. + AdvancedOptions.SetMetricsReportingCheckboxState = function( + checked, disabled) { + $('metricsReportingEnabled').checked = checked; + $('metricsReportingEnabled').disabled = disabled; + if (disabled) + $('metricsReportingEnabledText').className = 'disable-services-span'; + } + + AdvancedOptions.SetMetricsReportingSettingVisibility = function(visible) { + if (visible) { + $('metricsReportingSetting').style.display = 'block'; + } else { + $('metricsReportingSetting').style.display = 'none'; + } + } + + // Set the font size selected item. + AdvancedOptions.SetFontSize = function(font_size_value) { + var selectCtl = $('defaultFontSize'); + for (var i = 0; i < selectCtl.options.length; i++) { + if (selectCtl.options[i].value == font_size_value) { + selectCtl.selectedIndex = i; + if ($('Custom')) + selectCtl.remove($('Custom').index); + return; + } + } + + // Add/Select Custom Option in the font size label list. + if (!$('Custom')) { + var option = new Option(localStrings.getString('fontSizeLabelCustom'), + -1, false, true); + option.setAttribute("id", "Custom"); + selectCtl.add(option); + } + $('Custom').selected = true; + }; + + /** + * Populate the page zoom selector with values received from the caller. + * @param {Array} items An array of items to populate the selector. + * each object is an array with three elements as follows: + * 0: The title of the item (string). + * 1: The value of the item (number). + * 2: Whether the item should be selected (boolean). + */ + AdvancedOptions.SetupPageZoomSelector = function(items) { + var element = $('defaultZoomFactor'); + + // Remove any existing content. + element.textContent = ''; + + // Insert new child nodes into select element. + var value, title, selected; + for (var i = 0; i < items.length; i++) { + title = items[i][0]; + value = items[i][1]; + selected = items[i][2]; + element.appendChild(new Option(title, value, false, selected)); + } + }; + + // Set the enabled state for the autoOpenFileTypesResetToDefault button. + AdvancedOptions.SetAutoOpenFileTypesDisabledAttribute = function(disabled) { + if (!cr.isChromeOS) { + $('autoOpenFileTypesResetToDefault').disabled = disabled; + + if (disabled) + $('auto-open-file-types-label').classList.add('disabled'); + else + $('auto-open-file-types-label').classList.remove('disabled'); + } + }; + + // Set the enabled state for the proxy settings button. + AdvancedOptions.SetupProxySettingsSection = function(disabled, label) { + if (!cr.isChromeOS) { + $('proxiesConfigureButton').disabled = disabled; + $('proxiesLabel').textContent = label; + } + }; + + // Set the checked state for the sslCheckRevocation checkbox. + AdvancedOptions.SetCheckRevocationCheckboxState = function( + checked, disabled) { + $('sslCheckRevocation').checked = checked; + $('sslCheckRevocation').disabled = disabled; + }; + + // Set the checked state for the backgroundModeCheckbox element. + AdvancedOptions.SetBackgroundModeCheckboxState = function(checked) { + $('backgroundModeCheckbox').checked = checked; + }; + + // Set the Cloud Print proxy UI to enabled, disabled, or processing. + AdvancedOptions.SetupCloudPrintProxySection = function( + disabled, label, allowed) { + if (!cr.isChromeOS) { + $('cloudPrintProxyLabel').textContent = label; + if (disabled || !allowed) { + $('cloudPrintProxySetupButton').textContent = + localStrings.getString('cloudPrintProxyDisabledButton'); + $('cloudPrintProxyManageButton').style.display = 'none'; + } else { + $('cloudPrintProxySetupButton').textContent = + localStrings.getString('cloudPrintProxyEnabledButton'); + $('cloudPrintProxyManageButton').style.display = 'inline'; + } + $('cloudPrintProxySetupButton').disabled = !allowed; + } + }; + + AdvancedOptions.RemoveCloudPrintProxySection = function() { + if (!cr.isChromeOS) { + var proxySectionElm = $('cloud-print-proxy-section'); + if (proxySectionElm) + proxySectionElm.parentNode.removeChild(proxySectionElm); + } + }; + + // Export + return { + AdvancedOptions: AdvancedOptions + }; + +}); diff --git a/chrome/browser/resources/options2/alert_overlay.css b/chrome/browser/resources/options2/alert_overlay.css new file mode 100644 index 0000000..de5c236 --- /dev/null +++ b/chrome/browser/resources/options2/alert_overlay.css @@ -0,0 +1,3 @@ +#alertOverlayMessage { + width: 400px; +} diff --git a/chrome/browser/resources/options2/alert_overlay.html b/chrome/browser/resources/options2/alert_overlay.html new file mode 100644 index 0000000..3de00e7 --- /dev/null +++ b/chrome/browser/resources/options2/alert_overlay.html @@ -0,0 +1,12 @@ +<div id="alertOverlay" class="page" hidden> + <h1 id="alertOverlayTitle"></h1> + <div class="content-area"> + <div id="alertOverlayMessage"></div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="alertOverlayCancel" type="reset"></button> + <button id="alertOverlayOk" type="submit"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/alert_overlay.js b/chrome/browser/resources/options2/alert_overlay.js new file mode 100644 index 0000000..eb1fb8e --- /dev/null +++ b/chrome/browser/resources/options2/alert_overlay.js @@ -0,0 +1,144 @@ +// Copyright (c) 2010 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('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * AlertOverlay class + * Encapsulated handling of a generic alert. + * @class + */ + function AlertOverlay() { + OptionsPage.call(this, 'alertOverlay', '', 'alertOverlay'); + } + + cr.addSingletonGetter(AlertOverlay); + + AlertOverlay.prototype = { + // Inherit AlertOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Whether the page can be shown. Used to make sure the page is only + * shown via AlertOverlay.Show(), and not via the address bar. + * @private + */ + canShow_: false, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('alertOverlayOk').onclick = function(event) { + self.handleOK_(); + }; + + $('alertOverlayCancel').onclick = function(event) { + self.handleCancel_(); + }; + }, + + /** + * Handle the 'ok' button. Clear the overlay and call the ok callback if + * available. + * @private + */ + handleOK_: function() { + OptionsPage.closeOverlay(); + if (this.okCallback != undefined) { + this.okCallback.call(); + } + }, + + /** + * Handle the 'cancel' button. Clear the overlay and call the cancel + * callback if available. + * @private + */ + handleCancel_: function() { + OptionsPage.closeOverlay(); + if (this.cancelCallback != undefined) { + this.cancelCallback.call(); + } + }, + + /** + * The page is getting hidden. Don't let it be shown again. + */ + willHidePage: function() { + canShow_ = false; + }, + + /** @inheritDoc */ + canShowPage: function() { + return this.canShow_; + }, + }; + + /** + * Show an alert overlay with the given message, button titles, and + * callbacks. + * @param {string} title The alert title to display to the user. + * @param {string} message The alert message to display to the user. + * @param {string} okTitle The title of the OK button. If undefined or empty, + * no button is shown. + * @param {string} cancelTitle The title of the cancel button. If undefined or + * empty, no button is shown. + * @param {function} okCallback A function to be called when the user presses + * the ok button. The alert window will be closed automatically. Can be + * undefined. + * @param {function} cancelCallback A function to be called when the user + * presses the cancel button. The alert window will be closed + * automatically. Can be undefined. + */ + AlertOverlay.show = function( + title, message, okTitle, cancelTitle, okCallback, cancelCallback) { + if (title != undefined) { + $('alertOverlayTitle').textContent = title; + $('alertOverlayTitle').style.display = 'block'; + } else { + $('alertOverlayTitle').style.display = 'none'; + } + + if (message != undefined) { + $('alertOverlayMessage').textContent = message; + $('alertOverlayMessage').style.display = 'block'; + } else { + $('alertOverlayMessage').style.display = 'none'; + } + + if (okTitle != undefined && okTitle != '') { + $('alertOverlayOk').textContent = okTitle; + $('alertOverlayOk').style.display = 'block'; + } else { + $('alertOverlayOk').style.display = 'none'; + } + + if (cancelTitle != undefined && cancelTitle != '') { + $('alertOverlayCancel').textContent = cancelTitle; + $('alertOverlayCancel').style.display = 'inline'; + } else { + $('alertOverlayCancel').style.display = 'none'; + } + + var alertOverlay = AlertOverlay.getInstance(); + alertOverlay.okCallback = okCallback; + alertOverlay.cancelCallback = cancelCallback; + alertOverlay.canShow_ = true; + + // Intentionally don't show the URL in the location bar as we don't want + // people trying to navigate here by hand. + OptionsPage.showPageByName('alertOverlay', false); + } + + // Export + return { + AlertOverlay: AlertOverlay + }; +}); diff --git a/chrome/browser/resources/options2/autocomplete_list.js b/chrome/browser/resources/options2/autocomplete_list.js new file mode 100644 index 0000000..2aeb195 --- /dev/null +++ b/chrome/browser/resources/options2/autocomplete_list.js @@ -0,0 +1,239 @@ +// 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. + +cr.define('options', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + + /** + * Creates a new autocomplete list item. + * @param {Object} pageInfo The page this item represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function AutocompleteListItem(pageInfo) { + var el = cr.doc.createElement('div'); + el.pageInfo_ = pageInfo; + AutocompleteListItem.decorate(el); + return el; + } + + /** + * Decorates an element as an autocomplete list item. + * @param {!HTMLElement} el The element to decorate. + */ + AutocompleteListItem.decorate = function(el) { + el.__proto__ = AutocompleteListItem.prototype; + el.decorate(); + }; + + AutocompleteListItem.prototype = { + __proto__: ListItem.prototype, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + var title = this.pageInfo_['title']; + var url = this.pageInfo_['displayURL']; + var titleEl = this.ownerDocument.createElement('span'); + titleEl.className = 'title'; + titleEl.textContent = title || url; + this.appendChild(titleEl); + + if (title && title.length > 0 && url != title) { + var separatorEl = this.ownerDocument.createTextNode(' - '); + this.appendChild(separatorEl); + + var urlEl = this.ownerDocument.createElement('span'); + urlEl.className = 'url'; + urlEl.textContent = url; + this.appendChild(urlEl); + } + }, + }; + + /** + * Creates a new autocomplete list popup. + * @constructor + * @extends {cr.ui.List} + */ + var AutocompleteList = cr.ui.define('list'); + + AutocompleteList.prototype = { + __proto__: List.prototype, + + /** + * The text field the autocomplete popup is currently attached to, if any. + * @type {HTMLElement} + * @private + */ + targetInput_: null, + + /** + * Keydown event listener to attach to a text field. + * @type {Function} + * @private + */ + textFieldKeyHandler_: null, + + /** + * Input event listener to attach to a text field. + * @type {Function} + * @private + */ + textFieldInputHandler_: null, + + /** + * A function to call when new suggestions are needed. + * @type {Function} + * @private + */ + suggestionUpdateRequestCallback_: null, + + /** @inheritDoc */ + decorate: function() { + List.prototype.decorate.call(this); + this.classList.add('autocomplete-suggestions'); + this.selectionModel = new cr.ui.ListSingleSelectionModel; + + this.textFieldKeyHandler_ = this.handleAutocompleteKeydown_.bind(this); + var self = this; + this.textFieldInputHandler_ = function(e) { + if (self.suggestionUpdateRequestCallback_) + self.suggestionUpdateRequestCallback_(self.targetInput_.value); + }; + this.addEventListener('change', function(e) { + var input = self.targetInput; + if (!input || !self.selectedItem) + return; + input.value = self.selectedItem['url']; + // Programatically change the value won't trigger a change event, but + // clients are likely to want to know when changes happen, so fire one. + var changeEvent = document.createEvent('Event'); + changeEvent.initEvent('change', true, true); + input.dispatchEvent(changeEvent); + }); + // Start hidden; adding suggestions will unhide. + this.hidden = true; + }, + + /** @inheritDoc */ + createItem: function(pageInfo) { + return new AutocompleteListItem(pageInfo); + }, + + /** + * The suggestions to show. + * @type {Array} + */ + set suggestions(suggestions) { + this.dataModel = new ArrayDataModel(suggestions); + this.hidden = !this.targetInput_ || suggestions.length == 0; + }, + + /** + * A function to call when the attached input field's contents change. + * The function should take one string argument, which will be the text + * to autocomplete from. + * @type {Function} + */ + set suggestionUpdateRequestCallback(callback) { + this.suggestionUpdateRequestCallback_ = callback; + }, + + /** + * Attaches the popup to the given input element. Requires + * that the input be wrapped in a block-level container of the same width. + * @param {HTMLElement} input The input element to attach to. + */ + attachToInput: function(input) { + if (this.targetInput_ == input) + return; + + this.detach(); + this.targetInput_ = input; + this.style.width = input.getBoundingClientRect().width + 'px'; + this.hidden = false; // Necessary for positionPopupAroundElement to work. + cr.ui.positionPopupAroundElement(input, this, cr.ui.AnchorType.BELOW) + // Start hidden; when the data model gets results the list will show. + this.hidden = true; + + input.addEventListener('keydown', this.textFieldKeyHandler_, true); + input.addEventListener('input', this.textFieldInputHandler_); + }, + + /** + * Detaches the autocomplete popup from its current input element, if any. + */ + detach: function() { + var input = this.targetInput_ + if (!input) + return; + + input.removeEventListener('keydown', this.textFieldKeyHandler_); + input.removeEventListener('input', this.textFieldInputHandler_); + this.targetInput_ = null; + this.suggestions = []; + }, + + /** + * Makes sure that the suggestion list matches the width of the input it is. + * attached to. Should be called any time the input is resized. + */ + syncWidthToInput: function() { + var input = this.targetInput_ + if (input) + this.style.width = input.getBoundingClientRect().width + 'px'; + }, + + /** + * The text field the autocomplete popup is currently attached to, if any. + * @return {HTMLElement} + */ + get targetInput() { + return this.targetInput_; + }, + + /** + * Handles input field key events that should be interpreted as autocomplete + * commands. + * @param {Event} event The keydown event. + * @private + */ + handleAutocompleteKeydown_: function(event) { + if (this.hidden) + return; + var handled = false; + switch (event.keyIdentifier) { + case 'U+001B': // Esc + this.suggestions = []; + handled = true; + break; + case 'Enter': + var hadSelection = this.selectedItem != null; + this.suggestions = []; + // Only count the event as handled if a selection is being commited. + handled = hadSelection; + break; + case 'Up': + case 'Down': + this.dispatchEvent(event); + handled = true; + break; + } + // Don't let arrow keys affect the text field, or bubble up to, e.g., + // an enclosing list item. + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + }, + }; + + return { + AutocompleteList: AutocompleteList + }; +}); diff --git a/chrome/browser/resources/options2/autofill_edit_address_overlay.html b/chrome/browser/resources/options2/autofill_edit_address_overlay.html new file mode 100644 index 0000000..43a13c7 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_edit_address_overlay.html @@ -0,0 +1,99 @@ +<div id="autofill-edit-address-overlay" class="page" hidden> + <h1 id="autofill-address-title"></h1> + <div class="content-area"> + <div id="autofill-name-labels"> + <label for="first-name"> + <span i18n-content="autofillFirstNameLabel"></span> + </label> + <label for="middle-name"> + <span i18n-content="autofillMiddleNameLabel"></span> + </label> + <label for="last-name"> + <span i18n-content="autofillLastNameLabel"></span> + </label> + </div> + <list id="full-name-list"></list> + <div class="input"> + <label> + <div><span i18n-content="autofillCompanyNameLabel"></span></div> + <input id="company-name" type="text" class="autofill-form"> + </label> + </div> + <div class="input"> + <label> + <div><span i18n-content="autofillAddrLine1Label"></span></div> + <input id="addr-line-1" type="text" class="autofill-form"> + </label> + </div> + <div class="input"> + <label> + <div><span i18n-content="autofillAddrLine2Label"></span></div> + <input id="addr-line-2" type="text" class="autofill-form"> + </label> + </div> + <div class="table"> + <div class="row"> + <div class="input cell"> + <label for="city"> + <span i18n-content="autofillCityLabel"></span> + </label> + </div> + <div class="input cell"> + <label id="state-label" for="state"></label> + </div> + <div class="input cell"> + <label id="postal-code-label" for="postal-code"></label> + </div> + </div> + <div class="row"> + <div class="input cell"> + <input id="city" type="text" class="autofill-form"> + </div> + <div class="input cell"> + <input id="state" type="text" class="autofill-form"> + </div> + <div class="input cell"> + <input id="postal-code" type="text" class="autofill-form"> + </div> + </div> + </div> + <div class="input"> + <label> + <div> + <span i18n-content="autofillCountryLabel"></span> + </div> + <select id="country"></select> + </label> + </div> + <div class="table"> + <div class="row"> + <div class="input cell"> + <label for="phone-list"> + <span i18n-content="autofillPhoneLabel"></span> + </label> + </div> + <div class="input cell"> + <label for="email-list"> + <span i18n-content="autofillEmailLabel"></span> + </label> + </div> + </div> + <div class="row"> + <div class="input cell"> + <list id="phone-list" + i18n-values="placeholder:autofillAddPhonePlaceholder"></list> + </div> + <div class="input cell"> + <list id="email-list" + i18n-values="placeholder:autofillAddEmailPlaceholder"></list> + </div> + </div> + </div> + </div> + <div class="action-area button-strip"> + <button id="autofill-edit-address-cancel-button" type="reset" + i18n-content="cancel"></button> + <button id="autofill-edit-address-apply-button" type="submit" + i18n-content="ok" disabled></button> + </div> +</div> diff --git a/chrome/browser/resources/options2/autofill_edit_address_overlay.js b/chrome/browser/resources/options2/autofill_edit_address_overlay.js new file mode 100644 index 0000000..f41fd8c --- /dev/null +++ b/chrome/browser/resources/options2/autofill_edit_address_overlay.js @@ -0,0 +1,325 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + // The GUID of the loaded address. + var guid; + + /** + * AutofillEditAddressOverlay class + * Encapsulated handling of the 'Add Page' overlay page. + * @class + */ + function AutofillEditAddressOverlay() { + OptionsPage.call(this, 'autofillEditAddress', + templateData.autofillEditAddressTitle, + 'autofill-edit-address-overlay'); + } + + cr.addSingletonGetter(AutofillEditAddressOverlay); + + AutofillEditAddressOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createMultiValueLists_(); + + var self = this; + $('autofill-edit-address-cancel-button').onclick = function(event) { + self.dismissOverlay_(); + } + $('autofill-edit-address-apply-button').onclick = function(event) { + self.saveAddress_(); + self.dismissOverlay_(); + } + + self.guid = ''; + self.populateCountryList_(); + self.clearInputFields_(); + self.connectInputEvents_(); + }, + + /** + * Creates, decorates and initializes the multi-value lists for full name, + * phone, and email. + * @private + */ + createMultiValueLists_: function() { + var list = $('full-name-list'); + options.autofillOptions.AutofillNameValuesList.decorate(list); + list.autoExpands = true; + + list = $('phone-list'); + options.autofillOptions.AutofillPhoneValuesList.decorate(list); + list.autoExpands = true; + + list = $('email-list'); + options.autofillOptions.AutofillValuesList.decorate(list); + list.autoExpands = true; + }, + + /** + * Updates the data model for the list named |listName| with the values from + * |entries|. + * @param {String} listName The id of the list. + * @param {Array} entries The list of items to be added to the list. + */ + setMultiValueList_: function(listName, entries) { + // Add data entries. + var list = $(listName); + list.dataModel = new ArrayDataModel(entries); + + // Add special entry for adding new values. + list.dataModel.splice(list.dataModel.length, 0, null); + + // Update the status of the 'OK' button. + this.inputFieldChanged_(); + + var self = this; + list.dataModel.addEventListener( + 'splice', function(event) { self.inputFieldChanged_(); }); + list.dataModel.addEventListener( + 'change', function(event) { self.inputFieldChanged_(); }); + }, + + /** + * Updates the data model for the name list with the values from |entries|. + * @param {Array} names The list of names to be added to the list. + */ + setNameList_: function(names) { + // Add the given |names| as backing data for the list. + var list = $('full-name-list'); + list.dataModel = new ArrayDataModel(names); + + // Add special entry for adding new values. + list.dataModel.splice(list.dataModel.length, 0, null); + + var self = this; + list.dataModel.addEventListener( + 'splice', function(event) { self.inputFieldChanged_(); }); + list.dataModel.addEventListener( + 'change', function(event) { self.inputFieldChanged_(); }); + }, + + /** + * Clears any uncommitted input, resets the stored GUID and dismisses the + * overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + this.guid = ''; + OptionsPage.closeOverlay(); + }, + + /** + * Aggregates the values in the input fields into an array and sends the + * array to the Autofill handler. + * @private + */ + saveAddress_: function() { + var address = new Array(); + address[0] = this.guid; + var list = $('full-name-list'); + address[1] = list.dataModel.slice(0, list.dataModel.length - 1); + address[2] = $('company-name').value; + address[3] = $('addr-line-1').value; + address[4] = $('addr-line-2').value; + address[5] = $('city').value; + address[6] = $('state').value; + address[7] = $('postal-code').value; + address[8] = $('country').value; + list = $('phone-list'); + address[9] = list.dataModel.slice(0, list.dataModel.length - 1); + list = $('email-list'); + address[10] = list.dataModel.slice(0, list.dataModel.length - 1); + + chrome.send('setAddress', address); + }, + + /** + * Connects each input field to the inputFieldChanged_() method that enables + * or disables the 'Ok' button based on whether all the fields are empty or + * not. + * @private + */ + connectInputEvents_: function() { + var self = this; + $('company-name').oninput = $('addr-line-1').oninput = + $('addr-line-2').oninput = $('city').oninput = $('state').oninput = + $('postal-code').oninput = function(event) { + self.inputFieldChanged_(); + } + + $('country').onchange = function(event) { + self.countryChanged_(); + } + }, + + /** + * Checks the values of each of the input fields and disables the 'Ok' + * button if all of the fields are empty. + * @private + */ + inputFieldChanged_: function() { + // Length of lists are tested for <= 1 due to the "add" placeholder item + // in the list. + var disabled = + $('full-name-list').items.length <= 1 && + !$('company-name').value && + !$('addr-line-1').value && !$('addr-line-2').value && + !$('city').value && !$('state').value && !$('postal-code').value && + !$('country').value && $('phone-list').items.length <= 1 && + $('email-list').items.length <= 1; + $('autofill-edit-address-apply-button').disabled = disabled; + }, + + /** + * Updates the postal code and state field labels appropriately for the + * selected country. + * @private + */ + countryChanged_: function() { + var countryCode = $('country').value; + if (!countryCode) + countryCode = templateData.defaultCountryCode; + + var details = templateData.autofillCountryData[countryCode]; + var postal = $('postal-code-label'); + postal.textContent = details['postalCodeLabel']; + $('state-label').textContent = details['stateLabel']; + + // Also update the 'Ok' button as needed. + this.inputFieldChanged_(); + }, + + /** + * Populates the country <select> list. + * @private + */ + populateCountryList_: function() { + var countryData = templateData.autofillCountryData; + var defaultCountryCode = templateData.defaultCountryCode; + + // Build an array of the country names and their corresponding country + // codes, so that we can sort and insert them in order. + var countries = []; + for (var countryCode in countryData) { + var country = { + countryCode: countryCode, + name: countryData[countryCode]['name'] + }; + countries.push(country); + } + + // Sort the countries in alphabetical order by name. + countries = countries.sort(function(a, b) { + return a.name < b.name ? -1 : 1; + }); + + // Insert the empty and default countries at the beginning of the array. + var emptyCountry = { + countryCode: '', + name: '' + }; + var defaultCountry = { + countryCode: defaultCountryCode, + name: countryData[defaultCountryCode]['name'] + }; + var separator = { + countryCode: '', + name: '---', + disabled: true + } + countries.unshift(emptyCountry, defaultCountry, separator); + + // Add the countries to the country <select> list. + var countryList = $('country'); + for (var i = 0; i < countries.length; i++) { + var country = new Option(countries[i].name, countries[i].countryCode); + country.disabled = countries[i].disabled; + countryList.appendChild(country) + } + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + this.setNameList_([]); + $('company-name').value = ''; + $('addr-line-1').value = ''; + $('addr-line-2').value = ''; + $('city').value = ''; + $('state').value = ''; + $('postal-code').value = ''; + $('country').value = ''; + this.setMultiValueList_('phone-list', []); + this.setMultiValueList_('email-list', []); + + this.countryChanged_(); + }, + + /** + * Loads the address data from |address|, sets the input fields based on + * this data and stores the GUID of the address. + * @private + */ + loadAddress_: function(address) { + this.setInputFields_(address); + this.inputFieldChanged_(); + this.guid = address['guid']; + }, + + /** + * Sets the value of each input field according to |address| + * @private + */ + setInputFields_: function(address) { + this.setNameList_(address['fullName']); + $('company-name').value = address['companyName']; + $('addr-line-1').value = address['addrLine1']; + $('addr-line-2').value = address['addrLine2']; + $('city').value = address['city']; + $('state').value = address['state']; + $('postal-code').value = address['postalCode']; + $('country').value = address['country']; + this.setMultiValueList_('phone-list', address['phone']); + this.setMultiValueList_('email-list', address['email']); + + this.countryChanged_(); + }, + }; + + AutofillEditAddressOverlay.clearInputFields = function() { + AutofillEditAddressOverlay.getInstance().clearInputFields_(); + }; + + AutofillEditAddressOverlay.loadAddress = function(address) { + AutofillEditAddressOverlay.getInstance().loadAddress_(address); + }; + + AutofillEditAddressOverlay.setTitle = function(title) { + $('autofill-address-title').textContent = title; + }; + + AutofillEditAddressOverlay.setValidatedPhoneNumbers = function(numbers) { + AutofillEditAddressOverlay.getInstance().setMultiValueList_('phone-list', + numbers); + }; + + // Export + return { + AutofillEditAddressOverlay: AutofillEditAddressOverlay + }; +}); diff --git a/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.html b/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.html new file mode 100644 index 0000000..fa3825a --- /dev/null +++ b/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.html @@ -0,0 +1,30 @@ +<div id="autofill-edit-credit-card-overlay" class="page" hidden> + <h1 id="autofill-credit-card-title"></h1> + <div class="content-area"> + <div class="input"> + <label> + <span i18n-content="nameOnCardLabel"></span><br> + <input id="name-on-card" type="text"> + </label> + </div> + <div class="input"> + <label> + <span i18n-content="creditCardNumberLabel"></span><br> + <input id="credit-card-number" type="text"> + </label> + </div> + <div class="input"> + <label> + <span i18n-content="creditCardExpirationDateLabel"></span><br> + <select id="expiration-month"></select> + <select id="expiration-year"></select> + </label> + </div> + </div> + <div class="action-area button-strip"> + <button id="autofill-edit-credit-card-cancel-button" type="reset" + i18n-content="cancel"></button> + <button id="autofill-edit-credit-card-apply-button" type="submit" + i18n-content="ok" disabled></button> + </div> +</div> diff --git a/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.js b/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.js new file mode 100644 index 0000000..8402a10 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_edit_creditcard_overlay.js @@ -0,0 +1,205 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + // The GUID of the loaded credit card. + var guid_; + + /** + * AutofillEditCreditCardOverlay class + * Encapsulated handling of the 'Add Page' overlay page. + * @class + */ + function AutofillEditCreditCardOverlay() { + OptionsPage.call(this, 'autofillEditCreditCard', + templateData.autofillEditCreditCardTitle, + 'autofill-edit-credit-card-overlay'); + } + + cr.addSingletonGetter(AutofillEditCreditCardOverlay); + + AutofillEditCreditCardOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('autofill-edit-credit-card-cancel-button').onclick = function(event) { + self.dismissOverlay_(); + } + $('autofill-edit-credit-card-apply-button').onclick = function(event) { + self.saveCreditCard_(); + self.dismissOverlay_(); + } + + self.guid_ = ''; + self.hasEditedNumber_ = false; + self.clearInputFields_(); + self.connectInputEvents_(); + self.setDefaultSelectOptions_(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + this.guid_ = ''; + this.hasEditedNumber_ = false; + OptionsPage.closeOverlay(); + }, + + /** + * Aggregates the values in the input fields into an array and sends the + * array to the Autofill handler. + * @private + */ + saveCreditCard_: function() { + var creditCard = new Array(5); + creditCard[0] = this.guid_; + creditCard[1] = $('name-on-card').value; + creditCard[2] = $('credit-card-number').value; + creditCard[3] = $('expiration-month').value; + creditCard[4] = $('expiration-year').value; + chrome.send('setCreditCard', creditCard); + }, + + /** + * Connects each input field to the inputFieldChanged_() method that enables + * or disables the 'Ok' button based on whether all the fields are empty or + * not. + * @private + */ + connectInputEvents_: function() { + var ccNumber = $('credit-card-number'); + $('name-on-card').oninput = ccNumber.oninput = + $('expiration-month').onchange = $('expiration-year').onchange = + this.inputFieldChanged_.bind(this); + }, + + /** + * Checks the values of each of the input fields and disables the 'Ok' + * button if all of the fields are empty. + * @param {Event} opt_event Optional data for the 'input' event. + * @private + */ + inputFieldChanged_: function(opt_event) { + var disabled = !$('name-on-card').value && !$('credit-card-number').value; + $('autofill-edit-credit-card-apply-button').disabled = disabled; + }, + + /** + * Sets the default values of the options in the 'Expiration date' select + * controls. + * @private + */ + setDefaultSelectOptions_: function() { + // Set the 'Expiration month' default options. + var expirationMonth = $('expiration-month'); + expirationMonth.options.length = 0; + for (var i = 1; i <= 12; ++i) { + var text; + if (i < 10) + text = '0' + i; + else + text = i; + + var option = document.createElement('option'); + option.text = text; + option.value = text; + expirationMonth.add(option, null); + } + + // Set the 'Expiration year' default options. + var expirationYear = $('expiration-year'); + expirationYear.options.length = 0; + + var date = new Date(); + var year = parseInt(date.getFullYear()); + for (var i = 0; i < 10; ++i) { + var text = year + i; + var option = document.createElement('option'); + option.text = text; + option.value = text; + expirationYear.add(option, null); + } + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('name-on-card').value = ''; + $('credit-card-number').value = ''; + $('expiration-month').selectedIndex = 0; + $('expiration-year').selectedIndex = 0; + + // Reset the enabled status of the 'Ok' button. + this.inputFieldChanged_(); + }, + + /** + * Sets the value of each input field according to |creditCard| + * @private + */ + setInputFields_: function(creditCard) { + $('name-on-card').value = creditCard['nameOnCard']; + $('credit-card-number').value = creditCard['creditCardNumber']; + + // The options for the year select control may be out-dated at this point, + // e.g. the user opened the options page before midnight on New Year's Eve + // and then loaded a credit card profile to edit in the new year, so + // reload the select options just to be safe. + this.setDefaultSelectOptions_(); + + var idx = parseInt(creditCard['expirationMonth'], 10); + $('expiration-month').selectedIndex = idx - 1; + + expYear = creditCard['expirationYear']; + var date = new Date(); + var year = parseInt(date.getFullYear()); + for (var i = 0; i < 10; ++i) { + var text = year + i; + if (expYear == String(text)) + $('expiration-year').selectedIndex = i; + } + }, + + /** + * Loads the credit card data from |creditCard|, sets the input fields based + * on this data and stores the GUID of the credit card. + * @private + */ + loadCreditCard_: function(creditCard) { + this.setInputFields_(creditCard); + this.inputFieldChanged_(); + this.guid_ = creditCard['guid']; + }, + }; + + AutofillEditCreditCardOverlay.clearInputFields = function(title) { + AutofillEditCreditCardOverlay.getInstance().clearInputFields_(); + }; + + AutofillEditCreditCardOverlay.loadCreditCard = function(creditCard) { + AutofillEditCreditCardOverlay.getInstance().loadCreditCard_(creditCard); + }; + + AutofillEditCreditCardOverlay.setTitle = function(title) { + $('autofill-credit-card-title').textContent = title; + }; + + // Export + return { + AutofillEditCreditCardOverlay: AutofillEditCreditCardOverlay + }; +}); diff --git a/chrome/browser/resources/options2/autofill_options.css b/chrome/browser/resources/options2/autofill_options.css new file mode 100644 index 0000000..55521b27 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_options.css @@ -0,0 +1,37 @@ +.autofill-list-item { + -webkit-box-flex: 1; + -webkit-padding-start: 8px; + overflow: hidden; + text-overflow: ellipsis; +} + +.autofill-list-item + img { + -webkit-padding-end: 20px; + vertical-align: middle; +} + +#autofill-options > div:last-child { + margin-top: 15px; +} + +#autofill-options > div.settings-list > div:last-child { + border-top: 1px solid #d9d9d9; + padding: 5px 10px; +} + +#autofill-add-address, +#autofill-add-creditcard { + margin: 2px 0; +} + +.autofill-list-item + button, +.autofill-list-item + img + button { + background: #8aaaed !important; /* Gets overwritten by raw-button:hover */ + color: #fff; + margin-top: 0; +} + +#address-list > div:not(:hover) * button, +#creditcard-list > div:not(:hover) * button { + display: none; +} diff --git a/chrome/browser/resources/options2/autofill_options.html b/chrome/browser/resources/options2/autofill_options.html new file mode 100644 index 0000000..bfbd175 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_options.html @@ -0,0 +1,38 @@ +<div id="autofill-options" class="page" hidden> + <h1 i18n-content="autofillOptionsPage"></h1> +<if expr="os == 'darwin'"> + <div class="checkbox"> + <label> + <input pref="autofill.auxiliary_profiles_enabled" type="checkbox" + metric="Options_AutofillAuxiliaryProfiles"> + <span i18n-content="auxiliaryProfilesEnabled"></span> + </label> + </div> +</if> + <h3 i18n-content="autofillAddresses"></h3> + <div class="settings-list"> + <list id="address-list"></list> + <div> + <button id="autofill-add-address" i18n-content="autofillAddAddress"> + </button> + </div> + </div> + <h3 i18n-content="autofillCreditCards"></h3> + <div class="settings-list"> + <list id="creditcard-list"></list> + <div> + <button id="autofill-add-creditcard" + i18n-content="autofillAddCreditCard"></button> + </div> + </div> + <div> +<if expr="pp_ifdef('chromeos')"> + <a href="https://www.google.com/support/chromeos/bin/answer.py?answer=142893" + target="_blank" i18n-content="helpButton"></a> +</if> +<if expr="not pp_ifdef('chromeos')"> + <a href="https://www.google.com/support/chrome/bin/answer.py?answer=142893" + target="_blank" i18n-content="helpButton"></a> +</if> + </div> +</div> diff --git a/chrome/browser/resources/options2/autofill_options.js b/chrome/browser/resources/options2/autofill_options.js new file mode 100644 index 0000000..0b3a305 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_options.js @@ -0,0 +1,230 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + ///////////////////////////////////////////////////////////////////////////// + // AutofillOptions class: + + /** + * Encapsulated handling of Autofill options page. + * @constructor + */ + function AutofillOptions() { + OptionsPage.call(this, + 'autofill', + templateData.autofillOptionsPageTabTitle, + 'autofill-options'); + } + + cr.addSingletonGetter(AutofillOptions); + + AutofillOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The address list. + * @type {DeletableItemList} + * @private + */ + addressList_: null, + + /** + * The credit card list. + * @type {DeletableItemList} + * @private + */ + creditCardList_: null, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createAddressList_(); + this.createCreditCardList_(); + + var self = this; + $('autofill-add-address').onclick = function(event) { + self.showAddAddressOverlay_(); + }; + $('autofill-add-creditcard').onclick = function(event) { + self.showAddCreditCardOverlay_(); + }; + + // TODO(jhawkins): What happens when Autofill is disabled whilst on the + // Autofill options page? + }, + + /** + * Creates, decorates and initializes the address list. + * @private + */ + createAddressList_: function() { + this.addressList_ = $('address-list'); + options.autofillOptions.AutofillAddressList.decorate(this.addressList_); + this.addressList_.autoExpands = true; + }, + + /** + * Creates, decorates and initializes the credit card list. + * @private + */ + createCreditCardList_: function() { + this.creditCardList_ = $('creditcard-list'); + options.autofillOptions.AutofillCreditCardList.decorate( + this.creditCardList_); + this.creditCardList_.autoExpands = true; + }, + + /** + * Shows the 'Add address' overlay, specifically by loading the + * 'Edit address' overlay, emptying the input fields and modifying the + * overlay title. + * @private + */ + showAddAddressOverlay_: function() { + var title = localStrings.getString('addAddressTitle'); + AutofillEditAddressOverlay.setTitle(title); + AutofillEditAddressOverlay.clearInputFields(); + OptionsPage.navigateToPage('autofillEditAddress'); + }, + + /** + * Shows the 'Add credit card' overlay, specifically by loading the + * 'Edit credit card' overlay, emptying the input fields and modifying the + * overlay title. + * @private + */ + showAddCreditCardOverlay_: function() { + var title = localStrings.getString('addCreditCardTitle'); + AutofillEditCreditCardOverlay.setTitle(title); + AutofillEditCreditCardOverlay.clearInputFields(); + OptionsPage.navigateToPage('autofillEditCreditCard'); + }, + + /** + * Updates the data model for the address list with the values from + * |entries|. + * @param {Array} entries The list of addresses. + */ + setAddressList_: function(entries) { + this.addressList_.dataModel = new ArrayDataModel(entries); + }, + + /** + * Updates the data model for the credit card list with the values from + * |entries|. + * @param {Array} entries The list of credit cards. + */ + setCreditCardList_: function(entries) { + this.creditCardList_.dataModel = new ArrayDataModel(entries); + }, + + /** + * Removes the Autofill address represented by |guid|. + * @param {String} guid The GUID of the address to remove. + * @private + */ + removeAddress_: function(guid) { + chrome.send('removeAddress', [guid]); + }, + + /** + * Removes the Autofill credit card represented by |guid|. + * @param {String} guid The GUID of the credit card to remove. + * @private + */ + removeCreditCard_: function(guid) { + chrome.send('removeCreditCard', [guid]); + }, + + /** + * Requests profile data for the address represented by |guid| from the + * PersonalDataManager. Once the data is loaded, the AutofillOptionsHandler + * calls showEditAddressOverlay(). + * @param {String} guid The GUID of the address to edit. + * @private + */ + loadAddressEditor_: function(guid) { + chrome.send('loadAddressEditor', [guid]); + }, + + /** + * Requests profile data for the credit card represented by |guid| from the + * PersonalDataManager. Once the data is loaded, the AutofillOptionsHandler + * calls showEditCreditCardOverlay(). + * @param {String} guid The GUID of the credit card to edit. + * @private + */ + loadCreditCardEditor_: function(guid) { + chrome.send('loadCreditCardEditor', [guid]); + }, + + /** + * Shows the 'Edit address' overlay, using the data in |address| to fill the + * input fields. |address| is a list with one item, an associative array + * that contains the address data. + * @private + */ + showEditAddressOverlay_: function(address) { + var title = localStrings.getString('editAddressTitle'); + AutofillEditAddressOverlay.setTitle(title); + AutofillEditAddressOverlay.loadAddress(address); + OptionsPage.navigateToPage('autofillEditAddress'); + }, + + /** + * Shows the 'Edit credit card' overlay, using the data in |credit_card| to + * fill the input fields. |address| is a list with one item, an associative + * array that contains the credit card data. + * @private + */ + showEditCreditCardOverlay_: function(creditCard) { + var title = localStrings.getString('editCreditCardTitle'); + AutofillEditCreditCardOverlay.setTitle(title); + AutofillEditCreditCardOverlay.loadCreditCard(creditCard); + OptionsPage.navigateToPage('autofillEditCreditCard'); + }, + }; + + AutofillOptions.setAddressList = function(entries) { + AutofillOptions.getInstance().setAddressList_(entries); + }; + + AutofillOptions.setCreditCardList = function(entries) { + AutofillOptions.getInstance().setCreditCardList_(entries); + }; + + AutofillOptions.removeAddress = function(guid) { + AutofillOptions.getInstance().removeAddress_(guid); + }; + + AutofillOptions.removeCreditCard = function(guid) { + AutofillOptions.getInstance().removeCreditCard_(guid); + }; + + AutofillOptions.loadAddressEditor = function(guid) { + AutofillOptions.getInstance().loadAddressEditor_(guid); + }; + + AutofillOptions.loadCreditCardEditor = function(guid) { + AutofillOptions.getInstance().loadCreditCardEditor_(guid); + }; + + AutofillOptions.editAddress = function(address) { + AutofillOptions.getInstance().showEditAddressOverlay_(address); + }; + + AutofillOptions.editCreditCard = function(creditCard) { + AutofillOptions.getInstance().showEditCreditCardOverlay_(creditCard); + }; + + // Export + return { + AutofillOptions: AutofillOptions + }; + +}); + diff --git a/chrome/browser/resources/options2/autofill_options_list.js b/chrome/browser/resources/options2/autofill_options_list.js new file mode 100644 index 0000000..c9ed67c --- /dev/null +++ b/chrome/browser/resources/options2/autofill_options_list.js @@ -0,0 +1,506 @@ +// 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. + +cr.define('options.autofillOptions', function() { + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + const InlineEditableItem = options.InlineEditableItem; + const InlineEditableItemList = options.InlineEditableItemList; + + function AutofillEditProfileButton(guid, edit) { + var editButtonEl = document.createElement('button'); + editButtonEl.className = 'raw-button custom-appearance'; + editButtonEl.textContent = + templateData.autofillEditProfileButton; + editButtonEl.onclick = function(e) { edit(guid); }; + + // Don't select the row when clicking the button. + editButtonEl.onmousedown = function(e) { + e.stopPropagation(); + }; + + return editButtonEl; + } + + /** + * Creates a new address list item. + * @param {Array} entry An array of the form [guid, label]. + * @constructor + * @extends {options.DeletableItem} + */ + function AddressListItem(entry) { + var el = cr.doc.createElement('div'); + el.guid = entry[0]; + el.label = entry[1]; + el.__proto__ = AddressListItem.prototype; + el.decorate(); + + return el; + } + + AddressListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The stored label. + var label = this.ownerDocument.createElement('div'); + label.className = 'autofill-list-item'; + label.textContent = this.label; + this.contentElement.appendChild(label); + + // The 'Edit' button. + var editButtonEl = new AutofillEditProfileButton( + this.guid, + AutofillOptions.loadAddressEditor); + this.contentElement.appendChild(editButtonEl); + }, + }; + + /** + * Creates a new credit card list item. + * @param {Array} entry An array of the form [guid, label, icon]. + * @constructor + * @extends {options.DeletableItem} + */ + function CreditCardListItem(entry) { + var el = cr.doc.createElement('div'); + el.guid = entry[0]; + el.label = entry[1]; + el.icon = entry[2]; + el.description = entry[3]; + el.__proto__ = CreditCardListItem.prototype; + el.decorate(); + + return el; + } + + CreditCardListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The stored label. + var label = this.ownerDocument.createElement('div'); + label.className = 'autofill-list-item'; + label.textContent = this.label; + this.contentElement.appendChild(label); + + // The credit card icon. + var icon = this.ownerDocument.createElement('image'); + icon.src = this.icon; + icon.alt = this.description; + this.contentElement.appendChild(icon); + + // The 'Edit' button. + var editButtonEl = new AutofillEditProfileButton( + this.guid, + AutofillOptions.loadCreditCardEditor); + this.contentElement.appendChild(editButtonEl); + }, + }; + + /** + * Creates a new value list item. + * @param {AutofillValuesList} list The parent list of this item. + * @param {String} entry A string value. + * @constructor + * @extends {options.InlineEditableItem} + */ + function ValuesListItem(list, entry) { + var el = cr.doc.createElement('div'); + el.list = list; + el.value = entry ? entry : ''; + el.__proto__ = ValuesListItem.prototype; + el.decorate(); + + return el; + } + + ValuesListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + // Note: This must be set prior to calling |createEditableTextCell|. + this.isPlaceholder = !this.value; + + // The stored value. + var cell = this.createEditableTextCell(this.value); + this.contentElement.appendChild(cell); + this.input = cell.querySelector('input'); + + if (this.isPlaceholder) { + this.input.placeholder = this.list.getAttribute('placeholder'); + this.deletable = false; + } + + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** + * @return This item's value. + * @protected + */ + value_: function() { + return this.input.value; + }, + + /** + * @param {Object} value The value to test. + * @return true if the given value is non-empty. + * @protected + */ + valueIsNonEmpty_: function(value) { + return !!value; + }, + + /** + * @return true if value1 is logically equal to value2. + */ + valuesAreEqual_: function(value1, value2) { + return value1 === value2; + }, + + /** + * Clears the item's value. + * @protected + */ + clearValue_: function() { + this.input.value = ''; + }, + + /** + * Called when committing an edit. + * If this is an "Add ..." item, committing a non-empty value adds that + * value to the end of the values list, but also leaves this "Add ..." item + * in place. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var value = this.value_(); + var i = this.list.items.indexOf(this); + if (i < this.list.dataModel.length && + this.valuesAreEqual_(value, this.list.dataModel.item(i))) { + return; + } + + var entries = this.list.dataModel.slice(); + if (this.valueIsNonEmpty_(value) && + !entries.some(this.valuesAreEqual_.bind(this, value))) { + // Update with new value. + if (this.isPlaceholder) { + // It is important that updateIndex is done before validateAndSave. + // Otherwise we can not be sure about AddRow index. + this.list.dataModel.updateIndex(i); + this.list.validateAndSave(i, 0, value); + } else { + this.list.validateAndSave(i, 1, value); + } + } else { + // Reject empty values and duplicates. + if (!this.isPlaceholder) + this.list.dataModel.splice(i, 1); + else + this.clearValue_(); + } + }, + }; + + /** + * Creates a new name value list item. + * @param {AutofillNameValuesList} list The parent list of this item. + * @param {array} entry An array of [first, middle, last] names. + * @constructor + * @extends {options.ValuesListItem} + */ + function NameListItem(list, entry) { + var el = cr.doc.createElement('div'); + el.list = list; + el.first = entry ? entry[0] : ''; + el.middle = entry ? entry[1] : ''; + el.last = entry ? entry[2] : ''; + el.__proto__ = NameListItem.prototype; + el.decorate(); + + return el; + } + + NameListItem.prototype = { + __proto__: ValuesListItem.prototype, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + // Note: This must be set prior to calling |createEditableTextCell|. + this.isPlaceholder = !this.first && !this.middle && !this.last; + + // The stored value. + // For the simulated static "input element" to display correctly, the + // value must not be empty. We use a space to force the UI to render + // correctly when the value is logically empty. + var cell = this.createEditableTextCell(this.first); + this.contentElement.appendChild(cell); + this.firstNameInput = cell.querySelector('input'); + + cell = this.createEditableTextCell(this.middle); + this.contentElement.appendChild(cell); + this.middleNameInput = cell.querySelector('input'); + + cell = this.createEditableTextCell(this.last); + this.contentElement.appendChild(cell); + this.lastNameInput = cell.querySelector('input'); + + if (this.isPlaceholder) { + this.firstNameInput.placeholder = + templateData.autofillAddFirstNamePlaceholder; + this.middleNameInput.placeholder = + templateData.autofillAddMiddleNamePlaceholder; + this.lastNameInput.placeholder = + templateData.autofillAddLastNamePlaceholder; + this.deletable = false; + } + + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** @inheritDoc */ + value_: function() { + return [ this.firstNameInput.value, + this.middleNameInput.value, + this.lastNameInput.value ]; + }, + + /** @inheritDoc */ + valueIsNonEmpty_: function(value) { + return value[0] || value[1] || value[2]; + }, + + /** @inheritDoc */ + valuesAreEqual_: function(value1, value2) { + // First, check for null values. + if (!value1 || !value2) + return value1 == value2; + + return value1[0] === value2[0] && + value1[1] === value2[1] && + value1[2] === value2[2]; + }, + + /** @inheritDoc */ + clearValue_: function() { + this.firstNameInput.value = ''; + this.middleNameInput.value = ''; + this.lastNameInput.value = ''; + }, + }; + + /** + * Base class for shared implementation between address and credit card lists. + * @constructor + * @extends {options.DeletableItemList} + */ + var AutofillProfileList = cr.ui.define('list'); + + AutofillProfileList.prototype = { + __proto__: DeletableItemList.prototype, + + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + + this.addEventListener('blur', this.onBlur_); + }, + + /** + * When the list loses focus, unselect all items in the list. + * @private + */ + onBlur_: function() { + this.selectionModel.unselectAll(); + }, + }; + + /** + * Create a new address list. + * @constructor + * @extends {options.AutofillProfileList} + */ + var AutofillAddressList = cr.ui.define('list'); + + AutofillAddressList.prototype = { + __proto__: AutofillProfileList.prototype, + + decorate: function() { + AutofillProfileList.prototype.decorate.call(this); + }, + + /** @inheritDoc */ + activateItemAtIndex: function(index) { + AutofillOptions.loadAddressEditor(this.dataModel.item(index)[0]); + }, + + /** @inheritDoc */ + createItem: function(entry) { + return new AddressListItem(entry); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + AutofillOptions.removeAddress(this.dataModel.item(index)[0]); + }, + }; + + /** + * Create a new credit card list. + * @constructor + * @extends {options.DeletableItemList} + */ + var AutofillCreditCardList = cr.ui.define('list'); + + AutofillCreditCardList.prototype = { + __proto__: AutofillProfileList.prototype, + + decorate: function() { + AutofillProfileList.prototype.decorate.call(this); + }, + + /** @inheritDoc */ + activateItemAtIndex: function(index) { + AutofillOptions.loadCreditCardEditor(this.dataModel.item(index)[0]); + }, + + /** @inheritDoc */ + createItem: function(entry) { + return new CreditCardListItem(entry); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + AutofillOptions.removeCreditCard(this.dataModel.item(index)[0]); + }, + }; + + /** + * Create a new value list. + * @constructor + * @extends {options.InlineEditableItemList} + */ + var AutofillValuesList = cr.ui.define('list'); + + AutofillValuesList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** @inheritDoc */ + createItem: function(entry) { + return new ValuesListItem(this, entry); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + this.dataModel.splice(index, 1); + }, + + /** @inheritDoc */ + shouldFocusPlaceholder: function() { + return false; + }, + + /** + * Called when the list hierarchy as a whole loses or gains focus. + * If the list was focused in response to a mouse click, call into the + * superclass's implementation. If the list was focused in response to a + * keyboard navigation, focus the first item. + * If the list loses focus, unselect all the elements. + * @param {Event} e The change event. + * @private + */ + handleListFocusChange_: function(e) { + // We check to see whether there is a selected item as a proxy for + // distinguishing between mouse- and keyboard-originated focus events. + var selectedItem = this.selectedItem; + if (selectedItem) + InlineEditableItemList.prototype.handleListFocusChange_.call(this, e); + + if (!e.newValue) { + // When the list loses focus, unselect all the elements. + this.selectionModel.unselectAll(); + } else { + // When the list gains focus, select the first item if nothing else is + // selected. + var firstItem = this.getListItemByIndex(0); + if (!selectedItem && firstItem && e.newValue) + firstItem.handleFocus_(); + } + }, + + /** + * Called when a new list item should be validated; subclasses are + * responsible for implementing if validation is required. + * @param {number} index The index of the item that was inserted or changed. + * @param {number} remove The number items to remove. + * @param {string} value The value of the item to insert. + */ + validateAndSave: function(index, remove, value) { + this.dataModel.splice(index, remove, value); + }, + }; + + /** + * Create a new value list for phone number validation. + * @constructor + * @extends {options.AutofillValuesList} + */ + var AutofillNameValuesList = cr.ui.define('list'); + + AutofillNameValuesList.prototype = { + __proto__: AutofillValuesList.prototype, + + /** @inheritDoc */ + createItem: function(entry) { + return new NameListItem(this, entry); + }, + }; + + /** + * Create a new value list for phone number validation. + * @constructor + * @extends {options.AutofillValuesList} + */ + var AutofillPhoneValuesList = cr.ui.define('list'); + + AutofillPhoneValuesList.prototype = { + __proto__: AutofillValuesList.prototype, + + /** @inheritDoc */ + validateAndSave: function(index, remove, value) { + var numbers = this.dataModel.slice(0, this.dataModel.length - 1); + numbers.splice(index, remove, value); + var info = new Array(); + info[0] = index; + info[1] = numbers; + info[2] = $('country').value; + chrome.send('validatePhoneNumbers', info); + }, + }; + + return { + AddressListItem: AddressListItem, + CreditCardListItem: CreditCardListItem, + ValuesListItem: ValuesListItem, + NameListItem: NameListItem, + AutofillAddressList: AutofillAddressList, + AutofillCreditCardList: AutofillCreditCardList, + AutofillValuesList: AutofillValuesList, + AutofillNameValuesList: AutofillNameValuesList, + AutofillPhoneValuesList: AutofillPhoneValuesList, + }; +}); diff --git a/chrome/browser/resources/options2/autofill_overlay.css b/chrome/browser/resources/options2/autofill_overlay.css new file mode 100644 index 0000000..1bc2db4 --- /dev/null +++ b/chrome/browser/resources/options2/autofill_overlay.css @@ -0,0 +1,95 @@ +#autofill-edit-address-overlay { + min-width: 510px; +} + +#autofill-edit-credit-card-overlay { + min-width: 500px; +} + +div.table { + display: table; +} + +div.cell { + display: table-cell; +} + +div.row { + display: table-row; +} + +div.input { + padding: 2px; +} + +/* Size to match large name fields. */ +#company-name, #addr-line-1, #addr-line-2 { + width: 206px; +} + +#country { + max-width: 450px; +} + +#autofill-edit-address-overlay list { + /* Min height is a multiple of the list item height (32) */ + min-height: 32px; + width: 176px; +} + +#autofill-edit-address-overlay list div.static-text { + -webkit-box-flex: 1; + -webkit-border-radius: 2px; + -webkit-padding-start: 4px; + -webkit-padding-end: 4px; + border: 1px solid darkGray; + /* Set the line-height and min-height to match the height of an input element, + * so that even empty cells renderer with the correct height. + */ + line-height: 1.75em; + min-height: 1.75em; + width: 141px; +} + +#autofill-edit-address-overlay list input { + width: 151px; +} + +#autofill-name-labels { + -webkit-box-orient: horizontal; + /* Set the margin to compensate for each list item's close button and + * padding. + */ + -webkit-margin-end: 25px; + display: -webkit-box; +} + +#autofill-name-labels label { + -webkit-box-flex: 1; + display: block; + /* Set the minimum width to the size of an input element, so that all boxes + * have an equal amount of flex space to work with. + */ + min-width: 141px; +} + +#autofill-edit-address-overlay list#full-name-list div.static-text { + width: 131px; +} + +#autofill-edit-address-overlay list#full-name-list input { + width: 141px; +} + +#autofill-edit-address-overlay list#full-name-list { + width: 100%; +} + +#full-name-list div[role="listitem"] > div { + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +#full-name-list div[role="listitem"] > div > div { + -webkit-box-flex: 1; +} diff --git a/chrome/browser/resources/options2/browser_options.html b/chrome/browser/resources/options2/browser_options.html new file mode 100644 index 0000000..2af7b17 --- /dev/null +++ b/chrome/browser/resources/options2/browser_options.html @@ -0,0 +1,128 @@ +<div id="browserPage" class="page" hidden> + <h1 i18n-content="browserPage"></h1> + <div class="displaytable"> + <section id="startupSection"> + <h3 i18n-content="startupGroupName"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="startup" value="0" + pref="session.restore_on_startup" + metric="Options_Startup_Homepage"> + <span i18n-content="startupShowDefaultAndNewTab"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="startup" value="1" + pref="session.restore_on_startup" + metric="Options_Startup_LastSession"> + <span i18n-content="startupShowLastSession"></span> + </label> + </div> + <div class="radio"> + <label> + <input id="startupShowPagesButton" type="radio" name="startup" + pref="session.restore_on_startup" + value="4" metric="Options_Startup_Custom"> + <span id="startupShowPagesLabel" + i18n-content="startupShowPages"></span> + </label> + </div> + <div class="suboption"> + <div id="startupPageManagement" class="settings-list"> + <list id="startupPagesList"></list> + <div> + <button id="startupUseCurrentButton" + i18n-content="startupUseCurrent"></button> + </div> + </div> + <div id="startupPagesListDropmarker"></div> + </div> + </div> + </section> + <section> + <h3 i18n-content="homepageGroupName"></h3> + <div> + <div class="radio"> + <label> + <input id="homepageUseNTPButton" type="radio" name="homepage" + value="true" metric="Options_Homepage_IsNewTabPage" + pref="homepage_is_newtabpage"> + <span i18n-content="homepageUseNewTab"></span> + </label> + </div> + <div id="customHomePageGroup"> + <div class="radio"> + <label> + <input id="homepageUseURLButton" type="radio" name="homepage" + value="false" metric="Options_Homepage_IsNewTabPage" + pref="homepage_is_newtabpage"> + <span i18n-content="homepageUseURL"></span> + </label> + </div> + <div> + <input id="homepageURL" class="weakrtl favicon-cell" type="url" + data-type="url" pref="homepage"> + </div> + </div> + </div> + </section> + <section> + <h3 i18n-content="toolbarGroupName"></h3> + <div> + <div class="checkbox"> + <label> + <input id="toolbarShowHomeButton" pref="browser.show_home_button" + metric="Options_Homepage_HomeButton" type="checkbox"> + <span i18n-content="toolbarShowHomeButton"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="toolbarShowBookmarksBar" + pref="bookmark_bar.show_on_all_tabs" + metric="Options_ShowBookmarksBar" type="checkbox"> + <span i18n-content="toolbarShowBookmarksBar"></span> + </label> + </div> + </div> + </section> + <section> + <h3 i18n-content="defaultSearchGroupName"></h3> + <div id="defaultSearchEngineGroup"> + <div> + <select id="defaultSearchEngine" class="weakrtl"></select> + <button id="defaultSearchManageEnginesButton" + i18n-content="defaultSearchManageEngines"></button> + </div> + <div id="instantOption" class="checkbox"> + <label id="instantLabel"> + <!-- TODO(estade): metric? --> + <input id="instantFieldTrialCheckbox" type="checkbox" + checked="checked" hidden> + <input id="instantEnabledCheckbox" type="checkbox" + pref="instant.enabled"> + <span i18n-content="instantName"></span> + </label> + </div> + <div class="suboption informational-text"> + <span i18n-content="instantWarningText"></span> + <a target="_blank" i18n-values="href:instantLearnMoreLink" + i18n-content="learnMore"></a> + </div> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="defaultBrowserGroupName"></h3> + <div> + <button id="defaultBrowserUseAsDefaultButton" + i18n-content="defaultBrowserUseAsDefault"></button> + <div id="defaultBrowserState" + i18n-content="defaultBrowserUnknown"></div> + </div> + </section> +</if> + </div> +</div> diff --git a/chrome/browser/resources/options2/browser_options.js b/chrome/browser/resources/options2/browser_options.js new file mode 100644 index 0000000..ba59d3d --- /dev/null +++ b/chrome/browser/resources/options2/browser_options.js @@ -0,0 +1,366 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + // + // BrowserOptions class + // Encapsulated handling of browser options page. + // + function BrowserOptions() { + OptionsPage.call(this, 'browser', + templateData.browserPageTabTitle, + 'browserPage'); + } + + cr.addSingletonGetter(BrowserOptions); + + BrowserOptions.prototype = { + // Inherit BrowserOptions from OptionsPage. + __proto__: options.OptionsPage.prototype, + + startup_pages_pref_: { + 'name': 'session.urls_to_restore_on_startup', + 'disabled': false + }, + + /** + * At autocomplete list that can be attached to a text field during editing. + * @type {HTMLElement} + * @private + */ + autocompleteList_: null, + + // The cached value of the instant.confirm_dialog_shown preference. + instantConfirmDialogShown_: false, + + /** + * Initialize BrowserOptions page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Wire up controls. + $('startupUseCurrentButton').onclick = function(event) { + chrome.send('setStartupPagesToCurrentPages'); + }; + $('defaultSearchManageEnginesButton').onclick = function(event) { + OptionsPage.navigateToPage('searchEngines'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ManageSearchEngines']); + }; + $('defaultSearchEngine').onchange = this.setDefaultSearchEngine_; + + var self = this; + $('instantEnabledCheckbox').customChangeHandler = function(event) { + if (this.checked) { + if (self.instantConfirmDialogShown_) + chrome.send('enableInstant'); + else + OptionsPage.navigateToPage('instantConfirm'); + } else { + chrome.send('disableInstant'); + } + return true; + }; + + $('instantFieldTrialCheckbox').addEventListener('change', + function(event) { + this.checked = true; + chrome.send('disableInstant'); + }); + + Preferences.getInstance().addEventListener('instant.confirm_dialog_shown', + this.onInstantConfirmDialogShownChanged_.bind(this)); + + Preferences.getInstance().addEventListener('instant.enabled', + this.onInstantEnabledChanged_.bind(this)); + + Preferences.getInstance().addEventListener( + $('homepageUseNTPButton').pref, + this.onHomepageUseNTPChanged_); + var homepageField = $('homepageURL'); + homepageField.addEventListener('focus', function(event) { + self.autocompleteList_.attachToInput(homepageField); + }); + homepageField.addEventListener('blur', function(event) { + self.autocompleteList_.detach(); + }); + homepageField.addEventListener('keydown', function(event) { + // Remove focus when the user hits enter since people expect feedback + // indicating that they are done editing. + if (event.keyIdentifier == 'Enter') + homepageField.blur(); + }); + + // Text fields may change widths when the window changes size, so make + // sure the suggestion list stays in sync. + window.addEventListener('resize', function() { + self.autocompleteList_.syncWidthToInput(); + }); + + // Ensure that changes are committed when closing the page. + window.addEventListener('unload', function() { + if (document.activeElement == homepageField) + homepageField.blur(); + }); + + if (!cr.isChromeOS) { + $('defaultBrowserUseAsDefaultButton').onclick = function(event) { + chrome.send('becomeDefaultBrowser'); + }; + } + + var startupPagesList = $('startupPagesList'); + options.browser_options.StartupPageList.decorate(startupPagesList); + startupPagesList.autoExpands = true; + + // Check if we are in the guest mode. + if (cr.commandLine && cr.commandLine.options['--bwsi']) { + // Hide the startup section. + $('startupSection').hidden = true; + } else { + // Initialize control enabled states. + Preferences.getInstance().addEventListener('session.restore_on_startup', + this.updateCustomStartupPageControlStates_.bind(this)); + Preferences.getInstance().addEventListener( + this.startup_pages_pref_.name, + this.handleStartupPageListChange_.bind(this)); + + this.updateCustomStartupPageControlStates_(); + } + + var suggestionList = new options.AutocompleteList(); + suggestionList.autoExpands = true; + suggestionList.suggestionUpdateRequestCallback = + this.requestAutocompleteSuggestions_.bind(this); + $('main-content').appendChild(suggestionList); + this.autocompleteList_ = suggestionList; + startupPagesList.autocompleteList = suggestionList; + }, + + /** + * Called when the value of the instant.confirm_dialog_shown preference + * changes. Cache this value. + * @param {Event} event Change event. + * @private + */ + onInstantConfirmDialogShownChanged_: function(event) { + this.instantConfirmDialogShown_ = event.value['value']; + }, + + /** + * Called when the value of the instant.enabled preference changes. Request + * the state of the Instant field trial experiment. + * @param {Event} event Change event. + * @private + */ + onInstantEnabledChanged_: function(event) { + chrome.send('getInstantFieldTrialStatus'); + }, + + /** + * Called to set the Instant field trial status. + * @param {boolean} enabled If true, the experiment is enabled. + * @private + */ + setInstantFieldTrialStatus_: function(enabled) { + $('instantEnabledCheckbox').hidden = enabled; + $('instantFieldTrialCheckbox').hidden = !enabled; + $('instantLabel').htmlFor = enabled ? 'instantFieldTrialCheckbox' + : 'instantEnabledCheckbox'; + }, + + /** + * Called when the value of the homepage-use-NTP pref changes. + * Updates the disabled state of the homepage text field. + * Notice that the text field can be disabled for other reasons too + * (it can be managed by policy, for instance). + * @param {Event} event Change event. + * @private + */ + onHomepageUseNTPChanged_: function(event) { + var homepageField = $('homepageURL'); + var homepageUseURLButton = $('homepageUseURLButton'); + homepageField.setDisabled('radioNotSelected', + !homepageUseURLButton.checked); + }, + + /** + * Update the Default Browsers section based on the current state. + * @param {string} statusString Description of the current default state. + * @param {boolean} isDefault Whether or not the browser is currently + * default. + * @param {boolean} canBeDefault Whether or not the browser can be default. + * @private + */ + updateDefaultBrowserState_: function(statusString, isDefault, + canBeDefault) { + var label = $('defaultBrowserState'); + label.textContent = statusString; + + $('defaultBrowserUseAsDefaultButton').disabled = !canBeDefault || + isDefault; + }, + + /** + * Clears the search engine popup. + * @private + */ + clearSearchEngines_: function() { + $('defaultSearchEngine').textContent = ''; + }, + + /** + * Updates the search engine popup with the given entries. + * @param {Array} engines List of available search engines. + * @param {number} defaultValue The value of the current default engine. + * @param {boolean} defaultManaged Whether the default search provider is + * managed. If true, the default search provider can't be changed. + */ + updateSearchEngines_: function(engines, defaultValue, defaultManaged) { + this.clearSearchEngines_(); + engineSelect = $('defaultSearchEngine'); + engineSelect.disabled = defaultManaged; + engineCount = engines.length; + var defaultIndex = -1; + for (var i = 0; i < engineCount; i++) { + var engine = engines[i]; + var option = new Option(engine['name'], engine['index']); + if (defaultValue == option.value) + defaultIndex = i; + engineSelect.appendChild(option); + } + if (defaultIndex >= 0) + engineSelect.selectedIndex = defaultIndex; + }, + + /** + * Returns true if the custom startup page control block should + * be enabled. + * @returns {boolean} Whether the startup page controls should be + * enabled. + */ + shouldEnableCustomStartupPageControls: function(pages) { + return $('startupShowPagesButton').checked && + !this.startup_pages_pref_.disabled; + }, + + /** + * Updates the startup pages list with the given entries. + * @param {Array} pages List of startup pages. + * @private + */ + updateStartupPages_: function(pages) { + var model = new ArrayDataModel(pages); + // Add a "new page" row. + model.push({ + 'modelIndex': '-1' + }); + $('startupPagesList').dataModel = model; + }, + + /** + * Sets the enabled state of the custom startup page list controls + * based on the current startup radio button selection. + * @private + */ + updateCustomStartupPageControlStates_: function() { + var disable = !this.shouldEnableCustomStartupPageControls(); + var startupPagesList = $('startupPagesList'); + startupPagesList.disabled = disable; + startupPagesList.setAttribute('tabindex', disable ? -1 : 0); + // Explicitly set disabled state for input text elements. + var inputs = startupPagesList.querySelectorAll("input[type='text']"); + for (var i = 0; i < inputs.length; i++) + inputs[i].disabled = disable; + $('startupUseCurrentButton').disabled = disable; + }, + + /** + * Handle change events of the preference + * 'session.urls_to_restore_on_startup'. + * @param {event} preference changed event. + * @private + */ + handleStartupPageListChange_: function(event) { + this.startup_pages_pref_.disabled = event.value['disabled']; + this.updateCustomStartupPageControlStates_(); + }, + + /** + * Set the default search engine based on the popup selection. + */ + setDefaultSearchEngine_: function() { + var engineSelect = $('defaultSearchEngine'); + var selectedIndex = engineSelect.selectedIndex; + if (selectedIndex >= 0) { + var selection = engineSelect.options[selectedIndex]; + chrome.send('setDefaultSearchEngine', [String(selection.value)]); + } + }, + + /** + * Sends an asynchronous request for new autocompletion suggestions for the + * the given query. When new suggestions are available, the C++ handler will + * call updateAutocompleteSuggestions_. + * @param {string} query List of autocomplete suggestions. + * @private + */ + requestAutocompleteSuggestions_: function(query) { + chrome.send('requestAutocompleteSuggestions', [query]); + }, + + /** + * Updates the autocomplete suggestion list with the given entries. + * @param {Array} pages List of autocomplete suggestions. + * @private + */ + updateAutocompleteSuggestions_: function(suggestions) { + var list = this.autocompleteList_; + // If the trigger for this update was a value being selected from the + // current list, do nothing. + if (list.targetInput && list.selectedItem && + list.selectedItem['url'] == list.targetInput.value) + return; + list.suggestions = suggestions; + }, + }; + + BrowserOptions.updateDefaultBrowserState = function(statusString, isDefault, + canBeDefault) { + if (!cr.isChromeOS) { + BrowserOptions.getInstance().updateDefaultBrowserState_(statusString, + isDefault, + canBeDefault); + } + }; + + BrowserOptions.updateSearchEngines = function(engines, defaultValue, + defaultManaged) { + BrowserOptions.getInstance().updateSearchEngines_(engines, defaultValue, + defaultManaged); + }; + + BrowserOptions.updateStartupPages = function(pages) { + BrowserOptions.getInstance().updateStartupPages_(pages); + }; + + BrowserOptions.updateAutocompleteSuggestions = function(suggestions) { + BrowserOptions.getInstance().updateAutocompleteSuggestions_(suggestions); + }; + + BrowserOptions.setInstantFieldTrialStatus = function(enabled) { + BrowserOptions.getInstance().setInstantFieldTrialStatus_(enabled); + }; + + // Export + return { + BrowserOptions: BrowserOptions + }; + +}); diff --git a/chrome/browser/resources/options2/browser_options_page.css b/chrome/browser/resources/options2/browser_options_page.css new file mode 100644 index 0000000..2be1710 --- /dev/null +++ b/chrome/browser/resources/options2/browser_options_page.css @@ -0,0 +1,95 @@ +#startupPageManagement.settings-list > :last-child { + border-top: 1px solid #d9d9d9; + padding: 5px 10px; +} + +#startupPagesList { + min-height: 64px; +} + +#startupPagesList .title { + width: 40%; +} + +#startupPagesList .url { + -webkit-box-flex: 1; + color: #666; +} + +#startupPagesList > * { + max-width: 700px; +} + +#startupPagesListDropmarker { + background-clip: padding-box; + background-color: hsl(214, 91%, 65%); + border-bottom-color: transparent; + border-radius: 0; + border-top-color: transparent; + border: 2px solid hsl(214, 91%, 65%); + box-sizing: border-box; + display: none; + height: 6px; + overflow: hidden; + pointer-events: none; + position: fixed; + z-index: 10; +} + +#customHomePageGroup { + display: -webkit-box; + -webkit-box-orient: horizontal; +} + +#customHomePageGroup > :last-child { + -webkit-margin-start: 1ex; + -webkit-box-flex: 1; + position: relative; +} + +#homepageURL { + box-sizing: border-box; + padding-top: 3px; + width: 100%; +} + +#defaultSearchEngineGroup { + display: -webkit-box; + -webkit-box-orient: vertical; +} + +#defaultSearchEngineGroup > div { + display: -webkit-box; + -webkit-box-orient: horizontal; +} + +#defaultSearchEngine { + display: block; + -webkit-box-flex: 1; + max-width: 200px; +} + +#defaultSearchManageEnginesButton { + margin-top: 0px; + -webkit-margin-start: 10px; +} + +#defaultBrowserState { + margin-top: 6px; +} + +#instantOption { + margin-bottom: 3px; + margin-top: 10px +} + +#instantConfirmText { + font-family: inherit; + white-space: pre-wrap; + width: 500px; +} + +#instantConfirmLearnMore { + position: absolute; + bottom: 18px; +} diff --git a/chrome/browser/resources/options2/browser_options_startup_page_list.js b/chrome/browser/resources/options2/browser_options_startup_page_list.js new file mode 100644 index 0000000..cd10729 --- /dev/null +++ b/chrome/browser/resources/options2/browser_options_startup_page_list.js @@ -0,0 +1,310 @@ +// 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. + +cr.define('options.browser_options', function() { + const AutocompleteList = options.AutocompleteList; + const InlineEditableItem = options.InlineEditableItem; + const InlineEditableItemList = options.InlineEditableItemList; + + /** + * Creates a new startup page list item. + * @param {Object} pageInfo The page this item represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function StartupPageListItem(pageInfo) { + var el = cr.doc.createElement('div'); + el.pageInfo_ = pageInfo; + StartupPageListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a startup page list item. + * @param {!HTMLElement} el The element to decorate. + */ + StartupPageListItem.decorate = function(el) { + el.__proto__ = StartupPageListItem.prototype; + el.decorate(); + }; + + StartupPageListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Input field for editing the page url. + * @type {HTMLElement} + * @private + */ + urlField_: null, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + var pageInfo = this.pageInfo_; + + if (pageInfo['modelIndex'] == '-1') { + this.isPlaceholder = true; + pageInfo['title'] = localStrings.getString('startupAddLabel'); + pageInfo['url'] = ''; + } + + var titleEl = this.ownerDocument.createElement('div'); + titleEl.className = 'title'; + titleEl.classList.add('favicon-cell'); + titleEl.classList.add('weakrtl'); + titleEl.textContent = pageInfo['title']; + if (!this.isPlaceholder) { + titleEl.style.backgroundImage = url('chrome://favicon/' + + pageInfo['url']); + titleEl.title = pageInfo['tooltip']; + } + + this.contentElement.appendChild(titleEl); + + var urlEl = this.createEditableTextCell(pageInfo['url']); + urlEl.className = 'url'; + urlEl.classList.add('weakrtl'); + this.contentElement.appendChild(urlEl); + + var urlField = urlEl.querySelector('input') + urlField.required = true; + urlField.className = 'weakrtl'; + this.urlField_ = urlField; + + this.addEventListener('commitedit', this.onEditCommitted_); + + var self = this; + urlField.addEventListener('focus', function(event) { + self.parentNode.autocompleteList.attachToInput(urlField); + }); + urlField.addEventListener('blur', function(event) { + self.parentNode.autocompleteList.detach(); + }); + + if (!this.isPlaceholder) + this.draggable = true; + }, + + /** @inheritDoc */ + get currentInputIsValid() { + return this.urlField_.validity.valid; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + return this.urlField_.value != this.pageInfo_['url']; + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var url = this.urlField_.value; + if (this.isPlaceholder) + chrome.send('addStartupPage', [url]); + else + chrome.send('editStartupPage', [this.pageInfo_['modelIndex'], url]); + }, + }; + + var StartupPageList = cr.ui.define('list'); + + StartupPageList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * An autocomplete suggestion list for URL editing. + * @type {AutocompleteList} + */ + autocompleteList: null, + + /** + * The drop position information: "below" or "above". + */ + dropPos: null, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItemList.prototype.decorate.call(this); + + // Listen to drag and drop events. + this.addEventListener('dragstart', this.handleDragStart_.bind(this)); + this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); + this.addEventListener('dragover', this.handleDragOver_.bind(this)); + this.addEventListener('drop', this.handleDrop_.bind(this)); + this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); + this.addEventListener('dragend', this.handleDragEnd_.bind(this)); + }, + + /** @inheritDoc */ + createItem: function(pageInfo) { + var item = new StartupPageListItem(pageInfo); + item.urlField_.disabled = this.disabled; + return item; + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + chrome.send('removeStartupPages', [String(index)]); + }, + + /* + * Computes the target item of drop event. + * @param {Event} e The drop or dragover event. + * @private + */ + getTargetFromDropEvent_ : function(e) { + var target = e.target; + // e.target may be an inner element of the list item + while (target != null && !(target instanceof StartupPageListItem)) { + target = target.parentNode; + } + return target; + }, + + /* + * Handles the dragstart event. + * @param {Event} e The dragstart event. + * @private + */ + handleDragStart_: function(e) { + // Prevent dragging if the list is disabled. + if (this.disabled) { + e.preventDefault(); + return false; + } + + var target = e.target; + // StartupPageListItem should be the only draggable element type in the + // page but let's make sure. + if (target instanceof StartupPageListItem) { + this.draggedItem = target; + this.draggedItem.editable = false; + e.dataTransfer.effectAllowed = 'move'; + // We need to put some kind of data in the drag or it will be + // ignored. Use the URL in case the user drags to a text field or the + // desktop. + e.dataTransfer.setData('text/plain', target.urlField_.value); + } + }, + + /* + * Handles the dragenter event. + * @param {Event} e The dragenter event. + * @private + */ + handleDragEnter_: function(e) { + e.preventDefault(); + }, + + /* + * Handles the dragover event. + * @param {Event} e The dragover event. + * @private + */ + handleDragOver_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + // Determines whether the drop target is to accept the drop. + // The drop is only successful on another StartupPageListItem. + if (!(dropTarget instanceof StartupPageListItem) || + dropTarget == this.draggedItem || dropTarget.isPlaceholder) { + this.hideDropMarker_(); + return; + } + // Compute the drop postion. Should we move the dragged item to + // below or above the drop target? + var rect = dropTarget.getBoundingClientRect(); + var dy = e.clientY - rect.top; + var yRatio = dy / rect.height; + var dropPos = yRatio <= .5 ? 'above' : 'below'; + this.dropPos = dropPos; + this.showDropMarker_(dropTarget, dropPos); + e.preventDefault(); + }, + + /* + * Handles the drop event. + * @param {Event} e The drop event. + * @private + */ + handleDrop_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + this.hideDropMarker_(); + + // Insert the selection at the new position. + var newIndex = this.dataModel.indexOf(dropTarget.pageInfo_); + if (this.dropPos == 'below') + newIndex += 1; + + var selected = this.selectionModel.selectedIndexes; + var stringized_selected = []; + for (var j = 0; j < selected.length; j++) + stringized_selected.push(String(selected[j])); + + chrome.send('dragDropStartupPage', + [String(newIndex), stringized_selected] ); + }, + + /* + * Handles the dragleave event. + * @param {Event} e The dragleave event + * @private + */ + handleDragLeave_: function(e) { + this.hideDropMarker_(); + }, + + /** + * Handles the dragend event. + * @param {Event} e The dragend event + * @private + */ + handleDragEnd_: function(e) { + this.draggedItem.editable = true; + this.draggedItem.updateEditState(); + }, + + /* + * Shows and positions the marker to indicate the drop target. + * @param {HTMLElement} target The current target list item of drop + * @param {string} pos 'below' or 'above' + * @private + */ + showDropMarker_ : function(target, pos) { + window.clearTimeout(this.hideDropMarkerTimer_); + var marker = $('startupPagesListDropmarker'); + var rect = target.getBoundingClientRect(); + var markerHeight = 6; + if (pos == 'above') { + marker.style.top = (rect.top - markerHeight/2) + 'px'; + } else { + marker.style.top = (rect.bottom - markerHeight/2) + 'px'; + } + marker.style.width = rect.width + 'px'; + marker.style.left = rect.left + 'px'; + marker.style.display = 'block'; + }, + + /* + * Hides the drop marker. + * @private + */ + hideDropMarker_ : function() { + // Hide the marker in a timeout to reduce flickering as we move between + // valid drop targets. + window.clearTimeout(this.hideDropMarkerTimer_); + this.hideDropMarkerTimer_ = window.setTimeout(function() { + $('startupPagesListDropmarker').style.display = ''; + }, 100); + }, + }; + + return { + StartupPageList: StartupPageList + }; +}); diff --git a/chrome/browser/resources/options2/certificate_backup_overlay.html b/chrome/browser/resources/options2/certificate_backup_overlay.html new file mode 100644 index 0000000..b0bcea3 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_backup_overlay.html @@ -0,0 +1,38 @@ +<div id="certificateBackupOverlay" class="page" hidden> + <h1 i18n-content="certificateExportPasswordDescription"></h1> + <div class="content-area"> + <table> + <tr> + <td> + <label for="certificateBackupPassword"> + <span i18n-content="certificatePasswordLabel"></span> + </label> + </td> + <td> + <input id="certificateBackupPassword" type="password"> + </td> + </tr> + <tr> + <td> + <label for="certificateBackupPassword2"> + <span i18n-content="certificateConfirmPasswordLabel"></span> + </label> + </td> + <td> + <input id="certificateBackupPassword2" type="password"> + </td> + </tr> + </table> + <p> + <span i18n-content="certificateExportPasswordHelp"></span> + </p> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateBackupCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateBackupOkButton" type="submit" i18n-content="ok" + disabled></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/certificate_backup_overlay.js b/chrome/browser/resources/options2/certificate_backup_overlay.js new file mode 100644 index 0000000..e700347 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_backup_overlay.js @@ -0,0 +1,116 @@ +// Copyright (c) 2010 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('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * CertificateBackupOverlay class + * Encapsulated handling of the 'enter backup password' overlay page. + * @class + */ + function CertificateBackupOverlay() { + OptionsPage.call(this, 'certificateBackupOverlay', + '', + 'certificateBackupOverlay'); + } + + cr.addSingletonGetter(CertificateBackupOverlay); + + CertificateBackupOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('certificateBackupCancelButton').onclick = function(event) { + self.cancelBackup_(); + } + $('certificateBackupOkButton').onclick = function(event) { + self.finishBackup_(); + } + $('certificateBackupPassword').oninput = + $('certificateBackupPassword2').oninput = function(event) { + self.comparePasswords_(); + } + + self.clearInputFields_(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + OptionsPage.closeOverlay(); + }, + + /** + * Attempt the Backup operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishBackup_: function() { + chrome.send('exportPersonalCertificatePasswordSelected', + [$('certificateBackupPassword').value]); + $('certificateBackupCancelButton').disabled = true; + $('certificateBackupOkButton').disabled = true; + $('certificateBackupPassword').disabled = true; + $('certificateBackupPassword2').disabled = true; + }, + + /** + * Cancel the Backup operation. + * @private + */ + cancelBackup_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + + /** + * Compares the password fields and sets the button state appropriately. + * @private + */ + comparePasswords_: function() { + var password1 = $('certificateBackupPassword').value; + var password2 = $('certificateBackupPassword2').value; + $('certificateBackupOkButton').disabled = + !password1 || password1 != password2; + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('certificateBackupPassword').value = ''; + $('certificateBackupPassword2').value = ''; + $('certificateBackupPassword').disabled = false; + $('certificateBackupPassword2').disabled = false; + $('certificateBackupCancelButton').disabled = false; + $('certificateBackupOkButton').disabled = true; + }, + }; + + CertificateBackupOverlay.show = function() { + CertificateBackupOverlay.getInstance().clearInputFields_(); + OptionsPage.navigateToPage('certificateBackupOverlay'); + }; + + CertificateBackupOverlay.dismiss = function() { + CertificateBackupOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateBackupOverlay: CertificateBackupOverlay + }; +}); diff --git a/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.html b/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.html new file mode 100644 index 0000000..b45bfc9 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.html @@ -0,0 +1,33 @@ +<div id="certificateEditCaTrustOverlay" class="page" hidden> + <div class="content-area"> + <p> + <span id="certificateEditCaTrustDescription"></span> + </p> + <p> + <span i18n-content="certificateEditTrustLabel"></span> + <br> + <label id="certificateCaTrustSSLLabel"> + <input id="certificateCaTrustSSLCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustSSLLabel"></span> + </label> + <br> + <label id="certificateCaTrustEmailLabel"> + <input id="certificateCaTrustEmailCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustEmailLabel"></span> + </label> + <br> + <label id="certificateCaTrustObjSignLabel"> + <input id="certificateCaTrustObjSignCheckbox" type="checkbox"> + <span i18n-content="certificateCaTrustObjSignLabel"></span> + </label> + </p> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateEditCaTrustCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateEditCaTrustOkButton" type="submit" + i18n-content="ok"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.js b/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.js new file mode 100644 index 0000000..86fa5f7 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_edit_ca_trust_overlay.js @@ -0,0 +1,164 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * CertificateEditCaTrustOverlay class + * Encapsulated handling of the 'edit ca trust' and 'import ca' overlay pages. + * @class + */ + function CertificateEditCaTrustOverlay() { + OptionsPage.call(this, 'certificateEditCaTrustOverlay', + '', + 'certificateEditCaTrustOverlay'); + } + + cr.addSingletonGetter(CertificateEditCaTrustOverlay); + + CertificateEditCaTrustOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + OptionsPage.closeOverlay(); + }, + + /** + * Enables or disables input fields. + * @private + */ + enableInputs_: function(enabled) { + $('certificateCaTrustSSLCheckbox').disabled = + $('certificateCaTrustEmailCheckbox').disabled = + $('certificateCaTrustObjSignCheckbox').disabled = + $('certificateEditCaTrustCancelButton').disabled = + $('certificateEditCaTrustOkButton').disabled = !enabled; + }, + + /** + * Attempt the Edit operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishEdit_: function() { + // TODO(mattm): Send checked values as booleans. For now send them as + // strings, since WebUIBindings::send does not support any other types :( + chrome.send('editCaCertificateTrust', + [this.certId, + $('certificateCaTrustSSLCheckbox').checked.toString(), + $('certificateCaTrustEmailCheckbox').checked.toString(), + $('certificateCaTrustObjSignCheckbox').checked.toString()]); + this.enableInputs_(false); + }, + + /** + * Cancel the Edit operation. + * @private + */ + cancelEdit_: function() { + this.dismissOverlay_(); + }, + + /** + * Attempt the Import operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishImport_: function() { + // TODO(mattm): Send checked values as booleans. For now send them as + // strings, since WebUIBindings::send does not support any other types :( + chrome.send('importCaCertificateTrustSelected', + [$('certificateCaTrustSSLCheckbox').checked.toString(), + $('certificateCaTrustEmailCheckbox').checked.toString(), + $('certificateCaTrustObjSignCheckbox').checked.toString()]); + this.enableInputs_(false); + }, + + /** + * Cancel the Import operation. + * @private + */ + cancelImport_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + }; + + /** + * Callback from CertificateManagerHandler with the trust values. + * @param {boolean} trustSSL The initial value of SSL trust checkbox. + * @param {boolean} trustEmail The initial value of Email trust checkbox. + * @param {boolean} trustObjSign The initial value of Object Signing trust + */ + CertificateEditCaTrustOverlay.populateTrust = function( + trustSSL, trustEmail, trustObjSign) { + $('certificateCaTrustSSLCheckbox').checked = trustSSL; + $('certificateCaTrustEmailCheckbox').checked = trustEmail; + $('certificateCaTrustObjSignCheckbox').checked = trustObjSign; + CertificateEditCaTrustOverlay.getInstance().enableInputs_(true); + } + + /** + * Show the Edit CA Trust overlay. + * @param {string} certId The id of the certificate to be passed to the + * certificate manager model. + * @param {string} certName The display name of the certificate. + * checkbox. + */ + CertificateEditCaTrustOverlay.show = function(certId, certName) { + var self = CertificateEditCaTrustOverlay.getInstance(); + self.certId = certId; + $('certificateEditCaTrustCancelButton').onclick = function(event) { + self.cancelEdit_(); + } + $('certificateEditCaTrustOkButton').onclick = function(event) { + self.finishEdit_(); + } + $('certificateEditCaTrustDescription').textContent = + localStrings.getStringF('certificateEditCaTrustDescriptionFormat', + certName); + self.enableInputs_(false); + OptionsPage.navigateToPage('certificateEditCaTrustOverlay'); + chrome.send('getCaCertificateTrust', [certId]); + } + + /** + * Show the Import CA overlay. + * @param {string} certId The id of the certificate to be passed to the + * certificate manager model. + * @param {string} certName The display name of the certificate. + * checkbox. + */ + CertificateEditCaTrustOverlay.showImport = function(certName) { + var self = CertificateEditCaTrustOverlay.getInstance(); + // TODO(mattm): do we want a view certificate button here like firefox has? + $('certificateEditCaTrustCancelButton').onclick = function(event) { + self.cancelImport_(); + } + $('certificateEditCaTrustOkButton').onclick = function(event) { + self.finishImport_(); + } + $('certificateEditCaTrustDescription').textContent = + localStrings.getStringF('certificateImportCaDescriptionFormat', + certName); + CertificateEditCaTrustOverlay.populateTrust(false, false, false); + OptionsPage.navigateToPage('certificateEditCaTrustOverlay'); + } + + CertificateEditCaTrustOverlay.dismiss = function() { + CertificateEditCaTrustOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateEditCaTrustOverlay: CertificateEditCaTrustOverlay + }; +}); diff --git a/chrome/browser/resources/options2/certificate_import_error_overlay.html b/chrome/browser/resources/options2/certificate_import_error_overlay.html new file mode 100644 index 0000000..02a7e2d --- /dev/null +++ b/chrome/browser/resources/options2/certificate_import_error_overlay.html @@ -0,0 +1,13 @@ +<div id="certificateImportErrorOverlay" class="page" hidden> + <h1 id="certificateImportErrorOverlayTitle"></h1> + <div class="content-area"> + <div id="certificateImportErrorOverlayMessage"></div> + <ul id="certificateImportErrorOverlayCertErrors"></ul> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateImportErrorOverlayOk" type="submit" + i18n-content="ok"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/certificate_import_error_overlay.js b/chrome/browser/resources/options2/certificate_import_error_overlay.js new file mode 100644 index 0000000..efc99ff --- /dev/null +++ b/chrome/browser/resources/options2/certificate_import_error_overlay.js @@ -0,0 +1,68 @@ +// Copyright (c) 2010 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('options', function() { + + var OptionsPage = options.OptionsPage; + + /** + * CertificateImportErrorOverlay class + * Displays a list of certificates and errors. + * @class + */ + function CertificateImportErrorOverlay() { + OptionsPage.call(this, 'certificateImportErrorOverlay', '', + 'certificateImportErrorOverlay'); + } + + cr.addSingletonGetter(CertificateImportErrorOverlay); + + CertificateImportErrorOverlay.prototype = { + // Inherit CertificateImportErrorOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + $('certificateImportErrorOverlayOk').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + }, + }; + + /** + * Show an alert overlay with the given message, button titles, and + * callbacks. + * @param {string} title The alert title to display to the user. + * @param {string} message The alert message to display to the user. + * @param {Array} certErrors The list of cert errors. Each error should have + * a .name and .error attribute. + */ + CertificateImportErrorOverlay.show = function(title, message, certErrors) { + $('certificateImportErrorOverlayTitle').textContent = title; + $('certificateImportErrorOverlayMessage').textContent = message; + + ul = $('certificateImportErrorOverlayCertErrors'); + ul.innerHTML = ''; + for (var i = 0; i < certErrors.length; ++i) { + li = document.createElement("li"); + li.textContent = localStrings.getStringF('certificateImportErrorFormat', + certErrors[i].name, + certErrors[i].error); + ul.appendChild(li); + } + + OptionsPage.navigateToPage('certificateImportErrorOverlay'); + } + + // Export + return { + CertificateImportErrorOverlay: CertificateImportErrorOverlay + }; + +}); diff --git a/chrome/browser/resources/options2/certificate_manager.css b/chrome/browser/resources/options2/certificate_manager.css new file mode 100644 index 0000000..a88d231 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_manager.css @@ -0,0 +1,15 @@ +/* +Copyright (c) 2010 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. +*/ + +.certificate-tree-table { + width: 100%; +} + +.certificate-tree { + /* TODO(mattm): BLAH. Make this not statically sized. */ + height: 300px; +} + diff --git a/chrome/browser/resources/options2/certificate_manager.html b/chrome/browser/resources/options2/certificate_manager.html new file mode 100644 index 0000000..68ea65a --- /dev/null +++ b/chrome/browser/resources/options2/certificate_manager.html @@ -0,0 +1,129 @@ +<div id="certificateManagerPage" class="page" hidden> + <h1 i18n-content="certificateManagerPage"></h1> + <!-- Navigation tabs --> + <div class="subpages-nav-tabs"> + <span id="personal-certs-nav-tab" class="tab" + tab-contents="personalCertsTab"> + <span class="tab-label" + i18n-content="personalCertsTabTitle"></span> + <span class="active-tab-label" + i18n-content="personalCertsTabTitle"></span> + </span> + <span id="server-certs-nav-tab" class="tab" + tab-contents="serverCertsTab"> + <span class="tab-label" + i18n-content="serverCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="serverCertsTabTitle"></span> + </span> + <span id="ca-certs-nav-tab" class="tab" + tab-contents="caCertsTab"> + <span class="tab-label" i18n-content="caCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="caCertsTabTitle"></span> + </span> + <span id="other-certs-nav-tab" class="tab" + tab-contents="otherCertsTab"> + <span class="tab-label" + i18n-content="unknownCertsTabTitle"></span> + <span class="active-tab-label" i18n-content="unknownCertsTabTitle"></span> + </span> + </div> + <!-- TODO(mattm): get rid of use of tables? --> + <!-- Tab contents --> + <div id="personalCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="personalCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="personalCertsTab-tree" class="certificate-tree" + icon-visibility="parent"></tree> + </td></tr> + <tr><td> + <button id="personalCertsTab-view" i18n-content="view_certificate" + disabled></button> + <!-- TODO(mattm): + <button id="personalCertsTab-backup-all" + i18n-content="export_all_certificates" + disabled></button> + --> + <button id="personalCertsTab-import" i18n-content="import_certificate" + ></button> + <if expr="pp_ifdef('chromeos')"> + <button id="personalCertsTab-import-and-bind" + i18n-content="importAndBindCertificate" disabled></button> + </if> + <button id="personalCertsTab-backup" i18n-content="export_certificate" + disabled></button> + <button id="personalCertsTab-delete" i18n-content="delete_certificate" + disabled></button> + </td></tr> + </table> + </div> + <div id="serverCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="serverCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="serverCertsTab-tree" class="certificate-tree" + icon-visibility="parent"></tree> + </td></tr> + <tr><td> + <button id="serverCertsTab-view" i18n-content="view_certificate" + disabled></button> + <!-- TODO(mattm): + <button id="serverCertsTab-edit" i18n-content="edit_certificate" + disabled></button> + --> + <button id="serverCertsTab-import" i18n-content="import_certificate" + ></button> + <button id="serverCertsTab-export" i18n-content="export_certificate" + disabled></button> + <button id="serverCertsTab-delete" i18n-content="delete_certificate" + disabled></button> + </td></tr> + </table> + </div> + <div id="caCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="caCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="caCertsTab-tree" class="certificate-tree" + icon-visibility="parent"></tree> + </td></tr> + <tr><td> + <button id="caCertsTab-view" i18n-content="view_certificate" + disabled></button> + <button id="caCertsTab-edit" i18n-content="edit_certificate" + disabled></button> + <button id="caCertsTab-import" i18n-content="import_certificate" + ></button> + <button id="caCertsTab-export" i18n-content="export_certificate" + disabled></button> + <button id="caCertsTab-delete" i18n-content="delete_certificate" + disabled></button> + </td></tr> + </table> + </div> + <div id="otherCertsTab" class="subpages-tab-contents"> + <table class="certificate-tree-table"> + <tr><td> + <span i18n-content="unknownCertsTabDescription"></span> + </td></tr> + <tr><td> + <tree id="otherCertsTab-tree" class="certificate-tree" + icon-visibility="parent"></tree> + </td></tr> + <tr><td> + <button id="otherCertsTab-view" i18n-content="view_certificate" + disabled></button> + <button id="otherCertsTab-export" i18n-content="export_certificate" + disabled></button> + <button id="otherCertsTab-delete" i18n-content="delete_certificate" + disabled></button> + </td></tr> + </table> + </div> +</div> diff --git a/chrome/browser/resources/options2/certificate_manager.js b/chrome/browser/resources/options2/certificate_manager.js new file mode 100644 index 0000000..59a7efd --- /dev/null +++ b/chrome/browser/resources/options2/certificate_manager.js @@ -0,0 +1,253 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // CertificateManagerTab class: + + /** + * blah + * @param {!string} id The id of this tab. + */ + function CertificateManagerTab(id) { + this.tree = $(id + '-tree'); + + options.CertificatesTree.decorate(this.tree); + this.tree.addEventListener('change', + this.handleCertificatesTreeChange_.bind(this)); + + var tree = this.tree; + + this.viewButton = $(id + '-view'); + this.viewButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('viewCertificate', [selected.data.id]); + } + + this.editButton = $(id + '-edit'); + if (this.editButton !== null) { + if (id == 'serverCertsTab') { + this.editButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('editServerCertificate', [selected.data.id]); + } + } else if (id == 'caCertsTab') { + this.editButton.onclick = function(e) { + var data = tree.selectedItem.data; + CertificateEditCaTrustOverlay.show(data.id, data.name); + } + } + } + + this.backupButton = $(id + '-backup'); + if (this.backupButton !== null) { + this.backupButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('exportPersonalCertificate', [selected.data.id]); + } + } + + this.backupAllButton = $(id + '-backup-all'); + if (this.backupAllButton !== null) { + this.backupAllButton.onclick = function(e) { + chrome.send('exportAllPersonalCertificates', []); + } + } + + this.importButton = $(id + '-import'); + if (this.importButton !== null) { + if (id == 'personalCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importPersonalCertificate', [false]); + } + } else if (id == 'serverCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importServerCertificate', []); + } + } else if (id == 'caCertsTab') { + this.importButton.onclick = function(e) { + chrome.send('importCaCertificate', []); + } + } + } + + this.importAndBindButton = $(id + '-import-and-bind'); + if (this.importAndBindButton !== null) { + if (id == 'personalCertsTab') { + this.importAndBindButton.onclick = function(e) { + chrome.send('importPersonalCertificate', [true]); + } + } + } + + this.exportButton = $(id + '-export'); + if (this.exportButton !== null) { + this.exportButton.onclick = function(e) { + var selected = tree.selectedItem; + chrome.send('exportCertificate', [selected.data.id]); + } + } + + this.deleteButton = $(id + '-delete'); + this.deleteButton.onclick = function(e) { + var data = tree.selectedItem.data; + AlertOverlay.show( + localStrings.getStringF(id + 'DeleteConfirm', data.name), + localStrings.getString(id + 'DeleteImpact'), + localStrings.getString('ok'), + localStrings.getString('cancel'), + function() { + tree.selectedItem = null; + chrome.send('deleteCertificate', [data.id]); + }); + } + } + + CertificateManagerTab.prototype = { + + /** + * Update button state. + * @private + * @param {!Object} data The data of the selected item. + */ + updateButtonState: function(data) { + var isCert = !!data && data.id.substr(0, 5) == 'cert-'; + var readOnly = !!data && data.readonly; + var hasChildren = this.tree.items.length > 0; + this.viewButton.disabled = !isCert; + if (this.editButton !== null) + this.editButton.disabled = !isCert; + if (this.backupButton !== null) + this.backupButton.disabled = !isCert; + if (this.backupAllButton !== null) + this.backupAllButton.disabled = !hasChildren; + if (this.exportButton !== null) + this.exportButton.disabled = !isCert; + this.deleteButton.disabled = !isCert || readOnly; + }, + + /** + * Handles certificate tree selection change. + * @private + * @param {!Event} e The change event object. + */ + handleCertificatesTreeChange_: function(e) { + var data = null; + if (this.tree.selectedItem) { + data = this.tree.selectedItem.data; + } + + this.updateButtonState(data); + }, + + } + + // TODO(xiyuan): Use notification from backend instead of polling. + // TPM token check polling timer. + var tpmPollingTimer; + + // Initiate tpm token check if needed. + function checkTpmToken() { + var importAndBindButton = $('personalCertsTab-import-and-bind'); + + if (importAndBindButton && importAndBindButton.disabled) + chrome.send('checkTpmTokenReady'); + } + + // Stop tpm polling timer. + function stopTpmTokenCheckPolling() { + if (tpmPollingTimer) { + window.clearTimeout(tpmPollingTimer); + tpmPollingTimer = undefined; + } + } + + ///////////////////////////////////////////////////////////////////////////// + // CertificateManager class: + + /** + * Encapsulated handling of ChromeOS accounts options page. + * @constructor + */ + function CertificateManager(model) { + OptionsPage.call(this, 'certificates', + templateData.certificateManagerPageTabTitle, + 'certificateManagerPage'); + } + + cr.addSingletonGetter(CertificateManager); + + CertificateManager.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.personalTab = new CertificateManagerTab('personalCertsTab'); + this.serverTab = new CertificateManagerTab('serverCertsTab'); + this.caTab = new CertificateManagerTab('caCertsTab'); + this.otherTab = new CertificateManagerTab('otherCertsTab'); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + }, + + initalized_: false, + + /** + * Handler for OptionsPage's visible property change event. + * @private + * @param {Event} e Property change event. + */ + handleVisibleChange_: function(e) { + if (!this.initalized_ && this.visible) { + this.initalized_ = true; + OptionsPage.showTab($('personal-certs-nav-tab')); + chrome.send('populateCertificateManager'); + } + + if (cr.isChromeOS) { + // Ensure TPM token check on visible and stop polling when hidden. + if (this.visible) + checkTpmToken(); + else + stopTpmTokenCheckPolling(); + } + } + }; + + // CertificateManagerHandler callbacks. + CertificateManager.onPopulateTree = function(args) { + $(args[0]).populate(args[1]); + }; + + CertificateManager.exportPersonalAskPassword = function(args) { + CertificateBackupOverlay.show(); + }; + + CertificateManager.importPersonalAskPassword = function(args) { + CertificateRestoreOverlay.show(); + }; + + CertificateManager.onCheckTpmTokenReady = function(ready) { + var importAndBindButton = $('personalCertsTab-import-and-bind'); + if (importAndBindButton) { + importAndBindButton.disabled = !ready; + + // Check again after 5 seconds if Tpm is not ready and certificate manager + // is still visible. + if (!ready && CertificateManager.getInstance().visible) + tpmPollingTimer = window.setTimeout(checkTpmToken, 5000); + } + }; + + // Export + return { + CertificateManagerTab: CertificateManagerTab, + CertificateManager: CertificateManager + }; + +}); diff --git a/chrome/browser/resources/options2/certificate_restore_overlay.html b/chrome/browser/resources/options2/certificate_restore_overlay.html new file mode 100644 index 0000000..30b380d --- /dev/null +++ b/chrome/browser/resources/options2/certificate_restore_overlay.html @@ -0,0 +1,17 @@ +<div id="certificateRestoreOverlay" class="page" hidden> + <h1 i18n-content="certificateRestorePasswordDescription"></h1> + <div class="content-area"> + <label id="certificateRestorePasswordLabel"> + <span i18n-content="certificatePasswordLabel"></span> + <input id="certificateRestorePassword" type="password"> + </label> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="certificateRestoreCancelButton" type="reset" + i18n-content="cancel"></button> + <button id="certificateRestoreOkButton" type="submit" i18n-content="ok"> + </button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/certificate_restore_overlay.js b/chrome/browser/resources/options2/certificate_restore_overlay.js new file mode 100644 index 0000000..d76a329 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_restore_overlay.js @@ -0,0 +1,97 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * CertificateRestoreOverlay class + * Encapsulated handling of the 'enter restore password' overlay page. + * @class + */ + function CertificateRestoreOverlay() { + OptionsPage.call(this, 'certificateRestore', + '', + 'certificateRestoreOverlay'); + } + + cr.addSingletonGetter(CertificateRestoreOverlay); + + CertificateRestoreOverlay.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var self = this; + $('certificateRestoreCancelButton').onclick = function(event) { + self.cancelRestore_(); + } + $('certificateRestoreOkButton').onclick = function(event) { + self.finishRestore_(); + } + + self.clearInputFields_(); + }, + + /** + * Clears any uncommitted input, and dismisses the overlay. + * @private + */ + dismissOverlay_: function() { + this.clearInputFields_(); + OptionsPage.closeOverlay(); + }, + + /** + * Attempt the restore operation. + * The overlay will be left up with inputs disabled until the backend + * finishes and dismisses it. + * @private + */ + finishRestore_: function() { + chrome.send('importPersonalCertificatePasswordSelected', + [$('certificateRestorePassword').value]); + $('certificateRestoreCancelButton').disabled = true; + $('certificateRestoreOkButton').disabled = true; + }, + + /** + * Cancel the restore operation. + * @private + */ + cancelRestore_: function() { + chrome.send('cancelImportExportCertificate'); + this.dismissOverlay_(); + }, + + /** + * Clears the value of each input field. + * @private + */ + clearInputFields_: function() { + $('certificateRestorePassword').value = ''; + $('certificateRestoreCancelButton').disabled = false; + $('certificateRestoreOkButton').disabled = false; + }, + }; + + CertificateRestoreOverlay.show = function() { + CertificateRestoreOverlay.getInstance().clearInputFields_(); + OptionsPage.navigateToPage('certificateRestore'); + }; + + CertificateRestoreOverlay.dismiss = function() { + CertificateRestoreOverlay.getInstance().dismissOverlay_(); + }; + + // Export + return { + CertificateRestoreOverlay: CertificateRestoreOverlay + }; + +}); diff --git a/chrome/browser/resources/options2/certificate_tree.css b/chrome/browser/resources/options2/certificate_tree.css new file mode 100644 index 0000000..566eb21 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_tree.css @@ -0,0 +1,8 @@ +span.certUntrusted { + background-color: pink; + border: 1px solid red; + border-radius: 3px; + margin-right: 3px; + padding-left: 1px; + padding-right: 1px; +} diff --git a/chrome/browser/resources/options2/certificate_tree.js b/chrome/browser/resources/options2/certificate_tree.js new file mode 100644 index 0000000..ee5d075 --- /dev/null +++ b/chrome/browser/resources/options2/certificate_tree.js @@ -0,0 +1,128 @@ +// 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. + +cr.define('options', function() { + const Tree = cr.ui.Tree; + const TreeItem = cr.ui.TreeItem; + + /** + * Creates a new tree item for certificate data. + * @param {Object=} data Data used to create a certificate tree item. + * @constructor + * @extends {TreeItem} + */ + function CertificateTreeItem(data) { + // TODO(mattm): other columns + var treeItem = new TreeItem({ + label: data.name, + data: data + }); + treeItem.__proto__ = CertificateTreeItem.prototype; + + if (data.icon) { + treeItem.icon = data.icon; + } + + if (data.untrusted) { + var badge = document.createElement('span'); + badge.setAttribute('class', 'certUntrusted'); + badge.textContent = localStrings.getString("badgeCertUntrusted"); + treeItem.labelElement.insertBefore( + badge, treeItem.labelElement.firstChild); + } + + return treeItem; + } + + CertificateTreeItem.prototype = { + __proto__: TreeItem.prototype, + + /** + * The tree path id/. + * @type {string} + */ + get pathId() { + var parent = this.parentItem; + if (parent && parent instanceof CertificateTreeItem) { + return parent.pathId + ',' + this.data.id; + } else { + return this.data.id; + } + } + }; + + /** + * Creates a new cookies tree. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {Tree} + */ + var CertificatesTree = cr.ui.define('tree'); + + CertificatesTree.prototype = { + __proto__: Tree.prototype, + + /** @inheritDoc */ + decorate: function() { + Tree.prototype.decorate.call(this); + this.treeLookup_ = {}; + }, + + /** @inheritDoc */ + addAt: function(child, index) { + Tree.prototype.addAt.call(this, child, index); + if (child.data && child.data.id) + this.treeLookup_[child.data.id] = child; + }, + + /** @inheritDoc */ + remove: function(child) { + Tree.prototype.remove.call(this, child); + if (child.data && child.data.id) + delete this.treeLookup_[child.data.id]; + }, + + /** + * Clears the tree. + */ + clear: function() { + // Remove all fields without recreating the object since other code + // references it. + for (var id in this.treeLookup_){ + delete this.treeLookup_[id]; + } + this.textContent = ''; + }, + + /** + * Populate the tree. + * @param {Array} nodesData Nodes data array. + */ + populate: function(nodesData) { + this.clear(); + + for (var i = 0; i < nodesData.length; ++i) { + var subnodes = nodesData[i]['subnodes']; + delete nodesData[i]['subnodes']; + + var item = new CertificateTreeItem(nodesData[i]); + this.addAt(item, i); + + for (var j = 0; j < subnodes.length; ++j) { + var subitem = new CertificateTreeItem(subnodes[j]); + item.addAt(subitem, j); + } + // Make tree expanded by default. + item.expanded = true; + } + + cr.dispatchSimpleEvent(this, 'change'); + }, + }; + + return { + CertificatesTree: CertificatesTree + }; +}); + diff --git a/chrome/browser/resources/options2/chromeos/accounts_options.html b/chrome/browser/resources/options2/chromeos/accounts_options.html new file mode 100644 index 0000000..b324c4f --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/accounts_options.html @@ -0,0 +1,63 @@ +<div id="accountsPage" class="page" hidden> + <h1 i18n-content="accountsPage"></h1> + <div class="displaytable"> + <section> + <div class="option"> + <div id="ownerOnlyWarning" hidden> + <span i18n-content="owner_only"></span> + <span i18n-content="owner_user_id"></span> + </div> + <table class="option-control-table"> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="allowBwsiCheck" pref="cros.accounts.allowBWSI" + type="checkbox"> + <span i18n-content="allow_BWSI"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="showUserNamesCheck" + pref="cros.accounts.showUserNamesOnSignIn" type="checkbox"> + <span i18n-content="show_user_on_signin"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name"> + <div class="checkbox"> + <label> + <input id="useWhitelistCheck" pref="cros.accounts.allowGuest" + type="checkbox" inverted_pref> + <span i18n-content="use_whitelist"></span> + </label> + </div> + </td> + </tr> + <tr><td> </td></tr> + <tr><td> + <table class="user-list-table"> + <tr><td> + <list id="userList"></list> + </td></tr> + <tr><td class="user-name-edit-row"> + <label><span i18n-content="add_users"></span><br> + <input id="userNameEdit" type="text" + i18n-values="placeholder:username_edit_hint"> + </span> + </label> + </td></tr> + </table> + </td></tr> + </table> + </div> + </section> + </div> +</div> diff --git a/chrome/browser/resources/options2/chromeos/accounts_options.js b/chrome/browser/resources/options2/chromeos/accounts_options.js new file mode 100644 index 0000000..af6bf9c --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/accounts_options.js @@ -0,0 +1,165 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // AccountsOptions class: + + /** + * Encapsulated handling of ChromeOS accounts options page. + * @constructor + */ + function AccountsOptions(model) { + OptionsPage.call(this, 'accounts', templateData.accountsPageTabTitle, + 'accountsPage'); + // Whether to show the whitelist. + this.showWhitelist_ = false; + } + + cr.addSingletonGetter(AccountsOptions); + + AccountsOptions.prototype = { + // Inherit AccountsOptions from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initializes AccountsOptions page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up accounts page. + var userList = $('userList'); + userList.addEventListener('remove', this.handleRemoveUser_); + + var userNameEdit = $('userNameEdit'); + options.accounts.UserNameEdit.decorate(userNameEdit); + userNameEdit.addEventListener('add', this.handleAddUser_); + + // If the current user is not the owner, show some warning, + // and do not show the user list. + this.showWhitelist_ = AccountsOptions.currentUserIsOwner(); + if (this.showWhitelist_) { + options.accounts.UserList.decorate(userList); + } else { + if (!AccountsOptions.whitelistIsManaged()) { + $('ownerOnlyWarning').hidden = false; + } else { + this.managed = true; + } + } + + this.addEventListener('visibleChange', this.handleVisibleChange_); + + $('useWhitelistCheck').addEventListener('change', + this.handleUseWhitelistCheckChange_.bind(this)); + + Preferences.getInstance().addEventListener( + $('useWhitelistCheck').pref, + this.handleUseWhitelistPrefChange_.bind(this)); + }, + + /** + * Update user list control state. + * @private + */ + updateControls_: function() { + $('userList').disabled = + $('userNameEdit').disabled = !this.showWhitelist_ || + AccountsOptions.whitelistIsManaged() || + !$('useWhitelistCheck').checked; + }, + + /** + * Handler for OptionsPage's visible property change event. + * @private + * @param {Event} e Property change event. + */ + handleVisibleChange_: function(e) { + if (this.visible) { + this.updateControls_(); + if (this.showWhitelist_) + $('userList').redraw(); + } + }, + + /** + * Handler for allow guest check change. + * @private + */ + handleUseWhitelistCheckChange_: function(e) { + // Whitelist existing users when guest login is being disabled. + if ($('useWhitelistCheck').checked) { + chrome.send('whitelistExistingUsers', []); + } + + this.updateControls_(); + }, + + /** + * handler for allow guest pref change. + * @private + */ + handleUseWhitelistPrefChange_: function(e) { + this.updateControls_(); + }, + + /** + * Handler for "add" event fired from userNameEdit. + * @private + * @param {Event} e Add event fired from userNameEdit. + */ + handleAddUser_: function(e) { + chrome.send('whitelistUser', [e.user.email, e.user.name]); + }, + + /** + * Handler for "remove" event fired from userList. + * @private + * @param {Event} e Remove event fired from userList. + */ + handleRemoveUser_: function(e) { + chrome.send('unwhitelistUser', [e.user.username]); + } + }; + + /** + * Returns whether the current user is owner or not. + */ + AccountsOptions.currentUserIsOwner = function() { + return localStrings.getString('current_user_is_owner') == 'true'; + }; + + /** + * Returns whether we're currently in guest mode. + */ + AccountsOptions.loggedInAsGuest = function() { + return localStrings.getString('logged_in_as_guest') == 'true'; + }; + + /** + * Returns whether the whitelist is managed by policy or not. + */ + AccountsOptions.whitelistIsManaged = function() { + return localStrings.getString('whitelist_is_managed') == 'true'; + }; + + /** + * Update account picture. + * @param {string} username User for which to update the image. + */ + AccountsOptions.updateAccountPicture = function(username) { + if (this.showWhitelist_) + $('userList').updateAccountPicture(username); + }; + + // Export + return { + AccountsOptions: AccountsOptions + }; + +}); diff --git a/chrome/browser/resources/options2/chromeos/accounts_options_page.css b/chrome/browser/resources/options2/chromeos/accounts_options_page.css new file mode 100644 index 0000000..e32d550 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/accounts_options_page.css @@ -0,0 +1,87 @@ +.user-list-table { + border: 1px solid lightgrey; + border-collapse: collapse; + border-spacing: 0; +} + +.user-name-edit-row { + border: 1px solid lightgrey; + background-color: #ebeffa; + padding: 5px; +} + +.user-list-item { + padding: 2px; +} + +.user-icon { + border: 1px solid black; + width: 26px; + height: 26px; +} + +.user-email-label { + -webkit-margin-start: 10px; +} + +.user-name-label { + color: darkgray; + -webkit-margin-start: 10px; +} + +.user-email-name-block { + -webkit-box-flex: 1; + max-width: 318px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.remove-user-button { + background-image: url(chrome://theme/IDR_CLOSE_BAR); + width: 16px; + height: 16px; +} + +.remove-user-button:hover { + background-image: url(chrome://theme/IDR_CLOSE_BAR_H); +} + +#userList { + padding: 5px; + width: 366px; + height: 166px; +} + +#userList[disabled], +#userList[disabled] > [selected], +#userList[disabled] > :hover { + border-color: hsl(0, 0%, 85%); +} + +#userList[disabled] > [selected], +#userList[disabled] > :hover { + background-color: hsl(0,0%,90%); +} + +#userList[disabled] .remove-user-button { + visibility: hidden; +} + +#userNameEdit { + border: 1px solid lightgrey; + width: 366px; +} + +#ownerOnlyWarning { + margin-top: 10px; + margin-bottom: 10px; + padding-bottom: 1px; + -webkit-padding-start: 20px; + background-repeat: no-repeat; + background-image: url('warning.png'); +} + +input#userNameEdit:invalid { + background-color: #ff6666; +} diff --git a/chrome/browser/resources/options2/chromeos/accounts_user_list.js b/chrome/browser/resources/options2/chromeos/accounts_user_list.js new file mode 100644 index 0000000..3fe33b7 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/accounts_user_list.js @@ -0,0 +1,194 @@ +// 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. + +cr.define('options.accounts', function() { + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new user list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var UserList = cr.ui.define('list'); + + UserList.prototype = { + __proto__: List.prototype, + + pref: 'cros.accounts.users', + + /** @inheritDoc */ + decorate: function() { + List.prototype.decorate.call(this); + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + var self = this; + + // Listens to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.load_(event.value); + }); + }, + + createItem: function(user) { + return new UserListItem(user); + }, + + /** + * Finds the index of user by given username (canonicalized email). + * @private + * @param {string} username The username to look for. + * @return {number} The index of the found user or -1 if not found. + */ + indexOf_: function(username) { + var dataModel = this.dataModel; + if (!dataModel) + return -1; + + var length = dataModel.length; + for (var i = 0; i < length; ++i) { + var user = dataModel.item(i); + if (user.username == username) { + return i; + } + } + + return -1; + }, + + /** + * Update given user's account picture. + * @param {string} username User for which to update the image. + */ + updateAccountPicture: function(username) { + var index = this.indexOf_(username); + if (index >= 0) { + var item = this.getListItemByIndex(index); + if (item) + item.updatePicture(); + } + }, + + /** + * Loads given user list. + * @param {Array.<Object>} users An array of user info objects. + * @private + */ + load_: function(users) { + this.dataModel = new ArrayDataModel(users); + }, + + /** + * Removes given user from the list. + * @param {Object} user User info object to be removed from user list. + * @private + */ + removeUser_: function(user) { + var e = new Event('remove'); + e.user = user; + this.dispatchEvent(e); + } + }; + + /** + * Whether the user list is disabled. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(UserList, 'disabled', cr.PropertyKind.BOOL_ATTR); + + /** + * Creates a new user list item. + * @param user The user account this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function UserListItem(user) { + var el = cr.doc.createElement('div'); + el.user = user; + UserListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a user account item. + * @param {!HTMLElement} el The element to decorate. + */ + UserListItem.decorate = function(el) { + el.__proto__ = UserListItem.prototype; + el.decorate(); + }; + + UserListItem.prototype = { + __proto__: ListItem.prototype, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + this.className = 'user-list-item'; + + this.icon_ = this.ownerDocument.createElement('img'); + this.icon_.className = 'user-icon'; + this.updatePicture(); + + var labelEmail = this.ownerDocument.createElement('span'); + labelEmail.className = 'user-email-label'; + labelEmail.textContent = this.user.email; + + var labelName = this.ownerDocument.createElement('span'); + labelName.className = 'user-name-label'; + labelName.textContent = this.user.owner ? + localStrings.getStringF('username_format', this.user.name) : + this.user.name; + + var emailNameBlock = this.ownerDocument.createElement('div'); + emailNameBlock.className = 'user-email-name-block'; + emailNameBlock.appendChild(labelEmail); + emailNameBlock.appendChild(labelName); + emailNameBlock.title = this.user.owner ? + localStrings.getStringF('username_format', this.user.email) : + this.user.email; + + this.appendChild(this.icon_); + this.appendChild(emailNameBlock); + + if (!this.user.owner) { + var removeButton = this.ownerDocument.createElement('button'); + removeButton.className = + 'raw-button remove-user-button custom-appearance'; + removeButton.addEventListener( + 'click', this.handleRemoveButtonClick_.bind(this)); + this.appendChild(removeButton); + } + }, + + /** + * Handles click on the remove button. + * @param {Event} e Click event. + * @private + */ + handleRemoveButtonClick_: function(e) { + // Handle left button click + if (e.button == 0) + this.parentNode.removeUser_(this.user); + }, + + /** + * Reloads user picture. + */ + updatePicture: function() { + this.icon_.src = 'chrome://userimage/' + this.user.username + + '?id=' + (new Date()).getTime(); + } + }; + + return { + UserList: UserList + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/accounts_user_name_edit.js b/chrome/browser/resources/options2/chromeos/accounts_user_name_edit.js new file mode 100644 index 0000000..025db25 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/accounts_user_name_edit.js @@ -0,0 +1,121 @@ +// 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. + +cr.define('options.accounts', function() { + const Event = cr.Event; + + // Email alias only, assuming it's a gmail address. + // e.g. 'john' + // {name: 'john', email: 'john@gmail.com'} + const format1String = + '^\\s*([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+)\\s*$'; + // Email address only. + // e.g. 'john@chromium.org' + // {name: 'john', email: 'john@chromium.org'} + const format2String = + '^\\s*([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+)@' + + '([A-Za-z0-9\-]{2,63}\\..+)\\s*$'; + // Full format. + // e.g. '"John Doe" <john@chromium.org>' + // {name: 'John doe', email: 'john@chromium.org'} + const format3String = + '^\\s*"{0,1}([^"]+)"{0,1}\\s*' + + '<([\\w\\.!#\\$%&\'\\*\\+-\\/=\\?\\^`\\{\\|\\}~]+@' + + '[A-Za-z0-9\-]{2,63}\\..+)>\\s*$'; + + /** + * Creates a new user name edit element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLInputElement} + */ + var UserNameEdit = cr.ui.define('input'); + + UserNameEdit.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Called when an element is decorated as a user name edit. + */ + decorate: function() { + this.pattern = format1String + '|' + format2String + '|' + + format3String; + + this.onkeypress = this.handleKeyPress_.bind(this); + }, + + + /** + * Parses given str for user info. + * + * Note that the email parsing is based on RFC 5322 and does not support + * IMA (Internationalized Email Address). We take only the following chars + * as valid for an email alias (aka local-part): + * - Letters: a–z, A–Z + * - Digits: 0-9 + * - Characters: ! # $ % & ' * + - / = ? ^ _ ` { | } ~ + * - Dot: . (Note that we did not cover the cases that dot should not + * appear as first or last character and should not appear two or + * more times in a row.) + * + * @param {string} str A string to parse. + * @return {{name: string, email: string}} User info parsed from the string. + */ + parse: function(str) { + const format1 = new RegExp(format1String); + const format2 = new RegExp(format2String); + const format3 = new RegExp(format3String); + + var matches = format1.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[1] + '@gmail.com' + }; + } + + matches = format2.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[1] + '@' + matches[2] + }; + } + + matches = format3.exec(str); + if (matches) { + return { + name: matches[1], + email: matches[2] + }; + } + + return null; + }, + + /** + * Handler for key press event. + * @private + * @param {!Event} e The keypress event object. + */ + handleKeyPress_: function(e) { + // Enter + if (e.keyCode == 13) { + var user = this.parse(this.value); + if (user) { + var e = new Event('add'); + e.user = user; + this.dispatchEvent(e); + } + + this.select(); + } + } + }; + + return { + UserNameEdit: UserNameEdit + }; +}); + diff --git a/chrome/browser/resources/options2/chromeos/bluetooth_device_list.js b/chrome/browser/resources/options2/chromeos/bluetooth_device_list.js new file mode 100644 index 0000000..6187cbb --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/bluetooth_device_list.js @@ -0,0 +1,232 @@ +// 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. + +cr.define('options.system.bluetooth', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + + /** + * Bluetooth settings constants. + */ + function Constants() {} + + /** + * Enumeration of supported device types. + * @enum {string} + */ + // TODO(kevers): Prune list based on the set of devices that will be + // supported for V1 of the feature. The set will likely be restricted to + // mouse and keyboard. Others are temporarily included for testing device + // discovery. + Constants.DEVICE_TYPE = { + COMPUTER: 'computer', + HEADSET: 'headset', + KEYBOARD: 'input-keyboard', + MOUSE: 'input-mouse', + PHONE: 'phone', + }; + + /** + * Creates a new bluetooth list item. + * @param {{name: string, + * address: string, + * icon: Constants.DEVICE_TYPE, + * paired: boolean, + * connected: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * entered: number|undefined}} device + * Description of the Bluetooth device. + * @constructor + * @extends {options.DeletableItem} + */ + function BluetoothListItem(device) { + var el = cr.doc.createElement('div'); + el.__proto__ = BluetoothListItem.prototype; + el.data = {}; + for (var key in device) + el.data[key] = device[key]; + el.decorate(); + // Only show the close button for paired devices. + el.deletable = device.paired; + return el; + } + + BluetoothListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Description of the Bluetooth device. + * @type {{name: string, + * address: string, + * icon: Constants.DEVICE_TYPE, + * paired: boolean, + * connected: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * entered: number|undefined}} + */ + data: null, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + var label = this.ownerDocument.createElement('div'); + label.className = 'bluetooth-device-label'; + this.classList.add('bluetooth-device'); + this.connected = this.data.connected; + // Though strictly speaking, a connected device will also be paired, we + // are interested in tracking paired devices that are not connected. + this.paired = this.data.paired && !this.data.connected; + this.connecting = !!this.data.pairing; + var content = this.data.name; + // Update label for devices that are paired but not connected. + if (this.paired) { + content = content + ' (' + + templateData['bluetoothDeviceNotConnected'] + ')'; + } + label.textContent = content; + this.contentElement.appendChild(label); + }, + }; + + /** + * Class for displaying a list of Bluetooth devices. + * @constructor + * @extends {options.DeletableItemList} + */ + var BluetoothDeviceList = cr.ui.define('list'); + + BluetoothDeviceList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.addEventListener('blur', this.onBlur_); + this.addEventListener('change', this.onChange_); + this.clear(); + }, + + /** + * When the list loses focus, unselect all items in the list. + * @private + */ + onBlur_: function() { + // TODO(kevers): Should this be pushed up to the list class? + this.selectionModel.unselectAll(); + }, + + /** + * Updates the state of the button for adding a Bluetooth device in + * response to a change in the selected item. + * @private + */ + onChange_: function() { + var item = this.selectedItem; + var disabled = !item || item.paired || item.conencted; + $('bluetooth-add-device-apply-button').disabled = disabled; + }, + + /** + * Adds a bluetooth device to the list of available devices. A check is + * made to see if the device is already in the list, in which case the + * existing device is updated. + * @param {{name: string, + * address: string, + * icon: Constants.DEVICE_TYPE, + * paired: boolean, + * connected: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * entered: number|undefined}} device + * Description of the bluetooth device. + * @return {boolean} True if the devies was successfully added or updated. + */ + appendDevice: function(device) { + if (!this.isSupported_(device)) + return false; + var index = this.find(device.address); + if (index == undefined) { + this.dataModel.push(device); + this.redraw(); + } else { + this.dataModel.splice(index, 1, device); + this.redrawItem(index); + } + return true; + }, + + /** + * Perges all devices from the list. + */ + clear: function() { + this.dataModel = new ArrayDataModel([]); + this.redraw(); + }, + + /** + * Returns the index of the list entry with the matching address. + * @param {string} address Unique address of the Bluetooth device. + * @return {number|undefined} Index of the matching entry or + * undefined if no match found. + */ + find: function(address) { + var size = this.dataModel.length; + for (var i = 0; i < size; i++) { + var entry = this.dataModel.item(i); + if (entry.address == address) + return i; + } + }, + + /** @inheritDoc */ + createItem: function(entry) { + return new BluetoothListItem(entry); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var item = this.dataModel.item(index); + if (item && (item.connected || item.paired)) { + // Inform the bluetooth adapter that we are disconnecting the device. + chrome.send('updateBluetoothDevice', + [item.address, item.connected ? 'disconnect' : 'forget']); + } + this.dataModel.splice(index, 1); + // Invalidate the list since it has a stale cache after a splice + // involving a deletion. + this.invalidate(); + this.redraw(); + }, + + /** + * Tests if the bluetooth device is supported based on the type of device. + * @param {Object.<string,string>} device Desription of the device. + * @return {boolean} true if the device is supported. + * @private + */ + isSupported_: function(device) { + var target = device.icon; + for (var key in Constants.DEVICE_TYPE) { + if (Constants.DEVICE_TYPE[key] == target) + return true; + } + return false; + } + }; + + cr.defineProperty(BluetoothListItem, 'connected', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothListItem, 'paired', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothListItem, 'connecting', cr.PropertyKind.BOOL_ATTR); + + return { + BluetoothListItem: BluetoothListItem, + BluetoothDeviceList: BluetoothDeviceList, + Constants: Constants + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/bluetooth_list_element.js b/chrome/browser/resources/options2/chromeos/bluetooth_list_element.js new file mode 100644 index 0000000..1b6ef82 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/bluetooth_list_element.js @@ -0,0 +1,387 @@ +// 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. + +cr.define('options.system.bluetooth', function() { + /** + * Bluetooth settings constants. + */ + function Constants() {} + + /** + * Enumeration of supported device types. Each device type has an + * associated icon and CSS style. + * @enum {string} + */ + Constants.DEVICE_TYPE = { + COMPUTER: 'computer', + HEADSET: 'headset', + KEYBOARD: 'input-keyboard', + MOUSE: 'input-mouse', + PHONE: 'phone', + }; + + /** + * Enumeration of possible states for a bluetooth device. The value + * associated with each state maps to a localized string in the global + * variable 'templateData'. + * @enum {string} + */ + Constants.DEVICE_STATUS = { + CONNECTED: 'bluetoothDeviceConnected', + CONNECTING: 'bluetoothDeviceConnecting', + FAILED_PAIRING: 'bluetoothDeviceFailedPairing', + NOT_PAIRED: 'bluetoothDeviceNotPaired', + PAIRED: 'bluetoothDevicePaired' + }; + + /** + * Enumeration of possible states during pairing. The value associated + * with each state maps to a loalized string in the global variable + * 'tempateData'. + * @enum {string} + */ + Constants.PAIRING = { + CONFIRM_PASSKEY: 'bluetoothConfirmPasskey', + ENTER_PASSKEY: 'bluetoothEnterPasskey', + FAILED_CONNECT_INSTRUCTIONS: 'bluetoothFailedPairingInstructions', + REMOTE_PASSKEY: 'bluetoothRemotePasskey' + }; + + /** + * Creates an element for storing a list of bluetooth devices. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLDivElement} + */ + var BluetoothListElement = cr.ui.define('div'); + + BluetoothListElement.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + }, + + /** + * Loads given list of bluetooth devices. This list will comprise of + * devices that are currently connected. New devices are discovered + * via the 'Find devices' button. + * @param {Array} devices An array of bluetooth devices. + */ + load: function(devices) { + this.textContent = ''; + for (var i = 0; i < devices.length; i++) { + if (this.isSupported_(devices[i])) + this.appendChild(new BluetoothItem(devices[i])); + } + }, + + /** + * Adds a bluetooth device to the list of available devices. A check is + * made to see if the device is already in the list, in which case the + * existing device is updated. + * @param {Object.<string,string>} device Description of the bluetooth + * device. + * @return {boolean} True if the devies was successfully added or updated. + */ + appendDevice: function(device) { + if (!this.isSupported_(device)) + return false; + var item = new BluetoothItem(device); + var existing = this.findDevice(device.address); + if (existing) + this.replaceChild(item, existing); + else + this.appendChild(item); + return true; + }, + + /** + * Scans the list of elements corresponding to discovered Bluetooth + * devices for one with a matching address. + * @param {string} address The address of the device. + * @return {Element|undefined} Element corresponding to the device address + * or undefined if no corresponding element is found. + */ + findDevice: function(address) { + var candidate = this.firstChild; + while (candidate) { + if (candidate.data.address == address) + return candidate; + candidate = candidate.nextSibling; + } + }, + + /** + * Tests if the bluetooth device is supported based on the type of device. + * @param {Object.<string,string>} device Desription of the device. + * @return {boolean} true if the device is supported. + * @private + */ + isSupported_: function(device) { + var target = device.icon; + for (var key in Constants.DEVICE_TYPE) { + if (Constants.DEVICE_TYPE[key] == target) + return true; + } + return false; + } + }; + + /** + * Creates an element in the list of bluetooth devices. + * @param {{name: string, + * address: string, + * icon: Constants.DEVICE_TYPE, + * paired: boolean, + * connected: boolean, + * pairing: string|undefined, + * passkey: number|undefined, + * entered: number|undefined}} device + * Decription of the bluetooth device. + * @constructor + */ + function BluetoothItem(device) { + var el = $('bluetooth-item-template').cloneNode(true); + el.__proto__ = BluetoothItem.prototype; + el.removeAttribute('id'); + el.hidden = false; + el.data = {}; + for (var key in device) + el.data[key] = device[key]; + el.decorate(); + return el; + } + + BluetoothItem.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + this.className = 'bluetooth-item'; + this.connected = this.data.connected; + // Though strictly speaking, a connected device will also be paired, + // we are interested in tracking paired devices that are not connected. + this.paired = this.data.paired && !this.data.connected; + this.connecting = !!this.data.pairing; + this.addLabels_(); + this.addButtons_(); + }, + + /** + * Retrieves the descendent element with the matching class name. + * @param {string} className The class name for the target element. + * @return {Element|undefined} Returns the matching element if + * found and unique. + * @private + */ + getNodeByClass_:function(className) { + var elements = this.getElementsByClassName(className); + if (elements && elements.length == 1) + return elements[0]; + }, + + /** + * Sets the text content for an element. + * @param {string} className The class name of the target element. + * @param {string} label Text content for the element. + * @private + */ + setLabel_: function(className, label) { + var el = this.getNodeByClass_(className); + el.textContent = label; + }, + + /** + * Adds an element containing the display name, status and device pairing + * instructions. + * @private + */ + addLabels_: function() { + this.setLabel_('network-name-label', this.data.name); + var status; + if (this.data.connected) + status = Constants.DEVICE_STATUS.CONNECTED; + else if (this.data.pairing) + status = Constants.DEVICE_STATUS.CONNECTING; + if (status) { + var statusMessage = templateData[status]; + if (statusMessage) + this.setLabel_('network-status-label', statusMessage); + if (this.connecting) { + var spinner = this.getNodeByClass_('inline-spinner'); + spinner.hidden = false; + } + } + if (this.data.pairing) + this.addPairingInstructions_(); + }, + + /** + * Adds instructions on how to complete the pairing process. + * @param {!Element} textDiv Target element for inserting the instructions. + * @private + */ + addPairingInstructions_: function() { + var instructionsEl = this.getNodeByClass_('bluetooth-instructions'); + var message = templateData[this.data.pairing]; + var array = this.formatInstructions_(message); + for (var i = 0; i < array.length; i++) { + instructionsEl.appendChild(array[i]); + } + if (this.data.pairing == Constants.PAIRING.ENTER_PASSKEY) { + var input = this.ownerDocument.createElement('input'); + input.type = 'text'; + input.className = 'bluetooth-passkey-field'; + instructionsEl.appendChild(input); + } + }, + + /** + * Formats the pairing instruction, which may contain labels for + * substitution. The label '%1' is replaced with the passkey, and '%2' + * is replaced with the name of the bluetooth device. Formatting of the + * passkey depends on the type of validation. + * @param {string} instructions The source instructions to format. + * @return {Array.<Element>} Array of formatted elements. + */ + formatInstructions_: function(instructions) { + var array = []; + var index = instructions.indexOf('%'); + if (index >= 0) { + array.push(this.createTextElement_(instructions.substring(0, index))); + var labelPlaceholder = instructions.charAt(index + 1); + // ... handle the placeholder + switch (labelPlaceholder) { + case '1': + array.push(this.createPasskeyElement_()); + break; + case '2': + array.push(this.createTextElement_(this.data.name)); + } + array = array.concat(this.formatInstructions_(instructions.substring( + index + 2))); + } else { + array.push(this.createTextElement_(instructions)); + } + return array; + }, + + /** + * Formats an element for displaying the passkey. + * @return {Element} Element containing the passkey. + */ + createPasskeyElement_: function() { + var passkeyEl = document.createElement('div'); + if (this.data.pairing == Constants.PAIRING.REMOTE_PASSKEY) { + passkeyEl.className = 'bluetooth-remote-passkey'; + var key = String(this.data.passkey); + var progress = this.data.entered; + for (var i = 0; i < key.length; i++) { + var keyEl = document.createElement('div'); + keyEl.textContent = key.charAt(i); + keyEl.className = 'bluetooth-passkey-char'; + if (i < progress) + keyEl.classList.add('key-typed'); + passkeyEl.appendChild(keyEl); + } + // Add return key symbol. + var keyEl = document.createElement('div'); + keyEl.className = 'bluetooth-passkey-char'; + keyEl.textContent = '\u23ce'; + passkeyEl.appendChild(keyEl); + } else { + passkeyEl.className = 'bluetooth-confirm-passkey'; + passkeyEl.textContent = this.data.passkey; + } + return passkeyEl; + }, + + /** + * Adds a text element. + * @param {string} text The text content of the new element. + * @param {string=} opt_style Optional parameter for the CSS class for + * formatting the text element. + * @return {Element} Element containing the text. + */ + createTextElement_: function(text, array, opt_style) { + var el = this.ownerDocument.createElement('span'); + el.textContent = text; + if (opt_style) + el.className = opt_style; + return el; + }, + + /** + * Adds buttons for updating the connectivity of a device. + * @private. + */ + addButtons_: function() { + var buttonsDiv = this.getNodeByClass_('bluetooth-button-group'); + var buttonLabelKey = null; + var callbackType = null; + if (this.connected) { + buttonLabelKey = 'bluetoothDisconnectDevice'; + callbackType = 'disconnect'; + } else if (this.paired) { + buttonLabelKey = 'bluetoothForgetDevice'; + callbackType = 'forget'; + } else if (this.connecting) { + if (this.data.pairing == Constants.PAIRING.CONFIRM_PASSKEY) { + buttonLabelKey = 'bluetoothRejectPasskey'; + callbackType = 'reject'; + } else { + buttonLabelKey = 'bluetoothCancel'; + callbackType = 'cancel'; + } + } else { + buttonLabelKey = 'bluetoothConnectDevice'; + callbackType = 'connect'; + } + if (buttonLabelKey && callbackType) { + var buttonEl = this.ownerDocument.createElement('button'); + buttonEl.textContent = localStrings.getString(buttonLabelKey); + var self = this; + var callback = function(e) { + chrome.send('updateBluetoothDevice', + [self.data.address, callbackType]); + } + buttonEl.addEventListener('click', callback); + buttonsDiv.appendChild(buttonEl); + } + if (this.data.pairing == Constants.PAIRING.CONFIRM_PASSKEY || + this.data.pairing == Constants.PAIRING.ENTER_PASSKEY) { + var buttonEl = this.ownerDocument.createElement('button'); + buttonEl.className = 'accept-pairing-button'; + var msg = this.data.pairing == Constants.PAIRING.CONFIRM_PASSKEY ? + 'bluetoothAcceptPasskey' : 'bluetoothConnectDevice'; + buttonEl.textContent = localStrings.getString(msg); + var self = this; + var callback = function(e) { + var passkey = self.data.passkey; + if (self.data.pairing == Constants.PAIRING.ENTER_PASSKEY) { + var passkeyField = self.getNodeByClass_('bluetooth-passkey-field'); + passkey = passkeyField.value; + } + chrome.send('updateBluetoothDevice', + [self.data.address, 'connect', String(passkey)]); + } + buttonEl.addEventListener('click', callback); + buttonsDiv.insertBefore(buttonEl, buttonsDiv.firstChild); + } + this.appendChild(buttonsDiv); + } + }; + + cr.defineProperty(BluetoothItem, 'connected', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothItem, 'paired', cr.PropertyKind.BOOL_ATTR); + + cr.defineProperty(BluetoothItem, 'connecting', cr.PropertyKind.BOOL_ATTR); + + return { + Constants: Constants, + BluetoothListElement: BluetoothListElement + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/cellular_plan_element.js b/chrome/browser/resources/options2/chromeos/cellular_plan_element.js new file mode 100644 index 0000000..2eaa7d3 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/cellular_plan_element.js @@ -0,0 +1,132 @@ +// 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. + +cr.define('options.internet', function() { + /** + * Creates a new network list div. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLDivElement} + */ + var CellularPlanElement = cr.ui.define('div'); + + CellularPlanElement.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + }, + + /** + * Loads given network list. + * @param {Array} networks An array of network object. + */ + load: function(plans) { + this.textContent = ''; + if (!plans || !plans.length) { + var noplansDiv = this.ownerDocument.createElement('div'); + noplansDiv.textContent = localStrings.getString('noPlansFound'); + this.appendChild(detailsTable); + } else { + for (var i = 0; i < plans.length; ++i) { + this.appendChild(new CellularPlanItem(i, plans[i])); + } + } + } + }; + + /** + * Creates a new network item. + * @param {Object} network The network this represents. + * @constructor + * @extends {HTMLDivElement} + */ + function CellularPlanItem(idx, plan) { + var el = cr.doc.createElement('div'); + el.data = { + idx: idx, + planType: plan.planType, + name: plan.name, + planSummary: plan.planSummary, + dataRemaining: plan.dataRemaining, + planExpires: plan.planExpires, + warning: plan.warning + }; + CellularPlanItem.decorate(el); + return el; + } + + + /** + * Decorates an element as a network item. + * @param {!HTMLElement} el The element to decorate. + */ + CellularPlanItem.decorate = function(el) { + el.__proto__ = CellularPlanItem.prototype; + el.decorate(); + }; + + CellularPlanItem.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + this.className = 'cellular-plan'; + var detailsTable = this.createTable_('details-plan-table', + 'option-control-table'); + this.addRow_(detailsTable, 'plan-details-info', + 'option-name', 'planSummary', this.data.planSummary); + this.addRow_(detailsTable, 'plan-details-info', + 'option-name', null, localStrings.getString('planName'), + 'option-value', 'planName', this.data.name); + this.addRow_(detailsTable, 'plan-details-info', + 'option-name', null, localStrings.getString('dataRemaining'), + 'option-value', 'dataRemaining', this.data.dataRemaining); + this.addRow_(detailsTable, 'plan-details-info', + 'option-name', null, localStrings.getString('planExpires'), + 'option-value', 'dataRemaining', this.data.planExpires); + if (this.data.warning && this.data.warning != "") { + this.addRow_(detailsTable, 'plan-details-info', + 'option-name', 'planWarning', this.data.warning); + } + this.appendChild(detailsTable); + this.appendChild(this.ownerDocument.createElement('hr')); + }, + + createTable_: function(tableId, tableClass) { + var table = this.ownerDocument.createElement('table'); + table.id = tableId; + table.className = tableClass; + return table; + }, + + addRow_: function(table, rowClass, col1Class, col1Id, col1Value, + col2Class, col2Id, col2Value) { + var row = this.ownerDocument.createElement('tr'); + if (rowClass) + row.className = rowClass; + var col1 = this.ownerDocument.createElement('td'); + col1.className = col1Class; + if (col1Id) + col1.id = col1Id; + col1.textContent = col1Value; + if (!col2Class) + col1.setAttribute('colspan','2'); + row.appendChild(col1); + if (col2Class) { + var col2 = this.ownerDocument.createElement('td'); + col2.className = col2Class; + if (col2Id) + col2.id = col2Id; + col2.textContent = col2Value; + row.appendChild(col2); + } + table.appendChild(row); + } + }; + + return { + CellularPlanElement: CellularPlanElement + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/change_picture_options.css b/chrome/browser/resources/options2/chromeos/change_picture_options.css new file mode 100644 index 0000000..ced9728 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/change_picture_options.css @@ -0,0 +1,32 @@ +#images-grid { + -webkit-user-drag: none; + -webkit-user-select: none; + margin: 10px; + outline: none; + padding: 10px; +} + +#images-grid * { + margin: 0; + padding: 0; +} + +#images-grid img { + background-color: white; + height: 64px; + vertical-align: middle; + width: 64px; +} + +#images-grid [role=listitem] { + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.15); + display: inline-block; + margin: 10px; + padding: 3px; +} + +#images-grid [selected] { + border: 2px solid #06c; + padding: 2px; +} diff --git a/chrome/browser/resources/options2/chromeos/change_picture_options.html b/chrome/browser/resources/options2/chromeos/change_picture_options.html new file mode 100644 index 0000000..cafb7e6 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/change_picture_options.html @@ -0,0 +1,5 @@ +<div id="change-picture-page" class="page" hidden> + <h1 i18n-content="changePicturePage"></h1> + <span i18n-content="changePicturePageDescription"></span> + <grid id="images-grid"></grid> +</div> diff --git a/chrome/browser/resources/options2/chromeos/change_picture_options.js b/chrome/browser/resources/options2/chromeos/change_picture_options.js new file mode 100644 index 0000000..b986743 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/change_picture_options.js @@ -0,0 +1,267 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var UserImagesGrid = options.UserImagesGrid; + var ButtonImages = UserImagesGrid.ButtonImages; + + /** + * Array of button URLs used on this page. + * @type {Array.<string>} + */ + const ButtonImageUrls = [ + ButtonImages.TAKE_PHOTO, + ButtonImages.CHOOSE_FILE + ]; + + ///////////////////////////////////////////////////////////////////////////// + // ChangePictureOptions class: + + /** + * Encapsulated handling of ChromeOS change picture options page. + * @constructor + */ + function ChangePictureOptions() { + OptionsPage.call( + this, + 'changePicture', + localStrings.getString('changePicturePage'), + 'change-picture-page'); + } + + cr.addSingletonGetter(ChangePictureOptions); + + ChangePictureOptions.prototype = { + // Inherit ChangePictureOptions from OptionsPage. + __proto__: options.OptionsPage.prototype, + + /** + * Initializes ChangePictureOptions page. + */ + initializePage: function() { + // Call base class implementation to start preferences initialization. + OptionsPage.prototype.initializePage.call(this); + + var imageGrid = $('images-grid'); + UserImagesGrid.decorate(imageGrid); + + imageGrid.addEventListener('change', + this.handleImageSelected_.bind(this)); + imageGrid.addEventListener('activate', + this.handleImageActivated_.bind(this)); + imageGrid.addEventListener('dblclick', + this.handleImageDblClick_.bind(this)); + + // Add the "Choose file" button. + imageGrid.addItem(ButtonImages.CHOOSE_FILE, + localStrings.getString('chooseFile'), + this.handleChooseFile_.bind(this)); + + // Profile image data. + this.profileImage_ = imageGrid.addItem( + ButtonImages.PROFILE_PICTURE, + localStrings.getString('profilePhotoLoading')); + + // Old user image data (if present). + this.oldImage_ = null; + + chrome.send('onChangePicturePageInitialized'); + }, + + /** + * Called right after the page has been shown to user. + */ + didShowPage: function() { + $('images-grid').updateAndFocus(); + chrome.send('onChangePicturePageShown'); + }, + + /** + * Called right before the page is hidden. + */ + willHidePage: function() { + var imageGrid = $('images-grid'); + imageGrid.blur(); // Make sure the image grid is not active. + if (this.oldImage_) { + imageGrid.removeItem(this.oldImage_); + this.oldImage_ = null; + } + }, + + /** + * Closes current page, returning back to Personal Stuff page. + * @private + */ + closePage_: function() { + OptionsPage.navigateToPage('personal'); + }, + + /** + * Handles "Take photo" button activation. + * @private + */ + handleTakePhoto_: function() { + chrome.send('takePhoto'); + this.closePage_(); + }, + + /** + * Handles "Choose a file" button activation. + * @private + */ + handleChooseFile_: function() { + chrome.send('chooseFile'); + this.closePage_(); + }, + + /** + * Handles image selection change. + * @private + */ + handleImageSelected_: function() { + var imageGrid = $('images-grid'); + var url = imageGrid.selectedItemUrl; + // Ignore deselection, selection change caused by program itself and + // selection of one of the action buttons. + if (url && + !imageGrid.inProgramSelection && + ButtonImageUrls.indexOf(url) == -1) { + chrome.send('selectImage', [url]); + } + }, + + /** + * Handles image activation (by pressing Enter). + * @private + */ + handleImageActivated_: function() { + switch ($('images-grid').selectedItemUrl) { + case ButtonImages.TAKE_PHOTO: + this.handleTakePhoto_(); + break; + case ButtonImages.CHOOSE_FILE: + this.handleChooseFile_(); + break; + default: + this.closePage_(); + break; + } + }, + + /** + * Handles double click on the image grid. + * @param {Event} e Double click Event. + */ + handleImageDblClick_: function(e) { + // Close page unless the click target is the grid itself or + // any of the buttons. + var url = e.target.src; + if (url && ButtonImageUrls.indexOf(url) == -1) + this.closePage_(); + }, + + /** + * URL of the current user image. + * @type {string} + */ + get currentUserImageUrl() { + return 'chrome://userimage/' + PersonalOptions.getLoggedInUsername() + + '?id=' + (new Date()).getTime(); + }, + + /** + * Notifies about camera presence change. + * @param {boolean} present Whether a camera is present or not. + * @private + */ + setCameraPresent_: function(present) { + var imageGrid = $('images-grid'); + if (present && !this.takePhotoButton_) { + this.takePhotoButton_ = imageGrid.addItem( + ButtonImages.TAKE_PHOTO, + localStrings.getString('takePhoto'), + this.handleTakePhoto_.bind(this), + 1); + } else if (!present && this.takePhotoButton_) { + imageGrid.removeItem(this.takePhotoButton_); + this.takePhotoButton_ = null; + } + }, + + /** + * Adds or updates old user image taken from file/camera (neither a profile + * image nor a default one). + * @private + */ + setOldImage_: function() { + var imageGrid = $('images-grid'); + var url = this.currentUserImageUrl; + if (this.oldImage_) { + this.oldImage_ = imageGrid.updateItem(this.oldImage_, url); + } else { + // Insert next to the profile image. + var pos = imageGrid.indexOf(this.profileImage_) + 1; + this.oldImage_ = imageGrid.addItem(url, undefined, undefined, pos); + imageGrid.selectedItem = this.oldImage_; + } + }, + + /** + * Updates user's profile image. + * @param {string} imageUrl Profile image, encoded as data URL. + * @param {boolean} select If true, profile image should be selected. + * @private + */ + setProfileImage_: function(imageUrl, select) { + var imageGrid = $('images-grid'); + this.profileImage_ = imageGrid.updateItem( + this.profileImage_, imageUrl, localStrings.getString('profilePhoto')); + if (select) + imageGrid.selectedItem = this.profileImage_; + }, + + /** + * Selects user image with the given URL. + * @param {string} url URL of the image to select. + * @private + */ + setSelectedImage_: function(url) { + $('images-grid').selectedItemUrl = url; + }, + + /** + * Appends default images to the image grid. Should only be called once. + * @param {Array.<string>} images An array of URLs to default images. + * @private + */ + setDefaultImages_: function(images) { + var imageGrid = $('images-grid'); + for (var i = 0, url; url = images[i]; i++) { + imageGrid.addItem(url); + } + }, + }; + + // Forward public APIs to private implementations. + [ + 'setCameraPresent', + 'setDefaultImages', + 'setOldImage', + 'setProfileImage', + 'setSelectedImage', + ].forEach(function(name) { + ChangePictureOptions[name] = function(value1, value2) { + ChangePictureOptions.getInstance()[name + '_'](value1, value2); + }; + }); + + // Export + return { + ChangePictureOptions: ChangePictureOptions + }; + +}); + diff --git a/chrome/browser/resources/options2/chromeos/internet_detail.html b/chrome/browser/resources/options2/chromeos/internet_detail.html new file mode 100644 index 0000000..d722066 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_detail.html @@ -0,0 +1,364 @@ +<div id="detailsInternetPage" class="page" hidden> + <h1 id="inetTitle"></h1> + <!-- Navigation tabs --> + <div id="details-tab-strip" class="subpages-nav-tabs"> + <span id="wifiNetworkNavTab" class="tab wifi-details" + tab-contents="wifiNetworkTab"> + <span class="tab-label" + i18n-content="wifiNetworkTabLabel"></span> + <span class="active-tab-label" i18n-content="wifiNetworkTabLabel"></span> + </span> + <span id="vpnNavTab" class="tab vpn-details" + tab-contents="vpnTab"> + <span class="tab-label" + i18n-content="vpnTabLabel"></span> + <span class="active-tab-label" i18n-content="vpnTabLabel"></span> + </span> + <span id="cellularPlanNavTab" class="tab cellular-details cdma-only" + tab-contents="cellularPlanTab"> + <span class="tab-label" + i18n-content="cellularPlanTabLabel"></span> + <span class="active-tab-label" i18n-content="cellularPlanTabLabel"></span> + </span> + <span id="cellularConnNavTab" class="tab cellular-details" + tab-contents="cellularConnTab"> + <span class="tab-label" + i18n-content="cellularConnTabLabel"></span> + <span class="active-tab-label" i18n-content="cellularConnTabLabel"></span> + </span> + <span id="cellularDeviceNavTab" class="tab cellular-details" + tab-contents="cellularDeviceTab"> + <span class="tab-label" + i18n-content="cellularDeviceTabLabel"></span> + <span class="active-tab-label" + i18n-content="cellularDeviceTabLabel"></span> + </span> + <span id="internetNavTab" class="tab" tab-contents="internetTab"> + <span class="tab-label" i18n-content="networkTabLabel"></span> + <span class="active-tab-label" i18n-content="networkTabLabel"></span> + </span> + <span id="security-nav-tab" class="tab cellular-details gsm-only" + tab-contents="security-tab"> + <span class="tab-label" i18n-content="securityTabLabel"></span> + <span class="active-tab-label" i18n-content="securityTabLabel"></span> + </span> + </div> + <div id="wifiNetworkTab" class="subpages-tab-contents wifi-details"> + <section> + <table class="ssid-table"> + <tr> + <td class="option-name" i18n-content="inetSsid"></td> + <td id="inetSsid" class="option-value"></td> + </tr> + </table> + </section> + <section> + <table class="option-control-table"> + <tr class="prefer-network"> + <td> + <div class="checkbox"> + <label> + <input id="preferNetworkWifi" type="checkbox"> + <span i18n-content="inetPreferredNetwork"></span> + </label> + <span class="controlled-setting-indicator" data="preferred" + for="preferNetworkWifi"></span> + </div> + </td> + </tr> + <tr class="auto-connect-network"> + <td> + <div class="checkbox"> + <label> + <input id="autoConnectNetworkWifi" type="checkbox"> + <span i18n-content="inetAutoConnectNetwork"></span> + </label> + <span class="controlled-setting-indicator" data="autoConnect" + for="autoConnectNetworkWifi"></span> + </div> + </td> + </tr> + </table> + </section> + <section id="passwordNetwork" class="password-details"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="inetPassProtected"></td> + </tr> + </table> + </section> + <section id="sharedNetwork" class="shared-network"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="inetNetworkShared"></td> + </tr> + </table> + </section> + </div> + <div id="vpnTab" class="subpages-tab-contents vpn-details"> + <section> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="inetServiceName"></td> + <td id="inetServiceName" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="inetServerHostname"></td> + <td id="inetServerHostname" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="inetProviderType"></td> + <td id="inetProviderType" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="inetUsername"></td> + <td id="inetUsername" class="option-value"></td> + </tr> + </table> + </section> + </div> + <div id="cellularPlanTab" + class="subpages-tab-contents cellular-details cdma-only"> + <section> + <div> + <table id="details-plan-table" class="option-control-table"> + <tr class="plan-loading-info"> + <td i18n-content="planLoading" class="option-value"></td> + </tr> + <tr class="no-plan-info"> + <td i18n-content="noPlansFound" class="option-value"></td> + </tr> + </table> + </div> + <div id="planList"></div> + </section> + <section class="plan-details-info"> + <div class="checkbox"> + <label> + <input id="showPlanNotifications" type="checkbox" + pref="settings.internet.mobile.show_plan_notifications"> + <span i18n-content="showPlanNotifications"></span> + </label> + </div> + </section> + </div> + <div id="cellularConnTab" class="subpages-tab-contents cellular-details"> + <section id="cellularNetworkOptions"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="serviceName"></td> + <td id="serviceName" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="networkTechnology"></td> + <td id="networkTechnology" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="activationState"></td> + <td id="activationState" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="roamingState"></td> + <td id="roamingState" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="restrictedPool"></td> + <td id="restrictedPool" class="option-value"></td> + </tr> + <tr class="gsm-only"> + <td class="option-name" i18n-content="operatorName"></td> + <td id="operatorName" class="option-value"></td> + </tr> + <tr class="gsm-only"> + <td class="option-name" i18n-content="operatorCode"></td> + <td id="operatorCode" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="errorState"></td> + <td id="errorState" class="option-value"></td> + </tr> + <tr class="gsm-only apn-list-view"> + <td class="option-name" i18n-content="cellularApnLabel"></td> + <td id="cellularApnLabel" class="option-value"> + <select id="selectApn"> + <option value="-1" i18n-content="cellularApnOther"> + </option> + </select> + <span class="controlled-setting-indicator" data="providerApnList" + for="selectApn"></span> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnLabel"></td> + <td id="cellularApnLabel" class="option-value"> + <input id="cellularApn" type="text"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnUsername"></td> + <td id="cellularApnUsernameLabel" class="option-value"> + <input id="cellularApnUsername" type="text"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name" i18n-content="cellularApnPassword"></td> + <td id="cellularApnPasswordLabel" class="option-value"> + <input id="cellularApnPassword" type="password"> + </td> + </tr> + <tr class="gsm-only apn-details-view"> + <td class="option-name"></td> + <td class="option-value"> + <button id="cellularApnUseDefault" + i18n-content="cellularApnUseDefault"></button> + <button id="cellularApnSet" + i18n-content="cellularApnSet"></button> + <button id="cellularApnCancel" + i18n-content="cellularApnCancel"></button> + </td> + </tr> + <tr> + <td colspan="2"> + <div class="checkbox"> + <label> + <input id="autoConnectNetworkCellular" type="checkbox"> + <span i18n-content="inetAutoConnectNetwork"></span> + </label> + <span class="controlled-setting-indicator" data="autoConnect" + for="autoConnectNetworkCellular"></span> + </div> + </td> + </tr> + </table> + </section> + </div> + <div id="cellularDeviceTab" class="subpages-tab-contents cellular-details"> + <section id="cellularDeviceOptions"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="manufacturer"></td> + <td id="manufacturer" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="modelId"></td> + <td id="modelId" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="firmwareRevision"></td> + <td id="firmwareRevision" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="hardwareRevision"></td> + <td id="hardwareRevision" class="option-value"></td> + </tr> + <tr> + <td class="option-name" i18n-content="prlVersion"></td> + <td id="prlVersion" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MEID:</td> + <td id="meid" class="option-value"></td> + </tr> + <tr> + <td class="option-name">ESN:</td> + <td id="esn" class="option-value"></td> + </tr> + <tr> + <td class="option-name">IMEI:</td> + <td id="imei" class="option-value"></td> + </tr> + <tr class="gsm-only"> + <td class="option-name">IMSI:</td> + <td id="imsi" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MDN:</td> + <td id="mdn" class="option-value"></td> + </tr> + <tr> + <td class="option-name">MIN:</td> + <td id="min" class="option-value"></td> + </tr> + </table> + </section> + </div> + <div id="internetTab" class="subpages-tab-contents"> + <section id="advancedSection"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="connectionState"></td> + <td id="connectionState" class="option-value"></td> + </tr> + <tr id="hardwareAddressRow"> + <td class="option-name" i18n-content="hardwareAddress"></td> + <td id="hardwareAddress" class="option-value"></td> + </tr> + </table> + </section> + <section id="ipconfigSection"> + <div> + <div id="ipTypeDHCPDiv" class="radio"> + <label> + <input type="radio" name="iptype" id="ipTypeDHCP" value="1"> + <span i18n-content="useDHCP"></span> + </label> + <span class="controlled-setting-indicator" data="ipconfigDHCP" + for="ipTypeDHCP"></span> + </div> + <div id="ipTypeStaticDiv" class="radio"> + <label> + <input type="radio" name="iptype" id="ipTypeStatic" value="0"> + <span i18n-content="useStaticIP"></span> + </label> + <span class="controlled-setting-indicator" data="ipconfigStatic" + for="ipTypeStatic"></span> + </div> + <div class="suboption"> + <div id="ipConfigManagement" class="settings-list"> + <list id="ipConfigList"></list> + </div> + </div> + </div> + </section> + <section id="change-proxy-section"> + <div> + <button id="change-proxy-button" i18n-content="changeProxyButton"> + </button> + </div> + </section> + </div> + <div id="security-tab" + class="subpages-tab-contents cellular-details gsm-only"> + <div id="cellular-security-options"> + <section> + <div id="sim-pin-lock" class="checkbox"> + <label> + <input id="sim-card-lock-enabled" type="checkbox"> + <span i18n-content="lockSimCard"></span> + </label> + <span class="controlled-setting-indicator" data="simCardLockEnabled" + for="sim-card-lock-enabled"></span> + </div> + </section> + <section> + <div id="change-pin-area"> + <button id="change-pin" i18n-content="changePinButton"></button> + <span class="controlled-setting-indicator" data="simCardLockEnabled" + for="change-pin"></span> + </div> + </section> + </div> + </div> + <div class="action-area bottom-strip button-strip"> + <button id="detailsInternetDismiss" + i18n-content="detailsInternetDismiss"></button> + <button id="detailsInternetLogin" + i18n-content="connect_button"></button> + <button id="detailsInternetDisconnect" + i18n-content="disconnect_button"></button> + <button id="activateDetails" + i18n-content="activate_button"></button> + <button id="buyplanDetails" + i18n-content="buyplan_button"></button> + </div> +</div> diff --git a/chrome/browser/resources/options2/chromeos/internet_detail.js b/chrome/browser/resources/options2/chromeos/internet_detail.js new file mode 100644 index 0000000..5fb1e4e --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_detail.js @@ -0,0 +1,204 @@ +// 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. + +cr.define('options.internet', function() { + var OptionsPage = options.OptionsPage; + + /* + * Helper function to set hidden attribute on given element list. + * @param {Array} elements List of elements to be updated. + * @param {bool} hidden New hidden value. + */ + function updateHidden(elements, hidden) { + for (var i = 0, el; el = elements[i]; i++) { + el.hidden = hidden; + } + } + + ///////////////////////////////////////////////////////////////////////////// + // DetailsInternetPage class: + + /** + * Encapsulated handling of ChromeOS internet details overlay page. + * @constructor + */ + function DetailsInternetPage() { + OptionsPage.call(this, 'detailsInternetPage', null, 'detailsInternetPage'); + } + + cr.addSingletonGetter(DetailsInternetPage); + + DetailsInternetPage.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes DetailsInternetPage page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + }, + + /** + * Update details page controls. + * @private + */ + updateControls_: function() { + // Only show ipconfig section if network is connected OR if nothing on + // this device is connected. This is so that you can fix the ip configs + // if you can't connect to any network. + // TODO(chocobo): Once ipconfig is moved to flimflam service objects, + // we need to redo this logic to allow configuration of all networks. + $('ipconfigSection').hidden = !this.connected && this.deviceConnected; + + // Network type related. + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .cellular-details'), + !this.cellular); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .wifi-details'), + !this.wireless); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .vpn-details'), + !this.vpn); + + // Cell plan related. + $('planList').hidden = this.cellplanloading; + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .no-plan-info'), + !this.cellular || this.cellplanloading || this.hascellplan); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .plan-loading-info'), + !this.cellular || this.nocellplan || this.hascellplan); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .plan-details-info'), + !this.cellular || this.nocellplan || this.cellplanloading); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .gsm-only'), + !this.cellular || !this.gsm); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .cdma-only'), + !this.cellular || this.gsm); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .apn-list-view'), + !this.cellular || !this.gsm); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .apn-details-view'), + true); + + // Password and shared. + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .password-details'), + !this.wireless || !this.password); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .shared-network'), + !this.shared); + updateHidden( + cr.doc.querySelectorAll('#detailsInternetPage .prefer-network'), + !this.showPreferred); + } + }; + + /** + * Whether the underlying network is connected. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'connected', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the underlying network is wifi. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'wireless', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the underlying network shared wifi. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'shared', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the underlying network is a vpn. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'vpn', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the underlying network is ethernet. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'ethernet', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the underlying network is cellular. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'cellular', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the network is loading cell plan. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'cellplanloading', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the network has cell plan(s). Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'hascellplan', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the network has no cell plan. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'nocellplan', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether the network is gsm. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'gsm', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + /** + * Whether show password details for network. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(DetailsInternetPage, 'password', + cr.PropertyKind.JS, + DetailsInternetPage.prototype.updateControls_); + + // TODO(xiyuan): Check to see if it is safe to remove these attributes. + cr.defineProperty(DetailsInternetPage, 'hasactiveplan', + cr.PropertyKind.JS); + cr.defineProperty(DetailsInternetPage, 'activated', + cr.PropertyKind.JS); + cr.defineProperty(DetailsInternetPage, 'connecting', + cr.PropertyKind.JS); + cr.defineProperty(DetailsInternetPage, 'connected', + cr.PropertyKind.JS); + + return { + DetailsInternetPage: DetailsInternetPage + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/internet_detail_ip_config_list.js b/chrome/browser/resources/options2/chromeos/internet_detail_ip_config_list.js new file mode 100644 index 0000000..fe4cb9e --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_detail_ip_config_list.js @@ -0,0 +1,99 @@ +// 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. + +cr.define('options.internet', function() { + const InlineEditableItem = options.InlineEditableItem; + const InlineEditableItemList = options.InlineEditableItemList; + + /** + * Creates a new ip config list item. + * @param {Object} fieldInfo The ip config field this item represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function IPConfigListItem(fieldInfo) { + var el = cr.doc.createElement('div'); + el.fieldInfo_ = fieldInfo; + IPConfigListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a ip config list item. + * @param {!HTMLElement} el The element to decorate. + */ + IPConfigListItem.decorate = function(el) { + el.__proto__ = IPConfigListItem.prototype; + el.decorate(); + }; + + IPConfigListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Input field for editing the ip config values. + * @type {HTMLElement} + * @private + */ + valueField_: null, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + this.deletable = false; + + var fieldInfo = this.fieldInfo_; + + var nameEl = this.ownerDocument.createElement('div'); + nameEl.className = 'name'; + nameEl.textContent = fieldInfo['name']; + + this.contentElement.appendChild(nameEl); + + var valueEl = this.createEditableTextCell(fieldInfo['value']); + valueEl.className = 'value'; + this.contentElement.appendChild(valueEl); + + var valueField = valueEl.querySelector('input') + valueField.required = true; + this.valueField_ = valueField; + + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** @inheritDoc */ + get currentInputIsValid() { + return this.valueField_.validity.valid; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + return this.valueField_.value != this.fieldInfo_['value']; + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + this.fieldInfo_['value'] = this.valueField_.value; + }, + }; + + var IPConfigList = cr.ui.define('list'); + + IPConfigList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** @inheritDoc */ + createItem: function(fieldInfo) { + return new IPConfigListItem(fieldInfo); + }, + }; + + return { + IPConfigList: IPConfigList + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/internet_network_element.js b/chrome/browser/resources/options2/chromeos/internet_network_element.js new file mode 100644 index 0000000..bba5a54 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_network_element.js @@ -0,0 +1,316 @@ +// 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. + +cr.define('options.internet', function() { + + /** + * Network settings constants. These enums usually match their C++ + * counterparts. + */ + function Constants() {} + // Minimum length for wireless network password. + Constants.MIN_WIRELESS_PASSWORD_LENGTH = 5; + // Minimum length for SSID name. + Constants.MIN_WIRELESS_SSID_LENGTH = 1; + // Cellular activation states: + Constants.ACTIVATION_STATE_UNKNOWN = 0; + Constants.ACTIVATION_STATE_ACTIVATED = 1; + Constants.ACTIVATION_STATE_ACTIVATING = 2; + Constants.ACTIVATION_STATE_NOT_ACTIVATED = 3; + Constants.ACTIVATION_STATE_PARTIALLY_ACTIVATED = 4; + // Network types: + Constants.TYPE_UNKNOWN = 0; + Constants.TYPE_ETHERNET = 1; + Constants.TYPE_WIFI = 2; + Constants.TYPE_WIMAX = 3; + Constants.TYPE_BLUETOOTH = 4; + Constants.TYPE_CELLULAR = 5; + Constants.TYPE_VPN = 6; + // ONC sources: + Constants.ONC_SOURCE_USER_IMPORT = 1; + Constants.ONC_SOURCE_DEVICE_POLICY = 2; + Constants.ONC_SOURCE_USER_POLICY = 3; + + /** + * Creates a new network list div. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLDivElement} + */ + var NetworkElement = cr.ui.define('div'); + + NetworkElement.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + this.addEventListener('click', this.handleClick_); + }, + + /** + * Loads given network list. + * @param {Array} networks An array of network object. + */ + load: function(networks) { + this.textContent = ''; + + for (var i = 0; i < networks.length; ++i) { + this.appendChild(new NetworkItem(networks[i])); + } + }, + + /** + * Handles click on network list and triggers actions when clicked on + * a NetworkListItem button. + * @private + * @param {!Event} e The click event object. + */ + handleClick_: function(e) { + // We shouldn't respond to click events selecting an input, + // so return on those. + if (e.target.tagName == 'INPUT') { + return; + } + // Handle left button click + if (e.button == 0) { + var el = e.target; + // If click is on action buttons of a network item. + if (!(el.buttonType && el.networkType && el.servicePath)) { + if (el.buttonType) { + return; + } + // If click is on a network item or its label, walk up the DOM tree + // to find the network item. + var item = el; + while (item && !item.data) { + item = item.parentNode; + } + if (item.connecting) + return; + + if (item) { + var data = item.data; + // Don't try to connect to Ethernet or unactivated Cellular. + if (data && (data.networkType == 1 || + (data.networkType == 5 && data.activation_state != 1))) + return; + // If clicked on other networks item. + if (data && data.servicePath == '?') { + chrome.send('buttonClickCallback', + [String(data.networkType), + data.servicePath, + 'connect']); + } + } + } + } + } + }; + + /** + * Creates a new network item. + * @param {Object} network The network this represents. + * @constructor + * @extends {HTMLDivElement} + */ + function NetworkItem(network) { + var el = cr.doc.createElement('div'); + el.data = network; + NetworkItem.decorate(el); + return el; + } + + + /** + * Decorates an element as a network item. + * @param {!HTMLElement} el The element to decorate. + */ + NetworkItem.decorate = function(el) { + el.__proto__ = NetworkItem.prototype; + el.decorate(); + }; + + NetworkItem.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + this.className = 'network-item'; + this.connectable = this.data.connectable; + this.connected = this.data.connected; + this.connecting = this.data.connecting; + this.other = this.data.servicePath == '?'; + this.id = this.data.servicePath; + + // Insert a div holding the policy-managed indicator. + var policyIndicator = this.ownerDocument.createElement('div'); + policyIndicator.className = 'controlled-setting-indicator'; + cr.ui.decorate(policyIndicator, options.ControlledSettingIndicator); + + if (this.data.policyManaged) { + policyIndicator.controlledBy = 'policy'; + policyIndicator.setAttribute('textPolicy', + localStrings.getString('managedNetwork')); + } + this.appendChild(policyIndicator); + + // textDiv holds icon, name and status text. + var textDiv = this.ownerDocument.createElement('div'); + textDiv.className = 'network-item-text'; + if (this.data.iconURL) { + textDiv.style.backgroundImage = url(this.data.iconURL); + } + + var nameEl = this.ownerDocument.createElement('div'); + nameEl.className = 'network-name-label'; + nameEl.textContent = this.data.networkName; + textDiv.appendChild(nameEl); + + if (this.other) { + // No status and buttons for "Other..." + this.appendChild(textDiv); + return; + } + + // Only show status text if not empty. + if (this.data.networkStatus) { + var statusEl = this.ownerDocument.createElement('div'); + statusEl.className = 'network-status-label'; + statusEl.textContent = this.data.networkStatus; + textDiv.appendChild(statusEl); + } + + this.appendChild(textDiv); + + var spacerDiv = this.ownerDocument.createElement('div'); + spacerDiv.className = 'network-item-box-spacer'; + this.appendChild(spacerDiv); + + var buttonsDiv = this.ownerDocument.createElement('div'); + var self = this; + if (!this.data.remembered) { + var no_plan = + this.data.networkType == Constants.TYPE_CELLULAR && + this.data.needs_new_plan; + var show_activate = + (this.data.networkType == Constants.TYPE_CELLULAR && + this.data.activation_state != + Constants.ACTIVATION_STATE_ACTIVATED && + this.data.activation_state != + Constants.ACTIVATION_STATE_ACTIVATING); + + // Show [Activate] button for non-activated Cellular network. + if (show_activate || no_plan) { + var button_name = no_plan ? 'buyplan_button' : 'activate_button'; + buttonsDiv.appendChild( + this.createButton_(button_name, 'activate', + function(e) { + chrome.send('buttonClickCallback', + [String(self.data.networkType), + self.data.servicePath, + 'activate']); + })); + } + // Show disconnect button if not ethernet. + if (this.data.networkType != Constants.TYPE_ETHERNET && + this.data.connected) { + buttonsDiv.appendChild( + this.createButton_('disconnect_button', 'disconnect', + function(e) { + chrome.send('buttonClickCallback', + [String(self.data.networkType), + self.data.servicePath, + 'disconnect']); + })); + } + if (!this.data.connected && !this.data.connecting) { + // connect button (if not ethernet and not showing activate button) + if (this.data.networkType != Constants.TYPE_ETHERNET && + !show_activate && !no_plan) { + buttonsDiv.appendChild( + this.createButton_('connect_button', 'connect', + function(e) { + chrome.send('buttonClickCallback', + [String(self.data.networkType), + self.data.servicePath, + 'connect']); + })); + } + } + if (this.data.connected || + this.data.networkType == Constants.TYPE_ETHERNET || + this.data.networkType == Constants.TYPE_VPN || + this.data.networkType == Constants.TYPE_WIFI || + this.data.networkType == Constants.TYPE_CELLULAR) { + buttonsDiv.appendChild( + this.createButton_('options_button', 'options', + function(e) { + options.ProxyOptions.getInstance().setNetworkName( + self.data.networkName); + chrome.send('buttonClickCallback', + [String(self.data.networkType), + self.data.servicePath, + 'options']); + })); + } + } else { + // Put "Forget this network" button. + var button = this.createButton_('forget_button', 'forget', + function(e) { + chrome.send('buttonClickCallback', + [String(self.data.networkType), + self.data.servicePath, + 'forget']); + }); + button.disabled = this.data.policyManaged; + buttonsDiv.appendChild(button); + } + this.appendChild(buttonsDiv); + }, + + /** + * Creates a button for interacting with a network. + * @param {Object} name The name of the localStrings to use for the text. + * @param {Object} type The type of button. + */ + createButton_: function(name, type, callback) { + var buttonEl = this.ownerDocument.createElement('button'); + buttonEl.buttonType = type; + buttonEl.textContent = localStrings.getString(name); + buttonEl.addEventListener('click', callback); + return buttonEl; + } + }; + + /** + * Whether the underlying network is connected. Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(NetworkItem, 'connected', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the underlying network is currently connecting. + * Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(NetworkItem, 'connecting', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the underlying network is an other network for adding networks. + * Only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(NetworkItem, 'other', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the underlying network is connectable. + * @type {boolean} + */ + cr.defineProperty(NetworkItem, 'connectable', cr.PropertyKind.BOOL_ATTR); + + return { + Constants: Constants, + NetworkElement: NetworkElement + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/internet_options.html b/chrome/browser/resources/options2/chromeos/internet_options.html new file mode 100644 index 0000000..651540a --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_options.html @@ -0,0 +1,59 @@ +<div id="internetPage" class="page hide-indicators" hidden> + <h1 i18n-content="internetPage"></h1> + <div id="locked-network-banner" hidden> + <span id="locked-network-icon"></span> + <span id="access-locked-text" i18n-content="accessLockedMsg"></span> + </div> + <div class="displaytable"> + <section id="wireless-buttons"> + <h3 i18n-content="generalNetworkingTitle"></h3> + <div id="network-general-div"> + <div id="networking-controls" class="section-group"> + <button id="enable-wifi" hidden + i18n-content="enableWifi"></button> + <button id="disable-wifi" hidden + i18n-content="disableWifi"></button> + <button id="enable-cellular" hidden + i18n-content="enableCellular"></button> + <button id="disable-cellular" hidden + i18n-content="disableCellular"></button> + </div> + <div id="shared-proxies" class="checkbox"> + <label> + <input id="use-shared-proxies" type="checkbox" + pref="settings.use_shared_proxies"> + <span i18n-content="useSharedProxies"></span> + </label> + </div> + <div id="internet-owner-only-warning" hidden> + <span i18n-content="ownerOnly"></span> + <span i18n-content="ownerUserId"></span> + </div> + <div id="data-roaming" class="checkbox"> + <label> + <input id="enable-data-roaming" + pref="cros.signed.data_roaming_enabled" + metric="Options_Internet_DataRoaming" type="checkbox"> + <span i18n-content="enableDataRoaming"></span> + </label> + </div> + </div> + </section> + <section id="wired-section"> + <h3 i18n-content="wired_title" class="network-title"></h3> + <div id="wired-list" class="networks"></div> + </section> + <section id="wireless-section"> + <h3 i18n-content="wireless_title" class="network-title"></h3> + <div id="wireless-list" class="networks"></div> + </section> + <section id="vpn-section"> + <h3 i18n-content="vpn_title" class="network-title"></h3> + <div id="vpn-list" class="networks"></div> + </section> + <section id="remembered-section"> + <h3 i18n-content="remembered_title" class="network-title"></h3> + <div id="remembered-list" class="networks"></div> + </section> + </div> +</div> diff --git a/chrome/browser/resources/options2/chromeos/internet_options.js b/chrome/browser/resources/options2/chromeos/internet_options.js new file mode 100644 index 0000000..6391c9e --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_options.js @@ -0,0 +1,709 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + ///////////////////////////////////////////////////////////////////////////// + // InternetOptions class: + + /** + * Encapsulated handling of ChromeOS internet options page. + * @constructor + */ + function InternetOptions() { + OptionsPage.call(this, 'internet', templateData.internetPageTabTitle, + 'internetPage'); + } + + cr.addSingletonGetter(InternetOptions); + + // Inherit InternetOptions from OptionsPage. + InternetOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes InternetOptions page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + if (templateData.accessLocked) { + this.accesslocked = true; + } + + options.internet.NetworkElement.decorate($('wired-list')); + $('wired-list').load(templateData.wiredList); + options.internet.NetworkElement.decorate($('wireless-list')); + $('wireless-list').load(templateData.wirelessList); + options.internet.NetworkElement.decorate($('vpn-list')); + $('vpn-list').load(templateData.vpnList); + options.internet.NetworkElement.decorate($('remembered-list')); + $('remembered-list').load(templateData.rememberedList); + + this.updatePolicyIndicatorVisibility_(); + + options.internet.CellularPlanElement.decorate($('planList')); + + $('wired-section').hidden = (templateData.wiredList.length == 0); + $('wireless-section').hidden = (templateData.wirelessList.length == 0); + $('vpn-section').hidden = (templateData.vpnList.length == 0); + $('remembered-section').hidden = + (templateData.rememberedList.length == 0); + InternetOptions.setupAttributes(templateData); + $('detailsInternetDismiss').addEventListener('click', function(event) { + InternetOptions.setDetails(); + }); + $('detailsInternetLogin').addEventListener('click', function(event) { + InternetOptions.setDetails(); + InternetOptions.loginFromDetails(); + }); + $('detailsInternetDisconnect').addEventListener('click', function(event) { + InternetOptions.setDetails(); + InternetOptions.disconnectNetwork(); + }); + $('activateDetails').addEventListener('click', function(event) { + InternetOptions.activateFromDetails(); + }); + $('enable-wifi').addEventListener('click', function(event) { + event.target.disabled = true; + chrome.send('enableWifi', []); + }); + $('disable-wifi').addEventListener('click', function(event) { + event.target.disabled = true; + chrome.send('disableWifi', []); + }); + $('enable-cellular').addEventListener('click', function(event) { + event.target.disabled = true; + chrome.send('enableCellular', []); + }); + $('disable-cellular').addEventListener('click', function(event) { + event.target.disabled = true; + chrome.send('disableCellular', []); + }); + $('change-proxy-button').addEventListener('click', function(event) { + OptionsPage.closeOverlay(); + OptionsPage.showPageByName('proxy', false); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ShowProxySettings']); + }); + $('buyplanDetails').addEventListener('click', function(event) { + chrome.send('buyDataPlan', []); + OptionsPage.closeOverlay(); + }); + $('cellularApnUseDefault').addEventListener('click', function(event) { + var data = $('connectionState').data; + var apnSelector = $('selectApn'); + + if (data.userApnIndex != -1) { + apnSelector.remove(data.userApnIndex); + data.userApnIndex = -1; + } + + if (data.providerApnList.value.length > 0) { + var iApn = 0; + data.apn.apn = data.providerApnList.value[iApn].apn; + data.apn.username = data.providerApnList.value[iApn].username; + data.apn.password = data.providerApnList.value[iApn].password; + chrome.send('setApn', [String(data.servicePath), + String(data.apn.apn), + String(data.apn.username), + String(data.apn.password)]); + apnSelector.selectedIndex = iApn; + data.selectedApn = iApn; + } else { + data.apn.apn = ''; + data.apn.username = ''; + data.apn.password = ''; + apnSelector.selectedIndex = -1; + data.selectedApn = -1; + } + + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-list-view'), + false); + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-details-view'), + true); + }); + $('cellularApnSet').addEventListener('click', function(event) { + if ($('cellularApn').value == '') + return; + + var data = $('connectionState').data; + var apnSelector = $('selectApn'); + + data.apn.apn = String($('cellularApn').value); + data.apn.username = String($('cellularApnUsername').value); + data.apn.password = String($('cellularApnPassword').value); + chrome.send('setApn', [String(data.servicePath), + String(data.apn.apn), + String(data.apn.username), + String(data.apn.password)]); + + if (data.userApnIndex != -1) { + apnSelector.remove(data.userApnIndex); + data.userApnIndex = -1; + } + + var option = document.createElement('option'); + option.textContent = data.apn.apn; + option.value = -1; + option.selected = true; + apnSelector.add(option, apnSelector[apnSelector.length - 1]); + data.userApnIndex = apnSelector.length - 2 + data.selectedApn = data.userApnIndex; + + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-list-view'), + false); + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-details-view'), + true); + }); + $('cellularApnCancel').addEventListener('click', function(event) { + $('selectApn').selectedIndex = $('connectionState').data.selectedApn; + + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-list-view'), + false); + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-details-view'), + true); + }); + $('selectApn').addEventListener('change', function(event) { + var data = $('connectionState').data; + var apnSelector = $('selectApn'); + if (apnSelector[apnSelector.selectedIndex].value != -1) { + var apnList = data.providerApnList.value; + chrome.send('setApn', [String(data.servicePath), + String(apnList[apnSelector.selectedIndex].apn), + String(apnList[apnSelector.selectedIndex].username), + String(apnList[apnSelector.selectedIndex].password) + ]); + data.selectedApn = apnSelector.selectedIndex; + } else if (apnSelector.selectedIndex == data.userApnIndex) { + chrome.send('setApn', [String(data.servicePath), + String(data.apn.apn), + String(data.apn.username), + String(data.apn.password)]); + data.selectedApn = apnSelector.selectedIndex; + } else { + $('cellularApn').value = data.apn.apn; + $('cellularApnUsername').value = data.apn.username; + $('cellularApnPassword').value = data.apn.password; + + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-list-view'), + true); + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-details-view'), + false); + } + }); + $('sim-card-lock-enabled').addEventListener('click', function(event) { + var newValue = $('sim-card-lock-enabled').checked; + // Leave value as is because user needs to enter PIN code first. + // When PIN will be entered and value changed, + // we'll update UI to reflect that change. + $('sim-card-lock-enabled').checked = !newValue; + chrome.send('setSimCardLock', [newValue]); + }); + $('change-pin').addEventListener('click', function(event) { + chrome.send('changePin'); + }); + this.showNetworkDetails_(); + }, + + showNetworkDetails_: function() { + var params = parseQueryParams(window.location); + var servicePath = params.servicePath; + var networkType = params.networkType; + if (!servicePath || !servicePath.length || + !networkType || !networkType.length) + return; + var networkName = params.networkName; + if (networkName) + options.ProxyOptions.getInstance().setNetworkName(networkName); + chrome.send('buttonClickCallback', + [networkType, servicePath, "options"]); + }, + + updateHidden_: function(elements, hidden) { + for (var i = 0, el; el = elements[i]; i++) { + el.hidden = hidden; + } + }, + + /** + * Update internet page controls. + * @private + */ + updateControls_: function() { + accesslocked = this.accesslocked; + + $('locked-network-banner').hidden = !accesslocked; + $('wireless-buttons').hidden = accesslocked; + $('wired-section').hidden = accesslocked; + $('wireless-section').hidden = accesslocked; + $('vpn-section').hidden = accesslocked; + $('remembered-section').hidden = accesslocked; + + // Don't change hidden attribute on OptionsPage divs directly because it + // is used in supporting infrastructure now. + if (accesslocked && DetailsInternetPage.getInstance().visible) + this.closeOverlay(); + }, + + /** + * Updates the policy indicator visibility. Space is only allocated for the + * policy indicators if there is at least one visible. + * @private + */ + updatePolicyIndicatorVisibility_: function() { + var page = $('internetPage'); + if (page.querySelectorAll( + '.network-item > .controlled-setting-indicator[controlled-by]') + .length) { + page.classList.remove('hide-indicators'); + } else { + page.classList.add('hide-indicators'); + } + } + }; + + /** + * Whether access to this page is locked. + * @type {boolean} + */ + cr.defineProperty(InternetOptions, 'accesslocked', cr.PropertyKind.JS, + InternetOptions.prototype.updateControls_); + + InternetOptions.loginFromDetails = function () { + var data = $('connectionState').data; + var servicePath = data.servicePath; + chrome.send('buttonClickCallback', [String(data.type), + servicePath, + 'connect']); + OptionsPage.closeOverlay(); + }; + + InternetOptions.disconnectNetwork = function () { + var data = $('connectionState').data; + var servicePath = data.servicePath; + chrome.send('buttonClickCallback', [String(data.type), + servicePath, + 'disconnect']); + OptionsPage.closeOverlay(); + }; + + InternetOptions.activateFromDetails = function () { + var data = $('connectionState').data; + var servicePath = data.servicePath; + if (data.type == options.internet.Constants.TYPE_CELLULAR) { + chrome.send('buttonClickCallback', [String(data.type), + String(servicePath), + 'activate']); + } + OptionsPage.closeOverlay(); + }; + + InternetOptions.setDetails = function () { + var data = $('connectionState').data; + var servicePath = data.servicePath; + if (data.type == options.internet.Constants.TYPE_WIFI) { + chrome.send('setPreferNetwork', + [String(servicePath), + $('preferNetworkWifi').checked ? "true" : "false"]); + chrome.send('setAutoConnect', + [String(servicePath), + $('autoConnectNetworkWifi').checked ? "true" : "false"]); + } else if (data.type == options.internet.Constants.TYPE_CELLULAR) { + chrome.send('setAutoConnect', + [String(servicePath), + $('autoConnectNetworkCellular').checked ? "true" : "false"]); + } + + var ipConfigList = $('ipConfigList'); + chrome.send('setIPConfig',[String(servicePath), + $('ipTypeDHCP').checked ? "true" : "false", + ipConfigList.dataModel.item(0).value, + ipConfigList.dataModel.item(1).value, + ipConfigList.dataModel.item(2).value, + ipConfigList.dataModel.item(3).value]); + OptionsPage.closeOverlay(); + }; + + InternetOptions.setupAttributes = function(data) { + var buttons = $('wireless-buttons'); + if (data.wifiEnabled) { + $('disable-wifi').disabled = data.wifiBusy; + $('disable-wifi').hidden = false; + $('enable-wifi').hidden = true; + } else { + $('enable-wifi').disabled = data.wifiBusy; + $('enable-wifi').hidden = false; + $('disable-wifi').hidden = true; + } + if (data.cellularAvailable) { + if (data.cellularEnabled) { + $('disable-cellular').disabled = data.cellularBusy; + $('disable-cellular').hidden = false; + $('enable-cellular').hidden = true; + } else { + $('enable-cellular').disabled = data.cellularBusy; + $('enable-cellular').hidden = false; + $('disable-cellular').hidden = true; + } + if (!AccountsOptions.currentUserIsOwner()) + $('internet-owner-only-warning').hidden = false; + $('data-roaming').hidden = false; + } else { + $('enable-cellular').hidden = true; + $('disable-cellular').hidden = true; + $('data-roaming').hidden = true; + } + }; + + // + //Chrome callbacks + // + InternetOptions.refreshNetworkData = function (data) { + var self = InternetOptions.getInstance(); + if (data.accessLocked) { + self.accesslocked = true; + return; + } + self.accesslocked = false; + $('wired-list').load(data.wiredList); + $('wireless-list').load(data.wirelessList); + $('vpn-list').load(data.vpnList); + $('remembered-list').load(data.rememberedList); + + self.updatePolicyIndicatorVisibility_(); + + $('wired-section').hidden = (data.wiredList.length == 0); + $('wireless-section').hidden = (data.wirelessList.length == 0); + $('vpn-section').hidden = (data.vpnList.length == 0); + InternetOptions.setupAttributes(data); + $('remembered-section').hidden = (data.rememberedList.length == 0); + }; + + // TODO(xiyuan): This function seems belonging to DetailsInternetPage. + InternetOptions.updateCellularPlans = function (data) { + var detailsPage = DetailsInternetPage.getInstance(); + detailsPage.cellplanloading = false; + if (data.plans && data.plans.length) { + detailsPage.nocellplan = false + detailsPage.hascellplan = true; + $('planList').load(data.plans); + } else { + detailsPage.nocellplan = true; + detailsPage.hascellplan = false; + } + + detailsPage.hasactiveplan = !data.needsPlan; + detailsPage.activated = data.activated; + if (!data.activated) + $('detailsInternetLogin').hidden = true; + + $('buyplanDetails').hidden = !data.showBuyButton; + $('activateDetails').hidden = !data.showActivateButton; + }; + + InternetOptions.updateSecurityTab = function(requirePin) { + $('sim-card-lock-enabled').checked = requirePin; + $('change-pin').hidden = !requirePin; + }; + + InternetOptions.showDetailedInfo = function (data) { + var detailsPage = DetailsInternetPage.getInstance(); + // TODO(chocobo): Is this hack to cache the data here reasonable? + $('connectionState').data = data; + $('buyplanDetails').hidden = true; + $('activateDetails').hidden = true; + $('detailsInternetLogin').hidden = data.connected; + if (data.type == options.internet.Constants.TYPE_ETHERNET) + $('detailsInternetDisconnect').hidden = true; + else + $('detailsInternetDisconnect').hidden = !data.connected; + + detailsPage.deviceConnected = data.deviceConnected; + detailsPage.connecting = data.connecting; + detailsPage.connected = data.connected; + if (data.connected) { + $('inetTitle').textContent = localStrings.getString('inetStatus'); + } else { + $('inetTitle').textContent = localStrings.getString('inetConnect'); + } + $('connectionState').textContent = data.connectionState; + + var inetAddress = ''; + var inetSubnetAddress = ''; + var inetGateway = ''; + var inetDns = ''; + $('ipTypeDHCP').checked = true; + if (data.ipconfigStatic.value) { + inetAddress = data.ipconfigStatic.value.address; + inetSubnetAddress = data.ipconfigStatic.value.subnetAddress; + inetGateway = data.ipconfigStatic.value.gateway; + inetDns = data.ipconfigStatic.value.dns; + $('ipTypeStatic').checked = true; + } else if (data.ipconfigDHCP.value) { + inetAddress = data.ipconfigDHCP.value.address; + inetSubnetAddress = data.ipconfigDHCP.value.subnetAddress; + inetGateway = data.ipconfigDHCP.value.gateway; + inetDns = data.ipconfigDHCP.value.dns; + } + + // Hide the dhcp/static radio if needed. + $('ipTypeDHCPDiv').hidden = !data.showStaticIPConfig; + $('ipTypeStaticDiv').hidden = !data.showStaticIPConfig; + + // Hide change-proxy-button and change-proxy-section if not showing proxy. + $('change-proxy-button').hidden = !data.showProxy; + $('change-proxy-section').hidden = !data.showProxy; + + var ipConfigList = $('ipConfigList'); + ipConfigList.disabled = + $('ipTypeDHCP').checked || data.ipconfigStatic.controlledBy || + !data.showStaticIPConfig; + options.internet.IPConfigList.decorate(ipConfigList); + ipConfigList.autoExpands = true; + var model = new ArrayDataModel([]); + model.push({ + 'property': 'inetAddress', + 'name': localStrings.getString('inetAddress'), + 'value': inetAddress, + }); + model.push({ + 'property': 'inetSubnetAddress', + 'name': localStrings.getString('inetSubnetAddress'), + 'value': inetSubnetAddress, + }); + model.push({ + 'property': 'inetGateway', + 'name': localStrings.getString('inetGateway'), + 'value': inetGateway, + }); + model.push({ + 'property': 'inetDns', + 'name': localStrings.getString('inetDns'), + 'value': inetDns, + }); + ipConfigList.dataModel = model; + + $('ipTypeDHCP').addEventListener('click', function(event) { + // disable ipConfigList and switch back to dhcp values (if any) + if (data.ipconfigDHCP.value) { + var config = data.ipconfigDHCP.value; + ipConfigList.dataModel.item(0).value = config.address; + ipConfigList.dataModel.item(1).value = config.subnetAddress; + ipConfigList.dataModel.item(2).value = config.gateway; + ipConfigList.dataModel.item(3).value = config.dns; + } + ipConfigList.dataModel.updateIndex(0); + ipConfigList.dataModel.updateIndex(1); + ipConfigList.dataModel.updateIndex(2); + ipConfigList.dataModel.updateIndex(3); + // Unselect all so we don't keep the currently selected field editable. + ipConfigList.selectionModel.unselectAll(); + ipConfigList.disabled = true; + }); + + $('ipTypeStatic').addEventListener('click', function(event) { + // enable ipConfigList + ipConfigList.disabled = false; + ipConfigList.focus(); + ipConfigList.selectionModel.selectedIndex = 0; + }); + + if (data.hardwareAddress) { + $('hardwareAddress').textContent = data.hardwareAddress; + $('hardwareAddressRow').style.display = 'table-row'; + } else { + // This is most likely a device without a hardware address. + $('hardwareAddressRow').style.display = 'none'; + } + if (data.type == options.internet.Constants.TYPE_WIFI) { + OptionsPage.showTab($('wifiNetworkNavTab')); + detailsPage.wireless = true; + detailsPage.vpn = false; + detailsPage.ethernet = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + detailsPage.shared = data.shared; + $('inetSsid').textContent = data.ssid; + detailsPage.showPreferred = data.showPreferred; + $('preferNetworkWifi').checked = data.preferred.value; + $('preferNetworkWifi').disabled = !data.remembered; + $('autoConnectNetworkWifi').checked = data.autoConnect.value; + $('autoConnectNetworkWifi').disabled = !data.remembered; + detailsPage.password = data.encrypted; + } else if(data.type == options.internet.Constants.TYPE_CELLULAR) { + if (!data.gsm) + OptionsPage.showTab($('cellularPlanNavTab')); + else + OptionsPage.showTab($('cellularConnNavTab')); + detailsPage.ethernet = false; + detailsPage.wireless = false; + detailsPage.vpn = false; + detailsPage.cellular = true; + if (data.carrierUrl) { + var a = $('carrierUrl'); + if (!a) { + a = document.createElement('a'); + $('serviceName').appendChild(a); + a.id = 'carrierUrl'; + a.target = "_blank"; + } + a.href = data.carrierUrl; + a.textContent = data.serviceName; + } else { + $('serviceName').textContent = data.serviceName; + } + $('networkTechnology').textContent = data.networkTechnology; + $('activationState').textContent = data.activationState; + $('roamingState').textContent = data.roamingState; + $('restrictedPool').textContent = data.restrictedPool; + $('errorState').textContent = data.errorState; + $('manufacturer').textContent = data.manufacturer; + $('modelId').textContent = data.modelId; + $('firmwareRevision').textContent = data.firmwareRevision; + $('hardwareRevision').textContent = data.hardwareRevision; + $('prlVersion').textContent = data.prlVersion; + $('meid').textContent = data.meid; + $('imei').textContent = data.imei; + $('mdn').textContent = data.mdn; + $('esn').textContent = data.esn; + $('min').textContent = data.min; + detailsPage.gsm = data.gsm; + if (data.gsm) { + $('operatorName').textContent = data.operatorName; + $('operatorCode').textContent = data.operatorCode; + $('imsi').textContent = data.imsi; + + var apnSelector = $('selectApn'); + // Clear APN lists, keep only last element that "other". + while (apnSelector.length != 1) + apnSelector.remove(0); + var otherOption = apnSelector[0]; + data.selectedApn = -1; + data.userApnIndex = -1; + var apnList = data.providerApnList.value; + for (var i = 0; i < apnList.length; i++) { + var option = document.createElement('option'); + var name = apnList[i].localizedName; + if (name == '' && apnList[i].name != '') + name = apnList[i].name; + if (name == '') + name = apnList[i].apn; + else + name = name + ' (' + apnList[i].apn + ')'; + option.textContent = name; + option.value = i; + if ((data.apn.apn == apnList[i].apn && + data.apn.username == apnList[i].username && + data.apn.password == apnList[i].password) || + (data.apn.apn == '' && + data.lastGoodApn.apn == apnList[i].apn && + data.lastGoodApn.username == apnList[i].username && + data.lastGoodApn.password == apnList[i].password)) { + data.selectedApn = i; + } + // Insert new option before "other" option. + apnSelector.add(option, otherOption); + } + if (data.selectedApn == -1 && data.apn.apn != '') { + var option = document.createElement('option'); + option.textContent = data.apn.apn; + option.value = -1; + apnSelector.add(option, otherOption); + data.selectedApn = apnSelector.length - 2; + data.userApnIndex = data.selectedApn; + } + apnSelector.selectedIndex = data.selectedApn; + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-list-view'), + false); + InternetOptions.prototype.updateHidden_( + cr.doc.querySelectorAll('.apn-details-view'), + true); + + InternetOptions.updateSecurityTab(data.simCardLockEnabled.value); + } + $('autoConnectNetworkCellular').checked = data.autoConnect.value; + $('autoConnectNetworkCellular').disabled = false; + + $('buyplanDetails').hidden = !data.showBuyButton; + $('activateDetails').hidden = !data.showActivateButton; + if (data.showActivateButton) { + $('detailsInternetLogin').hidden = true; + } + + detailsPage.hascellplan = false; + if (data.connected) { + detailsPage.nocellplan = false; + detailsPage.cellplanloading = true; + chrome.send('refreshCellularPlan', [data.servicePath]) + } else { + detailsPage.nocellplan = true; + detailsPage.cellplanloading = false; + } + } else if (data.type == options.internet.Constants.TYPE_VPN) { + OptionsPage.showTab($('vpnNavTab')); + detailsPage.wireless = false; + detailsPage.vpn = true; + detailsPage.ethernet = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + $('inetServiceName').textContent = data.service_name; + $('inetServerHostname').textContent = data.server_hostname; + $('inetProviderType').textContent = data.provider_type; + $('inetUsername').textContent = data.username; + } else { + OptionsPage.showTab($('internetNavTab')); + detailsPage.ethernet = true; + detailsPage.wireless = false; + detailsPage.vpn = false; + detailsPage.cellular = false; + detailsPage.gsm = false; + } + + // Update controlled option indicators. + indicators = cr.doc.querySelectorAll( + '#detailsInternetPage .controlled-setting-indicator'); + for (var i = 0; i < indicators.length; i++) { + var dataProperty = indicators[i].getAttribute('data'); + if (dataProperty && data[dataProperty]) { + var controlledBy = data[dataProperty].controlledBy; + if (controlledBy) { + indicators[i].controlledBy = controlledBy; + var forElement = $(indicators[i].getAttribute('for')); + if (forElement) + forElement.disabled = true; + if (forElement.type == 'radio' && !forElement.checked) + indicators[i].hidden = true; + } else { + indicators[i].controlledBy = null; + } + } + } + + // Don't show page name in address bar and in history to prevent people + // navigate here by hand and solve issue with page session restore. + OptionsPage.showPageByName('detailsInternetPage', false); + }; + + InternetOptions.invalidNetworkSettings = function () { + alert(localStrings.getString('invalidNetworkSettings')); + }; + + // Export + return { + InternetOptions: InternetOptions + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/internet_options_page.css b/chrome/browser/resources/options2/chromeos/internet_options_page.css new file mode 100644 index 0000000..c7ab791 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/internet_options_page.css @@ -0,0 +1,171 @@ +#inetTitle { + border: none; +} + +#network-general-div { + -webkit-margin-start: 15px; +} + +#networking-controls { + display: -webkit-box; +} + +.networks { + -webkit-margin-start: 15px; + padding: 2px; +} + +.network-password { + left: 0px; + position: relative; +} + +.network-password > input, .network-password > select { + width: 200px; +} + +.network-item { + -webkit-box-align: center; + -webkit-padding-start: 10px; + border: 1px solid rgba(255,255,255,0); /* transparent white */ + border-radius: 2px; + display: -webkit-box; + height: 35px; +} + +.network-item:not([connecting]):hover { + border-color: hsl(214, 91%, 85%); + background-color: hsl(214, 91%, 97%); +} + +.network-item[connected] { + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0)); + background-color: hsl(214,91%,89%); + border-color: hsl(214, 91%, 65%); +} + +.network-item[connected]:hover { + background-color: hsl(214, 91%, 87%); + border-color: hsl(214, 91%, 65%); +} + +.network-item[connecting] { + background-color: hsl(214, 91%, 97%); + border-color: hsl(214, 91%, 85%); +} + +.network-item > .controlled-setting-indicator { + -webkit-margin-end: 5px; + width: 16px; +} + +.hide-indicators .network-item > .controlled-setting-indicator { + display: none; +} + +.network-item-text { + -webkit-padding-start: 30px; + background: left center no-repeat; + cursor: default; + display: table-cell; + height: 32px; + line-height: 100%; + max-width: 320px; + overflow: hidden; + vertical-align: middle; +} + +html[dir='rtl'] .network-item-text { + background: right center no-repeat; +} + +.network-item[connected] > * > .network-name-label { + font-weight: bold; +} + +.network-status-label { + color: grey; +} + +.network-item > * > button { + min-width: 100px; + visibility: hidden; + margin-right: 5px; +} + +.network-item:hover > * > button, +.network-item[connected] > * > button { + visibility: visible; +} + +.network-item-box-spacer { + -webkit-box-flex: 1; +} + +.displaytable > section > .network-title { + vertical-align: top; + padding-top: 20px; +} + +#detailsInternetPage { + min-width: 440px; + min-height: 420px; + padding-bottom: 40px; + position: relative; +} + +#details-plan-table { + width: 100%; +} + +#planSummary { + width: 350px; + padding-bottom: 5px; +} + +#planWarning { + width: 350px; + padding-top: 5px; + font-weight: bold; +} + +#locked-network-banner { + height: 31px; + width: 100%; + margin: 0; + padding-top: 10px; + vertical-align: middle; +} + +#locked-network-icon { + background-image: url("chrome://theme/IDR_WARNING"); + background-repeat: no-repeat; + background-position:center; + display: inline-block; + padding: 5px; + height: 21px; + vertical-align: middle; + width: 24px; +} + +#access-locked-text { + vertical-align: middle; +} + +#internet-owner-only-warning { + margin: 10px 0; + padding-bottom: 1px; + -webkit-padding-start: 20px; + background-repeat: no-repeat; + background-image: url('warning.png'); +} + +#ipConfigList .name { + width: 40%; +} + +#ipConfigList .value { + -webkit-box-flex: 1; + color: #666; +} diff --git a/chrome/browser/resources/options2/chromeos/language_chewing_options.html b/chrome/browser/resources/options2/chromeos/language_chewing_options.html new file mode 100644 index 0000000..f21e603 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/language_chewing_options.html @@ -0,0 +1,141 @@ +<div id="languageChewingPage" class="page" hidden> + <h1 i18n-content="languageChewingPage"></h1> + <section> + <table class="option-control-table"> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-auto-shift-cur" type="checkbox" + pref="settings.language.chewing_auto_shift_cur"> + <span i18n-content="Chewing_autoShiftCur"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-add-phrase-direction" type="checkbox" + pref="settings.language.chewing_add_phrase_direction"> + <span i18n-content="Chewing_addPhraseDirection"></span> + </label> + </div> + </td> + </tr> + <!-- Temporarily disabled. (crosbug.com/14185) + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-easy-symbol-input" type="checkbox" + pref="settings.language.chewing_easy_symbol_input"> + <span i18n-content="Chewing_easySymbolInput"></span> + </label> + </div> + </td> + </tr> + --> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-esc-clean-all-buf" type="checkbox" + pref="settings.language.chewing_esc_clean_all_buf"> + <span i18n-content="Chewing_escCleanAllBuf"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-force-lowercase-english" type="checkbox" + pref="settings.language.chewing_force_lowercase_english"> + <span i18n-content="Chewing_forceLowercaseEnglish"></span> + </label> + </div> + </td> + </tr> + <!-- Temporarily disabled. (crosbug.com/14185) + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-plain-zhuyin" type="checkbox" + pref="settings.language.chewing_plain_zhuyin"> + <span i18n-content="Chewing_plainZhuyin"></span> + </label> + </div> + </td> + </tr> + --> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-phrase-choice-rearward" type="checkbox" + pref="settings.language.chewing_phrase_choice_rearward"> + <span i18n-content="Chewing_phraseChoiceRearward"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="chewing-space-as-selection" type="checkbox" + pref="settings.language.chewing_space_as_selection"> + <span i18n-content="Chewing_spaceAsSelection"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="Chewing_maxChiSymbolLen"></td> + <td class="option-value"> + <input id="chewing-max-chi-symbol-len" class="control" type="range" + pref="settings.language.chewing_max_chi_symbol_len" + i18n-values="min:Chewing_maxChiSymbolLenMin; + max:Chewing_maxChiSymbolLenMax"> + <span id="chewing-max-chi-symbol-len-value"></span> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="Chewing_candPerPage"></td> + <td class="option-value"> + <select id="chewing-cand-per-page" class="control" data-type="number" + pref="settings.language.chewing_cand_per_page" + i18n-options="Chewing_candPerPageValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="Chewing_KBType"></td> + <td class="option-value"> + <select id="chewing-keyboard-type" class="control" data-type="string" + pref="settings.language.chewing_keyboard_type" + i18n-options="Chewing_KBTypeValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="Chewing_selKeys"></td> + <td class="option-value"> + <select id="chewing-sel-keys" class="control" data-type="string" + pref="settings.language.chewing_sel_keys" + i18n-options="Chewing_selKeysValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="Chewing_hsuSelKeyType"></td> + <td class="option-value"> + <select id="chewing-sel-key-type" class="control" data-type="number" + pref="settings.language.chewing_hsu_sel_key_type" + i18n-options="Chewing_hsuSelKeyTypeValue"></select> + </td> + </tr> + </table> + </section> +</div> diff --git a/chrome/browser/resources/options2/chromeos/language_customize_modifier_keys_overlay.html b/chrome/browser/resources/options2/chromeos/language_customize_modifier_keys_overlay.html new file mode 100644 index 0000000..3adb231 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/language_customize_modifier_keys_overlay.html @@ -0,0 +1,37 @@ +<div id="languageCustomizeModifierKeysOverlay" class="page" hidden> + <div class="content-area"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="xkbRemapSearchKeyToContent"></td> + <td class="option-value"> + <select id="xkb-remap-search-key-to" class="control" + data-type="number" i18n-options="xkbRemapSearchKeyToValue" + pref="settings.language.xkb_remap_search_key_to"></select> + </td> + </tr> + <tr> + <td class="option-name" + i18n-content="xkbRemapControlKeyToContent"></td> + <td class="option-value"> + <select id="xkb-remap-control-key-to" class="control" + data-type="number" i18n-options="xkbRemapControlKeyToValue" + pref="settings.language.xkb_remap_control_key_to"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="xkbRemapAltKeyToContent"></td> + <td class="option-value"> + <select id="xkb-remap-alt-key-to" class="control" data-type="number" + pref="settings.language.xkb_remap_alt_key_to" + i18n-options="xkbRemapAltKeyToValue"></select> + </td> + </tr> + </table> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="languageCustomizeModifierKeysOverleyDismissButton" + i18n-content="close"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/chromeos/language_hangul_options.html b/chrome/browser/resources/options2/chromeos/language_hangul_options.html new file mode 100644 index 0000000..f70ba76 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/language_hangul_options.html @@ -0,0 +1,18 @@ +<div id="languageHangulPage" class="page" hidden> + <h1 i18n-content="languageHangulPage"></h1> + <section> + <div class="option"> + <table class="option-control-table"> + <tr> + <td class="option-name" i18n-content="hangul_keyboard_layout"></td> + <td class="option-value"> + <select id="keyboard-layout-select" class="control" + data-type="string" + pref="settings.language.hangul_keyboard" + i18n-options="HangulkeyboardLayoutList"></select> + </td> + </tr> + </table> + </div> + </section> +</div> diff --git a/chrome/browser/resources/options2/chromeos/language_mozc_options.html b/chrome/browser/resources/options2/chromeos/language_mozc_options.html new file mode 100644 index 0000000..498b317 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/language_mozc_options.html @@ -0,0 +1,135 @@ +<div id="languageMozcPage" class="page" hidden> + <h1 i18n-content="languageMozcPage"></h1> + <section> + <table class="option-control-table"> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="mozc-incognito-mode" + pref="settings.language.mozc_incognito_mode" + type="checkbox"> + <span i18n-content="mozc_incognito_mode"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="mozc-use-auto-ime-turn-off" + pref="settings.language.mozc_use_auto_ime_turn_off" + type="checkbox"> + <span i18n-content="mozc_use_auto_ime_turn_off"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="mozc-use-history-suggest" type="checkbox" + pref="settings.language.mozc_use_history_suggest"> + <span i18n-content="mozc_use_history_suggest"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="mozc-use-dictionary-suggest" type="checkbox" + pref="settings.language.mozc_use_dictionary_suggest"> + <span i18n-content="mozc_use_dictionary_suggest"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_preedit_method"></td> + <td class="option-value"> + <select id="mozc-preedit-method" class="control" data-type="string" + pref="settings.language.mozc_preedit_method" + i18n-options="mozc_preedit_methodValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_session_keymap"></td> + <td class="option-value"> + <select id="mozc-session-keymap" class="control" data-type="string" + pref="settings.language.mozc_session_keymap" + i18n-options="mozc_session_keymapValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_punctuation_method"></td> + <td class="option-value"> + <select id="mozc-punctuation-method" class="control" + pref="settings.language.mozc_punctuation_method" + data-type="string" i18n-options="mozc_punctuation_methodValue"> + </select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_symbol_method"></td> + <td class="option-value"> + <select id="mozc-symbol-method" class="control" data-type="string" + pref="settings.language.mozc_symbol_method" + i18n-options="mozc_symbol_methodValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_space_character_form"> + </td> + <td class="option-value"> + <select id="mozc-space-character-form" class="control" + pref="settings.language.mozc_space_character_form" + data-type="string" i18n-options="mozc_space_character_formValue"> + </select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_history_learning_level"> + </td> + <td class="option-value"> + <select id="mozc-history-learning-level" class="control" + pref="settings.language.mozc_history_learning_level" + data-type="string" + i18n-options="mozc_history_learning_levelValue"></select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_shift_key_mode_switch"> + </td> + <td class="option-value"> + <select id="mozc-shift-key-mode-switch" class="control" + pref="settings.language.mozc_shift_key_mode_switch" + data-type="string" i18n-options="mozc_shift_key_mode_switchValue"> + </select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_numpad_character_form"> + </td> + <td class="option-value"> + <select id="mozc-numpad-character-form" class="control" + pref="settings.language.mozc_numpad_character_form" + data-type="string" i18n-options="mozc_numpad_character_formValue"> + </select> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="mozc_suggestions_size"> + </td> + <td class="option-value"> + <select id="mozc-suggestions-size" class="control" data-type="number" + pref="settings.language.mozc_suggestions_size" + i18n-options="mozc_suggestions_sizeValue"></select> + </td> + </tr> + </table> + </section> +</div> diff --git a/chrome/browser/resources/options2/chromeos/language_pinyin_options.html b/chrome/browser/resources/options2/chromeos/language_pinyin_options.html new file mode 100644 index 0000000..ad72fa4 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/language_pinyin_options.html @@ -0,0 +1,148 @@ +<div id="languagePinyinPage" class="page" hidden> + <h1 i18n-content="languagePinyinPage"></h1> + <section> + <table class="option-control-table"> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <labl> + <input id="pinyin-correct-pinyin" + pref="settings.language.pinyin_correct_pinyin" + type="checkbox"> + <span i18n-content="PinyinCorrectPinyin"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-fuzzy-pinyin" + pref="settings.language.pinyin_fuzzy_pinyin" + type="checkbox"> + <span i18n-content="PinyinFuzzyPinyin"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-shift-select-candidate" + pref="settings.language.pinyin_shift_select_candidate" + type="checkbox"> + <span i18n-content="PinyinShiftSelectCandidate"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-minus-equal-page" + pref="settings.language.pinyin_minus_equal_page" + type="checkbox"> + <span i18n-content="PinyinMinusEqualPage"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-comma-period-page" + pref="settings.language.pinyin_comma_period_page" + type="checkbox"> + <span i18n-content="PinyinCommaPeriodPage"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-auto-commit" + pref="settings.language.pinyin_auto_commit" + type="checkbox"> + <span i18n-content="PinyinAutoCommit"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-double-pinyin" + pref="settings.language.pinyin_double_pinyin" + type="checkbox"> + <span i18n-content="PinyinDoublePinyin"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-init-chinese" + pref="settings.language.pinyin_init_chinese" + type="checkbox"> + <span i18n-content="PinyinInitChinese"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-init-full" + pref="settings.language.pinyin_init_full" + type="checkbox"> + <span i18n-content="PinyinInitFull"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-init-full-punct" + pref="settings.language.pinyin_init_full_punct" + type="checkbox"> + <span i18n-content="PinyinInitFullPunct"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" colspan="2"> + <div class="checkbox"> + <label> + <input id="pinyin-init-simplified-chinese" + pref="settings.language.pinyin_init_simplified_chinese" + type="checkbox"> + <span i18n-content="PinyinInitSimplifiedChinese"></span> + </label> + </div> + </td> + </tr> + <tr> + <td class="option-name" i18n-content="PinyinDoublePinyinSchema"></td> + <td class="option-value"> + <select id="pinyin-double-pinyin-schema" class="control" + pref="settings.language.pinyin_double_pinyin_schema" + data-type="string" + i18n-options="PinyinDoublePinyinSchemaValue"></select> + </td> + </tr> + </table> + </section> +</div> diff --git a/chrome/browser/resources/options2/chromeos/proxy.css b/chrome/browser/resources/options2/chromeos/proxy.css new file mode 100644 index 0000000..28ee236 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/proxy.css @@ -0,0 +1,10 @@ +#ignoredHostList { + border: solid 1px #999999; + height: 100px; + -webkit-margin-start: 0px; +} + +#newHost { + -webkit-margin-start: 0px; + margin-top: 8px; +}
\ No newline at end of file diff --git a/chrome/browser/resources/options2/chromeos/proxy.html b/chrome/browser/resources/options2/chromeos/proxy.html new file mode 100644 index 0000000..0449c7b --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/proxy.html @@ -0,0 +1,141 @@ +<div id="proxyPage" class="page" hidden> + <div id="info-banner" class="managed-prefs-banner" hidden> + <span id="banner-icon" class="managed-prefs-icon"></span> + <span id="banner-text" class="managed-prefs-text"></span> + </div> + <h1 id="proxy-page-title" i18n-content="proxyPage"></h1> + <section> + <h3 i18n-content="proxy_config_title"></h3> + <div> + <div class="radio"> + <label> + <input id="directProxy" type="radio" name="proxytype" value="1" + pref="cros.session.proxy.type"> + <span i18n-content="proxyDirectInternetConnection"></span> + </label> + </div> + <div class="radio"> + <label> + <input id="manualProxy" type="radio" name="proxytype" value="2" + pref="cros.session.proxy.type"> + <span i18n-content="proxyManual"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="proxyAllProtocols" type="checkbox" + pref="cros.session.proxy.single"> + <span i18n-content="sameProxyProtocols"></span> + </label> + </div> + <div id="singleProxy"> + <table> + <tr> + <td> + <span i18n-content="httpProxy"></span> + <input id="proxyHostSingleName" type="text" size="30" + pref="cros.session.proxy.singlehttp" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + <input id="proxyHostSinglePort" type="text" data-type="number" + size="5" pref="cros.session.proxy.singlehttpport" disabled> + </td> + </tr> + </table> + </div> + <div id="multiProxy"> + <table> + <tr> + <td> + <span i18n-content="httpProxy"></span> + </td> + <td> + <input id="proxyHostName" type="text" size="30" + pref="cros.session.proxy.httpurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="proxyHostPort" type="text" data-type="number" size="5" + pref="cros.session.proxy.httpport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="secureHttpProxy"></span> + </td> + <td> + <input id="secureProxyHostName" type="text" size="30" + pref="cros.session.proxy.httpsurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="secureProxyPort" type="text" data-type="number" size="5" + pref="cros.session.proxy.httpsport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="ftpProxy"></span> + </td> + <td> + <input id="ftpProxy" type="text" size="30" + pref="cros.session.proxy.ftpurl" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="ftpProxyPort" type="text" data-type="number" size="5" + pref="cros.session.proxy.ftpport" disabled> + </td> + </tr> + <tr> + <td> + <span i18n-content="socksHost"></span> + </td> + <td> + <input id="socksHost" type="text" size="30" + pref="cros.session.proxy.socks" disabled> + </td> + <td> + <span i18n-content="proxyPort"></span> + </td> + <td> + <input id="socksPort" type="text" data-type="number" size="5" + pref="cros.session.proxy.socksport" disabled> + </td> + </tr> + </table> + </div> + <div class="radio"> + <label> + <input id="autoProxy" type="radio" name="proxytype" value="3" + pref="cros.session.proxy.type"> + <span i18n-content="proxyAutomatic"></span> + </label> + </div> + <div> + <label> + <span i18n-content="proxyConfigUrl"></span> + <input id="proxyConfig" type="url" size="60" + pref="cros.session.proxy.pacurl"> + </label> + </div> + </div> + </section> + <section id="advancedConfig"> + <h3 i18n-content="advanced_proxy_config"></h3> + <div class="option"> + <div i18n-content="proxyBypass"></div> + <list id="ignoredHostList"></list> + <input id="newHost" type="url" size="30"> + <button id="addHost" i18n-content="addHost"></button> + <button id="removeHost" i18n-content="removeHost"></button> + </div> + </section> +</div> diff --git a/chrome/browser/resources/options2/chromeos/proxy_options.js b/chrome/browser/resources/options2/chromeos/proxy_options.js new file mode 100644 index 0000000..f42a08d --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/proxy_options.js @@ -0,0 +1,244 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var Preferences = options.Preferences; + + ///////////////////////////////////////////////////////////////////////////// + // ProxyOptions class: + + /** + * Encapsulated handling of ChromeOS proxy options page. + * @constructor + */ + function ProxyOptions(model) { + OptionsPage.call(this, 'proxy', localStrings.getString('proxyPage'), + 'proxyPage'); + } + + cr.addSingletonGetter(ProxyOptions); + + /** + * UI pref change handler. + */ + function handlePrefUpdate(e) { + ProxyOptions.getInstance().updateControls(); + } + + /** + * Monitor pref change of given element. + */ + function observePrefsUI(el) { + Preferences.getInstance().addEventListener(el.pref, handlePrefUpdate); + } + + ProxyOptions.prototype = { + // Inherit ProxyOptions from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initializes ProxyOptions page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up ignored page. + options.proxyexceptions.ProxyExceptions.decorate($('ignoredHostList')); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + $('removeHost').addEventListener('click', this.handleRemoveExceptions_); + $('addHost').addEventListener('click', this.handleAddException_); + $('directProxy').addEventListener('click', this.disableManual_); + $('manualProxy').addEventListener('click', this.enableManual_); + $('autoProxy').addEventListener('click', this.disableManual_); + $('proxyAllProtocols').addEventListener('click', this.toggleSingle_); + + observePrefsUI($('directProxy')); + observePrefsUI($('manualProxy')); + observePrefsUI($('autoProxy')); + observePrefsUI($('proxyAllProtocols')); + }, + + proxyListInitialized_: false, + + /** + * Update controls state. + * @public + */ + updateControls: function() { + this.updateBannerVisibility_(); + this.toggleSingle_(); + if ($('manualProxy').checked) { + this.enableManual_(); + } else { + this.disableManual_(); + } + if (!this.proxyListInitialized_ && this.visible) { + this.proxyListInitialized_ = true; + $('ignoredHostList').redraw(); + } + }, + + /** + * Handler for OptionsPage's visible property change event. + * @private + * @param {Event} e Property change event. + */ + handleVisibleChange_: function(e) { + this.updateControls(); + }, + + /** + * Updates info banner visibility state. This function shows the banner + * if proxy is managed or shared-proxies is off for shared network. + * @private + */ + updateBannerVisibility_: function() { + var bannerDiv = $('info-banner'); + // Remove class and listener for click event in case they were added + // before and updateBannerVisibility_ is called repeatedly. + bannerDiv.classList.remove("clickable"); + bannerDiv.removeEventListener('click', this.handleSharedProxiesHint_); + + // Show banner and determine its message if necessary. + var controlledBy = $('directProxy').controlledBy; + if (controlledBy == '') { + bannerDiv.hidden = true; + } else { + bannerDiv.hidden = false; + // controlledBy must match strings loaded in proxy_handler.cc and + // set in proxy_cros_settings_provider.cc. + $('banner-text').textContent = localStrings.getString(controlledBy); + if (controlledBy == "enableSharedProxiesBannerText") { + bannerDiv.classList.add("clickable"); + bannerDiv.addEventListener('click', this.handleSharedProxiesHint_); + } + } + }, + + /** + * Handler for "click" event on yellow banner with enable-shared-proxies + * hint. + * @private + * @param {Event} e Click event fired from info-banner. + */ + handleSharedProxiesHint_: function(e) { + OptionsPage.navigateToPage("internet"); + }, + + /** + * Handler for when the user clicks on the checkbox to allow a + * single proxy usage. + * @private + * @param {Event} e Click Event. + */ + toggleSingle_: function(e) { + if ($('proxyAllProtocols').checked) { + $('multiProxy').style.display = 'none'; + $('singleProxy').style.display = 'block'; + } else { + $('multiProxy').style.display = 'block'; + $('singleProxy').style.display = 'none'; + } + }, + + /** + * Handler for selecting a radio button that will disable the manual + * controls. + * @private + * @param {Event} e Click event. + */ + disableManual_: function(e) { + $('advancedConfig').hidden = true; + $('proxyAllProtocols').disabled = true; + $('proxyHostName').disabled = true; + $('proxyHostPort').disabled = true; + $('proxyHostSingleName').disabled = true; + $('proxyHostSinglePort').disabled = true; + $('secureProxyHostName').disabled = true; + $('secureProxyPort').disabled = true; + $('ftpProxy').disabled = true; + $('ftpProxyPort').disabled = true; + $('socksHost').disabled = true; + $('socksPort').disabled = true; + $('proxyConfig').disabled = $('autoProxy').disabled || + !$('autoProxy').checked; + }, + + /** + * Handler for selecting a radio button that will enable the manual + * controls. + * @private + * @param {Event} e Click event. + */ + enableManual_: function(e) { + $('advancedConfig').hidden = false; + $('ignoredHostList').redraw(); + var all_disabled = $('manualProxy').disabled; + $('newHost').disabled = all_disabled; + $('removeHost').disabled = all_disabled; + $('addHost').disabled = all_disabled; + $('proxyAllProtocols').disabled = all_disabled; + $('proxyHostName').disabled = all_disabled; + $('proxyHostPort').disabled = all_disabled; + $('proxyHostSingleName').disabled = all_disabled; + $('proxyHostSinglePort').disabled = all_disabled; + $('secureProxyHostName').disabled = all_disabled; + $('secureProxyPort').disabled = all_disabled; + $('ftpProxy').disabled = all_disabled; + $('ftpProxyPort').disabled = all_disabled; + $('socksHost').disabled = all_disabled; + $('socksPort').disabled = all_disabled; + $('proxyConfig').disabled = true; + }, + + /** + * Handler for "add" event fired from userNameEdit. + * @private + * @param {Event} e Add event fired from userNameEdit. + */ + handleAddException_: function(e) { + var exception = $('newHost').value; + $('newHost').value = ''; + + exception = exception.trim(); + if (exception) + $('ignoredHostList').addException(exception); + }, + + /** + * Handler for when the remove button is clicked + * @private + */ + handleRemoveExceptions_: function(e) { + var selectedItems = $('ignoredHostList').selectedItems; + for (var x = 0; x < selectedItems.length; x++) { + $('ignoredHostList').removeException(selectedItems[x]); + } + }, + + /** + * Sets proxy page title using given network name. + * @param {string} network The network name to use in page title. + * @public + */ + setNetworkName: function(network) { + $('proxy-page-title').textContent = + localStrings.getStringF('proxyPageTitleFormat', network); + } + }; + + ProxyOptions.setNetworkName = function(network) { + ProxyOptions.getInstance().setNetworkName(network); + }; + + // Export + return { + ProxyOptions: ProxyOptions + }; + +}); diff --git a/chrome/browser/resources/options2/chromeos/proxy_rules_list.js b/chrome/browser/resources/options2/chromeos/proxy_rules_list.js new file mode 100644 index 0000000..7154495 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/proxy_rules_list.js @@ -0,0 +1,139 @@ +// 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. + +cr.define('options.proxyexceptions', function() { + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new exception list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var ProxyExceptions = cr.ui.define('list'); + + ProxyExceptions.prototype = { + __proto__: List.prototype, + + pref: 'cros.session.proxy.ignorelist', + + /** @inheritDoc */ + decorate: function() { + List.prototype.decorate.call(this); + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + this.addEventListener('click', this.handleClick_); + + var self = this; + + // Listens to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.load_(event.value); + }); + }, + + createItem: function(exception) { + return new ProxyExceptionsItem(exception); + }, + + /** + * Adds given exception to model and update backend. + * @param {Object} exception A exception to be added to exception list. + */ + addException: function(exception) { + this.dataModel.push(exception); + this.updateBackend_(); + }, + + /** + * Removes given exception from model and update backend. + */ + removeException: function(exception) { + var dataModel = this.dataModel; + + var index = dataModel.indexOf(exception); + if (index >= 0) { + dataModel.splice(index, 1); + this.updateBackend_(); + } + }, + + /** + * Handles the clicks on the list and triggers exception removal if the + * click is on the remove exception button. + * @private + * @param {!Event} e The click event object. + */ + handleClick_: function(e) { + // Handle left button click + if (e.button == 0) { + var el = e.target; + if (el.className == 'remove-exception-button') { + this.removeException(el.parentNode.exception); + } + } + }, + + /** + * Loads given exception list. + * @param {Array} exceptions An array of exception object. + */ + load_: function(exceptions) { + this.dataModel = new ArrayDataModel(exceptions); + }, + + /** + * Updates backend. + */ + updateBackend_: function() { + Preferences.setListPref(this.pref, this.dataModel.slice()); + } + }; + + /** + * Creates a new exception list item. + * @param exception The exception account this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function ProxyExceptionsItem(exception) { + var el = cr.doc.createElement('div'); + el.exception = exception; + ProxyExceptionsItem.decorate(el); + return el; + } + + /** + * Decorates an element as a exception account item. + * @param {!HTMLElement} el The element to decorate. + */ + ProxyExceptionsItem.decorate = function(el) { + el.__proto__ = ProxyExceptionsItem.prototype; + el.decorate(); + }; + + ProxyExceptionsItem.prototype = { + __proto__: ListItem.prototype, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + this.className = 'exception-list-item'; + + var labelException = this.ownerDocument.createElement('span'); + labelException.className = ''; + labelException.textContent = this.exception; + this.appendChild(labelException); + } + }; + + return { + ProxyExceptions: ProxyExceptions + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/system_options.html b/chrome/browser/resources/options2/chromeos/system_options.html new file mode 100644 index 0000000..bab4667 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/system_options.html @@ -0,0 +1,125 @@ +<div id="systemPage" class="page" hidden> + <h1 i18n-content="systemPage"></h1> + <div class="displaytable"> + <section> + <h3 i18n-content="datetimeTitle"></h3> + <div class="option-control-table"> + <span class="option-name" i18n-content="timezone"></span> + <div id="timezone-value"> + <select id="timezone-select" class="control" + i18n-options="timezoneList" + data-type="string" + pref="cros.system.timezone"></select> + </div> + <div class="checkbox"> + <label> + <input id="use-24hour-clock" + pref="settings.clock.use_24hour_clock" + type="checkbox"> + <span i18n-content="use24HourClock"></span> + </label> + </div> + </div> + </section> + <section> + <h3 i18n-content="screen"></h3> + <div id="brightness-value"> + <span i18n-content="brightness"></span> + <button id="brightness-decrease-button" + i18n-content="brightnessDecrease"></button> + <button id="brightness-increase-button" + i18n-content="brightnessIncrease"></button> + </div> + </section> + <section id="touchpad-controls" hidden> + <h3 i18n-content="touchpad"></h3> + <div class="option-control-table"> + <span class="option-name" i18n-content="sensitivity"></span> + <div id="touchpad-value"> + <div id="slider-control"> + <input id="sensitivity-range" type="range" min="1" max="5" + pref="settings.touchpad.sensitivity2" class="touch-slider"> + <div> + <span i18n-content="sensitivityLess"></span> + <span i18n-content="sensitivityMore" + class="touchpad-sensitivity-more"></span> + </div> + </div> + </div> + <div id="tap-to-click" class="checkbox"> + <label> + <input id="tap-to-click-check" + pref="settings.touchpad.enable_tap_to_click" + type="checkbox"> + <span i18n-content="enableTapToClick"></span> + </label> + </div> + </div> + </section> + <!-- By default, the bluetooth section is hidden. It is only + visible if the command line flag --enable_bluetooth is set. --> + <section id="bluetooth-devices" hidden> + <h3 i18n-content="bluetooth"></h3> + <div id="bluetooth-options-div"> + <div id="bluetooth-buttons"> + <button id="enable-bluetooth" i18n-content="enableBluetooth" hidden> + <button id="disable-bluetooth" i18n-content="disableBluetooth"> + </div> + <div id="no-bluetooth-devices-label" + i18n-content="noBluetoothDevicesFound"> + </div> + <div id="bluetooth-device-list"> + <!-- A list of connected devices is inserted here on page load. --> + <!-- The list of devices is updated asynchronously on clicking + 'Find Devices'. --> + </div> + <!-- Template for items in list of Bluetooth devices --> + <div id="bluetooth-item-template" class="bluetooth-item" hidden> + <div class="bluetooth-item-text"> + <div class="network-name-label"></div> + <div class="bluetooth-status"> + <span class="network-status-label"></span> + <div class="inline-spinner" hidden></div> + </div> + <div class="bluetooth-instructions"></div> + </div> + <div class="bluetooth-button-group"></div> + </div> + <div id = "bluetooth-finder-container"> + <button id="bluetooth-find-devices" + i18n-content="findBluetoothDevices"></button> + <div id="bluetooth-scanning-icon" + class="inline-spinner transparent"></div> + <span id="bluetooth-scanning-label" class="transparent" + i18n-content="bluetoothScanning"></span> + </div> + </div> + </section> + <section> + <h3 i18n-content="language"></h3> + <div class="option-control-table"> + <div class="option-name"> + <button id="language-button" i18n-content="languageCustomize"> + </button> + </div> + <div class="option-name"> + <button id="modifier-keys-button" + i18n-content="modifierKeysCustomize"></button> + </div> + </div> + </section> + <section> + <h3 i18n-content="accessibilityTitle"></h3> + <div class="option-control-table"> + <div class="option-name"> + <div class="checkbox"> + <label> + <input id="accesibility-check" type="checkbox"> + <span i18n-content="accessibility"></span> + </label> + </div> + </div> + </div> + </section> + </div> +</div> diff --git a/chrome/browser/resources/options2/chromeos/system_options.js b/chrome/browser/resources/options2/chromeos/system_options.js new file mode 100644 index 0000000..0a9faeb --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/system_options.js @@ -0,0 +1,202 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var RepeatingButton = cr.ui.RepeatingButton; + + ///////////////////////////////////////////////////////////////////////////// + // SystemOptions class: + + /** + * Encapsulated handling of ChromeOS system options page. + * @constructor + */ + + function SystemOptions() { + OptionsPage.call(this, 'system', templateData.systemPageTabTitle, + 'systemPage'); + } + + cr.addSingletonGetter(SystemOptions); + + // Inherit SystemOptions from OptionsPage. + SystemOptions.prototype = { + __proto__: options.OptionsPage.prototype, + + /** + * Initializes SystemOptions page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + // Disable time-related settings if we're not logged in as a real user. + if (AccountsOptions.loggedInAsGuest()) { + var timezone = $('timezone-select'); + if (timezone) + timezone.disabled = true; + var use_24hour_clock = $('use-24hour-clock'); + if (use_24hour_clock) + use_24hour_clock.disabled = true; + } + + options.system.bluetooth.BluetoothListElement.decorate( + $('bluetooth-device-list')); + + // TODO(kevers): Populate list of connected bluetooth devices. + // Set state of 'Enable bluetooth' checkbox. + $('bluetooth-find-devices').onclick = function(event) { + findBluetoothDevices_(); + }; + $('enable-bluetooth').onclick = function(event) { + chrome.send('bluetoothEnableChange', [Boolean(true)]); + }; + $('disable-bluetooth').onclick = function(event) { + chrome.send('bluetoothEnableChange', [Boolean(false)]); + }; + $('language-button').onclick = function(event) { + OptionsPage.navigateToPage('language'); + }; + $('modifier-keys-button').onclick = function(event) { + OptionsPage.navigateToPage('languageCustomizeModifierKeysOverlay'); + }; + $('accesibility-check').onchange = function(event) { + chrome.send('accessibilityChange', + [String($('accesibility-check').checked)]); + }; + initializeBrightnessButton_('brightness-decrease-button', + 'decreaseScreenBrightness'); + initializeBrightnessButton_('brightness-increase-button', + 'increaseScreenBrightness'); + } + }; + + /** + * Initializes a button for controlling screen brightness. + * @param {string} id Button ID. + * @param {string} callback Name of the callback function. + */ + function initializeBrightnessButton_(id, callback) { + var button = $(id); + cr.ui.decorate(button, RepeatingButton); + button.repeatInterval = 300; + button.addEventListener(RepeatingButton.Event.BUTTON_HELD, function(e) { + chrome.send(callback); + }); + } + + /** + * Scan for bluetooth devices. + * @private + */ + function findBluetoothDevices_() { + setVisibility_('bluetooth-scanning-label', true); + setVisibility_('bluetooth-scanning-icon', true); + + // Remove devices that are not currently connected. + var devices = $('bluetooth-device-list').childNodes; + for (var i = devices.length - 1; i >= 0; i--) { + var device = devices.item(i); + var data = device.data; + if (!data || data.status !== 'connected') + $('bluetooth-device-list').removeChild(device); + } + chrome.send('findBluetoothDevices'); + } + + /** + * Sets the visibility of an element. + * @param {string} id The id of the element. + * @param {boolean} visible True if the element should be made visible. + * @private + */ + function setVisibility_(id, visible) { + if (visible) + $(id).classList.remove("transparent"); + else + $(id).classList.add("transparent"); + } + + // + // Chrome callbacks + // + + /** + * Set the initial state of the accessibility checkbox. + */ + SystemOptions.SetAccessibilityCheckboxState = function(checked) { + $('accesibility-check').checked = checked; + }; + + /** + * Activate the bluetooth settings section on the System settings page. + */ + SystemOptions.showBluetoothSettings = function() { + $('bluetooth-devices').hidden = false; + }; + + /** + * Sets the state of the checkbox indicating if bluetooth is turned on. The + * state of the "Find devices" button and the list of discovered devices may + * also be affected by a change to the state. + * @param {boolean} checked Flag Indicating if Bluetooth is turned on. + */ + SystemOptions.setBluetoothState = function(checked) { + $('disable-bluetooth').hidden = !checked; + $('enable-bluetooth').hidden = checked; + $('bluetooth-finder-container').hidden = !checked; + $('no-bluetooth-devices-label').hidden = !checked; + if (!checked) { + setVisibility_('bluetooth-scanning-label', false); + setVisibility_('bluetooth-scanning-icon', false); + } + // Flush list of previously discovered devices if bluetooth is turned off. + if (!checked) { + var devices = $('bluetooth-device-list').childNodes; + for (var i = devices.length - 1; i >= 0; i--) { + var device = devices.item(i); + $('bluetooth-device-list').removeChild(device); + } + } + } + + /** + * Adds an element to the list of available bluetooth devices. If an element + * with a matching address is found, the existing element is updated. + * @param {{name: string, + * address: string, + * icon: string, + * paired: boolean, + * connected: boolean}} device + * Decription of the bluetooth device. + */ + SystemOptions.addBluetoothDevice = function(device) { + if ($('bluetooth-device-list').appendDevice(device)) + $('no-bluetooth-devices-label').hidden = true; + }; + + /** + * Hides the scanning label and icon that are used to indicate that a device + * search is in progress. + */ + SystemOptions.notifyBluetoothSearchComplete = function() { + setVisibility_('bluetooth-scanning-label', false); + setVisibility_('bluetooth-scanning-icon', false); + }; + + /** + * Displays the Touchpad Controls section when we detect a touchpad. + */ + SystemOptions.showTouchpadControls = function() { + $('touchpad-controls').hidden = false; + }; + + // Export + return { + SystemOptions: SystemOptions + }; + +}); diff --git a/chrome/browser/resources/options2/chromeos/system_options_page.css b/chrome/browser/resources/options2/chromeos/system_options_page.css new file mode 100644 index 0000000..d38ebdc --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/system_options_page.css @@ -0,0 +1,167 @@ +.touchpad-sensitivity-more { + float: right; +} + +html[dir=rtl] .touchpad-sensitivity-more { + float: left; +} + +.option-name { + display: inline; +} + +#timezone-value { + display: inline-block; + vertical-align: baseline; +} + +#touchpad-value, +#slider-control { + display: inline-block; + vertical-align: top; +} + +#bluetooth-options-div { + -webkit-box-orient: vertical; + display: -webkit-box; +} + +#no-bluetooth-devices-label { + -webkit-margin-after: 5px; + -webkit-margin-before: 5px; + color: gray; +} + +#bluetooth-finder-container, +#bluetooth-scanning-status { + -webkit-box-orient: horizontal; + display: -webkit-box; + vertical-align: baseline; +} + +#bluetooth-scanning-label, +#bluetooth-scanning-icon { + -webkit-transition: 250ms opacity; +} + +#bluetooth-scanning-label { + -webkit-margin-start: 5px; + color: gray; +} + +#bluetooth-scanning-icon { + -webkit-margin-start: 10px; + vertical-align: middle; +} + +#bluetooth-device-list { + display: table; + width: 100%; +} + +#bluetooth-device-list > * { + display: table-row; +} + +#bluetooth-device-list > * > * { + border-bottom: 4px solid rgba(255,255,255,1); + display: table-cell; +} + +.bluetooth-item > * > button { + visibility: hidden; + width: 100%; +} + +.bluetooth-item:first-child > * { + border-top: 4px solid rgba(255,255,255,1); +} + +.bluetooth-item:hover > * > button, +.bluetooth-item[connected] > * > button, +.bluetooth-item[connecting] > * > button { + visibility: visible; +} + +.bluetooth-item[connected] { + background-color: hsl(214, 91%, 89%); + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0)); + border-color: hsl(214, 91%, 65%); +} + +.bluetooth-item[paired] { + color: gray; +} + +.bluetooth-item:hover, +.bluetooth-item[connecting] { + background-color: hsl(214, 91%, 97%); + border-color: hsl(214, 91%, 65%); +} + +.bluetooth-item-text { + -webkit-padding-after: 3px; + -webkit-padding-before: 3px; + -webkit-padding-end: 5px; + -webkit-padding-start: 5px; + width: 100%; +} + +.bluetooth-item-text > * > .inline-spinner { + -webkit-margin-start: 5px; + -webkit-transform: translateY(3px); +} + +.bluetooth-instructions { + -webkit-margin-after: 5px; + -webkit-margin-before: 5px; + display: block; + line-height: 120%; +} + +.bluetooth-remote-passkey { + -webkit-box-align: baseline; + -webkit-box-orient: horizontal; + display: -webkit-inline-box; + margin-bottom: 5px; + margin-top: 5px; +} + +.bluetooth-confirm-passkey { + display: inline; + font-weight: bold; +} + +.bluetooth-passkey-char { + -webkit-margin-end: 3px; + -webkit-margin-start: 3px; + border: 1px solid black; + display: -webkit-box; + font-weight: bold; + padding: 2px; +} + +.bluetooth-passkey-char:first-child { + -webkit-margin-start: 10px; +} + +.bluetooth-passkey-char:last-child { + -webkit-margin-end: 10px; +} + +.bluetooth-passkey-char.key-typed { + background-color: hsl(214, 91%, 50%); + color: white; +} + +.bluetooth-passkey-field { + -webkit-margin-start: 10px; + width: 100px; +} + +.bluetooth-button-group { + -webkit-padding-end: 5px; + vertical-align: middle; +} + diff --git a/chrome/browser/resources/options2/chromeos/virtual_keyboard.css b/chrome/browser/resources/options2/chromeos/virtual_keyboard.css new file mode 100644 index 0000000..0752b3b --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/virtual_keyboard.css @@ -0,0 +1,24 @@ +/* + * 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. + */ + +.virtual-keyboard-column-headers { + display: -webkit-box; + font-size: 13px; + font-weight: bold; +} + +.virtual-keyboard-layout-column { + width: 250px; + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; +} + +#virtual-keyboard-manager list { + border-radius: 2px; + border: solid 1px #D9D9D9; + margin-bottom: 10px; + margin-top: 4px; +} diff --git a/chrome/browser/resources/options2/chromeos/virtual_keyboard.html b/chrome/browser/resources/options2/chromeos/virtual_keyboard.html new file mode 100644 index 0000000..c2c15fb --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/virtual_keyboard.html @@ -0,0 +1,13 @@ +<div id="virtual-keyboard-manager" class="page" hidden> + <h1 i18n-content="virtualKeyboardPage"></h1> + <div class="virtual-keyboard-column-headers"> + <div class="virtual-keyboard-layout-column"> + <h3 i18n-content="virtualKeyboardLayoutColumnTitle"></h3> + </div> + <div class="virtual-keyboard-keyboard-column"> + <h3 i18n-content="virtualKeyboardKeyboardColumnTitle"></h3> + </div> + </div> + <list id="virtual-keyboard-per-layout-list"></list> + <!-- TODO(yusukes): Add virtual-keyboards-per-site elements. --> +</div> diff --git a/chrome/browser/resources/options2/chromeos/virtual_keyboard.js b/chrome/browser/resources/options2/chromeos/virtual_keyboard.js new file mode 100644 index 0000000..359d190 --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/virtual_keyboard.js @@ -0,0 +1,92 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // VirtualKeyboardManager class: + + /** + * Virtual keyboard management page. + * @constructor + */ + function VirtualKeyboardManager() { + this.activeNavTab = null; + OptionsPage.call(this, + 'virtualKeyboards', + // The templateData.virtualKeyboardPageTabTitle is added + // in OptionsPageUIHandler::RegisterTitle(). + templateData.virtualKeyboardPageTabTitle, + 'virtual-keyboard-manager'); + } + + cr.addSingletonGetter(VirtualKeyboardManager); + + VirtualKeyboardManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The virtual keyboards list. + * @type {ItemList} + * @private + */ + virtualKeyboardsList_: null, + + /** @inheritDoc */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + this.createVirtualKeyboardsList_(); + }, + + /** @inheritDoc */ + didShowPage: function() { + chrome.send('updateVirtualKeyboardList'); + }, + + /** + * Creates, decorates and initializes the keyboards list. + * @private + */ + createVirtualKeyboardsList_: function() { + this.virtualKeyboardsList_ = $('virtual-keyboard-per-layout-list'); + options.VirtualKeyboardsList.decorate(this.virtualKeyboardsList_); + this.virtualKeyboardsList_.autoExpands = true; + }, + }; + + /** + * Sets the list of virtual keyboards shown in the view. This function is + * called by C++ code (e.g. chrome/browser/ui/webui/options/chromeos/). + * @param {Object} list A list of layouts with their registered virtual + * keyboards. + */ + VirtualKeyboardManager.updateVirtualKeyboardList = function(list) { + // See virtual_keyboard_list.js for an example of the format the list should + // take. + var filteredList = list.filter(function(element, index, array) { + // Don't show a layout which is supported by only one virtual keyboard + // extension. + return element.supportedKeyboards.length > 1; + }); + + // Sort the drop-down menu items by name. + filteredList.forEach(function(e) { + e.supportedKeyboards.sort(function(e1, e2) { + return e1.name > e2.name; + }); + }); + + // Sort the list by layout name. + $('virtual-keyboard-per-layout-list').setVirtualKeyboardList( + filteredList.sort(function(e1, e2) { + return e1.layoutName > e2.layoutName; + })); + }; + + // Export + return { + VirtualKeyboardManager: VirtualKeyboardManager, + }; +}); diff --git a/chrome/browser/resources/options2/chromeos/virtual_keyboard_list.js b/chrome/browser/resources/options2/chromeos/virtual_keyboard_list.js new file mode 100644 index 0000000..650de1a --- /dev/null +++ b/chrome/browser/resources/options2/chromeos/virtual_keyboard_list.js @@ -0,0 +1,146 @@ +// 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. + +cr.define('options', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const VirtualKeyboardOptions = options.VirtualKeyboardOptions; + + const localStrings = new LocalStrings(); + + /** + * Creates a virtual keyboard list item. + * + * Accepts values in the form + * { layout: 'us(dvorak)', + * layoutName: 'US Dvorak layout', + * preferredKeyboard: 'http://...', [optional] + * supportedKeyboards: [ + * { name: 'Simple Virtual Keyboard', + * isSystem: true, + * url: 'http://...' }, + * { name: '3rd party Virtual Keyboard', + * isSystem: false, + * url: 'http://...' }, + * ..., + * ] + * } + * @param {Object} entry A dictionary describing the virtual keyboards for a + * given layout. + * @constructor + * @extends {cr.ui.ListItem} + */ + function VirtualKeyboardListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = VirtualKeyboardListItem.prototype; + el.decorate(); + return el; + } + + VirtualKeyboardListItem.prototype = { + __proto__: ListItem.prototype, + + buildWidget_: function(data, delegate) { + // Layout name. + var layoutNameElement = document.createElement('div'); + layoutNameElement.textContent = data.layoutName; + layoutNameElement.className = 'virtual-keyboard-layout-column'; + this.appendChild(layoutNameElement); + + // Virtual keyboard selection. + var keyboardElement = document.createElement('div'); + var selectElement = document.createElement('select'); + var defaultOptionElement = document.createElement('option'); + defaultOptionElement.selected = (data.preferredKeyboard == null); + defaultOptionElement.textContent = + localStrings.getString('defaultVirtualKeyboard'); + defaultOptionElement.value = -1; + selectElement.appendChild(defaultOptionElement); + + for (var i = 0; i < data.supportedKeyboards.length; ++i) { + var optionElement = document.createElement('option'); + optionElement.selected = + (data.preferredKeyboard != null && + data.preferredKeyboard == data.supportedKeyboards[i].url); + optionElement.textContent = data.supportedKeyboards[i].name; + optionElement.value = i; + selectElement.appendChild(optionElement); + } + + selectElement.addEventListener('change', function(e) { + var index = e.target.value; + if (index == -1) { + // The 'Default' menu item is selected. Delete the preference. + delegate.clearPreference(data.layout); + } else { + delegate.setPreference( + data.layout, data.supportedKeyboards[index].url); + } + }); + + keyboardElement.appendChild(selectElement); + keyboardElement.className = 'virtual-keyboard-keyboard-column'; + this.appendChild(keyboardElement); + }, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + var delegate = { + clearPreference: function(layout) { + // Call a C++ function in chrome/browser/ui/webui/options/chromeos/. + chrome.send('clearVirtualKeyboardPreference', [layout]); + }, + setPreference: function(layout, url) { + chrome.send('setVirtualKeyboardPreference', [layout, url]); + }, + }; + + this.buildWidget_(this.dataItem, delegate); + }, + }; + + /** + * Create a new virtual keyboard list. + * @constructor + * @extends {cr.ui.List} + */ + var VirtualKeyboardsList = cr.ui.define('list'); + + VirtualKeyboardsList.prototype = { + __proto__: List.prototype, + + /** @inheritDoc */ + createItem: function(entry) { + return new VirtualKeyboardListItem(entry); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + + /** + * Set the virtual keyboards displayed by this list. + * See VirtualKeyboardListItem for an example of the format the list should + * take. + * + * @param {Object} list A list of layouts with their registered virtual + * keyboards. + */ + setVirtualKeyboardList: function(list) { + this.dataModel = new ArrayDataModel(list); + }, + }; + + return { + VirtualKeyboardListItem: VirtualKeyboardListItem, + VirtualKeyboardsList: VirtualKeyboardsList, + }; +}); diff --git a/chrome/browser/resources/options2/clear_browser_data_overlay.css b/chrome/browser/resources/options2/clear_browser_data_overlay.css new file mode 100644 index 0000000..b2cf716 --- /dev/null +++ b/chrome/browser/resources/options2/clear_browser_data_overlay.css @@ -0,0 +1,24 @@ +/* +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. +*/ + +#clearBrowserDataOverlay { + min-width: 500px; +} + +#clearBrowserDataOverlay > .content-area label { + margin: 5px 0; +} + +#clear-data-checkboxes { + -webkit-padding-start: 8px; + margin: 5px 0; +} + +#cbdThrobber { + margin: 4px 10px; + vertical-align: middle; + visibility: hidden; +} diff --git a/chrome/browser/resources/options2/clear_browser_data_overlay.html b/chrome/browser/resources/options2/clear_browser_data_overlay.html new file mode 100644 index 0000000..26ee6e1 --- /dev/null +++ b/chrome/browser/resources/options2/clear_browser_data_overlay.html @@ -0,0 +1,73 @@ +<div id="clearBrowserDataOverlay" class="page" hidden> + <h1 i18n-content="clearBrowserDataOverlay"></h1> + <div id="cbdContentArea" class="content-area"> + <span i18n-content="clearBrowserDataLabel"></span> + <select id="clearBrowserDataTimePeriod" + i18n-options="clearBrowserDataTimeList" + pref="browser.clear_data.time_period" + data-type="number"> + </select> + <div id="clear-data-checkboxes"> + <div class="checkbox"> + <label> + <input id="deleteBrowsingHistoryCheckbox" + pref="browser.clear_data.browsing_history" type="checkbox"> + <span i18n-content="deleteBrowsingHistoryCheckbox"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="deleteDownloadHistoryCheckbox" + pref="browser.clear_data.download_history" type="checkbox"> + <span i18n-content="deleteDownloadHistoryCheckbox"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="deleteCacheCheckbox" + pref="browser.clear_data.cache" type="checkbox"> + <span i18n-content="deleteCacheCheckbox"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="deleteCookiesCheckbox" + pref="browser.clear_data.cookies" type="checkbox"> + <span i18n-content="deleteCookiesFlashCheckbox" + class="clear-plugin-lso-data-enabled"></span> + <span i18n-content="deleteCookiesCheckbox" + class="clear-plugin-lso-data-disabled"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="deletePasswordsCheckbox" + pref="browser.clear_data.passwords" type="checkbox"> + <span i18n-content="deletePasswordsCheckbox"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="deleteFormDataCheckbox" + pref="browser.clear_data.form_data" type="checkbox"> + <span i18n-content="deleteFormDataCheckbox"></span> + </label> + </div> + </div> + </div> + <div class="action-area"> + <div class="flash-plugin-area"> + <a target="_blank" i18n-content="flash_storage_settings" + i18n-values="href:flash_storage_url"></a> + </div> + <div class="action-area-right"> + <div id="cbdThrobber" class="throbber"></div> + <div class="button-strip"> + <button id="clearBrowserDataDismiss" i18n-content="cancel"></button> + <button id="clearBrowserDataCommit" + i18n-content="clearBrowserDataCommit"> + </button> + </div> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/clear_browser_data_overlay.js b/chrome/browser/resources/options2/clear_browser_data_overlay.js new file mode 100644 index 0000000..8533ccc --- /dev/null +++ b/chrome/browser/resources/options2/clear_browser_data_overlay.js @@ -0,0 +1,110 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ClearBrowserDataOverlay class + * Encapsulated handling of the 'Clear Browser Data' overlay page. + * @class + */ + function ClearBrowserDataOverlay() { + OptionsPage.call(this, 'clearBrowserData', + templateData.clearBrowserDataOverlayTabTitle, + 'clearBrowserDataOverlay'); + } + + cr.addSingletonGetter(ClearBrowserDataOverlay); + + ClearBrowserDataOverlay.prototype = { + // Inherit ClearBrowserDataOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var f = this.updateCommitButtonState_.bind(this); + var types = ['browser.clear_data.browsing_history', + 'browser.clear_data.download_history', + 'browser.clear_data.cache', + 'browser.clear_data.cookies', + 'browser.clear_data.passwords', + 'browser.clear_data.form_data']; + types.forEach(function(type) { + Preferences.getInstance().addEventListener(type, f); + }); + + var checkboxes = document.querySelectorAll( + '#cbdContentArea input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].onclick = f; + } + this.updateCommitButtonState_(); + + $('clearBrowserDataDismiss').onclick = function(event) { + ClearBrowserDataOverlay.dismiss(); + }; + $('clearBrowserDataCommit').onclick = function(event) { + chrome.send('performClearBrowserData'); + }; + }, + + // Set the enabled state of the commit button. + updateCommitButtonState_: function() { + var checkboxes = document.querySelectorAll( + '#cbdContentArea input[type=checkbox]'); + var isChecked = false; + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].checked) { + isChecked = true; + break; + } + } + $('clearBrowserDataCommit').disabled = !isChecked; + }, + }; + + // + // Chrome callbacks + // + ClearBrowserDataOverlay.setClearingState = function(state) { + $('deleteBrowsingHistoryCheckbox').disabled = state; + $('deleteDownloadHistoryCheckbox').disabled = state; + $('deleteCacheCheckbox').disabled = state; + $('deleteCookiesCheckbox').disabled = state; + $('deletePasswordsCheckbox').disabled = state; + $('deleteFormDataCheckbox').disabled = state; + $('clearBrowserDataTimePeriod').disabled = state; + $('cbdThrobber').style.visibility = state ? 'visible' : 'hidden'; + + if (state) + $('clearBrowserDataCommit').disabled = true; + else + ClearBrowserDataOverlay.getInstance().updateCommitButtonState_(); + }; + + ClearBrowserDataOverlay.doneClearing = function() { + // The delay gives the user some feedback that the clearing + // actually worked. Otherwise the dialog just vanishes instantly in most + // cases. + window.setTimeout(function() { + ClearBrowserDataOverlay.dismiss(); + }, 200); + }; + + ClearBrowserDataOverlay.dismiss = function() { + OptionsPage.closeOverlay(); + this.setClearingState(false); + }; + + // Export + return { + ClearBrowserDataOverlay: ClearBrowserDataOverlay + }; +}); diff --git a/chrome/browser/resources/options2/content_settings.css b/chrome/browser/resources/options2/content_settings.css new file mode 100644 index 0000000..625ab18 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings.css @@ -0,0 +1,74 @@ +/* +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. +*/ + +.exception-pattern { + -webkit-box-flex: 1; + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; +} + +.exception-setting { + display: inline-block; + width: 120px; +} + +select.exception-setting { + vertical-align: middle; +} + +#exception-column-headers { + -webkit-margin-start: 17px; + display: -webkit-box; + margin-top: 17px; +} + +#exception-column-headers > div { + font-weight: bold; +} + +#exception-pattern-column { + -webkit-box-flex: 1; +} + +#exception-behavior-column { + width: 145px; +} + +.otr-explanation { + font-style: italic; +} + +#content-settings-exceptions-area list { + margin-bottom: 10px; + margin-top: 4px; +} + +#disable-plugins-container { + margin: 7px 0px; +} + +div[role="listitem"][managedby] { + color: #666; + font-style: italic; + position: relative; +} + +.settings-list div[role="listitem"][managedby="policy"], +.settings-list div[role="listitem"][managedby="extension"] { + background: -webkit-linear-gradient(#fff1b5, #fae692); + border-top: 0; + border-bottom: 1px solid #c9bd8d; +} + +list div[role="listitem"][managedby="policy"] .close-button { + background-image: url("chrome://theme/IDR_MANAGED"); + opacity: 1; +} + +list div[role="listitem"][managedby="extension"] .close-button { + background-image: url("chrome://theme/IDR_EXTENSIONS_SECTION_SMALL"); + opacity: 1; +} diff --git a/chrome/browser/resources/options2/content_settings.html b/chrome/browser/resources/options2/content_settings.html new file mode 100644 index 0000000..6d735f3 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings.html @@ -0,0 +1,279 @@ +<div id="content-settings-page" class="page" hidden> + <h1 i18n-content="contentSettingsPage"></h1> + <div class="displaytable"> + <!-- Cookie filter tab contents --> + <section> + <h3 i18n-content="cookies_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="cookies" value="allow"> + <span i18n-content="cookies_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="cookies" value="session"> + <span i18n-content="cookies_session_only"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="cookies" value="block"> + <span i18n-content="cookies_block"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input pref="profile.block_third_party_cookies" type="checkbox"> + <span i18n-content="cookies_block_3rd_party"></span> + </label> + </div> + <div class="checkbox"> + <label> + <input id="clear-cookies-on-exit" + pref="profile.clear_site_data_on_exit" type="checkbox"> + <span i18n-content="cookies_lso_clear_when_close" + class="clear-plugin-lso-data-enabled"></span> + <span i18n-content="cookies_clear_when_close" + class="clear-plugin-lso-data-disabled"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="cookies" + i18n-content="manage_exceptions"></button> + <button id="show-cookies-button" + i18n-content="cookies_show_cookies"></button> + </div> + </section> + <!-- Image filter --> + <section> + <h3 i18n-content="images_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="images" value="allow"> + <span i18n-content="images_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="images" value="block"> + <span i18n-content="images_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="images" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- JavaScript filter --> + <section> + <h3 i18n-content="javascript_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="javascript" value="allow"> + <span i18n-content="javascript_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="javascript" value="block"> + <span i18n-content="javascript_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="javascript" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Handlers settings --> + <if expr="pp_ifdef('enable_register_protocol_handler')"> + <section id="handlers-section"> + <h3 i18n-content="handlers_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="handlers" value="allow" + class="handler-radio"> + <span i18n-content="handlers_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="handlers" value="block" + class="handler-radio"> + <span i18n-content="handlers_block"></span> + </label> + </div> + <button id="manage-handlers-button" contentType="handlers" + i18n-content="manage_handlers"></button> + </div> + </section> + </if> + <!-- Plug-ins filter --> + <section> + <h3 i18n-content="plugins_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="plugins" value="allow"> + <span i18n-content="plugins_allow"></span> + </label> + </div> + <div id="click_to_play" class="radio"> + <label> + <input type="radio" name="plugins" value="ask"> + <span i18n-content="plugins_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="plugins" value="block"> + <span i18n-content="plugins_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="plugins" + i18n-content="manage_exceptions"></button> + <div id="disable-plugins-container"> + <a href="about:plugins" i18n-content="disableIndividualPlugins" + target="_blank"></a> + </div> + </div> + </section> + <!-- Pop-ups filter --> + <section> + <h3 i18n-content="popups_tab_label" class="content-settings-header"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="popups" value="allow"> + <span i18n-content="popups_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="popups" value="block"> + <span i18n-content="popups_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="popups" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Location filter --> + <section> + <h3 i18n-content="location_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="location" value="allow"> + <span i18n-content="location_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="location" value="ask"> + <span i18n-content="location_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="location" value="block"> + <span i18n-content="location_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="location" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Notifications filter tab contents --> + <section> + <h3 i18n-content="notifications_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="notifications" value="allow"> + <span i18n-content="notifications_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="notifications" value="ask"> + <span i18n-content="notifications_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="notifications" value="block"> + <span i18n-content="notifications_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="notifications" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Fullscreen filter --> + <section> + <h3 i18n-content="fullscreen_tab_label"></h3> + <div> + <button class="exceptions-list-button" contentType="fullscreen" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Mouse Lock filter --> + <section> + <h3 i18n-content="mouselock_tab_label"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="allow"> + <span i18n-content="mouselock_allow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="ask"> + <span i18n-content="mouselock_ask"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="mouselock" value="block"> + <span i18n-content="mouselock_block"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="mouselock" + i18n-content="manage_exceptions"></button> + </div> + </section> + <!-- Intent registration filter tab contents --> + <if expr="pp_ifdef('enable_web_intents')"> + <section id="intents-section"> + <h3 i18n-content="intentsTabLabel" class="content-settings-header"></h3> + <div> + <div class="radio"> + <label> + <input type="radio" name="intents" value="allow"> + <span i18n-content="intentsAllow"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="intents" value="ask"> + <span i18n-content="intentsAsk"></span> + </label> + </div> + <div class="radio"> + <label> + <input type="radio" name="intents" value="block"> + <span i18n-content="intentsBlock"></span> + </label> + </div> + <button class="exceptions-list-button" contentType="intents" + i18n-content="manage_exceptions"></button> + <button id="manage-intents-button" contentType="intents" + i18n-content="manageIntents"></button> + </div> + </section> + </if> + </div> +</div> diff --git a/chrome/browser/resources/options2/content_settings.js b/chrome/browser/resources/options2/content_settings.js new file mode 100644 index 0000000..94faa90 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings.js @@ -0,0 +1,154 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + ////////////////////////////////////////////////////////////////////////////// + // ContentSettings class: + + /** + * Encapsulated handling of content settings page. + * @constructor + */ + function ContentSettings() { + this.activeNavTab = null; + OptionsPage.call(this, 'content', templateData.contentSettingsPageTabTitle, + 'content-settings-page'); + } + + cr.addSingletonGetter(ContentSettings); + + ContentSettings.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + chrome.send('getContentFilterSettings'); + + var exceptionsButtons = + this.pageDiv.querySelectorAll('.exceptions-list-button'); + for (var i = 0; i < exceptionsButtons.length; i++) { + exceptionsButtons[i].onclick = function(event) { + var page = ContentSettingsExceptionsArea.getInstance(); + page.showList( + event.target.getAttribute('contentType')); + OptionsPage.navigateToPage('contentExceptions'); + // Add on the proper hash for the content type, and store that in the + // history so back/forward and tab restore works. + var hash = event.target.getAttribute('contentType'); + window.history.replaceState({pageName: page.name}, page.title, + '/' + page.name + "#" + hash); + }; + } + + var manageHandlersButton = $('manage-handlers-button'); + if (manageHandlersButton) { + manageHandlersButton.onclick = function(event) { + OptionsPage.navigateToPage('handlers'); + }; + } + + var manageIntentsButton = $('manage-intents-button'); + if (manageIntentsButton) { + manageIntentsButton.onclick = function(event) { + OptionsPage.navigateToPage('intents'); + }; + } + + // Cookies filter page --------------------------------------------------- + $('show-cookies-button').onclick = function(event) { + chrome.send('coreOptionsUserMetricsAction', ['Options_ShowCookies']); + OptionsPage.navigateToPage('cookies'); + }; + + if (!templateData.enable_click_to_play) + $('click_to_play').hidden = true; + + if (!templateData.enable_web_intents && $('intent-section')) + $('intent-section').hidden = true; + }, + }; + + ContentSettings.updateHandlersEnabledRadios = function(enabled) { + var selector = '#content-settings-page input[type=radio][value=' + + (enabled ? 'allow' : 'block') + '].handler-radio'; + document.querySelector(selector).checked = true; + }; + + /** + * Sets the values for all the content settings radios. + * @param {Object} dict A mapping from radio groups to the checked value for + * that group. + */ + ContentSettings.setContentFilterSettingsValue = function(dict) { + for (var group in dict) { + document.querySelector('input[type=radio][name=' + group + '][value=' + + dict[group]['value'] + ']').checked = true; + var radios = document.querySelectorAll('input[type=radio][name=' + + group + ']'); + var managedBy = dict[group]['managedBy']; + for (var i = 0, len = radios.length; i < len; i++) { + radios[i].disabled = (managedBy != 'default'); + radios[i].controlledBy = managedBy; + } + } + OptionsPage.updateManagedBannerVisibility(); + }; + + /** + * Initializes an exceptions list. + * @param {string} type The content type that we are setting exceptions for. + * @param {Array} list An array of pairs, where the first element of each pair + * is the filter string, and the second is the setting (allow/block). + */ + ContentSettings.setExceptions = function(type, list) { + var exceptionsList = + document.querySelector('div[contentType=' + type + ']' + + ' list[mode=normal]'); + exceptionsList.setExceptions(list); + }; + + ContentSettings.setHandlers = function(list) { + $('handlers-list').setHandlers(list); + }; + + ContentSettings.setIgnoredHandlers = function(list) { + $('ignored-handlers-list').setHandlers(list); + }; + + ContentSettings.setOTRExceptions = function(type, list) { + var exceptionsList = + document.querySelector('div[contentType=' + type + ']' + + ' list[mode=otr]'); + + exceptionsList.parentNode.hidden = false; + exceptionsList.setExceptions(list); + }; + + /** + * The browser's response to a request to check the validity of a given URL + * pattern. + * @param {string} type The content type. + * @param {string} mode The browser mode. + * @param {string} pattern The pattern. + * @param {bool} valid Whether said pattern is valid in the context of + * a content exception setting. + */ + ContentSettings.patternValidityCheckComplete = + function(type, mode, pattern, valid) { + var exceptionsList = + document.querySelector('div[contentType=' + type + '] ' + + 'list[mode=' + mode + ']'); + exceptionsList.patternValidityCheckComplete(pattern, valid); + }; + + // Export + return { + ContentSettings: ContentSettings + }; + +}); diff --git a/chrome/browser/resources/options2/content_settings_exceptions_area.html b/chrome/browser/resources/options2/content_settings_exceptions_area.html new file mode 100644 index 0000000..4e95c04 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings_exceptions_area.html @@ -0,0 +1,73 @@ +<div id="content-settings-exceptions-area" class="page" hidden> + <h1></h1> + <div id="exception-column-headers"> + <div id="exception-pattern-column" i18n-content="exceptionPatternHeader"> + </div> + <div id="exception-behavior-column" i18n-content="exceptionBehaviorHeader"> + </div> + </div> + <div contentType="cookies"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + <div class="flash-plugin-area"> + <a i18n-values="href:flash_storage_url" + i18n-content="flash_storage_settings" target="_blank"></a> + </div> + </div> + <div contentType="images"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + </div> + <div contentType="javascript"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + </div> + <div contentType="plugins"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + </div> + <div contentType="popups"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + </div> + <div contentType="location"> + <list mode="normal"></list> + </div> + <div contentType="notifications"> + <list mode="normal"></list> + </div> + <div contentType="intents"> + <list mode="normal"></list> + </div> + <div contentType="fullscreen"> + <list mode="normal"></list> + <div> + <span class="otr-explanation" + i18n-content="otr_exceptions_explanation"></span> + <list mode="otr"></list> + </div> + </div> + <div contentType="mouselock"> + <list mode="normal"></list> + </div> +</div> diff --git a/chrome/browser/resources/options2/content_settings_exceptions_area.js b/chrome/browser/resources/options2/content_settings_exceptions_area.js new file mode 100644 index 0000000..7adde68 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings_exceptions_area.js @@ -0,0 +1,552 @@ +// 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. + +cr.define('options.contentSettings', function() { + const InlineEditableItemList = options.InlineEditableItemList; + const InlineEditableItem = options.InlineEditableItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new exceptions list item. + * @param {string} contentType The type of the list. + * @param {string} mode The browser mode, 'otr' or 'normal'. + * @param {boolean} enableAskOption Whether to show an 'ask every time' + * option in the select. + * @param {Object} exception A dictionary that contains the data of the + * exception. + * @constructor + * @extends {options.InlineEditableItem} + */ + function ExceptionsListItem(contentType, mode, enableAskOption, exception) { + var el = cr.doc.createElement('div'); + el.mode = mode; + el.contentType = contentType; + el.enableAskOption = enableAskOption; + el.dataItem = exception; + el.__proto__ = ExceptionsListItem.prototype; + el.decorate(); + + return el; + } + + ExceptionsListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Called when an element is decorated as a list item. + */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + this.isPlaceholder = !this.pattern; + var patternCell = this.createEditableTextCell(this.pattern); + patternCell.className = 'exception-pattern'; + patternCell.classList.add('weakrtl'); + this.contentElement.appendChild(patternCell); + if (this.pattern) + this.patternLabel = patternCell.querySelector('.static-text'); + var input = patternCell.querySelector('input'); + + // TODO(stuartmorgan): Create an createEditableSelectCell abstracting + // this code. + // Setting label for display mode. |pattern| will be null for the 'add new + // exception' row. + if (this.pattern) { + var settingLabel = cr.doc.createElement('span'); + settingLabel.textContent = this.settingForDisplay(); + settingLabel.className = 'exception-setting'; + settingLabel.setAttribute('displaymode', 'static'); + this.contentElement.appendChild(settingLabel); + this.settingLabel = settingLabel; + } + + // Setting select element for edit mode. + var select = cr.doc.createElement('select'); + var optionAllow = cr.doc.createElement('option'); + optionAllow.textContent = templateData.allowException; + optionAllow.value = 'allow'; + select.appendChild(optionAllow); + + if (this.enableAskOption) { + var optionAsk = cr.doc.createElement('option'); + optionAsk.textContent = templateData.askException; + optionAsk.value = 'ask'; + select.appendChild(optionAsk); + } + + if (this.contentType == 'cookies') { + var optionSession = cr.doc.createElement('option'); + optionSession.textContent = templateData.sessionException; + optionSession.value = 'session'; + select.appendChild(optionSession); + } + + if (this.contentType != 'fullscreen') { + var optionBlock = cr.doc.createElement('option'); + optionBlock.textContent = templateData.blockException; + optionBlock.value = 'block'; + select.appendChild(optionBlock); + } + + this.contentElement.appendChild(select); + select.className = 'exception-setting'; + if (this.pattern) + select.setAttribute('displaymode', 'edit'); + + // Used to track whether the URL pattern in the input is valid. + // This will be true if the browser process has informed us that the + // current text in the input is valid. Changing the text resets this to + // false, and getting a response from the browser sets it back to true. + // It starts off as false for empty string (new exceptions) or true for + // already-existing exceptions (which we assume are valid). + this.inputValidityKnown = this.pattern; + // This one tracks the actual validity of the pattern in the input. This + // starts off as true so as not to annoy the user when he adds a new and + // empty input. + this.inputIsValid = true; + + this.input = input; + this.select = select; + + this.updateEditables(); + + // Editing notifications and geolocation is disabled for now. + if (this.contentType == 'notifications' || + this.contentType == 'location') { + this.editable = false; + } + + // If the source of the content setting exception is not the user + // preference, then the content settings exception is managed and the user + // can't edit it. + if (this.dataItem.source && + this.dataItem.source != 'preference') { + this.setAttribute('managedby', this.dataItem.source); + this.deletable = false; + this.editable = false; + } + + var listItem = this; + // Handle events on the editable nodes. + input.oninput = function(event) { + listItem.inputValidityKnown = false; + chrome.send('checkExceptionPatternValidity', + [listItem.contentType, listItem.mode, input.value]); + }; + + // Listen for edit events. + this.addEventListener('canceledit', this.onEditCancelled_); + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** + * The pattern (e.g., a URL) for the exception. + * @type {string} + */ + get pattern() { + return this.dataItem['displayPattern']; + }, + set pattern(pattern) { + this.dataItem['displayPattern'] = pattern; + }, + + /** + * The setting (allow/block) for the exception. + * @type {string} + */ + get setting() { + return this.dataItem['setting']; + }, + set setting(setting) { + this.dataItem['setting'] = setting; + }, + + /** + * Gets a human-readable setting string. + * @type {string} + */ + settingForDisplay: function() { + var setting = this.setting; + if (setting == 'allow') + return templateData.allowException; + else if (setting == 'block') + return templateData.blockException; + else if (setting == 'ask') + return templateData.askException; + else if (setting == 'session') + return templateData.sessionException; + }, + + /** + * Update this list item to reflect whether the input is a valid pattern. + * @param {boolean} valid Whether said pattern is valid in the context of + * a content exception setting. + */ + setPatternValid: function(valid) { + if (valid || !this.input.value) + this.input.setCustomValidity(''); + else + this.input.setCustomValidity(' '); + this.inputIsValid = valid; + this.inputValidityKnown = true; + }, + + /** + * Set the <input> to its original contents. Used when the user quits + * editing. + */ + resetInput: function() { + this.input.value = this.pattern; + }, + + /** + * Copy the data model values to the editable nodes. + */ + updateEditables: function() { + this.resetInput(); + + var settingOption = + this.select.querySelector('[value=\'' + this.setting + '\']'); + if (settingOption) + settingOption.selected = true; + }, + + /** @inheritDoc */ + get currentInputIsValid() { + return this.inputValidityKnown && this.inputIsValid; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + var livePattern = this.input.value; + var liveSetting = this.select.value; + return livePattern != this.pattern || liveSetting != this.setting; + }, + + /** + * Called when committing an edit. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var newPattern = this.input.value; + var newSetting = this.select.value; + + this.finishEdit(newPattern, newSetting); + }, + + /** + * Called when cancelling an edit; resets the control states. + * @param {Event} e The cancel event. + * @private + */ + onEditCancelled_: function() { + this.updateEditables(); + this.setPatternValid(true); + }, + + /** + * Editing is complete; update the model. + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.patternLabel.textContent = newPattern; + this.settingLabel.textContent = this.settingForDisplay(); + var oldPattern = this.pattern; + this.pattern = newPattern; + this.setting = newSetting; + + // TODO(estade): this will need to be updated if geolocation/notifications + // become editable. + if (oldPattern != newPattern) { + chrome.send('removeException', + [this.contentType, this.mode, oldPattern]); + } + + chrome.send('setException', + [this.contentType, this.mode, newPattern, newSetting]); + } + }; + + /** + * Creates a new list item for the Add New Item row, which doesn't represent + * an actual entry in the exceptions list but allows the user to add new + * exceptions. + * @param {string} contentType The type of the list. + * @param {string} mode The browser mode, 'otr' or 'normal'. + * @param {boolean} enableAskOption Whether to show an 'ask every time' + * option in the select. + * @constructor + * @extends {cr.ui.ExceptionsListItem} + */ + function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { + var el = cr.doc.createElement('div'); + el.mode = mode; + el.contentType = contentType; + el.enableAskOption = enableAskOption; + el.dataItem = []; + el.__proto__ = ExceptionsAddRowListItem.prototype; + el.decorate(); + + return el; + } + + ExceptionsAddRowListItem.prototype = { + __proto__: ExceptionsListItem.prototype, + + decorate: function() { + ExceptionsListItem.prototype.decorate.call(this); + + this.input.placeholder = templateData.addNewExceptionInstructions; + + // Do we always want a default of allow? + this.setting = 'allow'; + }, + + /** + * Clear the <input> and let the placeholder text show again. + */ + resetInput: function() { + this.input.value = ''; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + return this.input.value != ''; + }, + + /** + * Editing is complete; update the model. As long as the pattern isn't + * empty, we'll just add it. + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.resetInput(); + chrome.send('setException', + [this.contentType, this.mode, newPattern, newSetting]); + }, + }; + + /** + * Creates a new exceptions list. + * @constructor + * @extends {cr.ui.List} + */ + var ExceptionsList = cr.ui.define('list'); + + ExceptionsList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * Called when an element is decorated as a list. + */ + decorate: function() { + InlineEditableItemList.prototype.decorate.call(this); + + this.classList.add('settings-list'); + + for (var parentNode = this.parentNode; parentNode; + parentNode = parentNode.parentNode) { + if (parentNode.hasAttribute('contentType')) { + this.contentType = parentNode.getAttribute('contentType'); + break; + } + } + + this.mode = this.getAttribute('mode'); + + var exceptionList = this; + + // Whether the exceptions in this list allow an 'Ask every time' option. + this.enableAskOption = (this.contentType == 'plugins' && + templateData.enable_click_to_play); + + this.autoExpands = true; + this.reset(); + }, + + /** + * Creates an item to go in the list. + * @param {Object} entry The element from the data model for this row. + */ + createItem: function(entry) { + if (entry) { + return new ExceptionsListItem(this.contentType, + this.mode, + this.enableAskOption, + entry); + } else { + var addRowItem = new ExceptionsAddRowListItem(this.contentType, + this.mode, + this.enableAskOption); + addRowItem.deletable = false; + return addRowItem; + } + }, + + /** + * Sets the exceptions in the js model. + * @param {Object} entries A list of dictionaries of values, each dictionary + * represents an exception. + */ + setExceptions: function(entries) { + var deleteCount = this.dataModel.length; + + if (this.isEditable()) { + // We don't want to remove the Add New Exception row. + deleteCount = deleteCount - 1; + } + + var args = [0, deleteCount]; + args.push.apply(args, entries); + this.dataModel.splice.apply(this.dataModel, args); + }, + + /** + * The browser has finished checking a pattern for validity. Update the + * list item to reflect this. + * @param {string} pattern The pattern. + * @param {bool} valid Whether said pattern is valid in the context of + * a content exception setting. + */ + patternValidityCheckComplete: function(pattern, valid) { + var listItems = this.items; + for (var i = 0; i < listItems.length; i++) { + var listItem = listItems[i]; + // Don't do anything for messages for the item if it is not the intended + // recipient, or if the response is stale (i.e. the input value has + // changed since we sent the request to analyze it). + if (pattern == listItem.input.value) + listItem.setPatternValid(valid); + } + }, + + /** + * Returns whether the rows are editable in this list. + */ + isEditable: function() { + // Editing notifications and geolocation is disabled for now. + return !(this.contentType == 'notifications' || + this.contentType == 'location' || + this.contentType == 'fullscreen'); + }, + + /** + * Removes all exceptions from the js model. + */ + reset: function() { + if (this.isEditable()) { + // The null creates the Add New Exception row. + this.dataModel = new ArrayDataModel([null]); + } else { + this.dataModel = new ArrayDataModel([]); + } + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var listItem = this.getListItemByIndex(index); + if (listItem.undeletable) + return; + + var dataItem = listItem.dataItem; + var args = [listItem.contentType]; + if (listItem.contentType == 'location') + args.push(dataItem['origin'], dataItem['embeddingOrigin']); + else if (listItem.contentType == 'notifications') + args.push(dataItem['origin'], dataItem['setting']); + else + args.push(listItem.mode, listItem.pattern); + + chrome.send('removeException', args); + }, + }; + + var OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of content settings list subpage. + * @constructor + */ + function ContentSettingsExceptionsArea() { + OptionsPage.call(this, 'contentExceptions', + templateData.contentSettingsPageTabTitle, + 'content-settings-exceptions-area'); + } + + cr.addSingletonGetter(ContentSettingsExceptionsArea); + + ContentSettingsExceptionsArea.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var exceptionsLists = this.pageDiv.querySelectorAll('list'); + for (var i = 0; i < exceptionsLists.length; i++) { + options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); + } + + ContentSettingsExceptionsArea.hideOTRLists(); + + // If the user types in the URL without a hash, show just cookies. + this.showList('cookies'); + }, + + /** + * Shows one list and hides all others. + * @param {string} type The content type. + */ + showList: function(type) { + var header = this.pageDiv.querySelector('h1'); + header.textContent = templateData[type + '_header']; + + var divs = this.pageDiv.querySelectorAll('div[contentType]'); + for (var i = 0; i < divs.length; i++) { + if (divs[i].getAttribute('contentType') == type) + divs[i].hidden = false; + else + divs[i].hidden = true; + } + }, + + /** + * Called after the page has been shown. Show the content type for the + * location's hash. + */ + didShowPage: function() { + var hash = location.hash; + if (hash) + this.showList(hash.slice(1)); + }, + }; + + /** + * Called when the last incognito window is closed. + */ + ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { + this.hideOTRLists(); + }; + + /** + * Clears and hides the incognito exceptions lists. + */ + ContentSettingsExceptionsArea.hideOTRLists = function() { + var otrLists = document.querySelectorAll('list[mode=otr]'); + + for (var i = 0; i < otrLists.length; i++) { + otrLists[i].reset(); + otrLists[i].parentNode.hidden = true; + } + }; + + return { + ExceptionsListItem: ExceptionsListItem, + ExceptionsAddRowListItem: ExceptionsAddRowListItem, + ExceptionsList: ExceptionsList, + ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, + }; +}); diff --git a/chrome/browser/resources/options2/content_settings_ui.js b/chrome/browser/resources/options2/content_settings_ui.js new file mode 100644 index 0000000..dc7b0c4 --- /dev/null +++ b/chrome/browser/resources/options2/content_settings_ui.js @@ -0,0 +1,67 @@ +// 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. + +cr.define('options', function() { + + ////////////////////////////////////////////////////////////////////////////// + // ContentSettingsRadio class: + + // Define a constructor that uses an input element as its underlying element. + var ContentSettingsRadio = cr.ui.define('input'); + + ContentSettingsRadio.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'radio'; + var self = this; + + this.addEventListener('change', + function(e) { + chrome.send('setContentFilter', [this.name, this.value]); + }); + }, + }; + + /** + * Whether the content setting is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(ContentSettingsRadio, 'controlledBy', cr.PropertyKind.ATTR); + + ////////////////////////////////////////////////////////////////////////////// + // HandlersEnabledRadio class: + + // Define a constructor that uses an input element as its underlying element. + var HandlersEnabledRadio = cr.ui.define('input'); + + HandlersEnabledRadio.prototype = { + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'radio'; + var self = this; + + this.addEventListener('change', + function(e) { + chrome.send('setHandlersEnabled', [this.value == 'allow']); + }); + }, + }; + + // Export + return { + ContentSettingsRadio: ContentSettingsRadio, + HandlersEnabledRadio: HandlersEnabledRadio + }; + +}); + diff --git a/chrome/browser/resources/options2/controlled_setting.js b/chrome/browser/resources/options2/controlled_setting.js new file mode 100644 index 0000000..0ca4e31 --- /dev/null +++ b/chrome/browser/resources/options2/controlled_setting.js @@ -0,0 +1,138 @@ +// 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. + +cr.define('options', function() { + var Preferences = options.Preferences; + + /** + * A controlled setting indicator that can be placed on a setting as an + * indicator that the value is controlled by some external entity such as + * policy or an extension. + * @constructor + * @extends {HTMLSpanElement} + */ + var ControlledSettingIndicator = cr.ui.define('span'); + + ControlledSettingIndicator.prototype = { + __proto__: HTMLSpanElement.prototype, + + /** + * Decorates the base element to show the proper icon. + */ + decorate: function() { + var self = this; + var doc = self.ownerDocument; + + // Create the details and summary elements. + var detailsContainer = doc.createElement('details'); + detailsContainer.appendChild(doc.createElement('summary')); + + // This should really create a div element, but that breaks :hover. See + // https://bugs.webkit.org/show_bug.cgi?id=72957 + var bubbleContainer = doc.createElement('p'); + bubbleContainer.className = 'controlled-setting-bubble'; + detailsContainer.appendChild(bubbleContainer); + + self.appendChild(detailsContainer); + + // If there is a pref, track its controlledBy property in order to be able + // to bring up the correct bubble. + if (this.hasAttribute('pref')) { + Preferences.getInstance().addEventListener( + this.getAttribute('pref'), + function(event) { + if (event.value) { + var controlledBy = event.value['controlledBy']; + self.controlledBy = controlledBy ? controlledBy : null; + } + }); + } + + self.addEventListener('click', self.show_); + }, + + + /** + * Closes the bubble. + */ + close: function() { + this.querySelector('details').removeAttribute('open'); + this.ownerDocument.removeEventListener('click', this.closeHandler_, true); + }, + + /** + * Constructs the bubble DOM tree and shows it. + * @private + */ + show_: function() { + var self = this; + var doc = self.ownerDocument; + + // Clear out the old bubble contents. + var bubbleContainer = this.querySelector('.controlled-setting-bubble'); + if (bubbleContainer) { + while (bubbleContainer.hasChildNodes()) + bubbleContainer.removeChild(bubbleContainer.lastChild); + } + + // Work out the bubble text. + defaultStrings = { + 'policy' : localStrings.getString('controlledSettingPolicy'), + 'extension' : localStrings.getString('controlledSettingExtension'), + 'recommended' : localStrings.getString('controlledSettingRecommended'), + }; + + // No controller, no bubble. + if (!self.controlledBy || !self.controlledBy in defaultStrings) + return; + + var text = defaultStrings[self.controlledBy]; + + // Apply text overrides. + if (self.hasAttribute('text' + self.controlledBy)) + text = self.getAttribute('text' + self.controlledBy); + + // Create the DOM tree. + var bubbleText = doc.createElement('p'); + bubbleText.className = 'controlled-setting-bubble-text'; + bubbleText.textContent = text; + + var pref = self.getAttribute('pref'); + if (self.controlledBy == 'recommended' && pref) { + var container = doc.createElement('div'); + var action = doc.createElement('button'); + action.classList.add('link-button'); + action.classList.add('controlled-setting-bubble-action'); + action.textContent = + localStrings.getString('controlledSettingApplyRecommendation'); + action.addEventListener( + 'click', + function(e) { + Preferences.clearPref(pref); + }); + container.appendChild(action); + bubbleText.appendChild(container); + } + + bubbleContainer.appendChild(bubbleText); + + // One-time bubble-closing event handler. + self.closeHandler_ = this.close.bind(this); + doc.addEventListener('click', self.closeHandler_, true); + } + }; + + /** + * The controlling entity of the setting. Can take the values "policy", + * "extension", "recommended" or be unset. + */ + cr.defineProperty(ControlledSettingIndicator, 'controlledBy', + cr.PropertyKind.ATTR, + ControlledSettingIndicator.prototype.close); + + // Export. + return { + ControlledSettingIndicator : ControlledSettingIndicator + }; +}); diff --git a/chrome/browser/resources/options2/cookies_list.js b/chrome/browser/resources/options2/cookies_list.js new file mode 100644 index 0000000..4e70d1d --- /dev/null +++ b/chrome/browser/resources/options2/cookies_list.js @@ -0,0 +1,853 @@ +// 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. + +cr.define('options', function() { + const DeletableItemList = options.DeletableItemList; + const DeletableItem = options.DeletableItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + // This structure maps the various cookie type names from C++ (hence the + // underscores) to arrays of the different types of data each has, along with + // the i18n name for the description of that data type. + const cookieInfo = { + 'cookie': [ ['name', 'label_cookie_name'], + ['content', 'label_cookie_content'], + ['domain', 'label_cookie_domain'], + ['path', 'label_cookie_path'], + ['sendfor', 'label_cookie_send_for'], + ['accessibleToScript', 'label_cookie_accessible_to_script'], + ['created', 'label_cookie_created'], + ['expires', 'label_cookie_expires'] ], + 'app_cache': [ ['manifest', 'label_app_cache_manifest'], + ['size', 'label_local_storage_size'], + ['created', 'label_cookie_created'], + ['accessed', 'label_cookie_last_accessed'] ], + 'database': [ ['name', 'label_cookie_name'], + ['desc', 'label_webdb_desc'], + ['size', 'label_local_storage_size'], + ['modified', 'label_local_storage_last_modified'] ], + 'local_storage': [ ['origin', 'label_local_storage_origin'], + ['size', 'label_local_storage_size'], + ['modified', 'label_local_storage_last_modified'] ], + 'indexed_db': [ ['origin', 'label_indexed_db_origin'], + ['size', 'label_indexed_db_size'], + ['modified', 'label_indexed_db_last_modified'] ], + 'file_system': [ ['origin', 'label_file_system_origin'], + ['persistent', 'label_file_system_persistent_usage' ], + ['temporary', 'label_file_system_temporary_usage' ] ], + }; + + const localStrings = new LocalStrings(); + + /** + * Returns the item's height, like offsetHeight but such that it works better + * when the page is zoomed. See the similar calculation in @{code cr.ui.List}. + * This version also accounts for the animation done in this file. + * @param {Element} item The item to get the height of. + * @return {number} The height of the item, calculated with zooming in mind. + */ + function getItemHeight(item) { + var height = item.style.height; + // Use the fixed animation target height if set, in case the element is + // currently being animated and we'd get an intermediate height below. + if (height && height.substr(-2) == 'px') + return parseInt(height.substr(0, height.length - 2)); + return item.getBoundingClientRect().height; + } + + /** + * Create tree nodes for the objects in the data array, and insert them all + * into the given list using its @{code splice} method at the given index. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + * @return {Array.<CookieTreeNode>} An array of CookieTreeNodes added. + */ + function spliceTreeNodes(data, start, list) { + var nodes = data.map(function(x) { return new CookieTreeNode(x); }); + // Insert [start, 0] at the beginning of the array of nodes, making it + // into the arguments we want to pass to @{code list.splice} below. + nodes.splice(0, 0, start, 0); + list.splice.apply(list, nodes); + // Remove the [start, 0] prefix and return the array of nodes. + nodes.splice(0, 2); + return nodes; + } + + var parentLookup = {}; + var lookupRequests = {}; + + /** + * Creates a new list item for sites data. Note that these are created and + * destroyed lazily as they scroll into and out of view, so they must be + * stateless. We cache the expanded item in @{code CookiesList} though, so it + * can keep state. (Mostly just which item is selected.) + * @param {Object} origin Data used to create a cookie list item. + * @param {CookiesList} list The list that will contain this item. + * @constructor + * @extends {DeletableItem} + */ + function CookieListItem(origin, list) { + var listItem = new DeletableItem(null); + listItem.__proto__ = CookieListItem.prototype; + + listItem.origin = origin; + listItem.list = list; + listItem.decorate(); + + // This hooks up updateOrigin() to the list item, makes the top-level + // tree nodes (i.e., origins) register their IDs in parentLookup, and + // causes them to request their children if they have none. Note that we + // have special logic in the setter for the parent property to make sure + // that we can still garbage collect list items when they scroll out of + // view, even though it appears that we keep a direct reference. + if (origin) { + origin.parent = listItem; + origin.updateOrigin(); + } + + return listItem; + } + + CookieListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + this.siteChild = this.ownerDocument.createElement('div'); + this.siteChild.className = 'cookie-site'; + this.dataChild = this.ownerDocument.createElement('div'); + this.dataChild.className = 'cookie-data'; + this.sizeChild = this.ownerDocument.createElement('div'); + this.sizeChild.className = 'cookie-size'; + this.itemsChild = this.ownerDocument.createElement('div'); + this.itemsChild.className = 'cookie-items'; + this.infoChild = this.ownerDocument.createElement('div'); + this.infoChild.className = 'cookie-details'; + this.infoChild.hidden = true; + var remove = this.ownerDocument.createElement('button'); + remove.textContent = localStrings.getString('remove_cookie'); + remove.onclick = this.removeCookie_.bind(this); + this.infoChild.appendChild(remove); + var content = this.contentElement; + content.appendChild(this.siteChild); + content.appendChild(this.dataChild); + content.appendChild(this.sizeChild); + content.appendChild(this.itemsChild); + this.itemsChild.appendChild(this.infoChild); + if (this.origin && this.origin.data) { + this.siteChild.textContent = this.origin.data.title; + this.siteChild.setAttribute('title', this.origin.data.title); + } + this.itemList_ = []; + }, + + /** @type {boolean} */ + get expanded() { + return this.expanded_; + }, + set expanded(expanded) { + if (this.expanded_ == expanded) + return; + this.expanded_ = expanded; + if (expanded) { + var oldExpanded = this.list.expandedItem; + this.list.expandedItem = this; + this.updateItems_(); + if (oldExpanded) + oldExpanded.expanded = false; + this.classList.add('show-items'); + } else { + if (this.list.expandedItem == this) { + this.list.expandedItem = null; + } + this.style.height = ''; + this.itemsChild.style.height = ''; + this.classList.remove('show-items'); + } + }, + + /** + * The callback for the "remove" button shown when an item is selected. + * Requests that the currently selected cookie be removed. + * @private + */ + removeCookie_: function() { + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.node) + chrome.send('removeCookie', [item.node.pathId]); + } + }, + + /** + * Disable animation within this cookie list item, in preparation for making + * changes that will need to be animated. Makes it possible to measure the + * contents without displaying them, to set animation targets. + * @private + */ + disableAnimation_: function() { + this.itemsHeight_ = getItemHeight(this.itemsChild); + this.classList.add('measure-items'); + }, + + /** + * Enable animation after changing the contents of this cookie list item. + * See @{code disableAnimation_}. + * @private + */ + enableAnimation_: function() { + if (!this.classList.contains('measure-items')) + this.disableAnimation_(); + this.itemsChild.style.height = ''; + // This will force relayout in order to calculate the new heights. + var itemsHeight = getItemHeight(this.itemsChild); + var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_; + this.itemsChild.style.height = this.itemsHeight_ + 'px'; + // Force relayout before enabling animation, so that if we have + // changed things since the last layout, they will not be animated + // during subsequent layouts. + this.itemsChild.offsetHeight; + this.classList.remove('measure-items'); + this.itemsChild.style.height = itemsHeight + 'px'; + this.style.height = fixedHeight + 'px'; + }, + + /** + * Updates the origin summary to reflect changes in its items. + * Both CookieListItem and CookieTreeNode implement this API. + * This implementation scans the descendants to update the text. + */ + updateOrigin: function() { + var info = { + cookies: 0, + database: false, + localStorage: false, + appCache: false, + indexedDb: false, + fileSystem: false, + }; + if (this.origin) + this.origin.collectSummaryInfo(info); + var list = []; + if (info.cookies > 1) + list.push(localStrings.getStringF('cookie_plural', info.cookies)); + else if (info.cookies > 0) + list.push(localStrings.getString('cookie_singular')); + if (info.database || info.indexedDb) + list.push(localStrings.getString('cookie_database_storage')); + if (info.localStorage) + list.push(localStrings.getString('cookie_local_storage')); + if (info.appCache) + list.push(localStrings.getString('cookie_app_cache')); + if (info.fileSystem) + list.push(localStrings.getString('cookie_file_system')); + var text = ''; + for (var i = 0; i < list.length; ++i) + if (text.length > 0) + text += ', ' + list[i]; + else + text = list[i]; + this.dataChild.textContent = text; + if (info.quota && info.quota.totalUsage) { + this.sizeChild.textContent = info.quota.totalUsage; + } + + if (this.expanded) + this.updateItems_(); + }, + + /** + * Updates the items section to reflect changes, animating to the new state. + * Removes existing contents and calls @{code CookieTreeNode.createItems}. + * @private + */ + updateItems_: function() { + this.disableAnimation_(); + this.itemsChild.textContent = ''; + this.infoChild.hidden = true; + this.selectedIndex_ = -1; + this.itemList_ = []; + if (this.origin) + this.origin.createItems(this); + this.itemsChild.appendChild(this.infoChild); + this.enableAnimation_(); + }, + + /** + * Append a new cookie node "bubble" to this list item. + * @param {CookieTreeNode} node The cookie node to add a bubble for. + * @param {Element} div The DOM element for the bubble itself. + * @return {number} The index the bubble was added at. + */ + appendItem: function(node, div) { + this.itemList_.push({node: node, div: div}); + this.itemsChild.appendChild(div); + return this.itemList_.length - 1; + }, + + /** + * The currently selected cookie node ("cookie bubble") index. + * @type {number} + * @private + */ + selectedIndex_: -1, + + /** + * Get the currently selected cookie node ("cookie bubble") index. + * @type {number} + */ + get selectedIndex() { + return this.selectedIndex_; + }, + + /** + * Set the currently selected cookie node ("cookie bubble") index to + * @{code itemIndex}, unselecting any previously selected node first. + * @param {number} itemIndex The index to set as the selected index. + */ + set selectedIndex(itemIndex) { + // Get the list index up front before we change anything. + var index = this.list.getIndexOfListItem(this); + // Unselect any previously selected item. + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.div) + item.div.removeAttribute('selected'); + } + // Special case: decrementing -1 wraps around to the end of the list. + if (itemIndex == -2) + itemIndex = this.itemList_.length - 1; + // Check if we're going out of bounds and hide the item details. + if (itemIndex < 0 || itemIndex >= this.itemList_.length) { + this.selectedIndex_ = -1; + this.disableAnimation_(); + this.infoChild.hidden = true; + this.enableAnimation_(); + return; + } + // Set the new selected item and show the item details for it. + this.selectedIndex_ = itemIndex; + this.itemList_[itemIndex].div.setAttribute('selected', ''); + this.disableAnimation_(); + this.itemList_[itemIndex].node.setDetailText(this.infoChild, + this.list.infoNodes); + this.infoChild.hidden = false; + this.enableAnimation_(); + // If we're near the bottom of the list this may cause the list item to go + // beyond the end of the visible area. Fix it after the animation is done. + var list = this.list; + window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150); + }, + }; + + /** + * {@code CookieTreeNode}s mirror the structure of the cookie tree lazily, and + * contain all the actual data used to generate the {@code CookieListItem}s. + * @param {Object} data The data object for this node. + * @constructor + */ + function CookieTreeNode(data) { + this.data = data; + this.children = []; + } + + CookieTreeNode.prototype = { + /** + * Insert the given list of cookie tree nodes at the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + */ + insertAt: function(data, start) { + var nodes = spliceTreeNodes(data, start, this.children); + for (var i = 0; i < nodes.length; i++) + nodes[i].parent = this; + this.updateOrigin(); + }, + + /** + * Remove a cookie tree node from the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.children.length) { + this.children.splice(index, 1); + this.updateOrigin(); + } + }, + + /** + * Clears all children. + * Both CookiesList and CookieTreeNode implement this API. + * It is used by CookiesList.loadChildren(). + */ + clear: function() { + // We might leave some garbage in parentLookup for removed children. + // But that should be OK because parentLookup is cleared when we + // reload the tree. + this.children = []; + this.updateOrigin(); + }, + + /** + * The counter used by startBatchUpdates() and endBatchUpdates(). + * @type {number} + */ + batchCount_: 0, + + /** + * See cr.ui.List.startBatchUpdates(). + * Both CookiesList (via List) and CookieTreeNode implement this API. + */ + startBatchUpdates: function() { + this.batchCount_++; + }, + + /** + * See cr.ui.List.endBatchUpdates(). + * Both CookiesList (via List) and CookieTreeNode implement this API. + */ + endBatchUpdates: function() { + if (!--this.batchCount_) + this.updateOrigin(); + }, + + /** + * Requests updating the origin summary to reflect changes in this item. + * Both CookieListItem and CookieTreeNode implement this API. + */ + updateOrigin: function() { + if (!this.batchCount_ && this.parent) + this.parent.updateOrigin(); + }, + + /** + * Summarize the information in this node and update @{code info}. + * This will recurse into child nodes to summarize all descendants. + * @param {Object} info The info object from @{code updateOrigin}. + */ + collectSummaryInfo: function(info) { + if (this.children.length > 0) { + for (var i = 0; i < this.children.length; ++i) + this.children[i].collectSummaryInfo(info); + } else if (this.data && !this.data.hasChildren) { + if (this.data.type == 'cookie') { + info.cookies++; + } else if (this.data.type == 'database') { + info.database = true; + } else if (this.data.type == 'local_storage') { + info.localStorage = true; + } else if (this.data.type == 'app_cache') { + info.appCache = true; + } else if (this.data.type == 'indexed_db') { + info.indexedDb = true; + } else if (this.data.type == 'file_system') { + info.fileSystem = true; + } else if (this.data.type == 'quota') { + info.quota = this.data; + } + } + }, + + /** + * Create the cookie "bubbles" for this node, recursing into children + * if there are any. Append the cookie bubbles to @{code item}. + * @param {CookieListItem} item The cookie list item to create items in. + */ + createItems: function(item) { + if (this.children.length > 0) { + for (var i = 0; i < this.children.length; ++i) + this.children[i].createItems(item); + } else if (this.data && !this.data.hasChildren) { + var text = ''; + switch (this.data.type) { + case 'cookie': + case 'database': + text = this.data.name; + break; + case 'local_storage': + text = localStrings.getString('cookie_local_storage'); + break; + case 'app_cache': + text = localStrings.getString('cookie_app_cache'); + break; + case 'indexed_db': + text = localStrings.getString('cookie_indexed_db'); + break; + case 'file_system': + text = localStrings.getString('cookie_file_system'); + break; + } + if (!text) + return; + var div = item.ownerDocument.createElement('div'); + div.className = 'cookie-item'; + // Help out screen readers and such: this is a clickable thing. + div.setAttribute('role', 'button'); + div.textContent = text; + var index = item.appendItem(this, div); + div.onclick = function() { + if (item.selectedIndex == index) + item.selectedIndex = -1; + else + item.selectedIndex = index; + }; + } + }, + + /** + * Set the detail text to be displayed to that of this cookie tree node. + * Uses preallocated DOM elements for each cookie node type from @{code + * infoNodes}, and inserts the appropriate elements to @{code element}. + * @param {Element} element The DOM element to insert elements to. + * @param {Object.<string, {table: Element, info: Object.<string, + * Element>}>} infoNodes The map from cookie node types to maps from + * cookie attribute names to DOM elements to display cookie attribute + * values, created by @{code CookiesList.decorate}. + */ + setDetailText: function(element, infoNodes) { + var table; + if (this.data && !this.data.hasChildren) { + if (cookieInfo[this.data.type]) { + var info = cookieInfo[this.data.type]; + var nodes = infoNodes[this.data.type].info; + for (var i = 0; i < info.length; ++i) { + var name = info[i][0]; + if (name != 'id' && this.data[name]) + nodes[name].textContent = this.data[name]; + } + table = infoNodes[this.data.type].table; + } + } + while (element.childNodes.length > 1) + element.removeChild(element.firstChild); + if (table) + element.insertBefore(table, element.firstChild); + }, + + /** + * The parent of this cookie tree node. + * @type {?CookieTreeNode|CookieListItem} + */ + get parent(parent) { + // See below for an explanation of this special case. + if (typeof this.parent_ == 'number') + return this.list_.getListItemByIndex(this.parent_); + return this.parent_; + }, + set parent(parent) { + if (parent == this.parent) + return; + if (parent instanceof CookieListItem) { + // If the parent is to be a CookieListItem, then we keep the reference + // to it by its containing list and list index, rather than directly. + // This allows the list items to be garbage collected when they scroll + // out of view (except the expanded item, which we cache). This is + // transparent except in the setter and getter, where we handle it. + this.parent_ = parent.listIndex; + this.list_ = parent.list; + parent.addEventListener('listIndexChange', + this.parentIndexChanged_.bind(this)); + } else { + this.parent_ = parent; + } + if (this.data && this.data.id) { + if (parent) + parentLookup[this.data.id] = this; + else + delete parentLookup[this.data.id]; + } + if (this.data && this.data.hasChildren && + !this.children.length && !lookupRequests[this.data.id]) { + lookupRequests[this.data.id] = true; + chrome.send('loadCookie', [this.pathId]); + } + }, + + /** + * Called when the parent is a CookieListItem whose index has changed. + * See the code above that avoids keeping a direct reference to + * CookieListItem parents, to allow them to be garbage collected. + * @private + */ + parentIndexChanged_: function(event) { + if (typeof this.parent_ == 'number') { + this.parent_ = event.newValue; + // We set a timeout to update the origin, rather than doing it right + // away, because this callback may occur while the list items are + // being repopulated following a scroll event. Calling updateOrigin() + // immediately could trigger relayout that would reset the scroll + // position within the list, among other things. + window.setTimeout(this.updateOrigin.bind(this), 0); + } + }, + + /** + * The cookie tree path id. + * @type {string} + */ + get pathId() { + var parent = this.parent; + if (parent && parent instanceof CookieTreeNode) + return parent.pathId + ',' + this.data.id; + return this.data.id; + }, + }; + + /** + * Creates a new cookies list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {DeletableItemList} + */ + var CookiesList = cr.ui.define('list'); + + CookiesList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.classList.add('cookie-list'); + this.data_ = []; + this.dataModel = new ArrayDataModel(this.data_); + this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this)); + var sm = new ListSingleSelectionModel(); + sm.addEventListener('change', this.cookieSelectionChange_.bind(this)); + sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this)); + this.selectionModel = sm; + this.infoNodes = {}; + this.fixedHeight = false; + var doc = this.ownerDocument; + // Create a table for each type of site data (e.g. cookies, databases, + // etc.) and save it so that we can reuse it for all origins. + for (var type in cookieInfo) { + var table = doc.createElement('table'); + table.className = 'cookie-details-table'; + var tbody = doc.createElement('tbody'); + table.appendChild(tbody); + var info = {}; + for (var i = 0; i < cookieInfo[type].length; i++) { + var tr = doc.createElement('tr'); + var name = doc.createElement('td'); + var data = doc.createElement('td'); + var pair = cookieInfo[type][i]; + name.className = 'cookie-details-label'; + name.textContent = localStrings.getString(pair[1]); + data.className = 'cookie-details-value'; + data.textContent = ''; + tr.appendChild(name); + tr.appendChild(data); + tbody.appendChild(tr); + info[pair[0]] = data; + } + this.infoNodes[type] = {table: table, info: info}; + } + }, + + /** + * Handles key down events and looks for left and right arrows, then + * dispatches to the currently expanded item, if any. + * @param {Event} e The keydown event. + * @private + */ + handleKeyLeftRight_: function(e) { + var id = e.keyIdentifier; + if ((id == 'Left' || id == 'Right') && this.expandedItem) { + var cs = this.ownerDocument.defaultView.getComputedStyle(this); + var rtl = cs.direction == 'rtl'; + if ((!rtl && id == 'Left') || (rtl && id == 'Right')) + this.expandedItem.selectedIndex--; + else + this.expandedItem.selectedIndex++; + this.scrollIndexIntoView(this.expandedItem.listIndex); + // Prevent the page itself from scrolling. + e.preventDefault(); + } + }, + + /** + * Called on selection model selection changes. + * @param {Event} ce The selection change event. + * @private + */ + cookieSelectionChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.getListItemByIndex(change.index); + if (listItem) { + if (!change.selected) { + // We set a timeout here, rather than setting the item unexpanded + // immediately, so that if another item gets set expanded right + // away, it will be expanded before this item is unexpanded. It + // will notice that, and unexpand this item in sync with its own + // expansion. Later, this callback will end up having no effect. + window.setTimeout(function() { + if (!listItem.selected || !listItem.lead) + listItem.expanded = false; + }, 0); + } else if (listItem.lead) { + listItem.expanded = true; + } + } + }, this); + }, + + /** + * Called on selection model lead changes. + * @param {Event} pe The lead change event. + * @private + */ + cookieLeadChange_: function(pe) { + if (pe.oldValue != -1) { + var listItem = this.getListItemByIndex(pe.oldValue); + if (listItem) { + // See cookieSelectionChange_ above for why we use a timeout here. + window.setTimeout(function() { + if (!listItem.lead || !listItem.selected) + listItem.expanded = false; + }, 0); + } + } + if (pe.newValue != -1) { + var listItem = this.getListItemByIndex(pe.newValue); + if (listItem && listItem.selected) + listItem.expanded = true; + } + }, + + /** + * The currently expanded item. Used by CookieListItem above. + * @type {?CookieListItem} + */ + expandedItem: null, + + // from cr.ui.List + /** @inheritDoc */ + createItem: function(data) { + // We use the cached expanded item in order to allow it to maintain some + // state (like its fixed height, and which bubble is selected). + if (this.expandedItem && this.expandedItem.origin == data) + return this.expandedItem; + return new CookieListItem(data, this); + }, + + // from options.DeletableItemList + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var item = this.data_[index]; + if (item) { + var pathId = item.pathId; + if (pathId) + chrome.send('removeCookie', [pathId]); + } + }, + + /** + * Insert the given list of cookie tree nodes at the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {Array.<Object>} data The data objects for the nodes to add. + * @param {number} start The index at which to start inserting the nodes. + */ + insertAt: function(data, start) { + spliceTreeNodes(data, start, this.dataModel); + }, + + /** + * Remove a cookie tree node from the given index. + * Both CookiesList and CookieTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.data_.length) + this.dataModel.splice(index, 1); + }, + + /** + * Clears the list. + * Both CookiesList and CookieTreeNode implement this API. + * It is used by CookiesList.loadChildren(). + */ + clear: function() { + parentLookup = {}; + this.data_ = []; + this.dataModel = new ArrayDataModel(this.data_); + this.redraw(); + }, + + /** + * Add tree nodes by given parent. + * @param {Object} parent The parent node. + * @param {number} start The index at which to start inserting the nodes. + * @param {Array} nodesData Nodes data array. + * @private + */ + addByParent_: function(parent, start, nodesData) { + if (!parent) + return; + + parent.startBatchUpdates(); + parent.insertAt(nodesData, start); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Add tree nodes by parent id. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {number} start The index at which to start inserting the nodes. + * @param {Array} nodesData Nodes data array. + */ + addByParentId: function(parentId, start, nodesData) { + var parent = parentId ? parentLookup[parentId] : this; + this.addByParent_(parent, start, nodesData); + }, + + /** + * Removes tree nodes by parent id. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {number} start The index at which to start removing the nodes. + * @param {number} count Number of nodes to remove. + */ + removeByParentId: function(parentId, start, count) { + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + while (count-- > 0) + parent.remove(start); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Loads the immediate children of given parent node. + * This is used by cookies_view.js. + * @param {string} parentId Id of the parent node. + * @param {Array} children The immediate children of parent node. + */ + loadChildren: function(parentId, children) { + if (parentId) + delete lookupRequests[parentId]; + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + parent.clear(); + this.addByParent_(parent, 0, children); + parent.endBatchUpdates(); + }, + }; + + return { + CookiesList: CookiesList + }; +}); diff --git a/chrome/browser/resources/options2/cookies_view.css b/chrome/browser/resources/options2/cookies_view.css new file mode 100644 index 0000000..2aab6b2 --- /dev/null +++ b/chrome/browser/resources/options2/cookies_view.css @@ -0,0 +1,182 @@ +/* +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. +*/ + +/* styles for the cookies list elements in cookies_view.html */ +#remove-all-cookies-search-column { + bottom: 10px; + position: absolute; + right: 0; +} + +html[dir=rtl] #remove-all-cookies-search-column { + left: 0; + right: auto; +} + +#cookies-column-headers { + position: relative; + width: 100%; +} + +#cookies-column-headers h3 { + font-size: 105%; + font-weight: bold; + margin: 10px 0; +} + +/* notice the width and padding for these columns match up with those below */ +#cookies-site-column { + display: inline-block; + font-weight: bold; + width: 11em; +} + +#cookies-data-column { + -webkit-padding-start: 7px; + display: inline-block; + font-weight: bold; +} + +#cookies-list { + border: 1px solid #D9D9D9; + margin: 0; +} + + +/* enable animating the height of items */ +list.cookie-list .deletable-item { + -webkit-transition: height .15s ease-in-out; +} + +/* disable webkit-box display */ +list.cookie-list .deletable-item > :first-child { + display: block; +} + +/* force the X for deleting an origin to stay at the top */ +list.cookie-list > .deletable-item > .close-button { + position: absolute; + right: 2px; + top: 8px; +} + +html[dir=rtl] list.cookie-list > .deletable-item > .close-button { + left: 2px; + right: auto; +} + + +/* styles for the site (aka origin) and its summary */ +.cookie-site { + /* notice that the width, margin, and padding match up with those above */ + -webkit-margin-end: 2px; + -webkit-padding-start: 5px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + width: 11em; +} + +list.cookie-list > .deletable-item[selected] .cookie-site { + -webkit-user-select: text; +} + +.cookie-data { + display: inline-block; +} + +.cookie-size { + display: inline-block; + float: right; + margin-right: 3em; +} + +list.cookie-list > .deletable-item[selected] .cookie-data { + -webkit-user-select: text; +} + + +/* styles for the individual items (cookies, etc.) */ +.cookie-items { + /* notice that the margin and padding match up with those above */ + -webkit-margin-start: 11em; + -webkit-padding-start: 7px; + -webkit-transition: .15s ease-in-out; + height: 0; + opacity: 0; + /* make the cookie items wrap correctly */ + white-space: normal; +} + +.measure-items .cookie-items { + -webkit-transition: none; + height: auto; + visibility: hidden; +} + +.show-items .cookie-items { + opacity: 1; +} + +.cookie-items .cookie-item { + background: #E0E9F5; + border-radius: 5px; + border: 1px solid #8392AE; + display: inline-block; + font-size: 85%; + height: auto; + margin: 2px 4px 2px 0; + max-width: 100px; + min-width: 40px; + overflow: hidden; + padding: 0 3px; + text-align: center; + text-overflow: ellipsis; +} + +.cookie-items .cookie-item:hover { + background: #EEF3F9; + border-color: #647187; +} + +.cookie-items .cookie-item[selected] { + background: #F5F8F8; + border-color: #B2B2B2; +} + +.cookie-items .cookie-item[selected]:hover { + background: #F5F8F8; + border-color: #647187; +} + + +/* styles for the cookie details box */ +.cookie-details { + background: #F5F8F8; + border-radius: 5px; + border: 1px solid #B2B2B2; + margin-top: 2px; + padding: 5px; +} + +list.cookie-list > .deletable-item[selected] .cookie-details { + -webkit-user-select: text; +} + +.cookie-details-table { + table-layout: fixed; + width: 100%; +} + +.cookie-details-label { + vertical-align: top; + white-space: pre; + width: 10em; +} + +.cookie-details-value { + word-wrap: break-word; +} diff --git a/chrome/browser/resources/options2/cookies_view.html b/chrome/browser/resources/options2/cookies_view.html new file mode 100644 index 0000000..1a58487 --- /dev/null +++ b/chrome/browser/resources/options2/cookies_view.html @@ -0,0 +1,15 @@ +<div id="cookiesViewPage" class="page" hidden> + <h1 i18n-content="cookiesViewPage"></h1> + <div id="cookies-column-headers"> + <div id="cookies-site-column"><h3 i18n-content="cookie_domain"></h3></div> + <div id="cookies-data-column"><h3 i18n-content="cookie_local_data"></h3></div> + <div id="remove-all-cookies-search-column"> + <button id="remove-all-cookies-button" + i18n-content="remove_all_cookie"></button> + <input id="cookies-search-box" type="search" + i18n-values="placeholder:search_cookies" incremental + results="10" autosave="org.chromium.options.cookies.search"> + </div> + </div> + <list id="cookies-list"></list> +</div> diff --git a/chrome/browser/resources/options2/cookies_view.js b/chrome/browser/resources/options2/cookies_view.js new file mode 100644 index 0000000..dafa0cd --- /dev/null +++ b/chrome/browser/resources/options2/cookies_view.js @@ -0,0 +1,140 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // CookiesView class: + + /** + * Encapsulated handling of the cookies and other site data page. + * @constructor + */ + function CookiesView(model) { + OptionsPage.call(this, 'cookies', + templateData.cookiesViewPageTabTitle, + 'cookiesViewPage'); + } + + cr.addSingletonGetter(CookiesView); + + CookiesView.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The timer id of the timer set on search query change events. + * @type {number} + * @private + */ + queryDelayTimerId_: 0, + + /** + * The most recent search query, or null if the query is empty. + * @type {?string} + * @private + */ + lastQuery_ : null, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('cookies-search-box').addEventListener('search', + this.handleSearchQueryChange_.bind(this)); + + $('remove-all-cookies-button').onclick = function(e) { + chrome.send('removeAllCookies', []); + }; + + var cookiesList = $('cookies-list'); + options.CookiesList.decorate(cookiesList); + window.addEventListener('resize', this.handleResize_.bind(this)); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + }, + + /** + * Search cookie using text in |cookies-search-box|. + */ + searchCookie: function() { + this.queryDelayTimerId_ = 0; + var filter = $('cookies-search-box').value; + if (this.lastQuery_ != filter) { + this.lastQuery_ = filter; + chrome.send('updateCookieSearchResults', [filter]); + } + }, + + /** + * Handles search query changes. + * @param {!Event} e The event object. + * @private + */ + handleSearchQueryChange_: function(e) { + if (this.queryDelayTimerId_) + window.clearTimeout(this.queryDelayTimerId_); + + this.queryDelayTimerId_ = window.setTimeout( + this.searchCookie.bind(this), 500); + }, + + initialized_: false, + + /** + * Handler for OptionsPage's visible property change event. + * @param {Event} e Property change event. + * @private + */ + handleVisibleChange_: function(e) { + if (!this.visible) + return; + + // Resize the cookies list whenever the options page becomes visible. + this.handleResize_(null); + if (!this.initialized_) { + this.initialized_ = true; + this.searchCookie(); + } else { + $('cookies-list').redraw(); + } + + $('cookies-search-box').focus(); + }, + + /** + * Handler for when the window changes size. Resizes the cookies list to + * match the window height. + * @param {?Event} e Window resize event, or null if called directly. + * @private + */ + handleResize_: function(e) { + if (!this.visible) + return; + var cookiesList = $('cookies-list'); + // 25 pixels from the window bottom seems like a visually pleasing amount. + var height = window.innerHeight - cookiesList.offsetTop - 25; + cookiesList.style.height = height + 'px'; + }, + }; + + // CookiesViewHandler callbacks. + CookiesView.onTreeItemAdded = function(args) { + $('cookies-list').addByParentId(args[0], args[1], args[2]); + }; + + CookiesView.onTreeItemRemoved = function(args) { + $('cookies-list').removeByParentId(args[0], args[1], args[2]); + }; + + CookiesView.loadChildren = function(args) { + $('cookies-list').loadChildren(args[0], args[1]); + }; + + // Export + return { + CookiesView: CookiesView + }; + +}); diff --git a/chrome/browser/resources/options2/deletable_item_list.js b/chrome/browser/resources/options2/deletable_item_list.js new file mode 100644 index 0000000..80d4963 --- /dev/null +++ b/chrome/browser/resources/options2/deletable_item_list.js @@ -0,0 +1,185 @@ +// 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. + +cr.define('options', function() { + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + + /** + * Creates a deletable list item, which has a button that will trigger a call + * to deleteItemAtIndex(index) in the list. + */ + var DeletableItem = cr.ui.define('li'); + + DeletableItem.prototype = { + __proto__: ListItem.prototype, + + /** + * The element subclasses should populate with content. + * @type {HTMLElement} + * @private + */ + contentElement_: null, + + /** + * The close button element. + * @type {HTMLElement} + * @private + */ + closeButtonElement_: null, + + /** + * Whether or not this item can be deleted. + * @type {boolean} + * @private + */ + deletable_: true, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + this.classList.add('deletable-item'); + + this.contentElement_ = this.ownerDocument.createElement('div'); + this.appendChild(this.contentElement_); + + this.closeButtonElement_ = this.ownerDocument.createElement('button'); + this.closeButtonElement_.className = + 'raw-button close-button custom-appearance'; + this.closeButtonElement_.addEventListener('mousedown', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('mouseup', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('focus', + this.handleFocus_.bind(this)); + this.appendChild(this.closeButtonElement_); + }, + + /** + * Returns the element subclasses should add content to. + * @return {HTMLElement} The element subclasses should popuplate. + */ + get contentElement() { + return this.contentElement_; + }, + + /* Gets/sets the deletable property. An item that is not deletable doesn't + * show the delete button (although space is still reserved for it). + */ + get deletable() { + return this.deletable_; + }, + set deletable(value) { + this.deletable_ = value; + this.closeButtonElement_.disabled = !value; + }, + + /** + * Called when a focusable child element receives focus. Selects this item + * in the list selection model. + * @private + */ + handleFocus_: function() { + var list = this.parentNode; + var index = list.getIndexOfListItem(this); + list.selectionModel.selectedIndex = index; + list.selectionModel.anchorIndex = index; + }, + + /** + * Don't let the list have a crack at the event. We don't want clicking the + * close button to change the selection of the list. + * @param {Event} e The mouse down/up event object. + * @private + */ + handleMouseDownUpOnClose_: function(e) { + if (!e.target.disabled) + e.stopPropagation(); + }, + }; + + var DeletableItemList = cr.ui.define('list'); + + DeletableItemList.prototype = { + __proto__: List.prototype, + + /** @inheritDoc */ + decorate: function() { + List.prototype.decorate.call(this); + this.addEventListener('click', this.handleClick_); + this.addEventListener('keydown', this.handleKeyDown_); + }, + + /** + * Callback for onclick events. + * @param {Event} e The click event object. + * @private + */ + handleClick_: function(e) { + if (this.disabled) + return; + + var target = e.target; + if (target.classList.contains('close-button')) { + var listItem = this.getListItemAncestor(target); + var selected = this.selectionModel.selectedIndexes; + + // Check if the list item that contains the close button being clicked + // is not in the list of selected items. Only delete this item in that + // case. + var idx = this.getIndexOfListItem(listItem); + if (selected.indexOf(idx) == -1) { + this.deleteItemAtIndex(idx); + } else { + this.deleteSelectedItems_(); + } + } + }, + + /** + * Callback for keydown events. + * @param {Event} e The keydown event object. + * @private + */ + handleKeyDown_: function(e) { + // Map delete (and backspace on Mac) to item deletion (unless focus is + // in an input field, where it's intended for text editing). + if ((e.keyCode == 46 || (e.keyCode == 8 && cr.isMac)) && + e.target.tagName != 'INPUT') { + this.deleteSelectedItems_(); + // Prevent the browser from going back. + e.preventDefault(); + } + }, + + /** + * Deletes all the currently selected items that are deletable. + * @private + */ + deleteSelectedItems_: function() { + var selected = this.selectionModel.selectedIndexes; + // Reverse through the list of selected indexes to maintain the + // correct index values after deletion. + for (var j = selected.length - 1; j >= 0; j--) { + var index = selected[j]; + if (this.getListItemByIndex(index).deletable) + this.deleteItemAtIndex(index); + } + }, + + /** + * Called when an item should be deleted; subclasses are responsible for + * implementing. + * @param {number} index The index of the item that is being deleted. + */ + deleteItemAtIndex: function(index) { + }, + }; + + return { + DeletableItemList: DeletableItemList, + DeletableItem: DeletableItem, + }; +}); diff --git a/chrome/browser/resources/options2/extension_list.js b/chrome/browser/resources/options2/extension_list.js new file mode 100644 index 0000000..5cbad97 --- /dev/null +++ b/chrome/browser/resources/options2/extension_list.js @@ -0,0 +1,764 @@ +// 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. + +cr.define('options', function() { + 'use strict'; + + /** + * A lookup helper function to find the first node that has an id (starting + * at |node| and going up the parent chain). + * @param {Element} node The node to start looking at. + */ + function findIdNode(node) { + while (node && !node.id) { + node = node.parentNode; + } + return node; + } + + /** + * Creates a new list of extensions. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.div} + */ + var ExtensionsList = cr.ui.define('div'); + + var handlersInstalled = false; + + /** + * @type {Object.<string, boolean>} A map from extension id to a boolean + * indicating whether its details section is expanded. This persists + * between calls to decorate. + */ + var showingDetails = {}; + + /** + * @type {Object.<string, boolean>} A map from extension id to a boolean + * indicating whether the incognito warning is showing. This persists + * between calls to decorate. + */ + var showingWarning = {}; + + ExtensionsList.prototype = { + __proto__: HTMLDivElement.prototype, + + /** @inheritDoc */ + decorate: function() { + this.initControlsAndHandlers_(); + + this.deleteExistingExtensionNodes_(); + + this.showExtensionNodes_(); + }, + + /** + * Initializes the controls (toggle section and button) and installs + * handlers. + * @private + */ + initControlsAndHandlers_: function() { + // Make sure developer mode section is set correctly as per saved setting. + var toggleButton = $('toggle-dev-on'); + var toggleSection = $('dev'); + if (this.data_.developerMode) { + toggleSection.classList.add('dev-open'); + toggleSection.classList.remove('dev-closed'); + toggleButton.checked = true; + } else { + toggleSection.classList.remove('dev-open'); + toggleSection.classList.add('dev-closed'); + } + + // Instal global event handlers. + if (!handlersInstalled) { + var searchPage = SearchPage.getInstance(); + searchPage.addEventListener('searchChanged', + this.searchChangedHandler_.bind(this)); + + // Support full keyboard accessibility without making things ugly + // for users who click, by hiding some focus outlines when the user + // clicks anywhere, but showing them when the user presses any key. + this.ownerDocument.body.classList.add('hide-some-focus-outlines'); + this.ownerDocument.addEventListener('click', (function(e) { + this.ownerDocument.body.classList.add('hide-some-focus-outlines'); + return true; + }).bind(this), true); + this.ownerDocument.addEventListener('keydown', (function(e) { + this.ownerDocument.body.classList.remove('hide-some-focus-outlines'); + return true; + }).bind(this), true); + + handlersInstalled = true; + } + }, + + /** + * Deletes the existing Extension nodes from the page to make room for new + * ones. + * @private + */ + deleteExistingExtensionNodes_: function() { + while (this.hasChildNodes()){ + this.removeChild(this.firstChild); + } + }, + + /** + * Handles decorating the details section. + * @param {Element} details The div that the details should be attached to. + * @param {Object} extension The extension we are showing the details for. + * @private + */ + showExtensionNodes_: function() { + // Iterate over the extension data and add each item to the list. + for (var i = 0; i < this.data_.extensions.length; i++) { + var extension = this.data_.extensions[i]; + var id = extension.id; + + var wrapper = this.ownerDocument.createElement('div'); + + var expanded = showingDetails[id]; + var butterbar = showingWarning[id]; + + wrapper.classList.add(expanded ? 'extension-list-item-expanded' : + 'extension-list-item-collaped'); + if (!extension.enabled) + wrapper.classList.add('disabled'); + wrapper.id = id; + this.appendChild(wrapper); + + var vboxOuter = this.ownerDocument.createElement('div'); + vboxOuter.classList.add('vbox'); + vboxOuter.classList.add('extension-list-item'); + wrapper.appendChild(vboxOuter); + + var hbox = this.ownerDocument.createElement('div'); + hbox.classList.add('hbox'); + vboxOuter.appendChild(hbox); + + // Add a container div for the zippy, so we can extend the hit area. + var container = this.ownerDocument.createElement('div'); + // Clicking anywhere on the div expands/collapses the details. + container.classList.add('extension-zippy-container'); + container.title = expanded ? + localStrings.getString('extensionSettingsHideDetails') : + localStrings.getString('extensionSettingsShowDetails'); + container.tabIndex = 0; + container.setAttribute('role', 'button'); + container.setAttribute('aria-controls', extension.id + '_details'); + container.setAttribute('aria-expanded', expanded); + container.addEventListener('click', this.handleZippyClick_.bind(this)); + container.addEventListener('keydown', + this.handleZippyKeyDown_.bind(this)); + hbox.appendChild(container); + + // On the far left we have the zippy icon. + var div = this.ownerDocument.createElement('div'); + div.id = id + '_zippy'; + div.classList.add('extension-zippy-default'); + div.classList.add(expanded ? 'extension-zippy-expanded' : + 'extension-zippy-collapsed'); + container.appendChild(div); + + // Next to it, we have the extension icon. + var icon = this.ownerDocument.createElement('img'); + icon.classList.add('extension-icon'); + icon.src = extension.icon; + hbox.appendChild(icon); + + // Start a vertical box for showing the details. + var vbox = this.ownerDocument.createElement('div'); + vbox.classList.add('vbox'); + vbox.classList.add('stretch'); + vbox.classList.add('details-view'); + hbox.appendChild(vbox); + + div = this.ownerDocument.createElement('div'); + vbox.appendChild(div); + + // Title comes next. + var title = this.ownerDocument.createElement('span'); + title.classList.add('extension-title'); + title.textContent = extension.name; + vbox.appendChild(title); + + // Followed by version. + var version = this.ownerDocument.createElement('span'); + version.classList.add('extension-version'); + version.textContent = extension.version; + vbox.appendChild(version); + + // And the additional info label (unpacked/crashed). + if (extension.terminated || extension.isUnpacked) { + var version = this.ownerDocument.createElement('span'); + version.classList.add('extension-version'); + version.textContent = extension.terminated ? + localStrings.getString('extensionSettingsCrashMessage') : + localStrings.getString('extensionSettingsInDevelopment'); + vbox.appendChild(version); + } + + div = this.ownerDocument.createElement('div'); + vbox.appendChild(div); + + // And below that we have description (if provided). + if (extension.description.length > 0) { + var description = this.ownerDocument.createElement('span'); + description.classList.add('extension-description'); + description.textContent = extension.description; + vbox.appendChild(description); + } + + // Immediately following the description, we have the + // Options link (optional). + if (extension.options_url) { + var link = this.ownerDocument.createElement('a'); + link.classList.add('extension-links-trailing'); + link.textContent = localStrings.getString('extensionSettingsOptions'); + link.href = '#'; + link.addEventListener('click', this.handleOptions_.bind(this)); + vbox.appendChild(link); + } + + // Then the optional Visit Website link. + if (extension.homepageUrl) { + var link = this.ownerDocument.createElement('a'); + link.classList.add('extension-links-trailing'); + link.textContent = + localStrings.getString('extensionSettingsVisitWebsite'); + link.href = extension.homepageUrl; + vbox.appendChild(link); + } + + if (extension.warnings.length > 0) { + var warningsDiv = this.ownerDocument.createElement('div'); + warningsDiv.classList.add('extension-warnings'); + + var warningsHeader = this.ownerDocument.createElement('span'); + warningsHeader.classList.add('extension-warnings-title'); + warningsHeader.textContent = + localStrings.getString('extensionSettingsWarningsTitle'); + warningsDiv.appendChild(warningsHeader); + + var warningList = this.ownerDocument.createElement('ul'); + for (var j = 0; j < extension.warnings.length; ++j) { + var warningEntry = this.ownerDocument.createElement('li'); + warningEntry.textContent = extension.warnings[j]; + warningList.appendChild(warningEntry); + } + warningsDiv.appendChild(warningList); + + vbox.appendChild(warningsDiv); + } + + // And now the details section that is normally hidden. + var details = this.ownerDocument.createElement('div'); + details.classList.add('vbox'); + vbox.appendChild(details); + + this.decorateDetailsSection_(details, extension, expanded, butterbar); + + // And on the right of the details we have the Enable/Enabled checkbox. + div = this.ownerDocument.createElement('div'); + hbox.appendChild(div); + + var section = this.ownerDocument.createElement('section'); + section.classList.add('extension-enabling'); + div.appendChild(section); + + if (!extension.terminated) { + // The Enable checkbox. + var input = this.ownerDocument.createElement('input'); + input.addEventListener('click', this.handleEnable_.bind(this)); + input.type = 'checkbox'; + input.name = 'toggle-' + id; + input.disabled = !extension.mayDisable; + if (extension.enabled) + input.checked = true; + input.id = 'toggle-' + id; + section.appendChild(input); + var label = this.ownerDocument.createElement('label'); + label.classList.add('extension-enabling-label'); + if (extension.enabled) + label.classList.add('extension-enabling-label-bold'); + label.htmlFor = 'toggle-' + id; + label.id = 'toggle-' + id + '-label'; + if (extension.enabled) { + // Enabled (with a d). + label.textContent = + localStrings.getString('extensionSettingsEnabled'); + } else { + // Enable (no d). + label.textContent = + localStrings.getString('extensionSettingsEnable'); + } + section.appendChild(label); + } else { + // Extension has been terminated, show a Reload link. + var link = this.ownerDocument.createElement('a'); + link.classList.add('extension-links-trailing'); + link.id = extension.id; + link.textContent = + localStrings.getString('extensionSettingsReload'); + link.href = '#'; + link.addEventListener('click', this.handleReload_.bind(this)); + section.appendChild(link); + } + + // And, on the far right we have the uninstall button. + var button = this.ownerDocument.createElement('button'); + button.classList.add('extension-delete'); + button.id = id; + if (!extension.mayDisable) + button.disabled = true; + button.textContent = localStrings.getString('extensionSettingsRemove'); + button.addEventListener('click', this.handleUninstall_.bind(this)); + hbox.appendChild(button); + } + + // Do one pass to find what the size of the checkboxes should be. + var minCheckboxWidth = Infinity; + var maxCheckboxWidth = 0; + for (var i = 0; i < this.data_.extensions.length; ++i) { + var label = $('toggle-' + this.data_.extensions[i].id + '-label'); + if (label.offsetWidth > maxCheckboxWidth) + maxCheckboxWidth = label.offsetWidth; + if (label.offsetWidth < minCheckboxWidth) + minCheckboxWidth = label.offsetWidth; + } + + // Do another pass, making sure checkboxes line up. + var difference = maxCheckboxWidth - minCheckboxWidth; + for (var i = 0; i < this.data_.extensions.length; ++i) { + var label = $('toggle-' + this.data_.extensions[i].id + '-label'); + if (label.offsetWidth < maxCheckboxWidth) + label.style.WebkitMarginEnd = difference.toString() + 'px'; + } + }, + + /** + * Handles decorating the details section. + * @param {Element} details The div that the details should be attached to. + * @param {Object} extension The extension we are shoting the details for. + * @param {boolean} expanded Whether to show the details expanded or not. + * @param {boolean} showButterbar Whether to show the incognito warning or + * not. + * @private + */ + decorateDetailsSection_: function(details, extension, + expanded, showButterbar) { + // This container div is needed because vbox display + // overrides display:hidden. + var detailsContents = this.ownerDocument.createElement('div'); + detailsContents.classList.add(expanded ? 'extension-details-visible' : + 'extension-details-hidden'); + detailsContents.id = extension.id + '_details'; + details.appendChild(detailsContents); + + var div = this.ownerDocument.createElement('div'); + div.classList.add('informative-text'); + detailsContents.appendChild(div); + + // Keep track of how many items we'll show in the details section. + var itemsShown = 0; + + if (this.data_.developerMode) { + // First we have the id. + var content = this.ownerDocument.createElement('div'); + content.textContent = + localStrings.getString('extensionSettingsExtensionId') + + ' ' + extension.id; + div.appendChild(content); + itemsShown++; + + // Then, the path, if provided by unpacked extension. + if (extension.isUnpacked) { + content = this.ownerDocument.createElement('div'); + content.textContent = + localStrings.getString('extensionSettingsExtensionPath') + + ' ' + extension.path; + div.appendChild(content); + itemsShown++; + } + + // Then, the 'managed, cannot uninstall/disable' message. + if (!extension.mayDisable) { + content = this.ownerDocument.createElement('div'); + content.textContent = + localStrings.getString('extensionSettingsPolicyControlled'); + div.appendChild(content); + itemsShown++; + } + + // Then active views: + if (extension.views.length > 0) { + var table = this.ownerDocument.createElement('table'); + table.classList.add('extension-inspect-table'); + div.appendChild(table); + var tr = this.ownerDocument.createElement('tr'); + table.appendChild(tr); + var td = this.ownerDocument.createElement('td'); + td.classList.add('extension-inspect-left-column'); + tr.appendChild(td); + var span = this.ownerDocument.createElement('span'); + td.appendChild(span); + span.textContent = + localStrings.getString('extensionSettingsInspectViews'); + + td = this.ownerDocument.createElement('td'); + for (var i = 0; i < extension.views.length; ++i) { + // Then active views: + content = this.ownerDocument.createElement('div'); + var link = this.ownerDocument.createElement('a'); + link.classList.add('extension-links-view'); + link.textContent = extension.views[i].path; + link.id = extension.id; + link.href = '#'; + link.addEventListener('click', this.sendInspectMessage_.bind(this)); + content.appendChild(link); + + if (extension.views[i].incognito) { + var incognito = this.ownerDocument.createElement('span'); + incognito.classList.add('extension-links-view'); + incognito.textContent = + localStrings.getString('viewIncognito'); + content.appendChild(incognito); + } + + td.appendChild(content); + tr.appendChild(td); + + itemsShown++; + } + } + } + + var content = this.ownerDocument.createElement('div'); + detailsContents.appendChild(content); + + // Then Reload: + if (extension.enabled && extension.allow_reload) { + this.addLinkTo_(content, + localStrings.getString('extensionSettingsReload'), + extension.id, + this.handleReload_.bind(this)); + itemsShown++; + } + + // Then Show (Browser Action) Button: + if (extension.enabled && extension.enable_show_button) { + this.addLinkTo_(content, + localStrings.getString('extensionSettingsShowButton'), + extension.id, + this.handleShowButton_.bind(this)); + itemsShown++; + } + + if (extension.enabled) { + // The 'allow in incognito' checkbox. + var label = this.ownerDocument.createElement('label'); + label.classList.add('extension-checkbox-label'); + content.appendChild(label); + var input = this.ownerDocument.createElement('input'); + input.addEventListener('click', + this.handleToggleEnableIncognito_.bind(this)); + input.id = extension.id; + input.type = 'checkbox'; + if (extension.enabledIncognito) + input.checked = true; + label.appendChild(input); + var span = this.ownerDocument.createElement('span'); + span.classList.add('extension-checkbox-span'); + span.textContent = + localStrings.getString('extensionSettingsEnableIncognito'); + label.appendChild(span); + itemsShown++; + } + + if (extension.enabled && extension.wantsFileAccess) { + // The 'allow access to file URLs' checkbox. + label = this.ownerDocument.createElement('label'); + label.classList.add('extension-checkbox-label'); + content.appendChild(label); + var input = this.ownerDocument.createElement('input'); + input.addEventListener('click', + this.handleToggleAllowFileUrls_.bind(this)); + input.id = extension.id; + input.type = 'checkbox'; + if (extension.allowFileAccess) + input.checked = true; + label.appendChild(input); + var span = this.ownerDocument.createElement('span'); + span.classList.add('extension-checkbox-span'); + span.textContent = + localStrings.getString('extensionSettingsAllowFileAccess'); + label.appendChild(span); + itemsShown++; + } + + if (extension.enabled && !extension.is_hosted_app) { + // And add a hidden warning message for allowInIncognito. + content = this.ownerDocument.createElement('div'); + content.id = extension.id + '_incognitoWarning'; + content.classList.add('butter-bar'); + content.hidden = !showButterbar; + detailsContents.appendChild(content); + + var span = this.ownerDocument.createElement('span'); + span.innerHTML = + localStrings.getString('extensionSettingsIncognitoWarning'); + content.appendChild(span); + itemsShown++; + } + + var zippy = extension.id + '_zippy'; + $(zippy).hidden = !itemsShown; + + // If this isn't expanded now, make sure the newly-added controls + // are not part of the tab order. + if (!expanded) { + var detailsControls = details.querySelectorAll('a, input'); + for (var i = 0; i < detailsControls.length; i++) + detailsControls[i].tabIndex = -1; + } + }, + + /** + * A helper function to add contextual actions for extensions (action links) + * to the page. + */ + addLinkTo_: function(parent, linkText, id, handler) { + var link = this.ownerDocument.createElement('a'); + link.className = 'extension-links-trailing'; + link.textContent = linkText; + link.id = id; + link.href = '#'; + link.addEventListener('click', handler); + parent.appendChild(link); + }, + + /** + * A lookup helper function to find an extension based on an id. + * @param {string} id The |id| of the extension to look up. + * @private + */ + getExtensionWithId_: function(id) { + for (var i = 0; i < this.data_.extensions.length; ++i) { + if (this.data_.extensions[i].id == id) + return this.data_.extensions[i]; + } + return null; + }, + + /** + * Handles a key down on the zippy icon. + * @param {Event} e Key event. + * @private + */ + handleZippyKeyDown_: function(e) { + if (e.keyCode == 13 || e.keyCode == 32) // Enter or Space. + this.handleZippyClick_(e); + }, + + /** + * Handles the mouseclick on the zippy icon (that expands and collapses the + * details section). + * @param {Event} e Mouse event. + * @private + */ + handleZippyClick_: function(e) { + var node = findIdNode(e.target.parentNode); + var iter = this.firstChild; + while (iter) { + var zippy = $(iter.id + '_zippy'); + var details = $(iter.id + '_details'); + var container = zippy.parentElement; + if (iter.id == node.id) { + // Toggle visibility. + if (iter.classList.contains('extension-list-item-expanded')) { + // Hide yo kids! Hide yo wife! + showingDetails[iter.id] = false; + zippy.classList.remove('extension-zippy-expanded'); + zippy.classList.add('extension-zippy-collapsed'); + details.classList.remove('extension-details-visible'); + details.classList.add('extension-details-hidden'); + iter.classList.remove('extension-list-item-expanded'); + iter.classList.add('extension-list-item-collaped'); + container.setAttribute('aria-expanded', 'false'); + container.title = + localStrings.getString('extensionSettingsShowDetails'); + var detailsControls = details.querySelectorAll('a, input'); + for (var i = 0; i < detailsControls.length; i++) + detailsControls[i].tabIndex = -1; + + // Hide yo incognito warning. + var butterBar = + this.querySelector('#' + iter.id + '_incognitoWarning'); + if (butterBar !== null) { + butterBar.hidden = true; + showingWarning[iter.id] = false; + } + } else { + // Show the contents. + showingDetails[iter.id] = true; + zippy.classList.remove('extension-zippy-collapsed'); + zippy.classList.add('extension-zippy-expanded'); + details.classList.remove('extension-details-hidden'); + details.classList.add('extension-details-visible'); + iter.classList.remove('extension-list-item-collaped'); + iter.classList.add('extension-list-item-expanded'); + container.setAttribute('aria-expanded', 'true'); + container.title = + localStrings.getString('extensionSettingsHideDetails'); + var detailsControls = details.querySelectorAll('a, input'); + for (var i = 0; i < detailsControls.length; i++) + detailsControls[i].tabIndex = 0; + } + } + iter = iter.nextSibling; + } + }, + + /** + * Handles the 'searchChanged' event. This is used to limit the number of + * items to show in the list, when the user is searching for items with the + * search box. Otherwise, if one match is found, the whole list of + * extensions would be shown when we only want the matching items to be + * found. + * @param {Event} e Change event. + * @private + */ + searchChangedHandler_: function(e) { + var searchString = e.searchText; + var child = this.firstChild; + while (child) { + var extension = this.getExtensionWithId_(child.id); + if (searchString.length == 0) { + // Show all. + child.classList.remove('search-suppress'); + } else { + // If the search string does not appear within the text of the + // extension, then hide it. + if ((extension.name.toLowerCase().indexOf(searchString) < 0) && + (extension.version.toLowerCase().indexOf(searchString) < 0) && + (extension.description.toLowerCase().indexOf(searchString) < 0)) { + // Hide yo extension! + child.classList.add('search-suppress'); + } else { + // Show yourself! + child.classList.remove('search-suppress'); + } + } + child = child.nextSibling; + } + }, + + /** + * Handles the Reload Extension functionality. + * @param {Event} e Change event. + * @private + */ + handleReload_: function(e) { + var node = findIdNode(e.target); + chrome.send('extensionSettingsReload', [node.id]); + }, + + /** + * Handles the Show (Browser Action) Button functionality. + * @param {Event} e Change event. + * @private + */ + handleShowButton_: function(e) { + var node = findIdNode(e.target); + chrome.send('extensionSettingsShowButton', [node.id]); + }, + + /** + * Handles the Enable/Disable Extension functionality. + * @param {Event} e Change event. + * @private + */ + handleEnable_: function(e) { + var node = findIdNode(e.target.parentNode); + var extension = this.getExtensionWithId_(node.id); + chrome.send('extensionSettingsEnable', + [node.id, extension.enabled ? 'false' : 'true']); + chrome.send('extensionSettingsRequestExtensionsData'); + }, + + /** + * Handles the Uninstall Extension functionality. + * @param {Event} e Change event. + * @private + */ + handleUninstall_: function(e) { + var node = findIdNode(e.target.parentNode); + chrome.send('extensionSettingsUninstall', [node.id]); + chrome.send('extensionSettingsRequestExtensionsData'); + }, + + /** + * Handles the View Options link. + * @param {Event} e Change event. + * @private + */ + handleOptions_: function(e) { + var node = findIdNode(e.target.parentNode); + var extension = this.getExtensionWithId_(node.id); + chrome.send('extensionSettingsOptions', [extension.id]); + e.preventDefault(); + }, + + /** + * Handles the Enable Extension In Incognito functionality. + * @param {Event} e Change event. + * @private + */ + handleToggleEnableIncognito_: function(e) { + var node = findIdNode(e.target); + var butterBar = document.getElementById(node.id + '_incognitoWarning'); + butterBar.hidden = !e.target.checked; + showingWarning[node.id] = e.target.checked; + chrome.send('extensionSettingsEnableIncognito', + [node.id, String(e.target.checked)]); + }, + + /** + * Handles the Allow On File URLs functionality. + * @param {Event} e Change event. + * @private + */ + handleToggleAllowFileUrls_: function(e) { + var node = findIdNode(e.target); + chrome.send('extensionSettingsAllowFileAccess', + [node.id, String(e.target.checked)]); + }, + + /** + * Tell the C++ ExtensionDOMHandler to inspect the page detailed in + * |viewData|. + * @param {Event} e Change event. + * @private + */ + sendInspectMessage_: function(e) { + var extension = this.getExtensionWithId_(e.srcElement.id); + for (var i = 0; i < extension.views.length; ++i) { + if (extension.views[i].path == e.srcElement.innerText) { + // TODO(aa): This is ghetto, but WebUIBindings doesn't support sending + // anything other than arrays of strings, and this is all going to get + // replaced with V8 extensions soon anyway. + chrome.send('extensionSettingsInspect', [ + String(extension.views[i].renderProcessId), + String(extension.views[i].renderViewId) + ]); + } + } + }, + }; + + return { + ExtensionsList: ExtensionsList + }; +}); diff --git a/chrome/browser/resources/options2/extension_settings.css b/chrome/browser/resources/options2/extension_settings.css new file mode 100644 index 0000000..35ca397 --- /dev/null +++ b/chrome/browser/resources/options2/extension_settings.css @@ -0,0 +1,274 @@ +/* +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. +*/ + +.details-view { + -webkit-padding-end: 10px; +} + +.extension-list-item { + padding-bottom: 7px; + padding-top: 7px; + width: 100%; + -webkit-user-select: auto; +} + +/* Get rid of display: table, which causes width issues. */ +#extension-settings .displaytable { + display: block; +} +/* Get rid of display: table row, which causes width issues. */ +#extension-settings .displaytable > section { + display: block; +} +/* Get rid of display: table cell, which causes width issues. */ +#extension-settings .displaytable > section > * { + display: block; +} + +.extension-settings-content { + border-bottom : 0px solid #eee; + margin-top: 3px; +} + +#extension-settings-list { + min-height: 0; + overflow-y: hidden; +} + +/* Get rid of the light-blue background on list item hover. */ +#extension-settings-list:not([disabled]) > :hover { + background-color: white; + border-color: #CDCDCD; +} + +.butter-bar { + background: #FFF299; + padding: 2px 5px; + border-radius: 3px; + white-space: normal; +} + +.search-suppress { + display: none; + height: 0; +} + +.extension-list-item-collaped { + height: auto; + margin-bottom: 16px; + -webkit-transition: padding 300ms, overflow 300ms, opacity 700ms; +} + +.extension-list-item-expanded { + height: auto; + margin-bottom: 16px; + overflow: visible; + -webkit-transition: padding 300ms, overflow 300ms, opacity 700ms; +} + +.extension-settings { + overflow-x: hidden; +} + +.extension-icon { + height: 48px; + vertical-align: text-top; + width: 48px; + -webkit-padding-start: 15px; + -webkit-padding-end: 15px; + -webkit-user-select: none; +} + +.extension-title { + font-size: 16px; + font-weight: 500; + -webkit-padding-end: 20px; +} + +.extension-version { + font-size: 13px; + font-weight: 400; + -webkit-padding-end: 7px; +} + +.extension-description { + font-size: 13px; + white-space: normal; + -webkit-padding-end: 5px; +} + +.extension-checkbox-span { + -webkit-margin-start: 7px; +} + +.extension-checkbox-label { + -webkit-margin-end: 10px; +} + +.extension-delete { + -webkit-margin-start: 5px; +} + +.extension-details-hidden { + opacity: 0; + max-height: 0; + -webkit-transition: max-height 400ms, opacity 200ms; +} + +.extension-details-visible { + opacity: 1; + max-height: 1000px; + -webkit-transition: max-height 200ms, opacity 300ms; +} + +.extension-links-view { + -webkit-padding-start: 15px; +} + +.extension-links-trailing { + -webkit-padding-end: 7px; +} + +.extension-zippy-container { + cursor: pointer; + width: 20px; + -webkit-user-select: none; +} + +.extension-warnings-title { + color: red; +} + +.extension-warnings { + margin-top: 6px; +} + +.extension-warnings ul { + margin: 0; +} + +.extension-warnings > * { + white-space: normal; +} + +.informative-text { + color: gray; +} + +.extension-zippy-default { + background-image: url('zippy.png'); + background-repeat: no-repeat; + background-position: center top; + position: absolute; + left: 12px; + top: 25px; + width: 6px; + height: 16px; + opacity: .25; +} + +.extension-zippy-collapsed { + -webkit-transition: -webkit-transform 100ms; + -webkit-transform: rotate(0deg); +} + +.extension-zippy-collapsed:hover { + opacity: .5; + -webkit-transform: rotate(5deg); + -webkit-transition: -webkit-transform 100ms, opacity 100ms; +} + +.extension-zippy-expanded { + -webkit-transition: -webkit-transform 100ms; + -webkit-transform: rotate(90deg); +} + +.extension-zippy-expanded:hover { + -webkit-transition: -webkit-transform 100ms; + -webkit-transform: rotate(85deg); +} + +.extension-enabling { + position: relative; + top: 3px; +} + +.extension-enabling-label { + color: black; + -webkit-padding-start: 3px; + -webkit-padding-end: 9px; +} + +.extension-enabling-label-bold { + font-weight: bold; +} + +.extension-inspect-table { + padding: 0; + border-spacing: 0; +} + +.extension-inspect-left-column { + vertical-align: text-top; +} + +/* Dev */ + +.dev-open { + border-bottom: 1px solid rgb(205, 205, 205); + height: 32px; + padding-bottom: 7px; + padding-top: 13px; + -webkit-padding-start: 4px; + -webkit-padding-end: 3px; + -webkit-transition: padding 300ms, height 300ms, opacity 700ms; +} +.dev-closed { + height: 0; + opacity: 0; + padding-top: 9px; + -webkit-padding-start: 4px; + -webkit-padding-end: 3px; + -webkit-transition: padding 300ms, height 700ms, opacity 200ms; +} + +.dev-button-visible { + display: inherit; + opacity: 1; + -webkit-transition: opacity 200ms; +} + +.dev-button-hidden { + display: none; +} + +#suggest-gallery { + -webkit-padding-start: 10px; +} + +#dev-toggle { + display: block; + text-align: end; + margin-top: -28px; + -webkit-margin-end: 8px; +} + +#get-more-extensions-container { + display: -webkit-box; +} + +#get-more-extensions { + padding-top: 5px; + font-size: 15px; + -webkit-padding-start: 10px; +} + +/* Support full keyboard accessibility without making things ugly + for users who click, by hiding some focus outlines when the user + clicks anywhere, but showing them when the user presses any key. */ +body.hide-some-focus-outlines .extension-zippy-container { + outline: none; +} diff --git a/chrome/browser/resources/options2/extension_settings.html b/chrome/browser/resources/options2/extension_settings.html new file mode 100644 index 0000000..2858316 --- /dev/null +++ b/chrome/browser/resources/options2/extension_settings.html @@ -0,0 +1,40 @@ +<div id="extension-settings" class="page" hidden> + <h1 i18n-content="extensionSettings"></h1> + <div id="dev-toggle"> + <input id="toggle-dev-on" type="checkbox" value="off"></input> + <label for="toggle-dev-on" i18n-content="extensionSettingsDeveloperMode" /> + </div> + <div class="displaytable"> + <div id="dev" class="dev-closed"> + <table id="dev-table" width="100%"> + <tr> + <td> + <button id="load-unpacked" + i18n-content="extensionSettingsLoadUnpackedButton"></button> + <button id="pack-extension" + i18n-content="extensionSettingsPackButton"></button> + </td> + <td align="right"> + <button id="update-extensions-now" + i18n-content="extensionSettingsUpdateButton"></button> + </td> + </tr> + </table> + </div> + <section class="extension-settings-content"> + <div class="extension-settings"> + <list id="extension-settings-list"></list> + </div> + </section> + </div> + <section> + <div><strong id="no-extensions" + i18n-content="extensionSettingsNoExtensions" + hidden="true"></strong></div> + <div id="suggest-gallery" hidden="true"></div> + <div id="get-more-extensions-container" hidden="true"> + <img src="chrome://theme/IDR_WEBSTORE_ICON_32"> + <div id="get-more-extensions"></div> + </div> + </section> +</div> diff --git a/chrome/browser/resources/options2/extension_settings.js b/chrome/browser/resources/options2/extension_settings.js new file mode 100644 index 0000000..e88ba85 --- /dev/null +++ b/chrome/browser/resources/options2/extension_settings.js @@ -0,0 +1,184 @@ +// 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. + +// Used for observing function of the backend datasource for this page by +// tests. +var webui_responded_ = false; + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ExtensionsList = options.ExtensionsList; + + /** + * ExtensionSettings class + * Encapsulated handling of the 'Manage Extensions' page. + * @class + */ + function ExtensionSettings() { + OptionsPage.call(this, 'extensions', + templateData.extensionSettingsTabTitle, + 'extension-settings'); + } + + cr.addSingletonGetter(ExtensionSettings); + + ExtensionSettings.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + // This will request the data to show on the page and will get a response + // back in returnExtensionsData. + chrome.send('extensionSettingsRequestExtensionsData'); + + // Set up the developer mode button. + var toggleDevMode = $('toggle-dev-on'); + toggleDevMode.addEventListener('click', + this.handleToggleDevMode_.bind(this)); + + // Setup the gallery related links and text. + $('suggest-gallery').innerHTML = + localStrings.getString('extensionSettingsSuggestGallery'); + $('get-more-extensions').innerHTML = + localStrings.getString('extensionSettingsGetMoreExtensions'); + + // Set up the three dev mode buttons (load unpacked, pack and update). + $('load-unpacked').addEventListener('click', + this.handleLoadUnpackedExtension_.bind(this)); + $('pack-extension').addEventListener('click', + this.handlePackExtension_.bind(this)); + $('update-extensions-now').addEventListener('click', + this.handleUpdateExtensionNow_.bind(this)); + }, + + /** + * Utility function which asks the C++ to show a platform-specific file + * select dialog, and fire |callback| with the |filePath| that resulted. + * |selectType| can be either 'file' or 'folder'. |operation| can be 'load', + * 'packRoot', or 'pem' which are signals to the C++ to do some + * operation-specific configuration. + * @private + */ + showFileDialog_: function(selectType, operation, callback) { + handleFilePathSelected = function(filePath) { + callback(filePath); + handleFilePathSelected = function() {}; + }; + + chrome.send('extensionSettingsSelectFilePath', [selectType, operation]); + }, + + /** + * Handles the Load Unpacked Extension button. + * @param {Event} e Change event. + * @private + */ + handleLoadUnpackedExtension_: function(e) { + this.showFileDialog_('folder', 'load', function(filePath) { + chrome.send('extensionSettingsLoad', [String(filePath)]); + }); + + chrome.send('coreOptionsUserMetricsAction', + ['Options_LoadUnpackedExtension']); + }, + + /** + * Handles the Pack Extension button. + * @param {Event} e Change event. + * @private + */ + handlePackExtension_: function(e) { + OptionsPage.navigateToPage('packExtensionOverlay'); + chrome.send('coreOptionsUserMetricsAction', ['Options_PackExtension']); + }, + + /** + * Handles the Update Extension Now button. + * @param {Event} e Change event. + * @private + */ + handleUpdateExtensionNow_: function(e) { + chrome.send('extensionSettingsAutoupdate', []); + }, + + /** + * Handles the Toggle Dev Mode button. + * @param {Event} e Change event. + * @private + */ + handleToggleDevMode_: function(e) { + var dev = $('dev'); + if (!dev.classList.contains('dev-open')) { + // Make the Dev section visible. + dev.classList.add('dev-open'); + dev.classList.remove('dev-closed'); + + $('load-unpacked').classList.add('dev-button-visible'); + $('load-unpacked').classList.remove('dev-button-hidden'); + $('pack-extension').classList.add('dev-button-visible'); + $('pack-extension').classList.remove('dev-button-hidden'); + $('update-extensions-now').classList.add('dev-button-visible'); + $('update-extensions-now').classList.remove('dev-button-hidden'); + } else { + // Hide the Dev section. + dev.classList.add('dev-closed'); + dev.classList.remove('dev-open'); + + $('load-unpacked').classList.add('dev-button-hidden'); + $('load-unpacked').classList.remove('dev-button-visible'); + $('pack-extension').classList.add('dev-button-hidden'); + $('pack-extension').classList.remove('dev-button-visible'); + $('update-extensions-now').classList.add('dev-button-hidden'); + $('update-extensions-now').classList.remove('dev-button-visible'); + } + + chrome.send('extensionSettingsToggleDeveloperMode', []); + }, + }; + + /** + * Called by the dom_ui_ to re-populate the page with data representing + * the current state of installed extensions. + */ + ExtensionSettings.returnExtensionsData = function(extensionsData) { + webui_responded_ = true; + + $('no-extensions').hidden = true; + $('suggest-gallery').hidden = true; + $('get-more-extensions-container').hidden = true; + + if (extensionsData.extensions.length > 0) { + // Enforce order specified in the data or (if equal) then sort by + // extension name (case-insensitive). + extensionsData.extensions.sort(function(a, b) { + if (a.order == b.order) { + a = a.name.toLowerCase(); + b = b.name.toLowerCase(); + return a < b ? -1 : (a > b ? 1 : 0); + } else { + return a.order < b.order ? -1 : 1; + } + }); + + $('get-more-extensions-container').hidden = false; + } else { + $('no-extensions').hidden = false; + $('suggest-gallery').hidden = false; + } + + ExtensionsList.prototype.data_ = extensionsData; + + var extensionList = $('extension-settings-list'); + ExtensionsList.decorate(extensionList); + } + + // Export + return { + ExtensionSettings: ExtensionSettings + }; +}); diff --git a/chrome/browser/resources/options2/font_settings.css b/chrome/browser/resources/options2/font_settings.css new file mode 100644 index 0000000..6d90dbb --- /dev/null +++ b/chrome/browser/resources/options2/font_settings.css @@ -0,0 +1,41 @@ +#font-settings > section { + overflow: hidden; +} + +#font-settings input[type="range"] { + width: 100%; +} + +#minimum-font-sample { + height: 35px; + overflow: hidden; + width: 270px; +} + +.font-input-div { + -webkit-margin-end: 3em; + width: 12em; +} + +.font-input-div > div > select { + margin-bottom: 10px; +} + +.font-input { + width: 100%; +} + +.font-sample-div { + height: 70px; + overflow: hidden; + width: 270px; + direction: ltr; +} + +.font-settings-huge { + float: right; +} + +html[dir=rtl] .font-settings-huge { + float: left; +} diff --git a/chrome/browser/resources/options2/font_settings.html b/chrome/browser/resources/options2/font_settings.html new file mode 100644 index 0000000..c025230 --- /dev/null +++ b/chrome/browser/resources/options2/font_settings.html @@ -0,0 +1,82 @@ +<div id="font-settings" class="page" hidden> + <h1 i18n-content="fontSettingsPage"></h1> + <section> + <h3 i18n-content="fontSettingsStandard"></h3> + <div class="font-input-div"> + <div> + <select id="standard-font-family" class="font-input" data-type="string" + pref="webkit.webprefs.standard_font_family" + metric="Options_ChangeStandardFont"></select> + </div> + <div> + <input id="standard-font-size" type="range" min="0" max="24" + pref="webkit.webprefs.default_font_size"> + <div> + <span i18n-content="fontSettingsSizeTiny"></span> + <span i18n-content="fontSettingsSizeHuge" class="font-settings-huge"> + </span> + </div> + </div> + </div> + <div id="standard-font-sample" class="font-sample-div"></div> + </section> + <section> + <h3 i18n-content="fontSettingsSerif"></h3> + <div class="font-input-div"> + <div> + <select id="serif-font-family" class="font-input" data-type="string" + pref="webkit.webprefs.serif_font_family" + metric="Options_ChangeSerifFont"></select> + </div> + </div> + <div id="serif-font-sample" class="font-sample-div"></div> + </section> + <section> + <h3 i18n-content="fontSettingsSansSerif"></h3> + <div class="font-input-div"> + <div> + <select id="sans-serif-font-family" class="font-input" + data-type="string" + pref="webkit.webprefs.sansserif_font_family" + metric="Options_ChangeSansSerifFont"></select> + </div> + </div> + <div id="sans-serif-font-sample" class="font-sample-div"></div> + </section> + <section> + <h3 i18n-content="fontSettingsFixedWidth"></h3> + <div class="font-input-div"> + <div> + <select id="fixed-font-family" class="font-input" data-type="string" + pref="webkit.webprefs.fixed_font_family" + metric="Options_ChangeFixedFont"></select> + </div> + </div> + <div id="fixed-font-sample" class="font-sample-div"></div> + </section> + <section> + <h3 i18n-content="fontSettingsMinimumSize"></h3> + <div class="font-input-div"> + <div> + <input id="minimum-font-size" type="range" min="0" max="15" + pref="webkit.webprefs.minimum_font_size"> + <div> + <span i18n-content="fontSettingsSizeTiny"></span> + <span i18n-content="fontSettingsSizeHuge" class="font-settings-huge"> + </span> + </div> + </div> + </div> + <div id="minimum-font-sample" class="font-sample-div"></div> + </section> + <section> + <h3 i18n-content="fontSettingsEncoding"></h3> + <div class="font-input-div"> + <div> + <select id="font-encoding" data-type="string" + pref="intl.charset_default" + metric="Options_ChangeFontEncoding"></select> + </div> + </div> + </section> +</div> diff --git a/chrome/browser/resources/options2/font_settings.js b/chrome/browser/resources/options2/font_settings.js new file mode 100644 index 0000000..c00f525 --- /dev/null +++ b/chrome/browser/resources/options2/font_settings.js @@ -0,0 +1,234 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + + /** + * This is the absolute difference maintained between standard and + * fixed-width font sizes. Refer http://crbug.com/91922. + */ + const SIZE_DIFFERENCE_FIXED_STANDARD = 3; + + /** + * FontSettings class + * Encapsulated handling of the 'Fonts and Encoding' page. + * @class + */ + function FontSettings() { + OptionsPage.call(this, + 'fonts', + templateData.fontSettingsPageTabTitle, + 'font-settings'); + } + + cr.addSingletonGetter(FontSettings); + + FontSettings.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var standardFontRange = $('standard-font-size'); + standardFontRange.valueMap = [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, + 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 56, 64, 72]; + standardFontRange.continuous = false; + standardFontRange.notifyChange = this.standardRangeChanged_.bind(this); + standardFontRange.notifyPrefChange = + this.standardFontSizeChanged_.bind(this); + + var minimumFontRange = $('minimum-font-size'); + minimumFontRange.valueMap = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 20, 22, 24]; + minimumFontRange.continuous = false; + minimumFontRange.notifyChange = this.minimumRangeChanged_.bind(this); + minimumFontRange.notifyPrefChange = + this.minimumFontSizeChanged_.bind(this); + + var placeholder = localStrings.getString('fontSettingsPlaceholder'); + var elements = [$('standard-font-family'), $('serif-font-family'), + $('sans-serif-font-family'), $('fixed-font-family'), + $('font-encoding')]; + elements.forEach(function(el) { + el.appendChild(new Option(placeholder)); + el.setDisabled('noFontsAvailable', true); + }); + }, + + /** + * Called by the options page when this page has been shown. + */ + didShowPage: function() { + // The fonts list may be large so we only load it when this page is + // loaded for the first time. This makes opening the options window + // faster and consume less memory if the user never opens the fonts + // dialog. + if (!this.hasShown) { + chrome.send('fetchFontsData'); + this.hasShown = true; + } + }, + + /** + * Called as the user changes the standard font size. This allows for + * reflecting the change in the UI before the preference has been changed. + * @param {Element} el The slider input element. + * @param {number} value The mapped value currently set by the slider. + * @private + */ + standardRangeChanged_: function(el, value) { + var fontSampleEl = $('standard-font-sample'); + this.setUpFontSample_(fontSampleEl, value, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('serif-font-sample'); + this.setUpFontSample_(fontSampleEl, value, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('sans-serif-font-sample'); + this.setUpFontSample_(fontSampleEl, value, fontSampleEl.style.fontFamily, + true); + + fontSampleEl = $('fixed-font-sample'); + this.setUpFontSample_(fontSampleEl, + value - SIZE_DIFFERENCE_FIXED_STANDARD, + fontSampleEl.style.fontFamily, false); + }, + + /** + * Sets the 'default_fixed_font_size' preference when the standard font + * size has been changed by the user. + * @param {Element} el The slider input element. + * @param {number} value The mapped value that has been saved. + * @private + */ + standardFontSizeChanged_: function(el, value) { + Preferences.setIntegerPref('webkit.webprefs.default_fixed_font_size', + value - SIZE_DIFFERENCE_FIXED_STANDARD, ''); + }, + + /** + * Called as the user changes the miniumum font size. This allows for + * reflecting the change in the UI before the preference has been changed. + * @param {Element} el The slider input element. + * @param {number} value The mapped value currently set by the slider. + * @private + */ + minimumRangeChanged_: function(el, value) { + var fontSampleEl = $('minimum-font-sample'); + this.setUpFontSample_(fontSampleEl, value, fontSampleEl.style.fontFamily, + true); + }, + + /** + * Sets the 'minimum_logical_font_size' preference when the minimum font + * size has been changed by the user. + * @param {Element} el The slider input element. + * @param {number} value The mapped value that has been saved. + * @private + */ + minimumFontSizeChanged_: function(el, value) { + Preferences.setIntegerPref('webkit.webprefs.minimum_logical_font_size', + value, ''); + }, + + /** + * Sets the text, font size and font family of the sample text. + * @param {Element} el The div containing the sample text. + * @param {number} size The font size of the sample text. + * @param {string} font The font family of the sample text. + * @param {bool} showSize True if the font size should appear in the sample. + * @private + */ + setUpFontSample_: function(el, size, font, showSize) { + var prefix = showSize ? (size + ': ') : ''; + el.textContent = prefix + + localStrings.getString('fontSettingsLoremIpsum'); + el.style.fontSize = size + 'px'; + if (font) + el.style.fontFamily = font; + }, + + /** + * Populates a select list and selects the specified item. + * @param {Element} element The select element to populate. + * @param {Array} items The array of items from which to populate. + * @param {string} selectedValue The selected item. + * @private + */ + populateSelect_: function(element, items, selectedValue) { + // Remove any existing content. + element.textContent = ''; + + // Insert new child nodes into select element. + var value, text, selected, option; + for (var i = 0; i < items.length; i++) { + value = items[i][0]; + text = items[i][1]; + if (text) { + selected = value == selectedValue; + element.appendChild(new Option(text, value, false, selected)); + } else { + element.appendChild(document.createElement('hr')); + } + } + + element.setDisabled('noFontsAvailable', false); + } + }; + + // Chrome callbacks + FontSettings.setFontsData = function(fonts, encodings, selectedValues) { + FontSettings.getInstance().populateSelect_($('standard-font-family'), fonts, + selectedValues[0]); + FontSettings.getInstance().populateSelect_($('serif-font-family'), fonts, + selectedValues[1]); + FontSettings.getInstance().populateSelect_($('sans-serif-font-family'), + fonts, selectedValues[2]); + FontSettings.getInstance().populateSelect_($('fixed-font-family'), fonts, + selectedValues[3]); + FontSettings.getInstance().populateSelect_($('font-encoding'), encodings, + selectedValues[4]); + }; + + FontSettings.setUpStandardFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('standard-font-sample'), size, + font, true); + }; + + FontSettings.setUpSerifFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('serif-font-sample'), size, + font, true); + }; + + FontSettings.setUpSansSerifFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('sans-serif-font-sample'), + size, font, true); + }; + + FontSettings.setUpFixedFontSample = function(font, size) { + FontSettings.getInstance().setUpFontSample_($('fixed-font-sample'), + size, font, false); + }; + + FontSettings.setUpMinimumFontSample = function(size) { + // If size is less than 6, represent it as six in the sample to account + // for the minimum logical font size. + if (size < 6) + size = 6; + FontSettings.getInstance().setUpFontSample_($('minimum-font-sample'), size, + null, true); + }; + + // Export + return { + FontSettings: FontSettings + }; +}); + diff --git a/chrome/browser/resources/options2/handler_options.css b/chrome/browser/resources/options2/handler_options.css new file mode 100644 index 0000000..0f23446 --- /dev/null +++ b/chrome/browser/resources/options2/handler_options.css @@ -0,0 +1,57 @@ +/* + * 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. + */ + +.handlers-column-headers { + display: -webkit-box; + font-size: 13px; + font-weight: bold; +} + +.handlers-type-column { + width: 100px; + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; +} + +.handlers-site-column { + max-width: 180px; +} + +.handlers-site-column select { + max-width: 170px; +} + +.handlers-remove-column { + -webkit-box-flex: 1; +} + +.handlers-remove-link { + color: #555; + cursor: pointer; + opacity: 0; + padding-left: 14px; + text-decoration: underline; + -webkit-transition: 150ms opacity; +} + +div > .handlers-remove-column { + opacity: 0; +} + +div:not(.none):hover > .handlers-remove-column { + opacity: 1; +} + +#handlers { + min-height: 250px; +} + +#handler-options list { + border-radius: 2px; + border: solid 1px #D9D9D9; + margin-bottom: 10px; + margin-top: 4px; +} diff --git a/chrome/browser/resources/options2/handler_options.html b/chrome/browser/resources/options2/handler_options.html new file mode 100644 index 0000000..21284ca --- /dev/null +++ b/chrome/browser/resources/options2/handler_options.html @@ -0,0 +1,28 @@ +<div id="handler-options" class="page" hidden> + <h1 i18n-content="handlersPage"></h1> + <h3 i18n-content="handlers_active_heading"></h3> + <div class="handlers-column-headers"> + <div class="handlers-type-column"> + <div i18n-content="handlers_type_column_header"></div> + </div> + <div class="handlers-site-column"> + <div i18n-content="handlers_site_column_header"></div> + </div> + <div class="handlers-remove-column"></div> + </div> + <list id="handlers-list"></list> + + <div id="ignored-handlers-section"> + <h3 i18n-content="handlers_ignored_heading"></h3> + <div class="handlers-column-headers"> + <div class="handlers-type-column"> + <h3 i18n-content="handlers_type_column_header"></h3> + </div> + <div class="handlers-site-column"> + <h3 i18n-content="handlers_site_column_header"></h3> + </div> + <div class="handlers-remove-column"></div> + </div> + <list id="ignored-handlers-list"></list> + </div> +</div> diff --git a/chrome/browser/resources/options2/handler_options.js b/chrome/browser/resources/options2/handler_options.js new file mode 100644 index 0000000..039d9eb --- /dev/null +++ b/chrome/browser/resources/options2/handler_options.js @@ -0,0 +1,77 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // HandlerOptions class: + + /** + * Encapsulated handling of handler options page. + * @constructor + */ + function HandlerOptions() { + this.activeNavTab = null; + OptionsPage.call(this, + 'handlers', + templateData.handlersPageTabTitle, + 'handler-options'); + } + + cr.addSingletonGetter(HandlerOptions); + + HandlerOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The handlers list. + * @type {DeletableItemList} + * @private + */ + handlersList_: null, + + /** @inheritDoc */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.createHandlersList_(); + }, + + /** + * Creates, decorates and initializes the handlers list. + * @private + */ + createHandlersList_: function() { + this.handlersList_ = $('handlers-list'); + options.HandlersList.decorate(this.handlersList_); + this.handlersList_.autoExpands = true; + + this.ignoredHandlersList_ = $('ignored-handlers-list'); + options.IgnoredHandlersList.decorate(this.ignoredHandlersList_); + this.ignoredHandlersList_.autoExpands = true; + }, + }; + + /** + * Sets the list of handlers shown by the view. + * @param handlers to be shown in the view. + */ + HandlerOptions.setHandlers = function(handlers) { + $('handlers-list').setHandlers(handlers); + }; + + /** + * Sets the list of ignored handlers shown by the view. + * @param handlers to be shown in the view. + */ + HandlerOptions.setIgnoredHandlers = function(handlers) { + $('ignored-handlers-section').hidden = handlers.length == 0; + $('ignored-handlers-list').setHandlers(handlers); + }; + + return { + HandlerOptions: HandlerOptions + }; +}); diff --git a/chrome/browser/resources/options2/handler_options_list.js b/chrome/browser/resources/options2/handler_options_list.js new file mode 100644 index 0000000..661956c --- /dev/null +++ b/chrome/browser/resources/options2/handler_options_list.js @@ -0,0 +1,229 @@ +// 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. + +cr.define('options', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const HandlerOptions = options.HandlerOptions; + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + + const localStrings = new LocalStrings(); + + /** + * Creates a new ignored protocol / content handler list item. + * + * Accepts values in the form + * ['mailto', 'http://www.thesite.com/%s', 'The title of the protocol'], + * @param {Object} entry A dictionary describing the handlers for a given + * protocol. + * @constructor + * @extends {cr.ui.DeletableItemList} + */ + function IgnoredHandlersListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = IgnoredHandlersListItem.prototype; + el.decorate(); + return el; + } + + IgnoredHandlersListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // Protocol. + var protocolElement = document.createElement('div'); + protocolElement.textContent = this.dataItem[0]; + protocolElement.className = 'handlers-type-column'; + this.contentElement_.appendChild(protocolElement); + + // Site title. + var titleElement = document.createElement('div'); + titleElement.textContent = this.dataItem[2]; + titleElement.className = 'handlers-site-column'; + titleElement.title = this.dataItem[1]; + this.contentElement_.appendChild(titleElement); + }, + }; + + + var IgnoredHandlersList = cr.ui.define('list'); + + IgnoredHandlersList.prototype = { + __proto__: DeletableItemList.prototype, + + createItem: function(entry) { + return new IgnoredHandlersListItem(entry); + }, + + deleteItemAtIndex: function(index) { + chrome.send('removeIgnoredHandler', [this.dataModel.item(index)]); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + + /** + * Set the protocol handlers displayed by this list. See + * IgnoredHandlersListItem for an example of the format the list should + * take. + * + * @param {Object} list A list of ignored protocol handlers. + */ + setHandlers: function(list) { + this.dataModel = new ArrayDataModel(list); + }, + }; + + + + /** + * Creates a new protocol / content handler list item. + * + * Accepts values in the form + * { protocol: 'mailto', + * handlers: [ + * ['mailto', 'http://www.thesite.com/%s', 'The title of the protocol'], + * ..., + * ], + * } + * @param {Object} entry A dictionary describing the handlers for a given + * protocol. + * @constructor + * @extends {cr.ui.ListItem} + */ + function HandlerListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = HandlerListItem.prototype; + el.decorate(); + return el; + } + + HandlerListItem.prototype = { + __proto__: ListItem.prototype, + + buildWidget_: function(data, delegate) { + // Protocol. + var protocolElement = document.createElement('div'); + protocolElement.textContent = data.protocol; + protocolElement.className = 'handlers-type-column'; + this.appendChild(protocolElement); + + // Handler selection. + var handlerElement = document.createElement('div'); + var selectElement = document.createElement('select'); + var defaultOptionElement = document.createElement('option'); + defaultOptionElement.selected = data.default_handler == -1; + defaultOptionElement.textContent = + localStrings.getString('handlers_none_handler'); + defaultOptionElement.value = -1; + selectElement.appendChild(defaultOptionElement); + + for (var i = 0; i < data.handlers.length; ++i) { + var optionElement = document.createElement('option'); + optionElement.selected = i == data.default_handler; + optionElement.textContent = data.handlers[i][2]; + optionElement.value = i; + selectElement.appendChild(optionElement); + } + + selectElement.addEventListener('change', function (e) { + var index = e.target.value; + if (index == -1) { + this.classList.add('none'); + delegate.clearDefault(data.protocol); + } else { + handlerElement.classList.remove('none'); + delegate.setDefault(data.handlers[index]); + } + }); + handlerElement.appendChild(selectElement); + handlerElement.className = 'handlers-site-column'; + if (data.default_handler == -1) + this.classList.add('none'); + this.appendChild(handlerElement); + + // Remove link. + var removeElement = document.createElement('div'); + removeElement.textContent = + localStrings.getString('handlers_remove_link'); + removeElement.addEventListener('click', function (e) { + var value = selectElement ? selectElement.value : 0; + delegate.removeHandler(value, data.handlers[value]); + }); + removeElement.className = 'handlers-remove-column handlers-remove-link'; + this.appendChild(removeElement); + }, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + var self = this; + var delegate = { + removeHandler: function(index, handler) { + chrome.send('removeHandler', [handler]); + }, + setDefault: function(handler) { + chrome.send('setDefault', [handler]); + }, + clearDefault: function(protocol) { + chrome.send('clearDefault', [protocol]); + }, + }; + + this.buildWidget_(this.dataItem, delegate); + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var HandlersList = cr.ui.define('list'); + + HandlersList.prototype = { + __proto__: List.prototype, + + /** @inheritDoc */ + createItem: function(entry) { + return new HandlerListItem(entry); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + + /** + * Set the protocol handlers displayed by this list. + * See HandlerListItem for an example of the format the list should take. + * + * @param {Object} list A list of protocols with their registered handlers. + */ + setHandlers: function(list) { + this.dataModel = new ArrayDataModel(list); + }, + }; + + return { + IgnoredHandlersListItem: IgnoredHandlersListItem, + IgnoredHandlersList: IgnoredHandlersList, + HandlerListItem: HandlerListItem, + HandlersList: HandlersList, + }; +}); diff --git a/chrome/browser/resources/options2/import_data_overlay.css b/chrome/browser/resources/options2/import_data_overlay.css new file mode 100644 index 0000000..9db63f8 --- /dev/null +++ b/chrome/browser/resources/options2/import_data_overlay.css @@ -0,0 +1,23 @@ +#import-from-div { + margin-bottom: 20px; +} + +#import-checkboxes > div:not(:first-child) { + -webkit-padding-start: 8px; + margin: 5px 0; +} + +#import-throbber { + margin: 4px 10px; + vertical-align: middle; + visibility: hidden; +} + +#import-success-header { + font-size: 1.2em; +} + +#import-success-image { + text-align: center; + margin: 20px; +} diff --git a/chrome/browser/resources/options2/import_data_overlay.html b/chrome/browser/resources/options2/import_data_overlay.html new file mode 100644 index 0000000..355eced --- /dev/null +++ b/chrome/browser/resources/options2/import_data_overlay.html @@ -0,0 +1,70 @@ +<div id="import-data-overlay" class="page" hidden> + <h1 i18n-content="importDataOverlay"></h1> + <div id="import-data-configure"> + <div class="content-area"> + <div id="import-from-div"> + <span i18n-content="importFromLabel"></span> + <select id="import-browsers"> + <option i18n-content="importLoading"></option> + </select> + </div> + <div id="import-checkboxes"> + <div i18n-content="importDescription"></div> + <div> + <input id="import-history" type="checkbox" pref="import_history"> + <label for="import-history" i18n-content="importHistory"></label> + </div> + <div> + <input id="import-favorites" type="checkbox" pref="import_bookmarks"> + <label for="import-favorites" i18n-content="importFavorites"></label> + </div> + <div> + <input id="import-passwords" type="checkbox" + pref="import_saved_passwords"> + <label for="import-passwords" i18n-content="importPasswords"></label> + </div> + <div> + <input id="import-search" type="checkbox" pref="import_search_engine"> + <label for="import-search" i18n-content="importSearch"></label> + </div> + </div> + </div> + <div class="action-area"> + <div class="action-area-right"> + <div id="import-throbber" class="throbber"></div> + <div class="button-strip"> + <button id="import-data-cancel" i18n-content="cancel"></button> + <button id="import-data-commit" i18n-content="importCommit"></button> + </div> + </div> + </div> + </div> + <div id="import-data-success" hidden> + <div class="content-area"> + <div id="import-success-header"> + <strong i18n-content="importSucceeded"></strong> + </div> + <div id="import-success-image"> + <img src="success-large.png" /> + </div> + <div id="import-find-your-bookmarks"> + <span i18n-content="findYourImportedBookmarks"></span> + <div class="checkbox"> + <label> + <input id="import-data-show-bookmarks-bar" + pref="bookmark_bar.show_on_all_tabs" + metric="Options_ShowBookmarksBar" type="checkbox"> + <span i18n-content="toolbarShowBookmarksBar"></span> + </label> + </div> + </div> + </div> + <div class="action-area"> + <div class="action-area-right"> + <div class="button-strip"> + <button id="import-data-confirm" i18n-content="ok"></button> + </div> + </div> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/import_data_overlay.js b/chrome/browser/resources/options2/import_data_overlay.js new file mode 100644 index 0000000..2671aad --- /dev/null +++ b/chrome/browser/resources/options2/import_data_overlay.js @@ -0,0 +1,222 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + /** + * ImportDataOverlay class + * Encapsulated handling of the 'Import Data' overlay page. + * @class + */ + function ImportDataOverlay() { + OptionsPage.call(this, + 'importData', + templateData.importDataOverlayTabTitle, + 'import-data-overlay'); + } + + cr.addSingletonGetter(ImportDataOverlay); + + ImportDataOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].onchange = function() { + self.validateCommitButton_(); + }; + } + + $('import-browsers').onchange = function() { + self.updateCheckboxes_(); + self.validateCommitButton_(); + }; + + $('import-data-commit').onclick = function() { + chrome.send('importData', [ + String($('import-browsers').selectedIndex), + String($('import-history').checked), + String($('import-favorites').checked), + String($('import-passwords').checked), + String($('import-search').checked)]); + }; + + $('import-data-cancel').onclick = function() { + ImportDataOverlay.dismiss(); + }; + + $('import-data-show-bookmarks-bar').onchange = function() { + // Note: The callback 'toggleShowBookmarksBar' is handled within the + // browser options handler -- rather than the import data handler -- + // as the implementation is shared by several clients. + chrome.send('toggleShowBookmarksBar'); + } + + $('import-data-confirm').onclick = function() { + ImportDataOverlay.dismiss(); + }; + + // Form controls are disabled until the profile list has been loaded. + self.setControlsSensitive_(false); + }, + + /** + * Set enabled and checked state of the commit button. + * @private + */ + validateCommitButton_: function() { + var somethingToImport = + $('import-history').checked || $('import-favorites').checked || + $('import-passwords').checked || $('import-search').checked; + $('import-data-commit').disabled = !somethingToImport; + }, + + /** + * Sets the sensitivity of all the checkboxes and the commit button. + * @private + */ + setControlsSensitive_: function(sensitive) { + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) + this.setUpCheckboxState_(checkboxes[i], sensitive); + $('import-data-commit').disabled = !sensitive; + }, + + /** + * Set enabled and checked states a checkbox element. + * @param {Object} checkbox A checkbox element. + * @param {boolean} enabled The enabled state of the chekbox. + * @private + */ + setUpCheckboxState_: function(checkbox, enabled) { + checkbox.setDisabled("noProfileData", !enabled); + }, + + /** + * Update the enabled and checked states of all checkboxes. + * @private + */ + updateCheckboxes_: function() { + var index = $('import-browsers').selectedIndex; + var browserProfile; + if (this.browserProfiles.length > index) + browserProfile = this.browserProfiles[index]; + var importOptions = ['history', 'favorites', 'passwords', 'search']; + for (var i = 0; i < importOptions.length; i++) { + var checkbox = $('import-' + importOptions[i]); + var enable = browserProfile && browserProfile[importOptions[i]]; + this.setUpCheckboxState_(checkbox, enable); + } + }, + + /** + * Update the supported browsers popup with given entries. + * @param {array} browsers List of supported browsers name. + * @private + */ + updateSupportedBrowsers_: function(browsers) { + this.browserProfiles = browsers; + var browserSelect = $('import-browsers'); + browserSelect.remove(0); // Remove the 'Loading...' option. + browserSelect.textContent = ''; + var browserCount = browsers.length; + + if (browserCount == 0) { + var option = new Option(templateData.noProfileFound, 0); + browserSelect.appendChild(option); + + this.setControlsSensitive_(false); + } else { + this.setControlsSensitive_(true); + for (var i = 0; i < browserCount; i++) { + var browser = browsers[i] + var option = new Option(browser['name'], browser['index']); + browserSelect.appendChild(option); + } + + this.updateCheckboxes_(); + this.validateCommitButton_(); + } + }, + + /** + * Clear import prefs set when user checks/unchecks a checkbox so that each + * checkbox goes back to the default "checked" state (or alternatively, to + * the state set by a recommended policy). + * @private + */ + clearUserPrefs_: function() { + var importPrefs = ['import_history', + 'import_bookmarks', + 'import_saved_passwords', + 'import_search_engine']; + for (var i = 0; i < importPrefs.length; i++) + Preferences.clearPref(importPrefs[i], undefined); + }, + }; + + ImportDataOverlay.clearUserPrefs = function() { + ImportDataOverlay.getInstance().clearUserPrefs_(); + }; + + /** + * Update the supported browsers popup with given entries. + * @param {array} list of supported browsers name. + */ + ImportDataOverlay.updateSupportedBrowsers = function(browsers) { + ImportDataOverlay.getInstance().updateSupportedBrowsers_(browsers); + }; + + /** + * Update the UI to reflect whether an import operation is in progress. + * @param {boolean} state True if an import operation is in progress. + */ + ImportDataOverlay.setImportingState = function(state) { + var checkboxes = + document.querySelectorAll('#import-checkboxes input[type=checkbox]'); + for (var i = 0; i < checkboxes.length; i++) + checkboxes[i].setDisabled("Importing", state); + if (!state) + ImportDataOverlay.getInstance().updateCheckboxes_(); + $('import-browsers').disabled = state; + $('import-throbber').style.visibility = state ? "visible" : "hidden"; + ImportDataOverlay.getInstance().validateCommitButton_(); + }; + + /** + * Remove the import overlay from display. + */ + ImportDataOverlay.dismiss = function() { + ImportDataOverlay.clearUserPrefs(); + OptionsPage.closeOverlay(); + }; + + /** + * Show a message confirming the success of the import operation. + */ + ImportDataOverlay.confirmSuccess = function() { + var showBookmarksMessage = $('import-favorites').checked; + ImportDataOverlay.setImportingState(false); + $('import-data-configure').hidden = true; + $('import-data-success').hidden = false; + $('import-find-your-bookmarks').hidden = !showBookmarksMessage; + }; + + // Export + return { + ImportDataOverlay: ImportDataOverlay + }; +}); diff --git a/chrome/browser/resources/options2/inline_editable_list.js b/chrome/browser/resources/options2/inline_editable_list.js new file mode 100644 index 0000000..8aed93b --- /dev/null +++ b/chrome/browser/resources/options2/inline_editable_list.js @@ -0,0 +1,414 @@ +// 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. + +cr.define('options', function() { + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + + /** + * Creates a new list item with support for inline editing. + * @constructor + * @extends {options.DeletableListItem} + */ + function InlineEditableItem() { + var el = cr.doc.createElement('div'); + InlineEditableItem.decorate(el); + return el; + } + + /** + * Decorates an element as a inline-editable list item. Note that this is + * a subclass of DeletableItem. + * @param {!HTMLElement} el The element to decorate. + */ + InlineEditableItem.decorate = function(el) { + el.__proto__ = InlineEditableItem.prototype; + el.decorate(); + }; + + InlineEditableItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Whether or not this item can be edited. + * @type {boolean} + * @private + */ + editable_: true, + + /** + * Whether or not this is a placeholder for adding a new item. + * @type {boolean} + * @private + */ + isPlaceholder_: false, + + /** + * Fields associated with edit mode. + * @type {array} + * @private + */ + editFields_: null, + + /** + * Whether or not the current edit should be considered cancelled, rather + * than committed, when editing ends. + * @type {boolean} + * @private + */ + editCancelled_: true, + + /** + * The editable item corresponding to the last click, if any. Used to decide + * initial focus when entering edit mode. + * @type {HTMLElement} + * @private + */ + editClickTarget_: null, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + this.editFields_ = []; + this.addEventListener('mousedown', this.handleMouseDown_); + this.addEventListener('keydown', this.handleKeyDown_); + this.addEventListener('leadChange', this.handleLeadChange_); + }, + + /** @inheritDoc */ + selectionChanged: function() { + this.updateEditState(); + }, + + /** + * Called when this element gains or loses 'lead' status. Updates editing + * mode accordingly. + * @private + */ + handleLeadChange_: function() { + this.updateEditState(); + }, + + /** + * Updates the edit state based on the current selected and lead states. + */ + updateEditState: function() { + if (this.editable) + this.editing = this.selected && this.lead; + }, + + /** + * Whether the user is currently editing the list item. + * @type {boolean} + */ + get editing() { + return this.hasAttribute('editing'); + }, + set editing(editing) { + if (this.editing == editing) + return; + + if (editing) + this.setAttribute('editing', ''); + else + this.removeAttribute('editing'); + + if (editing) { + this.editCancelled_ = false; + + cr.dispatchSimpleEvent(this, 'edit', true); + + var focusElement = this.editClickTarget_ || this.initialFocusElement; + this.editClickTarget_ = null; + + // When this is called in response to the selectedChange event, + // the list grabs focus immediately afterwards. Thus we must delay + // our focus grab. + var self = this; + if (focusElement) { + window.setTimeout(function() { + // Make sure we are still in edit mode by the time we execute. + if (self.editing) { + focusElement.focus(); + focusElement.select(); + } + }, 50); + } + } else { + if (!this.editCancelled_ && this.hasBeenEdited && + this.currentInputIsValid) { + if (this.isPlaceholder) + this.parentNode.focusPlaceholder = true; + + this.updateStaticValues_(); + cr.dispatchSimpleEvent(this, 'commitedit', true); + } else { + this.resetEditableValues_(); + cr.dispatchSimpleEvent(this, 'canceledit', true); + } + } + }, + + /** + * Whether the item is editable. + * @type {boolean} + */ + get editable() { + return this.editable_; + }, + set editable(editable) { + this.editable_ = editable; + if (!editable) + this.editing = false; + }, + + /** + * Whether the item is a new item placeholder. + * @type {boolean} + */ + get isPlaceholder() { + return this.isPlaceholder_; + }, + set isPlaceholder(isPlaceholder) { + this.isPlaceholder_ = isPlaceholder; + if (isPlaceholder) + this.deletable = false; + }, + + /** + * The HTML element that should have focus initially when editing starts, + * if a specific element wasn't clicked. + * Defaults to the first <input> element; can be overriden by subclasses if + * a different element should be focused. + * @type {HTMLElement} + */ + get initialFocusElement() { + return this.contentElement.querySelector('input'); + }, + + /** + * Whether the input in currently valid to submit. If this returns false + * when editing would be submitted, either editing will not be ended, + * or it will be cancelled, depending on the context. + * Can be overrided by subclasses to perform input validation. + * @type {boolean} + */ + get currentInputIsValid() { + return true; + }, + + /** + * Returns true if the item has been changed by an edit. + * Can be overrided by subclasses to return false when nothing has changed + * to avoid unnecessary commits. + * @type {boolean} + */ + get hasBeenEdited() { + return true; + }, + + /** + * Returns a div containing an <input>, as well as static text if + * isPlaceholder is not true. + * @param {string} text The text of the cell. + * @return {HTMLElement} The HTML element for the cell. + * @private + */ + createEditableTextCell: function(text) { + var container = this.ownerDocument.createElement('div'); + + if (!this.isPlaceholder) { + var textEl = this.ownerDocument.createElement('div'); + textEl.className = 'static-text'; + textEl.textContent = text; + textEl.setAttribute('displaymode', 'static'); + container.appendChild(textEl); + } + + var inputEl = this.ownerDocument.createElement('input'); + inputEl.type = 'text'; + inputEl.value = text; + if (!this.isPlaceholder) { + inputEl.setAttribute('displaymode', 'edit'); + inputEl.staticVersion = textEl; + } else { + // At this point |this| is not attached to the parent list yet, so give + // a short timeout in order for the attachment to occur. + var self = this; + window.setTimeout(function() { + var list = self.parentNode; + if (list && list.focusPlaceholder) { + list.focusPlaceholder = false; + if (list.shouldFocusPlaceholder()) + inputEl.focus(); + } + }, 50); + } + + inputEl.addEventListener('focus', this.handleFocus_.bind(this)); + container.appendChild(inputEl); + this.editFields_.push(inputEl); + + return container; + }, + + /** + * Resets the editable version of any controls created by createEditable* + * to match the static text. + * @private + */ + resetEditableValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel && !this.isPlaceholder) + continue; + + if (editFields[i].tagName == 'INPUT') { + editFields[i].value = + this.isPlaceholder ? '' : staticLabel.textContent; + } + // Add more tag types here as new createEditable* methods are added. + + editFields[i].setCustomValidity(''); + } + }, + + /** + * Sets the static version of any controls created by createEditable* + * to match the current value of the editable version. Called on commit so + * that there's no flicker of the old value before the model updates. + * @private + */ + updateStaticValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel) + continue; + + if (editFields[i].tagName == 'INPUT') + staticLabel.textContent = editFields[i].value; + // Add more tag types here as new createEditable* methods are added. + } + }, + + /** + * Called a key is pressed. Handles committing and cancelling edits. + * @param {Event} e The key down event. + * @private + */ + handleKeyDown_: function(e) { + if (!this.editing) + return; + + var endEdit = false; + switch (e.keyIdentifier) { + case 'U+001B': // Esc + this.editCancelled_ = true; + endEdit = true; + break; + case 'Enter': + if (this.currentInputIsValid) + endEdit = true; + break; + } + + if (endEdit) { + // Blurring will trigger the edit to end; see InlineEditableItemList. + this.ownerDocument.activeElement.blur(); + // Make sure that handled keys aren't passed on and double-handled. + // (e.g., esc shouldn't both cancel an edit and close a subpage) + e.stopPropagation(); + } + }, + + /** + * Called when the list item is clicked. If the click target corresponds to + * an editable item, stores that item to focus when edit mode is started. + * @param {Event} e The mouse down event. + * @private + */ + handleMouseDown_: function(e) { + if (!this.editable || this.editing) + return; + + var clickTarget = e.target; + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + if (editFields[i] == clickTarget || + editFields[i].staticVersion == clickTarget) { + this.editClickTarget_ = editFields[i]; + return; + } + } + }, + }; + + /** + * Takes care of committing changes to inline editable list items when the + * window loses focus. + */ + function handleWindowBlurs() { + window.addEventListener('blur', function(e) { + var itemAncestor = findAncestor(document.activeElement, function(node) { + return node instanceof InlineEditableItem; + }); + if (itemAncestor); + document.activeElement.blur(); + }); + } + handleWindowBlurs(); + + var InlineEditableItemList = cr.ui.define('list'); + + InlineEditableItemList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Focuses the input element of the placeholder if true. + * @type {boolean} + */ + focusPlaceholder: false, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.setAttribute('inlineeditable', ''); + this.addEventListener('hasElementFocusChange', + this.handleListFocusChange_); + }, + + /** + * Called when the list hierarchy as a whole loses or gains focus; starts + * or ends editing for the lead item if necessary. + * @param {Event} e The change event. + * @private + */ + handleListFocusChange_: function(e) { + var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); + if (leadItem) { + if (e.newValue) + leadItem.updateEditState(); + else + leadItem.editing = false; + } + }, + + /** + * May be overridden by subclasses to disable focusing the placeholder. + * @return true if the placeholder element should be focused on edit commit. + */ + shouldFocusPlaceholder: function() { + return true; + }, + }; + + // Export + return { + InlineEditableItem: InlineEditableItem, + InlineEditableItemList: InlineEditableItemList, + }; +}); diff --git a/chrome/browser/resources/options2/instant_confirm_overlay.html b/chrome/browser/resources/options2/instant_confirm_overlay.html new file mode 100644 index 0000000..1e724ef --- /dev/null +++ b/chrome/browser/resources/options2/instant_confirm_overlay.html @@ -0,0 +1,16 @@ +<div id="instantConfirmOverlay" class="page" hidden> + <h1 i18n-content="instantConfirmTitle"></h1> + <!-- The text has line breaks, so we must use a pre. --> + <div class="content-area"> + <pre id="instantConfirmText" i18n-content="instantConfirmMessage"></pre> + <a id="instantConfirmLearnMore" target="_blank" i18n-content="learnMore" + i18n-values="href:instantLearnMoreLink"></a> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="instantConfirmCancel" i18n-content="cancel" + class="cancel-button"></button> + <button id="instantConfirmOk" i18n-content="ok"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/instant_confirm_overlay.js b/chrome/browser/resources/options2/instant_confirm_overlay.js new file mode 100644 index 0000000..01a9ee5 --- /dev/null +++ b/chrome/browser/resources/options2/instant_confirm_overlay.js @@ -0,0 +1,39 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + function InstantConfirmOverlay() { + OptionsPage.call(this, 'instantConfirm', + templateData.instantConfirmTitle, + 'instantConfirmOverlay'); + }; + + cr.addSingletonGetter(InstantConfirmOverlay); + + InstantConfirmOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('instantConfirmCancel').onclick = function() { + OptionsPage.closeOverlay(); + $('instantEnabledCheckbox').checked = false; + }; + + $('instantConfirmOk').onclick = function() { + OptionsPage.closeOverlay(); + chrome.send('enableInstant'); + }; + }, + }; + + // Export + return { + InstantConfirmOverlay: InstantConfirmOverlay + }; +}); diff --git a/chrome/browser/resources/options2/intents_list.js b/chrome/browser/resources/options2/intents_list.js new file mode 100644 index 0000000..e2dc21e --- /dev/null +++ b/chrome/browser/resources/options2/intents_list.js @@ -0,0 +1,707 @@ +// 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. + +// TODO(gbillock): refactor this together with CookiesList once we have +// a better sense from UX design what it'll look like and so what'll be shared. +cr.define('options', function() { + const DeletableItemList = options.DeletableItemList; + const DeletableItem = options.DeletableItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + const localStrings = new LocalStrings(); + + /** + * Returns the item's height, like offsetHeight but such that it works better + * when the page is zoomed. See the similar calculation in @{code cr.ui.List}. + * This version also accounts for the animation done in this file. + * @param {Element} item The item to get the height of. + * @return {number} The height of the item, calculated with zooming in mind. + */ + function getItemHeight(item) { + var height = item.style.height; + // Use the fixed animation target height if set, in case the element is + // currently being animated and we'd get an intermediate height below. + if (height && height.substr(-2) == 'px') + return parseInt(height.substr(0, height.length - 2)); + return item.getBoundingClientRect().height; + } + + // Map of parent pathIDs to node objects. + var parentLookup = {}; + + // Pending requests for child information. + var lookupRequests = {}; + + /** + * Creates a new list item for intent service data. Note that these are + * created and destroyed lazily as they scroll into and out of view, + * so they must be stateless. We cache the expanded item in + * @{code IntentsList} though, so it can keep state. + * (Mostly just which item is selected.) + * + * @param {Object} origin Data used to create an intents list item. + * @param {IntentsList} list The list that will contain this item. + * @constructor + * @extends {DeletableItem} + */ + function IntentsListItem(origin, list) { + var listItem = new DeletableItem(null); + listItem.__proto__ = IntentsListItem.prototype; + + listItem.origin = origin; + listItem.list = list; + listItem.decorate(); + + // This hooks up updateOrigin() to the list item, makes the top-level + // tree nodes (i.e., origins) register their IDs in parentLookup, and + // causes them to request their children if they have none. Note that we + // have special logic in the setter for the parent property to make sure + // that we can still garbage collect list items when they scroll out of + // view, even though it appears that we keep a direct reference. + if (origin) { + origin.parent = listItem; + origin.updateOrigin(); + } + + return listItem; + } + + IntentsListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function() { + this.siteChild = this.ownerDocument.createElement('div'); + this.siteChild.className = 'intents-site'; + this.dataChild = this.ownerDocument.createElement('div'); + this.dataChild.className = 'intents-data'; + this.itemsChild = this.ownerDocument.createElement('div'); + this.itemsChild.className = 'intents-items'; + this.infoChild = this.ownerDocument.createElement('div'); + this.infoChild.className = 'intents-details'; + this.infoChild.hidden = true; + var remove = this.ownerDocument.createElement('button'); + remove.textContent = localStrings.getString('removeIntent'); + remove.onclick = this.removeIntent_.bind(this); + this.infoChild.appendChild(remove); + var content = this.contentElement; + content.appendChild(this.siteChild); + content.appendChild(this.dataChild); + content.appendChild(this.itemsChild); + this.itemsChild.appendChild(this.infoChild); + if (this.origin && this.origin.data) { + this.siteChild.textContent = this.origin.data.site; + this.siteChild.setAttribute('title', this.origin.data.site); + } + this.itemList_ = []; + }, + + /** @type {boolean} */ + get expanded() { + return this.expanded_; + }, + set expanded(expanded) { + if (this.expanded_ == expanded) + return; + this.expanded_ = expanded; + if (expanded) { + var oldExpanded = this.list.expandedItem; + this.list.expandedItem = this; + this.updateItems_(); + if (oldExpanded) + oldExpanded.expanded = false; + this.classList.add('show-items'); + this.dataChild.hidden = true; + } else { + if (this.list.expandedItem == this) { + this.list.expandedItem = null; + } + this.style.height = ''; + this.itemsChild.style.height = ''; + this.classList.remove('show-items'); + this.dataChild.hidden = false; + } + }, + + /** + * The callback for the "remove" button shown when an item is selected. + * Requests that the currently selected intent service be removed. + * @private + */ + removeIntent_: function() { + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.node) + chrome.send('removeIntent', [item.node.pathId]); + } + }, + + /** + * Disable animation within this intents list item, in preparation for + * making changes that will need to be animated. Makes it possible to + * measure the contents without displaying them, to set animation targets. + * @private + */ + disableAnimation_: function() { + this.itemsHeight_ = getItemHeight(this.itemsChild); + this.classList.add('measure-items'); + }, + + /** + * Enable animation after changing the contents of this intents list item. + * See @{code disableAnimation_}. + * @private + */ + enableAnimation_: function() { + if (!this.classList.contains('measure-items')) + this.disableAnimation_(); + this.itemsChild.style.height = ''; + // This will force relayout in order to calculate the new heights. + var itemsHeight = getItemHeight(this.itemsChild); + var fixedHeight = getItemHeight(this) + itemsHeight - this.itemsHeight_; + this.itemsChild.style.height = this.itemsHeight_ + 'px'; + // Force relayout before enabling animation, so that if we have + // changed things since the last layout, they will not be animated + // during subsequent layouts. + this.itemsChild.offsetHeight; + this.classList.remove('measure-items'); + this.itemsChild.style.height = itemsHeight + 'px'; + this.style.height = fixedHeight + 'px'; + }, + + /** + * Updates the origin summary to reflect changes in its items. + * Both IntentsListItem and IntentsTreeNode implement this API. + * This implementation scans the descendants to update the text. + */ + updateOrigin: function() { + var text = ''; + for (var i = 0; i < this.origin.children.length; ++i) { + if (text.length > 0) + text += ', ' + this.origin.children[i].data.action; + else + text = this.origin.children[i].data.action; + } + this.dataChild.textContent = text; + + if (this.expanded) + this.updateItems_(); + }, + + /** + * Updates the items section to reflect changes, animating to the new state. + * Removes existing contents and calls @{code IntentsTreeNode.createItems}. + * @private + */ + updateItems_: function() { + this.disableAnimation_(); + this.itemsChild.textContent = ''; + this.infoChild.hidden = true; + this.selectedIndex_ = -1; + this.itemList_ = []; + if (this.origin) + this.origin.createItems(this); + this.itemsChild.appendChild(this.infoChild); + this.enableAnimation_(); + }, + + /** + * Append a new intents node "bubble" to this list item. + * @param {IntentsTreeNode} node The intents node to add a bubble for. + * @param {Element} div The DOM element for the bubble itself. + * @return {number} The index the bubble was added at. + */ + appendItem: function(node, div) { + this.itemList_.push({node: node, div: div}); + this.itemsChild.appendChild(div); + return this.itemList_.length - 1; + }, + + /** + * The currently selected intents node ("intents bubble") index. + * @type {number} + * @private + */ + selectedIndex_: -1, + + /** + * Get the currently selected intents node ("intents bubble") index. + * @type {number} + */ + get selectedIndex() { + return this.selectedIndex_; + }, + + /** + * Set the currently selected intents node ("intents bubble") index to + * @{code itemIndex}, unselecting any previously selected node first. + * @param {number} itemIndex The index to set as the selected index. + * TODO: KILL THIS + */ + set selectedIndex(itemIndex) { + // Get the list index up front before we change anything. + var index = this.list.getIndexOfListItem(this); + // Unselect any previously selected item. + if (this.selectedIndex_ >= 0) { + var item = this.itemList_[this.selectedIndex_]; + if (item && item.div) + item.div.removeAttribute('selected'); + } + // Special case: decrementing -1 wraps around to the end of the list. + if (itemIndex == -2) + itemIndex = this.itemList_.length - 1; + // Check if we're going out of bounds and hide the item details. + if (itemIndex < 0 || itemIndex >= this.itemList_.length) { + this.selectedIndex_ = -1; + this.disableAnimation_(); + this.infoChild.hidden = true; + this.enableAnimation_(); + return; + } + // Set the new selected item and show the item details for it. + this.selectedIndex_ = itemIndex; + this.itemList_[itemIndex].div.setAttribute('selected', ''); + this.disableAnimation_(); + this.infoChild.hidden = false; + this.enableAnimation_(); + // If we're near the bottom of the list this may cause the list item to go + // beyond the end of the visible area. Fix it after the animation is done. + var list = this.list; + window.setTimeout(function() { list.scrollIndexIntoView(index); }, 150); + }, + }; + + /** + * {@code IntentsTreeNode}s mirror the structure of the intents tree lazily, + * and contain all the actual data used to generate the + * {@code IntentsListItem}s. + * @param {Object} data The data object for this node. + * @constructor + */ + function IntentsTreeNode(data) { + this.data = data; + this.children = []; + } + + IntentsTreeNode.prototype = { + /** + * Insert an intents tree node at the given index. + * Both IntentsList and IntentsTreeNode implement this API. + * @param {Object} data The data object for the node to add. + * @param {number} index The index at which to insert the node. + */ + insertAt: function(data, index) { + var child = new IntentsTreeNode(data); + this.children.splice(index, 0, child); + child.parent = this; + this.updateOrigin(); + }, + + /** + * Remove an intents tree node from the given index. + * Both IntentsList and IntentsTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.children.length) { + this.children.splice(index, 1); + this.updateOrigin(); + } + }, + + /** + * Clears all children. + * Both IntentsList and IntentsTreeNode implement this API. + * It is used by IntentsList.loadChildren(). + */ + clear: function() { + // We might leave some garbage in parentLookup for removed children. + // But that should be OK because parentLookup is cleared when we + // reload the tree. + this.children = []; + this.updateOrigin(); + }, + + /** + * The counter used by startBatchUpdates() and endBatchUpdates(). + * @type {number} + */ + batchCount_: 0, + + /** + * See cr.ui.List.startBatchUpdates(). + * Both IntentsList (via List) and IntentsTreeNode implement this API. + */ + startBatchUpdates: function() { + this.batchCount_++; + }, + + /** + * See cr.ui.List.endBatchUpdates(). + * Both IntentsList (via List) and IntentsTreeNode implement this API. + */ + endBatchUpdates: function() { + if (!--this.batchCount_) + this.updateOrigin(); + }, + + /** + * Requests updating the origin summary to reflect changes in this item. + * Both IntentsListItem and IntentsTreeNode implement this API. + */ + updateOrigin: function() { + if (!this.batchCount_ && this.parent) + this.parent.updateOrigin(); + }, + + /** + * Create the intents services rows for this node. + * Append the rows to @{code item}. + * @param {IntentsListItem} item The intents list item to create items in. + */ + createItems: function(item) { + if (this.children.length > 0) { + for (var i = 0; i < this.children.length; ++i) + this.children[i].createItems(item); + } else if (this.data && !this.data.hasChildren) { + var div = item.ownerDocument.createElement('div'); + div.className = 'intents-item'; + // Help out screen readers and such: this is a clickable thing. + div.setAttribute('role', 'button'); + + var divAction = item.ownerDocument.createElement('div'); + divAction.className = 'intents-item-action'; + divAction.textContent = this.data.action; + div.appendChild(divAction); + + var divTypes = item.ownerDocument.createElement('div'); + divTypes.className = 'intents-item-types'; + var text = ""; + for (var i = 0; i < this.data.types.length; ++i) { + if (text != "") + text += ", "; + text += this.data.types[i]; + } + divTypes.textContent = text; + div.appendChild(divTypes); + + var divUrl = item.ownerDocument.createElement('div'); + divUrl.className = 'intents-item-url'; + divUrl.textContent = this.data.url; + div.appendChild(divUrl); + + var index = item.appendItem(this, div); + div.onclick = function() { + if (item.selectedIndex == index) + item.selectedIndex = -1; + else + item.selectedIndex = index; + }; + } + }, + + /** + * The parent of this intents tree node. + * @type {?IntentsTreeNode|IntentsListItem} + */ + get parent(parent) { + // See below for an explanation of this special case. + if (typeof this.parent_ == 'number') + return this.list_.getListItemByIndex(this.parent_); + return this.parent_; + }, + set parent(parent) { + if (parent == this.parent) + return; + + if (parent instanceof IntentsListItem) { + // If the parent is to be a IntentsListItem, then we keep the reference + // to it by its containing list and list index, rather than directly. + // This allows the list items to be garbage collected when they scroll + // out of view (except the expanded item, which we cache). This is + // transparent except in the setter and getter, where we handle it. + this.parent_ = parent.listIndex; + this.list_ = parent.list; + parent.addEventListener('listIndexChange', + this.parentIndexChanged_.bind(this)); + } else { + this.parent_ = parent; + } + + + if (parent) + parentLookup[this.pathId] = this; + else + delete parentLookup[this.pathId]; + + if (this.data && this.data.hasChildren && + !this.children.length && !lookupRequests[this.pathId]) { + lookupRequests[this.pathId] = true; + chrome.send('loadIntents', [this.pathId]); + } + }, + + /** + * Called when the parent is a IntentsListItem whose index has changed. + * See the code above that avoids keeping a direct reference to + * IntentsListItem parents, to allow them to be garbage collected. + * @private + */ + parentIndexChanged_: function(event) { + if (typeof this.parent_ == 'number') { + this.parent_ = event.newValue; + // We set a timeout to update the origin, rather than doing it right + // away, because this callback may occur while the list items are + // being repopulated following a scroll event. Calling updateOrigin() + // immediately could trigger relayout that would reset the scroll + // position within the list, among other things. + window.setTimeout(this.updateOrigin.bind(this), 0); + } + }, + + /** + * The intents tree path id. + * @type {string} + */ + get pathId() { + var parent = this.parent; + if (parent && parent instanceof IntentsTreeNode) + return parent.pathId + ',' + this.data.action; + return this.data.site; + }, + }; + + /** + * Creates a new intents list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {DeletableItemList} + */ + var IntentsList = cr.ui.define('list'); + + IntentsList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.classList.add('intents-list'); + this.data_ = []; + this.dataModel = new ArrayDataModel(this.data_); + this.addEventListener('keydown', this.handleKeyLeftRight_.bind(this)); + var sm = new ListSingleSelectionModel(); + sm.addEventListener('change', this.cookieSelectionChange_.bind(this)); + sm.addEventListener('leadIndexChange', this.cookieLeadChange_.bind(this)); + this.selectionModel = sm; + this.fixedHeight = false; + }, + + /** + * Handles key down events and looks for left and right arrows, then + * dispatches to the currently expanded item, if any. + * @param {Event} e The keydown event. + * @private + */ + handleKeyLeftRight_: function(e) { + var id = e.keyIdentifier; + if ((id == 'Left' || id == 'Right') && this.expandedItem) { + var cs = this.ownerDocument.defaultView.getComputedStyle(this); + var rtl = cs.direction == 'rtl'; + if ((!rtl && id == 'Left') || (rtl && id == 'Right')) + this.expandedItem.selectedIndex--; + else + this.expandedItem.selectedIndex++; + this.scrollIndexIntoView(this.expandedItem.listIndex); + // Prevent the page itself from scrolling. + e.preventDefault(); + } + }, + + /** + * Called on selection model selection changes. + * @param {Event} ce The selection change event. + * @private + */ + cookieSelectionChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.getListItemByIndex(change.index); + if (listItem) { + if (!change.selected) { + // We set a timeout here, rather than setting the item unexpanded + // immediately, so that if another item gets set expanded right + // away, it will be expanded before this item is unexpanded. It + // will notice that, and unexpand this item in sync with its own + // expansion. Later, this callback will end up having no effect. + window.setTimeout(function() { + if (!listItem.selected || !listItem.lead) + listItem.expanded = false; + }, 0); + } else if (listItem.lead) { + listItem.expanded = true; + } + } + }, this); + }, + + /** + * Called on selection model lead changes. + * @param {Event} pe The lead change event. + * @private + */ + cookieLeadChange_: function(pe) { + if (pe.oldValue != -1) { + var listItem = this.getListItemByIndex(pe.oldValue); + if (listItem) { + // See cookieSelectionChange_ above for why we use a timeout here. + window.setTimeout(function() { + if (!listItem.lead || !listItem.selected) + listItem.expanded = false; + }, 0); + } + } + if (pe.newValue != -1) { + var listItem = this.getListItemByIndex(pe.newValue); + if (listItem && listItem.selected) + listItem.expanded = true; + } + }, + + /** + * The currently expanded item. Used by IntentsListItem above. + * @type {?IntentsListItem} + */ + expandedItem: null, + + // from cr.ui.List + /** @inheritDoc */ + createItem: function(data) { + // We use the cached expanded item in order to allow it to maintain some + // state (like its fixed height, and which bubble is selected). + if (this.expandedItem && this.expandedItem.origin == data) + return this.expandedItem; + return new IntentsListItem(data, this); + }, + + // from options.DeletableItemList + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var item = this.data_[index]; + if (item) { + var pathId = item.pathId; + if (pathId) + chrome.send('removeIntent', [pathId]); + } + }, + + /** + * Insert an intents tree node at the given index. + * Both IntentsList and IntentsTreeNode implement this API. + * @param {Object} data The data object for the node to add. + * @param {number} index The index at which to insert the node. + */ + insertAt: function(data, index) { + this.dataModel.splice(index, 0, new IntentsTreeNode(data)); + }, + + /** + * Remove an intents tree node from the given index. + * Both IntentsList and IntentsTreeNode implement this API. + * @param {number} index The index of the tree node to remove. + */ + remove: function(index) { + if (index < this.data_.length) + this.dataModel.splice(index, 1); + }, + + /** + * Clears the list. + * Both IntentsList and IntentsTreeNode implement this API. + * It is used by IntentsList.loadChildren(). + */ + clear: function() { + parentLookup = {}; + this.data_ = []; + this.dataModel = new ArrayDataModel(this.data_); + this.redraw(); + }, + + /** + * Add tree nodes by given parent. + * Note: this method will be O(n^2) in the general case. Use it only to + * populate an empty parent or to insert single nodes to avoid this. + * @param {Object} parent The parent node. + * @param {number} start Start index of where to insert nodes. + * @param {Array} nodesData Nodes data array. + * @private + */ + addByParent_: function(parent, start, nodesData) { + if (!parent) + return; + + parent.startBatchUpdates(); + for (var i = 0; i < nodesData.length; ++i) + parent.insertAt(nodesData[i], start + i); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Add tree nodes by parent id. + * This is used by intents_view.js. + * Note: this method will be O(n^2) in the general case. Use it only to + * populate an empty parent or to insert single nodes to avoid this. + * @param {string} parentId Id of the parent node. + * @param {number} start Start index of where to insert nodes. + * @param {Array} nodesData Nodes data array. + */ + addByParentId: function(parentId, start, nodesData) { + var parent = parentId ? parentLookup[parentId] : this; + this.addByParent_(parent, start, nodesData); + }, + + /** + * Removes tree nodes by parent id. + * This is used by intents_view.js. + * @param {string} parentId Id of the parent node. + * @param {number} start Start index of nodes to remove. + * @param {number} count Number of nodes to remove. + */ + removeByParentId: function(parentId, start, count) { + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + while (count-- > 0) + parent.remove(start); + parent.endBatchUpdates(); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Loads the immediate children of given parent node. + * This is used by intents_view.js. + * @param {string} parentId Id of the parent node. + * @param {Array} children The immediate children of parent node. + */ + loadChildren: function(parentId, children) { + if (parentId) + delete lookupRequests[parentId]; + var parent = parentId ? parentLookup[parentId] : this; + if (!parent) + return; + + parent.startBatchUpdates(); + parent.clear(); + this.addByParent_(parent, 0, children); + parent.endBatchUpdates(); + }, + }; + + return { + IntentsList: IntentsList + }; +}); diff --git a/chrome/browser/resources/options2/intents_view.css b/chrome/browser/resources/options2/intents_view.css new file mode 100644 index 0000000..84c274d --- /dev/null +++ b/chrome/browser/resources/options2/intents_view.css @@ -0,0 +1,181 @@ +/* +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. +*/ + +/* Styles for the intents list elements in intents_view.html. */ + +#intents-column-headers { + position: relative; + width: 100%; +} + +#intents-column-headers h3 { + font-size: 105%; + font-weight: bold; + margin: 10px 0; +} + +/* Notice the width and padding for these columns match up with those below. */ +#intents-site-column { + display: inline-block; + font-weight: bold; + width: 11em; +} + +#intents-data-column { + -webkit-padding-start: 7px; + display: inline-block; + font-weight: bold; +} + +#intents-list { + border: 1px solid #d9d9d9; + margin: 0; +} + +/* Enable animating the height of items. */ +list.intents-list .deletable-item { + -webkit-transition: height .15s ease-in-out; +} + +/* Disable webkit-box display. */ +list.intents-list .deletable-item > :first-child { + display: block; +} + +/* Force the X for deleting an origin to stay at the top. */ +list.intents-list > .deletable-item > .close-button { + position: absolute; + right: 2px; + top: 8px; +} + +html[dir=rtl] list.intents-list > .deletable-item > .close-button { + left: 2px; + right: auto; +} + +/* Styles for the site (aka origin) and its summary. */ + +.intents-site { + /* Notice that the width, margin, and padding match up with those above. */ + -webkit-margin-end: 2px; + -webkit-padding-start: 5px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + width: 11em; +} + +list.intents-list > .deletable-item[selected] .intents-site { + -webkit-user-select: text; +} + +.intents-data { + display: inline-block; +} + +list.intents-list > .deletable-item[selected] .intents-data { + -webkit-user-select: text; +} + +.intents-items { + /* Notice that the margin and padding match up with those above. */ + -webkit-margin-start: 11em; + -webkit-padding-start: 7px; + -webkit-transition: .15s ease-in-out; + display: table; + height: 0; + opacity: 0; + /* Make the intents items wrap correctly. */ + white-space: normal; +} + +.measure-items .intents-items { + -webkit-transition: none; + height: auto; + visibility: hidden; +} + +.show-items .intents-items { + opacity: 1; +} + +.intents-items .intents-item { + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + background: #e0e9f5; + border: 1px solid #8392ae; + display: table-row; + font-size: 85%; + height: auto; + margin: 2px 4px 2px 0; + overflow: hidden; + padding: 0 3px; + text-align: center; + text-overflow: ellipsis; +} + +.intents-item .intents-item-action { + display: table-cell; + padding: 2px 5px; +} + +.intents-item .intents-item-types { + display: table-cell; + padding: 2px 5px; + overflow: hidden; +} + +.intents-item .intents-item-url { + display: table-cell; + padding: 2px 5px; + overflow: hidden; + text-overflow: ellipsis; +} + +.intents-items .intents-item:hover { + background: #eef3f9; + border-color: #647187; +} + +.intents-items .intents-item[selected] { + background: #f5f8f8; + border-color: #b2b2b2; +} + +.intents-items .intents-item[selected]:hover { + background: #f5f8f8; + border-color: #647187; +} + +/* Styles for the intents details box. */ + +.intents-details { + background: #f5f8f8; + border-radius: 5px; + border: 1px solid #b2b2b2; + margin-top: 2px; + padding: 5px; +} + +list.intents-list > .deletable-item[selected] .intents-details { + -webkit-user-select: text; +} + +.intents-details-table { + table-layout: fixed; + width: 100%; +} + +.intents-details-label { + vertical-align: top; + white-space: pre; + width: 10em; +} + +.intents-details-value { + word-wrap: break-word; +} diff --git a/chrome/browser/resources/options2/intents_view.html b/chrome/browser/resources/options2/intents_view.html new file mode 100644 index 0000000..6803c23 --- /dev/null +++ b/chrome/browser/resources/options2/intents_view.html @@ -0,0 +1,12 @@ +<div id="intents-view-page" class="page" hidden> + <h1 i18n-content="intentsViewPage"></h1> + <div id="intents-column-headers"> + <div id="intents-site-column"> + <h3 i18n-content="intentsDomain"></h3> + </div> + <div id="intents-data-column"> + <h3 i18n-content="intentsServiceData"></h3> + </div> + </div> + <list id="intents-list"></list> +</div> diff --git a/chrome/browser/resources/options2/intents_view.js b/chrome/browser/resources/options2/intents_view.js new file mode 100644 index 0000000..afc7d68 --- /dev/null +++ b/chrome/browser/resources/options2/intents_view.js @@ -0,0 +1,83 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + + ///////////////////////////////////////////////////////////////////////////// + // IntentsView class: + + /** + * Encapsulated handling of the Intents data page. + * @constructor + */ + function IntentsView(model) { + OptionsPage.call(this, 'intents', + templateData.intentsViewPageTabTitle, + 'intents-view-page'); + } + + cr.addSingletonGetter(IntentsView); + + IntentsView.prototype = { + __proto__: OptionsPage.prototype, + + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var intentsList = $('intents-list'); + options.IntentsList.decorate(intentsList); + window.addEventListener('resize', this.handleResize_.bind(this)); + + this.addEventListener('visibleChange', this.handleVisibleChange_); + }, + + initialized_: false, + + /** + * Handler for OptionsPage's visible property change event. + * @param {Event} e Property change event. + * @private + */ + handleVisibleChange_: function(e) { + if (!this.visible) + return; + + // Resize the intents list whenever the options page becomes visible. + this.handleResize_(null); + if (!this.initialized_) { + this.initialized_ = true; + chrome.send('loadIntents'); + } else { + $('intents-list').redraw(); + } + }, + + /** + * Handler for when the window changes size. Resizes the intents list to + * match the window height. + * @param {?Event} e Window resize event, or null if called directly. + * @private + */ + handleResize_: function(e) { + if (!this.visible) + return; + var intentsList = $('intents-list'); + // 25 pixels from the window bottom seems like a visually pleasing amount. + var height = window.innerHeight - intentsList.offsetTop - 25; + intentsList.style.height = height + 'px'; + }, + }; + + // IntentsViewHandler callbacks. + IntentsView.loadChildren = function(args) { + $('intents-list').loadChildren(args[0], args[1]); + }; + + // Export + return { + IntentsView: IntentsView + }; + +}); diff --git a/chrome/browser/resources/options2/language_add_language_overlay.html b/chrome/browser/resources/options2/language_add_language_overlay.html new file mode 100644 index 0000000..bca2e75 --- /dev/null +++ b/chrome/browser/resources/options2/language_add_language_overlay.html @@ -0,0 +1,28 @@ +<div id="add-language-overlay-page" class="page" hidden> +<!-- +Buttons are too small for touch in the touchui builds. +Use drop down box for touchui builds instead. +--> +<if expr="pp_ifdef('chromeos')"> + <ul id="add-language-overlay-language-list"> + </ul> + <button id="add-language-overlay-cancel-button" + i18n-content="cancel"></button> +</if> +<if expr="not pp_ifdef('chromeos')"> + <h1 i18n-content="add_language_title"></h1> + <div class="content-area"> + <label> + <span i18n-content="add_language_select_label"></span> + <select id="add-language-overlay-language-list"></select> + </label> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="add-language-overlay-cancel-button" i18n-content="cancel"> + </button> + <button id="add-language-overlay-ok-button" i18n-content="ok"></button> + </div> + </div> +</if> +</div> diff --git a/chrome/browser/resources/options2/language_add_language_overlay.js b/chrome/browser/resources/options2/language_add_language_overlay.js new file mode 100644 index 0000000..9fa856d --- /dev/null +++ b/chrome/browser/resources/options2/language_add_language_overlay.js @@ -0,0 +1,73 @@ +// 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. + +/////////////////////////////////////////////////////////////////////////////// +// AddLanguageOverlay class: + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of ChromeOS add language overlay page. + * @constructor + */ + function AddLanguageOverlay() { + OptionsPage.call(this, 'addLanguage', + localStrings.getString('add_button'), + 'add-language-overlay-page'); + } + + cr.addSingletonGetter(AddLanguageOverlay); + + AddLanguageOverlay.prototype = { + // Inherit AddLanguageOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initializes AddLanguageOverlay page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + // Set up the cancel button. + $('add-language-overlay-cancel-button').onclick = function(e) { + OptionsPage.closeOverlay(); + }; + + // Create the language list with which users can add a language. + var addLanguageList = $('add-language-overlay-language-list'); + var languageListData = templateData.languageList; + for (var i = 0; i < languageListData.length; i++) { + var language = languageListData[i]; + var displayText = language.displayName; + // If the native name is different, add it. + if (language.displayName != language.nativeDisplayName) { + displayText += ' - ' + language.nativeDisplayName; + } + + if (cr.isChromeOS) { + var button = document.createElement('button'); + button.className = 'link-button'; + button.textContent = displayText; + button.languageCode = language.code; + var li = document.createElement('li'); + li.languageCode = language.code; + li.appendChild(button); + addLanguageList.appendChild(li); + } else { + var option = document.createElement('option'); + option.value = language.code; + option.textContent = displayText; + addLanguageList.appendChild(option); + } + } + }, + }; + + return { + AddLanguageOverlay: AddLanguageOverlay + }; +}); diff --git a/chrome/browser/resources/options2/language_list.js b/chrome/browser/resources/options2/language_list.js new file mode 100644 index 0000000..3cbcb75 --- /dev/null +++ b/chrome/browser/resources/options2/language_list.js @@ -0,0 +1,487 @@ +// 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. + +cr.define('options', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Creates a new Language list item. + * @param {String} languageCode the languageCode. + * @constructor + * @extends {DeletableItem.ListItem} + */ + function LanguageListItem(languageCode) { + var el = cr.doc.createElement('li'); + el.__proto__ = LanguageListItem.prototype; + el.languageCode_ = languageCode; + el.decorate(); + return el; + }; + + LanguageListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * The language code of this language. + * @type {String} + * @private + */ + languageCode_: null, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + var languageCode = this.languageCode_; + var languageOptions = options.LanguageOptions.getInstance(); + this.deletable = languageOptions.languageIsDeletable(languageCode); + this.languageCode = languageCode; + this.languageName = cr.doc.createElement('div'); + this.languageName.className = 'language-name'; + this.languageName.textContent = + LanguageList.getDisplayNameFromLanguageCode(languageCode); + this.contentElement.appendChild(this.languageName); + this.title = + LanguageList.getNativeDisplayNameFromLanguageCode(languageCode); + this.draggable = true; + }, + }; + + /** + * Creates a new language list. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {cr.ui.List} + */ + var LanguageList = cr.ui.define('list'); + + /** + * Gets display name from the given language code. + * @param {string} languageCode Language code (ex. "fr"). + */ + LanguageList.getDisplayNameFromLanguageCode = function(languageCode) { + // Build the language code to display name dictionary at first time. + if (!this.languageCodeToDisplayName_) { + this.languageCodeToDisplayName_ = {}; + var languageList = templateData.languageList; + for (var i = 0; i < languageList.length; i++) { + var language = languageList[i]; + this.languageCodeToDisplayName_[language.code] = language.displayName; + } + } + + return this.languageCodeToDisplayName_[languageCode]; + } + + /** + * Gets native display name from the given language code. + * @param {string} languageCode Language code (ex. "fr"). + */ + LanguageList.getNativeDisplayNameFromLanguageCode = function(languageCode) { + // Build the language code to display name dictionary at first time. + if (!this.languageCodeToNativeDisplayName_) { + this.languageCodeToNativeDisplayName_ = {}; + var languageList = templateData.languageList; + for (var i = 0; i < languageList.length; i++) { + var language = languageList[i]; + this.languageCodeToNativeDisplayName_[language.code] = + language.nativeDisplayName; + } + } + + return this.languageCodeToNativeDisplayName_[languageCode]; + } + + /** + * Returns true if the given language code is valid. + * @param {string} languageCode Language code (ex. "fr"). + */ + LanguageList.isValidLanguageCode = function(languageCode) { + // Having the display name for the language code means that the + // language code is valid. + if (LanguageList.getDisplayNameFromLanguageCode(languageCode)) { + return true; + } + return false; + } + + LanguageList.prototype = { + __proto__: DeletableItemList.prototype, + + // The list item being dragged. + draggedItem: null, + // The drop position information: "below" or "above". + dropPos: null, + // The preference is a CSV string that describes preferred languages + // in Chrome OS. The language list is used for showing the language + // list in "Language and Input" options page. + preferredLanguagesPref: 'settings.language.preferred_languages', + // The preference is a CSV string that describes accept languages used + // for content negotiation. To be more precise, the list will be used + // in "Accept-Language" header in HTTP requests. + acceptLanguagesPref: 'intl.accept_languages', + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel; + + // HACK(arv): http://crbug.com/40902 + window.addEventListener('resize', this.redraw.bind(this)); + + // Listen to pref change. + if (cr.isChromeOS) { + Preferences.getInstance().addEventListener(this.preferredLanguagesPref, + this.handlePreferredLanguagesPrefChange_.bind(this)); + } else { + Preferences.getInstance().addEventListener(this.acceptLanguagesPref, + this.handleAcceptLanguagesPrefChange_.bind(this)); + } + + // Listen to drag and drop events. + this.addEventListener('dragstart', this.handleDragStart_.bind(this)); + this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); + this.addEventListener('dragover', this.handleDragOver_.bind(this)); + this.addEventListener('drop', this.handleDrop_.bind(this)); + this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); + }, + + createItem: function(languageCode) { + return new LanguageListItem(languageCode); + }, + + /* + * For each item, determines whether it's deletable. + */ + updateDeletable: function() { + var items = this.items; + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + var languageCode = item.languageCode; + var languageOptions = options.LanguageOptions.getInstance(); + item.deletable = languageOptions.languageIsDeletable(languageCode); + } + }, + + /* + * Adds a language to the language list. + * @param {string} languageCode language code (ex. "fr"). + */ + addLanguage: function(languageCode) { + // It shouldn't happen but ignore the language code if it's + // null/undefined, or already present. + if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) { + return; + } + this.dataModel.push(languageCode); + // Select the last item, which is the language added. + this.selectionModel.selectedIndex = this.dataModel.length - 1; + + this.savePreference_(); + }, + + /* + * Gets the language codes of the currently listed languages. + */ + getLanguageCodes: function() { + return this.dataModel.slice(); + }, + + /* + * Gets the language code of the selected language. + */ + getSelectedLanguageCode: function() { + return this.selectedItem; + }, + + /* + * Selects the language by the given language code. + * @returns {boolean} True if the operation is successful. + */ + selectLanguageByCode: function(languageCode) { + var index = this.dataModel.indexOf(languageCode); + if (index >= 0) { + this.selectionModel.selectedIndex = index; + return true; + } + return false; + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + if (index >= 0) { + this.dataModel.splice(index, 1); + // Once the selected item is removed, there will be no selected item. + // Select the item pointed by the lead index. + index = this.selectionModel.leadIndex; + this.savePreference_(); + } + return index; + }, + + /* + * Computes the target item of drop event. + * @param {Event} e The drop or dragover event. + * @private + */ + getTargetFromDropEvent_ : function(e) { + var target = e.target; + // e.target may be an inner element of the list item + while (target != null && !(target instanceof ListItem)) { + target = target.parentNode; + } + return target; + }, + + /* + * Handles the dragstart event. + * @param {Event} e The dragstart event. + * @private + */ + handleDragStart_: function(e) { + var target = e.target; + // ListItem should be the only draggable element type in the page, + // but just in case. + if (target instanceof ListItem) { + this.draggedItem = target; + e.dataTransfer.effectAllowed = 'move'; + // We need to put some kind of data in the drag or it will be + // ignored. Use the display name in case the user drags to a text + // field or the desktop. + e.dataTransfer.setData('text/plain', target.title); + } + }, + + /* + * Handles the dragenter event. + * @param {Event} e The dragenter event. + * @private + */ + handleDragEnter_: function(e) { + e.preventDefault(); + }, + + /* + * Handles the dragover event. + * @param {Event} e The dragover event. + * @private + */ + handleDragOver_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + // Determines whether the drop target is to accept the drop. + // The drop is only successful on another ListItem. + if (!(dropTarget instanceof ListItem) || + dropTarget == this.draggedItem) { + this.hideDropMarker_(); + return; + } + // Compute the drop postion. Should we move the dragged item to + // below or above the drop target? + var rect = dropTarget.getBoundingClientRect(); + var dy = e.clientY - rect.top; + var yRatio = dy / rect.height; + var dropPos = yRatio <= .5 ? 'above' : 'below'; + this.dropPos = dropPos; + this.showDropMarker_(dropTarget, dropPos); + e.preventDefault(); + }, + + /* + * Handles the drop event. + * @param {Event} e The drop event. + * @private + */ + handleDrop_: function(e) { + var dropTarget = this.getTargetFromDropEvent_(e); + this.hideDropMarker_(); + + // Delete the language from the original position. + var languageCode = this.draggedItem.languageCode; + var originalIndex = this.dataModel.indexOf(languageCode); + this.dataModel.splice(originalIndex, 1); + // Insert the language to the new position. + var newIndex = this.dataModel.indexOf(dropTarget.languageCode); + if (this.dropPos == 'below') + newIndex += 1; + this.dataModel.splice(newIndex, 0, languageCode); + // The cursor should move to the moved item. + this.selectionModel.selectedIndex = newIndex; + // Save the preference. + this.savePreference_(); + }, + + /* + * Handles the dragleave event. + * @param {Event} e The dragleave event + * @private + */ + handleDragLeave_ : function(e) { + this.hideDropMarker_(); + }, + + /* + * Shows and positions the marker to indicate the drop target. + * @param {HTMLElement} target The current target list item of drop + * @param {string} pos 'below' or 'above' + * @private + */ + showDropMarker_ : function(target, pos) { + window.clearTimeout(this.hideDropMarkerTimer_); + var marker = $('language-options-list-dropmarker'); + var rect = target.getBoundingClientRect(); + var markerHeight = 8; + if (pos == 'above') { + marker.style.top = (rect.top - markerHeight/2) + 'px'; + } else { + marker.style.top = (rect.bottom - markerHeight/2) + 'px'; + } + marker.style.width = rect.width + 'px'; + marker.style.left = rect.left + 'px'; + marker.style.display = 'block'; + }, + + /* + * Hides the drop marker. + * @private + */ + hideDropMarker_ : function() { + // Hide the marker in a timeout to reduce flickering as we move between + // valid drop targets. + window.clearTimeout(this.hideDropMarkerTimer_); + this.hideDropMarkerTimer_ = window.setTimeout(function() { + $('language-options-list-dropmarker').style.display = ''; + }, 100); + }, + + /** + * Handles preferred languages pref change. + * @param {Event} e The change event object. + * @private + */ + handlePreferredLanguagesPrefChange_: function(e) { + var languageCodesInCsv = e.value.value; + var languageCodes = languageCodesInCsv.split(','); + + // Add the UI language to the initial list of languages. This is to avoid + // a bug where the UI language would be removed from the preferred + // language list by sync on first login. + // See: crosbug.com/14283 + languageCodes.push(navigator.language); + languageCodes = this.filterBadLanguageCodes_(languageCodes); + this.load_(languageCodes); + }, + + /** + * Handles accept languages pref change. + * @param {Event} e The change event object. + * @private + */ + handleAcceptLanguagesPrefChange_: function(e) { + var languageCodesInCsv = e.value.value; + var languageCodes = this.filterBadLanguageCodes_( + languageCodesInCsv.split(',')); + this.load_(languageCodes); + }, + + /** + * Loads given language list. + * @param {Array} languageCodes List of language codes. + * @private + */ + load_: function(languageCodes) { + // Preserve the original selected index. See comments below. + var originalSelectedIndex = (this.selectionModel ? + this.selectionModel.selectedIndex : -1); + this.dataModel = new ArrayDataModel(languageCodes); + if (originalSelectedIndex >= 0 && + originalSelectedIndex < this.dataModel.length) { + // Restore the original selected index if the selected index is + // valid after the data model is loaded. This is neeeded to keep + // the selected language after the languge is added or removed. + this.selectionModel.selectedIndex = originalSelectedIndex; + // The lead index should be updated too. + this.selectionModel.leadIndex = originalSelectedIndex; + } else if (this.dataModel.length > 0){ + // Otherwise, select the first item if it's not empty. + // Note that ListSingleSelectionModel won't select an item + // automatically, hence we manually select the first item here. + this.selectionModel.selectedIndex = 0; + } + }, + + /** + * Saves the preference. + */ + savePreference_: function() { + // Encode the language codes into a CSV string. + if (cr.isChromeOS) + Preferences.setStringPref(this.preferredLanguagesPref, + this.dataModel.slice().join(',')); + // Save the same language list as accept languages preference as + // well, but we need to expand the language list, to make it more + // acceptable. For instance, some web sites don't understand 'en-US' + // but 'en'. See crosbug.com/9884. + var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice()); + Preferences.setStringPref(this.acceptLanguagesPref, + acceptLanguages.join(',')); + cr.dispatchSimpleEvent(this, 'save'); + }, + + /** + * Expands language codes to make these more suitable for Accept-Language. + * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA']. + * 'en' won't appear twice as this function eliminates duplicates. + * @param {Array} languageCodes List of language codes. + * @private + */ + expandLanguageCodes: function(languageCodes) { + var expandedLanguageCodes = []; + var seen = {}; // Used to eliminiate duplicates. + for (var i = 0; i < languageCodes.length; i++) { + var languageCode = languageCodes[i]; + if (!(languageCode in seen)) { + expandedLanguageCodes.push(languageCode); + seen[languageCode] = true; + } + var parts = languageCode.split('-'); + if (!(parts[0] in seen)) { + expandedLanguageCodes.push(parts[0]); + seen[parts[0]] = true; + } + } + return expandedLanguageCodes; + }, + + /** + * Filters bad language codes in case bad language codes are + * stored in the preference. Removes duplicates as well. + * @param {Array} languageCodes List of language codes. + * @private + */ + filterBadLanguageCodes_: function(languageCodes) { + var filteredLanguageCodes = []; + var seen = {}; + for (var i = 0; i < languageCodes.length; i++) { + // Check if the the language code is valid, and not + // duplicate. Otherwise, skip it. + if (LanguageList.isValidLanguageCode(languageCodes[i]) && + !(languageCodes[i] in seen)) { + filteredLanguageCodes.push(languageCodes[i]); + seen[languageCodes[i]] = true; + } + } + return filteredLanguageCodes; + }, + }; + + return { + LanguageList: LanguageList, + LanguageListItem: LanguageListItem + }; +}); diff --git a/chrome/browser/resources/options2/language_options.css b/chrome/browser/resources/options2/language_options.css new file mode 100644 index 0000000..55b7aa8 --- /dev/null +++ b/chrome/browser/resources/options2/language_options.css @@ -0,0 +1,239 @@ +.language-options { + display: -webkit-box; + margin: 10px 0; +} + +.language-options-lower-left button, +.language-options-right button { + min-width: 70px; +} + +.language-options h3 { + -webkit-margin-start: 12px; + font-size: 100%; + font-weight: bold; + margin-top: 12px; +} + +.language-options-contents { + -webkit-padding-start: 12px; + -webkit-padding-end: 12px; + padding-bottom: 10px; +} + +.language-options-header, .language-options-footer { + margin: 10px 0; +} + +.language-options-left, .language-options-right { + border: 1px solid #cccccc; + vertical-align: top; + padding: 0; + height: 400px; +} + +.language-options-left { + -webkit-box-orient: vertical; + display: -webkit-box; + background-color: #ebeff9; + width: 300px; +} + +/* On OS X we use the native OS spellchecker, so don't display the dictionary */ +/* pane. */ +html[os=mac] .language-options-left { + background-color: white; +} + +html[os=mac] .language-options-right { + visibility: hidden; +} + +.language-options-lower-left { + -webkit-box-flex: 0; + -webkit-padding-start: 12px; + padding-bottom: 10px; +} + +.language-options-right { + /* To share the center line with the left pane. */ + -webkit-margin-start: -1px; + width: 360px; +} + +.language-options-notification { + display: none; + background-color: #fff29e; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + padding: 12px 30px 12px 12px; +} + +#language-options-input-method-list button { + display: block; + -webkit-margin-start: 20px; +} + +#language-options-ui-language-button { + width: 95%; + -webkit-margin-start: 10px; +} + +#language-options-spell-check-language-button { + width: 95%; + -webkit-margin-start: 10px; +} + +#language-options-input-method-list label { + margin: 4px 0; +} + +#language-options-list { + -webkit-box-flex: 1; + outline: none; + padding: 1px 0 0; + width: 100%; +} + +#language-options-list .language-name { + -webkit-box-flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#language-options-list li { + -webkit-padding-start: 12px; + padding-top: 2px; + padding-bottom: 2px; +} + +#language-options-list-dropmarker { + background-color: hsl(214, 91%, 65%); + background-clip: padding-box; + border: 3px solid hsl(214, 91%, 65%); + border-bottom-color: transparent; + border-top-color: transparent; + border-radius: 0; + box-sizing: border-box; + display: none; + height: 8px; + overflow: hidden; + pointer-events: none; + position: fixed; + z-index: 10; +} + +#language-options-ui-restart-button { + margin-top: 12px; +} + +/* + * In ChromeOS we present the language choices as a big page of links. + */ + +html[os=chromeos] #add-language-overlay-language-list { + -webkit-column-count: 2; + -webkit-column-gap: 20px; +} + +html[os=chromeos] #add-language-overlay-cancel-button { + /* Place the button in the center. */ + display: block; + margin: auto; + margin-top: 15px; +} + +html[os=chromeos] #add-language-overlay-page { + width: 800px; + padding: 20px; +} + +html[os=chromeos] #add-language-overlay-page button.link-button { + padding: 0; + text-align: left; +} + +html[os=chromeos] #add-language-overlay-page ul { + padding: 0; + margin: 0; +} + +/* TODO(kochi): This is temporary copy from new_tab.css */ +/* Notification */ + +#notification { + position: relative; + background-color: hsl(52, 100%, 80%); + border: 1px solid rgb(211, 211, 211); + border-radius: 6px; + padding: 7px 15px; + white-space: nowrap; + display: table; + /* Set the height and margin so that the element does not use any vertical + space */ + height: 16px; + margin: -44px auto 12px auto; + font-weight: bold; + opacity: 0; + pointer-events: none; + -webkit-transition: opacity 150ms; + z-index: 1; + color: black; +} + +#notification > * { + display: table-cell; + max-width: 500px; + overflow: hidden; + text-overflow: ellipsis; +} + +#notification.show { + opacity: 1; + pointer-events: all; + -webkit-transition: opacity 1s; +} + +#notification .link { + cursor: pointer; + text-decoration: underline; + -webkit-appearance: none; + border: 0; + background: none; + color: rgba(0, 102, 204, 0.3); + -webkit-padding-start: 20px; +} + +#notification .link-color { + color: rgb(0, 102, 204); +} + +#chewing-max-chi-symbol-len { + width: 100px; + height: 30%; +} + +#add-language-overlay-page .content-area { + padding-bottom: 10px; +} + +.text-button, +.text-button:active, +.text-button:focus, +.text-button:hover { + -webkit-box-shadow: none; + background: transparent none; + border-color: transparent; + color: #000; +} + +button[disabled].text-button, +button[disabled].text-button:active, +button[disabled].text-button:focus, +button[disabled].text-button:hover { + -webkit-box-shadow: none; + background: transparent none; + border-color: transparent; + color: #AAA; +} diff --git a/chrome/browser/resources/options2/language_options.html b/chrome/browser/resources/options2/language_options.html new file mode 100644 index 0000000..46373c1 --- /dev/null +++ b/chrome/browser/resources/options2/language_options.html @@ -0,0 +1,78 @@ +<div id="languagePage" class="page" hidden> + <h1 i18n-content="languagePage"></h1> + <div id="notification"> + <span> </span> + <span class="link"><span class="link-color"></span></span> + </div> + <div class="language-options-header"> + <div i18n-content="add_language_instructions"></div> +<if expr="pp_ifdef('chromeos')"> + <div i18n-content="input_method_instructions"></div> +</if> + </div> + <div class="language-options"> + <div class="language-options-left"> + <h3 i18n-content="languages"></h3> + <list id="language-options-list"></list> + <div class="language-options-lower-left"> + <button id="language-options-add-button" + i18n-content="add_button"></button> + </div> + <div id="language-options-list-dropmarker"></div> + </div> + <div class="language-options-right"> + <h3 id="language-options-language-name"></h3> +<if expr="os == 'win32' or pp_ifdef('chromeos')"> + <div class="language-options-contents"> + <button id="language-options-ui-language-button"></button> + </div> +</if> + <div class="language-options-contents"> + <button id="language-options-spell-check-language-button"></button> + </div> + <div id="language-options-ui-notification-bar" + class="language-options-notification"> + <div i18n-content="restart_required"></div> +<if expr="pp_ifdef('chromeos')"> + <button id="language-options-ui-restart-button" + i18n-content="restart_button"></button> +</if> + </div> +<if expr="pp_ifdef('chromeos')"> + <h3 i18n-content="input_method"></h3> + <div id="language-options-input-method-list" + class="language-options-contents"> + </div> +</if> +<if expr="pp_ifdef('chromeos') and pp_ifdef('use_virtual_keyboard')"> + <div class="language-options-contents"> + <button id="language-options-virtual-keyboard" + i18n-content="virtual_keyboard_button"></button> + </div> +</if> + </div> + </div> + <div class="language-options-footer"> +<if expr="pp_ifdef('chromeos')"> + <div i18n-content="switch_input_methods_hint"></div> + <div i18n-content="select_previous_input_method_hint"></div> +</if> +<if expr="not pp_ifdef('chromeos') and not is_macosx"> + <div id="spell-check-option" class="checkbox"> + <label> + <input id="enable-spell-check" pref="browser.enable_spellchecking" + metric="Options_SpellCheck" type="checkbox"> + <span i18n-content="enable_spell_check"></span> + </label> + </div> + <div id="auto-spell-correction-option" class="checkbox" hidden> + <label> + <input id="enable-auto-spell-correction" + pref="browser.enable_autospellcorrect" + metric="Options_AutoSpellCorrection" type="checkbox"> + <span i18n-content="enable_auto_spell_correction"></span> + </label> + </div> +</if> + </div> +</div> diff --git a/chrome/browser/resources/options2/language_options.js b/chrome/browser/resources/options2/language_options.js new file mode 100644 index 0000000..47674ed --- /dev/null +++ b/chrome/browser/resources/options2/language_options.js @@ -0,0 +1,815 @@ +// 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. + +// TODO(kochi): Generalize the notification as a component and put it +// in js/cr/ui/notification.js . + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const LanguageList = options.LanguageList; + + // Some input methods like Chinese Pinyin have config pages. + // This is the map of the input method names to their config page names. + const INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME = { + 'mozc': 'languageMozc', + 'mozc-chewing': 'languageChewing', + 'mozc-dv': 'languageMozc', + 'mozc-hangul': 'languageHangul', + 'mozc-jp': 'languageMozc', + 'pinyin': 'languagePinyin', + 'pinyin-dv': 'languagePinyin', + }; + + ///////////////////////////////////////////////////////////////////////////// + // LanguageOptions class: + + /** + * Encapsulated handling of ChromeOS language options page. + * @constructor + */ + function LanguageOptions(model) { + OptionsPage.call(this, 'languages', templateData.languagePageTabTitle, + 'languagePage'); + } + + cr.addSingletonGetter(LanguageOptions); + + // Inherit LanguageOptions from OptionsPage. + LanguageOptions.prototype = { + __proto__: OptionsPage.prototype, + + /** + * Initializes LanguageOptions page. + * Calls base class implementation to starts preference initialization. + */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + var languageOptionsList = $('language-options-list'); + LanguageList.decorate(languageOptionsList); + + languageOptionsList.addEventListener('change', + this.handleLanguageOptionsListChange_.bind(this)); + languageOptionsList.addEventListener('save', + this.handleLanguageOptionsListSave_.bind(this)); + + this.addEventListener('visibleChange', + this.handleVisibleChange_.bind(this)); + + if (cr.isChromeOS) { + this.initializeInputMethodList_(); + this.initializeLanguageCodeToInputMethodIdsMap_(); + } + Preferences.getInstance().addEventListener(this.spellCheckDictionaryPref, + this.handleSpellCheckDictionaryPrefChange_.bind(this)); + + // Set up add button. + $('language-options-add-button').onclick = function(e) { + // Add the language without showing the overlay if it's specified in + // the URL hash (ex. lang_add=ja). Used for automated testing. + var match = document.location.hash.match(/\blang_add=([\w-]+)/); + if (match) { + var addLanguageCode = match[1]; + $('language-options-list').addLanguage(addLanguageCode); + } else { + OptionsPage.navigateToPage('addLanguage'); + } + }; + + if (cr.isChromeOS) { + // Listen to user clicks on the add language list. + var addLanguageList = $('add-language-overlay-language-list'); + addLanguageList.addEventListener('click', + this.handleAddLanguageListClick_.bind(this)); + } else { + // Listen to add language dialog ok button. + var addLanguageOkButton = $('add-language-overlay-ok-button'); + addLanguageOkButton.addEventListener('click', + this.handleAddLanguageOkButtonClick_.bind(this)); + + // Show experimental features if enabled. + if (templateData.experimentalSpellCheckFeatures == 'true') + $('auto-spell-correction-option').hidden = false; + + // Handle spell check enable/disable. + if (!cr.isMac) { + Preferences.getInstance().addEventListener( + this.enableSpellCheckPref, + this.updateEnableSpellCheck_.bind(this)); + } + } + + // Listen to user clicks on the "Change touch keyboard settings..." + // button (if it exists). + var virtualKeyboardButton = $('language-options-virtual-keyboard'); + if (virtualKeyboardButton) { + // TODO(yusukes): would be better to hide the button if no virtual + // keyboard is registered. + virtualKeyboardButton.onclick = function(e) { + OptionsPage.navigateToPage('virtualKeyboards'); + }; + } + }, + + // The preference is a boolean that enables/disables spell checking. + enableSpellCheckPref: 'browser.enable_spellchecking', + // The preference is a CSV string that describes preload engines + // (i.e. active input methods). + preloadEnginesPref: 'settings.language.preload_engines', + // The list of preload engines, like ['mozc', 'pinyin']. + preloadEngines_: [], + // The preference is a string that describes the spell check + // dictionary language, like "en-US". + spellCheckDictionaryPref: 'spellcheck.dictionary', + spellCheckDictionary_: "", + // The map of language code to input method IDs, like: + // {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...} + languageCodeToInputMethodIdsMap_: {}, + + /** + * Initializes the input method list. + */ + initializeInputMethodList_: function() { + var inputMethodList = $('language-options-input-method-list'); + var inputMethodListData = templateData.inputMethodList; + + // Add all input methods, but make all of them invisible here. We'll + // change the visibility in handleLanguageOptionsListChange_() based + // on the selected language. Note that we only have less than 100 + // input methods, so creating DOM nodes at once here should be ok. + for (var i = 0; i < inputMethodListData.length; i++) { + var inputMethod = inputMethodListData[i]; + var input = document.createElement('input'); + input.type = 'checkbox'; + input.inputMethodId = inputMethod.id; + // Listen to user clicks. + input.addEventListener('click', + this.handleCheckboxClick_.bind(this)); + var label = document.createElement('label'); + label.appendChild(input); + // Adding a space between the checkbox and the text. This is a bit + // dirty, but we rely on a space character for all other checkboxes. + label.appendChild(document.createTextNode( + ' ' + inputMethod.displayName)); + label.style.display = 'none'; + label.languageCodeSet = inputMethod.languageCodeSet; + // Add the configure button if the config page is present for this + // input method. + if (inputMethod.id in INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME) { + var pageName = INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME[inputMethod.id]; + var button = this.createConfigureInputMethodButton_(inputMethod.id, + pageName); + label.appendChild(button); + } + + inputMethodList.appendChild(label); + } + // Listen to pref change once the input method list is initialized. + Preferences.getInstance().addEventListener(this.preloadEnginesPref, + this.handlePreloadEnginesPrefChange_.bind(this)); + }, + + /** + * Creates a configure button for the given input method ID. + * @param {string} inputMethodId Input method ID (ex. "pinyin"). + * @param {string} pageName Name of the config page (ex. "languagePinyin"). + * @private + */ + createConfigureInputMethodButton_: function(inputMethodId, pageName) { + var button = document.createElement('button'); + button.textContent = localStrings.getString('configure'); + button.onclick = function(e) { + // Prevent the default action (i.e. changing the checked property + // of the checkbox). The button click here should not be handled + // as checkbox click. + e.preventDefault(); + chrome.send('inputMethodOptionsOpen', [inputMethodId]); + OptionsPage.navigateToPage(pageName); + } + return button; + }, + + /** + * Handles OptionsPage's visible property change event. + * @param {Event} e Property change event. + * @private + */ + handleVisibleChange_: function(e) { + if (this.visible) { + $('language-options-list').redraw(); + chrome.send('languageOptionsOpen'); + } + }, + + /** + * Handles languageOptionsList's change event. + * @param {Event} e Change event. + * @private + */ + handleLanguageOptionsListChange_: function(e) { + var languageOptionsList = $('language-options-list'); + var languageCode = languageOptionsList.getSelectedLanguageCode(); + // Select the language if it's specified in the URL hash (ex. lang=ja). + // Used for automated testing. + var match = document.location.hash.match(/\blang=([\w-]+)/); + if (match) { + var specifiedLanguageCode = match[1]; + if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) { + languageCode = specifiedLanguageCode; + } + } + this.updateSelectedLanguageName_(languageCode); + if (cr.isWindows || cr.isChromeOS) + this.updateUiLanguageButton_(languageCode); + if (!cr.isMac) + this.updateSpellCheckLanguageButton_(languageCode); + if (cr.isChromeOS) + this.updateInputMethodList_(languageCode); + this.updateLanguageListInAddLanguageOverlay_(); + }, + + /** + * Handles languageOptionsList's save event. + * @param {Event} e Save event. + * @private + */ + handleLanguageOptionsListSave_: function(e) { + if (cr.isChromeOS) { + // Sort the preload engines per the saved languages before save. + this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); + this.savePreloadEnginesPref_(); + } + }, + + /** + * Sorts preloadEngines_ by languageOptionsList's order. + * @param {Array} preloadEngines List of preload engines. + * @return {Array} Returns sorted preloadEngines. + * @private + */ + sortPreloadEngines_: function(preloadEngines) { + // For instance, suppose we have two languages and associated input + // methods: + // + // - Korean: hangul + // - Chinese: pinyin + // + // The preloadEngines preference should look like "hangul,pinyin". + // If the user reverse the order, the preference should be reorderd + // to "pinyin,hangul". + var languageOptionsList = $('language-options-list'); + var languageCodes = languageOptionsList.getLanguageCodes(); + + // Convert the list into a dictonary for simpler lookup. + var preloadEngineSet = {}; + for (var i = 0; i < preloadEngines.length; i++) { + preloadEngineSet[preloadEngines[i]] = true; + } + + // Create the new preload engine list per the language codes. + var newPreloadEngines = []; + for (var i = 0; i < languageCodes.length; i++) { + var languageCode = languageCodes[i]; + var inputMethodIds = this.languageCodeToInputMethodIdsMap_[ + languageCode]; + // Check if we have active input methods associated with the language. + for (var j = 0; j < inputMethodIds.length; j++) { + var inputMethodId = inputMethodIds[j]; + if (inputMethodId in preloadEngineSet) { + // If we have, add it to the new engine list. + newPreloadEngines.push(inputMethodId); + // And delete it from the set. This is necessary as one input + // method can be associated with more than one language thus + // we should avoid having duplicates in the new list. + delete preloadEngineSet[inputMethodId]; + } + } + } + + return newPreloadEngines; + }, + + /** + * Initializes the map of language code to input method IDs. + * @private + */ + initializeLanguageCodeToInputMethodIdsMap_: function() { + var inputMethodList = templateData.inputMethodList; + for (var i = 0; i < inputMethodList.length; i++) { + var inputMethod = inputMethodList[i]; + for (var languageCode in inputMethod.languageCodeSet) { + if (languageCode in this.languageCodeToInputMethodIdsMap_) { + this.languageCodeToInputMethodIdsMap_[languageCode].push( + inputMethod.id); + } else { + this.languageCodeToInputMethodIdsMap_[languageCode] = + [inputMethod.id]; + } + } + } + }, + + /** + * Updates the currently selected language name. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateSelectedLanguageName_: function(languageCode) { + var languageDisplayName = LanguageList.getDisplayNameFromLanguageCode( + languageCode); + var languageNativeDisplayName = + LanguageList.getNativeDisplayNameFromLanguageCode(languageCode); + // If the native name is different, add it. + if (languageDisplayName != languageNativeDisplayName) { + languageDisplayName += ' - ' + languageNativeDisplayName; + } + // Update the currently selected language name. + var languageName = $('language-options-language-name'); + if (languageDisplayName) { + languageName.hidden = false; + languageName.textContent = languageDisplayName; + } else { + languageName.hidden = true; + } + }, + + /** + * Updates the UI language button. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateUiLanguageButton_: function(languageCode) { + var uiLanguageButton = $('language-options-ui-language-button'); + // Check if the language code matches the current UI language. + if (languageCode == templateData.currentUiLanguageCode) { + // If it matches, the button just says that the UI language is + // currently in use. + uiLanguageButton.textContent = + localStrings.getString('is_displayed_in_this_language'); + // Make it look like a text label. + uiLanguageButton.className = 'text-button'; + // Remove the event listner. + uiLanguageButton.onclick = undefined; + } else if (languageCode in templateData.uiLanguageCodeSet) { + // If the language is supported as UI language, users can click on + // the button to change the UI language. + if (cr.commandLine && cr.commandLine.options['--bwsi']) { + // In the guest mode for ChromeOS, changing UI language does not make + // sense because it does not take effect after browser restart. + uiLanguageButton.hidden = true; + } else { + uiLanguageButton.textContent = + localStrings.getString('display_in_this_language'); + uiLanguageButton.className = ''; + // Send the change request to Chrome. + uiLanguageButton.onclick = function(e) { + chrome.send('uiLanguageChange', [languageCode]); + } + } + if (cr.isChromeOS) { + $('language-options-ui-restart-button').onclick = function(e) { + chrome.send('uiLanguageRestart'); + } + } + } else { + // If the language is not supported as UI language, the button + // just says that Chromium OS cannot be displayed in this language. + uiLanguageButton.textContent = + localStrings.getString('cannot_be_displayed_in_this_language'); + uiLanguageButton.className = 'text-button'; + uiLanguageButton.onclick = undefined; + } + uiLanguageButton.style.display = 'block'; + $('language-options-ui-notification-bar').style.display = 'none'; + }, + + /** + * Updates the spell check language button. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateSpellCheckLanguageButton_: function(languageCode) { + var display = 'block'; + var spellCheckLanguageButton = $( + 'language-options-spell-check-language-button'); + // Check if the language code matches the current spell check language. + if (languageCode == this.spellCheckDictionary_) { + // If it matches, the button just says that the spell check language is + // currently in use. + spellCheckLanguageButton.textContent = + localStrings.getString('is_used_for_spell_checking'); + // Make it look like a text label. + spellCheckLanguageButton.className = 'text-button'; + // Remove the event listner. + spellCheckLanguageButton.onclick = undefined; + } else if (languageCode in templateData.spellCheckLanguageCodeSet) { + // If the language is supported as spell check language, users can + // click on the button to change the spell check language. + spellCheckLanguageButton.textContent = + localStrings.getString('use_this_for_spell_checking'); + spellCheckLanguageButton.className = ''; + spellCheckLanguageButton.languageCode = languageCode; + // Add an event listner to the click event. + spellCheckLanguageButton.addEventListener('click', + this.handleSpellCheckLanguageButtonClick_.bind(this)); + } else if (!languageCode) { + display = 'none'; + } else { + // If the language is not supported as spell check language, the + // button just says that this language cannot be used for spell + // checking. + spellCheckLanguageButton.textContent = + localStrings.getString('cannot_be_used_for_spell_checking'); + spellCheckLanguageButton.className = 'text-button'; + spellCheckLanguageButton.onclick = undefined; + } + spellCheckLanguageButton.style.display = display; + $('language-options-ui-notification-bar').style.display = 'none'; + }, + + /** + * Updates the input method list. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateInputMethodList_: function(languageCode) { + // Give one of the checkboxes or buttons focus, if it's specified in the + // URL hash (ex. focus=mozc). Used for automated testing. + var focusInputMethodId = -1; + var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/); + if (match) { + focusInputMethodId = match[1]; + } + // Change the visibility of the input method list. Input methods that + // matches |languageCode| will become visible. + var inputMethodList = $('language-options-input-method-list'); + var labels = inputMethodList.querySelectorAll('label'); + for (var i = 0; i < labels.length; i++) { + var label = labels[i]; + if (languageCode in label.languageCodeSet) { + label.style.display = 'block'; + var input = label.childNodes[0]; + // Give it focus if the ID matches. + if (input.inputMethodId == focusInputMethodId) { + input.focus(); + } + } else { + label.style.display = 'none'; + } + } + + if (focusInputMethodId == 'add') { + $('language-options-add-button').focus(); + } + }, + + /** + * Updates the language list in the add language overlay. + * @param {string} languageCode Language code (ex. "fr"). + * @private + */ + updateLanguageListInAddLanguageOverlay_: function(languageCode) { + // Change the visibility of the language list in the add language + // overlay. Languages that are already active will become invisible, + // so that users don't add the same language twice. + var languageOptionsList = $('language-options-list'); + var languageCodes = languageOptionsList.getLanguageCodes(); + var languageCodeSet = {}; + for (var i = 0; i < languageCodes.length; i++) { + languageCodeSet[languageCodes[i]] = true; + } + var addLanguageList = $('add-language-overlay-language-list'); + var lis = addLanguageList.querySelectorAll('li'); + for (var i = 0; i < lis.length; i++) { + // The first child button knows the language code. + var button = lis[i].childNodes[0]; + if (button.languageCode in languageCodeSet) { + lis[i].style.display = 'none'; + } else { + lis[i].style.display = 'block'; + } + } + }, + + /** + * Handles preloadEnginesPref change. + * @param {Event} e Change event. + * @private + */ + handlePreloadEnginesPrefChange_: function(e) { + var value = e.value.value; + this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(',')); + this.updateCheckboxesFromPreloadEngines_(); + $('language-options-list').updateDeletable(); + }, + + /** + * Handles input method checkbox's click event. + * @param {Event} e Click event. + * @private + */ + handleCheckboxClick_ : function(e) { + var checkbox = e.target; + if (this.preloadEngines_.length == 1 && !checkbox.checked) { + // Don't allow disabling the last input method. + this.showNotification_( + localStrings.getString('please_add_another_input_method'), + localStrings.getString('ok_button')); + checkbox.checked = true; + return; + } + if (checkbox.checked) { + chrome.send('inputMethodEnable', [checkbox.inputMethodId]); + } else { + chrome.send('inputMethodDisable', [checkbox.inputMethodId]); + } + this.updatePreloadEnginesFromCheckboxes_(); + this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); + this.savePreloadEnginesPref_(); + }, + + /** + * Handles add language list's click event. + * @param {Event} e Click event. + */ + handleAddLanguageListClick_ : function(e) { + var languageOptionsList = $('language-options-list'); + var languageCode = e.target.languageCode; + // languageCode can be undefined, if click was made on some random + // place in the overlay, rather than a button. Ignore it. + if (!languageCode) { + return; + } + languageOptionsList.addLanguage(languageCode); + var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; + // Enable the first input method for the language added. + if (inputMethodIds && inputMethodIds[0] && + // Don't add the input method it's already present. This can + // happen if the same input method is shared among multiple + // languages (ex. English US keyboard is used for English US and + // Filipino). + this.preloadEngines_.indexOf(inputMethodIds[0]) == -1) { + this.preloadEngines_.push(inputMethodIds[0]); + this.updateCheckboxesFromPreloadEngines_(); + this.savePreloadEnginesPref_(); + } + OptionsPage.closeOverlay(); + }, + + /** + * Handles add language dialog ok button. + */ + handleAddLanguageOkButtonClick_ : function() { + var languagesSelect = $('add-language-overlay-language-list'); + var selectedIndex = languagesSelect.selectedIndex; + if (selectedIndex >= 0) { + var selection = languagesSelect.options[selectedIndex]; + $('language-options-list').addLanguage(String(selection.value)); + OptionsPage.closeOverlay(); + } + }, + + /** + * Checks if languageCode is deletable or not. + * @param {String} languageCode the languageCode to check for deletability. + */ + languageIsDeletable: function(languageCode) { + // Don't allow removing the language if it's as UI language. + if (languageCode == templateData.currentUiLanguageCode) + return false; + return (!cr.isChromeOS || + this.canDeleteLanguage_(languageCode)); + }, + + /** + * Handles browse.enable_spellchecking change. + * @param {Event} e Change event. + * @private + */ + updateEnableSpellCheck_: function() { + var value = !$('enable-spell-check').checked; + + $('language-options-spell-check-language-button').disabled = value; + }, + + /** + * Handles spellCheckDictionaryPref change. + * @param {Event} e Change event. + * @private + */ + handleSpellCheckDictionaryPrefChange_: function(e) { + var languageCode = e.value.value + this.spellCheckDictionary_ = languageCode; + var languageOptionsList = $('language-options-list'); + var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); + this.updateSpellCheckLanguageButton_(selectedLanguageCode); + }, + + /** + * Handles spellCheckLanguageButton click. + * @param {Event} e Click event. + * @private + */ + handleSpellCheckLanguageButtonClick_: function(e) { + var languageCode = e.target.languageCode; + // Save the preference. + Preferences.setStringPref(this.spellCheckDictionaryPref, + languageCode); + chrome.send('spellCheckLanguageChange', [languageCode]); + }, + + /** + * Checks whether it's possible to remove the language specified by + * languageCode and returns true if possible. This function returns false + * if the removal causes the number of preload engines to be zero. + * + * @param {string} languageCode Language code (ex. "fr"). + * @return {boolean} Returns true on success. + * @private + */ + canDeleteLanguage_: function(languageCode) { + // First create the set of engines to be removed from input methods + // associated with the language code. + var enginesToBeRemovedSet = {}; + var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; + for (var i = 0; i < inputMethodIds.length; i++) { + enginesToBeRemovedSet[inputMethodIds[i]] = true; + } + + // Then eliminate engines that are also used for other active languages. + // For instance, if "xkb:us::eng" is used for both English and Filipino. + var languageCodes = $('language-options-list').getLanguageCodes(); + for (var i = 0; i < languageCodes.length; i++) { + // Skip the target language code. + if (languageCodes[i] == languageCode) { + continue; + } + // Check if input methods used in this language are included in + // enginesToBeRemovedSet. If so, eliminate these from the set, so + // we don't remove this time. + var inputMethodIdsForAnotherLanguage = + this.languageCodeToInputMethodIdsMap_[languageCodes[i]]; + for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) { + var inputMethodId = inputMethodIdsForAnotherLanguage[j]; + if (inputMethodId in enginesToBeRemovedSet) { + delete enginesToBeRemovedSet[inputMethodId]; + } + } + } + + // Update the preload engine list with the to-be-removed set. + var newPreloadEngines = []; + for (var i = 0; i < this.preloadEngines_.length; i++) { + if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) { + newPreloadEngines.push(this.preloadEngines_[i]); + } + } + // Don't allow this operation if it causes the number of preload + // engines to be zero. + return (newPreloadEngines.length > 0); + }, + + /** + * Saves the preload engines preference. + * @private + */ + savePreloadEnginesPref_: function() { + Preferences.setStringPref(this.preloadEnginesPref, + this.preloadEngines_.join(',')); + }, + + /** + * Updates the checkboxes in the input method list from the preload + * engines preference. + * @private + */ + updateCheckboxesFromPreloadEngines_: function() { + // Convert the list into a dictonary for simpler lookup. + var dictionary = {}; + for (var i = 0; i < this.preloadEngines_.length; i++) { + dictionary[this.preloadEngines_[i]] = true; + } + + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); + } + }, + + /** + * Updates the preload engines preference from the checkboxes in the + * input method list. + * @private + */ + updatePreloadEnginesFromCheckboxes_: function() { + this.preloadEngines_ = []; + var inputMethodList = $('language-options-input-method-list'); + var checkboxes = inputMethodList.querySelectorAll('input'); + for (var i = 0; i < checkboxes.length; i++) { + if (checkboxes[i].checked) { + this.preloadEngines_.push(checkboxes[i].inputMethodId); + } + } + var languageOptionsList = $('language-options-list'); + languageOptionsList.updateDeletable(); + }, + + /** + * Filters bad preload engines in case bad preload engines are + * stored in the preference. Removes duplicates as well. + * @param {Array} preloadEngines List of preload engines. + * @private + */ + filterBadPreloadEngines_: function(preloadEngines) { + // Convert the list into a dictonary for simpler lookup. + var dictionary = {}; + for (var i = 0; i < templateData.inputMethodList.length; i++) { + dictionary[templateData.inputMethodList[i].id] = true; + } + + var filteredPreloadEngines = []; + var seen = {}; + for (var i = 0; i < preloadEngines.length; i++) { + // Check if the preload engine is present in the + // dictionary, and not duplicate. Otherwise, skip it. + if (preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) { + filteredPreloadEngines.push(preloadEngines[i]); + seen[preloadEngines[i]] = true; + } + } + return filteredPreloadEngines; + }, + + // TODO(kochi): This is an adapted copy from new_tab.js. + // If this will go as final UI, refactor this to share the component with + // new new tab page. + /** + * Shows notification + * @private + */ + notificationTimeout_: null, + showNotification_ : function(text, actionText, opt_delay) { + var notificationElement = $('notification'); + var actionLink = notificationElement.querySelector('.link-color'); + var delay = opt_delay || 10000; + + function show() { + window.clearTimeout(this.notificationTimeout_); + notificationElement.classList.add('show'); + document.body.classList.add('notification-shown'); + } + + function hide() { + window.clearTimeout(this.notificationTimeout_); + notificationElement.classList.remove('show'); + document.body.classList.remove('notification-shown'); + // Prevent tabbing to the hidden link. + actionLink.tabIndex = -1; + // Setting tabIndex to -1 only prevents future tabbing to it. If, + // however, the user switches window or a tab and then moves back to + // this tab the element may gain focus. We therefore make sure that we + // blur the element so that the element focus is not restored when + // coming back to this window. + actionLink.blur(); + } + + function delayedHide() { + this.notificationTimeout_ = window.setTimeout(hide, delay); + } + + notificationElement.firstElementChild.textContent = text; + actionLink.textContent = actionText; + + actionLink.onclick = hide; + actionLink.onkeydown = function(e) { + if (e.keyIdentifier == 'Enter') { + hide(); + } + }; + notificationElement.onmouseover = show; + notificationElement.onmouseout = delayedHide; + actionLink.onfocus = show; + actionLink.onblur = delayedHide; + // Enable tabbing to the link now that it is shown. + actionLink.tabIndex = 0; + + show(); + delayedHide(); + } + }; + + /** + * Chrome callback for when the UI language preference is saved. + */ + LanguageOptions.uiLanguageSaved = function() { + $('language-options-ui-language-button').style.display = 'none'; + $('language-options-ui-notification-bar').style.display = 'block'; + }; + + // Export + return { + LanguageOptions: LanguageOptions + }; +}); diff --git a/chrome/browser/resources/options2/manage_profile_overlay.css b/chrome/browser/resources/options2/manage_profile_overlay.css new file mode 100644 index 0000000..aa17290 --- /dev/null +++ b/chrome/browser/resources/options2/manage_profile_overlay.css @@ -0,0 +1,88 @@ +/* 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. + */ + +#manage-profile-overlay { + width: 500px; +} + +.profile-icon-grid-item { + border: none !important; + height: 31px; + margin: 4px 6px; + padding: 4px; + width: 38px; +} + +.profile-icon { + height: 31px; + width: 38px; +} + +#manage-profile-content > :first-child { + margin-bottom: 5px; +} + +#manage-profile-content > :last-child { + margin-top: 5px; +} + +#manage-profile-content > :not(:first-child):not(:last-child) { + margin-top: 5px; + margin-bottom: 5px; +} + +#manage-profile-name-div { + -webkit-box-align: baseline; + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +#manage-profile-name-label { + -webkit-margin-end: 20px; +} + +#manage-profile-name { + -webkit-box-flex: 1; + display: block; +} + +#manage-profile-name:invalid { + background-color: pink; +} + +#manage-profile-error-bubble { + -webkit-transition: max-height 200ms, padding 200ms; + background-color: #eeb939; + border-radius: 4px; + font-weight: bold; + margin-left: auto; + margin-right: auto; + max-height: 50px; + overflow: hidden; + padding: 1px 10px; + text-align: center; + width: 80%; +} + +#manage-profile-error-bubble[hidden] { + display: block !important; + max-height: 0; + padding: 0 10px; +} + +#manage-profile-icon-grid { + background-color: rgba(255, 255, 255, 0.75); + border: 1px solid rgba(0, 0, 0, 0.3); + padding: 2px; +} + +#delete-profile-message { + background-repeat: no-repeat; + -webkit-padding-start: 48px; +} + +html[dir='rtl'] #delete-profile-message { + background-position: right; +}
\ No newline at end of file diff --git a/chrome/browser/resources/options2/manage_profile_overlay.html b/chrome/browser/resources/options2/manage_profile_overlay.html new file mode 100644 index 0000000..afc4801 --- /dev/null +++ b/chrome/browser/resources/options2/manage_profile_overlay.html @@ -0,0 +1,38 @@ +<div id="manage-profile-overlay" class="page" hidden> + <!-- Dialog for managing profiles. --> + <div id="manage-profile-overlay-manage" hidden> + <h1 i18n-content="manageProfilesTitle"></h1> + <div id="manage-profile-content" class="content-area"> + <div id="manage-profile-name-div"> + <span id="manage-profile-name-label" + i18n-content="manageProfilesNameLabel"></span> + <input id="manage-profile-name" type="text" required> + </div> + <div id="manage-profile-error-bubble" hidden> + </div> + <div id="manage-profile-icon-label" + i18n-content="manageProfilesIconLabel"></div> + <grid id="manage-profile-icon-grid"></grid> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="manage-profile-cancel" i18n-content="cancel"></button> + <button id="manage-profile-ok" i18n-content="ok"></button> + </div> + </div> + </div> + <!-- Dialog for deleting profiles. --> + <div id="manage-profile-overlay-delete" hidden> + <h1 i18n-content="deleteProfileTitle"></h1> + <div class="content-area"> + <div id="delete-profile-message"></div> + </div> + <div class="action-area"> + <div class="button-strip"> + <button id="delete-profile-cancel" i18n-content="cancel"></button> + <button id="delete-profile-ok" + i18n-content="deleteProfileOK"></button> + </div> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/manage_profile_overlay.js b/chrome/browser/resources/options2/manage_profile_overlay.js new file mode 100644 index 0000000..18cccf2 --- /dev/null +++ b/chrome/browser/resources/options2/manage_profile_overlay.js @@ -0,0 +1,265 @@ +// 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. + +cr.define('options', function() { + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + + const localStrings = new LocalStrings(); + + /** + * ManageProfileOverlay class + * Encapsulated handling of the 'Manage profile...' overlay page. + * @constructor + * @class + */ + function ManageProfileOverlay() { + OptionsPage.call(this, + 'manageProfile', + templateData.manageProfileOverlayTabTitle, + 'manage-profile-overlay'); + }; + + cr.addSingletonGetter(ManageProfileOverlay); + + ManageProfileOverlay.prototype = { + // Inherit from OptionsPage. + __proto__: OptionsPage.prototype, + + // Info about the currently managed/deleted profile. + profileInfo_: null, + + // An object containing all known profile names. + profileNames_: {}, + + // The currently selected icon in the icon grid. + iconGridSelectedURL_: null, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + var iconGrid = $('manage-profile-icon-grid'); + options.ProfilesIconGrid.decorate(iconGrid); + iconGrid.addEventListener('change', function(e) { + self.onIconGridSelectionChanged_(); + }); + + $('manage-profile-name').oninput = this.onNameChanged_.bind(this); + $('manage-profile-cancel').onclick = + $('delete-profile-cancel').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + $('manage-profile-ok').onclick = function(event) { + OptionsPage.closeOverlay(); + self.submitManageChanges_(); + }; + $('delete-profile-ok').onclick = function(event) { + OptionsPage.closeOverlay(); + chrome.send('deleteProfile', [self.profileInfo_.filePath]); + }; + }, + + /** @inheritDoc */ + didShowPage: function() { + chrome.send('requestDefaultProfileIcons'); + + // Use the hash to specify the profile index. + var hash = location.hash; + if (hash) { + $('manage-profile-overlay-manage').hidden = false; + $('manage-profile-overlay-delete').hidden = true; + ManageProfileOverlay.getInstance().hideErrorBubble_(); + + chrome.send('requestProfileInfo', [parseInt(hash.slice(1), 10)]); + } + + $('manage-profile-name').focus(); + }, + + /** + * Set the profile info used in the dialog. + * @param {Object} profileInfo An object of the form: + * profileInfo = { + * name: "Profile Name", + * iconURL: "chrome://path/to/icon/image", + * filePath: "/path/to/profile/data/on/disk" + * isCurrentProfile: false, + * }; + * @private + */ + setProfileInfo_: function(profileInfo) { + this.iconGridSelectedURL_ = profileInfo.iconURL; + this.profileInfo_ = profileInfo; + $('manage-profile-name').value = profileInfo.name; + $('manage-profile-icon-grid').selectedItem = profileInfo.iconURL; + }, + + /** + * Sets the name of the currently edited profile. + * @private + */ + setProfileName_: function(name) { + if (this.profileInfo_) + this.profileInfo_.name = name; + $('manage-profile-name').value = name; + }, + + /** + * Set an array of default icon URLs. These will be added to the grid that + * the user will use to choose their profile icon. + * @param {Array.<string>} iconURLs An array of icon URLs. + * @private + */ + receiveDefaultProfileIcons_: function(iconURLs) { + $('manage-profile-icon-grid').dataModel = new ArrayDataModel(iconURLs); + + // Changing the dataModel resets the selectedItem. Re-select it, if there + // is one. + if (this.profileInfo_) + $('manage-profile-icon-grid').selectedItem = this.profileInfo_.iconURL; + + var grid = $('manage-profile-icon-grid'); + // Recalculate the measured item size. + grid.measured_ = null; + grid.columns = 0; + grid.redraw(); + }, + + /** + * Set a dictionary of all profile names. These are used to prevent the + * user from naming two profiles the same. + * @param {Object} profileNames A dictionary of profile names. + * @private + */ + receiveProfileNames_: function(profileNames) { + this.profileNames_ = profileNames; + }, + + /** + * Display the error bubble, with |errorText| in the bubble. + * @param {string} errorText The localized string id to display as an error. + * @private + */ + showErrorBubble_: function(errorText) { + var nameErrorEl = $('manage-profile-error-bubble'); + nameErrorEl.hidden = false; + nameErrorEl.textContent = localStrings.getString(errorText); + + $('manage-profile-ok').disabled = true; + }, + + /** + * Hide the error bubble. + * @private + */ + hideErrorBubble_: function() { + $('manage-profile-error-bubble').hidden = true; + $('manage-profile-ok').disabled = false; + }, + + /** + * oninput callback for <input> field. + * @param event The event object + * @private + */ + onNameChanged_: function(event) { + var newName = event.target.value; + var oldName = this.profileInfo_.name; + + if (newName == oldName) { + this.hideErrorBubble_(); + } else if (this.profileNames_[newName] != undefined) { + this.showErrorBubble_('manageProfilesDuplicateNameError'); + } else { + this.hideErrorBubble_(); + + var nameIsValid = $('manage-profile-name').validity.valid; + $('manage-profile-ok').disabled = !nameIsValid; + } + }, + + /** + * Called when the user clicks "OK". Saves the newly changed profile info. + * @private + */ + submitManageChanges_: function() { + var name = $('manage-profile-name').value; + var iconURL = $('manage-profile-icon-grid').selectedItem; + chrome.send('setProfileNameAndIcon', + [this.profileInfo_.filePath, name, iconURL]); + }, + + /** + * Called when the selected icon in the icon grid changes. + * @private + */ + onIconGridSelectionChanged_: function() { + var iconURL = $('manage-profile-icon-grid').selectedItem; + if (!iconURL || iconURL == this.iconGridSelectedURL_) + return; + this.iconGridSelectedURL_ = iconURL; + chrome.send('profileIconSelectionChanged', + [this.profileInfo_.filePath, iconURL]); + }, + + /** + * Display the "Manage Profile" dialog. + * @param {Object} profileInfo The profile object of the profile to manage. + * @private + */ + showManageDialog_: function(profileInfo) { + ManageProfileOverlay.setProfileInfo(profileInfo); + $('manage-profile-overlay-manage').hidden = false; + $('manage-profile-overlay-delete').hidden = true; + ManageProfileOverlay.getInstance().hideErrorBubble_(); + + // Intentionally don't show the URL in the location bar as we don't want + // people trying to navigate here by hand. + OptionsPage.showPageByName('manageProfile', false); + }, + + /** + * Display the "Delete Profile" dialog. + * @param {Object} profileInfo The profile object of the profile to delete. + * @private + */ + showDeleteDialog_: function(profileInfo) { + ManageProfileOverlay.setProfileInfo(profileInfo); + $('manage-profile-overlay-manage').hidden = true; + $('manage-profile-overlay-delete').hidden = false; + $('delete-profile-message').textContent = + localStrings.getStringF('deleteProfileMessage', profileInfo.name); + $('delete-profile-message').style.backgroundImage = 'url("' + + profileInfo.iconURL + '")'; + + // Intentionally don't show the URL in the location bar as we don't want + // people trying to navigate here by hand. + OptionsPage.showPageByName('manageProfile', false); + }, + }; + + // Forward public APIs to private implementations. + [ + 'receiveDefaultProfileIcons', + 'receiveProfileNames', + 'setProfileInfo', + 'setProfileName', + 'showManageDialog', + 'showDeleteDialog', + ].forEach(function(name) { + ManageProfileOverlay[name] = function(value) { + ManageProfileOverlay.getInstance()[name + '_'](value); + }; + }); + + // Export + return { + ManageProfileOverlay: ManageProfileOverlay + }; +}); diff --git a/chrome/browser/resources/options2/options.html b/chrome/browser/resources/options2/options.html new file mode 100644 index 0000000..5751729 --- /dev/null +++ b/chrome/browser/resources/options2/options.html @@ -0,0 +1,188 @@ +<!DOCTYPE HTML> +<html id="t" i18n-values="dir:textdirection"> +<head> +<meta charset="utf-8"> +<!-- Set the title to that of the default page so that the title doesn't flash + on load (for the most common case). --> +<title i18n-content="browserPageTabTitle"></title> + +<link rel="icon" href="../../../app/theme/settings_favicon.png"> +<link rel="stylesheet" href="chrome://resources/css/button.css"> +<link rel="stylesheet" href="chrome://resources/css/checkbox.css"> +<link rel="stylesheet" href="chrome://resources/css/list.css"> +<link rel="stylesheet" href="chrome://resources/css/chrome_shared.css"> +<link rel="stylesheet" href="chrome://resources/css/select.css"> +<link rel="stylesheet" href="chrome://resources/css/spinner.css"> +<link rel="stylesheet" href="chrome://resources/css/throbber.css"> +<link rel="stylesheet" href="chrome://resources/css/tree.css"> +<link rel="stylesheet" href="options_page.css"> +<link rel="stylesheet" href="advanced_options.css"> +<link rel="stylesheet" href="alert_overlay.css"> +<link rel="stylesheet" href="autofill_options.css"> +<link rel="stylesheet" href="autofill_overlay.css"> +<link rel="stylesheet" href="browser_options_page.css"> +<link rel="stylesheet" href="clear_browser_data_overlay.css"> +<link rel="stylesheet" href="content_settings.css"> +<link rel="stylesheet" href="cookies_view.css"> +<link rel="stylesheet" href="extension_settings.css"> +<link rel="stylesheet" href="font_settings.css"> +<if expr="pp_ifdef('enable_register_protocol_handler')"> + <link rel="stylesheet" href="handler_options.css"> +</if> +<link rel="stylesheet" href="import_data_overlay.css"> +<if expr="pp_ifdef('enable_web_intents')"> + <link rel="stylesheet" href="intents_view.css"> +</if> +<link rel="stylesheet" href="language_options.css"> +<link rel="stylesheet" href="manage_profile_overlay.css"> +<link rel="stylesheet" href="pack_extension_overlay.css"> +<link rel="stylesheet" href="password_manager.css"> +<link rel="stylesheet" href="password_manager_list.css"> +<link rel="stylesheet" href="personal_options.css"> +<link rel="stylesheet" href="search_engine_manager.css"> +<link rel="stylesheet" href="search_page.css"> +<link rel="stylesheet" href="subpages_tab_controls.css"> +<link rel="stylesheet" href="../sync_setup_overlay.css"> +<if expr="pp_ifdef('chromeos')"> + <link rel="stylesheet" href="about_page.css"> + <link rel="stylesheet" href="chromeos/accounts_options_page.css"> + <link rel="stylesheet" href="chromeos/change_picture_options.css"> + <link rel="stylesheet" href="chromeos/internet_options_page.css"> + <link rel="stylesheet" href="chromeos/proxy.css"> + <link rel="stylesheet" href="chromeos/system_options_page.css"> +</if> +<if expr="pp_ifdef('chromeos') and pp_ifdef('use_virtual_keyboard')"> + <link rel="stylesheet" href="chromeos/virtual_keyboard.css"> +</if> + +<if expr="not pp_ifdef('win32') and not pp_ifdef('darwin')"> + <link rel="stylesheet" href="certificate_manager.css"> + <link rel="stylesheet" href="certificate_tree.css"> +</if> + +<script src="chrome://resources/css/tree.css.js"></script> + +<script src="chrome://resources/js/cr.js"></script> +<script src="chrome://resources/js/cr/command_line.js"></script> +<script src="chrome://resources/js/cr/event_target.js"></script> +<script src="chrome://resources/js/cr/ui.js"></script> +<script src="chrome://resources/js/cr/ui/array_data_model.js"></script> +<script src="chrome://resources/js/cr/ui/list_selection_model.js"></script> +<script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script> +<script src="chrome://resources/js/cr/ui/list_single_selection_model.js"></script> +<script src="chrome://resources/js/cr/ui/list_item.js"></script> +<script src="chrome://resources/js/cr/ui/list.js"></script> +<script src="chrome://resources/js/cr/ui/grid.js"></script> +<script src="chrome://resources/js/cr/ui/position_util.js"></script> +<script src="chrome://resources/js/cr/ui/repeating_button.js"></script> +<script src="chrome://resources/js/cr/ui/tree.js"></script> +<script src="chrome://resources/js/local_strings.js"></script> +<script src="chrome://resources/js/util.js"></script> +<script src="chrome://settings-frame/options_bundle.js"></script> +</head> +<body i18n-values=".style.fontFamily:fontfamily;"> +<div id="overlay" class="overlay transparent" hidden> + <include src="alert_overlay.html"> + <include src="autofill_edit_address_overlay.html"> + <include src="autofill_edit_creditcard_overlay.html"> + <include src="clear_browser_data_overlay.html"> + <include src="import_data_overlay.html"> + <include src="instant_confirm_overlay.html"> + <include src="language_add_language_overlay.html"> + <include src="manage_profile_overlay.html"> + <include src="pack_extension_overlay.html"> + <include src="../sync_setup_overlay.html"> + <if expr="pp_ifdef('chromeos')"> + <include + src="chromeos/language_customize_modifier_keys_overlay.html"> + <include src="chromeos/internet_detail.html"> + </if> + <if expr="not pp_ifdef('win32') and not pp_ifdef('darwin')"> + <include src="certificate_restore_overlay.html"> + <include src="certificate_backup_overlay.html"> + <include src="certificate_edit_ca_trust_overlay.html"> + <include src="certificate_import_error_overlay.html"> + </if> +</div> +<div id="main-content"> + <div id="navbar-container"> + <h1 id="navbar-content-title" i18n-content="title"></h1> + <ul id="navbar"> + </ul> + </div> + <div id="mainview"> + <div id="managed-prefs-banner" class="managed-prefs-banner" hidden> + <span id="managed-prefs-icon" class="managed-prefs-icon"></span> + <span id="managed-prefs-text" class="managed-prefs-text"></span> + </div> + <div id="subpage-backdrop" hidden></div> + <div id="mainview-content"> + <div id="page-container"> + <!-- Please keep the main pages in desired order of display. This will + allow search results to display in the desired order. --> + <include src="search_page.html"> + <include src="browser_options.html"> + <include src="personal_options.html"> + <if expr="pp_ifdef('chromeos')"> + <include src="chromeos/system_options.html"> + <include src="chromeos/internet_options.html"> + <include src="chromeos/accounts_options.html"> + </if> + <include src="advanced_options.html"> + <include src="extension_settings.html"> + </div> + <div id="subpage-sheet-container-1" + class="subpage-sheet-container transparent" hidden> + <div id="subpage-sheet-1" class="subpage-sheet"> + <button class="raw-button close-subpage custom-appearance"></button> + <div class="subpage-sheet-contents"> + <if expr="pp_ifdef('chromeos')"> + <include src="about_page.html"> + <include src="chromeos/change_picture_options.html"> + <include src="chromeos/proxy.html"> + </if> + <if expr="not pp_ifdef('win32') and not pp_ifdef('darwin')"> + <include src="certificate_manager.html"> + </if> + <include src="autofill_options.html"> + <include src="content_settings.html"> + <include src="font_settings.html"> + <include src="language_options.html"> + <include src="password_manager.html"> + <include src="search_engine_manager.html"> + </div> + </div> + </div> + <div id="subpage-sheet-container-2" + class="subpage-sheet-container transparent" hidden> + <div id="subpage-sheet-2" class="subpage-sheet"> + <button class="raw-button close-subpage custom-appearance"></button> + <div class="subpage-sheet-contents"> + <if expr="pp_ifdef('chromeos')"> + <include src="chromeos/language_chewing_options.html"> + <include src="chromeos/language_hangul_options.html"> + <include src="chromeos/language_mozc_options.html"> + <include src="chromeos/language_pinyin_options.html"> + </if> + <if expr="pp_ifdef('chromeos') and pp_ifdef('use_virtual_keyboard')"> + <include src="chromeos/virtual_keyboard.html"> + </if> + <include src="cookies_view.html"> + <if expr="pp_ifdef('enable_register_protocol_handler')"> + <include src="handler_options.html"> + </if> + <if expr="pp_ifdef('enable_web_intents')"> + <include src="intents_view.html"> + </if> + <include src="content_settings_exceptions_area.html"> + </div> + </div> + </div> + </div> + </div> +</div> +<script src="chrome://settings-frame/strings.js"></script> +<script src="chrome://resources/js/i18n_template.js"></script> +<script src="chrome://resources/js/i18n_process.js"></script> +</body> +</html> diff --git a/chrome/browser/resources/options2/options.js b/chrome/browser/resources/options2/options.js new file mode 100644 index 0000000..5a744f3 --- /dev/null +++ b/chrome/browser/resources/options2/options.js @@ -0,0 +1,247 @@ +// 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. + +var AddLanguageOverlay = options.AddLanguageOverlay; +var AdvancedOptions = options.AdvancedOptions; +var AlertOverlay = options.AlertOverlay; +var AutofillEditAddressOverlay = options.AutofillEditAddressOverlay; +var AutofillEditCreditCardOverlay = options.AutofillEditCreditCardOverlay; +var AutofillOptions = options.AutofillOptions; +var BrowserOptions = options.BrowserOptions; +var ClearBrowserDataOverlay = options.ClearBrowserDataOverlay; +var ContentSettings = options.ContentSettings; +var ContentSettingsExceptionsArea = + options.contentSettings.ContentSettingsExceptionsArea; +var CookiesView = options.CookiesView; +var ExtensionSettings = options.ExtensionSettings; +var FontSettings = options.FontSettings; +var HandlerOptions = options.HandlerOptions; +var ImportDataOverlay = options.ImportDataOverlay; +var IntentsView = options.IntentsView; +var InstantConfirmOverlay = options.InstantConfirmOverlay; +var LanguageOptions = options.LanguageOptions; +var OptionsPage = options.OptionsPage; +var PackExtensionOverlay = options.PackExtensionOverlay; +var PasswordManager = options.PasswordManager; +var PersonalOptions = options.PersonalOptions; +var Preferences = options.Preferences; +var ManageProfileOverlay = options.ManageProfileOverlay; +var ProxyOptions = options.ProxyOptions; +var SearchEngineManager = options.SearchEngineManager; +var SearchPage = options.SearchPage; +var SyncSetupOverlay = options.SyncSetupOverlay; +var VirtualKeyboardManager = options.VirtualKeyboardManager; + +/** + * DOMContentLoaded handler, sets up the page. + */ +function load() { + // Decorate the existing elements in the document. + cr.ui.decorate('input[pref][type=checkbox]', options.PrefCheckbox); + cr.ui.decorate('input[pref][type=number]', options.PrefNumber); + cr.ui.decorate('input[pref][type=radio]', options.PrefRadio); + cr.ui.decorate('input[pref][type=range]', options.PrefRange); + cr.ui.decorate('select[pref]', options.PrefSelect); + cr.ui.decorate('input[pref][type=text]', options.PrefTextField); + cr.ui.decorate('input[pref][type=url]', options.PrefTextField); + cr.ui.decorate('button[pref]', options.PrefButton); + cr.ui.decorate('#content-settings-page input[type=radio]:not(.handler-radio)', + options.ContentSettingsRadio); + cr.ui.decorate('#content-settings-page input[type=radio].handler-radio', + options.HandlersEnabledRadio); + cr.ui.decorate('span.controlled-setting-indicator', + options.ControlledSettingIndicator); + + var menuOffPattern = /(^\?|&)menu=off($|&)/; + var menuDisabled = menuOffPattern.test(window.location.search); + // document.documentElement.setAttribute('hide-menu', menuDisabled); + // We can't use an attribute on the html element because of webkit bug + // 12519. Instead, we add a class. + if (menuDisabled) + document.documentElement.classList.add('hide-menu'); + + localStrings = new LocalStrings(); + + OptionsPage.register(SearchPage.getInstance()); + + OptionsPage.register(BrowserOptions.getInstance()); + OptionsPage.registerSubPage(SearchEngineManager.getInstance(), + BrowserOptions.getInstance(), + [$('defaultSearchManageEnginesButton')]); + OptionsPage.register(PersonalOptions.getInstance()); + OptionsPage.registerSubPage(AutofillOptions.getInstance(), + PersonalOptions.getInstance(), + [$('autofill-settings')]); + OptionsPage.registerSubPage(PasswordManager.getInstance(), + PersonalOptions.getInstance(), + [$('manage-passwords')]); + if (cr.isChromeOS) { + OptionsPage.register(SystemOptions.getInstance()); + OptionsPage.registerSubPage(AboutPage.getInstance(), + SystemOptions.getInstance()); + OptionsPage.registerSubPage(LanguageOptions.getInstance(), + SystemOptions.getInstance(), + [$('language-button')]); + OptionsPage.registerSubPage( + new OptionsPage('languageChewing', + templateData.languageChewingPageTabTitle, + 'languageChewingPage'), + LanguageOptions.getInstance()); + OptionsPage.registerSubPage( + new OptionsPage('languageHangul', + templateData.languageHangulPageTabTitle, + 'languageHangulPage'), + LanguageOptions.getInstance()); + OptionsPage.registerSubPage( + new OptionsPage('languageMozc', + templateData.languageMozcPageTabTitle, + 'languageMozcPage'), + LanguageOptions.getInstance()); + OptionsPage.registerSubPage( + new OptionsPage('languagePinyin', + templateData.languagePinyinPageTabTitle, + 'languagePinyinPage'), + LanguageOptions.getInstance()); + // Only use the VirtualKeyboardManager if the keyboard DOM elements (which + // it will assume exists) are present (i.e. if we were built with + // USE_VIRTUAL_KEYBOARD). + if ($('language-options-virtual-keyboard')) { + OptionsPage.registerSubPage(VirtualKeyboardManager.getInstance(), + LanguageOptions.getInstance()); + } + OptionsPage.register(InternetOptions.getInstance()); + } + OptionsPage.register(AdvancedOptions.getInstance()); + OptionsPage.registerSubPage(ContentSettings.getInstance(), + AdvancedOptions.getInstance(), + [$('privacyContentSettingsButton')]); + OptionsPage.registerSubPage(ContentSettingsExceptionsArea.getInstance(), + ContentSettings.getInstance()); + OptionsPage.registerSubPage(CookiesView.getInstance(), + ContentSettings.getInstance(), + [$('privacyContentSettingsButton'), + $('show-cookies-button')]); + // If HandlerOptions is null it means it got compiled out. + if (HandlerOptions) { + OptionsPage.registerSubPage(HandlerOptions.getInstance(), + ContentSettings.getInstance(), + [$('manage-handlers-button')]); + } + if (IntentsView && $('manage-intents-button')) { + OptionsPage.registerSubPage(IntentsView.getInstance(), + ContentSettings.getInstance(), + [$('manage-intents-button')]); + } + OptionsPage.registerSubPage(FontSettings.getInstance(), + AdvancedOptions.getInstance(), + [$('fontSettingsCustomizeFontsButton')]); + if (!cr.isChromeOS) { + OptionsPage.registerSubPage(LanguageOptions.getInstance(), + AdvancedOptions.getInstance(), + [$('language-button')]); + } + if (!cr.isWindows && !cr.isMac) { + OptionsPage.registerSubPage(CertificateManager.getInstance(), + AdvancedOptions.getInstance(), + [$('certificatesManageButton')]); + OptionsPage.registerOverlay(CertificateRestoreOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateBackupOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateEditCaTrustOverlay.getInstance(), + CertificateManager.getInstance()); + OptionsPage.registerOverlay(CertificateImportErrorOverlay.getInstance(), + CertificateManager.getInstance()); + } + OptionsPage.registerOverlay(AddLanguageOverlay.getInstance(), + LanguageOptions.getInstance()); + OptionsPage.registerOverlay(AlertOverlay.getInstance()); + OptionsPage.registerOverlay(AutofillEditAddressOverlay.getInstance(), + AutofillOptions.getInstance()); + OptionsPage.registerOverlay(AutofillEditCreditCardOverlay.getInstance(), + AutofillOptions.getInstance()); + OptionsPage.registerOverlay(ClearBrowserDataOverlay.getInstance(), + AdvancedOptions.getInstance(), + [$('privacyClearDataButton')]); + OptionsPage.registerOverlay(ImportDataOverlay.getInstance(), + PersonalOptions.getInstance()); + OptionsPage.registerOverlay(InstantConfirmOverlay.getInstance(), + BrowserOptions.getInstance()); + OptionsPage.registerOverlay(SyncSetupOverlay.getInstance(), + PersonalOptions.getInstance()); + OptionsPage.registerOverlay(ManageProfileOverlay.getInstance(), + PersonalOptions.getInstance()); + + OptionsPage.register(ExtensionSettings.getInstance()); + OptionsPage.registerOverlay(PackExtensionOverlay.getInstance(), + ExtensionSettings.getInstance()); + + if (cr.isChromeOS) { + OptionsPage.register(AccountsOptions.getInstance()); + OptionsPage.registerSubPage(ProxyOptions.getInstance(), + InternetOptions.getInstance()); + OptionsPage.registerSubPage(ChangePictureOptions.getInstance(), + PersonalOptions.getInstance(), + [$('change-picture-button')]); + OptionsPage.registerOverlay(DetailsInternetPage.getInstance(), + InternetOptions.getInstance()); + + var languageModifierKeysOverlay = new OptionsPage( + 'languageCustomizeModifierKeysOverlay', + localStrings.getString('languageCustomizeModifierKeysOverlay'), + 'languageCustomizeModifierKeysOverlay') + $('languageCustomizeModifierKeysOverleyDismissButton').onclick = + function() { + OptionsPage.closeOverlay(); + }; + OptionsPage.registerOverlay(languageModifierKeysOverlay, + SystemOptions.getInstance(), + [$('modifier-keys-button')]); + } + + Preferences.getInstance().initialize(); + OptionsPage.initialize(); + + var path = document.location.pathname; + + if (path.length > 1) { + // Skip starting slash and remove trailing slash (if any). + var pageName = path.slice(1).replace(/\/$/, ''); + // Proxy page is now per network and only reachable from internet details. + if (pageName != 'proxy') { + // Show page, but don't update history (there's already an entry for it). + OptionsPage.showPageByName(pageName, false); + } + } else { + OptionsPage.showDefaultPage(); + } + + var subpagesNavTabs = document.querySelectorAll('.subpages-nav-tabs'); + for(var i = 0; i < subpagesNavTabs.length; i++) { + subpagesNavTabs[i].onclick = function(event) { + OptionsPage.showTab(event.srcElement); + } + } + + // Allow platform specific CSS rules. + cr.enablePlatformSpecificCSSRules(); + + if (navigator.plugins['Shockwave Flash']) + document.documentElement.setAttribute('hasFlashPlugin', ''); + + // Clicking on the Settings title brings up the 'Basics' page. + $('navbar-content-title').onclick = function() { + OptionsPage.navigateToPage(BrowserOptions.getInstance().name); + }; +} + +document.addEventListener('DOMContentLoaded', load); + +window.onpopstate = function(e) { + options.OptionsPage.setState(e.state); +}; + +window.onbeforeunload = function() { + options.OptionsPage.willClose(); +}; diff --git a/chrome/browser/resources/options2/options_bundle.js b/chrome/browser/resources/options2/options_bundle.js new file mode 100644 index 0000000..f6e9939 --- /dev/null +++ b/chrome/browser/resources/options2/options_bundle.js @@ -0,0 +1,94 @@ +// 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. +// +// This file exists to aggregate all of the javascript used by the +// settings page into a single file which will be flattened and served +// as a single resource. +<include src="preferences.js"></include> +<include src="pref_ui.js"></include> +<include src="deletable_item_list.js"></include> +<include src="inline_editable_list.js"></include> +<include src="controlled_setting.js"></include> +<include src="options_page.js"></include> +<if expr="pp_ifdef('chromeos')"> + <include src="about_page.js"></include> + <include src="../chromeos/user_images_grid.js"></include> + <include src="chromeos/cellular_plan_element.js"></include> + <include src="chromeos/change_picture_options.js"></include> + <include src="chromeos/internet_detail_ip_config_list.js"></include> + <include src="chromeos/internet_network_element.js"></include> + <include src="chromeos/internet_options.js"></include> + <include src="chromeos/internet_detail.js"></include> + <include src="chromeos/system_options.js"></include> + <include src="chromeos/bluetooth_list_element.js"></include> + <include src="chromeos/accounts_options.js"></include> + <include src="chromeos/proxy_options.js"></include> + <include src="chromeos/proxy_rules_list.js"></include> + <include src="chromeos/accounts_user_list.js"></include> + <include src="chromeos/accounts_user_name_edit.js"></include> + <include src="chromeos/virtual_keyboard.js"></include> + <include src="chromeos/virtual_keyboard_list.js"></include> + var AboutPage = options.AboutPage; + var AccountsOptions = options.AccountsOptions; + var ChangePictureOptions = options.ChangePictureOptions; + var InternetOptions = options.InternetOptions; + var DetailsInternetPage = options.internet.DetailsInternetPage; + var SystemOptions = options.SystemOptions; +</if> +<if expr="not pp_ifdef('win32') and not pp_ifdef('darwin')"> + <include src="certificate_tree.js"></include> + <include src="certificate_manager.js"></include> + <include src="certificate_restore_overlay.js"></include> + <include src="certificate_backup_overlay.js"></include> + <include src="certificate_edit_ca_trust_overlay.js"></include> + <include src="certificate_import_error_overlay.js"></include> + var CertificateManager = options.CertificateManager; + var CertificateRestoreOverlay = options.CertificateRestoreOverlay; + var CertificateBackupOverlay = options.CertificateBackupOverlay; + var CertificateEditCaTrustOverlay = options.CertificateEditCaTrustOverlay; + var CertificateImportErrorOverlay = options.CertificateImportErrorOverlay; +</if> +<include src="advanced_options.js"></include> +<include src="alert_overlay.js"></include> +<include src="autocomplete_list.js"></include> +<include src="autofill_edit_address_overlay.js"></include> +<include src="autofill_edit_creditcard_overlay.js"></include> +<include src="autofill_options_list.js"></include> +<include src="autofill_options.js"></include> +<include src="browser_options.js"></include> +<include src="browser_options_startup_page_list.js"></include> +<include src="clear_browser_data_overlay.js"></include> +<include src="content_settings.js"></include> +<include src="content_settings_exceptions_area.js"></include> +<include src="content_settings_ui.js"></include> +<include src="cookies_list.js"></include> +<include src="cookies_view.js"></include> +<include src="extension_list.js"></include> +<include src="extension_settings.js"></include> +<include src="font_settings.js"></include> +<if expr="pp_ifdef('enable_register_protocol_handler')"> + <include src="handler_options.js"></script> + <include src="handler_options_list.js"></script> +</if> +<include src="import_data_overlay.js"></include> +<include src="instant_confirm_overlay.js"></include> +<if expr="pp_ifdef('enable_web_intents')"> + <include src="intents_list.js"></include> + <include src="intents_view.js"></include> +</if> +<include src="language_add_language_overlay.js"></include> +<include src="language_list.js"></include> +<include src="language_options.js"></include> +<include src="manage_profile_overlay.js"></include> +<include src="pack_extension_overlay.js"></include> +<include src="password_manager.js"></include> +<include src="password_manager_list.js"></include> +<include src="personal_options.js"></include> +<include src="personal_options_profile_list.js"></include> +<include src="profiles_icon_grid.js"></include> +<include src="search_engine_manager.js"></include> +<include src="search_engine_manager_engine_list.js"></include> +<include src="search_page.js"></include> +<include src="../sync_setup_overlay.js"></include> +<include src="options.js"></include> diff --git a/chrome/browser/resources/options2/options_page.css b/chrome/browser/resources/options2/options_page.css new file mode 100644 index 0000000..ff45d67 --- /dev/null +++ b/chrome/browser/resources/options2/options_page.css @@ -0,0 +1,736 @@ +.hbox { + display: -webkit-box; + -webkit-box-orient: horizontal; +} + +.vbox { + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.stretch { + -webkit-box-flex: 1; +} + +.frozen, +.subpage-sheet-container.frozen { + position: fixed; +} + +#search-field { + font-size: inherit; + margin: 0; +} + +/* + * Add padding to increase the touchable area of search box. Use original font + * size to avoid the width of search box exceeding the width of navbar. + */ +html[touch-optimized] #search-field { + font-size: 13px; + padding: 5px; +} +html[touch-optimized] #search-field::-webkit-search-cancel-button { + -webkit-transform: scale(1.5); +} + +/* + * For touch-optimized UI, make the radio/checkbox input boxes in + * options/preference pages easier to touch. + * TODO(rbyers): We need to solve this more generally for all web pages + * (crbug.com/99981), and perhaps temporarily for all WebUI (crbug.com/102482). + */ +html[touch-optimized] div.radio > label > span, +html[touch-optimized] div.checkbox > label > span { + -webkit-padding-start: 5px; +} + +html[touch-optimized] label > input[type=checkbox], +html[touch-optimized] label > input[type=radio] { + -webkit-transform: scale(1.4); +} + +/* + * Override the font-size rule in shared_options.css file. + * 16 px font-size proved to be more touch friendly. It increases the touchable + * area for buttons and input boxes. + */ +html[touch-optimized] body { + font-size: 16px; +} + +.overlay { + -webkit-box-align: center; + -webkit-box-orient: vertical; + -webkit-box-pack: center; + -webkit-transition: 250ms opacity; + background: -webkit-radial-gradient(rgba(127, 127, 127, 0.5), + rgba(127, 127, 127, 0.5) 35%, + rgba(0, 0, 0, 0.7)); + bottom: 0; + display: -webkit-box; + left: 0; + overflow: auto; + padding: 20px; + padding-bottom: 130px; + position: fixed; + right: 0; + top: 0; + z-index: 10; +} + +.raw-button, +.raw-button:hover, +.raw-button:active { + -webkit-box-shadow: none; + background-color: transparent; + background-repeat: no-repeat; + border: none; + min-width: 0; + padding: 1px 6px; +} + +.close-subpage { + background-image: url('chrome://theme/IDR_CLOSE_BAR'); + height: 16px; + min-width: 0; + position: relative; + top: 16px; + width: 16px; +} + +/* In TOUCH_UI builds, the IDR_CLOSE_BAR resource is double-size. */ +<if expr="pp_ifdef('touchui')"> +.close-subpage { + height: 32px; + width: 32px; +} +</if> + +.close-subpage:hover { + background-image: url('chrome://theme/IDR_CLOSE_BAR_H'); +} + +.close-subpage:active { + background-image: url('chrome://theme/IDR_CLOSE_BAR_P'); +} + +html[dir='ltr'] .close-subpage { + float: right; + right: 20px; +} + +html[dir='rtl'] .close-subpage { + float: left; + left: 20px; +} + +html.hide-menu .close-subpage { + display: none +} + +.content-area { + padding: 10px 15px 5px 15px; +} + +.action-area { + -webkit-box-align: center; + -webkit-box-orient: horizontal; + -webkit-box-pack: end; + border-top: 1px solid rgba(188, 193, 208, .5); + display: -webkit-box; + padding: 12px; +} + +html[dir='rtl'] .action-area { + left: 0; +} + +.action-area-right { + display: -webkit-box; +} + +.button-strip { + -webkit-box-orient: horizontal; + display: -webkit-box; +} + +.button-strip > button { + -webkit-margin-start: 10px; + display: block; +} + +.bottom-strip { + padding: 12px; + position: absolute; + right: 0px; + bottom: 0px; + border-top: none; +} + +.overlay .page { + -webkit-box-shadow: 0px 5px 80px #505050; + background: white; + border: 1px solid rgb(188, 193, 208); + border-radius: 2px; + min-width: 400px; + padding: 0; + position: relative; +} + +#subpage-backdrop { + -webkit-transition: 250ms opacity; + background-color: rgba(233, 238, 242, .5); + height: 100%; + left: 216px; + right: 216px; + position: fixed; + top: 0; + width: 100%; +} + +.subpage-sheet-container { + -webkit-transition: 250ms opacity, 100ms padding-left, 100ms padding-right; + box-sizing: border-box; + min-height: 100%; + position: absolute; + /* We set both left and right for the sake of RTL. */ + left: 0; + right: 0; + top: 0; + width: 100%; +} + +#subpage-sheet-container-1 { + -webkit-padding-start: 40px; + z-index: 5; +} + +#subpage-sheet-container-2 { + -webkit-padding-start: 80px; + z-index: 10; +} + +.subpage-sheet { + -webkit-box-shadow: #666 0px 2px 5px; + background-color: white; + border-left: 1px solid #b8b8b8; + box-sizing: border-box; + min-height: 100%; + width: 100%; + min-width: 651px; +} + +.subpage-sheet-contents { + box-sizing: border-box; + padding: 0px 20px 20px 20px; + width: 650px; +} + +.managed-prefs-banner { + background: -webkit-linear-gradient(#fff2b7, #fae691 97%, #878787); + height: 31px; + width: 100%; + margin: 0; + padding: 0; + position: relative; + vertical-align: middle; + z-index: 11; +} + +.managed-prefs-banner.clickable:active { + background: -webkit-linear-gradient(#878787, #fae691 3%, #fff2b7); +} + +.managed-prefs-icon { + background-image: url("chrome://theme/IDR_WARNING"); + background-repeat: no-repeat; + background-position:center; + display: inline-block; + padding: 5px; + height: 21px; + vertical-align: middle; + width: 24px; +} + +.managed-prefs-text { + vertical-align: middle; +} + +.subpage-sheet .page h1 { + margin-bottom: 10px; +} + +.overlay .page h1 { + background: -webkit-linear-gradient(white, #F8F8F8); + border-bottom: 1px solid rgba(188, 193, 208, .5); + font-size: 105%; + font-weight: bold; + padding: 10px 15px 8px 15px; +} + +.page list { + /* Min height is a multiple of the list item height (32) */ + min-height: 192px; +} + +/** + * TODO(kevers): Standardize formatting of sections to use display tables. + * For now, we require separate specialized rules for sections that are + * formatted as table rows. + */ +section { + -webkit-box-orient: horizontal; + border-bottom: 1px solid #eeeeee; + display: -webkit-box; + margin-top: 17px; + padding-bottom: 20px; +} + +div.page section:last-child { + border-bottom: none; +} + +h3 { + font-size: 105%; + font-weight: bold; + margin: 20px 0 10px 0; +} + +section > h3 { + margin: 0; + vertical-align: middle; + width: 130px; + -webkit-padding-end: 10px; +} + +section > div:only-of-type { + -webkit-box-flex: 1; +} + +/* Don't allow edge margin on the first/last child of a section. */ +section > h3 + * > *:last-child { + margin-bottom: 0; +} +section > h3 + * > *:first-child { + margin-top: 0; +} + +.option { + margin-top: 0; +} + +/* [hidden] does display:none, but its priority is too low in some cases. */ +[hidden] { + display: none !important; +} + +.transparent { + opacity: 0; +} + +.touch-slider { + -webkit-appearance: slider-horizontal; +} + + +.settings-list, +.settings-list-empty { + border: 1px solid #d9d9d9; + border-radius: 2px; +} + +.settings-list-empty { + background-color: #f4f4f4; + box-sizing: border-box; + min-height: 125px; + padding-left: 20px; + padding-top: 20px; +} + +list > * { + -webkit-box-align: center; + -webkit-transition: 150ms background-color; + box-sizing: border-box; + border-radius: 0; + display: -webkit-box; + height: 32px; + border: none; + margin: 0; +} + +list:not([disabled]) > :hover { + background-color: #e4ecf7; +} + +/* TODO(stuartmorgan): Once this becomes the list style for other WebUI pages + * these rules can be simplified (since they wont need to override other rules). + */ + +list:not([hasElementFocus]) > [selected], +list:not([hasElementFocus]) > [lead][selected] { + background-color: #d0d0d0; + background-image: none; +} + +list[hasElementFocus] > [selected], +list[hasElementFocus] > [lead][selected], +list:not([hasElementFocus]) > [selected]:hover, +list:not([hasElementFocus]) > [selected][lead]:hover { + background-color: #bbcee9; + background-image: none; +} + +list[hasElementFocus] > [lead], +list[hasElementFocus] > [lead][selected] { + border-top: 1px solid #7892b4; + border-bottom: 1px solid #7892b4; +} + +list[hasElementFocus] > [lead]:nth-child(2), +list[hasElementFocus] > [lead][selected]:nth-child(2) { + border-top: 1px solid transparent; +} + +list[hasElementFocus] > [lead]:nth-last-child(2), +list[hasElementFocus] > [lead][selected]:nth-last-child(2) { + border-bottom: 1px solid transparent; +} + +list[disabled] > [lead][selected], +list[disabled]:focus > [lead][selected] { + border: none; +} + +list[disabled] { + opacity: 0.6; +} + +list > .heading { + color: #666666; +} + +list > .heading:hover { + background-color: transparent; + border-color: transparent; +} + +list .deletable-item { + -webkit-box-align: center; +} + +list .deletable-item > :first-child { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-padding-end: 5px; + display: -webkit-box; +} + +list .close-button { + -webkit-transition: 150ms opacity; + background-color: transparent; + /* TODO(stuartmorgan): Replace with real images once they are available. */ + background-image: url("../../../app/theme/close_bar.png"); + border: none; + display: block; + height: 16px; + opacity: 1; + width: 16px; +} + +list > *:not(:hover):not([lead]) .close-button, +list > *:not(:hover):not([selected]) .close-button, +list:not([hasElementFocus]) > *:not(:hover) .close-button, +list[disabled] .close-button, +list .close-button[disabled] { + opacity: 0; + pointer-events: none; +} + +list .close-button:hover { + background-image: url("../../../app/theme/close_bar_h.png"); +} + +list .close-button:active { + background-image: url("../../../app/theme/close_bar_p.png"); +} + +list .static-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +list[inlineeditable] input { + box-sizing: border-box; + margin: 0; + width: 100%; +} + +list > :not([editing]) [displaymode="edit"] { + display: none; +} + +list > [editing] [displaymode="static"] { + display: none; +} + +list > [editing] input:invalid { + /* TODO(stuartmorgan): Replace with validity badge */ + background-color: pink; +} + +.option-name { + padding-right: 5px; +} + +html[dir=rtl].option-name { + padding-left: 5px; +} + +.favicon-cell { + -webkit-padding-start: 20px; + background-position: left; + background-repeat: no-repeat; +} + +input[type="url"].favicon-cell { + -webkit-padding-start: 22px; + background-position-x: 4px; +} + +/* TODO(jhawkins): Use something better than 99.3% when CSS3 background + * positioning is available. + */ +html[dir=rtl] input.favicon-cell { + background-position-x: 99.3%; +} + +list .favicon-cell { + -webkit-margin-start: 7px; + -webkit-padding-start: 26px; + display: block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +html[dir=rtl] list .favicon-cell { + background-position: right; +} + +html[enable-background-mode=false] #background-mode-section { + display: none; +} + +/* UI Controls */ + +/* LIST */ +html:not([os=mac]) list[hasElementFocus] { + outline: 1px solid rgba(0, 128, 256, 0.5); + outline-offset: -2px; +} + +/* This matches the native list outline on Mac */ +html[os=mac] list[hasElementFocus] { + outline-color: #759ad9; + outline-offset: -1px; + outline-style: auto; + outline-width: 5px; +} + +.suboption { + -webkit-margin-start: 23px; +} + +.informational-text { + color: grey; +} + +#main-content list.autocomplete-suggestions { + background-color: white; + border: 1px solid #aaa; + border-radius: 2px; + min-height: 0; + opacity: 0.9; + position: fixed; + z-index: 3; +} + +list.autocomplete-suggestions > div { + height: auto; +} + +list.autocomplete-suggestions:not([hasElementFocus]) > [selected], +list.autocomplete-suggestions:not([hasElementFocus]) > [lead][selected] { + background-color: #bbcee9; +} + +html:not([hasFlashPlugin]) .flash-plugin-area, +/* If the Flash plug-in supports the NPP_ClearSiteData API, we don't need to + * show the link to the Flash storage settings manager: + */ +html[flashPluginSupportsClearSiteData] .flash-plugin-area, +html:not([flashPluginSupportsClearSiteData]) .clear-plugin-lso-data-enabled, +html[flashPluginSupportsClearSiteData] .clear-plugin-lso-data-disabled { + display: none; +} + + +/* Display a collection of sections as a table in order to display nicely + * in multiple locales. + */ +.displaytable { + display: table; + width: 100%; +} + +.displaytable > section { + display: table-row; +} + +/* right table column containing settable options */ +.displaytable > section > h3 + div, +.displaytable > section > h3 + table { + padding-bottom: 20px; +} + +/* Setting the padding on the header so the alignment doesn't depend on the + * contents of the right table column. */ +.displaytable > section > h3 { + padding-top: 17px; +} + +.displaytable > section > * { + display: table-cell; + vertical-align: baseline; + border-bottom: 1px solid #eeeeee; +} + +/* do not display a border after the last section in the table */ +.displaytable:not([searching='true']) > section:last-child > * { + border-bottom: none; +} + +/* Controlled setting indicator and bubble. */ +.controlled-setting-indicator { + display: inline-block; + /* Establish a containing block for absolutely positioning the bubble. */ + position: relative; + vertical-align: text-bottom; +} + +.controlled-setting-indicator[controlled-by] summary { + background-size: contain; + height: 16px; + width: 16px; +} + +.controlled-setting-indicator summary::-webkit-details-marker { + display: none; +} + +.controlled-setting-indicator[controlled-by='policy'] summary { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY_GRAY'); +} + +.controlled-setting-indicator[controlled-by='policy'] summary:hover { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY'); +} + +.controlled-setting-indicator[controlled-by='extension'] summary { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_EXTENSION_GRAY'); +} + +.controlled-setting-indicator[controlled-by='extension'] summary:hover { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_EXTENSION'); +} + +.controlled-setting-indicator[controlled-by='recommended'] summary { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_RECOMMENDED_GRAY'); +} + +.controlled-setting-indicator[controlled-by='recommended'] summary:hover { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_RECOMMENDED'); +} + +.controlled-setting-bubble { + -webkit-margin-start: -20px; + background-color: white; + border-radius: 4px; + border: 1px solid #ccc; + box-shadow: 0 2px 2px #ddd; + margin-top: 10px; + padding: 10px; + position: absolute; + top: 50%; + z-index: 10; +} + +html[dir='ltr'] .controlled-setting-bubble { + left: 50%; +} + +html[dir='rtl'] .controlled-setting-bubble { + right: 50%; +} + +.controlled-setting-bubble::before { + -webkit-margin-start: 4px; + border-color: #ccc transparent; + border-style: solid; + border-width: 0 5px 5px; + content: ''; + position: absolute; + top: -5px; +} + +.controlled-setting-bubble::after { + -webkit-margin-start: 5px; + border-color: white transparent; + border-style: solid; + border-width: 0 4px 4px; + content: ''; + position: absolute; + top: -4px; +} + +.controlled-setting-bubble-text { + -webkit-padding-start: 30px; + background-repeat: no-repeat; + margin: 0; + min-height: 32px; + min-width: 200px; +} + +.controlled-setting-indicator[controlled-by='policy'] + .controlled-setting-bubble-text { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_MANDATORY_LARGE'); +} + +.controlled-setting-indicator[controlled-by='extension'] + .controlled-setting-bubble-text { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_EXTENSION_LARGE'); +} + +.controlled-setting-indicator[controlled-by='recommended'] + .controlled-setting-bubble-text { + background-image: + url('chrome://theme/IDR_CONTROLLED_SETTING_RECOMMENDED_LARGE'); +} + +html[dir='rtl'] .controlled-setting-bubble-text { + background-position: right top; +} + +.controlled-setting-bubble-action { + padding: 0 !important; +} diff --git a/chrome/browser/resources/options2/options_page.js b/chrome/browser/resources/options2/options_page.js new file mode 100644 index 0000000..0599e0e --- /dev/null +++ b/chrome/browser/resources/options2/options_page.js @@ -0,0 +1,1076 @@ +// 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. + +cr.define('options', function() { + ///////////////////////////////////////////////////////////////////////////// + // OptionsPage class: + + /** + * Base class for options page. + * @constructor + * @param {string} name Options page name, also defines id of the div element + * containing the options view and the name of options page navigation bar + * item as name+'PageNav'. + * @param {string} title Options page title, used for navigation bar + * @extends {EventTarget} + */ + function OptionsPage(name, title, pageDivName) { + this.name = name; + this.title = title; + this.pageDivName = pageDivName; + this.pageDiv = $(this.pageDivName); + this.tab = null; + } + + const SUBPAGE_SHEET_COUNT = 2; + + /** + * Main level option pages. Maps lower-case page names to the respective page + * object. + * @protected + */ + OptionsPage.registeredPages = {}; + + /** + * Pages which are meant to behave like modal dialogs. Maps lower-case overlay + * names to the respective overlay object. + * @protected + */ + OptionsPage.registeredOverlayPages = {}; + + /** + * Whether or not |initialize| has been called. + * @private + */ + OptionsPage.initialized_ = false; + + /** + * Gets the default page (to be shown on initial load). + */ + OptionsPage.getDefaultPage = function() { + return BrowserOptions.getInstance(); + }; + + /** + * Shows the default page. + */ + OptionsPage.showDefaultPage = function() { + this.navigateToPage(this.getDefaultPage().name); + }; + + /** + * "Navigates" to a page, meaning that the page will be shown and the + * appropriate entry is placed in the history. + * @param {string} pageName Page name. + */ + OptionsPage.navigateToPage = function(pageName) { + this.showPageByName(pageName, true); + }; + + /** + * Shows a registered page. This handles both top-level pages and sub-pages. + * @param {string} pageName Page name. + * @param {boolean} updateHistory True if we should update the history after + * showing the page. + * @private + */ + OptionsPage.showPageByName = function(pageName, updateHistory) { + // Find the currently visible root-level page. + var rootPage = null; + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (page.visible && !page.parentPage) { + rootPage = page; + break; + } + } + + // Find the target page. + var targetPage = this.registeredPages[pageName.toLowerCase()]; + if (!targetPage || !targetPage.canShowPage()) { + // If it's not a page, try it as an overlay. + if (!targetPage && this.showOverlay_(pageName, rootPage)) { + if (updateHistory) + this.updateHistoryState_(); + return; + } else { + targetPage = this.getDefaultPage(); + } + } + + pageName = targetPage.name.toLowerCase(); + var targetPageWasVisible = targetPage.visible; + + // Determine if the root page is 'sticky', meaning that it + // shouldn't change when showing a sub-page. This can happen for special + // pages like Search. + var isRootPageLocked = + rootPage && rootPage.sticky && targetPage.parentPage; + + // Notify pages if they will be hidden. + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + if (page.willHidePage && name != pageName && + !page.isAncestorOfPage(targetPage)) + page.willHidePage(); + } + + // Update visibilities to show only the hierarchy of the target page. + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + page.visible = name == pageName || + (!document.documentElement.classList.contains('hide-menu') && + page.isAncestorOfPage(targetPage)); + } + + // Update the history and current location. + if (updateHistory) + this.updateHistoryState_(); + + // Always update the page title. + document.title = targetPage.title; + + // Notify pages if they were shown. + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (!page.parentPage && isRootPageLocked) + continue; + if (!targetPageWasVisible && page.didShowPage && (name == pageName || + page.isAncestorOfPage(targetPage))) + page.didShowPage(); + } + }; + + /** + * Updates the visibility and stacking order of the subpage backdrop + * according to which subpage is topmost and visible. + * @private + */ + OptionsPage.updateSubpageBackdrop_ = function () { + var topmostPage = this.getTopmostVisibleNonOverlayPage_(); + var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; + + var subpageBackdrop = $('subpage-backdrop'); + if (nestingLevel > 0) { + var container = $('subpage-sheet-container-' + nestingLevel); + subpageBackdrop.style.zIndex = + parseInt(window.getComputedStyle(container).zIndex) - 1; + subpageBackdrop.hidden = false; + } else { + subpageBackdrop.hidden = true; + } + }; + + /** + * Scrolls the page to the correct position (the top when opening a subpage, + * or the old scroll position a previously hidden subpage becomes visible). + * @private + */ + OptionsPage.updateScrollPosition_ = function () { + var topmostPage = this.getTopmostVisibleNonOverlayPage_(); + var nestingLevel = topmostPage ? topmostPage.nestingLevel : 0; + + var container = (nestingLevel > 0) ? + $('subpage-sheet-container-' + nestingLevel) : $('page-container'); + + var scrollTop = container.oldScrollTop || 0; + container.oldScrollTop = undefined; + window.scroll(document.body.scrollLeft, scrollTop); + }; + + /** + * Pushes the current page onto the history stack, overriding the last page + * if it is the generic chrome://settings/. + * @private + */ + OptionsPage.updateHistoryState_ = function() { + var page = this.getTopmostVisiblePage(); + var path = location.pathname; + if (path) + path = path.slice(1).replace(/\/$/, ''); // Remove trailing slash. + // The page is already in history (the user may have clicked the same link + // twice). Do nothing. + if (path == page.name) + return; + + // If there is no path, the current location is chrome://settings/. + // Override this with the new page. + var historyFunction = path ? window.history.pushState : + window.history.replaceState; + historyFunction.call(window.history, + {pageName: page.name}, + page.title, + '/' + page.name); + // Update tab title. + document.title = page.title; + }; + + /** + * Shows a registered Overlay page. Does not update history. + * @param {string} overlayName Page name. + * @param {OptionPage} rootPage The currently visible root-level page. + * @return {boolean} whether we showed an overlay. + */ + OptionsPage.showOverlay_ = function(overlayName, rootPage) { + var overlay = this.registeredOverlayPages[overlayName.toLowerCase()]; + if (!overlay || !overlay.canShowPage()) + return false; + + if ((!rootPage || !rootPage.sticky) && overlay.parentPage) + this.showPageByName(overlay.parentPage.name, false); + + if (!overlay.visible) { + overlay.visible = true; + if (overlay.didShowPage) overlay.didShowPage(); + } + + return true; + }; + + /** + * Returns whether or not an overlay is visible. + * @return {boolean} True if an overlay is visible. + * @private + */ + OptionsPage.isOverlayVisible_ = function() { + return this.getVisibleOverlay_() != null; + }; + + /** + * Returns the currently visible overlay, or null if no page is visible. + * @return {OptionPage} The visible overlay. + */ + OptionsPage.getVisibleOverlay_ = function() { + for (var name in this.registeredOverlayPages) { + var page = this.registeredOverlayPages[name]; + if (page.visible) + return page; + } + return null; + }; + + /** + * Closes the visible overlay. Updates the history state after closing the + * overlay. + */ + OptionsPage.closeOverlay = function() { + var overlay = this.getVisibleOverlay_(); + if (!overlay) + return; + + overlay.visible = false; + if (overlay.didClosePage) overlay.didClosePage(); + this.updateHistoryState_(); + }; + + /** + * Hides the visible overlay. Does not affect the history state. + * @private + */ + OptionsPage.hideOverlay_ = function() { + var overlay = this.getVisibleOverlay_(); + if (overlay) + overlay.visible = false; + }; + + /** + * Returns the topmost visible page (overlays excluded). + * @return {OptionPage} The topmost visible page aside any overlay. + * @private + */ + OptionsPage.getTopmostVisibleNonOverlayPage_ = function() { + var topPage = null; + for (var name in this.registeredPages) { + var page = this.registeredPages[name]; + if (page.visible && + (!topPage || page.nestingLevel > topPage.nestingLevel)) + topPage = page; + } + + return topPage; + }; + + /** + * Returns the topmost visible page, or null if no page is visible. + * @return {OptionPage} The topmost visible page. + */ + OptionsPage.getTopmostVisiblePage = function() { + // Check overlays first since they're top-most if visible. + return this.getVisibleOverlay_() || this.getTopmostVisibleNonOverlayPage_(); + }; + + /** + * Closes the topmost open subpage, if any. + * @private + */ + OptionsPage.closeTopSubPage_ = function() { + var topPage = this.getTopmostVisiblePage(); + if (topPage && !topPage.isOverlay && topPage.parentPage) { + if (topPage.willHidePage) + topPage.willHidePage(); + topPage.visible = false; + } + + this.updateHistoryState_(); + }; + + /** + * Closes all subpages below the given level. + * @param {number} level The nesting level to close below. + */ + OptionsPage.closeSubPagesToLevel = function(level) { + var topPage = this.getTopmostVisiblePage(); + while (topPage && topPage.nestingLevel > level) { + if (topPage.willHidePage) + topPage.willHidePage(); + topPage.visible = false; + topPage = topPage.parentPage; + } + + this.updateHistoryState_(); + }; + + /** + * Updates managed banner visibility state based on the topmost page. + */ + OptionsPage.updateManagedBannerVisibility = function() { + var topPage = this.getTopmostVisiblePage(); + if (topPage) + topPage.updateManagedBannerVisibility(); + }; + + /** + * Shows the tab contents for the given navigation tab. + * @param {!Element} tab The tab that the user clicked. + */ + OptionsPage.showTab = function(tab) { + // Search parents until we find a tab, or the nav bar itself. This allows + // tabs to have child nodes, e.g. labels in separately-styled spans. + while (tab && !tab.classList.contains('subpages-nav-tabs') && + !tab.classList.contains('tab')) { + tab = tab.parentNode; + } + if (!tab || !tab.classList.contains('tab')) + return; + + // Find tab bar of the tab. + var tabBar = tab; + while (tabBar && !tabBar.classList.contains('subpages-nav-tabs')) { + tabBar = tabBar.parentNode; + } + if (!tabBar) + return; + + if (tabBar.activeNavTab != null) { + tabBar.activeNavTab.classList.remove('active-tab'); + $(tabBar.activeNavTab.getAttribute('tab-contents')).classList. + remove('active-tab-contents'); + } + + tab.classList.add('active-tab'); + $(tab.getAttribute('tab-contents')).classList.add('active-tab-contents'); + tabBar.activeNavTab = tab; + }; + + /** + * Registers new options page. + * @param {OptionsPage} page Page to register. + */ + OptionsPage.register = function(page) { + this.registeredPages[page.name.toLowerCase()] = page; + // Create and add new page <li> element to navbar. + var pageNav = document.createElement('li'); + pageNav.id = page.name + 'PageNav'; + pageNav.className = 'navbar-item'; + pageNav.setAttribute('pageName', page.name); + pageNav.setAttribute('role', 'tab'); + pageNav.textContent = page.pageDiv.querySelector('h1').textContent; + pageNav.tabIndex = -1; + pageNav.onclick = function(event) { + OptionsPage.navigateToPage(this.getAttribute('pageName')); + }; + pageNav.onkeydown = function(event) { + if ((event.keyCode == 37 || event.keyCode==38) && + this.previousSibling && this.previousSibling.onkeydown) { + // Left and up arrow moves back one tab. + OptionsPage.navigateToPage( + this.previousSibling.getAttribute('pageName')); + this.previousSibling.focus(); + } else if ((event.keyCode == 39 || event.keyCode == 40) && + this.nextSibling) { + // Right and down arrows move forward one tab. + OptionsPage.navigateToPage(this.nextSibling.getAttribute('pageName')); + this.nextSibling.focus(); + } + }; + pageNav.onkeypress = function(event) { + // Enter or space + if (event.keyCode == 13 || event.keyCode == 32) { + OptionsPage.navigateToPage(this.getAttribute('pageName')); + } + }; + var navbar = $('navbar'); + navbar.appendChild(pageNav); + page.tab = pageNav; + page.initializePage(); + }; + + /** + * Find an enclosing section for an element if it exists. + * @param {Element} element Element to search. + * @return {OptionPage} The section element, or null. + * @private + */ + OptionsPage.findSectionForNode_ = function(node) { + while (node = node.parentNode) { + if (node.nodeName == 'SECTION') + return node; + } + return null; + }; + + /** + * Registers a new Sub-page. + * @param {OptionsPage} subPage Sub-page to register. + * @param {OptionsPage} parentPage Associated parent page for this page. + * @param {Array} associatedControls Array of control elements that lead to + * this sub-page. The first item is typically a button in a root-level + * page. There may be additional buttons for nested sub-pages. + */ + OptionsPage.registerSubPage = function(subPage, + parentPage, + associatedControls) { + this.registeredPages[subPage.name.toLowerCase()] = subPage; + subPage.parentPage = parentPage; + if (associatedControls) { + subPage.associatedControls = associatedControls; + if (associatedControls.length) { + subPage.associatedSection = + this.findSectionForNode_(associatedControls[0]); + } + } + subPage.tab = undefined; + subPage.initializePage(); + }; + + /** + * Registers a new Overlay page. + * @param {OptionsPage} overlay Overlay to register. + * @param {OptionsPage} parentPage Associated parent page for this overlay. + * @param {Array} associatedControls Array of control elements associated with + * this page. + */ + OptionsPage.registerOverlay = function(overlay, + parentPage, + associatedControls) { + this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay; + overlay.parentPage = parentPage; + if (associatedControls) { + overlay.associatedControls = associatedControls; + if (associatedControls.length) { + overlay.associatedSection = + this.findSectionForNode_(associatedControls[0]); + } + } + + // Reverse the button strip for views. See the documentation of + // reverseButtonStrip_() for an explanation of why this is necessary. + if (cr.isViews) + this.reverseButtonStrip_(overlay); + + overlay.tab = undefined; + overlay.isOverlay = true; + overlay.initializePage(); + }; + + /** + * Reverses the child elements of a button strip. This is necessary because + * WebKit does not alter the tab order for elements that are visually reversed + * using -webkit-box-direction: reverse, and the button order is reversed for + * views. See https://bugs.webkit.org/show_bug.cgi?id=62664 for more + * information. + * @param {Object} overlay The overlay containing the button strip to reverse. + * @private + */ + OptionsPage.reverseButtonStrip_ = function(overlay) { + var buttonStrips = overlay.pageDiv.querySelectorAll('.button-strip'); + + // Reverse all button-strips in the overlay. + for (var j = 0; j < buttonStrips.length; j++) { + var buttonStrip = buttonStrips[j]; + + var childNodes = buttonStrip.childNodes; + for (var i = childNodes.length - 1; i >= 0; i--) + buttonStrip.appendChild(childNodes[i]); + } + }; + + /** + * Callback for window.onpopstate. + * @param {Object} data State data pushed into history. + */ + OptionsPage.setState = function(data) { + if (data && data.pageName) { + // It's possible an overlay may be the last top-level page shown. + if (this.isOverlayVisible_() && + !this.registeredOverlayPages[data.pageName.toLowerCase()]) { + this.hideOverlay_(); + } + + this.showPageByName(data.pageName, false); + } + }; + + /** + * Callback for window.onbeforeunload. Used to notify overlays that they will + * be closed. + */ + OptionsPage.willClose = function() { + var overlay = this.getVisibleOverlay_(); + if (overlay && overlay.didClosePage) + overlay.didClosePage(); + }; + + /** + * Freezes/unfreezes the scroll position of given level's page container. + * @param {boolean} freeze Whether the page should be frozen. + * @param {number} level The level to freeze/unfreeze. + * @private + */ + OptionsPage.setPageFrozenAtLevel_ = function(freeze, level) { + var container = level == 0 ? $('page-container') + : $('subpage-sheet-container-' + level); + + if (container.classList.contains('frozen') == freeze) + return; + + if (freeze) { + // Lock the width, since auto width computation may change. + container.style.width = window.getComputedStyle(container).width; + container.oldScrollTop = document.body.scrollTop; + container.classList.add('frozen'); + var verticalPosition = + container.getBoundingClientRect().top - container.oldScrollTop; + container.style.top = verticalPosition + 'px'; + this.updateFrozenElementHorizontalPosition_(container); + } else { + container.classList.remove('frozen'); + container.style.top = ''; + container.style.left = ''; + container.style.right = ''; + container.style.width = ''; + } + }; + + /** + * Freezes/unfreezes the scroll position of visible pages based on the current + * page stack. + */ + OptionsPage.updatePageFreezeStates = function() { + var topPage = OptionsPage.getTopmostVisiblePage(); + if (!topPage) + return; + var nestingLevel = topPage.isOverlay ? 100 : topPage.nestingLevel; + for (var i = 0; i <= SUBPAGE_SHEET_COUNT; i++) { + this.setPageFrozenAtLevel_(i < nestingLevel, i); + } + }; + + /** + * Initializes the complete options page. This will cause all C++ handlers to + * be invoked to do final setup. + */ + OptionsPage.initialize = function() { + chrome.send('coreOptionsInitialize'); + this.initialized_ = true; + + document.addEventListener('scroll', this.handleScroll_.bind(this)); + window.addEventListener('resize', this.handleResize_.bind(this)); + + if (!document.documentElement.classList.contains('hide-menu')) { + // Close subpages if the user clicks on the html body. Listen in the + // capturing phase so that we can stop the click from doing anything. + document.body.addEventListener('click', + this.bodyMouseEventHandler_.bind(this), + true); + // We also need to cancel mousedowns on non-subpage content. + document.body.addEventListener('mousedown', + this.bodyMouseEventHandler_.bind(this), + true); + + var self = this; + // Hook up the close buttons. + subpageCloseButtons = document.querySelectorAll('.close-subpage'); + for (var i = 0; i < subpageCloseButtons.length; i++) { + subpageCloseButtons[i].onclick = function() { + self.closeTopSubPage_(); + }; + }; + + // Install handler for key presses. + document.addEventListener('keydown', + this.keyDownEventHandler_.bind(this)); + + document.addEventListener('focus', this.manageFocusChange_.bind(this), + true); + } + + // Calculate and store the horizontal locations of elements that may be + // frozen later. + var sidebarWidth = + parseInt(window.getComputedStyle($('mainview')).webkitPaddingStart, 10); + $('page-container').horizontalOffset = sidebarWidth + + parseInt(window.getComputedStyle( + $('mainview-content')).webkitPaddingStart, 10); + for (var level = 1; level <= SUBPAGE_SHEET_COUNT; level++) { + var containerId = 'subpage-sheet-container-' + level; + $(containerId).horizontalOffset = sidebarWidth; + } + $('subpage-backdrop').horizontalOffset = sidebarWidth; + // Trigger the resize handler manually to set the initial state. + this.handleResize_(null); + }; + + /** + * Does a bounds check for the element on the given x, y client coordinates. + * @param {Element} e The DOM element. + * @param {number} x The client X to check. + * @param {number} y The client Y to check. + * @return {boolean} True if the point falls within the element's bounds. + * @private + */ + OptionsPage.elementContainsPoint_ = function(e, x, y) { + var clientRect = e.getBoundingClientRect(); + return x >= clientRect.left && x <= clientRect.right && + y >= clientRect.top && y <= clientRect.bottom; + }; + + /** + * Called when focus changes; ensures that focus doesn't move outside + * the topmost subpage/overlay. + * @param {Event} e The focus change event. + * @private + */ + OptionsPage.manageFocusChange_ = function(e) { + var focusableItemsRoot; + var topPage = this.getTopmostVisiblePage(); + if (!topPage) + return; + + if (topPage.isOverlay) { + // If an overlay is visible, that defines the tab loop. + focusableItemsRoot = topPage.pageDiv; + } else { + // If a subpage is visible, use its parent as the tab loop constraint. + // (The parent is used because it contains the close button.) + if (topPage.nestingLevel > 0) + focusableItemsRoot = topPage.pageDiv.parentNode; + } + + if (focusableItemsRoot && !focusableItemsRoot.contains(e.target)) + topPage.focusFirstElement(); + }; + + /** + * Called when the page is scrolled; moves elements that are position:fixed + * but should only behave as if they are fixed for vertical scrolling. + * @param {Event} e The scroll event. + * @private + */ + OptionsPage.handleScroll_ = function(e) { + var scrollHorizontalOffset = document.body.scrollLeft; + // position:fixed doesn't seem to work for horizontal scrolling in RTL mode, + // so only adjust in LTR mode (where scroll values will be positive). + if (scrollHorizontalOffset >= 0) { + $('navbar-container').style.left = -scrollHorizontalOffset + 'px'; + var subpageBackdrop = $('subpage-backdrop'); + subpageBackdrop.style.left = subpageBackdrop.horizontalOffset - + scrollHorizontalOffset + 'px'; + this.updateAllFrozenElementPositions_(); + } + }; + + /** + * Updates all frozen pages to match the horizontal scroll position. + * @private + */ + OptionsPage.updateAllFrozenElementPositions_ = function() { + var frozenElements = document.querySelectorAll('.frozen'); + for (var i = 0; i < frozenElements.length; i++) { + this.updateFrozenElementHorizontalPosition_(frozenElements[i]); + } + }; + + /** + * Updates the given frozen element to match the horizontal scroll position. + * @param {HTMLElement} e The frozen element to update + * @private + */ + OptionsPage.updateFrozenElementHorizontalPosition_ = function(e) { + if (document.documentElement.dir == 'rtl') + e.style.right = e.horizontalOffset + 'px'; + else + e.style.left = e.horizontalOffset - document.body.scrollLeft + 'px'; + }; + + /** + * Called when the page is resized; adjusts the size of elements that depend + * on the veiwport. + * @param {Event} e The resize event. + * @private + */ + OptionsPage.handleResize_ = function(e) { + // Set an explicit height equal to the viewport on all the subpage + // containers shorter than the viewport. This is used instead of + // min-height: 100% so that there is an explicit height for the subpages' + // min-height: 100%. + var viewportHeight = document.documentElement.clientHeight; + var subpageContainers = + document.querySelectorAll('.subpage-sheet-container'); + for (var i = 0; i < subpageContainers.length; i++) { + if (subpageContainers[i].scrollHeight > viewportHeight) + subpageContainers[i].style.removeProperty('height'); + else + subpageContainers[i].style.height = viewportHeight + 'px'; + } + }; + + /** + * A function to handle mouse events (mousedown or click) on the html body by + * closing subpages and/or stopping event propagation. + * @return {Event} a mousedown or click event. + * @private + */ + OptionsPage.bodyMouseEventHandler_ = function(event) { + // Do nothing if a subpage isn't showing. + var topPage = this.getTopmostVisiblePage(); + if (!topPage || topPage.isOverlay || !topPage.parentPage) + return; + + // Don't close subpages if a user is clicking in a select element. + // This is necessary because WebKit sends click events with strange + // coordinates when a user selects a new entry in a select element. + // See: http://crbug.com/87199 + if (event.srcElement.nodeName == 'SELECT') + return; + + // Do nothing if the client coordinates are not within the source element. + // This occurs if the user toggles a checkbox by pressing spacebar. + // This is a workaround to prevent keyboard events from closing the window. + // See: crosbug.com/15678 + if (event.clientX == -document.body.scrollLeft && + event.clientY == -document.body.scrollTop) { + return; + } + + // Don't interfere with navbar clicks. + if ($('navbar').contains(event.target)) + return; + + // Figure out which page the click happened in. + for (var level = topPage.nestingLevel; level >= 0; level--) { + var clickIsWithinLevel = level == 0 ? true : + OptionsPage.elementContainsPoint_( + $('subpage-sheet-' + level), event.clientX, event.clientY); + + if (!clickIsWithinLevel) + continue; + + // Event was within the topmost page; do nothing. + if (topPage.nestingLevel == level) + return; + + // Block propgation of both clicks and mousedowns, but only close subpages + // on click. + if (event.type == 'click') + this.closeSubPagesToLevel(level); + event.stopPropagation(); + event.preventDefault(); + return; + } + }; + + /** + * A function to handle key press events. + * @return {Event} a keydown event. + * @private + */ + OptionsPage.keyDownEventHandler_ = function(event) { + // Close the top overlay or sub-page on esc. + if (event.keyCode == 27) { // Esc + if (this.isOverlayVisible_()) + this.closeOverlay(); + else + this.closeTopSubPage_(); + } + }; + + OptionsPage.setClearPluginLSODataEnabled = function(enabled) { + if (enabled) { + document.documentElement.setAttribute( + 'flashPluginSupportsClearSiteData', ''); + } else { + document.documentElement.removeAttribute( + 'flashPluginSupportsClearSiteData'); + } + }; + + /** + * Re-initializes the C++ handlers if necessary. This is called if the + * handlers are torn down and recreated but the DOM may not have been (in + * which case |initialize| won't be called again). If |initialize| hasn't been + * called, this does nothing (since it will be later, once the DOM has + * finished loading). + */ + OptionsPage.reinitializeCore = function() { + if (this.initialized_) + chrome.send('coreOptionsInitialize'); + } + + OptionsPage.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * The parent page of this option page, or null for top-level pages. + * @type {OptionsPage} + */ + parentPage: null, + + /** + * The section on the parent page that is associated with this page. + * Can be null. + * @type {Element} + */ + associatedSection: null, + + /** + * An array of controls that are associated with this page. The first + * control should be located on a top-level page. + * @type {OptionsPage} + */ + associatedControls: null, + + /** + * Initializes page content. + */ + initializePage: function() {}, + + /** + * Updates managed banner visibility state. This function iterates over + * all input fields of a window and if any of these is marked as managed + * it triggers the managed banner to be visible. The banner can be enforced + * being on through the managed flag of this class but it can not be forced + * being off if managed items exist. + */ + updateManagedBannerVisibility: function() { + var bannerDiv = $('managed-prefs-banner'); + + var controlledByPolicy = false; + var controlledByExtension = false; + var inputElements = this.pageDiv.querySelectorAll('input[controlled-by]'); + for (var i = 0, len = inputElements.length; i < len; i++) { + if (inputElements[i].controlledBy == 'policy') + controlledByPolicy = true; + else if (inputElements[i].controlledBy == 'extension') + controlledByExtension = true; + } + if (!controlledByPolicy && !controlledByExtension) { + bannerDiv.hidden = true; + } else { + bannerDiv.hidden = false; + var height = window.getComputedStyle(bannerDiv).height; + if (controlledByPolicy && !controlledByExtension) { + $('managed-prefs-text').textContent = + templateData.policyManagedPrefsBannerText; + } else if (!controlledByPolicy && controlledByExtension) { + $('managed-prefs-text').textContent = + templateData.extensionManagedPrefsBannerText; + } else if (controlledByPolicy && controlledByExtension) { + $('managed-prefs-text').textContent = + templateData.policyAndExtensionManagedPrefsBannerText; + } + } + }, + + /** + * Gets page visibility state. + */ + get visible() { + return !this.pageDiv.hidden; + }, + + /** + * Sets page visibility. + */ + set visible(visible) { + if ((this.visible && visible) || (!this.visible && !visible)) + return; + + this.setContainerVisibility_(visible); + if (visible) { + this.pageDiv.hidden = false; + + if (this.tab) { + this.tab.classList.add('navbar-item-selected'); + this.tab.setAttribute('aria-selected', 'true'); + this.tab.tabIndex = 0; + } + } else { + this.pageDiv.hidden = true; + + if (this.tab) { + this.tab.classList.remove('navbar-item-selected'); + this.tab.setAttribute('aria-selected', 'false'); + this.tab.tabIndex = -1; + } + } + + OptionsPage.updatePageFreezeStates(); + + // The managed prefs banner is global, so after any visibility change + // update it based on the topmost page, not necessarily this page + // (e.g., if an ancestor is made visible after a child). + OptionsPage.updateManagedBannerVisibility(); + + // A subpage was shown or hidden. + if (!this.isOverlay && this.nestingLevel > 0) { + OptionsPage.updateSubpageBackdrop_(); + OptionsPage.updateScrollPosition_(); + } + + cr.dispatchPropertyChange(this, 'visible', visible, !visible); + }, + + /** + * Shows or hides this page's container. + * @param {boolean} visible Whether the container should be visible or not. + * @private + */ + setContainerVisibility_: function(visible) { + var container = null; + if (this.isOverlay) { + container = $('overlay'); + } else { + var nestingLevel = this.nestingLevel; + if (nestingLevel > 0) + container = $('subpage-sheet-container-' + nestingLevel); + } + var isSubpage = !this.isOverlay; + + if (!container) + return; + + if (container.hidden != visible) { + if (visible) { + // If the container is set hidden and then immediately set visible + // again, the fadeCompleted_ callback would cause it to be erroneously + // hidden again. Removing the transparent tag avoids that. + container.classList.remove('transparent'); + } + return; + } + + if (visible) { + container.hidden = false; + if (isSubpage) { + var computedStyle = window.getComputedStyle(container); + container.style.WebkitPaddingStart = + parseInt(computedStyle.WebkitPaddingStart, 10) + 100 + 'px'; + } + // Separate animating changes from the removal of display:none. + window.setTimeout(function() { + container.classList.remove('transparent'); + if (isSubpage) + container.style.WebkitPaddingStart = ''; + }); + } else { + var self = this; + container.addEventListener('webkitTransitionEnd', function f(e) { + if (e.propertyName != 'opacity') + return; + container.removeEventListener('webkitTransitionEnd', f); + self.fadeCompleted_(container); + }); + container.classList.add('transparent'); + } + }, + + /** + * Called when a container opacity transition finishes. + * @param {HTMLElement} container The container element. + * @private + */ + fadeCompleted_: function(container) { + if (container.classList.contains('transparent')) + container.hidden = true; + }, + + /** + * Focuses the first control on the page. + */ + focusFirstElement: function() { + // Sets focus on the first interactive element in the page. + var focusElement = + this.pageDiv.querySelector('button, input, list, select'); + if (focusElement) + focusElement.focus(); + }, + + /** + * The nesting level of this page. + * @type {number} The nesting level of this page (0 for top-level page) + */ + get nestingLevel() { + var level = 0; + var parent = this.parentPage; + while (parent) { + level++; + parent = parent.parentPage; + } + return level; + }, + + /** + * Whether the page is considered 'sticky', such that it will + * remain a top-level page even if sub-pages change. + * @type {boolean} True if this page is sticky. + */ + get sticky() { + return false; + }, + + /** + * Checks whether this page is an ancestor of the given page in terms of + * subpage nesting. + * @param {OptionsPage} page + * @return {boolean} True if this page is nested under |page| + */ + isAncestorOfPage: function(page) { + var parent = page.parentPage; + while (parent) { + if (parent == this) + return true; + parent = parent.parentPage; + } + return false; + }, + + /** + * Whether it should be possible to show the page. + * @return {boolean} True if the page should be shown + */ + canShowPage: function() { + return true; + }, + }; + + // Export + return { + OptionsPage: OptionsPage + }; +}); diff --git a/chrome/browser/resources/options2/pack_extension_overlay.css b/chrome/browser/resources/options2/pack_extension_overlay.css new file mode 100644 index 0000000..169750b --- /dev/null +++ b/chrome/browser/resources/options2/pack_extension_overlay.css @@ -0,0 +1,18 @@ +/* +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. +*/ + +.packExtensionHeading { + width: 520px; + padding-bottom: 5px; +} + +.packExtensionTextBoxes { + text-align: right; +} + +.packExtensionTextArea { + width: 260px; +} diff --git a/chrome/browser/resources/options2/pack_extension_overlay.html b/chrome/browser/resources/options2/pack_extension_overlay.html new file mode 100644 index 0000000..bd77789 --- /dev/null +++ b/chrome/browser/resources/options2/pack_extension_overlay.html @@ -0,0 +1,28 @@ +<div id="packExtensionOverlay" class="page" hidden> + <h1 i18n-content="packExtensionOverlay"></h1> + <div id="cbdContentArea" class="content-area"> + <div class="packExtensionHeading" i18n-content="packExtensionHeading"></div> + <div class="packExtensionTextBoxes"> + <label i18n-content="packExtensionRootDir"></label> + <input class="packExtensionTextArea" id="extensionRootDir" type="text" /> + <button id="browseExtensionDir" + i18n-content="packExtensionBrowseButton"></button> + </div> + <div class="packExtensionTextBoxes"> + <label i18n-content="packExtensionPrivateKey"></label> + <input class="packExtensionTextArea" + id="extensionPrivateKey" type="text" /> + <button id="browsePrivateKey" + i18n-content="packExtensionBrowseButton"></button> + </div> + </div> + <div class="action-area"> + <div class="action-area-right"> + <div class="button-strip"> + <button id="packExtensionDismiss" i18n-content="cancel"></button> + <button id="packExtensionCommit" + i18n-content="packExtensionCommit"></button> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/chrome/browser/resources/options2/pack_extension_overlay.js b/chrome/browser/resources/options2/pack_extension_overlay.js new file mode 100644 index 0000000..4154dcd --- /dev/null +++ b/chrome/browser/resources/options2/pack_extension_overlay.js @@ -0,0 +1,90 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * PackExtensionOverlay class + * Encapsulated handling of the 'Pack Extension' overlay page. + * @constructor + */ + function PackExtensionOverlay() { + OptionsPage.call(this, 'packExtensionOverlay', + templateData.packExtensionOverlayTabTitle, + 'packExtensionOverlay'); + } + + cr.addSingletonGetter(PackExtensionOverlay); + + PackExtensionOverlay.prototype = { + // Inherit PackExtensionOverlay from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to starts preference initialization. + OptionsPage.prototype.initializePage.call(this); + + $('packExtensionDismiss').onclick = function(event) { + OptionsPage.closeOverlay(); + }; + $('packExtensionCommit').onclick = function(event) { + var extensionPath = $('extensionRootDir').value; + var privateKeyPath = $('extensionPrivateKey').value; + chrome.send('pack', [extensionPath, privateKeyPath]); + }; + $('browseExtensionDir').addEventListener('click', + this.handleBrowseExtensionDir_.bind(this)); + $('browsePrivateKey').addEventListener('click', + this.handleBrowsePrivateKey_.bind(this)); + }, + + /** + * Utility function which asks the C++ to show a platform-specific file + * select dialog, and fire |callback| with the |filePath| that resulted. + * |selectType| can be either 'file' or 'folder'. |operation| can be 'load', + * 'packRoot', or 'pem' which are signals to the C++ to do some + * operation-specific configuration. + * @private + */ + showFileDialog_: function(selectType, operation, callback) { + handleFilePathSelected = function(filePath) { + callback(filePath); + handleFilePathSelected = function() {}; + }; + + chrome.send('extensionSettingsSelectFilePath', [selectType, operation]); + }, + + /** + * Handles the showing of the extension directory browser. + * @param {Event} e Change event. + * @private + */ + handleBrowseExtensionDir_: function(e) { + this.showFileDialog_('folder', 'load', function(filePath) { + $('extensionRootDir').value = filePath; + }); + }, + + /** + * Handles the showing of the extension private key file. + * @param {Event} e Change event. + * @private + */ + handleBrowsePrivateKey_: function(e) { + this.showFileDialog_('file', 'load', function(filePath) { + $('extensionPrivateKey').value = filePath; + }); + }, + }; + + // Export + return { + PackExtensionOverlay: PackExtensionOverlay + }; +}); diff --git a/chrome/browser/resources/options2/password_manager.css b/chrome/browser/resources/options2/password_manager.css new file mode 100644 index 0000000..76ec562 --- /dev/null +++ b/chrome/browser/resources/options2/password_manager.css @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#password-search-column { + bottom: 10px; + position: absolute; + right: 0; +} + +html[dir=rtl] #password-search-column { + left: 0; + right: auto; +} + +#password-list-headers { + position: relative; + width: 100%; +} + +#password-list-headers h3 { + font-size: 105%; + font-weight: bold; + margin: 10px 0; +} + +#passwords-title { + display: inline-block; +} diff --git a/chrome/browser/resources/options2/password_manager.html b/chrome/browser/resources/options2/password_manager.html new file mode 100644 index 0000000..333042f --- /dev/null +++ b/chrome/browser/resources/options2/password_manager.html @@ -0,0 +1,28 @@ +<div id="password-manager" class="page" hidden> + <h1 i18n-content="passwordsPage"></h1> + <div id="password-list-headers"> + <div id="passwords-title"> + <h3 i18n-content="savedPasswordsTitle"></h3> + </div> + <div id="password-search-column"> + <input id="password-search-box" type="search" + i18n-values="placeholder:passwordSearchPlaceholder" incremental + results="10" autosave="org.chromium.options.passwords.search"> + </div> + </div> + <list id="saved-passwords-list" class="settings-list"></list> + <div id="saved-passwords-list-empty-placeholder" + class="settings-list-empty" hidden> + <span i18n-content="passwordsNoPasswordsDescription"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:passwordManagerLearnMoreURL"></a> + </div> + <h3 i18n-content="passwordExceptionsTitle"></h3> + <list id="password-exceptions-list" class="settings-list"></list> + <div id="password-exceptions-list-empty-placeholder" hidden + class="settings-list-empty"> + <span i18n-content="passwordsNoExceptionsDescription"></span> + <a target="_blank" i18n-content="learnMore" + i18n-values="href:passwordManagerLearnMoreURL"></a> + </div> +</div> diff --git a/chrome/browser/resources/options2/password_manager.js b/chrome/browser/resources/options2/password_manager.js new file mode 100644 index 0000000..273d2c0 --- /dev/null +++ b/chrome/browser/resources/options2/password_manager.js @@ -0,0 +1,228 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + ///////////////////////////////////////////////////////////////////////////// + // PasswordManager class: + + /** + * Encapsulated handling of password and exceptions page. + * @constructor + */ + function PasswordManager() { + this.activeNavTab = null; + OptionsPage.call(this, + 'passwords', + templateData.passwordsPageTabTitle, + 'password-manager'); + } + + cr.addSingletonGetter(PasswordManager); + + PasswordManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * The saved passwords list. + * @type {DeletableItemList} + * @private + */ + savedPasswordsList_: null, + + /** + * The password exceptions list. + * @type {DeletableItemList} + * @private + */ + passwordExceptionsList_: null, + + /** + * The timer id of the timer set on search query change events. + * @type {number} + * @private + */ + queryDelayTimerId_: 0, + + /** + * The most recent search query, or null if the query is empty. + * @type {?string} + * @private + */ + lastQuery_: null, + + /** @inheritDoc */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + $('password-search-box').addEventListener('search', + this.handleSearchQueryChange_.bind(this)); + + this.createSavedPasswordsList_(); + this.createPasswordExceptionsList_(); + }, + + /** @inheritDoc */ + canShowPage: function() { + return !PersonalOptions.disablePasswordManagement(); + }, + + /** @inheritDoc */ + didShowPage: function() { + // Updating the password lists may cause a blocking platform dialog pop up + // (Mac, Linux), so we delay this operation until the page is shown. + chrome.send('updatePasswordLists'); + $('password-search-box').focus(); + }, + + /** + * Creates, decorates and initializes the saved passwords list. + * @private + */ + createSavedPasswordsList_: function() { + this.savedPasswordsList_ = $('saved-passwords-list'); + options.passwordManager.PasswordsList.decorate(this.savedPasswordsList_); + this.savedPasswordsList_.autoExpands = true; + }, + + /** + * Creates, decorates and initializes the password exceptions list. + * @private + */ + createPasswordExceptionsList_: function() { + this.passwordExceptionsList_ = $('password-exceptions-list'); + options.passwordManager.PasswordExceptionsList.decorate( + this.passwordExceptionsList_); + this.passwordExceptionsList_.autoExpands = true; + }, + + /** + * Handles search query changes. + * @param {!Event} e The event object. + * @private + */ + handleSearchQueryChange_: function(e) { + if (this.queryDelayTimerId_) + window.clearTimeout(this.queryDelayTimerId_); + + // Searching cookies uses a timeout of 500ms. We use a shorter timeout + // because there are probably fewer passwords and we want the UI to be + // snappier since users will expect that it's "less work." + this.queryDelayTimerId_ = window.setTimeout( + this.searchPasswords_.bind(this), 250); + }, + + /** + * Search passwords using text in |password-search-box|. + * @private + */ + searchPasswords_: function() { + this.queryDelayTimerId_ = 0; + var filter = $('password-search-box').value; + filter = (filter == '') ? null : filter; + if (this.lastQuery_ != filter) { + this.lastQuery_ = filter; + // Searching for passwords has the side effect of requerying the + // underlying password store. This is done intentionally, as on OS X and + // Linux they can change from outside and we won't be notified of it. + chrome.send('updatePasswordLists'); + } + }, + + /** + * Updates the visibility of the list and empty list placeholder. + * @param {!List} list The list to toggle visilibility for. + */ + updateListVisibility_: function(list) { + var empty = list.dataModel.length == 0; + var listPlaceHolderID = list.id + '-empty-placeholder'; + list.hidden = empty; + $(listPlaceHolderID).hidden = !empty; + }, + + /** + * Updates the data model for the saved passwords list with the values from + * |entries|. + * @param {Array} entries The list of saved password data. + */ + setSavedPasswordsList_: function(entries) { + if (this.lastQuery_) { + // Implement password searching here in javascript, rather than in C++. + // The number of saved passwords shouldn't be too big for us to handle. + var query = this.lastQuery_; + var filter = function(entry, index, list) { + // Search both URL and username. + if (entry[0].indexOf(query) >= 0 || entry[1].indexOf(query) >= 0) { + // Keep the original index so we can delete correctly. See also + // deleteItemAtIndex() in password_manager_list.js that uses this. + entry[3] = index; + return true; + } + return false; + }; + entries = entries.filter(filter); + } + this.savedPasswordsList_.dataModel = new ArrayDataModel(entries); + this.updateListVisibility_(this.savedPasswordsList_); + }, + + /** + * Updates the data model for the password exceptions list with the values + * from |entries|. + * @param {Array} entries The list of password exception data. + */ + setPasswordExceptionsList_: function(entries) { + this.passwordExceptionsList_.dataModel = new ArrayDataModel(entries); + this.updateListVisibility_(this.passwordExceptionsList_); + }, + }; + + /** + * Call to remove a saved password. + * @param rowIndex indicating the row to remove. + */ + PasswordManager.removeSavedPassword = function(rowIndex) { + chrome.send('removeSavedPassword', [String(rowIndex)]); + }; + + /** + * Call to remove a password exception. + * @param rowIndex indicating the row to remove. + */ + PasswordManager.removePasswordException = function(rowIndex) { + chrome.send('removePasswordException', [String(rowIndex)]); + }; + + /** + * Call to remove all saved passwords. + * @param tab contentType of the tab currently on. + */ + PasswordManager.removeAllPasswords = function() { + chrome.send('removeAllSavedPasswords'); + }; + + /** + * Call to remove all saved passwords. + * @param tab contentType of the tab currently on. + */ + PasswordManager.removeAllPasswordExceptions = function() { + chrome.send('removeAllPasswordExceptions'); + }; + + PasswordManager.setSavedPasswordsList = function(entries) { + PasswordManager.getInstance().setSavedPasswordsList_(entries); + }; + + PasswordManager.setPasswordExceptionsList = function(entries) { + PasswordManager.getInstance().setPasswordExceptionsList_(entries); + }; + + // Export + return { + PasswordManager: PasswordManager + }; + +}); diff --git a/chrome/browser/resources/options2/password_manager_list.css b/chrome/browser/resources/options2/password_manager_list.css new file mode 100644 index 0000000..576ecfd --- /dev/null +++ b/chrome/browser/resources/options2/password_manager_list.css @@ -0,0 +1,69 @@ +/* + * 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. + */ + +button.password-button { + -webkit-transition: opacity .15s; + background: #8aaaed; + color: #fff; + display: inline; + font-size: 90%; + font-weight: bold; + height: 18px; + opacity: 0.3; + padding: 0 2px; + position: absolute; + top: 5px; +} + +button.password-button:hover { + -webkit-transition: opacity .15s; + opacity: 1; +} + +html[dir='ltr'] button.password-button { + right: 2px; +} + +html[dir='rtl'] button.password-button { + left: 2px; +} + +input[type="password"].inactive-password { + background: transparent; + border: none; +} + +#saved-passwords-list .url { + box-sizing: border-box; + width: 40%; +} + +#saved-passwords-list .name { + -webkit-box-flex: 1; + width: 20%; +} + +#saved-passwords-list .password { + -webkit-box-flex: 1; + position: relative; +} + +#saved-passwords-list .password input[type="password"], +#saved-passwords-list .password input[type="text"] { + box-sizing: border-box; + width: 100%; +} + +#password-exceptions-list .url { + -webkit-box-flex: 1; +} + +#saved-passwords-list .url, +#saved-passwords-list .name, +#password-exceptions-list .url { + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/chrome/browser/resources/options2/password_manager_list.js b/chrome/browser/resources/options2/password_manager_list.js new file mode 100644 index 0000000..c3b822a --- /dev/null +++ b/chrome/browser/resources/options2/password_manager_list.js @@ -0,0 +1,283 @@ +// 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. + +cr.define('options.passwordManager', function() { + const ArrayDataModel = cr.ui.ArrayDataModel; + const DeletableItemList = options.DeletableItemList; + const DeletableItem = options.DeletableItem; + const List = cr.ui.List; + + /** + * Creates a new passwords list item. + * @param {Array} entry An array of the form [url, username, password]. When + * the list has been filtered, a fourth element [index] may be present. + * @constructor + * @extends {cr.ui.ListItem} + */ + function PasswordListItem(entry, showPasswords) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = PasswordListItem.prototype; + el.decorate(showPasswords); + + return el; + } + + PasswordListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** @inheritDoc */ + decorate: function(showPasswords) { + DeletableItem.prototype.decorate.call(this); + + // The URL of the site. + var urlLabel = this.ownerDocument.createElement('div'); + urlLabel.classList.add('favicon-cell'); + urlLabel.classList.add('weakrtl'); + urlLabel.classList.add('url'); + urlLabel.setAttribute('title', this.url); + urlLabel.textContent = this.url; + urlLabel.style.backgroundImage = url('chrome://favicon/' + this.url); + this.contentElement.appendChild(urlLabel); + + // The stored username. + var usernameLabel = this.ownerDocument.createElement('div'); + usernameLabel.className = 'name'; + usernameLabel.textContent = this.username; + this.contentElement.appendChild(usernameLabel); + + // The stored password. + var passwordInputDiv = this.ownerDocument.createElement('div'); + passwordInputDiv.className = 'password'; + + // The password input field. + var passwordInput = this.ownerDocument.createElement('input'); + passwordInput.type = 'password'; + passwordInput.className = 'inactive-password'; + passwordInput.readOnly = true; + passwordInput.value = showPasswords ? this.password : '********'; + passwordInputDiv.appendChild(passwordInput); + + // The show/hide button. + if (showPasswords) { + var button = this.ownerDocument.createElement('button'); + button.hidden = true; + button.classList.add('password-button'); + button.textContent = localStrings.getString('passwordShowButton'); + button.addEventListener('click', this.onClick_, true); + passwordInputDiv.appendChild(button); + } + + this.contentElement.appendChild(passwordInputDiv); + }, + + /** @inheritDoc */ + selectionChanged: function() { + var passwordInput = this.querySelector('input[type=password]'); + var textInput = this.querySelector('input[type=text]'); + var input = passwordInput || textInput; + var button = input.nextSibling; + // |button| doesn't exist when passwords can't be shown. + if (!button) + return; + if (this.selected) { + input.classList.remove('inactive-password'); + button.hidden = false; + } else { + input.classList.add('inactive-password'); + button.hidden = true; + } + }, + + /** + * On-click event handler. Swaps the type of the input field from password + * to text and back. + * @private + */ + onClick_: function(event) { + // The password is the input element previous to the button. + var button = event.currentTarget; + var passwordInput = button.previousSibling; + if (passwordInput.type == 'password') { + passwordInput.type = 'text'; + button.textContent = localStrings.getString('passwordHideButton'); + } else { + passwordInput.type = 'password'; + button.textContent = localStrings.getString('passwordShowButton'); + } + }, + + /** + * Get and set the URL for the entry. + * @type {string} + */ + get url() { + return this.dataItem[0]; + }, + set url(url) { + this.dataItem[0] = url; + }, + + /** + * Get and set the username for the entry. + * @type {string} + */ + get username() { + return this.dataItem[1]; + }, + set username(username) { + this.dataItem[1] = username; + }, + + /** + * Get and set the password for the entry. + * @type {string} + */ + get password() { + return this.dataItem[2]; + }, + set password(password) { + this.dataItem[2] = password; + }, + }; + + /** + * Creates a new PasswordExceptions list item. + * @param {Array} entry A pair of the form [url, username]. + * @constructor + * @extends {Deletable.ListItem} + */ + function PasswordExceptionsListItem(entry) { + var el = cr.doc.createElement('div'); + el.dataItem = entry; + el.__proto__ = PasswordExceptionsListItem.prototype; + el.decorate(); + + return el; + } + + PasswordExceptionsListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Call when an element is decorated as a list item. + */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + // The URL of the site. + var urlLabel = this.ownerDocument.createElement('div'); + urlLabel.className = 'url'; + urlLabel.classList.add('favicon-cell'); + urlLabel.classList.add('weakrtl'); + urlLabel.textContent = this.url; + urlLabel.style.backgroundImage = url('chrome://favicon/' + this.url); + this.contentElement.appendChild(urlLabel); + }, + + /** + * Get the url for the entry. + * @type {string} + */ + get url() { + return this.dataItem; + }, + set url(url) { + this.dataItem = url; + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var PasswordsList = cr.ui.define('list'); + + PasswordsList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Whether passwords can be revealed or not. + * @type {boolean} + * @private + */ + showPasswords_: true, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + Preferences.getInstance().addEventListener( + "profile.password_manager_allow_show_passwords", + this.onPreferenceChanged_.bind(this)); + }, + + /** + * Listener for changes on the preference. + * @param {Event} event The preference update event. + * @private + */ + onPreferenceChanged_: function(event) { + this.showPasswords_ = event.value.value; + this.redraw(); + }, + + /** @inheritDoc */ + createItem: function(entry) { + return new PasswordListItem(entry, this.showPasswords_); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var item = this.dataModel.item(index); + if (item && item.length > 3) { + // The fourth element, if present, is the original index to delete. + index = item[3]; + } + PasswordManager.removeSavedPassword(index); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + }; + + /** + * Create a new passwords list. + * @constructor + * @extends {cr.ui.List} + */ + var PasswordExceptionsList = cr.ui.define('list'); + + PasswordExceptionsList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + createItem: function(entry) { + return new PasswordExceptionsListItem(entry); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + PasswordManager.removePasswordException(index); + }, + + /** + * The length of the list. + */ + get length() { + return this.dataModel.length; + }, + }; + + return { + PasswordListItem: PasswordListItem, + PasswordExceptionsListItem: PasswordExceptionsListItem, + PasswordsList: PasswordsList, + PasswordExceptionsList: PasswordExceptionsList, + }; +}); diff --git a/chrome/browser/resources/options2/personal_options.css b/chrome/browser/resources/options2/personal_options.css new file mode 100644 index 0000000..ee18893 --- /dev/null +++ b/chrome/browser/resources/options2/personal_options.css @@ -0,0 +1,66 @@ +#account-picture-wrapper { + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.3); + display: inline-block; + margin: 5px 10px 5px 2px; + padding: 3px; +} + +#account-picture { + width: 70px; + height: 70px; + vertical-align: middle; +} + +#sync-buttons, #profiles-buttons { + margin-top: 10px; +} + +#start-stop-sync { + margin-left: 0; + margin-right: 5px; +} + +#profiles-list { + min-height: 0; + margin-bottom: 10px; +} + +#profiles-list > * { + height: 40px; +} + +.profile-img { + height: 31px; + padding: 3px; + vertical-align: middle; + width: 38px; +} + +.profile-item-current { + font-weight: bold; +} + +#themes-gallery-div { + margin: 10px 0; +} + +.sync-error { + background: #FFDBDB; + border: 1px solid #ce4c4c; + border-radius: 2px; + padding: 10px; +} + +.sync-error .link-button { + margin: 0 1ex; + padding: 0; +} + +#enable-auto-login-checkbox { + margin-top: 10px; +} + +#mac-passwords-warning { + margin-top: 10px; +} diff --git a/chrome/browser/resources/options2/personal_options.html b/chrome/browser/resources/options2/personal_options.html new file mode 100644 index 0000000..620ec06 --- /dev/null +++ b/chrome/browser/resources/options2/personal_options.html @@ -0,0 +1,162 @@ +<div id="personal-page" class="page" hidden> + <h1 i18n-content="personalPage"></h1> + <div class="displaytable"> +<if expr="pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="account"></h3> + <div> + <span id="account-picture-wrapper"> + <img id="account-picture" + src="chrome://theme/IDR_PROFILE_PICTURE_LOADING"> + </span> + <label> + <input id="enable-screen-lock" type="checkbox" + pref="settings.enable_screen_lock"> + <span i18n-content="enableScreenlock"></span> + </label> + <br> + <button id="change-picture-button" i18n-content="changePicture"> + </button> + </div> + </section> +</if> + <section id="sync-section"> + <h3 i18n-content="syncSection"></h3> + <div> + <div id="sync-overview" hidden> + <span i18n-content="syncOverview"></span> + <a i18n-values="href:syncLearnMoreURL" i18n-content="learnMore"></a> + </div> + <div id="sync-status" hidden> + <span id="sync-status-text"></span> + <button id="sync-action-link" class="link-button"></button> + </div> + <div id="sync-buttons"> + <button id="start-stop-sync" hidden></button> + <button id="customize-sync" i18n-content="customizeSync" hidden> + </button> + <div id="enable-auto-login-checkbox" class="checkbox" hidden> + <label> + <input id="enable-auto-login" pref="autologin.enabled" + metric="Options_Autologin" type="checkbox"> + <span i18n-content="autologinEnabled"></span> + </label> + </div> + </div> + </div> + </section> + <section id="profiles-section" hidden> + <h3 i18n-content="profiles"></h3> + <div> + <list id="profiles-list" class="settings-list" hidden></list> + <div id="profiles-single-message" i18n-content="profilesSingleUser"> + </div> + <div id="profiles-buttons"> + <button id="profiles-create" i18n-content="profilesCreate"></button> + <button id="profiles-manage" i18n-content="profilesManage" disabled> + </button> + <button id="profiles-delete" i18n-content="profilesDelete"> + </button> + </div> + </div> + </section> + <section> + <h3 i18n-content="passwords"></h3> + <div> + <div class="radio"> + <label> + <input id="passwords-offersave" type="radio" name="passwords_radio" + value="true" pref="profile.password_manager_enabled" + metric="Options_PasswordManager"> + <span i18n-content="passwordsAskToSave"></span> + </label> + </div> + <div class="radio"> + <label> + <input id="passwords-neversave" type="radio" name="passwords_radio" + value="false" pref="profile.password_manager_enabled" + metric="Options_PasswordManager"> + <span i18n-content="passwordsNeverSave"></span> + </label> + </div> + <div><button id="manage-passwords" i18n-content="manage_passwords" + pref="profile.password_manager_enabled"></button></div> + <div id="mac-passwords-warning" i18n-content="macPasswordsWarning" + hidden></div> + <!-- This makes the managed-banner appear when the "pref" is + configured by the IT administrator. --> + <input name="password_allow_show_hidden" type="text" + pref="profile.password_manager_allow_show_passwords" hidden> + </div> + </section> + <section id="autofill-section"> + <h3 i18n-content="autofill"></h3> + <div> + <div class="checkbox"> + <label> + <input id="autofill-enabled" pref="autofill.enabled" + metric="Options_FormAutofill" type="checkbox"> + <span i18n-content="autofillEnabled"></span> + </label> + </div> + <button id="autofill-settings" pref="autofill.enabled" + i18n-content="manageAutofillSettings"></button> + </div> + </section> +<if expr="not pp_ifdef('chromeos')"> + <section> + <h3 i18n-content="browsingData"></h3> + <div> + <button id="import-data" i18n-content="importData"></button> + </div> + </section> +</if> +<if expr="not pp_ifdef('toolkit_views') and is_posix and not is_macosx"> + <section> + <h3 i18n-content="appearance"></h3> + <div> + <div> + <button id="themes-GTK-button" + i18n-content="themesGTKButton"></button> + <button id="themes-reset" + i18n-content="themesSetClassic"></button> + </div> + <div id="themes-gallery-div"> + <a id="themes-gallery" i18n-content="themesGallery" + i18n-values="href:themesGalleryURL" target="_blank"></a> + </div> + <div class="radio"> + <label> + <input name="decorations_radio" + pref="browser.custom_chrome_frame" + type="radio" value="false" metric="Options_CustomFrame"> + <span i18n-content="showWindowDecorations"></span> + </label> + </div> + <div class="radio"> + <label> + <input name="decorations_radio" + pref="browser.custom_chrome_frame" + type="radio" value="true" metric="Options_CustomFrame"> + <span i18n-content="hideWindowDecorations"></span> + </label> + </div> + </div> + </section> +</if> +<if expr="pp_ifdef('toolkit_views') or os == 'win32' or os == 'darwin'"> + <section> + <h3 i18n-content="themes"></h3> + <div> + <div> + <button id="themes-reset" i18n-content="themesReset"></button> + </div> + <div id="themes-gallery-div"> + <a id="themes-gallery" i18n-content="themesGallery" + i18n-values="href:themesGalleryURL" target="_blank"></a> + </div> + </div> + </section> +</if> + </div> +</div> diff --git a/chrome/browser/resources/options2/personal_options.js b/chrome/browser/resources/options2/personal_options.js new file mode 100644 index 0000000..78d40dc --- /dev/null +++ b/chrome/browser/resources/options2/personal_options.js @@ -0,0 +1,372 @@ +// 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. + +cr.define('options', function() { + + var OptionsPage = options.OptionsPage; + var ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Encapsulated handling of personal options page. + * @constructor + */ + function PersonalOptions() { + OptionsPage.call(this, 'personal', + templateData.personalPageTabTitle, + 'personal-page'); + if (cr.isChromeOS) { + // Username (canonical email) of the currently logged in user or + // |kGuestUser| if a guest session is active. + this.username_ = localStrings.getString('username'); + } + } + + cr.addSingletonGetter(PersonalOptions); + + PersonalOptions.prototype = { + // Inherit PersonalOptions from OptionsPage. + __proto__: options.OptionsPage.prototype, + + // State variables. + syncEnabled: false, + syncSetupCompleted: false, + + // Initialize PersonalOptions page. + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + + // Sync. + $('sync-action-link').onclick = function(event) { + SyncSetupOverlay.showErrorUI(); + }; + $('start-stop-sync').onclick = function(event) { + if (self.syncSetupCompleted) + SyncSetupOverlay.showStopSyncingUI(); + else + SyncSetupOverlay.showSetupUI(); + }; + $('customize-sync').onclick = function(event) { + SyncSetupOverlay.showSetupUI(); + }; + + // Profiles. + var profilesList = $('profiles-list'); + options.personal_options.ProfileList.decorate(profilesList); + profilesList.autoExpands = true; + + profilesList.onchange = self.setProfileViewButtonsStatus_; + $('profiles-create').onclick = function(event) { + chrome.send('createProfile'); + }; + $('profiles-manage').onclick = function(event) { + var selectedProfile = self.getSelectedProfileItem_(); + if (selectedProfile) + ManageProfileOverlay.showManageDialog(selectedProfile); + }; + $('profiles-delete').onclick = function(event) { + var selectedProfile = self.getSelectedProfileItem_(); + if (selectedProfile) + ManageProfileOverlay.showDeleteDialog(selectedProfile); + }; + + // Passwords. + $('manage-passwords').onclick = function(event) { + OptionsPage.navigateToPage('passwords'); + OptionsPage.showTab($('passwords-nav-tab')); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ShowPasswordManager']); + }; + + // Autofill. + $('autofill-settings').onclick = function(event) { + OptionsPage.navigateToPage('autofill'); + chrome.send('coreOptionsUserMetricsAction', + ['Options_ShowAutofillSettings']); + }; + if (cr.isChromeOS && cr.commandLine && cr.commandLine.options['--bwsi']) { + // Hide Autofill options for the guest user. + $('autofill-section').hidden = true; + } + + // Appearance. + $('themes-reset').onclick = function(event) { + chrome.send('themesReset'); + }; + + if (!cr.isChromeOS) { + $('import-data').onclick = function(event) { + // Make sure that any previous import success message is hidden, and + // we're showing the UI to import further data. + $('import-data-configure').hidden = false; + $('import-data-success').hidden = true; + OptionsPage.navigateToPage('importData'); + chrome.send('coreOptionsUserMetricsAction', ['Import_ShowDlg']); + }; + + if ($('themes-GTK-button')) { + $('themes-GTK-button').onclick = function(event) { + chrome.send('themesSetGTK'); + }; + } + } else { + $('change-picture-button').onclick = function(event) { + OptionsPage.navigateToPage('changePicture'); + }; + this.updateAccountPicture_(); + + if (cr.commandLine && cr.commandLine.options['--bwsi']) { + // Disable the screen lock checkbox and change-picture-button in + // guest mode. + $('enable-screen-lock').disabled = true; + $('change-picture-button').disabled = true; + } + } + + if (PersonalOptions.disablePasswordManagement()) { + // Disable the Password Manager in guest mode. + $('passwords-offersave').disabled = true; + $('passwords-neversave').disabled = true; + $('passwords-offersave').value = false; + $('passwords-neversave').value = true; + $('manage-passwords').disabled = true; + } + + $('mac-passwords-warning').hidden = + !(localStrings.getString('macPasswordsWarning')); + + if (PersonalOptions.disableAutofillManagement()) { + $('autofill-settings').disabled = true; + + // Disable and turn off autofill. + var autofillEnabled = $('autofill-enabled'); + autofillEnabled.disabled = true; + autofillEnabled.checked = false; + cr.dispatchSimpleEvent(autofillEnabled, 'change'); + } + }, + + setSyncEnabled_: function(enabled) { + this.syncEnabled = enabled; + }, + + setAutoLoginVisible_ : function(visible) { + $('enable-auto-login-checkbox').hidden = !visible; + }, + + setSyncSetupCompleted_: function(completed) { + this.syncSetupCompleted = completed; + $('customize-sync').hidden = !completed; + }, + + setSyncStatus_: function(status) { + var statusSet = status != ''; + $('sync-overview').hidden = statusSet; + $('sync-status').hidden = !statusSet; + $('sync-status-text').innerHTML = status; + }, + + setSyncStatusErrorVisible_: function(visible) { + visible ? $('sync-status').classList.add('sync-error') : + $('sync-status').classList.remove('sync-error'); + }, + + setCustomizeSyncButtonEnabled_: function(enabled) { + $('customize-sync').disabled = !enabled; + }, + + setSyncActionLinkEnabled_: function(enabled) { + $('sync-action-link').disabled = !enabled; + }, + + setSyncActionLinkLabel_: function(status) { + $('sync-action-link').textContent = status; + + // link-button does is not zero-area when the contents of the button are + // empty, so explicitly hide the element. + $('sync-action-link').hidden = !status.length; + }, + + /** + * Display or hide the profiles section of the page. This is used for + * multi-profile settings. + * @param {boolean} visible True to show the section. + * @private + */ + setProfilesSectionVisible_: function(visible) { + $('profiles-section').hidden = !visible; + }, + + /** + * Get the selected profile item from the profile list. This also works + * correctly if the list is not displayed. + * @return {Object} the profile item object, or null if nothing is selected. + * @private + */ + getSelectedProfileItem_: function() { + var profilesList = $('profiles-list'); + if (profilesList.hidden) { + if (profilesList.dataModel.length > 0) + return profilesList.dataModel.item(0); + } else { + return profilesList.selectedItem; + } + return null; + }, + + /** + * Helper function to set the status of profile view buttons to disabled or + * enabled, depending on the number of profiles and selection status of the + * profiles list. + */ + setProfileViewButtonsStatus_: function() { + var profilesList = $('profiles-list'); + var selectedProfile = profilesList.selectedItem; + var hasSelection = selectedProfile != null; + var hasSingleProfile = profilesList.dataModel.length == 1; + $('profiles-manage').disabled = !hasSelection || + !selectedProfile.isCurrentProfile; + $('profiles-delete').disabled = !hasSelection && !hasSingleProfile; + }, + + /** + * Display the correct dialog layout, depending on how many profiles are + * available. + * @param {number} numProfiles The number of profiles to display. + */ + setProfileViewSingle_: function(numProfiles) { + var hasSingleProfile = numProfiles == 1; + $('profiles-list').hidden = hasSingleProfile; + $('profiles-single-message').hidden = !hasSingleProfile; + $('profiles-manage').hidden = hasSingleProfile; + $('profiles-delete').textContent = hasSingleProfile ? + templateData.profilesDeleteSingle : + templateData.profilesDelete; + }, + + /** + * Adds all |profiles| to the list. + * @param {Array.<Object>} An array of profile info objects. + * each object is of the form: + * profileInfo = { + * name: "Profile Name", + * iconURL: "chrome://path/to/icon/image", + * filePath: "/path/to/profile/data/on/disk", + * isCurrentProfile: false + * }; + */ + setProfilesInfo_: function(profiles) { + this.setProfileViewSingle_(profiles.length); + // add it to the list, even if the list is hidden so we can access it + // later. + $('profiles-list').dataModel = new ArrayDataModel(profiles); + this.setProfileViewButtonsStatus_(); + }, + + setStartStopButtonVisible_: function(visible) { + $('start-stop-sync').hidden = !visible; + }, + + setStartStopButtonEnabled_: function(enabled) { + $('start-stop-sync').disabled = !enabled; + }, + + setStartStopButtonLabel_: function(label) { + $('start-stop-sync').textContent = label; + }, + + setGtkThemeButtonEnabled_: function(enabled) { + if (!cr.isChromeOS && navigator.platform.match(/linux|BSD/i)) { + $('themes-GTK-button').disabled = !enabled; + } + }, + + setThemesResetButtonEnabled_: function(enabled) { + $('themes-reset').disabled = !enabled; + }, + + hideSyncSection_: function() { + $('sync-section').hidden = true; + }, + + /** + * Get the start/stop sync button DOM element. + * @return {DOMElement} The start/stop sync button. + * @private + */ + getStartStopSyncButton_: function() { + return $('start-stop-sync'); + }, + + /** + * (Re)loads IMG element with current user account picture. + */ + updateAccountPicture_: function() { + $('account-picture').src = + 'chrome://userimage/' + this.username_ + + '?id=' + (new Date()).getTime(); + }, + }; + + /** + * Returns whether the user should be able to manage (view and edit) their + * stored passwords. Password management is disabled in guest mode. + * @return {boolean} True if password management should be disabled. + */ + PersonalOptions.disablePasswordManagement = function() { + return cr.commandLine && cr.commandLine.options['--bwsi']; + }; + + /** + * Returns whether the user should be able to manage autofill settings. + * @return {boolean} True if password management should be disabled. + */ + PersonalOptions.disableAutofillManagement = function() { + return cr.commandLine && cr.commandLine.options['--bwsi']; + }; + + if (cr.isChromeOS) { + /** + * Returns username (canonical email) of the user logged in (ChromeOS only). + * @return {string} user email. + */ + PersonalOptions.getLoggedInUsername = function() { + return PersonalOptions.getInstance().username_; + }; + } + + // Forward public APIs to private implementations. + [ + 'getStartStopSyncButton', + 'hideSyncSection', + 'setAutoLoginVisible', + 'setCustomizeSyncButtonEnabled', + 'setGtkThemeButtonEnabled', + 'setProfilesInfo', + 'setProfilesSectionVisible', + 'setStartStopButtonEnabled', + 'setStartStopButtonLabel', + 'setStartStopButtonVisible', + 'setSyncActionLinkEnabled', + 'setSyncActionLinkLabel', + 'setSyncEnabled', + 'setSyncSetupCompleted', + 'setSyncStatus', + 'setSyncStatusErrorVisible', + 'setThemesResetButtonEnabled', + 'updateAccountPicture', + ].forEach(function(name) { + PersonalOptions[name] = function(value) { + return PersonalOptions.getInstance()[name + '_'](value); + }; + }); + + // Export + return { + PersonalOptions: PersonalOptions + }; + +}); diff --git a/chrome/browser/resources/options2/personal_options_profile_list.js b/chrome/browser/resources/options2/personal_options_profile_list.js new file mode 100644 index 0000000..64436a3 --- /dev/null +++ b/chrome/browser/resources/options2/personal_options_profile_list.js @@ -0,0 +1,105 @@ +// 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. + +cr.define('options.personal_options', function() { + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + var localStrings = new LocalStrings(); + + /** + * Creates a new profile list item. + * @param {Object} profileInfo The profile this item respresents. + * @constructor + * @extends {cr.ui.DeletableItem} + */ + function ProfileListItem(profileInfo) { + var el = cr.doc.createElement('div'); + el.profileInfo_ = profileInfo; + ProfileListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a profile list item. + * @param {!HTMLElement} el The element to decorate. + */ + ProfileListItem.decorate = function(el) { + el.__proto__ = ProfileListItem.prototype; + el.decorate(); + }; + + ProfileListItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Get the filepath for this profile list item. + * @return the file path of this item. + */ + get profilePath() { + return this.profileInfo_.filePath; + }, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + var profileInfo = this.profileInfo_; + + var iconEl = this.ownerDocument.createElement('img'); + iconEl.className = 'profile-img'; + iconEl.src = profileInfo.iconURL; + this.contentElement.appendChild(iconEl); + + var nameEl = this.ownerDocument.createElement('div'); + if (profileInfo.isCurrentProfile) + nameEl.classList.add('profile-item-current'); + this.contentElement.appendChild(nameEl); + + var displayName = profileInfo.name; + if (profileInfo.isCurrentProfile) + displayName = localStrings.getStringF( + 'profilesListItemCurrent', + profileInfo.name) + nameEl.textContent = displayName; + }, + }; + + var ProfileList = cr.ui.define('list'); + + ProfileList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel(); + }, + + /** @inheritDoc */ + createItem: function(pageInfo) { + var item = new ProfileListItem(pageInfo); + return item; + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + ManageProfileOverlay.showDeleteDialog(this.dataModel.item(index)); + }, + + /** @inheritDoc */ + activateItemAtIndex: function(index) { + // Don't allow the user to edit a profile that is not current. + var profileInfo = this.dataModel.item(index); + if (profileInfo.isCurrentProfile) + ManageProfileOverlay.showManageDialog(profileInfo); + }, + }; + + return { + ProfileList: ProfileList + }; +}); + diff --git a/chrome/browser/resources/options2/pref_ui.js b/chrome/browser/resources/options2/pref_ui.js new file mode 100644 index 0000000..0468f04 --- /dev/null +++ b/chrome/browser/resources/options2/pref_ui.js @@ -0,0 +1,723 @@ +// 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. + +cr.define('options', function() { + + var Preferences = options.Preferences; + + /** + * Allows an element to be disabled for several reasons. + * The element is disabled if at least one reason is true, and the reasons + * can be set separately. + * @private + * @param {!HTMLElement} el The element to update. + * @param {string} reason The reason for disabling the element. + * @param {boolean} disabled Whether the element should be disabled or enabled + * for the given |reason|. + */ + function updateDisabledState_(el, reason, disabled) { + if (!el.disabledReasons) + el.disabledReasons = {}; + if (el.disabled && (Object.keys(el.disabledReasons).length == 0)) { + // The element has been previously disabled without a reason, so we add + // one to keep it disabled. + el.disabledReasons['other'] = true; + } + if (!el.disabled) { + // If the element is not disabled, there should be no reason, except for + // 'other'. + delete el.disabledReasons['other']; + if (Object.keys(el.disabledReasons).length > 0) + console.error("Element is not disabled but should be"); + } + if (disabled) { + el.disabledReasons[reason] = true; + } else { + delete el.disabledReasons[reason]; + } + el.disabled = Object.keys(el.disabledReasons).length > 0; + } + + /** + * Helper function to update element's state from pref change event. + * @private + * @param {!HTMLElement} el The element to update. + * @param {!Event} event The pref change event. + */ + function updateElementState_(el, event) { + el.controlledBy = null; + + if (!event.value) + return; + + updateDisabledState_(el, 'notUserModifiable', event.value.disabled); + + el.controlledBy = event.value['controlledBy']; + + OptionsPage.updateManagedBannerVisibility(); + } + + ///////////////////////////////////////////////////////////////////////////// + // PrefCheckbox class: + // TODO(jhawkins): Refactor all this copy-pasted code! + + // Define a constructor that uses an input element as its underlying element. + var PrefCheckbox = cr.ui.define('input'); + + PrefCheckbox.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'checkbox'; + var self = this; + + self.initializeValueType(self.getAttribute('value-type')); + + // Listen to pref changes. + Preferences.getInstance().addEventListener( + this.pref, + function(event) { + var value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + + // Invert pref value if inverted_pref == true. + if (self.inverted_pref) + self.checked = !Boolean(value); + else + self.checked = Boolean(value); + + updateElementState_(self, event); + }); + + // Listen to user events. + this.addEventListener( + 'change', + function(e) { + if (self.customChangeHandler(e)) + return; + var value = self.inverted_pref ? !self.checked : self.checked; + switch(self.valueType) { + case 'number': + Preferences.setIntegerPref(self.pref, + Number(value), self.metric); + break; + case 'boolean': + Preferences.setBooleanPref(self.pref, + value, self.metric); + break; + } + }); + }, + + /** + * Sets up options in checkbox element. + * @param {String} valueType The preference type for this checkbox. + */ + initializeValueType: function(valueType) { + this.valueType = valueType || 'boolean'; + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + + /** + * This method is called first while processing an onchange event. If it + * returns false, regular onchange processing continues (setting the + * associated pref, etc). If it returns true, the rest of the onchange is + * not performed. I.e., this works like stopPropagation or cancelBubble. + * @param {Event} event Change event. + */ + customChangeHandler: function(event) { + return false; + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefCheckbox, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefCheckbox, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefCheckbox, 'metric', cr.PropertyKind.ATTR); + + /** + * Whether to use inverted pref value. + * @type {boolean} + */ + cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefRadio class: + + //Define a constructor that uses an input element as its underlying element. + var PrefRadio = cr.ui.define('input'); + + PrefRadio.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'radio'; + var self = this; + + // Listen to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + var value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + self.checked = String(value) == self.value; + + updateElementState_(self, event); + }); + + // Listen to user events. + this.addEventListener('change', + function(e) { + if(self.value == 'true' || self.value == 'false') { + Preferences.setBooleanPref(self.pref, + self.value == 'true', self.metric); + } else { + Preferences.setIntegerPref(self.pref, + parseInt(self.value, 10), self.metric); + } + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefRadio, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefRadio, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefRadio, 'metric', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefNumeric class: + + // Define a constructor that uses an input element as its underlying element. + var PrefNumeric = function() {}; + PrefNumeric.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + + updateElementState_(self, event); + }); + + // Listen to user events. + this.addEventListener('change', + function(e) { + if (this.validity.valid) { + Preferences.setIntegerPref(self.pref, self.value, self.metric); + } + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefNumeric, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefNumeric, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefNumeric, 'metric', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefNumber class: + + // Define a constructor that uses an input element as its underlying element. + var PrefNumber = cr.ui.define('input'); + + PrefNumber.prototype = { + // Set up the prototype chain + __proto__: PrefNumeric.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'number'; + PrefNumeric.prototype.decorate.call(this); + + // Listen to user events. + this.addEventListener('input', + function(e) { + if (this.validity.valid) { + Preferences.setIntegerPref(self.pref, self.value, self.metric); + } + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + ///////////////////////////////////////////////////////////////////////////// + // PrefRange class: + + // Define a constructor that uses an input element as its underlying element. + var PrefRange = cr.ui.define('input'); + + PrefRange.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * The map from input range value to the corresponding preference value. + */ + valueMap: undefined, + + /** + * If true, the associated pref will be modified on each onchange event; + * otherwise, the pref will only be modified on the onmouseup event after + * the drag. + */ + continuous: true, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + this.type = 'range'; + + // Update the UI when the pref changes. + Preferences.getInstance().addEventListener( + this.pref, this.onPrefChange_.bind(this)); + + // Listen to user events. + // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is + // fixed. + // https://bugs.webkit.org/show_bug.cgi?id=52256 + this.onchange = this.onChange_.bind(this); + this.onkeyup = this.onmouseup = this.onInputUp_.bind(this); + }, + + /** + * Event listener that updates the UI when the underlying pref changes. + * @param {Event} event The event that details the pref change. + * @private + */ + onPrefChange_: function(event) { + var value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + if (value != undefined) + this.value = this.valueMap ? this.valueMap.indexOf(value) : value; + }, + + /** + * onchange handler that sets the pref when the user changes the value of + * the input element. + * @private + */ + onChange_: function(event) { + if (this.continuous) + this.setRangePref_(); + + if (this.notifyChange) + this.notifyChange(this, this.mapValueToRange_(this.value)); + }, + + /** + * Sets the integer value of |pref| to the value of this element. + * @private + */ + setRangePref_: function() { + Preferences.setIntegerPref( + this.pref, this.mapValueToRange_(this.value), this.metric); + + if (this.notifyPrefChange) + this.notifyPrefChange(this, this.mapValueToRange_(this.value)); + }, + + /** + * onkeyup/onmouseup handler that modifies the pref if |continuous| is + * false. + * @private + */ + onInputUp_: function(event) { + if (!this.continuous) + this.setRangePref_(); + }, + + /** + * Maps the value of this element into the range provided by the client, + * represented by |valueMap|. + * @param {number} value The value to map. + * @private + */ + mapValueToRange_: function(value) { + return this.valueMap ? this.valueMap[value] : value; + }, + + /** + * Called when the client has specified non-continuous mode and the value of + * the range control changes. + * @param {Element} el This element. + * @param {number} value The value of this element. + */ + notifyChange: function(el, value) { + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefRange, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefRange, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefRange, 'metric', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefSelect class: + + // Define a constructor that uses a select element as its underlying element. + var PrefSelect = cr.ui.define('select'); + + PrefSelect.prototype = { + // Set up the prototype chain + __proto__: HTMLSelectElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + var value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + + // Make sure |value| is a string, because the value is stored as a + // string in the HTMLOptionElement. + value = value.toString(); + + updateElementState_(self, event); + + var found = false; + for (var i = 0; i < self.options.length; i++) { + if (self.options[i].value == value) { + self.selectedIndex = i; + found = true; + } + } + + // Item not found, select first item. + if (!found) + self.selectedIndex = 0; + + if (self.onchange != undefined) + self.onchange(event); + }); + + // Listen to user events. + this.addEventListener('change', + function(e) { + if (!self.dataType) { + console.error('undefined data type for <select> pref'); + return; + } + + switch(self.dataType) { + case 'number': + Preferences.setIntegerPref(self.pref, + self.options[self.selectedIndex].value, self.metric); + break; + case 'double': + Preferences.setDoublePref(self.pref, + self.options[self.selectedIndex].value, self.metric); + break; + case 'boolean': + var option = self.options[self.selectedIndex]; + var value = (option.value == 'true') ? true : false; + Preferences.setBooleanPref(self.pref, value, self.metric); + break; + case 'string': + Preferences.setStringPref(self.pref, + self.options[self.selectedIndex].value, self.metric); + break; + default: + console.error('unknown data type for <select> pref: ' + + self.dataType); + } + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefSelect, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefSelect, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefSelect, 'metric', cr.PropertyKind.ATTR); + + /** + * The data type for the preference options. + * @type {string} + */ + cr.defineProperty(PrefSelect, 'dataType', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefTextField class: + + // Define a constructor that uses an input element as its underlying element. + var PrefTextField = cr.ui.define('input'); + + PrefTextField.prototype = { + // Set up the prototype chain + __proto__: HTMLInputElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen to pref changes. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + self.value = event.value && event.value['value'] != undefined ? + event.value['value'] : event.value; + + updateElementState_(self, event); + }); + + // Listen to user events. + this.addEventListener('change', + function(e) { + switch(self.dataType) { + case 'number': + Preferences.setIntegerPref(self.pref, self.value, self.metric); + break; + case 'double': + Preferences.setDoublePref(self.pref, self.value, self.metric); + break; + case 'url': + Preferences.setURLPref(self.pref, self.value, self.metric); + break; + default: + Preferences.setStringPref(self.pref, self.value, self.metric); + break; + } + }); + + window.addEventListener('unload', + function() { + if (document.activeElement == self) + self.blur(); + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefTextField, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefTextField, 'controlledBy', cr.PropertyKind.ATTR); + + /** + * The user metric string. + * @type {string} + */ + cr.defineProperty(PrefTextField, 'metric', cr.PropertyKind.ATTR); + + /** + * The data type for the preference options. + * @type {string} + */ + cr.defineProperty(PrefTextField, 'dataType', cr.PropertyKind.ATTR); + + ///////////////////////////////////////////////////////////////////////////// + // PrefButton class: + + // Define a constructor that uses a button element as its underlying element. + var PrefButton = cr.ui.define('button'); + + PrefButton.prototype = { + // Set up the prototype chain + __proto__: HTMLButtonElement.prototype, + + /** + * Initialization function for the cr.ui framework. + */ + decorate: function() { + var self = this; + + // Listen to pref changes. This element behaves like a normal button and + // doesn't affect the underlying preference; it just becomes disabled + // when the preference is managed, and its value is false. + // This is useful for buttons that should be disabled when the underlying + // boolean preference is set to false by a policy or extension. + Preferences.getInstance().addEventListener(this.pref, + function(event) { + var e = { + value: { + 'disabled': event.value['disabled'] && !event.value['value'], + 'controlledBy': event.value['controlledBy'] + } + }; + updateElementState_(self, e); + }); + }, + + /** + * See |updateDisabledState_| above. + */ + setDisabled: function(reason, disabled) { + updateDisabledState_(this, reason, disabled); + }, + }; + + /** + * The preference name. + * @type {string} + */ + cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR); + + /** + * Whether the preference is controlled by something else than the user's + * settings (either 'policy' or 'extension'). + * @type {string} + */ + cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR); + + // Export + return { + PrefCheckbox: PrefCheckbox, + PrefNumber: PrefNumber, + PrefNumeric: PrefNumeric, + PrefRadio: PrefRadio, + PrefRange: PrefRange, + PrefSelect: PrefSelect, + PrefTextField: PrefTextField, + PrefButton: PrefButton + }; + +}); diff --git a/chrome/browser/resources/options2/preferences.js b/chrome/browser/resources/options2/preferences.js new file mode 100644 index 0000000..807e45e --- /dev/null +++ b/chrome/browser/resources/options2/preferences.js @@ -0,0 +1,185 @@ +// 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. + +cr.define('options', function() { + + ///////////////////////////////////////////////////////////////////////////// + // Preferences class: + + /** + * Preferences class manages access to Chrome profile preferences. + * @constructor + */ + function Preferences() { + } + + cr.addSingletonGetter(Preferences); + + /** + * Sets value of a boolean preference. + * and signals its changed value. + * @param {string} name Preference name. + * @param {boolean} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setBooleanPref = function(name, value, metric) { + var argumentList = [name, Boolean(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setBooleanPref', argumentList); + }; + + /** + * Sets value of an integer preference. + * and signals its changed value. + * @param {string} name Preference name. + * @param {number} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setIntegerPref = function(name, value, metric) { + var argumentList = [name, Number(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setIntegerPref', argumentList); + }; + + /** + * Sets value of a double-valued preference. + * and signals its changed value. + * @param {string} name Preference name. + * @param {number} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setDoublePref = function(name, value, metric) { + var argumentList = [name, Number(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setDoublePref', argumentList); + }; + + /** + * Sets value of a string preference. + * and signals its changed value. + * @param {string} name Preference name. + * @param {string} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setStringPref = function(name, value, metric) { + var argumentList = [name, String(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setStringPref', argumentList); + }; + + /** + * Sets value of a string preference that represents a URL + * and signals its changed value. The value will be fixed to be a valid URL. + * @param {string} name Preference name. + * @param {string} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setURLPref = function(name, value, metric) { + var argumentList = [name, String(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setURLPref', argumentList); + }; + + /** + * Sets value of a JSON list preference. + * and signals its changed value. + * @param {string} name Preference name. + * @param {Array} value New preference value. + * @param {string} metric User metrics identifier. + */ + Preferences.setListPref = function(name, value, metric) { + var argumentList = [name, JSON.stringify(value)]; + if (metric != undefined) argumentList.push(metric); + chrome.send('setListPref', argumentList); + }; + + /** + * Clears value of a JSON preference. + * @param {string} name Preference name. + * @param {string} metric User metrics identifier. + */ + Preferences.clearPref = function(name, metric) { + var argumentList = [name]; + if (metric != undefined) argumentList.push(metric); + chrome.send('clearPref', argumentList); + }; + + Preferences.prototype = { + __proto__: cr.EventTarget.prototype, + + // Map of registered preferences. + registeredPreferences_: {}, + + /** + * Adds an event listener to the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. This is called when the event is dispatched. + */ + addEventListener: function(type, handler) { + cr.EventTarget.prototype.addEventListener.call(this, type, handler); + this.registeredPreferences_[type] = true; + }, + + /** + * Initializes preference reading and change notifications. + */ + initialize: function() { + var params1 = ['Preferences.prefsFetchedCallback']; + var params2 = ['Preferences.prefsChangedCallback']; + for (var prefName in this.registeredPreferences_) { + params1.push(prefName); + params2.push(prefName); + } + chrome.send('fetchPrefs', params1); + chrome.send('observePrefs', params2); + }, + + /** + * Helper function for flattening of dictionary passed via fetchPrefs + * callback. + * @param {string} prefix Preference name prefix. + * @param {object} dict Map with preference values. + */ + flattenMapAndDispatchEvent_: function(prefix, dict) { + for (var prefName in dict) { + if (typeof dict[prefName] == 'object' && + !this.registeredPreferences_[prefix + prefName]) { + this.flattenMapAndDispatchEvent_(prefix + prefName + '.', + dict[prefName]); + } else { + var event = new cr.Event(prefix + prefName); + event.value = dict[prefName]; + this.dispatchEvent(event); + } + } + } + }; + + /** + * Callback for fetchPrefs method. + * @param {object} dict Map of fetched property values. + */ + Preferences.prefsFetchedCallback = function(dict) { + Preferences.getInstance().flattenMapAndDispatchEvent_('', dict); + }; + + /** + * Callback for observePrefs method. + * @param {array} notification An array defining changed preference values. + * notification[0] contains name of the change preference while its new value + * is stored in notification[1]. + */ + Preferences.prefsChangedCallback = function(notification) { + var event = new cr.Event(notification[0]); + event.value = notification[1]; + Preferences.getInstance().dispatchEvent(event); + }; + + // Export + return { + Preferences: Preferences + }; + +}); diff --git a/chrome/browser/resources/options2/profiles_icon_grid.js b/chrome/browser/resources/options2/profiles_icon_grid.js new file mode 100644 index 0000000..cdd9529 --- /dev/null +++ b/chrome/browser/resources/options2/profiles_icon_grid.js @@ -0,0 +1,68 @@ +// 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. + +cr.define('options', function() { + const ListItem = cr.ui.ListItem; + const Grid = cr.ui.Grid; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Creates a new profile icon grid item. + * @param {Object} iconURL The profile icon URL. + * @constructor + * @extends {cr.ui.GridItem} + */ + function ProfilesIconGridItem(iconURL) { + var el = cr.doc.createElement('span'); + el.iconURL_ = iconURL; + ProfilesIconGridItem.decorate(el); + return el; + } + + /** + * Decorates an element as a profile grid item. + * @param {!HTMLElement} el The element to decorate. + */ + ProfilesIconGridItem.decorate = function(el) { + el.__proto__ = ProfilesIconGridItem.prototype; + el.decorate(); + }; + + ProfilesIconGridItem.prototype = { + __proto__: ListItem.prototype, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + var imageEl = cr.doc.createElement('img'); + imageEl.className = 'profile-icon'; + imageEl.src = this.iconURL_; + this.appendChild(imageEl); + + this.className = 'profile-icon-grid-item'; + }, + }; + + var ProfilesIconGrid = cr.ui.define('grid'); + + ProfilesIconGrid.prototype = { + __proto__: Grid.prototype, + + /** @inheritDoc */ + decorate: function() { + Grid.prototype.decorate.call(this); + this.selectionModel = new ListSingleSelectionModel(); + }, + + /** @inheritDoc */ + createItem: function(iconURL) { + return new ProfilesIconGridItem(iconURL); + }, + }; + + return { + ProfilesIconGrid: ProfilesIconGrid + }; +}); + diff --git a/chrome/browser/resources/options2/search_engine_manager.css b/chrome/browser/resources/options2/search_engine_manager.css new file mode 100644 index 0000000..50c2db2 --- /dev/null +++ b/chrome/browser/resources/options2/search_engine_manager.css @@ -0,0 +1,64 @@ +.search-engine-list > div { + display: -webkit-box; +} + +.search-engine-list .favicon { + padding: 1px 7px 0px 7px; + height: 16px; +} + +.search-engine-list .name-column { + -webkit-box-align: center; + -webkit-padding-end: 1ex; + box-sizing: border-box; + display: -webkit-box; + width: 37%; +} + +.search-engine-list .name-column :last-child { + -webkit-box-flex: 1; +} + +.search-engine-list .keyword-column { + -webkit-padding-end: 1ex; + box-sizing: border-box; + width: 26%; +} + +.search-engine-list .url-column { + box-sizing: border-box; + width: 37%; +} + +.search-engine-list .keyword-column, +.search-engine-list .url-column { + color: #666666; +} + +.search-engine-list .default .name-column, +.search-engine-list .default .keyword-column { + font-weight: bold; +} + +/* For temporary Make Default button */ +.search-engine-list .url-column { + display: -webkit-box; + -webkit-box-align: center; +} + +.search-engine-list .url-column :first-child { + -webkit-box-flex: 1; +} + +.search-engine-list .url-column button { + -webkit-margin-start: 3px; + background: #8aaaed; + color: #fff; + margin-top: 0; +} + +.search-engine-list > :not(:hover):not([editing]) .url-column button { + display: none; +} + +/* End temporary Make Default button styling */ diff --git a/chrome/browser/resources/options2/search_engine_manager.html b/chrome/browser/resources/options2/search_engine_manager.html new file mode 100644 index 0000000..cc00a73 --- /dev/null +++ b/chrome/browser/resources/options2/search_engine_manager.html @@ -0,0 +1,19 @@ +<div id="search-engine-manager-page" class="page" hidden> + <h1 i18n-content="searchEngineManagerPage"></h1> + <h3 i18n-content="defaultSearchEngineListTitle"></h3> + <list id="default-search-engine-list" + class="search-engine-list settings-list"></list> + <h3 i18n-content="otherSearchEngineListTitle"></h3> + <list id="other-search-engine-list" + class="search-engine-list settings-list"></list> + <div id="extension-keyword-div" hidden> + <h3 id="extension-keyword-list-title" + i18n-content="extensionKeywordsListTitle"></h3> + <list id="extension-keyword-list" + class="search-engine-list settings-list"></list> + <div id="manage-extension-link"> + <a href="chrome://settings/extensions/" + i18n-content="manageExtensionsLinkText"></a> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options2/search_engine_manager.js b/chrome/browser/resources/options2/search_engine_manager.js new file mode 100644 index 0000000..57826d8 --- /dev/null +++ b/chrome/browser/resources/options2/search_engine_manager.js @@ -0,0 +1,125 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Encapsulated handling of search engine management page. + * @constructor + */ + function SearchEngineManager() { + this.activeNavTab = null; + OptionsPage.call(this, 'searchEngines', + templateData.searchEngineManagerPageTabTitle, + 'search-engine-manager-page'); + } + + cr.addSingletonGetter(SearchEngineManager); + + SearchEngineManager.prototype = { + __proto__: OptionsPage.prototype, + + /** + * List for default search engine options. + * @private + */ + defaultsList_: null, + + /** + * List for other search engine options. + * @private + */ + othersList_: null, + + /** + * List for extension keywords. + * @private + extensionList_ : null, + + /** inheritDoc */ + initializePage: function() { + OptionsPage.prototype.initializePage.call(this); + + this.defaultsList_ = $('default-search-engine-list'); + this.setUpList_(this.defaultsList_); + + this.othersList_ = $('other-search-engine-list'); + this.setUpList_(this.othersList_); + + this.extensionList_ = $('extension-keyword-list'); + this.setUpList_(this.extensionList_); + }, + + /** + * Sets up the given list as a search engine list + * @param {List} list The list to set up. + * @private + */ + setUpList_: function(list) { + options.search_engines.SearchEngineList.decorate(list); + list.autoExpands = true; + }, + + /** + * Updates the search engine list with the given entries. + * @private + * @param {Array} defaultEngines List of possible default search engines. + * @param {Array} otherEngines List of other search engines. + * @param {Array} keywords List of keywords from extensions. + */ + updateSearchEngineList_: function(defaultEngines, otherEngines, keywords) { + this.defaultsList_.dataModel = new ArrayDataModel(defaultEngines); + + otherEngines = otherEngines.map(function(x) { + return [x, x['name'].toLocaleLowerCase()]; + }).sort(function(a,b){ + return a[1].localeCompare(b[1]); + }).map(function(x){ + return x[0]; + }); + + var othersModel = new ArrayDataModel(otherEngines); + // Add a "new engine" row. + othersModel.push({ + 'modelIndex': '-1', + 'canBeEdited': true + }); + this.othersList_.dataModel = othersModel; + + if (keywords.length > 0) { + $('extension-keyword-div').hidden = false; + var extensionsModel = new ArrayDataModel(keywords); + this.extensionList_.dataModel = extensionsModel; + } else { + $('extension-keyword-div').hidden = true; + } + }, + }; + + SearchEngineManager.updateSearchEngineList = function(defaultEngines, + otherEngines, + keywords) { + SearchEngineManager.getInstance().updateSearchEngineList_(defaultEngines, + otherEngines, + keywords); + }; + + SearchEngineManager.validityCheckCallback = function(validity, modelIndex) { + // Forward to both lists; the one without a matching modelIndex will ignore + // it. + SearchEngineManager.getInstance().defaultsList_.validationComplete( + validity, modelIndex); + SearchEngineManager.getInstance().othersList_.validationComplete( + validity, modelIndex); + }; + + // Export + return { + SearchEngineManager: SearchEngineManager + }; + +}); + diff --git a/chrome/browser/resources/options2/search_engine_manager_engine_list.js b/chrome/browser/resources/options2/search_engine_manager_engine_list.js new file mode 100644 index 0000000..87ee2f5 --- /dev/null +++ b/chrome/browser/resources/options2/search_engine_manager_engine_list.js @@ -0,0 +1,316 @@ +// 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. + +cr.define('options.search_engines', function() { + const InlineEditableItemList = options.InlineEditableItemList; + const InlineEditableItem = options.InlineEditableItem; + const ListSelectionController = cr.ui.ListSelectionController; + + /** + * Creates a new search engine list item. + * @param {Object} searchEnigne The search engine this represents. + * @constructor + * @extends {cr.ui.ListItem} + */ + function SearchEngineListItem(searchEngine) { + var el = cr.doc.createElement('div'); + el.searchEngine_ = searchEngine; + SearchEngineListItem.decorate(el); + return el; + } + + /** + * Decorates an element as a search engine list item. + * @param {!HTMLElement} el The element to decorate. + */ + SearchEngineListItem.decorate = function(el) { + el.__proto__ = SearchEngineListItem.prototype; + el.decorate(); + }; + + SearchEngineListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * Input field for editing the engine name. + * @type {HTMLElement} + * @private + */ + nameField_: null, + + /** + * Input field for editing the engine keyword. + * @type {HTMLElement} + * @private + */ + keywordField_: null, + + /** + * Input field for editing the engine url. + * @type {HTMLElement} + * @private + */ + urlField_: null, + + /** + * Whether or not an input validation request is currently outstanding. + * @type {boolean} + * @private + */ + waitingForValidation_: false, + + /** + * Whether or not the current set of input is known to be valid. + * @type {boolean} + * @private + */ + currentlyValid_: false, + + /** @inheritDoc */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + var engine = this.searchEngine_; + + if (engine['modelIndex'] == '-1') { + this.isPlaceholder = true; + engine['name'] = ''; + engine['keyword'] = ''; + engine['url'] = ''; + } + + this.currentlyValid_ = !this.isPlaceholder; + + if (engine['default']) + this.classList.add('default'); + + this.deletable = engine['canBeRemoved']; + + // Construct the name column. + var nameColEl = this.ownerDocument.createElement('div'); + nameColEl.className = 'name-column'; + nameColEl.classList.add('weakrtl'); + this.contentElement.appendChild(nameColEl); + + // Add the favicon. + var faviconDivEl = this.ownerDocument.createElement('div'); + faviconDivEl.className = 'favicon'; + var imgEl = this.ownerDocument.createElement('img'); + imgEl.src = 'chrome://favicon/iconurl/' + engine['iconURL']; + faviconDivEl.appendChild(imgEl); + nameColEl.appendChild(faviconDivEl); + + var nameEl = this.createEditableTextCell(engine['displayName']); + nameEl.classList.add('weakrtl'); + nameColEl.appendChild(nameEl); + + // Then the keyword column. + var keywordEl = this.createEditableTextCell(engine['keyword']); + keywordEl.className = 'keyword-column'; + keywordEl.classList.add('weakrtl'); + this.contentElement.appendChild(keywordEl); + + // And the URL column. + var urlEl = this.createEditableTextCell(engine['url']); + var urlWithButtonEl = this.ownerDocument.createElement('div'); + urlWithButtonEl.appendChild(urlEl); + urlWithButtonEl.className = 'url-column'; + urlWithButtonEl.classList.add('weakrtl'); + this.contentElement.appendChild(urlWithButtonEl); + // Add the Make Default button. Temporary until drag-and-drop re-ordering + // is implemented. When this is removed, remove the extra div above. + if (engine['canBeDefault']) { + var makeDefaultButtonEl = this.ownerDocument.createElement('button'); + makeDefaultButtonEl.className = 'raw-button custom-appearance'; + makeDefaultButtonEl.textContent = + templateData.makeDefaultSearchEngineButton; + makeDefaultButtonEl.onclick = function(e) { + chrome.send('managerSetDefaultSearchEngine', [engine['modelIndex']]); + }; + // Don't select the row when clicking the button. + makeDefaultButtonEl.onmousedown = function(e) { + e.stopPropagation(); + }; + urlWithButtonEl.appendChild(makeDefaultButtonEl); + } + + // Do final adjustment to the input fields. + this.nameField_ = nameEl.querySelector('input'); + // The editable field uses the raw name, not the display name. + this.nameField_.value = engine['name']; + this.keywordField_ = keywordEl.querySelector('input'); + this.urlField_ = urlEl.querySelector('input'); + + if (engine['urlLocked']) + this.urlField_.disabled = true; + + if (this.isPlaceholder) { + this.nameField_.placeholder = + localStrings.getString('searchEngineTableNamePlaceholder'); + this.keywordField_.placeholder = + localStrings.getString('searchEngineTableKeywordPlaceholder'); + this.urlField_.placeholder = + localStrings.getString('searchEngineTableURLPlaceholder'); + } + + var fields = [ this.nameField_, this.keywordField_, this.urlField_ ]; + for (var i = 0; i < fields.length; i++) { + fields[i].oninput = this.startFieldValidation_.bind(this); + } + + // Listen for edit events. + if (engine['canBeEdited']) { + this.addEventListener('edit', this.onEditStarted_.bind(this)); + this.addEventListener('canceledit', this.onEditCancelled_.bind(this)); + this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); + } else { + this.editable = false; + } + }, + + /** @inheritDoc */ + get currentInputIsValid() { + return !this.waitingForValidation_ && this.currentlyValid_; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + var engine = this.searchEngine_; + return this.nameField_.value != engine['name'] || + this.keywordField_.value != engine['keyword'] || + this.urlField_.value != engine['url']; + }, + + /** + * Called when entering edit mode; starts an edit session in the model. + * @param {Event} e The edit event. + * @private + */ + onEditStarted_: function(e) { + var editIndex = this.searchEngine_['modelIndex']; + chrome.send('editSearchEngine', [String(editIndex)]); + this.startFieldValidation_(); + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + chrome.send('searchEngineEditCompleted', this.getInputFieldValues_()); + }, + + /** + * Called when cancelling an edit; informs the model and resets the control + * states. + * @param {Event} e The cancel event. + * @private + */ + onEditCancelled_: function() { + chrome.send('searchEngineEditCancelled'); + + // The name field has been automatically set to match the display name, + // but it should use the raw name instead. + this.nameField_.value = this.searchEngine_['name']; + this.currentlyValid_ = !this.isPlaceholder; + }, + + /** + * Returns the input field values as an array suitable for passing to + * chrome.send. The order of the array is important. + * @private + * @return {array} The current input field values. + */ + getInputFieldValues_: function() { + return [ this.nameField_.value, + this.keywordField_.value, + this.urlField_.value ]; + }, + + /** + * Begins the process of asynchronously validing the input fields. + * @private + */ + startFieldValidation_: function() { + this.waitingForValidation_ = true; + var args = this.getInputFieldValues_(); + args.push(this.searchEngine_['modelIndex']); + chrome.send('checkSearchEngineInfoValidity', args); + }, + + /** + * Callback for the completion of an input validition check. + * @param {Object} validity A dictionary of validitation results. + */ + validationComplete: function(validity) { + this.waitingForValidation_ = false; + // TODO(stuartmorgan): Implement the full validation UI with + // checkmark/exclamation mark icons and tooltips showing the errors. + if (validity['name']) { + this.nameField_.setCustomValidity(''); + } else { + this.nameField_.setCustomValidity( + templateData.editSearchEngineInvalidTitleToolTip); + } + + if (validity['keyword']) { + this.keywordField_.setCustomValidity(''); + } else { + this.keywordField_.setCustomValidity( + templateData.editSearchEngineInvalidKeywordToolTip); + } + + if (validity['url']) { + this.urlField_.setCustomValidity(''); + } else { + this.urlField_.setCustomValidity( + templateData.editSearchEngineInvalidURLToolTip); + } + + this.currentlyValid_ = validity['name'] && validity['keyword'] && + validity['url']; + }, + }; + + var SearchEngineList = cr.ui.define('list'); + + SearchEngineList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** @inheritDoc */ + createItem: function(searchEngine) { + return new SearchEngineListItem(searchEngine); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var modelIndex = this.dataModel.item(index)['modelIndex'] + chrome.send('removeSearchEngine', [String(modelIndex)]); + }, + + /** + * Passes the results of an input validation check to the requesting row + * if it's still being edited. + * @param {number} modelIndex The model index of the item that was checked. + * @param {Object} validity A dictionary of validitation results. + */ + validationComplete: function(validity, modelIndex) { + // If it's not still being edited, it no longer matters. + var currentSelection = this.selectedItem; + if (!currentSelection) + return; + var listItem = this.getListItem(currentSelection); + if (listItem.editing && currentSelection['modelIndex'] == modelIndex) + listItem.validationComplete(validity); + }, + }; + + // Export + return { + SearchEngineList: SearchEngineList + }; + +}); + diff --git a/chrome/browser/resources/options2/search_page.css b/chrome/browser/resources/options2/search_page.css new file mode 100644 index 0000000..942dd98 --- /dev/null +++ b/chrome/browser/resources/options2/search_page.css @@ -0,0 +1,36 @@ +.search-hidden { + display: none !important; +} + +.search-highlighted { + background-color: rgba(255, 240, 120, 0.9); +} + +.search-bubble { + -webkit-box-shadow: 0 2px 2px #888; + background-color: rgba(255, 240, 120, 0.8); + border-radius: 6px; + box-shadow: 0 2px 2px #888; + left: 0; + margin: 12px 0 0; + padding: 4px 10px; + pointer-events: none; + position: absolute; + text-align: center; + top: -1000px; /* minor hack: position off-screen by default */ + width: 100px; +} + +.search-bubble:after { + border-color: rgba(255, 240, 120, 0.9) transparent; + border-style: solid; + border-width: 0 10px 10px; + content: ""; + left: 50px; + position: absolute; + top: -10px; +} + +.search-bubble-wrapper { + position: relative; +} diff --git a/chrome/browser/resources/options2/search_page.html b/chrome/browser/resources/options2/search_page.html new file mode 100644 index 0000000..5379ca5f --- /dev/null +++ b/chrome/browser/resources/options2/search_page.html @@ -0,0 +1,10 @@ +<div id="searchPage" class="page" hidden> + <h1 i18n-content="searchPage"></h1> + <div id="searchPageNoMatches"> + <p i18n-content="searchPageNoMatches"></p> + <p><span i18n-content="searchPageHelpLabel"></span> + <a target="_blank" i18n-content="searchPageHelpTitle" + i18n-values="href:searchPageHelpURL"></a> + </p> + </div> +</div> diff --git a/chrome/browser/resources/options2/search_page.js b/chrome/browser/resources/options2/search_page.js new file mode 100644 index 0000000..8f0ca9f --- /dev/null +++ b/chrome/browser/resources/options2/search_page.js @@ -0,0 +1,598 @@ +// 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. + +cr.define('options', function() { + const OptionsPage = options.OptionsPage; + + /** + * Encapsulated handling of a search bubble. + * @constructor + */ + function SearchBubble(text) { + var el = cr.doc.createElement('div'); + SearchBubble.decorate(el); + el.textContent = text; + return el; + } + + SearchBubble.decorate = function(el) { + el.__proto__ = SearchBubble.prototype; + el.decorate(); + }; + + SearchBubble.prototype = { + __proto__: HTMLDivElement.prototype, + + decorate: function() { + this.className = 'search-bubble'; + + // We create a timer to periodically update the position of the bubbles. + // While this isn't all that desirable, it's the only sure-fire way of + // making sure the bubbles stay in the correct location as sections + // may dynamically change size at any time. + var self = this; + this.intervalId = setInterval(this.updatePosition.bind(this), 250); + }, + + /** + * Attach the bubble to the element. + */ + attachTo: function(element) { + var parent = element.parentElement; + if (!parent) + return; + if (parent.tagName == 'TD') { + // To make absolute positioning work inside a table cell we need + // to wrap the bubble div into another div with position:relative. + // This only works properly if the element is the first child of the + // table cell which is true for all options pages. + this.wrapper = cr.doc.createElement('div'); + this.wrapper.className = 'search-bubble-wrapper'; + this.wrapper.appendChild(this); + parent.insertBefore(this.wrapper, element); + } else { + parent.insertBefore(this, element); + } + }, + + /** + * Clear the interval timer and remove the element from the page. + */ + dispose: function() { + clearInterval(this.intervalId); + + var child = this.wrapper || this; + var parent = child.parentNode; + if (parent) + parent.removeChild(child); + }, + + /** + * Update the position of the bubble. Called at creation time and then + * periodically while the bubble remains visible. + */ + updatePosition: function() { + // This bubble is 'owned' by the next sibling. + var owner = (this.wrapper || this).nextSibling; + + // If there isn't an offset parent, we have nothing to do. + if (!owner.offsetParent) + return; + + // Position the bubble below the location of the owner. + var left = owner.offsetLeft + owner.offsetWidth / 2 - + this.offsetWidth / 2; + var top = owner.offsetTop + owner.offsetHeight; + + // Update the position in the CSS. Cache the last values for + // best performance. + if (left != this.lastLeft) { + this.style.left = left + 'px'; + this.lastLeft = left; + } + if (top != this.lastTop) { + this.style.top = top + 'px'; + this.lastTop = top; + } + } + } + + /** + * Encapsulated handling of the search page. + * @constructor + */ + function SearchPage() { + OptionsPage.call(this, 'search', templateData.searchPageTabTitle, + 'searchPage'); + } + + cr.addSingletonGetter(SearchPage); + + SearchPage.prototype = { + // Inherit SearchPage from OptionsPage. + __proto__: OptionsPage.prototype, + + /** + * A boolean to prevent recursion. Used by setSearchText_(). + * @type {Boolean} + * @private + */ + insideSetSearchText_: false, + + /** + * Initialize the page. + */ + initializePage: function() { + // Call base class implementation to start preference initialization. + OptionsPage.prototype.initializePage.call(this); + + var self = this; + + // Create a search field element. + var searchField = document.createElement('input'); + searchField.id = 'search-field'; + searchField.type = 'search'; + searchField.incremental = true; + searchField.placeholder = localStrings.getString('searchPlaceholder'); + searchField.setAttribute('aria-label', searchField.placeholder); + this.searchField = searchField; + + // Replace the contents of the navigation tab with the search field. + self.tab.textContent = ''; + self.tab.appendChild(searchField); + self.tab.onclick = self.tab.onkeydown = self.tab.onkeypress = undefined; + self.tab.tabIndex = -1; + self.tab.setAttribute('role', ''); + + // Don't allow the focus on the search navbar. http://crbug.com/77989 + self.tab.onfocus = self.tab.blur; + + // Handle search events. (No need to throttle, WebKit's search field + // will do that automatically.) + searchField.onsearch = function(e) { + self.setSearchText_(this.value); + }; + + // We update the history stack every time the search field blurs. This way + // we get a history entry for each search, roughly, but not each letter + // typed. + searchField.onblur = function(e) { + var query = SearchPage.canonicalizeQuery(searchField.value); + if (!query) + return; + + // Don't push the same page onto the history stack more than once (if + // the user clicks in the search field and away several times). + var currentHash = location.hash; + var newHash = '#' + escape(query); + if (currentHash == newHash) + return; + + // If there is no hash on the current URL, the history entry has no + // search query. Replace the history entry with no search with an entry + // that does have a search. Otherwise, add it onto the history stack. + var historyFunction = currentHash ? window.history.pushState : + window.history.replaceState; + historyFunction.call( + window.history, + {pageName: self.name}, + self.title, + '/' + self.name + newHash); + }; + + // Install handler for key presses. + document.addEventListener('keydown', + this.keyDownEventHandler_.bind(this)); + + // Focus the search field by default. + searchField.focus(); + }, + + /** + * @inheritDoc + */ + get sticky() { + return true; + }, + + /** + * Called after this page has shown. + */ + didShowPage: function() { + // This method is called by the Options page after all pages have + // had their visibilty attribute set. At this point we can perform the + // search specific DOM manipulation. + this.setSearchActive_(true); + }, + + /** + * Called before this page will be hidden. + */ + willHidePage: function() { + // This method is called by the Options page before all pages have + // their visibilty attribute set. Before that happens, we need to + // undo the search specific DOM manipulation that was performed in + // didShowPage. + this.setSearchActive_(false); + }, + + /** + * Update the UI to reflect whether we are in a search state. + * @param {boolean} active True if we are on the search page. + * @private + */ + setSearchActive_: function(active) { + // It's fine to exit if search wasn't active and we're not going to + // activate it now. + if (!this.searchActive_ && !active) + return; + + this.searchActive_ = active; + + if (active) { + var hash = location.hash; + if (hash) + this.searchField.value = unescape(hash.slice(1)); + } else { + // Just wipe out any active search text since it's no longer relevant. + this.searchField.value = ''; + } + + var pagesToSearch = this.getSearchablePages_(); + for (var key in pagesToSearch) { + var page = pagesToSearch[key]; + + if (!active) + page.visible = false; + + // Update the visible state of all top-level elements that are not + // sections (ie titles, button strips). We do this before changing + // the page visibility to avoid excessive re-draw. + for (var i = 0, childDiv; childDiv = page.pageDiv.children[i]; i++) { + if (childDiv.classList.contains('displaytable')) { + childDiv.setAttribute('searching', active ? 'true' : 'false'); + for (var j = 0, subDiv; subDiv = childDiv.children[j]; j++) { + if (active) { + if (subDiv.tagName != 'SECTION') + subDiv.classList.add('search-hidden'); + } else { + subDiv.classList.remove('search-hidden'); + } + } + } else { + if (active) + childDiv.classList.add('search-hidden'); + else + childDiv.classList.remove('search-hidden'); + } + } + + if (active) { + // When search is active, remove the 'hidden' tag. This tag may have + // been added by the OptionsPage. + page.pageDiv.hidden = false; + } + } + + if (active) { + this.setSearchText_(this.searchField.value); + } else { + // After hiding all page content, remove any search results. + this.unhighlightMatches_(); + this.removeSearchBubbles_(); + } + }, + + /** + * Set the current search criteria. + * @param {string} text Search text. + * @private + */ + setSearchText_: function(text) { + // Prevent recursive execution of this method. + if (this.insideSetSearchText_) return; + this.insideSetSearchText_ = true; + + // Cleanup the search query string. + text = SearchPage.canonicalizeQuery(text); + + // Notify listeners about the new search query, some pages may wish to + // show/hide elements based on the query. + var event = new cr.Event('searchChanged'); + event.searchText = text; + this.dispatchEvent(event); + + // Toggle the search page if necessary. + if (text.length) { + if (!this.searchActive_) + OptionsPage.navigateToPage(this.name); + } else { + if (this.searchActive_) + OptionsPage.showDefaultPage(); + + this.insideSetSearchText_ = false; + return; + } + + var foundMatches = false; + var bubbleControls = []; + + // Remove any prior search results. + this.unhighlightMatches_(); + this.removeSearchBubbles_(); + + // Generate search text by applying lowercase and escaping any characters + // that would be problematic for regular expressions. + var searchText = + text.toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + + // Generate a regular expression and replace string for hilighting + // search terms. + var regEx = new RegExp('(' + searchText + ')', 'ig'); + var replaceString = '<span class="search-highlighted">$1</span>'; + + // Initialize all sections. If the search string matches a title page, + // show sections for that page. + var page, pageMatch, childDiv, length; + var pagesToSearch = this.getSearchablePages_(); + for (var key in pagesToSearch) { + page = pagesToSearch[key]; + pageMatch = false; + if (searchText.length) { + pageMatch = this.performReplace_(regEx, replaceString, page.tab); + } + if (pageMatch) + foundMatches = true; + var elements = page.pageDiv.querySelectorAll('.displaytable > section'); + for (var i = 0, node; node = elements[i]; i++) { + if (pageMatch) + node.classList.remove('search-hidden'); + else + node.classList.add('search-hidden'); + } + } + + if (searchText.length) { + // Search all top-level sections for anchored string matches. + for (var key in pagesToSearch) { + page = pagesToSearch[key]; + var elements = + page.pageDiv.querySelectorAll('.displaytable > section'); + for (var i = 0, node; node = elements[i]; i++) { + if (this.performReplace_(regEx, replaceString, node)) { + node.classList.remove('search-hidden'); + foundMatches = true; + } + } + } + + // Search all sub-pages, generating an array of top-level sections that + // we need to make visible. + var subPagesToSearch = this.getSearchableSubPages_(); + var control, node; + for (var key in subPagesToSearch) { + page = subPagesToSearch[key]; + if (this.performReplace_(regEx, replaceString, page.pageDiv)) { + // Reveal the section for this search result. + section = page.associatedSection; + if (section) + section.classList.remove('search-hidden'); + + // Identify any controls that should have bubbles. + var controls = page.associatedControls; + if (controls) { + length = controls.length; + for (var i = 0; i < length; i++) + bubbleControls.push(controls[i]); + } + + foundMatches = true; + } + } + } + + // Configure elements on the search results page based on search results. + if (foundMatches) + $('searchPageNoMatches').classList.add('search-hidden'); + else + $('searchPageNoMatches').classList.remove('search-hidden'); + + // Create search balloons for sub-page results. + length = bubbleControls.length; + for (var i = 0; i < length; i++) + this.createSearchBubble_(bubbleControls[i], text); + + // Cleanup the recursion-prevention variable. + this.insideSetSearchText_ = false; + }, + + /** + * Performs a string replacement based on a regex and replace string. + * @param {RegEx} regex A regular expression for finding search matches. + * @param {String} replace A string to apply the replace operation. + * @param {Element} element An HTML container element. + * @returns {Boolean} true if the element was changed. + * @private + */ + performReplace_: function(regex, replace, element) { + var found = false; + var div, child, tmp; + + // Walk the tree, searching each TEXT node. + var walker = document.createTreeWalker(element, + NodeFilter.SHOW_TEXT, + null, + false); + var node = walker.nextNode(); + while (node) { + // Perform a search and replace on the text node value. + var newValue = node.nodeValue.replace(regex, replace); + if (newValue != node.nodeValue) { + // The text node has changed so that means we found at least one + // match. + found = true; + + // Create a temporary div element and set the innerHTML to the new + // value. + div = document.createElement('div'); + div.innerHTML = newValue; + + // Insert all the child nodes of the temporary div element into the + // document, before the original node. + child = div.firstChild; + while (child = div.firstChild) { + node.parentNode.insertBefore(child, node); + }; + + // Delete the old text node and advance the walker to the next + // node. + tmp = node; + node = walker.nextNode(); + tmp.parentNode.removeChild(tmp); + } else { + node = walker.nextNode(); + } + } + + return found; + }, + + /** + * Removes all search highlight tags from the document. + * @private + */ + unhighlightMatches_: function() { + // Find all search highlight elements. + var elements = document.querySelectorAll('.search-highlighted'); + + // For each element, remove the highlighting. + var parent, i; + for (var i = 0, node; node = elements[i]; i++) { + parent = node.parentNode; + + // Replace the highlight element with the first child (the text node). + parent.replaceChild(node.firstChild, node); + + // Normalize the parent so that multiple text nodes will be combined. + parent.normalize(); + } + }, + + /** + * Creates a search result bubble attached to an element. + * @param {Element} element An HTML element, usually a button. + * @param {string} text A string to show in the bubble. + * @private + */ + createSearchBubble_: function(element, text) { + // avoid appending multiple bubbles to a button. + var sibling = element.previousElementSibling; + if (sibling && (sibling.classList.contains('search-bubble') || + sibling.classList.contains('search-bubble-wrapper'))) + return; + + var parent = element.parentElement; + if (parent) { + var bubble = new SearchBubble(text); + bubble.attachTo(element); + bubble.updatePosition(); + } + }, + + /** + * Removes all search match bubbles. + * @private + */ + removeSearchBubbles_: function() { + var elements = document.querySelectorAll('.search-bubble'); + var length = elements.length; + for (var i = 0; i < length; i++) + elements[i].dispose(); + }, + + /** + * Builds a list of top-level pages to search. Omits the search page and + * all sub-pages. + * @returns {Array} An array of pages to search. + * @private + */ + getSearchablePages_: function() { + var name, page, pages = []; + for (name in OptionsPage.registeredPages) { + if (name != this.name) { + page = OptionsPage.registeredPages[name]; + if (!page.parentPage) + pages.push(page); + } + } + return pages; + }, + + /** + * Builds a list of sub-pages (and overlay pages) to search. Ignore pages + * that have no associated controls. + * @returns {Array} An array of pages to search. + * @private + */ + getSearchableSubPages_: function() { + var name, pageInfo, page, pages = []; + for (name in OptionsPage.registeredPages) { + page = OptionsPage.registeredPages[name]; + if (page.parentPage && page.associatedSection) + pages.push(page); + } + for (name in OptionsPage.registeredOverlayPages) { + page = OptionsPage.registeredOverlayPages[name]; + if (page.associatedSection && page.pageDiv != undefined) + pages.push(page); + } + return pages; + }, + + /** + * A function to handle key press events. + * @return {Event} a keydown event. + * @private + */ + keyDownEventHandler_: function(event) { + const ESCAPE_KEY_CODE = 27; + const FORWARD_SLASH_KEY_CODE = 191; + + switch(event.keyCode) { + case ESCAPE_KEY_CODE: + if (event.target == this.searchField) { + this.setSearchText_(''); + this.searchField.blur(); + event.stopPropagation(); + event.preventDefault(); + } + break; + case FORWARD_SLASH_KEY_CODE: + if (!/INPUT|SELECT|BUTTON|TEXTAREA/.test(event.target.tagName) && + !event.ctrlKey && !event.altKey) { + this.searchField.focus(); + event.stopPropagation(); + event.preventDefault(); + } + break; + } + }, + }; + + /** + * Standardizes a user-entered text query by removing extra whitespace. + * @param {string} The user-entered text. + * @return {string} The trimmed query. + */ + SearchPage.canonicalizeQuery = function(text) { + // Trim beginning and ending whitespace. + return text.replace(/^\s+|\s+$/g, ''); + }; + + // Export + return { + SearchPage: SearchPage + }; + +}); diff --git a/chrome/browser/resources/options2/subpages_tab_controls.css b/chrome/browser/resources/options2/subpages_tab_controls.css new file mode 100644 index 0000000..774520c --- /dev/null +++ b/chrome/browser/resources/options2/subpages_tab_controls.css @@ -0,0 +1,66 @@ +/* +Copyright (c) 2010 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. +*/ + +.subpages-nav-tabs .tab { + position: relative; + padding: 4px 8px; +} + +.subpages-nav-tabs .active-tab { + position: relative; + background: white; + border: 1px solid #A0A0A0; /* light gray */ + border-bottom: 2px solid white; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +/* To avoid tabs changing size when they are clicked and their labels become + * bold, we actually put two labels inside each tab: an inactive label and an + * active label. Only one is visible at a time, but the bold label is used to + * size the tab even when it's not visible. This keeps the tab size constant. + */ +.subpages-nav-tabs .active-tab-label { + font-weight: bold; +} + +.subpages-nav-tabs .tab-label { + position: absolute; + top: 5px; + left: 9px; +} + +html[dir=rtl] .subpages-nav-tabs .tab-label { + right: 9px; +} + +.subpages-nav-tabs .active-tab-label, +.subpages-nav-tabs .active-tab .tab-label { + visibility: hidden; +} + +/* .tab is not removed when .active-tab is added, so we must + * override the hidden visibility above in the active tab case. + */ +.subpages-nav-tabs .active-tab .active-tab-label { + visibility: visible; +} + +.subpages-nav-tabs { + padding: 4px; + border-bottom: 1px solid #A0A0A0; /* light gray */ + background: -webkit-linear-gradient(white, #F3F3F3); /* very light gray */ + margin-bottom: 15px; +} + +.subpages-tab-contents { + display: none; + -webkit-padding-start: 10px; +} + +.active-tab-contents { + display: block; +} diff --git a/chrome/browser/resources/options2_resources.grd b/chrome/browser/resources/options2_resources.grd new file mode 100644 index 0000000..f094fda --- /dev/null +++ b/chrome/browser/resources/options2_resources.grd @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<grit latest_public_release="0" current_release="1"> + <outputs> + <output filename="grit/options2_resources.h" type="rc_header"> + <emit emit_type='prepend'></emit> + </output> + <output filename="options2_resources.pak" type="data_package" /> + </outputs> + <release seq="1"> + <includes> + <include name="IDR_OPTIONS2_BUNDLE_JS" file="options2/options_bundle.js" flattenhtml="true" type="BINDATA" /> + <include name="IDR_OPTIONS2_HTML" file="options2/options.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" /> + </includes> + </release> +</grit> diff --git a/chrome/browser/resources/uber/uber.html b/chrome/browser/resources/uber/uber.html index 61469f8..d8f3bfd 100644 --- a/chrome/browser/resources/uber/uber.html +++ b/chrome/browser/resources/uber/uber.html @@ -13,7 +13,7 @@ <body> -<iframe src="chrome://settings/"></iframe> +<iframe src="chrome://settings-frame/"></iframe> <iframe src="chrome://extensions-frame/"></iframe> </body> |