Skip to content

Commit

Permalink
Add choiceAttribute option to allow objects as choices and listTempla…
Browse files Browse the repository at this point in the history
…te option to allow custom list item schema
  • Loading branch information
ZenCocoon committed Sep 3, 2010
1 parent 618416b commit 586102b
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 50 deletions.
104 changes: 55 additions & 49 deletions src/ui/controls/autocompleter.js
@@ -1,14 +1,14 @@


(function(UI) {

/** section: scripty2 ui
* class S2.UI.Autocompleter < S2.UI.Base
*
**/
UI.Autocompleter = Class.create(UI.Base, {
NAME: "S2.UI.Autocompleter",

/**
* new S2.UI.Autocompleter(element, options)
*
Expand All @@ -17,25 +17,25 @@
initialize: function(element, options) {
this.element = $(element);
var opt = this.setOptions(options);

UI.addClassNames(this.element, 'ui-widget ui-autocompleter');

this.input = this.element.down('input[type="text"]');

if (!this.input) {
this.input = new Element('input', { type: 'text' });
this.element.insert(this.input);
}

this.input.insert({ before: this.button });
this.input.setAttribute('autocomplete', 'off');

this.name = opt.parameterName || this.input.readAttribute('name');

if (opt.choices) {
this.choices = opt.choices.clone();
}

this.menu = new UI.Menu();
this.element.insert(this.menu.element);

Expand All @@ -47,17 +47,17 @@
top: (iLayout.get('top') + iLayout.get('margin-box-height')) + 'px'
});
}).bind(this).defer();

this.observers = {
blur: this._blur.bind(this),
keyup: this._keyup.bind(this),
keydown: this._keydown.bind(this),
selected: this._selected.bind(this)
};

this.addObservers();
},

addObservers: function() {
this.input.observe('blur', this.observers.blur);
this.input.observe('keyup', this.observers.keyup);
Expand All @@ -66,19 +66,19 @@
this.menu.observe('ui:menu:selected',
this.observers.selected);
},

_schedule: function() {
this._unschedule();
this._timeout = this._change.bind(this).delay(this.options.frequency);
},

_unschedule: function() {
if (this._timeout) window.clearTimeout(this._timeout);
},

_keyup: function(event) {
var value = this.input.getValue();

if (value) {
if (value.blank() || value.length < this.options.minCharacters) {
// Empty values mean the menu should be hidden and all timers
Expand All @@ -95,16 +95,16 @@
this.menu.close();
this._unschedule();
}

this._value = value;
},

_keydown: function(event) {
if (UI.modifierUsed(event)) return;
if (!this.menu.isOpen()) return;

var keyCode = event.keyCode || event.charCode;

switch (event.keyCode) {
case Event.KEY_UP:
this.menu.moveHighlight(-1);
Expand All @@ -128,80 +128,86 @@
break;
}
},

// TODO: Implement tokenizing.
_getInput: function() {
return this.input.getValue();
},

// TODO: Implement tokenizing.
_setInput: function(value) {
this.input.setValue(value);
},

_change: function() {
this.findChoices();
},

/**
* S2.UI.Autocompleter#findChoices() -> undefined
*
* Triggers an update of the choices presented to the user. If results
* come from the server, this is an asynchronous operation.
**/
findChoices: function() {
var value = this._getInput();
var choices = this.choices || [];
var value = this._getInput(),
choices = this.choices || [],
choiceValue,
choiceAttribute = this.options.choiceAttribute;

var results = choices.inject([], function(memo, choice) {
if (choice.toLowerCase().include(value.toLowerCase())) {
choiceValue = Object.isUndefined(choiceAttribute) ? choice : choice[choiceAttribute]
if (choiceValue.toLowerCase().include(value.toLowerCase())) {
memo.push(choice);
}
return memo;
});

this.setChoices(results);
},

setChoices: function(results) {
this.results = results;
this._updateMenu(results);
},

_updateMenu: function(results) {
var opt = this.options;

this.menu.clear();

// Build a case-insensitive regexp for highlighting the substring match.
var needle = new RegExp(RegExp.escape(this._value), 'i');
for (var i = 0, result, li, text; result = results[i]; i++) {
value = Object.isUndefined(opt.choiceAttribute) ? result : result[opt.choiceAttribute]

text = opt.highlightSubstring ?
result.replace(needle, "<b>$&</b>") :
result;
li = new Element('li').update(text);
value.replace(needle, "<b>$&</b>") :
value;

li = new Element('li').update(Object.isUndefined(opt.listTemplate) ? text : opt.listTemplate.evaluate({text: text, object: result}));

This comment has been minimized.

Copy link
@ZenCocoon

ZenCocoon Sep 3, 2010

Author Owner

Object.isTemplate would be sweet, shall a patch to Prototype be useful?

This comment has been minimized.

Copy link
@savetheclocktower

savetheclocktower Jan 23, 2011

Not sold on Object.isTemplate. Why not just do foo instanceof Template?

This comment has been minimized.

Copy link
@ZenCocoon

ZenCocoon Jan 24, 2011

Author Owner

Great point! Much better like that.

li.store('ui.autocompleter.value', result);
this.menu.addChoice(li);
}

if (results.length === 0) {
this.menu.close();
} else {
this.menu.open();
}
},

_moveMenuChoice: function(delta) {
var choices = this.list.down('li');
this._selectedIndex = (this._selectedIndex + delta).constrain(
-1, this.results.length - 1);

this._highlightMenuChoice();
},

_highlightMenuChoice: function(element) {
var choices = this.list.select('li'), index;

if (Object.isElement(element)) {
index = choices.indexOf(element);
} else if (Object.isNumber(element)) {
Expand All @@ -213,42 +219,42 @@
UI.removeClassNames(choices, 'ui-state-active');
if (index === -1 || index === null) return;
choices[index].addClassName('ui-state-active');

this._selectedIndex = index;
},

_selected: function(event) {
var memo = event.memo, li = memo.element;

if (li) {
var value = li.retrieve('ui.autocompleter.value');
var result = this.element.fire('ui:autocompleter:selected', {
instance: this,
value: value
});
if (result.stopped) return;
this._setInput(value);
this._setInput(Object.isUndefined(this.options.choiceAttribute) ? value : value[this.options.choiceAttribute]);
}
this.menu.close();
},

_blur: function(event) {
this._unschedule();
this.menu.close();
}
});

Object.extend(UI.Autocompleter, {
DEFAULT_OPTIONS: {
tokens: [],
frequency: 0.4,
minCharacters: 1,

highlightSubstring: true,

onShow: Prototype.K,
onHide: Prototype.K
}
});

})(S2.UI);
42 changes: 41 additions & 1 deletion test/functional/controls_autocompleter.html
Expand Up @@ -142,6 +142,46 @@ <h2>Standard Autocompleter</h2>
<li>The matching substring of a given choice <strong>should</strong> be bold.</li>
</ul>
</div> <!-- .description -->



<h2>Autocompleter using Objects as choices and Template to style list items</h2>

<span id="autocompleter2">
<input type="text" name="firstname" />
</span> <!-- #autocompleter2 -->

<script type="text/javascript">
var choices = [
{ client: { login: "madrobby", name: "Thomas Fuchs"} },
{ client: { login: "savetheclocktower", name: "Andrew Dupont"} },
{ client: { login: "RStankov", name: "Radoslav Stankov"} },
{ client: { login: "smith", name: "Nathan L Smith"} },
{ client: { login: "joe-loco", name: "Philipp Markovics"} },
{ client: { login: "kommen", name: "Dieter Komendera",} },
{ client: { login: "eric1234", name: "Eric Anderson"} },
{ client: { login: "sgruhier", name: "Sébastien Gruhier"} },
{ client: { login: "stepheneb", name: "Stephen Bannasch"} },
{ client: { login: "ZenCocoon", name: "Sébastien Grosjean"} },
{ client: { login: "TomK32", name: "Thomas R. Koll"} },
{ client: { login: "charettes", name: "Simon Charette"} },
{ client: { login: "rumble", name: "Mike Rumble"} }
];

new S2.UI.Autocompleter('autocompleter2', {
choices: choices.collect(function(o) { return o.client }),
choiceAttribute: 'name',
listTemplate: new Template('#{text} <em>(#{object.login})</em>')
});
</script>

<div class="description">
The choices are the scripty2 contributors at test's creation date. It should behave like a standard autocompleter, with a few exceptions:
<ul>
<li>It <strong>should</strong> accept an array of objects as choices</li>
<li>The list of completions <strong>should</strong> offer customized HTML items.</li>
<li>When selected, it <strong>should</strong> set the value with the right object attribute.</li>
<li>When <code>ui:autocompleter:selected</code> is fired, it <strong>should</strong> return the complete object as memo's value.</li>
</ul>
</div> <!-- .description -->
</body>
</html>

2 comments on commit 586102b

@savetheclocktower
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. Suggestions:

  • Can we call it choiceTemplate instead of listTemplate?
  • How about changing choiceAttribute to choiceValue and have it be a function? The default function could be Prototype.K or else a simple function that calls toString.
choiceValue: function(choice) { return choice.name; }

@ZenCocoon
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me, that makes things cleaner and even more flexible.

Please sign in to comment.