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 %}