Code Monkey home page Code Monkey logo

magicsuggest's Introduction

GitHub issues GitHub closed issues GitHub milestones npm GitHub Release Date


MagicSuggest v2.1.7 Updates deprecated jQuery methods

  • (fix) updates deprecated jQuery methods such as trim(), $.isArray, and Proxy().
  • (Update) It replaces them with native JavaScript alternatives.
  • (Fix) the jQuery method each() is also replaced with native JavaScript forEach() - where possible.

MagicSuggest v2.1.6 Bug Fix

  • (fix) Disabled arbitrary HTML and SCRIPT execution on input.

MagicSuggest v2.1.5

  • (fix) prepend close button instead of appending it
  • (fix) updated magicsuggest-min.js with current master code
  • (fea) dynamically update max selection size

MagicSuggest v2.0.0

MagicSuggest has a new home here: http://nicolasbize.com/magicsuggest/ It includes a great new API documentation, examples, tutorials and more!

Milestone change log:

MagicSuggest v.2.0.0

  • New home at http://nicolasbize.com/magicsuggest/ to run dynamic examples that gh-pages couldn't handle
  • Now runs with Bootstrap 3 (required)
  • Responsive design
  • No more intrusive loading

v1.3.1 BugFixing again! (Minor Tagged Milestone - August 17th, 2013)

v1.3.0 Some more features and bugfixing (Minor Tagged Milestone - May 25th, 2013)

  • (fea) combo component can now be fetched through the same div element (credits to meghuizen - https://github.com/meghuizen)
  • (fix) CSS bug with 1.2.7+ with triggerHidden (credits to ScullWM - https://github.com/ScullWM)
  • (fix) container would always render with 1 row even though a bunch of data was loaded (credits to travishaagen - https://github.com/travishaagen)
  • (fea) added minimum jQuery version to work in docs (credits to rajeshmeniya - https://github.com/rajeshmeniya)
  • (fix) input was not correctly enabled / disabled (credits to zerekw - https://github.com/zerekw)
  • (fea) added getName and setName to easily fetch/set form name of component (credits to jbmoens - https://github.com/jbmoens)
  • (fix) when a value is specified in the DOM original element, it is passed correctly to MS. (credits to jbmoens - https://github.com/jbmoens)
  • (fix) input space now always uses the remaining space as this leads to less issues.
  • (fea) combo has now more logic when used for a single selection combo box.
  • (fix) space taken for single selection on a small combo remains on one line. (credits to ScullWM - https://github.com/ScullWM)
  • (fea) multiple items can now be selected through the Ctrl key (credits to meghuizen - https://github.com/meghuizen)
  • (fea) trigger icon now uses pure CSS (credits to meghuizen - https://github.com/meghuizen)
  • (fea) cfg(data) can now take a function as parameter (credits to meghuizen - https://github.com/meghuizen)
  • (fea) cfg(data) can take a json object whose data items are within the results property
  • (fix) CSS has been fixed so it behaves correctly within a bootstrap modal (credits to daenuprobst - https://github.com/daenuprobst)
  • (fea) suggestion rendering optimized by reducing draw calls to one. (credits to meghuizen - https://github.com/meghuizen)
  • (fix) tags can now longer be removed when the combo is disabled (credits to grena - https://github.com/grena)
  • (fix) setting data was only going through visible set of suggestions (credits to grena - https://github.com/grena)
  • (fix) missing semi-colons, went through full jslint (credits to grena - https://github.com/grena)
  • (fix) suggestions were not appearing when maxSuggestions was set to 10. (credits to zerekw - https://github.com/zerekw and plasmaxy - https://github.com/plasmaxy)
  • (fix) the clear function was broken (credits to travishaagen - https://github.com/travishaagen)
  • (fea) the component's config can now be setup entirely from the DOM container element.
  • (fea) added a silent mode to selection changing methods in order to know if it was user-triggered or not. (credits to travishaagen - https://github.com/travishaagen)
  • (fea) added a setData(object) method to fill the combo after it has been rendered (credits to travishaagen - https://github.com/travishaagen)
  • (fix) ajax query was sent twice when the user was typing faster than the typeDelay (credits to arvenom - https://github.com/arvenom)
  • (fix) highlighting the search results was also highlighting html tags when using custom rendering (credits to pstuart2 - https://github.com/pstuart2)
  • (fea) added cfg(strictSuggest) so that user can choose how the suggestions will be made
  • (fea) added cfg(toggleOnClick) so that the user can expand/close the combo by clicking on it (credits to psulek - https://github.com/psulek)
  • (fix) empty suggestion text was wrongly triggered when performing initial ajax call (credits to curtgrimes - https://github.com/curtgrimes)
  • (fea) added cfg(selectionRenderer) (credits to pstuart2 - https://github.com/pstuart2)
  • (fix) empty text class was not triggered properly (credits to jods4 - https://github.com/jods4)
  • (fix) IE8 compatibility (credits to Airborn22 - https://github.com/Airborn22)
  • (fea) MagicSuggest can now be rendered from a select dom component. (credits to Yogu - https://github.com/Yogu)
  • (fea) on blur now automatically adds the typed text to the selection if free entries are allowed (credits to Airborn22 - https://github.com/Airborn22)
  • (fea) new public method empty() which will clear the user text.
  • (fix) make sure combo is filled prior to triggering load event
  • (fea) renamed some events for better readability

v1.2.0 Standardization on jQuery plugins (Minor Tagged Milestone - Mar. 4th 2013)

  • (fix) fixed disabled behaviour when one could still edit the emptyText
  • (fix) collapse method would throw an error
  • (cfg) typeDelay: Amount (in ms) between keyboard registers (credits to jayesbee - https://github.com/jayesbee)
  • (fea) standardized on jQuery plugin (credits to jayesbee - https://github.com/jayesbee)
  • (fea) added documentation examples
  • (cfg) name: name used for magicsuggest as a form element (credits to iambibhas - https://github.com/iambibhas)
  • (fix) start up rendering when value rendered as text
  • (cfg) dataParams: additional parameters for ajax request (credits to jayesbee - https://github.com/jayesbee)
  • (fix) other rendering issues with inner text

v1.1.0 Various enhancements and bug fixing (Minor Tagged Milestone - Feb. 19th 2013)

  • (fea) close cross style now blends in a bit more
  • (fea) escape now collapses the combo (without loosing focus)
  • (fix) can't enter entries made out of space
  • (cfg) noSuggestionText: text displayed when there are no suggestions from given data
  • (cfg) minCharsRenderer: allows to customize message when not enough characters are entered to trigger a search
  • (cfg) maxEntryRenderer: allows to customize message when too many characters have been entered
  • (cfg) maxEntryLength: amount of characters to limit user input
  • (cfg) style: custom style applied to the main container
  • (cfg) infoMsgCls: custom class to apply to the helper
  • (fea) new helper message on upper right to inform on the component status
  • (cfg) id: allows to give the component a custom ID
  • (cfg) inputCfg : allows additional parameters passed out to the INPUT tag. Enables usage of AngularJS's custom tags for ex.
  • (cfg) renderer : allows custom rendering within the combo.
  • (cfg) groupBy : allows grouping within the combo box listing.
  • (fix) blur event now registers correctly when selecting an element from the combo
  • (fix) flicker in IE when hovering trigger
  • (cfg) strictSuggest : set how suggestions will be proposed
  • (fix) maxResults is now correctly interpreted
  • (fix) maxSelection is now correctly interpreted
  • (cfg) method : set the ajax method, default to 'POST'
  • (fea) ajax request can now interpret multiple results from server base.
  • (fix) bug where the blur event would be triggered when clicking upon the page
  • (cfg) required : triggers invalid / valid events when not filled
  • (fea) validation through isValid() method

v1.0. initial component release

  • choose to allow free entries or not
  • keyboard management
  • theme ability
  • static and dynamic data processing
  • positionning

magicsuggest's People

Contributors

91bananas avatar adaosantos avatar bbodi avatar foglerek avatar georgekalikhman avatar jmalarcon avatar jmcameselle avatar jmikerq avatar justinaffinity avatar kcmfgi avatar kevinmckinley avatar krisgerhard avatar liquidsnk avatar lodestone avatar mjohnson8165 avatar nicolasbize avatar nikita240 avatar rabas avatar robryanx avatar weconquered avatar zergioz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

magicsuggest's Issues

Seperating tags by space

Hi nicolas!

Now tags are seperated by ",". It means if there no suggessions for the word and we typing "," sign we get new tag. How can I configure plugin for space seperator. I just want have a-z A-Z 0-9 and "-" signs in my tags

Thank you. Excuse me for my English.

Provide simple method to use with <select multiple>

When you have

<select multiple id="input"><option value="key1">value 1</option>...</select>

And simply call

$('#input').magicSuggest();

The magic suggest should automatically fetch the tags from the options, label them after their text and add those items whose selected attribute is set.

When the user changes something, the selected attributes should be kept in sync.

This would allow to use magic suggest as a beautiful renderer of a multiselect, without having to worry about details.

Existing values

Hey,

I have a problem with loading the existing items into the suggest box.

To load the items in the box I use the following code:

$(ms).on('load', function(){
ms.setValue( 'despeledonckers' );
});

But then the have to be in the data json I use for the autosuggest. But when I load all keywords into that json file (more then 10,000) it's extremly slow when clicking the bar for the firsttime.

Why should those we connected. Even if i set allowFreeEntries to true.

Any suggestions?

Best regards,
Tim

Custom Rendering selected Item

I would like to render and manage user selected items by myself, without rewriting '_render'/'_renderSelection' function (i guess).
How about a 'userselected(item)' trigger/event and 'autorender' (default: true) config option? Seeding the trigger with value[cfg.displayField] seems to be a solution?!?

BTW: Thank you for this great libary

Usage doesnt follow normal jquery plugins.

I would really like to use your plugin.. infact im trying to integrate it now.. with one issue. It uses a new MagicSuggest() instantiation instead of practically any other jquery plugin ive used where you would instantiate using the selector, ie. $('.suggestinput').magicSuggest();

Is there a reason why you didnt implement according to the normal practice?

Custom Binding using Knockout JS

Have you any advise for using this with Knockout JS custom bindings? When I bind the MagicSuggest div using custom binding, all the appropriate values are set except "Value" property which is set to a knockout observable.

Passing data as attribute

Hi Nicolas,

Thanks for solving issue #42 and great work on that widget!

Could you also extend this to the data field, i.e., so we can pass actual data values as attribute as well?

<input data="['a', 'b', 'c']" value="['a']">

Cannot pass resultAsString as attribute

Passing resultAsString="true" as an attribute in the input field has no effect on the rendering.

This is due to:

(1) the use of case sensitive properties in the 'cfg' object, while HTML5 treats attributes in a case insensitive way (Node.attributes returns lowercase names)

(2) the comparison operation 'cfg.resultAsString === true' is too restrictive (it only checks for boolean values, while attribute values typically are strings), try something like '(cfg.resultasstring == "true" || cfg.resultasstring == "1")'

Values default

Hey Everyone,

I can't get the plugin to work with values that should be in for default.

It works when using local data. But when I pull the data from a url (json) the default values dissapear. Any suggestions?

Cannot attach event handler to multiple MagicSuggest items?

I have multiple MS on my page and want to attach the same event handler to every one of them. I do the following:

var ms = $('.magicsuggest').magicSuggest({});
$(ms).each(function () {
$(this).on('selectionchange', function(event, combo, selection) {
== more event code here ==
});

While this is working great when there is only one single MS on the page, with multiple ones it fails: all items are transformed into a MS but

  1. Some attributes/options (e.g., data) are copied from one MS to the others
  2. The event 'onSelectionChange' is never fired

From what I could gather, it seems the .magicSuggest({}) method is transforming the list of DOM elements, and the $(ms).on(...) is not recognizing those anymore (while with one single element, it does continue to recognize it).

If I do the following:

var ms = $('.magicsuggest');
$(ms).each(function () {
$(this).magicSuggest({});
});
$(ms).each(function () {
$(this).on('selectionchange', function(event, combo, selection) {
///
});

it does seem to solve issue 1. but not 2.

What should I do?

ms not working with url data

Hi nicolas! Code above doesn't work.

     <script type="text/javascript">
$(document).ready(function() {
    var ms = $('#item_tags').magicSuggest({
        selectionStacked: true,
        data: 'merk.biz/tags/like/add',
        renderer: function(v){
            return '<div>' +
                        '<div><b>#' + v.name + '</b><i style="float:right">ร— '+v.count+'</i></div>' +
            '</div><div style="clear:both;"></div>';
        }
    });
});
</script>

url merk.biz/tags/like/add gives me result

      [{"id":"219","name":"dostawka","count":"2"},{"id":"210","name":"bildirish","count":"0"}]

when I paste this code to data like above it works:

      <script type="text/javascript">
$(document).ready(function() {
    var ms = $('#item_tags').magicSuggest({
        selectionStacked: true,
        data:  [{"id":"219","name":"dostawka","count":"2"},{"id":"210","name":"bildirish","count":"0"}],
        renderer: function(v){
            return '<div>' +
                        '<div><b>#' + v.name + '</b><i style="float:right">ร— '+v.count+'</i></div>' +
            '</div><div style="clear:both;"></div>';
        }
    });
});
</script>

I don't know what to do. Please help me.

Sending values to the server

<input type="text" id="skills" name="skills">
...
var magicsuggest = new MagicSuggest({
    allowFreeEntries: false,
    renderTo: $('#skills'),
    data: "/skills/",
    method: 'GET',
    useCommaKey: false,
});

The input field is inside a form. But I don't seem to receive the values when the form is submitted. Any hint?

unblur doesn't adds to selection

Hi,

I found an issue that if you type in something as a free entry and you don't push enter just click out the field for example to the submit button then the last entered entry is not added to the selected items. Would be great if it happens on "unblur" or something like that :)

Thanks,
Alex

Need to be able to send additional parameters in the AJAX call.

I needed to send additional parameters with the ajax call. so I patched your code slightly. Diff provided below. The jist, I added a dataParams config field, and before the ajax call is done I extend the dataParams config field with the 'query' parameter.

Cheers.

Index: magicsuggest-1.1.0.js
===================================================================
--- magicsuggest-1.1.0.js   (revision 0)
+++ magicsuggest-1.1.0.js   (revision 0)
@@ -0,0 +1,1305 @@
+/**
+ * All auto suggestion boxes are fucked up or badly written.
+ * This is an attempt to create something that doesn't suck...
+ *
+ * Requires: jQuery and the Class class for OOP.
+ *
+ * Author: Nicolas Bize
+ * Date: Feb. 8th 2013
+ * Version: 1.0
+ * Licence: MagicSuggest is licenced under MIT licence (http://www.opensource.org/licenses/mit-license.php)
+ */
+
+var MagicSuggest = Class.create({
+    /**
+     * Initializes the MagicSuggest component
+     * @param cfg - see config below
+     */
+    init: function(cfg){
+        /**********  CONFIGURATION PROPERTIES ************/
+        /**
+         * @cfg {Boolean} allowFreeEntries
+         * <p>Restricts or allows the user to validate typed entries </p>
+         * Defaults to <code>true</code>.
+         */
+        this.allowFreeEntries = cfg.allowFreeEntries !== undefined ? cfg.allowFreeEntries : true;
+
+        /**
+         * @cfg {Boolean} preselectSingleSuggestion
+         * <p>If a single suggestion comes out, it is preselected.</p>
+         * Defaults to <code>true</code>.
+         */
+        this.preselectSingleSuggestion = cfg.preselectSingleSuggestion !== undefined ? cfg.preselectSingleSuggestion : true;
+
+        /**
+         * @cfg {String} cls
+         * <p>A custom CSS class to apply to the field's underlying element</p>
+         * Defaults to <code>''</code>.
+         */
+        this.cls = cfg.cls || '';
+
+        /**
+         * @cfg {Array / String} data
+         * JSON Data source used to populate the combo box. 3 options are available here:<br/>
+         * <p><u>No Data Source (default)</u><br/>
+         *    When left null, the combo box will not suggest anything. It can still enable
+         *    the user to enter multiple entries if allowFreeEntries is set to true (default).</p>
+         * <p><u>Static Source</u><br/>
+         *    You can pass an array of JSON objects, an array of strings or even a single
+         *    CSV string as the data source.<br/>
+         *    For ex. data: [{id:0,name:"Paris"}, {id: 1, name: "New York"}]</p>
+         * <p><u>Url</u><br/>
+         *     You can pass the url from which the component will fetch its JSON data.<br/>
+         *     Data will be fetched using a POST ajax request that will include the entered
+         *     text as 'query' parameter</p>
+         * Defaults to <em>null</em>
+         */
+        this.data = cfg.data !== undefined ? cfg.data : null;
+
+        /**
+         * Additional parameters to the ajax call
+         */
+        this.dataParams = cfg.dataParams !== undefined ? cfg.dataParams : {};
+        
+        /**
+         * @cfg {Boolean} disabled
+         * <p>Start the component in a disabled state.</p>
+         * Defaults to <code>false</code>.
+         */
+        this.disabled = !!cfg.disabled;
+
+        /**
+         * @cfg {String} displayField
+         * <p>name of JSON object property displayed in the combo list</p>
+         * Defaults to <code>name</code>.
+         */
+        this.displayField = cfg.displayField || 'name';
+
+        /**
+         * @cfg {Boolean} editable
+         * <p>Set to false if you only want mouse interaction. In that case the combo will
+         * automatically expand on focus.</p>
+         * Defaults to <code>true</code>.
+         */
+        this.editable = cfg.editable !== undefined ? cfg.editable : true;
+
+        /**
+         * @cfg {String} emptyText
+         * <p>The default placeholder text when nothing has been entered</p>
+         * Defaults to <code>'Type or click here'</code> or just <code>'Click here'</code> if not editable.
+         */
+        this.emptyText = cfg.emptyText || (this.editable === true ? 'Type or click here' : 'Click here');
+
+        /**
+         * @cfg {String} emptyTextCls
+         * <p>A custom CSS class to style the empty text</p>
+         * Defaults to <code>'ms-empty-text'</code>.
+         */
+        this.emptyTextCls = cfg.emptyTextCls || 'ms-empty-text';
+
+        /**
+         * @cfg {Boolean} expanded
+         * <p>Set starting state for combo.</p>
+         * Defaults to <code>false</code>.
+         */
+        this.expanded = !!cfg.expanded;
+
+        /**
+         * @cfg {Boolean} expandOnFocus
+         * <p>Automatically expands combo on focus.</p>
+         * Defaults to <code>false</code>.
+         */
+        this.expandOnFocus = this.editable === false ? true : !!cfg.expandOnFocus;
+
+        /**
+         * @cfg {Boolean} hideTrigger
+         * <p>Set to true to hide the trigger on the right</p>
+         * Defaults to <code>false</code>.
+         */
+        this.hideTrigger = !!cfg.hideTrigger;
+
+        /**
+         * @cfg {Boolean} highlight
+         * <p>Set to true to highlight search input within displayed suggestions</p>
+         * Defaults to <code>true</code>.
+         */
+        this.highlight = cfg.highlight !== undefined ? cfg.highlight : true;
+
+        /**
+         * @cfg {String} id
+         * <p>A custom ID for this component</p>
+         * Defaults to 'ms-ctn-{n}' with n positive integer
+         */
+        this.id = cfg.id || ('ms-ctn-' + $('div[id^="ms-ctn"]').length);
+
+        /**
+         * @cfg {String} infoMsgCls
+         * <p>A class that is added to the info message appearing on the top-right part of the component</p>
+         * Defaults to ''
+         */
+        this.infoMsgCls = cfg.infoMsgCls || '';
+
+        /**
+         * @cfg {Object} inputCfg
+         * <p>Additional parameters passed out to the INPUT tag. Enables usage of AngularJS's custom tags for ex.</p>
+         * Defaults to <code>{}</code>
+         */
+        this.inputCfg = cfg.inputCfg || {};
+
+        /**
+         * @cfg {String} invalidCls
+         * <p>The class that is applied to show that the field is invalid</p>
+         * Defaults to ms-ctn-invalid
+         */
+        this.invalidCls = cfg.invalidCls || 'ms-ctn-invalid';
+
+        /**
+         * @cfg {String} groupBy
+         * <p>JSON property by which the list should be grouped</p>
+         * Defaults to null
+         */
+        this.groupBy = cfg.groupBy !== undefined ? cfg.groupBy : null;
+
+        /**
+         * @cfg {Boolean} matchCase
+         * <p>Set to true to filter data results according to case. Useless if the data is fetched remotely</p>
+         * Defaults to <code>false</code>.
+         */
+        this.matchCase = !!cfg.matchCase;
+
+        /**
+         * @cfg {Integer} maxDropHeight (in px)
+         * <p>Once expanded, the combo's height will take as much room as the # of available results.
+         *    In case there are too many results displayed, this will fix the drop down height.</p>
+         * Defaults to 290 px.
+         */
+        this.maxDropHeight = cfg.maxDropHeight || 290;
+
+        /**
+         * @cfg {Integer} maxEntryLength
+         * <p>Defines how long the user free entry can be. Set to null for no limit.</p>
+         * Defaults to null.
+         */
+        this.maxEntryLength = cfg.maxEntryLength !== undefined ? cfg.maxEntryLength : null;
+
+        /**
+         * @cfg {String} maxEntryRenderer
+         * <p>A function that defines the helper text when the max entry length has been surpassed.</p>
+         * Defaults to <code>function(v){return 'Please reduce your entry by ' + v + ' character' + (v > 1 ? 's':'');}</code>
+         */
+        this.maxEntryRenderer = cfg.maxEntryRenderer ||
+            function(v){return 'Please reduce your entry by ' + v + ' character' + (v > 1 ? 's':'');};
+
+
+        /**
+         * @cfg {Integer} maxSuggestions
+         * <p>The maximum number of results displayed in the combo drop down at once.</p>
+         * Defaults to null.
+         */
+        this.maxSuggestions = cfg.maxSuggestions !== undefined ? cfg.maxSuggestions : null;
+
+        /**
+         * @cfg {Integer} maxSelection
+         * <p>The maximum number of items the user can select if multiple selection is allowed.
+         *    Set to null to remove the limit.</p>
+         * Defaults to 10.
+         */
+        this.maxSelection = cfg.maxSelection !== undefined ? cfg.maxSelection : 10;
+
+        /**
+         * @cfg {Function} maxSelectionRenderer
+         * <p>A function that defines the helper text when the max selection amount has been reached. The function has a single
+         *    parameter which is the number of selected elements.</p>
+         * Defaults to <code>function(v){return 'You cannot choose more than ' + v + ' item' + (v > 1 ? 's':'');}</code>
+         */
+        this.maxSelectionRenderer = cfg.maxSelectionRenderer ||
+            function(v){return 'You cannot choose more than ' + v + ' item' + (v > 1 ? 's':'');};
+
+        /**
+         * @cfg {String} method
+         * <p>The method used by the ajax request.</p>
+         * Defaults to 'POST'
+         */
+        this.method = cfg.method || 'POST';
+
+        /**
+         * @cfg {Integer} minChars
+         * <p>The minimum number of characters the user must type before the combo expands and offers suggestions.
+         * Defaults to <code>0</code>.
+         */
+        this.minChars = $.isNumeric(cfg.minChars) ? cfg.minChars : 0;
+
+        /**
+         * @cfg {Function} minCharsRenderer
+         * <p>A function that defines the helper text when not enough letters are set. The function has a single
+         *    parameter which is the difference between the required amount of letters and the current one.</p>
+         * Defaults to <code>function(v){return 'Please type ' + v + ' more character' + (v > 1 ? 's':'');}</code>
+         */
+        this.minCharsRenderer = cfg.minCharsRenderer ||
+            function(v){return 'Please type ' + v + ' more character' + (v > 1 ? 's':'');};
+
+        /**
+         * @cfg {String} noSuggestionText
+         * <p>The text displayed when there are no suggestions.</p>
+         * Defaults to 'No suggestions"
+         */
+        this.noSuggestionText = cfg.noSuggestionText || 'No suggestions';
+
+        /**
+         * @cfg (function) renderer
+         * <p>A function used to define how the items will be presented in the combo</p>
+         * Defaults to <code>null</code>.
+         */
+        this.renderer = cfg.renderer || null;
+
+        /**
+         * @cfg (input DOM Element) renderTo
+         * <p>The input tag that will be transformed into the component</p>
+         * Defaults to <code>null</code>.
+         */
+        this.renderTo = cfg.renderTo || null;
+
+        /**
+         * @cfg {Boolean} required
+         * <p>Whether or not this field should be required</p>
+         * Defaults to false
+         */
+        this.required = !!cfg.required;
+
+        /**
+         * @cfg {Boolean} resultAsString
+         * <p>Set to true to render selection as comma separated string</p>
+         * Defaults to <code>false</code>.
+         */
+        this.resultAsString = !!cfg.resultAsString;
+
+        /**
+         * @cfg {String} selectionCls
+         * <p>A custom CSS class to add to a selected item</p>
+         * Defaults to <code>''</code>.
+         */
+        this.selectionCls = cfg.selectionCls || '';
+
+        /**
+         * @cfg {String} selectionPosition
+         * <p>Where the selected items will be displayed. Only 'right', 'bottom' and 'inner' are valid values</p>
+         * Defaults to <code>'inner'</code>, meaning the selected items will appear within the input box itself.
+         */
+        if($.type(cfg.selectionPosition) === 'string'){
+            if(['right', 'bottom', 'inner'].indexOf(cfg.selectionPosition.toLowerCase()) === -1){
+                throw "selectionPosition is not valid. Only 'right', 'bottom' and 'inner' are accepted";
+            }
+            this.selectionPosition = cfg.selectionPosition.toLowerCase();
+        } else {
+            this.selectionPosition = 'inner';
+        }
+
+        /**
+         * @cfg {Boolean} selectionStacked
+         * <p>Set to true to stack the selectioned items when positioned on the bottom
+         *    Requires the selectionPosition to be set to 'bottom'</p>
+         * Defaults to <code>false</code>.
+         */
+        this.selectionStacked = !!cfg.selectionStacked;
+        if(this.selectionStacked === true && this.selectionPosition !== 'bottom'){
+            this.selectionPosition = 'bottom';
+        }
+
+        /**
+         * @cfg {String} sortDir
+         * <p>Direction used for sorting. Only 'asc' and 'desc' are valid values</p>
+         * Defaults to <code>'asc'</code>.
+         */
+        if($.type(cfg.sortDir) === 'string'){
+            if(['asc', 'desc'].indexOf(cfg.sortDir.toLowerCase()) === -1){
+                throw "sortDir is not valid. Only 'asc' and 'desc' are accepted";
+            }
+            this.sortDir = cfg.sortDir.toLowerCase();
+        } else {
+            this.sortDir = 'asc';
+        }
+
+        /**
+         * @cfg {String} sortOrder
+         * <p>name of JSON object property for local result sorting.
+         *    Leave null if you do not wish the results to be ordered or if they are already ordered remotely.</p>
+         *
+         * Defaults to <code>null</code>.
+         */
+        this.sortOrder = cfg.sortOrder !== undefined ? cfg.sortOrder : null;
+
+        /**
+         * @cfg {Boolean} strictSuggest
+         * <p>If set to true, suggestions will have to start by user input (and not simply contain it as a substring)</p>
+         * Defaults to <code>false</code>.
+         */
+        this.strictSuggest = !!cfg.strictSuggest;
+
+        /**
+         * @cfg {String} style
+         * <p>Custom style added to the component container.</p>
+         *
+         * Defaults to <code>''</code>.
+         */
+        this.style = cfg.style || '';
+
+        /**
+         * @cfg {Boolean} useTabKey
+         * <p>If set to true, tab won't blur the component but will be registered as the ENTER key</p>
+         * Defaults to <code>false</code>.
+         */
+        this.useTabKey = !!cfg.useTabKey;
+
+        /**
+         * @cfg {Boolean} useCommaKey
+         * <p>If set to true, using comma will validate the user's choice</p>
+         * Defaults to <code>true</code>.
+         */
+        this.useCommaKey = cfg.useCommaKey !== undefined ? cfg.useCommaKey : true;
+
+
+        /**
+         * @cfg {Boolean} useZebraStyle
+         * <p>Determines whether or not the results will be displayed with a zebra table style</p>
+         * Defaults to <code>true</code>.
+         */
+        this.useZebraStyle = cfg.useZebraStyle !== undefined ? cfg.useZebraStyle : true;
+
+        /**
+         * @cfg {String/Object/Array} value
+         * <p>initial value for the field</p>
+         * Defaults to <code>null</code>.
+         */
+        this.value = cfg.value !== undefined ? cfg.value : null;
+
+        /**
+         * @cfg {String} valueField
+         * <p>name of JSON object property that represents its underlying value</p>
+         * Defaults to <code>id</code>.
+         */
+        this.valueField = cfg.valueField || 'id';
+
+        /**
+         * @cfg {Integer} width (in px)
+         * <p>Width of the component</p>
+         * Defaults to underlying element width.
+         */
+        this.width = cfg.width || $(this.renderTo).width();
+
+        /**********  EVENT LIST ************/
+        this._events = [
+        /**
+         * @event afterrender
+         * Fired when the component has finished rendering.
+         * @param this
+         */
+            'afterrender',
+
+        /**
+         * @event beforerender
+         * Fired before the component renders.
+         * @param this
+         */
+            'beforerender',
+
+        /**
+         * @event blur
+         * Fired when the component looses focus.
+         * @param this
+         */
+            'blur',
+
+        /**
+         * @event collapse
+         * Fired when the combo is collapsed.
+         * @param this
+         */
+            'collapse',
+
+        /**
+         * @event expand
+         * Fired when the combo is expanded.
+         * @param this
+         */
+            'expand',
+
+        /**
+         * @event focus
+         * Fired when the component gains focus.
+         * @param this
+         */
+            'focus',
+
+        /**
+         * @event onbeforeload
+         * Fired prior to an ajax request.
+         * @param this
+         */
+            'onbeforeload',
+
+        /**
+         * @event onload
+         * Fired when a key is pressed down within the component.
+         * @param this
+         */
+            'onkeydown',
+
+        /**
+         * @event onkeydown
+         * Fired when a key is pressed down within the component.
+         * @param this
+         */
+            'onkeydown',
+
+        /**
+         * @event onkeyup
+         * Fired when a key is released within the component.
+         * @param this
+         */
+            'onkeyup',
+
+        /**
+         * @event onload
+         * Fired once the ajax request has successfully finished.
+         * @param this
+         * @param json records
+         */
+            'onload',
+
+        /**
+         * @event ontriggerclick
+         * Fired when the user clicks the side trigger.
+         * @param this
+         */
+            'ontriggerclick',
+
+        /**
+         * @event selectionchange
+         * Fired when the selected values have changed.
+         * @param this
+         * @param selected items
+         */
+            'selectionchange'
+
+        ];
+
+        this._selection = []; // private array holder for our selected objects
+        this._comboItemHeight = 0; // private height for each combo item.
+
+        if(this.renderTo !== null){
+            this._doRender();
+        }
+        return this;
+    },
+
+
+    /**********  PUBLIC METHODS ************/
+    /**
+     * Add one or multiple json items to the current selection
+     * @param items - json object or array of json objects
+     */
+    addToSelection: function(items){
+        if(!this.maxSelection || this._selection.length < this.maxSelection){
+            if(!$.isArray(items)){
+                items = [items];
+            }
+            var ref = this, valuechanged = false;
+            $.each(items, function(index, json){
+                if(ref.getValue().indexOf(json[ref.valueField]) === -1){
+                    ref._selection.push(json);
+                    valuechanged = true;
+                }
+            });
+            if(valuechanged === true){
+                this._renderSelection(this.resultAsString);
+                this.input.val('');
+                $(this).trigger('selectionchange', [this, this.getSelectedItems()]);
+            }
+        }
+    },
+
+    /**
+     * Collapse the drop down part of the combo
+     */
+    collapse: function(){
+        if(this.expanded === true){
+            this.combobox.detach();
+            this.container.removeClass('ms-ctn-bootstrap-focus');
+            this.expanded = false;
+            $(this).trigger('collapse', [this]);
+        }
+    },
+
+    /**
+     * Set the component in a disabled state.
+     */
+    disable: function(){
+        this.container.addClass('ms-ctn-disabled');
+        this.disabled = true;
+    },
+
+    /**
+     * Set the component in a enable state.
+     */
+    enable: function(){
+        this.container.removeClass('ms-ctn-disabled');
+        this.disabled = false;
+    },
+
+    /**
+     * Expand the drop drown part of the combo.
+     */
+    expand: function(){
+        if(!this.expanded && this.input.val().length >= this.minChars){
+            this.combobox.appendTo(this.container);
+            this._processSuggestions();
+
+            this.expanded = true;
+            $(this).trigger('expand', [this]);
+        }
+    },
+
+    /**
+     * Retrieve component enabled status
+     */
+    isDisabled: function(){
+        return this.disabled;
+    },
+
+    /**
+     * Check whether or not the component has been rendered.
+     * @return {boolean}
+     */
+    isRendered: function(){
+        return this._rendered === true;
+    },
+
+    /**
+     * Checks whether the field is valid or not
+     * @return {boolean}
+     */
+    isValid: function(){
+        return this.required === false || this._selection.length > 0;
+    },
+
+    /**
+     * Retrieve an array of selected json objects
+     * @return {Array}
+     */
+    getSelectedItems: function(){
+        return this._selection;
+    },
+
+    /**
+     * Retrieve an array of selected values
+     */
+    getValue: function(){
+        var ref = this;
+        return $.map(this._selection, function(o) {
+            return o[ref.valueField];
+        });
+    },
+
+    /**
+     * Remove one or multiples json items from the current selection
+     * @param items - json object or array of json objects
+     */
+    removeFromSelection: function(items){
+        if(!$.isArray(items)){
+            items = [items];
+        }
+        var ref = this, valuechanged = false;
+        $.each(items, function(index, json){
+            var i = ref.getValue().indexOf(json[ref.valueField]);
+            if(i > -1){
+                ref._selection.splice(i, 1);
+                valuechanged = true;
+            }
+        });
+        if(valuechanged === true){
+            this._renderSelection(this.resultAsString);
+            $(this).trigger('selectionchange', [this, this.getSelectedItems()]);
+            if(this.expanded){
+                this._processSuggestions();
+            }
+        }
+    },
+
+    /**
+     * If not rendered, the component will dynamically render itself in the given element.
+     * @param el
+     */
+    render: function(el){
+        if(this.isRendered() === false){
+            this.renderTo = el;
+            this._doRender();
+        }
+    },
+
+    /**
+     * Sets a value for the combo box. Value must be a value or an array of value with data type matching valueField one.
+     * @param data
+     */
+    setValue: function(data){
+        var values = $.isArray(data) ? data : [data],
+            ref = this,
+            items = [];
+        $.each(this.combobox.children(), function(index, suggestion){
+            var obj = $(suggestion).data('json');
+            if(values.indexOf(obj[ref.valueField]) > -1){
+                items.push(obj);
+            }
+        });
+        if(items.length > 0){
+            this.addToSelection(items);
+        }
+
+    },
+
+    /**********  PRIVATE ************/
+
+    /**
+     * Render the component to the given input DOM element
+     * @private
+     */
+    _doRender: function(){
+        if(this.isRendered() === false){
+
+            $(this).trigger('beforerender', [this]);
+
+            // holds the main div, will relay the focus events to the contained input element.
+            this.container = $('<div/>', {
+                id: this.id,
+                'class': 'ms-ctn ' + this.cls +
+                    (this.disabled === true ? ' ms-ctn-disabled' : '') +
+                    (this.editable === true ? '' : ' ms-ctn-readonly'),
+                style: 'width: ' + this.width + 'px;' + this.style
+            });
+            this.container.focus($.proxy(this._onFocus, this));
+            this.container.blur($.proxy(this._onBlur, this));
+            this.container.keydown($.proxy(this._onHandleKeyDown, this));
+            this.container.keyup($.proxy(this._onHandleKeyUp, this));
+
+            // holds the input field
+            this.input = $('<input/>', $.extend({
+                id: 'ms-input-' + $('input[id^="ms-input"]').length,
+                type: 'text',
+                'class': this.emptyTextCls + (this.editable === true ? '' : ' ms-input-readonly'),
+                value: this.emptyText,
+                readonly: !this.editable,
+                style: 'width: ' + (this.width - (this.hideTrigger ? 16 : 44)) + 'px;'
+            }, this.inputCfg));
+            this.input.focus($.proxy(this._onInputFocus, this));
+
+            // holds the trigger on the right side
+            if(this.hideTrigger === false){
+                this.trigger = $('<div/>', {
+                    id: 'ms-trigger-' + $('div[id^="ms-trigger"]').length,
+                    'class': 'ms-trigger',
+                    html: '<div class="ms-trigger-ico"></div>'
+                });
+                this.trigger.click($.proxy(this._onTriggerClick, this));
+                this.container.append(this.trigger);
+            }
+
+            // holds the suggestions. will always be placed on focus
+            this.combobox = $('<div/>', {
+                id: 'ms-res-ctn-' + $('div[id^="ms-res-ctn"]').length,
+                'class': 'ms-res-ctn ',
+                style: 'width: ' + this.width + 'px; height: ' + this.maxDropHeight + 'px;'
+            });
+
+            this.selectionContainer = $('<div/>', {
+                id: 'ms-sel-ctn-' +  $('div[id^="ms-sel-ctn"]').length,
+                'class': 'ms-sel-ctn'
+            });
+            this.selectionContainer.click($.proxy(this._onFocus, this));
+
+            if(this.selectionPosition === 'inner'){
+                this.selectionContainer.append(this.input);
+            } else {
+                this.container.append(this.input);
+            }
+
+            this.helper = $('<div/>', {
+                'class': 'ms-helper ' + this.infoMsgCls
+            });
+            this._updateHelper();
+            this.container.append(this.helper);
+
+
+            // Render the whole thing
+            $(this.renderTo).replaceWith(this.container);
+
+            switch(this.selectionPosition){
+                case 'bottom':
+                    this.selectionContainer.insertAfter(this.container);
+                    if(this.selectionStacked === true){
+                        this.selectionContainer.width(this.container.width());
+                        this.selectionContainer.addClass('ms-stacked');
+                    }
+                    break;
+                case 'right':
+                    this.selectionContainer.insertAfter(this.container);
+                    this.container.css('float', 'left');
+                    break;
+                default:
+                    this.container.append(this.selectionContainer);
+                    break;
+            }
+
+            this._rendered = true;
+            this._processSuggestions();
+            if(this.value !== null){
+                this.setValue(this.value);
+
+                if(this.resultAsString === true){
+                    this._renderSelection(true);
+                }
+            }
+            $(this).trigger('afterrender', [this]);
+            var ref = this;
+            $("body").click(function(e) {
+                if(ref.container.has(e.target).length === 0 && e.target.className.indexOf('ms-res-item') < 0 &&
+                    ref.container[0] !== e.target){
+                    ref._onBlur();
+                }
+            });
+
+            if(this.expanded === true){
+                this.expanded = false;
+                this.expand();
+            }
+        }
+    },
+
+    /**
+     * Triggered when focusing on the container div. Will focus on the input field instead.
+     * @private
+     */
+    _onFocus: function(){
+        this.input.focus();
+    },
+
+    /**
+     * Triggered when focusing on the input text field.
+     * @private
+     */
+    _onInputFocus: function(){
+        if(this.isDisabled() === false){
+            this.container.addClass('ms-ctn-bootstrap-focus');
+            this.container.removeClass(this.invalidCls);
+            if(this.input.val() === this.emptyText){
+                this.input.removeClass(this.emptyTextCls);
+                this.input.val('');
+            }
+            var curLength = this.input.val().length;
+            if((this.expandOnFocus === true && curLength === 0) || curLength > this.minChars){
+                this.expand();
+            }
+            if(this._selection.length === this.maxSelection){
+                this._updateHelper(this.maxSelectionRenderer.call(this, this._selection.length));
+            } else if(curLength < this.minChars){
+                this._updateHelper(this.minCharsRenderer.call(this, this.minChars - curLength));
+            }
+            if(this.resultAsString === true){
+                this._renderSelection();
+            }
+            $(this).trigger('focus', [this]);
+        }
+    },
+
+    /**
+     * Triggered when blurring out of the component
+     * @private
+     */
+    _onBlur: function(){
+        this.container.removeClass('ms-ctn-bootstrap-focus');
+
+        this.collapse();
+
+        if(this.resultAsString === true){
+            this._renderSelection(true);
+        }
+        if(this.isValid() === false){
+            this.container.addClass('ms-ctn-invalid');
+        }
+
+        if(this.input.val() === ''){
+            this.input.addClass(this.emptyTextCls);
+            this.input.val(this.emptyText);
+        }
+        if(this.input.is(":focus")){
+            $(this).trigger('blur', [this]);
+        }
+    },
+
+    /**
+     * Triggered when the user presses a key while the component has focus
+     * This is where we want to handle all keys that don't require the user input field
+     * since it hasn't registered the key hit yet
+     * @param e keyEvent
+     * @private
+     */
+    _onHandleKeyDown: function(e){
+        // check how tab should be handled
+        var active = this.combobox.find('.ms-res-item-active:first'),
+            freeInput = this.input.val() !== this.emptyText ? this.input.val() : '';
+
+        $(this).trigger('onkeydown', [this, e]);
+
+        if(e.keyCode === 9 && (this.useTabKey === false ||
+            (this.useTabKey === true && active.length === 0 && this.input.val().length === 0))){
+            this._onBlur();
+            return;
+        }
+        switch(e.keyCode) {
+            case 8: //backspace
+                if(freeInput.length === 0 && this.getSelectedItems().length > 0 && this.selectionPosition === 'inner'){
+                    this._selection.pop();
+                    this._renderSelection();
+                    $(this).trigger('selectionchange', [this, this.getSelectedItems()]);
+                    this.input.focus();
+                    e.preventDefault();
+                }
+                break;
+            case 9: // tab
+                e.preventDefault();
+                break;
+            case 188:
+                e.preventDefault();
+                break;
+            case 40: // down
+                e.preventDefault();
+                this._moveSelectedRow("down");
+                break;
+            case 38: // up
+                e.preventDefault();
+                this._moveSelectedRow("up");
+                break;
+            default:
+                if(this._selection.length === this.maxSelection){
+                    e.preventDefault();
+                }
+                break;
+        }
+    },
+
+    /**
+     * Triggered when a key is released while the component has focus
+     * @param e
+     * @private
+     */
+    _onHandleKeyUp: function(e){
+        var freeInput = this.input.val() !== this.emptyText ? this.input.val() : '',
+            inputValid = this.input.val().trim().length > 0 && this.input.val() !== this.emptyText &&
+                (!this.maxEntryLength || this.input.val().trim().length < this.maxEntryLength),
+            selected,
+            obj = {},
+            ref = this;
+        $(this).trigger('onkeyup', [this, e]);
+
+        // collapse if escape, but keep focus.
+        if(e.keyCode === 27 && this.expanded){
+            this.combobox.height(0);
+        }
+        // ignore a bunch of keys
+        if((e.keyCode === 9 && this.useTabKey === false) || (e.keyCode > 13 && e.keyCode < 32)){
+            return;
+        }
+        switch(e.keyCode) {
+            case 40:case 38: // up, down
+                e.preventDefault();
+                break;
+            case 13:case 9:case 188:// enter, tab, comma
+                if(e.keyCode !== 188 || this.useCommaKey === true){
+                    e.preventDefault();
+                    if(this.combobox.height() > 0){ // if a selection is performed, select it and reset field
+                        selected = this.combobox.find('.ms-res-item-active:first');
+                        if(selected.length > 0){
+                            selected.click();
+                            return;
+                        }
+                    }
+                    // if no selection or if freetext entered and free entries allowed, add new obj to selection
+                    if(inputValid === true && this.allowFreeEntries === true){
+                        obj[this.displayField] = obj[this.valueField] = freeInput;
+                        this.addToSelection(obj);
+                        this.collapse(); // cause the combo suggestions to reset
+                        ref.input.focus();
+                    }
+                    break;
+                }
+            default:
+                if(this._selection.length === this.maxSelection){
+                    this._updateHelper(this.maxSelectionRenderer.call(this, this._selection.length));
+                } else {
+                    if(freeInput.length < this.minChars){
+                        this._updateHelper(this.minCharsRenderer.call(this, this.minChars - freeInput.length));
+                        if(this.expanded === true){
+                            this.combobox.height(0);
+                        }
+                    } else if(this.maxEntryLength && freeInput.length > this.maxEntryLength){
+                        this._updateHelper(this.maxEntryRenderer.call(this, freeInput.length - this.maxEntryLength));
+                        if(this.expanded === true){
+                            this.combobox.height(0);
+                        }
+                    } else {
+                        this.helper.hide();
+                        if(this.expanded === true){
+                            this._processSuggestions();
+                        } else if(freeInput.length >= this.minChars && this.expanded === false){
+                            this.expand();
+                        }
+                    }
+                }
+                break;
+        }
+    },
+
+    /**
+     * Triggered when clicking on the small trigger in the right
+     * @private
+     */
+    _onTriggerClick: function(){
+        if(this.isDisabled() === false){
+
+            $(this).trigger('ontriggerclick', [this]);
+
+            if(this.expanded === true){
+                this.collapse();
+            } else {
+                this.input.focus();
+                this.expand();
+            }
+        }
+    },
+
+    /**
+     * According to given data and query, sort and add suggestions in their container
+     * @private
+     */
+    _processSuggestions: function(){
+        var json = null;
+        if(this.data !== null){
+            if(typeof(this.data) === 'string' && this.data.indexOf(',') < 0){ // get results from ajax
+                $(this).trigger('onbeforeload', [this]);
+                var ref = this;
+                var params = $.extend({query: this.input.val()}, this.dataParams);
+               
+                $.ajax({
+                    type: this.method,
+                    url: this.data,
+                    data: params,
+                    success: function(items){
+                        if(typeof(items) === 'string'){
+                            json = JSON.parse(items);
+                        } else if(items.results !== undefined){
+                            json = items.results;
+                        } else if($.isArray(items)){
+                            json = items;
+                        }
+                        $(this).trigger('onload', [ref, json]);
+                        ref._displaySuggestions(ref._sortAndTrim(json));
+                    },
+                    error: function(){
+                        throw("Could not reach server");
+                    }
+                });
+            } else if(typeof(this.data) === 'string' && this.data.indexOf(',') > -1) { // results from csv string
+                this._displaySuggestions(this._sortAndTrim(this._getEntriesFromStringArray(this.data.split(','))));
+            } else { // results from local array
+                if(this.data.length > 0 && typeof(this.data[0]) === 'string'){ // results from array of strings
+                    this._displaySuggestions(this._sortAndTrim(this._getEntriesFromStringArray(this.data)));
+                } else { // regular json array
+                    this._displaySuggestions(this._sortAndTrim(this.data));
+                }
+            }
+        }
+    },
+
+    /**
+     * Returns an array of json objects from an array of strings.
+     * @private
+     */
+    _getEntriesFromStringArray: function(data){
+        var json = [], ref = this;
+        $.each(data, function(index, s){
+            var entry = {};
+            entry[ref.displayField] = entry[ref.valueField] = s.trim();
+            json.push(entry);
+        });
+        return json;
+    },
+
+    /**
+     * Sorts the results and cut them down to max # of displayed results at once
+     * @private
+     */
+    _sortAndTrim: function(data){
+        var ref = this,
+            q = this.input.val() !== this.emptyText ? this.input.val() : '',
+            filtered = [],
+            newSuggestions = [],
+            selectedValues = this.getValue();
+        // filter the data according to given input
+        if(q.length > 0){
+            $.each(data, function(index, obj){
+                var name = obj[ref.displayField];
+                if((ref.matchCase === true && name.indexOf(q) > -1) ||
+                   (ref.matchCase === false && name.toLowerCase().indexOf(q.toLowerCase()) > -1)){
+                    if(ref.strictSuggest === false || name.toLowerCase().indexOf(q.toLowerCase()) === 0){
+                        filtered.push(obj);
+                    }
+                }
+            });
+        } else {
+            filtered = data;
+        }
+        // take out the ones that have already been selected
+        $.each(filtered, function(index, obj){
+            if(selectedValues.indexOf(obj[ref.valueField]) === -1){
+                newSuggestions.push(obj);
+            }
+        });
+        // sort the data
+        if(this.sortOrder !== null){
+            newSuggestions.sort(function(a,b){
+                if(a[ref.sortOrder] < b[ref.sortOrder]){
+                    return ref.sortDir === 'asc' ? -1 : 1;
+                }
+                if(a[ref.sortOrder] > b[ref.sortOrder]){
+                    return ref.sortDir === 'asc' ? 1 : -1;
+                }
+                return 0;
+            });
+        }
+        // trim it down
+        if(this.maxSuggestions && this.maxSuggestions > 0){
+            newSuggestions = newSuggestions.slice(0, this.maxSuggestions);
+        }
+        // build groups
+        if(this.groupBy !== null){
+            this._groups = {};
+            $.each(newSuggestions, function(index, value){
+                if(ref._groups[value[ref.groupBy]] === undefined){
+                    ref._groups[value[ref.groupBy]] = {title: value[ref.groupBy], items: [value]};
+                } else {
+                    ref._groups[value[ref.groupBy]].items.push(value);
+                }
+            });
+        }
+        return newSuggestions;
+    },
+
+    /**
+     * Empties the result container and refills it with the array of json results in input
+     * @private
+     */
+    _displaySuggestions: function(data){
+        this.combobox.empty();
+        var ref = this,    // i hate the way jQuery handles scopes
+            resHeight = 0, // total height taken by displayed results.
+            nbGroups = 0;
+
+        if(this._groups === undefined){
+            this._renderComboItems(data);
+            resHeight = ref._comboItemHeight * data.length;
+        } else {
+            for(var grpName in this._groups){
+                nbGroups += 1;
+                $('<div/>', {
+                    'class': 'ms-res-group',
+                    html: grpName
+                }).appendTo(ref.combobox);
+                this._renderComboItems(this._groups[grpName].items, true);
+            }
+            resHeight = ref._comboItemHeight * (data.length + nbGroups);
+        }
+
+        if(resHeight < this.combobox.height() || resHeight < this.maxDropHeight){
+            this.combobox.height(resHeight);
+        } else if(resHeight >= this.combobox.height() && resHeight > this.maxDropHeight){
+            this.combobox.height(this.maxDropHeight);
+        }
+        if(data.length === 1 && this.preselectSingleSuggestion === true){
+            this.combobox.children().filter(':last').addClass('ms-res-item-active');
+        }
+        if(data.length === 0){
+            this._updateHelper(this.noSuggestionText);
+            this.combobox.height(0);
+        }
+    },
+
+    _renderComboItems: function(items, isGrouped){
+        var ref = this;
+        $.each(items, function(index, value){
+            var displayed = ref.renderer !== null ? ref.renderer.call(ref, value) : value[ref.displayField];
+            var resultItemEl = $('<div/>', {
+                'class': 'ms-res-item ' + (isGrouped ? 'ms-res-item-grouped ':'') +
+                    (index % 2 === 1 && ref.useZebraStyle === true ? 'ms-res-odd' : ''),
+                html: ref.highlight === true ? ref._highlightSuggestion(displayed) : displayed
+            }).data('json', value);
+            resultItemEl.click($.proxy(ref._onComboItemSelected, ref));
+            resultItemEl.mouseover($.proxy(ref._onComboItemMouseOver, ref));
+            ref.combobox.append(resultItemEl);
+        });
+        this._comboItemHeight = this.combobox.find('.ms-res-item:first').outerHeight();
+    },
+
+    /**
+     * Replaces html with highlighted html according to case
+     * @param html
+     * @private
+     */
+    _highlightSuggestion: function(html){
+        var q = this.input.val() !== this.emptyText ? this.input.val() : '';
+        if(q.length === 0){
+            return html; // nothing entered as input
+        }
+        if(this.matchCase === true){
+            html = html.replace(new RegExp('(' + q + ')','g'), '<em>$1</em>');
+        } else {
+            html = html.replace(new RegExp('(' + q + ')','gi'), '<em>$1</em>');
+        }
+        return html;
+    },
+
+    /**
+     * Triggered when hovering an element in the combo
+     * @param e
+     * @private
+     */
+    _onComboItemMouseOver: function(e){
+        this.combobox.children().removeClass('ms-res-item-active');
+        $(e.currentTarget).addClass('ms-res-item-active');
+    },
+
+    /**
+     * Triggered when an item is chosen from the list
+     * @param e
+     * @private
+     */
+    _onComboItemSelected: function(e){
+        this.addToSelection($(e.currentTarget).data('json'));
+        $(e.currentTarget).removeClass('ms-res-item-active');
+        this.collapse();
+        this.input.focus();
+    },
+
+    /**
+     * Renders the selected items into their container.
+     * @private
+     */
+    _renderSelection: function(asText){
+        var ref = this, w = 0, inputOffset = 0;
+        if(this.selectionPosition === 'inner'){
+            this.input.detach();
+        }
+        this.selectionContainer.empty();
+
+        $.each(this._selection, function(index, value){
+
+            var selectedItemEl, delItemEl;
+            // tag representing selected value
+            if(asText === true){
+                selectedItemEl = $('<div/>', {
+                    'class': 'ms-sel-item ms-sel-text ' + ref.selectionCls,
+                    html: value[ref.displayField] + (index === (ref._selection.length - 1) ? '' : ',')
+                }).data('json', value);
+            } else {
+                selectedItemEl = $('<div/>', {
+                    'class': 'ms-sel-item ' + ref.selectionCls,
+                    html: value[ref.displayField]
+                }).data('json', value);
+
+                // small cross img
+                delItemEl = $('<span/>', {
+                    'class': 'ms-close-btn'
+                }).data('json', value).appendTo(selectedItemEl);
+                delItemEl.click($.proxy(ref._onRemoveFromSelection, ref));
+            }
+
+            ref.selectionContainer.append(selectedItemEl);
+        });
+        if(this.selectionPosition === 'inner'){
+            // this really sucks... trying to figure out the best way to fill out the remaining space
+            this.selectionContainer.append(this.input);
+            this.input.width(0);
+            if(this.editable === true || this._selection.length === 0){
+                inputOffset = this.input.offset().left - this.selectionContainer.offset().left;
+                w = this.container.width() - inputOffset - 32 - (this.hideTrigger === true ? 0 : 42);
+                this.input.width(w < 100 ? 100 : w);
+            }
+            this.container.height(this.selectionContainer.height());
+        }
+    },
+
+    /**
+     * Triggered when clicking upon cross for deletion
+     * @param e
+     * @private
+     */
+    _onRemoveFromSelection: function(e){
+        this.removeFromSelection($(e.currentTarget).data('json'));
+    },
+
+    /**
+     * Moves the selected cursor amongst the list item
+     * @param dir - 'up' or 'down'
+     * @private
+     */
+    _moveSelectedRow: function(dir){
+        if(!this.expanded){
+            this.expand();
+        }
+        var list, start, active, scrollPos;
+        list = this.combobox.find(".ms-res-item");
+        if(dir === 'down'){
+            start = list.eq(0);
+        } else {
+            start = list.filter(':last');
+        }
+        active = this.combobox.find('.ms-res-item-active:first');
+        if(active.length > 0){
+            if(dir === 'down'){
+                start = active.nextAll('.ms-res-item').first();
+                if(start.length === 0){
+                    start = list.eq(0);
+                }
+                scrollPos = this.combobox.scrollTop();
+                this.combobox.scrollTop(0);
+                if(start[0].offsetTop + start.outerHeight() > this.combobox.height()){
+                    this.combobox.scrollTop(scrollPos + this._comboItemHeight);
+                }
+            } else {
+                start = active.prevAll('.ms-res-item').first();
+                if(start.length === 0){
+                    start = list.filter(':last');
+                    this.combobox.scrollTop(this._comboItemHeight * list.length);
+                }
+                if(start[0].offsetTop < this.combobox.scrollTop()){
+                    this.combobox.scrollTop(this.combobox.scrollTop() - this._comboItemHeight);
+                }
+            }
+        }
+        list.removeClass("ms-res-item-active");
+        start.addClass("ms-res-item-active");
+    },
+
+    /**
+     * Update the helper text
+     * @private
+     */
+    _updateHelper: function(html){
+        this.helper.html(html);
+        if(!this.helper.is(":visible")){
+            this.helper.fadeIn();
+        }
+    }
+
+
+
+});
+
+

Property changes on: magicsuggest-1.1.0.js
___________________________________________________________________
Added: svn:keywords
   + Id

Tag deletion when disabled ?

Hi, thank you for your plugin.

Is it a normal behaviour that we can delete tags when MagicSuggest is set to "disabled" state ?
The add input is disabled, fine, but tags are still removable.

I think it could be a great thing to hide ms-close-btn when MagicSuggest is disabled. What do you think ?

Initial values lost when use data url

I think this is a bug, i try several times to set ini values and I can't show it. Allways the input field is empty. Even if I put values like ['[email protected]','[email protected]'],

heres is my code:

data:'<?php echo base_url("empresas/clasi") ?>',
width: 620,
sortOrder: 'value',
//selectionPosition: 'bottom',
//selectionStacked: true,
name: 'clasificacion',
displayField: 'value',
strictSuggest: false,
minChars: 3,
allowFreeEntries: false,
maxSelection: 10,
value: '<?php $clasificacion = explode(',', $clasificacion); foreach($clasificacion as $value) { $result[] = $value; } echo json_encode( $result ); ?>',
maxSuggestions: 10,
   hideTrigger: true,
});

Ajax Query sent twice and on empty value

$('#autocomplete-ajax').magicSuggest({
width: 350,
maxSelection: 4,
minChars:3,
allowFreeEntries: false,
typeDelay: 400,
//hideTrigger: true,
emptyText: '',
names: 'cities',
data: '/home/getcities'
});

Calling magicSuggest on DocReady causes it run an ajax with empty params.
When I type the 3 letters it sends the same query twice.

Screen Shot 2013-03-22 at 11 40 47

Set initial value with remote dynamic data

Hi !

First, thank you for magicsuggest, it's really usefull !
I was trying to do the following :

        ms = $("#tag-selector").magicSuggest({
            data: "http://my-website.tld/tag-autocomplete-service"
            value: $("#my-input-hidden-with-tags").val().split(','),
        });

but i was unable to set the initial value in the magicsuggestion box unless i'm using a static data set ( ["foo","bar","hello"] ) and my intial values are in this static data-set

is there a way to set initial value while data is fetch through a remote service that provides values based on ?query parameter and not all possible values (too large)

I hope it's understandable ;-)

ms-trigger-ico without background image

The ms-trigger-ico now uses a background-image, with data:image/png. Why not using the same principel als bootstrap it's .caret option?

For example, replace the .ms-ctn .ms-trigger .ms-trigger-ico code in the CSS with the following:

.ms-ctn .ms-trigger .ms-trigger-ico {
    display: inline-block;
    width: 0;
    height: 0;
    vertical-align: top;
    border-top: 4px solid gray;
    border-right: 4px solid transparent;
    border-left: 4px solid transparent;
    content: "";
    margin-left: 9px;
    margin-top: 13px;
}

Incorrect applied .ms-empty-text class

Repro:

  1. create an empty MagicSuggest.
  2. add values with .addToSelection().
  3. try to type in a new item

Result: the text you type is light gray because it has .ms-empty-text class
Expected: standard black text

Reason:
.addToSelection() correctly empties the placeholder by calling ms.input.val('') but it forgets to remove the class. Because of the way checks are done later (e.g. in the focus handler), the class ends up never being removed.

Fix:
There are several ways to fix that, simplest being adding ms.input.removeClass(cfg.emptyTextCls) just after ms.input.val('').
It would be more clean to put the pair into a private setEmpty(true|false) method and make sure the mistake is done nowhere else in the code (I haven't checked).

Return as string

Hi nicolas!

I have input with id='item_tags'

    <input type='text' id='item_tags'>

Javascript

                var ms = $('#item_tags').magicSuggest({
                    data: 'data.php',
                });

By clicking this

    <button onclick='alert($("#item_tags").val())'>alert</button>

I get something like this: [1 2 4]
How can fix it for something like this "France Germany Japan"?

Behaviour inside Bootstrap Modal

When I place magicsuggest inside a twitter bootstrap modal, the dropdown list is not absolute (is that right?) but renders inside the modals body. The surrounding div (with vertical overflow probably set to scroll) then displays scrollbars.

maxSuggestions cannot set to 10

Firstly, thank you for providing us such a nice control. I used this in one of my project, and just found a problem. Here is the code I used:

$('#SearchBox').magicSuggest(
    {
        data: jsonData,
        emptyText: 'Enter Name',
        maxSelection: 5,
        maxSuggestions: 10,
        minChars: 0,
        preselectSingleSuggestion: false
    });

When I set the "maxSuggestions" to 10, the magicSuggest input box won't give any suggestion, though in the debug mode, I can see all the data are fetched correctly and has assigned to "newSuggestion" with 10 items. After that, something unknown happens and no suggestions is given on the web page. If I change this number to any other integer, it works fine. I try this on several machines and I got the same results. Is this a bug or something wrong on my side?

Thank you.

Need Help Fixing CSS

Hi first off, awesome auto suggest, most definitely using this.

I seem to be running into a bit of trouble getting it to integrate correctly with my website. Wherever I place it, it CSS seems to misalign everything with the auto-suggest input appearing several pixels above the words 'click or type here....', the two png images are visible as the darker one is placed again a few pixels below the lighter on.

I've followed the demo code example as precisely as possible. I don't mind getting my hands dirty so if you could point me in the right direction, I could probably try and fix it.

Free entry values not showing on page reload

Hi Nicolas

I've set up the control as per your example with an ajax query, and if I set a value on first load it works fine. However, I am validating the form and if there are validation errors after POST the page will redisplay. If I have input custom values ('allowFreeEntries' = TRUE), then these cannot be redisplayed since they are not present in the database. Perhaps an example will explain:

Database values: {1:"London", 2:"Paris", 3:New York}
Initial MS value:empty.

If I input "London, Paris" into the MS box and then submit the form, it will post [1,2] as expected. If the page redisplays, I simply set the value to [1,2] and retrieve the database values to recreate the MS Box.

If I input "London, Paris, Manchester" and submit, it posts [1,2,"Manchester"]. Now if the page redisplays it will lose the "Manchester", because it has no id and is not present in the database. Is there a way to fix this?

Cheers

How can I read the name given to a MagicSuggest?

Hi Nicolas,

Thanks to your previous post, I can now easily read the id of MS through:

combo.input[0].id

But how can I read its name? I am not posting the form to the server, but want to read the name through, e.g., jQuery. I have found the following trick, but it seems a bit convoluted:

name = $('#'+combo.input[0].id).parents('div.controls').find('input[name]').attr('name')

Any easier way to achieve this?

selectionRenderer: odd behavior when including advanced html including form inputs

Hi--

I"m getting some very odd behavior when I try to include some "advanced" html inside of the selectionRenderer option. In a nutshell, what I'm trying to do is use MS to allow users to select an item and then annotate the selected items (via form inputs). The selections render properly (both adding and removing selections), but when you click on a form input (created by selectionRenderer), the focus immediately shifts to the MS search input. Here's a fiddle:

http://jsfiddle.net/technotarek/WvRsK/

Note how the focus shifts to the search input if you try to focus on either the text area or the checkboxes.

Do you see any way to resolve this issue?

Trigger icon full height

screenshot041
Might be a thing of taste but I prefer to have the trigger icon to have the full (expanding) height of the select-box. This is what I changed:

.ms-ctn .ms-trigger{
height:100%;
position:absolute;
right:0;
}
.ms-sel-ctn{
padding-right:27px;
}

MagicSuggest as an autocomplete simple

Hi!
First of all thanks for your great script.

I actually implement MagicSuggest on my project and in many case, it's just great.
But in other case, I have trouble with the user interface if user should only select ONE result.
Here is how I configure it :

    $('#entreprise_idform').magicSuggest({
        sortOrder: 'entrepriseNom',
        displayField: 'entrepriseNom',
        valueFieldstring: 'id',
        maxEntryRendererfunction: 1,
        allowFreeEntriesboolean: false,
        selectionPosition: 'inner',
        hideTrigger: true,
        width: 206,
        maxSelection: 1,
        data: 'index.php?c=ajax&a=getEntrepriseJson'
    });

It work great but users have problem with the UI. It deform the "area" by showing a useless "textarea".

I've tried to hide it playing with css selector and !important css rules but the area still change his height...

I thing that don't create trouble if(maxEntryRendererfunction==freeInput.length){
//then no more input allowed
}

Else I will probably use another script specialy for this case.

Any idea ?
msuggest

Duplicate detection bug

Hi,

Here is an example :

        $('#mydiv').magicSuggest({
            width: 450,
            sortOrder: 'label',
            selectionPosition: 'bottom',
            selectionStacked: true,
            maxSelection : null,
            displayField: 'label',
            value: [1,3],
            data: [{id:1,label:'one'}, {id:2,label:'two'}, {id:3,label:'three'}]
        });

"one" and "three" are shown on init, so it's good, but If I try to add "three" again, it succeeds, so I have 2 instances of "three" in the tags ! It should forbid me to add a duplicate one.

If I continue and try to add "three" again to have it a third time, it doesn't work because it detects the duplicate.

It seems that the duplicate detection doesn't work on items who are loaded on initialization.

Add support for "toggle dropdown on input click"

Hi,
I need a feature where if you already has focus in associated and you click with mouse within this input it will toggle popup visibility (collapse/expand). This should be configurable. I made small change on my own to achieve this.
Steps to add this feature is to add some code in js file:

Add new config option:

inputClickToggles: true

Locate line:

ms.input.focus($.proxy(handlers._onInputFocus, this));

and add new line:

ms.input.click($.proxy(handlers._onInputClick, this));

Add new method '_onInputClick' under existing '_onInputFocus'

_onInputClick: function() {
    if (ms.isDisabled() === false && _hasFocus) {
        if (cfg.inputClickToggles === true) {
            if (cfg.expanded)
                ms.collapse();
            else
                ms.expand();
        }
    }
},

Ability to use a function as datasource

What would be a nice option to add, is the ability to use a function as datasource. This may look like the following example:

$('#ms').magicSuggest({
    data: function (response) {
        $.ajax({
            type: 'GET',
            url: 'http://www.example.com/cities/json',
            dataType: 'json',
            success: function (data) {
                var retarr = [];

                if (data.length > 0)
                for (i in data) {
                    var cobj = data[i];

                    retarr.push({name: cobj.name, id: cobj.id});
                }

                response(retarr);
            }
        });
    },
    sortOrder: 'name',
    maxResults: 1
});

or you may return the json data.

This will result in like the following code in _processSuggestions:

if(typeof(cfg.data) === 'function') { // get results from a function
    json = cfg.data.call(ms, function (jsondata) { 
        // you can use this function to set the json data
        self._displaySuggestions(self._sortAndTrim(jsondata));
        $(ms).trigger('load', [ms, jsondata]);
    });

    // or you can use the return value (if not empty) to set the json data
    if (typeof(json) == "object" && !!json) {
        self._displaySuggestions(self._sortAndTrim(json));
        $(ms).trigger('load', [ms, json]);
    }
}

Option to select multiple items with CTRL+Click while holding select open

What a nice addition to the options would be is, when selecting multiple items and you use CTRL+Click to click to add the items, the selectbox stays open. Now it behaves like a normal click and closes the select box.

When you want to add multiple items from the select box the following steps should happen:

  1. Click the button to open the selectbox
  2. CTRL+Click on the item to add to the selected items list
  3. Add the item to the selected items list
  4. Remove the item from the options of the selectbox
  5. Don't close the selectbox, because you used CTRL+Click
  6. On focus loss of the select box (clicking outside), close the selectbox again

This behavior makes it a bit faster to add items to the list.

Request for IE 7 support

Hi nicolas,
Thanks for the wonderfull plugin one thing i want to request you to give support for IE 7 that would be Great. nothing can beat this plug in if so.

Input looses events

Here is my input

   <input type="text" name="item_tags" id="item_tags" maxlength='64' onfocus='$("#item_tags_tooltip").slideDown();' onblur='$("#item_tags_tooltip").slideUp();' />

here is onfocus and onblur events. They are not working with MS. Is it possible to fix it?

ms.container always only 1 row high on initial render

MagicSuggest vertically expands to show multiple rows of selected tags. However, if you prepopulate it with data, ms.container will have a height for 1 row on initial render, even if it should be tall enough for multiple rows. This causes the display of selected tags to overflow below ms.container.

I was able to fix this for Chrome and Firefox by adding a call to ms.container.addClass('ms-ctn-bootstrap-focus') within addToSelection. However, this does not fix the issue with IE8, which I use for testing IE.

For example,

    /**
     * Add one or multiple json items to the current selection
     * @param items - json object or array of json objects
     * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
     */
    this.addToSelection = function(items, isSilent)
    {
        if (!cfg.maxSelection || _selection.length < cfg.maxSelection) {
            if (!$.isArray(items)) {
                items = [items];
            }
            var valuechanged = false;
            $.each(items, function(index, json) {
                if ($.inArray(json[cfg.valueField], ms.getValue()) === -1) {
                    _selection.push(json);
                    valuechanged = true;
                }
            });
            if(valuechanged === true) {
                ms.container.addClass('ms-ctn-bootstrap-focus');
                self._renderSelection();
                this.empty();
                if (isSilent !== true) {
                    $(this).trigger('selectionchange', [this, this.getSelectedItems()]);
                }
            }
        }
    };

Css bug with 1.2.7

Hi,
Thanks for you fast improvement. This morning I've just install the new version (thanks for the patch) and I've discover new css bugs.

Here is how I see the suggest with a basic bootstrap view (2.3): There is a horizontal scroller. To make him dispear i've to force "ms-ctn"+20px but it's inline style that sound calculate by magicsuggest.

bugmagicsuggest

Then new thing, the displayField name is now displayed in result, is it a new feature ? (hope no)
I could not reproduce this bug on the magicsuggest demo...

Custom ID-field

Hi, I really like the widget.

One problem I had was when I tried to use an array of objects without an "id" attribute.

It worked fine, but it was impossible to remove a selected item.

I really think there should be an "idField" option, like there is "displayField",
if you want to support custom data-objects.

"No suggestions" text appears on load when setting a data URL

When I define the data parameter as a URL, that URL gets accessed on load, causing the noSuggestionText to appear before even giving the element focus. I've fixed this temporarily by adding an extra condition in _displaySuggestions, so that the no suggestion text only shows when something has been entered in the box and not on load:

 _displaySuggestions: function(data) {

    if (data.length === 0 && ms.getRawValue() !== "") {
        // show noSuggestionText
    }

}

How to differentiate between one item and an URL?

The logic of your code stems that if only 1 string item is passed to data, it must be an URL. But it happens that there is actually only 1 possible item (you never know in advance). In this case, the code should not try to reach that 'url'.

You could use a regex or check for 'http' at the start of the string to make sure a url is indeed passed to data.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.