mirror of https://gitlab.federez.net/re2o/re2o
14 changed files with 1898 additions and 424 deletions
@ -1,386 +0,0 @@ |
|||||
# -*- mode: python; coding: utf-8 -*- |
|
||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il |
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en |
|
||||
# quelques clics. |
|
||||
# |
|
||||
# Copyright © 2017 Maël Kervella |
|
||||
# |
|
||||
# This program is free software; you can redistribute it and/or modify |
|
||||
# it under the terms of the GNU General Public License as published by |
|
||||
# the Free Software Foundation; either version 2 of the License, or |
|
||||
# (at your option) any later version. |
|
||||
# |
|
||||
# This program is distributed in the hope that it will be useful, |
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
# GNU General Public License for more details. |
|
||||
# |
|
||||
# You should have received a copy of the GNU General Public License along |
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., |
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
|
||||
|
|
||||
from django import template |
|
||||
from django.utils.safestring import mark_safe |
|
||||
from django.forms import TextInput |
|
||||
from bootstrap3.templatetags.bootstrap3 import bootstrap_form |
|
||||
from bootstrap3.utils import render_tag |
|
||||
from bootstrap3.forms import render_field |
|
||||
|
|
||||
register = template.Library() |
|
||||
|
|
||||
@register.simple_tag |
|
||||
def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): |
|
||||
""" |
|
||||
Render a form where some specific fields are rendered using Typeahead. |
|
||||
Using Typeahead really improves the performance, the speed and UX when |
|
||||
dealing with very large datasets (select with 50k+ elts for instance). |
|
||||
For convenience, it accepts the same parameters as a standard bootstrap |
|
||||
can accept. |
|
||||
|
|
||||
**Tag name**:: |
|
||||
|
|
||||
bootstrap_form_typeahead |
|
||||
|
|
||||
**Parameters**: |
|
||||
|
|
||||
form |
|
||||
The form that is to be rendered |
|
||||
|
|
||||
typeahead_fields |
|
||||
A list of field names (comma separated) that should be rendered |
|
||||
with typeahead instead of the default bootstrap renderer. |
|
||||
|
|
||||
bft_param |
|
||||
A dict of parameters for the bootstrap_form_typeahead tag. The |
|
||||
possible parameters are the following. |
|
||||
|
|
||||
choices |
|
||||
A dict of strings representing the choices in JS. The keys of |
|
||||
the dict are the names of the concerned fields. The choices |
|
||||
must be an array of objects. Each of those objects must at |
|
||||
least have the fields 'key' (value to send) and 'value' (value |
|
||||
to display). Other fields can be added as desired. |
|
||||
For a more complex structure you should also consider |
|
||||
reimplementing the engine and the match_func. |
|
||||
If not specified, the key is the id of the object and the value |
|
||||
is its string representation as in a normal bootstrap form. |
|
||||
Example : |
|
||||
'choices' : { |
|
||||
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', |
|
||||
'field_B':..., |
|
||||
... |
|
||||
} |
|
||||
|
|
||||
engine |
|
||||
A dict of strings representating the engine used for matching |
|
||||
queries and possible values with typeahead. The keys of the |
|
||||
dict are the names of the concerned fields. The string is valid |
|
||||
JS code. |
|
||||
If not specified, BloodHound with relevant basic properties is |
|
||||
used. |
|
||||
Example : |
|
||||
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} |
|
||||
|
|
||||
match_func |
|
||||
A dict of strings representing a valid JS function used in the |
|
||||
dataset to overload the matching engine. The keys of the dict |
|
||||
are the names of the concerned fields. This function is used |
|
||||
the source of the dataset. This function receives 2 parameters, |
|
||||
the query and the synchronize function as specified in |
|
||||
typeahead.js documentation. If needed, the local variables |
|
||||
'choices_<fieldname>' and 'engine_<fieldname>' contains |
|
||||
respectively the array of all possible values and the engine |
|
||||
to match queries with possible values. |
|
||||
If not specified, the function used display up to the 10 first |
|
||||
elements if the query is empty and else the matching results. |
|
||||
Example : |
|
||||
'match_func' : { |
|
||||
'field_A': 'function(q, sync) { engine.search(q, sync); }', |
|
||||
'field_B': ..., |
|
||||
... |
|
||||
} |
|
||||
|
|
||||
update_on |
|
||||
A dict of list of ids that the values depends on. The engine |
|
||||
and the typeahead properties are recalculated and reapplied. |
|
||||
Example : |
|
||||
'addition' : { |
|
||||
'field_A' : [ 'id0', 'id1', ... ] , |
|
||||
'field_B' : ... , |
|
||||
... |
|
||||
} |
|
||||
|
|
||||
See boostrap_form_ for other arguments |
|
||||
|
|
||||
**Usage**:: |
|
||||
|
|
||||
{% bootstrap_form_typeahead |
|
||||
form |
|
||||
[ '<field1>[,<field2>[,...]]' ] |
|
||||
[ { |
|
||||
[ 'choices': { |
|
||||
[ '<field1>': '<choices1>' |
|
||||
[, '<field2>': '<choices2>' |
|
||||
[, ... ] ] ] |
|
||||
} ] |
|
||||
[, 'engine': { |
|
||||
[ '<field1>': '<engine1>' |
|
||||
[, '<field2>': '<engine2>' |
|
||||
[, ... ] ] ] |
|
||||
} ] |
|
||||
[, 'match_func': { |
|
||||
[ '<field1>': '<match_func1>' |
|
||||
[, '<field2>': '<match_func2>' |
|
||||
[, ... ] ] ] |
|
||||
} ] |
|
||||
[, 'update_on': { |
|
||||
[ '<field1>': '<update_on1>' |
|
||||
[, '<field2>': '<update_on2>' |
|
||||
[, ... ] ] ] |
|
||||
} ] |
|
||||
} ] |
|
||||
[ <standard boostrap_form parameters> ] |
|
||||
%} |
|
||||
|
|
||||
**Example**: |
|
||||
|
|
||||
{% bootstrap_form_typeahead form 'ipv4' choices='[...]' %} |
|
||||
""" |
|
||||
|
|
||||
t_fields = typeahead_fields.split(',') |
|
||||
params = kwargs.get('bft_param', {}) |
|
||||
exclude = params.get('exclude', None) |
|
||||
exclude = exclude.split(',') if exclude else [] |
|
||||
t_choices = params.get('choices', {}) |
|
||||
t_engine = params.get('engine', {}) |
|
||||
t_match_func = params.get('match_func', {}) |
|
||||
t_update_on = params.get('update_on', {}) |
|
||||
hidden = [h.name for h in django_form.hidden_fields()] |
|
||||
|
|
||||
form = '' |
|
||||
for f_name, f_value in django_form.fields.items() : |
|
||||
if not f_name in exclude : |
|
||||
if f_name in t_fields and not f_name in hidden : |
|
||||
f_bound = f_value.get_bound_field( django_form, f_name ) |
|
||||
f_value.widget = TextInput( |
|
||||
attrs={ |
|
||||
'name': 'typeahead_'+f_name, |
|
||||
'placeholder': f_value.empty_label |
|
||||
} |
|
||||
) |
|
||||
form += render_field( |
|
||||
f_value.get_bound_field( django_form, f_name ), |
|
||||
*args, |
|
||||
**kwargs |
|
||||
) |
|
||||
form += render_tag( |
|
||||
'div', |
|
||||
content = hidden_tag( f_bound, f_name ) + |
|
||||
typeahead_js( |
|
||||
f_name, |
|
||||
f_value, |
|
||||
f_bound, |
|
||||
t_choices, |
|
||||
t_engine, |
|
||||
t_match_func, |
|
||||
t_update_on |
|
||||
) |
|
||||
) |
|
||||
else: |
|
||||
form += render_field( |
|
||||
f_value.get_bound_field(django_form, f_name), |
|
||||
*args, |
|
||||
**kwargs |
|
||||
) |
|
||||
|
|
||||
return mark_safe( form ) |
|
||||
|
|
||||
def input_id( f_bound ) : |
|
||||
""" The id of the HTML input element """ |
|
||||
return f_bound.auto_id |
|
||||
|
|
||||
def hidden_id( f_bound ): |
|
||||
""" The id of the HTML hidden input element """ |
|
||||
return input_id( f_bound ) +'_hidden' |
|
||||
|
|
||||
def hidden_tag( f_bound, f_name ): |
|
||||
""" The HTML hidden input element """ |
|
||||
return render_tag( |
|
||||
'input', |
|
||||
attrs={ |
|
||||
'id': hidden_id( f_bound ), |
|
||||
'name': f_bound.html_name, |
|
||||
'type': 'hidden', |
|
||||
'value': f_bound.value() or "" |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
def typeahead_js( f_name, f_value, f_bound, |
|
||||
t_choices, t_engine, t_match_func, t_update_on ) : |
|
||||
""" The whole script to use """ |
|
||||
|
|
||||
choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \ |
|
||||
else default_choices( f_value ) |
|
||||
|
|
||||
engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \ |
|
||||
else default_engine ( f_name ) |
|
||||
|
|
||||
match_func = mark_safe(t_match_func[f_name]) \ |
|
||||
if f_name in t_match_func.keys() else default_match_func( f_name ) |
|
||||
|
|
||||
update_on = t_update_on[f_name] if f_name in t_update_on.keys() else [] |
|
||||
|
|
||||
js_content = ( |
|
||||
'var choices_{f_name} = {choices};' |
|
||||
'var engine_{f_name};' |
|
||||
'var setup_{f_name} = function() {{' |
|
||||
'engine_{f_name} = {engine};' |
|
||||
'$( "#{input_id}" ).typeahead( "destroy" );' |
|
||||
'$( "#{input_id}" ).typeahead( {datasets} );' |
|
||||
'}};' |
|
||||
'$( "#{input_id}" ).bind( "typeahead:select", {updater} );' |
|
||||
'$( "#{input_id}" ).bind( "typeahead:change", {change} );' |
|
||||
'{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 ), |
|
||||
updater = typeahead_updater( f_bound ), |
|
||||
change = typeahead_change( f_bound ), |
|
||||
updates = ''.join( [ ( |
|
||||
'$( "#{u_id}" ).change( function() {{' |
|
||||
'setup_{f_name}();' |
|
||||
'{reset_input}' |
|
||||
'}} );' |
|
||||
).format( |
|
||||
u_id = u_id, |
|
||||
reset_input = reset_input( f_bound ), |
|
||||
f_name = f_name |
|
||||
) for u_id in update_on ] |
|
||||
), |
|
||||
init_input = init_input( f_name, f_bound ), |
|
||||
) |
|
||||
|
|
||||
return render_tag( 'script', content=mark_safe( js_content ) ) |
|
||||
|
|
||||
def init_input( f_name, f_bound ) : |
|
||||
""" The JS script to init the fields values """ |
|
||||
init_key = f_bound.value() or '""' |
|
||||
return ( |
|
||||
'$( "#{input_id}" ).typeahead("val", {init_val});' |
|
||||
'$( "#{hidden_id}" ).val( {init_key} );' |
|
||||
).format( |
|
||||
input_id = input_id( f_bound ), |
|
||||
init_val = '""' if init_key == '""' else |
|
||||
'engine_{f_name}.get( {init_key} )[0].value'.format( |
|
||||
f_name = f_name, |
|
||||
init_key = init_key |
|
||||
), |
|
||||
init_key = init_key, |
|
||||
hidden_id = hidden_id( f_bound ) |
|
||||
) |
|
||||
|
|
||||
def reset_input( f_bound ) : |
|
||||
""" The JS script to reset the fields values """ |
|
||||
return ( |
|
||||
'$( "#{input_id}" ).typeahead("val", "");' |
|
||||
'$( "#{hidden_id}" ).val( "" );' |
|
||||
).format( |
|
||||
input_id = input_id( f_bound ), |
|
||||
hidden_id = hidden_id( f_bound ) |
|
||||
) |
|
||||
|
|
||||
def default_choices( f_value ) : |
|
||||
""" The JS script creating the variable choices_<fieldname> """ |
|
||||
return '[{objects}]'.format( |
|
||||
objects = ','.join( |
|
||||
[ '{{key:{k},value:"{v}"}}'.format( |
|
||||
k = choice[0] if choice[0] != '' else '""', |
|
||||
v = choice[1] |
|
||||
) for choice in f_value.choices ] |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
def default_engine ( f_name ) : |
|
||||
""" The JS script creating the variable engine_<field_name> """ |
|
||||
return ( |
|
||||
'new Bloodhound({{' |
|
||||
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' |
|
||||
'queryTokenizer: Bloodhound.tokenizers.whitespace,' |
|
||||
'local: choices_{f_name},' |
|
||||
'identify: function(obj) {{ return obj.key; }}' |
|
||||
'}})' |
|
||||
).format( |
|
||||
f_name = f_name |
|
||||
) |
|
||||
|
|
||||
def default_datasets( f_name, match_func ) : |
|
||||
""" The JS script creating the datasets to use with typeahead """ |
|
||||
return ( |
|
||||
'{{' |
|
||||
'hint: true,' |
|
||||
'highlight: true,' |
|
||||
'minLength: 0' |
|
||||
'}},' |
|
||||
'{{' |
|
||||
'display: "value",' |
|
||||
'name: "{f_name}",' |
|
||||
'source: {match_func}' |
|
||||
'}}' |
|
||||
).format( |
|
||||
f_name = f_name, |
|
||||
match_func = match_func |
|
||||
) |
|
||||
|
|
||||
def default_match_func ( f_name ) : |
|
||||
""" The JS script creating the matching function to use with typeahed """ |
|
||||
return ( |
|
||||
'function ( q, sync ) {{' |
|
||||
'if ( q === "" ) {{' |
|
||||
'var first = choices_{f_name}.slice( 0, 5 ).map(' |
|
||||
'function ( obj ) {{ return obj.key; }}' |
|
||||
');' |
|
||||
'sync( engine_{f_name}.get( first ) );' |
|
||||
'}} else {{' |
|
||||
'engine_{f_name}.search( q, sync );' |
|
||||
'}}' |
|
||||
'}}' |
|
||||
).format( |
|
||||
f_name = f_name |
|
||||
) |
|
||||
|
|
||||
def typeahead_updater( f_bound ): |
|
||||
""" The JS script creating the function triggered when an item is |
|
||||
selected through typeahead """ |
|
||||
return ( |
|
||||
'function(evt, item) {{' |
|
||||
'$( "#{hidden_id}" ).val( item.key );' |
|
||||
'$( "#{hidden_id}" ).change();' |
|
||||
'return item;' |
|
||||
'}}' |
|
||||
).format( |
|
||||
hidden_id = hidden_id( f_bound ) |
|
||||
) |
|
||||
|
|
||||
def typeahead_change( f_bound ): |
|
||||
""" The JS script creating the function triggered when an item is changed |
|
||||
(i.e. looses focus and value has changed since the moment it gained focus |
|
||||
""" |
|
||||
return ( |
|
||||
'function(evt) {{' |
|
||||
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{' |
|
||||
'$( "#{hidden_id}" ).val( "" );' |
|
||||
'$( "#{hidden_id}" ).change();' |
|
||||
'}}' |
|
||||
'}}' |
|
||||
).format( |
|
||||
input_id = input_id( f_bound ), |
|
||||
hidden_id = hidden_id( f_bound ) |
|
||||
) |
|
||||
|
|
||||
@ -0,0 +1,572 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2017 Maël Kervella |
||||
|
# |
||||
|
# This program is free software; you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU General Public License as published by |
||||
|
# the Free Software Foundation; either version 2 of the License, or |
||||
|
# (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU General Public License along |
||||
|
# with this program; if not, write to the Free Software Foundation, Inc., |
||||
|
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
register = template.Library() |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): |
||||
|
""" |
||||
|
Render a form where some specific fields are rendered using Twitter |
||||
|
Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the |
||||
|
speed and UX when dealing with very large datasets (select with 50k+ elts |
||||
|
for instance). |
||||
|
When the fields specified should normally be rendered as a select with |
||||
|
single selectable option, Twitter Typeahead is used for a better display |
||||
|
and the matching query engine. When dealing with multiple selectable |
||||
|
options, sliptree's Bootstrap Tokenfield in addition with Typeahead. |
||||
|
For convenience, it accepts the same parameters as a standard bootstrap |
||||
|
can accept. |
||||
|
|
||||
|
**Tag name**:: |
||||
|
|
||||
|
massive_bootstrap_form |
||||
|
|
||||
|
**Parameters**: |
||||
|
|
||||
|
form (required) |
||||
|
The form that is to be rendered |
||||
|
|
||||
|
mbf_fields (optional) |
||||
|
A list of field names (comma separated) that should be rendered |
||||
|
with Typeahead/Tokenfield instead of the default bootstrap |
||||
|
renderer. |
||||
|
If not specified, all fields will be rendered as a normal bootstrap |
||||
|
field. |
||||
|
|
||||
|
mbf_param (optional) |
||||
|
A dict of parameters for the massive_bootstrap_form tag. The |
||||
|
possible parameters are the following. |
||||
|
|
||||
|
choices (optional) |
||||
|
A dict of strings representing the choices in JS. The keys of |
||||
|
the dict are the names of the concerned fields. The choices |
||||
|
must be an array of objects. Each of those objects must at |
||||
|
least have the fields 'key' (value to send) and 'value' (value |
||||
|
to display). Other fields can be added as desired. |
||||
|
For a more complex structure you should also consider |
||||
|
reimplementing the engine and the match_func. |
||||
|
If not specified, the key is the id of the object and the value |
||||
|
is its string representation as in a normal bootstrap form. |
||||
|
Example : |
||||
|
'choices' : { |
||||
|
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', |
||||
|
'field_B':..., |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
engine (optional) |
||||
|
A dict of strings representating the engine used for matching |
||||
|
queries and possible values with typeahead. The keys of the |
||||
|
dict are the names of the concerned fields. The string is valid |
||||
|
JS code. |
||||
|
If not specified, BloodHound with relevant basic properties is |
||||
|
used. |
||||
|
Example : |
||||
|
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} |
||||
|
|
||||
|
match_func (optional) |
||||
|
A dict of strings representing a valid JS function used in the |
||||
|
dataset to overload the matching engine. The keys of the dict |
||||
|
are the names of the concerned fields. This function is used |
||||
|
the source of the dataset. This function receives 2 parameters, |
||||
|
the query and the synchronize function as specified in |
||||
|
typeahead.js documentation. If needed, the local variables |
||||
|
'choices_<fieldname>' and 'engine_<fieldname>' contains |
||||
|
respectively the array of all possible values and the engine |
||||
|
to match queries with possible values. |
||||
|
If not specified, the function used display up to the 10 first |
||||
|
elements if the query is empty and else the matching results. |
||||
|
Example : |
||||
|
'match_func' : { |
||||
|
'field_A': 'function(q, sync) { engine.search(q, sync); }', |
||||
|
'field_B': ..., |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
update_on (optional) |
||||
|
A dict of list of ids that the values depends on. The engine |
||||
|
and the typeahead properties are recalculated and reapplied. |
||||
|
Example : |
||||
|
'addition' : { |
||||
|
'field_A' : [ 'id0', 'id1', ... ] , |
||||
|
'field_B' : ... , |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
See boostrap_form_ for other arguments |
||||
|
|
||||
|
**Usage**:: |
||||
|
|
||||
|
{% massive_bootstrap_form |
||||
|
form |
||||
|
[ '<field1>[,<field2>[,...]]' ] |
||||
|
[ mbf_param = { |
||||
|
[ 'choices': { |
||||
|
[ '<field1>': '<choices1>' |
||||
|
[, '<field2>': '<choices2>' |
||||
|
[, ... ] ] ] |
||||
|
} ] |
||||
|
[, 'engine': { |
||||
|
[ '<field1>': '<engine1>' |
||||
|
[, '<field2>': '<engine2>' |
||||
|
[, ... ] ] ] |
||||
|
} ] |
||||
|
[, 'match_func': { |
||||
|
[ '<field1>': '<match_func1>' |
||||
|
[, '<field2>': '<match_func2>' |
||||
|
[, ... ] ] ] |
||||
|
} ] |
||||
|
[, 'update_on': { |
||||
|
[ '<field1>': '<update_on1>' |
||||
|
[, '<field2>': '<update_on2>' |
||||
|
[, ... ] ] ] |
||||
|
} ] |
||||
|
} ] |
||||
|
[ <standard boostrap_form parameters> ] |
||||
|
%} |
||||
|
|
||||
|
**Example**: |
||||
|
|
||||
|
{% massive_bootstrap_form form 'ipv4' choices='[...]' %} |
||||
|
""" |
||||
|
|
||||
|
fields = mbf_fields.split(',') |
||||
|
param = kwargs.pop('mbf_param', {}) |
||||
|
exclude = param.get('exclude', '').split(',') |
||||
|
choices = param.get('choices', {}) |
||||
|
engine = param.get('engine', {}) |
||||
|
match_func = param.get('match_func', {}) |
||||
|
update_on = param.get('update_on', {}) |
||||
|
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 = { |
||||
|
'name': 'mbf_'+f_name, |
||||
|
'placeholder': f_value.empty_label |
||||
|
} |
||||
|
) |
||||
|
html += render_field( |
||||
|
f_value.get_bound_field( form, f_name ), |
||||
|
*args, |
||||
|
**kwargs |
||||
|
) |
||||
|
|
||||
|
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 ), |
||||
|
*args, |
||||
|
**kwargs |
||||
|
) |
||||
|
|
||||
|
return mark_safe( html ) |
||||
|
|
||||
|
def input_id( f_bound ) : |
||||
|
""" The id of the HTML input element """ |
||||
|
return f_bound.auto_id |
||||
|
|
||||
|
def hidden_id( f_bound ): |
||||
|
""" The id of the HTML hidden input element """ |
||||
|
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 """ |
||||
|
return render_tag( |
||||
|
'input', |
||||
|
attrs={ |
||||
|
'id': hidden_id( f_bound ), |
||||
|
'name': f_bound.html_name, |
||||
|
'type': 'hidden', |
||||
|
'value': f_bound.value() or "" |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
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 ) ) |
||||
|
|
||||
|
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 ) ) |
||||
|
|
||||
|
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_name, f_bound ), |
||||
|
remove = tokenfield_remove( f_name, 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};' |
||||
|
'var setup_{f_name} = function() {{' |
||||
|
'engine_{f_name} = {engine};' |
||||
|
'$( "#{input_id}" ).typeahead( "destroy" );' |
||||
|
'$( "#{input_id}" ).typeahead( {datasets} );' |
||||
|
'}};' |
||||
|
'$( "#{input_id}" ).bind( "typeahead:select", {select} );' |
||||
|
'$( "#{input_id}" ).bind( "typeahead:change", {change} );' |
||||
|
'{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 ), |
||||
|
select = typeahead_select( f_bound ), |
||||
|
change = typeahead_change( f_bound ), |
||||
|
updates = ''.join( [ ( |
||||
|
'$( "#{u_id}" ).change( function() {{' |
||||
|
'setup_{f_name}();' |
||||
|
'{reset_input}' |
||||
|
'}} );' |
||||
|
).format( |
||||
|
u_id = u_id, |
||||
|
reset_input = typeahead_reset_input( f_bound ), |
||||
|
f_name = f_name |
||||
|
) for u_id in update_on ] |
||||
|
), |
||||
|
init_input = typeahead_init_input( f_name, f_bound ), |
||||
|
) |
||||
|
|
||||
|
return render_tag( 'script', content=mark_safe( js_content ) ) |
||||
|
|
||||
|
def typeahead_init_input( f_name, f_bound ) : |
||||
|
""" The JS script to init the fields values """ |
||||
|
init_key = f_bound.value() or '""' |
||||
|
return ( |
||||
|
'$( "#{input_id}" ).typeahead("val", {init_val});' |
||||
|
'$( "#{hidden_id}" ).val( {init_key} );' |
||||
|
).format( |
||||
|
input_id = input_id( f_bound ), |
||||
|
init_val = '""' if init_key == '""' else |
||||
|
'engine_{f_name}.get( {init_key} )[0].value'.format( |
||||
|
f_name = f_name, |
||||
|
init_key = init_key |
||||
|
), |
||||
|
init_key = init_key, |
||||
|
hidden_id = hidden_id( f_bound ) |
||||
|
) |
||||
|
|
||||
|
def typeahead_reset_input( f_bound ) : |
||||
|
""" The JS script to reset the fields values """ |
||||
|
return ( |
||||
|
'$( "#{input_id}" ).typeahead("val", "");' |
||||
|
'$( "#{hidden_id}" ).val( "" );' |
||||
|
).format( |
||||
|
input_id = input_id( 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( |
||||
|
objects = ','.join( |
||||
|
[ '{{key:{k},value:"{v}"}}'.format( |
||||
|
k = choice[0] if choice[0] != '' else '""', |
||||
|
v = choice[1] |
||||
|
) for choice in f_value.choices ] |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
def default_engine ( f_name ) : |
||||
|
""" The JS script creating the variable engine_<field_name> """ |
||||
|
return ( |
||||
|
'new Bloodhound({{' |
||||
|
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' |
||||
|
'queryTokenizer: Bloodhound.tokenizers.whitespace,' |
||||
|
'local: choices_{f_name},' |
||||
|
'identify: function(obj) {{ return obj.key; }}' |
||||
|
'}})' |
||||
|
).format( |
||||
|
f_name = f_name |
||||
|
) |
||||
|
|
||||
|
def default_datasets( f_name, match_func ) : |
||||
|
""" The JS script creating the datasets to use with typeahead """ |
||||
|
return ( |
||||
|
'{{' |
||||
|
'hint: true,' |
||||
|
'highlight: true,' |
||||
|
'minLength: 0' |
||||
|
'}},' |
||||
|
'{{' |
||||
|
'display: "value",' |
||||
|
'name: "{f_name}",' |
||||
|
'source: {match_func}' |
||||
|
'}}' |
||||
|
).format( |
||||
|
f_name = f_name, |
||||
|
match_func = match_func |
||||
|
) |
||||
|
|
||||
|
def default_match_func ( f_name ) : |
||||
|
""" The JS script creating the matching function to use with typeahed """ |
||||
|
return ( |
||||
|
'function ( q, sync ) {{' |
||||
|
'if ( q === "" ) {{' |
||||
|
'var first = choices_{f_name}.slice( 0, 5 ).map(' |
||||
|
'function ( obj ) {{ return obj.key; }}' |
||||
|
');' |
||||
|
'sync( engine_{f_name}.get( first ) );' |
||||
|
'}} else {{' |
||||
|
'engine_{f_name}.search( q, sync );' |
||||
|
'}}' |
||||
|
'}}' |
||||
|
).format( |
||||
|
f_name = f_name |
||||
|
) |
||||
|
|
||||
|
def typeahead_select( f_bound ): |
||||
|
""" The JS script creating the function triggered when an item is |
||||
|
selected through typeahead """ |
||||
|
return ( |
||||
|
'function(evt, item) {{' |
||||
|
'$( "#{hidden_id}" ).val( item.key );' |
||||
|
'$( "#{hidden_id}" ).change();' |
||||
|
'return item;' |
||||
|
'}}' |
||||
|
).format( |
||||
|
hidden_id = hidden_id( f_bound ) |
||||
|
) |
||||
|
|
||||
|
def typeahead_change( f_bound ): |
||||
|
""" The JS script creating the function triggered when an item is changed |
||||
|
(i.e. looses focus and value has changed since the moment it gained focus |
||||
|
""" |
||||
|
return ( |
||||
|
'function(evt) {{' |
||||
|
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{' |
||||
|
'$( "#{hidden_id}" ).val( "" );' |
||||
|
'$( "#{hidden_id}" ).change();' |
||||
|
'}}' |
||||
|
'}}' |
||||
|
).format( |
||||
|
input_id = input_id( 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 k = evt.attrs.key;' |
||||
|
'if (!k) {{' |
||||
|
'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; }}' |
||||
|
'k = choices_{f_name}[i].key;' |
||||
|
'}}' |
||||
|
'var new_input = document.createElement("input");' |
||||
|
'new_input.type = "hidden";' |
||||
|
'new_input.id = "{hidden_id}_"+k.toString();' |
||||
|
'new_input.value = k.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_name, f_bound ): |
||||
|
""" The JS script triggered when a token is edited in tokenfield. """ |
||||
|
return ( |
||||
|
'function(evt) {{' |
||||
|
'var k = evt.attrs.key;' |
||||
|
'if (!k) {{' |
||||
|
'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 true; }}' |
||||
|
'k = choices_{f_name}[i].key;' |
||||
|
'}}' |
||||
|
'var old_input = document.getElementById(' |
||||
|
'"{hidden_id}_"+k.toString()' |
||||
|
');' |
||||
|
'old_input.parentNode.removeChild(old_input);' |
||||
|
'}}' |
||||
|
).format( |
||||
|
f_name = f_name, |
||||
|
hidden_id = hidden_id( f_bound ) |
||||
|
) |
||||
|
|
||||
|
def tokenfield_remove( f_name, f_bound ): |
||||
|
""" The JS script trigggered when a token is removed from tokenfield. """ |
||||
|
return ( |
||||
|
'function(evt) {{' |
||||
|
'var k = evt.attrs.key;' |
||||
|
'if (!k) {{' |
||||
|
'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 true; }}' |
||||
|
'k = choices_{f_name}[i].key;' |
||||
|
'}}' |
||||
|
'var old_input = document.getElementById(' |
||||
|
'"{hidden_id}_"+k.toString()' |
||||
|
');' |
||||
|
'old_input.parentNode.removeChild(old_input);' |
||||
|
'}}' |
||||
|
).format( |
||||
|
f_name = f_name, |
||||
|
hidden_id = hidden_id( f_bound ) |
||||
|
) |
||||
|
|
||||
@ -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; |
||||
|
} |
||||
@ -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
Loading…
Reference in new issue