ruhley/angular-color-picker

View on GitHub
src/scripts/controller.js

Summary

Maintainability
F
5 days
Test Coverage
import tinycolor from 'tinycolor2';

export default class AngularColorPickerController {
    constructor(_$scope, _$element, _$document, _$timeout, _ColorPickerOptions) {
        // set angular injected variables
        this.$scope = _$scope;
        this.$element = _$element;
        this.$document = _$document;
        this.$timeout = _$timeout;
        this.ColorPickerOptions = _ColorPickerOptions;

        // make the init function available from the $scope (for the directive link function)
        this.$scope.init = this.init.bind(this);

        // set default values
        this.ngModelOptions = {};
        this.hue = 0;
        this.saturation = undefined;
        this.lightness = undefined;
        this.opacity = undefined;

        this.basicEventTypes = ['hue', 'saturation', 'lightness', 'opacity'];
        this.fullEventTypes = ['color', 'hue', 'saturation', 'lightness', 'opacity'];
    }

    //---------------------------
    // init functions
    //---------------------------

    init() {

        // ng model options
        if (this.$scope.control[0].$options && this.$scope.control[0].$options.$$options) {
            this.ngModelOptions = this.$scope.control[0].$options.$$options;
        }

        // browser variables
        this.chrome = Boolean(window.chrome);
        let _android_version = window.navigator.userAgent.match(/Android\s([0-9\.]*)/i);
        this.android_version = _android_version && _android_version.length > 1 ? parseFloat(_android_version[1]) : NaN;

        // needed variables
        this.updateModel = true;

        // watchers
        this.initWatchers();

        // set default config settings
        this.initConfig();

        // mouse events
        this.initMouseEvents();
    }

    initConfig() {
        if (!this.options) {
            this.options = {};
        }

        this.mergeOptions(this.options, this.ColorPickerOptions);

        this.is_open = this.options.inline;

        if (this.options.inline) {
            this.options.close.show = false;
        }

        this.pickerDimensions = {
            width: 150,
            height: 150
        };

        this.sliderDimensions = {
            width: this.options.horizontal ? this.pickerDimensions.width : 20,
            height: this.options.horizontal ? 20 : this.pickerDimensions.height,
        };
    }

    mergeOptions(options, defaultOptions) {
        for (var attr in defaultOptions) {
            if (defaultOptions.hasOwnProperty(attr)) {
                if (!options || !options.hasOwnProperty(attr)) {
                    options[attr] = defaultOptions[attr];
                } else if (typeof defaultOptions[attr] === 'object') {
                    this.mergeOptions(options[attr], defaultOptions[attr]);
                }
            }
        }
    }

    //---------------------------
    // watcher functions
    //---------------------------

    initWatchers() {

        // ngModel

        this.$scope.$watch('AngularColorPickerController.internalNgModel', this.watchInternalNgModel.bind(this));
        this.$scope.$watch('AngularColorPickerController.ngModel', this.watchNgModel.bind(this));

        // options

        this.$scope.$watch('AngularColorPickerController.options.swatchPos', (newValue) => {
            if (newValue !== undefined) {
                this.initConfig();

                this.$timeout(() => {
                    this.updateSwatchBackground();
                });
            }
        });

        this.$scope.$watchGroup(
            [
                'AngularColorPickerController.options.format',
                'AngularColorPickerController.options.alpha',
                'AngularColorPickerController.options.case',
                'AngularColorPickerController.options.round',
                'AngularColorPickerController.options.restrictToFormat',
                'AngularColorPickerController.options.preserveInputFormat',
                'AngularColorPickerController.options.allowEmpty',
                'AngularColorPickerController.options.horizontal',
                'AngularColorPickerController.options.dynamicHue',
                'AngularColorPickerController.options.dynamicSaturation',
                'AngularColorPickerController.options.dynamicLightness',
                'AngularColorPickerController.options.dynamicAlpha'
            ],
            (newValue) => {
                if (newValue !== undefined) {
                    this.initConfig();
                    this.update();
                }
            }
        );

        this.$scope.$watchGroup(
            [
                'AngularColorPickerController.options.disabled',
                'AngularColorPickerController.options.swatchBootstrap',
                'AngularColorPickerController.options.swatchOnly',
                'AngularColorPickerController.options.swatch',
                'AngularColorPickerController.options.pos',
                'AngularColorPickerController.options.inline',
                'AngularColorPickerController.options.placeholder'
            ],
            (newValue) => {
                if (newValue !== undefined) {
                    this.initConfig();
                }
            }
        );

        // api

        this.$scope.$watch('AngularColorPickerController.api', this.setupApi.bind(this));

        // internal

        this.$scope.$watch('AngularColorPickerController.swatchColor', this.updateSwatchBackground.bind(this));

        this.$scope.$watch('AngularColorPickerController.hue', () => {
            this.valueUpdate('hue');
        });

        this.$scope.$watch('AngularColorPickerController.saturation', () => {
            this.valueUpdate('saturation');
        });

        this.$scope.$watch('AngularColorPickerController.lightness', () => {
            this.valueUpdate('lightness');
        });

        this.$scope.$watch('AngularColorPickerController.opacity', () => {
            this.valueUpdate('opacity');
        });
    }

    watchInternalNgModel(newValue, oldValue) {
        // the mouse is still moving so don't do anything yet
        if (this.colorMouse) {
            return;
        }

        // calculate and set color values
        this.watchNgModelSet(newValue);
    }

    /** Triggered on change to internal or external ngModel value */
    watchNgModel(newValue, oldValue) {
        // set initial value if not already set
        if (newValue !== undefined && !this.hasOwnProperty('initialNgModel')) {
            this.initialNgModel = newValue;
        }

        // sets the field to pristine or dirty for angular
        this.checkDirty(newValue);

        // update the internal model from external model
        this.internalNgModel = this.ngModelOptions.getterSetter ? this.ngModel() : this.ngModel;

        // the mouse is still moving so don't do anything yet
        if (this.colorMouse) {
            return;
        }

        // calculate and set color values
        this.watchNgModelSet(newValue);
    }

    /** Helper for watchNgModel to set internal values and validity */
    watchNgModelSet(newValue) {
        if (newValue !== undefined && newValue !== null) {
            var color = tinycolor(newValue);
            var isValid = this.isColorValid(color);

            if (isValid) {
                this.setColorValue(color);

                this.updateModel = false;

                this.$timeout(() => {
                    this.updateModel = true;
                });
            }

            this.$scope.control[0].$setValidity('color', isValid);
        } else {
            if (newValue === null || newValue === '') {
                this.hue = 0;
                this.saturation = undefined;
                this.lightness = undefined;
                this.opacity = undefined;
            }

            this.swatchColor = '';
        }
    }

    //---------------------------
    // mouse/touch event functions
    //---------------------------

    initMouseEvents() {
        const eventHandlers = {
            mouseDown: this.onMouseDown.bind(this),
            mouseUp: this.onMouseUp.bind(this),
            mouseMove: this.onMouseMove.bind(this),
            keyUp: this.onKeyUp.bind(this)
        };

        // setup mouse events
        this.$document.on('mousedown', eventHandlers.mouseDown);
        this.$document.on('mouseup', eventHandlers.mouseUp);
        this.$document.on('mousemove', eventHandlers.mouseMove);

        // setup touch events
        this.$document.on('touchstart', eventHandlers.mouseDown);
        this.$document.on('touchend', eventHandlers.mouseUp);
        this.$document.on('touchmove', eventHandlers.mouseMove);

        // setup key events
        this.$document.on('keyup', eventHandlers.keyUp);

        // grid click
        this.find('.color-picker-grid').on('click', (event) => {
            this.onClick('color', event);
        });
        this.find('.color-picker-grid').on('touchend', (event) => {
            this.onClick('color', event);
        });

        // hue click
        this.find('.color-picker-hue').on('click', (event) => {
            this.onClick('hue', event);
        });
        this.find('.color-picker-hue').on('touchend', (event) => {
            this.onClick('hue', event);
        });

        // saturation click
        this.find('.color-picker-saturation').on('click', (event) => {
            this.onClick('saturation', event);
        });
        this.find('.color-picker-saturation').on('touchend', (event) => {
            this.onClick('saturation', event);
        });

        // lightness click
        this.find('.color-picker-lightness').on('click', (event) => {
            this.onClick('lightness', event);
        });
        this.find('.color-picker-lightness').on('touchend', (event) => {
            this.onClick('lightness', event);
        });

        // opacity click
        this.find('.color-picker-opacity').on('click', (event) => {
            this.onClick('opacity', event);
        });
        this.find('.color-picker-opacity').on('touchend', (event) => {
            this.onClick('opacity', event);
        });

        this.find('.color-picker-input').on('focusin', this.onFocus.bind(this));
        this.find('.color-picker-input').on('focusout', this.onBlur.bind(this));

        //---------------------------
        // destroy
        //---------------------------

        this.$scope.$on('$destroy', () => {
            // remove mouse events
            this.$document.off('mousedown', eventHandlers.mouseDown);
            this.$document.off('mouseup', eventHandlers.mouseUp);
            this.$document.off('mousemove', eventHandlers.mouseMove);

            // remove touch events
            this.$document.off('touchstart', eventHandlers.mouseDown);
            this.$document.off('touchend', eventHandlers.mouseUp);
            this.$document.off('touchmove', eventHandlers.mouseMove);

            // remove key events
            this.$document.off('keyup', eventHandlers.keyUp);

            this.eventApiDispatch('onDestroy');
        });
    }

    onMouseDown(event) {
        this.has_moused_moved = false;

        // if disabled or not an element in this picker then do nothing
        if (this.options.disabled || this.find(event.target).length === 0) {
            return true;
        }

        for (var i = 0; i < this.fullEventTypes.length; i++) {
            this.onMouseDownType(this.fullEventTypes[i], event);
        }
    }

    onMouseDownType(type, event) {
        if (
            type === 'color' &&
            (event.target.classList.contains('color-picker-grid-inner') ||
            event.target.classList.contains('color-picker-picker') ||
            event.target.parentNode.classList.contains('color-picker-picker'))
        ) {
            this.mouseEventToggle(type, false, event);
        } else if (event.target.classList.contains(`color-picker-${type}`) || event.target.parentNode.classList.contains(`color-picker-${type}`)) {
           this.mouseEventToggle(type, false, event);
       }
    }

    onMouseUp(event) {
        // no current mouse events and not an element in the picker
        if (!this.anyMouseEvents() && this.find(event.target).length === 0) {
            this.setupApi();
            if (this.options.hide.click) {
                this.api.close(event);
            }
            this.$scope.$apply();
        } else {
            for (var i = 0; i < this.fullEventTypes.length; i++) {
                this.onMouseUpType(this.fullEventTypes[i], event);
            }
        }
    }

    onMouseUpType(type, event) {
        if (this[`${type}Mouse`] && this.has_moused_moved) {
            this.mouseEventToggle(type, true, event);
            this.onChange(event);
        }
    }

    onMouseMove(event) {
        for (var i = 0; i < this.fullEventTypes.length; i++) {
            this.onMouseMoveType(this.fullEventTypes[i], event);
        }
    }

    onMouseMoveType(type, event) {
        if (this[`${type}Mouse`]) {
            this.has_moused_moved = true;
            this.valueChange(type, event);
            this.$scope.$apply();
        }
    }

    onKeyUp(event) {
        // escape key
        if (this.options.hide.escape && event.keyCode === 27) {
            this.api.close(event);
        }
    }

    onClick(type, event) {
        if (!this.options.disabled && !this.has_moused_moved) {
            this.valueChange(type, event);
            this.mouseEventToggle(type, true, event);
            this.onChange(event);
        }
    }

    onChange(event) {
        // don't fire if it hasn't actually changed
        if (this.internalNgModel !== this.onChangeValue) {
            this.onChangeValue = this.internalNgModel;

            this.eventApiDispatch('onChange', [event]);
        }
    }

    onBlur(event) {
        if (this.internalNgModel !== this.onChangeValue || this.internalNgModel !== this.ngModel) {
            this.updateModel = true;
            this.update();
        }

        this.$scope.control[0].$setTouched();

        this.eventApiDispatch('onBlur', [event]);

        // if clicking outside the color picker
        if (this.options.hide.blur && this.find(event.relatedTarget).length === 0) {
            this.api.close(event);
        }
    }

    onSwatchClick($event) {
        if (this.options.show.swatch && !this.options.disabled) {
            this.api.open($event);
        }
    }

    onFocus($event) {
        if (this.options.show.focus) {
            this.api.open($event);
        }
    }

    //---------------------------
    // api functions
    //---------------------------

    /** Sets up the external api */
    setupApi() {
        if (!this.api) {
            this.api = {};
        }

        this.api.open = (event) => {
            // if already open then don't run show again
            if (this.is_open) {
                return true;
            }

            this.is_open = true;
            this.hueMouse = false;
            this.opacityMouse = false;
            this.colorMouse = false;

            // force redraw
            this.$scope.$applyAsync();

            // force the sliders to re-caculate their position
            for (var i = 0; i < this.basicEventTypes.length; i++) {
                this.valueUpdate(this.basicEventTypes[i]);
            }

            this.eventApiDispatch('onOpen', [event]);
        };

        this.api.close = (event) => {
            // check that it is not already closed
            if (!this.options.inline && (this.is_open || this.$element[0].querySelector('.color-picker-panel').offsetParent !== null)) {
                this.is_open = false;
                this.$scope.$applyAsync();

                this.update();
                this.eventApiDispatch('onClose', [event]);
            }
        };

        this.api.clear = (event) => {
            this.setNgModel(null);

            this.eventApiDispatch('onClear', [event]);
        };

        this.api.reset = (event) => {
            if (this.internalNgModel !== this.initialNgModel) {
                this.setNgModel(this.initialNgModel);

                this.eventApiDispatch('onReset', [event]);
            }
        };

        this.api.getElement = () => {
            return this.$element;
        };

        this.api.getScope = () => {
            return this.$scope;
        };
    }

    //---------------------------
    // model functions
    //---------------------------

    /** Sets the internal and external ngModel values */
    setNgModel(value) {
        this.internalNgModel = value;

        if (this.ngModelOptions.getterSetter) {
            this.ngModel(value);
        } else {
            this.ngModel = value;
        }
    }

    update() {
        if (!this.areAllValuesSet()) {
            return false;
        }

        var color = tinycolor(this.getColorValue());

        this.swatchColor = color.toHslString();

        this.updateGridBackground(color);
        this.updateHueBackground(color);
        this.huePosUpdate();
        this.updateSaturationBackground(color);
        this.saturationPosUpdate();
        this.updateLightnessBackground(color);
        this.lightnessPosUpdate();
        this.updateOpacityBackground(color);
        this.opacityPosUpdate();

        var skipUpdate = this.options.preserveInputFormat && tinycolor(this.internalNgModel).toHsvString() === color.toHsvString();

        if (this.updateModel && !skipUpdate) {
            let formats = {
                rgb: 'toRgbString',
                hex: 'toHex',
                hex8: 'toHex8',
                hexstring: 'toHexString',
                hex8string: 'toHex8String',
                hsv: 'toHsvString',
                hsl: 'toHslString',
                raw: 'clone',
            };

            let value = color[formats[this.options.format.toLowerCase()]]();

            if (this.options.format.match(/hex/i)) {
                value = this.options.case === 'upper' ? value.toUpperCase() : value.toLowerCase();
            }

            this.setNgModel(value);
        }
    }

    //---------------------------
    // generic value functions
    //---------------------------

    mouseEventToggle(type, up, event) {
        this.stopEvent(event);
        this[`${type}Mouse`] = !up;
        this.$scope.$apply();
    }

    valueChange(type, event) {
        this.stopEvent(event);

        if (type === 'color') {
            return this.colorChange(event);
        }

        var el = this.find(`.color-picker-${type}`);
        var eventPos = this.getEventPos(event);
        var max = this.getMaxFromType(type);

        this[type] = this.calculateSliderPos(el, eventPos, max);

        if (this[type] > max) {
            this[type] = max;
        } else if (this[type] < 0) {
            this[type] = 0;
        }
    }

    valueUpdate(type) {
        if (this[type] !== undefined) {
            if (type === 'saturation') {
                this[`${type}Pos`] = this[type];
            } else {
                var max = this.getMaxFromType(type);
                this[`${type}Pos`] = (1 - (this[type] / max)) * 100;
            }

            if (this[`${type}Pos`] < 0) {
                this[`${type}Pos`] = 0;
            } else if (this[`${type}Pos`] > 100) {
                this[`${type}Pos`] = 100;
            }

            if (this.options.round) {
                this.getRoundPos();
                this.updateRoundPos();
            }

            this[`${type}PosUpdate`]();
            this.update();
        }
    }

    //---------------------------
    // hue functions
    //---------------------------

    huePosUpdate() {
        var el = angular.element(this.$element[0].querySelector('.color-picker-hue .color-picker-slider'));

        if (this.options.horizontal) {
            el.css({
                'left': (this.sliderDimensions.width * this.huePos / 100) + 'px',
                'top': 0
            });
        } else {
            el.css({
                'left': 0,
                'top': (this.sliderDimensions.height * this.huePos / 100) + 'px'
            });
        }
    }

    updateHueBackground(color) {
        var el = this.find('.color-picker-hue .color-picker-overlay');
        var direction = this.options.horizontal ? 'left' : 'top';

        var zero_sixths = this.getColorValue(this.options.dynamicHue);
        var one_sixths = this.getColorValue(this.options.dynamicHue);
        var two_sixths = this.getColorValue(this.options.dynamicHue);
        var three_sixths = this.getColorValue(this.options.dynamicHue);
        var four_sixths = this.getColorValue(this.options.dynamicHue);
        var five_sixths = this.getColorValue(this.options.dynamicHue);
        var six_sixths = this.getColorValue(this.options.dynamicHue);

        zero_sixths.h = 0;
        one_sixths.h = 60;
        two_sixths.h = 120;
        three_sixths.h = 180;
        four_sixths.h = 240;
        five_sixths.h = 300;
        six_sixths.h = 359;

        el.css({
            'background': 'linear-gradient(to ' + direction + ', ' +
                tinycolor(zero_sixths).toRgbString() + ' 0%, ' +
                tinycolor(one_sixths).toRgbString() + ' 17%, ' +
                tinycolor(two_sixths).toRgbString() + ' 33%, ' +
                tinycolor(three_sixths).toRgbString() + ' 50%, ' +
                tinycolor(four_sixths).toRgbString() + ' 67%, ' +
                tinycolor(five_sixths).toRgbString() + ' 83%, ' +
                tinycolor(six_sixths).toRgbString() + ' 100%)'
        });
    }

    //---------------------------
    // saturation functions
    //---------------------------

    saturationPosUpdate() {
        var el;

        if (!this.options.round) {
            el = angular.element(this.$element[0].querySelector('.color-picker-grid .color-picker-picker'));

            el.css({
                'left': (this.pickerDimensions.height * this.saturationPos / 100) + 'px'
            });
        }

        el = angular.element(this.$element[0].querySelector('.color-picker-saturation .color-picker-slider'));

        if (this.options.horizontal) {
            el.css({
                'left': (this.sliderDimensions.width * (100 - this.saturationPos) / 100) + 'px',
                'top': 0
            });
        } else {
            el.css({
                'left': 0,
                'top': (this.sliderDimensions.height * (100 - this.saturationPos) / 100) + 'px'
            });
        }
    }

    updateSaturationBackground(color) {
        var el = this.find('.color-picker-saturation .color-picker-overlay');
        var direction = this.options.horizontal ? 'right' : 'bottom';
        var high = this.getColorValue(this.options.dynamicSaturation);
        var low = this.getColorValue(this.options.dynamicSaturation);

        high.s = '100%';
        low.s = '0%';

        el.css({
            'background': 'linear-gradient(to ' + direction + ', ' + tinycolor(high).toRgbString() + ' 0%, ' + tinycolor(low).toRgbString() + ' 100%)'
        });
    }

    //---------------------------
    // lightness functions
    //---------------------------

    lightnessPosUpdate() {
        var el;

        if (!this.options.round) {
            el = angular.element(this.$element[0].querySelector('.color-picker-grid .color-picker-picker'));

            el.css({
                'top': (this.pickerDimensions.width * this.lightnessPos / 100) + 'px'
            });
        }

        el = angular.element(this.$element[0].querySelector('.color-picker-lightness .color-picker-slider'));

        if (this.options.horizontal) {
            el.css({
                'left': (this.sliderDimensions.width * this.lightnessPos / 100) + 'px',
                'top': 0
            });
        } else {
            el.css({
                'left': 0,
                'top': (this.sliderDimensions.height * this.lightnessPos / 100) + 'px'
            });
        }
    }

    updateLightnessBackground(color) {
        var el = this.find('.color-picker-lightness .color-picker-overlay');
        var direction = this.options.horizontal ? 'right' : 'bottom';
        var bright = this.getColorValue(this.options.dynamicLightness);
        var middle = this.getColorValue(this.options.dynamicLightness);
        var dark = this.getColorValue(this.options.dynamicLightness);

        if (this.options.round) {
            bright.l = 100;
            middle.l = 50;
            dark.l = 0;
        } else {
            bright.v = 100;
            middle.v = 50;
            dark.v = 0;
        }

        el.css({
            'background': 'linear-gradient(to ' + direction + ', ' + tinycolor(bright).toRgbString() + ' 0%, ' + tinycolor(middle).toRgbString() + ' 50%, ' + tinycolor(dark).toRgbString() + ' 100%)'
        });
    }

    //---------------------------
    // opacity functions
    //---------------------------

    opacityPosUpdate() {
        var el = angular.element(this.$element[0].querySelector('.color-picker-opacity .color-picker-slider'));

        if (this.options.horizontal) {
            el.css({
                'left': (this.sliderDimensions.width * this.opacityPos / 100) + 'px',
                'top': 0
            });
        } else {
            el.css({
                'left': 0,
                'top': (this.sliderDimensions.height * this.opacityPos / 100) + 'px'
            });
        }
    }

    updateOpacityBackground(color) {
        var el = this.find('.color-picker-opacity .color-picker-overlay');
        var direction = this.options.horizontal ? 'right' : 'bottom';
        var opaque = this.getColorValue(this.options.dynamicAlpha);
        var transparent = this.getColorValue(this.options.dynamicAlpha);

        opaque.a = 1;
        transparent.a = 0;

        el.css({
            'background': 'linear-gradient(to ' + direction + ', ' + tinycolor(opaque).toRgbString() + ' 0%, ' + tinycolor(transparent).toRgbString() + ' 100%)'
        });
    }

    //---------------------------
    // color functions
    //---------------------------

    colorChange(event) {
        this.stopEvent(event);

        var el = this.find('.color-picker-grid-inner');
        var eventPos = this.getEventPos(event);
        var offset = this.offset(el);

        if (this.options.round) {
            this.colorChangeRound(el, offset, eventPos);
        } else {
            this.colorChangeSquare(el, offset, eventPos);
        }
    }

    colorChangeRound(el, offset, eventPos) {
        var dx = ((eventPos.pageX - offset.left) * 2.0 / el.prop('offsetWidth')) - 1.0;
        var dy = -((eventPos.pageY - offset.top) * 2.0 / el.prop('offsetHeight')) + 1.0;

        var tmpHue = Math.atan2(dy, dx);
        var degHue = Math.round(tmpHue * 57.29577951308233); // rad to deg
        if (degHue < 0) {
            degHue += 360;
        }
        this.hue = degHue;

        var tmpSaturation = Math.sqrt(dx * dx + dy * dy);

        if (tmpSaturation > 1) {
            tmpSaturation = 1;
        } else if (tmpSaturation < 0) {
            tmpSaturation = 0;
        }

        this.saturation = tmpSaturation * 100;

        if (this.lightness === undefined) {
            this.lightness = 50;
        }
    }

    colorChangeSquare(el, offset, eventPos) {
        this.saturation = ((eventPos.pageX - offset.left) / el.prop('offsetWidth')) * 100;
        this.lightness = (1 - ((eventPos.pageY - offset.top) / el.prop('offsetHeight'))) * 100;

        if (this.saturation > 100) {
            this.saturation = 100;
        } else if (this.saturation < 0) {
            this.saturation = 0;
        }

        if (this.lightness > 100) {
            this.lightness = 100;
        } else if (this.lightness < 0) {
            this.lightness = 0;
        }
    }

    updateGridBackground(color) {
        var el = this.find('.color-picker-grid .color-picker-overlay');
        var background = this.getColorValue();

        if (this.options.round) {
            background.s = '0%';
        } else {
            background.s = '100%';
            background.v = '100%';
            background.a = 1;
        }

        el.css({
            'background-color': tinycolor(background).toRgbString(),
            'opacity': color.getAlpha()
        });

        this.find('.color-picker-grid .color-picker-grid-inner').css({
            'opacity': color.getAlpha()
        });
    }

    updateSwatchBackground() {
        var el = angular.element(this.$element[0].querySelector('.color-picker-swatch'));
        el.css({
            'background-color': this.swatchColor
        });
    }

    //---------------------------
    // helper functions
    //---------------------------

    isColorValid(color) {
        let isValid = color.isValid();

        if (isValid && this.options.restrictToFormat) {
            let format = this.options.format;
            isValid = color.getFormat() === this.getTinyColorFormat();
        }

        if (!isValid && this.options.allowEmpty) {
            let input = color.getOriginalInput();

            if (input === undefined || input === null || input === '') {
                isValid = true;
            }
        }

        return isValid;
    }

    getTinyColorFormat() {
        if (this.options.format === 'hexString') {
            return 'hex';
        } else if (this.options.format === 'hex8String') {
            return 'hex8';
        }

        return this.options.format;
    }

    areAllValuesSet() {
        if (this.hue === undefined || this.saturation === undefined || this.lightness === undefined) {
            return false;
        }

        return true;
    }

    getColorValue(dynamicValues = true, includeOpacity = true) {
        let value = {
            h: this.hue,
            s: dynamicValues ? `${this.saturation}%` : '100%',
            v: dynamicValues ? `${this.lightness}%`: '100%'
        };

        if (this.options.round) {
            value = {
                h: this.hue,
                s: dynamicValues ? `${this.saturation}%` : '100%',
                l: dynamicValues ? `${this.lightness}%` : '50%'
            };
        }

        if (includeOpacity) {
            value.a = dynamicValues ? this.opacity / 100 : 1;
        }

        return value;
    }

    /* eslint-disable complexity */
    setColorValue(color) {
        let noMouseEvents = !this.anyMouseEvents();
        let hsl = this.options.round ? color.toHsl() : color.toHsv();

        if (noMouseEvents || this.hueMouse) {
            this.hue = hsl.h;
        }

        if (noMouseEvents || this.saturationMouse) {
            this.saturation = hsl.s * 100;
        }

        if (noMouseEvents || this.lightnessMouse) {
            this.lightness = (this.options.round ? hsl.l : hsl.v) * 100;
        }

        if (this.options.alpha && (noMouseEvents || this.opacityMouse)) {
            this.opacity = hsl.a * 100;
        }
    }
    /* eslint-enable complexity */

    checkDirty(color) {
        // check dirty/pristine state
        if (this.hasOwnProperty('initialNgModel')) {
            if (color === this.initialNgModel) {
                if (typeof this.$scope.control[0].$setPristine === 'function') {
                    this.$scope.control[0].$setPristine();
                }
            } else {
                if (typeof this.$scope.control[0].$setDirty === 'function') {
                    this.$scope.control[0].$setDirty();
                }
            }
        }
    }

    stopEvent(event) {
        event.stopPropagation();
        event.preventDefault();
    }

    getRoundPos() {
        var angle = this.hue * 0.01745329251994; // deg to rad
        var px = Math.cos(angle) * this.saturation;
        var py = -Math.sin(angle) * this.saturation;

        this.xPos = (px + 100.0) * 0.5;
        this.yPos = (py + 100.0) * 0.5;

        // because it are using percentages this can be half of 100%
        var center = 50;
        // distance of pointer from the center of the circle
        var distance = Math.pow(center - this.xPos, 2) + Math.pow(center - this.yPos, 2);
        // distance of edge of circle from the center of the circle
        var radius = Math.pow(center, 2);

        // if not inside the circle
        if (distance > radius) {
            var rads = Math.atan2(this.yPos - center, this.xPos - center);
            this.xPos = Math.cos(rads) * center + center;
            this.yPos = Math.sin(rads) * center + center;
        }
    }

    updateRoundPos() {
        var el = angular.element(this.$element[0].querySelector('.color-picker-grid .color-picker-picker'));

        el.css({
            left: (this.pickerDimensions.width * this.xPos / 100) + 'px',
            top: (this.pickerDimensions.height * this.yPos / 100) + 'px'
        });
    }

    getEventPos(event) {
        // if a touch event
        if (event.type.search('touch') === 0) {
            // if event modified by angular
            if (event.originalEvent && event.originalEvent.changedTouches) {
                return event.originalEvent.changedTouches[0];
                // if a standard js touch event
            } else if (event.changedTouches) {
                return event.changedTouches[0];
            }
        }

        // return a non-touch event
        return event;
    }

    calculateSliderPos(el, eventPos, multiplier) {
        if (this.options.horizontal) {
            return Math.round((1 - ((eventPos.pageX - this.offset(el).left) / el.prop('offsetWidth'))) * multiplier);
        }

        return Math.round((1 - ((eventPos.pageY - this.offset(el).top) / el.prop('offsetHeight'))) * multiplier);
    }

    eventApiDispatch(name, args) {
        if (this.eventApi && typeof this.eventApi[name] === 'function') {
            if (!args) {
                args = [];
            }

            args.unshift(this.internalNgModel);
            args.unshift(this.api);

            this.eventApi[name].apply(this, args);
        }
    }

    /** taken and modified from jQuery's find */
    find(selector) {
        var context = this.wrapper ? this.wrapper[0] : this.$element[0],
            results = [],
            nodeType;

        // Same basic safeguard as Sizzle
        if (!selector) {
            return results;
        }

        if (typeof selector === 'string') {
            // Early return if context is not an element or document
            if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) {
                return [];
            }

            results = context.querySelectorAll(selector);

        } else {
            if (context.contains(selector)) {
                results.push(selector);
            }
        }

        return angular.element(results);
    }

    /** taken and modified from jQuery's offset */
    offset(el) {
        var docElem, win, rect, doc, elem = el[0];

        if (!elem) {
            return;
        }

        // Support: IE<=11+
        // Running getBoundingClientRect on a
        // disconnected node in IE throws an error
        if (!elem.getClientRects().length) {
            return {
                top: 0,
                left: 0
            };
        }

        rect = elem.getBoundingClientRect();

        // Make sure element is not hidden (display: none)
        if (rect.width || rect.height) {
            doc = elem.ownerDocument;
            win = this.getWindowElements(doc);
            docElem = doc.documentElement;

            // hack for small chrome screens not position the clicks properly when the page is scrolled
            if (this.chrome && this.android_version < 6 && screen.width <= 768) {
                return {
                    top: rect.top - docElem.clientTop,
                    left: rect.left - docElem.clientLeft
                };
            }

            return {
                top: rect.top + win.pageYOffset - docElem.clientTop,
                left: rect.left + win.pageXOffset - docElem.clientLeft
            };
        }


        return rect;
    }

    getWindowElements(doc) {
        return doc !== null && doc === doc.window ? doc : doc.nodeType === 9 && doc.defaultView;
    }

    anyMouseEvents() {
        return this.colorMouse || this.hueMouse || this.saturationMouse || this.lightnessMouse || this.opacityMouse;
    }

    getMaxFromType(type) {
        return type === 'hue' ? 360 : 100;
    }
}

AngularColorPickerController.$inject = ['$scope', '$element', '$document', '$timeout', 'ColorPickerOptions'];