Skip to content

Commit 586102b

Browse files
committed
Add choiceAttribute option to allow objects as choices and listTemplate option to allow custom list item schema
1 parent 618416b commit 586102b

File tree

2 files changed

+96
-50
lines changed

2 files changed

+96
-50
lines changed

src/ui/controls/autocompleter.js

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11

22

33
(function(UI) {
4-
4+
55
/** section: scripty2 ui
66
* class S2.UI.Autocompleter < S2.UI.Base
77
*
88
**/
99
UI.Autocompleter = Class.create(UI.Base, {
1010
NAME: "S2.UI.Autocompleter",
11-
11+
1212
/**
1313
* new S2.UI.Autocompleter(element, options)
1414
*
@@ -17,25 +17,25 @@
1717
initialize: function(element, options) {
1818
this.element = $(element);
1919
var opt = this.setOptions(options);
20-
20+
2121
UI.addClassNames(this.element, 'ui-widget ui-autocompleter');
22-
22+
2323
this.input = this.element.down('input[type="text"]');
24-
24+
2525
if (!this.input) {
2626
this.input = new Element('input', { type: 'text' });
2727
this.element.insert(this.input);
2828
}
29-
29+
3030
this.input.insert({ before: this.button });
3131
this.input.setAttribute('autocomplete', 'off');
32-
32+
3333
this.name = opt.parameterName || this.input.readAttribute('name');
34-
34+
3535
if (opt.choices) {
3636
this.choices = opt.choices.clone();
3737
}
38-
38+
3939
this.menu = new UI.Menu();
4040
this.element.insert(this.menu.element);
4141

@@ -47,17 +47,17 @@
4747
top: (iLayout.get('top') + iLayout.get('margin-box-height')) + 'px'
4848
});
4949
}).bind(this).defer();
50-
50+
5151
this.observers = {
5252
blur: this._blur.bind(this),
5353
keyup: this._keyup.bind(this),
5454
keydown: this._keydown.bind(this),
5555
selected: this._selected.bind(this)
5656
};
57-
57+
5858
this.addObservers();
5959
},
60-
60+
6161
addObservers: function() {
6262
this.input.observe('blur', this.observers.blur);
6363
this.input.observe('keyup', this.observers.keyup);
@@ -66,19 +66,19 @@
6666
this.menu.observe('ui:menu:selected',
6767
this.observers.selected);
6868
},
69-
69+
7070
_schedule: function() {
7171
this._unschedule();
7272
this._timeout = this._change.bind(this).delay(this.options.frequency);
7373
},
74-
74+
7575
_unschedule: function() {
7676
if (this._timeout) window.clearTimeout(this._timeout);
7777
},
7878

7979
_keyup: function(event) {
8080
var value = this.input.getValue();
81-
81+
8282
if (value) {
8383
if (value.blank() || value.length < this.options.minCharacters) {
8484
// Empty values mean the menu should be hidden and all timers
@@ -95,16 +95,16 @@
9595
this.menu.close();
9696
this._unschedule();
9797
}
98-
98+
9999
this._value = value;
100100
},
101-
101+
102102
_keydown: function(event) {
103103
if (UI.modifierUsed(event)) return;
104104
if (!this.menu.isOpen()) return;
105-
105+
106106
var keyCode = event.keyCode || event.charCode;
107-
107+
108108
switch (event.keyCode) {
109109
case Event.KEY_UP:
110110
this.menu.moveHighlight(-1);
@@ -128,80 +128,86 @@
128128
break;
129129
}
130130
},
131-
131+
132132
// TODO: Implement tokenizing.
133133
_getInput: function() {
134134
return this.input.getValue();
135135
},
136-
136+
137137
// TODO: Implement tokenizing.
138138
_setInput: function(value) {
139139
this.input.setValue(value);
140140
},
141-
141+
142142
_change: function() {
143143
this.findChoices();
144144
},
145-
145+
146146
/**
147147
* S2.UI.Autocompleter#findChoices() -> undefined
148148
*
149149
* Triggers an update of the choices presented to the user. If results
150150
* come from the server, this is an asynchronous operation.
151151
**/
152152
findChoices: function() {
153-
var value = this._getInput();
154-
var choices = this.choices || [];
153+
var value = this._getInput(),
154+
choices = this.choices || [],
155+
choiceValue,
156+
choiceAttribute = this.options.choiceAttribute;
157+
155158
var results = choices.inject([], function(memo, choice) {
156-
if (choice.toLowerCase().include(value.toLowerCase())) {
159+
choiceValue = Object.isUndefined(choiceAttribute) ? choice : choice[choiceAttribute]
160+
if (choiceValue.toLowerCase().include(value.toLowerCase())) {
157161
memo.push(choice);
158162
}
159163
return memo;
160164
});
161-
165+
162166
this.setChoices(results);
163167
},
164-
168+
165169
setChoices: function(results) {
166170
this.results = results;
167171
this._updateMenu(results);
168172
},
169-
173+
170174
_updateMenu: function(results) {
171175
var opt = this.options;
172-
176+
173177
this.menu.clear();
174-
178+
175179
// Build a case-insensitive regexp for highlighting the substring match.
176180
var needle = new RegExp(RegExp.escape(this._value), 'i');
177181
for (var i = 0, result, li, text; result = results[i]; i++) {
182+
value = Object.isUndefined(opt.choiceAttribute) ? result : result[opt.choiceAttribute]
183+
178184
text = opt.highlightSubstring ?
179-
result.replace(needle, "<b>$&</b>") :
180-
result;
181-
182-
li = new Element('li').update(text);
185+
value.replace(needle, "<b>$&</b>") :
186+
value;
187+
188+
li = new Element('li').update(Object.isUndefined(opt.listTemplate) ? text : opt.listTemplate.evaluate({text: text, object: result}));
183189
li.store('ui.autocompleter.value', result);
184190
this.menu.addChoice(li);
185191
}
186-
192+
187193
if (results.length === 0) {
188194
this.menu.close();
189195
} else {
190196
this.menu.open();
191197
}
192198
},
193-
199+
194200
_moveMenuChoice: function(delta) {
195201
var choices = this.list.down('li');
196202
this._selectedIndex = (this._selectedIndex + delta).constrain(
197203
-1, this.results.length - 1);
198-
204+
199205
this._highlightMenuChoice();
200206
},
201-
207+
202208
_highlightMenuChoice: function(element) {
203209
var choices = this.list.select('li'), index;
204-
210+
205211
if (Object.isElement(element)) {
206212
index = choices.indexOf(element);
207213
} else if (Object.isNumber(element)) {
@@ -213,42 +219,42 @@
213219
UI.removeClassNames(choices, 'ui-state-active');
214220
if (index === -1 || index === null) return;
215221
choices[index].addClassName('ui-state-active');
216-
222+
217223
this._selectedIndex = index;
218224
},
219-
225+
220226
_selected: function(event) {
221227
var memo = event.memo, li = memo.element;
222-
228+
223229
if (li) {
224230
var value = li.retrieve('ui.autocompleter.value');
225231
var result = this.element.fire('ui:autocompleter:selected', {
226232
instance: this,
227233
value: value
228234
});
229235
if (result.stopped) return;
230-
this._setInput(value);
236+
this._setInput(Object.isUndefined(this.options.choiceAttribute) ? value : value[this.options.choiceAttribute]);
231237
}
232238
this.menu.close();
233239
},
234-
240+
235241
_blur: function(event) {
236242
this._unschedule();
237243
this.menu.close();
238244
}
239245
});
240-
246+
241247
Object.extend(UI.Autocompleter, {
242248
DEFAULT_OPTIONS: {
243249
tokens: [],
244250
frequency: 0.4,
245251
minCharacters: 1,
246-
252+
247253
highlightSubstring: true,
248-
254+
249255
onShow: Prototype.K,
250256
onHide: Prototype.K
251257
}
252258
});
253-
259+
254260
})(S2.UI);

test/functional/controls_autocompleter.html

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,46 @@ <h2>Standard Autocompleter</h2>
142142
<li>The matching substring of a given choice <strong>should</strong> be bold.</li>
143143
</ul>
144144
</div> <!-- .description -->
145-
145+
146+
147+
<h2>Autocompleter using Objects as choices and Template to style list items</h2>
148+
149+
<span id="autocompleter2">
150+
<input type="text" name="firstname" />
151+
</span> <!-- #autocompleter2 -->
152+
153+
<script type="text/javascript">
154+
var choices = [
155+
{ client: { login: "madrobby", name: "Thomas Fuchs"} },
156+
{ client: { login: "savetheclocktower", name: "Andrew Dupont"} },
157+
{ client: { login: "RStankov", name: "Radoslav Stankov"} },
158+
{ client: { login: "smith", name: "Nathan L Smith"} },
159+
{ client: { login: "joe-loco", name: "Philipp Markovics"} },
160+
{ client: { login: "kommen", name: "Dieter Komendera",} },
161+
{ client: { login: "eric1234", name: "Eric Anderson"} },
162+
{ client: { login: "sgruhier", name: "Sébastien Gruhier"} },
163+
{ client: { login: "stepheneb", name: "Stephen Bannasch"} },
164+
{ client: { login: "ZenCocoon", name: "Sébastien Grosjean"} },
165+
{ client: { login: "TomK32", name: "Thomas R. Koll"} },
166+
{ client: { login: "charettes", name: "Simon Charette"} },
167+
{ client: { login: "rumble", name: "Mike Rumble"} }
168+
];
169+
170+
new S2.UI.Autocompleter('autocompleter2', {
171+
choices: choices.collect(function(o) { return o.client }),
172+
choiceAttribute: 'name',
173+
listTemplate: new Template('#{text} <em>(#{object.login})</em>')
174+
});
175+
</script>
176+
177+
<div class="description">
178+
The choices are the scripty2 contributors at test's creation date. It should behave like a standard autocompleter, with a few exceptions:
179+
<ul>
180+
<li>It <strong>should</strong> accept an array of objects as choices</li>
181+
<li>The list of completions <strong>should</strong> offer customized HTML items.</li>
182+
<li>When selected, it <strong>should</strong> set the value with the right object attribute.</li>
183+
<li>When <code>ui:autocompleter:selected</code> is fired, it <strong>should</strong> return the complete object as memo's value.</li>
184+
</ul>
185+
</div> <!-- .description -->
146186
</body>
147187
</html>

0 commit comments

Comments
 (0)