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 )
|
completion = new App.UserOrganizationAutocompletion( attribute: attribute )
|
||||||
item = completion.element()
|
item = completion.element()
|
||||||
|
|
||||||
|
# searchable select
|
||||||
|
else if attribute.tag is 'searchable_select'
|
||||||
|
select = new App.SearchableSelect( attribute: attribute )
|
||||||
|
item = select.element()
|
||||||
|
|
||||||
# autocompletion
|
# autocompletion
|
||||||
else if attribute.tag is 'autocompletion'
|
else if attribute.tag is 'autocompletion'
|
||||||
item = $( App.view('generic/autocompletion')( attribute: attribute ) )
|
item = $( App.view('generic/autocompletion')( attribute: attribute ) )
|
||||||
|
|
|
@ -1388,5 +1388,24 @@ class schedulersRef extends App.ControllerContent
|
||||||
|
|
||||||
App.Config.set( 'layout_ref/schedulers', schedulersRef, 'Routes' )
|
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' )
|
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>
|
<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/schedulers">Schedulers</a></li>
|
||||||
<li><a href="#layout_ref/sla">Service Level Agreements</a></li>
|
<li><a href="#layout_ref/sla">Service Level Agreements</a></li>
|
||||||
<li><a href="#layout_ref/user_list">User List</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;
|
vertical-align: top;
|
||||||
margin: 2px 0 0 5px;
|
margin: 2px 0 0 5px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokenfield .token .token-label {
|
.tokenfield .token .token-label {
|
||||||
|
@ -4329,6 +4330,7 @@ footer {
|
||||||
height: 39px;
|
height: 39px;
|
||||||
line-height: 39px;
|
line-height: 39px;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown li:not(:first-child) {
|
.dropdown li:not(:first-child) {
|
||||||
|
@ -4340,7 +4342,7 @@ footer {
|
||||||
background: hsl(205,90%,60%);
|
background: hsl(205,90%,60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown li:not(.recipientList-controls):hover + li,
|
.dropdown li:hover + li,
|
||||||
.dropdown li.is-active + li {
|
.dropdown li.is-active + li {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
@ -4362,6 +4364,10 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown.dropdown--actions {
|
.dropdown.dropdown--actions {
|
||||||
|
.dropdown-menu {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -4468,6 +4474,10 @@ footer {
|
||||||
@extend .u-clickable;
|
@extend .u-clickable;
|
||||||
padding: 0 10px !important;
|
padding: 0 10px !important;
|
||||||
background: hsl(206,7%,28%) !important;
|
background: hsl(206,7%,28%) !important;
|
||||||
|
|
||||||
|
& + li {
|
||||||
|
box-shadow: 0 1px rgba(255,255,255,.13) inset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipientList-organizationMembers {
|
.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