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:
parent
4772752c51
commit
36a096af05
7 changed files with 222 additions and 1 deletions
|
@ -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 ) )
|
||||
|
|
|
@ -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' )
|
|
@ -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')
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
----------------
|
||||
|
|
Loading…
Reference in a new issue