@salla.sa/twilight-components
Version:
Salla Web Component
634 lines (633 loc) • 23.6 kB
JavaScript
/*!
* Crafted with ❤ by Salla
*/
import { h, Host, } from "@stencil/core";
import ArrowDown from "../../assets/svg/keyboard_arrow_down.svg";
export class SallaColorPicker {
constructor() {
/** Lazy-loaded to avoid bundling vanilla-picker (Babel polyfills) into main/hydrate bundle. */
this.picker = null;
this.pickerReady = null;
/**
* File input name for the native formData
*/
this.name = 'color';
/**
* Set if the color picker input is required or not
*/
this.required = false;
/**
* How to display the selected color in the text field
* (the text field still supports input in any format).
*/
this.format = 'hex';
/**
* Whether to have a "Cancel" button which closes the popup.
*/
this.showCancelButton = false;
/**
* Whether to show a text field for color value editing.
*/
this.showTextField = true;
/**
* Whether to enable adjusting the alpha channel.
*/
this.enableAlpha = false;
this.updateViewportCache = () => {
this.cachedViewportWidth = this.getViewportWidthFromBreakpoints();
};
this.cachedViewportWidth = 0; // Will be populated in componentDidLoad via requestAnimationFrame
}
colorChangeHandler(color) {
if (this.colorInput)
this.colorInput.value = color.hex;
this.colorChanged.emit(color);
}
async submittedHandler(color) {
await this.setColorValue(color.rgbaString, true);
if (this.canvas)
this.canvas.style.backgroundColor = color.rgbString;
if (this.colorInput) {
this.colorInput.value = color.hex;
this.colorInput.dispatchEvent(new window.Event('change', { bubbles: true }));
}
this.submitted.emit(color);
}
popupOpenedHandler(color) {
// Double rAF: defer setPopInPosition (getBoundingClientRect) until after paint to avoid forced reflow
requestAnimationFrame(() => {
requestAnimationFrame(() => this.setPopInPosition());
});
this.popupOpened.emit(color);
}
popupClosedHandler(color) {
this.popupClosed.emit(color);
}
/** Methods */
async ensurePicker() {
if (this.picker)
return this.picker;
if (this.pickerReady)
return await this.pickerReady;
throw new Error('Color picker not initialized');
}
/**
* Set the picker options.
*
* (Usually a new .parent and .color).
* @param {Object} options
*/
async setPickerOption(options) {
const picker = await this.ensurePicker();
picker.setOptions(options);
}
/**
* Move the popup to a different parent, optionally opening it at the same time.
*
* (Usually a new .parent and .color).
* @param {Options} option
*
* Whether to open the popup immediately.
* @param {boolean} openImmediately
*/
async movePopUp(options, openImmediately) {
const picker = await this.ensurePicker();
picker.movePopup(options, openImmediately);
}
/**
* Set/initialize the picker's color.
*
* Color name, RGBA/HSLA/HEX string, or RGBA array.
* @param {string} color
*
* If true, won't trigger onChange.
* @param {boolean} triggerEvent
*/
async setColorValue(color, triggerEvent) {
const picker = await this.ensurePicker();
picker.setColor(color, triggerEvent);
}
/**
* Show/open the picker.
*/
async openPicker() {
const picker = await this.ensurePicker();
picker.show();
}
/**
* Close/Hide the picker.
*/
async closePicker() {
const picker = await this.ensurePicker();
picker.hide();
}
/**
* Release all resources used by this picker instance.
*/
async destroyPicker() {
const picker = await this.ensurePicker();
picker.destroy();
this.picker = null;
this.pickerReady = null;
}
disconnectedCallback() {
window.removeEventListener('resize', this.updateViewportCache);
}
componentWillLoad() {
salla.onReady(() => {
this.color = this.color ? this.color : salla.config.get('theme.color.primary', '#5dd5c4');
});
}
/**
* Returns viewport width from matchMedia breakpoints (no layout read).
* Must match updateViewportCache breakpoints for consistency.
*/
getViewportWidthFromBreakpoints() {
if (typeof window === 'undefined')
return 1024;
if (window.matchMedia('(min-width: 1200px)').matches)
return 1200;
if (window.matchMedia('(min-width: 992px)').matches)
return 992;
if (window.matchMedia('(min-width: 768px)').matches)
return 768;
return 375;
}
setPopInPosition() {
// Use cached viewport width – reading innerWidth at popup open causes forced reflow.
// Fallback: matchMedia-based value when cache is 0 (e.g. SSR before hydration). Same breakpoints as updateViewportCache.
const viewportWidth = this.cachedViewportWidth || this.getViewportWidthFromBreakpoints();
const popup = this.host.querySelector('.picker_wrapper');
const widgetEl = this.host.querySelector('.s-color-picker-widget');
if (!popup || !widgetEl)
return;
const widgetPosition = widgetEl.getBoundingClientRect();
const widgetToWindowEq = viewportWidth / 2 - widgetPosition.width / 2;
const widgetInLeft = widgetToWindowEq > widgetPosition.x;
const widgetInRight = widgetToWindowEq < widgetPosition.x;
const widgetInCenter = widgetToWindowEq === widgetPosition.x;
const isMobile = !window.matchMedia('(min-width: 768px)').matches;
if (isMobile && widgetInLeft) {
popup.style.left = '0';
}
if (isMobile && widgetInRight) {
popup.style.left = 'auto';
}
if (!isMobile || (isMobile && ((!widgetInRight && !widgetInLeft) || widgetInCenter))) {
popup.style.left = `-95px`;
}
}
initColorPicker() {
this.pickerReady = import('vanilla-picker').then(mod => {
const Picker = mod.default;
const picker = new Picker({
parent: this.host,
color: this.color,
popup: 'bottom',
alpha: this.enableAlpha,
editor: this.showTextField,
editorFormat: this.format,
cancelButton: this.showCancelButton,
onChange: (color) => this.colorChangeHandler(color),
onDone: (color) => this.submittedHandler(color),
onOpen: (color) => this.popupOpenedHandler(color),
onClose: (color) => this.popupClosedHandler(color),
});
this.picker = picker;
return this.picker;
});
}
render() {
return (h(Host, { key: '2f142fd64afd5571574a7e1c40de3be16ede62e6', class: "s-color-picker-main" }, h("slot", { key: '92acdf14b2f42ac52d0a632ba6840a5dcebfa4d4', name: "widget" }, h("div", { key: '7ba074040299a91802109d0e1d2e78604e39d3a8', class: "s-color-picker-widget" }, h("div", { key: 'a6272207f79fd28e33b66e415a3e05b969ace983', class: "s-color-picker-widget-canvas", ref: dv => (this.canvas = dv) }), h("span", { key: 'e6fed242bb84fae6221cdec525658b68f7514ce7', innerHTML: ArrowDown }))), h("input", { key: '9eb1673f44e663f939124da45fec79c401736203', class: "s-hidden", name: this.name, required: this.required, value: this.color, ref: color => (this.colorInput = color) })));
}
componentDidLoad() {
if (this.canvas)
this.canvas.style.backgroundColor = this.color;
this.initColorPicker();
// Populate viewport cache in next frame (avoids forced reflow during initial render)
requestAnimationFrame(() => this.updateViewportCache());
window.addEventListener('resize', this.updateViewportCache);
if (this.colorInput) {
this.colorInput.addEventListener('invalid', e => {
this.invalidInput.emit(e);
});
this.colorInput.addEventListener('input', () => {
this.colorInput.setCustomValidity('');
this.colorInput.reportValidity();
});
}
}
static get is() { return "salla-color-picker"; }
static get originalStyleUrls() {
return {
"$": ["salla-color-picker.scss"]
};
}
static get styleUrls() {
return {
"$": ["salla-color-picker.css"]
};
}
static get properties() {
return {
"name": {
"type": "string",
"attribute": "name",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "File input name for the native formData"
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "'color'"
},
"required": {
"type": "boolean",
"attribute": "required",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Set if the color picker input is required or not"
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"color": {
"type": "string",
"attribute": "color",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Initial color for the picker."
},
"getter": false,
"setter": false,
"reflect": true
},
"format": {
"type": "string",
"attribute": "format",
"mutable": false,
"complexType": {
"original": "'hex' | 'hsl' | 'rgb'",
"resolved": "\"hex\" | \"hsl\" | \"rgb\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "How to display the selected color in the text field\n(the text field still supports input in any format)."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "'hex'"
},
"showCancelButton": {
"type": "boolean",
"attribute": "show-cancel-button",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Whether to have a \"Cancel\" button which closes the popup."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
},
"showTextField": {
"type": "boolean",
"attribute": "show-text-field",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Whether to show a text field for color value editing."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "true"
},
"enableAlpha": {
"type": "boolean",
"attribute": "enable-alpha",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Whether to enable adjusting the alpha channel."
},
"getter": false,
"setter": false,
"reflect": false,
"defaultValue": "false"
}
};
}
static get states() {
return {
"widgetColor": {}
};
}
static get events() {
return [{
"method": "colorChanged",
"name": "colorChanged",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event whenever the color changes."
},
"complexType": {
"original": "Color",
"resolved": "Color",
"references": {
"Color": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Color"
}
}
}
}, {
"method": "invalidInput",
"name": "invalidInput",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event emitted when the input is invalid."
},
"complexType": {
"original": "any",
"resolved": "any",
"references": {}
}
}, {
"method": "submitted",
"name": "submitted",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event emitter when the user clicks \"Ok\"."
},
"complexType": {
"original": "Color",
"resolved": "Color",
"references": {
"Color": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Color"
}
}
}
}, {
"method": "popupOpened",
"name": "popupOpened",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event emitter when the popup opens."
},
"complexType": {
"original": "Color",
"resolved": "Color",
"references": {
"Color": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Color"
}
}
}
}, {
"method": "popupClosed",
"name": "popupClosed",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Event emitter when the popup closes."
},
"complexType": {
"original": "Color",
"resolved": "Color",
"references": {
"Color": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Color"
}
}
}
}];
}
static get methods() {
return {
"setPickerOption": {
"complexType": {
"signature": "(options: Options) => Promise<void>",
"parameters": [{
"name": "options",
"type": "Options",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"Options": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Options"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Set the picker options.\n\n(Usually a new .parent and .color).",
"tags": [{
"name": "param",
"text": "options"
}]
}
},
"movePopUp": {
"complexType": {
"signature": "(options: Options, openImmediately: boolean) => Promise<void>",
"parameters": [{
"name": "options",
"type": "Options",
"docs": ""
}, {
"name": "openImmediately",
"type": "boolean",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"Options": {
"location": "import",
"path": "./interfaces",
"id": "src/components/salla-color-picker/interfaces.ts::Options"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Move the popup to a different parent, optionally opening it at the same time.\n\n(Usually a new .parent and .color).",
"tags": [{
"name": "param",
"text": "option Whether to open the popup immediately."
}, {
"name": "param",
"text": "openImmediately"
}]
}
},
"setColorValue": {
"complexType": {
"signature": "(color: string, triggerEvent: boolean) => Promise<void>",
"parameters": [{
"name": "color",
"type": "string",
"docs": "If true, won't trigger onChange."
}, {
"name": "triggerEvent",
"type": "boolean",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Set/initialize the picker's color.\n\nColor name, RGBA/HSLA/HEX string, or RGBA array.",
"tags": [{
"name": "param",
"text": "color If true, won't trigger onChange."
}, {
"name": "param",
"text": "triggerEvent"
}]
}
},
"openPicker": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Show/open the picker.",
"tags": []
}
},
"closePicker": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Close/Hide the picker.",
"tags": []
}
},
"destroyPicker": {
"complexType": {
"signature": "() => Promise<void>",
"parameters": [],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Release all resources used by this picker instance.",
"tags": []
}
}
};
}
static get elementRef() { return "host"; }
}