@datadobi/color-picker-field
Version:
color-picker-field
506 lines (449 loc) • 15.1 kB
JavaScript
import {html, PolymerElement} from '@polymer/polymer';
import '@datadobi/color-picker/color-picker.js';
import ColorPickerUtils from '@datadobi/color-picker/src/utils/color-picker-utils';
import '@polymer/iron-media-query/iron-media-query.js';
import '@polymer/iron-icon/iron-icon.js';
import '@vaadin/vaadin-text-field';
import '@vaadin/vaadin-button/src/vaadin-button.js';
import '@vaadin/vaadin-ordered-layout';
import '@vaadin/vaadin-context-menu/src/vaadin-context-menu.js';
import {tinycolor} from '@thebespokepixel/es-tinycolor';
/**
* `<color-picker-field>` allows to select a color using sliders, inputs or palettes.
*
* ```
* <color-picker-field></color-picker-field>
* ```
* @memberof global
* @demo demo/index.html
*/
class ColorPickerField extends PolymerElement {
static get template() {
return html`
<style>
[part="select-color-button"] {
overflow: hidden;
position: relative;
box-sizing: border-box;
flex: none;
width: 1.5em;
height: 1.5em;
line-height: 1;
}
[part="select-color-button-color"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: var(--lumo-border-radius);
}
[part="native-input"] {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
box-sizing: border-box;
opacity: 0;
display: none;
}
[part="native-input"][native-input] {
display: block;
}
:host([disabled]) [part="select-color-button"] {
opacity: .2;
}
:host([readonly]) [part="select-color-button"],
:host([readonly]) [part="switch-format-button"] {
pointer-events: none;
}
:host([readonly]) [part="switch-format-button"] {
display: none;
}
:host{
display: flex;
}
[part="select-color-button-icon"] {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 4px;
opacity: 0.7;
transition: opacity 0.3s ease;
}
:host(:not([readonly])) [part="select-color-button"]:hover [part="select-color-button-icon"] {
opacity: 1;
}
:host([theme~="color-picker-field-overlay"]) [part="overlay"] {
max-height: unset;
}
[part="switch-format-button"] {
padding: 2px;
box-sizing: border-box;
--lumo-primary-text-color: var(--lumo-contrast-70pct);
}
[part="switch-format-button"][disabled] {
pointer-events: none;
opacity: 0.4;
}
[part="popup-content"] {
display: flex;
flex-direction: column;
}
[part="footer"] {
display: flex;
justify-content: flex-end;
width: 100%;
}
</style>
<vaadin-text-field style="flex-grow: 1" value="{{value}}" id="text-field" disabled="{{disabled}}" readonly="{{readonly}}">
<span part="select-color-button" slot="prefix">
<vaadin-context-menu close-on="_closeColorPickerPopUp" open-on="click" theme="color-picker-field-overlay">
<template>
<vaadin-vertical-layout theme="spacing padding" part="popup-content">
<color-picker disable-alpha="[[disableAlpha]]" disable-hex="[[disableHex]]" disable-slslider="[[disableSLSlider]]"
disable-hsl="[[disableHsl]]"
disable-rgb="[[disableRgb]]"
last-used-format="{{lastUsedFormat}}"
palettes="[[palettes]]"
pinned-inputs="[[pinnedInputs]]"
pinned-palettes="[[pinnedPalettes]]"
previous-value="{{_previousColor}}"
step-alpha="[[stepAlpha]]"
step-hsl="[[stepHsl]]"
theme$="[[theme]]"
value="{{_popUpColor}}"></color-picker>
<vaadin-horizontal-layout theme="spacing" part="footer">
<vaadin-button on-click="_cancelPopUp" part="cancel"
theme$="[[theme]]">{{labelCancel}}</vaadin-button>
<vaadin-button on-click="_selectPopUpColor" part="submit" theme$="primary [[theme]]">{{labelSelect}}
</vaadin-button>
</vaadin-horizontal-layout>
</vaadin-vertical-layout>
</template>
<iron-media-query query="[[nativeInputMediaQuery]]" query-matches="{{_nativeInput}}"></iron-media-query>
<div part="select-color-button-color"></div>
<iron-icon icon="[[hoverIcon]]" id="select-color-button-icon" part="select-color-button-icon"></iron-icon>
</vaadin-context-menu>
<input native-input$="[[_nativeInput]]" part="native-input" type="color" value="{{value::change}}">
</span>
<vaadin-button slot="suffix" on-click="_nextFormat" hidden$="[[!_showChangeFormatButton(disableHex,disableRgb,disableHsl)]]" theme="tertiary-inline" part="switch-format-button">
<iron-icon icon="vaadin:sort"></iron-icon>
</vaadin-button>
</vaadin-text-field>
`;
}
static get is() {
return 'color-picker-field';
}
static get version() {
return '2.0.0-beta.4';
}
static get properties() {
return {
value: {
type: String,
notify: true
},
disabled: {
type: Boolean,
notify: true
},
readonly: {
type: Boolean,
notify: true
},
/**
* The label to show on the button to select a color in the color picker popup.
*/
labelSelect: {
type: String,
value: 'Select'
},
/**
* The label to show on the button to cancel/close the color picker popup.
*/
labelCancel: {
type: String,
value: 'Cancel'
},
/**
* The icon that is shown if hovering the color button.
*/
hoverIcon: {
type: String,
value: 'vaadin:paintbrush'
},
/**
* Media query used to enable native input.
*/
nativeInputMediaQuery: {
type: String
},
/**
* Set to true to disable **hex** input.
*/
disableHex: {
type: Boolean,
value: false,
observer: '_updateInputPattern'
},
/**
* Set to true to disable **rgb** input.
*/
disableRgb: {
type: Boolean,
value: false,
observer: '_updateInputPattern'
},
/**
* Set to true to disable **hsl** input.
*/
disableHsl: {
type: Boolean,
value: false,
observer: '_updateInputPattern'
},
/**
* Set to true to disable the SL Slider canvas
*/
disableSLSlider: {
type: Boolean,
value: true
},
/**
* Set to true to disable **alpha** input and **alpha** slider.
*/
disableAlpha: {
type: Boolean,
value: false,
observer: '_updateInputPattern'
},
/**
* Set to true to have all inputs visible all the time instead of having a switch button.
*/
pinnedInputs: {
type: Boolean,
value: false
},
/**
* Set to true to have all palettes visible all the time instead of having a switch button.
*/
pinnedPalettes: {
type: Boolean,
value: false
},
/**
* The format that the user used last as input or by switching inputs. One of \`hex\`,\`rgb\`,\`hsl\`.
*/
lastUsedFormat: {
type: String,
notify: true
},
/**
* The palettes to be shown. Should be an Array of Arrays, whereas the inner Arrays should contain valid
* CSS color codes or CSS Custom Properties.
*/
palettes: Array,
/**
* The precision step to use for alpha values.
*/
stepAlpha: {
type: Number,
value: 0.01
},
/**
* The precision step to use for hsl values.
*/
stepHsl: {
type: Number,
value: 1
},
/**
* Set to true to enable the history for selected colors. If the history is enabled it is not possible to
* use palettes as they are internally used for the history.
*/
enableHistory: {
type: Boolean,
value: false
},
/**
* The maximum amount of colors to be stored in the history.
*/
maxHistory: {
type: Number,
value: 10
},
/**
* Set to true to be able to use CSS Custom Properties as input value.
*/
enableCssCustomProperties: {
type: Boolean,
value: false
},
/**
* Set to true to show the button to change color formats.
*/
showChangeFormatButton: {
type: Boolean,
value: false
},
_nativeInput: Boolean,
_popUpColor: String,
_previousColor: String,
_selectColorButtonColor: Object,
_selectColorButtonIcon: Object,
_changeFormatButton: Object
};
}
constructor() {
super();
this._updateInputPattern();
}
ready() {
super.ready();
this._selectColorButtonIcon = this.shadowRoot.querySelector('[part="select-color-button-icon"]');
this._selectColorButtonColor = this.shadowRoot.querySelector('[part="select-color-button-color"]');
this._changeFormatButton = this.shadowRoot.querySelector('[part="switch-format-button"]');
this._textField = this.shadowRoot.querySelector('#text-field');
this._inputElement = this._textField.shadowRoot.querySelector('[part="value"]');
this._transferAttribute('value');
this._transferAttribute('disabled');
this._transferAttribute('readonly');
this._createPropertyObserver('value', this._updateOnValueChange);
this._updateOnValueChange(this.value);
}
_transferAttribute(attribute) {
if (this.hasAttribute(attribute)) {
this._textField.setAttribute(attribute, this.getAttribute(attribute));
}
}
_showChangeFormatButton() {
return this._getEnabledFormats().length > 1 && this.showChangeFormatButton;
}
_changeFormatButtonMouseDown(e) {
if (this.hasAttribute('focused')) {
e.preventDefault();
}
}
_nextFormat() {
this._inputElement.blur();
const allFormats = this._getEnabledFormats();
const format = ColorPickerField._getColorFormat(this._textField.value);
const nextFormat = allFormats[(allFormats.indexOf(format) + 1) % allFormats.length];
const resolvedColor = this._getResolvedColor();
const resolution = this['step' + nextFormat.charAt(0).toUpperCase() + nextFormat.slice(1)]
|| 1;
this._textField.value = ColorPickerUtils.getFormattedColor(resolvedColor, nextFormat, this.stepAlpha, resolution).replace(/,/g, ', ');
this._textField.dispatchEvent(new CustomEvent('change', {bubbles: true}));
this._inputElement.focus();
}
_selectPopUpColor(e) {
this._cancelPopUp(e);
this.value = this._popUpColor.replace(/,/g, ', ');
this._textField.dispatchEvent(new CustomEvent('change', {bubbles: true}));
}
_cancelPopUp(e) {
e.target.dispatchEvent(new CustomEvent('_closeColorPickerPopUp', {composed: true}));
}
_updateOnValueChange(value) {
this._textField.dispatchEvent(new CustomEvent('change', {bubbles: true}));
this._changeFormatButton.removeAttribute('disabled');
const validColor = this.value && '' !== this.value.trim() && this._textField.checkValidity();
if (validColor) {
const color = this._getResolvedColor();
if (color !== undefined) {
this._updateSelectedColor(color);
this._updateHistory(color);
} else {
this._changeFormatButton.setAttribute('disabled', 'disabled');
}
} else {
this._changeFormatButton.setAttribute('disabled', 'disabled');
}
return value;
}
_updateSelectedColor(color) {
this._selectColorButtonColor.style.background = color.toRgbString();
this._selectColorButtonIcon.style.color = ColorPickerUtils.getContrastColor(color);
this._popUpColor = color.toRgbString();
this._previousColor = color.toRgbString();
}
_updateHistory(color) {
if (this.enableHistory) {
const newColor = color.toRgbString();
this.palettes = [(this.palettes
? [newColor, ...this.palettes[0].filter(v => v !== newColor)].slice(0, this.maxHistory)
: [newColor])];
}
}
_getResolvedColor() {
return tinycolor(this.value);
}
_getEnabledFormats() {
const formats = [];
if (!this.disableHex) {
formats.push('hex');
}
if (!this.disableRgb) {
formats.push('rgb');
}
if (!this.disableHsl) {
formats.push('hsl');
}
return formats;
}
static _getColorFormat(color) {
const trimmedValue = color.trim();
if (trimmedValue.startsWith('hsl')) {
return 'hsl';
} else if (trimmedValue.startsWith('rgb')) {
return 'rgb';
} else {
return 'hex';
}
}
_updateInputPattern() {
const patterns = [];
if (!this.disableHex) {
patterns.push('#([0-9a-fA-F]{2}){2,3}');
patterns.push('#[0-9a-fA-F]{3}');
if (!this.disableAlpha) {
patterns.push('#([0-9a-fA-F]{2}){4}');
}
}
const countDecimalPlaces = number => {
return number ? String(Math.abs(number)).replace(/^\\d*\\.?(.*)?$/, '$1').length : 0;
};
const decimalRegex = decimalPlaces => {
return decimalPlaces > 0 ? `(\\\\.\\\\d{1,${decimalPlaces}})?` : '';
};
const decimalPlacesAlpha = countDecimalPlaces(this.stepAlpha);
const decimalAlpha = decimalRegex(decimalPlacesAlpha);
if (!this.disableRgb) {
patterns.push('rgb\\\\((-?\\\\d+\\\\s*,\\\\s*){2}(-?\\\\d+\\\\s*)\\\\)');
patterns.push('rgb\\\\((-?\\\\d+%\\\\s*,\\\\s*){2}(-?\\\\d+%\\\\s*)\\\\)');
if (!this.disableAlpha) {
patterns.push(`rgba\\\\((-?\\\\d+\\\\s*,\\\\s*){3}(-?\\\\d+${decimalAlpha}\\\\s*)\\\\)`);
patterns.push(`rgba\\\\((-?\\\\d+%\\\\s*,\\\\s*){3}(-?\\\\d+${decimalAlpha}\\\\s*)\\\\)`);
}
}
if (!this.disableHsl) {
const decimalPlacesHsl = countDecimalPlaces(this.stepHsl);
const decimalHsl = decimalRegex(decimalPlacesHsl);
patterns.push(
`hsl\\\\((-?\\\\d+${decimalHsl}\\\\s*,\\\\s*)(-?\\\\d+${decimalHsl}%\\\\s*,\\\\s*)(-?\\\\d+${decimalHsl}%\\\\s*)\\\\)`);
if (!this.disableAlpha) {
patterns.push(
`hsla\\\\((-?\\\\d+${decimalHsl}\\\\s*,\\\\s*)(-?\\\\d+${decimalHsl}%\\\\s*,\\\\s*){2}(-?\\\\d+${decimalAlpha}\\\\s*)\\\\)`);
}
}
if (this.enableCssCustomProperties) {
patterns.push('--[a-zA-Z0-9]+[a-zA-Z0-9-]*');
}
this.pattern = '(' + patterns.join('|') + ')';
}
}
customElements.define(ColorPickerField.is, ColorPickerField);