create App.SearchableSelect

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
This commit is contained in:
Felix Niklas 2015-06-19 16:31:54 +02:00
parent 4772752c51
commit 36a096af05
7 changed files with 222 additions and 1 deletions

View file

@ -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 ) )

View file

@ -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' )

View file

@ -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')

View file

@ -0,0 +1,21 @@
<div class="dropdown-toggle" data-toggle="dropdown">
<input
class="form-control js-input<%= " #{ @class }" if @class %>"
placeholder="<%= @placeholder %>"
value="<%= @valueName %>"
>
<input
class="searchableSelect-shadow form-control js-shadow"
id="<%= @id %>"
name="<%= @name %>"
<%= @required %>
<%= @autofocus %>
value="<%= @value %>"
>
<svg class="icon-arrow-down"><use xlink:href="#icon-arrow-down" /></svg>
</div>
<ul class="dropdown-menu dropdown-menu-left" role="menu">
<% for row in @options: %>
<li role="presentation" class="js-value" data-value="<%= row.value %>"><%= row.name %>
<% end %>
</ul>

View file

@ -4,6 +4,7 @@
<ul>
<li><a href="#layout_ref/search_select">Searchable Select</a></li>
<li><a href="#layout_ref/schedulers">Schedulers</a></li>
<li><a href="#layout_ref/sla">Service Level Agreements</a></li>
<li><a href="#layout_ref/user_list">User List</a></li>

View file

@ -0,0 +1,34 @@
<div class="main flex">
<h1>Searchable Select</h1>
<h2>Normal select</h2>
<div class="u-positionOrigin">
<select id="a" class="form-control">
<option>Apple</option>
<option>Microsoft</option>
<option>Google</option>
<option>Deutsche Bahn</option>
<option>Sparkasse</option>
<option>Deutsche Post</option>
<option>Mitfahrzentrale</option>
<option>Starbucks</option>
<option>Mac Donalds</option>
<option>Flixbus</option>
<option>Betahaus</option>
<option>Bruno Banani</option>
<option>Alpina</option>
<option>Samsung</option>
<option>ChariTea</option>
<option>fritz-kola</option>
<option>Vitamin Water</option>
<option>Znuny</option>
</select>
<svg class="icon-arrow-down"><use xlink:href="#icon-arrow-down" /></svg>
</div>
<h2>Searchable select</h2>
<p class="subtle">Should always have a placeholder.</p>
<div class="searchableSelectPlaceholder"></div>
</div>

View file

@ -4030,6 +4030,7 @@ footer {
vertical-align: top;
margin: 2px 0 0 5px;
height: 25px;
display: inline-block;
}
.tokenfield .token .token-label {
@ -4329,6 +4330,7 @@ footer {
height: 39px;
line-height: 39px;
padding: 0 15px;
cursor: pointer;
}
.dropdown li:not(:first-child) {
@ -4340,7 +4342,7 @@ footer {
background: hsl(205,90%,60%);
}
.dropdown li:not(.recipientList-controls):hover + li,
.dropdown li:hover + li,
.dropdown li.is-active + li {
box-shadow: none;
}
@ -4362,6 +4364,10 @@ footer {
}
.dropdown.dropdown--actions {
.dropdown-menu {
color: white;
}
li {
display: flex;
align-items: center;
@ -4468,6 +4474,10 @@ footer {
@extend .u-clickable;
padding: 0 10px !important;
background: hsl(206,7%,28%) !important;
& + li {
box-shadow: 0 1px rgba(255,255,255,.13) inset;
}
}
.recipientList-organizationMembers {
@ -5470,6 +5480,26 @@ output {
}
}
.searchableSelect {
position: relative;
.searchableSelect-shadow {
display: none;
}
&.dropdown li:hover:not(.is-active) {
background: none;
}
&.dropdown li.is-hidden {
display: none;
}
li:not(.is-active):hover + li {
box-shadow: 0 1px rgba(255,255,255,.13) inset;
}
}
/*
----------------