UNPKG

@acrodata/gradient-picker

Version:

A powerful and beautiful gradient picker.

966 lines (953 loc) 123 kB
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