### Chosen, a Select Box Enhancer for jQuery and Protoype by Patrick Filler for Harvest, http://getharvest.com Available for use under the MIT License, http://en.wikipedia.org/wiki/MIT_License Copyright (c) 2011 by Harvest ### root = exports ? this $ = jQuery $.fn.extend({ chosen: (data, options) -> $(this).each((input_field) -> new Chosen(this, data, options) unless ($ this).hasClass "chzn-done" ) }) class Chosen constructor: (elmn) -> this.set_default_values() @form_field = elmn @form_field_jq = $ @form_field @is_multiple = @form_field.multiple @default_text_default = if @form_field.multiple then "Select Some Options" else "Select an Option" this.set_up_html() this.register_observers() @form_field_jq.addClass "chzn-done" set_default_values: -> @click_test_action = (evt) => this.test_active_click(evt) @active_field = false @mouse_on_container = false @results_showing = false @result_highlighted = null @result_single_selected = null @choices = 0 set_up_html: -> @container_id = @form_field.id + "_chzn" @f_width = @form_field_jq.width() @default_text = if @form_field_jq.attr 'title' then @form_field_jq.attr 'title' else @default_text_default container_div = ($ "
", { id: @container_id class: 'chzn-container' style: 'width: ' + (@f_width) + 'px;' #use parens around @f_width so coffeescript doesn't think + ' px' is a function parameter }) if @is_multiple container_div.html '
' else container_div.html '' + @default_text + '
' @form_field_jq.hide().after container_div @container = ($ '#' + @container_id) @container.addClass( "chzn-container-" + (if @is_multiple then "multi" else "single") ) @dropdown = @container.find('div.chzn-drop').first() dd_top = @container.height() dd_width = (@f_width - get_side_border_padding(@dropdown)) @dropdown.css({"width": dd_width + "px", "top": dd_top + "px"}) @search_field = @container.find('input').first() @search_results = @container.find('ul.chzn-results').first() this.search_field_scale() @search_no_results = @container.find('li.no-results').first() if @is_multiple @search_choices = @container.find('ul.chzn-choices').first() @search_container = @container.find('li.search-field').first() else @search_container = @container.find('div.chzn-search').first() @selected_item = @container.find('.chzn-single').first() sf_width = dd_width - get_side_border_padding(@search_container) - get_side_border_padding(@search_field) @search_field.css( {"width" : sf_width + "px"} ) this.results_build() this.set_tab_index() register_observers: -> @container.click (evt) => this.container_click(evt) @container.mouseenter (evt) => this.mouse_enter(evt) @container.mouseleave (evt) => this.mouse_leave(evt) @search_results.click (evt) => this.search_results_click(evt) @search_results.mouseover (evt) => this.search_results_mouseover(evt) @search_results.mouseout (evt) => this.search_results_mouseout(evt) @form_field_jq.bind "liszt:updated", (evt) => this.results_update_field(evt) @search_field.blur (evt) => this.input_blur(evt) @search_field.keyup (evt) => this.keyup_checker(evt) @search_field.keydown (evt) => this.keydown_checker(evt) if @is_multiple @search_choices.click (evt) => this.choices_click(evt) @search_field.focus (evt) => this.input_focus(evt) else @selected_item.focus (evt) => this.activate_field(evt) container_click: (evt) -> if evt and evt.type is "click" evt.stopPropagation() if not @pending_destroy_click if not @active_field @search_field.val "" if @is_multiple $(document).click @click_test_action this.results_show() else if not @is_multiple and evt and ($(evt.target) is @selected_item || $(evt.target).parents("a.chzn-single").length) evt.preventDefault() this.results_toggle() this.activate_field() else @pending_destroy_click = false mouse_enter: -> @mouse_on_container = true mouse_leave: -> @mouse_on_container = false input_focus: (evt) -> setTimeout (=> this.container_click()), 50 unless @active_field input_blur: (evt) -> if not @mouse_on_container @active_field = false setTimeout (=> this.blur_test()), 100 blur_test: (evt) -> this.close_field() if not @active_field and @container.hasClass "chzn-container-active" close_field: -> $(document).unbind "click", @click_test_action if not @is_multiple @selected_item.attr "tabindex", @search_field.attr("tabindex") @search_field.attr "tabindex", -1 @active_field = false this.results_hide() @container.removeClass "chzn-container-active" this.winnow_results_clear() this.clear_backstroke() this.show_search_field_default() this.search_field_scale() activate_field: -> if not @is_multiple and not @active_field @search_field.attr "tabindex", (@selected_item.attr "tabindex") @selected_item.attr "tabindex", -1 @container.addClass "chzn-container-active" @active_field = true @search_field.val(@search_field.val()) @search_field.focus() test_active_click: (evt) -> if $(evt.target).parents('#' + @container.id).length @active_field = true else this.close_field() results_build: -> startTime = new Date() @parsing = true @results_data = SelectParser.select_to_array @form_field if @is_multiple and @choices > 0 @search_choices.find("li.search-choice").remove() @choices = 0 else if not @is_multiple @selected_item.find("span").text @default_text content = '' for data in @results_data if data.group content += this.result_add_group data else if !data.empty content += this.result_add_option data if data.selected and @is_multiple this.choice_build data else if data.selected and not @is_multiple @selected_item.find("span").text data.text this.show_search_field_default() this.search_field_scale() @search_results.html content @parsing = false result_add_group: (group) -> if not group.disabled group.dom_id = @form_field.id + "chzn_g_" + group.array_index '
  • ' + $("
    ").text(group.label).html() + '
  • ' else "" result_add_option: (option) -> if not option.disabled option.dom_id = @form_field.id + "chzn_o_" + option.array_index classes = if option.selected and @is_multiple then [] else ["active-result"] classes.push "result-selected" if option.selected classes.push "group-option" if option.group_array_index? '
  • ' + $("
    ").text(option.text).html() + '
  • ' else "" results_update_field: -> this.result_clear_highlight() @result_single_selected = null this.results_build() 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_toggle: -> if @results_showing this.results_hide() else this.results_show() results_show: -> if not @is_multiple @selected_item.addClass "chzn-single-with-drop" if @result_single_selected this.result_do_highlight( @result_single_selected ) dd_top = if @is_multiple then @container.height() else (@container.height() - 1) @dropdown.css {"top": dd_top + "px", "left":0} @results_showing = true @search_field.focus() @search_field.val @search_field.val() this.winnow_results() results_hide: -> @selected_item.removeClass "chzn-single-with-drop" unless @is_multiple this.result_clear_highlight() @dropdown.css {"left":"-9000px"} @results_showing = false set_tab_index: (el) -> if @form_field_jq.attr "tabindex" ti = @form_field_jq.attr "tabindex" @form_field_jq.attr "tabindex", -1 if @is_multiple @search_field.attr "tabindex", ti else @selected_item.attr "tabindex", ti @search_field.attr "tabindex", -1 show_search_field_default: -> if @is_multiple and @choices < 1 and not @active_field @search_field.val(@default_text) @search_field.addClass "default" else @search_field.val("") @search_field.removeClass "default" search_results_click: (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() 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() choices_click: (evt) -> evt.preventDefault() if( @active_field and not($(evt.target).hasClass "search-choice" or $(evt.target).parents('.search-choice').first) and not @results_showing ) this.results_show() choice_build: (item) -> choice_id = @form_field.id + "_chzn_c_" + item.array_index @choices += 1 @search_container.before '
  • ' + item.text + '
  • ' link = $('#' + choice_id).find("a").first() link.click (evt) => this.choice_destroy_link_click(evt) choice_destroy_link_click: (evt) -> evt.preventDefault() @pending_destroy_click = true this.choice_destroy $(evt.target) choice_destroy: (link) -> @choices -= 1 this.show_search_field_default() this.results_hide() if @is_multiple and @choices > 0 and @search_field.val().length < 1 this.result_deselect (link.attr "rel") link.parents('li').first().remove() result_select: -> if @result_highlight high = @result_highlight high_id = high.attr "id" this.result_clear_highlight() high.addClass "result-selected" if @is_multiple this.result_deactivate high else @result_single_selected = high position = high_id.substr(high_id.lastIndexOf("_") + 1 ) item = @results_data[position] item.selected = true @form_field.options[item.options_index].selected = true if @is_multiple this.choice_build item else @selected_item.find("span").first().text item.text this.results_hide() @search_field.val "" @form_field_jq.trigger "change" this.search_field_scale() result_activate: (el) -> el.addClass("active-result").show() result_deactivate: (el) -> el.removeClass("active-result").hide() result_deselect: (pos) -> result_data = @results_data[pos] result_data.selected = false @form_field.options[result_data.options_index].selected = false result = $("#" + @form_field.id + "chzn_o_" + pos) result.removeClass("result-selected").addClass("active-result").show() this.result_clear_highlight() this.winnow_results() @form_field_jq.trigger "change" this.search_field_scale() results_search: (evt) -> if @results_showing this.winnow_results() else this.results_show() winnow_results: -> startTime = new Date() this.no_results_clear() results = 0 searchText = if @search_field.val() is @default_text then "" else $.trim @search_field.val() regex = new RegExp('^' + searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i') zregex = new RegExp(searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i') for option in @results_data if not option.disabled and not option.empty if option.group $('#' + option.dom_id).hide() else if not (@is_multiple and option.selected) found = false result_id = option.dom_id if regex.test option.text found = true results += 1 else if option.text.indexOf(" ") >= 0 or option.text.indexOf("[") == 0 #TODO: replace this substitution of /\[\]/ with a list of characters to skip. parts = option.text.replace(/\[|\]/g, "").split(" ") if parts.length for part in parts if regex.test part found = true results += 1 if found if searchText.length startpos = option.text.search zregex text = option.text.substr(0, startpos + searchText.length) + '' + option.text.substr(startpos + searchText.length) text = text.substr(0, startpos) + '' + text.substr(startpos) else text = option.text $("#" + result_id).html text if $("#" + result_id).html != text this.result_activate $("#" + result_id) $("#" + @results_data[option.group_array_index].dom_id).show() if option.group_array_index? else this.result_clear_highlight() if @result_highlight and result_id is @result_highlight.attr 'id' this.result_deactivate $("#" + result_id) if results < 1 and searchText.length this.no_results searchText else this.winnow_results_set_highlight() winnow_results_clear: -> @search_field.val "" lis = @search_results.find("li") for li in lis li = $(li) if li.hasClass "group-result" li.show() else if not @is_multiple or not li.hasClass "result-selected" this.result_activate li winnow_results_set_highlight: -> if not @result_highlight do_high = @search_results.find(".active-result").first() if(do_high) this.result_do_highlight do_high no_results: (terms) -> no_results_html = $('
  • No results match ""
  • ') no_results_html.find("span").first().text(terms) @search_results.append no_results_html no_results_clear: -> @search_results.find(".no-results").remove() keydown_arrow: -> if not @result_highlight first_active = @search_results.find("li.active-result").first() this.result_do_highlight $(first_active) if first_active else if @results_showing next_sib = @result_highlight.nextAll("li.active-result").first() this.result_do_highlight next_sib if next_sib this.results_show() if not @results_showing 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 @choices > 0 this.result_clear_highlight() keydown_backstroke: -> if @pending_backstroke this.choice_destroy @pending_backstroke.find("a").first() this.clear_backstroke() else @pending_backstroke = @search_container.siblings("li.search-choice").last() @pending_backstroke.addClass "search-choice-focus" clear_backstroke: -> @pending_backstroke.removeClass "search-choice-focus" if @pending_backstroke @pending_backstroke = null keyup_checker: (evt) -> stroke = evt.which ? evt.keyCode this.search_field_scale() switch stroke when 8 if @is_multiple and @backstroke_length < 1 and @choices > 0 this.keydown_backstroke() else if not @pending_backstroke this.result_clear_highlight() this.results_search() when 13 evt.preventDefault() this.result_select() if this.results_showing when 27 this.results_hide() if @results_showing when 9, 38, 40, 16 # don't do anything on these keys else this.results_search() keydown_checker: (evt) -> stroke = evt.which ? evt.keyCode this.search_field_scale() this.clear_backstroke() if stroke != 8 and this.pending_backstroke switch stroke when 8 @backstroke_length = this.search_field.val().length break when 9 @mouse_on_container = false break when 13 evt.preventDefault() break when 38 evt.preventDefault() this.keyup_arrow() break when 40 this.keydown_arrow() break search_field_scale: -> if @is_multiple h = 0 w = 0 style_block = "position:absolute; left: -1000px; top: -1000px; display:none;" styles = ['font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing'] for style in styles style_block += style + ":" + @search_field.css(style) + ";" div = $('
    ', { 'style' : style_block }) div.text @search_field.val() $('body').append div w = div.width() + 25 div.remove() if( w > @f_width-10 ) w = @f_width - 10 @search_field.css({'width': w + 'px'}) dd_top = @container.height() @dropdown.css({"top": dd_top + "px"}) get_side_border_padding = (elmt) -> side_border_padding = elmt.outerWidth() - elmt.width() root.get_side_border_padding = get_side_border_padding class SelectParser constructor: -> @options_index = 0 @parsed = [] add_node: (child) -> if child.nodeName is "OPTGROUP" this.add_group child else this.add_option child add_group: (group) -> group_position = @parsed.length @parsed.push array_index: group_position group: true label: group.label children: 0 disabled: group.disabled this.add_option( option, group_position, group.disabled ) for option in group.childNodes add_option: (option, group_position, group_disabled) -> if option.nodeName is "OPTION" if option.text != "" if group_position? @parsed[group_position].children += 1 @parsed.push array_index: @parsed.length options_index: @options_index value: option.value text: option.text selected: option.selected disabled: if group_disabled is true then group_disabled else option.disabled group_array_index: group_position else @parsed.push array_index: @parsed.length options_index: @options_index empty: true @options_index += 1 SelectParser.select_to_array = (select) -> parser = new SelectParser() parser.add_node( child ) for child in select.childNodes parser.parsed root.SelectParser = SelectParser