8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-25 22:03:10 +00:00

Support de typeahead pour les select multiples avec tokenfield

This commit is contained in:
Maël Kervella 2017-10-14 18:32:17 +00:00
parent db30643c13
commit 7d8d6d85fe
6 changed files with 1499 additions and 71 deletions

View file

@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% if serviceform %}
<h3>Service</h3>
{% bootstrap_form serviceform %}
{% massive_bootstrap_form serviceform 'servers' %}
{% endif %}
{% if vlanform %}
<h3>Vlan</h3>

View file

@ -22,6 +22,7 @@
from django import template
from django.utils.safestring import mark_safe
from django.forms import TextInput
from django.forms.widgets import Select
from bootstrap3.templatetags.bootstrap3 import bootstrap_form
from bootstrap3.utils import render_tag
from bootstrap3.forms import render_field
@ -165,12 +166,26 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
hidden_fields = [h.name for h in form.hidden_fields()]
html = ''
for f_name, f_value in form.fields.items() :
if not f_name in exclude :
if f_name in fields and not f_name in hidden_fields :
if not isinstance(f_value.widget, Select) :
raise ValueError(
('Field named {f_name} from {form} is not a Select and'
'can\'t be rendered with massive_bootstrap_form.'
).format(
f_name=f_name,
form=form
)
)
multiple = f_value.widget.allow_multiple_selected
f_bound = f_value.get_bound_field( form, f_name )
f_value.widget = TextInput(
attrs={
attrs = {
'name': 'mbf_'+f_name,
'placeholder': f_value.empty_label
}
@ -180,19 +195,35 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
*args,
**kwargs
)
html += render_tag(
'div',
content = hidden_tag( f_bound, f_name ) +
mbf_js(
if multiple :
content = mbf_js(
f_name,
f_value,
f_bound,
multiple,
choices,
engine,
match_func,
update_on
)
else :
content = hidden_tag( f_bound, f_name ) + mbf_js(
f_name,
f_value,
f_bound,
multiple,
choices,
engine,
match_func,
update_on
)
html += render_tag(
'div',
content = content,
attrs = { 'id': custom_div_id( f_bound ) }
)
else:
html += render_field(
f_value.get_bound_field( form, f_name ),
@ -208,7 +239,11 @@ def input_id( f_bound ) :
def hidden_id( f_bound ):
""" The id of the HTML hidden input element """
return input_id( f_bound ) +'_hidden'
return input_id( f_bound ) + '_hidden'
def custom_div_id( f_bound ):
""" The id of the HTML div element containing values and script """
return input_id( f_bound ) + '_div'
def hidden_tag( f_bound, f_name ):
""" The HTML hidden input element """
@ -222,21 +257,61 @@ def hidden_tag( f_bound, f_name ):
}
)
def mbf_js( f_name, f_value, f_bound,
def mbf_js( f_name, f_value, f_bound, multiple,
choices_, engine_, match_func_, update_on_ ) :
""" The whole script to use """
choices = mark_safe( choices_[f_name] ) if f_name in choices_.keys() \
else default_choices( f_value )
choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys()
else default_choices( f_value ) )
engine = mark_safe( engine_[f_name] ) if f_name in engine_.keys() \
else default_engine ( f_name )
engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys()
else default_engine ( f_name ) )
match_func = mark_safe( match_func_[f_name] ) \
if f_name in match_func_.keys() else default_match_func( f_name )
match_func = ( mark_safe( match_func_[f_name] )
if f_name in match_func_.keys() else default_match_func( f_name ) )
update_on = update_on_[f_name] if f_name in update_on_.keys() else []
if multiple :
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
'var setup_{f_name} = function() {{'
'engine_{f_name} = {engine};'
'$( "#{input_id}" ).tokenfield( "destroy" );'
'$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
'}};'
'$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );'
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );'
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
'setup_{f_name}();'
'{init_input}'
'}} );'
).format(
f_name = f_name,
choices = choices,
engine = engine,
input_id = input_id( f_bound ),
datasets = default_datasets( f_name, match_func ),
create = tokenfield_create( f_name, f_bound ),
edit = tokenfield_edit( f_bound ),
remove = tokenfield_remove( f_bound ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
'setup_{f_name}();'
'{reset_input}'
'}} );'
).format(
u_id = u_id,
reset_input = tokenfield_reset_input( f_bound ),
f_name = f_name
) for u_id in update_on ]
),
init_input = tokenfield_init_input( f_name, f_bound ),
)
else :
js_content = (
'var choices_{f_name} = {choices};'
'var engine_{f_name};'
@ -245,7 +320,7 @@ def mbf_js( f_name, f_value, f_bound,
'$( "#{input_id}" ).typeahead( "destroy" );'
'$( "#{input_id}" ).typeahead( {datasets} );'
'}};'
'$( "#{input_id}" ).bind( "typeahead:select", {updater} );'
'$( "#{input_id}" ).bind( "typeahead:select", {select} );'
'$( "#{input_id}" ).bind( "typeahead:change", {change} );'
'{updates}'
'$( "#{input_id}" ).ready( function() {{'
@ -258,7 +333,7 @@ def mbf_js( f_name, f_value, f_bound,
engine = engine,
input_id = input_id( f_bound ),
datasets = default_datasets( f_name, match_func ),
updater = typeahead_updater( f_bound ),
select = typeahead_select( f_bound ),
change = typeahead_change( f_bound ),
updates = ''.join( [ (
'$( "#{u_id}" ).change( function() {{'
@ -267,16 +342,16 @@ def mbf_js( f_name, f_value, f_bound,
'}} );'
).format(
u_id = u_id,
reset_input = reset_input( f_bound ),
reset_input = typeahead_reset_input( f_bound ),
f_name = f_name
) for u_id in update_on ]
),
init_input = init_input( f_name, f_bound ),
init_input = typeahead_init_input( f_name, f_bound ),
)
return render_tag( 'script', content=mark_safe( js_content ) )
def init_input( f_name, f_bound ) :
def typeahead_init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
@ -293,7 +368,7 @@ def init_input( f_name, f_bound ) :
hidden_id = hidden_id( f_bound )
)
def reset_input( f_bound ) :
def typeahead_reset_input( f_bound ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).typeahead("val", "");'
@ -303,6 +378,31 @@ def reset_input( f_bound ) :
hidden_id = hidden_id( f_bound )
)
def tokenfield_init_input( f_name, f_bound ) :
""" The JS script to init the fields values """
init_key = f_bound.value() or '""'
return (
'$( "#{input_id}" ).tokenfield("setTokens", {init_val});'
).format(
input_id = input_id( f_bound ),
init_val = '""' if init_key == '""' else (
'engine_{f_name}.get( {init_key} ).map('
'function(o) {{ return o.value; }}'
')').format(
f_name = f_name,
init_key = init_key
),
init_key = init_key,
)
def tokenfield_reset_input( f_bound ) :
""" The JS script to reset the fields values """
return (
'$( "#{input_id}" ).tokenfield("setTokens", "");'
).format(
input_id = input_id( f_bound ),
)
def default_choices( f_value ) :
""" The JS script creating the variable choices_<fieldname> """
return '[{objects}]'.format(
@ -362,7 +462,7 @@ def default_match_func ( f_name ) :
f_name = f_name
)
def typeahead_updater( f_bound ):
def typeahead_select( f_bound ):
""" The JS script creating the function triggered when an item is
selected through typeahead """
return (
@ -391,3 +491,52 @@ def typeahead_change( f_bound ):
hidden_id = hidden_id( f_bound )
)
def tokenfield_create( f_name, f_bound ):
""" The JS script triggered when a new token is created in tokenfield. """
return (
'function(evt) {{'
'var data = evt.attrs.value;'
'var i = 0;'
'while ( i<choices_{f_name}.length &&'
'choices_{f_name}[i].value !== data ) {{'
'i++;'
'}}'
'if ( i === choices_{f_name}.length ) {{ return false; }}'
'var new_input = document.createElement("input");'
'new_input.type = "hidden";'
'new_input.id = "{hidden_id}_"+data;'
'new_input.value = choices_{f_name}[i].key.toString();'
'new_input.name = "{name}";'
'$( "#{div_id}" ).append(new_input);'
'}}'
).format(
f_name = f_name,
hidden_id = hidden_id( f_bound ),
name = f_bound.html_name,
div_id = custom_div_id( f_bound )
)
def tokenfield_edit( f_bound ):
""" The JS script triggered when a token is edited in tokenfield. """
return (
'function(evt) {{'
'var data = evt.attrs.value;'
'var old_input = document.getElementById( "{hidden_id}_"+data );'
'old_input.parentNode.removeChild(old_input);'
'}}'
).format(
hidden_id = hidden_id( f_bound )
)
def tokenfield_remove( f_bound ):
""" The JS script trigggered when a token is removed from tokenfield. """
return (
'function(evt) {{'
'var data = evt.attrs.value;'
'var old_input = document.getElementById( "{hidden_id}_"+data );'
'old_input.parentNode.removeChild(old_input);'
'}}'
).format(
hidden_id = hidden_id( f_bound )
)

210
static/css/bootstrap-tokenfield.css vendored Normal file
View file

@ -0,0 +1,210 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/
@-webkit-keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@-moz-keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
@keyframes blink {
0% {
border-color: #ededed;
}
100% {
border-color: #b94a48;
}
}
.tokenfield {
height: auto;
min-height: 34px;
padding-bottom: 0px;
}
.tokenfield.focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.tokenfield .token {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
display: inline-block;
border: 1px solid #d9d9d9;
background-color: #ededed;
white-space: nowrap;
margin: -1px 5px 5px 0;
height: 22px;
vertical-align: top;
cursor: default;
}
.tokenfield .token:hover {
border-color: #b9b9b9;
}
.tokenfield .token.active {
border-color: #52a8ec;
border-color: rgba(82, 168, 236, 0.8);
}
.tokenfield .token.duplicate {
border-color: #ebccd1;
-webkit-animation-name: blink;
animation-name: blink;
-webkit-animation-duration: 0.1s;
animation-duration: 0.1s;
-webkit-animation-direction: normal;
animation-direction: normal;
-webkit-animation-timing-function: ease;
animation-timing-function: ease;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.tokenfield .token.invalid {
background: none;
border: 1px solid transparent;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
border-bottom: 1px dotted #d9534f;
}
.tokenfield .token.invalid.active {
background: #ededed;
border: 1px solid #ededed;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.tokenfield .token .token-label {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 4px;
vertical-align: top;
}
.tokenfield .token .close {
font-family: Arial;
display: inline-block;
line-height: 100%;
font-size: 1.1em;
line-height: 1.49em;
margin-left: 5px;
float: none;
height: 100%;
vertical-align: top;
padding-right: 4px;
}
.tokenfield .token-input {
background: none;
width: 60px;
min-width: 60px;
border: 0;
height: 20px;
padding: 0;
margin-bottom: 6px;
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield .token-input:focus {
border-color: transparent;
outline: 0;
/* IE6-9 */
-webkit-box-shadow: none;
box-shadow: none;
}
.tokenfield.disabled {
cursor: not-allowed;
background-color: #eeeeee;
}
.tokenfield.disabled .token-input {
cursor: not-allowed;
}
.tokenfield.disabled .token:hover {
cursor: not-allowed;
border-color: #d9d9d9;
}
.tokenfield.disabled .token:hover .close {
cursor: not-allowed;
opacity: 0.2;
filter: alpha(opacity=20);
}
.has-warning .tokenfield.focus {
border-color: #66512c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
}
.has-error .tokenfield.focus {
border-color: #843534;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
}
.has-success .tokenfield.focus {
border-color: #2b542c;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
}
.tokenfield.input-sm,
.input-group-sm .tokenfield {
min-height: 30px;
padding-bottom: 0px;
}
.input-group-sm .token,
.tokenfield.input-sm .token {
height: 20px;
margin-bottom: 4px;
}
.input-group-sm .token-input,
.tokenfield.input-sm .token-input {
height: 18px;
margin-bottom: 5px;
}
.tokenfield.input-lg,
.input-group-lg .tokenfield {
height: auto;
min-height: 45px;
padding-bottom: 4px;
}
.input-group-lg .token,
.tokenfield.input-lg .token {
height: 25px;
}
.input-group-lg .token-label,
.tokenfield.input-lg .token-label {
line-height: 23px;
}
.input-group-lg .token .close,
.tokenfield.input-lg .token .close {
line-height: 1.3em;
}
.input-group-lg .token-input,
.tokenfield.input-lg .token-input {
height: 23px;
line-height: 23px;
margin-bottom: 6px;
vertical-align: top;
}
.tokenfield.rtl {
direction: rtl;
text-align: right;
}
.tokenfield.rtl .token {
margin: -1px 0 5px 5px;
}
.tokenfield.rtl .token .token-label {
padding-left: 0px;
padding-right: 4px;
}

View file

@ -0,0 +1,23 @@
#### Sliptree
- by Illimar Tambek for [Sliptree](http://sliptree.com)
- Copyright (c) 2013 by Sliptree
Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load diff

View file

@ -33,12 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Load CSS and JavaScript #}
{% bootstrap_css %}
<link href="/static/css/typeaheadjs.css" rel="stylesheet">
<link href="/static/css/bootstrap-tokenfield.css" rel="stylesheet">
{% comment %}<link href="/static/css/jquery-ui.css" rel="stylesheet">{% endcomment %}
{% bootstrap_javascript %}
<script src="/static/js/typeahead/typeahead.js"></script>
<script src="/static/js/handlebars/handlebars.js"></script>
<script src="/static/js/konami/konami.js"></script>
<script src="/static/js/sapphire.js"> var s=Sapphire(); Konami(s.activate); </script>
<script src="/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js"></script>
{% comment %}<script src="/static/js/jquery-ui.js"></script>{% endcomment %}
<link rel="stylesheet" href="{% static "/css/base.css" %}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>