Skip welcome & menu and move to editor
Welcome to JS Bin
Load cached copy from
 
<!DOCTYPE html>
<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

Dismiss x
public
Bin info
ilyavfpro
0viewers