diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index d47cb229a..6215501d9 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -8,7 +8,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DndModule } from 'ng2-dnd'; -import { ColorPickerModule } from 'ngx-color-picker'; import { CanDeactivateGuard, @@ -100,7 +99,6 @@ const routes: Routes = [ @NgModule({ imports: [ - ColorPickerModule, DndModule, SqxFrameworkModule, SqxSharedModule, diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 12d8ca479..ae1cef477 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -73,11 +73,7 @@ - + diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/src/Squidex/app/framework/angular/forms/code-editor.component.ts index 6d99cdef0..3bf6f7a04 100644 --- a/src/Squidex/app/framework/angular/forms/code-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/code-editor.component.ts @@ -18,7 +18,7 @@ import { declare var ace: any; -export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { +export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CodeEditorComponent), multi: true }; @@ -26,7 +26,7 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { selector: 'sqx-code-editor', styleUrls: ['./code-editor.component.scss'], templateUrl: './code-editor.component.html', - providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR], + providers: [SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) export class CodeEditorComponent extends ExternalControlComponent implements AfterViewInit { diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.html b/src/Squidex/app/framework/angular/forms/color-picker.component.html new file mode 100644 index 000000000..a891d3d98 --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/color-picker.component.html @@ -0,0 +1,17 @@ + + +
+
+
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.scss b/src/Squidex/app/framework/angular/forms/color-picker.component.scss new file mode 100644 index 000000000..85b930ea9 --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/color-picker.component.scss @@ -0,0 +1,18 @@ +@import '_mixins'; +@import '_vars'; + +:host /deep/ { + .color-picker { + & { + border-color: $color-border; + } + + .hex-text { + .box { + input { + border-color: $color-input; + } + } + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/color-picker.component.ts b/src/Squidex/app/framework/angular/forms/color-picker.component.ts new file mode 100644 index 000000000..321b173db --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/color-picker.component.ts @@ -0,0 +1,59 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { + MathHelper, + ModalModel, + StatefulControlComponent +} from '@app/framework/internal'; + +export const SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ColorPickerComponent), multi: true +}; + +interface State { + value?: string; + + foreground: string; +} + +@Component({ + selector: 'sqx-color-picker', + styleUrls: ['./color-picker.component.scss'], + templateUrl: './color-picker.component.html', + providers: [SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ColorPickerComponent extends StatefulControlComponent { + @Input() + public placeholder = ''; + + public modal = new ModalModel(); + + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { foreground: 'black' }); + } + + public writeValue(obj: any) { + let foreground = 'black'; + + if (MathHelper.toLuminance(MathHelper.parseColor(obj)!) < .5) { + foreground = 'white'; + } + + this.next(s => ({ ...s, value: obj, foreground })); + + this.callChange(obj); + } + + public blur() { + this.callTouched(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts index 428ae47c4..65692dd30 100644 --- a/src/Squidex/app/framework/angular/modals/modal-target.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-target.directive.ts @@ -15,10 +15,10 @@ import { positionModal } from '@app/shared'; selector: '[sqxModalTarget]' }) export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy { - private targetElement: any; + private targetElement: Element; @Input('sqxModalTarget') - public set target(element: any) { + public set target(element: Element) { if (element !== this.targetElement) { this.unsubscribeAll(); @@ -42,7 +42,7 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit constructor( private readonly renderer: Renderer2, - private readonly element: ElementRef + private readonly element: ElementRef ) { super(); } @@ -78,7 +78,11 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit const modalRef = this.element.nativeElement; const modalRect = this.element.nativeElement.getBoundingClientRect(); - const targetRect: ClientRect = this.targetElement.getBoundingClientRect(); + if (modalRect.width === 0 || modalRect.height === 0) { + return; + } + + const targetRect = this.targetElement.getBoundingClientRect(); let y = 0; let x = 0; diff --git a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts index 68069bc2b..8a1cb1a28 100644 --- a/src/Squidex/app/framework/angular/modals/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modals/modal-view.directive.ts @@ -87,7 +87,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { setTimeout(() => { this.startListening(); - }); + }, 1000); this.changeDetector.detectChanges(); } else if (!isOpen && this.renderedView) { diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 5b6604ccf..ca3e47aa7 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -8,6 +8,7 @@ export * from './angular/forms/autocomplete.component'; export * from './angular/forms/checkbox-group.component'; export * from './angular/forms/code-editor.component'; +export * from './angular/forms/color-picker.component'; export * from './angular/forms/confirm-click.directive'; export * from './angular/forms/control-errors.component'; export * from './angular/forms/copy.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 9f01654b1..816429552 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { ModuleWithProviders, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ColorPickerModule } from 'ngx-color-picker'; import { AnalyticsService, @@ -19,6 +20,7 @@ import { ClipboardService, CodeComponent, CodeEditorComponent, + ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, @@ -89,11 +91,13 @@ import { imports: [ FormsModule, CommonModule, - ReactiveFormsModule + ReactiveFormsModule, + ColorPickerModule ], declarations: [ AutocompleteComponent, CheckboxGroupComponent, + ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CodeComponent, @@ -158,6 +162,7 @@ import { CodeEditorComponent, CommonModule, CodeComponent, + ColorPickerComponent, ConfirmClickDirective, ControlErrorsComponent, CopyDirective, diff --git a/src/Squidex/app/framework/utils/math-helper.spec.ts b/src/Squidex/app/framework/utils/math-helper.spec.ts index af58f664c..8c4fbde93 100644 --- a/src/Squidex/app/framework/utils/math-helper.spec.ts +++ b/src/Squidex/app/framework/utils/math-helper.spec.ts @@ -71,4 +71,49 @@ describe('MathHelper', () => { expect(MathHelper.roundToMultipleOfTwo(13)).toBe(14); expect(MathHelper.roundToMultipleOfTwo(12.2)).toBe(12); }); + + it('should create color from long string', () => { + const color = MathHelper.parseColor('#336699')!; + + expect(color.r).toBe(0.2); + expect(color.g).toBe(0.4); + expect(color.b).toBe(0.6); + expect(color.a).toBe(1.0); + }); + + it('should create color from short string', () => { + const color = MathHelper.parseColor('#369')!; + + expect(color.r).toBe(0.2); + expect(color.g).toBe(0.4); + expect(color.b).toBe(0.6); + expect(color.a).toBe(1.0); + }); + + it('should create color from rgb string', () => { + const color = MathHelper.parseColor('rgb(51, 102, 153)')!; + + expect(color.r).toBe(0.2); + expect(color.g).toBe(0.4); + expect(color.b).toBe(0.6); + expect(color.a).toBe(1.0); + }); + + it('should create color from rgba string', () => { + const color = MathHelper.parseColor('rgba(51, 102, 153, 0.5)')!; + + expect(color.r).toBe(0.2); + expect(color.g).toBe(0.4); + expect(color.b).toBe(0.6); + expect(color.a).toBe(0.5); + }); + + it('should convert to luminance', () => { + expect(MathHelper.toLuminance(undefined!)).toBe(1); + + expect(MathHelper.toLuminance({ r: 0, g: 0, b: 0, a: 1 })).toBe(0); + expect(MathHelper.toLuminance({ r: 1, g: 1, b: 1, a: 1 })).toBe(1); + + expect(MathHelper.toLuminance({ r: 0.5, g: 0.5, b: 0.5, a: 1 })).toBe(0.5); + }); }); diff --git a/src/Squidex/app/framework/utils/math-helper.ts b/src/Squidex/app/framework/utils/math-helper.ts index 6a711380c..419464081 100644 --- a/src/Squidex/app/framework/utils/math-helper.ts +++ b/src/Squidex/app/framework/utils/math-helper.ts @@ -7,6 +7,65 @@ /* tslint:disable: no-bitwise */ +import { Types } from './types'; + +interface IColorDefinition { + regex: RegExp; + + process(bots: RegExpExecArray): Color; +} + +export interface Color { + r: number; + g: number; + b: number; + a: number; +} + +const ColorDefinitions: IColorDefinition[] = [ + { + regex: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*([\d\.]{1,})\)$/, + process: (bits) => { + return { + r: parseInt(bits[1], 10) / 255, + g: parseInt(bits[2], 10) / 255, + b: parseInt(bits[3], 10) / 255, + a: parseFloat(bits[4]) + }; + } + }, { + regex: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + process: (bits) => { + return { + r: parseInt(bits[1], 10) / 255, + g: parseInt(bits[2], 10) / 255, + b: parseInt(bits[3], 10) / 255, + a: 1 + }; + } + }, { + regex: /^(\w{2})(\w{2})(\w{2})$/, + process: (bits) => { + return { + r: parseInt(bits[1], 16) / 255, + g: parseInt(bits[2], 16) / 255, + b: parseInt(bits[3], 16) / 255, + a: 1 + }; + } + }, { + regex: /^(\w{1})(\w{1})(\w{1})$/, + process: (bits) => { + return { + r: parseInt(bits[1] + bits[1], 16) / 255, + g: parseInt(bits[2] + bits[2], 16) / 255, + b: parseInt(bits[3] + bits[3], 16) / 255, + a: 1 + }; + } + } +]; + export module MathHelper { export const EMPTY_GUID = '00000000-0000-0000-0000-000000000000'; @@ -83,4 +142,34 @@ export module MathHelper { return degree; } + + export function parseColor(value: string): Color | undefined { + if (!Types.isString(value)) { + return undefined; + } + + if (value.charAt(0) === '#') { + value = value.substr(1, 6); + } + + value = value.replace(/ /g, '').toLowerCase(); + + for (let colorDefinition of ColorDefinitions) { + const bits = colorDefinition.regex.exec(value); + + if (bits) { + return colorDefinition.process(bits); + } + } + + return undefined; + } + + export function toLuminance(color: Color) { + if (!color) { + return 1; + } + + return (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b) / color.a; + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/modal-positioner.ts b/src/Squidex/app/framework/utils/modal-positioner.ts index bca4ae7c4..e81ba3edc 100644 --- a/src/Squidex/app/framework/utils/modal-positioner.ts +++ b/src/Squidex/app/framework/utils/modal-positioner.ts @@ -18,7 +18,7 @@ const POSITION_RIGHT_CENTER = 'right'; const POSITION_RIGHT_TOP = 'right-top'; const POSITION_RIGHT_BOTTOM = 'right-bottom'; -export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportHeight: number, viewportWidth: number): { x: number, y: number } { +export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportWidth: number, viewportHeight: number): { x: number, y: number } { let y = 0; let x = 0;