Skip to content
Snippets Groups Projects
Sliders.ts 18 KiB
Newer Older
/**
 * Created by gavorhes on 12/10/2015.
 */


import provide from '../util/provide';
let nm = provide('collections');
const $ = require('jquery');

export interface TipSliderConfig {
    label: string;
    yearOptions: Array<{column: string, label: string}>
}

export interface TipPresetConfig {
    label: string;
    presets: Array<{column: string, value: number}>
}

export interface ChangeResponse{
Glenn Vorhes's avatar
Glenn Vorhes committed
    paramWeights: Object;
    region: string;
    mmVersion: string;
}

export interface ChangeCallback{
    (chg: ChangeResponse): any
}


class TipPresets implements TipPresetConfig {
    label: string;
    presets: Array<{column: string, value: number}>;
    domId: string;

    constructor(conf: TipPresetConfig) {
        this.label = conf.label;
        this.presets = conf.presets;
        this.domId = this.label.replace(/ /g, '').toLowerCase();

        let theSum = 0;

        for (let pr of this.presets){
            theSum += pr.value;
        }

        if (theSum != 100){
            throw 'preset sum does note equal 100';
        }
    }
}


class _Slider {
    _min: number;
    _max: number;
    _locked: boolean;
    atMin: boolean;
    atMax: boolean;
    _weightDefault: number;
    _weight: number;
    html: string;
    domId: string;
    name: string;

    labelLow: JQuery;
    labelHigh: JQuery;
    labelVal: JQuery;
    slider: JQuery;
    selectionBox: JQuery;
    chk: JQuery;

    selectedParam: any;
    selectedParamDefault: any;
    _dropdownSelection: string;

    /**
     * Slider constructor
     * @param sliderConfig - the configuration

     */
    constructor(sliderConfig: TipSliderConfig) {
        this._dropdownSelection = null;
        this._weight = null;
        this.name = sliderConfig.label;
        this.domId = this.name.toLowerCase().replace(/ /g, '-');

        this._locked = false;

        this._min = 0.0;
        this._max = 100;

        this.labelLow = null;
        this.labelHigh = null;
        this.labelVal = null;
        this.slider = null;
        this.chk = null;

        this.atMin = false;
        this.atMax = false;


        let sel = `<select class="${sliderConfig.yearOptions.length == 1 ? 'hidden-select' : 'show-select'}" id="${this.domId}_chg">`;

        for (let i = 0; i < sliderConfig.yearOptions.length; i++) {
            let itm = sliderConfig.yearOptions[i];
            sel += `<option value="${itm.column}">${itm.label}</option>`;
        }
        sel += '</select>';

        this.selectedParamDefault = this.selectedParam;

        this.html = '<div class="slider-div">' +
            `<label for="${this.domId}_chk" class="slider-label">${this.name}</label>` +
            sel + `<br>` +
            `<input id="${this.domId}_chk" type="checkbox" title="Lock/Unlock Slider">` +
            `<label id="${this.domId}_low" class="low-high"></label>` +
            `<input id="${this.domId}" type="range" value="50" min="0" max="100" step="0.1">` +
            `<label id="${this.domId}_high" class="low-high"></label>` +
            `<label id="${this.domId}_lbl" for="${this.domId}" class="percent-label"></label></div>`;
    }

    /**
     * add html to dom
     * @param {jQuery} $container - container element
     */
    addToDom($container) {
        $container.append(this.html);
        this.labelLow = $(`#${this.domId}_low`);
        this.labelHigh = $(`#${this.domId}_high`);
        this.labelVal = $(`#${this.domId}_lbl`);
        this.slider = $(`#${this.domId}`);
        this.selectionBox = $(`#${this.domId}_chg`);
        this.chk = $(`#${this.domId}_chk`);
    }

    /**
     * increment the slider
     * @param {number} delta change delta
     * @returns {number} the remainder not able to be allocated to this slider
     */
    increment(delta) {
        let remainder = 0;
        delta = Number(delta.toFixed(1));

        this._weight += delta;
        if (this._weight < this._min) {
            remainder = this._min - this._weight;
            this._weight = this._min;
            this.atMin = true;
        } else if (this._weight > this._max) {
            remainder = this._max - this._weight;
            this._weight = this._max;
            this.atMax = true;
        } else {
            this.atMin = false;
            this.atMax = false;
        }

        this.slider.val(this._weight.toFixed(1));
        this.labelVal.html(this._weight.toFixed(1) + '%');

        return remainder;
    }

    /**
     * set the value and drop down
     * @param {number} newVal the new value
     * @param {string} selectedParam the selected parameter
     */
    setValAndDropDown(newVal: number, selectedParam: string) {
        this.min = 0;
        this.max = 100;
        this.weight = newVal;
        this.slider.val(newVal.toFixed(1));
        this.selectionBox.val(selectedParam);
        this.selectedParam = selectedParam;
        this.locked = true;
    }

    /**
     *
     * @returns {boolean} if locked
     */
    get locked() {
        return this._locked;
    }

    /**
     *
     * @param {boolean} val if locked
     */
    set locked(val) {
        this._locked = val;
        this.slider.prop('disabled', this._locked);
        this.selectionBox.prop('disabled', this._locked);
        this.chk.prop('checked', !this._locked);
    }

    /**
     *
     * @returns {number} the minimum
     */
    get min() {
        return this._min;
    }

    /**
     *
     * @param {number} newVal new minimum
     */
    set min(newVal) {
        this._min = Number(newVal.toFixed(1));
        if (this._min < 0) {
            this._min = 0;
        }
        this.labelLow.html(this._min.toFixed(1));
        this.slider.attr('min', this._min.toFixed(1));
        this.atMin = this._weight == this._min;
    }

    /**
     *
     * @returns {number} the maximum
     */
    get max() {
        return this._max;
    }

    /**
     *
     * @param {number} newVal the maximum
     */
    set max(newVal) {
        this._max = Number(newVal.toFixed(1));
        if (this._max > 100) {
            this._max = 100.0;
        }
        this.labelHigh.html(this._max.toFixed(1));
        this.slider.attr('max', this._max.toFixed(1));
        this.atMax = this._weight == this._max;
    }

    /**
     *
     * @returns {number} the weight
     */
    get weight() {
        return this._weight;
    }

    /**
     *
     * @param {number} newVal the weight
     */
    set weight(newVal) {
        this._weight = Number(newVal.toFixed(1));
        this.labelVal.html(this._weight.toFixed(1) + '%');
        if (this._weight <= this._min) {
            this.atMin = true;
            this.atMax = false;
        } else if (this._weight >= this._max) {
            this.atMin = false;
            this.atMax = true;
        } else {
            this.atMin = false;
            this.atMax = false;
        }
    }
}

nm._Slider = _Slider;


/**
 * class to keep track of the sliders
 */
export class TipSliders {
    $container: JQuery;
    reservedPercent: number;
    lockedCount: number;
    notLockedCount: number;
    private _sliderList: Array<_Slider>;
    private _sliderLookup: {[s: string]: _Slider};
    private resetting: boolean;
    private _changedCallback: ChangeCallback;

    private _lockedList: Array<_Slider>;
    private _inRangeList: Array<_Slider>;
    private _atMinList: Array<_Slider>;
    private _atMaxList: Array<_Slider>;

Glenn Vorhes's avatar
Glenn Vorhes committed
    private _presetArray: Array<TipPresets>;
    private _presetLookup: {[s: string]: TipPresets};
Glenn Vorhes's avatar
Glenn Vorhes committed
    private _$presetSelector: JQuery;
    private _$regionSelector: JQuery;
    private _$versionSelector: JQuery;

    /**
     *
     * @param sliderConfigs
     * @param presetConfig
     * @param divId
     * @param presetSelector
     * @param regionSelector
     * @param versionSelector
     */
    constructor(sliderConfigs: Array<TipSliderConfig>, presetConfig: Array<TipPresetConfig>,
                divId: string, presetSelector: JQuery, regionSelector: JQuery, versionSelector: JQuery,
                chgCallback?: ChangeCallback) {
        this.resetting = false;
        this.reservedPercent = 0.0;
        this.$container = $('#' + divId);
        this.$container.addClass('slider-container');

        this._changedCallback = typeof chgCallback == 'function' ? chgCallback : () => {};
        this._$presetSelector = presetSelector;
        this._$regionSelector = regionSelector;
        this._$versionSelector = versionSelector;

        this._sliderList = [];
        this._sliderLookup = {};

        for (let i = 0; i < sliderConfigs.length; i++) {
            let sld = new _Slider(sliderConfigs[i]);
            this._sliderList.push(sld);
            this._sliderLookup[sld.domId] = sld;
            sld.addToDom(this.$container);
        }

        this._presetArray = [];
        this._presetLookup = {};

        for (let i = 0; i < presetConfig.length; i++) {
            let preset = new TipPresets(presetConfig[i]);

            let idx = (i + 1).toFixed();

            this._presetLookup[idx] = preset;
            this._presetArray.push(preset);
            this._$presetSelector.append(`<option value="${idx}">${preset.label}</option>`);
        }


        this._lockedList = [];
        this._inRangeList = [];
        this._atMinList = [];
        this._atMaxList = [];

        this.lockedCount = 10;
        this.notLockedCount = 0;

        this._splitSliderArray();


        this._$presetSelector.change(() => {
            this.setPresetValues();
            this._runChangedCallback();
        });

        this._$regionSelector.change(() => {
            this._runChangedCallback();
        });

        this._$versionSelector.change(() => {
            this._runChangedCallback();
        });

        this._$presetSelector.trigger('change');

        this._addEventListeners();
    }

    _runChangedCallback(){
        this._changedCallback(this.paramWeightsRegionVersion);
    }

    get changedCallback(): ChangeCallback{
        return this._changedCallback
    }

    set changedCallback(chg: ChangeCallback){
        this._changedCallback = chg;
        this._runChangedCallback();
        let idx = this._$presetSelector.val() || '1';

        let thePreset = this._presetLookup[idx];

        for (let i = 0; i < thePreset.presets.length; i++) {
            let presetValues = thePreset.presets[i];
            let theSlider = this._sliderList[i];

            theSlider.locked = true;
            theSlider.setValAndDropDown(presetValues.value, presetValues.column);
        }
    }

    /**
     * split array into subarrays holding the sliders
     * @private
     */
    _splitSliderArray() {
        this._lockedList = [];
        this._inRangeList = [];
        this._atMinList = [];
        this._atMaxList = [];

        for (let i = 0; i < this._sliderList.length; i++) {
            let sld = this._sliderList[i];

            if (sld.locked) {
                this._lockedList.push(sld);
            } else if (sld.atMin) {
                this._atMinList.push(sld);
            } else if (sld.atMax) {
                this._atMaxList.push(sld);
            } else {
                this._inRangeList.push(sld);
            }
        }
        this.lockedCount = this._lockedList.length;
        this.notLockedCount = this._sliderList.length - this.lockedCount;
    }

    /**
     * handle remainder, recursive to take care of min max overshoots
     * @param {number} remain the remainder
     * @param {string} skipDomId - this dom id
     * @private
     */
    _handleRemainder(remain, skipDomId) {

        remain = Number(remain.toFixed(1));
        if (remain == 0) {
            return;
        }

        this._splitSliderArray();

        let canChangeArray = [];
        for (let i = 0; i < this._inRangeList.length; i++) {
            let sld = this._inRangeList[i];
            if (sld.domId == skipDomId) {
                continue;
            }
            canChangeArray.push(sld);
        }

        if (remain > 0) {
            for (let i = 0; i < this._atMaxList.length; i++) {
                let sld = this._atMaxList[i];
                if (sld.domId == skipDomId) {
                    continue;
                }
                canChangeArray.push(sld);
            }
        } else {
            for (let i = 0; i < this._atMinList.length; i++) {
                let sld = this._atMinList[i];
                if (sld.domId == skipDomId) {
                    continue;
                }
                canChangeArray.push(sld);
            }
        }

        if (canChangeArray.length == 0) {
            return;
        }

        let inc = -1 * Number((remain / canChangeArray.length).toFixed(1));

        let newRemainder = 0;
        for (let i = 0; i < canChangeArray.length; i++) {
            newRemainder += canChangeArray[i].increment(inc);
        }

        this._handleRemainder(newRemainder, skipDomId);
    }

    /**
     *
     * @param {object} keyValList key and value list
     */
    setValues(keyValList) {
        this.resetting = true;
        for (let k in keyValList) {
            if (keyValList.hasOwnProperty(k)) {
                this._sliderLookup[k].setValAndDropDown(keyValList[k][0], keyValList[k][1]);
            }
        }
        this.resetting = false;
    }

    /**
     * get the weight sum
     * @returns {number} the weight sum
     */
    getSum() {
        let total = 0;
        for (let i = 0; i < this._sliderList.length; i++) {
            let sld = this._sliderList[i];
            total += Number(sld.weight.toFixed(1));
        }

        return total;
    }

    /**
     * get the parameter weights
     * @returns {object} lookup with parameter weights
     */
    getParams() {
        let paramWeights = {};
        for (let i = 0; i < this._sliderList.length; i++) {
            let sld = this._sliderList[i];
            paramWeights[sld.selectedParam] = Number(sld.weight.toFixed(1));
        }

        return paramWeights;
    }

    _addEventListeners() {
        let ___this = this;


        //change function
        this.$container.find('input[type="range"]').change(function () {
                if (___this.resetting) {
                    return;
                }

                let $this = $(this);
                let domId = this['id'];
                let sldr = ___this._sliderLookup[domId];

                let newValue = parseFloat($this.val());

                let oldValue = sldr.weight;
                let diff = newValue - oldValue;
                diff = Number(diff.toFixed(1));

                sldr.weight = Number(newValue.toFixed(1));

                ___this._handleRemainder(diff, domId);

                //cleanup, make sure the sum is still 100
                let sum = Number(___this.getSum().toFixed(1));

                if (sum > 100) {
                    loop1:
                        while (true) {
                            for (let i = 0; i < ___this._sliderList.length; i++) {
                                let sld = ___this._sliderList[i];
                                if (sld.domId == domId || sld.locked || sld.atMin) {
                                    continue;
                                }
                                sld.increment(-0.1);
                                sum -= 0.1;
                                if (sum.toFixed(1) == '100.0') {
                                    break loop1;
                                }
                            }
                        }
                } else if (sum < 100) {
                    loop1:
                        while (true) {
                            for (let i = 0; i < ___this._sliderList.length; i++) {
                                let sld = ___this._sliderList[i];
                                if (sld.domId == domId || sld.locked || sld.atMax) {
                                    continue;
                                }
                                sld.increment(0.1);
                                sum += 0.1;
                                if (sum.toFixed(1) == '100.0') {
                                    break loop1;
                                }
                            }
                        }
                }

                ___this._$presetSelector.val('0');
                ___this._runChangedCallback();
            }
        );

        //update the selected parameter when the selection is changed
        $('.show-select').change(function () {
            if (___this.resetting) {
                return;
            }
            ___this._sliderLookup[this['id'].replace('_chg', '')].selectedParam = $(this).val();

            ___this._$presetSelector.val('0');

            ___this._runChangedCallback();

        });

        //lock the slider and update the reserved percent
        this.$container.find('input[type="checkbox"]').change(function () {
            let domEl = this;

            ___this._sliderLookup[domEl.id.replace('_chk', '')].locked = !domEl.checked;
            ___this.reservedPercent = 0.0;
            ___this.notLockedCount = 0;

            let notLockedSliders = [];

            for (let i = 0; i < ___this._sliderList.length; i++) {
                let sld = ___this._sliderList[i];
                if (sld.locked) {
                    ___this.reservedPercent += sld.weight;
                    continue;
                }
                notLockedSliders.push(sld);
                ___this.notLockedCount++;
            }

            for (let i = 0; i < ___this._sliderList.length; i++) {
                let sld = ___this._sliderList[i];
                if (sld.locked) {
                    continue;
                }
                sld.max = 100 - ___this.reservedPercent;
            }

            if (notLockedSliders.length == 1) {
                notLockedSliders[0].min = notLockedSliders[0].weight;
            } else {
                for (let i = 0; i < notLockedSliders.length; i++) {
                    notLockedSliders[i].min = 0;
                }
            }
        });

    }

    get paramWeightsRegionVersion(): ChangeResponse{
        return {paramWeights: this.getParams(),
Glenn Vorhes's avatar
Glenn Vorhes committed
            region: this._$regionSelector.val() as string, mmVersion: this._$versionSelector.val() as string}
    }
}

nm.Sliders = TipSliders;
export default TipSliders;