### Chosen source: generate output using 'cake build' Copyright (c) 2011 by Harvest ### root = this class Chosen constructor: (@form_field, @options={}) -> this.set_default_values() @is_multiple = @form_field.multiple @is_rtl = @form_field.hasClassName "chzn-rtl" @default_text_default = if @form_field.multiple then "Select Some Options" else "Select an Option" this.set_up_html() this.register_observers() @form_field.addClassName "chzn-done" 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 @result_single_selected = null @allow_single_deselect = @options.allow_single_deselect || false @choices = 0 @results_none_found = @options.no_results_text or "No results match" # HTML Templates @single_temp = new Template('#{default}
') @multi_temp = new Template('
') @choice_temp = new Template('
  • #{choice}
  • ') @no_results_temp = new Template('
  • ' + @results_none_found + ' "#{terms}"
  • ') set_up_html: -> @container_id = @form_field.identify().replace(/(:|\.)/g, '_') + "_chzn" @f_width = if @form_field.getStyle("width") then parseInt @form_field.getStyle("width"), 10 else @form_field.getWidth() container_props = 'id': @container_id 'class': "chzn-container#{ if @is_rtl then ' chzn-rtl' else '' }" 'style': 'width: ' + (@f_width) + 'px' #use parens around @f_width so coffeescript doesn't think + ' px' is a function parameter @default_text = if @form_field.readAttribute 'data-placeholder' then @form_field.readAttribute 'data-placeholder' else @default_text_default base_template = if @is_multiple then new Element('div', container_props).update( @multi_temp.evaluate({ "default": @default_text}) ) else new Element('div', container_props).update( @single_temp.evaluate({ "default":@default_text }) ) @form_field.hide().insert({ after: base_template }) @container = $(@container_id) @container.addClassName( "chzn-container-" + (if @is_multiple then "multi" else "single") ) @dropdown = @container.down('div.chzn-drop') dd_top = @container.getHeight() dd_width = (@f_width - get_side_border_padding(@dropdown)) @dropdown.setStyle({"width": dd_width + "px", "top": dd_top + "px"}) @search_field = @container.down('input') @search_results = @container.down('ul.chzn-results') this.search_field_scale() @search_no_results = @container.down('li.no-results') if @is_multiple @search_choices = @container.down('ul.chzn-choices') @search_container = @container.down('li.search-field') else @search_container = @container.down('div.chzn-search') @selected_item = @container.down('.chzn-single') sf_width = dd_width - get_side_border_padding(@search_container) - get_side_border_padding(@search_field) @search_field.setStyle( {"width" : sf_width + "px"} ) this.results_build() this.set_tab_index() register_observers: -> @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) @form_field.observe "liszt:updated", (evt) => this.results_update_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) if @is_multiple @search_choices.observe "click", (evt) => this.choices_click(evt) @search_field.observe "focus", (evt) => this.input_focus(evt) search_field_disabled: -> @is_disabled = @form_field.disabled if(@is_disabled) @container.addClassName 'chzn-disabled' @search_field.disabled = true @selected_item.stopObserving "focus", @activate_action if !@is_multiple this.close_field() else @container.removeClassName 'chzn-disabled' @search_field.disabled = false @selected_item.observe "focus", @activate_action if !@is_multiple container_mousedown: (evt) -> if !@is_disabled target = evt.target if evt and evt.type is "mousedown" evt.stop() if not @pending_destroy_click and target.nodeName != "ABBR" if not @active_field @search_field.clear() if @is_multiple document.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.chzn-single")) this.results_toggle() this.activate_field() else @pending_destroy_click = false container_mouseup: (evt) -> this.results_reset(evt) if evt.target.nodeName is "ABBR" mouse_enter: -> @mouse_on_container = true mouse_leave: -> @mouse_on_container = false input_focus: (evt) -> setTimeout this.container_mousedown.bind(this), 50 unless @active_field input_blur: (evt) -> if not @mouse_on_container @active_field = false setTimeout this.blur_test.bind(this), 100 blur_test: (evt) -> this.close_field() if not @active_field and @container.hasClassName("chzn-container-active") close_field: -> document.stopObserving "click", @click_test_action if not @is_multiple @selected_item.tabIndex = @search_field.tabIndex @search_field.tabIndex = -1 @active_field = false this.results_hide() @container.removeClassName "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.tabIndex = @selected_item.tabIndex @selected_item.tabIndex = -1 @container.addClassName "chzn-container-active" @active_field = true @search_field.value = @search_field.value @search_field.focus() test_active_click: (evt) -> if evt.target.up('#' + @container_id) @active_field = true else this.close_field() results_build: -> startTime = new Date() @parsing = true @results_data = root.SelectParser.select_to_array @form_field if @is_multiple and @choices > 0 @search_choices.select("li.search-choice").invoke("remove") @choices = 0 else if not @is_multiple @selected_item.down("span").update(@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.down("span").update( data.html ) this.search_field_disabled() this.show_search_field_default() this.search_field_scale() @search_results.update content @parsing = false result_add_group: (group) -> if not group.disabled group.dom_id = @container_id + "_g_" + group.array_index '
  • ' + group.label.escapeHTML() + '
  • ' else "" result_add_option: (option) -> if not option.disabled option.dom_id = @container_id + "_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? '
  • ' + option.html + '
  • ' else "" results_update_field: -> this.result_clear_highlight() @result_single_selected = null this.results_build() 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_toggle: -> if @results_showing this.results_hide() else this.results_show() results_show: -> if not @is_multiple @selected_item.addClassName('chzn-single-with-drop') if @result_single_selected this.result_do_highlight( @result_single_selected ) dd_top = if @is_multiple then @container.getHeight() else (@container.getHeight() - 1) @dropdown.setStyle {"top": dd_top + "px", "left":0} @results_showing = true @search_field.focus() @search_field.value = @search_field.value this.winnow_results() results_hide: -> @selected_item.removeClassName('chzn-single-with-drop') unless @is_multiple this.result_clear_highlight() @dropdown.setStyle({"left":"-9000px"}) @results_showing = false set_tab_index: (el) -> if @form_field.tabIndex ti = @form_field.tabIndex @form_field.tabIndex = -1 if @is_multiple @search_field.tabIndex = ti else @selected_item.tabIndex = ti @search_field.tabIndex = -1 show_search_field_default: -> if @is_multiple and @choices < 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_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') choices_click: (evt) -> evt.preventDefault() if( @active_field and not(evt.target.hasClassName('search-choice') or evt.target.up('.search-choice')) and not @results_showing ) this.results_show() choice_build: (item) -> choice_id = @container_id + "_c_" + item.array_index @choices += 1 @search_container.insert before: @choice_temp.evaluate id: choice_id choice: item.html position: item.array_index link = $(choice_id).down('a') link.observe "click", (evt) => this.choice_destroy_link_click(evt) choice_destroy_link_click: (evt) -> evt.preventDefault() if not @is_disabled @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.value.length < 1 this.result_deselect link.readAttribute("rel") link.up('li').remove() results_reset: (evt) -> @form_field.options[0].selected = true @selected_item.down("span").update(@default_text) this.show_search_field_default() evt.target.remove(); result_select: (evt) -> if @result_highlight high = @result_highlight this.result_clear_highlight() if @is_multiple this.result_deactivate high else @search_results.descendants(".result-selected").invoke "removeClassName", "result-selected" @result_single_selected = high high.addClassName("result-selected") 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.down("span").update(item.html) @selected_item.down("span").insert { after: "" } if @allow_single_deselect this.results_hide() unless evt.metaKey and @is_multiple @search_field.value = "" @form_field.simulate("change") if typeof Event.simulate is 'function' this.search_field_scale() result_activate: (el) -> el.addClassName("active-result").show() result_deactivate: (el) -> el.removeClassName("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 = $(@container_id + "_o_" + pos) result.removeClassName("result-selected").addClassName("active-result").show() this.result_clear_highlight() this.winnow_results() @form_field.simulate("change") if typeof Event.simulate is 'function' 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.value is @default_text then "" else @search_field.value.strip().escapeHTML() 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.html found = true results += 1 else if option.html.indexOf(" ") >= 0 or option.html.indexOf("[") == 0 #TODO: replace this substitution of /\[\]/ with a list of characters to skip. parts = option.html.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.html.search zregex text = option.html.substr(0, startpos + searchText.length) + '' + option.html.substr(startpos + searchText.length) text = text.substr(0, startpos) + '' + text.substr(startpos) else text = option.html $(result_id).update text if $(result_id).innerHTML != 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_id) is @result_highlight 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.clear() lis = @search_results.select("li") for li in lis if li.hasClassName("group-result") li.show() else if not @is_multiple or not li.hasClassName("result-selected") this.result_activate li winnow_results_set_highlight: -> if not @result_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 @no_results_temp.evaluate( terms: terms ) no_results_clear: -> nr = null nr.remove() while nr = @search_results.down(".no-results") keydown_arrow: -> actives = @search_results.select("li.active-result") if actives.length if not @result_highlight this.result_do_highlight actives.first() else if @results_showing sibs = @result_highlight.nextSiblings() nexts = sibs.intersect(actives) this.result_do_highlight nexts.first() if nexts.length this.results_show() if not @results_showing 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 @choices > 0 this.result_clear_highlight() keydown_backstroke: -> if @pending_backstroke this.choice_destroy @pending_backstroke.down("a") this.clear_backstroke() else @pending_backstroke = @search_container.siblings("li.search-choice").last() @pending_backstroke.addClassName("search-choice-focus") clear_backstroke: -> @pending_backstroke.removeClassName("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(evt) if this.results_showing when 27 this.results_hide() if @results_showing when 9, 38, 40, 16, 91, 17 # 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.value.length when 9 @mouse_on_container = false when 13 evt.preventDefault() when 38 evt.preventDefault() this.keyup_arrow() when 40 this.keydown_arrow() 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.getStyle(style) + ";" div = new Element('div', { 'style' : style_block }).update(@search_field.value.escapeHTML()) document.body.appendChild(div) w = Element.measure(div, 'width') + 25 div.remove() if( w > @f_width-10 ) w = @f_width - 10 @search_field.setStyle({'width': w + 'px'}) dd_top = @container.getHeight() @dropdown.setStyle({"top": dd_top + "px"}) root.Chosen = Chosen # Prototype does not support version numbers so we add it ourselves if Prototype.Browser.IE if /MSIE (\d+\.\d+);/.test(navigator.userAgent) Prototype.BrowserFeatures['Version'] = new Number(RegExp.$1); get_side_border_padding = (elmt) -> layout = new Element.Layout(elmt) side_border_padding = layout.get("border-left") + layout.get("border-right") + layout.get("padding-left") + layout.get("padding-right") root.get_side_border_padding = get_side_border_padding