Colorpicker.js

'use strict';

import Extension from './Extension';
import defaults from './options';
import coreExtensions from 'extensions';
import $ from 'jquery';
import SliderHandler from './SliderHandler';
import PopupHandler from './PopupHandler';
import InputHandler from './InputHandler';
import ColorHandler from './ColorHandler';
import PickerHandler from './PickerHandler';
import AddonHandler from './AddonHandler';
import ColorItem from './ColorItem';

let colorPickerIdCounter = 0;

let root = (typeof self !== 'undefined' ? self : this); // window

/**
 * Colorpicker widget class
 */
class Colorpicker {
  /**
   * Color class
   *
   * @static
   * @type {Color}
   */
  static get Color() {
    return ColorItem;
  }

  /**
   * Extension class
   *
   * @static
   * @type {Extension}
   */
  static get Extension() {
    return Extension;
  }

  /**
   * Internal color object
   *
   * @type {Color|null}
   */
  get color() {
    return this.colorHandler.color;
  }

  /**
   * Internal color format
   *
   * @type {String|null}
   */
  get format() {
    return this.colorHandler.format;
  }

  /**
   * Getter of the picker element
   *
   * @returns {jQuery|HTMLElement}
   */
  get picker() {
    return this.pickerHandler.picker;
  }

  /**
   * @fires Colorpicker#colorpickerCreate
   * @param {Object|String} element
   * @param {Object} options
   * @constructor
   */
  constructor(element, options) {
    colorPickerIdCounter += 1;
    /**
     * The colorpicker instance number
     * @type {number}
     */
    this.id = colorPickerIdCounter;

    /**
     * Latest colorpicker event
     *
     * @type {{name: String, e: *}}
     */
    this.lastEvent = {
      alias: null,
      e: null
    };

    /**
     * The element that the colorpicker is bound to
     *
     * @type {*|jQuery}
     */
    this.element = $(element)
      .addClass('colorpicker-element')
      .attr('data-colorpicker-id', this.id);

    /**
     * @type {defaults}
     */
    this.options = $.extend(true, {}, defaults, options, this.element.data());

    /**
     * @type {boolean}
     * @private
     */
    this.disabled = false;

    /**
     * Extensions added to this instance
     *
     * @type {Extension[]}
     */
    this.extensions = [];

    /**
     * The element where the
     * @type {*|jQuery}
     */
    this.container = (
      this.options.container === true ||
      (this.options.container !== true && this.options.inline === true)
    ) ? this.element : this.options.container;

    this.container = (this.container !== false) ? $(this.container) : false;

    /**
     * @type {InputHandler}
     */
    this.inputHandler = new InputHandler(this);
    /**
     * @type {ColorHandler}
     */
    this.colorHandler = new ColorHandler(this);
    /**
     * @type {SliderHandler}
     */
    this.sliderHandler = new SliderHandler(this);
    /**
     * @type {PopupHandler}
     */
    this.popupHandler = new PopupHandler(this, root);
    /**
     * @type {PickerHandler}
     */
    this.pickerHandler = new PickerHandler(this);
    /**
     * @type {AddonHandler}
     */
    this.addonHandler = new AddonHandler(this);

    this.init();

    // Emit a create event
    $($.proxy(function () {
      /**
       * (Colorpicker) When the Colorpicker instance has been created and the DOM is ready.
       *
       * @event Colorpicker#colorpickerCreate
       */
      this.trigger('colorpickerCreate');
    }, this));
  }

  /**
   * Initializes the plugin
   * @private
   */
  init() {
    // Init addon
    this.addonHandler.bind();

    // Init input
    this.inputHandler.bind();

    // Init extensions (before initializing the color)
    this.initExtensions();

    // Init color
    this.colorHandler.bind();

    // Init picker
    this.pickerHandler.bind();

    // Init sliders and popup
    this.sliderHandler.bind();
    this.popupHandler.bind();

    // Inject into the DOM (this may make it visible)
    this.pickerHandler.attach();

    // Update all components
    this.update();

    if (this.inputHandler.isDisabled()) {
      this.disable();
    }
  }

  /**
   * Initializes the plugin extensions
   * @private
   */
  initExtensions() {
    if (!Array.isArray(this.options.extensions)) {
      this.options.extensions = [];
    }

    if (this.options.debug) {
      this.options.extensions.push({name: 'debugger'});
    }

    // Register and instantiate extensions
    this.options.extensions.forEach((ext) => {
      this.registerExtension(Colorpicker.extensions[ext.name.toLowerCase()], ext.options || {});
    });
  }

  /**
   * Creates and registers the given extension
   *
   * @param {Extension} ExtensionClass The extension class to instantiate
   * @param {Object} [config] Extension configuration
   * @returns {Extension}
   */
  registerExtension(ExtensionClass, config = {}) {
    let ext = new ExtensionClass(this, config);

    this.extensions.push(ext);
    return ext;
  }

  /**
   * Destroys the current instance
   *
   * @fires Colorpicker#colorpickerDestroy
   */
  destroy() {
    let color = this.color;

    this.sliderHandler.unbind();
    this.inputHandler.unbind();
    this.popupHandler.unbind();
    this.colorHandler.unbind();
    this.addonHandler.unbind();
    this.pickerHandler.unbind();

    this.element
      .removeClass('colorpicker-element')
      .removeData('colorpicker')
      .removeData('color')
      .off('.colorpicker');

    /**
     * (Colorpicker) When the instance is destroyed with all events unbound.
     *
     * @event Colorpicker#colorpickerDestroy
     */
    this.trigger('colorpickerDestroy', color);
  }

  /**
   * Shows the colorpicker widget if hidden.
   * If the colorpicker is disabled this call will be ignored.
   *
   * @fires Colorpicker#colorpickerShow
   * @param {Event} [e]
   */
  show(e) {
    this.popupHandler.show(e);
  }

  /**
   * Hides the colorpicker widget.
   *
   * @fires Colorpicker#colorpickerHide
   * @param {Event} [e]
   */
  hide(e) {
    this.popupHandler.hide(e);
  }

  /**
   * Toggles the colorpicker between visible and hidden.
   *
   * @fires Colorpicker#colorpickerShow
   * @fires Colorpicker#colorpickerHide
   * @param {Event} [e]
   */
  toggle(e) {
    this.popupHandler.toggle(e);
  }

  /**
   * Returns the current color value as string
   *
   * @param {String|*} [defaultValue]
   * @returns {String|*}
   */
  getValue(defaultValue = null) {
    let val = this.colorHandler.color;

    val = (val instanceof ColorItem) ? val : defaultValue;

    if (val instanceof ColorItem) {
      return val.string(this.format);
    }

    return val;
  }

  /**
   * Sets the color manually
   *
   * @fires Colorpicker#colorpickerChange
   * @param {String|Color} val
   */
  setValue(val) {
    if (this.isDisabled()) {
      return;
    }
    let ch = this.colorHandler;

    if (
      (ch.hasColor() && !!val && ch.color.equals(val)) ||
      (!ch.hasColor() && !val)
    ) {
      // same color or still empty
      return;
    }

    ch.color = val ? ch.createColor(val, this.options.autoInputFallback, this.options.autoHexInputFallback) : null;

    /**
     * (Colorpicker) When the color is set programmatically with setValue().
     *
     * @event Colorpicker#colorpickerChange
     */
    this.trigger('colorpickerChange', ch.color, val);

    // force update if color has changed to empty
    this.update();
  }

  /**
   * Updates the UI and the input color according to the internal color.
   *
   * @fires Colorpicker#colorpickerUpdate
   */
  update() {
    if (this.colorHandler.hasColor()) {
      this.inputHandler.update();
    } else {
      this.colorHandler.assureColor();
    }

    this.addonHandler.update();
    this.pickerHandler.update();

    /**
     * (Colorpicker) Fired when the widget is updated.
     *
     * @event Colorpicker#colorpickerUpdate
     */
    this.trigger('colorpickerUpdate');
  }

  /**
   * Enables the widget and the input if any
   *
   * @fires Colorpicker#colorpickerEnable
   * @returns {boolean}
   */
  enable() {
    this.inputHandler.enable();
    this.disabled = false;
    this.picker.removeClass('colorpicker-disabled');

    /**
     * (Colorpicker) When the widget has been enabled.
     *
     * @event Colorpicker#colorpickerEnable
     */
    this.trigger('colorpickerEnable');
    return true;
  }

  /**
   * Disables the widget and the input if any
   *
   * @fires Colorpicker#colorpickerDisable
   * @returns {boolean}
   */
  disable() {
    this.inputHandler.disable();
    this.disabled = true;
    this.picker.addClass('colorpicker-disabled');

    /**
     * (Colorpicker) When the widget has been disabled.
     *
     * @event Colorpicker#colorpickerDisable
     */
    this.trigger('colorpickerDisable');
    return true;
  }

  /**
   * Returns true if this instance is enabled
   * @returns {boolean}
   */
  isEnabled() {
    return !this.isDisabled();
  }

  /**
   * Returns true if this instance is disabled
   * @returns {boolean}
   */
  isDisabled() {
    return this.disabled === true;
  }

  /**
   * Triggers a Colorpicker event.
   *
   * @param eventName
   * @param color
   * @param value
   */
  trigger(eventName, color = null, value = null) {
    this.element.trigger({
      type: eventName,
      colorpicker: this,
      color: color ? color : this.color,
      value: value ? value : this.getValue()
    });
  }
}

/**
 * Colorpicker extension classes, indexed by extension name
 *
 * @static
 * @type {Object} a map between the extension name and its class
 */
Colorpicker.extensions = coreExtensions;

export default Colorpicker;