busy-web/components

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

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * @module Components
 *
 */
import { assert } from '@ember/debug';
import { loc } from '@ember/string';
import { isNone, isEmpty } from '@ember/utils';
import { computed, get, set } 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';

/**
 * `Component/BCSelect`
 * Component select menu for displaying a list of items to a user.
 *
 * @class BCSelect
 * @namespace Components
 *
 * @property model {array} Array of key-value pair objects
 * @property itemLabel {string} The key of the key-value pair to display in the option list.
 * @property defaultLabel {string} The start label to display in the select menu. `Default: Select`
 * @property defaultFirstOption {boolean} True if the first option in the model array should be displayed. `Default: false`
 * @property menuTitle {string} This should be set if the select menu doesn't change titles when selected. This option overrides defaultLabel and defaultFirstOption.
 * @property onSelect {string} The function name to call when an item is selected. onSelect will pass the selected item to the listener.
 * @property targetObject {object} The View where the onSelect function can be called. `Default: controller`
 */
export default Component.extend(CloseOnEscape, BindOutsideClick, {
    layout,

    classNames: ['bc-select'],
    classNameBindings: ['isOpen:active', 'isTop:top', 'small'],

    __closeActionName: 'closeMenu',

    /**
     * isOpen tracks the menu open state
     *
     * @private
     * @property isOpen
     * @type boolean
     */
    isOpen: false,
    isTop: false,
    small: false,
    openTop: false,
    highlightSlected: true,

    model: null,

    /**
     * internal use to track the current selected item from the list
     *
     * @private
     * @property selectedItem
     * @type object
     */
    selectedItem: computed('model.@each._selected', 'model.[]', function() {
        let selected = null;
        if (!isNone(get(this, 'model'))) {
            selected = this.getSelected();
        }
        return selected;
    }),

    getSelected() {
        const items = get(this, 'model');
        let selected = null;
        if (typeof items === 'object' && typeof items.forEach === 'function') {
            items.forEach(item => {
                if (get(item, '_selected')) {
                    selected = item;
                }
            });
        } else {
            Object.keys(items).forEach(key => {
                if (get(items[key], '_selected')) {
                    selected = items[key];
                }
            });
        }
        return selected;
    },

    itemLabel: '',
    defaultLabel: loc('Select'),
    defaultFirstOption: false,

    menuTitle: computed('selectedItem', function() {
        let label = get(this, 'defaultLabel');
        let selectedItem = get(this, 'selectedItem');

        assert('"itemLabel" must be set to a property of the model', !isEmpty(get(this, 'itemLabel')));

        if (!isNone(selectedItem)) {
            label = get(selectedItem, get(this, 'itemLabel'));
        } else if (get(this, 'defaultFirstOption')) {
            selectedItem = get(this, 'model').objectAt(0);
            label = get(selectedItem, get(this, 'itemLabel'));
        }
        return label;
    }),

    checkPosition() {
        const elem = this.$().offsetParent();

        let menuHeightTop = elem.height() - (elem.height() - (this.$().offset().top - elem.offset().top)) - 20; // height of the container minus the the bottom space from the top of the button.
        let menuHeightBot = elem.height() - ((this.$().offset().top + this.$().height()) - elem.offset().top) - 20; // height of the container minus the bottom of the select button.
        const maxHeight = parseInt(window.getComputedStyle(this.$('.select-container').get(0))['max-height'], 10);

        if (menuHeightTop > 200) {
            if (menuHeightBot < maxHeight) {
                if (menuHeightTop > menuHeightBot) {
                    if (menuHeightTop < maxHeight) {
                        menuHeightTop = menuHeightTop > 150 ? menuHeightTop : 150;
                        this.$('.select-container').css('max-height', menuHeightTop);
                    }
                    return true;
                } else {
                    menuHeightBot = menuHeightBot > 150 ? menuHeightBot : 150;
                    this.$('.select-container').css('max-height', menuHeightBot);
                }
            }
        }
        return false;
    },

    unselectAll() {
        const items = get(this, 'model');
        if (typeof items === 'object' && typeof items.forEach === 'function') {
            items.forEach(item => {
                set(item, '_selected', false);
            });
        } else {
            Object.keys(items).forEach(key => {
                set(items[key], '_selected', false);
            });
        }
    },

    /**
     * handles an item click action
     * and sends the action to any listeners
     *
     * @private
     * @method itemClicked
     * @param item {object} The list item that was clicked
     * @returns {void}
     */
    itemClicked(item) {
        this.unselectAll();

        set(item, '_selected', true);
        this.close();

        this.sendAction('onSelect', item);
    },

    onOutsideClick() { this.close(); },
    onEscape() {
        this.close();
        return false;
    },

    open() {
        if (!get(this, 'isDestroyed')) {
            this.bindEscape();
            this.bindClick();

            set(this, 'isOpen', true);
            if (get(this, 'openTop')) {
                set(this, 'isTop', true);
            } else {
                if (this.checkPosition()) {
                    set(this, 'isTop', true);
                } else {
                    set(this, 'isTop', false);
                }
            }
        }
    },

    close() {
        if (!get(this, 'isDestroyed')) {
            this.unbindEscape();
            this.unbindClick();
            set(this, 'isOpen', false);
        }
    },

    actions: {
        openMenu() {
            if (!get(this, 'isOpen')) {
                this.open();
            } else {
                this.close();
            }
        },

        closeMenu() {
            this.close();
        },

        clickItemAction(item) {
            if (!get(item, '_unselectable')) {
                this.itemClicked(item);
            }
            return false;
        },

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