Autocomplete migrated to jquery-ui autocomplete

No longer bundled with the library (as jquery-ui is bundled with the
main software, including the autocomplete part).

Rewrites had to be made to migrate, as several API things had changed
when jquery-ui took over the library.

Currently (like before, right?) this only autocompletes in the end of
the current textarea. So you can't jump back in the middle of a text
and autocomplete a user or group. This is a serious deficiency, though
not a regression.
This commit is contained in:
Mikael Nordfeldth 2013-09-16 22:10:08 +02:00
parent d480ed42d1
commit 8935a2f866
16 changed files with 96 additions and 4958 deletions

View File

@ -1,53 +0,0 @@
(function(SN, $) {
var origInit = SN.Init.NoticeFormSetup;
SN.Init.NoticeFormSetup = function(form) {
origInit(form);
// Only attach to traditional-style forms
var textarea = form.find('.notice_data-text:first');
if (textarea.length == 0) {
return;
}
function fullName(row) {
if (typeof row.fullname == "string" && row.fullname != '') {
return row.nickname + ' (' + row.fullname + ')';
} else {
return row.nickname;
}
}
var apiUrl = $('#autocomplete-api').attr('data-url');
textarea.autocomplete(apiUrl, {
multiple: true,
multipleSeparator: " ",
minChars: 1,
formatItem: function(row, i, max){
row = eval("(" + row + ")");
// the display:inline is because our INSANE stylesheets
// override the standard display of all img tags for no
// good reason.
var div = $('<div><img style="display:inline; vertical-align: middle"> <span></span></div>')
.find('img').attr('src', row.avatar).end()
.find('span').text(fullName(row)).end()
return div.html();
},
formatMatch: function(row, i, max){
row = eval("(" + row + ")");
return row.nickname;
},
formatResult: function(row){
row = eval("(" + row + ")");
switch(row.type)
{
case 'user':
return '@' + row.nickname;
case 'group':
return '!' + row.nickname;
}
}
});
};
})(SN, jQuery);

View File

@ -43,15 +43,7 @@ class AutocompletePlugin extends Plugin
if (common_logged_in()) { if (common_logged_in()) {
$action->element('span', array('id' => 'autocomplete-api', $action->element('span', array('id' => 'autocomplete-api',
'data-url' => common_local_url('autocomplete'))); 'data-url' => common_local_url('autocomplete')));
$action->script($this->path('jquery-autocomplete/jquery.autocomplete.pack.js')); $action->script($this->path('js/autocomplete.go.js'));
$action->script($this->path('Autocomplete.js'));
}
}
function onEndShowStatusNetStyles($action)
{
if (common_logged_in()) {
$action->cssLink($this->path('jquery-autocomplete/jquery.autocomplete.css'));
} }
} }

View File

@ -22,8 +22,9 @@
* @category Plugin * @category Plugin
* @package StatusNet * @package StatusNet
* @author Craig Andrews <candrews@integralblue.com> * @author Craig Andrews <candrews@integralblue.com>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2008-2009 StatusNet, Inc. * @copyright 2008-2009 StatusNet, Inc.
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org * @copyright 2009-2013 Free Software Foundation, Inc http://www.fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/ * @link http://status.net/
*/ */
@ -40,11 +41,14 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
* @category Plugin * @category Plugin
* @package StatusNet * @package StatusNet
* @author Craig Andrews <candrews@integralblue.com> * @author Craig Andrews <candrews@integralblue.com>
* @author Mikael Nordfeldth <mmn@hethane.se>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/ * @link http://status.net/
*/ */
class AutocompleteAction extends Action class AutocompleteAction extends Action
{ {
protected $needLogin = true;
private $result; private $result;
/** /**
@ -80,12 +84,12 @@ class AutocompleteAction extends Action
{ {
return '"' . implode(':', array($this->arg('action'), return '"' . implode(':', array($this->arg('action'),
common_user_cache_hash(), common_user_cache_hash(),
crc32($this->arg('q')), //the actual string can have funny characters in we don't want showing up in the etag crc32($this->arg('term')), //the actual string can have funny characters in we don't want showing up in the etag
$this->arg('limit'), $this->arg('limit'),
$this->lastModified())) . '"'; $this->lastModified())) . '"';
} }
function prepare($args) protected function prepare($args)
{ {
// If we die, show short error messages. // If we die, show short error messages.
StatusNet::setApi(true); StatusNet::setApi(true);
@ -97,29 +101,30 @@ class AutocompleteAction extends Action
// TRANS: Client exception in autocomplete plugin. // TRANS: Client exception in autocomplete plugin.
throw new ClientException(_m('Access forbidden.'), true); throw new ClientException(_m('Access forbidden.'), true);
} }
$this->groups=array(); $this->groups=array();
$this->users=array(); $this->users=array();
$q = $this->arg('q'); $term = $this->arg('term');
$limit = $this->arg('limit'); $limit = $this->arg('limit');
if($limit > 200) $limit=200; //prevent DOS attacks if($limit > 200) $limit=200; //prevent DOS attacks
if(substr($q,0,1)=='@'){ if(substr($term,0,1)=='@'){
//user search //user search
$q=substr($q,1); $term=substr($term,1);
$user = new User(); $user = new User();
$user->limit($limit); $user->limit($limit);
$user->whereAdd('nickname like \'' . trim($user->escape($q), '\'') . '%\''); $user->whereAdd('nickname like \'' . trim($user->escape($term), '\'') . '%\'');
if($user->find()){ if($user->find()){
while($user->fetch()) { while($user->fetch()) {
$this->users[]=clone($user); $this->users[]=clone($user);
} }
} }
} }
if(substr($q,0,1)=='!'){ if(substr($term,0,1)=='!'){
//group search //group search
$q=substr($q,1); $term=substr($term,1);
$group = new User_group(); $group = new User_group();
$group->limit($limit); $group->limit($limit);
$group->whereAdd('nickname like \'' . trim($group->escape($q), '\'') . '%\''); $group->whereAdd('nickname like \'' . trim($group->escape($term), '\'') . '%\'');
if($group->find()){ if($group->find()){
while($group->fetch()) { while($group->fetch()) {
$this->groups[]=clone($group); $this->groups[]=clone($group);
@ -129,9 +134,10 @@ class AutocompleteAction extends Action
return true; return true;
} }
function handle($args) protected function handle()
{ {
parent::handle($args); parent::handle();
$results = array(); $results = array();
foreach($this->users as $user){ foreach($this->users as $user){
$profile = $user->getProfile(); $profile = $user->getProfile();
@ -143,8 +149,9 @@ class AutocompleteAction extends Action
$avatar = Avatar::defaultImage(AVATAR_MINI_SIZE); $avatar = Avatar::defaultImage(AVATAR_MINI_SIZE);
} }
$results[] = array( $results[] = array(
'nickname' => $user->nickname, 'value' => '@'.$profile->nickname,
'fullname'=> $profile->fullname, 'nickname' => $profile->nickname,
'label'=> $profile->getFancyName(),
'avatar' => $avatar, 'avatar' => $avatar,
'type' => 'user' 'type' => 'user'
); );
@ -157,14 +164,13 @@ class AutocompleteAction extends Action
$avatar = User_group::defaultLogo(AVATAR_MINI_SIZE); $avatar = User_group::defaultLogo(AVATAR_MINI_SIZE);
} }
$results[] = array( $results[] = array(
'value' => '!'.$group->nickname,
'nickname' => $group->nickname, 'nickname' => $group->nickname,
'fullname'=> $group->fullname, 'label'=> $group->getFancyName(),
'avatar' => $avatar, 'avatar' => $avatar,
'type' => 'group'); 'type' => 'group');
} }
foreach($results as $result) { print json_encode($results);
print json_encode($result) . "\n";
}
} }
/** /**

View File

@ -1,20 +0,0 @@
1.0.2
-----
* Fixed missing semicolon
1.0.1
-----
* Fixed element creation (<ul> to <ul/> and <li> to </li>)
* Fixed ac_even class (was ac_event)
* Fixed bgiframe usage: now its really optional
* Removed the blur-on-return workaround, added a less obtrusive one only for Opera
* Fixed hold cursor keys: Opera needs keypress, everyone else keydown to scroll through result list when holding cursor key
* Updated package to jQuery 1.2.5, removing dimensions
* Fixed multiple-mustMatch: Remove only the last term when no match is found
* Fixed multiple without mustMatch: Don't select the last active when no match is found (on tab/return)
* Fixed multiple cursor position: Put cursor at end of input after selecting a value
1.0
---
* First release.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

View File

@ -1,48 +0,0 @@
.ac_results {
padding: 0px;
border: 1px solid black;
background-color: white;
overflow: hidden;
z-index: 99999;
}
.ac_results ul {
width: 100%;
list-style-position: outside;
list-style: none;
padding: 0;
margin: 0;
}
.ac_results li {
margin: 0px;
padding: 2px 5px;
cursor: default;
display: block;
/*
if width will be 100% horizontal scrollbar will apear
when scroll mode will be used
*/
/*width: 100%;*/
font: menu;
font-size: 12px;
/*
it is very important, if line-height not setted or setted
in relative units scroll will be broken in firefox
*/
line-height: 16px;
overflow: hidden;
}
.ac_loading {
background: white url('indicator.gif') right center no-repeat;
}
.ac_odd {
background-color: #eee;
}
.ac_over {
background-color: #0A246A;
color: white;
}

View File

@ -1,759 +0,0 @@
/*
* Autocomplete - jQuery plugin 1.0.2
*
* Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
*
*/
;(function($) {
$.fn.extend({
autocomplete: function(urlOrData, options) {
var isUrl = typeof urlOrData == "string";
options = $.extend({}, $.Autocompleter.defaults, {
url: isUrl ? urlOrData : null,
data: isUrl ? null : urlOrData,
delay: isUrl ? $.Autocompleter.defaults.delay : 10,
max: options && !options.scroll ? 10 : 150
}, options);
// if highlight is set to false, replace it with a do-nothing function
options.highlight = options.highlight || function(value) { return value; };
// if the formatMatch option is not specified, then use formatItem for backwards compatibility
options.formatMatch = options.formatMatch || options.formatItem;
return this.each(function() {
new $.Autocompleter(this, options);
});
},
result: function(handler) {
return this.bind("result", handler);
},
search: function(handler) {
return this.trigger("search", [handler]);
},
flushCache: function() {
return this.trigger("flushCache");
},
setOptions: function(options){
return this.trigger("setOptions", [options]);
},
unautocomplete: function() {
return this.trigger("unautocomplete");
}
});
$.Autocompleter = function(input, options) {
var KEY = {
UP: 38,
DOWN: 40,
DEL: 46,
TAB: 9,
RETURN: 13,
ESC: 27,
COMMA: 188,
PAGEUP: 33,
PAGEDOWN: 34,
BACKSPACE: 8
};
// Create $ object for input element
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
var timeout;
var previousValue = "";
var cache = $.Autocompleter.Cache(options);
var hasFocus = 0;
var lastKeyPressCode;
var config = {
mouseDownOnSelect: false
};
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
var blockSubmit;
// prevent form submit in opera when selecting with return key
$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
if (blockSubmit) {
blockSubmit = false;
return false;
}
});
// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
// track last key pressed
lastKeyPressCode = event.keyCode;
switch(event.keyCode) {
case KEY.UP:
event.preventDefault();
if ( select.visible() ) {
select.prev();
} else {
onChange(0, true);
}
break;
case KEY.DOWN:
event.preventDefault();
if ( select.visible() ) {
select.next();
} else {
onChange(0, true);
}
break;
case KEY.PAGEUP:
event.preventDefault();
if ( select.visible() ) {
select.pageUp();
} else {
onChange(0, true);
}
break;
case KEY.PAGEDOWN:
event.preventDefault();
if ( select.visible() ) {
select.pageDown();
} else {
onChange(0, true);
}
break;
// matches also semicolon
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
case KEY.TAB:
case KEY.RETURN:
if( selectCurrent() ) {
// stop default to prevent a form submit, Opera needs special handling
event.preventDefault();
blockSubmit = true;
return false;
}
break;
case KEY.ESC:
select.hide();
break;
default:
clearTimeout(timeout);
timeout = setTimeout(onChange, options.delay);
break;
}
}).focus(function(){
// track whether the field has focus, we shouldn't process any
// results if the field no longer has focus
hasFocus++;
}).blur(function() {
hasFocus = 0;
if (!config.mouseDownOnSelect) {
hideResults();
}
}).click(function() {
// show select when clicking in a focused field
if ( hasFocus++ > 1 && !select.visible() ) {
onChange(0, true);
}
}).bind("search", function() {
// TODO why not just specifying both arguments?
var fn = (arguments.length > 1) ? arguments[1] : null;
function findValueCallback(q, data) {
var result;
if( data && data.length ) {
for (var i=0; i < data.length; i++) {
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
result = data[i];
break;
}
}
}
if( typeof fn == "function" ) fn(result);
else $input.trigger("result", result && [result.data, result.value]);
}
$.each(trimWords($input.val()), function(i, value) {
request(value, findValueCallback, findValueCallback);
});
}).bind("flushCache", function() {
cache.flush();
}).bind("setOptions", function() {
$.extend(options, arguments[1]);
// if we've updated the data, repopulate
if ( "data" in arguments[1] )
cache.populate();
}).bind("unautocomplete", function() {
select.unbind();
$input.unbind();
$(input.form).unbind(".autocomplete");
});
function selectCurrent() {
var selected = select.selected();
if( !selected )
return false;
var v = selected.result;
previousValue = v;
if ( options.multiple ) {
var words = trimWords($input.val());
if ( words.length > 1 ) {
v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
}
v += options.multipleSeparator;
}
$input.val(v);
hideResultsNow();
$input.trigger("result", [selected.data, selected.value]);
return true;
}
function onChange(crap, skipPrevCheck) {
if( lastKeyPressCode == KEY.DEL ) {
select.hide();
return;
}
var currentValue = $input.val();
if ( !skipPrevCheck && currentValue == previousValue )
return;
previousValue = currentValue;
currentValue = lastWord(currentValue);
if ( currentValue.length >= options.minChars) {
$input.addClass(options.loadingClass);
if (!options.matchCase)
currentValue = currentValue.toLowerCase();
request(currentValue, receiveData, hideResultsNow);
} else {
stopLoading();
select.hide();
}
};
function trimWords(value) {
if ( !value ) {
return [""];
}
var words = value.split( options.multipleSeparator );
var result = [];
$.each(words, function(i, value) {
if ( $.trim(value) )
result[i] = $.trim(value);
});
return result;
}
function lastWord(value) {
if ( !options.multiple )
return value;
var words = trimWords(value);
return words[words.length - 1];
}
// fills in the input box w/the first match (assumed to be the best match)
// q: the term entered
// sValue: the first matching result
function autoFill(q, sValue){
// autofill in the complete box w/the first match as long as the user hasn't entered in more data
// if the last user key pressed was backspace, don't autofill
if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
// fill in the value (keep the case the user has typed)
$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
// select the portion of the value not typed by the user (so the next character will erase)
$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
}
};
function hideResults() {
clearTimeout(timeout);
timeout = setTimeout(hideResultsNow, 200);
};
function hideResultsNow() {
var wasVisible = select.visible();
select.hide();
clearTimeout(timeout);
stopLoading();
if (options.mustMatch) {
// call search and run callback
$input.search(
function (result){
// if no value found, clear the input box
if( !result ) {
if (options.multiple) {
var words = trimWords($input.val()).slice(0, -1);
$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
}
else
$input.val( "" );
}
}
);
}
if (wasVisible)
// position cursor at end of input field
$.Autocompleter.Selection(input, input.value.length, input.value.length);
};
function receiveData(q, data) {
if ( data && data.length && hasFocus ) {
stopLoading();
select.display(data, q);
autoFill(q, data[0].value);
select.show();
} else {
hideResultsNow();
}
};
function request(term, success, failure) {
if (!options.matchCase)
term = term.toLowerCase();
var data = cache.load(term);
// recieve the cached data
if (data && data.length) {
success(term, data);
// if an AJAX url has been supplied, try loading the data now
} else if( (typeof options.url == "string") && (options.url.length > 0) ){
var extraParams = {
timestamp: +new Date()
};
$.each(options.extraParams, function(key, param) {
extraParams[key] = typeof param == "function" ? param() : param;
});
$.ajax({
// try to leverage ajaxQueue plugin to abort previous requests
mode: "abort",
// limit abortion to this input
port: "autocomplete" + input.name,
dataType: options.dataType,
url: options.url,
data: $.extend({
q: lastWord(term),
limit: options.max
}, extraParams),
success: function(data) {
var parsed = options.parse && options.parse(data) || parse(data);
cache.add(term, parsed);
success(term, parsed);
}
});
} else {
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
select.emptyList();
failure(term);
}
};
function parse(data) {
var parsed = [];
var rows = data.split("\n");
for (var i=0; i < rows.length; i++) {
var row = $.trim(rows[i]);
if (row) {
row = row.split("|");
parsed[parsed.length] = {
data: row,
value: row[0],
result: options.formatResult && options.formatResult(row, row[0]) || row[0]
};
}
}
return parsed;
};
function stopLoading() {
$input.removeClass(options.loadingClass);
};
};
$.Autocompleter.defaults = {
inputClass: "ac_input",
resultsClass: "ac_results",
loadingClass: "ac_loading",
minChars: 1,
delay: 400,
matchCase: false,
matchSubset: true,
matchContains: false,
cacheLength: 10,
max: 100,
mustMatch: false,
extraParams: {},
selectFirst: true,
formatItem: function(row) { return row[0]; },
formatMatch: null,
autoFill: false,
width: 0,
multiple: false,
multipleSeparator: ", ",
highlight: function(value, term) {
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
},
scroll: true,
scrollHeight: 180
};
$.Autocompleter.Cache = function(options) {
var data = {};
var length = 0;
function matchSubset(s, sub) {
if (!options.matchCase)
s = s.toLowerCase();
var i = s.indexOf(sub);
if (i == -1) return false;
return i == 0 || options.matchContains;
};
function add(q, value) {
if (length > options.cacheLength){
flush();
}
if (!data[q]){
length++;
}
data[q] = value;
}
function populate(){
if( !options.data ) return false;
// track the matches
var stMatchSets = {},
nullData = 0;
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
if( !options.url ) options.cacheLength = 1;
// track all options for minChars = 0
stMatchSets[""] = [];
// loop through the array and create a lookup structure
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
var rawValue = options.data[i];
// if rawValue is a string, make an array otherwise just reference the array
rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
var value = options.formatMatch(rawValue, i+1, options.data.length);
if ( value === false )
continue;
var firstChar = value.charAt(0).toLowerCase();
// if no lookup array for this character exists, look it up now
if( !stMatchSets[firstChar] )
stMatchSets[firstChar] = [];
// if the match is a string
var row = {
value: value,
data: rawValue,
result: options.formatResult && options.formatResult(rawValue) || value
};
// push the current match into the set list
stMatchSets[firstChar].push(row);
// keep track of minChars zero items
if ( nullData++ < options.max ) {
stMatchSets[""].push(row);
}
};
// add the data items to the cache
$.each(stMatchSets, function(i, value) {
// increase the cache size
options.cacheLength++;
// add to the cache
add(i, value);
});
}
// populate any existing data
setTimeout(populate, 25);
function flush(){
data = {};
length = 0;
}
return {
flush: flush,
add: add,
populate: populate,
load: function(q) {
if (!options.cacheLength || !length)
return null;
/*
* if dealing w/local data and matchContains than we must make sure
* to loop through all the data collections looking for matches
*/
if( !options.url && options.matchContains ){
// track all matches
var csub = [];
// loop through all the data grids for matches
for( var k in data ){
// don't search through the stMatchSets[""] (minChars: 0) cache
// this prevents duplicates
if( k.length > 0 ){
var c = data[k];
$.each(c, function(i, x) {
// if we've got a match, add it to the array
if (matchSubset(x.value, q)) {
csub.push(x);
}
});
}
}
return csub;
} else
// if the exact item exists, use it
if (data[q]){
return data[q];
} else
if (options.matchSubset) {
for (var i = q.length - 1; i >= options.minChars; i--) {
var c = data[q.substr(0, i)];
if (c) {
var csub = [];
$.each(c, function(i, x) {
if (matchSubset(x.value, q)) {
csub[csub.length] = x;
}
});
return csub;
}
}
}
return null;
}
};
};
$.Autocompleter.Select = function (options, input, select, config) {
var CLASSES = {
ACTIVE: "ac_over"
};
var listItems,
active = -1,
data,
term = "",
needsInit = true,
element,
list;
// Create results
function init() {
if (!needsInit)
return;
element = $("<div/>")
.hide()
.addClass(options.resultsClass)
.css("position", "absolute")
.appendTo(document.body);
list = $("<ul/>").appendTo(element).mouseover( function(event) {
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
$(target(event)).addClass(CLASSES.ACTIVE);
}
}).click(function(event) {
$(target(event)).addClass(CLASSES.ACTIVE);
select();
// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
input.focus();
return false;
}).mousedown(function() {
config.mouseDownOnSelect = true;
}).mouseup(function() {
config.mouseDownOnSelect = false;
});
if( options.width > 0 )
element.css("width", options.width);
needsInit = false;
}
function target(event) {
var element = event.target;
while(element && element.tagName != "LI")
element = element.parentNode;
// more fun with IE, sometimes event.target is empty, just ignore it then
if(!element)
return [];
return element;
}
function moveSelect(step) {
listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
movePosition(step);
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
if(options.scroll) {
var offset = 0;
listItems.slice(0, active).each(function() {
offset += this.offsetHeight;
});
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
} else if(offset < list.scrollTop()) {
list.scrollTop(offset);
}
}
};
function movePosition(step) {
active += step;
if (active < 0) {
active = listItems.size() - 1;
} else if (active >= listItems.size()) {
active = 0;
}
}
function limitNumberOfItems(available) {
return options.max && options.max < available
? options.max
: available;
}
function fillList() {
list.empty();
var max = limitNumberOfItems(data.length);
for (var i=0; i < max; i++) {
if (!data[i])
continue;
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
if ( formatted === false )
continue;
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
$.data(li, "ac_data", data[i]);
}
listItems = list.find("li");
if ( options.selectFirst ) {
listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
active = 0;
}
// apply bgiframe if available
if ( $.fn.bgiframe )
list.bgiframe();
}
return {
display: function(d, q) {
init();
data = d;
term = q;
fillList();
},
next: function() {
moveSelect(1);
},
prev: function() {
moveSelect(-1);
},
pageUp: function() {
if (active != 0 && active - 8 < 0) {
moveSelect( -active );
} else {
moveSelect(-8);
}
},
pageDown: function() {
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
moveSelect( listItems.size() - 1 - active );
} else {
moveSelect(8);
}
},
hide: function() {
element && element.hide();
listItems && listItems.removeClass(CLASSES.ACTIVE);
active = -1;
},
visible : function() {
return element && element.is(":visible");
},
current: function() {
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
},
show: function() {
var offset = $(input).offset();
element.css({
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
top: offset.top + input.offsetHeight,
left: offset.left
}).show();
if(options.scroll) {
list.scrollTop(0);
list.css({
maxHeight: options.scrollHeight,
overflow: 'auto'
});
if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
var listHeight = 0;
listItems.each(function() {
listHeight += this.offsetHeight;
});
var scrollbarsVisible = listHeight > options.scrollHeight;
list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
if (!scrollbarsVisible) {
// IE doesn't recalculate width when scrollbar disappears
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
}
}
}
},
selected: function() {
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
return selected && selected.length && $.data(selected[0], "ac_data");
},
emptyList: function (){
list && list.empty();
},
unbind: function() {
element && element.remove();
}
};
};
$.Autocompleter.Selection = function(field, start, end) {
if( field.createTextRange ){
var selRange = field.createTextRange();
selRange.collapse(true);
selRange.moveStart("character", start);
selRange.moveEnd("character", end);
selRange.select();
} else if( field.setSelectionRange ){
field.setSelectionRange(start, end);
} else {
if( field.selectionStart ){
field.selectionStart = start;
field.selectionEnd = end;
}
}
field.focus();
};
})(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,116 +0,0 @@
/**
* Ajax Queue Plugin
*
* Homepage: http://jquery.com/plugins/project/ajaxqueue
* Documentation: http://docs.jquery.com/AjaxQueue
*/
/**
<script>
$(function(){
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
});
</script>
<ul style="position: absolute; top: 5px; right: 5px;"></ul>
*/
/*
* Queued Ajax requests.
* A new Ajax request won't be started until the previous queued
* request has finished.
*/
/*
* Synced Ajax requests.
* The Ajax request will happen as soon as you call this method, but
* the callbacks (success/error/complete) won't fire until all previous
* synced requests have been completed.
*/
(function($) {
var ajax = $.ajax;
var pendingRequests = {};
var synced = [];
var syncedData = [];
$.ajax = function(settings) {
// create settings for compatibility with ajaxSetup
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
var port = settings.port;
switch(settings.mode) {
case "abort":
if ( pendingRequests[port] ) {
pendingRequests[port].abort();
}
return pendingRequests[port] = ajax.apply(this, arguments);
case "queue":
var _old = settings.complete;
settings.complete = function(){
if ( _old )
_old.apply( this, arguments );
jQuery([ajax]).dequeue("ajax" + port );;
};
jQuery([ ajax ]).queue("ajax" + port, function(){
ajax( settings );
});
return;
case "sync":
var pos = synced.length;
synced[ pos ] = {
error: settings.error,
success: settings.success,
complete: settings.complete,
done: false
};
syncedData[ pos ] = {
error: [],
success: [],
complete: []
};
settings.error = function(){ syncedData[ pos ].error = arguments; };
settings.success = function(){ syncedData[ pos ].success = arguments; };
settings.complete = function(){
syncedData[ pos ].complete = arguments;
synced[ pos ].done = true;
if ( pos == 0 || !synced[ pos-1 ] )
for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
synced[i] = null;
syncedData[i] = null;
}
};
}
return ajax.apply(this, arguments);
};
})(jQuery);

View File

@ -1,10 +0,0 @@
/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $
* $Rev: 2447 $
*
* Version 2.1.1
*/
(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,163 +0,0 @@
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> global settings needed for thickbox <<<-----------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
*{padding: 0; margin: 0;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox specific link and font settings <<<------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_window {
font: 12px Arial, Helvetica, sans-serif;
color: #333333;
}
#TB_secondLine {
font: 10px Arial, Helvetica, sans-serif;
color:#666666;
}
#TB_window a:link {color: #666666;}
#TB_window a:visited {color: #666666;}
#TB_window a:hover {color: #000;}
#TB_window a:active {color: #666666;}
#TB_window a:focus{color: #666666;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox settings <<<-----------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_overlay {
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
}
.TB_overlayMacFFBGHack {background: url(macFFBgHack.png) repeat;}
.TB_overlayBG {
background-color:#000;
filter:alpha(opacity=75);
-moz-opacity: 0.75;
opacity: 0.75;
}
* html #TB_overlay { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_window {
position: fixed;
background: #ffffff;
z-index: 102;
color:#000000;
display:none;
border: 4px solid #525252;
text-align:left;
top:50%;
left:50%;
}
* html #TB_window { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_window img#TB_Image {
display:block;
margin: 15px 0 0 15px;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-top: 1px solid #666;
border-left: 1px solid #666;
}
#TB_caption{
height:25px;
padding:7px 30px 10px 25px;
float:left;
}
#TB_closeWindow{
height:25px;
padding:11px 25px 10px 0;
float:right;
}
#TB_closeAjaxWindow{
padding:7px 10px 5px 0;
margin-bottom:1px;
text-align:right;
float:right;
}
#TB_ajaxWindowTitle{
float:left;
padding:7px 0 5px 10px;
margin-bottom:1px;
}
#TB_title{
background-color:#e8e8e8;
height:27px;
}
#TB_ajaxContent{
clear:both;
padding:2px 15px 15px 15px;
overflow:auto;
text-align:left;
line-height:1.4em;
}
#TB_ajaxContent.TB_modal{
padding:15px;
}
#TB_ajaxContent p{
padding:5px 0px 5px 0px;
}
#TB_load{
position: fixed;
display:none;
height:13px;
width:208px;
z-index:103;
top: 50%;
left: 50%;
margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */
}
* html #TB_load { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_HideSelect{
z-index:99;
position:fixed;
top: 0;
left: 0;
background-color:#fff;
border:none;
filter:alpha(opacity=0);
-moz-opacity: 0;
opacity: 0;
height:100%;
width:100%;
}
* html #TB_HideSelect { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_iframeContent{
clear:both;
border:none;
margin-bottom:-1px;
margin-top:1px;
_margin-bottom:1px;
}

View File

@ -1,166 +0,0 @@
TODO
- test formatItem implementation that returns (clickable) anchors
- bug: handle del key; eg. type a letter, remove it using del, type same letter again: nothing happens
- handle up/down keys in textarea (prevent default while select is open?)
- docs: max:0 works, too, "removing" it(??)
- fix ac_loading/options.loadingClass
- support/enable request urls like foo/bar/10 instead of foo/q=10
- urlencode request term before passing to $.ajax/data; evaluate why $.ajax doesn't handle that itself, if at all; try with umlauts, russian/danish/chinese characeters (see validate)
- test what happens when an element gets focused programmatically (maybe even before then autcomplete is applied)
- check if blur on selecting can be removed
- fix keyhandling to ignore metakeys, eg. shift; especially important for chinese characters that need more then one key
- enhance mustMatch: provide event/callback when a value gets deleted
- handle tab key different then enter, eg. don't blur field or prevent default, just let it move on; in any case, no need to blur the field when selecting a value via tab, unlike return
- prevent redundant requests on
- superstring returned no result, no need to query again for substring, eg. pete returned nothing, peter won't either
- previous query mustn't be requested again, eg. pete returns 10 lines, peter nothing, backspace to pete should get the 10 lines from cache (may need TimeToLive setting for cache to invalidate it)
- incorporate improvements and suggestions by Hector: http://beta.winserver.com/public/test/MultiSuggestTest.wct
- json support: An optional JSON format, that assumes a certain JSON format as default and just looks for a dataType "json" to be activated; [records], where each record is { id:String, label:String, moreOptionalValues... }
- accept callback as first argument to let users implement their own dynamic data (no caching) - consider async API
- allow users to keep their incomplete value when pressing tab, just mimic the default-browser-autocomplete: tab doesn't select any proposed value -> tab closes the select and works normal otherwise
- small bug in your autocomplete, When setting autoFill:true I would expect formatResult to be called on autofill, it seems not to be the case.
- add a callback to allow decoding the response
- allow modification of not-last value in multiple-fields
@option Number size Limit the number of items to show at once. Default:
@option Function parse - TEST AND DOCUMENT ME
- add option to display selectbox on focus
$input.bind("show", function() {
if ( !select.visible() ) {
onChange(0, true);
}
});
- reference: http://capxous.com/
- add "try ..." hints to demo
- check out demos
- reference: http://createwebapp.com/demo/
- add option to hide selectbox when no match is found - see comment by Ian on plugin page (14. Juli 2007 04:31)
- add example for reinitializing an autocomplete using unbind()
- Add option to pass through additional arguments to $.ajax, like type to use POST instead of GET
- I found out that the problem with UTF-8 not being correctly sent can be solved on the server side by applying (PHP) rawurldecode() function, which decodes the Unicode characters sent by GET method and therefore URL-encoded.
-> add that hint to docs and examples
But I am trying this with these three values: “foo bar”, “foo foo”, and “foo far”, and if I enter “b” (or “ba”) nothing matches, if I enter “f” all three do match, and if I enter “fa” the last one matches.
The problem seems to be that the cache is implemented with a first-character hashtable, so only after matching the first character, the latter ones are searched for.
xml example:
<script type="text/javascript">
function parseXML(data) {
var results = [];
var branches = $(data).find('item');
$(branches).each(function() {
var text = $.trim($(this).find('text').text());
var value = $.trim($(this).find('value').text());
//console.log(text);
//console.log(value);
results[results.length] = {'data': this, 'result': value, 'value': text};
});
$(results).each(function() {
//console.log('value', this.value);
//console.log('text', this.text);
});
//console.log(results);
return results;
};
$(YourOojHere).autocomplete(SERVER_AJAX_URL, {parse: parseXML});
</script>
<?xml version="1.0"?>
<ajaxresponse>
<item>
<text>
<![CDATA[<b>FreeNode:</b> irc.freenode.net:6667]]>
</text>
<value><![CDATA[irc.freenode.net:6667]]></value>
</item><item>
<text>
<![CDATA[<b>irc.oftc.net</b>:6667]]>
</text>
<value><![CDATA[irc.oftc.net:6667]]></value>
</item><item>
<text>
<![CDATA[<b>irc.undernet.org</b>:6667]]>
</text>
<value><![CDATA[irc.undernet.org:6667]]></value>
</item>
</ajaxresponse>
Hi all,
I use Autocomplete 1.0 Alpha mostly for form inputs bound to foreign
key columns. For instance I have a user_position table with two
columns: user_id and position_id. On new appointment form I have two
autocomplete text inputs with the following code:
<input type="text" id="user_id" class="ac_input" tabindex="1" />
<input type="text" id="position_id" class="ac_input" tabindex="2" />
As you can see the inputs do not have a name attribute, and when the
form is submitted their values are not sent, which is all right since
they will contain strings like:
'John Doe'
'Sales Manager'
whereas our backend expects something like:
23
14
which are the user_id for John Doe and position_id for Sales Manager.
To send these values I have two hidden inputs in the form like this:
<input type="hidden" name="user_id" value="">
<input type="hidden" name="position_id" value="">
Also I have the following code in the $().ready function:
$("#user_id").result(function(event, data, formatted) {
$("input[@name=user_id]").val(data[1]);
});
$("#position_id").result(function(event, data, formatted) {
$("input[@name=position_id]").val(data[1]);
});
As could be seen these functions stuff user_id and position_id values
(in our example 23 and 14) into the hidden inputs, and when the form
is submitted these values are sent:
user_id = 23
position_id = 14
The backend script then takes care of adding a record to our
user_position table containing those values.
I wonder how could the plugin code be modified to simplify the setup
by taking care of adding hidden inputs and updating the value of
hidden inputs as default behavior. I have successfully attempted a
simpler solution - writing a wrapper to perform these additional tasks
and invoke autocomplete as well. I hope my intention is clear enough,
if not, this is exactly the expected outcome:
Before:
<script type="text/javascript"
src="jquery.autocomplete-modified.js"></script>
<input type="text" name="user_id" class="ac_input" tabindex="1" />
After:
<input type="text" id="user_id" class="ac_input" tabindex="1" />
<input type="hidden" name="user_id" value="23">
Last word, I know this looks like a tall order, and I do not hope
someone will make a complete working mod for me, but rather would very
much appreciate helpful advise and directions.
Many thanks in advance
Majid

View File

@ -0,0 +1,71 @@
/** Init for Autocomplete (requires jquery-ui)
*
* @package Plugin
* @author Mikael Nordfeldth <mmn@hethane.se>
* @copyright 2013 Free Software Foundation, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
*/
var origInit = SN.Init.NoticeFormSetup;
SN.Init.NoticeFormSetup = function(form) {
origInit(form);
// Only attach to traditional-style forms
var textarea = form.find('.notice_data-text:first');
if (textarea.length == 0) {
return;
}
function termSplit(val) {
return val.split(/ \s*/);
}
function extractLast( term ) {
return termSplit(term).pop();
}
var apiUrl = $('#autocomplete-api').attr('data-url');
// migrated "multiple" and "multipleSeparator" from
// http://www.learningjquery.com/2010/06/autocomplete-migration-guide
textarea
.bind('keydown', function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).data( "ui-autocomplete" ).menu.active ) {
event.preventDefault();
}
})
.autocomplete({
minLength: 1, // 1 is default
source: function (request, response) {
$.getJSON( apiUrl, {
term: extractLast(request.term)
}, response );
},
search: function () {
// custom minLength, we though we match the 1 below
var term = extractLast(this.value);
if (term.length <= 1) {
return false;
}
},
focus: function () {
// prevent value inserted on focus
return false;
},
select: function (event, ui) {
var terms = termSplit(this.value);
terms.pop(); // remove latest term
terms.push(ui.item.value); // insert
terms.push(''); // empty element, for the join()
this.value = terms.join(' ');
return false;
},
})
.data('ui-autocomplete')._renderItem = function (ul, item) {
return $('<li></li>')
.data('ui-autocomplete-item', item)
.append('<a><img style="display:inline; vertical-align: middle"><span /></a>')
.find('img').attr('src', item.avatar).end()
.find('span').text(item.label).end()
.appendTo(ul);
};
};