UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

348 lines (347 loc) • 10.7 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. */ import { Component, Element, Event, h, Listen, Method, Prop, State, Watch } from "@stencil/core"; import { hexChar, hexToRGB, isLonghandHex, isValidHex, normalizeHex, rgbToHex } from "../color-picker/utils"; import Color from "color"; import { CSS } from "./resources"; import { focusElement } from "../../utils/dom"; import { TEXT } from "../color-picker/resources"; const DEFAULT_COLOR = Color(); export class ColorPickerHexInput { constructor() { //-------------------------------------------------------------------------- // // Public Properties // //-------------------------------------------------------------------------- /** * When false, empty color (null) will be allowed as a value. Otherwise, a color value is always enforced by the component. * * When true, clearing the input and blurring will restore the last valid color set. When false, it will set it to empty. */ this.allowEmpty = false; /** * Label used for the hex input. * @default "Hex" */ this.intlHex = TEXT.hex; /** * Label used for the hex input when there is no color selected. * @default "No color" */ this.intlNoColor = TEXT.noColor; /** * The component's scale. */ this.scale = "m"; /** * The hex value. */ this.value = normalizeHex(DEFAULT_COLOR.hex()); this.onCalciteInputBlur = () => { const node = this.inputNode; const inputValue = node.value; const hex = `#${inputValue}`; const willClearValue = this.allowEmpty && !inputValue; if (willClearValue || (isValidHex(hex) && isLonghandHex(hex))) { return; } // manipulating DOM directly since rerender doesn't update input value node.value = this.allowEmpty && !this.internalColor ? "" : this.formatForInternalInput(rgbToHex(this.internalColor.object())); }; this.onInputChange = () => { const inputValue = this.inputNode.value; let value; if (inputValue) { const hex = inputValue; const color = hexToRGB(`#${hex}`); if (!color) { return; } value = normalizeHex(hex); } else if (this.allowEmpty) { value = null; } this.value = value; this.calciteColorPickerHexInputChange.emit(); }; /** * The last valid/selected color. Used as a fallback if an invalid hex code is entered. */ this.internalColor = DEFAULT_COLOR; this.previousNonNullValue = this.value; //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- this.storeInputRef = (node) => { this.inputNode = node; }; } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { const { allowEmpty, value } = this; if (value) { const normalized = normalizeHex(value); if (isValidHex(normalized)) { this.internalColor = Color(normalized); this.value = normalized; } return; } if (allowEmpty) { this.internalColor = null; this.value = null; } } handleValueChange(value, oldValue) { if (value) { const normalized = normalizeHex(value); if (isValidHex(normalized)) { const { internalColor } = this; const changed = !internalColor || normalized !== normalizeHex(internalColor.hex()); this.internalColor = Color(normalized); this.previousNonNullValue = normalized; this.value = normalized; if (changed) { this.calciteColorPickerHexInputChange.emit(); } return; } } else if (this.allowEmpty) { this.internalColor = null; this.value = null; this.calciteColorPickerHexInputChange.emit(); return; } this.value = oldValue; } // using @Listen as a workaround for VDOM listener not firing onInputKeyDown(event) { const { altKey, ctrlKey, metaKey, shiftKey } = event; const { internalColor, value } = this; const key = event.key; if (key === "Tab" || key === "Enter") { this.onInputChange(); return; } const isNudgeKey = key === "ArrowDown" || key === "ArrowUp"; if (isNudgeKey) { if (!value) { this.value = this.previousNonNullValue; event.preventDefault(); return; } const direction = key === "ArrowUp" ? 1 : -1; const bump = shiftKey ? 10 : 1; this.value = normalizeHex(this.nudgeRGBChannels(internalColor, bump * direction).hex()); event.preventDefault(); return; } const withModifiers = altKey || ctrlKey || metaKey; const singleChar = key.length === 1; const validHexChar = hexChar.test(key); if (singleChar && !withModifiers && !validHexChar) { event.preventDefault(); } } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- render() { const { intlHex, value } = this; const hexInputValue = this.formatForInternalInput(value); return (h("div", { class: CSS.container }, h("calcite-input", { class: CSS.input, label: intlHex, maxLength: 6, onCalciteInputBlur: this.onCalciteInputBlur, onCalciteInputChange: this.onInputChange, prefixText: "#", ref: this.storeInputRef, scale: this.scale, value: hexInputValue }), hexInputValue ? (h("calcite-color-picker-swatch", { active: true, class: CSS.preview, color: `#${hexInputValue}`, scale: this.scale })) : null)); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { focusElement(this.inputNode); } formatForInternalInput(hex) { return hex ? hex.replace("#", "") : ""; } nudgeRGBChannels(color, amount) { return Color.rgb(color.array().map((channel) => channel + amount)); } static get is() { return "calcite-color-picker-hex-input"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["color-picker-hex-input.scss"] }; } static get styleUrls() { return { "$": ["color-picker-hex-input.css"] }; } static get properties() { return { "allowEmpty": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When false, empty color (null) will be allowed as a value. Otherwise, a color value is always enforced by the component.\n\nWhen true, clearing the input and blurring will restore the last valid color set. When false, it will set it to empty." }, "attribute": "allow-empty", "reflect": false, "defaultValue": "false" }, "intlHex": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "default", "text": "\"Hex\"" }], "text": "Label used for the hex input." }, "attribute": "intl-hex", "reflect": false, "defaultValue": "TEXT.hex" }, "intlNoColor": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "default", "text": "\"No color\"" }], "text": "Label used for the hex input when there is no color selected." }, "attribute": "intl-no-color", "reflect": false, "defaultValue": "TEXT.noColor" }, "scale": { "type": "string", "mutable": false, "complexType": { "original": "Scale", "resolved": "\"l\" | \"m\" | \"s\"", "references": { "Scale": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "The component's scale." }, "attribute": "scale", "reflect": true, "defaultValue": "\"m\"" }, "value": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The hex value." }, "attribute": "value", "reflect": true, "defaultValue": "normalizeHex(DEFAULT_COLOR.hex())" } }; } static get states() { return { "internalColor": {} }; } static get events() { return [{ "method": "calciteColorPickerHexInputChange", "name": "calciteColorPickerHexInputChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the hex value changes." }, "complexType": { "original": "any", "resolved": "any", "references": {} } }]; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets focus on the component.", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "value", "methodName": "handleValueChange" }]; } static get listeners() { return [{ "name": "keydown", "method": "onInputKeyDown", "target": undefined, "capture": true, "passive": false }]; } }