From 36a096af05a804ea0f0000c99e96d3039d33a4d5 Mon Sep 17 00:00:00 2001 From: Felix Niklas Date: Fri, 19 Jun 2015 16:31:54 +0200 Subject: [PATCH] create App.SearchableSelect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit together with @thorsteneckel: ✔️ dropdown stays open when the input is focused ✔️ click on item puts the name into the text-field ✔️ first item gets highlighted on dropdown-open ✔️ greedy regex filter while typing /a*.b*./i ✔️ first item gets highlighted while filtering ✔️ key up and down lets you navigate ✔️ enter applies the currently highlighted value ✔️ dropdown hides on ESC ✔️ dropdown can be opend via enter or key down when focused ✔️ returns the selected options value, not the name via a hidden input ✔️ preselect a value via selected: true on attribute.options --- .../_application_controller_form.js.coffee | 5 + .../app/controllers/layout_ref.js.coffee | 19 +++ .../lib/app_post/searchable_select.js.coffee | 111 ++++++++++++++++++ .../views/generic/searchable_select.jst.eco | 21 ++++ .../app/views/layout_ref/index.jst.eco | 1 + .../views/layout_ref/search_select.jst.eco | 34 ++++++ app/assets/stylesheets/zammad.css.scss | 32 ++++- 7 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/app/lib/app_post/searchable_select.js.coffee create mode 100644 app/assets/javascripts/app/views/generic/searchable_select.jst.eco create mode 100644 app/assets/javascripts/app/views/layout_ref/search_select.jst.eco diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee index 19d5b6e99..9bceebe26 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee @@ -1097,6 +1097,11 @@ class App.ControllerForm extends App.Controller completion = new App.UserOrganizationAutocompletion( attribute: attribute ) item = completion.element() + # searchable select + else if attribute.tag is 'searchable_select' + select = new App.SearchableSelect( attribute: attribute ) + item = select.element() + # autocompletion else if attribute.tag is 'autocompletion' item = $( App.view('generic/autocompletion')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/layout_ref.js.coffee b/app/assets/javascripts/app/controllers/layout_ref.js.coffee index 618add337..20c5df29b 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.js.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.js.coffee @@ -1388,5 +1388,24 @@ class schedulersRef extends App.ControllerContent App.Config.set( 'layout_ref/schedulers', schedulersRef, 'Routes' ) +class searchableSelectRef extends App.ControllerContent + + constructor: -> + super + @render() + + render: -> + searchableSelectObject = new App.SearchableSelect + attribute: + name: 'project-name' + id: 'project-name-123' + placeholder: 'Enter Project Name' + options: [{"value":0,"name":"Apple"},{"value":1,"name":"Microsoft","selected":true},{"value":2,"name":"Google"},{"value":3,"name":"Deutsche Bahn"},{"value":4,"name":"Sparkasse"},{"value":5,"name":"Deutsche Post"},{"value":6,"name":"Mitfahrzentrale"},{"value":7,"name":"Starbucks"},{"value":8,"name":"Mac Donalds"},{"value":9,"name":"Flixbus"},{"value":10,"name":"Betahaus"},{"value":11,"name":"Bruno Banani"},{"value":12,"name":"Alpina"},{"value":13,"name":"Samsung"},{"value":14,"name":"ChariTea"},{"value":15,"name":"fritz-kola"},{"value":16,"name":"Vitamin Water"},{"value":17,"name":"Znuny"}] + + @html App.view('layout_ref/search_select') + + @$('.searchableSelectPlaceholder').replaceWith( searchableSelectObject.el ) + +App.Config.set( 'layout_ref/search_select', searchableSelectRef, 'Routes' ) App.Config.set( 'LayoutRef', { prio: 1700, parent: '#current_user', name: 'Layout Reference', translate: true, target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/app_post/searchable_select.js.coffee b/app/assets/javascripts/app/lib/app_post/searchable_select.js.coffee new file mode 100644 index 000000000..eb5a8b762 --- /dev/null +++ b/app/assets/javascripts/app/lib/app_post/searchable_select.js.coffee @@ -0,0 +1,111 @@ +class App.SearchableSelect extends Spine.Controller + + events: + 'input .js-input': 'filterList' + 'click .js-value': 'selectItem' + 'mouseenter .js-value': 'highlightItem' + 'shown.bs.dropdown': 'onDropdownShown' + 'hidden.bs.dropdown': 'onDropdownHidden' + + elements: + '.js-value': 'values' + '.js-input': 'input' + '.js-shadow': 'shadowInput' + + className: 'searchableSelect dropdown dropdown--actions' + + element: => + @el + + constructor: -> + super + @render() + + render: -> + firstSelected = _.find @options.attribute.options, (option) -> option.selected + + if firstSelected + @options.attribute.valueName = firstSelected.name + @options.attribute.value = firstSelected.value + + @html App.view('generic/searchable_select')( @options.attribute ) + + @input.on 'keydown', @navigate + + onDropdownShown: => + @input.on 'click', @stopPropagation + @highlightFirst() + @isOpen = true + + onDropdownHidden: => + @input.off 'click', @stopPropagation + @values.removeClass '.is-active' + @isOpen = false + + toggle: => + @$('[data-toggle="dropdown"]').dropdown('toggle') + + stopPropagation: (event) -> + event.stopPropagation() + + navigate: (event) => + switch event.keyCode + when 40 then @nudge event, 1 # down + when 38 then @nudge event, -1 # up + when 13 then @selectHighlightedItem() # enter + when 27 then @onEscape() + + onEscape: -> + @toggle() if @isOpen + + nudge: (event, direction) -> + return @toggle() if not @isOpen + + event.preventDefault() + visibleValues = @values.not('.is-hidden') + highlightedItem = @values.filter('.is-active') + currentPosition = visibleValues.index(highlightedItem) + + currentPosition += direction + + return if currentPosition < 0 + return if currentPosition > visibleValues.size() - 1 + + @values.removeClass('is-active') + visibleValues.eq(currentPosition).addClass('is-active') + + selectItem: (event) -> + @input.val event.currentTarget.textContent.trim() + @shadowInput.val event.currentTarget.getAttribute('data-value') + + selectHighlightedItem: -> + if not @isOpen + return @toggle() + + @input.val @values.filter('.is-active').text().trim() + @shadowInput.val @values.filter('.is-active').attr('data-value') + @toggle() + + filterList: (event) => + @toggle() if not @isOpen + + query = @input.val() + @filterByQuery query + + filterByQuery: (query) -> + regex = new RegExp(query.split('').join('.*'), 'i') + + @values + .addClass 'is-hidden' + .filter -> + this.textContent.match(regex) + .removeClass 'is-hidden' + + @highlightFirst() + + highlightFirst: -> + @values.removeClass('is-active').not('.is-hidden').first().addClass 'is-active' + + highlightItem: (event) => + @values.removeClass('is-active') + $(event.currentTarget).addClass('is-active') \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/searchable_select.jst.eco b/app/assets/javascripts/app/views/generic/searchable_select.jst.eco new file mode 100644 index 000000000..aedfd2c91 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/searchable_select.jst.eco @@ -0,0 +1,21 @@ + + \ No newline at end of file diff --git a/app/assets/javascripts/app/views/layout_ref/index.jst.eco b/app/assets/javascripts/app/views/layout_ref/index.jst.eco index fa4c1bf0e..1593b38b8 100644 --- a/app/assets/javascripts/app/views/layout_ref/index.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/index.jst.eco @@ -4,6 +4,7 @@