Merge "Horizon Checkboxes are now themeable."

This commit is contained in:
Jenkins 2016-03-03 17:09:48 +00:00 committed by Gerrit Code Review
commit 721c963b9d
24 changed files with 288 additions and 87 deletions

View File

@ -209,6 +209,7 @@ full use of the Bootstrap theme architecture.
* Login_ * Login_
* Tabs_ * Tabs_
* Alerts_ * Alerts_
* Checkboxes_
Step 1 Step 1
------ ------
@ -321,6 +322,13 @@ Alerts
Alerts use the basic Bootstrap brand colors. See **Colors** section of your Alerts use the basic Bootstrap brand colors. See **Colors** section of your
variables file for specifics. variables file for specifics.
Checkboxes
----------
Horizon uses icon fonts to represent checkboxes. In order to customize
this, you simply need to override the standard scss. For an example of
this, see themes/material/static/horizon/components/_checkboxes.scss
Bootswatch and Material Design Bootswatch and Material Design
------------------------------ ------------------------------

View File

@ -33,6 +33,8 @@ from horizon.forms.fields import IPv4 # noqa
from horizon.forms.fields import IPv6 # noqa from horizon.forms.fields import IPv6 # noqa
from horizon.forms.fields import MultiIPField # noqa 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 ThemableCheckboxSelectMultiple # 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
@ -45,6 +47,8 @@ __all__ = [
"ModalFormMixin", "ModalFormMixin",
"DynamicTypedChoiceField", "DynamicTypedChoiceField",
"DynamicChoiceField", "DynamicChoiceField",
"ThemableCheckboxInput",
"ThemableCheckboxSelectMultiple",
"IPField", "IPField",
"IPv4", "IPv4",
"IPv6", "IPv6",

View File

@ -16,6 +16,7 @@ import re
import netaddr import netaddr
import six import six
import uuid
from django.core.exceptions import ValidationError # noqa from django.core.exceptions import ValidationError # noqa
from django.core import urlresolvers from django.core import urlresolvers
@ -253,3 +254,44 @@ class DynamicChoiceField(fields.ChoiceField):
class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField): class DynamicTypedChoiceField(DynamicChoiceField, fields.TypedChoiceField):
"""Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``.""" """Simple mix of ``DynamicChoiceField`` and ``TypedChoiceField``."""
pass pass
class ThemableCheckboxInput(widgets.CheckboxInput):
"""A subclass of the ``Checkbox`` widget which renders extra markup to
allow a custom checkbox experience.
"""
def render(self, name, value, attrs=None):
label_for = attrs.get('id', '')
if not label_for:
attrs['id'] = uuid.uuid4()
label_for = attrs['id']
return html.format_html(
u'<div class="themable-checkbox">{}<label for="{}"></label></div>',
super(ThemableCheckboxInput, self).render(name, value, attrs),
label_for
)
class ThemableCheckboxChoiceInput(widgets.CheckboxChoiceInput):
def render(self, name=None, value=None, attrs=None, choices=()):
if self.id_for_label:
label_for = html.format_html(' for="{}"', self.id_for_label)
else:
label_for = ''
attrs = dict(self.attrs, **attrs) if attrs else self.attrs
return html.format_html(
u'<div class="themable-checkbox">{}<label{}>' +
u'<span>{}</span></label></div>',
self.tag(attrs), label_for, self.choice_label
)
class ThemableCheckboxFieldRenderer(widgets.CheckboxFieldRenderer):
choice_input_class = ThemableCheckboxChoiceInput
class ThemableCheckboxSelectMultiple(widgets.CheckboxSelectMultiple):
renderer = ThemableCheckboxFieldRenderer
_empty_value = []

View File

@ -89,9 +89,12 @@ horizon.datatables = {
// Only replace row if the html content has changed // Only replace row if the html content has changed
if($new_row.html() !== $row.html()) { if($new_row.html() !== $row.html()) {
if($row.find('.table-row-multi-select:checkbox').is(':checked')) {
// Directly accessing the checked property of the element
// is MUCH faster than using jQuery's helper method
if($row.find('.table-row-multi-select')[0].checked) {
// Preserve the checkbox if it's already clicked // Preserve the checkbox if it's already clicked
$new_row.find('.table-row-multi-select:checkbox').prop('checked', true); $new_row.find('.table-row-multi-select').prop('checked', true);
} }
$row.replaceWith($new_row); $row.replaceWith($new_row);
// Reset tablesorter's data cache. // Reset tablesorter's data cache.
@ -151,37 +154,50 @@ horizon.datatables = {
}); });
}, },
validate_button: function ($form) { validate_button: function ($form, disable_button) {
// Enable or disable table batch action buttons based on row selection. // Enable or disable table batch action buttons based on row selection.
$form = $form || $(".table_wrapper > form"); $form = $form || $(".table_wrapper > form");
$form.each(function () { $form.each(function () {
var $this = $(this); var $this = $(this);
var checkboxes = $this.find(".table-row-multi-select:checkbox"); var $action_buttons = $this.find('.table_actions button[data-batch-action="true"]');
var action_buttons = $this.find('.table_actions button[data-batch-action="true"]'); if (typeof disable_button == undefined) {
action_buttons.toggleClass("disabled", !checkboxes.filter(":checked").length); disable_button = $this.find(".table-row-multi-select").filter(":checked").length > 0;
}
$action_buttons.toggleClass("disabled", disable_button);
}); });
}, },
initialize_checkboxes_behavior: function() { initialize_checkboxes_behavior: function() {
// Bind the "select all" checkbox action. // Bind the "select all" checkbox action.
$('div.table_wrapper, #modal_wrapper').on('click', 'table thead .multi_select_column .table-row-multi-select:checkbox', function() { $('.table_wrapper, #modal_wrapper')
var $this = $(this), .on('change', '.table-row-multi-select', function() {
$table = $this.closest('table'), var $this = $(this);
is_checked = $this.prop('checked'), var $table = $this.closest('table');
checkboxes = $table.find('tbody .table-row-multi-select:visible:checkbox'); var is_checked = $this.prop('checked');
checkboxes.prop('checked', is_checked);
}); if ($this.hasClass('multi-select-header')) {
// Change "select all" checkbox behavior while any checkbox is checked/unchecked.
$("div.table_wrapper, #modal_wrapper").on("click", 'table tbody .table-row-multi-select:checkbox', function () { // Only select / deselect the visible rows
var $table = $(this).closest('table'); $table.find('tbody tr:visible .table-row-multi-select')
var $multi_select_checkbox = $table.find('thead .multi_select_column .table-row-multi-select:checkbox'); .prop('checked', is_checked);
var any_unchecked = $table.find("tbody .table-row-multi-select:checkbox").not(":checked");
$multi_select_checkbox.prop('checked', any_unchecked.length === 0); } else {
});
// Enable/disable table batch action buttons when row selection changes. // Find the master checkbox
$("div.table_wrapper, #modal_wrapper").on("click", '.table-row-multi-select:checkbox', function () { var $multi_select_checkbox = $table.find('.multi-select-header');
horizon.datatables.validate_button($(this).closest("form"));
}); // Determine if there are any unchecked checkboxes in the table
var $checkboxes = $table.find('tbody .table-row-multi-select');
var not_checked = $checkboxes.not(':checked').length;
is_checked = $checkboxes.length != not_checked;
// If there are none, then check the master checkbox
$multi_select_checkbox.prop('checked', not_checked == 0);
}
// Pass in whether it should be visible, no point in doing this twice
horizon.datatables.validate_button($this.closest('form'), !is_checked);
});
}, },
initialize_table_tooltips: function() { initialize_table_tooltips: function() {
@ -229,7 +245,7 @@ horizon.datatables.confirm = function (action) {
var actions_div = $(action).closest("div"); var actions_div = $(action).closest("div");
if(actions_div.hasClass("table_actions") || actions_div.hasClass("table_actions_menu")) { if(actions_div.hasClass("table_actions") || actions_div.hasClass("table_actions_menu")) {
// One or more checkboxes selected // One or more checkboxes selected
$("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checkbox:checked").each(function() { $("#"+closest_table_id+" tr[data-display]").has(".table-row-multi-select:checked").each(function() {
name_array.push(" \"" + $(this).attr("data-display") + "\""); name_array.push(" \"" + $(this).attr("data-display") + "\"");
}); });
name_array.join(", "); name_array.join(", ");
@ -480,11 +496,30 @@ horizon.datatables.set_table_sorting = function (parent) {
}); });
}; };
horizon.datatables.add_table_checkboxes = function(parent) { horizon.datatables.add_table_checkboxes = function($parent) {
$(parent).find('table thead .multi_select_column').each(function(index, thead) { $($parent).find('table thead .multi_select_column').each(function() {
if (!$(thead).find('.table-row-multi-select:checkbox').length && var $thead = $(this);
$(thead).parents('table').find('tbody .table-row-multi-select:checkbox').length) { if (!$thead.find('.table-row-multi-select').length &&
$(thead).append('<input type="checkbox" class="table-row-multi-select">'); $thead.parents('table').find('tbody .table-row-multi-select').length) {
// Build up the themable checkbox
var $container = $(document.createElement('div'))
.addClass('themable-checkbox');
// Create the input checkbox
var $input = $(document.createElement('input'))
.attr('type', 'checkbox')
.addClass('table-row-multi-select multi-select-header')
.uniqueId()
.appendTo($container);
// Create the label
$(document.createElement('label'))
.attr('for', $input.attr('id'))
.appendTo($container);
// Append to the thead last, for speed
$thead.append($container);
} }
}); });
}; };
@ -576,10 +611,11 @@ horizon.addInitFunction(horizon.datatables.init = function() {
horizon.datatables.initialize_table_tooltips(); horizon.datatables.initialize_table_tooltips();
// Trigger run-once setup scripts for tables. // Trigger run-once setup scripts for tables.
horizon.datatables.add_table_checkboxes($('body')); var $body = $('body');
horizon.datatables.set_table_sorting($('body')); horizon.datatables.add_table_checkboxes($body);
horizon.datatables.set_table_query_filter($('body')); horizon.datatables.set_table_sorting($body);
horizon.datatables.set_table_fixed_filter($('body')); horizon.datatables.set_table_query_filter($body);
horizon.datatables.set_table_fixed_filter($body);
horizon.datatables.disable_actions_on_submit(); horizon.datatables.disable_actions_on_submit();
// Also apply on tables in modal views. // Also apply on tables in modal views.

View File

@ -37,6 +37,7 @@ import six
from horizon import conf from horizon import conf
from horizon import exceptions from horizon import exceptions
from horizon.forms import ThemableCheckboxInput
from horizon import messages from horizon import messages
from horizon.tables.actions import FilterAction # noqa from horizon.tables.actions import FilterAction # noqa
from horizon.tables.actions import LinkAction # noqa from horizon.tables.actions import LinkAction # noqa
@ -672,7 +673,7 @@ class Cell(html.HTMLElement):
if column.auto == "multi_select": if column.auto == "multi_select":
data = "" data = ""
if row.can_be_selected(datum): if row.can_be_selected(datum):
widget = forms.CheckboxInput(check_test=lambda value: False) widget = ThemableCheckboxInput(check_test=lambda value: False)
# Convert value to string to avoid accidental type conversion # Convert value to string to avoid accidental type conversion
data = widget.render('object_ids', data = widget.render('object_ids',
six.text_type(table.get_object_id(datum)), six.text_type(table.get_object_id(datum)),

View File

@ -3,27 +3,7 @@
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}"> <div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
{% if field|is_checkbox %} {% if field|is_checkbox %}
<div class="{{ classes.single_value }}"> <div class="{{ classes.single_value }}">
<div class="checkbox"> {% include 'horizon/common/fields/_themable_checkbox.html' %}
{% if field.auto_id %}
<label
{% if field.field.required and form.required_css_class %}
class="{{ form.required_css_class }}"
{% endif %}>
{{ field }}
<span>{{ field.label }}</span>
{% if field.field.required %}{% include "horizon/common/_form_field_required.html" %}{% endif %}
{% if field.help_text %}
<span class="help-icon" data-toggle="tooltip"
data-placement="top" title="{{ field.help_text|safe }}">
<span class="fa fa-question-circle"></span>
</span>
{% endif %}
</label>
{% endif %}
{% for error in field.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
</div>
</div> </div>
{% elif field|is_radio %} {% elif field|is_radio %}
{% if field.auto_id %} {% if field.auto_id %}

View File

@ -3,7 +3,13 @@
<div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}"> <div class="form-group{% if field.errors %} has-error{% endif %} {{ field.css_classes }}">
<label class="control-label col-sm-3 {% if field.field.required %}{{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label> <label class="control-label col-sm-3 {% if field.field.required %}{{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="col-sm-9 {{ classes.value }} {{ field|wrapper_classes }}"> <div class="col-sm-9 {{ classes.value }} {{ field|wrapper_classes }}">
{{ field|add_bootstrap_class }} {% if field|is_checkbox %}
{% with is_vertical=1 %}
{% include 'horizon/common/fields/_themable_checkbox.html' %}
{% endwith %}
{% else %}
{{ field|add_bootstrap_class }}
{% endif %}
{% for error in field.errors %} {% for error in field.errors %}
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span> <span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
{% empty %} {% empty %}

View File

@ -0,0 +1,24 @@
<div class="themable-checkbox">
{% if field.auto_id %}
{{ field }}
<label
{% if field.field.required and form.required_css_class %}
class="{{ form.required_css_class }}"
{% endif %}
for="{{ field.auto_id }}">
{% if not is_vertical %}
<span>{{ field.label }}</span>
{% endif %}
{% if field.field.required %}{% include "horizon/common/_form_field_required.html" %}{% endif %}
{% if field.help_text %}
<span class="help-icon" data-toggle="tooltip"
data-placement="top" title="{{ field.help_text|safe }}">
<span class="fa fa-question-circle"></span>
</span>
{% endif %}
</label>
{% endif %}
{% for error in field.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
</div>

View File

@ -13,6 +13,7 @@
import django.forms import django.forms
from django import template as django_template from django import template as django_template
register = django_template.Library() register = django_template.Library()

View File

@ -9,6 +9,7 @@
window.WEBROOT = '{{ WEBROOT }}'; window.WEBROOT = '{{ WEBROOT }}';
</script> </script>
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.js' type='text/javascript' charset="utf-8"></script> <script src='{{ STATIC_URL }}horizon/lib/jquery/jquery.js' type='text/javascript' charset="utf-8"></script>
<script src='{{ STATIC_URL }}horizon/lib/jquery-ui/ui/jquery-ui.js' type='text/javascript' charset="utf-8"></script>
<script src='{{ STATIC_URL }}horizon/lib/jquery/jquery-migrate.js' type='text/javascript' charset="utf-8"></script> <script src='{{ STATIC_URL }}horizon/lib/jquery/jquery-migrate.js' type='text/javascript' charset="utf-8"></script>
<script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js"></script> <script src="{{ STATIC_URL }}horizon/lib/jquery/jquery.tablesorter.js"></script>
<script src="{{ STATIC_URL }}horizon/lib/angular/angular.js" type="text/javascript" charset="utf-8"></script> <script src="{{ STATIC_URL }}horizon/lib/angular/angular.js" type="text/javascript" charset="utf-8"></script>
@ -123,4 +124,4 @@
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View File

@ -338,10 +338,10 @@ class GetUserHomeTests(BaseHorizonTests):
conf.HORIZON_CONFIG._setup() conf.HORIZON_CONFIG._setup()
def test_using_callable(self): def test_using_callable(self):
def fancy_user_fnc(user): def themable_user_fnc(user):
return user.username.upper() return user.username.upper()
settings.HORIZON_CONFIG['user_home'] = fancy_user_fnc settings.HORIZON_CONFIG['user_home'] = themable_user_fnc
conf.HORIZON_CONFIG._setup() conf.HORIZON_CONFIG._setup()
self.assertEqual(self.test_user.username.upper(), self.assertEqual(self.test_user.username.upper(),

View File

@ -37,7 +37,7 @@ class LazyLoadedTabsTests(test.SeleniumTestCase):
table_selector = 'div.tab-content > div#{0} > div.table_wrapper'.format( table_selector = 'div.tab-content > div#{0} > div.table_wrapper'.format(
tab_id) tab_id)
button_selector = 'button#lazy_puppies__action_delete' button_selector = 'button#lazy_puppies__action_delete'
checkbox_selector = 'td.multi_select_column > input[type=checkbox]' checkbox_selector = 'td.multi_select_column input[type=checkbox]'
select_all_selector = 'th.multi_select_column input[type=checkbox]' select_all_selector = 'th.multi_select_column input[type=checkbox]'
def setUp(self): def setUp(self):

View File

@ -527,6 +527,10 @@
<input type="checkbox"> Checkbox <input type="checkbox"> Checkbox
</label> </label>
</div> </div>
<div class="themable-checkbox">
<input type="checkbox" id="themable-checkbox">
<label for="themable-checkbox" translate>Checkbox</label>
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -338,7 +338,7 @@ class AddRouterToFirewall(RouterInsertionFormBase):
router_ids = forms.MultipleChoiceField( router_ids = forms.MultipleChoiceField(
label=_("Add Routers"), label=_("Add Routers"),
required=False, required=False,
widget=forms.CheckboxSelectMultiple(), widget=forms.ThemableCheckboxSelectMultiple(),
help_text=_("Add selected router(s) to the firewall.")) help_text=_("Add selected router(s) to the firewall."))
failure_url = 'horizon:project:firewalls:index' failure_url = 'horizon:project:firewalls:index'
@ -363,7 +363,7 @@ class RemoveRouterFromFirewall(RouterInsertionFormBase):
router_ids = forms.MultipleChoiceField( router_ids = forms.MultipleChoiceField(
label=_("Associated Routers"), label=_("Associated Routers"),
required=False, required=False,
widget=forms.CheckboxSelectMultiple(), widget=forms.ThemableCheckboxSelectMultiple(),
help_text=_("Unselect the router(s) to be removed from firewall.")) help_text=_("Unselect the router(s) to be removed from firewall."))
failure_url = 'horizon:project:firewalls:index' failure_url = 'horizon:project:firewalls:index'

View File

@ -164,7 +164,7 @@ class SelectRulesAction(workflows.Action):
rule = forms.MultipleChoiceField( rule = forms.MultipleChoiceField(
label=_("Rules"), label=_("Rules"),
required=False, required=False,
widget=forms.CheckboxSelectMultiple(), widget=forms.ThemableCheckboxSelectMultiple(),
help_text=_("Create a policy with selected rules.")) help_text=_("Create a policy with selected rules."))
class Meta(object): class Meta(object):
@ -206,7 +206,7 @@ class SelectRoutersAction(workflows.Action):
router = forms.MultipleChoiceField( router = forms.MultipleChoiceField(
label=_("Routers"), label=_("Routers"),
required=False, required=False,
widget=forms.CheckboxSelectMultiple(), widget=forms.ThemableCheckboxSelectMultiple(),
help_text=_("Create a firewall with selected routers.")) help_text=_("Create a firewall with selected routers."))
class Meta(object): class Meta(object):

View File

@ -1617,11 +1617,11 @@ class InstanceTests(helpers.TestCase):
else: else:
self.assertNotContains(res, boot_from_image_field_label) self.assertNotContains(res, boot_from_image_field_label)
checked_label = '<label for="id_network_0"><input checked="checked"' checked_box = '<input checked="checked" id="id_network_0"'
if only_one_network: if only_one_network:
self.assertContains(res, checked_label) self.assertContains(res, checked_box)
else: else:
self.assertNotContains(res, checked_label) self.assertNotContains(res, checked_box)
disk_config_field_label = 'Disk Partition' disk_config_field_label = 'Disk Partition'
if disk_config: if disk_config:

View File

@ -551,12 +551,13 @@ class SetAccessControlsAction(workflows.Action):
label=_("Confirm Admin Password"), label=_("Confirm Admin Password"),
required=False, required=False,
widget=forms.PasswordInput(render_value=False)) widget=forms.PasswordInput(render_value=False))
groups = forms.MultipleChoiceField(label=_("Security Groups"), groups = forms.MultipleChoiceField(
required=False, label=_("Security Groups"),
initial=["default"], required=False,
widget=forms.CheckboxSelectMultiple(), initial=["default"],
help_text=_("Launch instance in these " widget=forms.ThemableCheckboxSelectMultiple(),
"security groups.")) help_text=_("Launch instance in these "
"security groups."))
class Meta(object): class Meta(object):
name = _("Access & Security") name = _("Access & Security")
@ -701,14 +702,15 @@ class PostCreationStep(workflows.Step):
class SetNetworkAction(workflows.Action): class SetNetworkAction(workflows.Action):
network = forms.MultipleChoiceField(label=_("Networks"), network = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(), label=_("Networks"),
error_messages={ widget=forms.ThemableCheckboxSelectMultiple(),
'required': _( error_messages={
"At least one network must" 'required': _(
" be specified.")}, "At least one network must"
help_text=_("Launch instance with" " be specified.")},
" these networks")) help_text=_("Launch instance with"
" these networks"))
if api.neutron.is_port_profiles_supported(): if api.neutron.is_port_profiles_supported():
widget = None widget = None
else: else:

View File

@ -0,0 +1,38 @@
@import '/horizon/lib/font-awesome/scss/variables';
@import '/horizon/lib/font-awesome/scss/mixins';
//
// Checkboxes
// This will ONLY work when the label's 'for' attribute
// shares the input[type=checkbox]'s 'id' value
// --------------------------------------------------
.themable-checkbox {
// Hide the real checkbox
input[type=checkbox] {
display:none;
// The checkbox - Unchecked
& + label {
margin-bottom: 0; // remove the Bootstrap margin
&:before {
@include fa-icon();
content: $fa-var-square-o;
width: 1em;
vertical-align: middle;
}
& > span {
padding-left: $padding-small-vertical;
vertical-align: middle;
}
}
// The checkbox - Checked
&:checked + label:before {
content: $fa-var-check-square-o;
}
}
}

View File

@ -17,6 +17,7 @@
// Dashboard Components // Dashboard Components
@import "components/bar_charts"; @import "components/bar_charts";
@import "components/charts"; @import "components/charts";
@import "components/checkboxes";
@import "components/datepicker"; @import "components/datepicker";
@import "components/forms"; @import "components/forms";
@import "components/help_panel"; @import "components/help_panel";

View File

@ -9,6 +9,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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 re
import six import six
from selenium.webdriver.common import by from selenium.webdriver.common import by
@ -94,17 +96,31 @@ class CheckBoxMixin(object):
if self.is_marked(): if self.is_marked():
self.element.click() self.element.click()
@property
def name(self):
"""Themable checkboxes use a <label> with font-awesome icon as a
control element while the <input> widget is hidden. Still the name
needs to be extracted from <label>. Attribute "for" is used for that,
since it mirrors <input> "id" which in turn is derived from <input>'s
name.
"""
for_attribute = self.element.get_attribute('for')
indirect_name = re.search(r'id_(.*)', for_attribute)
return (indirect_name and indirect_name.group(1)) or None
class CheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin):
class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
"""Checkbox field.""" """Checkbox field."""
_element_locator_str_suffix = 'label > input[type=checkbox]' _element_locator_str_suffix = \
'.themable-checkbox input[type=checkbox] + label'
class ProjectPageCheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin): class ProjectPageCheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
"""Checkbox field for Project-page.""" """Checkbox field for Project-page."""
_element_locator_str_suffix = 'div > input[type=checkbox]' _element_locator_str_suffix = \
'div > .themable-checkbox input[type=checkbox] + label'
class ChooseFileFormFieldRegion(BaseFormFieldRegion): class ChooseFileFormFieldRegion(BaseFormFieldRegion):

View File

@ -24,7 +24,10 @@ class RowRegion(baseregion.BaseRegion):
"""Classic table row.""" """Classic table row."""
_cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS) _cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS)
_row_checkbox_locator = (by.By.CSS_SELECTOR, 'td > input') _row_checkbox_locator = (
by.By.CSS_SELECTOR,
'td .themable-checkbox [type="checkbox"] + label'
)
def __init__(self, driver, conf, src_elem, column_names): def __init__(self, driver, conf, src_elem, column_names):
self.column_names = column_names self.column_names = column_names

View File

@ -36,6 +36,10 @@ form label {
} }
} }
.help-icon {
color: $gray;
}
// Note (hurgleburgler) : the combination of display: table-cell, width: 100% // Note (hurgleburgler) : the combination of display: table-cell, width: 100%
// and max-width has strange consequences in CSS. This is required to make sure // and max-width has strange consequences in CSS. This is required to make sure
// that the width does not stretch beyond 100%. Please see the answer to // that the width does not stretch beyond 100%. Please see the answer to

View File

@ -1,6 +1,7 @@
@import "icons"; @import "icons";
@import "components/context_selection"; @import "components/context_selection";
@import "components/messages"; @import "components/messages";
@import "components/checkboxes";
@import "components/hamburger"; @import "components/hamburger";
@import "components/magic_search"; @import "components/magic_search";
@import "components/navbar"; @import "components/navbar";

View File

@ -0,0 +1,29 @@
//
// Custom Material Checkboxes
// --------------------------------------------------
@import "/horizon/lib/mdi/scss/icons";
.themable-checkbox {
input[type=checkbox] {
// The checkbox - Unchecked
& + label {
@extend .mdi-checkbox-blank-outline;
&:before {
@extend .mdi;
font-size: $font-size-h5;
}
}
// The checkbox - Checked
&:checked + label {
@extend .mdi-checkbox-marked;
&:before {
color: $brand-primary;
}
}
}
}