html/javascripts/controls.js in rails-0.13.0 vs html/javascripts/controls.js in rails-0.13.1
- old
+ new
@@ -1,6 +1,7 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
@@ -17,11 +18,10 @@
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
var children = $(element).childNodes;
var text = "";
var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");
@@ -35,62 +35,90 @@
}
return text;
}
-Ajax.Autocompleter = Class.create();
-Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
- initialize: function(element, update, url, options) {
+// Autocompleter.Base handles all the autocompletion functionality
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least,
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method
+// should get the text for which to provide autocompletion by
+// invoking this.getEntry(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: new Array (',', '\n') } which
+// enables autocompletion on multiple tokens. This is most
+// useful when one of the tokens is \n (a newline), as it
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+ base_initialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.has_focus = false;
this.changed = false;
this.active = false;
this.index = 0;
- this.entry_count = 0;
- this.url = url;
+ this.entry_count = 0;
- this.setOptions(options);
- this.options.asynchronous = true;
- this.options.onComplete = this.onComplete.bind(this)
+ if (this.setOptions)
+ this.setOptions(options);
+ else
+ this.options = {}
+
+ this.options.tokens = this.options.tokens || new Array();
this.options.frequency = this.options.frequency || 0.4;
this.options.min_chars = this.options.min_chars || 1;
- this.options.method = 'post';
-
- this.options.onShow = this.options.onShow ||
- function(element, update){
- if(!update.style.position || update.style.position=='absolute') {
- update.style.position = 'absolute';
+ this.options.onShow = this.options.onShow ||
+ function(element, update){
+ if(!update.style.position || update.style.position=='absolute') {
+ update.style.position = 'absolute';
var offsets = Position.cumulativeOffset(element);
update.style.left = offsets[0] + 'px';
update.style.top = (offsets[1] + element.offsetHeight) + 'px';
update.style.width = element.offsetWidth + 'px';
- }
- new Effect.Appear(update,{duration:0.3});
- };
+ }
+ new Effect.Appear(update,{duration:0.15});
+ };
this.options.onHide = this.options.onHide ||
- function(element, update){ new Effect.Fade(update,{duration:0.3}) };
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
-
if(this.options.indicator)
this.indicator = $(this.options.indicator);
+
+ if (typeof(this.options.tokens) == 'string')
+ this.options.tokens = new Array(this.options.tokens);
this.observer = null;
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
-
+
show: function() {
if(this.update.style.display=='none') this.options.onShow(this.element, this.update);
if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
- 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(apacity=0);" ' +
- 'src="javascript:;" frameborder="0" scrolling="no"></iframe>');
+ 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
@@ -109,55 +137,11 @@
},
stopIndicator: function() {
if(this.indicator) Element.hide(this.indicator);
},
-
- onObserverEvent: function() {
- this.changed = false;
- if(this.element.value.length>=this.options.min_chars) {
- this.startIndicator();
- this.options.parameters = this.options.callback ?
- this.options.callback(this.element, Form.Element.getValue(this.element)) :
- Form.Element.serialize(this.element);
- new Ajax.Request(this.url, this.options);
- } else {
- this.active = false;
- this.hide();
- }
- },
-
- addObservers: function(element) {
- Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
- Event.observe(element, "click", this.onClick.bindAsEventListener(this));
- },
-
- onComplete: function(request) {
- if(!this.changed && this.has_focus) {
- this.update.innerHTML = request.responseText;
- Element.cleanWhitespace(this.update);
- Element.cleanWhitespace(this.update.firstChild);
- if(this.update.firstChild && this.update.firstChild.childNodes) {
- this.entry_count =
- this.update.firstChild.childNodes.length;
- for (var i = 0; i < this.entry_count; i++) {
- entry = this.get_entry(i);
- entry.autocompleteIndex = i;
- this.addObservers(entry);
- }
- } else {
- this.entry_count = 0;
- }
-
- this.stopIndicator();
-
- this.index = 0;
- this.render();
- }
- },
-
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
@@ -253,9 +237,210 @@
},
select_entry: function() {
this.active = false;
value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
- this.element.value = value;
+ this.updateElement(value);
this.element.focus();
+ },
+
+ updateElement: function(value) {
+ var last_token_pos = this.findLastToken();
+ if (last_token_pos != -1) {
+ var new_value = this.element.value.substr(0, last_token_pos + 1);
+ var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/);
+ if (whitespace)
+ new_value += whitespace[0];
+ this.element.value = new_value + value;
+ } else {
+ this.element.value = value;
+ }
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.has_focus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.firstChild);
+
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
+ this.entry_count =
+ this.update.firstChild.childNodes.length;
+ for (var i = 0; i < this.entry_count; i++) {
+ entry = this.get_entry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entry_count = 0;
+ }
+
+ this.stopIndicator();
+
+ this.index = 0;
+ this.render();
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ if(this.getEntry().length>=this.options.min_chars) {
+ this.startIndicator();
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ getEntry: function() {
+ var token_pos = this.findLastToken();
+ if (token_pos != -1)
+ var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+ else
+ var ret = this.element.value;
+
+ return /\n/.test(ret) ? '' : ret;
+ },
+
+ findLastToken: function() {
+ var last_token_pos = -1;
+
+ for (var i=0; i<this.options.tokens.length; i++) {
+ var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]);
+ if (this_token_pos > last_token_pos)
+ last_token_pos = this_token_pos;
+ }
+ return last_token_pos;
}
-});
\ No newline at end of file
+}
+
+Ajax.Autocompleter = Class.create();
+Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),
+Object.extend(new Ajax.Base(), {
+ initialize: function(element, update, url, options) {
+ this.base_initialize(element, update, options);
+ this.options.asynchronous = true;
+ this.options.onComplete = this.onComplete.bind(this)
+ this.options.method = 'post';
+ this.options.defaultParams = this.options.parameters || null;
+ this.url = url;
+ },
+
+ getUpdatedChoices: function() {
+ entry = encodeURIComponent(this.element.name) + '=' +
+ encodeURIComponent(this.getEntry());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+
+}));
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partial_search - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option full_search to true (default: off).
+//
+// - full_search - Search anywhere in autocomplete array strings.
+//
+// - partial_chars - How many characters to enter before triggering
+// a partial match (unlike min_chars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignore_case - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+ initialize: function(element, update, array, options) {
+ this.base_initialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partial_search: true,
+ partial_chars: 2,
+ ignore_case: true,
+ full_search: false,
+ selector: function(instance) {
+ var ret = new Array(); // Beginning matches
+ var partial = new Array(); // Inside matches
+ var entry = instance.getEntry();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+ var elem = instance.options.array[i];
+ var found_pos = instance.options.ignore_case ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (found_pos != -1) {
+ if (found_pos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partial_chars &&
+ instance.options.partial_search && found_pos != -1) {
+ if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) {
+ partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" +
+ elem.substr(found_pos, entry.length) + "</strong>" + elem.substr(
+ found_pos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ found_pos = instance.options.ignore_case ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) :
+ elem.indexOf(entry, found_pos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || {});
+ }
+});