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