Repository: meltingice/ajax-chosen Branch: master Commit: eeb6c5d1640b Files: 14 Total size: 50.4 KB Directory structure: gitextract_e1h6102r/ ├── .gitignore ├── .gitmodules ├── Cakefile ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── data.json ├── docs/ │ ├── ajax-chosen.html │ └── docco.css ├── index.html ├── lib/ │ └── ajax-chosen.js ├── package.json └── src/ └── ajax-chosen.coffee ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store sftp-config.json node_modules ================================================ FILE: .gitmodules ================================================ [submodule "dist/chosen"] path = dist/chosen url = https://github.com/harvesthq/chosen.git ================================================ FILE: Cakefile ================================================ fs = require 'fs' {exec} = require 'child_process' util = require 'util' {jsmin} = require 'jsmin' targetName = "ajax-chosen" ### CoffeeScript Options ### csSrcDir = "src" csTargetDir = "lib" targetCoffee = "#{csSrcDir}/build.coffee" targetJS = "#{csTargetDir}/#{targetName}.js" targetMinJS = "#{csTargetDir}/#{targetName}.min.js" coffeeOpts = "-b -j #{targetName}.js -o #{csTargetDir} -c #{targetCoffee}" coffeeFiles = [ "ajax-chosen" ] ### Event System ### finishedCallback = {} finished = (type) -> finishedCallback[type]() if finishedCallback[type]? finishListener = (type, cb) -> finishedCallback[type] = cb notify = (msg) -> return if not growl? growl.notify msg, {title: "Heello Development", image: "Terminal"} ### Tasks ### task 'docs', 'Generates documentation for the coffee files', -> util.log 'Invoking docco on the CoffeeScript source files' files = coffeeFiles files[i] = "#{csSrcDir}/#{files[i]}.coffee" for i in [0...files.length] exec "docco #{files.join(' ')}", (err, stdout, stderr) -> util.log err if err util.log "Documentation built into docs/ folder." task 'watch', 'Automatically recompile the CoffeeScript files when updated', -> util.log "Watching for changes in #{csSrcDir}" for jsFile in coffeeFiles then do (jsFile) -> fs.watchFile "#{csSrcDir}/#{jsFile}.coffee", (curr, prev) -> if +curr.mtime isnt +prev.mtime util.log "#{csSrcDir}/#{jsFile}.coffee updated" invoke 'build' task 'build', 'Compile and minify all CoffeeScript source files', -> finishListener 'js', -> invoke 'minify' invoke 'compile' task 'compile', 'Compile all CoffeeScript source files', -> util.log "Building #{targetJS}" contents = [] remaining = coffeeFiles.length util.log "Appending #{coffeeFiles.length} files to #{targetCoffee}" for file, index in coffeeFiles then do (file, index) -> fs.readFile "#{csSrcDir}/#{file}.coffee", "utf8", (err, fileContents) -> util.log err if err contents[index] = fileContents util.log "[#{index + 1}] #{file}.coffee" process() if --remaining is 0 process = -> fs.writeFile targetCoffee, contents.join("\n\n"), "utf8", (err) -> util.log err if err exec "coffee #{coffeeOpts}", (err, stdout, stderr) -> util.log err if err util.log "Compiled #{targetJS}" fs.unlink targetCoffee, (err) -> util.log err if err finished('js') task 'minify', 'Minify the CoffeeScript files', -> util.log "Minifying #{targetJS}" fs.readFile targetJS, "utf8", (err, contents) -> fs.writeFile targetMinJS, jsmin(contents), "utf8", (err) -> util.log err if err ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Ryan LeFevre 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, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 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. ================================================ FILE: Makefile ================================================ all: coffee -o lib/ -c src/ ================================================ FILE: README.md ================================================ # Ajax-Chosen **This project is no longer maintained and is likely extremely out of date with the original chosen library. There are much better alternatives out there now.** This project is an addition to the excellent [Chosen jQuery plugin](https://github.com/harvesthq/chosen) that makes HTML input forms more friendly. Chosen adds search boxes to `select` HTML elements, so I felt it could use the addition of ajax autocomplete for awesomely dynamic forms. This script bootstraps the existing Chosen plugin without making any modifications to the original code. Eventually, I would love to see this functionality built-in to the library, but until then, this seems to work pretty well. ## How to Use This plugin exposes a new jQuery function named `ajaxChosen` that we call on a `select` element. The first argument consists of the options passed to the jQuery $.ajax function. The `data` parameter is optional, and the `success` callback is also optional. The second argument is a callback that tells the plugin what HTML `option` elements to make. It is passed the data returned from the ajax call, and you have to return an array of objects for which each item has a `value` property corresponding to the HTML `option` elements' `value` attribute, and a `text` property corresponding to the text to display for each option. In other words: [{"value": 3, "text": "Ohio"}] becomes: or for grouping: [{ group: true, text: "Europe", items: [ { "value": "10", "text": "Stockholm" }, { "value": "23", "text": "London" } ] }, { group: true, text: "Asia", items: [ { "value": "36", "text": "Beijing" }, { "value": "20", "text": "Tokyo" } ] }] becomes: Note: Due to a bug in Chosen, it is necessary to change `choosen.css`. Add display: list-item; to .chzn-container .chzn-results .group-result { class ### Options There are some additional ajax-chosen specific options you can pass into the first argument to control its behavior. * `minTermLength`: minimum number of characters that must be typed before an ajax call is fired * `afterTypeDelay`: how many milliseconds to wait after typing stops to fire the ajax call * `jsonTermKey`: the ajax request key to use for the search query (defaults to `term`) ## Example Code ``` js $("#example-input").ajaxChosen({ type: 'GET', url: '/ajax-chosen/data.php', dataType: 'json' }, function (data) { var results = []; $.each(data, function (i, val) { results.push({ value: val.value, text: val.text }); }); return results; }); ``` To have the results grouped in `optgroup` elements, have the function return a list of group objects instead: ``` js $("#example-input").ajaxChosen({ type: 'GET', url: '/ajax-chosen/grouped.php', dataType: 'json' }, function (data) { var results = []; $.each(data, function (i, val) { var group = { // here's a group object: group: true, text: val.name, // label for the group items: [] // individual options within the group }; $.each(val.items, function (i1, val1) { group.items.push({value: val1.value, text: val1.text}); }); results.push(group); }); return results; }); ``` ## Developing ajax-chosen In order to install development dependencies, you can run in the ajax-chosen directory: ``` npm install -d ``` ajax-chosen is written in Coffeescript, so there is a Cakefile provided that will perform all necessary tasks for you. Simply run `cake` to see all available commands. ================================================ FILE: VERSION ================================================ 0.2.0 ================================================ FILE: data.json ================================================ { "states": [ "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Puerto Rico", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Virgin Islands", "Washington", "West Virginia", "Wisconsin", "Wyoming" ] } ================================================ FILE: docs/ajax-chosen.html ================================================ ajax-chosen.coffee

ajax-chosen.coffee

do ($ = jQuery) ->

  $.fn.ajaxChosen = (settings = {}, callback = ->) ->
    defaultOptions =
      minTermLength: 3
      afterTypeDelay: 500
      jsonTermKey: "term"

This will come in handy later.

    select = @
    
    chosenXhr = null
    

Merge options with defaults

    options = $.extend {}, defaultOptions, settings

Load chosen. To make things clear, I have taken the liberty of using the .chzn-autoselect class to specify input elements we want to use with ajax autocomplete.

    @chosen()
    
    @each ->

Now that chosen is loaded normally, we can bootstrap it with our ajax autocomplete code.

      $(@).next('.chzn-container')
        .find(".search-field > input, .chzn-search > input")
        .bind 'keyup', ->

This code will be executed every time the user types a letter into the input form that chosen has created

          

Retrieve the current value of the input form

          val = $.trim $(@).attr('value')

Depending on how much text the user has typed, let them know if they need to keep typing or if we are looking for their data

          msg = if val.length < options.minTermLength then "Keep typing..." else "Looking for '" + val + "'"
          select.next('.chzn-container').find('.no-results').text(msg)
          

If input text has not changed ... do nothing

          return false if val is $(@).data('prevVal')

Set the current search term so we don't execute the ajax call if the user hits a key that isn't an input letter/number/symbol

          $(@).data('prevVal', val)
          

At this point, we have a new term/query ... the old timer is no longer valid. Clear it.

We delay searches by a small amount so that we don't flood the server with ajax requests.

          clearTimeout(@timer) if @timer
          

Some simple validation so we don't make excess ajax calls. I am assuming you don't want to perform a search with less than 3 characters.

          return false if val.length < options.minTermLength
          

This is a useful reference for later

          field = $(@)
          

Default term key is term. Specify alternative in options.options.jsonTermKey

          options.data = {} if not options.data?
          options.data[options.jsonTermKey] = val
          

If the user provided an ajax success callback, store it so we can call it after our bootstrapping is finished.

          success ?= options.success
          

Create our own callback that will be executed when the ajax call is finished.

          options.success = (data) ->

Exit if the data we're given is invalid

            return if not data?
            

Go through all of the

            selected_values = []
            select.find('option').each -> 
              if not $(@).is(":selected")
                $(@).remove() 
              else
                selected_values.push $(@).val() + "-" + $(@).text()
                

Send the ajax results to the user callback so we can get an object of value => text pairs to inject as

            items = callback data
            

Iterate through the given data and inject the

            $.each items, (value, text) ->
              if $.inArray(value + "-" + text, selected_values) == -1
                $("<option />")
                  .attr('value', value)
                  .html(text)
                  .appendTo(select)
                

Tell chosen that the contents of the

            select.trigger("liszt:updated")
            

Finally, call the user supplied callback (if it exists)

            success(data) if success?

For some reason, the contents of the input field get removed once you call trigger above. Often, this can be very annoying (and can make some searches impossible), so we add the value the user was typing back into the input field.

            field.attr('value', val)

Because non-ajax Chosen isn't constantly re-building results, when it DOES rebuild results (during liszt:updated above, it clears the input search field before scaling it. This causes the input field width to be at it's minimum, which is about 25px.

The proper way to fix this would be create a new method in chosen for rebuilding results without clearing the input field. Or to call Chosen.searchfieldscale() after resetting the value above. This isn't possible with the current state of Chosen. The quick fix is to simply reset the width of the field after we reset the value of the input text.

            field.css('width','auto')
                      

Execute the ajax call to search for autocomplete data with a timer

          @timer = setTimeout -> 
            chosenXhr.abort() if chosenXhr
            chosenXhr = $.ajax(options)
          , options.afterTypeDelay

================================================ FILE: docs/docco.css ================================================ /*--------------------- Layout and Typography ----------------------------*/ body { font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; font-size: 15px; line-height: 22px; color: #252519; margin: 0; padding: 0; } a { color: #261a3b; } a:visited { color: #261a3b; } p { margin: 0 0 15px 0; } h1, h2, h3, h4, h5, h6 { margin: 0px 0 15px 0; } h1 { margin-top: 40px; } #container { position: relative; } #background { position: fixed; top: 0; left: 525px; right: 0; bottom: 0; background: #f5f5ff; border-left: 1px solid #e5e5ee; z-index: -1; } #jump_to, #jump_page { background: white; -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; font: 10px Arial; text-transform: uppercase; cursor: pointer; text-align: right; } #jump_to, #jump_wrapper { position: fixed; right: 0; top: 0; padding: 5px 10px; } #jump_wrapper { padding: 0; display: none; } #jump_to:hover #jump_wrapper { display: block; } #jump_page { padding: 5px 0 3px; margin: 0 0 25px 25px; } #jump_page .source { display: block; padding: 5px 10px; text-decoration: none; border-top: 1px solid #eee; } #jump_page .source:hover { background: #f5f5ff; } #jump_page .source:first-child { } table td { border: 0; outline: 0; } td.docs, th.docs { max-width: 450px; min-width: 450px; min-height: 5px; padding: 10px 25px 1px 50px; overflow-x: hidden; vertical-align: top; text-align: left; } .docs pre { margin: 15px 0 15px; padding-left: 15px; } .docs p tt, .docs p code { background: #f8f8ff; border: 1px solid #dedede; font-size: 12px; padding: 0 0.2em; } .pilwrap { position: relative; } .pilcrow { font: 12px Arial; text-decoration: none; color: #454545; position: absolute; top: 3px; left: -20px; padding: 1px 2px; opacity: 0; -webkit-transition: opacity 0.2s linear; } td.docs:hover .pilcrow { opacity: 1; } td.code, th.code { padding: 14px 15px 16px 25px; width: 100%; vertical-align: top; background: #f5f5ff; border-left: 1px solid #e5e5ee; } pre, tt, code { font-size: 12px; line-height: 18px; font-family: Monaco, Consolas, "Lucida Console", monospace; margin: 0; padding: 0; } /*---------------------- Syntax Highlighting -----------------------------*/ td.linenos { background-color: #f0f0f0; padding-right: 10px; } span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } body .hll { background-color: #ffffcc } body .c { color: #408080; font-style: italic } /* Comment */ body .err { border: 1px solid #FF0000 } /* Error */ body .k { color: #954121 } /* Keyword */ body .o { color: #666666 } /* Operator */ body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ body .cp { color: #BC7A00 } /* Comment.Preproc */ body .c1 { color: #408080; font-style: italic } /* Comment.Single */ body .cs { color: #408080; font-style: italic } /* Comment.Special */ body .gd { color: #A00000 } /* Generic.Deleted */ body .ge { font-style: italic } /* Generic.Emph */ body .gr { color: #FF0000 } /* Generic.Error */ body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ body .gi { color: #00A000 } /* Generic.Inserted */ body .go { color: #808080 } /* Generic.Output */ body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ body .gs { font-weight: bold } /* Generic.Strong */ body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ body .gt { color: #0040D0 } /* Generic.Traceback */ body .kc { color: #954121 } /* Keyword.Constant */ body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ body .kp { color: #954121 } /* Keyword.Pseudo */ body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ body .kt { color: #B00040 } /* Keyword.Type */ body .m { color: #666666 } /* Literal.Number */ body .s { color: #219161 } /* Literal.String */ body .na { color: #7D9029 } /* Name.Attribute */ body .nb { color: #954121 } /* Name.Builtin */ body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ body .no { color: #880000 } /* Name.Constant */ body .nd { color: #AA22FF } /* Name.Decorator */ body .ni { color: #999999; font-weight: bold } /* Name.Entity */ body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ body .nf { color: #0000FF } /* Name.Function */ body .nl { color: #A0A000 } /* Name.Label */ body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ body .nt { color: #954121; font-weight: bold } /* Name.Tag */ body .nv { color: #19469D } /* Name.Variable */ body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ body .w { color: #bbbbbb } /* Text.Whitespace */ body .mf { color: #666666 } /* Literal.Number.Float */ body .mh { color: #666666 } /* Literal.Number.Hex */ body .mi { color: #666666 } /* Literal.Number.Integer */ body .mo { color: #666666 } /* Literal.Number.Oct */ body .sb { color: #219161 } /* Literal.String.Backtick */ body .sc { color: #219161 } /* Literal.String.Char */ body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ body .s2 { color: #219161 } /* Literal.String.Double */ body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ body .sh { color: #219161 } /* Literal.String.Heredoc */ body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ body .sx { color: #954121 } /* Literal.String.Other */ body .sr { color: #BB6688 } /* Literal.String.Regex */ body .s1 { color: #219161 } /* Literal.String.Single */ body .ss { color: #19469D } /* Literal.String.Symbol */ body .bp { color: #954121 } /* Name.Builtin.Pseudo */ body .vc { color: #19469D } /* Name.Variable.Class */ body .vg { color: #19469D } /* Name.Variable.Global */ body .vi { color: #19469D } /* Name.Variable.Instance */ body .il { color: #666666 } /* Literal.Number.Integer.Long */ ================================================ FILE: index.html ================================================ Ajax-Chosen: Bootstrapping a Popular jQuery Plugin to add Ajax Autocomplete

Ajax-Chosen: Bootstrapping a jQuery Plugin

Multi Select



Single Select

================================================ FILE: lib/ajax-chosen.js ================================================ // Generated by CoffeeScript 1.4.0 (function($) { return $.fn.ajaxChosen = function(settings, callback, chosenOptions) { var chosenXhr, defaultOptions, options, select; if (settings == null) { settings = {}; } if (chosenOptions == null) { chosenOptions = {}; } defaultOptions = { minTermLength: 3, afterTypeDelay: 500, jsonTermKey: "term", keepTypingMsg: "Keep typing...", lookingForMsg: "Looking for" }; select = this; chosenXhr = null; options = $.extend({}, defaultOptions, $(select).data(), settings); this.chosen(chosenOptions ? chosenOptions : {}); return this.each(function() { return $(this).next('.chzn-container').find(".search-field > input, .chzn-search > input").bind('keyup', function() { var field, msg, success, untrimmed_val, val; untrimmed_val = $(this).val(); val = $.trim($(this).val()); msg = val.length < options.minTermLength ? options.keepTypingMsg : options.lookingForMsg + (" '" + val + "'"); select.next('.chzn-container').find('.no-results').text(msg); if (val === $(this).data('prevVal')) { return false; } $(this).data('prevVal', val); if (this.timer) { clearTimeout(this.timer); } if (val.length < options.minTermLength) { return false; } field = $(this); if (options.data == null) { options.data = {}; } options.data[options.jsonTermKey] = val; if (options.dataCallback != null) { options.data = options.dataCallback(options.data); } success = options.success; options.success = function(data) { var items, nbItems, selected_values; if (data == null) { return; } selected_values = []; select.find('option').each(function() { if (!$(this).is(":selected")) { return $(this).remove(); } else { return selected_values.push($(this).val() + "-" + $(this).text()); } }); select.find('optgroup:empty').each(function() { return $(this).remove(); }); items = callback != null ? callback(data, field) : data; nbItems = 0; $.each(items, function(i, element) { var group, text, value; nbItems++; if (element.group) { group = select.find("optgroup[label='" + element.text + "']"); if (!group.size()) { group = $(""); } group.attr('label', element.text).appendTo(select); return $.each(element.items, function(i, element) { var text, value; if (typeof element === "string") { value = i; text = element; } else { value = element.value; text = element.text; } if ($.inArray(value + "-" + text, selected_values) === -1) { return $("