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:
parent
929d1ed50d
commit
00b842e989
horizon
static/horizon/js
tables
templates/horizon/common
openstack_dashboard
static/dashboard/scss
test/integration_tests/regions
@ -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),
|
||||
|
@ -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."""
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% load i18n bootstrap %}
|
||||
<div class="table_actions clearfix">
|
||||
{% block table_filter %}
|
||||
{% if filter.filter_type == 'fixed' %}
|
||||
@ -24,11 +24,9 @@
|
||||
</div>
|
||||
{% elif filter.filter_type == 'server' %}
|
||||
<div class="table_search">
|
||||
<select name="{{ filter.get_param_name }}_field" class="form-control">
|
||||
{% for choice in filter.filter_choices %}
|
||||
<option value="{{ choice.0 }}" {% if choice.0 == filter.filter_field %} selected{% endif %}>{{ choice.1 }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options value=filter.filter_field %}
|
||||
{% include 'horizon/common/fields/_themable_select.html' %}
|
||||
{% endwith %}
|
||||
<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>
|
||||
</div>
|
||||
@ -48,7 +46,7 @@
|
||||
If additional actions are defined, scoop them into the actions dropdown menu
|
||||
{% endcomment %}
|
||||
{% 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="#">
|
||||
{% if table_actions_buttons|length > 0 %}
|
||||
{% trans "More Actions" %}
|
||||
|
@ -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 %}
|
@ -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;
|
||||
|
@ -0,0 +1,17 @@
|
||||
//
|
||||
// Themable Selects
|
||||
// ---------------------------------
|
||||
|
||||
.themable-select {
|
||||
.dropdown-title {
|
||||
text-align: left;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user