@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
455 lines (454 loc) • 16 kB
JavaScript
/*!
* 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"
}];
}
}