| 1 | /* |
|---|
| 2 | The combobox control. This is very simililar to the local array |
|---|
| 3 | autocompleter but with the addition that if a specified control |
|---|
| 4 | (like an image) is clicked it will show a default list. |
|---|
| 5 | Otherwise it behaves exactly like the local array autocompleter. |
|---|
| 6 | |
|---|
| 7 | The constructor is the same as the local array autocompleter. |
|---|
| 8 | |
|---|
| 9 | To specify a different array for the 'show default' control, |
|---|
| 10 | add the option 'defaultArray', and pass to it a valid javascript array. |
|---|
| 11 | This control won't work so well if the two arrays you pass in are |
|---|
| 12 | wildly different. Generally you need to have the defaultArray be a |
|---|
| 13 | subset of the main array. |
|---|
| 14 | |
|---|
| 15 | I've commented in-line to help explain some of the choices. |
|---|
| 16 | |
|---|
| 17 | One last thing: a 'super' would be *really* nice for stuff like this. |
|---|
| 18 | There is a lot of dup code because I can't call super; or super.onBlur(). |
|---|
| 19 | I guess I could name the methods something different, and we can do that |
|---|
| 20 | to keep the code DRY, but I left the dupe lines in to make it clear as to |
|---|
| 21 | what is going on. |
|---|
| 22 | |
|---|
| 23 | An example (in html) is: |
|---|
| 24 | <script type="text/javascript"> |
|---|
| 25 | //*horrible* baby names (first names! try and guess which ones are for |
|---|
| 26 | // boys and girls. ugh) |
|---|
| 27 | var all_options = ['Ambrosia','Attica','Autumn Night','Avellana','Bow Hunter', |
|---|
| 28 | 'Brielle','Brooklynn','Cash','Cinsere','Crisiant','Cyndee','Delphine','Donte', |
|---|
| 29 | 'Drudwen','Dusk','Electra','Emmaleigh','Enfys','Enobi','Eurwen','Faerin', |
|---|
| 30 | 'Faleiry','Francessca','Franka','Fritha','Gage','Gaiety','Gennavieve','Gibson']; |
|---|
| 31 | // the worst. Ok, couldn't cull it down enough, so any 5 |
|---|
| 32 | var default_options = ['Avellana','Enobi','Eurwen','Faleiry','Gage']; |
|---|
| 33 | |
|---|
| 34 | </script> |
|---|
| 35 | TEST:<input type="text" id="name-field"/> |
|---|
| 36 | <image id='name-arrow' src="/images/combo_box_arrow.png"/> |
|---|
| 37 | <div class="auto_complete" id="lookup_auto_complete"></div> |
|---|
| 38 | <%= javascript_tag("new Autocompleter.Combobox('name-field', |
|---|
| 39 | 'lookup_auto_complete', 'name-arrow', |
|---|
| 40 | all_options,{partialChars:1, ignoreCase:true, fullSearch:true, frequency:0.1, |
|---|
| 41 | choices:6, defaultArray:default_options, tokens: ',' });") %> |
|---|
| 42 | */ |
|---|
| 43 | |
|---|
| 44 | Autocompleter.Combobox = Class.create(); |
|---|
| 45 | Object.extend(Object.extend(Autocompleter.Combobox.prototype, Autocompleter.Local.prototype), { |
|---|
| 46 | |
|---|
| 47 | initialize: function(element, update, selectDefault, array, options) { |
|---|
| 48 | this.baseInitialize(element, update, options); |
|---|
| 49 | this.selectDefault = selectDefault; |
|---|
| 50 | //this keeps track of whether or not the click is currently over the 'selectDefault'. See |
|---|
| 51 | // below for more detail. |
|---|
| 52 | this.overSelectDefault = false; |
|---|
| 53 | this.clickedSelectDefault = 0; |
|---|
| 54 | |
|---|
| 55 | this.options.array = this.options.array || array; |
|---|
| 56 | this.options.defaultArray = this.options.defaultArray || this.options.array; |
|---|
| 57 | this.options.defaultChoices = this.options.defaultChoices || this.options.defaultArray.length; |
|---|
| 58 | |
|---|
| 59 | Event.observe(this.selectDefault, 'click', this.click.bindAsEventListener(this)); |
|---|
| 60 | |
|---|
| 61 | // these events are all in place to coordinate between te onBlur event for the input |
|---|
| 62 | // field and the click event for the selectDefault. See below for more detail. |
|---|
| 63 | Event.observe(this.selectDefault, "mouseover", this.onMouseover.bindAsEventListener(this)); |
|---|
| 64 | Event.observe(this.selectDefault, "mouseout", this.onMouseout.bindAsEventListener(this)); |
|---|
| 65 | Event.observe(this.element, "keypress", this.resetClick.bindAsEventListener(this)); |
|---|
| 66 | }, |
|---|
| 67 | |
|---|
| 68 | /* |
|---|
| 69 | The one issue I had (and hence the extra Event.observers and this.overSelectDefault and this.clickedSelectDefault) |
|---|
| 70 | was that if you had the input field highlighted and clicked _anywhere_ outside the input field, the |
|---|
| 71 | 'onBlur' method would be called. This makes sense except in the case where you hit the 'selectDefault' |
|---|
| 72 | element, in which case you don't want to hide the dropdown but show it! |
|---|
| 73 | |
|---|
| 74 | I first tried to see if the events were called in any particular order (deterministic). I didn't |
|---|
| 75 | think this would work but I thought it was worth a shot. The answer depends on the browser, but |
|---|
| 76 | most are non-deterministic. Because they aren't called in a pre-defined order, that also meant |
|---|
| 77 | that you can't set a state flag in one (like 'I just clicked the selectDefault element!'), and expect |
|---|
| 78 | to check it in the other event ('hide me IF the selectDefault was not clicked!'). |
|---|
| 79 | |
|---|
| 80 | The other hope was that the event could give me information about either which element was |
|---|
| 81 | *active* when the event was triggered (this is different than asking which element is tied to the |
|---|
| 82 | event, which is given to you by prototype's Event.element), or the X,Y coordinates of the event to |
|---|
| 83 | see if I could compare that to the location of the selectDefault element (prototype's Position.within). |
|---|
| 84 | |
|---|
| 85 | Neither of those worked. Firefox has an attribute for events that tell you what element you were on |
|---|
| 86 | when the event was triggered. IE supposedly has something similar, but opera and safari do not. So |
|---|
| 87 | that won't work. And, the onBlur event doesn't set the X and Y coordinates (or any of the numerous |
|---|
| 88 | position attributes) in either firefox or safari. doah! |
|---|
| 89 | |
|---|
| 90 | So the end solution was to set a flag on a mouseover and mouseout on the selectDefault element. |
|---|
| 91 | The idea is that this will occur before a click and there will be time to set the flag before |
|---|
| 92 | the 'onClick' and 'onBlur' events are triggered simultaneously. This works really well, but leaves one |
|---|
| 93 | last catch: sometimes when you click the selectDefault you *want* the default menu to disappear. Hence |
|---|
| 94 | the reason to keep track of the state of the selectDefault element (previously clicked or not clicked). |
|---|
| 95 | */ |
|---|
| 96 | |
|---|
| 97 | onMouseover: function(event){ |
|---|
| 98 | this.overSelectDefault = true; |
|---|
| 99 | }, |
|---|
| 100 | |
|---|
| 101 | onMouseout: function(event){ |
|---|
| 102 | this.overSelectDefault = false; |
|---|
| 103 | }, |
|---|
| 104 | |
|---|
| 105 | resetClick :function(event) { |
|---|
| 106 | this.clickedSelectDefault = 0; |
|---|
| 107 | }, |
|---|
| 108 | |
|---|
| 109 | onBlur: function(event, force) { |
|---|
| 110 | if (this.overSelectDefault && !force) |
|---|
| 111 | { |
|---|
| 112 | //no need to blur |
|---|
| 113 | return; |
|---|
| 114 | } |
|---|
| 115 | // this is where you can call super.onBlur(); |
|---|
| 116 | setTimeout(this.hide.bind(this), 250); |
|---|
| 117 | this.hasFocus = false; |
|---|
| 118 | this.active = false; |
|---|
| 119 | this.clickedSelectDefault = false; |
|---|
| 120 | }, |
|---|
| 121 | |
|---|
| 122 | click: function(event) { |
|---|
| 123 | this.clickedSelectDefault = Math.abs(this.clickedSelectDefault - 1); |
|---|
| 124 | this.element.focus(); |
|---|
| 125 | this.changed = false; |
|---|
| 126 | this.hasFocus = true; |
|---|
| 127 | this.getAllChoices(); |
|---|
| 128 | if ( Element.getStyle(this.update, 'display') != 'none' && this.clickedSelectDefault != 1) { |
|---|
| 129 | this.onBlur(event, true); |
|---|
| 130 | } |
|---|
| 131 | }, |
|---|
| 132 | |
|---|
| 133 | // this is *almost* exactly the same as Autocompleter.local.selector method |
|---|
| 134 | // there can definitely be some refactoring done in the control.js file |
|---|
| 135 | // to keep it DRY |
|---|
| 136 | // What I changed was that if the user hits the defaultSelect element, I |
|---|
| 137 | // *always* want to show all the elements in the defaultSelect 'dropdown'. |
|---|
| 138 | // that way there is some consistency about what is shown when the user |
|---|
| 139 | // hits that element. |
|---|
| 140 | // but to recognize that they might have already typed something in the |
|---|
| 141 | // input field, this function (like the one in Autocomplete.local) highlights |
|---|
| 142 | // the matching chars, if there are any. |
|---|
| 143 | getAllChoices: function(e) { |
|---|
| 144 | var ret = []; // Beginning matches |
|---|
| 145 | var partial = []; // Inside matches |
|---|
| 146 | var entry = this.getToken(); |
|---|
| 147 | var count = 0; |
|---|
| 148 | for (var i = 0; i < this.options.defaultArray.length && |
|---|
| 149 | ret.length < this.options.defaultChoices; i++) { |
|---|
| 150 | var elem = this.options.defaultArray[i]; |
|---|
| 151 | var foundPos = this.options.ignoreCase ? |
|---|
| 152 | elem.toLowerCase().indexOf(entry.toLowerCase()) : |
|---|
| 153 | elem.indexOf(entry); |
|---|
| 154 | if (entry == "" || foundPos == -1) |
|---|
| 155 | { |
|---|
| 156 | ret.push("<li>" + elem + "</li>"); |
|---|
| 157 | continue; |
|---|
| 158 | } |
|---|
| 159 | while (foundPos != -1) { |
|---|
| 160 | if (foundPos == 0 && elem.length != entry.length) { |
|---|
| 161 | ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + |
|---|
| 162 | elem.substr(entry.length) + "</li>"); |
|---|
| 163 | break; |
|---|
| 164 | } else if (entry.length >= this.options.partialChars && |
|---|
| 165 | this.options.partialSearch && foundPos != -1) { |
|---|
| 166 | if (this.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { |
|---|
| 167 | partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" + |
|---|
| 168 | elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( |
|---|
| 169 | foundPos + entry.length) + "</li>"); |
|---|
| 170 | break; |
|---|
| 171 | } |
|---|
| 172 | } |
|---|
| 173 | |
|---|
| 174 | foundPos = this.options.ignoreCase ? |
|---|
| 175 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : |
|---|
| 176 | elem.indexOf(entry, foundPos + 1); |
|---|
| 177 | |
|---|
| 178 | } |
|---|
| 179 | } |
|---|
| 180 | if (partial.length) |
|---|
| 181 | ret = ret.concat(partial.slice(0, this.options.defaultChoices - ret.length)) |
|---|
| 182 | this.updateChoices("<ul>" + ret.join('') + "</ul>"); |
|---|
| 183 | } |
|---|
| 184 | }); |
|---|