UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

981 lines (973 loc) • 47.1 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 { proxyCustomElement, HTMLElement, createEvent, h } from '@stencil/core/internal/client'; import { c as color, d as defineCustomElement$a } from './color-picker-swatch.js'; import { D as DEFAULT_COLOR, T as TEXT, a as DIMENSIONS, H as HSV_LIMITS, C as CSS, d as defineCustomElement$b, b as DEFAULT_STORAGE_KEY_PREFIX, R as RGB_LIMITS } from './color-picker-hex-input.js'; import { f as focusElement } from './dom.js'; import { n as normalizeHex, C as CSSColorMode, p as parseMode, c as colorEqual } from './utils.js'; import { c as clamp } from './math.js'; import { u as updateHostInteraction } from './interactive.js'; import { d as defineCustomElement$c } from './button.js'; import { d as defineCustomElement$9 } from './icon.js'; import { d as defineCustomElement$8 } from './input.js'; import { d as defineCustomElement$7 } from './loader.js'; import { d as defineCustomElement$6 } from './progress.js'; import { d as defineCustomElement$5 } from './tab.js'; import { d as defineCustomElement$4 } from './tab-nav.js'; import { d as defineCustomElement$3 } from './tab-title.js'; import { d as defineCustomElement$2 } from './tabs.js'; import { d as debounce, i as isObject } from './debounce.js'; /** Error message constants. */ var FUNC_ERROR_TEXT = 'Expected a function'; /** * Creates a throttled function that only invokes `func` at most once per * every `wait` milliseconds. The throttled function comes with a `cancel` * method to cancel delayed `func` invocations and a `flush` method to * immediately invoke them. Provide `options` to indicate whether `func` * should be invoked on the leading and/or trailing edge of the `wait` * timeout. The `func` is invoked with the last arguments provided to the * throttled function. Subsequent calls to the throttled function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the throttled function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until to the next tick, similar to `setTimeout` with a timeout of `0`. * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `_.throttle` and `_.debounce`. * * @static * @memberOf _ * @since 0.1.0 * @category Function * @param {Function} func The function to throttle. * @param {number} [wait=0] The number of milliseconds to throttle invocations to. * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=true] * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new throttled function. * @example * * // Avoid excessively updating the position while scrolling. * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); * * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); * jQuery(element).on('click', throttled); * * // Cancel the trailing throttled invocation. * jQuery(window).on('popstate', throttled.cancel); */ function throttle(func, wait, options) { var leading = true, trailing = true; if (typeof func != 'function') { throw new TypeError(FUNC_ERROR_TEXT); } if (isObject(options)) { leading = 'leading' in options ? !!options.leading : leading; trailing = 'trailing' in options ? !!options.trailing : trailing; } return debounce(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); } const colorPickerCss = "@-webkit-keyframes in{0%{opacity:0}100%{opacity:1}}@keyframes in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}@keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}:root{--calcite-animation-timing:calc(150ms * var(--calcite-internal-duration-factor));--calcite-internal-duration-factor:var(--calcite-duration-factor, 1);--calcite-internal-animation-timing-fast:calc(100ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-medium:calc(200ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-slow:calc(300ms * var(--calcite-internal-duration-factor))}.calcite-animate{opacity:0;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:var(--calcite-animation-timing);animation-duration:var(--calcite-animation-timing)}.calcite-animate__in{-webkit-animation-name:in;animation-name:in}.calcite-animate__in-down{-webkit-animation-name:in-down;animation-name:in-down}.calcite-animate__in-up{-webkit-animation-name:in-up;animation-name:in-up}.calcite-animate__in-scale{-webkit-animation-name:in-scale;animation-name:in-scale}:root{--calcite-popper-transition:var(--calcite-animation-timing)}:host([hidden]){display:none}:host{display:inline-block;font-size:var(--calcite-font-size--2);line-height:1rem;font-weight:var(--calcite-font-weight-normal)}:host([disabled]){pointer-events:none;cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:var(--calcite-ui-opacity-disabled)}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}:host([scale=s]) .container{width:160px}:host([scale=s]) .saved-colors{grid-template-columns:repeat(auto-fill, minmax(20px, 1fr))}:host([scale=s]) .channels{-ms-flex-direction:column;flex-direction:column}:host([scale=s]) .channel{width:100%;margin-bottom:4px}:host([scale=s]) .channel:last-child{margin-bottom:0}:host([scale=m]) .container{width:272px}:host([scale=l]) .header{padding-bottom:0px}:host([scale=l]){font-size:var(--calcite-font-size--1);line-height:1rem}:host([scale=l]) .container{width:464px}:host([scale=l]) .color-field-and-slider{margin-bottom:-20px}:host([scale=l]) .section{padding:0 16px 16px}:host([scale=l]) .section:first-of-type{padding-top:16px}:host([scale=l]) .saved-colors{grid-template-columns:repeat(auto-fill, minmax(28px, 1fr));grid-gap:12px;padding-top:16px}:host([scale=l]) .control-section{-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-align:baseline;align-items:baseline}:host([scale=l]) .control-section>:nth-child(2){-webkit-margin-start:12px;margin-inline-start:12px}:host([scale=l]) .color-hex-options{display:-ms-flexbox;display:flex;-ms-flex-negative:1;flex-shrink:1;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:distribute;justify-content:space-around;min-height:98px;width:160px}:host([scale=l]) .color-mode-container{-ms-flex-negative:3;flex-shrink:3}:host([appearance=minimal]) .container{border:none}.container{background-color:var(--calcite-ui-foreground-1);display:inline-block;border:1px solid var(--calcite-ui-border-1)}.color-field-and-slider-wrap{position:relative}.scope{pointer-events:none;position:absolute;margin-bottom:0px;margin-right:0px;height:0.5rem;width:0.5rem;padding:0px;font-size:var(--calcite-font-size--1);outline-offset:0;outline-color:transparent;-webkit-transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out;transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out;margin-top:-0.25rem;margin-left:-0.25rem;outline-offset:10px}.scope:focus{outline:2px solid var(--calcite-ui-brand);outline-offset:12px}.color-field-and-slider{margin-bottom:-16px}.color-field-and-slider--interactive{cursor:pointer}.control-section{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}.section{padding:0 12px 12px}.section:first-of-type{padding-top:12px}.color-hex-options,.section--split{-ms-flex-positive:1;flex-grow:1}.header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding-bottom:0.25rem;color:var(--calcite-ui-text-1)}.header--hex,.color-mode-container{padding-top:12px}.channels{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between}.channel{width:31%}.saved-colors{padding-top:12px;display:grid;grid-template-columns:repeat(auto-fill, minmax(24px, 1fr));grid-gap:8px;width:100%}.saved-colors-buttons{display:-ms-flexbox;display:flex}.saved-color{outline-offset:0;outline-color:transparent;-webkit-transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out;transition:outline-offset 100ms ease-in-out, outline-color 100ms ease-in-out;cursor:pointer}.saved-color:focus{outline:2px solid var(--calcite-ui-brand);outline-offset:2px}.saved-color:hover{-webkit-transition:outline-color var(--calcite-internal-animation-timing-fast) ease-in-out;transition:outline-color var(--calcite-internal-animation-timing-fast) ease-in-out;outline:2px solid var(--calcite-ui-border-2);outline-offset:2px}"; const throttleFor60FpsInMs = 16; const defaultValue = normalizeHex(DEFAULT_COLOR.hex()); const defaultFormat = "auto"; const ColorPicker = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement { constructor() { super(); this.__registerHost(); this.__attachShadow(); this.calciteColorPickerChange = createEvent(this, "calciteColorPickerChange", 7); this.calciteColorPickerInput = createEvent(this, "calciteColorPickerInput", 7); //-------------------------------------------------------------------------- // // 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; /** specify the appearance - default (containing border), or minimal (no containing border) */ this.appearance = "default"; /** * Internal prop for advanced use-cases. * * @internal */ this.color = DEFAULT_COLOR; /** * When true, disabled prevents user interaction. */ this.disabled = false; /** * The format of the value property. * * When "auto", the format will be inferred from `value` when set. * @default "auto" */ this.format = defaultFormat; /** When true, hides the hex input */ this.hideHex = false; /** When true, hides the RGB/HSV channel inputs */ this.hideChannels = false; /** When true, hides the saved colors section */ this.hideSaved = false; /** Label used for the blue channel * @default "B" */ this.intlB = TEXT.b; /** Label used for the blue channel description * @default "Blue" */ this.intlBlue = TEXT.blue; /** Label used for the delete color button. * @default "Delete color" */ this.intlDeleteColor = TEXT.deleteColor; /** Label used for the green channel * @default "G" */ this.intlG = TEXT.g; /** Label used for the green channel description * @default "Green" */ this.intlGreen = TEXT.green; /** Label used for the hue channel * @default "H" */ this.intlH = TEXT.h; /** Label used for the HSV mode * @default "HSV" */ this.intlHsv = TEXT.hsv; /** Label used for the hex input * @default "Hex" */ this.intlHex = TEXT.hex; /** Label used for the hue channel description * @default "Hue" */ this.intlHue = TEXT.hue; /** * Label used for the hex input when there is no color selected. * @default "No color" */ this.intlNoColor = TEXT.noColor; /** Label used for the red channel * @default "R" */ this.intlR = TEXT.r; /** Label used for the red channel description * @default "Red" */ this.intlRed = TEXT.red; /** Label used for the RGB mode * @default "RGB" */ this.intlRgb = TEXT.rgb; /** Label used for the saturation channel * @default "S" */ this.intlS = TEXT.s; /** Label used for the saturation channel description * @default "Saturation" */ this.intlSaturation = TEXT.saturation; /** Label used for the save color button. * @default "Save color" */ this.intlSaveColor = TEXT.saveColor; /** Label used for the saved colors section * @default "Saved" */ this.intlSaved = TEXT.saved; /** Label used for the value channel * @default "V" */ this.intlV = TEXT.v; /** Label used for the * @default "Value" */ this.intlValue = TEXT.value; /** * The scale of the color picker. */ this.scale = "m"; /** * The color value. * * This value can be either a {@link https://developer.mozilla.org/en-US/docs/Web/CSS/color|CSS string} * a RGB, HSL or HSV object. * * The type will be preserved as the color is updated. * @default "#007ac2" * @see [ColorValue](https://github.com/Esri/calcite-components/blob/master/src/components/color-picker/interfaces.ts#L10) */ this.value = defaultValue; this.colorFieldAndSliderHovered = false; this.hueThumbState = "idle"; this.internalColorUpdateContext = null; this.mode = CSSColorMode.HEX; this.shiftKeyChannelAdjustment = 0; this.sliderThumbState = "idle"; this.colorFieldAndSliderInteractive = false; this.channelMode = "rgb"; this.channels = this.toChannels(DEFAULT_COLOR); this.dimensions = DIMENSIONS.m; this.savedColors = []; this.handleTabActivate = (event) => { this.channelMode = event.currentTarget.getAttribute("data-color-mode"); this.updateChannelsFromColor(this.color); }; this.handleColorFieldScopeKeyDown = (event) => { const key = event.key; const arrowKeyToXYOffset = { ArrowUp: { x: 0, y: -10 }, ArrowRight: { x: 10, y: 0 }, ArrowDown: { x: 0, y: 10 }, ArrowLeft: { x: -10, y: 0 } }; if (arrowKeyToXYOffset[key]) { event.preventDefault(); this.scopeOrientation = key === "ArrowDown" || key === "ArrowUp" ? "vertical" : "horizontal"; this.captureColorFieldColor(this.colorFieldScopeLeft + arrowKeyToXYOffset[key].x || 0, this.colorFieldScopeTop + arrowKeyToXYOffset[key].y || 0, false); } }; this.handleHueScopeKeyDown = (event) => { const modifier = event.shiftKey ? 10 : 1; const key = event.key; const arrowKeyToXOffset = { ArrowUp: 1, ArrowRight: 1, ArrowDown: -1, ArrowLeft: -1 }; if (arrowKeyToXOffset[key]) { event.preventDefault(); const delta = arrowKeyToXOffset[key] * modifier; const hue = this.baseColorFieldColor.hue(); const color = this.baseColorFieldColor.hue(hue + delta); this.internalColorSet(color, false); } }; this.handleHexInputChange = (event) => { event.stopPropagation(); const { allowEmpty, color: color$1 } = this; const input = event.target; const hex = input.value; if (allowEmpty && !hex) { this.internalColorSet(null); return; } const normalizedHex = color$1 && normalizeHex(color$1.hex()); if (hex !== normalizedHex) { this.internalColorSet(color(hex)); } }; this.handleSavedColorSelect = (event) => { const swatch = event.currentTarget; this.internalColorSet(color(swatch.color)); }; this.handleChannelInput = (event) => { const input = event.currentTarget; const internalInput = event.detail.nativeEvent.target; const channelIndex = Number(input.getAttribute("data-channel-index")); const limit = this.channelMode === "rgb" ? RGB_LIMITS[Object.keys(RGB_LIMITS)[channelIndex]] : HSV_LIMITS[Object.keys(HSV_LIMITS)[channelIndex]]; let inputValue; if (this.allowEmpty && !input.value) { inputValue = ""; } else { const value = Number(input.value) + this.shiftKeyChannelAdjustment; const clamped = clamp(value, 0, limit); inputValue = clamped.toString(); } input.value = inputValue; internalInput.value = inputValue; }; this.handleChannelChange = (event) => { const input = event.currentTarget; const channelIndex = Number(input.getAttribute("data-channel-index")); const channels = [...this.channels]; const shouldClearChannels = this.allowEmpty && !input.value; if (shouldClearChannels) { this.channels = [null, null, null]; this.internalColorSet(null); return; } channels[channelIndex] = Number(input.value); this.updateColorFromChannels(channels); }; this.handleSavedColorKeyDown = (event) => { if (event.key === " " || event.key === "Enter") { event.preventDefault(); event.stopPropagation(); this.handleSavedColorSelect(event); } }; this.handleColorFieldAndSliderMouseLeave = () => { this.colorFieldAndSliderInteractive = false; this.colorFieldAndSliderHovered = false; if (this.sliderThumbState !== "drag" && this.hueThumbState !== "drag") { this.hueThumbState = "idle"; this.sliderThumbState = "idle"; this.drawColorFieldAndSlider(); } }; this.handleColorFieldAndSliderMouseDown = (event) => { const { offsetX, offsetY } = event; const region = this.getCanvasRegion(offsetY); if (region === "color-field") { this.hueThumbState = "drag"; this.captureColorFieldColor(offsetX, offsetY); this.colorFieldScopeNode.focus(); } else if (region === "slider") { this.sliderThumbState = "drag"; this.captureHueSliderColor(offsetX); this.hueScopeNode.focus(); } // prevent text selection outside of color field & slider area event.preventDefault(); document.addEventListener("mousemove", this.globalMouseMoveHandler); document.addEventListener("mouseup", this.globalMouseUpHandler, { once: true }); this.activeColorFieldAndSliderRect = this.fieldAndSliderRenderingContext.canvas.getBoundingClientRect(); }; this.globalMouseUpHandler = () => { const previouslyDragging = this.sliderThumbState === "drag" || this.hueThumbState === "drag"; this.hueThumbState = "idle"; this.sliderThumbState = "idle"; this.activeColorFieldAndSliderRect = null; this.drawColorFieldAndSlider(); if (previouslyDragging) { this.calciteColorPickerChange.emit(); } }; this.globalMouseMoveHandler = (event) => { const { el, dimensions } = this; const sliderThumbDragging = this.sliderThumbState === "drag"; const hueThumbDragging = this.hueThumbState === "drag"; if (!el.isConnected || (!sliderThumbDragging && !hueThumbDragging)) { return; } let samplingX; let samplingY; const colorFieldAndSliderRect = this.activeColorFieldAndSliderRect; const { clientX, clientY } = event; if (this.colorFieldAndSliderHovered) { samplingX = clientX - colorFieldAndSliderRect.x; samplingY = clientY - colorFieldAndSliderRect.y; } else { const colorFieldWidth = dimensions.colorField.width; const colorFieldHeight = dimensions.colorField.height; const hueSliderHeight = dimensions.slider.height; if (clientX < colorFieldAndSliderRect.x + colorFieldWidth && clientX > colorFieldAndSliderRect.x) { samplingX = clientX - colorFieldAndSliderRect.x; } else if (clientX < colorFieldAndSliderRect.x) { samplingX = 0; } else { samplingX = colorFieldWidth; } if (clientY < colorFieldAndSliderRect.y + colorFieldHeight + hueSliderHeight && clientY > colorFieldAndSliderRect.y) { samplingY = clientY - colorFieldAndSliderRect.y; } else if (clientY < colorFieldAndSliderRect.y) { samplingY = 0; } else { samplingY = colorFieldHeight + hueSliderHeight; } } if (hueThumbDragging) { this.captureColorFieldColor(samplingX, samplingY, false); } else { this.captureHueSliderColor(samplingX); } }; this.handleColorFieldAndSliderMouseEnterOrMove = ({ offsetX, offsetY }) => { const { dimensions: { colorField, slider, thumb } } = this; this.colorFieldAndSliderInteractive = offsetY <= colorField.height + slider.height; this.colorFieldAndSliderHovered = true; const region = this.getCanvasRegion(offsetY); if (region === "color-field") { const prevHueThumbState = this.hueThumbState; const color = this.baseColorFieldColor.hsv(); const centerX = Math.round(color.saturationv() / (HSV_LIMITS.s / colorField.width)); const centerY = Math.round(colorField.height - color.value() / (HSV_LIMITS.v / colorField.height)); const hoveringThumb = this.containsPoint(offsetX, offsetY, centerX, centerY, thumb.radius); let transitionedBetweenHoverAndIdle = false; if (prevHueThumbState === "idle" && hoveringThumb) { this.hueThumbState = "hover"; transitionedBetweenHoverAndIdle = true; } else if (prevHueThumbState === "hover" && !hoveringThumb) { this.hueThumbState = "idle"; transitionedBetweenHoverAndIdle = true; } if (this.hueThumbState !== "drag") { if (transitionedBetweenHoverAndIdle) { // refresh since we won't update color and thus no redraw this.drawColorFieldAndSlider(); } } } else if (region === "slider") { const sliderThumbColor = this.baseColorFieldColor.hsv().saturationv(100).value(100); const prevSliderThumbState = this.sliderThumbState; const sliderThumbCenterX = Math.round(sliderThumbColor.hue() / (360 / slider.width)); const sliderThumbCenterY = Math.round((slider.height + this.getSliderCapSpacing()) / 2) + colorField.height; const hoveringSliderThumb = this.containsPoint(offsetX, offsetY, sliderThumbCenterX, sliderThumbCenterY, thumb.radius); let sliderThumbTransitionedBetweenHoverAndIdle = false; if (prevSliderThumbState === "idle" && hoveringSliderThumb) { this.sliderThumbState = "hover"; sliderThumbTransitionedBetweenHoverAndIdle = true; } else if (prevSliderThumbState === "hover" && !hoveringSliderThumb) { this.sliderThumbState = "idle"; sliderThumbTransitionedBetweenHoverAndIdle = true; } if (this.sliderThumbState !== "drag") { if (sliderThumbTransitionedBetweenHoverAndIdle) { // refresh since we won't update color and thus no redraw this.drawColorFieldAndSlider(); } } } }; this.storeColorFieldScope = (node) => { this.colorFieldScopeNode = node; }; this.storeHueScope = (node) => { this.hueScopeNode = node; }; this.renderChannelsTabTitle = (channelMode) => { const { channelMode: activeChannelMode, intlRgb, intlHsv } = this; const active = channelMode === activeChannelMode; const label = channelMode === "rgb" ? intlRgb : intlHsv; return (h("calcite-tab-title", { active: active, class: CSS.colorMode, "data-color-mode": channelMode, key: channelMode, onCalciteTabsActivate: this.handleTabActivate }, label)); }; this.renderChannelsTab = (channelMode) => { const { channelMode: activeChannelMode, channels, intlB, intlBlue, intlG, intlGreen, intlH, intlHue, intlR, intlRed, intlS, intlSaturation, intlV, intlValue } = this; const active = channelMode === activeChannelMode; const isRgb = channelMode === "rgb"; const channelLabels = isRgb ? [intlR, intlG, intlB] : [intlH, intlS, intlV]; const channelAriaLabels = isRgb ? [intlRed, intlGreen, intlBlue] : [intlHue, intlSaturation, intlValue]; return (h("calcite-tab", { active: active, class: CSS.control, key: channelMode }, h("div", { class: CSS.channels }, channels.map((channel, index) => this.renderChannel(channel, index, channelLabels[index], channelAriaLabels[index]))))); }; this.renderChannel = (value, index, label, ariaLabel) => (h("calcite-input", { class: CSS.channel, "data-channel-index": index, label: ariaLabel, numberButtonType: "none", onCalciteInputChange: this.handleChannelChange, onCalciteInputInput: this.handleChannelInput, prefixText: label, scale: this.scale === "l" ? "m" : "s", type: "number", value: value === null || value === void 0 ? void 0 : value.toString() })); this.deleteColor = () => { const colorToDelete = this.color.hex(); const inStorage = this.savedColors.indexOf(colorToDelete) > -1; if (!inStorage) { return; } const savedColors = this.savedColors.filter((color) => color !== colorToDelete); this.savedColors = savedColors; const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`; if (this.storageId) { localStorage.setItem(storageKey, JSON.stringify(savedColors)); } }; this.saveColor = () => { const colorToSave = this.color.hex(); const alreadySaved = this.savedColors.indexOf(colorToSave) > -1; if (alreadySaved) { return; } const savedColors = [...this.savedColors, colorToSave]; this.savedColors = savedColors; const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`; if (this.storageId) { localStorage.setItem(storageKey, JSON.stringify(savedColors)); } }; this.drawColorFieldAndSlider = throttle(() => { if (!this.fieldAndSliderRenderingContext) { return; } this.drawColorField(); this.drawHueSlider(); }, throttleFor60FpsInMs); this.captureColorFieldColor = (x, y, skipEqual = true) => { const { dimensions: { colorField: { height, width } } } = this; const saturation = Math.round((HSV_LIMITS.s / width) * x); const value = Math.round((HSV_LIMITS.v / height) * (height - y)); this.internalColorSet(this.baseColorFieldColor.hsv().saturationv(saturation).value(value), skipEqual); }; this.initColorFieldAndSlider = (canvas) => { this.fieldAndSliderRenderingContext = canvas.getContext("2d"); this.setCanvasContextSize(canvas, { width: this.dimensions.colorField.width, height: this.dimensions.colorField.height + this.dimensions.slider.height + this.getSliderCapSpacing() * 2 }); this.drawColorFieldAndSlider(); }; } handleColorChange(color, oldColor) { this.drawColorFieldAndSlider(); this.updateChannelsFromColor(color); this.previousColor = oldColor; if (this.internalColorUpdateContext) { return; } this.value = this.toValue(color); } handleFormatChange(format) { this.setMode(format); this.value = this.toValue(this.color); } handleScaleChange(scale = "m") { this.updateDimensions(scale); } handleValueChange(value, oldValue) { const { allowEmpty, format } = this; const checkMode = !allowEmpty || value; let modeChanged = false; if (checkMode) { const nextMode = parseMode(value); if (!nextMode || (format !== "auto" && nextMode !== format)) { this.showIncompatibleColorWarning(value, format); this.value = oldValue; return; } modeChanged = this.mode !== nextMode; this.setMode(nextMode); } const dragging = this.sliderThumbState === "drag" || this.hueThumbState === "drag"; if (this.internalColorUpdateContext) { if (this.internalColorUpdateContext === "initial") { return; } this.calciteColorPickerInput.emit(); if (!dragging) { this.calciteColorPickerChange.emit(); } return; } const color$1 = allowEmpty && !value ? null : color(value); const colorChanged = !colorEqual(color$1, this.color); if (modeChanged || colorChanged) { this.color = color$1; this.calciteColorPickerInput.emit(); if (!dragging) { this.calciteColorPickerChange.emit(); } } } //-------------------------------------------------------------------------- // // Internal State/Props // //-------------------------------------------------------------------------- get baseColorFieldColor() { return this.color || this.previousColor || DEFAULT_COLOR; } // using @Listen as a workaround for VDOM listener not firing handleChannelKeyUpOrDown(event) { this.shiftKeyChannelAdjustment = 0; const key = event.key; if ((key !== "ArrowUp" && key !== "ArrowDown") || !event.composedPath().some((node) => { var _a; return (_a = node.classList) === null || _a === void 0 ? void 0 : _a.contains(CSS.channel); })) { return; } const { shiftKey } = event; event.preventDefault(); if (!this.color) { this.internalColorSet(this.previousColor); event.stopPropagation(); return; } // this gets applied to the input's up/down arrow increment/decrement const complementaryBump = 9; this.shiftKeyChannelAdjustment = key === "ArrowUp" && shiftKey ? complementaryBump : key === "ArrowDown" && shiftKey ? -complementaryBump : 0; } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { return focusElement(this.colorFieldScopeNode); } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- componentWillLoad() { const { allowEmpty, color: color$1, format, value } = this; const willSetNoColor = allowEmpty && !value; const parsedMode = parseMode(value); const valueIsCompatible = willSetNoColor || (format === "auto" && parsedMode) || format === parsedMode; const initialColor = willSetNoColor ? null : valueIsCompatible ? color(value) : color$1; if (!valueIsCompatible) { this.showIncompatibleColorWarning(value, format); } this.setMode(format); this.internalColorSet(initialColor, false, "initial"); this.updateDimensions(this.scale); const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`; if (this.storageId && localStorage.getItem(storageKey)) { this.savedColors = JSON.parse(localStorage.getItem(storageKey)); } } disconnectedCallback() { document.removeEventListener("mousemove", this.globalMouseMoveHandler); document.removeEventListener("mouseup", this.globalMouseUpHandler); } componentDidRender() { updateHostInteraction(this); } //-------------------------------------------------------------------------- // // Render Methods // //-------------------------------------------------------------------------- render() { const { allowEmpty, color, intlDeleteColor, hideHex, hideChannels, hideSaved, intlHex, intlSaved, intlSaveColor, savedColors, scale } = this; const selectedColorInHex = color ? color.hex() : null; const hexInputScale = scale === "l" ? "m" : "s"; const { colorFieldAndSliderInteractive, colorFieldScopeTop, colorFieldScopeLeft, hueScopeLeft, hueScopeTop, scopeOrientation, dimensions: { colorField: { height: colorFieldHeight, width: colorFieldWidth }, slider: { height: sliderHeight } } } = this; const hueTop = hueScopeTop !== null && hueScopeTop !== void 0 ? hueScopeTop : sliderHeight / 2 + colorFieldHeight; const hueLeft = hueScopeLeft !== null && hueScopeLeft !== void 0 ? hueScopeLeft : (colorFieldWidth * DEFAULT_COLOR.hue()) / HSV_LIMITS.h; const noColor = color === null; const vertical = scopeOrientation === "vertical"; return (h("div", { class: CSS.container }, h("div", { class: CSS.colorFieldAndSliderWrap }, h("canvas", { class: { [CSS.colorFieldAndSlider]: true, [CSS.colorFieldAndSliderInteractive]: colorFieldAndSliderInteractive }, onMouseDown: this.handleColorFieldAndSliderMouseDown, onMouseEnter: this.handleColorFieldAndSliderMouseEnterOrMove, onMouseLeave: this.handleColorFieldAndSliderMouseLeave, onMouseMove: this.handleColorFieldAndSliderMouseEnterOrMove, ref: this.initColorFieldAndSlider }), h("div", { "aria-label": vertical ? this.intlValue : this.intlSaturation, "aria-valuemax": vertical ? HSV_LIMITS.v : HSV_LIMITS.s, "aria-valuemin": "0", "aria-valuenow": (vertical ? color === null || color === void 0 ? void 0 : color.saturationv() : color === null || color === void 0 ? void 0 : color.value()) || "0", class: { [CSS.scope]: true, [CSS.colorFieldScope]: true }, onKeyDown: this.handleColorFieldScopeKeyDown, ref: this.storeColorFieldScope, role: "slider", style: { top: `${colorFieldScopeTop || 0}px`, left: `${colorFieldScopeLeft || 0}px` }, tabindex: "0" }), h("div", { "aria-label": this.intlHue, "aria-valuemax": HSV_LIMITS.h, "aria-valuemin": "0", "aria-valuenow": (color === null || color === void 0 ? void 0 : color.round().hue()) || DEFAULT_COLOR.round().hue(), class: { [CSS.scope]: true, [CSS.hueScope]: true }, onKeyDown: this.handleHueScopeKeyDown, ref: this.storeHueScope, role: "slider", style: { top: `${hueTop}px`, left: `${hueLeft}px` }, tabindex: "0" })), hideHex && hideChannels ? null : (h("div", { class: { [CSS.controlSection]: true, [CSS.section]: true } }, hideHex ? null : (h("div", { class: CSS.hexOptions }, h("span", { class: { [CSS.header]: true, [CSS.headerHex]: true } }, intlHex), h("calcite-color-picker-hex-input", { allowEmpty: allowEmpty, class: CSS.control, onCalciteColorPickerHexInputChange: this.handleHexInputChange, scale: hexInputScale, value: selectedColorInHex }))), hideChannels ? null : (h("calcite-tabs", { class: { [CSS.colorModeContainer]: true, [CSS.splitSection]: true }, scale: hexInputScale }, h("calcite-tab-nav", { slot: "tab-nav" }, this.renderChannelsTabTitle("rgb"), this.renderChannelsTabTitle("hsv")), this.renderChannelsTab("rgb"), this.renderChannelsTab("hsv"))))), hideSaved ? null : (h("div", { class: { [CSS.savedColorsSection]: true, [CSS.section]: true } }, h("div", { class: CSS.header }, h("label", null, intlSaved), h("div", { class: CSS.savedColorsButtons }, h("calcite-button", { appearance: "transparent", class: CSS.deleteColor, color: "neutral", disabled: noColor, iconStart: "minus", label: intlDeleteColor, onClick: this.deleteColor, scale: hexInputScale, type: "button" }), h("calcite-button", { appearance: "transparent", class: CSS.saveColor, color: "neutral", disabled: noColor, iconStart: "plus", label: intlSaveColor, onClick: this.saveColor, scale: hexInputScale, type: "button" }))), savedColors.length > 0 ? (h("div", { class: CSS.savedColors }, [ ...savedColors.map((color) => (h("calcite-color-picker-swatch", { active: selectedColorInHex === color, class: CSS.savedColor, color: color, key: color, onClick: this.handleSavedColorSelect, onKeyDown: this.handleSavedColorKeyDown, scale: scale, tabIndex: 0 }))) ])) : null)))); } // -------------------------------------------------------------------------- // // Private Methods // //-------------------------------------------------------------------------- showIncompatibleColorWarning(value, format) { console.warn(`ignoring color value (${value}) as it is not compatible with the current format (${format})`); } setMode(format) { this.mode = format === "auto" ? this.mode : format; } captureHueSliderColor(x) { const { dimensions: { slider: { width } } } = this; const hue = (360 / width) * x; this.internalColorSet(this.baseColorFieldColor.hue(hue), false); } getCanvasRegion(y) { const { dimensions: { colorField: { height: colorFieldHeight }, slider: { height: sliderHeight } } } = this; if (y <= colorFieldHeight) { return "color-field"; } if (y <= colorFieldHeight + sliderHeight) { return "slider"; } return "none"; } internalColorSet(color, skipEqual = true, context = "internal") { if (skipEqual && colorEqual(color, this.color)) { return; } this.internalColorUpdateContext = context; this.color = color; this.value = this.toValue(color); this.internalColorUpdateContext = null; } toValue(color, format = this.mode) { if (!color) { return null; } const hexMode = "hex"; if (format.includes(hexMode)) { return normalizeHex(color.round()[hexMode]()); } if (format.includes("-css")) { return color[format.replace("-css", "").replace("a", "")]().round().string(); } const colorObject = color[format]().round().object(); if (format.endsWith("a")) { // normalize alpha prop colorObject.a = colorObject.alpha; delete colorObject.alpha; } return colorObject; } getSliderCapSpacing() { const { dimensions: { slider: { height }, thumb: { radius } } } = this; return radius * 2 - height; } updateDimensions(scale = "m") { this.dimensions = DIMENSIONS[scale]; } drawColorField() { const context = this.fieldAndSliderRenderingContext; const { dimensions: { colorField: { height, width } } } = this; context.fillStyle = this.baseColorFieldColor.hsv().saturationv(100).value(100).string(); context.fillRect(0, 0, width, height); const whiteGradient = context.createLinearGradient(0, 0, width, 0); whiteGradient.addColorStop(0, "rgba(255,255,255,1)"); whiteGradient.addColorStop(1, "rgba(255,255,255,0)"); context.fillStyle = whiteGradient; context.fillRect(0, 0, width, height); const blackGradient = context.createLinearGradient(0, 0, 0, height); blackGradient.addColorStop(0, "rgba(0,0,0,0)"); blackGradient.addColorStop(1, "rgba(0,0,0,1)"); context.fillStyle = blackGradient; context.fillRect(0, 0, width, height); this.drawActiveColorFieldColor(); } setCanvasContextSize(canvas, { height, width }) { const devicePixelRatio = window.devicePixelRatio || 1; canvas.width = width * devicePixelRatio; canvas.height = height * devicePixelRatio; canvas.style.height = `${height}px`; canvas.style.width = `${width}px`; const context = canvas.getContext("2d"); context.scale(devicePixelRatio, devicePixelRatio); } containsPoint(testPointX, testPointY, boundsX, boundsY, boundsRadius) { return (Math.pow(testPointX - boundsX, 2) + Math.pow(testPointY - boundsY, 2) <= Math.pow(boundsRadius, 2)); } drawActiveColorFieldColor() { const { color } = this; if (!color) { return; } const hsvColor = color.hsv(); const { dimensions: { colorField: { height, width }, thumb: { radius } } } = this; const x = hsvColor.saturationv() / (HSV_LIMITS.s / width); const y = height - hsvColor.value() / (HSV_LIMITS.v / height); requestAnimationFrame(() => { this.colorFieldScopeLeft = x; this.colorFieldScopeTop = y; }); this.drawThumb(this.fieldAndSliderRenderingContext, radius, x, y, hsvColor, this.hueThumbState); } drawThumb(context, radius, x, y, color, state) { const startAngle = 0; const endAngle = 2 * Math.PI; context.beginPath(); context.arc(x, y, radius, startAngle, endAngle); context.shadowBlur = state === "hover" ? 32 : 16; context.shadowColor = `rgba(0, 0, 0, ${state === "drag" ? 0.32 : 0.16})`; context.fillStyle = "#fff"; context.fill(); context.beginPath(); context.arc(x, y, radius - 3, startAngle, endAngle); context.shadowBlur = 0; context.shadowColor = "transparent"; context.fillStyle = color.rgb().string(); context.fill(); } drawActiveHueSliderColor() { const { color } = this; if (!color) { return; } const hsvColor = color.hsv().saturationv(100).value(100); const { dimensions: { colorField: { height: colorFieldHeight }, slider: { height, width }, thumb: { radius } } } = this; const x = hsvColor.hue() / (360 / width); const y = height / 2 + colorFieldHeight; requestAnimationFrame(() => { this.hueScopeLeft = x; this.hueScopeTop = y; }); this.drawThumb(this.fieldAndSliderRenderingContext, radius, x, y, hsvColor, this.sliderThumbState); } drawHueSlider() { const context = this.fieldAndSliderRenderingContext; const { dimensions: { colorField: { height: colorFieldHeight }, slider: { height, width } } } = this; const gradient = context.createLinearGradient(0, 0, width, 0); const hueSliderColorStopKeywords = ["red", "yellow", "lime", "cyan", "blue", "magenta", "red"]; const offset = 1 / (hueSliderColorStopKeywords.length - 1); let currentOffset = 0; hueSliderColorStopKeywords.forEach((keyword) => { gradient.addColorStop(currentOffset, color(keyword).string()); currentOffset += offset; }); context.fillStyle = gradient; context.clearRect(0, colorFieldHeight, width, height + this.getSliderCapSpacing() * 2); context.fillRect(0, colorFieldHeight, width, height); this.drawActiveHueSliderColor(); } updateColorFromChannels(channels) { this.internalColorSet(color(channels, this.channelMode)); } updateChannelsFromColor(color) { this.channels = color ? this.toChannels(color) : [null, null, null]; } toChannels(color) { const { channelMode } = this; return color[channelMode]() .array() .map((value) => Math.floor(value)); } get el() { return this; } static get watchers() { return { "color": ["handleColorChange"], "format": ["handleFormatChange"], "scale": ["handleScaleChange"], "value": ["handleValueChange"] }; } static get style() { return colorPickerCss; } }, [1, "calcite-color-picker", { "allowEmpty": [4, "allow-empty"], "appearance": [513], "color": [1040], "disabled": [516], "format": [1], "hideHex": [4, "hide-hex"], "hideChannels": [4, "hide-channels"], "hideSaved": [4, "hide-saved"], "intlB": [1, "intl-b"], "intlBlue": [1, "intl-blue"], "intlDeleteColor": [1, "intl-delete-color"], "intlG": [1, "intl-g"], "intlGreen": [1, "intl-green"], "intlH": [1, "intl-h"], "intlHsv": [1, "intl-hsv"], "intlHex": [1, "intl-hex"], "intlHue": [1, "intl-hue"], "intlNoColor": [1, "intl-no-color"], "intlR": [1, "intl-r"], "intlRed": [1, "intl-red"], "intlRgb": [1, "intl-rgb"], "intlS": [1, "intl-s"], "intlSaturation": [1, "intl-saturation"], "intlSaveColor": [1, "intl-save-color"], "intlSaved": [1, "intl-saved"], "intlV": [1, "intl-v"], "intlValue": [1, "intl-value"], "scale": [513], "storageId": [1, "storage-id"], "value": [1025], "colorFieldAndSliderInteractive": [32], "channelMode": [32], "channels": [32], "dimensions": [32], "savedColors": [32], "colorFieldScopeTop": [32], "colorFieldScopeLeft": [32], "scopeOrientation": [32], "hueScopeLeft": [32], "hueScopeTop": [32], "setFocus": [64] }, [[2, "keydown", "handleChannelKeyUpOrDown"], [2, "keyup", "handleChannelKeyUpOrDown"]]]); function defineCustomElement$1() { if (typeof customElements === "undefined") { return; } const components = ["calcite-color-picker", "calcite-button", "calcite-color-picker-hex-input", "calcite-color-picker-swatch", "calcite-icon", "calcite-input", "calcite-loader", "calcite-progress", "calcite-tab", "calcite-tab-nav", "calcite-tab-title", "calcite-tabs"]; components.forEach(tagName => { switch (tagName) { case "calcite-color-picker": if (!customElements.get(tagName)) { customElements.define(tagName, ColorPicker); } break; case "calcite-button": if (!customElements.get(tagName)) { defineCustomElement$c(); } break; case "calcite-color-picker-hex-input": if (!customElements.get(tagName)) { defineCustomElement$b(); } break; case "calcite-color-picker-swatch": if (!customElements.get(tagName)) { defineCustomElement$a(); } break; case "calcite-icon": if (!customElements.get(tagName)) { defineCustomElement$9(); } break; case "calcite-input": if (!customElements.get(tagName)) { defineCustomElement$8(); } break; case "calcite-loader": if (!customElements.get(tagName)) { defineCustomElement$7(); } break; case "calcite-progress": if (!customElements.get(tagName)) { defineCustomElement$6(); } break; case "calcite-tab": if (!customElements.get(tagName)) { defineCustomElement$5(); } break; case "calcite-tab-nav": if (!customElements.get(tagName)) { defineCustomElement$4(); } break; case "calcite-tab-title": if (!customElements.get(tagName)) { defineCustomElement$3(); } break; case "calcite-tabs": if (!customElements.get(tagName)) { defineCustomElement$2(); } break; } }); } defineCustomElement$1(); const CalciteColorPicker = ColorPicker; const defineCustomElement = defineCustomElement$1; export { CalciteColorPicker, defineCustomElement };