Source: components/sl-select.js

import Ember from 'ember';
import InputBased from '../mixins/sl-input-based';
import TooltipEnabled from '../mixins/sl-tooltip-enabled';
import layout from '../templates/components/sl-select';

/**
 * @module
 * @augments ember/Component
 * @augments module:mixins/sl-input-based
 * @augments module:mixins/sl-tooltip-based
 */
export default Ember.Component.extend( InputBased, TooltipEnabled, {

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNames: [
        'form-group',
        'sl-select'
    ],

    /** @type {Object} */
    layout,

    // -------------------------------------------------------------------------
    // Actions

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    /**
     * Whether to show the search filter input or not
     *
     * @type {Boolean}
     */
    disableSearch: false,

    /**
     * The internal input element, used for Select2's bindings
     *
     * @type {?Object}
     */
    input: null,

    /**
     * The maximum number of selections allowed when `multiple` is enabled
     *
     * @type {?Number}
     */
    maximumSelectionSize: null,

    /**
     * Whether to allow multiple selections
     *
     * @type {Boolean}
     */
    multiple: false,

    /**
     * The path key for each option object's description
     *
     * @type {String}
     */
    optionDescriptionPath: 'description',

    /**
     * The path key for each option object's label
     *
     * @type {String}
     */
    optionLabelPath: 'label',

    /**
     * The path key for each option object's value
     *
     * @type {String}
     */
    optionValuePath: 'value',

    /**
     * The current value of the select input
     *
     * @type {?Array|String}
     */
    value: null,

    // -------------------------------------------------------------------------
    // Observers

    /**
     * Teardown the select2 to prevent memory leaks
     *
     * @function
     * @returns {undefined}
     */
    destroySelect2: Ember.on(
        'willClearRender',
        function() {
            this.input.off( 'change' ).select2( 'destroy' );
        }
    ),

    /**
     * Set up select2 initialization after the element is inserted in the DOM
     *
     * @function
     * @returns {undefined}
     */
    setupSelect2: Ember.on(
        'didInsertElement',
        function() {
            const input = this.$( 'input' ).select2({
                maximumSelectionSize: this.get( 'maximumSelectionSize' ),
                multiple: this.get( 'multiple' ),
                placeholder: this.get( 'placeholder' ),

                formatResult: ( item ) => {
                    if ( !item ) {
                        return null;
                    }

                    if ( Ember.typeOf( item ) !== 'object' && Ember.typeOf( item ) !== 'instance' ) {
                        return item;
                    }

                    const description = Ember.get(
                        item,
                        this.get( 'optionDescriptionPath' )
                    );

                    let output = Ember.get(
                        item,
                        this.get( 'optionLabelPath' )
                    );

                    if ( description ) {
                        output += ' <span class="text-muted">' +
                            description + '</span>';
                    }

                    return output;
                },

                formatSelection: ( item ) => {
                    if ( !item ) {
                        return null;
                    }

                    const typeOfItem = Ember.typeOf( item );

                    if (
                        'object' === typeOfItem ||
                        'instance' === typeOfItem
                    ) {
                        return Ember.get( item, this.get( 'optionLabelPath' ) );
                    }

                    return item;
                },

                id: ( item ) => {
                    let value = item;
                    const typeOfItem = Ember.typeOf( item );

                    if (
                        'object' === typeOfItem ||
                        'instance' === typeOfItem
                    ) {
                        const optionValuePath = this.get( 'optionValuePath' );
                        value = Ember.get( item, optionValuePath );
                    }

                    return value;
                },

                initSelection: ( element, callback ) => {
                    const value = element.val();

                    if ( !value || !value.length ) {
                        return callback( [] );
                    }

                    const content = this.get( 'content' );
                    const contentLength = content.length;
                    const filteredContent = [];
                    const multiple = this.get( 'multiple' );
                    const optionValuePath = this.get( 'optionValuePath' );
                    const values = 'array' === Ember.typeOf( value ) ? value : value.split( ',' );
                    let unmatchedValues = values.length;

                    for ( let i = 0; i < contentLength; i++ ) {
                        const item = content[i];
                        const typeOfItem = Ember.typeOf( item );
                        const text = 'object' === typeOfItem ||
                            'instance' === typeOfItem ?
                            Ember.get( item, optionValuePath ) :
                            item;

                        const matchIndex = values.indexOf( text.toString() );

                        if ( matchIndex !== -1 ) {
                            filteredContent[ matchIndex ] = item;
                            if ( 0 === --unmatchedValues ) {
                                break;
                            }
                        }
                    }

                    if ( 0 === unmatchedValues ) {
                        element.select2( 'readonly', false );
                    } else {
                        element.select2( 'readonly', true );

                        const warning = 'sl-select:select2#initSelection was' +
                            ' not able to map each "' + optionValuePath + '"' +
                            ' to an object from "content". The remaining keys' +
                            ' are: ' + values + '. The input will be disabled' +
                            ' until a) the desired objects are added to the' +
                            ' "content" array, or b) the "value" is changed.';

                        Ember.warn( warning, !values.length );
                    }

                    return callback(
                        multiple ?
                        filteredContent :
                        Ember.get( filteredContent, 'firstObject' )
                    );
                },

                minimumResultsForSearch: this.get( 'disableSearch' ) ? -1 : 0,

                query: ( query ) => {
                    const content = this.get( 'content' ) || [];
                    const optionLabelPath = this.get( 'optionLabelPath' );
                    const select2 = input.data( 'select2' ).opts;

                    query.callback({
                        results: content.reduce( ( results, item ) => {
                            const typeOfItem = Ember.typeOf( item );
                            const text = 'object' === typeOfItem ||
                                'instance' === typeOfItem ?
                                Ember.get( item, optionLabelPath ) :
                                item;

                            if (
                                text &&
                                select2.matcher( query.term, text.toString() )
                            ) {
                                results.push( item );
                            }

                            return results;
                        }, [] )
                    });
                }
            });

            input.on( 'change', () => {
                this.set( 'value', input.select2( 'val' ) );
            });

            if ( !this.get( 'multiple' ) ) {
                this.$( 'input.select2-input' ).attr(
                    'placeholder',
                    'Search...'
                );
            }

            this.input = input;
        }
    )

    // -------------------------------------------------------------------------
    // Methods

});