Horizon selects are now themable: Table Actions

Horizon was using a standard select input.  Unfortunately,
this type of input is only customizable to a small extent.

Also, a bug was noted.  Things were being marked as btn-groups
that were not button groups.  This was fixed.

Co-Authored-By: Ryan Peters <rjpeter2@gmail.com>
Co-Authored-By: Matthew Wood <woodm1979@gmail.com>
Co-Authored-By: Brian Tully <brian.tully@hp.com>

Change-Id: I048f001bf71c5d9a8d13451b7e5a892122f481c8
Partially-implements: blueprint horizon-theme-css-reorg
This commit is contained in:
Diana Whitten 2016-03-07 11:41:48 -07:00
parent 929d1ed50d
commit 00b842e989
8 changed files with 231 additions and 18 deletions

View File

@ -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"); $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.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.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.modals.addModalInitFunction(horizon.forms.init_examples);
horizon.forms.handle_snapshot_source(); horizon.forms.handle_snapshot_source();
@ -255,14 +356,14 @@ horizon.addInitFunction(horizon.forms.init = function () {
horizon.forms.handle_subnet_subnetpool(); horizon.forms.handle_subnet_subnetpool();
if (!horizon.conf.disable_password_reveal) { 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.modals.addModalInitFunction(
horizon.forms.add_password_fields_reveal_buttons); horizon.forms.add_password_fields_reveal_buttons);
} }
// Bind event handlers to confirm dangerous actions. // Bind event handlers to confirm dangerous actions.
// Stops angular form buttons from triggering this event // 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); horizon.datatables.confirm(this);
evt.preventDefault(); evt.preventDefault();
}); });
@ -278,10 +379,13 @@ horizon.addInitFunction(horizon.forms.init = function () {
$switchables = $fieldset.find('select.switchable'); $switchables = $fieldset.find('select.switchable');
$switchables.each(function (index, switchable) { $switchables.each(function (index, switchable) {
var $switchable = $(switchable), var $switchable = $(switchable);
slug = $switchable.data('slug'), var slug = $switchable.data('slug');
visible = $switchable.is(':visible'), var isThemable = $switchable.parent('.themable-select').length > 0;
val = $switchable.val(); var visible = isThemable
? $switchable.siblings('.dropdown-toggle').is(':visible')
: $switchable.is(':visible');
var val = $switchable.val();
function handle_switched_field(index, input){ function handle_switched_field(index, input){
var $input = $(input), var $input = $(input),

View File

@ -523,6 +523,12 @@ class FilterAction(BaseAction):
return True return True
return False 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): class NameFilterAction(FilterAction):
"""A filter action for name property.""" """A filter action for name property."""

View File

@ -1,4 +1,4 @@
{% load i18n %} {% load i18n bootstrap %}
<div class="table_actions clearfix"> <div class="table_actions clearfix">
{% block table_filter %} {% block table_filter %}
{% if filter.filter_type == 'fixed' %} {% if filter.filter_type == 'fixed' %}
@ -24,11 +24,9 @@
</div> </div>
{% elif filter.filter_type == 'server' %} {% elif filter.filter_type == 'server' %}
<div class="table_search"> <div class="table_search">
<select name="{{ filter.get_param_name }}_field" class="form-control"> {% with name=filter.get_param_name|add:'_field' options=filter.get_select_options value=filter.filter_field %}
{% for choice in filter.filter_choices %} {% include 'horizon/common/fields/_themable_select.html' %}
<option value="{{ choice.0 }}" {% if choice.0 == filter.filter_field %} selected{% endif %}>{{ choice.1 }}</option> {% endwith %}
{% endfor %}
</select>
<input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" /> <input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
<button type="submit" class="btn btn-default {{ filter.get_final_css|safe }}" {{ filter.attr_string_nc|safe }}>{% trans "Filter" %}</button> <button type="submit" class="btn btn-default {{ filter.get_final_css|safe }}" {{ filter.attr_string_nc|safe }}>{% trans "Filter" %}</button>
</div> </div>
@ -48,7 +46,7 @@
If additional actions are defined, scoop them into the actions dropdown menu If additional actions are defined, scoop them into the actions dropdown menu
{% endcomment %} {% endcomment %}
{% if table_actions_menu|length > 0 %} {% if table_actions_menu|length > 0 %}
<div class="btn-group table_actions_menu"> <div class="table_actions_menu">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">
{% if table_actions_buttons|length > 0 %} {% if table_actions_buttons|length > 0 %}
{% trans "More Actions" %} {% trans "More Actions" %}

View File

@ -0,0 +1,61 @@
{% load horizon %}
{% minifyspace %}
<div class="themable-select dropdown" xmlns="http://www.w3.org/1999/html">
<button type="button" class="btn btn-default dropdown-toggle"
data-toggle="dropdown"{% if value %}
title="{{ value }}" {% endif %}
aria-expanded="false">
<span class="dropdown-title">
{% if initial_value %}
{{ initial_value }}
{% elif value %}
{% for option in options %}
{% if option.0 == value %}
{{ option.1 }}
{% endif %}
{% endfor %}
{% else %}
{{ options.0.1 }}
{% endif %}
</span>
<span class="fa fa-caret-down"></span>
</button>
<ul class="dropdown-menu">
{% for option in options %}
<li data-original-index="{{ forloop.counter0 }}">
<a data-select-value="{{ option.0 }}"
{% if option.2 %}
{{ option.2|safe|default:'' }}
{% endif %}>{{ option.1 }}</a>
</li>
{% endfor %}
</ul>
<select
{% if id %}
id="{{ id }}"{% endif %}
{% if name %}
name="{{ name }}"
{% endif %}
{% for k,v in select_attrs.items %}
{% if k == 'class' %}
class="form-control {{ v }}"
{% else %}
{{ k|safe }}="{{ v }}"
{% endif %}
{% endfor %}
>
{% for option in options %}
<option value="{{ option.0 }}"
{% if option.0 == value %}
selected="selected"
{% endif %}
{% if option.2 %}
{{ option.2|safe }}
{% endif %}>
{{ option.1 }}
</option>
{% endfor %}
</select>
</div>
{% endminifyspace %}

View File

@ -34,6 +34,20 @@
@include box-shadow(none); @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('default', $dropdown-link-color, $dropdown-link-hover-bg);
@include dropdown-button('primary', $btn-primary-bg, $btn-primary-bg, $btn-primary-color); @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); @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); @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 { .table_actions {
float: right; float: right;
@extend .form-inline; @extend .form-inline;

View File

@ -0,0 +1,17 @@
//
// Themable Selects
// ---------------------------------
.themable-select {
.dropdown-title {
text-align: left;
&:after {
content: ' ';
}
}
select {
display: none;
}
}

View File

@ -36,6 +36,7 @@
@import "components/resource_browser"; @import "components/resource_browser";
@import "components/resource_topology"; @import "components/resource_topology";
@import "components/selection_menu"; @import "components/selection_menu";
@import "components/selects";
@import "components/sidebar"; @import "components/sidebar";
@import "components/tables"; @import "components/tables";
@import "components/transfer_tables"; @import "components/transfer_tables";

View File

@ -57,7 +57,7 @@ class TableRegion(baseregion.BaseRegion):
_search_button_locator = (by.By.CSS_SELECTOR, _search_button_locator = (by.By.CSS_SELECTOR,
'div.table_search > button') 'div.table_search > button')
_search_option_locator = (by.By.CSS_SELECTOR, _search_option_locator = (by.By.CSS_SELECTOR,
'div.table_search select.form-control') 'div.table_search > .themable-select')
marker_name = 'marker' marker_name = 'marker'
prev_marker_name = 'prev_marker' prev_marker_name = 'prev_marker'
@ -72,6 +72,10 @@ class TableRegion(baseregion.BaseRegion):
def _prev_locator(self): def _prev_locator(self):
return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.prev_marker_name 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): def __init__(self, driver, conf):
self._default_src_locator = self._table_locator(self.__class__.name) self._default_src_locator = self._table_locator(self.__class__.name)
super(TableRegion, self).__init__(driver, conf) super(TableRegion, self).__init__(driver, conf)
@ -105,8 +109,10 @@ class TableRegion(baseregion.BaseRegion):
self._click_search_btn() self._click_search_btn()
def set_filter_value(self, value): def set_filter_value(self, value):
srch_option = self._get_element(*self._search_option_locator) search_menu = self._get_element(*self._search_option_locator)
return self._select_dropdown_by_value(value, srch_option) 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): def get_row(self, column_name, text, exact_match=True):
"""Get row that contains specified text in specified column. """Get row that contains specified text in specified column.