magic_suggest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2a65f5eea68f7c103a1ab460495ffec9670982c2
4
+ data.tar.gz: 4c2b1070ef3d9cb331d1bb35a3620d122d955358
5
+ SHA512:
6
+ metadata.gz: 77d10dbf8321b564c14a108ee23c5268a515ba731b89b32a197abd6303dcb5feae4664acb6cfcda706cfb96cfa3911369f6b42eeb8965f0e8cae8ef5d7ba5bd4
7
+ data.tar.gz: f82a295710327f4420b00b36050a056cab32d30ecd9fa809bbf5c6b0ff6e05b6f805c91f968991fc87163240e5094bea4bdeaa2a5a2585abf8537a41bec1a701
@@ -0,0 +1,6 @@
1
+ require "magic_suggest/version"
2
+
3
+ module MagicSuggest
4
+ class Engine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module MagicSuggest
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ (function($){"use strict";var MagicSuggest=function(element,options){var ms=this;var defaults={allowFreeEntries:true,allowDuplicates:false,ajaxConfig:{},autoSelect:true,selectFirst:false,queryParam:"query",beforeSend:function(){},cls:"",data:null,dataUrlParams:{},disabled:false,disabledField:null,displayField:"name",editable:true,expanded:false,expandOnFocus:false,groupBy:null,hideTrigger:false,highlight:true,id:null,infoMsgCls:"",inputCfg:{},invalidCls:"ms-inv",matchCase:false,maxDropHeight:290,maxEntryLength:null,maxEntryRenderer:function(v){return"Please reduce your entry by "+v+" character"+(v>1?"s":"")},maxSuggestions:null,maxSelection:10,maxSelectionRenderer:function(v){return"You cannot choose more than "+v+" item"+(v>1?"s":"")},method:"POST",minChars:0,minCharsRenderer:function(v){return"Please type "+v+" more character"+(v>1?"s":"")},mode:"local",name:null,noSuggestionText:"No suggestions",placeholder:"Type or click here",renderer:null,required:false,resultAsString:false,resultAsStringDelimiter:",",resultsField:"results",selectionCls:"",selectionContainer:null,selectionPosition:"inner",selectionRenderer:null,selectionStacked:false,sortDir:"asc",sortOrder:null,strictSuggest:false,style:"",toggleOnClick:false,typeDelay:400,useTabKey:false,useCommaKey:true,useZebraStyle:false,value:null,valueField:"id",vregex:null,vtype:null};var conf=$.extend({},options);var cfg=$.extend(true,{},defaults,conf);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(cfg.allowDuplicates||$.inArray(json[cfg.valueField],ms.getValue())===-1){_selection.push(json);valuechanged=true}});if(valuechanged===true){self._renderSelection();this.empty();if(isSilent!==true){$(this).trigger("selectionchange",[this,this.getSelection()])}}}this.input.attr("placeholder",cfg.selectionPosition==="inner"&&this.getValue().length>0?"":cfg.placeholder)};this.clear=function(isSilent){this.removeFromSelection(_selection.slice(0),isSilent)};this.collapse=function(){if(cfg.expanded===true){this.combobox.detach();cfg.expanded=false;$(this).trigger("collapse",[this])}};this.disable=function(){this.container.addClass("ms-ctn-disabled");cfg.disabled=true;ms.input.attr("disabled",true)};this.empty=function(){this.input.val("")};this.enable=function(){this.container.removeClass("ms-ctn-disabled");cfg.disabled=false;ms.input.attr("disabled",false)};this.expand=function(){if(!cfg.expanded&&(this.input.val().length>=cfg.minChars||this.combobox.children().size()>0)){this.combobox.appendTo(this.container);self._processSuggestions();cfg.expanded=true;$(this).trigger("expand",[this])}};this.isDisabled=function(){return cfg.disabled};this.isValid=function(){var valid=cfg.required===false||_selection.length>0;if(cfg.vtype||cfg.vregex){$.each(_selection,function(index,item){valid=valid&&self._validateSingleItem(item[cfg.valueField])})}return valid};this.getDataUrlParams=function(){return cfg.dataUrlParams};this.getName=function(){return cfg.name};this.getSelection=function(){return _selection};this.getRawValue=function(){return ms.input.val()};this.getValue=function(){return $.map(_selection,function(o){return o[cfg.valueField]})};this.removeFromSelection=function(items,isSilent){if(!$.isArray(items)){items=[items]}var valuechanged=false;$.each(items,function(index,json){var i=$.inArray(json[cfg.valueField],ms.getValue());if(i>-1){_selection.splice(i,1);valuechanged=true}});if(valuechanged===true){self._renderSelection();if(isSilent!==true){$(this).trigger("selectionchange",[this,this.getSelection()])}if(cfg.expandOnFocus){ms.expand()}if(cfg.expanded){self._processSuggestions()}}this.input.attr("placeholder",cfg.selectionPosition==="inner"&&this.getValue().length>0?"":cfg.placeholder)};this.getData=function(){return _cbData};this.setData=function(data){cfg.data=data;self._processSuggestions()};this.setName=function(name){cfg.name=name;if(name){cfg.name+=name.indexOf("[]")>0?"":"[]"}if(ms._valueContainer){$.each(ms._valueContainer.children(),function(i,el){el.name=cfg.name})}};this.setSelection=function(items){this.clear();this.addToSelection(items)};this.setValue=function(values){var items=[];$.each(values,function(index,value){var found=false;$.each(_cbData,function(i,item){if(item[cfg.valueField]==value){items.push(item);found=true;return false}});if(!found){if(typeof value==="object"){items.push(value)}else{var json={};json[cfg.valueField]=value;json[cfg.displayField]=value;items.push(json)}}});if(items.length>0){this.addToSelection(items)}};this.setDataUrlParams=function(params){cfg.dataUrlParams=$.extend({},params)};var _selection=[],_comboItemHeight=0,_timer,_hasFocus=false,_groups=null,_cbData=[],_ctrlDown=false,KEYCODES={BACKSPACE:8,TAB:9,ENTER:13,CTRL:17,ESC:27,SPACE:32,UPARROW:38,DOWNARROW:40,COMMA:188};var self={_displaySuggestions:function(data){ms.combobox.show();ms.combobox.empty();var resHeight=0,nbGroups=0;if(_groups===null){self._renderComboItems(data);resHeight=_comboItemHeight*data.length}else{for(var grpName in _groups){nbGroups+=1;$("<div/>",{"class":"ms-res-group",html:grpName}).appendTo(ms.combobox);self._renderComboItems(_groups[grpName].items,true)}var _groupItemHeight=ms.combobox.find(".ms-res-group").outerHeight();if(_groupItemHeight!==null){var tmpResHeight=nbGroups*_groupItemHeight;resHeight=_comboItemHeight*data.length+tmpResHeight}else{resHeight=_comboItemHeight*(data.length+nbGroups)}}if(resHeight<ms.combobox.height()||resHeight<=cfg.maxDropHeight){ms.combobox.height(resHeight)}else if(resHeight>=ms.combobox.height()&&resHeight>cfg.maxDropHeight){ms.combobox.height(cfg.maxDropHeight)}if(data.length===1&&cfg.autoSelect===true){ms.combobox.children().filter(":not(.ms-res-item-disabled):last").addClass("ms-res-item-active")}if(cfg.selectFirst===true){ms.combobox.children().filter(":not(.ms-res-item-disabled):first").addClass("ms-res-item-active")}if(data.length===0&&ms.getRawValue()!==""){var noSuggestionText=cfg.noSuggestionText.replace(/\{\{.*\}\}/,ms.input.val());self._updateHelper(noSuggestionText);ms.collapse()}if(cfg.allowFreeEntries===false){if(data.length===0){$(ms.input).addClass(cfg.invalidCls);ms.combobox.hide()}else{$(ms.input).removeClass(cfg.invalidCls)}}},_getEntriesFromStringArray:function(data){var json=[];$.each(data,function(index,s){var entry={};entry[cfg.displayField]=entry[cfg.valueField]=$.trim(s);json.push(entry)});return json},_highlightSuggestion:function(html){var q=ms.input.val();var specialCharacters=["^","$","*","+","?",".","(",")",":","!","|","{","}","[","]"];$.each(specialCharacters,function(index,value){q=q.replace(value,"\\"+value)});if(q.length===0){return html}var glob=cfg.matchCase===true?"g":"gi";return html.replace(new RegExp("("+q+")(?!([^<]+)?>)",glob),"<em>$1</em>")},_moveSelectedRow:function(dir){if(!cfg.expanded){ms.expand()}var list,start,active,scrollPos;list=ms.combobox.find(".ms-res-item:not(.ms-res-item-disabled)");if(dir==="down"){start=list.eq(0)}else{start=list.filter(":last")}active=ms.combobox.find(".ms-res-item-active:not(.ms-res-item-disabled):first");if(active.length>0){if(dir==="down"){start=active.nextAll(".ms-res-item:not(.ms-res-item-disabled)").first();if(start.length===0){start=list.eq(0)}scrollPos=ms.combobox.scrollTop();ms.combobox.scrollTop(0);if(start[0].offsetTop+start.outerHeight()>ms.combobox.height()){ms.combobox.scrollTop(scrollPos+_comboItemHeight)}}else{start=active.prevAll(".ms-res-item:not(.ms-res-item-disabled)").first();if(start.length===0){start=list.filter(":last");ms.combobox.scrollTop(_comboItemHeight*list.length)}if(start[0].offsetTop<ms.combobox.scrollTop()){ms.combobox.scrollTop(ms.combobox.scrollTop()-_comboItemHeight)}}}list.removeClass("ms-res-item-active");start.addClass("ms-res-item-active")},_processSuggestions:function(source){var json=null,data=source||cfg.data;if(data!==null){if(typeof data==="function"){data=data.call(ms,ms.getRawValue())}if(typeof data==="string"){$(ms).trigger("beforeload",[ms]);var queryParams={};queryParams[cfg.queryParam]=ms.input.val();var params=$.extend(queryParams,cfg.dataUrlParams);$.ajax($.extend({type:cfg.method,url:data,data:params,beforeSend:cfg.beforeSend,success:function(asyncData){json=typeof asyncData==="string"?JSON.parse(asyncData):asyncData;self._processSuggestions(json);$(ms).trigger("load",[ms,json]);if(self._asyncValues){ms.setValue(typeof self._asyncValues==="string"?JSON.parse(self._asyncValues):self._asyncValues);self._renderSelection();delete self._asyncValues}},error:function(){throw"Could not reach server"}},cfg.ajaxConfig));return}else{if(data.length>0&&typeof data[0]==="string"){_cbData=self._getEntriesFromStringArray(data)}else{_cbData=data[cfg.resultsField]||data}}var sortedData=cfg.mode==="remote"?_cbData:self._sortAndTrim(_cbData);self._displaySuggestions(self._group(sortedData))}},_render:function(el){ms.setName(cfg.name);ms.container=$("<div/>",{"class":"ms-ctn form-control "+(cfg.resultAsString?"ms-as-string ":"")+cfg.cls+($(el).hasClass("input-lg")?" input-lg":"")+($(el).hasClass("input-sm")?" input-sm":"")+(cfg.disabled===true?" ms-ctn-disabled":"")+(cfg.editable===true?"":" ms-ctn-readonly")+(cfg.hideTrigger===false?"":" ms-no-trigger"),style:cfg.style,id:cfg.id});ms.container.focus($.proxy(handlers._onFocus,this));ms.container.blur($.proxy(handlers._onBlur,this));ms.container.keydown($.proxy(handlers._onKeyDown,this));ms.container.keyup($.proxy(handlers._onKeyUp,this));ms.input=$("<input/>",$.extend({type:"text","class":cfg.editable===true?"":" ms-input-readonly",readonly:!cfg.editable,placeholder:cfg.placeholder,disabled:cfg.disabled},cfg.inputCfg));ms.input.focus($.proxy(handlers._onInputFocus,this));ms.input.click($.proxy(handlers._onInputClick,this));ms.combobox=$("<div/>",{"class":"ms-res-ctn dropdown-menu"}).height(cfg.maxDropHeight);ms.combobox.on("click","div.ms-res-item",$.proxy(handlers._onComboItemSelected,this));ms.combobox.on("mouseover","div.ms-res-item",$.proxy(handlers._onComboItemMouseOver,this));if(cfg.selectionContainer){ms.selectionContainer=cfg.selectionContainer;$(ms.selectionContainer).addClass("ms-sel-ctn")}else{ms.selectionContainer=$("<div/>",{"class":"ms-sel-ctn"})}ms.selectionContainer.click($.proxy(handlers._onFocus,this));if(cfg.selectionPosition==="inner"&&!cfg.selectionContainer){ms.selectionContainer.append(ms.input)}else{ms.container.append(ms.input)}ms.helper=$("<span/>",{"class":"ms-helper "+cfg.infoMsgCls});self._updateHelper();ms.container.append(ms.helper);$(el).replaceWith(ms.container);if(!cfg.selectionContainer){switch(cfg.selectionPosition){case"bottom":ms.selectionContainer.insertAfter(ms.container);if(cfg.selectionStacked===true){ms.selectionContainer.width(ms.container.width());ms.selectionContainer.addClass("ms-stacked")}break;case"right":ms.selectionContainer.insertAfter(ms.container);ms.container.css("float","left");break;default:ms.container.append(ms.selectionContainer);break}}if(cfg.hideTrigger===false){ms.trigger=$("<div/>",{"class":"ms-trigger",html:'<div class="ms-trigger-ico"></div>'});ms.trigger.click($.proxy(handlers._onTriggerClick,this));ms.container.append(ms.trigger)}$(window).resize($.proxy(handlers._onWindowResized,this));if(cfg.value!==null||cfg.data!==null){if(typeof cfg.data==="string"){self._asyncValues=cfg.value;self._processSuggestions()}else{self._processSuggestions();if(cfg.value!==null){ms.setValue(cfg.value);self._renderSelection()}}}$("body").click(function(e){if(ms.container.hasClass("ms-ctn-focus")&&ms.container.has(e.target).length===0&&e.target.className.indexOf("ms-res-item")<0&&e.target.className.indexOf("ms-close-btn")<0&&ms.container[0]!==e.target){handlers._onBlur()}});if(cfg.expanded===true){cfg.expanded=false;ms.expand()}},_renderComboItems:function(items,isGrouped){var ref=this,html="";$.each(items,function(index,value){var displayed=cfg.renderer!==null?cfg.renderer.call(ref,value):value[cfg.displayField];var disabled=cfg.disabledField!==null&&value[cfg.disabledField]===true;var resultItemEl=$("<div/>",{"class":"ms-res-item "+(isGrouped?"ms-res-item-grouped ":"")+(disabled?"ms-res-item-disabled ":"")+(index%2===1&&cfg.useZebraStyle===true?"ms-res-odd":""),html:cfg.highlight===true?self._highlightSuggestion(displayed):displayed,"data-json":JSON.stringify(value)});html+=$("<div/>").append(resultItemEl).html()});ms.combobox.append(html);_comboItemHeight=ms.combobox.find(".ms-res-item:first").outerHeight()},_renderSelection:function(){var ref=this,w=0,inputOffset=0,items=[],asText=cfg.resultAsString===true&&!_hasFocus;ms.selectionContainer.find(".ms-sel-item").remove();if(ms._valueContainer!==undefined){ms._valueContainer.remove()}$.each(_selection,function(index,value){var selectedItemEl,delItemEl,selectedItemHtml=cfg.selectionRenderer!==null?cfg.selectionRenderer.call(ref,value):value[cfg.displayField];var validCls=self._validateSingleItem(value[cfg.displayField])?"":" ms-sel-invalid";if(asText===true){selectedItemEl=$("<div/>",{"class":"ms-sel-item ms-sel-text "+cfg.selectionCls+validCls,html:selectedItemHtml+(index===_selection.length-1?"":cfg.resultAsStringDelimiter)}).data("json",value)}else{selectedItemEl=$("<div/>",{"class":"ms-sel-item "+cfg.selectionCls+validCls,html:selectedItemHtml}).data("json",value);if(cfg.disabled===false){delItemEl=$("<span/>",{"class":"ms-close-btn"}).data("json",value).appendTo(selectedItemEl);delItemEl.click($.proxy(handlers._onTagTriggerClick,ref))}}items.push(selectedItemEl)});ms.selectionContainer.prepend(items);ms._valueContainer=$("<div/>",{style:"display: none;"});$.each(ms.getValue(),function(i,val){var el=$("<input/>",{type:"hidden",name:cfg.name,value:val});el.appendTo(ms._valueContainer)});ms._valueContainer.appendTo(ms.selectionContainer);if(cfg.selectionPosition==="inner"&&!cfg.selectionContainer){ms.input.width(0);inputOffset=ms.input.offset().left-ms.selectionContainer.offset().left;w=ms.container.width()-inputOffset-42;ms.input.width(w)}if(_selection.length===cfg.maxSelection){self._updateHelper(cfg.maxSelectionRenderer.call(this,_selection.length))}else{ms.helper.hide()}},_selectItem:function(item){if(cfg.maxSelection===1){_selection=[]}ms.addToSelection(item.data("json"));item.removeClass("ms-res-item-active");if(cfg.expandOnFocus===false||_selection.length===cfg.maxSelection){ms.collapse()}if(!_hasFocus){ms.input.focus()}else if(_hasFocus&&(cfg.expandOnFocus||_ctrlDown)){self._processSuggestions();if(_ctrlDown){ms.expand()}}},_sortAndTrim:function(data){var q=ms.getRawValue(),filtered=[],newSuggestions=[],selectedValues=ms.getValue();if(q.length>0){$.each(data,function(index,obj){var name=obj[cfg.displayField];if(cfg.matchCase===true&&name.indexOf(q)>-1||cfg.matchCase===false&&name.toLowerCase().indexOf(q.toLowerCase())>-1){if(cfg.strictSuggest===false||name.toLowerCase().indexOf(q.toLowerCase())===0){filtered.push(obj)}}})}else{filtered=data}$.each(filtered,function(index,obj){if(cfg.allowDuplicates||$.inArray(obj[cfg.valueField],selectedValues)===-1){newSuggestions.push(obj)}});if(cfg.sortOrder!==null){newSuggestions.sort(function(a,b){if(a[cfg.sortOrder]<b[cfg.sortOrder]){return cfg.sortDir==="asc"?-1:1}if(a[cfg.sortOrder]>b[cfg.sortOrder]){return cfg.sortDir==="asc"?1:-1}return 0})}if(cfg.maxSuggestions&&cfg.maxSuggestions>0){newSuggestions=newSuggestions.slice(0,cfg.maxSuggestions)}return newSuggestions},_group:function(data){if(cfg.groupBy!==null){_groups={};$.each(data,function(index,value){var props=cfg.groupBy.indexOf(".")>-1?cfg.groupBy.split("."):cfg.groupBy;var prop=value[cfg.groupBy];if(typeof props!="string"){prop=value;while(props.length>0){prop=prop[props.shift()]}}if(_groups[prop]===undefined){_groups[prop]={title:prop,items:[value]}}else{_groups[prop].items.push(value)}})}return data},_updateHelper:function(html){ms.helper.html(html);if(!ms.helper.is(":visible")){ms.helper.fadeIn()}},_validateSingleItem:function(value){if(cfg.vregex!==null&&cfg.vregex instanceof RegExp){return cfg.vregex.test(value)}else if(cfg.vtype!==null){switch(cfg.vtype){case"alpha":return/^[a-zA-Z_]+$/.test(value);case"alphanum":return/^[a-zA-Z0-9_]+$/.test(value);case"email":return/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/.test(value);case"url":return/(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i.test(value);case"ipaddress":return/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(value)}}return true}};var handlers={_onBlur:function(){ms.container.removeClass("ms-ctn-focus");ms.collapse();_hasFocus=false;if(ms.getRawValue()!==""&&cfg.allowFreeEntries===true){var obj={};obj[cfg.displayField]=obj[cfg.valueField]=ms.getRawValue().trim();ms.addToSelection(obj)}self._renderSelection();if(ms.isValid()===false){ms.container.addClass(cfg.invalidCls)}else if(ms.input.val()!==""&&cfg.allowFreeEntries===false){ms.empty();self._updateHelper("")}$(ms).trigger("blur",[ms])},_onComboItemMouseOver:function(e){var target=$(e.currentTarget);if(!target.hasClass("ms-res-item-disabled")){ms.combobox.children().removeClass("ms-res-item-active");target.addClass("ms-res-item-active")}},_onComboItemSelected:function(e){var target=$(e.currentTarget);if(!target.hasClass("ms-res-item-disabled")){self._selectItem($(e.currentTarget))}},_onFocus:function(){ms.input.focus()},_onInputClick:function(){if(ms.isDisabled()===false&&_hasFocus){if(cfg.toggleOnClick===true){if(cfg.expanded){ms.collapse()}else{ms.expand()}}}},_onInputFocus:function(){if(ms.isDisabled()===false&&!_hasFocus){_hasFocus=true;ms.container.addClass("ms-ctn-focus");ms.container.removeClass(cfg.invalidCls);var curLength=ms.getRawValue().length;if(cfg.expandOnFocus===true){ms.expand()}if(_selection.length===cfg.maxSelection){self._updateHelper(cfg.maxSelectionRenderer.call(this,_selection.length))}else if(curLength<cfg.minChars){self._updateHelper(cfg.minCharsRenderer.call(this,cfg.minChars-curLength))}self._renderSelection();$(ms).trigger("focus",[ms])}},_onKeyDown:function(e){var active=ms.combobox.find(".ms-res-item-active:not(.ms-res-item-disabled):first"),freeInput=ms.input.val();$(ms).trigger("keydown",[ms,e]);if(e.keyCode===KEYCODES.TAB&&(cfg.useTabKey===false||cfg.useTabKey===true&&active.length===0&&ms.input.val().length===0)){handlers._onBlur();return}switch(e.keyCode){case KEYCODES.BACKSPACE:if(freeInput.length===0&&ms.getSelection().length>0&&cfg.selectionPosition==="inner"){_selection.pop();self._renderSelection();$(ms).trigger("selectionchange",[ms,ms.getSelection()]);ms.input.attr("placeholder",cfg.selectionPosition==="inner"&&ms.getValue().length>0?"":cfg.placeholder);ms.input.focus();e.preventDefault()}break;case KEYCODES.TAB:case KEYCODES.ESC:e.preventDefault();break;case KEYCODES.ENTER:if(freeInput!==""||cfg.expanded){e.preventDefault()}break;case KEYCODES.COMMA:if(cfg.useCommaKey===true){e.preventDefault()}break;case KEYCODES.CTRL:_ctrlDown=true;break;case KEYCODES.DOWNARROW:e.preventDefault();self._moveSelectedRow("down");break;case KEYCODES.UPARROW:e.preventDefault();self._moveSelectedRow("up");break;default:if(_selection.length===cfg.maxSelection){e.preventDefault()}break}},_onKeyUp:function(e){var freeInput=ms.getRawValue(),inputValid=$.trim(ms.input.val()).length>0&&(!cfg.maxEntryLength||$.trim(ms.input.val()).length<=cfg.maxEntryLength),selected,obj={};$(ms).trigger("keyup",[ms,e]);clearTimeout(_timer);if(e.keyCode===KEYCODES.ESC&&cfg.expanded){ms.combobox.hide()}if(e.keyCode===KEYCODES.TAB&&cfg.useTabKey===false||e.keyCode>KEYCODES.ENTER&&e.keyCode<KEYCODES.SPACE){if(e.keyCode===KEYCODES.CTRL){_ctrlDown=false}return}switch(e.keyCode){case KEYCODES.UPARROW:case KEYCODES.DOWNARROW:e.preventDefault();break;case KEYCODES.ENTER:case KEYCODES.TAB:case KEYCODES.COMMA:if(e.keyCode!==KEYCODES.COMMA||cfg.useCommaKey===true){e.preventDefault();if(cfg.expanded===true){selected=ms.combobox.find(".ms-res-item-active:not(.ms-res-item-disabled):first");if(selected.length>0){self._selectItem(selected);return}}if(inputValid===true&&cfg.allowFreeEntries===true){obj[cfg.displayField]=obj[cfg.valueField]=freeInput.trim();ms.addToSelection(obj);ms.collapse();ms.input.focus()}break}default:if(_selection.length===cfg.maxSelection){self._updateHelper(cfg.maxSelectionRenderer.call(this,_selection.length))}else{if(freeInput.length<cfg.minChars){self._updateHelper(cfg.minCharsRenderer.call(this,cfg.minChars-freeInput.length));if(cfg.expanded===true){ms.collapse()}}else if(cfg.maxEntryLength&&freeInput.length>cfg.maxEntryLength){self._updateHelper(cfg.maxEntryRenderer.call(this,freeInput.length-cfg.maxEntryLength));if(cfg.expanded===true){ms.collapse()}}else{ms.helper.hide();if(cfg.minChars<=freeInput.length){_timer=setTimeout(function(){if(cfg.expanded===true){self._processSuggestions()}else{ms.expand()}},cfg.typeDelay)}}}break}},_onTagTriggerClick:function(e){ms.removeFromSelection($(e.currentTarget).data("json"))},_onTriggerClick:function(){if(ms.isDisabled()===false&&!(cfg.expandOnFocus===true&&_selection.length===cfg.maxSelection)){$(ms).trigger("triggerclick",[ms]);if(cfg.expanded===true){ms.collapse()}else{var curLength=ms.getRawValue().length;if(curLength>=cfg.minChars){ms.input.focus();ms.expand()}else{self._updateHelper(cfg.minCharsRenderer.call(this,cfg.minChars-curLength))}}}},_onWindowResized:function(){self._renderSelection()}};if(element!==null){self._render(element)}};$.fn.magicSuggest=function(options){var obj=$(this);if(obj.size()===1&&obj.data("magicSuggest")){return obj.data("magicSuggest")}obj.each(function(i){var cntr=$(this);if(cntr.data("magicSuggest")){return}if(this.nodeName.toLowerCase()==="select"){options.data=[];options.value=[];$.each(this.children,function(index,child){if(child.nodeName&&child.nodeName.toLowerCase()==="option"){options.data.push({id:child.value,name:child.text});if($(child).attr("selected")){options.value.push(child.value)}}})}var def={};$.each(this.attributes,function(i,att){def[att.name]=att.name==="value"&&att.value!==""?JSON.parse(att.value):att.value});var field=new MagicSuggest(this,$.extend([],$.fn.magicSuggest.defaults,options,def));cntr.data("magicSuggest",field);field.container.data("magicSuggest",field)});if(obj.size()===1){return obj.data("magicSuggest")}return obj};$.fn.magicSuggest.defaults={}})(jQuery);
@@ -0,0 +1,1565 @@
1
+ /**
2
+ * Multiple Selection Component for Bootstrap
3
+ * Check nicolasbize.github.io/magicsuggest/ for latest updates.
4
+ *
5
+ * Author: Nicolas Bize
6
+ * Created: Feb 8th 2013
7
+ * Last Updated: Oct 16th 2014
8
+ * Version: 2.1.4
9
+ * Licence: MagicSuggest is licenced under MIT licence (http://opensource.org/licenses/MIT)
10
+ */
11
+ (function($)
12
+ {
13
+ "use strict";
14
+ var MagicSuggest = function(element, options)
15
+ {
16
+ var ms = this;
17
+
18
+ /**
19
+ * Initializes the MagicSuggest component
20
+ */
21
+ var defaults = {
22
+ /********** CONFIGURATION PROPERTIES ************/
23
+ /**
24
+ * Restricts or allows the user to validate typed entries.
25
+ * Defaults to true.
26
+ */
27
+ allowFreeEntries: true,
28
+
29
+ /**
30
+ * Restricts or allows the user to add the same entry more than once
31
+ * Defaults to false.
32
+ */
33
+ allowDuplicates: false,
34
+
35
+ /**
36
+ * Additional config object passed to each $.ajax call
37
+ */
38
+ ajaxConfig: {},
39
+
40
+ /**
41
+ * If a single suggestion comes out, it is preselected.
42
+ */
43
+ autoSelect: true,
44
+
45
+ /**
46
+ * Auto select the first matching item with multiple items shown
47
+ */
48
+ selectFirst: false,
49
+
50
+ /**
51
+ * Allow customization of query parameter
52
+ */
53
+ queryParam: 'query',
54
+
55
+ /**
56
+ * A function triggered just before the ajax request is sent, similar to jQuery
57
+ */
58
+ beforeSend: function(){ },
59
+
60
+ /**
61
+ * A custom CSS class to apply to the field's underlying element.
62
+ */
63
+ cls: '',
64
+
65
+ /**
66
+ * JSON Data source used to populate the combo box. 3 options are available here:
67
+ * No Data Source (default)
68
+ * When left null, the combo box will not suggest anything. It can still enable the user to enter
69
+ * multiple entries if allowFreeEntries is * set to true (default).
70
+ * Static Source
71
+ * You can pass an array of JSON objects, an array of strings or even a single CSV string as the
72
+ * data source.For ex. data: [* {id:0,name:"Paris"}, {id: 1, name: "New York"}]
73
+ * You can also pass any json object with the results property containing the json array.
74
+ * Url
75
+ * You can pass the url from which the component will fetch its JSON data.Data will be fetched
76
+ * using a POST ajax request that will * include the entered text as 'query' parameter. The results
77
+ * fetched from the server can be:
78
+ * - an array of JSON objects (ex: [{id:...,name:...},{...}])
79
+ * - a string containing an array of JSON objects ready to be parsed (ex: "[{id:...,name:...},{...}]")
80
+ * - a JSON object whose data will be contained in the results property
81
+ * (ex: {results: [{id:...,name:...},{...}]
82
+ * Function
83
+ * You can pass a function which returns an array of JSON objects (ex: [{id:...,name:...},{...}])
84
+ * The function can return the JSON data or it can use the first argument as function to handle the data.
85
+ * Only one (callback function or return value) is needed for the function to succeed.
86
+ * See the following example:
87
+ * function (response) { var myjson = [{name: 'test', id: 1}]; response(myjson); return myjson; }
88
+ */
89
+ data: null,
90
+
91
+ /**
92
+ * Additional parameters to the ajax call
93
+ */
94
+ dataUrlParams: {},
95
+
96
+ /**
97
+ * Start the component in a disabled state.
98
+ */
99
+ disabled: false,
100
+
101
+ /**
102
+ * Name of JSON object property that defines the disabled behaviour
103
+ */
104
+ disabledField: null,
105
+
106
+ /**
107
+ * Name of JSON object property displayed in the combo list
108
+ */
109
+ displayField: 'name',
110
+
111
+ /**
112
+ * Set to false if you only want mouse interaction. In that case the combo will
113
+ * automatically expand on focus.
114
+ */
115
+ editable: true,
116
+
117
+ /**
118
+ * Set starting state for combo.
119
+ */
120
+ expanded: false,
121
+
122
+ /**
123
+ * Automatically expands combo on focus.
124
+ */
125
+ expandOnFocus: false,
126
+
127
+ /**
128
+ * JSON property by which the list should be grouped
129
+ */
130
+ groupBy: null,
131
+
132
+ /**
133
+ * Set to true to hide the trigger on the right
134
+ */
135
+ hideTrigger: false,
136
+
137
+ /**
138
+ * Set to true to highlight search input within displayed suggestions
139
+ */
140
+ highlight: true,
141
+
142
+ /**
143
+ * A custom ID for this component
144
+ */
145
+ id: null,
146
+
147
+ /**
148
+ * A class that is added to the info message appearing on the top-right part of the component
149
+ */
150
+ infoMsgCls: '',
151
+
152
+ /**
153
+ * Additional parameters passed out to the INPUT tag. Enables usage of AngularJS's custom tags for ex.
154
+ */
155
+ inputCfg: {},
156
+
157
+ /**
158
+ * The class that is applied to show that the field is invalid
159
+ */
160
+ invalidCls: 'ms-inv',
161
+
162
+ /**
163
+ * Set to true to filter data results according to case. Useless if the data is fetched remotely
164
+ */
165
+ matchCase: false,
166
+
167
+ /**
168
+ * Once expanded, the combo's height will take as much room as the # of available results.
169
+ * In case there are too many results displayed, this will fix the drop down height.
170
+ */
171
+ maxDropHeight: 290,
172
+
173
+ /**
174
+ * Defines how long the user free entry can be. Set to null for no limit.
175
+ */
176
+ maxEntryLength: null,
177
+
178
+ /**
179
+ * A function that defines the helper text when the max entry length has been surpassed.
180
+ */
181
+ maxEntryRenderer: function(v) {
182
+ return 'Please reduce your entry by ' + v + ' character' + (v > 1 ? 's':'');
183
+ },
184
+
185
+ /**
186
+ * The maximum number of results displayed in the combo drop down at once.
187
+ */
188
+ maxSuggestions: null,
189
+
190
+ /**
191
+ * The maximum number of items the user can select if multiple selection is allowed.
192
+ * Set to null to remove the limit.
193
+ */
194
+ maxSelection: 10,
195
+
196
+ /**
197
+ * A function that defines the helper text when the max selection amount has been reached. The function has a single
198
+ * parameter which is the number of selected elements.
199
+ */
200
+ maxSelectionRenderer: function(v) {
201
+ return 'You cannot choose more than ' + v + ' item' + (v > 1 ? 's':'');
202
+ },
203
+
204
+ /**
205
+ * The method used by the ajax request.
206
+ */
207
+ method: 'POST',
208
+
209
+ /**
210
+ * The minimum number of characters the user must type before the combo expands and offers suggestions.
211
+ */
212
+ minChars: 0,
213
+
214
+ /**
215
+ * A function that defines the helper text when not enough letters are set. The function has a single
216
+ * parameter which is the difference between the required amount of letters and the current one.
217
+ */
218
+ minCharsRenderer: function(v) {
219
+ return 'Please type ' + v + ' more character' + (v > 1 ? 's':'');
220
+ },
221
+
222
+ /**
223
+ * Whether or not sorting / filtering should be done remotely or locally.
224
+ * Use either 'local' or 'remote'
225
+ */
226
+ mode: 'local',
227
+
228
+ /**
229
+ * The name used as a form element.
230
+ */
231
+ name: null,
232
+
233
+ /**
234
+ * The text displayed when there are no suggestions.
235
+ */
236
+ noSuggestionText: 'No suggestions',
237
+
238
+ /**
239
+ * The default placeholder text when nothing has been entered
240
+ */
241
+ placeholder: 'Type or click here',
242
+
243
+ /**
244
+ * A function used to define how the items will be presented in the combo
245
+ */
246
+ renderer: null,
247
+
248
+ /**
249
+ * Whether or not this field should be required
250
+ */
251
+ required: false,
252
+
253
+ /**
254
+ * Set to true to render selection as a delimited string
255
+ */
256
+ resultAsString: false,
257
+
258
+ /**
259
+ * Text delimiter to use in a delimited string.
260
+ */
261
+ resultAsStringDelimiter: ',',
262
+
263
+ /**
264
+ * Name of JSON object property that represents the list of suggested objects
265
+ */
266
+ resultsField: 'results',
267
+
268
+ /**
269
+ * A custom CSS class to add to a selected item
270
+ */
271
+ selectionCls: '',
272
+
273
+ /**
274
+ * An optional element replacement in which the selection is rendered
275
+ */
276
+ selectionContainer: null,
277
+
278
+ /**
279
+ * Where the selected items will be displayed. Only 'right', 'bottom' and 'inner' are valid values
280
+ */
281
+ selectionPosition: 'inner',
282
+
283
+ /**
284
+ * A function used to define how the items will be presented in the tag list
285
+ */
286
+ selectionRenderer: null,
287
+
288
+ /**
289
+ * Set to true to stack the selectioned items when positioned on the bottom
290
+ * Requires the selectionPosition to be set to 'bottom'
291
+ */
292
+ selectionStacked: false,
293
+
294
+ /**
295
+ * Direction used for sorting. Only 'asc' and 'desc' are valid values
296
+ */
297
+ sortDir: 'asc',
298
+
299
+ /**
300
+ * name of JSON object property for local result sorting.
301
+ * Leave null if you do not wish the results to be ordered or if they are already ordered remotely.
302
+ */
303
+ sortOrder: null,
304
+
305
+ /**
306
+ * If set to true, suggestions will have to start by user input (and not simply contain it as a substring)
307
+ */
308
+ strictSuggest: false,
309
+
310
+ /**
311
+ * Custom style added to the component container.
312
+ */
313
+ style: '',
314
+
315
+ /**
316
+ * If set to true, the combo will expand / collapse when clicked upon
317
+ */
318
+ toggleOnClick: false,
319
+
320
+
321
+ /**
322
+ * Amount (in ms) between keyboard registers.
323
+ */
324
+ typeDelay: 400,
325
+
326
+ /**
327
+ * If set to true, tab won't blur the component but will be registered as the ENTER key
328
+ */
329
+ useTabKey: false,
330
+
331
+ /**
332
+ * If set to true, using comma will validate the user's choice
333
+ */
334
+ useCommaKey: true,
335
+
336
+
337
+ /**
338
+ * Determines whether or not the results will be displayed with a zebra table style
339
+ */
340
+ useZebraStyle: false,
341
+
342
+ /**
343
+ * initial value for the field
344
+ */
345
+ value: null,
346
+
347
+ /**
348
+ * name of JSON object property that represents its underlying value
349
+ */
350
+ valueField: 'id',
351
+
352
+ /**
353
+ * regular expression to validate the values against
354
+ */
355
+ vregex: null,
356
+
357
+ /**
358
+ * type to validate against
359
+ */
360
+ vtype: null
361
+ };
362
+
363
+ var conf = $.extend({},options);
364
+ var cfg = $.extend(true, {}, defaults, conf);
365
+
366
+ /********** PUBLIC METHODS ************/
367
+ /**
368
+ * Add one or multiple json items to the current selection
369
+ * @param items - json object or array of json objects
370
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
371
+ */
372
+ this.addToSelection = function(items, isSilent)
373
+ {
374
+ if (!cfg.maxSelection || _selection.length < cfg.maxSelection) {
375
+ if (!$.isArray(items)) {
376
+ items = [items];
377
+ }
378
+ var valuechanged = false;
379
+ $.each(items, function(index, json) {
380
+ if (cfg.allowDuplicates || $.inArray(json[cfg.valueField], ms.getValue()) === -1) {
381
+ _selection.push(json);
382
+ valuechanged = true;
383
+ }
384
+ });
385
+ if(valuechanged === true) {
386
+ self._renderSelection();
387
+ this.empty();
388
+ if (isSilent !== true) {
389
+ $(this).trigger('selectionchange', [this, this.getSelection()]);
390
+ }
391
+ }
392
+ }
393
+ this.input.attr('placeholder', (cfg.selectionPosition === 'inner' && this.getValue().length > 0) ? '' : cfg.placeholder);
394
+ };
395
+
396
+ /**
397
+ * Clears the current selection
398
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
399
+ */
400
+ this.clear = function(isSilent)
401
+ {
402
+ this.removeFromSelection(_selection.slice(0), isSilent); // clone array to avoid concurrency issues
403
+ };
404
+
405
+ /**
406
+ * Collapse the drop down part of the combo
407
+ */
408
+ this.collapse = function()
409
+ {
410
+ if (cfg.expanded === true) {
411
+ this.combobox.detach();
412
+ cfg.expanded = false;
413
+ $(this).trigger('collapse', [this]);
414
+ }
415
+ };
416
+
417
+ /**
418
+ * Set the component in a disabled state.
419
+ */
420
+ this.disable = function()
421
+ {
422
+ this.container.addClass('ms-ctn-disabled');
423
+ cfg.disabled = true;
424
+ ms.input.attr('disabled', true);
425
+ };
426
+
427
+ /**
428
+ * Empties out the combo user text
429
+ */
430
+ this.empty = function(){
431
+ this.input.val('');
432
+ };
433
+
434
+ /**
435
+ * Set the component in a enable state.
436
+ */
437
+ this.enable = function()
438
+ {
439
+ this.container.removeClass('ms-ctn-disabled');
440
+ cfg.disabled = false;
441
+ ms.input.attr('disabled', false);
442
+ };
443
+
444
+ /**
445
+ * Expand the drop drown part of the combo.
446
+ */
447
+ this.expand = function()
448
+ {
449
+ if (!cfg.expanded && (this.input.val().length >= cfg.minChars || this.combobox.children().size() > 0)) {
450
+ this.combobox.appendTo(this.container);
451
+ self._processSuggestions();
452
+ cfg.expanded = true;
453
+ $(this).trigger('expand', [this]);
454
+ }
455
+ };
456
+
457
+ /**
458
+ * Retrieve component enabled status
459
+ */
460
+ this.isDisabled = function()
461
+ {
462
+ return cfg.disabled;
463
+ };
464
+
465
+ /**
466
+ * Checks whether the field is valid or not
467
+ * @return {boolean}
468
+ */
469
+ this.isValid = function()
470
+ {
471
+ var valid = cfg.required === false || _selection.length > 0;
472
+ if(cfg.vtype || cfg.vregex){
473
+ $.each(_selection, function(index, item){
474
+ valid = valid && self._validateSingleItem(item[cfg.valueField]);
475
+ });
476
+ }
477
+ return valid;
478
+ };
479
+
480
+ /**
481
+ * Gets the data params for current ajax request
482
+ */
483
+ this.getDataUrlParams = function()
484
+ {
485
+ return cfg.dataUrlParams;
486
+ };
487
+
488
+ /**
489
+ * Gets the name given to the form input
490
+ */
491
+ this.getName = function()
492
+ {
493
+ return cfg.name;
494
+ };
495
+
496
+ /**
497
+ * Retrieve an array of selected json objects
498
+ * @return {Array}
499
+ */
500
+ this.getSelection = function()
501
+ {
502
+ return _selection;
503
+ };
504
+
505
+ /**
506
+ * Retrieve the current text entered by the user
507
+ */
508
+ this.getRawValue = function(){
509
+ return ms.input.val();
510
+ };
511
+
512
+ /**
513
+ * Retrieve an array of selected values
514
+ */
515
+ this.getValue = function()
516
+ {
517
+ return $.map(_selection, function(o) {
518
+ return o[cfg.valueField];
519
+ });
520
+ };
521
+
522
+ /**
523
+ * Remove one or multiples json items from the current selection
524
+ * @param items - json object or array of json objects
525
+ * @param isSilent - (optional) set to true to suppress 'selectionchange' event from being triggered
526
+ */
527
+ this.removeFromSelection = function(items, isSilent)
528
+ {
529
+ if (!$.isArray(items)) {
530
+ items = [items];
531
+ }
532
+ var valuechanged = false;
533
+ $.each(items, function(index, json) {
534
+ var i = $.inArray(json[cfg.valueField], ms.getValue());
535
+ if (i > -1) {
536
+ _selection.splice(i, 1);
537
+ valuechanged = true;
538
+ }
539
+ });
540
+ if (valuechanged === true) {
541
+ self._renderSelection();
542
+ if(isSilent !== true){
543
+ $(this).trigger('selectionchange', [this, this.getSelection()]);
544
+ }
545
+ if(cfg.expandOnFocus){
546
+ ms.expand();
547
+ }
548
+ if(cfg.expanded) {
549
+ self._processSuggestions();
550
+ }
551
+ }
552
+ this.input.attr('placeholder', (cfg.selectionPosition === 'inner' && this.getValue().length > 0) ? '' : cfg.placeholder);
553
+ };
554
+
555
+ /**
556
+ * Get current data
557
+ */
558
+ this.getData = function(){
559
+ return _cbData;
560
+ };
561
+
562
+ /**
563
+ * Set up some combo data after it has been rendered
564
+ * @param data
565
+ */
566
+ this.setData = function(data){
567
+ cfg.data = data;
568
+ self._processSuggestions();
569
+ };
570
+
571
+ /**
572
+ * Sets the name for the input field so it can be fetched in the form
573
+ * @param name
574
+ */
575
+ this.setName = function(name){
576
+ cfg.name = name;
577
+ if(name){
578
+ cfg.name += name.indexOf('[]') > 0 ? '' : '[]';
579
+ }
580
+ if(ms._valueContainer){
581
+ $.each(ms._valueContainer.children(), function(i, el){
582
+ el.name = cfg.name;
583
+ });
584
+ }
585
+ };
586
+
587
+ /**
588
+ * Sets the current selection with the JSON items provided
589
+ * @param items
590
+ */
591
+ this.setSelection = function(items){
592
+ this.clear();
593
+ this.addToSelection(items);
594
+ };
595
+
596
+ /**
597
+ * Sets a value for the combo box. Value must be an array of values with data type matching valueField one.
598
+ * @param data
599
+ */
600
+ this.setValue = function(values)
601
+ {
602
+ var items = [];
603
+
604
+ $.each(values, function(index, value) {
605
+ // first try to see if we have the full objects from our data set
606
+ var found = false;
607
+ $.each(_cbData, function(i,item){
608
+ if(item[cfg.valueField] == value){
609
+ items.push(item);
610
+ found = true;
611
+ return false;
612
+ }
613
+ });
614
+ if(!found){
615
+ if(typeof(value) === 'object'){
616
+ items.push(value);
617
+ } else {
618
+ var json = {};
619
+ json[cfg.valueField] = value;
620
+ json[cfg.displayField] = value;
621
+ items.push(json);
622
+ }
623
+ }
624
+ });
625
+ if(items.length > 0) {
626
+ this.addToSelection(items);
627
+ }
628
+ };
629
+
630
+ /**
631
+ * Sets data params for subsequent ajax requests
632
+ * @param params
633
+ */
634
+ this.setDataUrlParams = function(params)
635
+ {
636
+ cfg.dataUrlParams = $.extend({},params);
637
+ };
638
+
639
+ /********** PRIVATE ************/
640
+ var _selection = [], // selected objects
641
+ _comboItemHeight = 0, // height for each combo item.
642
+ _timer,
643
+ _hasFocus = false,
644
+ _groups = null,
645
+ _cbData = [],
646
+ _ctrlDown = false,
647
+ KEYCODES = {
648
+ BACKSPACE: 8,
649
+ TAB: 9,
650
+ ENTER: 13,
651
+ CTRL: 17,
652
+ ESC: 27,
653
+ SPACE: 32,
654
+ UPARROW: 38,
655
+ DOWNARROW: 40,
656
+ COMMA: 188
657
+ };
658
+
659
+ var self = {
660
+
661
+ /**
662
+ * Empties the result container and refills it with the array of json results in input
663
+ * @private
664
+ */
665
+ _displaySuggestions: function(data) {
666
+ ms.combobox.show();
667
+ ms.combobox.empty();
668
+
669
+ var resHeight = 0, // total height taken by displayed results.
670
+ nbGroups = 0;
671
+
672
+ if(_groups === null) {
673
+ self._renderComboItems(data);
674
+ resHeight = _comboItemHeight * data.length;
675
+ }
676
+ else {
677
+ for(var grpName in _groups) {
678
+ nbGroups += 1;
679
+ $('<div/>', {
680
+ 'class': 'ms-res-group',
681
+ html: grpName
682
+ }).appendTo(ms.combobox);
683
+ self._renderComboItems(_groups[grpName].items, true);
684
+ }
685
+ var _groupItemHeight = ms.combobox.find('.ms-res-group').outerHeight();
686
+ if(_groupItemHeight !== null) {
687
+ var tmpResHeight = nbGroups * _groupItemHeight;
688
+ resHeight = (_comboItemHeight * data.length) + tmpResHeight;
689
+ } else {
690
+ resHeight = _comboItemHeight * (data.length + nbGroups);
691
+ }
692
+ }
693
+
694
+ if(resHeight < ms.combobox.height() || resHeight <= cfg.maxDropHeight) {
695
+ ms.combobox.height(resHeight);
696
+ }
697
+ else if(resHeight >= ms.combobox.height() && resHeight > cfg.maxDropHeight) {
698
+ ms.combobox.height(cfg.maxDropHeight);
699
+ }
700
+
701
+ if(data.length === 1 && cfg.autoSelect === true) {
702
+ ms.combobox.children().filter(':not(.ms-res-item-disabled):last').addClass('ms-res-item-active');
703
+ }
704
+
705
+ if (cfg.selectFirst === true) {
706
+ ms.combobox.children().filter(':not(.ms-res-item-disabled):first').addClass('ms-res-item-active');
707
+ }
708
+
709
+ if(data.length === 0 && ms.getRawValue() !== "") {
710
+ var noSuggestionText = cfg.noSuggestionText.replace(/\{\{.*\}\}/, ms.input.val());
711
+ self._updateHelper(noSuggestionText);
712
+ ms.collapse();
713
+ }
714
+
715
+ // When free entry is off, add invalid class to input if no data matches
716
+ if(cfg.allowFreeEntries === false) {
717
+ if(data.length === 0) {
718
+ $(ms.input).addClass(cfg.invalidCls);
719
+ ms.combobox.hide();
720
+ } else {
721
+ $(ms.input).removeClass(cfg.invalidCls);
722
+ }
723
+ }
724
+ },
725
+
726
+ /**
727
+ * Returns an array of json objects from an array of strings.
728
+ * @private
729
+ */
730
+ _getEntriesFromStringArray: function(data) {
731
+ var json = [];
732
+ $.each(data, function(index, s) {
733
+ var entry = {};
734
+ entry[cfg.displayField] = entry[cfg.valueField] = $.trim(s);
735
+ json.push(entry);
736
+ });
737
+ return json;
738
+ },
739
+
740
+ /**
741
+ * Replaces html with highlighted html according to case
742
+ * @param html
743
+ * @private
744
+ */
745
+ _highlightSuggestion: function(html) {
746
+ var q = ms.input.val();
747
+
748
+ //escape special regex characters
749
+ var specialCharacters = ['^', '$', '*', '+', '?', '.', '(', ')', ':', '!', '|', '{', '}', '[', ']'];
750
+
751
+ $.each(specialCharacters, function (index, value) {
752
+ q = q.replace(value, "\\" + value);
753
+ })
754
+
755
+ if(q.length === 0) {
756
+ return html; // nothing entered as input
757
+ }
758
+
759
+ var glob = cfg.matchCase === true ? 'g' : 'gi';
760
+ return html.replace(new RegExp('(' + q + ')(?!([^<]+)?>)', glob), '<em>$1</em>');
761
+ },
762
+
763
+ /**
764
+ * Moves the selected cursor amongst the list item
765
+ * @param dir - 'up' or 'down'
766
+ * @private
767
+ */
768
+ _moveSelectedRow: function(dir) {
769
+ if(!cfg.expanded) {
770
+ ms.expand();
771
+ }
772
+ var list, start, active, scrollPos;
773
+ list = ms.combobox.find(".ms-res-item:not(.ms-res-item-disabled)");
774
+ if(dir === 'down') {
775
+ start = list.eq(0);
776
+ }
777
+ else {
778
+ start = list.filter(':last');
779
+ }
780
+ active = ms.combobox.find('.ms-res-item-active:not(.ms-res-item-disabled):first');
781
+ if(active.length > 0) {
782
+ if(dir === 'down') {
783
+ start = active.nextAll('.ms-res-item:not(.ms-res-item-disabled)').first();
784
+ if(start.length === 0) {
785
+ start = list.eq(0);
786
+ }
787
+ scrollPos = ms.combobox.scrollTop();
788
+ ms.combobox.scrollTop(0);
789
+ if(start[0].offsetTop + start.outerHeight() > ms.combobox.height()) {
790
+ ms.combobox.scrollTop(scrollPos + _comboItemHeight);
791
+ }
792
+ }
793
+ else {
794
+ start = active.prevAll('.ms-res-item:not(.ms-res-item-disabled)').first();
795
+ if(start.length === 0) {
796
+ start = list.filter(':last');
797
+ ms.combobox.scrollTop(_comboItemHeight * list.length);
798
+ }
799
+ if(start[0].offsetTop < ms.combobox.scrollTop()) {
800
+ ms.combobox.scrollTop(ms.combobox.scrollTop() - _comboItemHeight);
801
+ }
802
+ }
803
+ }
804
+ list.removeClass("ms-res-item-active");
805
+ start.addClass("ms-res-item-active");
806
+ },
807
+
808
+ /**
809
+ * According to given data and query, sort and add suggestions in their container
810
+ * @private
811
+ */
812
+ _processSuggestions: function(source) {
813
+ var json = null, data = source || cfg.data;
814
+ if(data !== null) {
815
+ if(typeof(data) === 'function'){
816
+ data = data.call(ms, ms.getRawValue());
817
+ }
818
+ if(typeof(data) === 'string') { // get results from ajax
819
+ $(ms).trigger('beforeload', [ms]);
820
+ var queryParams = {}
821
+ queryParams[cfg.queryParam] = ms.input.val();
822
+ var params = $.extend(queryParams, cfg.dataUrlParams);
823
+ $.ajax($.extend({
824
+ type: cfg.method,
825
+ url: data,
826
+ data: params,
827
+ beforeSend: cfg.beforeSend,
828
+ success: function(asyncData){
829
+ json = typeof(asyncData) === 'string' ? JSON.parse(asyncData) : asyncData;
830
+ self._processSuggestions(json);
831
+ $(ms).trigger('load', [ms, json]);
832
+ if(self._asyncValues){
833
+ ms.setValue(typeof(self._asyncValues) === 'string' ? JSON.parse(self._asyncValues) : self._asyncValues);
834
+ self._renderSelection();
835
+ delete(self._asyncValues);
836
+ }
837
+ },
838
+ error: function(){
839
+ throw("Could not reach server");
840
+ }
841
+ }, cfg.ajaxConfig));
842
+ return;
843
+ } else { // results from local array
844
+ if(data.length > 0 && typeof(data[0]) === 'string') { // results from array of strings
845
+ _cbData = self._getEntriesFromStringArray(data);
846
+ } else { // regular json array or json object with results property
847
+ _cbData = data[cfg.resultsField] || data;
848
+ }
849
+ }
850
+ var sortedData = cfg.mode === 'remote' ? _cbData : self._sortAndTrim(_cbData);
851
+ self._displaySuggestions(self._group(sortedData));
852
+
853
+ }
854
+ },
855
+
856
+ /**
857
+ * Render the component to the given input DOM element
858
+ * @private
859
+ */
860
+ _render: function(el) {
861
+ ms.setName(cfg.name); // make sure the form name is correct
862
+ // holds the main div, will relay the focus events to the contained input element.
863
+ ms.container = $('<div/>', {
864
+ 'class': 'ms-ctn form-control ' + (cfg.resultAsString ? 'ms-as-string ' : '') + cfg.cls +
865
+ ($(el).hasClass('input-lg') ? ' input-lg' : '') +
866
+ ($(el).hasClass('input-sm') ? ' input-sm' : '') +
867
+ (cfg.disabled === true ? ' ms-ctn-disabled' : '') +
868
+ (cfg.editable === true ? '' : ' ms-ctn-readonly') +
869
+ (cfg.hideTrigger === false ? '' : ' ms-no-trigger'),
870
+ style: cfg.style,
871
+ id: cfg.id
872
+ });
873
+ ms.container.focus($.proxy(handlers._onFocus, this));
874
+ ms.container.blur($.proxy(handlers._onBlur, this));
875
+ ms.container.keydown($.proxy(handlers._onKeyDown, this));
876
+ ms.container.keyup($.proxy(handlers._onKeyUp, this));
877
+
878
+ // holds the input field
879
+ ms.input = $('<input/>', $.extend({
880
+ type: 'text',
881
+ 'class': cfg.editable === true ? '' : ' ms-input-readonly',
882
+ readonly: !cfg.editable,
883
+ placeholder: cfg.placeholder,
884
+ disabled: cfg.disabled
885
+ }, cfg.inputCfg));
886
+
887
+ ms.input.focus($.proxy(handlers._onInputFocus, this));
888
+ ms.input.click($.proxy(handlers._onInputClick, this));
889
+
890
+ // holds the suggestions. will always be placed on focus
891
+ ms.combobox = $('<div/>', {
892
+ 'class': 'ms-res-ctn dropdown-menu'
893
+ }).height(cfg.maxDropHeight);
894
+
895
+ // bind the onclick and mouseover using delegated events (needs jQuery >= 1.7)
896
+ ms.combobox.on('click', 'div.ms-res-item', $.proxy(handlers._onComboItemSelected, this));
897
+ ms.combobox.on('mouseover', 'div.ms-res-item', $.proxy(handlers._onComboItemMouseOver, this));
898
+
899
+ if(cfg.selectionContainer){
900
+ ms.selectionContainer = cfg.selectionContainer;
901
+ $(ms.selectionContainer).addClass('ms-sel-ctn');
902
+ } else {
903
+ ms.selectionContainer = $('<div/>', {
904
+ 'class': 'ms-sel-ctn'
905
+ });
906
+ }
907
+ ms.selectionContainer.click($.proxy(handlers._onFocus, this));
908
+
909
+ if(cfg.selectionPosition === 'inner' && !cfg.selectionContainer) {
910
+ ms.selectionContainer.append(ms.input);
911
+ }
912
+ else {
913
+ ms.container.append(ms.input);
914
+ }
915
+
916
+ ms.helper = $('<span/>', {
917
+ 'class': 'ms-helper ' + cfg.infoMsgCls
918
+ });
919
+ self._updateHelper();
920
+ ms.container.append(ms.helper);
921
+
922
+
923
+ // Render the whole thing
924
+ $(el).replaceWith(ms.container);
925
+
926
+ if(!cfg.selectionContainer){
927
+ switch(cfg.selectionPosition) {
928
+ case 'bottom':
929
+ ms.selectionContainer.insertAfter(ms.container);
930
+ if(cfg.selectionStacked === true) {
931
+ ms.selectionContainer.width(ms.container.width());
932
+ ms.selectionContainer.addClass('ms-stacked');
933
+ }
934
+ break;
935
+ case 'right':
936
+ ms.selectionContainer.insertAfter(ms.container);
937
+ ms.container.css('float', 'left');
938
+ break;
939
+ default:
940
+ ms.container.append(ms.selectionContainer);
941
+ break;
942
+ }
943
+ }
944
+
945
+
946
+ // holds the trigger on the right side
947
+ if(cfg.hideTrigger === false) {
948
+ ms.trigger = $('<div/>', {
949
+ 'class': 'ms-trigger',
950
+ html: '<div class="ms-trigger-ico"></div>'
951
+ });
952
+ ms.trigger.click($.proxy(handlers._onTriggerClick, this));
953
+ ms.container.append(ms.trigger);
954
+ }
955
+
956
+ $(window).resize($.proxy(handlers._onWindowResized, this));
957
+
958
+ // do not perform an initial call if we are using ajax unless we have initial values
959
+ if(cfg.value !== null || cfg.data !== null){
960
+ if(typeof(cfg.data) === 'string'){
961
+ self._asyncValues = cfg.value;
962
+ self._processSuggestions();
963
+ } else {
964
+ self._processSuggestions();
965
+ if(cfg.value !== null){
966
+ ms.setValue(cfg.value);
967
+ self._renderSelection();
968
+ }
969
+ }
970
+
971
+ }
972
+
973
+ $("body").click(function(e) {
974
+ if(ms.container.hasClass('ms-ctn-focus') &&
975
+ ms.container.has(e.target).length === 0 &&
976
+ e.target.className.indexOf('ms-res-item') < 0 &&
977
+ e.target.className.indexOf('ms-close-btn') < 0 &&
978
+ ms.container[0] !== e.target) {
979
+ handlers._onBlur();
980
+ }
981
+ });
982
+
983
+ if(cfg.expanded === true) {
984
+ cfg.expanded = false;
985
+ ms.expand();
986
+ }
987
+ },
988
+
989
+ /**
990
+ * Renders each element within the combo box
991
+ * @private
992
+ */
993
+ _renderComboItems: function(items, isGrouped) {
994
+ var ref = this, html = '';
995
+ $.each(items, function(index, value) {
996
+ var displayed = cfg.renderer !== null ? cfg.renderer.call(ref, value) : value[cfg.displayField];
997
+ var disabled = cfg.disabledField !== null && value[cfg.disabledField] === true;
998
+ var resultItemEl = $('<div/>', {
999
+ 'class': 'ms-res-item ' + (isGrouped ? 'ms-res-item-grouped ':'') +
1000
+ (disabled ? 'ms-res-item-disabled ':'') +
1001
+ (index % 2 === 1 && cfg.useZebraStyle === true ? 'ms-res-odd' : ''),
1002
+ html: cfg.highlight === true ? self._highlightSuggestion(displayed) : displayed,
1003
+ 'data-json': JSON.stringify(value)
1004
+ });
1005
+ html += $('<div/>').append(resultItemEl).html();
1006
+ });
1007
+ ms.combobox.append(html);
1008
+ _comboItemHeight = ms.combobox.find('.ms-res-item:first').outerHeight();
1009
+ },
1010
+
1011
+ /**
1012
+ * Renders the selected items into their container.
1013
+ * @private
1014
+ */
1015
+ _renderSelection: function() {
1016
+ var ref = this, w = 0, inputOffset = 0, items = [],
1017
+ asText = cfg.resultAsString === true && !_hasFocus;
1018
+
1019
+ ms.selectionContainer.find('.ms-sel-item').remove();
1020
+ if(ms._valueContainer !== undefined) {
1021
+ ms._valueContainer.remove();
1022
+ }
1023
+
1024
+ $.each(_selection, function(index, value){
1025
+
1026
+ var selectedItemEl, delItemEl,
1027
+ selectedItemHtml = cfg.selectionRenderer !== null ? cfg.selectionRenderer.call(ref, value) : value[cfg.displayField];
1028
+
1029
+ var validCls = self._validateSingleItem(value[cfg.displayField]) ? '' : ' ms-sel-invalid';
1030
+
1031
+ // tag representing selected value
1032
+ if(asText === true) {
1033
+ selectedItemEl = $('<div/>', {
1034
+ 'class': 'ms-sel-item ms-sel-text ' + cfg.selectionCls + validCls,
1035
+ html: selectedItemHtml + (index === (_selection.length - 1) ? '' : cfg.resultAsStringDelimiter)
1036
+ }).data('json', value);
1037
+ }
1038
+ else {
1039
+ selectedItemEl = $('<div/>', {
1040
+ 'class': 'ms-sel-item ' + cfg.selectionCls + validCls,
1041
+ html: selectedItemHtml
1042
+ }).data('json', value);
1043
+
1044
+ if(cfg.disabled === false){
1045
+ // small cross img
1046
+ delItemEl = $('<span/>', {
1047
+ 'class': 'ms-close-btn'
1048
+ }).data('json', value).appendTo(selectedItemEl);
1049
+
1050
+ delItemEl.click($.proxy(handlers._onTagTriggerClick, ref));
1051
+ }
1052
+ }
1053
+
1054
+ items.push(selectedItemEl);
1055
+ });
1056
+ ms.selectionContainer.prepend(items);
1057
+
1058
+ // store the values, behaviour of multiple select
1059
+ ms._valueContainer = $('<div/>', {
1060
+ style: 'display: none;'
1061
+ });
1062
+ $.each(ms.getValue(), function(i, val){
1063
+ var el = $('<input/>', {
1064
+ type: 'hidden',
1065
+ name: cfg.name,
1066
+ value: val
1067
+ });
1068
+ el.appendTo(ms._valueContainer);
1069
+ });
1070
+ ms._valueContainer.appendTo(ms.selectionContainer);
1071
+
1072
+ if(cfg.selectionPosition === 'inner' && !cfg.selectionContainer) {
1073
+ ms.input.width(0);
1074
+ inputOffset = ms.input.offset().left - ms.selectionContainer.offset().left;
1075
+ w = ms.container.width() - inputOffset - 42;
1076
+ ms.input.width(w);
1077
+ }
1078
+
1079
+ if(_selection.length === cfg.maxSelection){
1080
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1081
+ } else {
1082
+ ms.helper.hide();
1083
+ }
1084
+ },
1085
+
1086
+ /**
1087
+ * Select an item either through keyboard or mouse
1088
+ * @param item
1089
+ * @private
1090
+ */
1091
+ _selectItem: function(item) {
1092
+ if(cfg.maxSelection === 1){
1093
+ _selection = [];
1094
+ }
1095
+ ms.addToSelection(item.data('json'));
1096
+ item.removeClass('ms-res-item-active');
1097
+ if(cfg.expandOnFocus === false || _selection.length === cfg.maxSelection){
1098
+ ms.collapse();
1099
+ }
1100
+ if(!_hasFocus){
1101
+ ms.input.focus();
1102
+ } else if(_hasFocus && (cfg.expandOnFocus || _ctrlDown)){
1103
+ self._processSuggestions();
1104
+ if(_ctrlDown){
1105
+ ms.expand();
1106
+ }
1107
+ }
1108
+ },
1109
+
1110
+ /**
1111
+ * Sorts the results and cut them down to max # of displayed results at once
1112
+ * @private
1113
+ */
1114
+ _sortAndTrim: function(data) {
1115
+ var q = ms.getRawValue(),
1116
+ filtered = [],
1117
+ newSuggestions = [],
1118
+ selectedValues = ms.getValue();
1119
+ // filter the data according to given input
1120
+ if(q.length > 0) {
1121
+ $.each(data, function(index, obj) {
1122
+ var name = obj[cfg.displayField];
1123
+ if((cfg.matchCase === true && name.indexOf(q) > -1) ||
1124
+ (cfg.matchCase === false && name.toLowerCase().indexOf(q.toLowerCase()) > -1)) {
1125
+ if(cfg.strictSuggest === false || name.toLowerCase().indexOf(q.toLowerCase()) === 0) {
1126
+ filtered.push(obj);
1127
+ }
1128
+ }
1129
+ });
1130
+ }
1131
+ else {
1132
+ filtered = data;
1133
+ }
1134
+ // take out the ones that have already been selected
1135
+ $.each(filtered, function(index, obj) {
1136
+ if (cfg.allowDuplicates || $.inArray(obj[cfg.valueField], selectedValues) === -1) {
1137
+ newSuggestions.push(obj);
1138
+ }
1139
+ });
1140
+ // sort the data
1141
+ if(cfg.sortOrder !== null) {
1142
+ newSuggestions.sort(function(a,b) {
1143
+ if(a[cfg.sortOrder] < b[cfg.sortOrder]) {
1144
+ return cfg.sortDir === 'asc' ? -1 : 1;
1145
+ }
1146
+ if(a[cfg.sortOrder] > b[cfg.sortOrder]) {
1147
+ return cfg.sortDir === 'asc' ? 1 : -1;
1148
+ }
1149
+ return 0;
1150
+ });
1151
+ }
1152
+ // trim it down
1153
+ if(cfg.maxSuggestions && cfg.maxSuggestions > 0) {
1154
+ newSuggestions = newSuggestions.slice(0, cfg.maxSuggestions);
1155
+ }
1156
+ return newSuggestions;
1157
+
1158
+ },
1159
+
1160
+ _group: function(data){
1161
+ // build groups
1162
+ if(cfg.groupBy !== null) {
1163
+ _groups = {};
1164
+
1165
+ $.each(data, function(index, value) {
1166
+ var props = cfg.groupBy.indexOf('.') > -1 ? cfg.groupBy.split('.') : cfg.groupBy;
1167
+ var prop = value[cfg.groupBy];
1168
+ if(typeof(props) != 'string'){
1169
+ prop = value;
1170
+ while(props.length > 0){
1171
+ prop = prop[props.shift()];
1172
+ }
1173
+ }
1174
+ if(_groups[prop] === undefined) {
1175
+ _groups[prop] = {title: prop, items: [value]};
1176
+ }
1177
+ else {
1178
+ _groups[prop].items.push(value);
1179
+ }
1180
+ });
1181
+ }
1182
+ return data;
1183
+ },
1184
+
1185
+ /**
1186
+ * Update the helper text
1187
+ * @private
1188
+ */
1189
+ _updateHelper: function(html) {
1190
+ ms.helper.html(html);
1191
+ if(!ms.helper.is(":visible")) {
1192
+ ms.helper.fadeIn();
1193
+ }
1194
+ },
1195
+
1196
+ /**
1197
+ * Validate an item against vtype or vregex
1198
+ * @private
1199
+ */
1200
+ _validateSingleItem: function(value){
1201
+ if(cfg.vregex !== null && cfg.vregex instanceof RegExp){
1202
+ return cfg.vregex.test(value);
1203
+ } else if(cfg.vtype !== null) {
1204
+ switch(cfg.vtype){
1205
+ case 'alpha':
1206
+ return (/^[a-zA-Z_]+$/).test(value);
1207
+ case 'alphanum':
1208
+ return (/^[a-zA-Z0-9_]+$/).test(value);
1209
+ case 'email':
1210
+ return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/).test(value);
1211
+ case 'url':
1212
+ return (/(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i).test(value);
1213
+ case 'ipaddress':
1214
+ return (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/).test(value);
1215
+ }
1216
+ }
1217
+ return true;
1218
+ }
1219
+ };
1220
+
1221
+ var handlers = {
1222
+ /**
1223
+ * Triggered when blurring out of the component
1224
+ * @private
1225
+ */
1226
+ _onBlur: function() {
1227
+ ms.container.removeClass('ms-ctn-focus');
1228
+ ms.collapse();
1229
+ _hasFocus = false;
1230
+ if(ms.getRawValue() !== '' && cfg.allowFreeEntries === true){
1231
+ var obj = {};
1232
+ obj[cfg.displayField] = obj[cfg.valueField] = ms.getRawValue().trim();
1233
+ ms.addToSelection(obj);
1234
+ }
1235
+ self._renderSelection();
1236
+
1237
+ if(ms.isValid() === false) {
1238
+ ms.container.addClass(cfg.invalidCls);
1239
+ }
1240
+
1241
+ else if(ms.input.val() !== '' && cfg.allowFreeEntries === false) {
1242
+ ms.empty();
1243
+ self._updateHelper('');
1244
+ }
1245
+
1246
+ $(ms).trigger('blur', [ms]);
1247
+ },
1248
+
1249
+ /**
1250
+ * Triggered when hovering an element in the combo
1251
+ * @param e
1252
+ * @private
1253
+ */
1254
+ _onComboItemMouseOver: function(e) {
1255
+ var target = $(e.currentTarget);
1256
+ if(!target.hasClass('ms-res-item-disabled')){
1257
+ ms.combobox.children().removeClass('ms-res-item-active');
1258
+ target.addClass('ms-res-item-active');
1259
+ }
1260
+ },
1261
+
1262
+ /**
1263
+ * Triggered when an item is chosen from the list
1264
+ * @param e
1265
+ * @private
1266
+ */
1267
+ _onComboItemSelected: function(e) {
1268
+ var target = $(e.currentTarget);
1269
+ if(!target.hasClass('ms-res-item-disabled')){
1270
+ self._selectItem($(e.currentTarget));
1271
+ }
1272
+ },
1273
+
1274
+ /**
1275
+ * Triggered when focusing on the container div. Will focus on the input field instead.
1276
+ * @private
1277
+ */
1278
+ _onFocus: function() {
1279
+ ms.input.focus();
1280
+ },
1281
+
1282
+ /**
1283
+ * Triggered when clicking on the input text field
1284
+ * @private
1285
+ */
1286
+ _onInputClick: function(){
1287
+ if (ms.isDisabled() === false && _hasFocus) {
1288
+ if (cfg.toggleOnClick === true) {
1289
+ if (cfg.expanded){
1290
+ ms.collapse();
1291
+ } else {
1292
+ ms.expand();
1293
+ }
1294
+ }
1295
+ }
1296
+ },
1297
+
1298
+ /**
1299
+ * Triggered when focusing on the input text field.
1300
+ * @private
1301
+ */
1302
+ _onInputFocus: function() {
1303
+ if(ms.isDisabled() === false && !_hasFocus) {
1304
+ _hasFocus = true;
1305
+ ms.container.addClass('ms-ctn-focus');
1306
+ ms.container.removeClass(cfg.invalidCls);
1307
+
1308
+ var curLength = ms.getRawValue().length;
1309
+ if(cfg.expandOnFocus === true){
1310
+ ms.expand();
1311
+ }
1312
+
1313
+ if(_selection.length === cfg.maxSelection) {
1314
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1315
+ } else if(curLength < cfg.minChars) {
1316
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - curLength));
1317
+ }
1318
+
1319
+ self._renderSelection();
1320
+ $(ms).trigger('focus', [ms]);
1321
+ }
1322
+ },
1323
+
1324
+ /**
1325
+ * Triggered when the user presses a key while the component has focus
1326
+ * This is where we want to handle all keys that don't require the user input field
1327
+ * since it hasn't registered the key hit yet
1328
+ * @param e keyEvent
1329
+ * @private
1330
+ */
1331
+ _onKeyDown: function(e) {
1332
+ // check how tab should be handled
1333
+ var active = ms.combobox.find('.ms-res-item-active:not(.ms-res-item-disabled):first'),
1334
+ freeInput = ms.input.val();
1335
+ $(ms).trigger('keydown', [ms, e]);
1336
+
1337
+ if(e.keyCode === KEYCODES.TAB && (cfg.useTabKey === false ||
1338
+ (cfg.useTabKey === true && active.length === 0 && ms.input.val().length === 0))) {
1339
+ handlers._onBlur();
1340
+ return;
1341
+ }
1342
+ switch(e.keyCode) {
1343
+ case KEYCODES.BACKSPACE:
1344
+ if(freeInput.length === 0 && ms.getSelection().length > 0 && cfg.selectionPosition === 'inner') {
1345
+ _selection.pop();
1346
+ self._renderSelection();
1347
+ $(ms).trigger('selectionchange', [ms, ms.getSelection()]);
1348
+ ms.input.attr('placeholder', (cfg.selectionPosition === 'inner' && ms.getValue().length > 0) ? '' : cfg.placeholder);
1349
+ ms.input.focus();
1350
+ e.preventDefault();
1351
+ }
1352
+ break;
1353
+ case KEYCODES.TAB:
1354
+ case KEYCODES.ESC:
1355
+ e.preventDefault();
1356
+ break;
1357
+ case KEYCODES.ENTER:
1358
+ if(freeInput !== '' || cfg.expanded){
1359
+ e.preventDefault();
1360
+ }
1361
+ break;
1362
+ case KEYCODES.COMMA:
1363
+ if(cfg.useCommaKey === true){
1364
+ e.preventDefault();
1365
+ }
1366
+ break;
1367
+ case KEYCODES.CTRL:
1368
+ _ctrlDown = true;
1369
+ break;
1370
+ case KEYCODES.DOWNARROW:
1371
+ e.preventDefault();
1372
+ self._moveSelectedRow("down");
1373
+ break;
1374
+ case KEYCODES.UPARROW:
1375
+ e.preventDefault();
1376
+ self._moveSelectedRow("up");
1377
+ break;
1378
+ default:
1379
+ if(_selection.length === cfg.maxSelection) {
1380
+ e.preventDefault();
1381
+ }
1382
+ break;
1383
+ }
1384
+ },
1385
+
1386
+ /**
1387
+ * Triggered when a key is released while the component has focus
1388
+ * @param e
1389
+ * @private
1390
+ */
1391
+ _onKeyUp: function(e) {
1392
+ var freeInput = ms.getRawValue(),
1393
+ inputValid = $.trim(ms.input.val()).length > 0 &&
1394
+ (!cfg.maxEntryLength || $.trim(ms.input.val()).length <= cfg.maxEntryLength),
1395
+ selected,
1396
+ obj = {};
1397
+
1398
+ $(ms).trigger('keyup', [ms, e]);
1399
+
1400
+ clearTimeout(_timer);
1401
+
1402
+ // collapse if escape, but keep focus.
1403
+ if(e.keyCode === KEYCODES.ESC && cfg.expanded) {
1404
+ ms.combobox.hide();
1405
+ }
1406
+ // ignore a bunch of keys
1407
+ if((e.keyCode === KEYCODES.TAB && cfg.useTabKey === false) || (e.keyCode > KEYCODES.ENTER && e.keyCode < KEYCODES.SPACE)) {
1408
+ if(e.keyCode === KEYCODES.CTRL){
1409
+ _ctrlDown = false;
1410
+ }
1411
+ return;
1412
+ }
1413
+ switch(e.keyCode) {
1414
+ case KEYCODES.UPARROW:
1415
+ case KEYCODES.DOWNARROW:
1416
+ e.preventDefault();
1417
+ break;
1418
+ case KEYCODES.ENTER:
1419
+ case KEYCODES.TAB:
1420
+ case KEYCODES.COMMA:
1421
+ if(e.keyCode !== KEYCODES.COMMA || cfg.useCommaKey === true) {
1422
+ e.preventDefault();
1423
+ if(cfg.expanded === true){ // if a selection is performed, select it and reset field
1424
+ selected = ms.combobox.find('.ms-res-item-active:not(.ms-res-item-disabled):first');
1425
+ if(selected.length > 0) {
1426
+ self._selectItem(selected);
1427
+ return;
1428
+ }
1429
+ }
1430
+ // if no selection or if freetext entered and free entries allowed, add new obj to selection
1431
+ if(inputValid === true && cfg.allowFreeEntries === true) {
1432
+ obj[cfg.displayField] = obj[cfg.valueField] = freeInput.trim();
1433
+ ms.addToSelection(obj);
1434
+ ms.collapse(); // reset combo suggestions
1435
+ ms.input.focus();
1436
+ }
1437
+ break;
1438
+ }
1439
+ default:
1440
+ if(_selection.length === cfg.maxSelection){
1441
+ self._updateHelper(cfg.maxSelectionRenderer.call(this, _selection.length));
1442
+ }
1443
+ else {
1444
+ if(freeInput.length < cfg.minChars) {
1445
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - freeInput.length));
1446
+ if(cfg.expanded === true) {
1447
+ ms.collapse();
1448
+ }
1449
+ }
1450
+ else if(cfg.maxEntryLength && freeInput.length > cfg.maxEntryLength) {
1451
+ self._updateHelper(cfg.maxEntryRenderer.call(this, freeInput.length - cfg.maxEntryLength));
1452
+ if(cfg.expanded === true) {
1453
+ ms.collapse();
1454
+ }
1455
+ }
1456
+ else {
1457
+ ms.helper.hide();
1458
+ if(cfg.minChars <= freeInput.length){
1459
+ _timer = setTimeout(function() {
1460
+ if(cfg.expanded === true) {
1461
+ self._processSuggestions();
1462
+ } else {
1463
+ ms.expand();
1464
+ }
1465
+ }, cfg.typeDelay);
1466
+ }
1467
+ }
1468
+ }
1469
+ break;
1470
+ }
1471
+ },
1472
+
1473
+ /**
1474
+ * Triggered when clicking upon cross for deletion
1475
+ * @param e
1476
+ * @private
1477
+ */
1478
+ _onTagTriggerClick: function(e) {
1479
+ ms.removeFromSelection($(e.currentTarget).data('json'));
1480
+ },
1481
+
1482
+ /**
1483
+ * Triggered when clicking on the small trigger in the right
1484
+ * @private
1485
+ */
1486
+ _onTriggerClick: function() {
1487
+ if(ms.isDisabled() === false && !(cfg.expandOnFocus === true && _selection.length === cfg.maxSelection)) {
1488
+ $(ms).trigger('triggerclick', [ms]);
1489
+ if(cfg.expanded === true) {
1490
+ ms.collapse();
1491
+ } else {
1492
+ var curLength = ms.getRawValue().length;
1493
+ if(curLength >= cfg.minChars){
1494
+ ms.input.focus();
1495
+ ms.expand();
1496
+ } else {
1497
+ self._updateHelper(cfg.minCharsRenderer.call(this, cfg.minChars - curLength));
1498
+ }
1499
+ }
1500
+ }
1501
+ },
1502
+
1503
+ /**
1504
+ * Triggered when the browser window is resized
1505
+ * @private
1506
+ */
1507
+ _onWindowResized: function() {
1508
+ self._renderSelection();
1509
+ }
1510
+ };
1511
+
1512
+ // startup point
1513
+ if(element !== null) {
1514
+ self._render(element);
1515
+ }
1516
+ };
1517
+
1518
+ $.fn.magicSuggest = function(options) {
1519
+ var obj = $(this);
1520
+
1521
+ if(obj.size() === 1 && obj.data('magicSuggest')) {
1522
+ return obj.data('magicSuggest');
1523
+ }
1524
+
1525
+ obj.each(function(i) {
1526
+ // assume $(this) is an element
1527
+ var cntr = $(this);
1528
+
1529
+ // Return early if this element already has a plugin instance
1530
+ if(cntr.data('magicSuggest')){
1531
+ return;
1532
+ }
1533
+
1534
+ if(this.nodeName.toLowerCase() === 'select'){ // rendering from select
1535
+ options.data = [];
1536
+ options.value = [];
1537
+ $.each(this.children, function(index, child){
1538
+ if(child.nodeName && child.nodeName.toLowerCase() === 'option'){
1539
+ options.data.push({id: child.value, name: child.text});
1540
+ if($(child).attr('selected')){
1541
+ options.value.push(child.value);
1542
+ }
1543
+ }
1544
+ });
1545
+ }
1546
+
1547
+ var def = {};
1548
+ // set values from DOM container element
1549
+ $.each(this.attributes, function(i, att){
1550
+ def[att.name] = att.name === 'value' && att.value !== '' ? JSON.parse(att.value) : att.value;
1551
+ });
1552
+
1553
+ var field = new MagicSuggest(this, $.extend([], $.fn.magicSuggest.defaults, options, def));
1554
+ cntr.data('magicSuggest', field);
1555
+ field.container.data('magicSuggest', field);
1556
+ });
1557
+
1558
+ if(obj.size() === 1) {
1559
+ return obj.data('magicSuggest');
1560
+ }
1561
+ return obj;
1562
+ };
1563
+
1564
+ $.fn.magicSuggest.defaults = {};
1565
+ })(jQuery);