diff --git a/projects/demo/src/app/app.module.ts b/projects/demo/src/app/app.module.ts index 0ec5033..17e81e5 100644 --- a/projects/demo/src/app/app.module.ts +++ b/projects/demo/src/app/app.module.ts @@ -12,6 +12,7 @@ import { DemoComponent } from './demo/demo.component'; import { OnSubmitErrorsComponent } from './on-submit-errors/on-submit-errors.component'; import { CustomFormComponent } from './custom-form-component/custom-form.component'; import { MySuperFormComponent } from './custom-form-component/my-super-form/my-super-form.component'; +import SimpleComponent from "./simple-comp/simple.component"; const routes: Routes = [ { @@ -46,6 +47,7 @@ const routes: Routes = [ OnSubmitErrorsComponent, CustomFormComponent, MySuperFormComponent, + SimpleComponent, ], providers: [], bootstrap: [AppComponent] diff --git a/projects/demo/src/app/demo/demo.component.html b/projects/demo/src/app/demo/demo.component.html index 2df4902..6c5f9df 100644 --- a/projects/demo/src/app/demo/demo.component.html +++ b/projects/demo/src/app/demo/demo.component.html @@ -11,6 +11,13 @@ + + + + E + + + @@ -35,8 +42,8 @@ - - + +
@@ -72,6 +79,14 @@ I am a small piece of text + + Tooltip with template + + + + + +
@@ -208,3 +223,4 @@ Save + diff --git a/projects/demo/src/app/simple-comp/simple.component.html b/projects/demo/src/app/simple-comp/simple.component.html new file mode 100644 index 0000000..d28433e --- /dev/null +++ b/projects/demo/src/app/simple-comp/simple.component.html @@ -0,0 +1 @@ +Hello {{text}}! diff --git a/projects/demo/src/app/simple-comp/simple.component.scss b/projects/demo/src/app/simple-comp/simple.component.scss new file mode 100644 index 0000000..7dcd791 --- /dev/null +++ b/projects/demo/src/app/simple-comp/simple.component.scss @@ -0,0 +1,3 @@ +:host { + background: orange; +} diff --git a/projects/demo/src/app/simple-comp/simple.component.ts b/projects/demo/src/app/simple-comp/simple.component.ts new file mode 100644 index 0000000..ddb8803 --- /dev/null +++ b/projects/demo/src/app/simple-comp/simple.component.ts @@ -0,0 +1,17 @@ +import { Component} from "@angular/core"; + +@Component({ + selector: 'app-simple', + templateUrl: './simple.component.html', + styleUrls: ['./simple.component.scss'], +}) +export default class SimpleComponent { + public text = 'world'; + + ngOnInit(): void { + setTimeout(() => { + console.log('update plz'); + this.text = 'hello again!'; + }, 1000); + } +} diff --git a/projects/klippa/ngx-enhancy-forms/package.json b/projects/klippa/ngx-enhancy-forms/package.json index 916f46d..cf2712f 100644 --- a/projects/klippa/ngx-enhancy-forms/package.json +++ b/projects/klippa/ngx-enhancy-forms/package.json @@ -1,6 +1,6 @@ { "name": "@klippa/ngx-enhancy-forms", - "version": "16.21.0", + "version": "16.22.12", "publishConfig": { "access": "public" }, diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.html b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.html index 33d1eee..2cc9bfd 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.html +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.html @@ -3,7 +3,7 @@
×
+
diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.scss b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.scss index fc74339..0364d26 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.scss +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.scss @@ -56,6 +56,13 @@ input { } } +.peakBtn { + border: none; + background: transparent; + height: 100%; + padding: 0; +} + .showErrors { border-color: $default-warning; } diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.ts b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.ts index 2064cb6..77c76d9 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.ts +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/text-input/text-input.component.ts @@ -1,4 +1,4 @@ -import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Component, EventEmitter, Input, Output, TemplateRef} from '@angular/core'; import {NG_VALUE_ACCESSOR} from '@angular/forms'; import {ValueAccessorBase} from '../value-accessor-base/value-accessor-base.component'; @@ -9,11 +9,25 @@ import {ValueAccessorBase} from '../value-accessor-base/value-accessor-base.comp providers: [{provide: NG_VALUE_ACCESSOR, useExisting: TextInputComponent, multi: true}], }) export class TextInputComponent extends ValueAccessorBase { + private isPeekingPassword = false; + @Input() placeholder: string; @Input() type: 'text' | 'password' = 'text'; @Input() clearable = false; @Input() icon: 'search'; @Input() hasBorderLeft = true; @Input() hasBorderRight = true; + @Input() passwordPeekIcon: TemplateRef; @Output() onBlur = new EventEmitter(); + + public togglePeakPassword(): void { + this.isPeekingPassword = !this.isPeekingPassword; + } + + public getType(): 'text' | 'password' { + if (this.type === 'text') { + return 'text'; + } + return this.isPeekingPassword ? 'text' : 'password'; + } } diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.html b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.html index 0e4a5f8..8432128 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.html +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.html @@ -6,5 +6,5 @@ [ngClass]="{showErrors: isInErrorState()}" #nativeInputRef /> -
+
diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.scss b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.scss index 7a087f8..879f7e8 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.scss +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.scss @@ -31,6 +31,11 @@ $height: 20px; height: $height; border: 1px solid $border-color; border-radius: $width; + background: #EAECF0; + + &.transparentBackground { + background: transparent; + } &:before { content: ''; diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.ts b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.ts index ea3df86..bc40045 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.ts +++ b/projects/klippa/ngx-enhancy-forms/src/lib/elements/toggle/toggle.component.ts @@ -9,4 +9,5 @@ import {ValueAccessorBase} from '../value-accessor-base/value-accessor-base.comp providers: [{provide: NG_VALUE_ACCESSOR, useExisting: ToggleComponent, multi: true}], }) export class ToggleComponent extends ValueAccessorBase { + @Input() transparentBackground = true; } diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/form/form-element/form-element.component.ts b/projects/klippa/ngx-enhancy-forms/src/lib/form/form-element/form-element.component.ts index ee87f4a..2b6804b 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/form/form-element/form-element.component.ts +++ b/projects/klippa/ngx-enhancy-forms/src/lib/form/form-element/form-element.component.ts @@ -5,7 +5,7 @@ import { ElementRef, Inject, InjectionToken, - Input, + Input, OnDestroy, Optional, TemplateRef, ViewChild @@ -17,6 +17,7 @@ import {isValueSet, stringIsSetAndFilled} from '../../util/values'; import {FormComponent} from '../form.component'; import {awaitableForNextCycle} from '../../util/angular'; import {getAllLimitingContainers} from '../../util/dom'; +import {Subscription} from "rxjs"; export const FORM_ERROR_MESSAGES = new InjectionToken('form.error.messages'); @@ -33,12 +34,14 @@ export const DEFAULT_ERROR_MESSAGES: FormErrorMessages = { date: 'Enter a valid date', }; +type PopupState = 'onHover' | 'lockedOpen'; + @Component({ selector: 'klp-form-element', templateUrl: './form-element.component.html', styleUrls: ['./form-element.component.scss'], }) -export class FormElementComponent implements AfterViewInit { +export class FormElementComponent implements AfterViewInit, OnDestroy { public attachedControl: AbstractControl; @Input() public caption: string; @Input() public direction: 'horizontal' | 'vertical' = 'horizontal'; @@ -60,7 +63,8 @@ export class FormElementComponent implements AfterViewInit { public customErrorHandlers: Array<{ error: string; templateRef: TemplateRef }> = []; private input: ValueAccessorBase; public errorFullyVisible: boolean; - private popupState: 'lockedOpen' | 'lockedClosed' | 'onHover' = 'onHover'; + private popupState: PopupState = 'onHover'; + private subscriptions: Array = []; constructor( @Optional() private parent: FormComponent, @@ -72,11 +76,12 @@ export class FormElementComponent implements AfterViewInit { async ngAfterViewInit(): Promise { await awaitableForNextCycle(); this.fieldInput?.setTailTpl(this.tailTpl); - this.fieldInput?.onTouch.asObservable().subscribe((e) => { + const subscription = this.fieldInput?.onTouch.asObservable().subscribe(() => { this.determinePopupState(); }); - - [...getAllLimitingContainers(this.elRef.nativeElement), window].forEach(e => e.addEventListener('scroll', this.setErrorTooltipOffset)); + if (isValueSet(subscription)) { + this.subscriptions.push(subscription); + } } public shouldShowErrorMessages(): boolean { @@ -95,22 +100,40 @@ export class FormElementComponent implements AfterViewInit { this.input = input; - this.attachedControl.statusChanges.subscribe((e) => { + const subscription = this.attachedControl.statusChanges.subscribe(() => { this.determinePopupState(); }); + this.subscriptions.push(subscription); this.determinePopupState(); } public determinePopupState(): void { + const prevState = this.popupState; if (stringIsSetAndFilled(this.getErrorToShow())) { this.popupState = 'onHover'; - return; - } - if (isValueSet(this.getWarningToShow())) { + } else if (isValueSet(this.getWarningToShow())) { this.popupState = 'lockedOpen'; + } else { + this.popupState = 'onHover'; + } + + this.setUpErrorTooltipListeners(prevState, this.popupState); + } + + private setUpErrorTooltipListeners(prev: PopupState, current: PopupState): void { + if (prev === current) { return; } - this.popupState = 'onHover'; + const containers = [...getAllLimitingContainers(this.elRef.nativeElement), window]; + if (current === 'lockedOpen') { + containers.forEach(e => { + e.addEventListener('scroll', this.setErrorTooltipOffset); + }); + } else { + containers.forEach(e => { + e.removeEventListener('scroll', this.setErrorTooltipOffset); + }); + } } public unregisterControl(formControl: UntypedFormControl): void { @@ -169,10 +192,11 @@ export class FormElementComponent implements AfterViewInit { } getScrollableParent(node): any { - if (node == null) { - return null; + if (node === window.document.documentElement) { + return window.document.documentElement; } - if (node.scrollHeight > node.clientHeight) { + const overflowY = getComputedStyle(node).overflowY; + if (node.clientHeight < node.scrollHeight && (overflowY === 'auto' || overflowY === 'scroll')) { return node; } else { return this.getScrollableParent(node.parentNode); @@ -180,9 +204,16 @@ export class FormElementComponent implements AfterViewInit { } scrollTo(): void { - this.internalComponentRef.nativeElement.scrollIntoView(true); - // to give some breathing room, we scroll 100px more to the top - this.getScrollableParent(this.internalComponentRef.nativeElement)?.scrollBy(0, -100); + const parent = this.getScrollableParent(this.internalComponentRef.nativeElement); + const parentTop = parent === window.document.documentElement ? 0 : parent.getBoundingClientRect().top; + const elementTop = this.internalComponentRef.nativeElement.getBoundingClientRect().top; + const parentScrollTop = parent.scrollTop; + const answer = elementTop - parentTop + parentScrollTop; + + parent.scrollTo({ + top: answer - 30, + behavior: 'smooth' + }); } isRequired(): boolean { @@ -242,7 +273,9 @@ export class FormElementComponent implements AfterViewInit { } public closePopup(): void { + const prevState = this.popupState; this.popupState = 'onHover'; + this.setUpErrorTooltipListeners(prevState, this.popupState); } public togglePopup(): void { @@ -252,11 +285,13 @@ export class FormElementComponent implements AfterViewInit { if (this.errorFullyVisible) { return; } + const prevState = this.popupState; if (this.popupState === 'lockedOpen') { this.popupState = 'onHover'; } else { this.popupState = 'lockedOpen'; } + this.setUpErrorTooltipListeners(prevState, this.popupState); } public setErrorTooltipOffset = (): void => { @@ -268,4 +303,8 @@ export class FormElementComponent implements AfterViewInit { this.fixedWrapper.nativeElement.style.transform = `translateY(${popupOffsetY}px)`; } }; + + ngOnDestroy(): void { + this.subscriptions.forEach(e => e.unsubscribe()); + } } diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/form/form.component.ts b/projects/klippa/ngx-enhancy-forms/src/lib/form/form.component.ts index 3add587..e2c86d5 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/form/form.component.ts +++ b/projects/klippa/ngx-enhancy-forms/src/lib/form/form.component.ts @@ -286,7 +286,7 @@ export class FormComponent implements OnInit, OnDestroy, OnChanges { formGroupValue): Promise { if (this.formGroup.invalid) { - this.activeControls.find((e) => !e.formControl.valid)?.formElement?.scrollTo(); + this.activeControls.find((e) => e.formControl.invalid)?.formElement?.scrollTo(); this.setDisabledStatesForAllControls(originalDisabledStates); return Promise.reject(invalidFieldsSymbol); } else { diff --git a/projects/klippa/ngx-enhancy-forms/src/lib/withTooltip.component.ts b/projects/klippa/ngx-enhancy-forms/src/lib/withTooltip.component.ts index 8a25e8e..1e5b5a9 100644 --- a/projects/klippa/ngx-enhancy-forms/src/lib/withTooltip.component.ts +++ b/projects/klippa/ngx-enhancy-forms/src/lib/withTooltip.component.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, Input} from "@angular/core"; +import {ApplicationRef, Directive, ElementRef, Input, OnChanges, SimpleChanges, TemplateRef} from "@angular/core"; import {stringIsSetAndFilled} from "./util/values"; const triangleSize = '12px'; @@ -12,25 +12,33 @@ const colors = { @Directive({ selector: '[klpWithTooltip]' }) -export class WithTooltipDirective { +export class WithTooltipDirective implements OnChanges{ private div: HTMLElement; private triangle: HTMLElement; private triangleWhite: HTMLElement; @Input() klpWithTooltip: 'orange'| 'black' = 'orange'; @Input() tooltipText: string; - constructor(el: ElementRef) { + @Input() tooltipTemplate: TemplateRef; + @Input() position: 'top' | 'bottom' = 'top'; + private templateInstance: HTMLElement; + constructor(private el: ElementRef, private appRef: ApplicationRef) { el.nativeElement.addEventListener('mouseenter', () => { - const textToDisplay = this.tooltipText || el.nativeElement.innerText.trim(); + let textToDisplay: string; + if (!this.templateInstance) { + textToDisplay = this.tooltipText || el.nativeElement.innerText.trim(); + } if (!stringIsSetAndFilled(this.klpWithTooltip)) { return; } - if (textToDisplay.length < 1) { + if (!stringIsSetAndFilled(textToDisplay) && !this.tooltipTemplate) { return; } if (stringIsSetAndFilled(this.tooltipText)) { if (this.tooltipText === el.nativeElement.innerText) { return; } + } else if (this.tooltipTemplate) { + // no need to check here, just render the template } else { if (el.nativeElement.offsetWidth >= el.nativeElement.scrollWidth) { return; @@ -45,8 +53,13 @@ export class WithTooltipDirective { this.div.style.color = `${colors[this.klpWithTooltip].noAlpha}`; this.div.style.position = 'fixed'; this.div.style.left = `${el.nativeElement.getBoundingClientRect().x}px`; - this.div.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; - this.div.style.transform = `translate(calc(-100% + ${el.nativeElement.getBoundingClientRect().width}px), calc(-100% - 0.3rem))`; + if (this.position === 'top') { + this.div.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; + this.div.style.transform = `translate(calc(-100% + ${el.nativeElement.getBoundingClientRect().width}px), calc(-100% - 0.3rem))`; + } else if (this.position === 'bottom') { + this.div.style.top = `${el.nativeElement.getBoundingClientRect().y + el.nativeElement.getBoundingClientRect().height}px`; + this.div.style.transform = `translate(calc(-100% + ${el.nativeElement.getBoundingClientRect().width}px), calc(0% + 0.3rem))`; + } this.div.style.maxWidth = '200px'; this.div.style.whiteSpace = 'break-spaces'; this.div.style.backgroundColor = 'white'; @@ -55,15 +68,31 @@ export class WithTooltipDirective { this.div.style.padding = '0.3rem 0.5rem'; this.div.style.boxSizing = 'border-box'; this.div.style.borderRadius = '3px'; - this.div.textContent = textToDisplay; + if (stringIsSetAndFilled(textToDisplay)) { + this.div.textContent = textToDisplay; + } else if (this.templateInstance) { + this.div.style.maxWidth = 'none'; + this.div.style.visibility = 'hidden'; + this.div.appendChild(this.templateInstance); + setTimeout(() => { + const color = getComputedStyle(this.templateInstance).backgroundColor || getComputedStyle(this.templateInstance).background; + this.div.style.backgroundColor = color; + this.div.style.visibility = 'visible'; + }); + } el.nativeElement.prepend(this.div); this.triangle = document.createElement('div'); this.triangle.style.zIndex = `${zIndexStart + 1}`; this.triangle.style.position = 'fixed'; this.triangle.style.left = `calc(${el.nativeElement.getBoundingClientRect().x + el.nativeElement.getBoundingClientRect().width}px - 2rem)`; - this.triangle.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; - this.triangle.style.transform = `translate(-50%, calc(-100% + 0.1rem))`; + if (this.position === 'top') { + this.triangle.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; + this.triangle.style.transform = `translate(-50%, calc(-100% + 0.1rem))`; + } else if (this.position === 'bottom') { + this.triangle.style.top = `${el.nativeElement.getBoundingClientRect().y + el.nativeElement.getBoundingClientRect().height}px`; + this.triangle.style.transform = `translate(-50%, 0rem) rotate(180deg)`; + } this.triangle.style.width = '0'; this.triangle.style.height = '0'; this.triangle.style.borderLeft = `${triangleSize} solid transparent`; @@ -75,13 +104,29 @@ export class WithTooltipDirective { this.triangleWhite.style.zIndex = `${zIndexStart + 3}`; this.triangleWhite.style.position = 'fixed'; this.triangleWhite.style.left = `calc(${el.nativeElement.getBoundingClientRect().x + el.nativeElement.getBoundingClientRect().width}px - 2rem)`; - this.triangleWhite.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; - this.triangleWhite.style.transform = `translate(-50%, calc(-100% + 0.1rem - 2px))`; + if (this.position === 'top') { + this.triangleWhite.style.top = `${el.nativeElement.getBoundingClientRect().y}px`; + this.triangleWhite.style.transform = `translate(-50%, calc(-100% + 0.1rem - 2px))`; + } else if (this.position === 'bottom') { + this.triangleWhite.style.top = `${el.nativeElement.getBoundingClientRect().y + el.nativeElement.getBoundingClientRect().height}px`; + this.triangleWhite.style.transform = `translate(-50%, -2px) rotate(180deg)`; + } this.triangleWhite.style.width = '0'; this.triangleWhite.style.height = '0'; this.triangleWhite.style.borderLeft = `${triangleSize} solid transparent`; this.triangleWhite.style.borderRight = `${triangleSize} solid transparent`; - this.triangleWhite.style.borderTop = `${triangleSize} solid white`; + + if (stringIsSetAndFilled(textToDisplay)) { + this.triangleWhite.style.borderTop = `${triangleSize} solid white`; + } else if (this.templateInstance) { + this.div.style.visibility = 'hidden'; + setTimeout(() => { + const color = getComputedStyle(this.templateInstance).backgroundColor || getComputedStyle(this.templateInstance).background; + this.triangleWhite.style.borderTop = `${triangleSize} solid ${color}`; + this.div.style.visibility = 'visible'; + }); + } + el.nativeElement.prepend(this.triangleWhite); }); @@ -97,4 +142,12 @@ export class WithTooltipDirective { } catch (ex) {} }); } + + public ngOnChanges(simpleChanges: SimpleChanges): void { + if (simpleChanges.tooltipTemplate?.currentValue) { + const viewRef = this.tooltipTemplate.createEmbeddedView(null); + this.appRef.attachView(viewRef); + this.templateInstance = viewRef.rootNodes[0]; + } + } }