Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

add swWidgetFormJQueryMultiAutocompleter

git-svn-id: http://svn.symfony-project.com/plugins/swToolboxPlugin/sf1.1/trunk@13671 ee427ae8-e902-0410-961c-c3ed070cd9f9
  • Loading branch information...
commit b9c7cc6f1eba96aa029caeb59e9003c46e43f381 1 parent 6c52e07
Thomas authored
19 LICENSE
... ... @@ -0,0 +1,19 @@
  1 +Copyright (c) 2008 Thomas Rabaix
  2 +
  3 +Permission is hereby granted, free of charge, to any person obtaining a copy
  4 +of this software and associated documentation files (the "Software"), to deal
  5 +in the Software without restriction, including without limitation the rights
  6 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7 +copies of the Software, and to permit persons to whom the Software is furnished
  8 +to do so, subject to the following conditions:
  9 +
  10 +The above copyright notice and this permission notice shall be included in all
  11 +copies or substantial portions of the Software.
  12 +
  13 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  19 +THE SOFTWARE.
34 LICENSE.autocomplete
... ... @@ -0,0 +1,34 @@
  1 +swWidgetFormJQueryMultiAutocompleter.php is based on sfWidgetFormJQueryAutocompleter from
  2 +sfFormExtraPlugin available at http://www.symfony-project.org/plugins/sfFormExtraPlugin
  3 +
  4 +Copyright (c) 2008 Fabien Potencier
  5 +
  6 +Permission is hereby granted, free of charge, to any person obtaining a copy
  7 +of this software and associated documentation files (the "Software"), to deal
  8 +in the Software without restriction, including without limitation the rights
  9 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10 +copies of the Software, and to permit persons to whom the Software is furnished
  11 +to do so, subject to the following conditions:
  12 +
  13 +The above copyright notice and this permission notice shall be included in all
  14 +copies or substantial portions of the Software.
  15 +
  16 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22 +THE SOFTWARE.
  23 +
  24 +
  25 +---
  26 +
  27 +jquery.autocompleter.js
  28 +
  29 +Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  30 +
  31 +Dual licensed under the MIT and GPL licenses:
  32 + http://www.opensource.org/licenses/mit-license.php
  33 + http://www.gnu.org/licenses/gpl.html
  34 +
110 lib/widgets/swWidgetFormJQueryMultiAutocompleter.php
... ... @@ -0,0 +1,110 @@
  1 +<?php
  2 +
  3 +/*
  4 + * This file is part of the swToolboxPlugin package.
  5 + * (c) Thomas Rabaix <thomas.rabaix@soleoweb.com>
  6 + * adapted from the work of Fabien Potencier <fabien.potencier@symfony-project.com>
  7 + *
  8 + * For the full copyright and license information, please view the LICENSE
  9 + * file that was distributed with this source code.
  10 + */
  11 +
  12 +/**
  13 + * swWidgetFormJQueryMultiAutocompleter represents an autocompleter input widget rendered by JQuery.
  14 + *
  15 + * This autocompleter can be use to display multiple values.
  16 + *
  17 + * This widget needs JQuery to work.
  18 + *
  19 + * You also need to include the JavaScripts and stylesheets files returned by the getJavaScripts()
  20 + * and getStylesheets() methods.
  21 + *
  22 + * If you use symfony 1.2, it can be done automatically for you.
  23 + *
  24 + * @package swToolboxPlugin
  25 + * @subpackage widgets
  26 + * @author Thomas Rabaix <thomas.rabaix@soleoweb.com>
  27 + * @version SVN: $Id$
  28 + */
  29 +class swWidgetFormJQueryMultiAutocompleter extends sfWidgetFormInput
  30 +{
  31 + /**
  32 + * Configures the current widget.
  33 + *
  34 + * Available options:
  35 + *
  36 + * * url: The URL to call to get the choices to use (required)
  37 + * * config: A JavaScript array that configures the JQuery autocompleter widget
  38 + *
  39 + * @param array $options An array of options
  40 + * @param array $attributes An array of default HTML attributes
  41 + *
  42 + * @see sfWidgetForm
  43 + */
  44 + protected function configure($options = array(), $attributes = array())
  45 + {
  46 + $this->addRequiredOption('url');
  47 + $this->addOption('config', '{ }');
  48 +
  49 + parent::configure($options, $attributes);
  50 + }
  51 +
  52 + /**
  53 + * @param string $name The element name
  54 + * @param string $value The value displayed in this widget
  55 + * @param array $attributes An array of HTML attributes to be merged with the default HTML attributes
  56 + * @param array $errors An array of errors for the field
  57 + *
  58 + * @return string An HTML tag string
  59 + *
  60 + * @see sfWidgetForm
  61 + */
  62 + public function render($name, $value = null, $attributes = array(), $errors = array())
  63 + {
  64 + return $this->renderTag('input', array('type' => 'hidden', 'name' => $name, 'value' => $value)).
  65 + parent::render('autocomplete_'.$name, $value, $attributes, $errors).
  66 + sprintf(<<<EOF
  67 +<script type="text/javascript">
  68 + \$("#%s")
  69 + .autocomplete('%s', \$.extend({}, {
  70 + dataType: 'json',
  71 + multiple: true,
  72 + parse: function(data) {
  73 + var parsed = [];
  74 + for (key in data) {
  75 + parsed[parsed.length] = { data: [ data[key], key ], value: data[key], result: data[key] };
  76 + }
  77 + return parsed;
  78 + }
  79 + }, %s))
  80 + .result(function(event, data) { \$("#%s").val(jQuery(event.target).val()); });
  81 +</script>
  82 +EOF
  83 + ,
  84 + $this->generateId('autocomplete_'.$name),
  85 + $this->getOption('url'),
  86 + $this->getOption('config'),
  87 + $this->generateId($name)
  88 + );
  89 + }
  90 +
  91 + /**
  92 + * Gets the stylesheet paths associated with the widget.
  93 + *
  94 + * @return array An array of stylesheet paths
  95 + */
  96 + public function getStylesheets()
  97 + {
  98 + return array('/swToolboxPlugin/js/autocompleter/jquery.autocompleter.css' => 'print');
  99 + }
  100 +
  101 + /**
  102 + * Gets the JavaScript paths associated with the widget.
  103 + *
  104 + * @return array An array of JavaScript paths
  105 + */
  106 + public function getJavascripts()
  107 + {
  108 + return array('/swToolboxPlugin/js/autocompleter/jquery.autocompleter.js');
  109 + }
  110 +}
BIN  web/js/autocompleter/indicator.gif
48 web/js/autocompleter/jquery.autocompleter.css
... ... @@ -0,0 +1,48 @@
  1 +.ac_results {
  2 + padding: 0px;
  3 + border: 1px solid black;
  4 + background-color: white;
  5 + overflow: hidden;
  6 + z-index: 99999;
  7 +}
  8 +
  9 +.ac_results ul {
  10 + width: 100%;
  11 + list-style-position: outside;
  12 + list-style: none;
  13 + padding: 0;
  14 + margin: 0;
  15 +}
  16 +
  17 +.ac_results li {
  18 + margin: 0px;
  19 + padding: 2px 5px;
  20 + cursor: default;
  21 + display: block;
  22 + /*
  23 + if width will be 100% horizontal scrollbar will apear
  24 + when scroll mode will be used
  25 + */
  26 + /*width: 100%;*/
  27 + font: menu;
  28 + font-size: 12px;
  29 + /*
  30 + it is very important, if line-height not setted or setted
  31 + in relative units scroll will be broken in firefox
  32 + */
  33 + line-height: 16px;
  34 + overflow: hidden;
  35 +}
  36 +
  37 +.ac_loading {
  38 + background: white url('indicator.gif') right center no-repeat;
  39 +}
  40 +
  41 +.ac_odd {
  42 + background-color: #eee;
  43 +}
  44 +
  45 +.ac_over {
  46 + background-color: #0A246A;
  47 + color: white;
  48 +}
762 web/js/autocompleter/jquery.autocompleter.js
... ... @@ -0,0 +1,762 @@
  1 +/*
  2 + * Autocomplete - jQuery plugin 1.1pre
  3 + *
  4 + * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  5 + *
  6 + * Dual licensed under the MIT and GPL licenses:
  7 + * http://www.opensource.org/licenses/mit-license.php
  8 + * http://www.gnu.org/licenses/gpl.html
  9 + *
  10 + * Revision: $Id: jquery.autocomplete.js 5785 2008-07-12 10:37:33Z joern.zaefferer $
  11 + *
  12 + */
  13 +
  14 +;(function($) {
  15 +
  16 +$.fn.extend({
  17 + autocomplete: function(urlOrData, options) {
  18 + var isUrl = typeof urlOrData == "string";
  19 + options = $.extend({}, $.Autocompleter.defaults, {
  20 + url: isUrl ? urlOrData : null,
  21 + data: isUrl ? null : urlOrData,
  22 + delay: isUrl ? $.Autocompleter.defaults.delay : 10,
  23 + max: options && !options.scroll ? 10 : 150
  24 + }, options);
  25 +
  26 + // if highlight is set to false, replace it with a do-nothing function
  27 + options.highlight = options.highlight || function(value) { return value; };
  28 +
  29 + // if the formatMatch option is not specified, then use formatItem for backwards compatibility
  30 + options.formatMatch = options.formatMatch || options.formatItem;
  31 +
  32 + return this.each(function() {
  33 + new $.Autocompleter(this, options);
  34 + });
  35 + },
  36 + result: function(handler) {
  37 + return this.bind("result", handler);
  38 + },
  39 + search: function(handler) {
  40 + return this.trigger("search", [handler]);
  41 + },
  42 + flushCache: function() {
  43 + return this.trigger("flushCache");
  44 + },
  45 + setOptions: function(options){
  46 + return this.trigger("setOptions", [options]);
  47 + },
  48 + unautocomplete: function() {
  49 + return this.trigger("unautocomplete");
  50 + }
  51 +});
  52 +
  53 +$.Autocompleter = function(input, options) {
  54 +
  55 + var KEY = {
  56 + UP: 38,
  57 + DOWN: 40,
  58 + DEL: 46,
  59 + TAB: 9,
  60 + RETURN: 13,
  61 + ESC: 27,
  62 + COMMA: 188,
  63 + PAGEUP: 33,
  64 + PAGEDOWN: 34,
  65 + BACKSPACE: 8
  66 + };
  67 +
  68 + // Create $ object for input element
  69 + var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  70 +
  71 + var timeout;
  72 + var previousValue = "";
  73 + var cache = $.Autocompleter.Cache(options);
  74 + var hasFocus = 0;
  75 + var lastKeyPressCode;
  76 + var config = {
  77 + mouseDownOnSelect: false
  78 + };
  79 + var select = $.Autocompleter.Select(options, input, selectCurrent, config);
  80 +
  81 + var blockSubmit;
  82 +
  83 + // prevent form submit in opera when selecting with return key
  84 + $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
  85 + if (blockSubmit) {
  86 + blockSubmit = false;
  87 + return false;
  88 + }
  89 + });
  90 +
  91 + // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
  92 + $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
  93 + // track last key pressed
  94 + lastKeyPressCode = event.keyCode;
  95 + switch(event.keyCode) {
  96 +
  97 + case KEY.UP:
  98 + event.preventDefault();
  99 + if ( select.visible() ) {
  100 + select.prev();
  101 + } else {
  102 + onChange(0, true);
  103 + }
  104 + break;
  105 +
  106 + case KEY.DOWN:
  107 + event.preventDefault();
  108 + if ( select.visible() ) {
  109 + select.next();
  110 + } else {
  111 + onChange(0, true);
  112 + }
  113 + break;
  114 +
  115 + case KEY.PAGEUP:
  116 + event.preventDefault();
  117 + if ( select.visible() ) {
  118 + select.pageUp();
  119 + } else {
  120 + onChange(0, true);
  121 + }
  122 + break;
  123 +
  124 + case KEY.PAGEDOWN:
  125 + event.preventDefault();
  126 + if ( select.visible() ) {
  127 + select.pageDown();
  128 + } else {
  129 + onChange(0, true);
  130 + }
  131 + break;
  132 +
  133 + // matches also semicolon
  134 + case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
  135 + case KEY.TAB:
  136 + case KEY.RETURN:
  137 + if( selectCurrent() ) {
  138 + // stop default to prevent a form submit, Opera needs special handling
  139 + event.preventDefault();
  140 + blockSubmit = true;
  141 + return false;
  142 + }
  143 + break;
  144 +
  145 + case KEY.ESC:
  146 + select.hide();
  147 + break;
  148 +
  149 + default:
  150 + clearTimeout(timeout);
  151 + timeout = setTimeout(onChange, options.delay);
  152 + break;
  153 + }
  154 + }).focus(function(){
  155 + // track whether the field has focus, we shouldn't process any
  156 + // results if the field no longer has focus
  157 + hasFocus++;
  158 + }).blur(function() {
  159 + hasFocus = 0;
  160 + if (!config.mouseDownOnSelect) {
  161 + hideResults();
  162 + }
  163 + }).click(function() {
  164 + // show select when clicking in a focused field
  165 + if ( hasFocus++ > 1 && !select.visible() ) {
  166 + onChange(0, true);
  167 + }
  168 + }).bind("search", function() {
  169 + // TODO why not just specifying both arguments?
  170 + var fn = (arguments.length > 1) ? arguments[1] : null;
  171 + function findValueCallback(q, data) {
  172 + var result;
  173 + if( data && data.length ) {
  174 + for (var i=0; i < data.length; i++) {
  175 + if( data[i].result.toLowerCase() == q.toLowerCase() ) {
  176 + result = data[i];
  177 + break;
  178 + }
  179 + }
  180 + }
  181 + if( typeof fn == "function" ) fn(result);
  182 + else $input.trigger("result", result && [result.data, result.value]);
  183 + }
  184 + $.each(trimWords($input.val()), function(i, value) {
  185 + request(value, findValueCallback, findValueCallback);
  186 + });
  187 + }).bind("flushCache", function() {
  188 + cache.flush();
  189 + }).bind("setOptions", function() {
  190 + $.extend(options, arguments[1]);
  191 + // if we've updated the data, repopulate
  192 + if ( "data" in arguments[1] )
  193 + cache.populate();
  194 + }).bind("unautocomplete", function() {
  195 + select.unbind();
  196 + $input.unbind();
  197 + $(input.form).unbind(".autocomplete");
  198 + });
  199 +
  200 +
  201 + function selectCurrent() {
  202 + var selected = select.selected();
  203 + if( !selected )
  204 + return false;
  205 +
  206 + var v = selected.result;
  207 + previousValue = v;
  208 +
  209 + if ( options.multiple ) {
  210 + var words = trimWords($input.val());
  211 + if ( words.length > 1 ) {
  212 + v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
  213 + }
  214 + v += options.multipleSeparator;
  215 + }
  216 +
  217 + $input.val(v);
  218 + hideResultsNow();
  219 + $input.trigger("result", [selected.data, selected.value]);
  220 + return true;
  221 + }
  222 +
  223 + function onChange(crap, skipPrevCheck) {
  224 + if( lastKeyPressCode == KEY.DEL ) {
  225 + select.hide();
  226 + return;
  227 + }
  228 +
  229 + var currentValue = $input.val();
  230 +
  231 + if ( !skipPrevCheck && currentValue == previousValue )
  232 + return;
  233 +
  234 + previousValue = currentValue;
  235 +
  236 + currentValue = lastWord(currentValue);
  237 + if ( currentValue.length >= options.minChars) {
  238 + $input.addClass(options.loadingClass);
  239 + if (!options.matchCase)
  240 + currentValue = currentValue.toLowerCase();
  241 + request(currentValue, receiveData, hideResultsNow);
  242 + } else {
  243 + stopLoading();
  244 + select.hide();
  245 + }
  246 + };
  247 +
  248 + function trimWords(value) {
  249 + if ( !value ) {
  250 + return [""];
  251 + }
  252 + var words = value.split( options.multipleSeparator );
  253 + var result = [];
  254 + $.each(words, function(i, value) {
  255 + if ( $.trim(value) )
  256 + result[i] = $.trim(value);
  257 + });
  258 + return result;
  259 + }
  260 +
  261 + function lastWord(value) {
  262 + if ( !options.multiple )
  263 + return value;
  264 + var words = trimWords(value);
  265 + return words[words.length - 1];
  266 + }
  267 +
  268 + // fills in the input box w/the first match (assumed to be the best match)
  269 + // q: the term entered
  270 + // sValue: the first matching result
  271 + function autoFill(q, sValue){
  272 + // autofill in the complete box w/the first match as long as the user hasn't entered in more data
  273 + // if the last user key pressed was backspace, don't autofill
  274 + if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
  275 + // fill in the value (keep the case the user has typed)
  276 + $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
  277 + // select the portion of the value not typed by the user (so the next character will erase)
  278 + $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
  279 + }
  280 + };
  281 +
  282 + function hideResults() {
  283 + clearTimeout(timeout);
  284 + timeout = setTimeout(hideResultsNow, 200);
  285 + };
  286 +
  287 + function hideResultsNow() {
  288 + var wasVisible = select.visible();
  289 + select.hide();
  290 + clearTimeout(timeout);
  291 + stopLoading();
  292 + if (options.mustMatch) {
  293 + // call search and run callback
  294 + $input.search(
  295 + function (result){
  296 + // if no value found, clear the input box
  297 + if( !result ) {
  298 + if (options.multiple) {
  299 + var words = trimWords($input.val()).slice(0, -1);
  300 + $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
  301 + }
  302 + else
  303 + $input.val( "" );
  304 + }
  305 + }
  306 + );
  307 + }
  308 + if (wasVisible)
  309 + // position cursor at end of input field
  310 + $.Autocompleter.Selection(input, input.value.length, input.value.length);
  311 + };
  312 +
  313 + function receiveData(q, data) {
  314 + if ( data && data.length && hasFocus ) {
  315 + stopLoading();
  316 + select.display(data, q);
  317 + autoFill(q, data[0].value);
  318 + select.show();
  319 + } else {
  320 + hideResultsNow();
  321 + }
  322 + };
  323 +
  324 + function request(term, success, failure) {
  325 + if (!options.matchCase)
  326 + term = term.toLowerCase();
  327 + var data = cache.load(term);
  328 + // recieve the cached data
  329 + if (data && data.length) {
  330 + success(term, data);
  331 + // if an AJAX url has been supplied, try loading the data now
  332 + } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  333 +
  334 + var extraParams = {
  335 + timestamp: +new Date()
  336 + };
  337 + $.each(options.extraParams, function(key, param) {
  338 + extraParams[key] = typeof param == "function" ? param() : param;
  339 + });
  340 +
  341 + $.ajax({
  342 + // try to leverage ajaxQueue plugin to abort previous requests
  343 + mode: "abort",
  344 + // limit abortion to this input
  345 + port: "autocomplete" + input.name,
  346 + dataType: options.dataType,
  347 + url: options.url,
  348 + data: $.extend({
  349 + q: lastWord(term),
  350 + limit: options.max
  351 + }, extraParams),
  352 + success: function(data) {
  353 + var parsed = options.parse && options.parse(data) || parse(data);
  354 + cache.add(term, parsed);
  355 + success(term, parsed);
  356 + }
  357 + });
  358 + } else {
  359 + // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
  360 + select.emptyList();
  361 + failure(term);
  362 + }
  363 + };
  364 +
  365 + function parse(data) {
  366 + var parsed = [];
  367 + var rows = data.split("\n");
  368 + for (var i=0; i < rows.length; i++) {
  369 + var row = $.trim(rows[i]);
  370 + if (row) {
  371 + row = row.split("|");
  372 + parsed[parsed.length] = {
  373 + data: row,
  374 + value: row[0],
  375 + result: options.formatResult && options.formatResult(row, row[0]) || row[0]
  376 + };
  377 + }
  378 + }
  379 + return parsed;
  380 + };
  381 +
  382 + function stopLoading() {
  383 + $input.removeClass(options.loadingClass);
  384 + };
  385 +
  386 +};
  387 +
  388 +$.Autocompleter.defaults = {
  389 + inputClass: "ac_input",
  390 + resultsClass: "ac_results",
  391 + loadingClass: "ac_loading",
  392 + minChars: 1,
  393 + delay: 400,
  394 + matchCase: false,
  395 + matchSubset: true,
  396 + matchContains: false,
  397 + cacheLength: 10,
  398 + max: 100,
  399 + mustMatch: false,
  400 + extraParams: {},
  401 + selectFirst: true,
  402 + formatItem: function(row) { return row[0]; },
  403 + formatMatch: null,
  404 + autoFill: false,
  405 + width: 0,
  406 + multiple: false,
  407 + multipleSeparator: ", ",
  408 + highlight: function(value, term) {
  409 + return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  410 + },
  411 + scroll: true,
  412 + scrollHeight: 180
  413 +};
  414 +
  415 +$.Autocompleter.Cache = function(options) {
  416 +
  417 + var data = {};
  418 + var length = 0;
  419 +
  420 + function matchSubset(s, sub) {
  421 + if (!options.matchCase)
  422 + s = s.toLowerCase();
  423 + var i = s.indexOf(sub);
  424 + if (options.matchContains == "word"){
  425 + i = s.toLowerCase().search("\\b" + sub.toLowerCase());
  426 + }
  427 + if (i == -1) return false;
  428 + return i == 0 || options.matchContains;
  429 + };
  430 +
  431 + function add(q, value) {
  432 + if (length > options.cacheLength){
  433 + flush();
  434 + }
  435 + if (!data[q]){
  436 + length++;
  437 + }
  438 + data[q] = value;
  439 + }
  440 +
  441 + function populate(){
  442 + if( !options.data ) return false;
  443 + // track the matches
  444 + var stMatchSets = {},
  445 + nullData = 0;
  446 +
  447 + // no url was specified, we need to adjust the cache length to make sure it fits the local data store
  448 + if( !options.url ) options.cacheLength = 1;
  449 +
  450 + // track all options for minChars = 0
  451 + stMatchSets[""] = [];
  452 +
  453 + // loop through the array and create a lookup structure
  454 + for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
  455 + var rawValue = options.data[i];
  456 + // if rawValue is a string, make an array otherwise just reference the array
  457 + rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
  458 +
  459 + var value = options.formatMatch(rawValue, i+1, options.data.length);
  460 + if ( value === false )
  461 + continue;
  462 +
  463 + var firstChar = value.charAt(0).toLowerCase();
  464 + // if no lookup array for this character exists, look it up now
  465 + if( !stMatchSets[firstChar] )
  466 + stMatchSets[firstChar] = [];
  467 +
  468 + // if the match is a string
  469 + var row = {
  470 + value: value,
  471 + data: rawValue,
  472 + result: options.formatResult && options.formatResult(rawValue) || value
  473 + };
  474 +
  475 + // push the current match into the set list
  476 + stMatchSets[firstChar].push(row);
  477 +
  478 + // keep track of minChars zero items
  479 + if ( nullData++ < options.max ) {
  480 + stMatchSets[""].push(row);
  481 + }
  482 + };
  483 +
  484 + // add the data items to the cache
  485 + $.each(stMatchSets, function(i, value) {
  486 + // increase the cache size
  487 + options.cacheLength++;
  488 + // add to the cache
  489 + add(i, value);
  490 + });
  491 + }
  492 +
  493 + // populate any existing data
  494 + setTimeout(populate, 25);
  495 +
  496 + function flush(){
  497 + data = {};
  498 + length = 0;
  499 + }
  500 +
  501 + return {
  502 + flush: flush,
  503 + add: add,
  504 + populate: populate,
  505 + load: function(q) {
  506 + if (!options.cacheLength || !length)
  507 + return null;
  508 + /*
  509 + * if dealing w/local data and matchContains than we must make sure
  510 + * to loop through all the data collections looking for matches
  511 + */
  512 + if( !options.url && options.matchContains ){
  513 + // track all matches
  514 + var csub = [];
  515 + // loop through all the data grids for matches
  516 + for( var k in data ){
  517 + // don't search through the stMatchSets[""] (minChars: 0) cache
  518 + // this prevents duplicates
  519 + if( k.length > 0 ){
  520 + var c = data[k];
  521 + $.each(c, function(i, x) {
  522 + // if we've got a match, add it to the array
  523 + if (matchSubset(x.value, q)) {
  524 + csub.push(x);
  525 + }
  526 + });
  527 + }
  528 + }
  529 + return csub;
  530 + } else
  531 + // if the exact item exists, use it
  532 + if (data[q]){
  533 + return data[q];
  534 + } else
  535 + if (options.matchSubset) {
  536 + for (var i = q.length - 1; i >= options.minChars; i--) {
  537 + var c = data[q.substr(0, i)];
  538 + if (c) {
  539 + var csub = [];
  540 + $.each(c, function(i, x) {
  541 + if (matchSubset(x.value, q)) {
  542 + csub[csub.length] = x;
  543 + }
  544 + });
  545 + return csub;
  546 + }
  547 + }
  548 + }
  549 + return null;
  550 + }
  551 + };
  552 +};
  553 +
  554 +$.Autocompleter.Select = function (options, input, select, config) {
  555 + var CLASSES = {
  556 + ACTIVE: "ac_over"
  557 + };
  558 +
  559 + var listItems,
  560 + active = -1,
  561 + data,
  562 + term = "",
  563 + needsInit = true,
  564 + element,
  565 + list;
  566 +
  567 + // Create results
  568 + function init() {
  569 + if (!needsInit)
  570 + return;
  571 + element = $("<div/>")
  572 + .hide()
  573 + .addClass(options.resultsClass)
  574 + .css("position", "absolute")
  575 + .appendTo(document.body);
  576 +
  577 + list = $("<ul/>").appendTo(element).mouseover( function(event) {
  578 + if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
  579 + active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
  580 + $(target(event)).addClass(CLASSES.ACTIVE);
  581 + }
  582 + }).click(function(event) {
  583 + $(target(event)).addClass(CLASSES.ACTIVE);
  584 + select();
  585 + // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
  586 + input.focus();
  587 + return false;
  588 + }).mousedown(function() {
  589 + config.mouseDownOnSelect = true;
  590 + }).mouseup(function() {
  591 + config.mouseDownOnSelect = false;
  592 + });
  593 +
  594 + if( options.width > 0 )
  595 + element.css("width", options.width);
  596 +
  597 + needsInit = false;
  598 + }
  599 +
  600 + function target(event) {
  601 + var element = event.target;
  602 + while(element && element.tagName != "LI")
  603 + element = element.parentNode;
  604 + // more fun with IE, sometimes event.target is empty, just ignore it then
  605 + if(!element)
  606 + return [];
  607 + return element;
  608 + }
  609 +
  610 + function moveSelect(step) {
  611 + listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
  612 + movePosition(step);
  613 + var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
  614 + if(options.scroll) {
  615 + var offset = 0;
  616 + listItems.slice(0, active).each(function() {
  617 + offset += this.offsetHeight;
  618 + });
  619 + if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
  620 + list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
  621 + } else if(offset < list.scrollTop()) {
  622 + list.scrollTop(offset);
  623 + }
  624 + }
  625 + };
  626 +
  627 + function movePosition(step) {
  628 + active += step;
  629 + if (active < 0) {
  630 + active = listItems.size() - 1;
  631 + } else if (active >= listItems.size()) {
  632 + active = 0;
  633 + }
  634 + }
  635 +
  636 + function limitNumberOfItems(available) {
  637 + return options.max && options.max < available
  638 + ? options.max
  639 + : available;
  640 + }
  641 +
  642 + function fillList() {
  643 + list.empty();
  644 + var max = limitNumberOfItems(data.length);
  645 + for (var i=0; i < max; i++) {
  646 + if (!data[i])
  647 + continue;
  648 + var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
  649 + if ( formatted === false )
  650 + continue;
  651 + var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
  652 + $.data(li, "ac_data", data[i]);
  653 + }
  654 + listItems = list.find("li");
  655 + if ( options.selectFirst ) {
  656 + listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
  657 + active = 0;
  658 + }
  659 + // apply bgiframe if available
  660 + if ( $.fn.bgiframe )
  661 + list.bgiframe();
  662 + }
  663 +
  664 + return {
  665 + display: function(d, q) {
  666 + init();
  667 + data = d;
  668 + term = q;
  669 + fillList();
  670 + },
  671 + next: function() {
  672 + moveSelect(1);
  673 + },
  674 + prev: function() {
  675 + moveSelect(-1);
  676 + },
  677 + pageUp: function() {
  678 + if (active != 0 && active - 8 < 0) {
  679 + moveSelect( -active );
  680 + } else {
  681 + moveSelect(-8);
  682 + }
  683 + },
  684 + pageDown: function() {
  685 + if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
  686 + moveSelect( listItems.size() - 1 - active );
  687 + } else {
  688 + moveSelect(8);
  689 + }
  690 + },
  691 + hide: function() {
  692 + element && element.hide();
  693 + listItems && listItems.removeClass(CLASSES.ACTIVE);
  694 + active = -1;
  695 + },
  696 + visible : function() {
  697 + return element && element.is(":visible");
  698 + },
  699 + current: function() {
  700 + return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
  701 + },
  702 + show: function() {
  703 + var offset = $(input).offset();
  704 + element.css({
  705 + width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
  706 + top: offset.top + input.offsetHeight,
  707 + left: offset.left
  708 + }).show();
  709 + if(options.scroll) {
  710 + list.scrollTop(0);
  711 + list.css({
  712 + maxHeight: options.scrollHeight,
  713 + overflow: 'auto'
  714 + });
  715 +
  716 + if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
  717 + var listHeight = 0;
  718 + listItems.each(function() {
  719 + listHeight += this.offsetHeight;
  720 + });
  721 + var scrollbarsVisible = listHeight > options.scrollHeight;
  722 + list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
  723 + if (!scrollbarsVisible) {
  724 + // IE doesn't recalculate width when scrollbar disappears
  725 + listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
  726 + }
  727 + }
  728 +
  729 + }
  730 + },
  731 + selected: function() {
  732 + var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
  733 + return selected && selected.length && $.data(selected[0], "ac_data");
  734 + },
  735 + emptyList: function (){
  736 + list && list.empty();
  737 + },
  738 + unbind: function() {
  739 + element && element.remove();
  740 + }
  741 + };
  742 +};
  743 +
  744 +$.Autocompleter.Selection = function(field, start, end) {
  745 + if( field.createTextRange ){
  746 + var selRange = field.createTextRange();
  747 + selRange.collapse(true);
  748 + selRange.moveStart("character", start);
  749 + selRange.moveEnd("character", end);
  750 + selRange.select();
  751 + } else if( field.setSelectionRange ){
  752 + field.setSelectionRange(start, end);
  753 + } else {
  754 + if( field.selectionStart ){
  755 + field.selectionStart = start;
  756 + field.selectionEnd = end;
  757 + }
  758 + }
  759 + field.focus();
  760 +};
  761 +
  762 +})(jQuery);

0 comments on commit b9c7cc6

Please sign in to comment.
Something went wrong with that request. Please try again.