e310ac59d2efe941089246962f5a9ff143ed7fd9
[roojs1] / Roo / bootstrap / ComboBox.js
1 /*
2  * - LGPL
3  * * 
4  */
5
6 /**
7  * @class Roo.bootstrap.ComboBox
8  * @extends Roo.bootstrap.Component
9  * A combobox control with support for autocomplete, remote-loading, paging and many other features.
10  * @cfg {Boolean} showSearchBar  (true|false) default false
11  * 
12  * @constructor
13  * Create a new ComboBox.
14  * @param {Object} config Configuration options
15  */
16 Roo.bootstrap.ComboBox = function(config){
17     Roo.bootstrap.ComboBox.superclass.constructor.call(this, config);
18
19 };
20
21 Roo.extend(Roo.bootstrap.ComboBox, Roo.bootstrap.Component, {
22     
23     showSearchBar : false,
24     resultsSelector : ".select2-results",
25     
26     getAutoCreate : function()
27     {
28         var id = Roo.id();
29         
30         var cfg = {
31             tag: 'select',
32             id: id,
33             cn: [
34                 {
35                     tag: 'option',
36                     value: 'AK',
37                     html: 'Alaska'
38                 },
39                 {
40                     tag: 'option',
41                     value: 'HI',
42                     html: 'Hawfaii'
43                 },
44                 {
45                     tag: 'option',
46                     value: 'CA',
47                     html: 'California'
48                 }
49             ]
50         };
51        
52         return cfg;
53         
54     },
55     
56     initEvents : function()
57     {
58         Roo.log('initEvents');
59         
60         this.container = Roo.bootstrap.ComboBox.SingleSelect2.createContainer();
61         
62         var liveRegion = {
63             tag: 'span',
64             role: 'status',
65             cls: 'select2-hidden-accessible'
66         }
67         
68         this.liveRegion = Roo.get(document.body).createChild(liveRegion);
69         
70         this.containerId="s2id_"+(this.el.attr("id") || Roo.id());
71         
72         this.container.attr("id", "s2id_"+(this.el.attr("id") || Roo.id()));
73         
74         this.container.attr("title", this.el.attr("title"));
75         
76         this.container.attr("style", this.el.attr("style"));
77         
78         this.container.addClass(this.el.attr('class'));
79         
80         this.container.on("click", this.killEvent);
81         
82         this.elementTabIndex = this.el.attr("tabindex");
83         
84         this.dropdown = this.container.select(".select2-drop", true).first();
85         
86         this.dropdown.addClass(this.el.attr('class'));
87         
88         this.dropdown.on("click", this.killEvent);
89         
90         this.results = this.container.select(this.resultsSelector, true).first();
91         
92         this.search = this.container.select("input.select2-input", true).first();
93         
94         this.initContainer();
95         
96     },
97     
98     populateResults: function(container, results, query) {
99         var populate, id=this.opts.id, liveRegion=this.liveRegion;
100
101         populate=function(results, container, depth) {
102
103             var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
104
105             results = this.sortResults(results, container, query);
106
107             for (i = 0, l = results.length; i < l; i = i + 1) {
108
109                 result=results[i];
110
111                 disabled = (result.disabled === true);
112                 selectable = (!disabled) && (id(result) !== undefined);
113
114                 compound=result.children && result.children.length > 0;
115
116                 node=new Roo.Element(document.createElement("li"), true);
117                 node.addClass("select2-results-dept-"+depth);
118                 node.addClass("select2-result");
119                 node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
120                 if (disabled) { node.addClass("select2-disabled"); }
121                 if (compound) { node.addClass("select2-result-with-children"); }
122                 node.addClass(this.formatResultCssClass(result));
123                 node.attr("role", "presentation");
124
125                 label=new Roo.Element(document.createElement("div"), true);
126                 label.addClass("select2-result-label");
127                 label.attr("id", "select2-result-label-" + nextUid());
128                 label.attr("role", "option");
129
130                 formatted=this.formatResult(result, label, query, this.defaultEscapeMarkup);
131                 if (formatted!==undefined) {
132                     label.html(formatted);
133                     node.append(label);
134                 }
135
136
137                 if (compound) {
138
139                     innerContainer=new Roo.Element(document.createElement("ul"), true);
140                     innerContainer.addClass("select2-result-sub");
141                     populate(result.children, innerContainer, depth+1);
142                     node.append(innerContainer);
143                 }
144
145                 node.data("select2-data", result);
146                 container.append(node);
147             }
148
149             liveRegion.text(this.formatMatches(results.length));
150         };
151
152         populate(results, container, 0);
153     },
154     
155     sortResults: function (results, container, query) 
156     {
157         return results;
158     },
159     
160     formatResultCssClass: function(data) 
161     {
162         return data.css;
163     },
164     
165     formatResult: function(result, container, query, escapeMarkup) 
166     {
167         var markup=[];
168         markMatch(result.text, query.term, markup, escapeMarkup);
169         return markup.join("");
170     },
171     
172     defaultEscapeMarkup : function (markup) {
173         var replace_map = {
174             '\\': '&#92;',
175             '&': '&amp;',
176             '<': '&lt;',
177             '>': '&gt;',
178             '"': '&quot;',
179             "'": '&#39;',
180             "/": '&#47;'
181         };
182
183         return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
184             return replace_map[match];
185         });
186     },
187     
188     formatMatches: function (matches) 
189     { 
190         return matches + " results are available, use up and down arrow keys to navigate."; 
191     },
192     
193     initContainer: function () {
194
195         var idSuffix = Roo.id(),
196             elementLabel;
197         
198         this.hideSearchBar();
199         
200         if (this.showSearchBar) {
201             this.showSearchBar();
202         }
203         
204         this.selection = this.container.select(".select2-choice", true).first();
205
206         this.focusser = this.container.select(".select2-focusser", true).first();
207
208         // add aria associations
209         this.selection.select(".select2-chosen",true).first().attr("id", "select2-chosen-" + idSuffix);
210         this.focusser.attr("aria-labelledby", "select2-chosen-" + idSuffix);
211         this.results.attr("id", "select2-results-" + idSuffix);
212         this.search.attr("aria-owns", "select2-results-" + idSuffix);
213
214         // rewrite labels from original element to focusser
215         this.focusser.attr("id", "s2id_" + idSuffix);
216
217         Roo.get(this.focusser.getPrevSibling()).attr('for', this.focusser.attr('id'));
218         
219         // Ensure the original element retains an accessible name
220 //        var originalTitle = this.el.attr("title");
221 //        this.opts.element.attr("title", (originalTitle || elementLabel.text()));
222
223         this.focusser.attr("tabindex", this.elementTabIndex);
224
225         // write label for search field using the label from the focusser element
226         this.search.attr("id", this.focusser.attr('id') + '_search');
227
228         Roo.get(this.search.getPrevSibling()).attr('for', this.search.attr('id'));
229         
230         this.search.on("keydown", this.bind(function (e) {
231             if (!this.isInterfaceEnabled()) return;
232
233             if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
234                 // prevent the page from scrolling
235                 killEvent(e);
236                 return;
237             }
238
239             switch (e.which) {
240                 case KEY.UP:
241                 case KEY.DOWN:
242                     this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
243                     killEvent(e);
244                     return;
245                 case KEY.ENTER:
246                     this.selectHighlighted();
247                     killEvent(e);
248                     return;
249                 case KEY.TAB:
250                     this.selectHighlighted({noFocus: true});
251                     return;
252                 case KEY.ESC:
253                     this.cancel(e);
254                     killEvent(e);
255                     return;
256             }
257         }));
258
259         this.search.on("blur", this.bind(function(e) {
260
261             // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
262             // without this the search field loses focus which is annoying
263             if (document.activeElement === this.body.get(0)) {
264                 window.setTimeout(this.bind(function() {
265                     if (this.opened()) {
266                         this.search.focus();
267                     }
268                 }), 0);
269             }
270         }));
271
272         this.focusser.on("keydown", this.bind(function (e) {
273             console.log('focusser on keydown');
274             if (!this.isInterfaceEnabled()) return;
275
276             if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
277                 return;
278             }
279
280             if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
281                 killEvent(e);
282                 return;
283             }
284
285             if (e.which == KEY.DOWN || e.which == KEY.UP
286                 || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
287
288                 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) return;
289
290                 this.open();
291                 killEvent(e);
292                 return;
293             }
294
295             if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
296                 if (this.opts.allowClear) {
297                     this.clear();
298                 }
299                 killEvent(e);
300                 return;
301             }
302         }));
303
304
305         installKeyUpChangeEvent(this.focusser);
306         this.focusser.on("keyup-change input", this.bind(function(e) {
307             if (this.opts.minimumResultsForSearch >= 0) {
308                 e.stopPropagation();
309                 if (this.opened()) return;
310                 this.open();
311             }
312         }));
313
314         selection.on("mousedown touchstart", "abbr", this.bind(function (e) {
315             if (!this.isInterfaceEnabled()) return;
316             this.clear();
317             killEventImmediately(e);
318             this.close();
319             this.selection.focus();
320         }));
321
322         selection.on("mousedown touchstart", this.bind(function (e) {
323             // Prevent IE from generating a click event on the body
324             reinsertElement(selection);
325
326             if (!this.container.hasClass("select2-container-active")) {
327                 this.opts.element.trigger($.Event("select2-focus"));
328             }
329
330             if (this.opened()) {
331                 this.close();
332             } else if (this.isInterfaceEnabled()) {
333                 this.open();
334             }
335
336             killEvent(e);
337         }));
338
339         dropdown.on("mousedown touchstart", this.bind(function() {
340             if (this.opts.shouldFocusInput(this)) {
341                 this.search.focus();
342             }
343         }));
344
345         selection.on("focus", this.bind(function(e) {
346             killEvent(e);
347         }));
348
349         this.focusser.on("focus", this.bind(function(){
350             if (!this.container.hasClass("select2-container-active")) {
351                 this.opts.element.trigger($.Event("select2-focus"));
352             }
353             this.container.addClass("select2-container-active");
354         })).on("blur", this.bind(function() {
355             if (!this.opened()) {
356                 this.container.removeClass("select2-container-active");
357                 this.opts.element.trigger($.Event("select2-blur"));
358             }
359         }));
360         this.search.on("focus", this.bind(function(){
361             if (!this.container.hasClass("select2-container-active")) {
362                 this.opts.element.trigger($.Event("select2-focus"));
363             }
364             this.container.addClass("select2-container-active");
365         }));
366
367         this.initContainerWidth();
368         this.opts.element.addClass("select2-offscreen");
369         this.setPlaceholder();
370
371     },
372     
373     showSearchBar: function() {
374         
375         if(!this.dropdown.select(".select2-search",true).first().hasClass("select2-search-hidden")){
376             this.dropdown.select(".select2-search",true).first().addClass("select2-search-hidden");
377         }
378         
379         if(!this.dropdown.select(".select2-search",true).first().hasClass("select2-offscreen")){
380             this.dropdown.select(".select2-search",true).first().addClass("select2-offscreen");
381         }
382         
383         if(!this.dropdown.hasClass("select2-with-searchbox")){
384             this.dropdown.addClass("select2-with-searchbox");
385         }
386         
387         if(!this.container.hasClass("select2-with-searchbox")){
388             this.container.addClass("select2-with-searchbox");
389         }
390         
391     },
392     
393     hideSearchBar: function() {
394         
395         if(this.dropdown.select(".select2-search",true).first().hasClass("select2-search-hidden")){
396             this.dropdown.select(".select2-search",true).first().removeClass("select2-search-hidden");
397         }
398         
399         if(this.dropdown.select(".select2-search",true).first().hasClass("select2-offscreen")){
400             this.dropdown.select(".select2-search",true).first().removeClass("select2-offscreen");
401         }
402         
403         if(this.dropdown.hasClass("select2-with-searchbox")){
404             this.dropdown.removeClass("select2-with-searchbox");
405         }
406         
407         if(this.container.hasClass("select2-with-searchbox")){
408             this.container.removeClass("select2-with-searchbox");
409         }
410         
411     },
412     
413     killEvent : function (event) {
414         Roo.log('KILLEVENT');
415         event.preventDefault();
416         event.stopPropagation();
417     },
418     
419     moveHighlight: function (delta) {
420         var choices = this.findHighlightableChoices(),
421             index = this.highlight();
422
423         while (index > -1 && index < choices.length) {
424             index += delta;
425             var choice = $(choices[index]);
426             if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
427                 this.highlight(index);
428                 break;
429             }
430         }
431     },
432     
433     findHighlightableChoices: function() {
434         return this.results.select(".select2-result-selectable:not(.select2-disabled):not(.select2-selected)", true).elements;
435     }
436     
437     
438 });
439
440 Roo.apply(Roo.bootstrap.ComboBox,  {
441     SingleSelect2 : {
442         createContainer: function () {
443             var container = new Roo.Element(document.createElement("div"), true);
444             container.addClass("select2-container");
445             
446             container.createChild({
447                 tag: 'a',
448                 href: 'javascript:void(0)',
449                 cls: 'select2-choice',
450                 tabindex: '-1',
451                 cn: [
452                     {
453                         tag: 'span',
454                         cls: 'select2-chosen',
455                         html: '&#160;'
456                     },
457                     {
458                         tag: 'abbr',
459                         cls: 'select2-search-choice-close'
460                     },
461                     {
462                         tag: 'span',
463                         cls: 'select2-arrow',
464                         role: 'presentation'
465                     },
466                     {
467                         tag: 'b',
468                         role: 'presentation'
469                     }
470                 ]
471             });
472             
473             container.createChild({
474                 tag: 'label',
475                 'for': '',
476                 cls: 'select2-offscreen'
477             });
478             
479             container.createChild({
480                 tag: 'input',
481                 type: 'text',
482                 cls: 'select2-focusser select2-offscreen',
483                 'aria-haspopup': true,
484                 role: 'button'
485             });
486             
487             container.createChild({
488                 tag: 'div',
489                 cls: 'select2-drop select2-display-none',
490                 cn: [
491                     {
492                         tag: 'div',
493                         cls: 'select2-search',
494                         cn: [
495                             {
496                                 tag: 'label',
497                                 'for': '',
498                                 cls: 'select2-offscreen'
499                             },
500                             {
501                                 tag: 'input',
502                                 type: 'text',
503                                 cls: 'select2-input',
504                                 role: 'combobox',
505                                 autocomplete: 'off',
506                                 autocorrect: 'off',
507                                 autocapitalize: 'off',
508                                 spellcheck: false,
509                                 'aria-expanded': true,
510                                 'aria-autocomplete': 'lsit'
511                             },
512                             
513                         ]
514                     },
515                     {
516                         tag: 'ul',
517                         cls: 'select2-results',
518                         role: 'listbox'
519                     }
520                 ]
521             });
522             
523             return container;
524         }
525     }
526         
527 });