@ngneat/helipopper
Version:
A Powerful Tooltip and Popover for Angular Applications
769 lines (759 loc) • 35.2 kB
JavaScript
import * as i0 from '@angular/core';
import { ElementRef, inject, NgZone, Injectable, booleanAttribute, InjectionToken, input, output, model, EventEmitter, computed, DestroyRef, PLATFORM_ID, Injector, effect, untracked, Directive, Inject, Output } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable, defer, from, map as map$1, of, shareReplay, Subject, fromEvent, merge } from 'rxjs';
import { auditTime, map, switchMap, filter, takeUntil } from 'rxjs/operators';
import * as i1 from '@ngneat/overview';
import { isString, isComponent, isTemplateRef } from '@ngneat/overview';
import { TIPPY_LOADER, TIPPY_CONFIG } from '@ngneat/helipopper/config';
// Let's retrieve the native `IntersectionObserver` implementation hidden by
// `__zone_symbol__IntersectionObserver`. This would be the unpatched version of
// the observer present in zone.js environments.
// Otherwise, if the user is using zoneless change detection (and zone.js is not included),
// we fall back to the native implementation. Accessing the native implementation
// allows us to remove `runOutsideAngular` calls and reduce indentation,
// making the code a bit more readable.
const IntersectionObserver = globalThis['__zone_symbol__IntersectionObserver'] ||
globalThis.IntersectionObserver;
let supportsIntersectionObserver = false;
let supportsResizeObserver = false;
if (typeof window !== 'undefined') {
supportsIntersectionObserver = 'IntersectionObserver' in window;
supportsResizeObserver = 'ResizeObserver' in window;
}
function inView(host, options = {
root: null,
threshold: 0.3,
}) {
const element = coerceElement(host);
return new Observable((subscriber) => {
if (!supportsIntersectionObserver) {
subscriber.next();
subscriber.complete();
return;
}
const observer = new IntersectionObserver((entries) => {
// Several changes may occur in the same tick, we want to check the latest entry state.
const entry = entries[entries.length - 1];
if (entry.isIntersecting) {
subscriber.next();
subscriber.complete();
}
}, options);
observer.observe(element);
return () => observer.disconnect();
});
}
function isElementOverflow(host) {
// Don't access the `offsetWidth` multiple times since it triggers layout updates.
const hostOffsetWidth = host.offsetWidth;
return (hostOffsetWidth > host.parentElement.offsetWidth ||
hostOffsetWidth < host.scrollWidth);
}
function overflowChanges(host) {
const element = coerceElement(host);
return dimensionsChanges(element).pipe(auditTime(150), map(() => isElementOverflow(element)));
}
function dimensionsChanges(target) {
return new Observable((subscriber) => {
if (!supportsResizeObserver) {
subscriber.next();
subscriber.complete();
return;
}
const observer = new ResizeObserver(() => subscriber.next(true));
observer.observe(target);
return () => observer.disconnect();
});
}
function onlyTippyProps(allProps) {
const tippyProps = {};
const ownProps = [
'useTextContent',
'variations',
'useHostWidth',
'defaultVariation',
'beforeRender',
'isLazy',
'variation',
'isEnabled',
'className',
'onlyTextOverflow',
'data',
'content',
'context',
'hideOnEscape',
'customHost',
'injector',
'preserveView',
'vcr',
'popperWidth',
'zIndexGetter',
'staticWidthHost',
];
const overriddenMethods = ['onShow', 'onHidden', 'onCreate'];
Object.keys(allProps).forEach((prop) => {
if (!ownProps.includes(prop) && !overriddenMethods.includes(prop)) {
tippyProps[prop] = allProps[prop];
}
});
return tippyProps;
}
function normalizeClassName(className) {
const classes = typeof className === 'string' ? className.split(' ') : className;
return classes.map((klass) => klass?.trim()).filter(Boolean);
}
function coerceCssPixelValue(value) {
if (value == null) {
return '';
}
return typeof value === 'string' ? value : `${value}px`;
}
function coerceElement(element) {
return element instanceof ElementRef ? element.nativeElement : element;
}
let observer;
const elementHiddenHandlers = new WeakMap();
function observeVisibility(host, hiddenHandler) {
observer ??= new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
elementHiddenHandlers.get(entry.target)();
}
});
});
elementHiddenHandlers.set(host, hiddenHandler);
observer.observe(host);
return () => {
elementHiddenHandlers.delete(host);
observer.unobserve(host);
};
}
// We need to use `isPromise` instead of checking whether
// `value instanceof Promise`. In zone.js patched environments, `global.Promise`
// is the `ZoneAwarePromise`.
// `import(...)` returns a native promise (not a `ZoneAwarePromise`), causing
// `instanceof` check to be falsy.
function isPromise(value) {
return typeof value?.then === 'function';
}
class TippyFactory {
constructor() {
this._ngZone = inject(NgZone);
this._loader = inject(TIPPY_LOADER);
this._tippyImpl$ = null;
}
/**
* This returns an observable because the user should provide a `loader`
* function, which may return a promise if the tippy.js library is to be
* loaded asynchronously.
*/
create(target, props) {
// We use `shareReplay` to ensure that subsequent emissions are
// synchronous and to avoid triggering the `defer` callback repeatedly
// when new subscribers arrive.
this._tippyImpl$ ||= defer(() => {
const maybeTippy = this._ngZone.runOutsideAngular(() => this._loader());
return isPromise(maybeTippy)
? from(maybeTippy).pipe(map$1((tippy) => tippy.default))
: of(maybeTippy);
}).pipe(shareReplay());
return this._tippyImpl$.pipe(map$1((tippy) => {
return this._ngZone.runOutsideAngular(() => tippy(target, props));
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyFactory, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyFactory, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Transforms a value (typically a string) to a boolean.
* Intended to be used as a transform function of an input.
*
* @see https://material.angular.io/cdk/coercion/overview
*/
const coerceBooleanAttribute = booleanAttribute;
const TIPPY_REF = /* @__PURE__ */ new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'TIPPY_REF' : '');
function injectTippyRef() {
const instance = inject(TIPPY_REF, { optional: true });
if (!instance) {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
throw new Error('tp is not provided in the current context or on one of its ancestors');
}
else {
throw new Error(`[tp]: ${1 /* TippyErrorCode.TippyNotProvided */}`);
}
}
return instance;
}
// These are the default values used by `tippy.js`.
// We are providing them as default input values.
// The `tippy.js` repository has been archived and is unlikely to
// change in the future, so it is safe to use these values as defaults.
const defaultAppendTo = () => document.body;
const defaultDelay = 0;
const defaultDuration = [300, 250];
const defaultInteractiveBorder = 2;
const defaultMaxWidth = 350;
const defaultOffset = [0, 10];
const defaultPlacement = 'top';
const defaultTrigger = 'mouseenter focus';
const defaultTriggerTarget = null;
const defaultZIndex = 9999;
const defaultAnimation = 'fade';
class TippyDirective {
// It should be a getter because computations are cached until
// any of the producers change.
get hostWidth() {
return this.host().getBoundingClientRect().width;
}
constructor(globalConfig, injector, viewService, vcr, ngZone, hostRef) {
this.globalConfig = globalConfig;
this.injector = injector;
this.viewService = viewService;
this.vcr = vcr;
this.ngZone = ngZone;
this.hostRef = hostRef;
this.appendTo = input(defaultAppendTo, {
alias: 'tpAppendTo',
});
this.content = input('', { alias: 'tp' });
this.delay = input(defaultDelay, {
alias: 'tpDelay',
});
this.duration = input(defaultDuration, {
alias: 'tpDuration',
});
this.hideOnClick = input(true, {
alias: 'tpHideOnClick',
});
this.interactive = input(false, {
alias: 'tpInteractive',
});
this.interactiveBorder = input(defaultInteractiveBorder, {
alias: 'tpInteractiveBorder',
});
this.maxWidth = input(defaultMaxWidth, {
alias: 'tpMaxWidth',
});
// Note that some of the input signal types are declared explicitly because the compiler
// also uses types from `@popperjs/core` and requires a type annotation.
this.offset = input(defaultOffset, {
alias: 'tpOffset',
});
this.placement = input(defaultPlacement, {
alias: 'tpPlacement',
});
this.popperOptions = input({}, {
alias: 'tpPopperOptions',
});
this.showOnCreate = input(false, {
alias: 'tpShowOnCreate',
});
this.trigger = input(defaultTrigger, {
alias: 'tpTrigger',
});
this.triggerTarget = input(defaultTriggerTarget, {
alias: 'tpTriggerTarget',
});
this.zIndex = input(defaultZIndex, {
alias: 'tpZIndex',
});
this.animation = input(defaultAnimation, {
alias: 'tpAnimation',
});
this.useTextContent = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpUseTextContent',
});
this.isLazy = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpIsLazy',
});
this.variation = input(undefined, { alias: 'tpVariation' });
this.isEnabled = input(true, { alias: 'tpIsEnabled' });
this.className = input('', { alias: 'tpClassName' });
this.onlyTextOverflow = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpOnlyTextOverflow',
});
this.staticWidthHost = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpStaticWidthHost',
});
this.data = input(undefined, { alias: 'tpData' });
this.useHostWidth = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpUseHostWidth',
});
this.hideOnEscape = input(false, {
transform: coerceBooleanAttribute,
alias: 'tpHideOnEscape',
});
this.popperWidth = input(undefined, {
alias: 'tpPopperWidth',
});
this.customHost = input(undefined, { alias: 'tpHost' });
this.onShow = output({ alias: 'tpOnShow' });
this.onHide = output({ alias: 'tpOnHide' });
this.isVisible = model(false, { alias: 'tpIsVisible' });
this.visible = new EventEmitter();
this.viewRef = null;
this.variationDefined = false;
this.viewOptions$ = null;
/**
* We had use `visible` event emitter previously as a `takeUntil` subscriber in multiple places
* within the directive.
* This is for internal use only; thus we don't have to deal with the `visible` event emitter
* and trigger change detections only when the `visible` event is being listened outside
* in the template (`<button [tippy]="..." (visible)="..."></button>`).
*/
this.visibleInternal = new Subject();
this.visibilityObserverCleanup = null;
this.contentChanged = new Subject();
this.host = computed(() => this.customHost() || this.hostRef.nativeElement);
this.destroyRef = inject(DestroyRef);
this.isServer = isPlatformServer(inject(PLATFORM_ID));
this.tippyFactory = inject(TippyFactory);
this.destroyed = false;
if (this.isServer)
return;
this.setupListeners();
this.destroyRef.onDestroy(() => {
this.destroyed = true;
this.instance?.destroy();
this.destroyView();
this.visibilityObserverCleanup?.();
});
}
ngOnChanges(changes) {
if (this.isServer)
return;
const variation = this.variation() || this.globalConfig.defaultVariation || '';
const props = Object.keys(changes)
// `isVisible` is not required as a prop since we update it manually
// in an effect-like manner.
.filter((key) => key !== 'isVisible')
.reduce((accumulator, key) => ({ ...accumulator, [key]: changes[key].currentValue }), { ...this.globalConfig.variations?.[variation] });
this.updateProps(props);
}
ngOnInit() {
if (this.useHostWidth()) {
this.props.maxWidth = this.hostWidth;
}
}
ngAfterViewInit() {
if (this.isServer)
return;
if (this.isLazy()) {
const hostInView$ = inView(this.host());
if (this.onlyTextOverflow()) {
hostInView$
.pipe(switchMap(() => this.isOverflowing$()), takeUntilDestroyed(this.destroyRef))
.subscribe((isElementOverflow) => {
this.checkOverflow(isElementOverflow);
});
}
else {
hostInView$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.createInstance();
});
}
}
else if (this.onlyTextOverflow()) {
this.isOverflowing$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isElementOverflow) => {
this.checkOverflow(isElementOverflow);
});
}
else {
this.createInstance();
}
}
destroyView() {
this.viewOptions$ = null;
this.viewRef?.destroy();
this.viewRef = null;
}
/**
* This method is useful when you append to an element that you might remove from the DOM.
* In such cases we want to hide the tooltip and let it go through the destroy lifecycle.
* For example, if you have a grid row with an element that you toggle using the display CSS property on hover.
*/
observeHostVisibility() {
if (this.isServer)
return;
// We don't want to observe the host visibility if we are appending to the body.
if (this.props.appendTo && this.props.appendTo !== document.body) {
this.visibilityObserverCleanup?.();
return this.visibleInternal
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((isVisible) => {
if (isVisible) {
this.visibilityObserverCleanup = observeVisibility(this.instance.reference, () => {
this.hide();
// Because we have animation on the popper it doesn't close immediately doesn't trigger the `tpVisible` event.
// Tippy is relying on the transitionend event to trigger the `onHidden` callback.
// https://github.com/atomiks/tippyjs/blob/master/src/dom-utils.ts#L117
// This event never fires because the popper is removed from the DOM before the transition ends.
if (this.props.animation) {
this.onHidden();
}
});
}
else {
this.visibilityObserverCleanup?.();
}
});
}
}
show() {
this.instance?.show();
}
hide() {
this.instance?.hide();
}
enable() {
this.instance?.enable();
}
disable() {
this.instance?.disable();
}
updateProps(props) {
this.setProps({ ...this.props, ...props });
}
setProps(props) {
this.props = props;
this.instance?.setProps(onlyTippyProps(props));
}
setStatus(isEnabled) {
isEnabled ? this.instance?.enable() : this.instance?.disable();
}
createInstance() {
if (!this.content() && !this.useTextContent()) {
return;
}
this.tippyFactory
.create(this.host(), {
allowHTML: true,
appendTo: document.body,
...(this.globalConfig.zIndexGetter
? { zIndex: this.globalConfig.zIndexGetter() }
: {}),
...onlyTippyProps(this.globalConfig),
...onlyTippyProps(this.props),
onMount: (instance) => {
const isVisible = true;
this.isVisible.set(isVisible);
this.visibleInternal.next(isVisible);
if (this.visible.observed) {
this.ngZone.run(() => this.visible.next(isVisible));
}
this.useHostWidth() && this.listenToHostResize();
this.globalConfig.onMount?.(instance);
},
onCreate: (instance) => {
instance.popper.classList.add(`tippy-variation-${this.variation() || this.globalConfig.defaultVariation}`);
if (this.className()) {
for (const klass of normalizeClassName(this.className())) {
instance.popper.classList.add(klass);
}
}
this.globalConfig.onCreate?.(instance);
if (this.isVisible() === true) {
instance.show();
}
},
onShow: (instance) => {
instance.reference.setAttribute('data-tippy-open', '');
// We're re-entering because we might create an Angular component,
// which should be done within the zone.
const content = this.ngZone.run(() => this.resolveContent(instance));
if (isString(content)) {
instance.setProps({ allowHTML: false });
if (!content?.trim()) {
this.disable();
}
else {
this.enable();
}
}
instance.setContent(content);
this.hideOnEscape() && this.handleEscapeButton();
if (this.useHostWidth()) {
this.setInstanceWidth(instance, this.hostWidth);
}
else if (this.popperWidth()) {
this.setInstanceWidth(instance, this.popperWidth());
}
this.globalConfig.onShow?.(instance);
this.onShow.emit();
},
onHide(instance) {
instance.reference.removeAttribute('data-tippy-open');
},
onHidden: (instance) => {
this.onHidden(instance);
},
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((instance) => {
this.instance = instance;
this.setStatus(this.isEnabled());
this.setProps(this.props);
this.variation() === 'contextMenu' && this.handleContextMenu();
});
}
resolveContent(instance) {
const content = this.content();
if (!this.viewOptions$ && !isString(content)) {
const injector = Injector.create({
providers: [
{
provide: TIPPY_REF,
useValue: this.instance,
},
],
parent: this.injector,
});
const data = this.data();
if (isComponent(content)) {
this.instance.data = data;
this.viewOptions$ = {
injector,
};
}
else if (isTemplateRef(content)) {
this.viewOptions$ = {
injector,
context: {
data,
$implicit: this.hide.bind(this),
},
};
}
}
this.viewRef = this.viewService.createView(content, {
vcr: this.vcr,
...this.viewOptions$,
});
// We need to call `detectChanges` for OnPush components to update their content.
if (isComponent(content)) {
// `ɵcmp` is a component defition set for any component.
// Checking the `onPush` property of the component definition is a
// smarter way to determine whether we need to call `detectChanges()`,
// as users may be unaware of setting the binding.
const isOnPush = content.ɵcmp?.onPush;
if (isOnPush) {
this.viewRef.detectChanges();
}
}
let newContent = this.viewRef.getElement();
if (this.useTextContent()) {
newContent = instance.reference.textContent;
}
if (isString(newContent) && this.globalConfig.beforeRender) {
newContent = this.globalConfig.beforeRender(newContent);
}
return newContent;
}
handleContextMenu() {
fromEvent(this.host(), 'contextmenu')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
event.preventDefault();
this.instance.setProps({
getReferenceClientRect: () => ({
width: 0,
height: 0,
top: event.clientY,
bottom: event.clientY,
left: event.clientX,
right: event.clientX,
}),
});
this.instance.show();
});
}
handleEscapeButton() {
fromEvent(document.body, 'keydown')
.pipe(filter(({ code }) => code === 'Escape'), takeUntil(this.visibleInternal.pipe(filter((v) => !v))), takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.hide());
}
checkOverflow(isElementOverflow) {
if (isElementOverflow) {
if (!this.instance) {
this.createInstance();
}
else {
this.instance.enable();
}
}
else {
this.instance?.disable();
}
}
listenToHostResize() {
dimensionsChanges(this.host())
.pipe(takeUntil(this.visibleInternal), takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.setInstanceWidth(this.instance, this.hostWidth);
});
}
setInstanceWidth(instance, width) {
const inPixels = coerceCssPixelValue(width);
instance.popper.style.width = inPixels;
instance.popper.style.maxWidth = inPixels;
instance.popper.firstElementChild.style.maxWidth = inPixels;
}
onHidden(instance = this.instance) {
this.destroyView();
const isVisible = false;
// `model()` uses `OutputEmitterRef` internally to emit events when the
// signal changes. If the directive is destroyed, it will throw an error:
// "Unexpected emit for destroyed `OutputRef`".
if (!this.destroyed) {
this.isVisible.set(isVisible);
this.onHide.emit();
}
this.visibleInternal.next(isVisible);
if (this.visible.observed) {
this.ngZone.run(() => this.visible.next(isVisible));
}
this.globalConfig.onHidden?.(instance);
}
isOverflowing$() {
const host = this.host();
const notifiers$ = [overflowChanges(host)];
// We need to handle cases where the host has a static width but the content might change
if (this.staticWidthHost()) {
notifiers$.push(this.contentChanged.pipe(
// We need to wait for the content to be rendered before we can check if it's overflowing.
switchMap(() => {
return new Observable((subscriber) => {
const id = window.requestAnimationFrame(() => {
subscriber.next();
subscriber.complete();
});
return () => cancelAnimationFrame(id);
});
}), map(() => isElementOverflow(host))));
}
return merge(...notifiers$);
}
setupListeners() {
effect(() => {
// Capture signal read to track its changes.
this.content();
untracked(() => this.contentChanged.next());
});
effect(() => this.setStatus(this.isEnabled()));
effect(() => {
const isVisible = this.isVisible();
isVisible ? this.show() : this.hide();
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyDirective, deps: [{ token: TIPPY_CONFIG }, { token: i0.Injector }, { token: i1.ViewService }, { token: i0.ViewContainerRef }, { token: i0.NgZone }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.0.0", type: TippyDirective, isStandalone: true, selector: "[tp]", inputs: { appendTo: { classPropertyName: "appendTo", publicName: "tpAppendTo", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "tp", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "tpDelay", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "tpDuration", isSignal: true, isRequired: false, transformFunction: null }, hideOnClick: { classPropertyName: "hideOnClick", publicName: "tpHideOnClick", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "tpInteractive", isSignal: true, isRequired: false, transformFunction: null }, interactiveBorder: { classPropertyName: "interactiveBorder", publicName: "tpInteractiveBorder", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "tpMaxWidth", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "tpOffset", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "tpPlacement", isSignal: true, isRequired: false, transformFunction: null }, popperOptions: { classPropertyName: "popperOptions", publicName: "tpPopperOptions", isSignal: true, isRequired: false, transformFunction: null }, showOnCreate: { classPropertyName: "showOnCreate", publicName: "tpShowOnCreate", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "tpTrigger", isSignal: true, isRequired: false, transformFunction: null }, triggerTarget: { classPropertyName: "triggerTarget", publicName: "tpTriggerTarget", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "tpZIndex", isSignal: true, isRequired: false, transformFunction: null }, animation: { classPropertyName: "animation", publicName: "tpAnimation", isSignal: true, isRequired: false, transformFunction: null }, useTextContent: { classPropertyName: "useTextContent", publicName: "tpUseTextContent", isSignal: true, isRequired: false, transformFunction: null }, isLazy: { classPropertyName: "isLazy", publicName: "tpIsLazy", isSignal: true, isRequired: false, transformFunction: null }, variation: { classPropertyName: "variation", publicName: "tpVariation", isSignal: true, isRequired: false, transformFunction: null }, isEnabled: { classPropertyName: "isEnabled", publicName: "tpIsEnabled", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "tpClassName", isSignal: true, isRequired: false, transformFunction: null }, onlyTextOverflow: { classPropertyName: "onlyTextOverflow", publicName: "tpOnlyTextOverflow", isSignal: true, isRequired: false, transformFunction: null }, staticWidthHost: { classPropertyName: "staticWidthHost", publicName: "tpStaticWidthHost", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "tpData", isSignal: true, isRequired: false, transformFunction: null }, useHostWidth: { classPropertyName: "useHostWidth", publicName: "tpUseHostWidth", isSignal: true, isRequired: false, transformFunction: null }, hideOnEscape: { classPropertyName: "hideOnEscape", publicName: "tpHideOnEscape", isSignal: true, isRequired: false, transformFunction: null }, popperWidth: { classPropertyName: "popperWidth", publicName: "tpPopperWidth", isSignal: true, isRequired: false, transformFunction: null }, customHost: { classPropertyName: "customHost", publicName: "tpHost", isSignal: true, isRequired: false, transformFunction: null }, isVisible: { classPropertyName: "isVisible", publicName: "tpIsVisible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onShow: "tpOnShow", onHide: "tpOnHide", isVisible: "tpIsVisibleChange", visible: "tpVisible" }, exportAs: ["tippy"], usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyDirective, decorators: [{
type: Directive,
args: [{
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tp]',
exportAs: 'tippy',
standalone: true,
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [TIPPY_CONFIG]
}] }, { type: i0.Injector }, { type: i1.ViewService }, { type: i0.ViewContainerRef }, { type: i0.NgZone }, { type: i0.ElementRef }], propDecorators: { visible: [{
type: Output,
args: ['tpVisible']
}] } });
class TippyService {
constructor(globalConfig, view, injector) {
this.globalConfig = globalConfig;
this.view = view;
this.injector = injector;
this._tippyFactory = inject(TippyFactory);
}
create(host, content, options = {}) {
const variation = options.variation || this.globalConfig.defaultVariation || '';
const config = {
onShow: (instance) => {
host.setAttribute('data-tippy-open', '');
if (!instance.$viewOptions) {
instance.$viewOptions = {
injector: Injector.create({
providers: [
{
provide: TIPPY_REF,
useValue: instance,
},
],
parent: options.injector || this.injector,
}),
};
if (isTemplateRef(content)) {
instance.$viewOptions.context = {
$implicit: instance.hide.bind(instance),
...options.context,
};
}
else if (isComponent(content)) {
instance.context = options.context;
instance.data = options.data;
}
}
instance.view ||= this.view.createView(content, {
...options,
...instance.$viewOptions,
});
instance.setContent(instance.view.getElement());
options?.onShow?.(instance);
},
onHidden: (instance) => {
host.removeAttribute('data-tippy-open');
if (!options.preserveView) {
instance.view?.destroy();
instance.view = null;
}
options?.onHidden?.(instance);
},
...onlyTippyProps(this.globalConfig),
...this.globalConfig.variations?.[variation],
...onlyTippyProps(options),
onCreate: (instance) => {
instance.popper.classList.add(`tippy-variation-${variation}`);
if (options.className) {
for (const klass of normalizeClassName(options.className)) {
instance.popper.classList.add(klass);
}
}
this.globalConfig.onCreate?.(instance);
options.onCreate?.(instance);
},
};
return this._tippyFactory.create(host, config);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyService, deps: [{ token: TIPPY_CONFIG }, { token: i1.ViewService }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: TippyService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [TIPPY_CONFIG]
}] }, { type: i1.ViewService }, { type: i0.Injector }] });
/**
* Generated bundle index. Do not edit.
*/
export { TIPPY_REF, TippyDirective, TippyService, inView, injectTippyRef, overflowChanges };
//# sourceMappingURL=ngneat-helipopper.mjs.map