UNPKG

@dotglitch/ngx-common

Version:

Angular components and utilities that are commonly used.

358 lines (353 loc) 23.8 kB
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