coral-spectrum/coral-component-colorpicker/src/scripts/ColorArea.js
/**
* Copyright 2021 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {BaseComponent} from '../../../coral-base-component';
import colorArea from '../templates/colorArea';
import {validate, transform, events, commons, i18n, Keys} from '../../../coral-utils';
import { TinyColor } from '@ctrl/tinycolor';
import colorUtil from "./ColorUtil";
const CLASSNAME = '_coral-ColorPicker-ColorArea';
/**
@class Coral.ColorPicker.ColorArea
@classdesc A ColorPicker area component to select Saturation and Value
@htmltag coral-colorpicker-colorarea
@extends {HTMLElement}
@extends {BaseComponent}
*/
class ColorArea extends BaseComponent(HTMLElement) {
constructor() {
super();
this._delegateEvents(commons.extend(this._events, {
'key:up': '_handleKey',
'key:right': '_handleKey',
'key:down': '_handleKey',
'key:left': '_handleKey',
'key:pageUp': '_handleKey',
'key:pageDown': '_handleKey',
'key:home': '_handleKey',
'key:end': '_handleKey',
'input': '_onInputChangeHandler',
'touchstart': '_onMouseDown',
'mousedown': '_onMouseDown',
'capture:focus': '_focus',
'capture:blur': '_blur'
}));
// Templates
this._elements = {};
colorArea.call(this._elements, {commons, i18n});
// default values
this._x = 1;
this._y = 1;
this._hue = 120;
this._minX = 0;
this._minY = 0;
this._maxX = 1;
this._maxY = 1;
this._stepX = 0.01;
this._stepY = 0.01;
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
const frag = document.createDocumentFragment();
// Render template
frag.appendChild(this._elements.colorAreaGradient);
frag.appendChild(this._elements.colorHandle);
frag.appendChild(this._elements.sliderX);
frag.appendChild(this._elements.sliderY);
// Support cloneNode
while (this.firstChild) {
const child = this.firstChild;
if (child.nodeType === Node.ELEMENT_NODE && child.hasAttribute('handle')) {
this.removeChild(child);
}
else {
frag.appendChild(child);
}
}
this.appendChild(frag);
// These should be used to set a property since property handler aren't called until elements are attached to dom.
// Attribute values are delivered to change-listeners even if element isn't attached to dom yet, so attributes
// can be set to e.g. this._elements.colorHandle.
this._handle = this.querySelector('._coral-ColorPicker-ColorArea-colorHandle');
this._sliderX = this.querySelector('._coral-ColorPicker-ColorArea-slider[name="x"]');
this._sliderY = this.querySelector('._coral-ColorPicker-ColorArea-slider[name="y"]');
this._gradient = this.querySelector('._coral-ColorPicker-ColorArea-gradient');
this._updateHue(this._hue);
this.x = this._x;
this.y = this._y;
this._updateHandle(this._hue, this.x, this.y, this.color);
this._reflectAttribute('color', this.color);
}
/**
The ColorArea label.
@default ''
@type {String}
@htmlattribute label
@htmlattributereflected
*/
get label() {
return this._label;
}
set label(value) {
this._label = value;
this._reflectAttribute('label', this._label);
if(this._elements.sliderX.getAttribute('aria-label') !== this._label) {
this._elements.sliderX.setAttribute('aria-label', this._label);
this._elements.sliderY.setAttribute('aria-label', this._label);
}
}
/**
The ColorArea x value. value should be in multiple of x-step size.
@default 1
@type {String}
@htmlattribute label
@htmlattributereflected
*/
get x() {
return this._x;
}
set x(value) {
let rawX = Number(value, 10);
if(parseFloat(rawX).toFixed(3) !== parseFloat(this._x).toFixed(3)) {
if(isNaN(rawX)) {
rawX = this._minX;
}
this._x = this._snapValueToStep(rawX, this._minX, this._maxX, this._stepX);
this._reflectAttribute('x', this._x);
this.color = this._toHsvString(this._hue, this._x, this.y);
}
this._elements.sliderX.setAttribute('aria-valuetext', `${i18n.get('Saturation')}: ${Math.round(this._x / (this._maxX - this._minX) * 100)}%`);
this._elements.sliderX.setAttribute('title', this.color);
this._elements.sliderX.setAttribute('value', this._x);
}
/** @private */
_toHsvString(hue, x, y) {
const s = `${Math.round(this._x / (this._maxX - this._minX) * 100)}%`;
const v = `${Math.round(this._y / (this._maxY - this._minY) * 100)}%`;
return `hsv(${this._hue}, ${s}, ${v})`;
}
/**
The ColorArea y value. value should be in multiple of y-step size.
@default 1
@type {String}
@htmlattribute label
@htmlattributereflected
*/
get y() {
return this._y;
}
set y(value) {
let rawY = Number(value, 10);
if(parseFloat(rawY).toFixed(3) !== parseFloat(this._y).toFixed(3)) {
if(isNaN(rawY)) {
rawY = this._minY;
}
this._y = this._snapValueToStep(rawY, this._minY, this._maxY, this._stepY);
this._reflectAttribute('y', this._y);
this.color = this._toHsvString(this._hue, this.x, this._y);
}
this._elements.sliderY.setAttribute('aria-valuetext', `${i18n.get('Brightness')}: ${Math.round(this._y / (this._maxY - this._minY) * 100)}%`);
this._elements.sliderY.setAttribute('title', this.color);
this._elements.sliderY.setAttribute('value', this._y);
}
/**
The ColorArea color string in hsla format.
@default hsla(0, 100%, 50%, 1)
@type {String}
@htmlattribute color
@htmlattributereflected
*/
get color() {
return colorUtil.toHslString(this._hue, new TinyColor({h:this._hue, s:this.x, v:this.y}).toHslString());
}
set color(value) {
let color = new TinyColor(value);
if(!color.isValid) {
color = new TinyColor("hsla(120, 100%, 50%, 1)");
value = color.toHslString();
}
// if color strings are equal or colors are equivalent
if(this.color === value || new TinyColor(this.color).toString(color.format) === color) {
return;
}
const {h,s,v} = colorUtil.extractHsv(value);
if(h !== this._hue) {
this._updateHue(colorUtil.getHue(value));
}
if(s !== this._x) {
this.x = s;
}
if(v !== this.Y) {
this.y = v;
}
this._updateHandle(this._hue, this.x, this.y, this.color);
this._reflectAttribute('color', this.color);
}
/** @ignore */
static get observedAttributes() {
return super.observedAttributes.concat([
'label',
'x',
'y',
'disabled',
'color'
]);
}
/**
Whether this field is disabled or not.
@type {Boolean}
@default false
@htmlattribute disabled
@htmlattributereflected
*/
get disabled() {
return this._disabled || false;
}
set disabled(value) {
this._disabled = transform.booleanAttr(value);
this._reflectAttribute('disabled', this._disabled);
this.classList.toggle('is-disabled', this._disabled);
this[this._disabled ? 'setAttribute' : 'removeAttribute']('aria-disabled', this._disabled);
this._elements.sliderX[this._disabled ? 'setAttribute' : 'removeAttribute']('disabled', this._disabled);
this._elements.sliderY[this._disabled ? 'setAttribute' : 'removeAttribute']('disabled', this._disabled);
this._elements.colorHandle[this._disabled ? 'setAttribute' : 'removeAttribute']('disabled', this._disabled);
}
focus() {
this._sliderX.focus();
}
/** @private **/
_updateHue(hue) {
this._hue = hue;
if(this._gradient){
this._gradient.style.background = `linear-gradient(to top, black 0%, hsla(${this._hue}, 100%, 0%, 0) 100%),
linear-gradient(to right, white 0%, hsla(${this._hue}, 100%, 0%, 0) 100%),
hsl(${this._hue}, 100%, 50%)`;
}
}
/** @private */
_snapValueToStep(rawValue, min, max, step) {
const remainder = (rawValue - min) % step;
let snappedValue = rawValue;
if (Math.abs(remainder) * 2 >= step) {
snappedValue = rawValue - remainder + step;
}
else {
snappedValue = rawValue - remainder;
}
if (snappedValue < min) {
snappedValue = min;
}
else if (snappedValue > max) {
snappedValue = min + Math.floor((max - min) / step) * step;
}
return snappedValue;
}
/** @private */
_updateHandle(hue, x, y, colorStr) {
let percent = 100 - ((y - this._minY) / (this._maxY - this._minY) * 100);
if(this._handle) {
this._handle.style.top = `${percent}%`;
}
percent = (x - this._minX) / (this._maxX - this._minX) * 100;
if(this._handle) {
this._handle.style.left = `${percent}%`;
}
this._elements.colorHandle.setAttribute('color', colorStr);
}
/** @private */
_focusHandle(isFocused) {
if(this._handle) {
if(isFocused === true) {
this._handle.focus();
}
else {
this._handle.blur();
}
}
}
/** @private */
_changeValue(x, y) {
if(this.x !== x || this.y !== y) {
var currX = this.x;
var currY = this.y;
this.x = x;
this.y = y;
if(this.x !== currX || this.y !== currY) {
this.trigger('change');
}
}
}
_focusX() {
this._sliderX.focus();
}
_focusY() {
this._sliderY.focus();
}
/******* Events Handling **************/
/** @private */
_onInputChangeHandler(event) {
this._focusHandle(true);
event.stopPropagation();
if(event.target === this._sliderX) {
this._changeValue(event.target.value, this.y);
}
else {
this._changeValue(this.x, event.target.value);
}
}
/** @private */
_handleKey(event) {
this._focusHandle(true);
event.preventDefault();
event.stopPropagation();
let y = this.y;
let x = this.x;
// increase
if (event.keyCode === Keys.keyToCode('up') ||
event.keyCode === Keys.keyToCode('pageUp')) {
y += this._stepY;
this._focusY();
}
// decrease
else if (event.keyCode === Keys.keyToCode('down') ||
event.keyCode === Keys.keyToCode('pageDown')) {
y -= this._stepY;
this._focusY();
}
// increase
if (event.keyCode === Keys.keyToCode('right')) {
x += this._stepX;
this._focusX();
}
// decrease
else if (event.keyCode === Keys.keyToCode('left')) {
x -= this._stepX;
this._focusX();
}
// min
else if (event.keyCode === Keys.keyToCode('home')) {
x = this._minX;
y = this._minY;
this._focusX();
}
// max
else if (event.keyCode === Keys.keyToCode('end')) {
x = this._maxX;
y = this._maxY;
this._focusX();
}
this._changeValue(x, y);
}
/** @private */
_onMouseDown() {
if (event instanceof MouseEvent) {
if ((event.which || event.button) !== 1) {
return;
}
}
event.preventDefault();
this._handle.classList.add('is-dragged');
document.body.classList.add('u-coral-closedHand');
this.focus();
this._focusHandle(true);
const {x,y} = this._getValuesFromCoord(this._getPoint(event));
this._changeValue(x, y);
const classNameSelector = "." + CLASSNAME;
this._draggingHandler = this._handleDragging.bind(this);
this._mouseUpHandler = this._mouseUp.bind(this);
events.on('mousemove.CoralArea', this._draggingHandler);
events.on('mouseup.CoralArea', this._mouseUpHandler);
events.on('touchmove.CoralArea', this._draggingHandler);
events.on('touchend.CoralArea', this._mouseUpHandler);
events.on('touchcancel.CoralArea', this._mouseUpHandler);
}
/** @private */
_getValuesFromCoord(point) {
const boundingClientRect = this.getBoundingClientRect();
const height = boundingClientRect.height;
const width = boundingClientRect.width;
let posY = point.clientY;
let posX = point.clientX;
if(posY < boundingClientRect.top) {
posY = boundingClientRect.top;
}
else if(posY > boundingClientRect.bottom) {
posY = boundingClientRect.bottom;
}
if(posX < boundingClientRect.left) {
posX = boundingClientRect.left;
}
else if(posX > boundingClientRect.right) {
posX = boundingClientRect.right;
}
let positionFraction = (height -(posY - boundingClientRect.top)) / height;
const rawY = this._minY + positionFraction * (this._maxY - this._minY);
positionFraction = (posX - boundingClientRect.left) / width;
const rawX = this._minX + positionFraction * (this._maxX - this._minX);
return {x: rawX, y: rawY};
}
/** @private */
_handleDragging(event) {
const {x,y} = this._getValuesFromCoord(this._getPoint(event));
this._changeValue(x, y);
event.preventDefault();
}
/** @private */
_mouseUp(event) {
this._handle.style.cursor = 'grab';
this._handle.classList.remove('is-dragged');
document.body.classList.remove('u-coral-closedHand');
this._focusHandle(false);
const classNameSelector = "." + CLASSNAME;
events.off('mousemove.CoralArea', this._draggingHandler);
events.off('touchmove.CoralArea', this._draggingHandler);
events.off('mouseup.CoralArea', this._mouseUpHandler);
events.off('touchend.CoralArea', this._mouseUpHandler);
events.off('touchcancel.CoralArea', this._mouseUpHandler);
this._currentHandle = null;
this._draggingHandler = null;
this._mouseUpHandler = null;
}
/**
@private
@return {Object} which contains the real coordinates
*/
_getPoint(event) {
if (event.changedTouches && event.changedTouches.length > 0) {
return event.changedTouches[0];
}
else if (event.touches && event.touches.length > 0) {
return event.touches[0];
}
return event;
}
/**
Handles "focusin" event.
@private
*/
_focus(event) {
this._focusHandle(true);
}
/**
Handles "focusout" event.
@private
*/
_blur(event) {
this._focusHandle(false);
}
}
export default ColorArea;