Horizon Dropdown now inherits from Bootstrap Theme

Horizon dropdowns now use proper Bootstrap markup to allow for theme
inheritance.  It was noticed during this effort that Horizon was
using a mixture of <a> and <button> elements within dropdowns, and
having to override CSS so that their styles were shared.  This
created unnecessary complexity in the CSS:

http://getbootstrap.com/components/#dropdowns

It was also noted that we were passing Bootstrap classes defined in
the Python code to the templates through a static string that was
concatenated within all the other needed attributes.  This makes
overriding the templates with custom classes difficult.  New logic was
added to allow classes to be separate.  This more granular approach
was added to the table drop down templates.

Partially-Implements: blueprint horizon-theme-css-reorg
Partially-Implements: blueprint bootstrap-html-standards
Partial-bug: #1490207

Change-Id: Id82999e5db37035968d39361ba9be4ff87c26f66
This commit is contained in:
Diana Whitten 2015-10-08 12:22:08 -07:00
parent eae45b143c
commit 053b5f30d7
10 changed files with 182 additions and 153 deletions

View File

@ -37,7 +37,7 @@ from horizon.utils import html
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# For Bootstrap integration; can be overridden in settings. # For Bootstrap integration; can be overridden in settings.
ACTION_CSS_CLASSES = ("btn", "btn-default") ACTION_CSS_CLASSES = ()
STRING_SEPARATOR = "__" STRING_SEPARATOR = "__"

View File

@ -0,0 +1,42 @@
{% load horizon %}
{% minifyspace %}
{% if action.method != "GET" %}
<button {{ action.attr_string_nc|safe }}
class="{% if is_single %}btn btn-default {% endif %}{% if is_small %}btn-sm {% endif %}{{ action.get_final_css|safe }}"
name="action"
{% if action.help_text %}
help_text="{{ action.help_text }}"
{% endif %}
type="submit"
{% if is_table_action %}
value="{{ action.get_param_name }}">
{% if action.icon != None %}
<span class="fa fa-{{ action.icon }}"></span>
{% endif %}
{% if action.handles_multiple %}
{{ action.verbose_name_plural }}
{% else %}
{{ action.verbose_name }}
{% endif %}
{% else %}
value="{{ action.table.name }}__{{ action.name }}__{{ row_id }}">
{{ action.verbose_name }}
{% endif %}
</button>
{% else %}
<a {{ action.attr_string_nc|safe }}
class="{% if is_single %}btn btn-default {% endif %}{% if is_small %}btn-sm {% endif %}{{ action.get_final_css|safe }}"
{% if is_table_action %}
href="{{ action.get_link_url }}"
title="{{ action.verbose_name }}">
{% if action.icon != None %}
<span class="fa fa-{{ action.icon }}"></span>
{% endif %}
{% else %}
href="{{ action.bound_url }}">
{% endif %}
{{ action.verbose_name }}
</a>
{% endif %}
{% endminifyspace %}

View File

@ -1,5 +0,0 @@
{% if action.method != "GET" %}
<button {{ action.attr_string|safe }} {% if action.help_text %}help_text="{{ action.help_text }}"{% endif %} name="action" value="{{ action.table.name }}__{{ action.name }}__{{ row_id }}" type="submit">{{ action.verbose_name }}</button>
{% else %}
<a href='{{ action.bound_url }}' {{ action.attr_string|safe }}>{{ action.verbose_name }}</a>
{% endif %}

View File

@ -1,27 +1,28 @@
{% load horizon i18n %} {% load horizon i18n %}
{% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #} {% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #}
{% if row_actions|length > 1 %}
<div class="btn-group {% if pull_right %}pull-right{% endif %}"> {% if row_actions|length == 1 %}
{% for action in row_actions %} {% include "horizon/common/_data_table_action.html" with action=row_actions.0 is_single=1 %}
{% elif row_actions|length > 1 %}
<div class="btn-group {% if pull_right %}pull-right{% endif %}">
{% for action in row_actions %}
{% if forloop.first %} {% if forloop.first %}
{% include "horizon/common/_data_table_row_action_dropdown.html" %} {% include "horizon/common/_data_table_action.html" with is_small=1 is_single=1 %}
<a class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" href="#"> <a class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" href="#">
<span class="fa fa-caret-down"></span> <span class="fa fa-caret-down"></span>
</a> </a>
<ul class="dropdown-menu row_actions dropdown-menu-right"> <ul class="dropdown-menu dropdown-menu-right row_actions">
{% else %} {% else %}
<li> <li>
{% include "horizon/common/_data_table_row_action_dropdown.html" %} {% include "horizon/common/_data_table_action.html" %}
</li> </li>
{% endif %} {% endif %}
{% if forloop.last %} {% if forloop.last %}
</ul> </ul>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if row_actions|length == 1 %}
{% include "horizon/common/_data_table_row_action_dropdown.html" with action=row_actions.0 %}
{% endif %}
{% endspaceless %} {% endspaceless %}

View File

@ -1,11 +0,0 @@
{% if action.method != "GET" %}
<button {{ action.attr_string|safe }} name="action" value="{{ action.get_param_name }}" {% if action.help_text %}help_text="{{ action.help_text }}"{% endif %} type="submit">
{% if action.icon != None %}<span class="fa fa-{{ action.icon }}"></span> {% endif %}
{% if action.handles_multiple %}{{ action.verbose_name_plural }}{% else %}{{ action.verbose_name }}{% endif %}
</button>
{% else %}
<a href='{{ action.get_link_url }}' title='{{ action.verbose_name }}' {{ action.attr_string|safe }}>
{% if action.icon != None %}<span class="fa fa-{{ action.icon }}"></span> {% endif %}
{{ action.verbose_name }}
</a>
{% endif %}

View File

@ -30,14 +30,23 @@
{% endfor %} {% endfor %}
</select> </select>
<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 }}" />
<button type="submit" {{ filter.attr_string|safe }}>{% trans "Filter" %}</button> <button type="submit" class="btn btn-default {{ filter.get_final_css|safe }}" {{ filter.attr_string_nc|safe }}>{% trans "Filter" %}</button>
</div> </div>
{% endif %} {% endif %}
{% endblock table_filter %} {% endblock table_filter %}
{% block table_actions %} {% block table_actions %}
{% comment %}
For each single action in the Table Actions area
{% endcomment %}
{% for action in table_actions_buttons %} {% for action in table_actions_buttons %}
{% include "horizon/common/_data_table_table_action.html" %} {% include "horizon/common/_data_table_action.html" with is_table_action=1 is_single=1 %}
{% endfor %} {% endfor %}
{% comment %}
If additional actions are defined, scoop them into the actions dropdown menu
{% endcomment %}
{% if table_actions_menu|length > 0 %} {% if table_actions_menu|length > 0 %}
<div class="btn-group table_actions_menu"> <div class="btn-group table_actions_menu">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">
@ -51,11 +60,12 @@
<ul class="dropdown-menu dropdown-menu-right"> <ul class="dropdown-menu dropdown-menu-right">
{% for action in table_actions_menu %} {% for action in table_actions_menu %}
<li> <li>
{% include "horizon/common/_data_table_table_action.html" %} {% include "horizon/common/_data_table_action.html" with is_table_action=1 %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% endblock table_actions %} {% endblock table_actions %}
</div> </div>

View File

@ -19,6 +19,7 @@ from horizon.contrib import bootstrap_datepicker
from django.conf import settings from django.conf import settings
from django import template from django import template
from django.template import Node
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -26,10 +27,19 @@ from django.utils.translation import ugettext_lazy as _
from horizon.base import Horizon # noqa from horizon.base import Horizon # noqa
from horizon import conf from horizon import conf
register = template.Library() register = template.Library()
class MinifiedNode(Node):
def __init__(self, nodelist):
self.nodelist = nodelist
def render(self, context):
return ' '.join(
force_text(self.nodelist.render(context).strip()).split()
)
@register.filter @register.filter
def has_permissions(user, component): def has_permissions(user, component):
"""Checks if the given user meets the permissions requirements for """Checks if the given user meets the permissions requirements for
@ -198,3 +208,28 @@ def datepicker_locale():
locale_mapping = getattr(settings, 'DATEPICKER_LOCALES', locale_mapping = getattr(settings, 'DATEPICKER_LOCALES',
bootstrap_datepicker.LOCALE_MAPPING) bootstrap_datepicker.LOCALE_MAPPING)
return locale_mapping.get(translation.get_language(), 'en') return locale_mapping.get(translation.get_language(), 'en')
@register.tag
def minifyspace(parser, token):
"""Removes whitespace including tab and newline characters. Do not use this
if you are using a <pre> tag
Example usage::
{% minifyspace %}
<p>
<a title="foo"
href="foo/">
Foo
</a>
</p>
{% endminifyspace %}
This example would return this HTML::
<p><a title="foo" href="foo/">Foo</a></p>
"""
nodelist = parser.parse(('endminifyspace',))
parser.delete_first_token()
return MinifiedNode(nodelist)

View File

@ -33,13 +33,17 @@ class HTMLElement(object):
""" """
return {} return {}
def get_final_attrs(self): def get_final_attrs(self, classes=True):
"""Returns a dict containing the final attributes of this element """Returns a dict containing the final attributes of this element
which will be rendered. which will be rendered.
""" """
final_attrs = copy.copy(self.get_default_attrs()) final_attrs = copy.copy(self.get_default_attrs())
final_attrs.update(self.attrs) final_attrs.update(self.attrs)
final_attrs['class'] = self.get_final_css() if classes:
final_attrs['class'] = self.get_final_css()
else:
final_attrs.pop('class', None)
return final_attrs return final_attrs
def get_final_css(self): def get_final_css(self):
@ -58,6 +62,13 @@ class HTMLElement(object):
""" """
return flatatt(self.get_final_attrs()) return flatatt(self.get_final_attrs())
@property
def attr_string_nc(self):
"""Returns a flattened string of HTML attributes based on the
``attrs`` dict provided to the class.
"""
return flatatt(self.get_final_attrs(False))
@property @property
def class_string(self): def class_string(self):
"""Returns a list of class name of HTML Element in string.""" """Returns a list of class name of HTML Element in string."""

View File

@ -1,3 +1,56 @@
/* Table Dropdowns */
/* Unfortunately, we want to style a button in a dropdown
the same way that we style an anchor. This isn't possible
in the current Bootstrap:
https://github.com/twbs/bootstrap/issues/10248
Until it is, wrap all buttons with anchors ...
and we have this workaround.
*/
/* Specificity required */
.table_actions_menu .dropdown-menu > li > button,
.actions_column .dropdown-menu > li > button {
border: none;
margin: 0; // prevent the form-inline styles from messing with margin
padding: 3px 20px; // Hardcoded in Bootstrap also, see _dropdowns.scss
color: $dropdown-link-color;
white-space: nowrap; // prevent links from breaking onto new lines
min-width: 100%;
text-align: left;
background: transparent;
display: block;
clear: both;
font-weight: normal;
line-height: $line-height-base;
&:hover,
&:focus {
text-decoration: none;
color: $dropdown-link-hover-color;
background-color: $dropdown-link-hover-bg;
}
&.disabled,
&[disabled] {
cursor: not-allowed;
pointer-events: none; // Future-proof disabling of clicks
@include opacity(.65);
@include box-shadow(none);
}
&.btn-primary {
color: $brand-primary;
}
&.btn-danger {
color: $brand-danger;
}
&.btn-warning {
color: $brand-warning;
}
&.btn-info {
color: $brand-info;
}
}
.table_actions { .table_actions {
float: right; float: right;
@extend .form-inline; @extend .form-inline;

View File

@ -26,6 +26,7 @@
@import "components/network_topology"; @import "components/network_topology";
@import "components/context_selection"; @import "components/context_selection";
@import "components/pie_charts"; @import "components/pie_charts";
@import "components/table_actions";
@import "/framework/framework"; @import "/framework/framework";
@import "components/tables"; @import "components/tables";
@import "components/table_actions"; @import "components/table_actions";
@ -411,14 +412,13 @@ a.current_item:hover h4 {
} }
} }
/* Actions dropdown */ /* Tables */
/* This works around a known bug in Bootstrap, the
.actions_column { wrapping of button groups within the cell of a table:
https://github.com/twbs/bootstrap/issues/3130
*/
td .btn-group {
white-space: nowrap; white-space: nowrap;
padding: 10px;
position: relative;
width: 1em;
background-clip: padding-box;
// We want the actions column to be a small button, but // We want the actions column to be a small button, but
// we can't get to the class attribute yet to customize // we can't get to the class attribute yet to customize
@ -426,118 +426,11 @@ a.current_item:hover h4 {
.btn { .btn {
@extend .btn-sm; @extend .btn-sm;
} }
}
form.actions_column { & > .btn-group,
width: auto; & > .btn {
font-family: $font-family-base; float: none;
} }
// TODO(hurgleburgler): We need to fix this, we still support IE8+
.actions_column .btn-group {
display: inline-flex;
display: -ms-inline-flexbox;
-ms-flex-direction: row;
display: -webkit-inline-flex;
display: -moz-inline-flex;
}
.actions_column .row_actions a,
.actions_column .row_actions input,
.actions_column .row_actions button,
div.table_actions_menu .dropdown-menu a,
div.table_actions_menu .dropdown-menu input,
div.table_actions_menu .dropdown-menu button {
background: none;
float: none;
display: block;
padding: 5px 10px;
color: $text-color;
text-align: left;
border-radius: 0;
border: 0 none;
@include box-shadow(none);
}
.actions_column .row_actions .hide {
display: none;
}
.actions_column .btn-action-required {
font-weight: bold;
}
.tab-content {
overflow: visible;
}
/* Makes size consistent across browsers when mixing "btn-group" and "small" */
.btn.hide, .btn-group .hide {
display: none;
}
.btn-group .dropdown-toggle:focus {
outline: none;
}
.dropdown-menu button {
line-height: 18px; /* Matches rule for ".dropdown-menu a" in bootstrap */
width: 100%;
}
.btn-group .dropdown-menu .btn {
border-radius: 0;
}
.dropdown-menu .btn.btn-danger,
.dropdown-menu .btn.btn-danger:hover,
.dropdown-menu .btn.btn-success,
.dropdown-menu .btn.btn-success:hover,
.dropdown-menu .btn.btn-info,
.dropdown-menu .btn.btn-info:hover {
text-shadow: none; /* remove default bootstrap shadowing from button text. */
}
.dropdown-menu li:hover {
background: none;
}
.actions_column .dropdown-menu a:hover,
.actions_column .dropdown-menu button:hover,
div.table_actions_menu .dropdown-menu a:hover,
div.table_actions_menu .dropdown-menu button:hover {
background-color: $gray-lighter;
}
.dropdown-menu .btn.btn-danger {
color: $brand-danger;
}
.dropdown-menu .btn.btn-danger:hover {
background-color: $gray-lighter;
}
/* Overrides for single-action rows (no dropdown) */
tr .actions_column ul.row_actions.single,
tr:hover .actions_column ul.row_actions.single,
.actions_column ul.row_actions.single,
.actions_column ul.row_actions.single:hover {
border: none;
}
.actions_column ul.row_actions.single li.action {
display: block;
}
.actions_column ul.row_actions.single li.action:hover {
background-color: transparent;
}
.actions_column ul.row_actions.single a,
.actions_column ul.row_actions.single input,
.actions_column ul.row_actions.single button {
color: $brand-info;
}
.actions_column ul.row_actions.single a:hover,
.actions_column ul.row_actions.single input:hover,
.actions_column ul.row_actions.single button:hover {
color: $text-color;
} }
div.input input[type="checkbox"] { div.input input[type="checkbox"] {