// @ts-ignore: esbuild custom loader
import styles from './hue.pcss';
import { CUSTOM_EVENT_COLOR_HSV_CHANGED, sendHueCustomEvent } from '../../domain/events-provider';
import { getHueByLeft, getLeftByHue, parseColor } from '../../domain/color-provider';
/*
Usage:
------
*/
class Hue extends HTMLElement {
// this id attribute is used for custom events
private cid: string;
private $hue: HTMLElement | null;
private $pointer: HTMLElement | null;
private hue = 0; // [0, 360]
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.onKeyDown = this.onKeyDown.bind(this);
this.hsvChanged = this.hsvChanged.bind(this);
}
render() {
if (this.$pointer) {
this.$pointer.style.left = `${getLeftByHue(this.hue)}%`;
}
// update outer color to change the button, and
// send the updated color to the user
sendHueCustomEvent(this.cid, this.hue);
}
hsvChanged(evt: CustomEvent) {
if (!evt || !evt.detail || !evt.detail.cid) return;
// handle only current instance
if (evt.detail.cid !== this.cid) return;
if (this.hue !== evt.detail.h) {
this.hue = evt.detail.h;
this.render();
}
}
// we need to handle both MouseEvent and TouchEvent --->
// eslint-disable-next-line
onChange(evt: any) {
if (!this.$hue) return;
if (evt.preventDefault) {
evt.preventDefault();
}
const { width: boxWidth, left: boxLeft } = this.$hue.getBoundingClientRect();
if (boxWidth === 0) return;
const mouseX = typeof evt.clientX === 'number' ? evt.clientX : evt.touches[0].clientX;
const left = Math.min(Math.max(0, mouseX - boxLeft), boxWidth);
const percent = Math.min(Math.max(0, Math.round((left * 100) / boxWidth)), 100);
this.hue = getHueByLeft(percent);
this.render();
}
onKeyDown(evt: KeyboardEvent) {
this.$pointer?.focus();
switch (evt.key) {
case 'ArrowLeft': {
let percent = getLeftByHue(this.hue);
percent = Math.max(0, percent - 1);
this.hue = getHueByLeft(percent);
this.render();
break;
}
case 'ArrowRight': {
let percent = getLeftByHue(this.hue);
percent = Math.min(100, percent + 1);
this.hue = getHueByLeft(percent);
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);
}
/**
* when the custom element connected to DOM
*/
connectedCallback() {
if (!this.shadowRoot) return;
this.cid = this.getAttribute('cid') || '';
const color = parseColor(this.getAttribute('color'));
this.hue = color.toHsv().h;
this.shadowRoot.innerHTML = `
`;
this.$hue = this.shadowRoot.querySelector('.hue');
this.$pointer = this.shadowRoot.querySelector('.pointer-box');
this.$hue?.addEventListener('mousedown', this.onMouseDown);
this.$hue?.addEventListener('mouseup', this.onMouseUp);
this.$hue?.addEventListener('touchmove', this.onChange);
this.$hue?.addEventListener('touchstart', this.onChange);
this.$pointer?.addEventListener('keydown', this.onKeyDown);
document.addEventListener(CUSTOM_EVENT_COLOR_HSV_CHANGED, this.hsvChanged);
}
/**
* when the custom element disconnected from DOM
*/
disconnectedCallback() {
this.$hue?.removeEventListener('mousedown', this.onMouseDown);
this.$hue?.removeEventListener('mouseup', this.onMouseUp);
this.$hue?.removeEventListener('touchmove', this.onChange);
this.$hue?.removeEventListener('touchstart', this.onChange);
this.$pointer?.removeEventListener('keydown', this.onKeyDown);
document.removeEventListener(CUSTOM_EVENT_COLOR_HSV_CHANGED, this.hsvChanged);
}
/**
* when attributes change
*/
attributeChangedCallback(_attrName: string, _oldVal: string, newVal: string) {
const color = parseColor(newVal);
const hsv = color.toHsv();
this.hue = hsv.h;
this.render();
}
}
export default Hue;