Created
November 26, 2013 16:17
-
-
Save coderstash/7661201 to your computer and use it in GitHub Desktop.
Multi Select Component for Ember JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Put your CSS here */ | |
html, body { | |
margin: 20px; | |
} | |
/* for js bin */ | |
ul {margin: 0px; padding:0px;} | |
li {list-style-type: none} | |
.notice {color:red;} | |
.selected {background: #D1C903; color:white; padding: 5px; margin: 0px;} | |
.is_visible {display: block;} | |
.is_hidden {display: none;} | |
.content_section {position: relative; float: left; width: 100%; margin: 20px 0px;} | |
/* Selects */ | |
label {width: 100%;display: block;padding: 0px 0px 5px 0px;} | |
.select_wrap{margin-bottom:0px;width:205px;margin-right:5px;position: relative;float: left;} | |
.select_wrap.disabled,label.disabled{opacity:0.4;-ms-filter:alpha(opacity=40);} | |
.ie8 .select_wrap.disabled span.select,.ie8 .select_wrap.disabled span.select_toggle{background-color:#f5f5f5;} | |
/* Ember Custom Select Component */ | |
.multi_select_component input {width: 195px;padding: 3px;} | |
.multi_select_component .selected_option {position: relative;float: left;overflow: hidden;white-space: nowrap;background: #FFF;width: 193px;height: 14px;border: 1px solid #BBB;padding: 3px 5px 6px;} | |
.multi_select_component .selected_option span.select_toggle{display:block;right:0px;width:18px;border: none; cursor: pointer;} | |
.multi_select_component .selected_option span.select_toggle .toggle{border:4px solid transparent;border-top:4px solid #0095D8;width:0px;height:0px;position:absolute;top:9px;left:5px;display:block;} | |
.multi_select_component .select_options {background: #FFF;border: 1px solid #BBB; position: fixed;z-index: 20;padding: 0px 0px;} | |
.multi_select_component .select_options .option {white-space: nowrap;padding: 3px 5px 3px;width: 100%;min-width: 203px;box-sizing: border-box;height: 22px; cursor: pointer;} | |
.multi_select_component .select_options .option:hover, .multi_select_component .select_options .option.is_active {background: #0095D8; color:#fff;} | |
.multi_select_component .focus_wrap {position: absolute;width: 232px;height: 231px;right: 0px;top: 0px;} | |
.multi_select_component .list_options {position: relative;border-bottom: 1px solid #CCC; background: #fff; width: 100%;} | |
.multi_select_component .available_options {position: relative; max-height: 200px; overflow-y:scroll;overflow-x: hidden;} | |
.multi_select_component .selected_options {position: relative; border-top: 1px solid #CCC; max-height: 200px; overflow-y:scroll;overflow-x: hidden;} | |
.multi_select_component .selected_options .option.is_active {background: #FFA500; color:#fff;} | |
.multi_select_component span.select_toggle {display: block;right: 1px;width: 18px;border: none;cursor: pointer; top: 2px;height: 23px;position: absolute; bottom:0; } | |
.multi_select_component span.select_toggle .toggle {border: 4px solid rgba(0, 0, 0, 0);border-top: 4px solid #0095D8;width: 0px;height: 0px;position: absolute;top: 9px;left: 5px;display: block;} | |
.ie8 input {border: 1px solid #bbb;} | |
.ie8 .multi_select_component span.select_toggle {height: 21px; right: 1px;} | |
.mac.chrome .multi_select_component span.select_toggle {height: 22px;} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Ember Starter Kit</title> | |
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/normalize/2.1.0/normalize.css"> | |
</head> | |
<body> | |
<script type="text/x-handlebars"> | |
{{outlet}} | |
</script> | |
<script type="text/x-handlebars" data-template-name="index"> | |
{{multi-select label='Ember Multi Select' availableOptions=optionsForSelect selectedOptions=controllerSelectedOptions allSelectedProperty=allOptionsAreSelected }} | |
<section class='content_section'> | |
My Currently Selected Options | |
<ul> | |
{{#each option in controllerSelectedOptions }} | |
<li class='selected'>{{option.optionName}} - {{option.optionValue}}</li> | |
{{/each}} | |
</ul> | |
</section> | |
<section class='content_section notice'> | |
{{#if allOptionsAreSelected }} | |
All options have been selected | |
{{/if}} | |
</section> | |
</script> | |
<script type="text/x-handlebars" data-template-name="components/multi-select"> | |
<label> | |
{{ label }} | |
{{#if isRequired }}{{required}}{{/if}} | |
{{#if isInvalid }}<span class='icon small_icon field_error'>Required</span>{{/if}} | |
</label> | |
<section class="select_wrap" > | |
{{input value=filterInput type='text' placeholder=placeholderText}} | |
<span class="select_toggle" {{action focusInput on='click'}}><span class="toggle"></span></span> | |
<ul {{bind-attr class='optionsVisible:is_visible:is_hidden :select_options'}}> | |
<section class='list_options'> | |
<li class='option' {{action selectAll this on='click'}}>All</li> | |
<li class='option' {{action clearAll this on='click'}}>Clear</li> | |
</section> | |
<section class="available_options"> | |
{{#each filteredOptions }} | |
<li {{bind-attr class='isActive:is_active isHidden:is_hidden :option'}} {{action optionIsSelected this on='click'}}>{{ optionName }}</li> | |
{{/each}} | |
</section> | |
{{#if selectedOptions }} | |
<section class="selected_options"> | |
{{#each selectedOptions }} | |
<li class='is_active option' {{action removeFromSelectedList this on='click'}}>{{ optionName }}</li> | |
{{/each}} | |
</section> | |
{{/if}} | |
</ul> | |
</section> | |
</script> | |
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> | |
<script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script> | |
<script src="http://builds.emberjs.com/tags/v1.1.2/ember.min.js"></script> | |
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery-scrollTo/1.4.6/jquery.scrollTo.min.js"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App = Ember.Application.create(); | |
App.Router.map(function() { | |
// put your routes here | |
}); | |
App.IndexController = Ember.Controller.extend({ | |
optionsForSelect: [ | |
{optionName: 'George Washington', optionValue: 'gw'}, | |
{optionName: 'John Adams', optionValue: 'ja'}, | |
{optionName: 'Thomas Jefferson', optionValue: 'tj'}, | |
{optionName: 'James Madison', optionValue: 'jm'}, | |
{optionName: 'James Monroe', optionValue: 'jmon'}, | |
{optionName: 'John Quincy Adams', optionValue: 'jqa'}], | |
controllerSelectedOptions: [], | |
allOptionsAreSelected: false | |
}); | |
App.MultiSelectComponent = Ember.Component.extend({ | |
classNames: ['multi_select_component', 'field'], | |
optionsVisible: false, | |
selectedAction: null, | |
focusAction: null, | |
requiredProperty: null, | |
isRequired: false, | |
isInvalid: false, | |
currentFocusedOption: null, | |
placeholderText: function () { | |
if (Ember.isEmpty(this.get('selectedOptions'))) { | |
return 'Select one or more'; | |
} else if (this.get('selectedOptions').length === 1) { | |
return Ember.get(this.get('selectedOptions').objectAt(0), 'optionName'); | |
} else { | |
return 'Multiple values selected'; | |
} | |
} .property('selectedOptions.length'), | |
filterInput: function () { | |
return this.get('placeholderText'); | |
} .property('placeholderText'), | |
filteredOptions: function () { | |
var filterValue = this.get('filterInput'), | |
completeList = this.get('availableOptions'), | |
regex = new RegExp(this.get('filterInput'), 'i'), | |
filterList = null; | |
if (!filterValue || filterValue === this.get('placeholderText')) { | |
return this.get('availableOptions'); | |
} | |
return completeList.filter(function (item) { | |
return item.optionName.match(regex); | |
}); | |
} .property('filterInput', 'availableOptions.@each'), | |
resetOptionsList: function () { | |
if (this.get('selectedOptions.length') === 0) { | |
this.resetActiveStates(); | |
this.resetHiddenStates(); | |
this.resetFilters(); | |
this.set('allSelectedProperty', false); | |
} | |
} .observes('selectedOptions.length'), | |
nonAlphaNumericInput: function (event) { | |
if ( | |
(event.keyCode >= 48 && event.keyCode <= 57) || (event.keyCode >= 65 && event.keyCode <= 90) || (event.keyCode >= 96 && event.keyCode <= 105) || (event.keyCode == 46 || event.keyCode == 32 || event.keyCode == 8 || event.keyCode == 9 || event.keyCode == 27 || event.keyCode == 13 || event.keyCode == 189 || event.keyCode == 190) || (event.keyCode >= 35 && event.keyCode <= 40) | |
) { | |
return false; | |
} else { | |
event.preventDefault(); | |
return true; | |
} | |
}, | |
focusIn: function () { | |
if (this.get('optionsVisible') === true) { | |
return false; | |
} | |
this.set('filterInput', null); | |
this.set('optionsVisible', true); | |
this.setOptionsPlacement(); | |
}, | |
keyDown: function (e) { | |
var arrowKey = e.keyCode === 38 ? -1 : e.keyCode === 40 ? 1 : 0, // up or down | |
enterKey = e.keyCode === 13; | |
// Dont allow certain keyCodes that break the regex filter | |
if (this.nonAlphaNumericInput(e)) { | |
e.preventDefault(); | |
} | |
if (arrowKey) { | |
this.send('changeSelection', { | |
direction: arrowKey | |
}); | |
} | |
if (enterKey) { | |
this.send('enterSelection'); | |
} | |
}, | |
mouseLeave: function () { | |
this.set('optionsVisible', false); | |
this.$('input').blur(); | |
this.resetFilters(); | |
}, | |
resetFocus: function () { | |
// Need to blur focus because IE is AWESOME!! | |
this.$('input').blur(); | |
this.$('input').focus(); | |
}, | |
resetFilters: function () { | |
this.set('currentFocusedOption', null); | |
this.set('filterInput', this.get('placeholderText')); | |
}, | |
resetActiveStates: function () { | |
this.get('filteredOptions').setEach('isActive', false); | |
}, | |
resetHiddenStates: function () { | |
this.get('availableOptions').setEach('isHidden', false); | |
}, | |
toggleAllSelected: function () { | |
if (this.get('selectedOptions.length') === this.get('availableOptions.length')) { | |
this.set('allSelectedProperty', true); | |
} else { | |
this.set('allSelectedProperty', false); | |
} | |
}.observes('selectedOptions.length'), | |
getCurrentIndex: function (direction) { | |
var currentObject = this.get('currentFocusedOption'), | |
optionsList = this.get('filteredOptions'), | |
optionsCount = this.get('filteredOptions').get('length'); | |
if (Ember.isEmpty(currentObject)) { | |
return optionsCount - 1; | |
} else { | |
return optionsList.indexOf(currentObject) + direction == -1 ? optionsCount + direction : (optionsList.indexOf(currentObject) + direction) % optionsCount; | |
} | |
}, | |
nextVisibleObject: function (idx, direction) { | |
var optionsList = this.get('filteredOptions'), | |
optionsCount = this.get('filteredOptions').get('length'), | |
currentObject = optionsList.objectAt(idx); | |
if (Ember.get(currentObject, 'isHidden')) { | |
idx = idx + direction == -1 ? optionsCount + direction : (optionsList.indexOf(currentObject) + direction) % optionsCount; | |
return this.nextVisibleObject(idx, direction); | |
} else { | |
return currentObject; | |
} | |
}, | |
setActiveObject: function (selectedObject) { | |
this.resetActiveStates(); | |
Ember.set(selectedObject, 'isActive', true); | |
this.set('currentFocusedOption', selectedObject); | |
this.scrollToActiveObject(); | |
}, | |
scrollToActiveObject: function () { | |
$('.available_options').scrollTo('.is_active', 50, { | |
offset: -150 | |
}); | |
}, | |
setOptionsPlacement: function () { | |
var container = this.$(), | |
inputElement = container.find('input'), | |
filters = container.find('.select_options'), | |
targetPosition = inputElement.offset().top + inputElement.outerHeight(); | |
filters.css({ | |
top: targetPosition | |
}); | |
}, | |
actions: { | |
selectAll: function () { | |
this.set('selectedOptions', []); | |
this.get('selectedOptions').pushObjects(this.get('availableOptions')); | |
this.get('availableOptions').forEach(function (option) { | |
Ember.set(option, 'isHidden', true); | |
}); | |
this.resetActiveStates(); | |
this.set('allSelectedProperty', true); | |
}, | |
clearAll: function () { | |
this.set('selectedOptions', []); | |
this.resetFocus(); | |
}, | |
removeFromSelectedList: function (option) { | |
this.get('selectedOptions').removeObject(option); | |
Ember.set(option, 'isHidden', false); | |
this.set('allSelectedProperty', false); | |
this.resetFilters(); | |
this.resetFocus(); | |
}, | |
enterSelection: function () { | |
var currentSelected = this.get('currentFocusedOption'); | |
if (Ember.isEmpty(currentSelected)) { | |
return false; | |
} else { | |
this.send('optionIsSelected', currentSelected); | |
} | |
}, | |
changeSelection: function (params) { | |
if (Ember.typeOf(params) === 'object' && params.hasOwnProperty('direction')) { | |
this.setActiveObject(this.nextVisibleObject(this.getCurrentIndex(params.direction), params.direction)); | |
} | |
}, | |
optionIsSelected: function (option) { | |
if (this.get('selectedOptions').contains(option)) { | |
return false; | |
} | |
this.get('selectedOptions').pushObject(option); | |
Ember.set(option, 'isHidden', true); | |
Ember.set(option, 'isActive', false); | |
this.resetFilters(); | |
this.set('filterInput', null); | |
this.resetFocus(); | |
}, | |
focusInput: function () { | |
this.resetFocus(); | |
} | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment