@acrodata/gradient-picker
Version:
A powerful and beautiful gradient picker.
966 lines (953 loc) • 123 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, ViewEncapsulation, ChangeDetectionStrategy, Input, inject, ChangeDetectorRef, booleanAttribute, forwardRef, ElementRef, EventEmitter, ViewChild, Output } from '@angular/core';
import * as i1 from '@angular/forms';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import { CdkDrag } from '@angular/cdk/drag-drop';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { TinyColor } from '@ctrl/tinycolor';
import * as i1$1 from 'ngx-color/chrome';
import { ColorChromeModule } from 'ngx-color/chrome';
class GradientFormGroup {
label = '';
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientFormGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: GradientFormGroup, isStandalone: true, selector: "gradient-form-group", inputs: { label: "label" }, host: { classAttribute: "gradient-form-group" }, ngImport: i0, template: `
@if (label) {
<label class="gradient-form-label" for="" [title]="label">{{ label }}</label>
}
<ng-content />
`, isInline: true, styles: [".gradient-form-group{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:4px var(--gp-container-horizontal-padding, 12px);font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit)}.gradient-form-group .gradient-input-field,.gradient-form-group .gradient-unit-input{flex:1}.gradient-form-label{width:48px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientFormGroup, decorators: [{
type: Component,
args: [{ selector: 'gradient-form-group', standalone: true, imports: [], template: `
@if (label) {
<label class="gradient-form-label" for="" [title]="label">{{ label }}</label>
}
<ng-content />
`, host: {
class: 'gradient-form-group',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".gradient-form-group{display:flex;align-items:center;justify-content:space-between;gap:4px;padding:4px var(--gp-container-horizontal-padding, 12px);font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit)}.gradient-form-group .gradient-input-field,.gradient-form-group .gradient-unit-input{flex:1}.gradient-form-label{width:48px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
}], propDecorators: { label: [{
type: Input
}] } });
class GradientInputField {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientInputField, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: GradientInputField, isStandalone: true, selector: "gradient-input-field", host: { classAttribute: "gradient-input-field" }, ngImport: i0, template: `
<ng-content />
`, isInline: true, styles: [".gradient-input-field{position:relative;display:inline-flex;align-items:center;flex-wrap:wrap;min-height:24px;background-color:var(--gp-input-background-color, #f5f5f5);outline:1px solid var(--gp-input-outline-color, #e6e6e6);outline-offset:-1px;border-radius:var(--gp-input-shape, 4px);color:var(--gp-container-text-color, rgba(0, 0, 0, .9));font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit);overflow:hidden}.gradient-input-field:hover{outline-color:var(--gp-input-hover-outline-color, #d6d6d6)}.gradient-input-field:focus-within{outline-color:var(--gp-input-focus-outline-color, #0d99ff)}.gradient-input-field .gradient-colorpicker-toggle{margin:2px}.gradient-input-field input,.gradient-input-field select{flex:1;width:100%;height:var(--gp-input-height, 24px);padding:var(--gp-input-padding, 0 4px);border:none;background:none;font-family:inherit;font-size:inherit;color:inherit;-webkit-font-smoothing:antialiased}.gradient-input-field input:focus,.gradient-input-field select:focus{outline:none}.gradient-input-field input[type=number]{appearance:textfield}.gradient-input-field input[type=number]::-webkit-outer-spin-button,.gradient-input-field input[type=number]::-webkit-inner-spin-button{appearance:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientInputField, decorators: [{
type: Component,
args: [{ selector: 'gradient-input-field', standalone: true, imports: [], template: `
<ng-content />
`, host: {
class: 'gradient-input-field',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".gradient-input-field{position:relative;display:inline-flex;align-items:center;flex-wrap:wrap;min-height:24px;background-color:var(--gp-input-background-color, #f5f5f5);outline:1px solid var(--gp-input-outline-color, #e6e6e6);outline-offset:-1px;border-radius:var(--gp-input-shape, 4px);color:var(--gp-container-text-color, rgba(0, 0, 0, .9));font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit);overflow:hidden}.gradient-input-field:hover{outline-color:var(--gp-input-hover-outline-color, #d6d6d6)}.gradient-input-field:focus-within{outline-color:var(--gp-input-focus-outline-color, #0d99ff)}.gradient-input-field .gradient-colorpicker-toggle{margin:2px}.gradient-input-field input,.gradient-input-field select{flex:1;width:100%;height:var(--gp-input-height, 24px);padding:var(--gp-input-padding, 0 4px);border:none;background:none;font-family:inherit;font-size:inherit;color:inherit;-webkit-font-smoothing:antialiased}.gradient-input-field input:focus,.gradient-input-field select:focus{outline:none}.gradient-input-field input[type=number]{appearance:textfield}.gradient-input-field input[type=number]::-webkit-outer-spin-button,.gradient-input-field input[type=number]::-webkit-inner-spin-button{appearance:none}\n"] }]
}] });
function split(input, separator = ',') {
const result = [];
let l = 0;
let parentCount = 0;
separator = new RegExp(separator);
for (let i = 0; i < input.length; i++) {
if (input[i] === '(') {
parentCount++;
}
else if (input[i] === ')') {
parentCount--;
}
if (parentCount === 0 && separator.test(input[i])) {
result.push(input.slice(l, i).trim());
l = i + 1;
}
}
result.push(input.slice(l).trim());
return result;
}
function resolveStops(v) {
const stops = [];
for (let i = 0, n = v.length; i < n; i++) {
const [color, offset, offset2] = split(v[i], /\s+/);
if (isHint(v[i])) {
stops.push({
color: '',
offset: resolveLength(v[i]),
hint: resolveLength(v[i]),
});
}
else {
stops.push({
color,
offset: resolveLength(offset),
});
if (offset2) {
stops.push({
color,
offset: resolveLength(offset2),
});
}
}
}
return stops;
}
const REGEX = /^(-?\d*\.?\d*)(%|vw|vh|px|em|rem|deg|rad|grad|turn|ch|vmin|vmax)?$/;
function isHint(v) {
return REGEX.test(v);
}
function resolveLength(v) {
if (!v)
return undefined;
const [, value, unit] = v.trim().match(REGEX) || [];
return { value: Number(value), unit: unit ?? 'px' };
}
const positionKeyword = new Set(['center', 'left', 'top', 'right', 'bottom']);
function isPositionKeyword(v) {
return positionKeyword.has(v) || isNaN(parseFloat(v));
}
function extendPosition(v) {
const res = Array(2).fill('');
for (let i = 0; i < 2; i++) {
// If the x position is the length, the y position should also be the length
// at 100% => at 100% 50%
if (!v[i])
res[i] = i == 0 || isPositionKeyword(v[i - 1]) ? 'center' : '50%';
else
res[i] = v[i];
}
return res;
}
function resolvePosition(v = '') {
let posArr = extendPosition(v.split(' ').filter(v => v));
// Correct the positions of x and y
// top center => center top
// center left => left center
if (['top', 'bottom'].includes(posArr[0]) || ['left', 'right'].includes(posArr[1])) {
posArr = posArr.reverse();
}
const position = {
x: { type: 'keyword', value: 'center' },
y: { type: 'keyword', value: 'center' },
};
position.x = isPositionKeyword(posArr[0])
? { type: 'keyword', value: posArr[0] }
: { type: 'length', value: posArr[0] };
position.y = isPositionKeyword(posArr[1])
? { type: 'keyword', value: posArr[1] }
: { type: 'length', value: posArr[1] };
return position;
}
function splitByColorInterp(input) {
const regex = /\bin\s+([a-z0-9-]+(?:\s+(?:shorter|longer|increasing|decreasing)\s+hue)?)\b/i;
const match = input.match(regex);
if (!match) {
return [input];
}
// match[0]: in color interpolation method
const matchedStr = match[0];
// match[1]: color interpolation method
const colorInterpMethod = match[1];
const parts = input.split(matchedStr);
const remainingStr = (parts[0] + parts[1]).trim();
return [remainingStr, colorInterpMethod];
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method
function resolveColorInterp(input) {
const [space, ...method] = input.split(' ');
return {
space: space,
method: method.length > 0 ? method.join(' ') : undefined,
};
}
class GradientUnitInput {
cdr = inject(ChangeDetectorRef);
disabled = false;
units = [];
value = null;
unit = '';
onChange = () => { };
onTouched = () => { };
writeValue(value) {
const vu = resolveLength(value);
if (vu) {
this.value = vu.value;
this.unit = vu.unit;
}
this.cdr.markForCheck();
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
onValueChange() {
const value = this.value != null ? this.value + this.unit : '';
this.onChange(value);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientUnitInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: GradientUnitInput, isStandalone: true, selector: "gradient-unit-input", inputs: { disabled: ["disabled", "disabled", booleanAttribute], units: "units" }, host: { classAttribute: "gradient-unit-input" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GradientUnitInput),
multi: true,
},
], ngImport: i0, template: `
<input type="number" [(ngModel)]="value" (change)="onValueChange()" />
<select [(ngModel)]="unit" (change)="onValueChange()">
@for (unit of units; track $index) {
<option [value]="unit">{{ unit }}</option>
}
</select>
`, isInline: true, styles: [".gradient-unit-input{display:inline-flex;min-height:24px;background-color:var(--gp-input-background-color, #f5f5f5);outline:1px solid var(--gp-input-outline-color, #e6e6e6);outline-offset:-1px;border-radius:var(--gp-input-shape, 4px);color:var(--gp-container-text-color, rgba(0, 0, 0, .9));font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit);overflow:hidden}.gradient-unit-input:hover{outline-color:var(--gp-input-hover-outline-color, #d6d6d6)}.gradient-unit-input:focus-within{outline-color:var(--gp-input-focus-outline-color, #0d99ff)}.gradient-unit-input input,.gradient-unit-input select{appearance:none;height:var(--gp-input-height, 24px);padding:var(--gp-input-padding, 0 4px);border:none;background:none;font-family:inherit;font-size:inherit;color:inherit;-webkit-font-smoothing:antialiased}.gradient-unit-input input:focus,.gradient-unit-input select:focus{outline:none}.gradient-unit-input input{flex:1;width:100%}.gradient-unit-input input[type=number]{appearance:textfield}.gradient-unit-input input[type=number]::-webkit-outer-spin-button,.gradient-unit-input input[type=number]::-webkit-inner-spin-button{appearance:none}.gradient-unit-input select{text-align:center}.gradient-unit-input select:hover,.gradient-unit-input select:focus{background-color:var(--gp-unit-select-hover-background-color, rgba(0, 0, 0, .12))}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientUnitInput, decorators: [{
type: Component,
args: [{ selector: 'gradient-unit-input', standalone: true, imports: [FormsModule], template: `
<input type="number" [(ngModel)]="value" (change)="onValueChange()" />
<select [(ngModel)]="unit" (change)="onValueChange()">
@for (unit of units; track $index) {
<option [value]="unit">{{ unit }}</option>
}
</select>
`, host: {
class: 'gradient-unit-input',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GradientUnitInput),
multi: true,
},
], styles: [".gradient-unit-input{display:inline-flex;min-height:24px;background-color:var(--gp-input-background-color, #f5f5f5);outline:1px solid var(--gp-input-outline-color, #e6e6e6);outline-offset:-1px;border-radius:var(--gp-input-shape, 4px);color:var(--gp-container-text-color, rgba(0, 0, 0, .9));font-size:var(--gp-container-text-size, 12px);font-family:var(--gp-container-text-font, inherit);overflow:hidden}.gradient-unit-input:hover{outline-color:var(--gp-input-hover-outline-color, #d6d6d6)}.gradient-unit-input:focus-within{outline-color:var(--gp-input-focus-outline-color, #0d99ff)}.gradient-unit-input input,.gradient-unit-input select{appearance:none;height:var(--gp-input-height, 24px);padding:var(--gp-input-padding, 0 4px);border:none;background:none;font-family:inherit;font-size:inherit;color:inherit;-webkit-font-smoothing:antialiased}.gradient-unit-input input:focus,.gradient-unit-input select:focus{outline:none}.gradient-unit-input input{flex:1;width:100%}.gradient-unit-input input[type=number]{appearance:textfield}.gradient-unit-input input[type=number]::-webkit-outer-spin-button,.gradient-unit-input input[type=number]::-webkit-inner-spin-button{appearance:none}.gradient-unit-input select{text-align:center}.gradient-unit-input select:hover,.gradient-unit-input select:focus{background-color:var(--gp-unit-select-hover-background-color, rgba(0, 0, 0, .12))}\n"] }]
}], propDecorators: { disabled: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], units: [{
type: Input
}] } });
class GradientCheckbox {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: GradientCheckbox, isStandalone: true, selector: "[gradientCheckbox]", host: { classAttribute: "gradient-checkbox" }, ngImport: i0, template: `
<ng-content />
`, isInline: true, styles: [".gradient-checkbox{display:inline-flex;align-items:center;gap:4px}.gradient-checkbox input[type=checkbox]{width:auto;height:auto;margin:0}.gradient-checkbox input[type=checkbox]:focus-visible{outline:1px solid var(--gp-input-focus-outline-color, #0d99ff)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientCheckbox, decorators: [{
type: Component,
args: [{ selector: '[gradientCheckbox]', standalone: true, imports: [], template: `
<ng-content />
`, host: {
class: 'gradient-checkbox',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".gradient-checkbox{display:inline-flex;align-items:center;gap:4px}.gradient-checkbox input[type=checkbox]{width:auto;height:auto;margin:0}.gradient-checkbox input[type=checkbox]:focus-visible{outline:1px solid var(--gp-input-focus-outline-color, #0d99ff)}\n"] }]
}] });
class GradientRadioButton {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientRadioButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: GradientRadioButton, isStandalone: true, selector: "[gradientRadioButton]", host: { classAttribute: "gradient-radio-button" }, ngImport: i0, template: `
<ng-content />
`, isInline: true, styles: [".gradient-radio-button{display:inline-flex;align-items:center;flex:1;gap:8px;padding:var(--gp-input-padding, 0 4px)}.gradient-radio-button input[type=radio]{flex:unset;width:auto;height:auto;margin:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientRadioButton, decorators: [{
type: Component,
args: [{ selector: '[gradientRadioButton]', standalone: true, imports: [], template: `
<ng-content />
`, host: {
class: 'gradient-radio-button',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".gradient-radio-button{display:inline-flex;align-items:center;flex:1;gap:8px;padding:var(--gp-input-padding, 0 4px)}.gradient-radio-button input[type=radio]{flex:unset;width:auto;height:auto;margin:0}\n"] }]
}] });
class GradientIconButton {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientIconButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: GradientIconButton, isStandalone: true, selector: "gradient-icon-button", host: { classAttribute: "gradient-icon-button" }, ngImport: i0, template: `
<ng-content />
`, isInline: true, styles: [".gradient-icon-button{display:inline-block;width:24px;height:24px}.gradient-icon-button button{width:100%;height:100%;padding:0;color:var(--gp-icon-button-text-color, inherit);background-color:var(--gp-icon-button-background-color, transparent);border:none;border-radius:var(--gp-icon-button-shape, 4px)}.gradient-icon-button button:hover{background-color:var(--gp-icon-button-hover-background-color, rgba(0, 0, 0, .06))}.gradient-icon-button button:active{background-color:var(--gp-icon-button-active-background-color, rgba(0, 0, 0, .12))}.gradient-icon-button button:focus-visible{background-color:var(--gp-icon-button-focus-background-color, transparent);outline:1px solid var(--gp-icon-button-focus-outline-color, #0d99ff);outline-offset:-1px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientIconButton, decorators: [{
type: Component,
args: [{ selector: 'gradient-icon-button', standalone: true, imports: [], template: `
<ng-content />
`, host: {
class: 'gradient-icon-button',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".gradient-icon-button{display:inline-block;width:24px;height:24px}.gradient-icon-button button{width:100%;height:100%;padding:0;color:var(--gp-icon-button-text-color, inherit);background-color:var(--gp-icon-button-background-color, transparent);border:none;border-radius:var(--gp-icon-button-shape, 4px)}.gradient-icon-button button:hover{background-color:var(--gp-icon-button-hover-background-color, rgba(0, 0, 0, .06))}.gradient-icon-button button:active{background-color:var(--gp-icon-button-active-background-color, rgba(0, 0, 0, .12))}.gradient-icon-button button:focus-visible{background-color:var(--gp-icon-button-focus-background-color, transparent);outline:1px solid var(--gp-icon-button-focus-outline-color, #0d99ff);outline-offset:-1px}\n"] }]
}] });
class GradientColorpickerToggle {
cdr = inject(ChangeDetectorRef);
elementRef = inject(ElementRef);
colorpicker = null;
triggerEvent = 'click';
overlayOrigin = this.elementRef;
color = '';
ngOnInit() {
if (this.colorpicker) {
this.colorpicker.overlayOrigin = this.overlayOrigin;
}
}
onClick(e) {
if (this.colorpicker && this.triggerEvent === 'click') {
this.colorpicker.overlayOrigin = this.overlayOrigin;
this.colorpicker.toggle();
}
}
onDblClick(e) {
if (this.colorpicker && this.triggerEvent === 'dblclick') {
this.colorpicker.overlayOrigin = this.overlayOrigin;
this.colorpicker.toggle();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientColorpickerToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: GradientColorpickerToggle, isStandalone: true, selector: "gradient-colorpicker-toggle", inputs: { colorpicker: ["for", "colorpicker"], triggerEvent: "triggerEvent", overlayOrigin: "overlayOrigin", color: "color" }, host: { classAttribute: "gradient-colorpicker-toggle" }, ngImport: i0, template: `
<button
type="button"
[class.gradient-colorpicker-empty-color]="!color"
[style.background-color]="color"
(click)="onClick($event)"
(dblclick)="onDblClick($event)"
>
toggle
</button>
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientColorpickerToggle, decorators: [{
type: Component,
args: [{
selector: 'gradient-colorpicker-toggle',
standalone: true,
imports: [],
template: `
<button
type="button"
[class.gradient-colorpicker-empty-color]="!color"
[style.background-color]="color"
(click)="onClick($event)"
(dblclick)="onDblClick($event)"
>
toggle
</button>
`,
host: {
class: 'gradient-colorpicker-toggle',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
}]
}], propDecorators: { colorpicker: [{
type: Input,
args: ['for']
}], triggerEvent: [{
type: Input
}], overlayOrigin: [{
type: Input
}], color: [{
type: Input
}] } });
class GradientColorpicker {
cdr = inject(ChangeDetectorRef);
elementRef = inject(ElementRef);
disabled = false;
overlayOrigin = this.elementRef;
isOpen = false;
color = '';
format = 'hex';
onChange = () => { };
onTouched = () => { };
writeValue(value) {
if (value) {
this.color = value;
this.getFormat();
}
this.cdr.markForCheck();
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
onColorChange(e) {
this.color = {
hex: e.color.rgb.a === 1 ? e.color.hex : new TinyColor(e.color.rgb).toHex8String(),
rgb: new TinyColor(e.color.rgb).toRgbString(),
hsl: new TinyColor(e.color.hsl).toHslString(),
hsv: new TinyColor(e.color.hsv).toHsvString(),
}[this.format];
this.cdr.markForCheck();
this.onChange(this.color);
}
getFormat() {
const color = new TinyColor(this.color);
if (color.format === 'rgb' || color.format === 'hsl' || color.format === 'hsv') {
this.format = color.format;
}
else {
this.format = 'hex';
}
this.cdr.markForCheck();
}
open() {
this.isOpen = true;
this.cdr.markForCheck();
}
close() {
this.isOpen = false;
this.cdr.markForCheck();
}
toggle() {
this.isOpen = !this.isOpen;
this.cdr.markForCheck();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientColorpicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "18.2.7", type: GradientColorpicker, isStandalone: true, selector: "gradient-colorpicker", inputs: { disabled: ["disabled", "disabled", booleanAttribute], overlayOrigin: "overlayOrigin" }, host: { classAttribute: "gradient-colorpicker" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GradientColorpicker),
multi: true,
},
], ngImport: i0, template: "<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"overlayOrigin\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n (overlayOutsideClick)=\"close()\"\n (detach)=\"close()\"\n>\n <color-chrome\n class=\"gradient-colorpicker-panel\"\n [color]=\"color\"\n (onChangeComplete)=\"onColorChange($event)\"\n />\n</ng-template>\n", styles: [".gradient-colorpicker-panel input{background-color:inherit}.gradient-colorpicker-toggle{display:inline-flex;width:20px;height:20px;background-image:conic-gradient(transparent 25%,#ccc 25% 50%,transparent 50% 75%,#ccc 75%);background-size:8px 8px;background-color:#fff;border-radius:2px}.gradient-colorpicker-toggle>button{position:relative;display:inline-block;width:100%;height:100%;padding:0;border:none;border-radius:inherit;background-color:#fff;text-indent:-9999px;cursor:inherit;outline:none}.gradient-colorpicker-toggle>button:focus{outline:2px solid var(--gp-input-focus-outline-color, #0d99ff)}.gradient-colorpicker-empty-color:after{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(to bottom right,transparent 47%,red 47% 53%,transparent 53%)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: ColorChromeModule }, { kind: "component", type: i1$1.ChromeComponent, selector: "color-chrome", inputs: ["disableAlpha"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: GradientColorpicker, decorators: [{
type: Component,
args: [{ selector: 'gradient-colorpicker', standalone: true, imports: [FormsModule, ColorChromeModule, CdkConnectedOverlay], host: {
class: 'gradient-colorpicker',
}, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GradientColorpicker),
multi: true,
},
], template: "<ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"overlayOrigin\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n (overlayOutsideClick)=\"close()\"\n (detach)=\"close()\"\n>\n <color-chrome\n class=\"gradient-colorpicker-panel\"\n [color]=\"color\"\n (onChangeComplete)=\"onColorChange($event)\"\n />\n</ng-template>\n", styles: [".gradient-colorpicker-panel input{background-color:inherit}.gradient-colorpicker-toggle{display:inline-flex;width:20px;height:20px;background-image:conic-gradient(transparent 25%,#ccc 25% 50%,transparent 50% 75%,#ccc 75%);background-size:8px 8px;background-color:#fff;border-radius:2px}.gradient-colorpicker-toggle>button{position:relative;display:inline-block;width:100%;height:100%;padding:0;border:none;border-radius:inherit;background-color:#fff;text-indent:-9999px;cursor:inherit;outline:none}.gradient-colorpicker-toggle>button:focus{outline:2px solid var(--gp-input-focus-outline-color, #0d99ff)}.gradient-colorpicker-empty-color:after{content:\"\";position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(to bottom right,transparent 47%,red 47% 53%,transparent 53%)}\n"] }]
}], propDecorators: { disabled: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], overlayOrigin: [{
type: Input
}] } });
function normalizeDirectionalValue(v) {
v = v.trim().replace(/\s+/g, ' ');
const map = {
'left top': 'top left',
'right top': 'top right',
'left bottom': 'bottom left',
'right bottom': 'bottom right',
};
return map[v] || v;
}
function resolveLinearOrientation(angle) {
if (angle.startsWith('to ')) {
return {
type: 'directional',
value: normalizeDirectionalValue(angle.replace('to ', '')),
};
}
if (['turn', 'deg', 'grad', 'rad'].some(unit => angle.endsWith(unit))) {
return {
type: 'angular',
value: angle,
};
}
return null;
}
function parseLinearGradient(input) {
if (!/^(repeating-)?linear-gradient/.test(input))
throw new SyntaxError(`could not find syntax for this item: ${input}`);
const [, repeating, props] = input
.replace(/[\n\t]/g, '')
.match(/(repeating-)?linear-gradient\((.+)\)/);
const result = {
repeating: Boolean(repeating),
orientation: { type: 'directional', value: 'bottom' },
stops: [],
};
const properties = split(props);
const [prefixStr, colorInterpStr] = splitByColorInterp(properties[0]);
const orientation = resolveLinearOrientation(prefixStr);
if (orientation) {
result.orientation = orientation;
}
if (colorInterpStr) {
result.color = resolveColorInterp(colorInterpStr);
}
if (orientation || colorInterpStr) {
properties.shift();
}
return { ...result, stops: resolveStops(properties) };
}
function stringifyLinearGradient(input) {
const { repeating, orientation, color, stops } = input;
const type = repeating ? 'repeating-linear-gradient' : 'linear-gradient';
const prefixArr = [];
const orientationVal = orientation.value.trim()
? orientation.type === 'angular'
? orientation.value
: 'to ' + orientation.value
: '';
if (orientationVal) {
prefixArr.push(orientationVal);
}
if (color && color.space) {
prefixArr.push(`in ${color.space} ${color.method || ''}`.trim());
}
const props = [];
if (prefixArr.length > 0) {
props.push(prefixArr.join(' '));
}
const stopsStr = stops
.map(s => `${s.color} ${s.offset?.value}${s.offset?.unit}`.trim())
.join(', ');
props.push(stopsStr);
return `${type}(${props.join(', ')})`;
}
const rgExtentKeywords = new Set([
'closest-corner',
'closest-side',
'farthest-corner',
'farthest-side',
]);
function isRgExtentKeyword(v) {
return rgExtentKeywords.has(v);
}
function isColor(v) {
if (/(circle|ellipse|at|in)/.test(v) || rgExtentKeywords.has(v))
return false;
return /^(rgba?|hwb|hsl|lab|lch|oklab|color|#|[a-zA-Z]+)/.test(v);
}
function parseRadialGradient(input) {
if (!/(repeating-)?radial-gradient/.test(input))
throw new SyntaxError(`could not find syntax for this item: ${input}`);
const [, repeating, props] = input
.replace(/[\n\t]/g, '')
.match(/(repeating-)?radial-gradient\((.+)\)/);
const result = {
repeating: Boolean(repeating),
shape: 'ellipse',
size: [
{
type: 'keyword',
value: 'farthest-corner',
},
],
position: {
x: { type: 'keyword', value: 'center' },
y: { type: 'keyword', value: 'center' },
},
stops: [],
};
const properties = split(props);
// handle like radial-gradient(rgba(0,0,0,0), #ee7621)
if (isColor(properties[0])) {
return { ...result, stops: resolveStops(properties) };
}
const [prefixStr, colorInterpStr] = splitByColorInterp(properties[0]);
const prefix = prefixStr.split('at').map(v => v.trim());
const shape = ((prefix[0] || '').match(/(circle|ellipse)/) || [])[1];
const unitKeywordReg =
// eslint-disable-next-line max-len
/(-?\d+\.?\d*(vw|vh|px|em|rem|%|rad|grad|turn|deg)?|closest-corner|closest-side|farthest-corner|farthest-side)/g;
const size = (prefix[0] || '').match(unitKeywordReg) || [];
if (!shape) {
if (size.length === 1 && !isRgExtentKeyword(size[0])) {
result.shape = 'circle';
}
else {
result.shape = 'ellipse';
}
}
else {
result.shape = shape;
}
if (size.length === 0) {
size.push('farthest-corner');
}
result.size = size.map(v => {
if (isRgExtentKeyword(v)) {
return { type: 'keyword', value: v };
}
else {
return { type: 'length', value: v };
}
});
result.position = resolvePosition(prefix[1]);
if (colorInterpStr) {
result.color = resolveColorInterp(colorInterpStr);
}
if (shape || size.length > 0 || prefix[1])
properties.shift();
return {
...result,
stops: resolveStops(properties),
};
}
function stringifyRadialGradient(input) {
const { repeating, shape, size, position, color, stops } = input;
const type = repeating ? 'repeating-radial-gradient' : 'radial-gradient';
const sizes = size.map(s => s.value);
const posX = position.x.value;
const posY = position.y.value;
const pos = posX.trim() || posY.trim() ? 'at ' + `${posX} ${posY}`.trim() : '';
const prefixArr = [`${shape} ${sizes.join(' ')} ${pos}`];
if (color && color.space) {
prefixArr.push(`in ${color.space} ${color.method || ''}`.trim());
}
const stopsStr = stops
.map(s => `${s.color} ${s.offset?.value}${s.offset?.unit}`.trim())
.join(', ');
return `${type}(${prefixArr.join(' ')}, ${stopsStr})`;
}
const set = new Set(['from', 'in', 'at']);
function resolvePrefix(k, props, start, end) {
switch (k) {
case 'from':
return { angle: props.slice(start, end).join(' ') };
case 'at':
return { position: resolvePosition(props.slice(start, end).join(' ')) };
case 'in': {
const arr = props.slice(start, end);
return {
color: resolveColorInterp(arr.join(' ')),
};
}
default:
return null;
}
}
function parseConicGradient(input) {
if (!/(repeating-)?conic-gradient/.test(input))
throw new SyntaxError(`could not find syntax for this item: ${input}`);
const [, repeating, props] = input
.replace(/[\n\t]/g, '')
.match(/(repeating-)?conic-gradient\((.+)\)/);
const result = {
repeating: Boolean(repeating),
angle: '0deg',
position: {
x: { type: 'keyword', value: 'center' },
y: { type: 'keyword', value: 'center' },
},
stops: [],
};
const properties = split(props).map(v => v.trim());
const prefix = split(properties[0], /\s+/);
let k = '';
let j = 0;
for (let i = 0, n = prefix.length; i < n; i++) {
if (set.has(prefix[i])) {
if (i > 0) {
Object.assign(result, resolvePrefix(k, prefix, j, i));
}
k = prefix[i];
j = i + 1;
}
}
if (k) {
Object.assign(result, resolvePrefix(k, prefix, j, prefix.length));
properties.shift();
}
return { ...result, stops: resolveStops(properties) };
}
function stringifyConicGradient(input) {
const { repeating, angle, position, color, stops } = input;
const type = repeating ? 'repeating-conic-gradient' : 'conic-gradient';
const prefixArr = [];
if (angle.trim()) {
prefixArr.push(`from ${angle}`);
}
const posX = position.x.value;
const posY = position.y.value;
const pos = posX.trim() || posY.trim() ? 'at ' + `${posX} ${posY}`.trim() : '';
if (pos) {
prefixArr.push(pos);
}
if (color && color.space) {
prefixArr.push(`in ${color.space} ${color.method || ''}`.trim());
}
const props = [];
if (prefixArr.length > 0) {
props.push(prefixArr.join(' '));
}
const stopsStr = stops
.map(s => `${s.color} ${s.offset?.value}${s.offset?.unit}`.trim())
.join(', ');
props.push(stopsStr);
return `${type}(${props.join(', ')})`;
}
/**
* Reorder an element at a specified index by condition
*
* @param array The original array
* @param index The element at this index will be checked and moved to its correct sorted location.
* @param compareWith1 The comparison function used to determine if the element needs to move left.
* @param compareWith2 The comparison function used to determine if the element needs to move right.
* @param callback The callback function after the elements have been swapped.
* @returns
*/
function reorderElementByCondition(array = [], index = 0, compareWith1 = (a, b) => a < b, compareWith2 = (a, b) => a > b, callback) {
// Make a copy to avoid modifying the original array reference
const newArr = [...array];
if (index < 0 || index >= newArr.length) {
return array;
}
// Now, we need to move this potentially out-of-place element
// to its correct sorted position.
// This is essentially an insertion sort pass for a single element.
let i = index;
while (i > 0 && compareWith1(newArr[i], newArr[i - 1])) {
// Swap elements
[newArr[i], newArr[i - 1]] = [newArr[i - 1], newArr[i]];
i--;
callback?.(i);
}
while (i < newArr.length - 1 && compareWith2(newArr[i], newArr[i + 1])) {
// Swap elements
[newArr[i], newArr[i + 1]] = [newArr[i + 1], newArr[i]];
i++;
callback?.(i);
}
return newArr;
}
/**
* Linearly interpolate between two colors.
*
* @param fromColor The starting color in any format supported by TinyColor.
* @param toColor The ending color in any format supported by TinyColor.
* @param percentage The interpolation percentage between 0 (`fromColor`) and 1 (`toColor`)
* @returns
*/
function interpolateColor(fromColor, toColor, percentage = 0.5) {
const c1 = new TinyColor(fromColor);
const c2 = new TinyColor(toColor);
// Convert to premultiplied alpha
const c1_pre = {
r: c1.r * c1.a,
g: c1.g * c1.a,
b: c1.b * c1.a,
a: c1.a,
};
const c2_pre = {
r: c2.r * c2.a,
g: c2.g * c2.a,
b: c2.b * c2.a,
a: c2.a,
};
// Linearly interpolate the premultiplied RGBA components
const interpolatedR_pre = c1_pre.r * (1 - percentage) + c2_pre.r * percentage;
const interpolatedG_pre = c1_pre.g * (1 - percentage) + c2_pre.g * percentage;
const interpolatedB_pre = c1_pre.b * (1 - percentage) + c2_pre.b * percentage;
const interpolatedA = c1_pre.a * (1 - percentage) + c2_pre.a * percentage;
// Convert back to non-premultiplied alpha format (if alpha is not 0)
const finalR = interpolatedA > 0 ? interpolatedR_pre / interpolatedA : 0;
const finalG = interpolatedA > 0 ? interpolatedG_pre / interpolatedA : 0;
const finalB = interpolatedA > 0 ? interpolatedB_pre / interpolatedA : 0;
const finalColor = new TinyColor({
r: Math.round(finalR),
g: Math.round(finalG),
b: Math.round(finalB),
a: interpolatedA,
});
return interpolatedA === 1 ? finalColor.toHexString() : finalColor.toRgbString();
}
/**
* Fill undefined offset in stops.
*
* @param stops
* @returns
*/
function fillUndefinedOffsets(stops) {
if (stops.length === 0)
return stops;
// Ensure the start and end positions are defined.
if (!stops[0] || stops[0].offset == null) {
stops[0].offset = { value: 0, unit: '%' };
}
const lastIndex = stops.length - 1;
if (!stops[lastIndex] || stops[lastIndex].offset == null) {
stops[lastIndex].offset = { value: 100, unit: '%' };
}
stops.forEach((item, index) => {
if (item.offset != null)
return;
// Find the nearest defined offset to the left of the current item by using
// findIndex to search backward from the current index.
const startIndex = stops
.slice(0, index)
.reverse()
.findIndex(x => x.offset != null);
const prevDefinedIndex = index - 1 - startIndex;
const startOffsetValue = stops[prevDefinedIndex].offset.value;
// Find the nearest defined offset to the right of the current item by using
// findIndex to search forward from the current index.
const endIndex = stops.slice(index + 1).findIndex(x => x.offset != null);
const nextDefinedIndex = index + 1 + endIndex;
const endOffsetValue = stops[nextDefinedIndex].offset.value;
// Calculate the number of gaps between two defined values.
const totalGaps = nextDefinedIndex - prevDefinedIndex;
const totalDifference = endOffsetValue - startOffsetValue;
// Calculate the index of the current undefined value within the entire gaps.
const gapIndex = index - prevDefinedIndex;
const newOffsetValue = startOffsetValue + (gapIndex / totalGaps) * totalDifference;
item.offset = { value: newOffsetValue, unit: '%' };
});
return stops;
}
/**
* Reverse the color stops array.
*
* @param stops
* @returns
*/
function reverseColorStops(stops) {
return stops.reverse().map(stop => {
if (stop.offset?.value != null) {
stop.offset.value = 100 - stop.offset.value;
}
return stop;
});
}
/**
* Convert angle to percentage (e.g. `45deg`, `0.25turn`, `3.14rad`, `100grad`).
*
* @param value
* @param unit
* @returns
*/
function angleToPercentage(value, unit) {
let degrees;
switch (unit) {
case 'deg':
degrees = value;
break;
case 'rad':
degrees = value * (180 / Math.PI);
break;
case 'turn':
degrees = value * 360;
break;
case 'grad':
degrees = value * 0.9;
break;
default:
return value;
}
// Calculate the percentage within 360 degrees and ensure the
// percentage value is between 0 and 100.
let percentage = (degrees / 360) * 100;
// Handle negative values or values exceeding 360 degrees by using
// the modulo operator to constrain the angle within [0, 360).
if (percentage < 0) {
percentage = (percentage % 100) + 100;
}
else if (percentage >= 100) {
percentage = percentage % 100;
}
return percentage;
}
/**
* Convert angle values in the gradient stops array to percentages.
*
* @param stops
* @returns
*/
function convertAngleToPercentage(stops) {
return stops.map(stop => {
if (stop.offset && angleUnits.includes(stop.offset.unit)) {
const { value, unit } = stop.offset;
stop.offset.value = angleToPercentage(value, unit);
stop.offset.unit = '%';
}
return stop;
});
}
/**
* A unified function for parsing all gradient types.
*
* @param input
*/
function parseGradient(input) {
if (input.includes('linear')) {
return parseLinearGradient(input);
}
else if (input.includes('radial')) {
return parseRadialGradient(input);
}
else if (input.includes('conic')) {
return parseConicGradient(input);
}
else {
return null;
}
}
const angleUnits = ['deg', 'rad', 'turn', 'grad'];
const lengthUnits = ['%', 'px', 'em', 'rem', 'vw', 'vh', 'ch'];
const positionXKeywords = ['left', 'center', 'right'];
const positionYKeywords = ['top', 'center', 'bottom'];
const rectangularColorSpaces = [
'srgb',
'srgb-linear',
'display-p3',
'a98-rgb',
'prophoto-rgb',
'rec2020',
'lab',
'oklab',
'xyz',
'xyz-d50',
'xyz-d65',
];
const polarColorSpaces = ['hsl', 'hwb', 'lch', 'oklch'];
const hueInterpolationMethods = [
'shorter hue',
'longer hue',
'increasing hue',
'decreasing hue',
];
let uniqueIdCounter = 0;
class GradientStops {
cdr = inject(ChangeDetectorRef);
elementRef = inject(ElementRef);
track;
disabled = false;
colorStops = [];
colorStopsChange = new EventEmitter();
sliderColorStops = [];
trackWidth = 0;
gradientColor = '';
isDragging = false;
selectedStop;
onChange = () => { };
onTouched = () => { };
ngOnChanges(changes) {
if (changes['colorStops']) {
this.getStops();
this.getGradientColor();
}
}
ngAfterViewInit() {
this.getStops();
this.getGradientColor();
}
writeValue(value) {
if (Array.isArray(value)) {
this.colorStops = value;
this.getStops();
this.getGradientColor();
}
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
getStops() {
if (!this.track)
return;
this.trackWidth = this.track.nativeElement.offsetWidth;
this.sliderColorStops = fillUndefinedOffsets(convertAngleToPercentage(this.colorStops)).map(stop => {
const offset = stop.offset || { value: 0, unit: '%' };
const posX = Math.min(offset.unit === '%' ? (offset.value / 100) * this.trackWidth : offset.value, thi