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 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