busy-web/components

View on GitHub
addon/components/bc-select-menu.js

Summary

Maintainability
A
45 mins
Test Coverage
/**
 * @module Components
 *
 */
import $ from 'jquery';
import { A } from '@ember/array';
import { isNone, isEmpty } from '@ember/utils';
import EmberObject, { get, set, computed } from '@ember/object';
import Component from '@ember/component';
import CloseOnEscape from '../mixins/close-on-escape';
import BindOutsideClick from '../mixins/bind-outside-click';
import layout from '../templates/components/bc-select-menu';

/***/

function forEachOption(data, callback, target=null) {
    if (data.each) {
        data.each((idx, element) => {
            const el = $(element);
            callback.call(target, el, idx);
        });
    }
}

/**
 * `Component/BcSelectMenu`
 *
 * @class BcSelectMenu
 * @namespace Components
 * @extends Ember.Component
 */
export default Component.extend(CloseOnEscape, BindOutsideClick, {
  layout,
    classNames: ['bc-select-menu'],
    classNameBindings: ['right', 'isMenuOpen:open', 'fullwidth', 'large'],

    __closeActionName: 'closeMenu',

    /**
     * Flag for a larger drop down button
     *
     * @public
     * @property large
     * @type {boolean}
     */
    large: false,

    /**
     * Flag for open and close of the drop down
     *
     * @private
     * @property isMenuOpen
     * @type {boolean}
     */
    isMenuOpen: false,

    /**
     * Sets the class `right` so the dialog is
     * formatted for the right side of the screen
     *
     * @public
     * @property right
     * @type {boolean} default: false
     */
    right: false,

    /**
     * sets the class fullwidth to force the
     * select menu to use 100% of its container
     *
     * @public
     * @property fullwidth
     * @type {boolean}
     */
    fullwidth: false,

    /**
     * Forces the drop down to keep the same label
     * after an option is selected
     *
     * @public
     * @property disableChange
     * @type {boolean} default: false
     */
    disableChange: false,

    /**
     * Forces the drop down to remain open after
     * an option is selected
     *
     * @public
     * @property keepOpen
     * @type {boolean} default: false
     */
    keepOpen: false,

    /**
     * Default label for an unselected drop down initial state
     *
     * If this option is not set then the first item in the options list will
     * be used.
     *
     * @public
     * @property label
     * @type {string}
     * @default null
     */
    label: null,

    /**
        * action event triggered when a list option is clicked
        *
        * @public
        * @property onSelect
        * @type {event}
        */
    onSelect: null,

    /**
     * Array of options after parsed
     *
     * @private
     * @property listItem
     * @type {object[]}
     */
    listItem: null,

    /**
     * The current selected item from the list. This will be null
     * until an item is first selected.
     *
     * @private
     * @property selected
     * @type {object}
     */
    selected: null,

    /**
     * The text to display in the select menu button
     *
     * @private
     * @property selectedText
     * @type {string}
     */
    selectedText: computed('selected', 'label', 'listItem.[]', function() {
        if (!isNone(get(this, 'selected'))) { // look for a selected option first
            return get(this, 'selected.label');
        } else if (!isEmpty(get(this, 'label'))) { // if no selected option then look for a provided label
            return get(this, 'label');
        } else if (!isNone(get(this, 'listItem'))) { // no option or label then set it to the first option label
            return get(this, 'listItem.firstObject.label');
        } else { // otherwise just return an empty string
            return '';
        }
    }),

    /**
     * Initializes the listItem array
     *
     * @private
     * @method setup
     * @returns {void}
     */
    didRender() {
        this._super();

        if (this.$()) {
            // get options list
            const data = this.$('.hidden-template').children();
            if (this.hasChanges(data)) {
                this.createOptionsList(data);
            }
        }
    },

    /**
     * Override method that gets called to
     * validate the input and check if this item should
     * be added to the options list
     *
     * @private
     * @method shouldCreateOption
     * @param el {object} jquery element object
     * @returns {boolean}
     */
    shouldCreateOption(el) {
        // check if the element is set to hidden
        return isNone(el.attr('hidden'));
    },

    /**
     * Override method for creating a option item
     * object. for classes that need additional functionality
     * this can be overridden to create a more advanced object.
     *
     * @private
     * @method createOption
     * @param el {object} jquery element object
     * @returns {object}
     */
    createOption(el) {
        // create the option object
        const opt = EmberObject.create({
            class: el.attr('class'),
            label: el.text(),
            value: el.val(),
            selected: el.is(':selected'),
            disabled: el.is(':disabled')
        });
        return opt;
    },

    /**
     * Creates a new options item list.
     *
     * This method gets called by didRender setup callback each time
     * the template is rerendered and changes have been detected to
     * the options item list
     *
     * @private
     * @method createOptionsList
     * @params data {object} jquery child array from the `hidden-template` container element
     * @returns {void}
     */
    createOptionsList(data) {
        // create data array for option data
        const dataArray = A([]);

        // loop through option data
        forEachOption(data, el => {
            // dont add option that are set to hidden
            if (this.shouldCreateOption(el)) {
                // create the option item
                const opt = this.createOption(el);

                if (get(opt, 'selected')) {
                    this.setSelected(opt);
                }

                // add option to list
                dataArray.pushObject(opt);
            }
        });

        // set new list items
        set(this, 'listItem', dataArray);
    },

    /**
     * Checks the array for any addition changes to items in the array.
     *
     * returns true if a new item is found or any properties on a
     * option item have changed
     *
     * @private
     * @method hasChanges
     * @param data {object} jquery child array from the `hidden-template` container element
     * @returns {boolean}
     */
    hasChanges(data) {
        let hasChanges = false;
        forEachOption(data, el => {
            // if no changes detected yet
            if (!hasChanges) {
                // the list is empty so all items have changed
                if (isEmpty(get(this, 'listItem'))) {
                    hasChanges = true;
                } else {
                    // create option obj from element
                    const option = this.createOption(el);

                    // dont include default labels in the changes
                    if (get(option, 'class') !== 'default-label') {
                        // get old option item
                        const oldOpt = get(this, 'listItem').findBy('value', get(option, 'value'));

                        // item not found in list items
                        if (isNone(oldOpt)) {
                            hasChanges = true;
                        } else {
                            // check all keys in old opt for changes
                            Object.keys(option).forEach((key) => {
                                // item property does not mathc old property
                                if (get(oldOpt, key) !== get(option, key)) {
                                    hasChanges = true;
                                }
                            });
                        }
                    }
                }
            }
        });
        return hasChanges;
    },

    /**
     * Sets the selected option and unsets all other options
     * in the listItem option array
     *
     * @public
     * @method setSelected
     * @params option {object} selcted option object
     * @return {void}
     */
    setSelected(option) {
        if (!get(this, 'disableChange')) {
            set(this, 'selected', option);
        }
    },

    onOutsideClick() { this.send('closeMenu'); },
    onEscape() {
        this.send('closeMenu');
        return false;
    },

    actions: {
        /**
         * Toggles the menu open or close
         *
         * Action method
         *
         * @private
         * @method toggleMenu
         * @returns {void}
         */
        toggleMenu() {
            if (!get(this, 'isMenuOpen')) {
                this.bindEscape();
                this.bindClick();
                set(this, 'isMenuOpen', true);
            } else {
                this.send('closeMenu');
            }
        },

        /**
         * Closes the menu
         *
         * Action method
         *
         * @private
         * @method closeMenu
         * @returns {void}
         */
        closeMenu() {
            this.unbindEscape();
            this.unbindClick();
            set(this, 'isMenuOpen', false);
        },

        /**
         * Sends the onSelect event action
         *
         * Action Method
         *
         * @private
         * @method selectAction
         * @params option {object} An option html object
         * @returns {void}
         */
        selectAction(option) {
            // do nothing if disabled is set to
            // true for the item selected
            if (!get(option, 'disabled')) {
                // close menu unless keep open
                // is set to true
                if (!get(this, 'keepOpen')) {
                    this.send('closeMenu');
                }

                // send the value of the option to the onSelect callback
                this.sendAction('onSelect', get(option, 'value'));
            }
        },

        stopPropagation() {
            return false;
        }
    }
});