mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-27 07:02:26 +00:00
Remove templatetag massive bootstrap form
This commit is contained in:
parent
82f48cfce0
commit
e6f8b6426a
1 changed files with 0 additions and 752 deletions
|
@ -1,752 +0,0 @@
|
||||||
# -*- mode: python; coding: utf-8 -*-
|
|
||||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. 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.
|
|
||||||
|
|
||||||
""" Templatetag used to render massive django form selects into bootstrap
|
|
||||||
forms that can still be manipulating even if there is multiple tens of
|
|
||||||
thousands of elements in the select. It's made possible using JS libaries
|
|
||||||
Twitter Typeahead and Splitree's Tokenfield.
|
|
||||||
See docstring of massive_bootstrap_form for a detailed explaantion on how
|
|
||||||
to use this templatetag.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.forms import TextInput
|
|
||||||
from django.forms.widgets import Select
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
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 :
|
|
||||||
'update_on' : {
|
|
||||||
'field_A' : [ 'id0', 'id1', ... ] ,
|
|
||||||
'field_B' : ... ,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
gen_select (optional)
|
|
||||||
A dict of boolean telling if the form should either generate
|
|
||||||
the normal select (set to true) and then use it to generate
|
|
||||||
the possible choices and then remove it or either (set to
|
|
||||||
false) generate the choices variable in this tag and do not
|
|
||||||
send any select.
|
|
||||||
Sending the select before can be usefull to permit the use
|
|
||||||
without any JS enabled but it will execute more code locally
|
|
||||||
for the client so the loading might be slower.
|
|
||||||
If not specified, this variable is set to true for each field
|
|
||||||
Example :
|
|
||||||
'gen_select' : {
|
|
||||||
'field_A': True ,
|
|
||||||
'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>'
|
|
||||||
[, ... ] ] ]
|
|
||||||
} ],
|
|
||||||
[, 'gen_select': {
|
|
||||||
[ '<field1>': '<gen_select1>'
|
|
||||||
[, '<field2>': '<gen_select2>'
|
|
||||||
[, ... ] ] ]
|
|
||||||
} ]
|
|
||||||
} ]
|
|
||||||
[ <standard boostrap_form parameters> ]
|
|
||||||
%}
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
{% massive_bootstrap_form form 'ipv4' choices='[...]' %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
mbf_form = MBFForm(form, mbf_fields.split(","), *args, **kwargs)
|
|
||||||
return mbf_form.render()
|
|
||||||
|
|
||||||
|
|
||||||
class MBFForm:
|
|
||||||
""" An object to hold all the information and useful methods needed to
|
|
||||||
create and render a massive django form into an actual HTML and JS
|
|
||||||
code able to handle it correctly.
|
|
||||||
Every field that is not listed is rendered as a normal bootstrap_field.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, form, mbf_fields, *args, **kwargs):
|
|
||||||
# The django form object
|
|
||||||
self.form = form
|
|
||||||
# The fields on which to use JS
|
|
||||||
self.fields = mbf_fields
|
|
||||||
|
|
||||||
# Other bootstrap_form arguments to render the fields
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
# Fields to exclude form the form rendering
|
|
||||||
self.exclude = self.kwargs.get("exclude", "").split(",")
|
|
||||||
|
|
||||||
# All the mbf parameters specified byt the user
|
|
||||||
param = kwargs.pop("mbf_param", {})
|
|
||||||
self.choices = param.get("choices", {})
|
|
||||||
self.engine = param.get("engine", {})
|
|
||||||
self.match_func = param.get("match_func", {})
|
|
||||||
self.update_on = param.get("update_on", {})
|
|
||||||
self.gen_select = param.get("gen_select", {})
|
|
||||||
self.hidden_fields = [h.name for h in self.form.hidden_fields()]
|
|
||||||
|
|
||||||
# HTML code to insert inside a template
|
|
||||||
self.html = ""
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
""" HTML code for the fully rendered form with all the necessary form
|
|
||||||
"""
|
|
||||||
for name, field in self.form.fields.items():
|
|
||||||
if name not in self.exclude:
|
|
||||||
|
|
||||||
if name in self.fields and name not in self.hidden_fields:
|
|
||||||
mbf_field = MBFField(
|
|
||||||
name,
|
|
||||||
field,
|
|
||||||
field.get_bound_field(self.form, name),
|
|
||||||
self.choices.get(name, None),
|
|
||||||
self.engine.get(name, None),
|
|
||||||
self.match_func.get(name, None),
|
|
||||||
self.update_on.get(name, None),
|
|
||||||
self.gen_select.get(name, True),
|
|
||||||
*self.args,
|
|
||||||
**self.kwargs
|
|
||||||
)
|
|
||||||
self.html += mbf_field.render()
|
|
||||||
|
|
||||||
else:
|
|
||||||
f = field.get_bound_field(self.form, name), self.args, self.kwargs
|
|
||||||
self.html += render_field(
|
|
||||||
field.get_bound_field(self.form, name),
|
|
||||||
*self.args,
|
|
||||||
**self.kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
return mark_safe(self.html)
|
|
||||||
|
|
||||||
|
|
||||||
class MBFField:
|
|
||||||
""" An object to hold all the information and useful methods needed to
|
|
||||||
create and render a massive django form field into an actual HTML and JS
|
|
||||||
code able to handle it correctly.
|
|
||||||
Twitter Typeahead is used for the display and the matching of queries and
|
|
||||||
in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage
|
|
||||||
multiple values.
|
|
||||||
A div with only non visible elements is created after the div containing
|
|
||||||
the displayed input. It's used to store the actual data that will be sent
|
|
||||||
to the server """
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name_,
|
|
||||||
field_,
|
|
||||||
bound_,
|
|
||||||
choices_,
|
|
||||||
engine_,
|
|
||||||
match_func_,
|
|
||||||
update_on_,
|
|
||||||
gen_select_,
|
|
||||||
*args_,
|
|
||||||
**kwargs_
|
|
||||||
):
|
|
||||||
|
|
||||||
# Verify this field is a Select (or MultipleSelect) (only supported)
|
|
||||||
if not isinstance(field_.widget, Select):
|
|
||||||
raise ValueError(
|
|
||||||
(
|
|
||||||
"Field named {f_name} is not a Select and"
|
|
||||||
"can't be rendered with massive_bootstrap_form."
|
|
||||||
).format(f_name=name_)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Name of the field
|
|
||||||
self.name = name_
|
|
||||||
# Django field object
|
|
||||||
self.field = field_
|
|
||||||
# Bound Django field associated with field
|
|
||||||
self.bound = bound_
|
|
||||||
|
|
||||||
# Id for the main visible input
|
|
||||||
self.input_id = self.bound.auto_id
|
|
||||||
# Id for a hidden input used to store the value
|
|
||||||
self.hidden_id = self.input_id + "_hidden"
|
|
||||||
# Id for another div containing hidden inputs and script
|
|
||||||
self.div2_id = self.input_id + "_div"
|
|
||||||
|
|
||||||
# Should the standard select should be generated
|
|
||||||
self.gen_select = gen_select_
|
|
||||||
# Is it select with multiple values possible (use of tokenfield)
|
|
||||||
self.multiple = self.field.widget.allow_multiple_selected
|
|
||||||
# JS for the choices variable (user specified or default)
|
|
||||||
self.choices = choices_ or self.default_choices()
|
|
||||||
# JS for the engine variable (typeahead) (user specified or default)
|
|
||||||
self.engine = engine_ or self.default_engine()
|
|
||||||
# JS for the matching function (typeahead) (user specified or default)
|
|
||||||
self.match_func = match_func_ or self.default_match_func()
|
|
||||||
# JS for the datasets variable (typeahead) (user specified or default)
|
|
||||||
self.datasets = self.default_datasets()
|
|
||||||
# Ids of other fields to bind a reset/reload with when changed
|
|
||||||
self.update_on = update_on_ or []
|
|
||||||
|
|
||||||
# Whole HTML code to insert in the template
|
|
||||||
self.html = ""
|
|
||||||
# JS code in the script tag
|
|
||||||
self.js_script = ""
|
|
||||||
# Input tag to display instead of select
|
|
||||||
self.replace_input = None
|
|
||||||
|
|
||||||
# Other bootstrap_form arguments to render the fields
|
|
||||||
self.args = args_
|
|
||||||
self.kwargs = kwargs_
|
|
||||||
|
|
||||||
def default_choices(self):
|
|
||||||
""" JS code of the variable choices_<fieldname> """
|
|
||||||
|
|
||||||
if self.gen_select:
|
|
||||||
return (
|
|
||||||
"function plop(o) {{"
|
|
||||||
"var c = [];"
|
|
||||||
"for( let i=0 ; i<o.length ; i++) {{"
|
|
||||||
" c.push( {{ key: o[i].value, value: o[i].text }} );"
|
|
||||||
"}}"
|
|
||||||
"return c;"
|
|
||||||
'}} ($("#{select_id}")[0].options)'
|
|
||||||
).format(select_id=self.input_id)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return "[{objects}]".format(
|
|
||||||
objects=",".join(
|
|
||||||
[
|
|
||||||
'{{key:{k},value:"{v}"}}'.format(
|
|
||||||
k=choice[0] if choice[0] != "" else '""', v=choice[1]
|
|
||||||
)
|
|
||||||
for choice in self.field.choices
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def default_engine(self):
|
|
||||||
""" Default JS code of the variable engine_<field_name> """
|
|
||||||
return (
|
|
||||||
"new Bloodhound({{"
|
|
||||||
' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
|
|
||||||
" queryTokenizer: Bloodhound.tokenizers.whitespace,"
|
|
||||||
" local: choices_{name},"
|
|
||||||
" identify: function(obj) {{ return obj.key; }}"
|
|
||||||
"}})"
|
|
||||||
).format(name=self.name)
|
|
||||||
|
|
||||||
def default_datasets(self):
|
|
||||||
""" Default JS script of the datasets to use with typeahead """
|
|
||||||
return (
|
|
||||||
"{{"
|
|
||||||
" hint: true,"
|
|
||||||
" highlight: true,"
|
|
||||||
" minLength: 0"
|
|
||||||
"}},"
|
|
||||||
"{{"
|
|
||||||
' display: "value",'
|
|
||||||
' name: "{name}",'
|
|
||||||
" source: {match_func}"
|
|
||||||
"}}"
|
|
||||||
).format(name=self.name, match_func=self.match_func)
|
|
||||||
|
|
||||||
def default_match_func(self):
|
|
||||||
""" Default JS code of the matching function to use with typeahed """
|
|
||||||
return (
|
|
||||||
"function ( q, sync ) {{"
|
|
||||||
' if ( q === "" ) {{'
|
|
||||||
" var first = choices_{name}.slice( 0, 5 ).map("
|
|
||||||
" function ( obj ) {{ return obj.key; }}"
|
|
||||||
" );"
|
|
||||||
" sync( engine_{name}.get( first ) );"
|
|
||||||
" }} else {{"
|
|
||||||
" engine_{name}.search( q, sync );"
|
|
||||||
" }}"
|
|
||||||
"}}"
|
|
||||||
).format(name=self.name)
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
""" HTML code for the fully rendered field """
|
|
||||||
self.gen_displayed_div()
|
|
||||||
self.gen_hidden_div()
|
|
||||||
return mark_safe(self.html)
|
|
||||||
|
|
||||||
def gen_displayed_div(self):
|
|
||||||
""" Generate HTML code for the div that contains displayed tags """
|
|
||||||
if self.gen_select:
|
|
||||||
self.html += render_field(self.bound, *self.args, **self.kwargs)
|
|
||||||
|
|
||||||
self.field.widget = TextInput(
|
|
||||||
attrs={
|
|
||||||
"name": "mbf_" + self.name,
|
|
||||||
"placeholder": getattr(self.field, "empty_label", _("Nothing")),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.replace_input = render_field(self.bound, *self.args, **self.kwargs)
|
|
||||||
|
|
||||||
if not self.gen_select:
|
|
||||||
self.html += self.replace_input
|
|
||||||
|
|
||||||
def gen_hidden_div(self):
|
|
||||||
""" Generate HTML code for the div that contains hidden tags """
|
|
||||||
self.gen_full_js()
|
|
||||||
|
|
||||||
content = self.js_script
|
|
||||||
if not self.multiple and not self.gen_select:
|
|
||||||
content += self.hidden_input()
|
|
||||||
|
|
||||||
self.html += render_tag("div", content=content, attrs={"id": self.div2_id})
|
|
||||||
|
|
||||||
def hidden_input(self):
|
|
||||||
""" HTML for the hidden input element """
|
|
||||||
return render_tag(
|
|
||||||
"input",
|
|
||||||
attrs={
|
|
||||||
"id": self.hidden_id,
|
|
||||||
"name": self.bound.html_name,
|
|
||||||
"type": "hidden",
|
|
||||||
"value": self.bound.value() or "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def gen_full_js(self):
|
|
||||||
""" Generate the full script tag containing the JS code """
|
|
||||||
self.create_js()
|
|
||||||
self.fill_js()
|
|
||||||
self.get_script()
|
|
||||||
|
|
||||||
def create_js(self):
|
|
||||||
""" Generate a template for the whole script to use depending on
|
|
||||||
gen_select and multiple """
|
|
||||||
if self.gen_select:
|
|
||||||
if self.multiple:
|
|
||||||
self.js_script = (
|
|
||||||
'$( "#{input_id}" ).ready( function() {{'
|
|
||||||
" var choices_{f_name} = {choices};"
|
|
||||||
" {del_select}"
|
|
||||||
" 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", {tok_create} );'
|
|
||||||
' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
|
|
||||||
' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
|
|
||||||
" {tok_updates}"
|
|
||||||
" setup_{f_name}();"
|
|
||||||
" {tok_init_input}"
|
|
||||||
"}} );"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.js_script = (
|
|
||||||
'$( "#{input_id}" ).ready( function() {{'
|
|
||||||
" var choices_{f_name} = {choices};"
|
|
||||||
" {del_select}"
|
|
||||||
" {gen_hidden}"
|
|
||||||
" 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", {typ_select} );'
|
|
||||||
' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
|
|
||||||
" {typ_updates}"
|
|
||||||
" setup_{f_name}();"
|
|
||||||
" {typ_init_input}"
|
|
||||||
"}} );"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if self.multiple:
|
|
||||||
self.js_script = (
|
|
||||||
"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", {tok_create} );'
|
|
||||||
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
|
|
||||||
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
|
|
||||||
"{tok_updates}"
|
|
||||||
'$( "#{input_id}" ).ready( function() {{'
|
|
||||||
" setup_{f_name}();"
|
|
||||||
" {tok_init_input}"
|
|
||||||
"}} );"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.js_script = (
|
|
||||||
"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", {typ_select} );'
|
|
||||||
'$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
|
|
||||||
"{typ_updates}"
|
|
||||||
'$( "#{input_id}" ).ready( function() {{'
|
|
||||||
" setup_{f_name}();"
|
|
||||||
" {typ_init_input}"
|
|
||||||
"}} );"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure the visible element doesn't have the same name as the hidden elements
|
|
||||||
# Otherwise, in the POST request, they collide and an incoherent value is sent
|
|
||||||
self.js_script += (
|
|
||||||
'$( "#{input_id}" ).ready( function() {{'
|
|
||||||
' $( "#{input_id}" ).attr("name", "mbf_{f_name}");'
|
|
||||||
"}} );"
|
|
||||||
)
|
|
||||||
|
|
||||||
def fill_js(self):
|
|
||||||
""" Fill the template with the correct values """
|
|
||||||
self.js_script = self.js_script.format(
|
|
||||||
f_name=self.name,
|
|
||||||
choices=self.choices,
|
|
||||||
del_select=self.del_select(),
|
|
||||||
gen_hidden=self.gen_hidden(),
|
|
||||||
engine=self.engine,
|
|
||||||
input_id=self.input_id,
|
|
||||||
datasets=self.datasets,
|
|
||||||
typ_select=self.typeahead_select(),
|
|
||||||
typ_change=self.typeahead_change(),
|
|
||||||
tok_create=self.tokenfield_create(),
|
|
||||||
tok_edit=self.tokenfield_edit(),
|
|
||||||
tok_remove=self.tokenfield_remove(),
|
|
||||||
typ_updates=self.typeahead_updates(),
|
|
||||||
tok_updates=self.tokenfield_updates(),
|
|
||||||
tok_init_input=self.tokenfield_init_input(),
|
|
||||||
typ_init_input=self.typeahead_init_input(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_script(self):
|
|
||||||
""" Insert the JS code inside a script tag """
|
|
||||||
self.js_script = render_tag("script", content=mark_safe(self.js_script))
|
|
||||||
|
|
||||||
def del_select(self):
|
|
||||||
""" JS code to delete the select if it has been generated and replace
|
|
||||||
it with an input. """
|
|
||||||
return (
|
|
||||||
'var p = $("#{select_id}").parent()[0];'
|
|
||||||
"var new_input = `{replace_input}`;"
|
|
||||||
"p.innerHTML = new_input;"
|
|
||||||
).format(select_id=self.input_id, replace_input=self.replace_input)
|
|
||||||
|
|
||||||
def gen_hidden(self):
|
|
||||||
""" JS code to add a hidden tag to store the value. """
|
|
||||||
return (
|
|
||||||
'var d = $("#{div2_id}")[0];'
|
|
||||||
'var i = document.createElement("input");'
|
|
||||||
'i.id = "{hidden_id}";'
|
|
||||||
'i.name = "{html_name}";'
|
|
||||||
'i.value = "";'
|
|
||||||
'i.type = "hidden";'
|
|
||||||
"d.appendChild(i);"
|
|
||||||
).format(
|
|
||||||
div2_id=self.div2_id,
|
|
||||||
hidden_id=self.hidden_id,
|
|
||||||
html_name=self.bound.html_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
def typeahead_init_input(self):
|
|
||||||
""" JS code to init the fields values """
|
|
||||||
init_key = self.bound.value() or '""'
|
|
||||||
return (
|
|
||||||
'$( "#{input_id}" ).typeahead("val", {init_val});'
|
|
||||||
'$( "#{hidden_id}" ).val( {init_key} );'
|
|
||||||
).format(
|
|
||||||
input_id=self.input_id,
|
|
||||||
init_val='""'
|
|
||||||
if init_key == '""'
|
|
||||||
else "engine_{name}.get( {init_key} )[0].value".format(
|
|
||||||
name=self.name, init_key=init_key
|
|
||||||
),
|
|
||||||
init_key=init_key,
|
|
||||||
hidden_id=self.hidden_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def typeahead_reset_input(self):
|
|
||||||
""" JS code to reset the fields values """
|
|
||||||
return (
|
|
||||||
'$( "#{input_id}" ).typeahead("val", "");' '$( "#{hidden_id}" ).val( "" );'
|
|
||||||
).format(input_id=self.input_id, hidden_id=self.hidden_id)
|
|
||||||
|
|
||||||
def typeahead_select(self):
|
|
||||||
""" JS code to create 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=self.hidden_id)
|
|
||||||
|
|
||||||
def typeahead_change(self):
|
|
||||||
""" JS code of 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=self.input_id, hidden_id=self.hidden_id)
|
|
||||||
|
|
||||||
def typeahead_updates(self):
|
|
||||||
""" JS code for binding external fields changes with a reset """
|
|
||||||
reset_input = self.typeahead_reset_input()
|
|
||||||
updates = [
|
|
||||||
(
|
|
||||||
'$( "#{u_id}" ).change( function() {{'
|
|
||||||
" setup_{name}();"
|
|
||||||
" {reset_input}"
|
|
||||||
"}} );"
|
|
||||||
).format(u_id=u_id, name=self.name, reset_input=reset_input)
|
|
||||||
for u_id in self.update_on
|
|
||||||
]
|
|
||||||
return "".join(updates)
|
|
||||||
|
|
||||||
def tokenfield_init_input(self):
|
|
||||||
""" JS code to init the fields values """
|
|
||||||
init_key = self.bound.value() or '""'
|
|
||||||
return ('$( "#{input_id}" ).tokenfield("setTokens", {init_val});').format(
|
|
||||||
input_id=self.input_id,
|
|
||||||
init_val='""'
|
|
||||||
if init_key == '""'
|
|
||||||
else (
|
|
||||||
"engine_{name}.get( {init_key} ).map("
|
|
||||||
" function(o) {{ return o.value; }}"
|
|
||||||
")"
|
|
||||||
).format(name=self.name, init_key=init_key),
|
|
||||||
)
|
|
||||||
|
|
||||||
def tokenfield_reset_input(self):
|
|
||||||
""" JS code to reset the fields values """
|
|
||||||
return ('$( "#{input_id}" ).tokenfield("setTokens", "");').format(
|
|
||||||
input_id=self.input_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def tokenfield_create(self):
|
|
||||||
""" JS code 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_{name}.length &&"
|
|
||||||
" choices_{name}[i].value !== data ) {{"
|
|
||||||
" i++;"
|
|
||||||
" }}"
|
|
||||||
" if ( i === choices_{name}.length ) {{ return false; }}"
|
|
||||||
" k = choices_{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 = "{html_name}";'
|
|
||||||
' $( "#{div2_id}" ).append(new_input);'
|
|
||||||
"}}"
|
|
||||||
).format(
|
|
||||||
name=self.name,
|
|
||||||
hidden_id=self.hidden_id,
|
|
||||||
html_name=self.bound.html_name,
|
|
||||||
div2_id=self.div2_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
def tokenfield_edit(self):
|
|
||||||
""" JS code 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_{name}.length &&"
|
|
||||||
" choices_{name}[i].value !== data ) {{"
|
|
||||||
" i++;"
|
|
||||||
" }}"
|
|
||||||
" if ( i === choices_{name}.length ) {{ return true; }}"
|
|
||||||
" k = choices_{name}[i].key;"
|
|
||||||
" }}"
|
|
||||||
" var old_input = document.getElementById("
|
|
||||||
' "{hidden_id}_"+k.toString()'
|
|
||||||
" );"
|
|
||||||
" old_input.parentNode.removeChild(old_input);"
|
|
||||||
"}}"
|
|
||||||
).format(name=self.name, hidden_id=self.hidden_id)
|
|
||||||
|
|
||||||
def tokenfield_remove(self):
|
|
||||||
""" JS code 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_{name}.length &&"
|
|
||||||
" choices_{name}[i].value !== data ) {{"
|
|
||||||
" i++;"
|
|
||||||
" }}"
|
|
||||||
" if ( i === choices_{name}.length ) {{ return true; }}"
|
|
||||||
" k = choices_{name}[i].key;"
|
|
||||||
" }}"
|
|
||||||
" var old_input = document.getElementById("
|
|
||||||
' "{hidden_id}_"+k.toString()'
|
|
||||||
" );"
|
|
||||||
" old_input.parentNode.removeChild(old_input);"
|
|
||||||
"}}"
|
|
||||||
).format(name=self.name, hidden_id=self.hidden_id)
|
|
||||||
|
|
||||||
def tokenfield_updates(self):
|
|
||||||
""" JS code for binding external fields changes with a reset """
|
|
||||||
reset_input = self.tokenfield_reset_input()
|
|
||||||
updates = [
|
|
||||||
(
|
|
||||||
'$( "#{u_id}" ).change( function() {{'
|
|
||||||
" setup_{name}();"
|
|
||||||
" {reset_input}"
|
|
||||||
"}} );"
|
|
||||||
).format(u_id=u_id, name=self.name, reset_input=reset_input)
|
|
||||||
for u_id in self.update_on
|
|
||||||
]
|
|
||||||
return "".join(updates)
|
|
Loading…
Reference in a new issue