From db30643c139adaecc9708444e835ef146aa7fa49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 13:27:56 +0000 Subject: [PATCH 1/3] Renomme bft en mfb (massive_bootstrap_form) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plus adapté aux nouvelles fonctions incoming --- .../templates/cotisations/edit_facture.html | 4 +- machines/templates/machines/machine.html | 14 +-- machines/views.py | 28 ++--- .../preferences/edit_preferences.html | 4 +- ...typeahead.py => massive_bootstrap_form.py} | 105 ++++++++++-------- topologie/templates/topologie/switch.html | 10 +- topologie/templates/topologie/topo.html | 4 +- topologie/views.py | 6 +- users/templates/users/user.html | 4 +- 9 files changed, 93 insertions(+), 86 deletions(-) rename re2o/templatetags/{bootstrap_form_typeahead.py => massive_bootstrap_form.py} (81%) diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 11e454f5..f1af2b8b 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de factures{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Editer la facture

- {% bootstrap_form_typeahead factureform 'user' %} + {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

Articles de la facture

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 4f68b6ee..9f5e93b5 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de machines{% endblock %} @@ -78,10 +78,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if interfaceform %}

Interface

- {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} @@ -98,15 +98,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if extensionform %}

Extension

- {% bootstrap_form_typeahead extensionform 'origin' %} + {% massive_bootstrap_form extensionform 'origin' %} {% endif %} {% if mxform %}

Enregistrement MX

- {% bootstrap_form_typeahead mxform 'name' %} + {% massive_bootstrap_form mxform 'name' %} {% endif %} {% if nsform %}

Enregistrement NS

- {% bootstrap_form_typeahead nsform 'ns' %} + {% massive_bootstrap_form nsform 'ns' %} {% endif %} {% if txtform %}

Enregistrement TXT

diff --git a/machines/views.py b/machines/views.py index 0e00dc67..5ea4b8b3 100644 --- a/machines/views.py +++ b/machines/views.py @@ -55,7 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension, from users.models import User from users.models import all_has_access from preferences.models import GeneralOption, OptionalMachine -from re2o.templatetags.bootstrap_form_typeahead import hidden_id, input_id +from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id def all_active_interfaces(): """Renvoie l'ensemble des machines autorisées à sortir sur internet """ @@ -85,7 +85,7 @@ def f_type_id( is_type_tt ): return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' def generate_ipv4_choices( form ) : - """ Generate the parameter choices for the bootstrap_form_typeahead tag + """ Generate the parameter choices for the massive_bootstrap_form tag """ f_ipv4 = form.fields['ipv4'] used_mtype_id = [] @@ -112,7 +112,7 @@ def generate_ipv4_choices( form ) : return choices def generate_ipv4_engine( is_type_tt ) : - """ Generate the parameter engine for the bootstrap_form_typeahead tag + """ Generate the parameter engine for the massive_bootstrap_form tag """ return ( 'new Bloodhound( {{' @@ -126,7 +126,7 @@ def generate_ipv4_engine( is_type_tt ) : ) def generate_ipv4_match_func( is_type_tt ) : - """ Generate the parameter match_func for the bootstrap_form_typeahead tag + """ Generate the parameter match_func for the massive_bootstrap_form tag """ return ( 'function(q, sync) {{' @@ -142,20 +142,20 @@ def generate_ipv4_match_func( is_type_tt ) : type_id = f_type_id( is_type_tt ) ) -def generate_ipv4_bft_param( form, is_type_tt ): - """ Generate all the parameters to use with the bootstrap_form_typeahead +def generate_ipv4_mbf_param( form, is_type_tt ): + """ Generate all the parameters to use with the massive_bootstrap_form tag """ i_choices = { 'ipv4': generate_ipv4_choices( form ) } i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } - i_bft_param = { + i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, 'update_on': i_update_on } - return i_bft_param + return i_mbf_param @login_required def new_machine(request, userid): @@ -203,8 +203,8 @@ def new_machine(request, userid): reversion.set_comment("Création") messages.success(request, "La machine a été créée") return redirect("/users/profil/" + str(user.id)) - i_bft_param = generate_ipv4_bft_param( interface, False ) - return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) + return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def edit_interface(request, interfaceid): @@ -243,8 +243,8 @@ def edit_interface(request, interfaceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) messages.success(request, "La machine a été modifiée") return redirect("/users/profil/" + str(interface.machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_machine(request, machineid): @@ -302,8 +302,8 @@ def new_interface(request, machineid): reversion.set_comment("Création") messages.success(request, "L'interface a été ajoutée") return redirect("/users/profil/" + str(machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_interface(request, interfaceid): diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 610889dd..02f006c1 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification des préférences{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} -{% bootstrap_form_typeahead options 'utilisateur_asso' %} +{% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/re2o/templatetags/bootstrap_form_typeahead.py b/re2o/templatetags/massive_bootstrap_form.py similarity index 81% rename from re2o/templatetags/bootstrap_form_typeahead.py rename to re2o/templatetags/massive_bootstrap_form.py index 4c665361..f35c43e7 100644 --- a/re2o/templatetags/bootstrap_form_typeahead.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -29,32 +29,40 @@ from bootstrap3.forms import render_field register = template.Library() @register.simple_tag -def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): +def massive_bootstrap_form(form, mbf_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). + 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**:: - bootstrap_form_typeahead + massive_bootstrap_form **Parameters**: - form + form (required) The form that is to be rendered - typeahead_fields + mbf_fields (optional) A list of field names (comma separated) that should be rendered - with typeahead instead of the default bootstrap renderer. + with Typeahead/Tokenfield instead of the default bootstrap + renderer. + If not specified, all fields will be rendered as a normal bootstrap + field. - bft_param - A dict of parameters for the bootstrap_form_typeahead tag. The + mbf_param (optional) + A dict of parameters for the massive_bootstrap_form tag. The possible parameters are the following. - choices + 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 @@ -71,7 +79,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): ... } - engine + 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 @@ -81,7 +89,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): Example : 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} - match_func + 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 @@ -100,7 +108,7 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): ... } - update_on + 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 : @@ -114,10 +122,10 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): **Usage**:: - {% bootstrap_form_typeahead + {% massive_bootstrap_form form [ '[,[,...]]' ] - [ { + [ mbf_param = { [ 'choices': { [ '': '' [, '': '' @@ -144,56 +152,55 @@ def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): **Example**: - {% bootstrap_form_typeahead form 'ipv4' choices='[...]' %} + {% massive_bootstrap_form 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()] + 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()] - form = '' - for f_name, f_value in django_form.fields.items() : + html = '' + for f_name, f_value in 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 ) + if f_name in fields and not f_name in hidden_fields : + f_bound = f_value.get_bound_field( form, f_name ) f_value.widget = TextInput( attrs={ - 'name': 'typeahead_'+f_name, + 'name': 'mbf_'+f_name, 'placeholder': f_value.empty_label } ) - form += render_field( - f_value.get_bound_field( django_form, f_name ), + html += render_field( + f_value.get_bound_field( form, f_name ), *args, **kwargs ) - form += render_tag( + html += render_tag( 'div', content = hidden_tag( f_bound, f_name ) + - typeahead_js( + mbf_js( f_name, f_value, f_bound, - t_choices, - t_engine, - t_match_func, - t_update_on + choices, + engine, + match_func, + update_on ) ) else: - form += render_field( - f_value.get_bound_field(django_form, f_name), + html += render_field( + f_value.get_bound_field( form, f_name ), *args, **kwargs ) - return mark_safe( form ) + return mark_safe( html ) def input_id( f_bound ) : """ The id of the HTML input element """ @@ -215,20 +222,20 @@ def hidden_tag( f_bound, f_name ): } ) -def typeahead_js( f_name, f_value, f_bound, - t_choices, t_engine, t_match_func, t_update_on ) : +def mbf_js( f_name, f_value, f_bound, + choices_, engine_, match_func_, update_on_ ) : """ The whole script to use """ - choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \ + choices = mark_safe( choices_[f_name] ) if f_name in choices_.keys() \ else default_choices( f_value ) - engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \ + engine = mark_safe( engine_[f_name] ) if f_name in 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 ) + match_func = mark_safe( match_func_[f_name] ) \ + if f_name in 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 [] + update_on = update_on_[f_name] if f_name in update_on_.keys() else [] js_content = ( 'var choices_{f_name} = {choices};' diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index cb84e846..fe224678 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'un switch{% endblock %} @@ -47,16 +47,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if topoform %} - {% bootstrap_form_typeahead topoform 'switch_interface' %} + {% massive_bootstrap_form topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form_typeahead machineform 'user' %} + {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_bft_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index bd07c2db..e14b72a7 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modificationd 'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} - {% bootstrap_form_typeahead topoform 'room,related,machine_interface' %} + {% massive_bootstrap_form topoform 'room,related,machine_interface' %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}
diff --git a/topologie/views.py b/topologie/views.py index ceb06f0a..1fccae75 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -51,7 +51,7 @@ from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm -from machines.views import generate_ipv4_bft_param +from machines.views import generate_ipv4_mbf_param from preferences.models import AssoOption, GeneralOption @@ -381,7 +381,7 @@ def new_switch(request): reversion.set_comment("Création") messages.success(request, "Le switch a été créé") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface, False ) + i_bft_param = generate_ipv4_mbf_param( interface, False ) return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required @@ -442,7 +442,7 @@ def edit_switch(request, switch_id): ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") - i_bft_param = generate_ipv4_bft_param( interface_form, False ) + i_bft_param = generate_ipv4_mbf_param( interface_form, False ) return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) @login_required diff --git a/users/templates/users/user.html b/users/templates/users/user.html index 62d05146..756b4153 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% bootstrap_form_typeahead userform 'room' %} + {% massive_bootstrap_form userform 'room' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
From 7d8d6d85fe2913db1026e750a8991f3767e69e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 18:32:17 +0000 Subject: [PATCH 2/3] Support de typeahead pour les select multiples avec tokenfield --- machines/templates/machines/machine.html | 2 +- re2o/templatetags/massive_bootstrap_form.py | 289 +++-- static/css/bootstrap-tokenfield.css | 210 ++++ static/js/bootstrap-tokenfield/LICENSE.md | 23 + .../bootstrap-tokenfield.js | 1042 +++++++++++++++++ templates/base.html | 4 + 6 files changed, 1499 insertions(+), 71 deletions(-) create mode 100644 static/css/bootstrap-tokenfield.css create mode 100644 static/js/bootstrap-tokenfield/LICENSE.md create mode 100644 static/js/bootstrap-tokenfield/bootstrap-tokenfield.js diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 9f5e93b5..86bf7b90 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if serviceform %}

Service

- {% bootstrap_form serviceform %} + {% massive_bootstrap_form serviceform 'servers' %} {% endif %} {% if vlanform %}

Vlan

diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index f35c43e7..cf6c01fe 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -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,34 +166,64 @@ 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 : - 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 - ) - html += render_tag( - 'div', - content = hidden_tag( f_bound, f_name ) + - mbf_js( - f_name, - f_value, - f_bound, - choices, - engine, - match_func, - update_on + + 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 ), @@ -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,61 +257,101 @@ 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 [] - 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 ), - ) + 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};' + '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 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_ """ 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= 0) + this._delimiters[whitespace] = '\\s' + + if (dash >= 0) { + delete this._delimiters[dash] + this._delimiters.unshift('-') + } + + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] + $.each(this._delimiters, function (index, character) { + var pos = $.inArray(character, specialCharacters) + if (pos >= 0) _self._delimiters[index] = '\\' + character; + }); + + // Store original input width + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null + , elStyleWidth = element.style.width + , elCSSWidth + , elWidth = this.$element.width() + + if (elRules) { + $.each( elRules, function (i, rule) { + if (rule.style.width) { + elCSSWidth = rule.style.width; + } + }); + } + + // Move original input out of the way + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', + originalStyles = { position: this.$element.css('position') }; + originalStyles[hidingPosition] = this.$element.css(hidingPosition); + + this.$element + .data('original-styles', originalStyles) + .data('original-tabindex', this.$element.prop('tabindex')) + .css('position', 'absolute') + .css(hidingPosition, '-10000px') + .prop('tabindex', -1) + + // Create a wrapper + this.$wrapper = $('
') + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') + + // Create a new input + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) + this.$input = $('') + .appendTo( this.$wrapper ) + .prop( 'placeholder', this.$element.prop('placeholder') ) + .prop( 'id', id + '-tokenfield' ) + .prop( 'tabindex', this.$element.data('original-tabindex') ) + + // Re-route original input label to new input + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$input.prop('id') ) + } + + // Set up a copy helper to handle copy & paste + this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) + + // Set wrapper width + if (elStyleWidth) { + this.$wrapper.css('width', elStyleWidth); + } + else if (elCSSWidth) { + this.$wrapper.css('width', elCSSWidth); + } + // If input is inside inline-form with no width set, set fixed width + else if (this.$element.parents('.form-inline').length) { + this.$wrapper.width( elWidth ) + } + + // Set tokenfield disabled, if original or fieldset input is disabled + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { + this.disable(); + } + + // Set tokenfield readonly, if original input is readonly + if (this.$element.prop('readonly')) { + this.readonly(); + } + + // Set up mirror for input auto-sizing + this.$mirror = $(''); + this.$input.css('min-width', this.options.minWidth + 'px') + $.each([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent' + ], function (i, val) { + _self.$mirror[0].style[val] = _self.$input.css(val); + }); + this.$mirror.appendTo( 'body' ) + + // Insert tokenfield to HTML + this.$wrapper.insertBefore( this.$element ) + this.$element.prependTo( this.$wrapper ) + + // Calculate inner input width + this.update() + + // Create initial tokens, if any + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) + + // Start listening to events + this.listen() + + // Initialize autocomplete, if necessary + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { + var side = this.textDirection === 'rtl' ? 'right' : 'left' + , autocompleteOptions = $.extend({ + minLength: this.options.showAutocompleteOnFocus ? 0 : null, + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } + }, this.options.autocomplete ) + + this.$input.autocomplete( autocompleteOptions ) + } + + // Initialize typeahead, if necessary + if ( ! $.isEmptyObject( this.options.typeahead ) ) { + + var typeaheadOptions = this.options.typeahead + , defaults = { + minLength: this.options.showAutocompleteOnFocus ? 0 : null + } + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] + + args[0] = $.extend( {}, defaults, args[0] ) + + this.$input.typeahead.apply( this.$input, args ) + this.typeahead = true + } + } + + Tokenfield.prototype = { + + constructor: Tokenfield + + , createToken: function (attrs, triggerChange) { + var _self = this + + if (typeof attrs === 'string') { + attrs = { value: attrs, label: attrs } + } else { + // Copy objects to prevent contamination of data sources. + attrs = $.extend( {}, attrs ) + } + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + // Normalize label and value + attrs.value = $.trim(attrs.value.toString()); + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value + + // Bail out if has no value or label, or label is too short + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return + + // Bail out if maximum number of tokens is reached + if (this.options.limit && this.getTokens().length >= this.options.limit) return + + // Allow changing token data before creating it + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) + this.$element.trigger(createEvent) + + // Bail out if there if attributes are empty or event was defaultPrevented + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return + + var $token = $('
') + .append('') + .append('×') + .data('attrs', attrs) + + // Insert token into HTML + if (this.$input.hasClass('tt-input')) { + // If the input has typeahead enabled, insert token before it's parent + this.$input.parent().before( $token ) + } else { + this.$input.before( $token ) + } + + // Temporarily set input width to minimum + this.$input.css('width', this.options.minWidth + 'px') + + var $tokenLabel = $token.find('.token-label') + , $closeButton = $token.find('.close') + + // Determine maximum possible token label width + if (!this.maxTokenWidth) { + this.maxTokenWidth = + this.$wrapper.width() - $closeButton.outerWidth() - + parseInt($closeButton.css('margin-left'), 10) - + parseInt($closeButton.css('margin-right'), 10) - + parseInt($token.css('border-left-width'), 10) - + parseInt($token.css('border-right-width'), 10) - + parseInt($token.css('padding-left'), 10) - + parseInt($token.css('padding-right'), 10) + parseInt($tokenLabel.css('border-left-width'), 10) - + parseInt($tokenLabel.css('border-right-width'), 10) - + parseInt($tokenLabel.css('padding-left'), 10) - + parseInt($tokenLabel.css('padding-right'), 10) + parseInt($tokenLabel.css('margin-left'), 10) - + parseInt($tokenLabel.css('margin-right'), 10) + } + + $tokenLabel.css('max-width', this.maxTokenWidth) + if (this.options.html) + $tokenLabel.html(attrs.label) + else + $tokenLabel.text(attrs.label) + + // Listen to events on token + $token + .on('mousedown', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = true + }) + .on('click', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = false + + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + return _self.toggle( $token ) + } + + _self.activate( $token, e.shiftKey, e.shiftKey ) + }) + .on('dblclick', function (e) { + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false + _self.edit( $token ) + }) + + $closeButton + .on('click', $.proxy(this.remove, this)) + + // Trigger createdtoken event on the original field + // indicating that the token is now in the DOM + this.$element.trigger($.Event('tokenfield:createdtoken', { + attrs: attrs, + relatedTarget: $token.get(0) + })) + + // Trigger change event on the original field + if (triggerChange) { + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) + } + + // Update tokenfield dimensions + var _self = this + setTimeout(function () { + _self.update() + }, 0) + + // Return original element + return this.$element.get(0) + } + + , setTokens: function (tokens, add, triggerChange) { + if (!add) this.$wrapper.find('.token').remove() + + if (!tokens) return + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + if (typeof tokens === 'string') { + if (this._delimiters.length) { + // Split based on delimiters + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) + } else { + tokens = [tokens]; + } + } + + var _self = this + $.each(tokens, function (i, attrs) { + _self.createToken(attrs, triggerChange) + }) + + return this.$element.get(0) + } + + , getTokenData: function($token) { + var data = $token.map(function() { + var $token = $(this); + return $token.data('attrs') + }).get(); + + if (data.length == 1) { + data = data[0]; + } + + return data; + } + + , getTokens: function(active) { + var self = this + , tokens = [] + , activeClass = active ? '.active' : '' // get active tokens only + this.$wrapper.find( '.token' + activeClass ).each( function() { + tokens.push( self.getTokenData( $(this) ) ) + }) + return tokens + } + + , getTokensList: function(delimiter, beautify, active) { + delimiter = delimiter || this._firstDelimiter + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify + + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') + return $.map( this.getTokens(active), function (token) { + return token.value + }).join(separator) + } + + , getInput: function() { + return this.$input.val() + } + + , setInput: function (val) { + if (this.$input.hasClass('tt-input')) { + // Typeahead acts weird when simply setting input value to empty, + // so we set the query to empty instead + this.$input.typeahead('val', val) + } else { + this.$input.val(val) + } + } + + , listen: function () { + var _self = this + + this.$element + .on('change', $.proxy(this.change, this)) + + this.$wrapper + .on('mousedown',$.proxy(this.focusInput, this)) + + this.$input + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('paste', $.proxy(this.paste, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + this.$copyHelper + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Secondary listeners for input width calculation + this.$input + .on('keypress', $.proxy(this.update, this)) + .on('keyup', $.proxy(this.update, this)) + + this.$input + .on('autocompletecreate', function() { + // Set minimum autocomplete menu width + var $_menuElement = $(this).data('ui-autocomplete').menu.element + + var minWidth = _self.$wrapper.outerWidth() - + parseInt( $_menuElement.css('border-left-width'), 10 ) - + parseInt( $_menuElement.css('border-right-width'), 10 ) + + $_menuElement.css( 'min-width', minWidth + 'px' ) + }) + .on('autocompleteselect', function (e, ui) { + if (_self.createToken( ui.item )) { + _self.$input.val('') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + return false + }) + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { + // Create token + if (_self.createToken( datum )) { + _self.$input.typeahead('val', '') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + }) + + // Listen to window resize + $(window).on('resize', $.proxy(this.update, this )) + + } + + , keydown: function (e) { + + if (!this.focused) return + + var _self = this + + switch(e.keyCode) { + case 8: // backspace + if (!this.$input.is(document.activeElement)) break + this.lastInputValue = this.$input.val() + break + + case 37: // left arrow + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) + break + + case 38: // up arrow + upDown('prev') + break + + case 39: // right arrow + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) + break + + case 40: // down arrow + upDown('next') + break + + case 65: // a (to handle ctrl + a) + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break + this.activateAll() + e.preventDefault() + break + + case 9: // tab + case 13: // enter + + // We will handle creating tokens from autocomplete in autocomplete events + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break + + // We will handle creating tokens from typeahead in typeahead events + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break + + // Create token + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { + return this.createTokensFromInput(e, this.$input.data('edit')); + } + + // Edit token + if (e.keyCode === 13) { + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break + if (!_self.options.allowEditing) break + this.edit( this.$wrapper.find('.token.active') ) + } + } + + function leftRight(direction) { + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + direction += 'All' + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') + if (!$token.length) return + + _self.preventInputFocus = true + _self.preventDeactivation = true + + _self.activate( $token ) + e.preventDefault() + + } else { + _self[direction]( e.shiftKey ) + e.preventDefault() + } + } + + function upDown(direction) { + if (!e.shiftKey) return + + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') + if (!$token.length) return + + _self.activate( $token ) + } + + var opposite = direction === 'prev' ? 'next' : 'prev' + , position = direction === 'prev' ? 'first' : 'last' + + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { + _self.deactivate( $(this) ) + }) + + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) + e.preventDefault() + } + + this.lastKeyDown = e.keyCode + } + + , keypress: function(e) { + + // Comma + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { + if (this.$input.val()) { + this.createTokensFromInput(e) + } + return false; + } + } + + , keyup: function (e) { + this.preventInputFocus = false + + if (!this.focused) return + + switch(e.keyCode) { + case 8: // backspace + if (this.$input.is(document.activeElement)) { + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break + + this.preventDeactivation = true + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') + + if (!$prevToken.length) break + + this.activate( $prevToken ) + } else { + this.remove(e) + } + break + + case 46: // delete + this.remove(e, 'next') + break + } + this.lastKeyUp = e.keyCode + } + + , focus: function (e) { + this.focused = true + this.$wrapper.addClass('focus') + + if (this.$input.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + + if (this.options.showAutocompleteOnFocus) { + this.search() + } + } + } + + , blur: function (e) { + + this.focused = false + this.$wrapper.removeClass('focus') + + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + } + + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { + this.createTokensFromInput(e) + } + + this.preventDeactivation = false + this.preventCreateTokens = false + } + + , paste: function (e) { + var _self = this + + // Add tokens to existing ones + if (_self.options.allowPasting) { + setTimeout(function () { + _self.createTokensFromInput(e) + }, 1) + } + } + + , change: function (e) { + if ( e.initiator === 'tokenfield' ) return // Prevent loops + + this.setTokens( this.$element.val() ) + } + + , createTokensFromInput: function (e, focus) { + if (this.$input.val().length < this.options.minLength) + return // No input, simply return + + var tokensBefore = this.getTokensList() + this.setTokens( this.$input.val(), true ) + + if (tokensBefore == this.getTokensList() && this.$input.val().length) + return false // No tokens were added, do nothing (prevent form submit) + + this.setInput('') + + if (this.$input.data( 'edit' )) { + this.unedit(focus) + } + + return false // Prevent form being submitted + } + + , next: function (add) { + if (add) { + var $firstActiveToken = this.$wrapper.find('.active:first') + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $firstActiveToken ) + } + + var $lastActiveToken = this.$wrapper.find('.active:last') + , $nextToken = $lastActiveToken.nextAll('.token:first') + + if (!$nextToken.length) { + this.$input.focus() + return + } + + this.activate($nextToken, add) + } + + , prev: function (add) { + + if (add) { + var $lastActiveToken = this.$wrapper.find('.active:last') + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $lastActiveToken ) + } + + var $firstActiveToken = this.$wrapper.find('.active:first') + , $prevToken = $firstActiveToken.prevAll('.token:first') + + if (!$prevToken.length) { + $prevToken = this.$wrapper.find('.token:first') + } + + if (!$prevToken.length && !add) { + this.$input.focus() + return + } + + this.activate( $prevToken, add ) + } + + , activate: function ($token, add, multi, remember) { + + if (!$token) return + + if (typeof remember === 'undefined') var remember = true + + if (multi) var add = true + + this.$copyHelper.focus() + + if (!add) { + this.$wrapper.find('.active').removeClass('active') + if (remember) { + this.$firstActiveToken = $token + } else { + delete this.$firstActiveToken + } + } + + if (multi && this.$firstActiveToken) { + // Determine first active token and the current tokens indicies + // Account for the 1 hidden textarea by subtracting 1 from both + var i = this.$firstActiveToken.index() - 2 + , a = $token.index() - 2 + , _self = this + + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { + _self.activate( $(this), true ) + }) + } + + $token.addClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , activateAll: function() { + var _self = this + + this.$wrapper.find('.token').each( function (i) { + _self.activate($(this), i !== 0, false, false) + }) + } + + , deactivate: function($token) { + if (!$token) return + + $token.removeClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , toggle: function($token) { + if (!$token) return + + $token.toggleClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , edit: function ($token) { + if (!$token) return + + var attrs = $token.data('attrs') + + // Allow changing input value before editing + var options = { attrs: attrs, relatedTarget: $token.get(0) } + var editEvent = $.Event('tokenfield:edittoken', options) + this.$element.trigger( editEvent ) + + // Edit event can be cancelled if default is prevented + if (editEvent.isDefaultPrevented()) return + + $token.find('.token-label').text(attrs.value) + var tokenWidth = $token.outerWidth() + + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + + $token.replaceWith( $_input ) + + this.preventCreateTokens = true + + this.$input.val( attrs.value ) + .select() + .data( 'edit', true ) + .width( tokenWidth ) + + this.update(); + + // Indicate that token is now being edited, and is replaced with an input field in the DOM + this.$element.trigger($.Event('tokenfield:editedtoken', options )) + } + + , unedit: function (focus) { + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + $_input.appendTo( this.$wrapper ) + + this.$input.data('edit', false) + this.$mirror.text('') + + this.update() + + // Because moving the input element around in DOM + // will cause it to lose focus, we provide an option + // to re-focus the input after appending it to the wrapper + if (focus) { + var _self = this + setTimeout(function () { + _self.$input.focus() + }, 1) + } + } + + , remove: function (e, direction) { + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return + + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') + + if (e.type !== 'click') { + if (!direction) var direction = 'prev' + this[direction]() + + // Was it the first token? + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 + } + + // Prepare events and their options + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } + , removeEvent = $.Event('tokenfield:removetoken', options) + + this.$element.trigger(removeEvent); + + // Remove event can be intercepted and cancelled + if (removeEvent.isDefaultPrevented()) return + + var removedEvent = $.Event('tokenfield:removedtoken', options) + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) + + // Remove token from DOM + $token.remove() + + // Trigger events + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) + + // Focus, when necessary: + // When there are no more tokens, or if this was the first token + // and it was removed with backspace or it was clicked on + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() + + // Adjust input width + this.$input.css('width', this.options.minWidth + 'px') + this.update() + + // Cancel original event handlers + e.preventDefault() + e.stopPropagation() + } + + /** + * Update tokenfield dimensions + */ + , update: function (e) { + var value = this.$input.val() + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) + , inputPadding = inputPaddingLeft + inputPaddingRight + + if (this.$input.data('edit')) { + + if (!value) { + value = this.$input.prop("placeholder") + } + if (value === this.$mirror.text()) return + + this.$mirror.text(value) + + var mirrorWidth = this.$mirror.width() + 10; + if ( mirrorWidth > this.$wrapper.width() ) { + return this.$input.width( this.$wrapper.width() ) + } + + this.$input.width( mirrorWidth ) + } + else { + //temporary reset width to minimal value to get proper results + this.$input.width(this.options.minWidth); + + var w = (this.textDirection === 'rtl') + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; + // + // some usecases pre-render widget before attaching to DOM, + // dimensions returned by jquery will be NaN -> we default to 100% + // so placeholder won't be cut off. + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); + } + } + + , focusInput: function (e) { + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return + // Focus only after the current call stack has cleared, + // otherwise has no effect. + // Reason: mousedown is too early - input will lose focus + // after mousedown. However, since the input may be moved + // in DOM, there may be no click or mouseup event triggered. + var _self = this + setTimeout(function() { + _self.$input.focus() + }, 0) + } + + , search: function () { + if ( this.$input.data('ui-autocomplete') ) { + this.$input.autocomplete('search') + } + } + + , disable: function () { + this.setProperty('disabled', true); + } + + , enable: function () { + this.setProperty('disabled', false); + } + + , readonly: function () { + this.setProperty('readonly', true); + } + + , writeable: function () { + this.setProperty('readonly', false); + } + + , setProperty: function(property, value) { + this['_' + property] = value; + this.$input.prop(property, value); + this.$element.prop(property, value); + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); + } + + , destroy: function() { + // Set field value + this.$element.val( this.getTokensList() ); + // Restore styles and properties + this.$element.css( this.$element.data('original-styles') ); + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); + + // Re-route tokenfield label to original input + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$element.prop('id') ) + } + + // Move original element outside of tokenfield wrapper + this.$element.insertBefore( this.$wrapper ); + + // Remove tokenfield-related data + this.$element.removeData('original-styles') + .removeData('original-tabindex') + .removeData('bs.tokenfield'); + + // Remove tokenfield from DOM + this.$wrapper.remove(); + this.$mirror.remove(); + + var $_element = this.$element; + + return $_element; + } + + } + + + /* TOKENFIELD PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.tokenfield + + $.fn.tokenfield = function (option, param) { + var value + , args = [] + + Array.prototype.push.apply( args, arguments ); + + var elements = this.each(function () { + var $this = $(this) + , data = $this.data('bs.tokenfield') + , options = typeof option == 'object' && option + + if (typeof option === 'string' && data && data[option]) { + args.shift() + value = data[option].apply(data, args) + } else { + if (!data && typeof option !== 'string' && !param) { + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) + $this.trigger('tokenfield:initialize') + } + } + }) + + return typeof value !== 'undefined' ? value : elements; + } + + $.fn.tokenfield.defaults = { + minWidth: 60, + minLength: 0, + html: true, + allowEditing: true, + allowPasting: true, + limit: 0, + autocomplete: {}, + typeahead: {}, + showAutocompleteOnFocus: false, + createTokensOnBlur: false, + delimiter: ',', + beautify: true, + inputType: 'text' + } + + $.fn.tokenfield.Constructor = Tokenfield + + + /* TOKENFIELD NO CONFLICT + * ================== */ + + $.fn.tokenfield.noConflict = function () { + $.fn.tokenfield = old + return this + } + + return Tokenfield; + +})); diff --git a/templates/base.html b/templates/base.html index 42505b23..dfaca5eb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,12 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Load CSS and JavaScript #} {% bootstrap_css %} + + {% comment %}{% endcomment %} {% bootstrap_javascript %} + + {% comment %}{% endcomment %} {{ site_name }} : {% block title %}Accueil{% endblock %} From 9ac078ea5bf05a6bd9b437f0f4df7d8a6c18671f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Sat, 14 Oct 2017 21:38:38 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Utilise=20l'id=20des=20objets=20plut=C3=B4t?= =?UTF-8?q?=20que=20leur=20nom=20pour=20les=20id=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/templatetags/massive_bootstrap_form.py | 62 +++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index cf6c01fe..df7edc1f 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -296,8 +296,8 @@ def mbf_js( f_name, f_value, f_bound, multiple, 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 ), + edit = tokenfield_edit( f_name, f_bound ), + remove = tokenfield_remove( f_name, f_bound ), updates = ''.join( [ ( '$( "#{u_id}" ).change( function() {{' 'setup_{f_name}();' @@ -495,17 +495,21 @@ 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