2021-03-06 17:01:39 +00:00
|
|
|
import { LitElement, customElement, html, internalProperty, property, query, unsafeCSS } from 'lit-element';
|
|
|
|
import { classMap } from 'lit-html/directives/class-map';
|
|
|
|
import { styleMap } from 'lit-html/directives/style-map';
|
|
|
|
import { event, EventEmitter } from '../../internal/event';
|
2021-02-26 14:09:13 +00:00
|
|
|
import styles from 'sass:./color-picker.scss';
|
|
|
|
import { SlDropdown, SlInput } from '../../shoelace';
|
2020-07-15 21:30:37 +00:00
|
|
|
import color from 'color';
|
2021-02-26 14:09:13 +00:00
|
|
|
import { clamp } from '../../internal/math';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/**
|
2020-07-17 10:09:10 +00:00
|
|
|
* @since 2.0
|
2020-07-15 21:30:37 +00:00
|
|
|
* @status stable
|
|
|
|
*
|
2021-02-26 14:09:13 +00:00
|
|
|
* @dependency sl-button
|
|
|
|
* @dependency sl-dropdown
|
|
|
|
* @dependency sl-icon
|
|
|
|
* @dependency sl-input
|
|
|
|
*
|
2020-07-15 21:30:37 +00:00
|
|
|
* @part base - The component's base wrapper.
|
2020-08-17 12:23:06 +00:00
|
|
|
* @part trigger - The color picker's dropdown trigger.
|
2020-07-15 21:30:37 +00:00
|
|
|
* @part swatches - The container that holds swatches.
|
|
|
|
* @part swatch - Each individual swatch.
|
|
|
|
* @part grid - The color grid.
|
|
|
|
* @part grid-handle - The color grid's handle.
|
2020-08-17 12:23:06 +00:00
|
|
|
* @part hue-slider - The hue slider.
|
|
|
|
* @part opacity-slider - The opacity slider.
|
2020-07-15 21:30:37 +00:00
|
|
|
* @part slider - Hue and opacity sliders.
|
|
|
|
* @part slider-handle - Hue and opacity slider handles.
|
|
|
|
* @part preview - The preview color.
|
|
|
|
* @part input - The text input.
|
2020-12-14 21:40:39 +00:00
|
|
|
* @part format-button - The toggle format button's base.
|
2020-07-15 21:30:37 +00:00
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@customElement('sl-color-picker')
|
|
|
|
export class SlColorPicker extends LitElement {
|
|
|
|
static styles = unsafeCSS(styles);
|
|
|
|
|
|
|
|
@query('[part="input"]') input: SlInput;
|
|
|
|
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
|
|
|
@query('.color-dropdown') dropdown: SlDropdown;
|
2021-02-26 14:09:13 +00:00
|
|
|
|
|
|
|
private bypassValueParse = false;
|
|
|
|
private lastValueEmitted: string;
|
2021-03-06 17:01:39 +00:00
|
|
|
|
|
|
|
@internalProperty() private inputValue = '';
|
|
|
|
@internalProperty() private hue = 0;
|
|
|
|
@internalProperty() private saturation = 100;
|
|
|
|
@internalProperty() private lightness = 100;
|
|
|
|
@internalProperty() private alpha = 100;
|
|
|
|
@internalProperty() private showCopyFeedback = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/** The current color. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() value = '#ffffff';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The format to use for the display value. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA
|
|
|
|
* respectively. The color picker will always accept user input in any format (including CSS color names) and convert
|
|
|
|
* it to the desired format.
|
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() format: 'hex' | 'rgb' | 'hsl' = 'hex';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
/** Renders the color picker inline rather than inside a dropdown. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) inline = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2020-10-09 21:45:16 +00:00
|
|
|
/** Determines the size of the color picker's trigger. This has no effect on inline color pickers. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() size: 'small' | 'medium' | 'large' = 'medium';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2020-12-11 22:10:30 +00:00
|
|
|
/** Removes the format toggle. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ attribute: 'no-format-toggle', type: Boolean }) noFormatToggle = false;
|
2020-12-11 22:10:30 +00:00
|
|
|
|
2020-09-04 12:57:34 +00:00
|
|
|
/** The input's name attribute. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() name = '';
|
2020-09-04 12:57:34 +00:00
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
/** Disables the color picker. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) disabled = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2020-09-04 14:18:46 +00:00
|
|
|
/**
|
|
|
|
* This will be true when the control is in an invalid state. Validity is determined by the `setCustomValidity()`
|
|
|
|
* method using the browser's constraint validation API.
|
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) invalid = false;
|
2020-09-04 14:18:46 +00:00
|
|
|
|
2020-08-25 21:07:28 +00:00
|
|
|
/**
|
|
|
|
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
|
|
|
|
* `overflow: auto|scroll`.
|
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) hoist = false;
|
2020-08-25 21:07:28 +00:00
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
/** Whether to show the opacity slider. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) opacity = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/** By default, the value will be set in lowercase. Set this to true to set it in uppercase instead. */
|
2021-03-06 17:01:39 +00:00
|
|
|
@property({ type: Boolean }) uppercase = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An array of predefined color swatches to display. Can include any format the color picker can parse, including
|
|
|
|
* HEX(A), RGB(A), HSL(A), and CSS color names.
|
|
|
|
*/
|
2021-03-06 17:01:39 +00:00
|
|
|
@property() swatches = [
|
2020-07-15 21:30:37 +00:00
|
|
|
'#d0021b',
|
|
|
|
'#f5a623',
|
|
|
|
'#f8e71c',
|
|
|
|
'#8b572a',
|
|
|
|
'#7ed321',
|
|
|
|
'#417505',
|
|
|
|
'#bd10e0',
|
|
|
|
'#9013fe',
|
|
|
|
'#4a90e2',
|
|
|
|
'#50e3c2',
|
|
|
|
'#b8e986',
|
|
|
|
'#000',
|
|
|
|
'#444',
|
|
|
|
'#888',
|
|
|
|
'#ccc',
|
|
|
|
'#fff'
|
|
|
|
];
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
/** Emitted when the color picker's value changes. */
|
|
|
|
@event('sl-change') slChange: EventEmitter<void>;
|
|
|
|
|
|
|
|
/** Emitted when the color picker opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
|
|
|
@event('sl-show') slShow: EventEmitter<void>;
|
|
|
|
|
|
|
|
/** Emitted after the color picker opens and all transitions are complete. */
|
|
|
|
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
|
|
|
|
|
|
|
/** Emitted when the color picker closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
|
|
|
@event('sl-hide') slHide: EventEmitter<void>;
|
|
|
|
|
|
|
|
/** Emitted after the color picker closes and all transitions are complete. */
|
|
|
|
@event('sl-after-hide') slAfterHide: EventEmitter<void>;
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
super.connectedCallback();
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
if (!this.setColor(this.value)) {
|
|
|
|
this.setColor(`#ffff`);
|
|
|
|
}
|
|
|
|
|
2020-09-04 14:18:46 +00:00
|
|
|
this.inputValue = this.value;
|
2020-07-15 21:30:37 +00:00
|
|
|
this.lastValueEmitted = this.value;
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
2020-12-11 22:10:30 +00:00
|
|
|
/** Returns the current value as a string in the specified format. */
|
2021-02-26 14:09:13 +00:00
|
|
|
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' = 'hex') {
|
2020-12-11 22:10:30 +00:00
|
|
|
const currentColor = this.parseColor(
|
|
|
|
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!currentColor) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (format) {
|
|
|
|
case 'hex':
|
|
|
|
return currentColor.hex;
|
|
|
|
case 'hexa':
|
|
|
|
return currentColor.hexa;
|
|
|
|
case 'rgb':
|
|
|
|
return currentColor.rgb.string;
|
|
|
|
case 'rgba':
|
|
|
|
return currentColor.rgba.string;
|
|
|
|
case 'hsl':
|
|
|
|
return currentColor.hsl.string;
|
|
|
|
case 'hsla':
|
|
|
|
return currentColor.hsla.string;
|
|
|
|
default:
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-04 14:18:46 +00:00
|
|
|
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
2021-02-26 14:09:13 +00:00
|
|
|
reportValidity() {
|
2020-09-04 14:18:46 +00:00
|
|
|
// If the input is invalid, show the dropdown so the browser can focus on it
|
|
|
|
if (!this.inline && this.input.invalid) {
|
2021-01-04 19:11:23 +00:00
|
|
|
return new Promise<void>(resolve => {
|
2020-09-04 14:18:46 +00:00
|
|
|
this.dropdown.addEventListener(
|
2020-10-09 21:45:16 +00:00
|
|
|
'sl-after-show',
|
2020-09-04 14:18:46 +00:00
|
|
|
() => {
|
|
|
|
this.input.reportValidity();
|
2021-01-04 19:11:23 +00:00
|
|
|
resolve();
|
2020-09-04 14:18:46 +00:00
|
|
|
},
|
|
|
|
{ once: true }
|
|
|
|
);
|
|
|
|
this.dropdown.show();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return this.input.reportValidity();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
2021-02-26 14:09:13 +00:00
|
|
|
setCustomValidity(message: string) {
|
|
|
|
this.input.setCustomValidity(message);
|
2020-09-04 14:18:46 +00:00
|
|
|
this.invalid = this.input.invalid;
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
handleCopy() {
|
2021-02-26 14:09:13 +00:00
|
|
|
this.input.select();
|
|
|
|
document.execCommand('copy');
|
|
|
|
this.previewButton.focus();
|
|
|
|
this.showCopyFeedback = true;
|
|
|
|
this.previewButton.addEventListener('animationend', () => (this.showCopyFeedback = false), { once: true });
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2020-12-11 22:10:30 +00:00
|
|
|
handleFormatToggle() {
|
|
|
|
const formats = ['hex', 'rgb', 'hsl'];
|
|
|
|
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
|
|
|
|
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
handleAlphaDrag(event: any) {
|
2021-02-26 14:09:13 +00:00
|
|
|
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__alpha') as HTMLElement;
|
2020-07-15 21:30:37 +00:00
|
|
|
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
|
|
|
const { width } = container.getBoundingClientRect();
|
|
|
|
|
|
|
|
handle.focus();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
this.handleDrag(event, container, x => {
|
|
|
|
this.alpha = clamp((x / width) * 100, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleHueDrag(event: any) {
|
2021-02-26 14:09:13 +00:00
|
|
|
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__hue') as HTMLElement;
|
2020-07-15 21:30:37 +00:00
|
|
|
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
|
|
|
const { width } = container.getBoundingClientRect();
|
|
|
|
|
|
|
|
handle.focus();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
this.handleDrag(event, container, x => {
|
|
|
|
this.hue = clamp((x / width) * 360, 0, 360);
|
|
|
|
this.syncValues();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleGridDrag(event: any) {
|
2021-02-26 14:09:13 +00:00
|
|
|
const grid = this.shadowRoot!.querySelector('.color-picker__grid') as HTMLElement;
|
2020-07-15 21:30:37 +00:00
|
|
|
const handle = grid.querySelector('.color-picker__grid-handle') as HTMLElement;
|
|
|
|
const { width, height } = grid.getBoundingClientRect();
|
|
|
|
|
|
|
|
handle.focus();
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
this.handleDrag(event, grid, (x, y) => {
|
|
|
|
this.saturation = clamp((x / width) * 100, 0, 100);
|
|
|
|
this.lightness = clamp(100 - (y / height) * 100, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
handleDrag(event: any, container: HTMLElement, onMove: (x: number, y: number) => void) {
|
|
|
|
if (this.disabled) {
|
2021-02-26 14:09:13 +00:00
|
|
|
return;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const move = (event: any) => {
|
|
|
|
const dims = container.getBoundingClientRect();
|
2021-02-26 14:09:13 +00:00
|
|
|
const defaultView = container.ownerDocument.defaultView!;
|
|
|
|
const offsetX = dims.left + defaultView.pageXOffset;
|
|
|
|
const offsetY = dims.top + defaultView.pageYOffset;
|
2020-07-15 21:30:37 +00:00
|
|
|
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
|
|
|
|
const y = (event.changedTouches ? event.changedTouches[0].pageY : event.pageY) - offsetY;
|
|
|
|
|
|
|
|
onMove(x, y);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Move on init
|
|
|
|
move(event);
|
|
|
|
|
|
|
|
const stop = () => {
|
|
|
|
document.removeEventListener('mousemove', move);
|
|
|
|
document.removeEventListener('touchmove', move);
|
|
|
|
document.removeEventListener('mouseup', stop);
|
|
|
|
document.removeEventListener('touchend', stop);
|
|
|
|
};
|
|
|
|
|
|
|
|
document.addEventListener('mousemove', move);
|
|
|
|
document.addEventListener('touchmove', move);
|
|
|
|
document.addEventListener('mouseup', stop);
|
|
|
|
document.addEventListener('touchend', stop);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleAlphaKeyDown(event: KeyboardEvent) {
|
|
|
|
const increment = event.shiftKey ? 10 : 1;
|
|
|
|
|
|
|
|
if (event.key === 'ArrowLeft') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.alpha = clamp(this.alpha - increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.alpha = clamp(this.alpha + increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'Home') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.alpha = 0;
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'End') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.alpha = 100;
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleHueKeyDown(event: KeyboardEvent) {
|
|
|
|
const increment = event.shiftKey ? 10 : 1;
|
|
|
|
|
|
|
|
if (event.key === 'ArrowLeft') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.hue = clamp(this.hue - increment, 0, 360);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.hue = clamp(this.hue + increment, 0, 360);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'Home') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.hue = 0;
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'End') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.hue = 360;
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleGridKeyDown(event: KeyboardEvent) {
|
|
|
|
const increment = event.shiftKey ? 10 : 1;
|
|
|
|
|
|
|
|
if (event.key === 'ArrowLeft') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.saturation = clamp(this.saturation - increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowRight') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.saturation = clamp(this.saturation + increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowUp') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.lightness = clamp(this.lightness + increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowDown') {
|
|
|
|
event.preventDefault();
|
|
|
|
this.lightness = clamp(this.lightness - increment, 0, 100);
|
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-04 14:18:46 +00:00
|
|
|
handleInputChange(event: CustomEvent) {
|
2020-07-15 21:30:37 +00:00
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
|
|
|
|
|
this.setColor(target.value);
|
|
|
|
target.value = this.value;
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
|
|
|
|
2020-09-04 14:18:46 +00:00
|
|
|
handleInputKeyDown(event: KeyboardEvent) {
|
2020-07-15 21:30:37 +00:00
|
|
|
if (event.key === 'Enter') {
|
2020-09-04 14:18:46 +00:00
|
|
|
this.setColor(this.input.value);
|
|
|
|
this.input.value = this.value;
|
|
|
|
setTimeout(() => this.input.select());
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleDropdownShow(event: CustomEvent) {
|
|
|
|
event.stopPropagation();
|
2021-03-06 17:01:39 +00:00
|
|
|
this.slShow.emit();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleDropdownAfterShow(event: CustomEvent) {
|
|
|
|
event.stopPropagation();
|
2021-03-06 17:01:39 +00:00
|
|
|
this.slAfterShow.emit();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleDropdownHide(event: CustomEvent) {
|
|
|
|
event.stopPropagation();
|
2021-03-06 17:01:39 +00:00
|
|
|
this.slHide.emit();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleDropdownAfterHide(event: CustomEvent) {
|
|
|
|
event.stopPropagation();
|
2021-03-06 17:01:39 +00:00
|
|
|
this.slAfterHide.emit();
|
2020-12-11 22:10:30 +00:00
|
|
|
this.showCopyFeedback = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
normalizeColorString(colorString: string) {
|
|
|
|
//
|
|
|
|
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
|
|
|
|
// hex colors when the # is missing. This pre-parser tries to normalize these edge cases to provide a better
|
|
|
|
// experience for users who type in color values.
|
|
|
|
//
|
2020-07-27 11:36:17 +00:00
|
|
|
if (/rgba?/i.test(colorString)) {
|
2020-07-15 21:30:37 +00:00
|
|
|
const rgba = colorString
|
|
|
|
.replace(/[^\d.%]/g, ' ')
|
|
|
|
.split(' ')
|
|
|
|
.map(val => val.trim())
|
|
|
|
.filter(val => val.length);
|
|
|
|
|
|
|
|
if (rgba.length < 4) {
|
|
|
|
rgba[3] = '1';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rgba[3].indexOf('%') > -1) {
|
|
|
|
rgba[3] = (Number(rgba[3].replace(/%/g, '')) / 100).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
|
|
|
}
|
|
|
|
|
2020-07-27 11:36:17 +00:00
|
|
|
if (/hsla?/i.test(colorString)) {
|
2020-07-15 21:30:37 +00:00
|
|
|
const hsla = colorString
|
|
|
|
.replace(/[^\d.%]/g, ' ')
|
|
|
|
.split(' ')
|
|
|
|
.map(val => val.trim())
|
|
|
|
.filter(val => val.length);
|
|
|
|
|
|
|
|
if (hsla.length < 4) {
|
|
|
|
hsla[3] = '1';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hsla[3].indexOf('%') > -1) {
|
|
|
|
hsla[3] = (Number(hsla[3].replace(/%/g, '')) / 100).toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
|
|
|
}
|
|
|
|
|
2020-07-27 11:36:17 +00:00
|
|
|
if (/^[0-9a-f]+$/i.test(colorString)) {
|
2020-07-15 21:30:37 +00:00
|
|
|
return `#${colorString}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return colorString;
|
|
|
|
}
|
|
|
|
|
|
|
|
parseColor(colorString: string) {
|
|
|
|
function toHex(value: number) {
|
|
|
|
const hex = Math.round(value).toString(16);
|
|
|
|
return hex.length === 1 ? `0${hex}` : hex;
|
|
|
|
}
|
|
|
|
|
|
|
|
let parsed: any;
|
|
|
|
|
|
|
|
// The color module has a weak parser, so we normalize certain things to make the user experience better
|
|
|
|
colorString = this.normalizeColorString(colorString);
|
|
|
|
|
|
|
|
try {
|
|
|
|
parsed = color(colorString);
|
|
|
|
} catch {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const hsl = {
|
|
|
|
h: parsed.hsl().color[0],
|
|
|
|
s: parsed.hsl().color[1],
|
|
|
|
l: parsed.hsl().color[2],
|
|
|
|
a: parsed.hsl().valpha
|
|
|
|
};
|
|
|
|
|
|
|
|
const rgb = {
|
|
|
|
r: parsed.rgb().color[0],
|
|
|
|
g: parsed.rgb().color[1],
|
|
|
|
b: parsed.rgb().color[2],
|
|
|
|
a: parsed.rgb().valpha
|
|
|
|
};
|
|
|
|
|
|
|
|
const hex = {
|
|
|
|
r: toHex(parsed.rgb().color[0]),
|
|
|
|
g: toHex(parsed.rgb().color[1]),
|
|
|
|
b: toHex(parsed.rgb().color[2]),
|
|
|
|
a: toHex(parsed.rgb().valpha * 255)
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
hsl: {
|
|
|
|
h: hsl.h,
|
|
|
|
s: hsl.s,
|
|
|
|
l: hsl.l,
|
|
|
|
string: this.setLetterCase(`hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)`)
|
|
|
|
},
|
|
|
|
hsla: {
|
|
|
|
h: hsl.h,
|
|
|
|
s: hsl.s,
|
|
|
|
l: hsl.l,
|
|
|
|
a: hsl.a,
|
|
|
|
string: this.setLetterCase(
|
|
|
|
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${Number(
|
|
|
|
hsl.a.toFixed(2).toString()
|
|
|
|
)})`
|
|
|
|
)
|
|
|
|
},
|
|
|
|
rgb: {
|
|
|
|
r: rgb.r,
|
|
|
|
g: rgb.g,
|
|
|
|
b: rgb.b,
|
|
|
|
string: this.setLetterCase(`rgb(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)})`)
|
|
|
|
},
|
|
|
|
rgba: {
|
|
|
|
r: rgb.r,
|
|
|
|
g: rgb.g,
|
|
|
|
b: rgb.b,
|
|
|
|
a: rgb.a,
|
|
|
|
string: this.setLetterCase(
|
|
|
|
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${Number(
|
|
|
|
rgb.a.toFixed(2).toString()
|
|
|
|
)})`
|
|
|
|
)
|
|
|
|
},
|
|
|
|
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
|
|
|
|
hexa: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}${hex.a}`)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
setColor(colorString: string) {
|
|
|
|
const newColor = this.parseColor(colorString);
|
|
|
|
|
|
|
|
if (!newColor) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.hue = newColor.hsla.h;
|
|
|
|
this.saturation = newColor.hsla.s;
|
|
|
|
this.lightness = newColor.hsla.l;
|
|
|
|
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
|
|
|
|
|
|
|
this.syncValues();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
setLetterCase(string: string) {
|
2020-12-11 22:10:30 +00:00
|
|
|
if (typeof string !== 'string') return '';
|
2020-07-15 21:30:37 +00:00
|
|
|
return this.uppercase ? string.toUpperCase() : string.toLowerCase();
|
|
|
|
}
|
|
|
|
|
|
|
|
syncValues() {
|
|
|
|
const currentColor = this.parseColor(
|
|
|
|
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!currentColor) {
|
2021-02-26 14:09:13 +00:00
|
|
|
return;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the value
|
|
|
|
if (this.format === 'hsl') {
|
2020-09-04 14:18:46 +00:00
|
|
|
this.inputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
|
2020-07-15 21:30:37 +00:00
|
|
|
} else if (this.format === 'rgb') {
|
2020-09-04 14:18:46 +00:00
|
|
|
this.inputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
|
2020-07-15 21:30:37 +00:00
|
|
|
} else {
|
2020-09-04 14:18:46 +00:00
|
|
|
this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Setting this.value will trigger the watcher which parses the new color. We want to bypass that behavior because
|
|
|
|
// a) we've already done it in this function and b) conversion/rounding can lead to values changing slightly.
|
|
|
|
this.bypassValueParse = true;
|
2020-09-04 14:18:46 +00:00
|
|
|
this.value = this.inputValue;
|
2020-07-15 21:30:37 +00:00
|
|
|
this.bypassValueParse = false;
|
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
formatChanged() {
|
2021-02-26 14:09:13 +00:00
|
|
|
this.syncValues();
|
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
opacityChanged() {
|
2021-02-26 14:09:13 +00:00
|
|
|
this.alpha = 100;
|
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
valueChanged(newValue: string, oldValue: string) {
|
2021-02-26 14:09:13 +00:00
|
|
|
if (!this.bypassValueParse) {
|
|
|
|
const newColor = this.parseColor(newValue);
|
|
|
|
|
|
|
|
if (newColor) {
|
|
|
|
this.inputValue = this.value;
|
|
|
|
this.hue = newColor.hsla.h;
|
|
|
|
this.saturation = newColor.hsla.s;
|
|
|
|
this.lightness = newColor.hsla.l;
|
|
|
|
this.alpha = newColor.hsla.a * 100;
|
|
|
|
} else {
|
|
|
|
this.inputValue = oldValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.value !== this.lastValueEmitted) {
|
2021-03-06 17:01:39 +00:00
|
|
|
this.slChange.emit();
|
2021-02-26 14:09:13 +00:00
|
|
|
this.lastValueEmitted = this.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
render() {
|
|
|
|
const x = this.saturation;
|
|
|
|
const y = 100 - this.lightness;
|
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
const colorPicker = html`
|
|
|
|
<div
|
|
|
|
part="base"
|
|
|
|
class=${classMap({
|
|
|
|
'color-picker': true,
|
|
|
|
'color-picker--inline': this.inline,
|
|
|
|
'color-picker--disabled': this.disabled
|
|
|
|
})}
|
|
|
|
aria-disabled=${this.disabled ? 'true' : 'false'}
|
|
|
|
>
|
2020-07-15 21:30:37 +00:00
|
|
|
<div
|
2021-02-26 14:09:13 +00:00
|
|
|
part="grid"
|
|
|
|
class="color-picker__grid"
|
|
|
|
style=${styleMap({ backgroundColor: `hsl(${this.hue}deg, 100%, 50%)` })}
|
2021-03-06 17:01:39 +00:00
|
|
|
@mousedown=${this.handleGridDrag}
|
|
|
|
@touchstart=${this.handleGridDrag}
|
2020-07-15 21:30:37 +00:00
|
|
|
>
|
2021-02-26 14:09:13 +00:00
|
|
|
<span
|
|
|
|
part="grid-handle"
|
|
|
|
class="color-picker__grid-handle"
|
|
|
|
style=${styleMap({
|
|
|
|
top: `${y}%`,
|
|
|
|
left: `${x}%`,
|
|
|
|
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
|
|
|
|
})}
|
|
|
|
role="slider"
|
|
|
|
aria-label="HSL"
|
|
|
|
aria-valuetext=${`hsl(${Math.round(this.hue)}, ${Math.round(this.saturation)}%, ${Math.round(
|
|
|
|
this.lightness
|
|
|
|
)}%)`}
|
|
|
|
tabindex=${this.disabled ? null : '0'}
|
2021-03-06 17:01:39 +00:00
|
|
|
@keydown=${this.handleGridKeyDown}
|
|
|
|
></span>
|
2021-02-26 14:09:13 +00:00
|
|
|
</div>
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
<div class="color-picker__controls">
|
|
|
|
<div class="color-picker__sliders">
|
|
|
|
<div
|
|
|
|
part="slider hue-slider"
|
|
|
|
class="color-picker__hue color-picker__slider"
|
2021-03-06 17:01:39 +00:00
|
|
|
@mousedown=${this.handleHueDrag}
|
|
|
|
@touchstart=${this.handleHueDrag}
|
2020-12-11 22:10:30 +00:00
|
|
|
>
|
2021-02-26 14:09:13 +00:00
|
|
|
<span
|
|
|
|
part="slider-handle"
|
|
|
|
class="color-picker__slider-handle"
|
|
|
|
style=${styleMap({
|
|
|
|
left: `${this.hue === 0 ? 0 : 100 / (360 / this.hue)}%`
|
|
|
|
})}
|
|
|
|
role="slider"
|
|
|
|
aria-label="hue"
|
|
|
|
aria-orientation="horizontal"
|
|
|
|
aria-valuemin="0"
|
|
|
|
aria-valuemax="360"
|
|
|
|
aria-valuenow=${Math.round(this.hue)}
|
|
|
|
tabindex=${this.disabled ? null : 0}
|
2021-03-06 17:01:39 +00:00
|
|
|
@keydown=${this.handleHueKeyDown}
|
|
|
|
></span>
|
2021-02-26 14:09:13 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
${this.opacity
|
|
|
|
? html`
|
|
|
|
<div
|
|
|
|
part="slider opacity-slider"
|
|
|
|
class="color-picker__alpha color-picker__slider color-picker__transparent-bg"
|
2021-03-06 17:01:39 +00:00
|
|
|
@mousedown="${this.handleAlphaDrag}"
|
|
|
|
@touchstart="${this.handleAlphaDrag}"
|
2021-02-26 14:09:13 +00:00
|
|
|
>
|
|
|
|
<div
|
|
|
|
class="color-picker__alpha-gradient"
|
|
|
|
style=${styleMap({
|
|
|
|
backgroundImage: `linear-gradient(
|
2021-03-06 17:01:39 +00:00
|
|
|
to right,
|
|
|
|
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, 0%) 0%,
|
2021-02-26 14:09:13 +00:00
|
|
|
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%) 100%
|
|
|
|
)`
|
|
|
|
})}
|
2021-03-06 17:01:39 +00:00
|
|
|
></div>
|
2021-02-26 14:09:13 +00:00
|
|
|
<span
|
|
|
|
part="slider-handle"
|
|
|
|
class="color-picker__slider-handle"
|
|
|
|
style=${styleMap({
|
|
|
|
left: `${this.alpha}%`
|
|
|
|
})}
|
|
|
|
role="slider"
|
|
|
|
aria-label="alpha"
|
|
|
|
aria-orientation="horizontal"
|
|
|
|
aria-valuemin="0"
|
|
|
|
aria-valuemax="100"
|
|
|
|
aria-valuenow=${Math.round(this.alpha)}
|
|
|
|
tabindex=${this.disabled ? null : '0'}
|
2021-03-06 17:01:39 +00:00
|
|
|
@keydown=${this.handleAlphaKeyDown}
|
|
|
|
></span>
|
2021-02-26 14:09:13 +00:00
|
|
|
</div>
|
|
|
|
`
|
|
|
|
: ''}
|
2020-07-15 21:30:37 +00:00
|
|
|
</div>
|
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
part="preview"
|
|
|
|
class="color-picker__preview color-picker__transparent-bg"
|
|
|
|
style=${styleMap({
|
|
|
|
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
|
|
|
})}
|
2021-03-06 17:01:39 +00:00
|
|
|
@click=${this.handleCopy}
|
2021-02-26 14:09:13 +00:00
|
|
|
>
|
|
|
|
<sl-icon
|
|
|
|
name="check"
|
|
|
|
class=${classMap({
|
|
|
|
'color-picker__copy-feedback': true,
|
|
|
|
'color-picker__copy-feedback--visible': this.showCopyFeedback,
|
|
|
|
'color-picker__copy-feedback--dark': this.lightness > 50
|
|
|
|
})}
|
2021-03-06 17:01:39 +00:00
|
|
|
></sl-icon>
|
2021-02-26 14:09:13 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
2020-12-11 22:10:30 +00:00
|
|
|
|
2021-02-26 14:09:13 +00:00
|
|
|
<div class="color-picker__user-input">
|
|
|
|
<sl-input
|
|
|
|
part="input"
|
|
|
|
size="small"
|
|
|
|
type="text"
|
|
|
|
name=${this.name}
|
|
|
|
autocomplete="off"
|
|
|
|
autocorrect="off"
|
|
|
|
autocapitalize="off"
|
|
|
|
spellcheck="false"
|
2021-03-06 17:01:39 +00:00
|
|
|
value=${this.inputValue}
|
|
|
|
?disabled=${this.disabled}
|
|
|
|
@keydown=${this.handleInputKeyDown}
|
|
|
|
@sl-change=${this.handleInputChange}
|
|
|
|
></sl-input>
|
2021-02-26 14:09:13 +00:00
|
|
|
|
|
|
|
${!this.noFormatToggle
|
|
|
|
? html`
|
2021-03-06 17:01:39 +00:00
|
|
|
<sl-button exportparts="base:format-button" size="small" @click=${this.handleFormatToggle}>
|
2021-02-26 14:09:13 +00:00
|
|
|
${this.setLetterCase(this.format)}
|
|
|
|
</sl-button>
|
|
|
|
`
|
|
|
|
: ''}
|
2020-07-15 21:30:37 +00:00
|
|
|
</div>
|
2021-02-26 14:09:13 +00:00
|
|
|
|
|
|
|
${this.swatches
|
|
|
|
? html`
|
|
|
|
<div part="swatches" class="color-picker__swatches">
|
|
|
|
${this.swatches.map(swatch => {
|
|
|
|
return html`
|
|
|
|
<div
|
|
|
|
part="swatch"
|
|
|
|
class="color-picker__swatch color-picker__transparent-bg"
|
|
|
|
tabindex=${this.disabled ? null : '0'}
|
|
|
|
role="button"
|
|
|
|
aria-label=${swatch}
|
2021-03-06 17:01:39 +00:00
|
|
|
@click=${() => !this.disabled && this.setColor(swatch)}
|
|
|
|
@keydown=${(event: KeyboardEvent) =>
|
2021-02-26 14:09:13 +00:00
|
|
|
!this.disabled && event.key === 'Enter' && this.setColor(swatch)}
|
|
|
|
>
|
2021-03-06 17:01:39 +00:00
|
|
|
<div class="color-picker__swatch-color" style=${styleMap({ backgroundColor: swatch })}></div>
|
2021-02-26 14:09:13 +00:00
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
`
|
|
|
|
: ''}
|
|
|
|
</div>
|
|
|
|
`;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
// Render inline
|
|
|
|
if (this.inline) {
|
2021-02-26 14:09:13 +00:00
|
|
|
return colorPicker;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Render as a dropdown
|
2021-02-26 14:09:13 +00:00
|
|
|
return html`
|
2020-07-15 21:30:37 +00:00
|
|
|
<sl-dropdown
|
|
|
|
class="color-dropdown"
|
2021-02-26 14:09:13 +00:00
|
|
|
aria-disabled=${this.disabled ? 'true' : 'false'}
|
|
|
|
.containing-element=${this}
|
|
|
|
hoist=${this.hoist}
|
2021-03-06 17:01:39 +00:00
|
|
|
@sl-show=${this.handleDropdownShow}
|
|
|
|
@sl-after-show=${this.handleDropdownAfterShow}
|
|
|
|
@sl-hide=${this.handleDropdownHide}
|
|
|
|
@sl-after-hide=${this.handleDropdownAfterHide}
|
2020-07-15 21:30:37 +00:00
|
|
|
>
|
|
|
|
<button
|
2020-08-17 12:23:06 +00:00
|
|
|
part="trigger"
|
2020-07-15 21:30:37 +00:00
|
|
|
slot="trigger"
|
2021-02-26 14:09:13 +00:00
|
|
|
class=${classMap({
|
2020-07-15 21:30:37 +00:00
|
|
|
'color-dropdown__trigger': true,
|
|
|
|
'color-dropdown__trigger--disabled': this.disabled,
|
|
|
|
'color-dropdown__trigger--small': this.size === 'small',
|
|
|
|
'color-dropdown__trigger--medium': this.size === 'medium',
|
|
|
|
'color-dropdown__trigger--large': this.size === 'large',
|
|
|
|
'color-picker__transparent-bg': true
|
2021-02-26 14:09:13 +00:00
|
|
|
})}
|
|
|
|
style=${styleMap({
|
2020-07-15 21:30:37 +00:00
|
|
|
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
2021-02-26 14:09:13 +00:00
|
|
|
})}
|
2020-07-15 21:30:37 +00:00
|
|
|
type="button"
|
2021-03-06 17:01:39 +00:00
|
|
|
></button>
|
2021-02-26 14:09:13 +00:00
|
|
|
${colorPicker}
|
2020-07-15 21:30:37 +00:00
|
|
|
</sl-dropdown>
|
2021-02-26 14:09:13 +00:00
|
|
|
`;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
}
|