<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
</head>
<body>
<script type="text/stache" id="template">
<multi-select select-all>
{{#each items}}
<option value="{{value}}">{{text}}</option>
{{/each}}
</multi-select>
</script>
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="//canjs.com/release/2.3.7/can.jquery.js"></script>
<script src="//canjs.com/release/2.3.7/can.map.define.js"></script>
<script src="//canjs.com/release/2.3.7/can.stache.js"></script>
<script type="text/stache" id="multi-select-tpl">
<div class="orig-options">
<content></content>
</div>
{{#if _list}}
<div class="btn-group open">
<button type="button" class="multiselect dropdown-toggle btn btn-default" title="None selected" aria-expanded="true"
can-click="{toggle}">
<span class="multiselect-selected-text">
{{#if selected.length}}
{{#if areAllSelected}}
{{allSelectedText}}
{{else}}
{{#eq selected.length 1}}
{{#if selected.0.text}} {{selected.0.text}} {{else}} {{selected.0}} {{/if}}
{{else}} {{selected.length}} selected {{/eq}}
{{/if}}
{{else}}
None selected
{{/if}}
</span> <b class="caret"></b>
</button>
{{#if isOpened}}
<ul class="multiselect-container dropdown-menu">
{{#if selectAll}}
<li class="{{#if areAllSelected}}checked{{/if}}">
<a tabindex="-1">
<label class="checkbox">
<input type="checkbox" can-value="{areAllSelected}">{{selectAllText}}
</label>
</a>
</li>
{{/if}}
{{#each _list}}
<li class="{{#if isSelected}}checked{{/if}}">
<a tabindex="{{@index}}">
<label class="checkbox">
<input type="checkbox" {{#if isSelected}}checked{{/if}} can-click="{select .}">{{#if text}} {{text}} {{else}} {{.}} {{/if}}
</label>
</a>
</li>
{{/each}}
</ul>
{{/if}}
</div>
{{/if}}
</script>
<script>
var template = can.view('multi-select-tpl');
var VM = can.Map.extend({
define: {
// API:
/**
* Option to turn on "Select All" checkbox.
*/
selectAll: {
value: false,
set: function(val){
if (val === '' || val === 'true' || val === true){
return true;
}
if (val === 'default'){
return 'default';
}
return false;
}
},
/**
* Option to provide a text of "Select All" checkbox.
*/
selectAllText: {
value: 'Select All'
},
/**
* Option to provide a text for label when all items are selected.
*/
allSelectedText: {
value: 'All Selected'
},
/**
* Option to provide a property name where value should be retrieved from.
*/
valueProp: {
value: 'value'
},
/**
* Option to provide a property name where text should be retrieved from.
*/
textProp: {
value: 'text'
},
/**
* Option to provide a property name where isSelected should be defined off.
*/
selectedProp: {
value: 'isSelected'
},
areAllSelected: {
get: function(){
return this.attr('_list.length') === this.attr('selected.length');
},
set: function(val){
if (!this.attr('_list.length')){
return val;
}
can.batch.start();
this.attr('_list').each(item => {
item.attr('isSelected', val);
});
can.batch.stop();
return val;
}
},
/**
* Source list of items for select options passed from parent context.
*/
list: {
value: []
},
/**
* Internal list of items for select options
*/
_list: {
value: []
},
/**
* List contains selected items of this._list
* @return {can.List} List of selected items.
*/
selected: {
get: function(){
return this.attr('_list').filter(item => item.attr('isSelected'));
}
},
/**
* @return {array} Array of selected values.
*/
selectedValues: {
get: function(){
return [].map.call(this.attr('selected'), item => item.attr('value'));
}
},
/**
* @return {array} Array of selected items (original from list if passed, or the same as _selected_.
*/
selectedItems: {
get: function(){
return [].map.call(this.attr('selected'), item => {
return item.attr('_item') || item;
});
}
},
/**
* Flag to show/hide list of items
*/
isOpened: {
type: 'boolean',
value: false
},
/**
* MutationObserver to updated _list on new items rendered in content.
*/
observer: {
type: '*'
}
},
select: function(item){
item.attr('isSelected', !item.attr('isSelected'));
},
toggle: function(){
this.attr('isOpened', !this.attr('isOpened'));
},
close: function(){
this.attr('isOpened', false);
},
/**
* Main init function for internal _list.
* @param {can.List} items
*/
initList: function(items){
var mappedItems;
// If no template content with <option> tags then get items from list:
if (!items || !items.length){
items = mapItems(this.attr('list'), this.attr('valueProp'), this.attr('textProp'), this.attr('selectedProp'));
}
// Preselect all:
if (this.attr('selectAll') === 'default'){
mappedItems = items.map(item => { return item.isSelected = true, item; });
} else {
mappedItems = items;
}
this.attr('_list').replace(mappedItems);
},
addItem: function(item){
this.attr('_list').push(item);
},
removeItem: function(item){
var pos = [].reduce.call(this.attr('_list'), function(acc, _item, i){
return _item.value === item.value ? i : acc;
}, -1);
this.attr('_list').splice(pos, 1);
}
});
can.Component.extend({
tag: 'multi-select',
template: template,
viewModel: VM,
events: {
inserted: function(el, ev){
var self = this;
this.viewModel.initList(getItems(el.find('option')));
var target = el.find('.orig-options')[0];
// Observe changes of the DOM option list:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
getItems(mutation.addedNodes).forEach(option => self.viewModel.addItem(option));
getItems(mutation.removedNodes).forEach(option => self.viewModel.removeItem(option));
});
});
// configuration of the observer:
var config = { childList: true };
// pass in the target node, as well as the observer options
observer.observe(target, config);
this.viewModel.attr('observer', observer);
},
/**
* Destroy the Mutation Observer when this component is torn down.
*/
removed: function(){
//stop observing
this.viewModel.attr('observer').disconnect();
},
'{document} click': function(el, ev){
if($(this.element).has(ev.target).length === 0){
this.viewModel.close();
}
}
}
});
/**
* Turns a nodeList list of OPTION elements into an array of data.
* @param {[type]} nodeList The node list containing the options.
* @return {[type]} An array representing the original OPTION elements.
*/
function getItems(nodeList){
return makeArr(nodeList)
.filter(node => node.nodeName === "OPTION")
.map(option => getItemFromOption(option));
}
/**
* Makes an object for internal list out of OPTION DOM element.
* @param {DOMNode} el
* @returns {{value: *, text: *, isSelected: *}}
*/
function getItemFromOption(el){
var $el = $(el);
return {value: $el.val(), text: $el.text(), isSelected: $el.is(':selected')};
}
/**
* Makes array from array-like structure and returns it.
* @param arrayLike
* @returns {Array.<T>}
*/
function makeArr(arrayLike){
return [].slice.call(arrayLike);
}
/**
* Maps value, text, and isSelected to attributes that exist on the provided list of data.
* @param {[type]} list The multi-select list.
* @param {[type]} valProp The property where the value resides in each list item.
* @param {[type]} textProp The property where the text / label resides in each list item.
* @param {[type]} selectedProp The property where the isChecked/Boolean resides in each list item.
* @return {[type]} An array of objects that contain value, text, and isSelected from
* the original list.
*/
function mapItems(list, valProp, textProp, selectedProp){
if (!list || !list.length){
return [];
}
return [].map.call(list, function(item, n){
if (item[valProp] === undefined || item[valProp] === null){
console.warn('A ' + valProp + ' property is undefined/null at index ' + n + '.');
}
return {
value: item[valProp],
text: item[textProp],
isSelected: !!item[selectedProp],
_item: item
};
});
}
</script>
</body>
</html>
var template1 = '' +
'<multi-select select-all ' +
' selected-values="{selectedValues}"> ' +
' <option value="0" selected>Option Zero</option>' +
' {{#each items}} ' +
' <option value="{{value}}">{{text}}</option> ' +
' {{/each}} ' +
'</multi-select> ' +
'Selected values: {{selected}} ';
var template2 = '<br><br>' +
'<multi-select select-all ' +
' list="{items}" ' +
' selected-values="{selectedValues2}">' +
'</multi-select> ' +
'Selected values: {{selected2}} ';
var VM = can.Map.extend({
define: {
items: {
value: [
{text: "Option One", value: 1},
{text: "Option Two", value: 2},
{text: "Option Three", value: 3}
]
},
selectedValues: {
value: []
},
selected: {
get: function(){
return this.attr('selectedValues').join(', ');
}
},
selectedValues2: {
value: []
},
selected2: {
get: function(){
return this.attr('selectedValues2').join(', ');
}
}
}
});
var vm = new VM(),
frag1 = can.stache(template1)(vm),
frag2 = can.stache(template2)(vm);
$('body').append(frag1);
//$('body').append('<br><br>');
$('body').append(frag2);
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |