/**
* The autocompletion feature implemented with RightJS
*
* Home page: http://rightjs.org/ui/autocompleter
*
* @copyright (C) 2009 Nikolay V. Nemshilov aka St.
*/
if (!RightJS) { throw "Gimme RightJS. Please." };
/**
* The RightJS UI Autocompleter unit base class
*
* Copyright (C) Nikolay V. Nemshilov aka St.
*/
var Autocompleter = new Class(Observer, {
extend: {
EVENTS: $w('show hide update load select done'),
Options: {
url: document.location.href,
param: 'search',
method: 'get',
minLength: 1, // the minimal length when it starts work
threshold: 200, // the typing pause threshold
cache: true, // the use the results cache
local: null, // an optional local search results list
fxName: 'slide',
fxDuration: 'short',
spinner: 'native',
relName: 'autocompleter'
},
// scans the document for autocompletion fields
rescan: function(scope) {
var key = Autocompleter.Options.relName;
var reg = new RegExp('^'+key+'+\\[(.*?)\\]$');
($(scope)||document).select('input[rel^="'+key+'"]').each(function(input) {
if (!input.autocompleter) {
var data = input.get('data-'+key+'-options');
var options = Object.merge(eval('('+data+')'));
var match = input.get('rel').match(reg);
if (match) {
var url = match[1];
// if looks like a list of local options
if (url.match(/^['"].*?['"]$/)) {
options.local = eval('['+url+']');
} else if (!url.blank()) {
options.url = url;
}
}
new Autocompleter(input, options);
}
});
}
},
/**
* basic constructor
*
* @param mixed the input element reference, a string id or the element instance
* @param Object options
*/
initialize: function(input, options) {
this.$super(options);
// storing the callbacks so we could detach them later
this._watch = this.watch.bind(this);
this._hide = this.hide.bind(this);
this.input = $(input).onKeyup(this._watch).onBlur(this._hide);
this.container = $E('div', {'class': 'autocompleter'}).insertTo(this.input, 'after');
this.input.autocompleter = this;
},
// kills the autocompleter
destroy: function() {
this.input.stopObserving('keyup', this._watch).stopObserving('blur', this._hide);
delete(this.input.autocompleter);
return this;
},
// catching up with some additonal options
setOptions: function(options) {
this.$super(options);
// building the correct url template with a placeholder
if (!this.options.url.includes('%{search}')) {
this.options.url += (this.options.url.includes('?') ? '&' : '?') + this.options.param + '=%{search}';
}
},
// handles the list appearance
show: function() {
if (this.container.hidden()) {
var dims = this.input.dimensions();
this.container.setStyle({
top: (dims.top + dims.height) + 'px',
left: dims.left + 'px',
width: dims.width + 'px'
}).show(this.options.fxName, {
duration: this.options.fxDuration,
onFinish: this.fire.bind(this, 'show')
});
}
return Autocompleter.current = this;
},
// handles the list hidding
hide: function() {
if (this.container.visible()) {
this.container.hide();
this.fire.bind(this, 'hide');
}
Autocompleter.current = null;
return this;
},
// selects the next item on the list
prev: function() {
return this.select('prev', this.container.select('li').last());
},
// selects the next item on the list
next: function() {
return this.select('next', this.container.first('li'));
},
// marks it done
done: function(current) {
var current = current || this.container.first('li.current');
if (current) {
this.input.value = current.innerHTML.stripTags();
}
return this.fire('done').hide();
},
// protected
// works with the 'prev' and 'next' methods
select: function(what, fallback) {
var current = this.container.first('li.current');
if (current) {
current = current[what]('li') || current;
}
return this.fire('select', (current || fallback).radioClass('current'));
},
// receives the keyboard events out of the input element
watch: function(event) {
// skip the overlaping key codes that are already watched in the hooker.js
if ([27,37,38,39,40,13].include(event.keyCode)) return;
if (this.input.value.length >= this.options.minLength) {
if (this.timeout) {
this.timeout.cancel();
}
this.timeout = this.trigger.bind(this).delay(this.options.threshold);
} else {
return this.hide();
}
},
// triggers the actual action
trigger: function() {
this.timeout = null;
this.cache = this.cache || {};
var search = this.input.value;
if (search.length < this.options.minLength) return this.hide();
if (this.cache[search]) {
this.suggest(this.cache[search], search);
} else if (this.options.local) {
this.suggest(this.findLocal(search), search);
} else {
this.request = Xhr.load(this.options.url.replace('%{search}', encodeURIComponent(search)), {
method: this.options.method,
spinner: this.getSpinner(),
onComplete: function(response) {
this.fire('load').suggest(response.responseText, search);
}.bind(this)
});
}
},
// updates the suggestions list
suggest: function(result, search) {
this.container.update(result).select('li').each(function(li) {
// we reassiging the events each time so the were no doublecalls
li.onmouseover = function() { li.radioClass('current'); };
li.onmousedown = function() { this.done(li); }.bind(this);
}, this);
// saving the result in cache
if (this.options.cache) {
this.cache[search] = result;
}
return this.fire('update').show();
},
// performs the locals search
findLocal: function(search) {
var regexp = new RegExp("("+RegExp.escape(search)+")", 'ig');
return $E('ul').insert(
this.options.local.map(function(option) {
if (regexp.test(option)) {
return $E('li', {html:
option.replace(regexp, '$1')
});
}
}).compact()
);
},
// builds a native textual spinner if necessary
getSpinner: function() {
this._spinner = this._spinner || this.options.spinner;
// building the native spinner
if (this._spinner == 'native') {
this._spinner = $E('div', {
'class': 'autocompleter-spinner'
}).insertTo(this.input, 'after');
var dots = '123'.split('').map(function(i) {
return $E('div', {'class': 'dot-'+i, html: '»'});
});
(function() {
var dot = dots.shift();
dots.push(dot);
this._spinner.update(dot);
}.bind(this)).periodical(400);
}
// repositioning the native spinner
if (this.options.spinner == 'native') {
var dims = this.input.dimensions();
this._spinner.setStyle('visiblity: hidden').show();
this._spinner.setStyle({
visibility: 'visible',
top: (dims.top + 1) + 'px',
height: (dims.height - 2) + 'px',
lineHeight: (dims.height - 2) + 'px',
left: (dims.left + dims.width - this._spinner.offsetWidth - 1) + 'px'
}).hide();
}
return this._spinner;
}
});
/**
* The document events hooking
*
* Copyright (C) Nikolay V. Nemshilov aka St.
*/
document.on({
ready: function() {
Autocompleter.rescan();
},
// the autocompletion list navigation
keydown: function(event) {
if (Autocompleter.current) {
var name;
switch (event.keyCode) {
case 27: name = 'hide'; break;
case 37: name = 'prev'; break;
case 39: name = 'next'; break;
case 38: name = 'prev'; break;
case 40: name = 'next'; break;
case 13: name = 'done'; break;
}
if (name) {
Autocompleter.current[name]();
event.stop();
}
}
}
});
document.write("");