Merge "Horizon selects are now themable: Volumes"
This commit is contained in:
commit
4e384db0cf
@ -35,6 +35,9 @@ from horizon.forms.fields import MultiIPField # noqa
|
||||
from horizon.forms.fields import SelectWidget # noqa
|
||||
from horizon.forms.fields import ThemableCheckboxInput # 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 ModalFormView # noqa
|
||||
|
||||
@ -49,6 +52,9 @@ __all__ = [
|
||||
"DynamicChoiceField",
|
||||
"ThemableCheckboxInput",
|
||||
"ThemableCheckboxSelectMultiple",
|
||||
"ThemableChoiceField",
|
||||
"ThemableDynamicChoiceField",
|
||||
"ThemableSelectWidget",
|
||||
"IPField",
|
||||
"IPv4",
|
||||
"IPv6",
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
import netaddr
|
||||
@ -23,9 +24,12 @@ from django.core import urlresolvers
|
||||
from django.forms import fields
|
||||
from django.forms.utils import flatatt # noqa
|
||||
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.functional import Promise # noqa
|
||||
from django.utils import html
|
||||
from django.utils.safestring import mark_safe # noqa
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
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'},
|
||||
transform_html_attrs=get_title )
|
||||
|
||||
@ -187,26 +191,84 @@ class SelectWidget(widgets.Select):
|
||||
other_html = (u' selected="selected"'
|
||||
if option_value in selected_choices else '')
|
||||
|
||||
if callable(self.transform_html_attrs):
|
||||
html_attrs = self.transform_html_attrs(option_label)
|
||||
other_html += flatatt(html_attrs)
|
||||
other_html += self.transform_option_html_attrs(option_label)
|
||||
|
||||
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)):
|
||||
for data_attr in self.data_attrs:
|
||||
data_value = html.conditional_escape(
|
||||
force_text(getattr(option_label,
|
||||
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):
|
||||
option_label = self.transform(option_label)
|
||||
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)
|
||||
return html.conditional_escape(force_text(option_label))
|
||||
|
||||
return u'<option value="%s"%s>%s</option>' % (
|
||||
html.escape(option_value), other_html,
|
||||
html.conditional_escape(force_text(option_label)))
|
||||
def transform_option_html_attrs(self, option_label):
|
||||
if not callable(self.transform_html_attrs):
|
||||
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
|
||||
use in callbacks to handle dynamic changes to the available choices.
|
||||
"""
|
||||
@ -231,6 +293,15 @@ class DynamicSelectWidget(widgets.Select):
|
||||
return self.add_item_link
|
||||
|
||||
|
||||
class ThemableDynamicSelectWidget(ThemableSelectWidget, DynamicSelectWidget):
|
||||
pass
|
||||
|
||||
|
||||
class ThemableChoiceField(fields.ChoiceField):
|
||||
"""Bootstrap based select field."""
|
||||
widget = ThemableSelectWidget
|
||||
|
||||
|
||||
class DynamicChoiceField(fields.ChoiceField):
|
||||
"""A subclass of ``ChoiceField`` with additional properties that make
|
||||
dynamically updating its elements easier.
|
||||
@ -251,6 +322,10 @@ class DynamicChoiceField(fields.ChoiceField):
|
||||
self.widget.add_item_link_args = add_item_link_args
|
||||
|
||||
|
||||
class ThemableDynamicChoiceField(DynamicChoiceField):
|
||||
widget = ThemableDynamicSelectWidget
|
||||
|
||||
|
||||
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
|
||||
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
|
||||
pass
|
||||
|
@ -24,7 +24,7 @@
|
||||
</div>
|
||||
{% elif filter.filter_type == 'server' %}
|
||||
<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' %}
|
||||
{% endwith %}
|
||||
<input class="form-control" value="{{ filter.filter_string|default:'' }}" type="text" name="{{ filter.get_param_name }}" />
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% load horizon %}
|
||||
|
||||
{% 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"
|
||||
data-toggle="dropdown"{% if value %}
|
||||
title="{{ value }}" {% endif %}
|
||||
@ -38,9 +38,7 @@
|
||||
name="{{ name }}"
|
||||
{% endif %}
|
||||
{% for k,v in select_attrs.items %}
|
||||
{% if k == 'class' %}
|
||||
class="form-control {{ v }}"
|
||||
{% else %}
|
||||
{% if k != 'class' %}
|
||||
{{ k|safe }}="{{ v }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
@ -4,9 +4,7 @@
|
||||
{% block modal-body %}
|
||||
{% if show_attach %}
|
||||
<h3>{% trans "Attach To Instance" %}</h3>
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -80,28 +80,29 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
description = forms.CharField(max_length=255, widget=forms.Textarea(
|
||||
attrs={'rows': 4}),
|
||||
label=_("Description"), required=False)
|
||||
volume_source_type = forms.ChoiceField(label=_("Volume Source"),
|
||||
required=False,
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'switchable',
|
||||
'data-slug': 'source'}))
|
||||
volume_source_type = forms.ChoiceField(
|
||||
label=_("Volume Source"),
|
||||
required=False,
|
||||
widget=forms.ThemableSelectWidget(attrs={
|
||||
'class': 'switchable',
|
||||
'data-slug': 'source'}))
|
||||
snapshot_source = forms.ChoiceField(
|
||||
label=_("Use snapshot as a source"),
|
||||
widget=forms.SelectWidget(
|
||||
widget=forms.ThemableSelectWidget(
|
||||
attrs={'class': 'snapshot-selector'},
|
||||
data_attrs=('size', 'name'),
|
||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||
required=False)
|
||||
image_source = forms.ChoiceField(
|
||||
label=_("Use image as a source"),
|
||||
widget=forms.SelectWidget(
|
||||
widget=forms.ThemableSelectWidget(
|
||||
attrs={'class': 'image-selector'},
|
||||
data_attrs=('size', 'name', 'min_disk'),
|
||||
transform=lambda x: "%s (%s)" % (x.name, filesizeformat(x.bytes))),
|
||||
required=False)
|
||||
volume_source = forms.ChoiceField(
|
||||
label=_("Use a volume as source"),
|
||||
widget=forms.SelectWidget(
|
||||
widget=forms.ThemableSelectWidget(
|
||||
attrs={'class': 'image-selector'},
|
||||
data_attrs=('size', 'name'),
|
||||
transform=lambda x: "%s (%s GiB)" % (x.name, x.size)),
|
||||
@ -109,7 +110,7 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
type = forms.ChoiceField(
|
||||
label=_("Type"),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
widget=forms.ThemableSelectWidget(
|
||||
attrs={'class': 'switched',
|
||||
'data-switch-on': 'source',
|
||||
'data-source-no_source_type': _('Type'),
|
||||
@ -118,7 +119,7 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
availability_zone = forms.ChoiceField(
|
||||
label=_("Availability Zone"),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
widget=forms.ThemableSelectWidget(
|
||||
attrs={'class': 'switched',
|
||||
'data-switch-on': 'source',
|
||||
'data-source-no_source_type': _('Availability Zone'),
|
||||
@ -419,9 +420,9 @@ class CreateForm(forms.SelfHandlingForm):
|
||||
|
||||
|
||||
class AttachForm(forms.SelfHandlingForm):
|
||||
instance = forms.ChoiceField(label=_("Attach to Instance"),
|
||||
help_text=_("Select an instance to "
|
||||
"attach to."))
|
||||
instance = forms.ThemableChoiceField(label=_("Attach to Instance"),
|
||||
help_text=_("Select an instance to "
|
||||
"attach to."))
|
||||
|
||||
device = forms.CharField(label=_("Device Name"),
|
||||
widget=forms.TextInput(attrs={'placeholder':
|
||||
@ -645,7 +646,7 @@ class UploadToImageForm(forms.SelfHandlingForm):
|
||||
attrs={'readonly': 'readonly'}))
|
||||
image_name = forms.CharField(max_length=255, label=_('Image Name'))
|
||||
disk_format = forms.ChoiceField(label=_('Disk Format'),
|
||||
widget=forms.Select(),
|
||||
widget=forms.ThemableSelectWidget(),
|
||||
required=False)
|
||||
force = forms.BooleanField(
|
||||
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'),
|
||||
widget=forms.TextInput(
|
||||
attrs={'readonly': 'readonly'}))
|
||||
volume_type = forms.ChoiceField(label=_('Type'))
|
||||
volume_type = forms.ThemableChoiceField(label=_('Type'))
|
||||
MIGRATION_POLICY_CHOICES = [('never', _('Never')),
|
||||
('on-demand', _('On Demand'))]
|
||||
migration_policy = forms.ChoiceField(label=_('Migration Policy'),
|
||||
widget=forms.Select(),
|
||||
widget=forms.ThemableSelectWidget(),
|
||||
choices=(MIGRATION_POLICY_CHOICES),
|
||||
initial='never',
|
||||
required=False)
|
||||
|
@ -37,17 +37,19 @@
|
||||
&.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);
|
||||
|
@ -3,15 +3,72 @@
|
||||
// ---------------------------------
|
||||
|
||||
.themable-select {
|
||||
.dropdown-title {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
}
|
||||
.dropdown-title {
|
||||
text-align: left;
|
||||
|
||||
&:after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
display: none;
|
||||
&,
|
||||
&.form-control {
|
||||
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):
|
||||
_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"]')
|
||||
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
|
||||
|
||||
@ -239,7 +239,9 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
||||
@property
|
||||
def instance_selector(self):
|
||||
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):
|
||||
detach_button = self.attachments_table.find_element(
|
||||
@ -248,9 +250,5 @@ class VolumeAttachForm(forms.BaseFormRegion):
|
||||
return forms.BaseFormRegion(self.driver, self.conf)
|
||||
|
||||
def attach_instance(self, instance_name):
|
||||
instance = filter(lambda x: x.startswith(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.instance_selector.text = instance_name
|
||||
self.submit()
|
||||
|
@ -36,7 +36,7 @@ class FieldFactory(baseregion.BaseRegion):
|
||||
field_cls._element_locator_str_suffix))
|
||||
elements = super(FieldFactory, self)._get_elements(*locator)
|
||||
for element in elements:
|
||||
yield field_cls(self.driver, self.conf, element)
|
||||
yield field_cls(self.driver, self.conf, src_elem=element)
|
||||
|
||||
@classmethod
|
||||
def register_field_cls(cls, field_class, base_classes=None):
|
||||
@ -174,7 +174,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
|
||||
class SelectFormFieldRegion(BaseFormFieldRegion):
|
||||
"""Select box field."""
|
||||
|
||||
_element_locator_str_suffix = 'div > select'
|
||||
_element_locator_str_suffix = 'div > select.form-control'
|
||||
|
||||
def is_displayed(self):
|
||||
return self.element._el.is_displayed()
|
||||
@ -218,6 +218,67 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
|
||||
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):
|
||||
"""Base class for forms."""
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
@import "components/navbar";
|
||||
@import "components/pie_charts";
|
||||
@import "components/quota";
|
||||
@import "components/selects";
|
||||
@import "components/sidebar";
|
||||
@import "components/tables";
|
||||
@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/messages";
|
||||
@import "components/navbar";
|
||||
@import "components/selects";
|
||||
@import "components/sidebar";
|
||||
|
||||
.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…
Reference in New Issue
Block a user