@dotglitch/ngx-common
Version:
Angular components and utilities that are commonly used.
358 lines (353 loc) • 23.8 kB
JavaScript
import { NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
import * as i0 from '@angular/core';
import { TemplateRef, HostListener, Input, Optional, Inject, Component, Directive } from '@angular/core';
import * as i1 from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { createApplication } from '@angular/platform-browser';
import { MenuComponent, getPosition } from '@dotglitch/ngx-common/core';
import { firstValueFrom } from 'rxjs';
import { ulid } from 'ulidx';
const zone = new Zone(Zone.current, { name: "@dotglitch_menu", properties: {} });
const calcTooltipBounds = async (template, data, matDialogConfig) => {
const args = {
data: data || {},
template,
config: {},
selfCords: { left: "0px", top: "0px" },
ownerCords: { x: 0, y: 0, width: 0, height: 0 },
id: null
};
// dimensions should be in px... Might need to handle vw/v
if (matDialogConfig?.width && matDialogConfig?.height) {
return {
width: parseInt(matDialogConfig.width),
height: parseInt(matDialogConfig.height),
top: 0,
left: 0,
right: 0,
bottom: 0
};
}
return new Promise((res, rej) => {
zone.run(async () => {
// Forcibly bootstrap the ctx menu outside of the client application's zone.
const app = await createApplication({
providers: [
{ provide: MAT_DIALOG_DATA, useValue: args }
]
});
const del = document.createElement("div");
del.style.position = "absolute";
del.style.left = '-1000vw';
document.body.append(del);
const base = app.bootstrap(TooltipComponent, del);
const { instance } = base;
await firstValueFrom(app.isStable);
const el = instance.viewContainer?.element?.nativeElement;
const rect = el.getBoundingClientRect();
app.destroy();
del.remove();
res(rect);
});
});
};
class TooltipComponent {
constructor(viewContainer, _data, dialog, // optional only for the purpose of estimating dimensions
dialogRef) {
this.viewContainer = viewContainer;
this._data = _data;
this.dialog = dialog;
this.dialogRef = dialogRef;
this.isTemplate = false;
this.isMenu = false;
this.hasBootstrapped = false;
this.pointerIsOnVoid = false;
this.isLockedOpen = false;
this.clientWidth = window.innerWidth;
this.clientHeight = window.innerHeight;
this.coverRectCords = {
top: 0,
left: 0,
height: 0,
width: 0
};
// Defaults are set before @Input() hooks evaluate
this.data = this.data || this._data?.data || {};
this.config = this.config || this._data?.config;
this.dialog = this.dialog || this._data?.dialog;
this.template = this.template || this._data?.template;
this.ownerCords = this.ownerCords || this._data?.ownerCords;
this.selfCords = this.selfCords || this._data?.selfCords;
this.isLockedOpen = this._data?.isLockedOpen || this.config?.stayOpen;
}
ngOnInit() {
const selfY = parseInt(this.selfCords.top.replace('px', ''));
const selfX = parseInt(this.selfCords.left.replace('px', ''));
this.coverRectCords = {
top: this.ownerCords.y - selfY - 16,
left: this.ownerCords.x - selfX - 16,
height: this.ownerCords.height + 32,
width: this.ownerCords.width + 32
};
if (Array.isArray(this.template))
this.isMenu = true;
else if (this.template instanceof TemplateRef)
this.isTemplate = true;
else if (typeof this.template == "function")
this.isTemplate = false;
else
throw new Error("Unrecognized template object provided.");
// TODO: resolve the event hook with the .void element
setTimeout(() => {
this.hasBootstrapped = true;
if (this.pointerIsOnVoid && !this.isLockedOpen)
this.dialogRef.close();
}, 200);
}
ngAfterViewInit() {
const el = this.viewContainer.element.nativeElement;
el.addEventListener("keydown", evt => {
this.isLockedOpen = true;
});
el.addEventListener("pointerdown", evt => {
this.isLockedOpen = true;
});
el.addEventListener("touch", evt => {
this.isLockedOpen = true;
});
}
onKeyDown(evt) {
if (this.config?.freezeOnKeyCode) {
if (evt.code == this.config.freezeOnKeyCode)
this.isLockedOpen = true;
}
}
onVoidPointerDown(evt) {
if (!this.isLockedOpen) {
const el = this.viewContainer.element.nativeElement;
el.querySelector(".void").remove();
setTimeout(() => {
const clonedEvt = new PointerEvent("pointerdown", evt);
const target = document.elementFromPoint(evt.clientX, evt.clientY);
console.log("DEBUG EVENTS", { evt, clonedEvt });
target.dispatchEvent(clonedEvt);
}, 15);
}
this.closeOnVoid(true);
}
// If the void element gets stuck open, make wheel events pass through.
onWheel(evt) {
const el = this.viewContainer.element.nativeElement;
el.style.display = "none";
const target = document.elementFromPoint(evt.clientX, evt.clientY);
el.style.display = "block";
target.scroll({
top: evt.deltaY + target.scrollTop,
left: evt.deltaX + target.scrollLeft,
behavior: "smooth"
});
}
/**
* Close the tooltip if these actions occur
*/
onClose() {
if (!this.isLockedOpen)
this.dialogRef?.close();
this.clientWidth = window.innerWidth;
this.clientHeight = window.innerHeight;
}
closeOnVoid(force = false) {
if (!this.isLockedOpen || force)
this.dialogRef.close();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipComponent, deps: [{ token: i0.ViewContainerRef }, { token: MAT_DIALOG_DATA, optional: true }, { token: i1.MatDialog, optional: true }, { token: i1.MatDialogRef, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TooltipComponent, isStandalone: true, selector: "ngx-tooltip", inputs: { data: "data", config: "config", ownerCords: "ownerCords", selfCords: "selfCords", template: "template" }, host: { listeners: { "window:keydown": "onKeyDown($event)", "window:resize": "onClose()", "window:blur": "onClose()", "pointerleave": "onClose()" } }, ngImport: i0, template: "<!-- Mouse event blocker for pointer leave -->\n@if (coverRectCords) {\n <!-- <div\n class=\"owner-mask\"\n [style.top]=\"coverRectCords.top + 'px'\"\n [style.left]=\"coverRectCords.left + 'px'\"\n [style.height]=\"coverRectCords.height + 'px'\"\n [style.width]=\"coverRectCords.width + 'px'\"\n style=\"z-index: -1;\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n ></div> -->\n\n <div class=\"void left\"\n [style.top]=\"'0px'\"\n [style.left]=\"'0px'\"\n [style.height]=\"'100%'\"\n [style.width]=\"(ownerCords.left) + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void top\"\n [style.top]=\"'0px'\"\n [style.left]=\"ownerCords.left + 'px'\"\n [style.height]=\"ownerCords.top + 'px'\"\n [style.width]=\"ownerCords.width + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void right\"\n [style.top]=\"'0px'\"\n [style.left]=\"(ownerCords.left + ownerCords.width) + 'px'\"\n [style.height]=\"'100%'\"\n [style.width]=\"(clientWidth - (ownerCords.left + ownerCords.width)) + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void\"\n [style.top]=\"(ownerCords.top + ownerCords.height) + 'px'\"\n [style.left]=\"ownerCords.left + 'px'\"\n [style.height]=\"(clientHeight - (ownerCords.top + ownerCords.height)) + 'px'\"\n [style.width]=\"ownerCords.width + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n}\n\n\n<div\n #container\n class=\"container\"\n>\n @if (isMenu) {\n <ngx-menu\n [config]=\"config\"\n [data]=\"data\"\n [ownerCords]=\"ownerCords\"\n [selfCords]=\"selfCords\"\n [items]=\"$any(template)\"\n [isLockedOpen]=\"config.stayOpen\"\n />\n }\n @else if (isTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"$any(template)\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'element': container,\n 'tooltip': this\n }\"\n ></ng-container>\n }\n @else {\n <ng-container\n [ngComponentOutlet]=\"$any(template)\"\n >\n </ng-container>\n }\n</div>\n", styles: ["::ng-deep .cdk-overlay-container .ngx-tooltip{--mdc-dialog-container-color: var(--ngx-tooltip-background-color, #2f2f2f)}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog__container{transform-origin:top left}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog--open .mdc-dialog__container{transform:none}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog__surface{overflow:visible;background-color:#0000}::ng-deep .cdk-overlay-container .context-menu-backdrop.cdk-overlay-backdrop-showing{opacity:0}::ng-deep .cdk-overlay-pane.ngx-tooltip .mat-dialog-container{padding:0}:host{min-width:2px;min-height:2px;display:block}.void,.owner-mask{position:absolute}.void{top:-100vh;right:-100vw;bottom:-100vh;left:-100vw;z-index:-2;position:fixed}.container{width:100%;height:100%;background:var(--ngx-tooltip-background-color, #333);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "component", type: MenuComponent, selector: "ngx-menu", inputs: ["data", "items", "config", "id", "overlayOverlap", "hoverDelay", "showDebugOverlay", "targetBounds", "ownerCords", "selfCords", "parentItem", "parentContext", "isLockedOpen"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipComponent, decorators: [{
type: Component,
args: [{ selector: 'ngx-tooltip', imports: [
NgTemplateOutlet,
NgComponentOutlet,
MenuComponent
], standalone: true, template: "<!-- Mouse event blocker for pointer leave -->\n@if (coverRectCords) {\n <!-- <div\n class=\"owner-mask\"\n [style.top]=\"coverRectCords.top + 'px'\"\n [style.left]=\"coverRectCords.left + 'px'\"\n [style.height]=\"coverRectCords.height + 'px'\"\n [style.width]=\"coverRectCords.width + 'px'\"\n style=\"z-index: -1;\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n ></div> -->\n\n <div class=\"void left\"\n [style.top]=\"'0px'\"\n [style.left]=\"'0px'\"\n [style.height]=\"'100%'\"\n [style.width]=\"(ownerCords.left) + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void top\"\n [style.top]=\"'0px'\"\n [style.left]=\"ownerCords.left + 'px'\"\n [style.height]=\"ownerCords.top + 'px'\"\n [style.width]=\"ownerCords.width + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void right\"\n [style.top]=\"'0px'\"\n [style.left]=\"(ownerCords.left + ownerCords.width) + 'px'\"\n [style.height]=\"'100%'\"\n [style.width]=\"(clientWidth - (ownerCords.left + ownerCords.width)) + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n <div class=\"void\"\n [style.top]=\"(ownerCords.top + ownerCords.height) + 'px'\"\n [style.left]=\"ownerCords.left + 'px'\"\n [style.height]=\"(clientHeight - (ownerCords.top + ownerCords.height)) + 'px'\"\n [style.width]=\"ownerCords.width + 'px'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && closeOnVoid()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"onVoidPointerDown($event)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n}\n\n\n<div\n #container\n class=\"container\"\n>\n @if (isMenu) {\n <ngx-menu\n [config]=\"config\"\n [data]=\"data\"\n [ownerCords]=\"ownerCords\"\n [selfCords]=\"selfCords\"\n [items]=\"$any(template)\"\n [isLockedOpen]=\"config.stayOpen\"\n />\n }\n @else if (isTemplate) {\n <ng-container\n [ngTemplateOutlet]=\"$any(template)\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'element': container,\n 'tooltip': this\n }\"\n ></ng-container>\n }\n @else {\n <ng-container\n [ngComponentOutlet]=\"$any(template)\"\n >\n </ng-container>\n }\n</div>\n", styles: ["::ng-deep .cdk-overlay-container .ngx-tooltip{--mdc-dialog-container-color: var(--ngx-tooltip-background-color, #2f2f2f)}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog__container{transform-origin:top left}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog--open .mdc-dialog__container{transform:none}::ng-deep .cdk-overlay-container .ngx-tooltip .mdc-dialog__surface{overflow:visible;background-color:#0000}::ng-deep .cdk-overlay-container .context-menu-backdrop.cdk-overlay-backdrop-showing{opacity:0}::ng-deep .cdk-overlay-pane.ngx-tooltip .mat-dialog-container{padding:0}:host{min-width:2px;min-height:2px;display:block}.void,.owner-mask{position:absolute}.void{top:-100vh;right:-100vw;bottom:-100vh;left:-100vw;z-index:-2;position:fixed}.container{width:100%;height:100%;background:var(--ngx-tooltip-background-color, #333);border-radius:6px;overflow:hidden}\n"] }]
}], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [MAT_DIALOG_DATA]
}] }, { type: i1.MatDialog, decorators: [{
type: Optional
}] }, { type: i1.MatDialogRef, decorators: [{
type: Optional
}] }], propDecorators: { data: [{
type: Input
}], config: [{
type: Input
}], ownerCords: [{
type: Input
}], selfCords: [{
type: Input
}], template: [{
type: Input
}], onKeyDown: [{
type: HostListener,
args: ["window:keydown", ['$event']]
}], onClose: [{
type: HostListener,
args: ["window:resize"]
}, {
type: HostListener,
args: ["window:blur"]
}, {
type: HostListener,
args: ["pointerleave"]
}] } });
class TooltipDirective {
constructor(dialog, viewContainer) {
this.dialog = dialog;
this.viewContainer = viewContainer;
/**
* Configuration for opening the app menu
*/
this.config = {};
/**
* Arbitrary data to pass into the template
*/
this.data = {};
this.isCursorOverTarget = false;
this.dialogIsOpen = false;
}
ngAfterViewInit() {
const el = this.viewContainer.element.nativeElement;
this.config?.triggers?.forEach(t => {
el.addEventListener(t, () => {
if (t == "click")
this.config.stayOpen = true;
this.open();
});
});
}
async open() {
if (!this.dialogIsOpen) {
const el = this.viewContainer.element.nativeElement;
this.dialogIsOpen = true;
await openTooltip(this.dialog, this.template, this.data, el, this.config);
this.dialogIsOpen = false;
}
}
async onPointerEnter(evt) {
// If the template is not a template ref, do nothing.
if (!(this.template instanceof TemplateRef))
return;
if (Array.isArray(this.config?.triggers) && !this.config.triggers.includes("hover")) {
return;
}
this.isCursorOverTarget = true;
setTimeout(async () => {
// If the cursor moved away in the time
if (!this.isCursorOverTarget)
return;
this.open();
}, this.config.delay ?? 250);
}
async onPointerLeave(evt) {
this.isCursorOverTarget = false;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipDirective, deps: [{ token: i1.MatDialog }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: TooltipDirective, isStandalone: true, selector: "[ngx-tooltip]", inputs: { template: ["ngx-tooltip", "template"], config: ["ngx-tooltip-config", "config"], data: ["ngx-tooltip-context", "data"] }, host: { listeners: { "pointerenter": "onPointerEnter($event)", "pointerleave": "onPointerLeave($event)" } }, providers: [
MatDialog
], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TooltipDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-tooltip]',
providers: [
MatDialog
],
standalone: true
}]
}], ctorParameters: () => [{ type: i1.MatDialog }, { type: i0.ViewContainerRef }], propDecorators: { template: [{
type: Input,
args: ["ngx-tooltip"]
}], config: [{
type: Input,
args: ["ngx-tooltip-config"]
}], data: [{
type: Input,
args: ["ngx-tooltip-context"]
}], onPointerEnter: [{
type: HostListener,
args: ['pointerenter', ['$event']]
}], onPointerLeave: [{
type: HostListener,
args: ['pointerleave', ['$event']]
}] } });
// Helper to open the context menu without using the directive.
const openTooltip = async (dialog, template, data, el, config, focusTrap = false, matPopupOptions) => {
const component = Array.isArray(template) ? MenuComponent : template;
const rect = await calcTooltipBounds(component, data, matPopupOptions);
const ownerCords = el.getBoundingClientRect();
const cords = getPosition(el, config, rect);
const specificId = ulid();
return firstValueFrom(dialog.open(TooltipComponent, {
autoFocus: focusTrap,
restoreFocus: focusTrap,
data: {
dialog,
data: data,
template: template,
config: config,
matPopupOptions,
ownerCords: ownerCords,
selfCords: cords,
id: specificId
},
panelClass: ["ngx-tooltip", 'ngx-' + specificId].concat(config?.customClass || []),
position: cords,
hasBackdrop: false,
...matPopupOptions
})
.afterClosed());
};
class DropdownDirective extends TooltipDirective {
constructor() {
super(...arguments);
/**
* Configuration for opening the app menu
*/
this._config = {};
}
ngOnInit() {
// Set default values
this._config.position = this._config.position ?? "bottom";
this._config.alignment = this._config.alignment ?? "start";
this._config.stayOpen = this._config.stayOpen ?? true;
this.config = this._config;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.3.12", type: DropdownDirective, isStandalone: true, selector: "[ngx-dropdown],[ngx-dropdown-config]", inputs: { template: ["ngx-dropdown", "template"], _config: ["ngx-dropdown-config", "_config"] }, providers: [
MatDialog
], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DropdownDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngx-dropdown],[ngx-dropdown-config]',
providers: [
MatDialog
],
standalone: true
}]
}], propDecorators: { template: [{
type: Input,
args: ["ngx-dropdown"]
}], _config: [{
type: Input,
args: ["ngx-dropdown-config"]
}] } });
/**
* Generated bundle index. Do not edit.
*/
export { DropdownDirective, TooltipComponent, TooltipDirective, calcTooltipBounds, openTooltip };
//# sourceMappingURL=dotglitch-ngx-common-tooltip.mjs.map