2021-01-01 14:27:58 +00:00
|
|
|
/* global colorConverter */
|
|
|
|
/* global colorMimicry */
|
2017-11-15 12:59:24 +00:00
|
|
|
'use strict';
|
|
|
|
|
2017-11-27 06:56:22 +00:00
|
|
|
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
|
2021-01-01 14:27:58 +00:00
|
|
|
const cm = window.CodeMirror && this;
|
2017-11-15 12:59:24 +00:00
|
|
|
const CSS_PREFIX = 'colorpicker-';
|
|
|
|
const HUE_COLORS = [
|
|
|
|
{hex: '#ff0000', start: .0},
|
|
|
|
{hex: '#ffff00', start: .17},
|
|
|
|
{hex: '#00ff00', start: .33},
|
|
|
|
{hex: '#00ffff', start: .50},
|
|
|
|
{hex: '#0000ff', start: .67},
|
|
|
|
{hex: '#ff00ff', start: .83},
|
2020-11-18 11:17:15 +00:00
|
|
|
{hex: '#ff0000', start: 1},
|
2017-11-15 12:59:24 +00:00
|
|
|
];
|
2020-10-26 15:03:41 +00:00
|
|
|
const MIN_HEIGHT = 220;
|
|
|
|
const MARGIN = 8;
|
|
|
|
let maxHeight = '0px';
|
2017-11-15 12:59:24 +00:00
|
|
|
|
|
|
|
let HSV = {};
|
|
|
|
let currentFormat;
|
2018-04-17 19:35:23 +00:00
|
|
|
const prevHSV = {};
|
2017-11-15 12:59:24 +00:00
|
|
|
|
|
|
|
let initialized = false;
|
|
|
|
let shown = false;
|
|
|
|
let options = {};
|
|
|
|
|
2020-10-26 15:03:41 +00:00
|
|
|
let /** @type {HTMLElement} */ $root;
|
|
|
|
let /** @type {HTMLElement} */ $sat;
|
|
|
|
let /** @type {HTMLElement} */ $satPointer;
|
|
|
|
let /** @type {HTMLElement} */ $hue;
|
|
|
|
let /** @type {HTMLElement} */ $hueKnob;
|
|
|
|
let /** @type {HTMLElement} */ $opacity;
|
|
|
|
let /** @type {HTMLElement} */ $opacityBar;
|
|
|
|
let /** @type {HTMLElement} */ $opacityKnob;
|
|
|
|
let /** @type {HTMLElement} */ $swatch;
|
|
|
|
let /** @type {HTMLElement} */ $formatChangeButton;
|
|
|
|
let /** @type {HTMLElement} */ $hexCode;
|
|
|
|
let /** @type {HTMLElement} */ $palette;
|
2017-11-15 12:59:24 +00:00
|
|
|
const $inputGroups = {};
|
|
|
|
const $inputs = {};
|
|
|
|
const $rgb = {};
|
|
|
|
const $hsl = {};
|
|
|
|
const $hexLettercase = {};
|
|
|
|
|
2017-11-25 13:46:57 +00:00
|
|
|
const allowInputFocus = !('ontouchstart' in document) || window.innerHeight > 800;
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
const dragging = {
|
|
|
|
saturationPointerPos: {x: 0, y: 0},
|
|
|
|
hueKnobPos: 0,
|
|
|
|
saturation: false,
|
|
|
|
hue: false,
|
|
|
|
opacity: false,
|
2020-10-26 15:03:41 +00:00
|
|
|
popup: false,
|
2017-11-15 12:59:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let prevFocusedElement;
|
|
|
|
let lastOutputColor;
|
|
|
|
let userActivity;
|
|
|
|
|
|
|
|
const PUBLIC_API = {
|
|
|
|
$root,
|
|
|
|
show,
|
|
|
|
hide,
|
|
|
|
setColor,
|
|
|
|
getColor,
|
|
|
|
options,
|
|
|
|
};
|
|
|
|
return PUBLIC_API;
|
|
|
|
|
|
|
|
//region DOM
|
|
|
|
|
|
|
|
function init() {
|
2020-10-26 15:03:41 +00:00
|
|
|
/** @returns {HTMLElement} */
|
|
|
|
function $(cls, props = {}, children = []) {
|
|
|
|
if (Array.isArray(props) || typeof props === 'string' || props instanceof Node) {
|
|
|
|
children = props;
|
|
|
|
props = {};
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
2020-10-26 15:03:41 +00:00
|
|
|
const el = document.createElement(props.tag || 'div');
|
|
|
|
el.className = toArray(cls).map(c => c ? CSS_PREFIX + c : '').join(' ');
|
|
|
|
el.append(...toArray(children));
|
|
|
|
if (props) delete props.tag;
|
2017-11-15 12:59:24 +00:00
|
|
|
return Object.assign(el, props);
|
|
|
|
}
|
|
|
|
const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source;
|
2020-10-26 15:03:41 +00:00
|
|
|
$root = $('popup', {
|
|
|
|
oninput: setFromInputs,
|
|
|
|
onkeydown: setFromKeyboard,
|
|
|
|
}, [
|
|
|
|
$sat = $('saturation-container', {
|
|
|
|
onmousedown: onSaturationMouseDown,
|
|
|
|
onmouseup: onSaturationMouseUp,
|
|
|
|
}, [
|
|
|
|
$('saturation', [
|
|
|
|
$('value', [
|
2017-11-15 12:59:24 +00:00
|
|
|
$satPointer = $('drag-pointer'),
|
2020-10-26 15:03:41 +00:00
|
|
|
]),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
$('popup-mover', {onmousedown: onPopupMoveStart}),
|
|
|
|
$('sliders', [
|
|
|
|
$('hue', {onmousedown: onHueMouseDown}, [
|
|
|
|
$hue = $('hue-container', [
|
|
|
|
$hueKnob = $('hue-knob', {onmousedown: onHueKnobMouseDown}),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
$('opacity', [
|
|
|
|
$opacity = $('opacity-container', {onmousedown: onOpacityMouseDown}, [
|
2017-11-15 12:59:24 +00:00
|
|
|
$opacityBar = $('opacity-bar'),
|
2020-10-26 15:03:41 +00:00
|
|
|
$opacityKnob = $('opacity-knob', {onmousedown: onOpacityKnobMouseDown}),
|
|
|
|
]),
|
|
|
|
]),
|
2017-11-15 12:59:24 +00:00
|
|
|
$('empty'),
|
|
|
|
$swatch = $('swatch'),
|
2020-10-26 15:03:41 +00:00
|
|
|
]),
|
|
|
|
$(['input-container', 'hex'], [
|
|
|
|
$inputGroups.hex = $(['input-group', 'hex'], [
|
|
|
|
$(['input-field', 'hex'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$hexCode = $('input', {tag: 'input', type: 'text', spellcheck: false,
|
2020-11-18 11:17:15 +00:00
|
|
|
pattern: /^\s*#([a-fA-F\d]{3}([a-fA-F\d]([a-fA-F\d]{2}([a-fA-F\d]{2})?)?)?)\s*$/.source,
|
2017-11-15 12:59:24 +00:00
|
|
|
}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', [
|
|
|
|
$hexLettercase.true = $('title-action', {onclick: onHexLettercaseClicked}, 'HEX'),
|
2017-11-15 12:59:24 +00:00
|
|
|
'\xA0/\xA0',
|
2020-10-26 15:03:41 +00:00
|
|
|
$hexLettercase.false = $('title-action', {onclick: onHexLettercaseClicked}, 'hex'),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
$inputGroups.rgb = $(['input-group', 'rgb'], [
|
|
|
|
$(['input-field', 'rgb-r'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$rgb.r = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'R'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'rgb-g'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$rgb.g = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'G'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'rgb-b'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$rgb.b = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'B'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'rgb-a'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$rgb.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'A'),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
$inputGroups.hsl = $(['input-group', 'hsl'], [
|
|
|
|
$(['input-field', 'hsl-h'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$hsl.h = $('input', {tag: 'input', type: 'number', step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'H'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'hsl-s'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$hsl.s = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'S'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'hsl-l'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$hsl.l = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'L'),
|
|
|
|
]),
|
|
|
|
$(['input-field', 'hsl-a'], [
|
2017-11-15 12:59:24 +00:00
|
|
|
$hsl.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}),
|
2020-10-26 15:03:41 +00:00
|
|
|
$('title', 'A'),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
$('format-change', [
|
|
|
|
$formatChangeButton = $('format-change-button', {onclick: setFromFormatElement}, '↔'),
|
|
|
|
]),
|
2021-09-24 06:39:49 +00:00
|
|
|
window.EyeDropper &&
|
|
|
|
$('dropper', {
|
|
|
|
tag: 'img',
|
|
|
|
onclick: () => new window.EyeDropper().open().then(r => setFromColor(r.sRGBHex), () => 0),
|
|
|
|
srcset: '/images/eyedropper/16px.png, /images/eyedropper/32px.png 2x',
|
|
|
|
}),
|
2020-10-26 15:03:41 +00:00
|
|
|
]),
|
|
|
|
$palette = $('palette', {
|
|
|
|
onclick: onPaletteClicked,
|
|
|
|
oncontextmenu: onPaletteClicked,
|
|
|
|
}),
|
|
|
|
]);
|
2017-11-15 12:59:24 +00:00
|
|
|
|
|
|
|
$inputs.hex = [$hexCode];
|
|
|
|
$inputs.rgb = [$rgb.r, $rgb.g, $rgb.b, $rgb.a];
|
|
|
|
$inputs.hsl = [$hsl.h, $hsl.s, $hsl.l, $hsl.a];
|
|
|
|
const inputsToArray = inputs => inputs.map(el => parseFloat(el.value));
|
|
|
|
const inputsToHexString = () => $hexCode.value.trim();
|
|
|
|
const inputsToRGB = ([r, g, b, a] = inputsToArray($inputs.rgb)) => ({r, g, b, a, type: 'rgb'});
|
|
|
|
const inputsToHSL = ([h, s, l, a] = inputsToArray($inputs.hsl)) => ({h, s, l, a, type: 'hsl'});
|
|
|
|
Object.defineProperty($inputs.hex, 'color', {get: inputsToHexString});
|
|
|
|
Object.defineProperty($inputs.rgb, 'color', {get: inputsToRGB});
|
|
|
|
Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL});
|
|
|
|
Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color});
|
2018-04-17 19:35:23 +00:00
|
|
|
Object.defineProperty($inputs, 'colorString', {
|
2020-11-18 11:17:15 +00:00
|
|
|
get: () => currentFormat && colorConverter.format($inputs[currentFormat].color),
|
2018-04-17 19:35:23 +00:00
|
|
|
});
|
2017-11-15 12:59:24 +00:00
|
|
|
|
2018-01-07 08:20:55 +00:00
|
|
|
HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex)));
|
2020-10-26 15:03:41 +00:00
|
|
|
$root.style.setProperty('--margin', MARGIN + 'px');
|
2017-11-15 12:59:24 +00:00
|
|
|
initialized = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region Public API
|
|
|
|
|
|
|
|
function show(opt) {
|
|
|
|
if (!initialized) {
|
|
|
|
init();
|
|
|
|
}
|
|
|
|
HSV = {};
|
|
|
|
currentFormat = '';
|
|
|
|
options = PUBLIC_API.options = opt;
|
|
|
|
prevFocusedElement = document.activeElement;
|
|
|
|
userActivity = 0;
|
2017-11-21 15:39:13 +00:00
|
|
|
lastOutputColor = opt.color || '';
|
2017-11-15 12:59:24 +00:00
|
|
|
$formatChangeButton.title = opt.tooltipForSwitcher || '';
|
2020-10-26 15:03:41 +00:00
|
|
|
maxHeight = `${opt.maxHeight || 300}px`;
|
2017-11-15 12:59:24 +00:00
|
|
|
|
2020-10-31 20:45:37 +00:00
|
|
|
$root.className = [...$root.classList]
|
|
|
|
.filter(c => !c.startsWith(`${CSS_PREFIX}theme-`))
|
|
|
|
.concat(`${CSS_PREFIX}theme-${['dark', 'light'].includes(opt.theme) ? opt.theme : guessTheme()}`)
|
|
|
|
.join(' ');
|
2017-12-22 06:31:28 +00:00
|
|
|
|
2017-12-02 14:17:43 +00:00
|
|
|
document.body.appendChild($root);
|
2017-12-22 06:31:28 +00:00
|
|
|
shown = true;
|
|
|
|
|
|
|
|
registerEvents();
|
|
|
|
setFromColor(opt.color);
|
|
|
|
setFromHexLettercaseElement();
|
2020-10-18 13:40:11 +00:00
|
|
|
if (Array.isArray(options.palette)) {
|
2020-10-31 20:45:37 +00:00
|
|
|
renderPalette();
|
2020-10-26 15:03:41 +00:00
|
|
|
}
|
|
|
|
if (!isNaN(options.left) && !isNaN(options.top)) {
|
|
|
|
reposition();
|
2020-10-18 13:40:11 +00:00
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2018-09-06 16:33:10 +00:00
|
|
|
function hide() {
|
2017-11-15 12:59:24 +00:00
|
|
|
if (shown) {
|
2018-09-06 16:33:10 +00:00
|
|
|
colorpickerCallback('');
|
2017-11-15 12:59:24 +00:00
|
|
|
unregisterEvents();
|
|
|
|
focusNoScroll(prevFocusedElement);
|
|
|
|
$root.remove();
|
|
|
|
shown = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setColor(color) {
|
|
|
|
switch (typeof color) {
|
|
|
|
case 'string':
|
2018-01-07 08:20:55 +00:00
|
|
|
color = colorConverter.parse(color);
|
2017-11-15 12:59:24 +00:00
|
|
|
break;
|
|
|
|
case 'object': {
|
|
|
|
const {r, g, b, a} = color;
|
|
|
|
if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
|
|
|
|
color = {r, g, b, a, type: 'rgb'};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const {h, s, l} = color;
|
|
|
|
if (!isNaN(h) && !isNaN(s) && !isNaN(l)) {
|
|
|
|
color = {h, s, l, a, type: 'hsl'};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// fallthrough
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (color) {
|
|
|
|
if (!initialized) {
|
2017-11-14 13:06:29 +00:00
|
|
|
init();
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
setFromColor(color);
|
|
|
|
}
|
|
|
|
return Boolean(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getColor(type) {
|
|
|
|
if (!initialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
readCurrentColorFromRamps();
|
2018-01-07 08:20:55 +00:00
|
|
|
const color = type === 'hsl' ?
|
|
|
|
colorConverter.HSVtoHSL(HSV) :
|
|
|
|
colorConverter.HSVtoRGB(HSV);
|
2017-11-15 12:59:24 +00:00
|
|
|
return type ? colorToString(color, type) : color;
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region DOM-to-state
|
|
|
|
|
|
|
|
function readCurrentColorFromRamps() {
|
|
|
|
if ($sat.offsetWidth === 0) {
|
|
|
|
HSV.h = HSV.s = HSV.v = 0;
|
|
|
|
} else {
|
|
|
|
const {x, y} = dragging.saturationPointerPos;
|
2018-01-07 08:20:55 +00:00
|
|
|
HSV.h = colorConverter.snapToInt((dragging.hueKnobPos / $hue.offsetWidth) * 360);
|
2017-11-15 12:59:24 +00:00
|
|
|
HSV.s = x / $sat.offsetWidth;
|
|
|
|
HSV.v = ($sat.offsetHeight - y) / $sat.offsetHeight;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFromSaturationElement(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
const w = $sat.offsetWidth;
|
|
|
|
const h = $sat.offsetHeight;
|
2020-10-26 15:03:41 +00:00
|
|
|
const bb = $root.getBoundingClientRect();
|
|
|
|
const deltaX = event.clientX - bb.left;
|
|
|
|
const deltaY = event.clientY - bb.top;
|
2017-11-15 12:59:24 +00:00
|
|
|
const x = dragging.saturationPointerPos.x = constrain(0, w, deltaX);
|
|
|
|
const y = dragging.saturationPointerPos.y = constrain(0, h, deltaY);
|
|
|
|
|
|
|
|
$satPointer.style.left = `${x - 5}px`;
|
|
|
|
$satPointer.style.top = `${y - 5}px`;
|
|
|
|
|
|
|
|
readCurrentColorFromRamps();
|
|
|
|
renderInputs();
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFromHueElement(event) {
|
|
|
|
const {left, width} = getScreenBounds($hue);
|
2017-11-27 07:49:42 +00:00
|
|
|
const currentX = event ? getTouchPosition(event).clientX :
|
2018-01-07 08:20:55 +00:00
|
|
|
left + width * colorConverter.constrainHue(HSV.h) / 360;
|
2017-11-15 12:59:24 +00:00
|
|
|
const normalizedH = constrain(0, 1, (currentX - left) / width);
|
|
|
|
const x = dragging.hueKnobPos = width * normalizedH;
|
|
|
|
$hueKnob.style.left = (x - Math.round($hueKnob.offsetWidth / 2)) + 'px';
|
|
|
|
$sat.style.backgroundColor = hueDistanceToColorString(normalizedH);
|
|
|
|
HSV.h = event ? Math.round(normalizedH * 360) : HSV.h;
|
|
|
|
renderInputs();
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFromOpacityElement(event) {
|
|
|
|
const {left, width} = getScreenBounds($opacity);
|
|
|
|
const normalized = constrain(0, 1, (getTouchPosition(event).clientX - left) / width);
|
|
|
|
const x = width * normalized;
|
|
|
|
$opacityKnob.style.left = (x - Math.ceil($opacityKnob.offsetWidth / 2)) + 'px';
|
|
|
|
HSV.a = Math.round(normalized * 100) / 100;
|
|
|
|
renderInputs();
|
|
|
|
}
|
|
|
|
|
2017-11-21 22:15:52 +00:00
|
|
|
function setFromFormatElement({shiftKey}) {
|
2017-11-15 12:59:24 +00:00
|
|
|
userActivity = performance.now();
|
|
|
|
HSV.a = isNaN(HSV.a) ? 1 : HSV.a;
|
2017-11-21 22:15:52 +00:00
|
|
|
const formats = ['hex', 'rgb', 'hsl'];
|
|
|
|
const dir = shiftKey ? -1 : 1;
|
|
|
|
const total = formats.length;
|
2018-04-17 19:35:23 +00:00
|
|
|
if ($inputs.colorString === $inputs.prevColorString) {
|
|
|
|
Object.assign(HSV, prevHSV);
|
|
|
|
}
|
2017-11-21 22:15:52 +00:00
|
|
|
switchInputGroup(formats[(formats.indexOf(currentFormat) + dir + total) % total]);
|
2017-11-15 12:59:24 +00:00
|
|
|
renderInputs();
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFromHexLettercaseElement() {
|
|
|
|
const isUpper = Boolean(options.hexUppercase);
|
|
|
|
$hexLettercase[isUpper].dataset.active = '';
|
|
|
|
delete $hexLettercase[!isUpper].dataset.active;
|
|
|
|
const value = $hexCode.value;
|
|
|
|
$hexCode.value = isUpper ? value.toUpperCase() : value.toLowerCase();
|
|
|
|
setFromInputs();
|
|
|
|
}
|
|
|
|
|
2017-11-22 12:20:10 +00:00
|
|
|
function setFromInputs(event) {
|
|
|
|
userActivity = event ? performance.now() : userActivity;
|
2017-11-15 12:59:24 +00:00
|
|
|
if ($inputs[currentFormat].every(validateInput)) {
|
|
|
|
setFromColor($inputs.color);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-21 22:15:52 +00:00
|
|
|
function setFromKeyboard(event) {
|
2020-10-13 18:14:54 +00:00
|
|
|
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: meta} = event;
|
|
|
|
switch (key) {
|
|
|
|
case 'Tab':
|
|
|
|
case 'PageUp':
|
|
|
|
case 'PageDown':
|
2017-11-21 22:15:52 +00:00
|
|
|
if (!ctrl && !alt && !meta) {
|
|
|
|
const el = document.activeElement;
|
|
|
|
const inputs = $inputs[currentFormat];
|
2017-11-22 01:32:20 +00:00
|
|
|
const lastInput = inputs[inputs.length - 1];
|
2020-10-13 18:14:54 +00:00
|
|
|
if (key === 'Tab' && shift && el === inputs[0]) {
|
2017-11-25 13:46:57 +00:00
|
|
|
maybeFocus(lastInput);
|
2020-10-13 18:14:54 +00:00
|
|
|
} else if (key === 'Tab' && !shift && el === lastInput) {
|
2017-11-25 13:46:57 +00:00
|
|
|
maybeFocus(inputs[0]);
|
2020-10-13 18:14:54 +00:00
|
|
|
} else if (key !== 'Tab' && !shift) {
|
|
|
|
setFromFormatElement({shift: key === 'PageUp' || shift});
|
2017-11-22 01:32:20 +00:00
|
|
|
} else {
|
|
|
|
return;
|
2017-11-21 22:15:52 +00:00
|
|
|
}
|
2017-11-22 01:32:20 +00:00
|
|
|
event.preventDefault();
|
2017-11-21 22:15:52 +00:00
|
|
|
}
|
|
|
|
return;
|
2020-10-13 18:14:54 +00:00
|
|
|
case 'ArrowUp':
|
|
|
|
case 'ArrowDown':
|
2017-11-21 22:15:52 +00:00
|
|
|
if (!event.metaKey &&
|
|
|
|
document.activeElement.localName === 'input' &&
|
|
|
|
document.activeElement.checkValidity()) {
|
|
|
|
setFromKeyboardIncrement(event);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setFromKeyboardIncrement(event) {
|
|
|
|
const el = document.activeElement;
|
2020-10-13 18:14:54 +00:00
|
|
|
const {key, ctrlKey: ctrl, altKey: alt, shiftKey: shift} = event;
|
|
|
|
const dir = key === 'ArrowUp' ? 1 : -1;
|
2017-11-21 22:15:52 +00:00
|
|
|
let value, newValue;
|
|
|
|
if (currentFormat === 'hex') {
|
|
|
|
value = el.value.trim();
|
|
|
|
const isShort = value.length <= 5;
|
2017-11-23 16:28:37 +00:00
|
|
|
const [r, g, b, a = ''] = el.value.match(isShort ? /[\da-f]/gi : /[\da-f]{2}/gi);
|
2017-11-21 22:15:52 +00:00
|
|
|
let ceiling, data;
|
|
|
|
if (!ctrl && !shift && !alt) {
|
|
|
|
ceiling = isShort ? 0xFFF : 0xFFFFFF;
|
|
|
|
data = [[true, r + g + b]];
|
|
|
|
} else {
|
|
|
|
ceiling = isShort ? 15 : 255;
|
|
|
|
data = [[ctrl, r], [shift, g], [alt, b]];
|
|
|
|
}
|
|
|
|
newValue = '#' + data.map(([affected, part]) => {
|
|
|
|
part = constrain(0, ceiling, parseInt(part, 16) + dir * (affected ? 1 : 0));
|
|
|
|
return (part + ceiling + 1).toString(16).slice(1);
|
|
|
|
}).join('') + a;
|
|
|
|
newValue = options.hexUppercase ? newValue.toUpperCase() : newValue.toLowerCase();
|
|
|
|
} else if (!alt) {
|
|
|
|
value = parseFloat(el.value);
|
|
|
|
const isHue = el === $inputs.hsl[0];
|
|
|
|
const isAlpha = el === $inputs[currentFormat][3];
|
2017-11-21 23:26:49 +00:00
|
|
|
const isRGB = currentFormat === 'rgb';
|
2017-11-21 22:15:52 +00:00
|
|
|
const min = isHue ? -360 : 0;
|
2017-11-21 23:26:49 +00:00
|
|
|
const max = isHue ? 360 : isAlpha ? 1 : isRGB ? 255 : 100;
|
2017-11-21 22:15:52 +00:00
|
|
|
const scale = isAlpha ? .01 : 1;
|
2017-11-21 23:26:49 +00:00
|
|
|
const delta =
|
|
|
|
shift && !ctrl ? 10 :
|
|
|
|
ctrl && !shift ? (isHue || isRGB ? 100 : 50) :
|
|
|
|
1;
|
2017-11-21 22:15:52 +00:00
|
|
|
newValue = constrain(min, max, value + delta * scale * dir);
|
|
|
|
newValue = isAlpha ? alphaToString(newValue) : newValue;
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
userActivity = performance.now();
|
|
|
|
if (newValue !== undefined && newValue !== value) {
|
|
|
|
el.value = newValue;
|
|
|
|
setFromColor($inputs.color);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
function validateInput(el) {
|
2017-11-21 15:39:13 +00:00
|
|
|
const isAlpha = el === $inputs[currentFormat][3];
|
2017-11-15 12:59:24 +00:00
|
|
|
let isValid = (isAlpha || el.value.trim()) && el.checkValidity();
|
|
|
|
if (!isAlpha && !isValid && currentFormat === 'rgb') {
|
|
|
|
isValid = parseAs(el, parseInt);
|
|
|
|
} else if (isAlpha && !isValid) {
|
|
|
|
isValid = parseAs(el, parseFloat);
|
|
|
|
}
|
|
|
|
if (isAlpha && isValid) {
|
|
|
|
isValid = lastOutputColor !== colorToString($inputs.color);
|
|
|
|
}
|
|
|
|
return isValid;
|
|
|
|
}
|
|
|
|
//endregion
|
|
|
|
//region State-to-DOM
|
|
|
|
|
2017-11-21 15:39:13 +00:00
|
|
|
function setFromColor(color) {
|
2018-01-07 08:20:55 +00:00
|
|
|
color = typeof color === 'string' ? colorConverter.parse(color) : color;
|
|
|
|
color = color || colorConverter.parse('#f00');
|
|
|
|
const newHSV = color.type === 'hsl' ?
|
|
|
|
colorConverter.HSLtoHSV(color) :
|
|
|
|
colorConverter.RGBtoHSV(color);
|
2020-10-18 13:40:11 +00:00
|
|
|
if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) {
|
2017-11-15 12:59:24 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
HSV = newHSV;
|
|
|
|
renderKnobs(color);
|
|
|
|
switchInputGroup(color.type);
|
|
|
|
setFromHueElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
function switchInputGroup(format) {
|
|
|
|
if (currentFormat === format) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (currentFormat) {
|
|
|
|
delete $inputGroups[currentFormat].dataset.active;
|
|
|
|
} else {
|
|
|
|
for (const format in $inputGroups) {
|
|
|
|
delete $inputGroups[format].dataset.active;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$inputGroups[format].dataset.active = '';
|
2017-11-25 13:46:57 +00:00
|
|
|
maybeFocus($inputs[format][0]);
|
2017-11-15 12:59:24 +00:00
|
|
|
currentFormat = format;
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderKnobs(color) {
|
|
|
|
const x = $sat.offsetWidth * HSV.s;
|
|
|
|
const y = $sat.offsetHeight * (1 - HSV.v);
|
|
|
|
$satPointer.style.left = (x - 5) + 'px';
|
|
|
|
$satPointer.style.top = (y - 5) + 'px';
|
|
|
|
dragging.saturationPointerPos = {x, y};
|
|
|
|
|
2017-11-27 07:49:42 +00:00
|
|
|
const hueX = $hue.offsetWidth * constrain(0, 1, HSV.h / 360);
|
2017-11-15 12:59:24 +00:00
|
|
|
$hueKnob.style.left = (hueX - 7.5) + 'px';
|
|
|
|
dragging.hueKnobPos = hueX;
|
|
|
|
|
|
|
|
const opacityX = $opacity.offsetWidth * (isNaN(HSV.a) ? 1 : HSV.a);
|
|
|
|
$opacityKnob.style.left = (opacityX - 7.5) + 'px';
|
|
|
|
|
|
|
|
$sat.style.backgroundColor = color;
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderInputs() {
|
2018-01-07 08:20:55 +00:00
|
|
|
const rgb = colorConverter.HSVtoRGB(HSV);
|
2017-11-15 12:59:24 +00:00
|
|
|
switch (currentFormat) {
|
|
|
|
case 'hex':
|
2018-01-07 17:00:22 +00:00
|
|
|
rgb.a = HSV.a;
|
2017-11-15 12:59:24 +00:00
|
|
|
$hexCode.value = colorToString(rgb, 'hex');
|
|
|
|
break;
|
|
|
|
case 'rgb': {
|
|
|
|
$rgb.r.value = rgb.r;
|
|
|
|
$rgb.g.value = rgb.g;
|
|
|
|
$rgb.b.value = rgb.b;
|
|
|
|
$rgb.a.value = alphaToString() || 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'hsl': {
|
2018-01-07 08:20:55 +00:00
|
|
|
const {h, s, l} = colorConverter.HSVtoHSL(HSV);
|
2017-11-15 12:59:24 +00:00
|
|
|
$hsl.h.value = h;
|
|
|
|
$hsl.s.value = s;
|
|
|
|
$hsl.l.value = l;
|
|
|
|
$hsl.a.value = alphaToString() || 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$swatch.style.backgroundColor = colorToString(rgb, 'rgb');
|
|
|
|
$opacityBar.style.background = 'linear-gradient(to right,' +
|
|
|
|
colorToString(Object.assign(rgb, {a: 0}), 'rgb') + ',' +
|
|
|
|
colorToString(Object.assign(rgb, {a: 1}), 'rgb') + ')';
|
2018-04-17 19:35:23 +00:00
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
colorpickerCallback();
|
2018-04-17 19:35:23 +00:00
|
|
|
|
|
|
|
const colorString = $inputs.colorString;
|
|
|
|
if ($inputs.prevColorString === colorString) {
|
|
|
|
// keep the internal HSV calculated initially for this color format
|
|
|
|
Object.assign(HSV, prevHSV);
|
|
|
|
} else {
|
|
|
|
// remember the internal HSV
|
|
|
|
$inputs.prevColorString = colorString;
|
|
|
|
Object.assign(prevHSV, HSV);
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region Event listeners
|
|
|
|
|
2020-10-26 15:03:41 +00:00
|
|
|
/** @param {MouseEvent} event */
|
|
|
|
function onPopupMoveStart(event) {
|
|
|
|
if (!event.button && !hasModifiers(event)) {
|
|
|
|
captureMouse(event, 'popup');
|
|
|
|
$root.dataset.moving = '';
|
|
|
|
const [x, y] = ($root.style.transform.match(/[-.\d]+/g) || []).map(parseFloat);
|
|
|
|
dragging.popupX = event.clientX - (x || 0);
|
|
|
|
dragging.popupY = event.clientY - (y || 0);
|
|
|
|
document.addEventListener('mouseup', onPopupMoveEnd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param {MouseEvent} event */
|
|
|
|
function onPopupMove({clientX: x, clientY: y}) {
|
|
|
|
$root.style.transform = `translate(${x - dragging.popupX}px, ${y - dragging.popupY}px)`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param {MouseEvent} event */
|
|
|
|
function onPopupMoveEnd(event) {
|
|
|
|
if (!event.button) {
|
|
|
|
document.addEventListener('mouseup', onPopupMoveEnd);
|
|
|
|
delete $root.dataset.moving;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param {MouseEvent} event */
|
|
|
|
function onPopupResizeStart(event) {
|
|
|
|
if (event.target === $root && !event.button && !hasModifiers(event)) {
|
|
|
|
document.addEventListener('mouseup', onPopupResizeEnd);
|
|
|
|
$root.dataset.resizing = '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param {MouseEvent} event */
|
|
|
|
function onPopupResizeEnd(event) {
|
|
|
|
if (!event.button) {
|
|
|
|
delete $root.dataset.resizing;
|
|
|
|
document.removeEventListener('mouseup', onPopupResizeEnd);
|
|
|
|
if (maxHeight !== $root.style.height) {
|
|
|
|
maxHeight = $root.style.height;
|
|
|
|
PUBLIC_API.options.maxHeight = parseFloat(maxHeight);
|
|
|
|
fitPaletteHeight();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
function onHexLettercaseClicked() {
|
|
|
|
options.hexUppercase = !options.hexUppercase;
|
|
|
|
setFromHexLettercaseElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
function onSaturationMouseDown(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
if (captureMouse(event, 'saturation')) {
|
2017-11-15 12:59:24 +00:00
|
|
|
setFromSaturationElement(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-21 15:39:13 +00:00
|
|
|
function onSaturationMouseUp(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
releaseMouse(event, 'saturation');
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function onHueKnobMouseDown(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
captureMouse(event, 'hue');
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2017-11-21 15:39:13 +00:00
|
|
|
function onOpacityKnobMouseDown(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
captureMouse(event, 'opacity');
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function onHueMouseDown(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
if (captureMouse(event, 'hue')) {
|
2017-11-15 12:59:24 +00:00
|
|
|
setFromHueElement(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onOpacityMouseDown(event) {
|
2017-11-27 10:04:00 +00:00
|
|
|
if (captureMouse(event, 'opacity')) {
|
2017-11-15 12:59:24 +00:00
|
|
|
setFromOpacityElement(event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 13:40:11 +00:00
|
|
|
/** @param {MouseEvent} e */
|
|
|
|
function onPaletteClicked(e) {
|
2020-10-26 15:03:41 +00:00
|
|
|
if (e.target !== e.currentTarget && e.target.__color) {
|
2020-10-18 13:40:11 +00:00
|
|
|
if (!e.button && setColor(e.target.__color)) {
|
|
|
|
userActivity = performance.now();
|
|
|
|
colorpickerCallback();
|
2020-10-26 15:03:41 +00:00
|
|
|
} else if (e.button && options.paletteCallback) {
|
|
|
|
e.preventDefault(); // suppress the default context menu
|
2020-10-18 13:40:11 +00:00
|
|
|
options.paletteCallback(e.target);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
function onMouseUp(event) {
|
2020-10-26 15:03:41 +00:00
|
|
|
releaseMouse(event, ['saturation', 'hue', 'opacity', 'popup']);
|
2017-12-21 13:04:12 +00:00
|
|
|
if (onMouseDown.outsideClick) {
|
|
|
|
if (!prevFocusedElement) hide();
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2017-12-05 21:24:27 +00:00
|
|
|
function onMouseDown(event) {
|
2017-12-21 13:04:12 +00:00
|
|
|
onMouseDown.outsideClick = !event.button && !event.target.closest('.colorpicker-popup');
|
|
|
|
if (onMouseDown.outsideClick) {
|
|
|
|
prevFocusedElement = null;
|
|
|
|
captureMouse(event);
|
2017-12-05 21:24:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
function onMouseMove(event) {
|
2020-10-26 15:03:41 +00:00
|
|
|
if (event.button) return;
|
|
|
|
if (dragging.saturation) setFromSaturationElement(event);
|
|
|
|
if (dragging.hue) setFromHueElement(event);
|
|
|
|
if (dragging.opacity) setFromOpacityElement(event);
|
|
|
|
if (dragging.popup) onPopupMove(event);
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function onKeyDown(e) {
|
2020-10-26 15:03:41 +00:00
|
|
|
if (!hasModifiers(e)) {
|
2020-10-13 18:14:54 +00:00
|
|
|
switch (e.key) {
|
|
|
|
case 'Enter':
|
|
|
|
case 'Escape':
|
2017-11-15 12:59:24 +00:00
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
2018-09-06 16:33:10 +00:00
|
|
|
hide();
|
2017-11-15 12:59:24 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onCloseRequest(event) {
|
|
|
|
if (event.detail !== PUBLIC_API) {
|
|
|
|
hide();
|
2021-01-01 14:27:58 +00:00
|
|
|
} else if (!prevFocusedElement && cm) {
|
2017-12-21 13:04:12 +00:00
|
|
|
// we're between mousedown and mouseup and colorview wants to re-open us in this cm
|
|
|
|
// so we'll prevent onMouseUp from hiding us to avoid flicker
|
|
|
|
prevFocusedElement = cm.display.input;
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region Event utilities
|
|
|
|
|
|
|
|
function colorpickerCallback(colorString = currentColorToString()) {
|
2018-09-06 16:33:10 +00:00
|
|
|
const isCallable = typeof options.callback === 'function';
|
|
|
|
// hiding
|
|
|
|
if (!colorString && isCallable) {
|
2017-11-27 06:56:22 +00:00
|
|
|
options.callback('');
|
2018-09-06 16:33:10 +00:00
|
|
|
return;
|
2017-11-27 06:56:22 +00:00
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
if (
|
|
|
|
userActivity &&
|
2018-09-06 16:33:10 +00:00
|
|
|
$inputs[currentFormat].every(el => el.checkValidity())
|
2017-11-15 12:59:24 +00:00
|
|
|
) {
|
|
|
|
lastOutputColor = colorString.replace(/\b0\./g, '.');
|
2018-09-06 16:33:10 +00:00
|
|
|
if (isCallable) {
|
|
|
|
options.callback(lastOutputColor);
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-27 10:04:00 +00:00
|
|
|
function captureMouse({button}, mode) {
|
|
|
|
if (button !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
|
|
document.addEventListener('mousemove', onMouseMove);
|
2017-11-27 10:04:00 +00:00
|
|
|
if (!mode) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-26 15:03:41 +00:00
|
|
|
for (const m of toArray(mode)) {
|
2017-11-27 10:04:00 +00:00
|
|
|
dragging[m] = true;
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
userActivity = performance.now();
|
2017-11-27 10:04:00 +00:00
|
|
|
return true;
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 15:03:41 +00:00
|
|
|
function hasModifiers(e) {
|
|
|
|
return e.shiftKey || e.ctrlKey || e.altKey || e.metaKey;
|
|
|
|
}
|
|
|
|
|
2017-11-27 10:04:00 +00:00
|
|
|
function releaseMouse(event, mode) {
|
|
|
|
if (event && event.button !== 0) {
|
|
|
|
return;
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
|
|
document.removeEventListener('mousemove', onMouseMove);
|
2017-11-27 10:04:00 +00:00
|
|
|
if (!mode) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-26 15:03:41 +00:00
|
|
|
for (const m of toArray(mode)) {
|
2017-11-27 10:04:00 +00:00
|
|
|
dragging[m] = false;
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
userActivity = performance.now();
|
2017-11-27 10:04:00 +00:00
|
|
|
return true;
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getTouchPosition(event) {
|
|
|
|
return event.touches && event.touches[0] || event;
|
|
|
|
}
|
|
|
|
|
|
|
|
function registerEvents() {
|
|
|
|
window.addEventListener('keydown', onKeyDown, true);
|
2017-12-05 21:14:21 +00:00
|
|
|
window.addEventListener('mousedown', onMouseDown, true);
|
2017-11-15 12:59:24 +00:00
|
|
|
window.addEventListener('close-colorpicker-popup', onCloseRequest, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
function unregisterEvents() {
|
|
|
|
window.removeEventListener('keydown', onKeyDown, true);
|
2017-12-05 21:14:21 +00:00
|
|
|
window.removeEventListener('mousedown', onMouseDown, true);
|
2017-12-21 13:04:12 +00:00
|
|
|
window.removeEventListener('close-colorpicker-popup', onCloseRequest, true);
|
2017-11-15 12:59:24 +00:00
|
|
|
releaseMouse();
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region Color conversion utilities
|
|
|
|
|
2017-12-21 13:04:12 +00:00
|
|
|
function colorToString(color, type = currentFormat) {
|
2018-01-07 08:20:55 +00:00
|
|
|
return colorConverter.format(color, type, options.hexUppercase);
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2018-01-07 08:20:55 +00:00
|
|
|
function alphaToString(a = HSV.a) {
|
|
|
|
return colorConverter.formatAlpha(a);
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function currentColorToString(format = currentFormat, alpha = HSV.a) {
|
2018-01-07 08:20:55 +00:00
|
|
|
const converted = format === 'hsl' ?
|
|
|
|
colorConverter.HSVtoHSL(HSV) :
|
|
|
|
colorConverter.HSVtoRGB(HSV);
|
2017-11-15 12:59:24 +00:00
|
|
|
converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha;
|
|
|
|
return colorToString(converted, format);
|
|
|
|
}
|
|
|
|
|
|
|
|
function mixColorToString(start, end, amount) {
|
|
|
|
const obj = {
|
|
|
|
r: start.r + (end.r - start.r) * amount,
|
|
|
|
g: start.g + (end.g - start.g) * amount,
|
|
|
|
b: start.b + (end.b - start.b) * amount,
|
|
|
|
a: 1,
|
|
|
|
};
|
|
|
|
return colorToString(obj, 'hex');
|
|
|
|
}
|
|
|
|
|
|
|
|
function hueDistanceToColorString(hueRatio) {
|
|
|
|
let prevColor;
|
|
|
|
for (const color of HUE_COLORS) {
|
|
|
|
if (prevColor && color.start >= hueRatio) {
|
|
|
|
return mixColorToString(prevColor, color,
|
|
|
|
(hueRatio - prevColor.start) / (color.start - prevColor.start));
|
|
|
|
}
|
|
|
|
prevColor = color;
|
|
|
|
}
|
|
|
|
return HUE_COLORS[0].hex;
|
|
|
|
}
|
|
|
|
|
|
|
|
//endregion
|
|
|
|
//region Miscellaneous utilities
|
|
|
|
|
|
|
|
function reposition() {
|
|
|
|
const width = $root.offsetWidth;
|
|
|
|
const height = $root.offsetHeight;
|
2017-12-22 06:31:28 +00:00
|
|
|
const maxTop = window.innerHeight - height;
|
|
|
|
const maxTopUnobscured = options.top <= maxTop ? maxTop : options.top - height - 20;
|
|
|
|
const maxRight = window.innerWidth - width;
|
|
|
|
const maxRightUnobscured = options.left <= maxRight ? maxRight : options.left - width;
|
2018-08-16 16:58:05 +00:00
|
|
|
const left = constrain(0, Math.max(0, maxRightUnobscured), options.left);
|
|
|
|
const top = constrain(0, Math.max(0, maxTopUnobscured), options.top);
|
2020-10-26 15:03:41 +00:00
|
|
|
$root.style.left = left + 'px';
|
|
|
|
$root.style.top = top + 'px';
|
2020-10-31 20:45:37 +00:00
|
|
|
$root.style.transform = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderPalette() {
|
|
|
|
// Might need to clear a lot of elements so this is known to be faster than textContent = ''
|
|
|
|
while ($palette.firstChild) $palette.firstChild.remove();
|
|
|
|
$palette.append(...options.palette);
|
|
|
|
if (options.palette.length) {
|
|
|
|
$root.dataset.resizable = '';
|
|
|
|
$root.addEventListener('mousedown', onPopupResizeStart);
|
|
|
|
fitPaletteHeight();
|
|
|
|
} else {
|
|
|
|
delete $root.dataset.resizable;
|
|
|
|
$root.removeEventListener('mousedown', onPopupResizeStart);
|
|
|
|
}
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 15:03:41 +00:00
|
|
|
function fitPaletteHeight() {
|
|
|
|
const fit = MIN_HEIGHT + $palette.scrollHeight + MARGIN;
|
|
|
|
$root.style.setProperty('--fit-height', Math.min(fit, parseFloat(maxHeight)) + 'px');
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
2017-11-25 13:46:57 +00:00
|
|
|
function maybeFocus(el) {
|
|
|
|
if (allowInputFocus) {
|
|
|
|
el.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
function focusNoScroll(el) {
|
|
|
|
if (el) {
|
|
|
|
const {scrollY: y, scrollX: x} = window;
|
|
|
|
el.focus({preventScroll: true});
|
|
|
|
el = null;
|
|
|
|
if (window.scrollY !== y || window.scrollX !== x) {
|
2017-12-08 19:06:17 +00:00
|
|
|
setTimeout(window.scrollTo, 0, x, y);
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getScreenBounds(el) {
|
|
|
|
const bounds = el.getBoundingClientRect();
|
|
|
|
const {scrollTop, scrollLeft} = document.scrollingElement;
|
|
|
|
return {
|
|
|
|
top: bounds.top + scrollTop,
|
|
|
|
left: bounds.left + scrollLeft,
|
|
|
|
width: bounds.width,
|
|
|
|
height: bounds.height,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function guessTheme() {
|
2017-12-17 19:07:37 +00:00
|
|
|
const el = options.guessBrightness ||
|
2021-01-01 14:27:58 +00:00
|
|
|
cm && ((cm.display.renderedView || [])[0] || {}).text ||
|
|
|
|
cm && cm.display.lineDiv;
|
|
|
|
const bgLuma = colorMimicry(el, {bg: 'backgroundColor'}).bgLuma;
|
2017-12-17 19:07:37 +00:00
|
|
|
return bgLuma < .5 ? 'dark' : 'light';
|
2017-11-15 12:59:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function constrain(min, max, value) {
|
|
|
|
return value < min ? min : value > max ? max : value;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseAs(el, parser) {
|
|
|
|
const num = parser(el.value);
|
2017-11-27 07:49:42 +00:00
|
|
|
if (!isNaN(num) &&
|
|
|
|
(!el.min || num >= parseFloat(el.min)) &&
|
|
|
|
(!el.max || num <= parseFloat(el.max))) {
|
2017-11-15 12:59:24 +00:00
|
|
|
el.value = num;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-26 15:03:41 +00:00
|
|
|
function toArray(val) {
|
|
|
|
return !val ? [] : Array.isArray(val) ? val : [val];
|
|
|
|
}
|
|
|
|
|
2017-11-15 12:59:24 +00:00
|
|
|
//endregion
|
2017-11-27 06:56:22 +00:00
|
|
|
};
|