// @ts-ignore: esbuild custom loader
import styles from './saturation.pcss';
import { CUSTOM_EVENT_COLOR_HSV_CHANGED, CUSTOM_EVENT_COLOR_HUE_CHANGED, sendHsvCustomEvent } from '../../domain/events-provider';
import { getHueBackground, getLeftBySaturation, getTopByValue, parseColor, SATURATION_STEP } from '../../domain/color-provider';
/*
Usage:
------
*/
class Saturation extends HTMLElement {
// this id attribute is used for custom events
private cid: string;
private $saturation: HTMLElement | null;
private $color: HTMLElement | null;
private $pointer: HTMLElement | null;
private hue = 0; // [0, 360]
private saturation = 0; // [0, 1]
private value = 0; // [0, 1]
static get observedAttributes() {
return ['color'];
}
constructor() {
super();
this.attachShadow({
mode: 'open', // 'closed', 'open',
});
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onChange = this.onChange.bind(this);
this.onPointerKeyDown = this.onPointerKeyDown.bind(this);
this.hsvChanged = this.hsvChanged.bind(this);
this.hueChanged = this.hueChanged.bind(this);
}
render(sendEvent = true) {
// re-render
if (this.$pointer) {
this.$pointer.style.left = getLeftBySaturation(this.saturation);
this.$pointer.style.top = getTopByValue(this.value);
}
if (this.$color) {
this.$color.setAttribute('style', `background: ${getHueBackground(this.hue)}`);
}
if (sendEvent) {
// update outer color to change the button, and
// send the updated color to the user
sendHsvCustomEvent(this.cid, this.hue, this.saturation, this.value);
}
}
// we need to handle both MouseEvent and TouchEvent --->
// eslint-disable-next-line
onChange(evt: any) {
if (!this.$saturation) return;
const { width: boxWidth, height: boxHeight, left: boxLeft, top: boxTop } = this.$saturation.getBoundingClientRect();
if (boxWidth === 0 || boxHeight === 0) return;
const mouseX = typeof evt.clientX === 'number' ? evt.clientX : evt.touches[0].clientX;
const mouseY = typeof evt.clientY === 'number' ? evt.clientY : evt.touches[0].clientY;
const lPos = Math.min(Math.max(0, mouseX - boxLeft), boxWidth);
const tPos = Math.min(Math.max(0, mouseY - boxTop), boxHeight);
this.saturation = lPos / boxWidth;
this.value = 1 - tPos / boxHeight;
this.render();
}
onPointerKeyDown(evt: KeyboardEvent) {
switch (evt.key) {
case 'ArrowLeft': {
this.saturation = Math.max(0, this.saturation - SATURATION_STEP);
this.render();
break;
}
case 'ArrowRight': {
this.saturation = Math.min(1, this.saturation + SATURATION_STEP);
this.render();
break;
}
case 'ArrowUp': {
this.value = Math.min(1, this.value + SATURATION_STEP);
this.render();
break;
}
case 'ArrowDown': {
evt.preventDefault();
this.value = Math.max(0, this.value - SATURATION_STEP);
this.render();
break;
}
}
}
onMouseDown(evt: MouseEvent) {
if (evt.preventDefault) {
evt.preventDefault();
}
this.onChange(evt);
window.addEventListener('mousemove', this.onChange);
window.addEventListener('mouseup', this.onMouseUp);
window.setTimeout(() => {
this.$pointer?.focus();
}, 0);
}
onMouseUp() {
window.removeEventListener('mousemove', this.onChange);
window.removeEventListener('mouseup', this.onChange);
}
hsvChanged(evt: CustomEvent) {
if (!evt || !evt.detail || !evt.detail.cid) return;
// handle only current instance
if (evt.detail.cid !== this.cid) return;
let changed = false;
if (this.hue !== evt.detail.h) {
this.hue = evt.detail.h;
changed = true;
}
if (this.saturation !== evt.detail.s) {
this.saturation = evt.detail.s;
changed = true;
}
if (this.value !== evt.detail.v) {
this.value = evt.detail.v;
changed = true;
}
if (changed) {
this.render(false);
}
}
hueChanged(evt: CustomEvent) {
if (!evt || !evt.detail || !evt.detail.cid) return;
// handle only current instance
if (evt.detail.cid !== this.cid) return;
this.hue = evt.detail.h;
this.render();
}
/**
* when the custom element connected to DOM
*/
connectedCallback() {
if (!this.shadowRoot) return;
this.cid = this.getAttribute('cid') || '';
const color = parseColor(this.getAttribute('color'));
const hsv = color.toHsv();
this.hue = hsv.h;
this.saturation = hsv.s;
this.value = hsv.v;
const top = getTopByValue(this.value);
const left = getLeftBySaturation(this.saturation);
this.shadowRoot.innerHTML = `
`;
this.$saturation = this.shadowRoot.querySelector('.saturation');
this.$color = this.shadowRoot.querySelector('.box');
this.$pointer = this.shadowRoot.querySelector('.pointer');
this.$pointer?.addEventListener('keydown', this.onPointerKeyDown);
this.$saturation?.addEventListener('mousedown', this.onMouseDown);
this.$saturation?.addEventListener('mouseup', this.onMouseUp);
this.$saturation?.addEventListener('touchmove', this.onChange);
this.$saturation?.addEventListener('touchstart', this.onChange);
document.addEventListener(CUSTOM_EVENT_COLOR_HSV_CHANGED, this.hsvChanged);
document.addEventListener(CUSTOM_EVENT_COLOR_HUE_CHANGED, this.hueChanged);
}
/**
* when the custom element disconnected from DOM
*/
disconnectedCallback() {
this.$saturation?.removeEventListener('mousedown', this.onMouseDown);
this.$saturation?.removeEventListener('mouseup', this.onMouseUp);
this.$saturation?.removeEventListener('touchmove', this.onChange);
this.$saturation?.removeEventListener('touchstart', this.onChange);
this.$pointer?.removeEventListener('keydown', this.onPointerKeyDown);
document.removeEventListener(CUSTOM_EVENT_COLOR_HSV_CHANGED, this.hsvChanged);
document.removeEventListener(CUSTOM_EVENT_COLOR_HUE_CHANGED, this.hueChanged);
}
/**
* when attributes change
*/
attributeChangedCallback(_attrName: string, _oldVal: string, newVal: string) {
const color = parseColor(newVal);
const hsv = color.toHsv();
this.hue = hsv.h;
this.saturation = hsv.s;
this.value = hsv.v;
this.render(false);
}
}
export default Saturation;