diff options
author | stuartmorgan@chromium.org <stuartmorgan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-12 00:40:09 +0000 |
---|---|---|
committer | stuartmorgan@chromium.org <stuartmorgan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-01-12 00:40:09 +0000 |
commit | 9394fced7d8c03bb4b81941c99155bb775b68d04 (patch) | |
tree | 39b8d802c8b0cda5395ee3d7678d55045f755b2f | |
parent | cba920c94cb99e0c7b6a0dd06b124902699a0aee (diff) | |
download | chromium_src-9394fced7d8c03bb4b81941c99155bb775b68d04.zip chromium_src-9394fced7d8c03bb4b81941c99155bb775b68d04.tar.gz chromium_src-9394fced7d8c03bb4b81941c99155bb775b68d04.tar.bz2 |
DOMUI Prefs: Replace search engine edit overlay with inline editing.
Validation feedback still needs polish, but this is completely functional, so I'll leave that for a follow-up.
This introduces a new shared class for editable lists; we should migrate the existing editable lists to it, expanding as necessary, but I didn't want to make this patch too unwieldly.
BUG=63825,61742
TEST=Search engines should be editable and addable inline in DOMUI prefs.
Review URL: http://codereview.chromium.org/6151004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@71121 0039d316-1c4b-4281-b951-d872f2087c98
14 files changed, 594 insertions, 332 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index fe58bc3..e8df9bb 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -1133,6 +1133,10 @@ each locale. --> desc="Title of the description column in the search engines editor"> Name </message> + <message name="IDS_SEARCH_ENGINES_EDITOR_URL_COLUMN" + desc="Title of the URL column in the search engines editor"> + URL + </message> <message name="IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_LABEL" desc="Prefix before the search engine description text field"> Name: @@ -6893,6 +6897,15 @@ Keep your key file in a safe place. You will need it to create new versions of y desc="The label of a link that brings up the keyword editor (aka search engine editor)"> Manage </message> + <message name="IDS_SEARCH_ENGINE_ADD_NEW_NAME_PLACEHOLDER" desc="Placeholder text for name before the user adds a new search engine" > + Add a new search engine + </message> + <message name="IDS_SEARCH_ENGINE_ADD_NEW_KEYWORD_PLACEHOLDER" desc="Placeholder text for keyword before the user adds a new search engine" > + Keyword + </message> + <message name="IDS_SEARCH_ENGINE_ADD_NEW_URL_PLACEHOLDER" desc="Placeholder text for URL before the user adds a new search engine" > + URL with %s in place of query + </message> <if expr="not pp_ifdef('use_titlecase') or os != 'linux2'"> <message name="IDS_OPTIONS_DEFAULTBROWSER_GROUP_NAME" desc="The title of the default browser group"> diff --git a/chrome/browser/dom_ui/options/search_engine_manager_handler.cc b/chrome/browser/dom_ui/options/search_engine_manager_handler.cc index 78c8cd3..1c211f6 100644 --- a/chrome/browser/dom_ui/options/search_engine_manager_handler.cc +++ b/chrome/browser/dom_ui/options/search_engine_manager_handler.cc @@ -54,34 +54,22 @@ void SearchEngineManagerHandler::GetLocalizedValues( l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_COLUMN)); localized_strings->SetString("searchEngineTableKeywordHeader", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN)); - localized_strings->SetString("addSearchEngineButton", - l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_NEW_BUTTON)); - localized_strings->SetString("editSearchEngineButton", + localized_strings->SetString("searchEngineTableURLHeader", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_EDIT_BUTTON)); localized_strings->SetString("makeDefaultSearchEngineButton", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_MAKE_DEFAULT_BUTTON)); - // Overlay strings. - localized_strings->SetString("editSearchEngineTitle", - l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_EDIT_WINDOW_TITLE)); - localized_strings->SetString("editSearchEngineNameLabel", - l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_DESCRIPTION_LABEL)); - localized_strings->SetString("editSearchEngineKeywordLabel", - l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_KEYWORD_LABEL)); - localized_strings->SetString("editSearchEngineURLLabel", - l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_EDITOR_URL_LABEL)); + localized_strings->SetString("searchEngineTableNamePlaceholder", + l10n_util::GetStringUTF16(IDS_SEARCH_ENGINE_ADD_NEW_NAME_PLACEHOLDER)); + localized_strings->SetString("searchEngineTableKeywordPlaceholder", + l10n_util::GetStringUTF16(IDS_SEARCH_ENGINE_ADD_NEW_KEYWORD_PLACEHOLDER)); + localized_strings->SetString("searchEngineTableURLPlaceholder", + l10n_util::GetStringUTF16(IDS_SEARCH_ENGINE_ADD_NEW_URL_PLACEHOLDER)); localized_strings->SetString("editSearchEngineInvalidTitleToolTip", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_INVALID_TITLE_TT)); localized_strings->SetString("editSearchEngineInvalidKeywordToolTip", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_INVALID_KEYWORD_TT)); localized_strings->SetString("editSearchEngineInvalidURLToolTip", l10n_util::GetStringUTF16(IDS_SEARCH_ENGINES_INVALID_URL_TT)); - localized_strings->SetString("editSearchEngineURLExplanation", - l10n_util::GetStringUTF16( - IDS_SEARCH_ENGINES_EDITOR_URL_DESCRIPTION_LABEL)); - localized_strings->SetString("editSearchEngineOkayButton", - l10n_util::GetStringUTF16(IDS_OK)); - localized_strings->SetString("editSearchEngineCancelButton", - l10n_util::GetStringUTF16(IDS_CANCEL)); } void SearchEngineManagerHandler::RegisterMessages() { @@ -170,6 +158,8 @@ DictionaryValue* SearchEngineManagerHandler::CreateDictionaryForEngine( dict->SetString("keyword", table_model->GetText( index, IDS_SEARCH_ENGINES_EDITOR_KEYWORD_COLUMN)); const TemplateURL* template_url = list_controller_->GetTemplateURL(index); + dict->SetString("url", WideToUTF16Hack(template_url->url()->DisplayURL())); + dict->SetBoolean("urlLocked", template_url->prepopulate_id() > 0); GURL icon_url = template_url->GetFavIconURL(); if (icon_url.is_valid()) dict->SetString("iconURL", icon_url.spec()); @@ -225,17 +215,6 @@ void SearchEngineManagerHandler::EditSearchEngine(const ListValue* args) { edit_url = list_controller_->GetTemplateURL(index); edit_controller_.reset( new EditSearchEngineController(edit_url, this, dom_ui_->GetProfile())); - - if (edit_url) { - DictionaryValue engine_details; - engine_details.SetString("name", WideToUTF16Hack(edit_url->short_name())); - engine_details.SetString("keyword", WideToUTF16Hack(edit_url->keyword())); - engine_details.SetString("url", - WideToUTF16Hack(edit_url->url()->DisplayURL())); - engine_details.SetBoolean("urlLocked", edit_url->prepopulate_id() > 0); - dom_ui_->CallJavascriptFunction(L"EditSearchEngineOverlay.setEditDetails", - engine_details); - } } void SearchEngineManagerHandler::OnEditedKeyword( @@ -259,9 +238,11 @@ void SearchEngineManagerHandler::CheckSearchEngineInfoValidity( string16 name; string16 keyword; std::string url; + std::string modelIndex; if (!args->GetString(ENGINE_NAME, &name) || !args->GetString(ENGINE_KEYWORD, &keyword) || - !args->GetString(ENGINE_URL, &url)) { + !args->GetString(ENGINE_URL, &url) || + !args->GetString(3, &modelIndex)) { NOTREACHED(); return; } @@ -270,8 +251,9 @@ void SearchEngineManagerHandler::CheckSearchEngineInfoValidity( validity.SetBoolean("name", edit_controller_->IsTitleValid(name)); validity.SetBoolean("keyword", edit_controller_->IsKeywordValid(keyword)); validity.SetBoolean("url", edit_controller_->IsURLValid(url)); + StringValue indexValue(modelIndex); dom_ui_->CallJavascriptFunction( - L"EditSearchEngineOverlay.validityCheckCallback", validity); + L"SearchEngineManager.validityCheckCallback", validity, indexValue); } void SearchEngineManagerHandler::EditCancelled(const ListValue* args) { diff --git a/chrome/browser/resources/options/edit_search_engine_overlay.css b/chrome/browser/resources/options/edit_search_engine_overlay.css deleted file mode 100644 index fad104e..0000000 --- a/chrome/browser/resources/options/edit_search_engine_overlay.css +++ /dev/null @@ -1,36 +0,0 @@ -#editSearchEngineOverlay { - width: 500px; -} - -#editSearchEngineOverlay table label { - text-align: end; -} - -#editSearchEngineOverlay table, -#editSearchEngineOverlay table td:nth-child(2) { - width: 100%; -} - -#editSearchEngineOverlay table input { - width: 100%; - box-sizing: border-box; -} - -#editSearchEngineOverlay .action-area { - margin-top: 2ex; -} - -.valid-badge, .alert-badge { - width: 22px; - height: 21px; - background-position: 50% 1px; - background-repeat: no-repeat; -} - -.valid-badge { - background-image: url("../../../../app/resources/input_good.png"); -} - -.alert-badge { - background-image: url("../../../app/theme/input_alert.png"); -} diff --git a/chrome/browser/resources/options/edit_search_engine_overlay.html b/chrome/browser/resources/options/edit_search_engine_overlay.html deleted file mode 100644 index 19c425a..0000000 --- a/chrome/browser/resources/options/edit_search_engine_overlay.html +++ /dev/null @@ -1,40 +0,0 @@ -<div class="page hidden" id="editSearchEngineOverlay"> - <h1 i18n-content="editSearchEngineTitle"></h1> - - <form id="editSearchEngineForm"> - <table> - <tr> - <td><label for="editSearchEngineName"><span - i18n-content="editSearchEngineNameLabel"></span></label></td> - <td><input type="text" id="editSearchEngineName"></td> - <td><div id="editSearchEngineNameValidity" - class="alert-badge"> </div></td> - </tr> - <tr> - <td><label for="editSearchEngineKeyword"><span - i18n-content="editSearchEngineKeywordLabel"></span></label></td> - <td><input type="text" id="editSearchEngineKeyword"></td> - <td><div id="editSearchEngineKeywordValidity" - class="alert-badge"> </div></td> - </tr> - <tr> - <td><label for="editSearchEngineURL"><span - i18n-content="editSearchEngineURLLabel"></span></label></td> - <td><input type="url" id="editSearchEngineURL"></td> - <td><div id="editSearchEngineURLValidity" - class="alert-badge"> </div></td> - </tr> - <tr> - <td></td> - <td><span i18n-content="editSearchEngineURLExplanation"><span></td> - <td></td> - </table> - - <div class="action-area button-strip"> - <button type="reset" - i18n-content="editSearchEngineCancelButton"></button> - <button type="submit" id="editSearchEngineOkayButton" disabled - i18n-content="editSearchEngineOkayButton"></button> - </div> - </form> -</div> diff --git a/chrome/browser/resources/options/edit_search_engine_overlay.js b/chrome/browser/resources/options/edit_search_engine_overlay.js deleted file mode 100644 index a4f72b5..0000000 --- a/chrome/browser/resources/options/edit_search_engine_overlay.js +++ /dev/null @@ -1,158 +0,0 @@ -// 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; - - /** - * EditSearchEngineOverlay class - * Encapsulated handling of the 'Edit Search Engine' overlay page. - * @class - * @constructor - */ - function EditSearchEngineOverlay() { - OptionsPage.call(this, 'editSearchEngineOverlay', - templateData.editSearchEngineTitle, - 'editSearchEngineOverlay'); - } - - cr.addSingletonGetter(EditSearchEngineOverlay); - - EditSearchEngineOverlay.prototype = { - __proto__: OptionsPage.prototype, - - /** - * Initializes the page. - */ - initializePage: function() { - OptionsPage.prototype.initializePage.call(this); - - var self = this; - var editForm = $('editSearchEngineForm'); - editForm.onreset = function(e) { - chrome.send('searchEngineEditCancelled'); - self.dismissOverlay_(); - }; - editForm.onsubmit = function(e) { - chrome.send('searchEngineEditCompleted', self.getInputFieldValues_()); - self.dismissOverlay_(); - return false; - }; - var fieldIDs = ['editSearchEngineName', - 'editSearchEngineKeyword', - 'editSearchEngineURL']; - for (var i = 0; i < fieldIDs.length; i++) { - var field = $(fieldIDs[i]); - field.oninput = this.validateFields_.bind(this); - field.onkeydown = function(e) { - if (e.keyCode == 27) // Esc - editForm.reset(); - }; - } - }, - - /** - * Clears any uncommited input, and dismisses the overlay. - * @private - */ - dismissOverlay_: function() { - this.setEditDetails_(); - OptionsPage.clearOverlays(); - }, - - /** - * Fills the text fields from the given search engine. - * @private - */ - setEditDetails_: function(engineDetails) { - if (engineDetails) { - $('editSearchEngineName').value = engineDetails['name']; - $('editSearchEngineKeyword').value = engineDetails['keyword']; - var urlField = $('editSearchEngineURL'); - urlField.value = engineDetails['url']; - urlField.disabled = engineDetails['urlLocked']; - this.validateFields_(); - } else { - $('editSearchEngineName').value = ''; - $('editSearchEngineKeyword').value = ''; - $('editSearchEngineURL').value = ''; - var invalid = { name: false, keyword: false, url: false }; - this.updateValidityWithResults_(invalid); - } - }, - - /** - * Starts the process of asynchronously validating the user input. Results - * will be reported to updateValidityWithResults_. - * @private - */ - validateFields_: function() { - chrome.send('checkSearchEngineInfoValidity', this.getInputFieldValues_()); - }, - - /** - * Sets the validation images and the enabled state of the Add button based - * on the current values of the text fields. - * @private - * @param {Object} The dictionary of validity states. - */ - updateValidityWithResults_: function(validity) { - this.setBadgeValidity_($('editSearchEngineNameValidity'), - validity['name'], - 'editSearchEngineInvalidTitleToolTip'); - this.setBadgeValidity_($('editSearchEngineKeywordValidity'), - validity['keyword'], - 'editSearchEngineInvalidKeywordToolTip'); - this.setBadgeValidity_($('editSearchEngineURLValidity'), - validity['url'], - 'editSearchEngineInvalidURLToolTip'); - $('editSearchEngineOkayButton').disabled = - !(validity['name'] && validity['keyword'] && validity['url']); - }, - - /** - * Updates the state of the given validity indicator badge. - * @private - * @param {HTMLElement} The badge element to adjust. - * @param {boolean} Whether or not the badge should be set to the valid - * state. - * @param {string} The tooltip string id for the invalid state. - */ - setBadgeValidity_: function(element, isValid, tooltip_id) { - if (isValid) { - element.className = 'valid-badge'; - element.title = ''; - } else { - element.className = 'alert-badge'; - element.title = localStrings.getString(tooltip_id); - } - }, - - /** - * 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 [ $('editSearchEngineName').value, - $('editSearchEngineKeyword').value, - $('editSearchEngineURL').value ]; - } - }; - - EditSearchEngineOverlay.setEditDetails = function(engineDetails) { - EditSearchEngineOverlay.getInstance().setEditDetails_(engineDetails); - }; - - EditSearchEngineOverlay.validityCheckCallback = function(validity) { - EditSearchEngineOverlay.getInstance().updateValidityWithResults_(validity); - }; - - // Export - return { - EditSearchEngineOverlay: EditSearchEngineOverlay - }; - -}); diff --git a/chrome/browser/resources/options/inline_editable_list.js b/chrome/browser/resources/options/inline_editable_list.js new file mode 100644 index 0000000..ec0f4a4 --- /dev/null +++ b/chrome/browser/resources/options/inline_editable_list.js @@ -0,0 +1,210 @@ +// 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 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 the current edit should be considered cancelled, rather + * than committed, when editing ends. + * @type {boolean} + * @private + */ + editCancelled_: true, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + this.addEventListener('keydown', this.handleKeyDown_.bind(this)); + }, + + /** @inheritDoc */ + selectionChanged: function() { + if (this.editable) + this.editing = this.selected; + }, + + /** + * 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.initialFocusElement; + // When this is called in response to the selectedChange event, + // the list grabs focus immediately afterwards. Thus we must delay + // our focus grab. + if (focusElement) { + window.setTimeout(function() { + focusElement.focus(); + focusElement.select(); + }, 50); + } + } else { + if (!this.editCancelled_ && this.hasBeenEdited() && + this.currentInputIsValid) { + cr.dispatchSimpleEvent(this, 'commitedit', true); + } else { + 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; + }, + + /** + * The HTML element that should have focus initially when editing starts. + * Should be overriden by subclasses. + * @type {HTMLElement} + */ + get initialFocusElement() { + return null; + }, + + /** + * 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. + */ + 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. + */ + hasBeenEdited: function() { + return true; + }, + + /** + * 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(); + } + }, + }; + + var InlineEditableItemList = cr.ui.define('list'); + + InlineEditableItemList.prototype = { + __proto__: DeletableItemList.prototype, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.addEventListener('blur', this.handleBlur_.bind(this), true); + }, + + /** + * Called when an element in the list is blurred. Removes selection (thus + * ending edit) if focus moves outside the list. + * @param {Event} e The blur event. + * @private + */ + handleBlur_: function(e) { + // When the blur event happens we do not know who is getting focus so we + // delay this a bit until we know if the new focus node is outside the + // list. + var list = this; + var doc = e.target.ownerDocument; + window.setTimeout(function() { + var activeElement = doc.activeElement; + if (!list.contains(activeElement)) + list.selectionModel.unselectAll(); + }, 50); + }, + }; + + // Export + return { + InlineEditableItem: InlineEditableItem, + InlineEditableItemList: InlineEditableItemList, + }; +}); diff --git a/chrome/browser/resources/options/options.html b/chrome/browser/resources/options/options.html index 2885d51..2a17cc2 100644 --- a/chrome/browser/resources/options/options.html +++ b/chrome/browser/resources/options/options.html @@ -22,7 +22,6 @@ <link rel="stylesheet" href="clear_browser_data.css"> <link rel="stylesheet" href="content_settings.css"> <link rel="stylesheet" href="cookies_view.css"> -<link rel="stylesheet" href="edit_search_engine_overlay.css"> <link rel="stylesheet" href="password_manager.css"> <link rel="stylesheet" href="password_manager_list.css"> <link rel="stylesheet" href="personal_options.css"> @@ -60,6 +59,7 @@ <script src="preferences.js"></script> <script src="pref_ui.js"></script> <script src="deletable_item_list.js"></script> +<script src="inline_editable_list.js"></script> <script src="list_inline_header_selection_controller.js"></script> <script src="options_page.js"></script> <if expr="pp_ifdef('chromeos')"> @@ -116,7 +116,6 @@ <script src="content_settings_ui.js"></script> <script src="cookies_tree.js"></script> <script src="cookies_view.js"></script> -<script src="edit_search_engine_overlay.js"></script> <script src="font_settings.js"></script> <script src="font_settings_ui.js"></script> <script src="import_data_overlay.js"></script> @@ -139,7 +138,6 @@ <include src="alert_overlay.html"> <include src="autofill_edit_address_overlay.html"> <include src="autofill_edit_creditcard_overlay.html"> - <include src="edit_search_engine_overlay.html"> <include src="import_data_overlay.html"> <include src="instant_confirm_overlay.html"> <if expr="pp_ifdef('chromeos')"> diff --git a/chrome/browser/resources/options/options.js b/chrome/browser/resources/options/options.js index d7734703..c5fdf07 100644 --- a/chrome/browser/resources/options/options.js +++ b/chrome/browser/resources/options/options.js @@ -14,7 +14,6 @@ var ContentSettings = options.ContentSettings; var ContentSettingsExceptionsArea = options.contentSettings.ContentSettingsExceptionsArea; var CookiesView = options.CookiesView; -var EditSearchEngineOverlay = options.EditSearchEngineOverlay; var FontSettings = options.FontSettings; var ImportDataOverlay = options.ImportDataOverlay; var InstantConfirmOverlay = options.InstantConfirmOverlay; @@ -124,7 +123,6 @@ function load() { OptionsPage.registerOverlay(AlertOverlay.getInstance()); OptionsPage.registerOverlay(AutoFillEditAddressOverlay.getInstance()); OptionsPage.registerOverlay(AutoFillEditCreditCardOverlay.getInstance()); - OptionsPage.registerOverlay(EditSearchEngineOverlay.getInstance()); OptionsPage.registerOverlay(ImportDataOverlay.getInstance()); OptionsPage.registerOverlay(InstantConfirmOverlay.getInstance()); diff --git a/chrome/browser/resources/options/options_page.css b/chrome/browser/resources/options/options_page.css index 6e04d75..ce6fd74 100644 --- a/chrome/browser/resources/options/options_page.css +++ b/chrome/browser/resources/options/options_page.css @@ -417,7 +417,9 @@ list .deletable-item { } list .deletable-item > :first-child { + -webkit-box-align: center; -webkit-box-flex: 1; + -webkit-padding-end: 3px; display: -webkit-box; } diff --git a/chrome/browser/resources/options/search_engine_manager.css b/chrome/browser/resources/options/search_engine_manager.css index 1eac1c4..0a0523c 100644 --- a/chrome/browser/resources/options/search_engine_manager.css +++ b/chrome/browser/resources/options/search_engine_manager.css @@ -18,7 +18,7 @@ border-top: 1px solid #d9d9d9; } -#searchEngineList .heading .name { +#searchEngineList .heading .name-column { font-weight: bold; } @@ -31,24 +31,72 @@ display: -webkit-box; } -#searchEngineList .name { +#searchEngineList .favicon { + padding: 1px 7px 0px 7px; + height: 16px; +} + +#searchEngineList .name-column { box-sizing: border-box; - width: 50%; + width: 37%; } -#searchEngineList .keyword { - -webkit-box-flex: 1; +#searchEngineList .keyword-column { + width: 26%; } -#searchEngineList > div:not(.heading) .keyword { +#searchEngineList .url-column { + width: 37%; +} + +#searchEngineList > div:not(.heading) .keyword-column, +#searchEngineList > div:not(.heading) .url-column { color: #666666; } +#searchEngineList .name-column, +#searchEngineList .keyword-column, +#searchEngineList .url-column { + -webkit-padding-end: 1ex; +} + #searchEngineList .default { font-weight: bold; } -#searchEngineList .name, #searchEngineList .keyword { +#searchEngineList .default .url-column { + font-weight: normal; +} + +#searchEngineList .name-column { + display: -webkit-box; +} + +#searchEngineList .name-column :last-child { + -webkit-box-flex: 1; +} + +#searchEngineList input { + box-sizing: border-box; + margin: 0; + width: 100%; +} + +#searchEngineList .static-text { overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; +} + +#searchEngineList > :not([editing]) [editmode=true] { + display: none; +} + +#searchEngineList > [editing] [editmode=false] { + display: none; +} + +#searchEngineList input.invalid { + /* TODO(stuartmorgan): Replace with actual badging */ + background-color: pink; } diff --git a/chrome/browser/resources/options/search_engine_manager.html b/chrome/browser/resources/options/search_engine_manager.html index 58d1ab1..4d8946e 100644 --- a/chrome/browser/resources/options/search_engine_manager.html +++ b/chrome/browser/resources/options/search_engine_manager.html @@ -5,10 +5,6 @@ <list id="searchEngineList"></list> </div> <div> - <div><button id="addSearchEngineButton" - i18n-content="addSearchEngineButton"></div> - <div><button id="editSearchEngineButton" disabled - i18n-content="editSearchEngineButton"></div> <div><button id="makeDefaultSearchEngineButton" disabled i18n-content="makeDefaultSearchEngineButton"></div> </div> diff --git a/chrome/browser/resources/options/search_engine_manager.js b/chrome/browser/resources/options/search_engine_manager.js index 7a56bad..9a99f57 100644 --- a/chrome/browser/resources/options/search_engine_manager.js +++ b/chrome/browser/resources/options/search_engine_manager.js @@ -37,23 +37,19 @@ cr.define('options', function() { this.selectionChanged_.bind(this)); var self = this; - $('addSearchEngineButton').onclick = function(event) { - chrome.send('editSearchEngine', ["-1"]); - OptionsPage.showOverlay('editSearchEngineOverlay'); - }; - $('editSearchEngineButton').onclick = function(event) { - chrome.send('editSearchEngine', [self.selectedModelIndex_]); - OptionsPage.showOverlay('editSearchEngineOverlay'); + // This is a temporary hack to allow the "Make Default" button to + // continue working despite the new list behavior of removing selection + // on focus loss. + // Once drag-and-drop is supported, so items can be moved into the default + // section, this button will go away entirely. + $('makeDefaultSearchEngineButton').onmousedown = function(event) { + self.pendingDefaultEngine_ = self.list_.selectedItem; }; $('makeDefaultSearchEngineButton').onclick = function(event) { chrome.send('managerSetDefaultSearchEngine', - [self.selectedModelIndex_]); + [self.pendingDefaultEngine_['modelIndex']]); + self.pendingDefaultEngine_ = null; }; - - // Remove Windows-style accelerators from button labels. - // TODO(stuartmorgan): Remove this once the strings are updated. - $('addSearchEngineButton').textContent = - localStrings.getStringWithoutAccelerator('addSearchEngineButton'); }, /** @@ -62,16 +58,11 @@ cr.define('options', function() { * @param {Array} engineList List of available search engines. */ updateSearchEngineList_: function(engineList) { - this.list_.dataModel = new ArrayDataModel(engineList); - }, - - /** - * Returns the currently selected list item's underlying model index. - * @private - */ - get selectedModelIndex_() { - var listIndex = this.list_.selectionModel.selectedIndex; - return this.list_.dataModel.item(listIndex)['modelIndex']; + var model = new ArrayDataModel(engineList); + model.push({ + 'modelIndex': '-1' + }); + this.list_.dataModel = model; }, /** @@ -80,11 +71,7 @@ cr.define('options', function() { * @param {!cr.Event} e Event with change info. */ selectionChanged_: function(e) { - var selectedIndex = this.list_.selectionModel.selectedIndex; - var engine = selectedIndex != -1 ? - this.list_.dataModel.item(selectedIndex) : null; - - $('editSearchEngineButton').disabled = engine == null; + var engine = this.list_.selectedItem || this.pendingDefaultEngine_; $('makeDefaultSearchEngineButton').disabled = !(engine && engine['canBeDefault']); }, @@ -94,6 +81,11 @@ cr.define('options', function() { SearchEngineManager.getInstance().updateSearchEngineList_(engineList); }; + SearchEngineManager.validityCheckCallback = function(validity, modelIndex) { + SearchEngineManager.getInstance().list_.validationComplete(validity, + modelIndex); + }; + // Export return { SearchEngineManager: SearchEngineManager diff --git a/chrome/browser/resources/options/search_engine_manager_engine_list.js b/chrome/browser/resources/options/search_engine_manager_engine_list.js index 1e7d93e..b375337 100644 --- a/chrome/browser/resources/options/search_engine_manager_engine_list.js +++ b/chrome/browser/resources/options/search_engine_manager_engine_list.js @@ -3,8 +3,8 @@ // found in the LICENSE file. cr.define('options.search_engines', function() { - const DeletableItem = options.DeletableItem; - const DeletableItemList = options.DeletableItemList; + const InlineEditableItemList = options.InlineEditableItemList; + const InlineEditableItem = options.InlineEditableItem; const ListInlineHeaderSelectionController = options.ListInlineHeaderSelectionController; @@ -31,46 +31,290 @@ cr.define('options.search_engines', function() { }; SearchEngineListItem.prototype = { - __proto__: DeletableItem.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 this is a placeholder for adding an engine. + * @type {boolean} + * @private + */ + isPlaceholder_: false, + + /** + * 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() { - DeletableItem.prototype.decorate.call(this); + InlineEditableItem.prototype.decorate.call(this); var engine = this.searchEngine_; - if (engine['heading']) + if (engine['modelIndex'] == '-1') { + this.isPlaceholder_ = true; + engine['name'] = ''; + engine['keyword'] = ''; + engine['url'] = ''; + } + + this.currentlyValid_ = !this.isPlaceholder_; + + if (engine['heading']) { this.classList.add('heading'); - else if (engine['default']) + this.editable = false; + } else if (engine['default']) { this.classList.add('default'); + } this.deletable = engine['canBeRemoved']; - var nameEl = this.ownerDocument.createElement('div'); - nameEl.className = 'name'; + var nameText = engine['name']; + var keywordText = engine['keyword']; + var urlText = engine['url']; if (engine['heading']) { - nameEl.textContent = engine['heading']; - } else { - nameEl.textContent = engine['name']; - nameEl.classList.add('favicon-cell'); - nameEl.style.backgroundImage = url('chrome://favicon/iconurl/' + - engine['iconURL']); + nameText = engine['heading']; + keywordText = localStrings.getString('searchEngineTableKeywordHeader'); + urlText = localStrings.getString('searchEngineTableURLHeader'); + } + + // Construct the name column. + var nameColEl = this.ownerDocument.createElement('div'); + nameColEl.className = 'name-column'; + this.contentElement.appendChild(nameColEl); + + // For non-heading rows, start with a favicon. + if (!engine['heading']) { + 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); } - this.contentElement.appendChild(nameEl); - var keywordEl = this.ownerDocument.createElement('div'); - keywordEl.className = 'keyword'; - keywordEl.textContent = engine['heading'] ? - localStrings.getString('searchEngineTableKeywordHeader') : - engine['keyword']; + var nameEl = this.createEditableTextCell_(nameText); + nameColEl.appendChild(nameEl); + + // Then the keyword column. + var keywordEl = this.createEditableTextCell_(keywordText); + keywordEl.className = 'keyword-column'; this.contentElement.appendChild(keywordEl); + + // And the URL column. + var urlEl = this.createEditableTextCell_(urlText); + urlEl.className = 'url-column'; + this.contentElement.appendChild(urlEl); + + // Do final adjustment to the input fields. + if (!engine['heading']) { + this.nameField_ = nameEl.querySelector('input'); + 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. + this.addEventListener('edit', this.onEditStarted_.bind(this)); + this.addEventListener('canceledit', this.onEditCancelled_.bind(this)); + this.addEventListener('commitedit', this.onEditCommitted_.bind(this)); + }, + + /** + * Returns a div containing an <input>, as well as static text if needed. + * @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('editmode', false); + container.appendChild(textEl); + } + + var inputEl = this.ownerDocument.createElement('input'); + inputEl.type = 'text'; + inputEl.value = text; + if (!this.isPlaceholder_) { + inputEl.setAttribute('editmode', true); + inputEl.staticVersion = textEl; + } + container.appendChild(inputEl); + + return container; + }, + + /** @inheritDoc */ + get initialFocusElement() { + return this.nameField_; + }, + + /** @inheritDoc */ + get currentInputIsValid() { + return !this.waitingForValidation_ && this.currentlyValid_; + }, + + /** @inheritDoc */ + hasBeenEdited: function(e) { + 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)]); + }, + + /** + * Called when committing an edit; updates the model. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + chrome.send('searchEngineEditCompleted', this.getInputFieldValues_()); + // Update the static version immediately to prevent flickering before + // the model update callback updates the UI. + var editFields = [ this.nameField_, this.keywordField_, this.urlField_ ]; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (staticLabel) + staticLabel.textContent = editFields[i].value; + } + }, + + /** + * 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'); + var engine = this.searchEngine_; + this.nameField_.value = engine['name']; + this.keywordField_.value = engine['keyword']; + this.urlField_.value = engine['url']; + + var editFields = [ this.nameField_, this.keywordField_, this.urlField_ ]; + for (var i = 0; i < editFields.length; i++) { + editFields[i].classList.remove('invalid'); + } + 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. + if (validity['name']) + this.nameField_.classList.remove('invalid'); + else + this.nameField_.classList.add('invalid'); + + if (validity['keyword']) + this.keywordField_.classList.remove('invalid'); + else + this.keywordField_.classList.add('invalid'); + + if (validity['url']) + this.urlField_.classList.remove('invalid'); + else + this.urlField_.classList.add('invalid'); + + this.currentlyValid_ = validity['name'] && validity['keyword'] && + validity['url']; }, }; var SearchEngineList = cr.ui.define('list'); SearchEngineList.prototype = { - __proto__: DeletableItemList.prototype, + __proto__: InlineEditableItemList.prototype, /** @inheritDoc */ createItem: function(searchEngine) { @@ -95,6 +339,20 @@ cr.define('options.search_engines', function() { canSelectIndex: function(index) { return !this.dataModel.item(index).hasOwnProperty('heading'); }, + + /** + * 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; + var listItem = this.getListItem(currentSelection); + if (listItem.editing && currentSelection['modelIndex'] == modelIndex) + listItem.validationComplete(validity); + }, }; // Export diff --git a/chrome/browser/resources/shared/js/cr/ui/list_single_selection_model.js b/chrome/browser/resources/shared/js/cr/ui/list_single_selection_model.js index f68818c..ff3c8c6 100644 --- a/chrome/browser/resources/shared/js/cr/ui/list_single_selection_model.js +++ b/chrome/browser/resources/shared/js/cr/ui/list_single_selection_model.js @@ -140,7 +140,6 @@ cr.define('cr.ui', function() { if (this.selectedIndexBefore_ != this.selectedIndex_) { var e = new Event('change'); var indexes = [this.selectedIndexBefore_, this.selectedIndex_]; - indexes.sort(); e.changes = indexes.filter(function(index) { return index != -1; }).map(function(index) { |