Horizon selects are now themable: Volumes
Horizon was using a standard select input. Unfortunately, this type of input is only customizable to a small extent. I/We have created a new input type for forms to use that is a bootstrap dropdown. This will allow bootstrap themes to affect selects the same way they do for check boxes and radio buttons and the like. Material's inputs are a bit special, so custom css was added to keep style parity with Bootswatch's Paper. co-authored-by: brian.tully@hp.com co-authored-by: hurgleburgler@gmail.com ThemableSelect widget template has been modified so that shadow select element does not contain any classes (which are usually used for customizing appearance - since it's hidden, it doesn't need that). Given that we could match legacy select element as 'div > select.form-control' with no possibility that SelectFormFieldRegion (legacy select widget region) will bind a hidden part of ThemableSelect widget and intercept all commands send to ThemableSelectFormFieldRegion. ThemableSelectFormFieldRegion was modified to still use legacy widget wrapper for getting 'name' and 'value' properties, while doing all other things on its own, not using Selenium wrapper for <select>. Co-Authored-By: Timur Sufiev <tsufiev@mirantis.com> Change-Id: Ie921b3adc2e1d3388d3c2aa1f76afe3af6ceb87b Partially-implements: blueprint horizon-theme-css-reorg
This commit is contained in:
parent
9d7cff5d96
commit
20c17d28d3
@ -35,6 +35,9 @@ from horizon.forms.fields import MultiIPField # noqa
|
|||||||
from horizon.forms.fields import SelectWidget # noqa
|
from horizon.forms.fields import SelectWidget # noqa
|
||||||
from horizon.forms.fields import ThemableCheckboxInput # noqa
|
from horizon.forms.fields import ThemableCheckboxInput # noqa
|
||||||
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
|
from horizon.forms.fields import ThemableCheckboxSelectMultiple # noqa
|
||||||
|
from horizon.forms.fields import ThemableChoiceField # noqa
|
||||||
|
from horizon.forms.fields import ThemableDynamicChoiceField # noqa
|
||||||
|
from horizon.forms.fields import ThemableSelectWidget # noqa
|
||||||
from horizon.forms.views import ModalFormMixin # noqa
|
from horizon.forms.views import ModalFormMixin # noqa
|
||||||
from horizon.forms.views import ModalFormView # noqa
|
from horizon.forms.views import ModalFormView # noqa
|
||||||
|
|
||||||
@ -49,6 +52,9 @@ __all__ = [
|
|||||||
"DynamicChoiceField",
|
"DynamicChoiceField",
|
||||||
"ThemableCheckboxInput",
|
"ThemableCheckboxInput",
|
||||||
"ThemableCheckboxSelectMultiple",
|
"ThemableCheckboxSelectMultiple",
|
||||||
|
"ThemableChoiceField",
|
||||||
|
"ThemableDynamicChoiceField",
|
||||||
|
"ThemableSelectWidget",
|
||||||
"IPField",
|
"IPField",
|
||||||
"IPv4",
|
"IPv4",
|
||||||
"IPv6",
|
"IPv6",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import itertools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
@ -23,9 +24,12 @@ from django.core import urlresolvers
|
|||||||
from django.forms import fields
|
from django.forms import fields
|
||||||
from django.forms.utils import flatatt # noqa
|
from django.forms.utils import flatatt # noqa
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
|
from django.template import Context # noqa
|
||||||
|
from django.template.loader import get_template # noqa
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import Promise # noqa
|
from django.utils.functional import Promise # noqa
|
||||||
from django.utils import html
|
from django.utils import html
|
||||||
|
from django.utils.safestring import mark_safe # noqa
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
|
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
|
||||||
@ -164,7 +168,7 @@ class SelectWidget(widgets.Select):
|
|||||||
....
|
....
|
||||||
....
|
....
|
||||||
|
|
||||||
widget=forms.SelectWidget( attrs={'class': 'switchable',
|
widget=forms.ThemableSelect( attrs={'class': 'switchable',
|
||||||
'data-slug': 'source'},
|
'data-slug': 'source'},
|
||||||
transform_html_attrs=get_title )
|
transform_html_attrs=get_title )
|
||||||
|
|
||||||
@ -187,26 +191,84 @@ class SelectWidget(widgets.Select):
|
|||||||
other_html = (u' selected="selected"'
|
other_html = (u' selected="selected"'
|
||||||
if option_value in selected_choices else '')
|
if option_value in selected_choices else '')
|
||||||
|
|
||||||
if callable(self.transform_html_attrs):
|
other_html += self.transform_option_html_attrs(option_label)
|
||||||
html_attrs = self.transform_html_attrs(option_label)
|
|
||||||
other_html += flatatt(html_attrs)
|
|
||||||
|
|
||||||
|
data_attr_html = self.get_data_attrs(option_label)
|
||||||
|
if data_attr_html:
|
||||||
|
other_html += ' ' + data_attr_html
|
||||||
|
|
||||||
|
option_label = self.transform_option_label(option_label)
|
||||||
|
|
||||||
|
return u'<option value="%s"%s>%s</option>' % (
|
||||||
|
html.escape(option_value), other_html, option_label)
|
||||||
|
|
||||||
|
def get_data_attrs(self, option_label):
|
||||||
|
other_html = []
|
||||||
if not isinstance(option_label, (six.string_types, Promise)):
|
if not isinstance(option_label, (six.string_types, Promise)):
|
||||||
for data_attr in self.data_attrs:
|
for data_attr in self.data_attrs:
|
||||||
data_value = html.conditional_escape(
|
data_value = html.conditional_escape(
|
||||||
force_text(getattr(option_label,
|
force_text(getattr(option_label,
|
||||||
data_attr, "")))
|
data_attr, "")))
|
||||||
other_html += ' data-%s="%s"' % (data_attr, data_value)
|
other_html.append('data-%s="%s"' % (data_attr, data_value))
|
||||||
|
return ' '.join(other_html)
|
||||||
|
|
||||||
if callable(self.transform):
|
def transform_option_label(self, option_label):
|
||||||
|
if (not isinstance(option_label, (six.string_types, Promise)) and
|
||||||
|
callable(self.transform)):
|
||||||
option_label = self.transform(option_label)
|
option_label = self.transform(option_label)
|
||||||
|
return html.conditional_escape(force_text(option_label))
|
||||||
|
|
||||||
return u'<option value="%s"%s>%s</option>' % (
|
def transform_option_html_attrs(self, option_label):
|
||||||
html.escape(option_value), other_html,
|
if not callable(self.transform_html_attrs):
|
||||||
html.conditional_escape(force_text(option_label)))
|
return ''
|
||||||
|
return flatatt(self.transform_html_attrs(option_label))
|
||||||
|
|
||||||
|
|
||||||
class DynamicSelectWidget(widgets.Select):
|
class ThemableSelectWidget(SelectWidget):
|
||||||
|
"""Bootstrap base select field widget."""
|
||||||
|
def render(self, name, value, attrs=None, choices=()):
|
||||||
|
# NOTE(woodnt): Currently the "attrs" contents are being added to the
|
||||||
|
# select that's hidden. It's unclear whether this is the
|
||||||
|
# desired behavior. In some cases, the attribute should
|
||||||
|
# remain solely on the now-hidden select. But in others
|
||||||
|
# if it should live on the bootstrap button (visible)
|
||||||
|
# or both.
|
||||||
|
|
||||||
|
new_choices = []
|
||||||
|
for opt_value, opt_label in itertools.chain(self.choices, choices):
|
||||||
|
other_html = self.transform_option_html_attrs(opt_label)
|
||||||
|
|
||||||
|
data_attr_html = self.get_data_attrs(opt_label)
|
||||||
|
if data_attr_html:
|
||||||
|
other_html += ' ' + data_attr_html
|
||||||
|
|
||||||
|
opt_label = self.transform_option_label(opt_label)
|
||||||
|
|
||||||
|
if other_html:
|
||||||
|
new_choices.append((opt_value, opt_label, other_html))
|
||||||
|
else:
|
||||||
|
new_choices.append((opt_value, opt_label))
|
||||||
|
|
||||||
|
initial_value = value
|
||||||
|
if value is None and new_choices:
|
||||||
|
initial_value = new_choices[0][1]
|
||||||
|
|
||||||
|
attrs = self.build_attrs(attrs)
|
||||||
|
id = attrs.pop('id', 'id_%s' % name)
|
||||||
|
|
||||||
|
template = get_template('horizon/common/fields/_themable_select.html')
|
||||||
|
context = Context({
|
||||||
|
'name': name,
|
||||||
|
'options': new_choices,
|
||||||
|
'id': id,
|
||||||
|
'value': value,
|
||||||
|
'initial_value': initial_value,
|
||||||
|
'select_attrs': attrs,
|
||||||
|
})
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicSelectWidget(SelectWidget):
|
||||||
"""A subclass of the ``Select`` widget which renders extra attributes for
|
"""A subclass of the ``Select`` widget which renders extra attributes for
|
||||||
use in callbacks to handle dynamic changes to the available choices.
|
use in callbacks to handle dynamic changes to the available choices.
|
||||||
"""
|
"""
|
||||||
@ -231,6 +293,15 @@ class DynamicSelectWidget(widgets.Select):
|
|||||||
return self.add_item_link
|
return self.add_item_link
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableDynamicSelectWidget(ThemableSelectWidget, DynamicSelectWidget):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableChoiceField(fields.ChoiceField):
|
||||||
|
"""Bootstrap based select field."""
|
||||||
|
widget = ThemableSelectWidget
|
||||||
|
|
||||||
|
|
||||||
class DynamicChoiceField(fields.ChoiceField):
|
class DynamicChoiceField(fields.ChoiceField):
|
||||||
"""A subclass of ``ChoiceField`` with additional properties that make
|
"""A subclass of ``ChoiceField`` with additional properties that make
|
||||||
dynamically updating its elements easier.
|
dynamically updating its elements easier.
|
||||||
@ -251,6 +322,10 @@ class DynamicChoiceField(fields.ChoiceField):
|
|||||||
self.widget.add_item_link_args = add_item_link_args
|
self.widget.add_item_link_args = add_item_link_args
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableDynamicChoiceField(DynamicChoiceField):
|
||||||
|
widget = ThemableDynamicSelectWidget
|
||||||
|
|
||||||
|
|
||||||
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
||||||
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
||||||
pass
|
pass
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% elif filter.filter_type == 'server' %}
|
{% elif filter.filter_type == 'server' %}
|
||||||
<div class="table_search">
|
<div class="table_search">
|
||||||
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options value=filter.filter_field %}
|
{% with name=filter.get_param_name|add:'_field' options=filter.get_select_options stand_alone=1 value=filter.filter_field %}
|
||||||
{% include 'horizon/common/fields/_themable_select.html' %}
|
{% include 'horizon/common/fields/_themable_select.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<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 }}" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% load horizon %}
|
{% load horizon %}
|
||||||
|
|
||||||
{% minifyspace %}
|
{% minifyspace %}
|
||||||
<div class="themable-select dropdown" xmlns="http://www.w3.org/1999/html">
|
<div class="themable-select dropdown {% if not stand_alone %} form-control{% endif %}" xmlns="http://www.w3.org/1999/html">
|
||||||
<button type="button" class="btn btn-default dropdown-toggle"
|
<button type="button" class="btn btn-default dropdown-toggle"
|
||||||
data-toggle="dropdown"{% if value %}
|
data-toggle="dropdown"{% if value %}
|
||||||
title="{{ value }}" {% endif %}
|
title="{{ value }}" {% endif %}
|
||||||
@ -38,9 +38,7 @@
|
|||||||
name="{{ name }}"
|
name="{{ name }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for k,v in select_attrs.items %}
|
{% for k,v in select_attrs.items %}
|
||||||
{% if k == 'class' %}
|
{% if k != 'class' %}
|
||||||
class="form-control {{ v }}"
|
|
||||||
{% else %}
|
|
||||||
{{ k|safe }}="{{ v }}"
|
{{ k|safe }}="{{ v }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
{% block modal-body %}
|
{% block modal-body %}
|
||||||
{% if show_attach %}
|
{% if show_attach %}
|
||||||
<h3>{% trans "Attach To Instance" %}</h3>
|
<h3>{% trans "Attach To Instance" %}</h3>
|
||||||
<fieldset>
|
|
||||||
{% include "horizon/common/_form_fields.html" %}
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
</fieldset>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -80,28 +80,29 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
||||||
attrs={'rows': 4}),
|
attrs={'rows': 4}),
|
||||||
label=_("Description"), required=False)
|
label=_("Description"), required=False)
|
||||||
volume_source_type = forms.ChoiceField(label=_("Volume Source"),
|
volume_source_type = forms.ChoiceField(
|
||||||
|
label=_("Volume Source"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(attrs={
|
widget=forms.ThemableSelectWidget(attrs={
|
||||||
'class': 'switchable',
|
'class': 'switchable',
|
||||||
'data-slug': 'source'}))
|
'data-slug': 'source'}))
|
||||||
snapshot_source = forms.ChoiceField(
|
snapshot_source = forms.ChoiceField(
|
||||||
label=_("Use snapshot as a source"),
|
label=_("Use snapshot as a source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'snapshot-selector'},
|
attrs={'class': 'snapshot-selector'},
|
||||||
data_attrs=('size', 'name'),
|
data_attrs=('size', 'name'),
|
||||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||||
required=False)
|
required=False)
|
||||||
image_source = forms.ChoiceField(
|
image_source = forms.ChoiceField(
|
||||||
label=_("Use image as a source"),
|
label=_("Use image as a source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'image-selector'},
|
attrs={'class': 'image-selector'},
|
||||||
data_attrs=('size', 'name', 'min_disk'),
|
data_attrs=('size', 'name', 'min_disk'),
|
||||||
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
||||||
required=False)
|
required=False)
|
||||||
volume_source = forms.ChoiceField(
|
volume_source = forms.ChoiceField(
|
||||||
label=_("Use a volume as source"),
|
label=_("Use a volume as source"),
|
||||||
widget=forms.SelectWidget(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'image-selector'},
|
attrs={'class': 'image-selector'},
|
||||||
data_attrs=('size', 'name'),
|
data_attrs=('size', 'name'),
|
||||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||||
@ -109,7 +110,7 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
type = forms.ChoiceField(
|
type = forms.ChoiceField(
|
||||||
label=_("Type"),
|
label=_("Type"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'switched',
|
attrs={'class': 'switched',
|
||||||
'data-switch-on': 'source',
|
'data-switch-on': 'source',
|
||||||
'data-source-no_source_type': _('Type'),
|
'data-source-no_source_type': _('Type'),
|
||||||
@ -118,7 +119,7 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
availability_zone = forms.ChoiceField(
|
availability_zone = forms.ChoiceField(
|
||||||
label=_("Availability Zone"),
|
label=_("Availability Zone"),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(
|
widget=forms.ThemableSelectWidget(
|
||||||
attrs={'class': 'switched',
|
attrs={'class': 'switched',
|
||||||
'data-switch-on': 'source',
|
'data-switch-on': 'source',
|
||||||
'data-source-no_source_type': _('Availability Zone'),
|
'data-source-no_source_type': _('Availability Zone'),
|
||||||
@ -419,7 +420,7 @@ class CreateForm(forms.SelfHandlingForm):
|
|||||||
|
|
||||||
|
|
||||||
class AttachForm(forms.SelfHandlingForm):
|
class AttachForm(forms.SelfHandlingForm):
|
||||||
instance = forms.ChoiceField(label=_("Attach to Instance"),
|
instance = forms.ThemableChoiceField(label=_("Attach to Instance"),
|
||||||
help_text=_("Select an instance to "
|
help_text=_("Select an instance to "
|
||||||
"attach to."))
|
"attach to."))
|
||||||
|
|
||||||
@ -645,7 +646,7 @@ class UploadToImageForm(forms.SelfHandlingForm):
|
|||||||
attrs={'readonly': 'readonly'}))
|
attrs={'readonly': 'readonly'}))
|
||||||
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
||||||
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
||||||
widget=forms.Select(),
|
widget=forms.ThemableSelectWidget(),
|
||||||
required=False)
|
required=False)
|
||||||
force = forms.BooleanField(
|
force = forms.BooleanField(
|
||||||
label=pgettext_lazy("Force upload volume in in-use status to image",
|
label=pgettext_lazy("Force upload volume in in-use status to image",
|
||||||
@ -751,11 +752,11 @@ class RetypeForm(forms.SelfHandlingForm):
|
|||||||
name = forms.CharField(label=_('Volume Name'),
|
name = forms.CharField(label=_('Volume Name'),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'readonly': 'readonly'}))
|
attrs={'readonly': 'readonly'}))
|
||||||
volume_type = forms.ChoiceField(label=_('Type'))
|
volume_type = forms.ThemableChoiceField(label=_('Type'))
|
||||||
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
||||||
('on-demand', _('On Demand'))]
|
('on-demand', _('On Demand'))]
|
||||||
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
||||||
widget=forms.Select(),
|
widget=forms.ThemableSelectWidget(),
|
||||||
choices=(MIGRATION_POLICY_CHOICES),
|
choices=(MIGRATION_POLICY_CHOICES),
|
||||||
initial='never',
|
initial='never',
|
||||||
required=False)
|
required=False)
|
||||||
|
@ -37,17 +37,19 @@
|
|||||||
&.btn-primary {
|
&.btn-primary {
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-danger {
|
&.btn-danger {
|
||||||
color: $brand-danger;
|
color: $brand-danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-warning {
|
&.btn-warning {
|
||||||
color: $brand-warning;
|
color: $brand-warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-info {
|
&.btn-info {
|
||||||
color: $brand-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);
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
// ---------------------------------
|
// ---------------------------------
|
||||||
|
|
||||||
.themable-select {
|
.themable-select {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.dropdown-title {
|
.dropdown-title {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
@ -12,6 +14,61 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
&,
|
||||||
|
&.form-control {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dropdowns within forms should look like all other
|
||||||
|
// form elements
|
||||||
|
.form-group .themable-select {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: $input-bg;
|
||||||
|
background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
|
||||||
|
border-color: $input-border;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
li > a {
|
||||||
|
@include text-overflow();
|
||||||
|
color: $input-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $dropdown-link-hover-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For vertical forms, we'll want the button to take on the entire width.
|
||||||
|
// think old-school launch-instance.
|
||||||
|
form:not(.form-inline) .form-group .themable-select {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.dropdown-toggle {
|
||||||
|
@extend .btn-block;
|
||||||
|
|
||||||
|
.fa-caret-down {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-title {
|
||||||
|
@include text-overflow();
|
||||||
|
width: calc(100% - 1em);
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
}
|
}
|
@ -228,7 +228,7 @@ class VolumesPage(basepage.BaseNavigationPage):
|
|||||||
|
|
||||||
|
|
||||||
class VolumeAttachForm(forms.BaseFormRegion):
|
class VolumeAttachForm(forms.BaseFormRegion):
|
||||||
_attach_to_instance_selector = (By.CSS_SELECTOR, 'select[name="instance"]')
|
_attach_to_instance_selector = (By.CSS_SELECTOR, 'div > .themable-select')
|
||||||
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
|
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
|
||||||
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
|
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
|
||||||
|
|
||||||
@ -239,7 +239,9 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
|||||||
@property
|
@property
|
||||||
def instance_selector(self):
|
def instance_selector(self):
|
||||||
src_elem = self._get_element(*self._attach_to_instance_selector)
|
src_elem = self._get_element(*self._attach_to_instance_selector)
|
||||||
return forms.SelectFormFieldRegion(self.driver, self.conf, src_elem)
|
return forms.ThemableSelectFormFieldRegion(
|
||||||
|
self.driver, self.conf, src_elem=src_elem,
|
||||||
|
strict_options_match=False)
|
||||||
|
|
||||||
def detach(self, volume, instance):
|
def detach(self, volume, instance):
|
||||||
detach_button = self.attachments_table.find_element(
|
detach_button = self.attachments_table.find_element(
|
||||||
@ -248,9 +250,5 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
|||||||
return forms.BaseFormRegion(self.driver, self.conf)
|
return forms.BaseFormRegion(self.driver, self.conf)
|
||||||
|
|
||||||
def attach_instance(self, instance_name):
|
def attach_instance(self, instance_name):
|
||||||
instance = filter(lambda x: x.startswith(instance_name),
|
self.instance_selector.text = instance_name
|
||||||
self.instance_selector.options.values())
|
|
||||||
if not instance:
|
|
||||||
raise AttributeError("Unable to select {0}".format(instance_name))
|
|
||||||
self.instance_selector.text = instance[0]
|
|
||||||
self.submit()
|
self.submit()
|
||||||
|
@ -36,7 +36,7 @@ class FieldFactory(baseregion.BaseRegion):
|
|||||||
field_cls._element_locator_str_suffix))
|
field_cls._element_locator_str_suffix))
|
||||||
elements = super(FieldFactory, self)._get_elements(*locator)
|
elements = super(FieldFactory, self)._get_elements(*locator)
|
||||||
for element in elements:
|
for element in elements:
|
||||||
yield field_cls(self.driver, self.conf, element)
|
yield field_cls(self.driver, self.conf, src_elem=element)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register_field_cls(cls, field_class, base_classes=None):
|
def register_field_cls(cls, field_class, base_classes=None):
|
||||||
@ -174,7 +174,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
|
|||||||
class SelectFormFieldRegion(BaseFormFieldRegion):
|
class SelectFormFieldRegion(BaseFormFieldRegion):
|
||||||
"""Select box field."""
|
"""Select box field."""
|
||||||
|
|
||||||
_element_locator_str_suffix = 'div > select'
|
_element_locator_str_suffix = 'div > select.form-control'
|
||||||
|
|
||||||
def is_displayed(self):
|
def is_displayed(self):
|
||||||
return self.element._el.is_displayed()
|
return self.element._el.is_displayed()
|
||||||
@ -218,6 +218,67 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
|
|||||||
self.element.select_by_value(value)
|
self.element.select_by_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
|
||||||
|
"""Select box field."""
|
||||||
|
|
||||||
|
_element_locator_str_suffix = 'div > .themable-select'
|
||||||
|
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
|
||||||
|
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
|
||||||
|
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
|
||||||
|
|
||||||
|
def __init__(self, driver, conf, strict_options_match=True, **kwargs):
|
||||||
|
super(ThemableSelectFormFieldRegion, self).__init__(
|
||||||
|
driver, conf, **kwargs)
|
||||||
|
self.strict_options_match = strict_options_match
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden_element(self):
|
||||||
|
elem = self._get_element(*self._raw_select_locator)
|
||||||
|
return SelectFormFieldRegion(self.driver, self.conf, src_elem=elem)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.hidden_element.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._get_element(*self._selected_label_locator).text.strip()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.hidden_element.value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
return self._get_elements(*self._dropdown_menu_locator)
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, text):
|
||||||
|
if text != self.text:
|
||||||
|
self.src_elem.click()
|
||||||
|
for option in self.options:
|
||||||
|
if self.strict_options_match:
|
||||||
|
match = text == option.text.strip()
|
||||||
|
else:
|
||||||
|
match = option.text.startswith(text)
|
||||||
|
if match:
|
||||||
|
option.click()
|
||||||
|
return
|
||||||
|
raise ValueError('Widget "%s" does have an option with text "%s"'
|
||||||
|
% (self.name, text))
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
if value != self.value:
|
||||||
|
self.src_elem.click()
|
||||||
|
for option in self.options:
|
||||||
|
if value == option.get_attribute('data-select-value'):
|
||||||
|
option.click()
|
||||||
|
return
|
||||||
|
raise ValueError('Widget "%s" does have an option with value "%s"'
|
||||||
|
% (self.name, value))
|
||||||
|
|
||||||
|
|
||||||
class BaseFormRegion(baseregion.BaseRegion):
|
class BaseFormRegion(baseregion.BaseRegion):
|
||||||
"""Base class for forms."""
|
"""Base class for forms."""
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
@import "components/navbar";
|
@import "components/navbar";
|
||||||
@import "components/pie_charts";
|
@import "components/pie_charts";
|
||||||
@import "components/quota";
|
@import "components/quota";
|
||||||
|
@import "components/selects";
|
||||||
@import "components/sidebar";
|
@import "components/sidebar";
|
||||||
@import "components/tables";
|
@import "components/tables";
|
||||||
@import "components/table_actions";
|
@import "components/table_actions";
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.form-group .themable-select .dropdown-menu {
|
||||||
|
@extend .dropdown-menu-right;
|
||||||
|
}
|
@ -7,6 +7,7 @@
|
|||||||
@import "components/magic_search";
|
@import "components/magic_search";
|
||||||
@import "components/messages";
|
@import "components/messages";
|
||||||
@import "components/navbar";
|
@import "components/navbar";
|
||||||
|
@import "components/selects";
|
||||||
@import "components/sidebar";
|
@import "components/sidebar";
|
||||||
|
|
||||||
.login .splash-logo {
|
.login .splash-logo {
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
// The dropdowns within forms should look like all other
|
||||||
|
// form elements, so this mimics Bootswatch's Paper's
|
||||||
|
// bootswatch.scss form-group styles
|
||||||
|
|
||||||
|
.form-group .themable-select {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&.active,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
@include box-shadow(inset 0 -1px 0 $table-border-color);
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@include box-shadow(inset 0 -2px 0 $brand-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled],
|
||||||
|
&[readonly] {
|
||||||
|
@include box-shadow(none);
|
||||||
|
border-bottom: 1px dotted $table-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
&-sm {
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-lg {
|
||||||
|
font-size: $font-size-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open .dropdown-menu {
|
||||||
|
background-color: $dropdown-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin material-select-states($state, $color) {
|
||||||
|
.form-group.#{$state} .themable-select {
|
||||||
|
&,
|
||||||
|
&.open {
|
||||||
|
.btn {
|
||||||
|
&,
|
||||||
|
&:hover,
|
||||||
|
&.active,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
border-bottom: none;
|
||||||
|
@include box-shadow(inset 0 -2px 0 $color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include material-select-states('has-warning', $brand-warning);
|
||||||
|
@include material-select-states('has-error', $brand-danger);
|
||||||
|
@include material-select-states('has-success', $brand-success);
|
Loading…
x
Reference in New Issue
Block a user