diff --git a/horizon/static/horizon/js/horizon.forms.js b/horizon/static/horizon/js/horizon.forms.js index cec82559ad..991badff1e 100644 --- a/horizon/static/horizon/js/horizon.forms.js +++ b/horizon/static/horizon/js/horizon.forms.js @@ -239,11 +239,112 @@ horizon.forms.init_examples = function (el) { $el.find("#create_image_form input#id_copy_from").attr("placeholder", "http://example.com/image.iso"); }; +horizon.forms.init_themable_select = function ($elem) { + "use strict"; + + // If not specified, find them all + $elem = $elem || $('body'); + + // If a jQuery object isn't passed in ... make it one + $elem = $elem instanceof jQuery ? $elem : $($elem); + + // Pass in a container OR the themable select itself + $elem = $elem.hasClass('themable-select') ? $elem : $elem.find('.themable-select'); + + // Update the select value if dropdown value changes + $elem.on('click', 'li a', function () { + var $this = $(this); + var $container = $this.closest('.themable-select'); + var value = $this.data('selectValue'); + + // Find select ... if we've searched for it before, then its cached on 'data-select' + var $select = $container.data('mySelect'); + if (!$select) { + $select = $container.find('select'); + $container.data('mySelect', $select); + } + + // Set the select if necessary + if($select.val() !== value) { + $select.val(value).change(); + } + }); + + $elem.find('li a[title]').tooltip(); + + // We need to rebuild the dropdown if the Select html ever + // changes via javascript. Mutation Observers are DOM change + // listeners. http://stackoverflow.com/a/11546242 + MutationObserver = window.MutationObserver || window.WebKitMutationObserver; // eslint-disable-line no-native-reassign + + var $targets = $elem.find('select'); + for (var ii = 0; ii < $targets.length; ii++) { + var observer = new MutationObserver(function (mutations) { // eslint-disable-line no-loop-func + + // Will return many mutations for a select box changing, + // we just need the target of one. + var $select = $(mutations[0].target).closest('select'); + var $options = $select.find('option'); + var list = []; + + for (var jj = 0; jj < $options.length; jj++) { + + // Build new list item and anchor tag. + var $list_item = $(document.createElement('li')) + .attr('data-original-index', jj) + .attr('select-value', $options[jj].attr('value')); + + var $anchor = $(document.createElement('a')); + + // Append option text to anchor, then to list item. + $anchor.text($($options[jj]).text()).appendTo($list_item); + list[jj] = $list_item; + } + + // Add the new list to the dropdown. + $select.siblings('.dropdown-menu').html(list).change(); + }); + + var config = { + childList: true, + subtree: true, + attributes: false, + characterData: true + }; + + observer.observe($targets[ii], config); + } + + // Update the dropdown if select value changes + $elem.children('select').on('change', function () { + var $this = $(this); + var thisVal = $this.val(); + var thisLabel = $this.find('option[value="' + thisVal + '"]').text(); + + // Go find the title element + var $title = $this.parents('.themable-select').find('.dropdown-title'); + + // Set dropdown title to first option if the select menu is unset + if (thisLabel === null || thisLabel.length === 0) { + thisLabel = $this.find('option').first().text(); + } + + // Update the dropdown-title if necessary. + if (thisLabel !== $title.text()) { + $title.text(thisLabel); + } + }); +}; + horizon.addInitFunction(horizon.forms.init = function () { - horizon.forms.handle_submit($('body')); + var $body = $('body'); + horizon.forms.handle_submit($body); horizon.modals.addModalInitFunction(horizon.forms.handle_submit); - horizon.forms.init_examples($("body")); + horizon.forms.init_themable_select(); + horizon.modals.addModalInitFunction(horizon.forms.init_themable_select); + + horizon.forms.init_examples($body); horizon.modals.addModalInitFunction(horizon.forms.init_examples); horizon.forms.handle_snapshot_source(); @@ -255,14 +356,14 @@ horizon.addInitFunction(horizon.forms.init = function () { horizon.forms.handle_subnet_subnetpool(); if (!horizon.conf.disable_password_reveal) { - horizon.forms.add_password_fields_reveal_buttons($("body")); + horizon.forms.add_password_fields_reveal_buttons($body); horizon.modals.addModalInitFunction( horizon.forms.add_password_fields_reveal_buttons); } // Bind event handlers to confirm dangerous actions. // Stops angular form buttons from triggering this event - $("body").on("click", "form button:not([ng-click]).btn-danger", function (evt) { + $body.on("click", "form button:not([ng-click]).btn-danger", function (evt) { horizon.datatables.confirm(this); evt.preventDefault(); }); @@ -278,10 +379,13 @@ horizon.addInitFunction(horizon.forms.init = function () { $switchables = $fieldset.find('select.switchable'); $switchables.each(function (index, switchable) { - var $switchable = $(switchable), - slug = $switchable.data('slug'), - visible = $switchable.is(':visible'), - val = $switchable.val(); + var $switchable = $(switchable); + var slug = $switchable.data('slug'); + var isThemable = $switchable.parent('.themable-select').length > 0; + var visible = isThemable + ? $switchable.siblings('.dropdown-toggle').is(':visible') + : $switchable.is(':visible'); + var val = $switchable.val(); function handle_switched_field(index, input){ var $input = $(input), diff --git a/horizon/tables/actions.py b/horizon/tables/actions.py index 7d89cd5326..1f1e356d83 100644 --- a/horizon/tables/actions.py +++ b/horizon/tables/actions.py @@ -523,6 +523,12 @@ class FilterAction(BaseAction): return True return False + def get_select_options(self): + """Provide the value and string for the template to render. + """ + if self.filter_choices: + return [x[:2] for x in self.filter_choices] + class NameFilterAction(FilterAction): """A filter action for name property.""" diff --git a/horizon/templates/horizon/common/_data_table_table_actions.html b/horizon/templates/horizon/common/_data_table_table_actions.html index 40cb31cdc5..a7b6c6fd7d 100644 --- a/horizon/templates/horizon/common/_data_table_table_actions.html +++ b/horizon/templates/horizon/common/_data_table_table_actions.html @@ -1,4 +1,4 @@ -{% load i18n %} +{% load i18n bootstrap %}
{% block table_filter %} {% if filter.filter_type == 'fixed' %} @@ -24,11 +24,9 @@
{% elif filter.filter_type == 'server' %} @@ -48,7 +46,7 @@ If additional actions are defined, scoop them into the actions dropdown menu {% endcomment %} {% if table_actions_menu|length > 0 %} -
+
{% if table_actions_buttons|length > 0 %} {% trans "More Actions" %} diff --git a/horizon/templates/horizon/common/fields/_themable_select.html b/horizon/templates/horizon/common/fields/_themable_select.html new file mode 100644 index 0000000000..8ac18e620e --- /dev/null +++ b/horizon/templates/horizon/common/fields/_themable_select.html @@ -0,0 +1,61 @@ +{% load horizon %} + +{% minifyspace %} + +{% endminifyspace %} diff --git a/openstack_dashboard/static/dashboard/scss/components/_dropdowns.scss b/openstack_dashboard/static/dashboard/scss/components/_dropdowns.scss index 7807894937..843298cd7c 100644 --- a/openstack_dashboard/static/dashboard/scss/components/_dropdowns.scss +++ b/openstack_dashboard/static/dashboard/scss/components/_dropdowns.scss @@ -34,6 +34,20 @@ @include box-shadow(none); } + &.btn-primary { + color: $brand-primary; + } + &.btn-danger { + color: $brand-danger; + } + &.btn-warning { + color: $brand-warning; + } + &.btn-info { + color: $brand-info; + } + + @include dropdown-button('default', $dropdown-link-color, $dropdown-link-hover-bg); @include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color); @include dropdown-button('info', $btn-info-bg, $btn-info-bg, $btn-info-color); @@ -41,6 +55,12 @@ @include dropdown-button('danger', $btn-danger-bg, $btn-danger-bg, $btn-danger-color); } +.table_search .themable-select, +.table_actions_menu { + display: inline-block; + position: relative; +} + .table_actions { float: right; @extend .form-inline; diff --git a/openstack_dashboard/static/dashboard/scss/components/_selects.scss b/openstack_dashboard/static/dashboard/scss/components/_selects.scss new file mode 100644 index 0000000000..2b7a4ead19 --- /dev/null +++ b/openstack_dashboard/static/dashboard/scss/components/_selects.scss @@ -0,0 +1,17 @@ +// +// Themable Selects +// --------------------------------- + +.themable-select { + .dropdown-title { + text-align: left; + + &:after { + content: ' '; + } + } + + select { + display: none; + } +} diff --git a/openstack_dashboard/static/dashboard/scss/horizon.scss b/openstack_dashboard/static/dashboard/scss/horizon.scss index b1b68fb117..b3d3f304d9 100644 --- a/openstack_dashboard/static/dashboard/scss/horizon.scss +++ b/openstack_dashboard/static/dashboard/scss/horizon.scss @@ -36,6 +36,7 @@ @import "components/resource_browser"; @import "components/resource_topology"; @import "components/selection_menu"; +@import "components/selects"; @import "components/sidebar"; @import "components/tables"; @import "components/transfer_tables"; diff --git a/openstack_dashboard/test/integration_tests/regions/tables.py b/openstack_dashboard/test/integration_tests/regions/tables.py index 9cdcc49bc4..1bb6ab26cf 100644 --- a/openstack_dashboard/test/integration_tests/regions/tables.py +++ b/openstack_dashboard/test/integration_tests/regions/tables.py @@ -57,7 +57,7 @@ class TableRegion(baseregion.BaseRegion): _search_button_locator = (by.By.CSS_SELECTOR, 'div.table_search > button') _search_option_locator = (by.By.CSS_SELECTOR, - 'div.table_search select.form-control') + 'div.table_search > .themable-select') marker_name = 'marker' prev_marker_name = 'prev_marker' @@ -72,6 +72,10 @@ class TableRegion(baseregion.BaseRegion): def _prev_locator(self): return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.prev_marker_name + def _search_menu_value_locator(self, value): + return (by.By.CSS_SELECTOR, + 'ul.dropdown-menu a[data-select-value="%s"]' % value) + def __init__(self, driver, conf): self._default_src_locator = self._table_locator(self.__class__.name) super(TableRegion, self).__init__(driver, conf) @@ -105,8 +109,10 @@ class TableRegion(baseregion.BaseRegion): self._click_search_btn() def set_filter_value(self, value): - srch_option = self._get_element(*self._search_option_locator) - return self._select_dropdown_by_value(value, srch_option) + search_menu = self._get_element(*self._search_option_locator) + search_menu.click() + item_locator = self._search_menu_value_locator(value) + search_menu.find_element(*item_locator).click() def get_row(self, column_name, text, exact_match=True): """Get row that contains specified text in specified column.