UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

455 lines (454 loc) • 16 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { h } from "@stencil/core"; import { alphaToOpacity, hexChar, hexify, isLonghandHex, isValidHex, normalizeHex, opacityToAlpha, rgbToHex } from "../color-picker/utils"; import { CSS } from "./resources"; import Color from "color"; import { focusElement } from "../../utils/dom"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { OPACITY_LIMITS } from "../color-picker/resources"; const DEFAULT_COLOR = Color(); export class ColorPickerHexInput { constructor() { this.onHexInputBlur = () => { const node = this.hexInputNode; const inputValue = node.value; const hex = `#${inputValue}`; const { allowEmpty, internalColor } = this; const willClearValue = allowEmpty && !inputValue; const isLonghand = isLonghandHex(hex); // ensure modified pasted hex values are committed since we prevent default to remove the # char. this.onHexInputChange(); if (willClearValue || (isValidHex(hex) && isLonghand)) { return; } // manipulating DOM directly since rerender doesn't update input value node.value = allowEmpty && !internalColor ? "" : this.formatHexForInternalInput(rgbToHex( // always display hex input in RRGGBB format internalColor.object())); }; this.onOpacityInputBlur = () => { const node = this.opacityInputNode; const inputValue = node.value; const { allowEmpty, internalColor } = this; const willClearValue = allowEmpty && !inputValue; if (willClearValue) { return; } // manipulating DOM directly since rerender doesn't update input value node.value = allowEmpty && !internalColor ? "" : this.formatOpacityForInternalInput(internalColor); }; this.onHexInputChange = () => { const nodeValue = this.hexInputNode.value; let value = nodeValue; if (value) { const normalized = normalizeHex(value, false); const preserveExistingAlpha = isValidHex(normalized) && this.alphaChannel; if (preserveExistingAlpha && this.internalColor) { const alphaHex = normalizeHex(this.internalColor.hexa(), true).slice(-2); value = `${normalized + alphaHex}`; } } this.internalSetValue(value, this.value); }; this.onOpacityInputChange = () => { const node = this.opacityInputNode; let value; if (!node.value) { value = node.value; } else { const alpha = opacityToAlpha(Number(node.value)); value = this.internalColor?.alpha(alpha).hexa(); } this.internalSetValue(value, this.value); }; this.onInputKeyDown = (event) => { const { altKey, ctrlKey, metaKey, shiftKey } = event; const { alphaChannel, hexInputNode, internalColor, value } = this; const { key } = event; const composedPath = event.composedPath(); if (key === "Tab" || key === "Enter") { if (composedPath.includes(hexInputNode)) { this.onHexInputChange(); } else { this.onOpacityInputChange(); } if (key === "Enter") { event.preventDefault(); } return; } const isNudgeKey = key === "ArrowDown" || key === "ArrowUp"; const oldValue = this.value; if (isNudgeKey) { if (!value) { this.internalSetValue(this.previousNonNullValue, oldValue); event.preventDefault(); return; } const direction = key === "ArrowUp" ? 1 : -1; const bump = shiftKey ? 10 : 1; this.internalSetValue(hexify(this.nudgeRGBChannels(internalColor, bump * direction, composedPath.includes(hexInputNode) ? "rgb" : "a"), alphaChannel), oldValue); event.preventDefault(); return; } const withModifiers = altKey || ctrlKey || metaKey; const singleChar = key.length === 1; const validHexChar = hexChar.test(key); if (singleChar && !withModifiers && !validHexChar) { event.preventDefault(); } }; this.onHexInputPaste = (event) => { const hex = event.clipboardData.getData("text"); if (isValidHex(hex)) { event.preventDefault(); this.hexInputNode.value = hex.slice(1); this.hexInputNode.internalSyncChildElValue(); } }; this.previousNonNullValue = this.value; this.storeHexInputRef = (node) => { this.hexInputNode = node; }; this.storeOpacityInputRef = (node) => { this.opacityInputNode = node; }; this.allowEmpty = false; this.alphaChannel = false; this.hexLabel = "Hex"; this.messages = undefined; this.numberingSystem = undefined; this.scale = "m"; this.value = normalizeHex(hexify(DEFAULT_COLOR, this.alphaChannel), this.alphaChannel, true); this.internalColor = DEFAULT_COLOR; } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { const { allowEmpty, alphaChannel, value } = this; if (value) { const normalized = normalizeHex(value, alphaChannel); if (isValidHex(normalized, alphaChannel)) { this.internalSetValue(normalized, normalized, false); } return; } if (allowEmpty) { this.internalSetValue(null, null, false); } } componentWillLoad() { setUpLoadableComponent(this); } componentDidLoad() { setComponentLoaded(this); } handleValueChange(value, oldValue) { this.internalSetValue(value, oldValue, false); } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- render() { const { alphaChannel, hexLabel, internalColor, messages, scale, value } = this; const hexInputValue = this.formatHexForInternalInput(value); const opacityInputValue = this.formatOpacityForInternalInput(internalColor); const inputScale = scale === "l" ? "m" : "s"; return (h("div", { class: CSS.container }, h("calcite-input", { class: CSS.hexInput, label: messages?.hex || hexLabel, maxLength: 6, numberingSystem: this.numberingSystem, onCalciteInputChange: this.onHexInputChange, onCalciteInternalInputBlur: this.onHexInputBlur, onKeyDown: this.onInputKeyDown, onPaste: this.onHexInputPaste, prefixText: "#", scale: inputScale, value: hexInputValue, // eslint-disable-next-line react/jsx-sort-props ref: this.storeHexInputRef }), alphaChannel ? (h("calcite-input-number", { class: CSS.opacityInput, key: "opacity-input", label: messages?.opacity, max: OPACITY_LIMITS.max, maxLength: 3, min: OPACITY_LIMITS.min, numberButtonType: "none", numberingSystem: this.numberingSystem, onCalciteInputNumberChange: this.onOpacityInputChange, onCalciteInternalInputNumberBlur: this.onOpacityInputBlur, onKeyDown: this.onInputKeyDown, scale: inputScale, suffixText: "%", value: opacityInputValue, // eslint-disable-next-line react/jsx-sort-props ref: this.storeOpacityInputRef })) : null)); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { await componentLoaded(this); focusElement(this.hexInputNode); } //-------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- internalSetValue(value, oldValue, emit = true) { if (value) { const { alphaChannel } = this; const normalized = normalizeHex(value, alphaChannel, alphaChannel); if (isValidHex(normalized, alphaChannel)) { const { internalColor: currentColor } = this; const nextColor = Color(normalized); const normalizedLonghand = normalizeHex(hexify(nextColor, alphaChannel), alphaChannel); const changed = !currentColor || normalizedLonghand !== normalizeHex(hexify(currentColor, alphaChannel), alphaChannel); this.internalColor = nextColor; this.previousNonNullValue = normalizedLonghand; this.value = normalizedLonghand; if (changed && emit) { this.calciteColorPickerHexInputChange.emit(); } return; } } else if (this.allowEmpty) { this.internalColor = null; this.value = null; if (emit) { this.calciteColorPickerHexInputChange.emit(); } return; } this.value = oldValue; } formatHexForInternalInput(hex) { return hex ? hex.replace("#", "").slice(0, 6) : ""; } formatOpacityForInternalInput(color) { return color ? `${alphaToOpacity(color.alpha())}` : ""; } nudgeRGBChannels(color, amount, context) { let nudgedChannels; const channels = color.array(); const rgbChannels = channels.slice(0, 3); if (context === "rgb") { const nudgedRGBChannels = rgbChannels.map((channel) => channel + amount); nudgedChannels = [ ...nudgedRGBChannels, this.alphaChannel ? channels[3] : undefined ]; } else { const nudgedAlpha = opacityToAlpha(alphaToOpacity(color.alpha()) + amount); nudgedChannels = [...rgbChannels, nudgedAlpha]; } return Color(nudgedChannels); } 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`, an empty color (`null`) will be allowed as a `value`. Otherwise, a color value is enforced on the component.\n\nWhen `true`, a color value is enforced, and clearing the input or blurring will restore the last valid `value`. When `false`, an empty color (`null`) will be allowed as a `value`." }, "attribute": "allow-empty", "reflect": false, "defaultValue": "false" }, "alphaChannel": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When true, the component will allow updates to the color's alpha value." }, "attribute": "alpha-channel", "reflect": false, "defaultValue": "false" }, "hexLabel": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "deprecated", "text": "use `messages` instead" }], "text": "Specifies accessible label for the input field." }, "attribute": "hex-label", "reflect": false, "defaultValue": "\"Hex\"" }, "messages": { "type": "unknown", "mutable": false, "complexType": { "original": "ColorPickerMessages", "resolved": "{ b: string; blue: string; deleteColor: string; g: string; green: string; h: string; hsv: string; hex: string; hue: string; noColor: string; opacity: string; r: string; red: string; rgb: string; s: string; saturation: string; saveColor: string; saved: string; v: string; value: string; }", "references": { "ColorPickerMessages": { "location": "import", "path": "../color-picker/assets/color-picker/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Messages are passed by parent component for accessible labels." } }, "numberingSystem": { "type": "string", "mutable": false, "complexType": { "original": "NumberingSystem", "resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"", "references": { "NumberingSystem": { "location": "import", "path": "../../utils/locale" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Specifies the Unicode numeral system used by the component for localization." }, "attribute": "numbering-system", "reflect": false }, "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": "Specifies the size of the component." }, "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(\n hexify(DEFAULT_COLOR, this.alphaChannel),\n this.alphaChannel,\n true\n )" } }; } static get states() { return { "internalColor": {} }; } static get events() { return [{ "method": "calciteColorPickerHexInputChange", "name": "calciteColorPickerHexInputChange", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Emitted when the hex value changes." }, "complexType": { "original": "void", "resolved": "void", "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" }]; } }