Repository: harvesthq/chosen Branch: main Commit: c914ecd5120c Files: 37 Total size: 479.8 KB Directory structure: gitextract_85pi0xyb/ ├── .github/ │ ├── ISSUE_TEMPLATE.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE.md ├── README.md ├── coffee/ │ ├── chosen.jquery.coffee │ ├── chosen.proto.coffee │ └── lib/ │ ├── abstract-chosen.coffee │ └── select-parser.coffee ├── composer.json ├── contributing.md ├── package-travis.yml ├── package.json ├── public/ │ ├── docsupport/ │ │ ├── init.js │ │ ├── init.proto.js │ │ ├── prism.css │ │ ├── prism.js │ │ ├── prototype-1.7.0.0.js │ │ └── style.css │ ├── index.html │ ├── index.proto.html │ └── options.html ├── publish-package.sh ├── sass/ │ └── chosen.scss ├── spec/ │ ├── jquery/ │ │ ├── basic.spec.coffee │ │ ├── bugfixes.spec.coffee │ │ ├── events.spec.coffee │ │ ├── max_shown_results.spec.coffee │ │ └── searching.spec.coffee │ └── proto/ │ ├── basic.spec.coffee │ ├── bugfixes.spec.coffee │ ├── events.spec.coffee │ ├── max_shown_results.spec.coffee │ └── searching.spec.coffee └── tasks/ └── package.coffee ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ Summarize your issue here. ### Steps to reproduce Tell us how to reproduce this issue. 1. This is the first step 2. This is the second step 3. Further steps, etc. Additionally, please link to a working demo that shows the issue so we can attempt to reproduce. You can use [this template](https://jsfiddle.net/j7k727cp/) as a base. Alternatively, confirm that the [Chosen demo page](http://harvesthq.github.io/chosen/) shows the issue. ### Expected behavior Tell us what should happen. ### Actual behavior Tell us what happens instead. ### Environment - **Chosen Version**: - **jQuery or Prototype Version**: - **Browser and Version**: - **OS and Version**: ### Additional information Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, or potential solutions and workarounds. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Summary Provide a general description of the code changes in your pull request. Please double-check that: - [ ] All changes were made in CoffeeScript files, **not** JavaScript files. - [ ] You used [Grunt](https://github.com/harvesthq/chosen/blob/master/contributing.md#grunt) to build the JavaScript files and tested them locally. - [ ] You've updated both the jQuery *and* Prototype versions. - [ ] You haven't manually updated the version number in `package.json`. - [ ] If necessary, you've updated [the documentation](https://github.com/harvesthq/chosen/blob/master/public/options.html). See the [Pull Requests section of our Contributing Guidelines](https://github.com/harvesthq/chosen/blob/master/contributing.md#pull-requests) for more details. ### References If your pull request is in reference to one or more open GitHub issues, please mention them here to keep the conversations linked together. ================================================ FILE: .gitignore ================================================ .DS_Store node_modules .project public/*.js public/*.css public/*.json public/LICENSE.md chosen*.zip .sass-cache .ruby-version .rbenv-gemsets .grunt _SpecRunner.html spec/public ================================================ FILE: .travis.yml ================================================ sudo: false language: node_js node_js: - 6 addons: apt: sources: - git-core packages: - git before_install: npm install -g grunt-cli before_script: grunt build package-npm package-bower after_success: ./publish-package.sh env: global: secure: "SOYNh0YO4eLAM38FQxrg7iqytXgdjJHRkmj/1lFzGrGeuuXP6Owe/2TaMyTJXWb9nHAAtRRwQyhAUE07eKhxI6b3YNyozeRulMK4B0K8P3P1B2MslpROyvQYtZupno3dWc0tyvsQ3ucnZE25mtetH6KYcwiI+vHv6hT8HnzBnp0=" ================================================ FILE: Gruntfile.coffee ================================================ module.exports = (grunt) -> require('load-grunt-tasks')(grunt) grunt.loadNpmTasks('grunt1.0-dom-munger') # the naming convention of the package does not allow auto-discovery. grunt.initConfig pkg: grunt.file.readJSON('package.json') version_tag: 'v<%= pkg.version %>' comments: """ /*! Chosen, a Select Box Enhancer for jQuery and Prototype by Patrick Filler for Harvest, http://getharvest.com Version <%= pkg.version %> Full source at https://github.com/harvesthq/chosen Copyright (c) 2011-<%= grunt.template.today('yyyy') %> Harvest http://getharvest.com MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md This file is generated by `grunt build`, do not edit it by hand. */ \n """ minified_comments: "/* Chosen <%= version_tag %> | (c) 2011-<%= grunt.template.today('yyyy') %> by Harvest | MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md */\n" concat: options: banner: '<%= comments %>' jquery: src: ['public/chosen.jquery.js'] dest: 'public/chosen.jquery.js' proto: src: ['public/chosen.proto.js'] dest: 'public/chosen.proto.js' css: src: ['public/chosen.css'] dest: 'public/chosen.css' copy: main: src: 'LICENSE.md' dest: 'public/' php: src: 'composer.json' dest: 'public/' coffee: options: join: true jquery: files: 'public/chosen.jquery.js': ['coffee/lib/select-parser.coffee', 'coffee/lib/abstract-chosen.coffee', 'coffee/chosen.jquery.coffee'] proto: files: 'public/chosen.proto.js': ['coffee/lib/select-parser.coffee', 'coffee/lib/abstract-chosen.coffee', 'coffee/chosen.proto.coffee'] test: files: 'spec/public/jquery_specs.js': 'spec/jquery/*.spec.coffee' 'spec/public/proto_specs.js': 'spec/proto/*.spec.coffee' uglify: options: banner: '<%= minified_comments %>' jquery: options: ie8: true mangle: reserved: ['jQuery'] files: 'public/chosen.jquery.min.js': ['public/chosen.jquery.js'] proto: files: 'public/chosen.proto.min.js': ['public/chosen.proto.js'] sass: options: outputStyle: 'expanded' chosen_css: files: 'public/chosen.css': 'sass/chosen.scss' postcss: options: processors: [ require('autoprefixer')(browsers: 'last 2 versions, IE 8') ] main: src: 'public/chosen.css' cssmin: options: banner: '<%= minified_comments %>' keepSpecialComments: 0 main: src: 'public/chosen.css' dest: 'public/chosen.min.css' watch: default: files: ['coffee/**/*.coffee', 'sass/*.scss'] tasks: ['build', 'jasmine'] test: files: ['spec/**/*.coffee'] tasks: ['jasmine'] jasmine: jquery: options: vendor: [ 'public/docsupport/jquery-3.2.1.min.js' ] specs: 'spec/public/jquery_specs.js' src: [ 'public/chosen.jquery.js' ] jquery_old: options: vendor: [ 'public/docsupport/jquery-1.12.4.min.js' ] specs: 'spec/public/jquery_specs.js' src: [ 'public/chosen.jquery.js' ] proto: options: vendor: [ 'public/docsupport/prototype-1.7.0.0.js' 'node_modules/simulant/dist/simulant.umd.js' ] specs: 'spec/public/proto_specs.js' src: [ 'public/chosen.proto.js' ] grunt.loadTasks 'tasks' grunt.registerTask 'default', ['build'] grunt.registerTask 'build', ['coffee:jquery', 'coffee:proto', 'sass', 'concat', 'uglify', 'postcss', 'cssmin', 'copy'] grunt.registerTask 'test', ['coffee', 'jasmine'] grunt.registerTask 'test:jquery', ['coffee:test', 'coffee:jquery', 'jasmine:jquery', 'jasmine:jquery_old'] grunt.registerTask 'test:proto', ['coffee:test', 'coffee:proto', 'jasmine:proto'] ================================================ FILE: LICENSE.md ================================================ #### Chosen - by Patrick Filler for [Harvest](http://getharvest.com) - Copyright (c) 2011-2016 by Harvest Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License) 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: README.md ================================================ # Deprecated #### This version of Chosen is not currently under active development while we decide on its future direction. --- # Chosen Chosen is a library for making long, unwieldy select boxes more user friendly. - jQuery support: 1.7+ - Prototype support: 1.7+ For **documentation**, usage, and examples, see: http://harvesthq.github.io/chosen/ For **downloads**, see: https://github.com/harvesthq/chosen/releases/ ### Package managers Chosen is available through [Bower](https://bower.io/), [npm](https://www.npmjs.com), and [Composer](https://getcomposer.org/), _however, the package names are not the same_. To install with Bower: ``` bower install chosen ``` To install with npm: ``` npm install chosen-js ``` To install with Composer: ``` composer require harvesthq/chosen ``` The compiled files for these packages are automatically generated and stored in a [2nd Chosen repository](https://github.com/harvesthq/chosen-package). No pull requests will be accepted to that repository. ### Contributing to this project We welcome all to participate in making Chosen the best software it can be. The repository is maintained by only a few people, but has accepted contributions from over 50 authors after reviewing hundreds of pull requests related to thousands of issues. You can help reduce the maintainers' workload (and increase your chance of having an accepted contribution to Chosen) by following the [guidelines for contributing](contributing.md). * [Bug reports](contributing.md#bugs) * [Feature requests](contributing.md#features) * [Pull requests](contributing.md#pull-requests) ### Chosen Credits - Concept and development by [Patrick Filler](http://patrickfiller.com) for [Harvest](http://getharvest.com/). - Design and CSS by [Matthew Lettini](http://matthewlettini.com/) - Repository maintained by [@pfiller](http://github.com/pfiller), [@kenearley](http://github.com/kenearley), [@stof](http://github.com/stof), [@koenpunt](http://github.com/koenpunt), and [@tjschuck](http://github.com/tjschuck). - Chosen includes [contributions by many fine folks](https://github.com/harvesthq/chosen/contributors). ================================================ FILE: coffee/chosen.jquery.coffee ================================================ $ = jQuery $.fn.extend({ chosen: (options) -> # Do no harm and return as soon as possible for unsupported browsers, namely IE6 and IE7 # Continue on if running IE document type but in compatibility mode return this unless AbstractChosen.browser_is_supported() this.each (input_field) -> $this = $ this chosen = $this.data('chosen') if options is 'destroy' if chosen instanceof Chosen chosen.destroy() return unless chosen instanceof Chosen $this.data('chosen', new Chosen(this, options)) return }) class Chosen extends AbstractChosen setup: -> @form_field_jq = $ @form_field @current_selectedIndex = @form_field.selectedIndex set_up_html: -> container_classes = ["chosen-container"] container_classes.push "chosen-container-" + (if @is_multiple then "multi" else "single") container_classes.push @form_field.className if @inherit_select_classes && @form_field.className container_classes.push "chosen-rtl" if @is_rtl container_props = 'class': container_classes.join ' ' 'title': @form_field.title container_props.id = @form_field.id.replace(/[^\w]/g, '_') + "_chosen" if @form_field.id.length @container = ($ "
", container_props) # CSP without 'unsafe-inline' doesn't allow setting the style attribute directly @container.width this.container_width() if @is_multiple @container.html this.get_multi_html() else @container.html this.get_single_html() @form_field_jq.hide().after @container @dropdown = @container.find('div.chosen-drop').first() @search_field = @container.find('input').first() @search_results = @container.find('ul.chosen-results').first() this.search_field_scale() @search_no_results = @container.find('li.no-results').first() if @is_multiple @search_choices = @container.find('ul.chosen-choices').first() @search_container = @container.find('li.search-field').first() else @search_container = @container.find('div.chosen-search').first() @selected_item = @container.find('.chosen-single').first() this.results_build() this.set_tab_index() this.set_label_behavior() on_ready: -> @form_field_jq.trigger("chosen:ready", {chosen: this}) register_observers: -> @container.on 'touchstart.chosen', (evt) => this.container_mousedown(evt); return @container.on 'touchend.chosen', (evt) => this.container_mouseup(evt); return @container.on 'mousedown.chosen', (evt) => this.container_mousedown(evt); return @container.on 'mouseup.chosen', (evt) => this.container_mouseup(evt); return @container.on 'mouseenter.chosen', (evt) => this.mouse_enter(evt); return @container.on 'mouseleave.chosen', (evt) => this.mouse_leave(evt); return @search_results.on 'mouseup.chosen', (evt) => this.search_results_mouseup(evt); return @search_results.on 'mouseover.chosen', (evt) => this.search_results_mouseover(evt); return @search_results.on 'mouseout.chosen', (evt) => this.search_results_mouseout(evt); return @search_results.on 'mousewheel.chosen DOMMouseScroll.chosen', (evt) => this.search_results_mousewheel(evt); return @search_results.on 'touchstart.chosen', (evt) => this.search_results_touchstart(evt); return @search_results.on 'touchmove.chosen', (evt) => this.search_results_touchmove(evt); return @search_results.on 'touchend.chosen', (evt) => this.search_results_touchend(evt); return @form_field_jq.on "chosen:updated.chosen", (evt) => this.results_update_field(evt); return @form_field_jq.on "chosen:activate.chosen", (evt) => this.activate_field(evt); return @form_field_jq.on "chosen:open.chosen", (evt) => this.container_mousedown(evt); return @form_field_jq.on "chosen:close.chosen", (evt) => this.close_field(evt); return @search_field.on 'blur.chosen', (evt) => this.input_blur(evt); return @search_field.on 'keyup.chosen', (evt) => this.keyup_checker(evt); return @search_field.on 'keydown.chosen', (evt) => this.keydown_checker(evt); return @search_field.on 'focus.chosen', (evt) => this.input_focus(evt); return @search_field.on 'cut.chosen', (evt) => this.clipboard_event_checker(evt); return @search_field.on 'paste.chosen', (evt) => this.clipboard_event_checker(evt); return if @is_multiple @search_choices.on 'click.chosen', (evt) => this.choices_click(evt); return else @container.on 'click.chosen', (evt) -> evt.preventDefault(); return # gobble click of anchor destroy: -> $(@container[0].ownerDocument).off 'click.chosen', @click_test_action @form_field_label.off 'click.chosen' if @form_field_label.length > 0 if @search_field[0].tabIndex @form_field_jq[0].tabIndex = @search_field[0].tabIndex @container.remove() @form_field_jq.removeData('chosen') @form_field_jq.show() search_field_disabled: -> @is_disabled = @form_field.disabled || @form_field_jq.parents('fieldset').is(':disabled') @container.toggleClass 'chosen-disabled', @is_disabled @search_field[0].disabled = @is_disabled unless @is_multiple @selected_item.off 'focus.chosen', this.activate_field if @is_disabled this.close_field() else unless @is_multiple @selected_item.on 'focus.chosen', this.activate_field container_mousedown: (evt) -> return if @is_disabled if evt and evt.type in ['mousedown', 'touchstart'] and not @results_showing evt.preventDefault() if not (evt? and ($ evt.target).hasClass "search-choice-close") if not @active_field @search_field.val "" if @is_multiple $(@container[0].ownerDocument).on 'click.chosen', @click_test_action this.results_show() else if not @is_multiple and evt and (($(evt.target)[0] == @selected_item[0]) || $(evt.target).parents("a.chosen-single").length) evt.preventDefault() this.results_toggle() this.activate_field() container_mouseup: (evt) -> this.results_reset(evt) if evt.target.nodeName is "ABBR" and not @is_disabled search_results_mousewheel: (evt) -> delta = evt.originalEvent.deltaY or -evt.originalEvent.wheelDelta or evt.originalEvent.detail if evt.originalEvent if delta? evt.preventDefault() delta = delta * 40 if evt.type is 'DOMMouseScroll' @search_results.scrollTop(delta + @search_results.scrollTop()) blur_test: (evt) -> this.close_field() if not @active_field and @container.hasClass "chosen-container-active" close_field: -> $(@container[0].ownerDocument).off "click.chosen", @click_test_action @active_field = false this.results_hide() @container.removeClass "chosen-container-active" this.clear_backstroke() this.show_search_field_default() this.search_field_scale() @search_field.blur() activate_field: -> return if @is_disabled @container.addClass "chosen-container-active" @active_field = true @search_field.val(@search_field.val()) @search_field.focus() test_active_click: (evt) -> active_container = $(evt.target).closest('.chosen-container') if active_container.length and @container[0] == active_container[0] @active_field = true else this.close_field() results_build: -> @parsing = true @selected_option_count = null @results_data = SelectParser.select_to_array @form_field if @is_multiple @search_choices.find("li.search-choice").remove() else this.single_set_selected_text() if @disable_search or @form_field.options.length <= @disable_search_threshold @search_field[0].readOnly = true @container.addClass "chosen-container-single-nosearch" else @search_field[0].readOnly = false @container.removeClass "chosen-container-single-nosearch" this.update_results_content this.results_option_build({first:true}) this.search_field_disabled() this.show_search_field_default() this.search_field_scale() @parsing = false result_do_highlight: (el) -> if el.length this.result_clear_highlight() @result_highlight = el @result_highlight.addClass "highlighted" maxHeight = parseInt @search_results.css("maxHeight"), 10 visible_top = @search_results.scrollTop() visible_bottom = maxHeight + visible_top high_top = @result_highlight.position().top + @search_results.scrollTop() high_bottom = high_top + @result_highlight.outerHeight() if high_bottom >= visible_bottom @search_results.scrollTop if (high_bottom - maxHeight) > 0 then (high_bottom - maxHeight) else 0 else if high_top < visible_top @search_results.scrollTop high_top result_clear_highlight: -> @result_highlight.removeClass "highlighted" if @result_highlight @result_highlight = null results_show: -> if @is_multiple and @max_selected_options <= this.choices_count() @form_field_jq.trigger("chosen:maxselected", {chosen: this}) return false @container.addClass "chosen-with-drop" @results_showing = true @search_field.focus() @search_field.val this.get_search_field_value() this.winnow_results() @form_field_jq.trigger("chosen:showing_dropdown", {chosen: this}) update_results_content: (content) -> @search_results.html content results_hide: -> if @results_showing this.result_clear_highlight() @container.removeClass "chosen-with-drop" @form_field_jq.trigger("chosen:hiding_dropdown", {chosen: this}) @results_showing = false set_tab_index: (el) -> if @form_field.tabIndex ti = @form_field.tabIndex @form_field.tabIndex = -1 @search_field[0].tabIndex = ti set_label_behavior: -> @form_field_label = @form_field_jq.parents("label") # first check for a parent label if not @form_field_label.length and @form_field.id.length @form_field_label = $("label[for='#{@form_field.id}']") #next check for a for=#{id} if @form_field_label.length > 0 @form_field_label.on 'click.chosen', this.label_click_handler show_search_field_default: -> if @is_multiple and this.choices_count() < 1 and not @active_field @search_field.val(@default_text) @search_field.addClass "default" else @search_field.val("") @search_field.removeClass "default" search_results_mouseup: (evt) -> target = if $(evt.target).hasClass "active-result" then $(evt.target) else $(evt.target).parents(".active-result").first() if target.length @result_highlight = target this.result_select(evt) @search_field.focus() search_results_mouseover: (evt) -> target = if $(evt.target).hasClass "active-result" then $(evt.target) else $(evt.target).parents(".active-result").first() this.result_do_highlight( target ) if target search_results_mouseout: (evt) -> this.result_clear_highlight() if $(evt.target).hasClass("active-result") or $(evt.target).parents('.active-result').first() choice_build: (item) -> choice = $('', { class: "search-choice" }).html("#{this.choice_label(item)}") if item.disabled choice.addClass 'search-choice-disabled' else close_link = $('', { class: 'search-choice-close', 'data-option-array-index': item.array_index }) close_link.on 'click.chosen', (evt) => this.choice_destroy_link_click(evt) choice.append close_link @search_container.before choice choice_destroy_link_click: (evt) -> evt.preventDefault() evt.stopPropagation() this.choice_destroy $(evt.target) unless @is_disabled choice_destroy: (link) -> if this.result_deselect( link[0].getAttribute("data-option-array-index") ) if @active_field @search_field.focus() else this.show_search_field_default() this.results_hide() if @is_multiple and this.choices_count() > 0 and this.get_search_field_value().length < 1 link.parents('li').first().remove() this.search_field_scale() results_reset: -> this.reset_single_select_options() @form_field.options[0].selected = true this.single_set_selected_text() this.show_search_field_default() this.results_reset_cleanup() this.trigger_form_field_change() this.results_hide() if @active_field results_reset_cleanup: -> @current_selectedIndex = @form_field.selectedIndex @selected_item.find("abbr").remove() result_select: (evt) -> if @result_highlight high = @result_highlight this.result_clear_highlight() if @is_multiple and @max_selected_options <= this.choices_count() @form_field_jq.trigger("chosen:maxselected", {chosen: this}) return false if @is_multiple high.removeClass("active-result") else this.reset_single_select_options() high.addClass("result-selected") item = @results_data[ high[0].getAttribute("data-option-array-index") ] item.selected = true @form_field.options[item.options_index].selected = true @selected_option_count = null if @is_multiple this.choice_build item else this.single_set_selected_text(this.choice_label(item)) if @is_multiple && (!@hide_results_on_select || (evt.metaKey or evt.ctrlKey)) if evt.metaKey or evt.ctrlKey this.winnow_results(skip_highlight: true) else @search_field.val("") this.winnow_results() else this.results_hide() this.show_search_field_default() this.trigger_form_field_change selected: @form_field.options[item.options_index].value if @is_multiple || @form_field.selectedIndex != @current_selectedIndex @current_selectedIndex = @form_field.selectedIndex evt.preventDefault() this.search_field_scale() single_set_selected_text: (text=@default_text) -> if text is @default_text @selected_item.addClass("chosen-default") else this.single_deselect_control_build() @selected_item.removeClass("chosen-default") @selected_item.find("span").html(text) result_deselect: (pos) -> result_data = @results_data[pos] if not @form_field.options[result_data.options_index].disabled result_data.selected = false @form_field.options[result_data.options_index].selected = false @selected_option_count = null this.result_clear_highlight() this.winnow_results() if @results_showing this.trigger_form_field_change deselected: @form_field.options[result_data.options_index].value this.search_field_scale() return true else return false single_deselect_control_build: -> return unless @allow_single_deselect @selected_item.find("span").first().after "" unless @selected_item.find("abbr").length @selected_item.addClass("chosen-single-with-deselect") get_search_field_value: -> @search_field.val() get_search_text: -> $.trim this.get_search_field_value() escape_html: (text) -> $('').text(text).html() winnow_results_set_highlight: -> selected_results = if not @is_multiple then @search_results.find(".result-selected.active-result") else [] do_high = if selected_results.length then selected_results.first() else @search_results.find(".active-result").first() this.result_do_highlight do_high if do_high? no_results: (terms) -> no_results_html = this.get_no_results_html(terms) @search_results.append no_results_html @form_field_jq.trigger("chosen:no_results", {chosen:this}) no_results_clear: -> @search_results.find(".no-results").remove() keydown_arrow: -> if @results_showing and @result_highlight next_sib = @result_highlight.nextAll("li.active-result").first() this.result_do_highlight next_sib if next_sib else this.results_show() keyup_arrow: -> if not @results_showing and not @is_multiple this.results_show() else if @result_highlight prev_sibs = @result_highlight.prevAll("li.active-result") if prev_sibs.length this.result_do_highlight prev_sibs.first() else this.results_hide() if this.choices_count() > 0 this.result_clear_highlight() keydown_backstroke: -> if @pending_backstroke this.choice_destroy @pending_backstroke.find("a").first() this.clear_backstroke() else next_available_destroy = @search_container.siblings("li.search-choice").last() if next_available_destroy.length and not next_available_destroy.hasClass("search-choice-disabled") @pending_backstroke = next_available_destroy if @single_backstroke_delete @keydown_backstroke() else @pending_backstroke.addClass "search-choice-focus" clear_backstroke: -> @pending_backstroke.removeClass "search-choice-focus" if @pending_backstroke @pending_backstroke = null search_field_scale: -> return unless @is_multiple style_block = position: 'absolute' left: '-1000px' top: '-1000px' display: 'none' whiteSpace: 'pre' styles = ['fontSize', 'fontStyle', 'fontWeight', 'fontFamily', 'lineHeight', 'textTransform', 'letterSpacing'] for style in styles style_block[style] = @search_field.css(style) div = $('').css(style_block) div.text this.get_search_field_value() $('body').append div width = div.width() + 25 div.remove() if @container.is(':visible') width = Math.min(@container.outerWidth() - 10, width) @search_field.width(width) trigger_form_field_change: (extra) -> @form_field_jq.trigger "input", extra @form_field_jq.trigger "change", extra ================================================ FILE: coffee/chosen.proto.coffee ================================================ class @Chosen extends AbstractChosen setup: -> @current_selectedIndex = @form_field.selectedIndex set_up_html: -> container_classes = ["chosen-container"] container_classes.push "chosen-container-" + (if @is_multiple then "multi" else "single") container_classes.push @form_field.className if @inherit_select_classes && @form_field.className container_classes.push "chosen-rtl" if @is_rtl container_props = 'class': container_classes.join ' ' 'title': @form_field.title container_props.id = @form_field.id.replace(/[^\w]/g, '_') + "_chosen" if @form_field.id.length @container = new Element('div', container_props) # CSP without 'unsafe-inline' doesn't allow setting the style attribute directly @container.setStyle(width: this.container_width()) if @is_multiple @container.update this.get_multi_html() else @container.update this.get_single_html() @form_field.hide().insert({ after: @container }) @dropdown = @container.down('div.chosen-drop') @search_field = @container.down('input') @search_results = @container.down('ul.chosen-results') this.search_field_scale() @search_no_results = @container.down('li.no-results') if @is_multiple @search_choices = @container.down('ul.chosen-choices') @search_container = @container.down('li.search-field') else @search_container = @container.down('div.chosen-search') @selected_item = @container.down('.chosen-single') this.results_build() this.set_tab_index() this.set_label_behavior() on_ready: -> @form_field.fire("chosen:ready", {chosen: this}) register_observers: -> @container.observe "touchstart", (evt) => this.container_mousedown(evt) @container.observe "touchend", (evt) => this.container_mouseup(evt) @container.observe "mousedown", (evt) => this.container_mousedown(evt) @container.observe "mouseup", (evt) => this.container_mouseup(evt) @container.observe "mouseenter", (evt) => this.mouse_enter(evt) @container.observe "mouseleave", (evt) => this.mouse_leave(evt) @search_results.observe "mouseup", (evt) => this.search_results_mouseup(evt) @search_results.observe "mouseover", (evt) => this.search_results_mouseover(evt) @search_results.observe "mouseout", (evt) => this.search_results_mouseout(evt) @search_results.observe "mousewheel", (evt) => this.search_results_mousewheel(evt) @search_results.observe "DOMMouseScroll", (evt) => this.search_results_mousewheel(evt) @search_results.observe "touchstart", (evt) => this.search_results_touchstart(evt) @search_results.observe "touchmove", (evt) => this.search_results_touchmove(evt) @search_results.observe "touchend", (evt) => this.search_results_touchend(evt) @form_field.observe "chosen:updated", (evt) => this.results_update_field(evt) @form_field.observe "chosen:activate", (evt) => this.activate_field(evt) @form_field.observe "chosen:open", (evt) => this.container_mousedown(evt) @form_field.observe "chosen:close", (evt) => this.close_field(evt) @search_field.observe "blur", (evt) => this.input_blur(evt) @search_field.observe "keyup", (evt) => this.keyup_checker(evt) @search_field.observe "keydown", (evt) => this.keydown_checker(evt) @search_field.observe "focus", (evt) => this.input_focus(evt) @search_field.observe "cut", (evt) => this.clipboard_event_checker(evt) @search_field.observe "paste", (evt) => this.clipboard_event_checker(evt) if @is_multiple @search_choices.observe "click", (evt) => this.choices_click(evt) else @container.observe "click", (evt) => evt.preventDefault() # gobble click of anchor destroy: -> @container.ownerDocument.stopObserving "click", @click_test_action for event in ['chosen:updated', 'chosen:activate', 'chosen:open', 'chosen:close'] @form_field.stopObserving(event) @container.stopObserving() @search_results.stopObserving() @search_field.stopObserving() @form_field_label.stopObserving() if @form_field_label? if @is_multiple @search_choices.stopObserving() @container.select(".search-choice-close").each (choice) -> choice.stopObserving() else @selected_item.stopObserving() if @search_field.tabIndex @form_field.tabIndex = @search_field.tabIndex @container.remove() @form_field.show() search_field_disabled: -> @is_disabled = @form_field.disabled || @form_field.up('fieldset')?.disabled || false if @is_disabled @container.addClassName 'chosen-disabled' else @container.removeClassName 'chosen-disabled' @search_field.disabled = @is_disabled unless @is_multiple @selected_item.stopObserving 'focus', this.activate_field if @is_disabled this.close_field() else unless @is_multiple @selected_item.observe 'focus', this.activate_field container_mousedown: (evt) -> return if @is_disabled if evt and evt.type in ['mousedown', 'touchstart'] and not @results_showing evt.preventDefault() if not (evt? and evt.target.hasClassName "search-choice-close") if not @active_field @search_field.clear() if @is_multiple @container.ownerDocument.observe "click", @click_test_action this.results_show() else if not @is_multiple and evt and (evt.target is @selected_item || evt.target.up("a.chosen-single")) this.results_toggle() this.activate_field() container_mouseup: (evt) -> this.results_reset(evt) if evt.target.nodeName is "ABBR" and not @is_disabled search_results_mousewheel: (evt) -> delta = evt.deltaY or -evt.wheelDelta or evt.detail if delta? evt.preventDefault() delta = delta * 40 if evt.type is 'DOMMouseScroll' @search_results.scrollTop = delta + @search_results.scrollTop blur_test: (evt) -> this.close_field() if not @active_field and @container.hasClassName("chosen-container-active") close_field: -> @container.ownerDocument.stopObserving "click", @click_test_action @active_field = false this.results_hide() @container.removeClassName "chosen-container-active" this.clear_backstroke() this.show_search_field_default() this.search_field_scale() @search_field.blur() activate_field: -> return if @is_disabled @container.addClassName "chosen-container-active" @active_field = true @search_field.value = this.get_search_field_value() @search_field.focus() test_active_click: (evt) -> if evt.target.up('.chosen-container') is @container @active_field = true else this.close_field() results_build: -> @parsing = true @selected_option_count = null @results_data = SelectParser.select_to_array @form_field if @is_multiple @search_choices.select("li.search-choice").invoke("remove") else this.single_set_selected_text() if @disable_search or @form_field.options.length <= @disable_search_threshold @search_field.readOnly = true @container.addClassName "chosen-container-single-nosearch" else @search_field.readOnly = false @container.removeClassName "chosen-container-single-nosearch" this.update_results_content this.results_option_build({first:true}) this.search_field_disabled() this.show_search_field_default() this.search_field_scale() @parsing = false result_do_highlight: (el) -> this.result_clear_highlight() @result_highlight = el @result_highlight.addClassName "highlighted" maxHeight = parseInt @search_results.getStyle('maxHeight'), 10 visible_top = @search_results.scrollTop visible_bottom = maxHeight + visible_top high_top = @result_highlight.positionedOffset().top high_bottom = high_top + @result_highlight.getHeight() if high_bottom >= visible_bottom @search_results.scrollTop = if (high_bottom - maxHeight) > 0 then (high_bottom - maxHeight) else 0 else if high_top < visible_top @search_results.scrollTop = high_top result_clear_highlight: -> @result_highlight.removeClassName('highlighted') if @result_highlight @result_highlight = null results_show: -> if @is_multiple and @max_selected_options <= this.choices_count() @form_field.fire("chosen:maxselected", {chosen: this}) return false @container.addClassName "chosen-with-drop" @results_showing = true @search_field.focus() @search_field.value = this.get_search_field_value() this.winnow_results() @form_field.fire("chosen:showing_dropdown", {chosen: this}) update_results_content: (content) -> @search_results.update content results_hide: -> if @results_showing this.result_clear_highlight() @container.removeClassName "chosen-with-drop" @form_field.fire("chosen:hiding_dropdown", {chosen: this}) @results_showing = false set_tab_index: (el) -> if @form_field.tabIndex ti = @form_field.tabIndex @form_field.tabIndex = -1 @search_field.tabIndex = ti set_label_behavior: -> @form_field_label = @form_field.up("label") # first check for a parent label if not @form_field_label? @form_field_label = $$("label[for='#{@form_field.id}']").first() #next check for a for=#{id} if @form_field_label? @form_field_label.observe "click", this.label_click_handler show_search_field_default: -> if @is_multiple and this.choices_count() < 1 and not @active_field @search_field.value = @default_text @search_field.addClassName "default" else @search_field.value = "" @search_field.removeClassName "default" search_results_mouseup: (evt) -> target = if evt.target.hasClassName("active-result") then evt.target else evt.target.up(".active-result") if target @result_highlight = target this.result_select(evt) @search_field.focus() search_results_mouseover: (evt) -> target = if evt.target.hasClassName("active-result") then evt.target else evt.target.up(".active-result") this.result_do_highlight( target ) if target search_results_mouseout: (evt) -> this.result_clear_highlight() if evt.target.hasClassName('active-result') or evt.target.up('.active-result') choice_build: (item) -> choice = new Element('li', { class: "search-choice" }).update("#{this.choice_label(item)}") if item.disabled choice.addClassName 'search-choice-disabled' else close_link = new Element('a', { href: '#', class: 'search-choice-close', rel: item.array_index }) close_link.observe "click", (evt) => this.choice_destroy_link_click(evt) choice.insert close_link @search_container.insert { before: choice } choice_destroy_link_click: (evt) -> evt.preventDefault() evt.stopPropagation() this.choice_destroy evt.target unless @is_disabled choice_destroy: (link) -> if this.result_deselect link.readAttribute("rel") if @active_field @search_field.focus() else this.show_search_field_default() this.results_hide() if @is_multiple and this.choices_count() > 0 and this.get_search_field_value().length < 1 link.up('li').remove() this.search_field_scale() results_reset: -> this.reset_single_select_options() @form_field.options[0].selected = true this.single_set_selected_text() this.show_search_field_default() this.results_reset_cleanup() this.trigger_form_field_change() this.results_hide() if @active_field results_reset_cleanup: -> @current_selectedIndex = @form_field.selectedIndex deselect_trigger = @selected_item.down("abbr") deselect_trigger.remove() if(deselect_trigger) result_select: (evt) -> if @result_highlight high = @result_highlight this.result_clear_highlight() if @is_multiple and @max_selected_options <= this.choices_count() @form_field.fire("chosen:maxselected", {chosen: this}) return false if @is_multiple high.removeClassName("active-result") else this.reset_single_select_options() high.addClassName("result-selected") item = @results_data[ high.getAttribute("data-option-array-index") ] item.selected = true @form_field.options[item.options_index].selected = true @selected_option_count = null if @is_multiple this.choice_build item else this.single_set_selected_text(this.choice_label(item)) if @is_multiple && (!@hide_results_on_select || (evt.metaKey or evt.ctrlKey)) if evt.metaKey or evt.ctrlKey this.winnow_results(skip_highlight: true) else @search_field.value = "" this.winnow_results() else this.results_hide() this.show_search_field_default() this.trigger_form_field_change() if @is_multiple || @form_field.selectedIndex != @current_selectedIndex @current_selectedIndex = @form_field.selectedIndex evt.preventDefault() this.search_field_scale() single_set_selected_text: (text=@default_text) -> if text is @default_text @selected_item.addClassName("chosen-default") else this.single_deselect_control_build() @selected_item.removeClassName("chosen-default") @selected_item.down("span").update(text) result_deselect: (pos) -> result_data = @results_data[pos] if not @form_field.options[result_data.options_index].disabled result_data.selected = false @form_field.options[result_data.options_index].selected = false @selected_option_count = null this.result_clear_highlight() this.winnow_results() if @results_showing this.trigger_form_field_change() this.search_field_scale() return true else return false single_deselect_control_build: -> return unless @allow_single_deselect @selected_item.down("span").insert { after: "" } unless @selected_item.down("abbr") @selected_item.addClassName("chosen-single-with-deselect") get_search_field_value: -> @search_field.value get_search_text: -> this.get_search_field_value().strip() escape_html: (text) -> text.escapeHTML() winnow_results_set_highlight: -> if not @is_multiple do_high = @search_results.down(".result-selected.active-result") if not do_high? do_high = @search_results.down(".active-result") this.result_do_highlight do_high if do_high? no_results: (terms) -> @search_results.insert this.get_no_results_html(terms) @form_field.fire("chosen:no_results", {chosen: this}) no_results_clear: -> nr = null nr.remove() while nr = @search_results.down(".no-results") keydown_arrow: -> if @results_showing and @result_highlight next_sib = @result_highlight.next('.active-result') this.result_do_highlight next_sib if next_sib else this.results_show() keyup_arrow: -> if not @results_showing and not @is_multiple this.results_show() else if @result_highlight sibs = @result_highlight.previousSiblings() actives = @search_results.select("li.active-result") prevs = sibs.intersect(actives) if prevs.length this.result_do_highlight prevs.first() else this.results_hide() if this.choices_count() > 0 this.result_clear_highlight() keydown_backstroke: -> if @pending_backstroke this.choice_destroy @pending_backstroke.down("a") this.clear_backstroke() else next_available_destroy = @search_container.siblings().last() if next_available_destroy and next_available_destroy.hasClassName("search-choice") and not next_available_destroy.hasClassName("search-choice-disabled") @pending_backstroke = next_available_destroy @pending_backstroke.addClassName("search-choice-focus") if @pending_backstroke if @single_backstroke_delete @keydown_backstroke() else @pending_backstroke.addClassName("search-choice-focus") clear_backstroke: -> @pending_backstroke.removeClassName("search-choice-focus") if @pending_backstroke @pending_backstroke = null search_field_scale: -> return unless @is_multiple style_block = position: 'absolute' left: '-1000px' top: '-1000px' display: 'none' whiteSpace: 'pre' styles = ['fontSize', 'fontStyle', 'fontWeight', 'fontFamily', 'lineHeight', 'textTransform', 'letterSpacing'] for style in styles style_block[style] = @search_field.getStyle(style) div = new Element('div').update(this.escape_html(this.get_search_field_value())) # CSP without 'unsafe-inline' doesn't allow setting the style attribute directly div.setStyle(style_block) document.body.appendChild(div) width = div.measure('width') + 25 div.remove() if container_width = @container.getWidth() width = Math.min(container_width - 10, width) @search_field.setStyle(width: width + 'px') trigger_form_field_change: -> triggerHtmlEvent @form_field, 'input' triggerHtmlEvent @form_field, 'change' triggerHtmlEvent = (element, eventType) -> if element.dispatchEvent # Modern way: try evt = new Event(eventType, bubbles: true, cancelable: true) catch evt = document.createEvent('HTMLEvents') evt.initEvent(eventType, true, true); element.dispatchEvent(evt) else # Old IE: element.fireEvent("on#{eventType}", document.createEventObject()); ================================================ FILE: coffee/lib/abstract-chosen.coffee ================================================ class AbstractChosen constructor: (@form_field, @options={}) -> return unless AbstractChosen.browser_is_supported() @is_multiple = @form_field.multiple this.set_default_text() this.set_default_values() this.setup() this.set_up_html() this.register_observers() # instantiation done, fire ready this.on_ready() set_default_values: -> @click_test_action = (evt) => this.test_active_click(evt) @activate_action = (evt) => this.activate_field(evt) @active_field = false @mouse_on_container = false @results_showing = false @result_highlighted = null @is_rtl = @options.rtl || /\bchosen-rtl\b/.test(@form_field.className) @allow_single_deselect = if @options.allow_single_deselect? and @form_field.options[0]? and @form_field.options[0].text is "" then @options.allow_single_deselect else false @disable_search_threshold = @options.disable_search_threshold || 0 @disable_search = @options.disable_search || false @enable_split_word_search = if @options.enable_split_word_search? then @options.enable_split_word_search else true @group_search = if @options.group_search? then @options.group_search else true @search_contains = @options.search_contains || false @single_backstroke_delete = if @options.single_backstroke_delete? then @options.single_backstroke_delete else true @max_selected_options = @options.max_selected_options || Infinity @inherit_select_classes = @options.inherit_select_classes || false @display_selected_options = if @options.display_selected_options? then @options.display_selected_options else true @display_disabled_options = if @options.display_disabled_options? then @options.display_disabled_options else true @include_group_label_in_selected = @options.include_group_label_in_selected || false @max_shown_results = @options.max_shown_results || Number.POSITIVE_INFINITY @case_sensitive_search = @options.case_sensitive_search || false @hide_results_on_select = if @options.hide_results_on_select? then @options.hide_results_on_select else true set_default_text: -> if @form_field.getAttribute("data-placeholder") @default_text = @form_field.getAttribute("data-placeholder") else if @is_multiple @default_text = @options.placeholder_text_multiple || @options.placeholder_text || AbstractChosen.default_multiple_text else @default_text = @options.placeholder_text_single || @options.placeholder_text || AbstractChosen.default_single_text @default_text = this.escape_html(@default_text) @results_none_found = @form_field.getAttribute("data-no_results_text") || @options.no_results_text || AbstractChosen.default_no_result_text choice_label: (item) -> if @include_group_label_in_selected and item.group_label? "#{this.escape_html(item.group_label)}#{item.html}" else item.html mouse_enter: -> @mouse_on_container = true mouse_leave: -> @mouse_on_container = false input_focus: (evt) -> if @is_multiple setTimeout (=> this.container_mousedown()), 50 unless @active_field else @activate_field() unless @active_field input_blur: (evt) -> if not @mouse_on_container @active_field = false setTimeout (=> this.blur_test()), 100 label_click_handler: (evt) => if @is_multiple this.container_mousedown(evt) else this.activate_field() results_option_build: (options) -> content = '' shown_results = 0 for data in @results_data data_content = '' if data.group data_content = this.result_add_group data else data_content = this.result_add_option data if data_content != '' shown_results++ content += data_content # this select logic pins on an awkward flag # we can make it better if options?.first if data.selected and @is_multiple this.choice_build data else if data.selected and not @is_multiple this.single_set_selected_text(this.choice_label(data)) if shown_results >= @max_shown_results break content result_add_option: (option) -> return '' unless option.search_match return '' unless this.include_option_in_results(option) classes = [] classes.push "active-result" if !option.disabled and !(option.selected and @is_multiple) classes.push "disabled-result" if option.disabled and !(option.selected and @is_multiple) classes.push "result-selected" if option.selected classes.push "group-option" if option.group_array_index? classes.push option.classes if option.classes != "" option_el = document.createElement("li") option_el.className = classes.join(" ") option_el.style.cssText = option.style if option.style option_el.setAttribute("data-option-array-index", option.array_index) option_el.innerHTML = option.highlighted_html or option.html option_el.title = option.title if option.title this.outerHTML(option_el) result_add_group: (group) -> return '' unless group.search_match || group.group_match return '' unless group.active_options > 0 classes = [] classes.push "group-result" classes.push group.classes if group.classes group_el = document.createElement("li") group_el.className = classes.join(" ") group_el.innerHTML = group.highlighted_html or this.escape_html(group.label) group_el.title = group.title if group.title this.outerHTML(group_el) results_update_field: -> this.set_default_text() this.results_reset_cleanup() if not @is_multiple this.result_clear_highlight() this.results_build() this.winnow_results() if @results_showing reset_single_select_options: () -> for result in @results_data result.selected = false if result.selected results_toggle: -> if @results_showing this.results_hide() else this.results_show() results_search: (evt) -> if @results_showing this.winnow_results() else this.results_show() winnow_results: (options) -> this.no_results_clear() results = 0 query = this.get_search_text() escapedQuery = query.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") regex = this.get_search_regex(escapedQuery) for option in @results_data option.search_match = false results_group = null search_match = null option.highlighted_html = '' if this.include_option_in_results(option) if option.group option.group_match = false option.active_options = 0 if option.group_array_index? and @results_data[option.group_array_index] results_group = @results_data[option.group_array_index] results += 1 if results_group.active_options is 0 and results_group.search_match results_group.active_options += 1 text = if option.group then option.label else option.text unless option.group and not @group_search search_match = this.search_string_match(text, regex) option.search_match = search_match? results += 1 if option.search_match and not option.group if option.search_match if query.length startpos = search_match.index prefix = text.slice(0, startpos) fix = text.slice(startpos, startpos + query.length) suffix = text.slice(startpos + query.length) option.highlighted_html = "#{this.escape_html(prefix)}#{this.escape_html(fix)}#{this.escape_html(suffix)}" results_group.group_match = true if results_group? else if option.group_array_index? and @results_data[option.group_array_index].search_match option.search_match = true this.result_clear_highlight() if results < 1 and query.length this.update_results_content "" this.no_results query else this.update_results_content this.results_option_build() this.winnow_results_set_highlight() unless options?.skip_highlight get_search_regex: (escaped_search_string) -> regex_string = if @search_contains then escaped_search_string else "(^|\\s|\\b)#{escaped_search_string}[^\\s]*" regex_string = "^#{regex_string}" unless @enable_split_word_search or @search_contains regex_flag = if @case_sensitive_search then "" else "i" new RegExp(regex_string, regex_flag) search_string_match: (search_string, regex) -> match = regex.exec(search_string) match.index += 1 if !@search_contains && match?[1] # make up for lack of lookbehind operator in regex match choices_count: -> return @selected_option_count if @selected_option_count? @selected_option_count = 0 for option in @form_field.options @selected_option_count += 1 if option.selected return @selected_option_count choices_click: (evt) -> evt.preventDefault() this.activate_field() this.results_show() unless @results_showing or @is_disabled keydown_checker: (evt) -> stroke = evt.which ? evt.keyCode this.search_field_scale() this.clear_backstroke() if stroke != 8 and @pending_backstroke switch stroke when 8 # backspace @backstroke_length = this.get_search_field_value().length break when 9 # tab this.result_select(evt) if @results_showing and not @is_multiple @mouse_on_container = false break when 13 # enter evt.preventDefault() if @results_showing break when 27 # escape evt.preventDefault() if @results_showing break when 32 # space evt.preventDefault() if @disable_search break when 38 # up arrow evt.preventDefault() this.keyup_arrow() break when 40 # down arrow evt.preventDefault() this.keydown_arrow() break keyup_checker: (evt) -> stroke = evt.which ? evt.keyCode this.search_field_scale() switch stroke when 8 # backspace if @is_multiple and @backstroke_length < 1 and this.choices_count() > 0 this.keydown_backstroke() else if not @pending_backstroke this.result_clear_highlight() this.results_search() break when 13 # enter evt.preventDefault() this.result_select(evt) if this.results_showing break when 27 # escape this.results_hide() if @results_showing break when 9, 16, 17, 18, 38, 40, 91 # don't do anything on these keys else this.results_search() break clipboard_event_checker: (evt) -> return if @is_disabled setTimeout (=> this.results_search()), 50 container_width: -> return if @options.width? then @options.width else "#{@form_field.offsetWidth}px" include_option_in_results: (option) -> return false if @is_multiple and (not @display_selected_options and option.selected) return false if not @display_disabled_options and option.disabled return false if option.empty return true search_results_touchstart: (evt) -> @touch_started = true this.search_results_mouseover(evt) search_results_touchmove: (evt) -> @touch_started = false this.search_results_mouseout(evt) search_results_touchend: (evt) -> this.search_results_mouseup(evt) if @touch_started outerHTML: (element) -> return element.outerHTML if element.outerHTML tmp = document.createElement("div") tmp.appendChild(element) tmp.innerHTML get_single_html: -> """ #{@default_text}