Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1710 #1704 #1711

Merged
merged 7 commits into from
May 21, 2024
11 changes: 11 additions & 0 deletions apps/doc/src/app/components/tooltip/tooltip-example.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,20 @@
<ng-template
[(documentationPropertyValue)]="prizmTooltipTheme"
[documentationPropertyValues]="prizmTooltipThemeVariants"
[documentationPropertyDeprecated]="true"
documentationPropertyName="prizmHintTheme"
documentationPropertyType="PrizmTheme | null"
documentationPropertyMode="input"
>
use prizmTooltipTheme
</ng-template>

<ng-template
[(documentationPropertyValue)]="prizmTooltipTheme"
[documentationPropertyValues]="prizmTooltipThemeVariants"
documentationPropertyName="prizmTooltipTheme"
documentationPropertyType="PrizmTheme | null"
documentationPropertyMode="input"
>
Tooltip Theme (set theme for modal container)
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ export class PrizmConfirmPopupDirective<
@Output('prizmConfirmPopupShowed')
override prizmHintShowed = new EventEmitter<boolean>();

protected override readonly containerComponent = PrizmConfirmPopupContainerComponent;
protected override readonly onHoverActive = false;
public override readonly containerComponent = PrizmConfirmPopupContainerComponent;
public override readonly onHoverActive = false;

@HostListener('document:click', ['$event.target']) public onClick(target: HTMLElement): void {
if (this.elementRef.nativeElement.contains(target)) {
Expand Down
9 changes: 5 additions & 4 deletions libs/components/src/lib/directives/hint/hint.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,18 @@ export class PrizmHintDirective<
@Output()
readonly prizmHintShowed = new EventEmitter<boolean>();

protected readonly onHoverActive: boolean = true;
public onHoverActive: boolean = true;

content!: PolymorphContent;
overlay!: PrizmOverlayControl;
protected readonly containerComponent: Type<unknown> = PrizmHintContainerComponent;
protected readonly show$ = new Subject<boolean>();
public containerComponent: Type<unknown> = PrizmHintContainerComponent;
public readonly show$ = new Subject<boolean>();
protected readonly destroyListeners$ = new Subject<void>();

private readonly prizmOverlayService: PrizmOverlayService = inject(PrizmOverlayService);

private readonly renderer: Renderer2 = inject(Renderer2);
protected readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef);
public readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef);
private readonly destroy$: PrizmDestroyService = inject(PrizmDestroyService);
private readonly hoveredService: PrizmHoveredService = inject(PrizmHoveredService);

Expand Down Expand Up @@ -218,6 +218,7 @@ export class PrizmHintDirective<
.create({
parentInjector: this.injector,
});

if (this.onHoverActive) {
combineLatest([
this.hoveredService.createHovered$(this.host),
Expand Down
108 changes: 57 additions & 51 deletions libs/components/src/lib/directives/tooltip/tooltip.directive.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable @angular-eslint/no-input-rename */
import { Directive, EventEmitter, forwardRef, HostListener, Input, Output } from '@angular/core';
import { PrizmDestroyService, prizmGenerateId } from '@prizm-ui/helpers';
import { Directive, forwardRef, HostListener, inject, Input } from '@angular/core';
import { PrizmDestroyService } from '@prizm-ui/helpers';
import { PrizmTooltipContainerComponent } from './tooltip-container.component';
import { PRIZM_TOOLTIP_OPTIONS } from './tooltip-options';
import { prizmDefaultProp, prizmRequiredSetter } from '@prizm-ui/core';
import { PolymorphContent } from '../polymorph';
import { PRIZM_HINT_OPTIONS, PrizmHintOptions } from '../hint/hint-options';
import { PRIZM_HINT_OPTIONS } from '../hint/hint-options';
import { PrizmHintDirective } from '../hint/hint.directive';
import { PrizmTheme } from '@prizm-ui/theme';

@Directive({
selector: '[prizmTooltip]:not(ng-container)',
Expand All @@ -17,69 +17,75 @@ import { PrizmHintDirective } from '../hint/hint.directive';
useExisting: forwardRef(() => PRIZM_TOOLTIP_OPTIONS),
},
],
hostDirectives: [
{
directive: PrizmHintDirective,
inputs: [
'prizmAutoReposition: prizmAutoReposition',
'prizmHintHideDelay: prizmTooltipHideDelay',
'prizmHintDirection: prizmTooltipDirection',
'prizmHintId: prizmTooltipId',
'prizmHint: prizmTooltip',
'prizmHintShowDelay: prizmTooltipShowDelay',
'prizmHintTheme: prizmTooltipTheme',
'prizmHintHost: prizmTooltipHost',
'prizmHintContext: prizmTooltipContext',
'prizmHintCanShow: prizmTooltipCanShow',
],
outputs: ['prizmHintShowed: prizmTooltipShowed'],
},
],
exportAs: 'prizmTooltip',
})
export class PrizmTooltipDirective extends PrizmHintDirective {
@Input('prizmAutoReposition')
@prizmDefaultProp()
override prizmAutoReposition: PrizmHintOptions['autoReposition'] = this.options.autoReposition;

@Input('prizmTooltipDirection')
@prizmDefaultProp()
override prizmHintDirection: PrizmHintOptions['direction'] = this.options.direction;

@Input('prizmTooltipId')
@prizmDefaultProp()
override prizmHintId: string = 'hintId_' + prizmGenerateId();

@Input('prizmTooltipShowDelay')
@prizmDefaultProp()
override prizmHintShowDelay: PrizmHintOptions['showDelay'] = this.options.showDelay;

@Input('prizmTooltipHideDelay')
@prizmDefaultProp()
override prizmHintHideDelay: PrizmHintOptions['hideDelay'] = this.options.hideDelay;

@Input('prizmTooltipHost')
@prizmDefaultProp()
override prizmHintHost: HTMLElement | null = null;

@Input('prizmTooltipContext')
@prizmDefaultProp()
override prizmHintContext = {};

@Input('prizmTooltipCanShow')
@prizmDefaultProp()
override prizmHintCanShow = true;

@Input('prizmTooltip')
@prizmRequiredSetter()
override set prizmHint(value: PolymorphContent | null) {
export class PrizmTooltipDirective {
@Input()
set prizmTooltip(value: PolymorphContent | null) {
if (!value) {
this.content = '';
this.hostedHint.content = '';
return;
}

this.content = value;
this.hostedHint.content = value;
this.hostedHint.prizmHint = value;
}
get prizmTooltip() {
return this.hostedHint.content;
}

/**
* @deprecate since v4
* now for tooltip use only prizmTooltipTheme
* */
@Input()
set prizmHintTheme(theme: PrizmTheme | null) {
this.hostedHint.prizmHintTheme = theme;
}
/**
* @deprecate since v4
* now for tooltip use only prizmTooltipTheme
* */
get prizmHintTheme(): PrizmTheme | null {
return this.hostedHint.prizmHintTheme;
}

// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('prizmTooltipShowed')
override prizmHintShowed = new EventEmitter<boolean>();
public readonly hostedHint = inject(PrizmHintDirective);

constructor() {
this.hostedHint.containerComponent = PrizmTooltipContainerComponent;
this.hostedHint.onHoverActive = false;
}

protected override readonly containerComponent = PrizmTooltipContainerComponent;
protected override readonly onHoverActive = false;
protected clickedInside = false;
@HostListener('document:click', ['$event.target']) public onClick(target: HTMLElement): void {
if (this.overlay.viewEl?.contains(target)) return;
const clickedOnElement = this.elementRef.nativeElement.contains(target);
if (this.hostedHint.overlay?.viewEl?.contains(target)) return;
const clickedOnElement = this.hostedHint.elementRef.nativeElement.contains(target);
if (clickedOnElement && !this.clickedInside) this.clickedInside = true;
if (!this.clickedInside) return;
this.show$.next(clickedOnElement);
this.hostedHint.show$.next(clickedOnElement);
}

@HostListener('document:keydown.esc', ['$event'])
public closeOnEsc(): void {
if (this.show) this.show = false;
if (this.hostedHint.show) this.hostedHint.show = false;
}
}
128 changes: 99 additions & 29 deletions libs/doc/base/src/lib/components/host/host-element.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,40 +49,55 @@ export class PrizmDocHostElementService implements OnDestroy {
.subscribe();
}

/**
* Retrieves the input and output properties of an Angular component or directive.
*
* @template T - The type of the component class.
* @param {Type<T>} componentClass - The class of the Angular component or directive.
* @returns {object} An object containing input and output properties, their keys and values,
* the original metadata, and the component's selector.
* @throws {Error} If the provided class is not an Angular component or directive.
*/
private getListComponentInputsOutputs<T>(componentClass: Type<T>) {
const inputs = new Map<string, string>();
const outputs = new Map<string, string>();
const inputKeys = new Set<string>();
const inputValues = new Set<string>();
const outputKeys = new Set<string>();
const outputValues = new Set<string>();
let selector: string | null = null;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const componentMetadata = componentClass['ɵcmp'] || componentClass['ɵdir'];
if (componentMetadata) {
selector = componentMetadata.selectors?.[0]?.[0] as string;
const inputProperties = componentMetadata.inputs;
const outputProperties = componentMetadata.outputs;

for (const inputName in inputProperties) {
const classPropertyName = inputProperties[inputName];
const inputFromSet = inputs.get(classPropertyName);
if (inputFromSet && inputFromSet !== classPropertyName) continue;
inputs.set(classPropertyName, inputName);
}

for (const outputKey in outputProperties) {
const classPropertyName = outputProperties[outputKey];
const nameFromSet = outputs.get(classPropertyName);
if (nameFromSet && nameFromSet !== classPropertyName) continue;
outputs.set(classPropertyName, outputKey);
}
} else {
console.error('The provided class is not an Angular component.');
// Retrieve component metadata
const componentMetadata = (componentClass as any)['ɵcmp'] || (componentClass as any)['ɵdir'];
if (!componentMetadata) {
throw new Error('The provided class is not an Angular component or directive.');
}

// Extract the component's selector
selector = componentMetadata.selectors?.[0]?.[0] as string;

const hasHostDirectives = !!componentMetadata.hostDirectives?.length;
// Process collected properties and fill the sets
this.processProperties(inputKeys, inputValues, componentMetadata.inputs, !hasHostDirectives);
this.processProperties(outputKeys, outputValues, componentMetadata.outputs, !hasHostDirectives);

if (hasHostDirectives) {
this.processProperties(
inputKeys,
inputValues,
this.collectProperties(componentMetadata.hostDirectives, 'inputs')
);
this.processProperties(
outputKeys,
outputValues,
this.collectProperties(componentMetadata.hostDirectives, 'outputs')
);
}

// Return the collected information
return {
inputs: [...inputs.values()],
inputProperties: [...inputs.keys()],
outputs: [...outputs.values()],
outputProperties: [...outputs.keys()],
inputs: [...inputValues],
inputProperties: [...inputKeys],
outputs: [...outputValues],
outputProperties: [...outputKeys],
origin: {
inputs: componentMetadata.inputs,
outputs: componentMetadata.outputs,
Expand All @@ -91,10 +106,62 @@ export class PrizmDocHostElementService implements OnDestroy {
};
}

/**
* Processes the properties and fills the provided sets with keys and values.
*
* @param {Set<string>} keysSet - The set to store the property keys.
* @param {Set<string>} valuesSet - The set to store the property values.
* @param {{ [key: string]: string }} properties - The properties to process.
*/
private processProperties(
keysSet: Set<string>,
valuesSet: Set<string>,
properties: { [key: string]: string },
skipByKey = false
) {
for (const key in properties) {
if (key in properties) {
if (skipByKey) {
// if extended components > filter parent input and outputs
if (keysSet.has(properties[key])) continue;
valuesSet.add(key);
keysSet.add(properties[key]);
} else {
keysSet.add(key);
valuesSet.add(properties[key]);
}
}
}
}

/**
* Collects properties of a specific type (inputs or outputs) from the given directives.
*
* @param {any[]} directives - The array of host directives.
* @param {'inputs' | 'outputs'} propertyType - The type of properties to collect ('inputs' or 'outputs').
* @returns {{ [key: string]: string }} An object containing the collected properties.
*/
private collectProperties(
directives: any[],
propertyType: 'inputs' | 'outputs'
): { [key: string]: string } {
const properties: { [key: string]: string } = {};

if (directives) {
for (const directive of directives) {
const directiveProperties = directive[propertyType];
if (directiveProperties) {
Object.assign(properties, directiveProperties);
}
}
}

return properties;
}

private updateComponentInfo(listenerElementKey: string, el: ElementRef): void {
const currentOutputMap = this.outputMap.get(listenerElementKey) || new Map();
const metaComponentData = this.getListComponentInputsOutputs(el.nativeElement.constructor);

this.outputs.set(
listenerElementKey,
metaComponentData.outputs.map(i => ({
Expand Down Expand Up @@ -144,9 +211,12 @@ export class PrizmDocHostElementService implements OnDestroy {
eventRealKey: string,
hasNotListener = false
): void {
if (eventRealKey == null) return;
if (!el.nativeElement?.[eventRealKey]) {
console.error(`Prizm component output <${eventRealKey}> does not exists`, {
name: eventRealKey,
type,
hasNotListener,
el: el.nativeElement,
});
return;
Expand Down
1 change: 1 addition & 0 deletions libs/helpers/src/lib/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './merge';
export * from './order-by';
export * from './difference';
export * from './directive';
export * from './invert-object';
Loading
Loading