Merge "Horizon Forms should allow themable number spinners"

This commit is contained in:
Jenkins 2017-06-29 08:26:47 +00:00 committed by Gerrit Code Review
commit 614bc7600a
12 changed files with 282 additions and 4 deletions

View File

@ -249,6 +249,14 @@ horizon.forms.init_themable_select = function ($elem) {
// Pass in a container OR the themable select itself
$elem = $elem.hasClass('themable-select') ? $elem : $elem.find('.themable-select');
var initialized_class = 'select-initialized';
if ($elem.hasClass(initialized_class)) {
return;
}
$elem.addClass(initialized_class);
// Update the select value if dropdown value changes
$elem.on('click', 'li a', function () {
var $this = $(this);
@ -338,14 +346,117 @@ horizon.forms.init_themable_select = function ($elem) {
});
};
horizon.forms.init_new_selects = function () {
$(document).on('DOMNodeInserted', function(e) {
var $target = $(e.target);
var newInputs = $target.find('.themable-select').not('.select-initialized');
for (var ii = 0; ii < newInputs.length; ii++) {
horizon.forms.init_themable_select($(newInputs[ii]));
}
});
};
horizon.forms.getSpinnerValue = function(val, defaultVal) {
val = parseInt(val, 10);
return isNaN(val) ? defaultVal : val;
};
horizon.forms.checkSpinnerValue = function($input) {
var val = $input.attr('value');
var max = horizon.forms.getSpinnerValue($input.attr('max'), Number.MAX_SAFE_INTEGER);
var min = horizon.forms.getSpinnerValue($input.attr('min'), 0);
var $parent = $input.parents('.themable-spinner');
var $up = $parent.find('.spinner-up');
var $down = $parent.find('.spinner-down');
$parent.find('.themable-spinner-btn').removeAttr('disabled');
if (val <= min) {
// Disable if we've hit the min
$down.attr('disabled', true);
} else if (val >= max) {
// Disable if we've hit the max
$up.attr('disabled', true);
}
};
horizon.forms.init_themable_spinner = function ($elem) {
"use strict";
// If not specified, find them all
$elem = $elem || $('body');
// If a jQuery object isn't passed in ... make it one
$elem = $elem instanceof jQuery ? $elem : $($elem);
// Pass in a container OR the themable spinner itself
$elem = $elem.hasClass('themable-spinner') ? $elem : $elem.find('.themable-spinner');
// Remove elements already initialized
var initialized_class = 'spinner-initialized';
$elem = $elem.not('.' + initialized_class);
var $input = $elem.find('input[type="number"]');
horizon.forms.checkSpinnerValue($input);
$elem.addClass(initialized_class)
.on('click', '.btn', function() {
var $this = $(this);
var $input = $this.parents('.themable-spinner').find('input');
var max = horizon.forms.getSpinnerValue($input.attr('max'), Number.MAX_SAFE_INTEGER);
var min = horizon.forms.getSpinnerValue($input.attr('min'), 0);
var step = horizon.forms.getSpinnerValue($input.attr('step'), 1);
var originalVal = $input.val();
var val = parseInt(originalVal === '' ? min || 0 : $input.val(), 10);
var new_val = val - step;
if ($this.hasClass('spinner-up')) {
new_val = originalVal ? val + step : min;
// Increase the step if we can
if (max == undefined || new_val <= max) {
$input.val(new_val).trigger('input').trigger('change');
}
} else {
new_val = originalVal ? val - step : min;
// Decrease the step if we can
if (min == undefined || new_val >= min) {
$input.val(new_val).trigger('input').trigger('change');
}
}
});
$input.on('change', function(e) {
horizon.forms.checkSpinnerValue($(e.delegateTarget));
});
};
horizon.forms.init_new_spinners = function () {
$(document).on('DOMNodeInserted', function(e) {
var $target = $(e.target);
var newInputs = $target.find('.themable-spinner').not('.spinner-initialized');
for (var ii = 0; ii < newInputs.length; ii++) {
horizon.forms.init_themable_spinner($(newInputs[ii]));
}
});
};
horizon.addInitFunction(horizon.forms.init = function () {
var $body = $('body');
horizon.forms.handle_submit($body);
horizon.modals.addModalInitFunction(horizon.forms.handle_submit);
horizon.forms.init_themable_select();
horizon.forms.init_new_selects();
horizon.modals.addModalInitFunction(horizon.forms.init_themable_select);
horizon.forms.init_themable_spinner();
horizon.forms.init_new_spinners();
horizon.forms.handle_snapshot_source();
horizon.forms.handle_volume_source();
horizon.forms.handle_image_source();

View File

@ -26,7 +26,11 @@
</span>
</div>
{% else %}
{{ field|add_bootstrap_class }}
{% if field|is_number %}
{% include 'horizon/common/fields/_themable_spinner.html' %}
{% else %}
{{ field|add_bootstrap_class }}
{% endif %}
{% endif %}
{% endwith %}
</div>

View File

@ -10,6 +10,8 @@
{% with is_horizontal=1 %}
{% include 'horizon/common/fields/_themable_checkbox.html' %}
{% endwith %}
{% elif field|is_number %}
{% include 'horizon/common/fields/_themable_spinner.html' %}
{% elif field|is_radio %}
{% with is_horizontal=1 %}
{% include 'horizon/common/fields/_themable_radiobutton.html' %}

View File

@ -0,0 +1,25 @@
{% load form_helpers %}
<div class="input-group themable-spinner">
{{ field|add_bootstrap_class|autocomplete:'off' }}
<div class="input-group-btn-vertical">
<div class="input-group-btn-vertical-container">
<div class="btn-container themable-spinner-inc">
<button
class="btn themable-spinner-btn btn-default btn-xs spinner-up"
type="button"
{% if field.field.max_value and field.field.max_value <= field.field.initial %}disabled{% endif %}>
<span class="fa fa-caret-up hz-spinner-icon-up"></span>
</button>
</div>
<div class="btn-container themable-spinner-dec">
<button
class="btn themable-spinner-btn btn-default btn-xs spinner-down"
type="button"
{% if field.field.min_value and field.field.min_value >= field.field.initial %}disabled{% endif %}>
<span class="fa fa-caret-down hz-spinner-icon-down"></span>
</button>
</div>
</div>
</div>
</div>

View File

@ -36,6 +36,12 @@ def add_bootstrap_class(field):
return field
@register.filter
def autocomplete(field, value='on'):
field.field.widget.attrs['autocomplete'] = value
return field
@register.filter
def is_checkbox(field):
return isinstance(field.field.widget, django.forms.CheckboxInput)
@ -56,6 +62,11 @@ def is_file(field):
return isinstance(field.field.widget, django.forms.FileInput)
@register.filter
def is_number(field):
return isinstance(field.field.widget, django.forms.NumberInput)
@register.filter
def add_item_url(field):
if hasattr(field.field.widget, 'get_add_item_url'):

View File

@ -299,11 +299,11 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
self.assertTemplateUsed(res, views.WorkflowView.template_name)
if django.VERSION >= (1, 10):
pattern = ('<input class="form-control" '
pattern = ('<input autocomplete="off" class="form-control" '
'id="id_subnet" min="-1" '
'name="subnet" type="number" value="10" required/>')
else:
pattern = ('<input class="form-control" '
pattern = ('<input autocomplete="off" class="form-control" '
'id="id_subnet" min="-1" '
'name="subnet" type="number" value="10"/>')
self.assertContains(res, pattern, html=True)

View File

@ -589,7 +589,10 @@ class StackTests(test.TestCase):
'name="__param_param{0}" type="{1}"/>')
self.assertContains(res, input_str.format(1, 'text'), html=True)
self.assertContains(res, input_str.format(2, 'number'), html=True)
# the custom number spinner produces an input element
# that doesn't match the input_strs above
# validate with id alone
self.assertContains(res, 'id="id___param_param2"')
self.assertContains(res, input_str.format(3, 'text'), html=True)
self.assertContains(res, input_str.format(4, 'text'), html=True)
self.assertContains(

View File

@ -0,0 +1,60 @@
.themable-spinner {
display: flex;
// Remove default spinner styles here
// **********************************
input {
-moz-appearance: textfield;
}
& input::-webkit-inner-spin-button,
& input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0; /* Removes leftover margin */
}
// **********************************
input[disabled],
input[readonly] {
& + .input-group-btn-vertical {
.btn {
@include opacity(.65);
@include box-shadow(none);
cursor: $cursor-disabled;
pointer-events: none;
}
}
}
.input-group-btn-vertical {
.btn {
line-height: 1;
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-top: 0;
padding-bottom: 0;
height: 100%;
&.spinner-up {
border-bottom-right-radius: 0;
}
&.spinner-down {
border-top-right-radius: 0;
}
&-container {
line-height: 1;
flex: 0 1 50%;
}
}
&-container {
display: flex;
flex-direction: column;
height: 100%;
}
}
}

View File

@ -44,6 +44,7 @@
@import "components/selection_menu";
@import "components/selects";
@import "components/sidebar";
@import "components/spinners";
@import "components/tab";
@import "components/tables";
@import "components/transfer_tables";

View File

@ -0,0 +1,17 @@
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@ -1,3 +1,4 @@
@import "animations";
@import "icons";
@import "components/checkboxes";
@import "components/context_selection";
@ -14,6 +15,7 @@
@import "components/radiobuttons";
@import "components/selects";
@import "components/sidebar";
@import "components/spinners";
@import "components/trees";
.login .splash-logo {

View File

@ -0,0 +1,42 @@
.themable-spinner {
.input-group-btn-vertical {
.btn {
&,
&:hover,
&:active,
&.active,
&:focus {
box-shadow: none;
background-color: transparent;
border: none;
display: block;
@include animation(fadeIn 1s);
&[disabled] {
display: none;
}
}
}
.hz-spinner-icon-up {
@extend .mdi-plus;
}
.hz-spinner-icon-down {
@extend .mdi-minus;
padding-right: $padding-xs-horizontal;
}
&-container {
flex-direction: row;
}
}
&-inc {
order: 1;
}
&-dec {
order: 0;
}
}