Merge "Horizon Forms should allow themable number spinners"
This commit is contained in:
commit
614bc7600a
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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' %}
|
||||
|
@ -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>
|
@ -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'):
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -0,0 +1,17 @@
|
||||
@-webkit-keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user