UNPKG

@dotglitch/ngx-common

Version:

Angular components and utilities that are commonly used.

706 lines (702 loc) 599 kB
import * as i0 from '@angular/core'; import { TemplateRef, Component, Optional, Inject, Input, HostListener, Directive, InjectionToken, Pipe, Injectable, EventEmitter, isDevMode, ViewContainerRef, ViewChild, Output, NgModule, ViewEncapsulation, SecurityContext, ContentChild, ViewChildren, createComponent } from '@angular/core'; import * as i1$1 from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; import { NgTemplateOutlet, NgComponentOutlet, DOCUMENT, NgIf, NgForOf, DatePipe } from '@angular/common'; import * as i1 from '@angular/platform-browser'; import { createApplication } from '@angular/platform-browser'; import { firstValueFrom, debounceTime, of, Subject, BehaviorSubject } from 'rxjs'; import * as i3$1 from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon'; import * as i5 from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import * as i3 from '@angular/cdk/portal'; import { ComponentPortal, PortalModule } from '@angular/cdk/portal'; import { ulid } from 'ulidx'; import { createInstance, INDEXEDDB } from 'localforage'; import * as i2 from '@angular/cdk/dialog'; import { retry } from 'rxjs/operators'; import * as i1$2 from '@angular/common/http'; import * as i4 from '@angular/material/input'; import { MatInputModule } from '@angular/material/input'; import * as i6 from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling'; import * as i7 from 'ngx-scrollbar'; import { NgScrollbar, NgScrollbarModule } from 'ngx-scrollbar'; import * as i4$1 from '@angular/material/form-field'; import { __decorate, __param } from 'tslib'; import * as i4$2 from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs'; import * as i6$2 from '@angular/material/sidenav'; import { MatDrawerContainer, MatSidenavModule } from '@angular/material/sidenav'; import * as i3$2 from 'angular-split'; import { AngularSplitModule } from 'angular-split'; import * as i6$1 from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { TabulatorFull } from 'tabulator-tables'; import * as i7$1 from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import * as i9 from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button'; import * as i2$1 from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion'; import * as path from 'path-browserify'; import * as React from 'react'; import { createRoot } from 'react-dom/client'; /** * This utils file exists outside of the strict angular DI zone * This enables opening popups without requiring absolute DI bindings. */ const getPosition = (el, config = {}, bounds) => { // Bounds of the popup owner const src = !!el['nodeName'] ? el.getBoundingClientRect() : { // It's a pointer event, so we'll take the X and Y from the pointer. x: el['clientX'], y: el['clientY'], // Set a default tiny size, so we don't divide by zero. width: 0.0001, height: 0.0001 }; // Popup bounds const { width, height } = bounds; const winh = window.innerHeight; const winw = window.innerWidth; const cords = { top: null, left: null }; if (config?.position == "left" || config?.position == "right" || !config?.position) { switch (config?.alignment) { case "end": { // vertically bind to bottom cords.top = src.y + src.height - height; break; } case "afterend": { // vertically bind below bottom cords.top = src.y + src.height; break; } case "beforestart": { // vertically bind above top cords.top = src.y - height; break; } case "start": { // vertically bind to top cords.top = src.y; break; } case "center": default: { // vertically center cords.top = (src.y + (src.height / 2)) - (height / 2); break; } } // Apply bounds to prevent the dialog from being cut-off screen // Lower bound cords.top = Math.max(config?.edgePadding || 0, cords.top); // Upper bound cords.top = Math.min(winh - height, cords.top); if (config?.position == "left") { cords.left = src.x - (width + (config?.arrowSize || 0) + (config?.arrowPadding || 0)); } if (config?.position == "right" || !config?.position) { cords.left = src.x + (src.width + (config?.arrowSize || 0) + (config?.arrowPadding || 0)); } // Lower bound cords.left = Math.max(config?.edgePadding || 0, cords.left); // Upper bound cords.left = Math.min(winw - width, cords.left); } else if (config?.position == "top" || config?.position == "bottom") { switch (config?.alignment) { case "end": { // vertically bind to right cords.left = src.x + src.width - width; break; } case "afterend": { // vertically bind past right cords.left = src.x + src.width; break; } case "beforestart": { // vertically bind before left cords.left = src.x - width; break; } case "start": { // vertically bind to left cords.left = src.x; break; } case "center": default: { // vertically center cords.left = (src.x + (src.width / 2)) - (width / 2); break; } } // Apply bounds to prevent the dialog from being cut-off screen // Lower bound cords.left = Math.max(config?.edgePadding || 0, cords.left); // Upper bound cords.left = Math.min(winw - width, cords.left); if (config?.position == "top") { cords.top = src.y - (height + (config?.arrowSize || 0) + (config?.arrowPadding || 0)); } if (config?.position == "bottom") { cords.top = src.y + (src.height + (config?.arrowSize || 0) + (config?.arrowPadding || 0)); } // Lower bound cords.top = Math.max(config?.edgePadding || 0, cords.top); // Upper bound cords.top = Math.min(winh - height, cords.top); } // Assign unit cords.top = cords.top + 'px'; cords.left = cords.left + 'px'; return cords; }; const zone$1 = new Zone(Zone.current, { name: "@dotglitch_menu", properties: {} }); const calcMenuItemBounds = async (menuItems, dataObj) => { const data = { data: dataObj, items: menuItems, config: {}, selfCords: { left: "0px", top: "0px" }, ownerCords: { x: 0, y: 0, width: 0, height: 0 }, id: null }; return calcComponentBounds(MenuComponent, data); }; const calcComponentBounds = async (component, data) => { return new Promise((res, rej) => { zone$1.run(async () => { const app = await createApplication({ providers: [ { provide: MAT_DIALOG_DATA, useValue: data } ] }); const del = document.createElement("div"); del.classList.add("ngx-menu"); del.style.position = "absolute"; del.style.left = '-1000vw'; document.body.append(del); const base = app.bootstrap(component, del); const { instance } = base; await firstValueFrom(app.isStable); const el = instance.viewContainer?.element?.nativeElement; const rect = el.getBoundingClientRect(); app.destroy(); del.remove(); res(rect); }); }); }; const $data = Symbol("data"); const $hover = Symbol("hover"); class MenuComponent { constructor(viewContainer, sanitizer, _data, dialog, // optional only for the purpose of estimating dimensions dialogRef) { this.viewContainer = viewContainer; this.sanitizer = sanitizer; this._data = _data; this.dialog = dialog; this.dialogRef = dialogRef; this.overlayOverlap = 32; this.hoverDelay = 400; this.showDebugOverlay = false; this.isLockedOpen = false; this.hasBootstrapped = false; this.pointerIsOnVoid = false; this.pointerHasBeenOverMask = false; this.parentIsNgxMenu = false; this.coverRectCords = { top: 0, left: 0, height: 0, width: 0 }; // Check if there are any slashes or dots -- that will clearly exclude it from being a mat icon this.matIconRx = /[\/\.]/i; this.showIconColumn = true; this.showShortcutColumn = true; this.childDialogs = []; // Defaults are set before @Input() hooks evaluate this.dialog = this.dialog || this._data?.dialog; this.data = this._data?.data; this.ownerCords = this._data?.ownerCords; this.selfCords = this._data?.selfCords; this.items = this._data?.items; this.config = this._data?.config; this.id = this._data?.id; this.parentItem = this._data?.parentItem; this.parentContext = this._data?.parentContext; this.isLockedOpen = this.isLockedOpen || this._data?.config?.['_isLockedOpen']; this.parentIsNgxMenu = this._data?.parentIsNgxMenu; this.targetBounds = this._data?.targetBounds; this.template = this._data?.template; this.templateType = this.template instanceof TemplateRef ? "template" : "component"; if (this.templateType == "component") { this.componentPortal = new ComponentPortal(this.template); } } ngOnInit() { this.items?.forEach(i => { if (typeof i == "string") return; // Set defaults i['_disabled'] = false; i['_visible'] = true; i['_context'] = (typeof i.context == "function") ? i.context(this.data) : i.context; if (i.label) try { i['_formattedLabel'] = this.formatLabel(i.label); } catch (e) { console.warn(e); } if (typeof i.isDisabled == "function") try { i['_disabled'] = i.isDisabled(this.data || {}, i['_context']); } catch (e) { console.warn(e); } if (typeof i.isVisible == "function") try { i['_visible'] = i.isVisible(this.data || {}, i['_context']); } catch (e) { console.warn(e); } if (typeof i.linkTemplate == "function") try { i['_link'] = i.linkTemplate(this.data || {}, i['_context']); } catch (e) { console.warn(e); } if (typeof i.iconTemplate == "function") try { i['_icon'] = i.iconTemplate(this.data || {}, i['_context']); } catch (e) { console.warn(e); } }); // Show the icon column if there are any items with an icon this.showIconColumn = !!this.items?.find(i => typeof i == "object" && typeof i['icon'] == "string" && i['icon'].length > 2); this.showShortcutColumn = !!this.items?.find(i => typeof i == "object" && typeof i['shortcut'] == "string" && i['shortcut'].length > 2); if (this.ownerCords) { const selfY = parseInt(this.selfCords.top?.replace('px', '') || '0'); const selfX = parseInt(this.selfCords.left?.replace('px', '') || '0'); this.coverRectCords = { top: this.ownerCords.y - selfY - (this.overlayOverlap / 2), left: this.ownerCords.x - selfX - (this.overlayOverlap / 2), height: this.ownerCords.height + this.overlayOverlap, width: this.ownerCords.width + this.overlayOverlap }; } if (this.config?.stayOpen) this.isLockedOpen = true; setTimeout(() => { this.hasBootstrapped = true; }, 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; }); } ngOnDestroy() { // this.childDialogs.forEach(d => d.close({ [$data]: true })); } /** * */ async onMenuItemClick(item, row, keepOpen = false) { if (typeof item == 'string') return null; if (item.separator) return null; const context = await item['_context']; // If cache is enabled, only load if we don't have any children. const forceLoad = (item.cacheResolvedChildren ? !item.children : true); if (item.childrenResolver && forceLoad) { item['_isResolving'] = true; item['_children'] = await item.childrenResolver(this.data, context); item['_isResolving'] = false; } else if (typeof item.children == "function" && forceLoad) { item['_isResolving'] = true; item['_children'] = await item.children(this.data, context); item['_isResolving'] = false; } else { item['_children'] = item.children; } if (item['_children'] || item.childTemplate) row['_open'] = true; if (!item.childTemplate && !item.children) { if (typeof item.action == "function") { const res = await item.action(this.data, context); this.close(res === undefined ? true : res); return res; } // If no action, this is simply a text item. return null; } // Need X pos, Y pos, width and height const bounds = row.getBoundingClientRect(); const cords = { top: null, left: null, bottom: null, // right: null }; // Set position coordinates const targetBounds = await (item.childTemplate ? calcComponentBounds(MenuComponent, { template: item.childTemplate }) : calcMenuItemBounds(item['_children'], this.data)); const { width, height } = targetBounds; if (bounds.y + height > window.innerHeight) cords.bottom = "0px"; if (bounds.x + bounds.width + width > window.innerWidth) cords.left = ((bounds.x - width)) + "px"; if (!cords.bottom) cords.top = bounds.y + "px"; if (!cords.left) cords.left = bounds.x + bounds.width + "px"; const config = structuredClone(this.config); config['_isLockedOpen'] = keepOpen; // Do not project in the top left corner -- this scenario // happens when a dialog opens as the parent is killed. if (cords.left == '0px' && cords.top == '0px') return; const dialogRef = this.dialog.open(MenuComponent, { position: cords, panelClass: ["ngx-menu"].concat(this.config?.customClass || []), backdropClass: "ngx-menu-backdrop", hasBackdrop: false, data: { data: this.data, ownerCords: row.getBoundingClientRect(), selfCords: cords, parentItem: item, parentContext: context, items: item['_children'], template: item.childTemplate, config: config, parentIsNgxMenu: true, targetBounds } }); let _s = dialogRef .afterClosed() .subscribe(async (result) => { // Clicked "void" on a submenu if (typeof result == "object" && result[$data] == true) { this.close(result); } // Went back to parent menu -- do not close (same as result == null) else if (typeof result == "object" && result[$data] == false) { } // Got some other result value else if (result != null) { // Perform action callback if (typeof item.action == 'function') { this.close(await item.action(result, context)); } // Just close. else { this.close(); } } row['_open'] = false; this.childDialogs.splice(this.childDialogs.indexOf(dialogRef), 1); _s.unsubscribe(); }); this.childDialogs.push(dialogRef); return dialogRef; } /** * * @param label * @returns */ formatLabel(label) { return label.replace(/_([a-z0-9])_/i, (match, group) => `<u>${group}</u>`); } /** * Close the context menu under these circumstances */ // @HostListener("window:resize", ['event']) // @HostListener("window:blur", ['event']) close(result) { this.childDialogs.forEach(d => d.close()); this.dialogRef?.close(result); } closeOnVoid(force = false) { if (!this.isLockedOpen || force) { this.close({ [$data]: force }); } } startHoverTimer(item, row) { // Invert check to make the logic simpler // TL;DR: if (any) of these are true, we will do the hover action if (!(Array.isArray(item.children) && item.children.length > 0 || typeof item.children == "function" || item.childTemplate || item.childrenResolver)) return; item[$hover] = setTimeout(() => { delete item[$hover]; if (!this.pointerIsOnVoid) { this.childDialogs.forEach(cd => cd.close()); row['_open'] = true; this.onMenuItemClick(item, row); } }, this.hoverDelay); } stopHoverTimer(item) { item[$hover] && clearTimeout(item[$hover]); delete item[$hover]; } startCloseTimer() { this.closeTimer = setTimeout(() => { this.closeOnVoid(); }, 500); } stopCloseTimer() { clearTimeout(this.closeTimer); } /** * Check if the dialog is clipping offscreen * if so, move it back into view. */ onResize() { const el = this.viewContainer?.element?.nativeElement; if (!el) return; const { width, height, x, y } = el.getBoundingClientRect(); const target = document.querySelector(".ngx-menu"); if (!target) return; // Move back into view if we're clipping outside of the bottom if (y + height > window.innerHeight) { const newTop = (window.innerHeight - (height + (this.config.edgePadding || 12))) + "px"; target.style['margin-top'] = newTop; } // Move back into view if we're clipping off the right if (x + width > window.innerWidth) { const newLeft = (window.innerWidth - (width + (this.config.edgePadding || 12))) + "px"; target.style['margin-left'] = newLeft; } } // 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" }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MenuComponent, deps: [{ token: i0.ViewContainerRef }, { token: i1.DomSanitizer }, { token: MAT_DIALOG_DATA, optional: true }, { token: i1$1.MatDialog, optional: true }, { token: i1$1.MatDialogRef, optional: true }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.1.2", type: MenuComponent, isStandalone: true, selector: "ngx-menu", inputs: { data: "data", items: "items", config: "config", id: "id", overlayOverlap: "overlayOverlap", hoverDelay: "hoverDelay", showDebugOverlay: "showDebugOverlay", targetBounds: "targetBounds", ownerCords: "ownerCords", selfCords: "selfCords", parentItem: "parentItem", parentContext: "parentContext", isLockedOpen: "isLockedOpen" }, host: { listeners: { "window:resize": "onResize()" }, properties: { "attr.tx": "targetBounds?.x", "attr.ty": "targetBounds?.y", "attr.th": "targetBounds?.height", "attr.tw": "targetBounds?.width" } }, ngImport: i0, template: "<!-- Mouse event blocker for pointer leave -->\n@if (coverRectCords && !parentIsNgxMenu) {\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.background]=\"showDebugOverlay ? '#f004' : '#0000'\"\n style=\"z-index: -1\"\n (pointerenter)=\"pointerHasBeenOverMask=true\"\n (pointerleave)=\"stopCloseTimer()\"\n (pointermove)=\"pointerHasBeenOverMask=true\"\n (click)=\"isLockedOpen = true\"\n ></div>\n}\n\n@if (!parentIsNgxMenu) {\n <div class=\"void\"\n [style.background]=\"showDebugOverlay ? '#00f4' : '#0000'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && !isLockedOpen && startCloseTimer()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"hasBootstrapped && closeOnVoid(true)\"\n (pointermove)=\"hasBootstrapped && !isLockedOpen && startCloseTimer()\"\n (click)=\"closeOnVoid(true)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n}\n\n@if (!template) {\n <table (pointerenter)=\"stopCloseTimer()\">\n <tbody>\n @for (item of items; track item) {\n <!-- A row with a click action -->\n @if (item != 'separator' && item.separator != true && item['_visible']) {\n <tr #row\n [class.disabled]=\"item['_disabled']\"\n (click)=\"!item['_disabled'] && onMenuItemClick(item, row, true)\"\n [class.hover]=\"row['hover']\"\n [class.open]=\"row['_open']\"\n (pointerenter)=\"row['hover'] = true; startHoverTimer(item, row)\"\n (pointerleave)=\"row['hover'] = false; stopHoverTimer(item)\"\n >\n\n @if (showIconColumn) {\n <td class=\"icon\">\n @if (matIconRx.test(item['_icon'] ?? item.icon)) {\n <img [src]=\"item['_icon'] ?? item.icon\"/>\n }\n @else {\n <mat-icon\n [fontIcon]=\"item['_icon'] ?? item.icon\"\n [style.color]=\"item.iconColor\"\n />\n }\n </td>\n }\n\n <!-- 'Normal' action based item -->\n <td class=\"label\"\n [style.padding-left]=\"showIconColumn ? 0 : '16px'\"\n >\n <a\n #anchor\n [attr.target]=\"item.linkTarget\"\n [attr.href]=\"(item['_link'] || item.link) ? sanitizer.bypassSecurityTrustUrl(item['_link'] || item.link) : undefined\"\n >\n @if ($any(item.labelTemplate)?.prototype) {\n <ng-container\n [ngTemplateOutlet]=\"$any(item).labelTemplate\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'context': item['_context'],\n 'item': item,\n 'element': anchor,\n 'menu': this\n }\"\n />\n }\n @else {\n @if ($any(item)?.labelTemplate) {\n {{$any(item)?.labelTemplate(data || {})}}\n }\n @else {\n <div [innerHTML]=\"item['_formattedLabel']\"></div>\n }\n }\n </a>\n </td>\n\n @if (showShortcutColumn) {\n <td class=\"shortcut\">\n {{item.shortcutLabel}}\n </td>\n }\n\n <td style=\"min-width: 16px\">\n @if ((\n (item['children']?.length > 0) ||\n (item['_children']?.length > 0) ||\n item.childTemplate ||\n item.children?.['call'] ||\n item.childrenResolver\n ) &&\n !item['_isResolving']\n ) {\n <mat-icon\n style=\"transform: translateY(2px)\"\n >\n chevron_right\n </mat-icon>\n }\n\n @if (item['_isResolving']) {\n <mat-progress-spinner\n mode=\"indeterminate\"\n [diameter]=\"20\"\n style=\"margin-right: 4px\"\n />\n }\n </td>\n </tr>\n }\n @else if (item != 'separator' && item.separator == true) {\n <!-- Separator with label -->\n <tr\n class=\"disabled separator\"\n >\n <td\n class=\"center\"\n [attr.colspan]=\"2 + (showIconColumn ? 1 : 0) + (showShortcutColumn ? 1 : 0)\"\n >\n <span class=\"hr\">\n {{item['label'] || ''}}\n </span>\n </td>\n </tr>\n }\n @else if (item == 'separator') {\n <!-- Separator -->\n <tr\n class=\"disabled separator\"\n >\n <td\n [attr.colspan]=\"2 + (showIconColumn ? 1 : 0) + (showShortcutColumn ? 1 : 0)\"\n >\n <hr/>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n@else {\n @if (templateType == 'template') {\n <div style=\"display: contents;\" (pointerenter)=\"stopCloseTimer()\">\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'context': parentContext,\n 'item': parentItem,\n 'element': this.viewContainer?.element?.nativeElement,\n 'menu': this\n }\"\n />\n </div>\n }\n @else {\n <div style=\"display: contents;\" (pointerenter)=\"stopCloseTimer()\">\n <ng-container\n [cdkPortalOutlet]=\"componentPortal\"\n />\n </div>\n }\n}\n\n@if (showDebugOverlay) {\n <div>\n <div>hbs: {{hasBootstrapped}}</div>\n <div>pov: {{pointerIsOnVoid}}</div>\n <div>ilo: {{isLockedOpen}}</div>\n <div>hbom: {{pointerHasBeenOverMask}}</div>\n\n <div>type: {{templateType}}</div>\n </div>\n}\n", styles: ["::ng-deep .cdk-overlay-container .ngx-menu{--mdc-dialog-container-color: var(--ngx-menu-background-color, #2f2f2f)}::ng-deep .cdk-overlay-container .ngx-menu .mdc-dialog__container{transform-origin:top left}::ng-deep .cdk-overlay-container .ngx-menu .mdc-dialog--open .mdc-dialog__container{transform:none}::ng-deep .cdk-overlay-pane.ngx-menu .mat-mdc-dialog-surface{overflow:visible}:host{-webkit-user-select:none;user-select:none;z-index:1;position:relative;display:block}table{border-spacing:0;border-radius:5px;padding:4px 0;overflow:hidden}tr{color:var(--ngx-menu-text-color, #ccc);font-size:var(--ngx-menu-font-size, 14px);cursor:pointer;transition:background-color 75ms ease,color 75ms ease}tr:not(.disabled).hover,tr:not(.disabled).open{background-color:var(--ngx-menu-hover-background-color, #94ebeb);color:var(--ngx-menu-hover-text-color, #000)}tr:not(.disabled).hover a,tr:not(.disabled).open a{color:var(--ngx-menu-hover-text-color, #000)}tr:not(.separator){height:36px}tr.disabled .label{color:var(--ngx-menu-disabled-text-color, #919191)}tr .center{text-align:center}tr a{outline:0;display:flex;align-items:center;gap:10px;justify-content:space-between;height:100%;width:100%;position:relative;left:-16px;width:calc(100% + 32px);padding:0 16px}tr .label{min-width:100px}tr img{max-width:100%;max-height:100%;aspect-ratio:1}.hr{height:1px;text-align:center;position:relative}.hr:before,.hr:after{content:\"\";background:var(--ngx-menu-separator-color, #2a2a2a);display:block;position:absolute;top:0;bottom:0;height:1px;margin:auto;width:300px}.hr:before{right:calc(100% + 4px)}.hr:after{left:calc(100% + 4px)}hr{background:var(--ngx-menu-separator-color, #2a2a2a);border:0;height:1px;margin:0}.icon{width:24px;height:24px;padding-left:10px}.icon mat-icon{transform:translateY(2px)}.shortcut{color:var(--ngx-menu-shortcut-text-color, #848484);text-align:end;padding-right:10px;padding-left:12px}.label{height:var(--ngx-menu-item-height, 30px)}td{vertical-align:middle}.void,.owner-mask{position:absolute}.void{top:-100vh;right:-100vw;bottom:-100vh;left:-100vw;z-index:-2}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: PortalModule }, { kind: "directive", type: i3.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i5.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: MenuComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-menu', imports: [ NgTemplateOutlet, PortalModule, MatIconModule, MatProgressSpinnerModule ], standalone: true, host: { "[attr.tx]": "targetBounds?.x", "[attr.ty]": "targetBounds?.y", "[attr.th]": "targetBounds?.height", "[attr.tw]": "targetBounds?.width", }, template: "<!-- Mouse event blocker for pointer leave -->\n@if (coverRectCords && !parentIsNgxMenu) {\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.background]=\"showDebugOverlay ? '#f004' : '#0000'\"\n style=\"z-index: -1\"\n (pointerenter)=\"pointerHasBeenOverMask=true\"\n (pointerleave)=\"stopCloseTimer()\"\n (pointermove)=\"pointerHasBeenOverMask=true\"\n (click)=\"isLockedOpen = true\"\n ></div>\n}\n\n@if (!parentIsNgxMenu) {\n <div class=\"void\"\n [style.background]=\"showDebugOverlay ? '#00f4' : '#0000'\"\n (pointerenter)=\"pointerIsOnVoid = true; hasBootstrapped && !isLockedOpen && startCloseTimer()\"\n (pointerleave)=\"pointerIsOnVoid = false\"\n (pointerdown)=\"hasBootstrapped && closeOnVoid(true)\"\n (pointermove)=\"hasBootstrapped && !isLockedOpen && startCloseTimer()\"\n (click)=\"closeOnVoid(true)\"\n (wheel)=\"onWheel($event)\"\n ></div>\n}\n\n@if (!template) {\n <table (pointerenter)=\"stopCloseTimer()\">\n <tbody>\n @for (item of items; track item) {\n <!-- A row with a click action -->\n @if (item != 'separator' && item.separator != true && item['_visible']) {\n <tr #row\n [class.disabled]=\"item['_disabled']\"\n (click)=\"!item['_disabled'] && onMenuItemClick(item, row, true)\"\n [class.hover]=\"row['hover']\"\n [class.open]=\"row['_open']\"\n (pointerenter)=\"row['hover'] = true; startHoverTimer(item, row)\"\n (pointerleave)=\"row['hover'] = false; stopHoverTimer(item)\"\n >\n\n @if (showIconColumn) {\n <td class=\"icon\">\n @if (matIconRx.test(item['_icon'] ?? item.icon)) {\n <img [src]=\"item['_icon'] ?? item.icon\"/>\n }\n @else {\n <mat-icon\n [fontIcon]=\"item['_icon'] ?? item.icon\"\n [style.color]=\"item.iconColor\"\n />\n }\n </td>\n }\n\n <!-- 'Normal' action based item -->\n <td class=\"label\"\n [style.padding-left]=\"showIconColumn ? 0 : '16px'\"\n >\n <a\n #anchor\n [attr.target]=\"item.linkTarget\"\n [attr.href]=\"(item['_link'] || item.link) ? sanitizer.bypassSecurityTrustUrl(item['_link'] || item.link) : undefined\"\n >\n @if ($any(item.labelTemplate)?.prototype) {\n <ng-container\n [ngTemplateOutlet]=\"$any(item).labelTemplate\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'context': item['_context'],\n 'item': item,\n 'element': anchor,\n 'menu': this\n }\"\n />\n }\n @else {\n @if ($any(item)?.labelTemplate) {\n {{$any(item)?.labelTemplate(data || {})}}\n }\n @else {\n <div [innerHTML]=\"item['_formattedLabel']\"></div>\n }\n }\n </a>\n </td>\n\n @if (showShortcutColumn) {\n <td class=\"shortcut\">\n {{item.shortcutLabel}}\n </td>\n }\n\n <td style=\"min-width: 16px\">\n @if ((\n (item['children']?.length > 0) ||\n (item['_children']?.length > 0) ||\n item.childTemplate ||\n item.children?.['call'] ||\n item.childrenResolver\n ) &&\n !item['_isResolving']\n ) {\n <mat-icon\n style=\"transform: translateY(2px)\"\n >\n chevron_right\n </mat-icon>\n }\n\n @if (item['_isResolving']) {\n <mat-progress-spinner\n mode=\"indeterminate\"\n [diameter]=\"20\"\n style=\"margin-right: 4px\"\n />\n }\n </td>\n </tr>\n }\n @else if (item != 'separator' && item.separator == true) {\n <!-- Separator with label -->\n <tr\n class=\"disabled separator\"\n >\n <td\n class=\"center\"\n [attr.colspan]=\"2 + (showIconColumn ? 1 : 0) + (showShortcutColumn ? 1 : 0)\"\n >\n <span class=\"hr\">\n {{item['label'] || ''}}\n </span>\n </td>\n </tr>\n }\n @else if (item == 'separator') {\n <!-- Separator -->\n <tr\n class=\"disabled separator\"\n >\n <td\n [attr.colspan]=\"2 + (showIconColumn ? 1 : 0) + (showShortcutColumn ? 1 : 0)\"\n >\n <hr/>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n@else {\n @if (templateType == 'template') {\n <div style=\"display: contents;\" (pointerenter)=\"stopCloseTimer()\">\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"{\n '$implicit': data,\n 'dialog': dialogRef,\n 'context': parentContext,\n 'item': parentItem,\n 'element': this.viewContainer?.element?.nativeElement,\n 'menu': this\n }\"\n />\n </div>\n }\n @else {\n <div style=\"display: contents;\" (pointerenter)=\"stopCloseTimer()\">\n <ng-container\n [cdkPortalOutlet]=\"componentPortal\"\n />\n </div>\n }\n}\n\n@if (showDebugOverlay) {\n <div>\n <div>hbs: {{hasBootstrapped}}</div>\n <div>pov: {{pointerIsOnVoid}}</div>\n <div>ilo: {{isLockedOpen}}</div>\n <div>hbom: {{pointerHasBeenOverMask}}</div>\n\n <div>type: {{templateType}}</div>\n </div>\n}\n", styles: ["::ng-deep .cdk-overlay-container .ngx-menu{--mdc-dialog-container-color: var(--ngx-menu-background-color, #2f2f2f)}::ng-deep .cdk-overlay-container .ngx-menu .mdc-dialog__container{transform-origin:top left}::ng-deep .cdk-overlay-container .ngx-menu .mdc-dialog--open .mdc-dialog__container{transform:none}::ng-deep .cdk-overlay-pane.ngx-menu .mat-mdc-dialog-surface{overflow:visible}:host{-webkit-user-select:none;user-select:none;z-index:1;position:relative;display:block}table{border-spacing:0;border-radius:5px;padding:4px 0;overflow:hidden}tr{color:var(--ngx-menu-text-color, #ccc);font-size:var(--ngx-menu-font-size, 14px);cursor:pointer;transition:background-color 75ms ease,color 75ms ease}tr:not(.disabled).hover,tr:not(.disabled).open{background-color:var(--ngx-menu-hover-background-color, #94ebeb);color:var(--ngx-menu-hover-text-color, #000)}tr:not(.disabled).hover a,tr:not(.disabled).open a{color:var(--ngx-menu-hover-text-color, #000)}tr:not(.separator){height:36px}tr.disabled .label{color:var(--ngx-menu-disabled-text-color, #919191)}tr .center{text-align:center}tr a{outline:0;display:flex;align-items:center;gap:10px;justify-content:space-between;height:100%;width:100%;position:relative;left:-16px;width:calc(100% + 32px);padding:0 16px}tr .label{min-width:100px}tr img{max-width:100%;max-height:100%;aspect-ratio:1}.hr{height:1px;text-align:center;position:relative}.hr:before,.hr:after{content:\"\";background:var(--ngx-menu-separator-color, #2a2a2a);display:block;position:absolute;top:0;bottom:0;height:1px;margin:auto;width:300px}.hr:before{right:calc(100% + 4px)}.hr:after{left:calc(100% + 4px)}hr{background:var(--ngx-menu-separator-color, #2a2a2a);border:0;height:1px;margin:0}.icon{width:24px;height:24px;padding-left:10px}.icon mat-icon{transform:translateY(2px)}.shortcut{color:var(--ngx-menu-shortcut-text-color, #848484);text-align:end;padding-right:10px;padding-left:12px}.label{height:var(--ngx-menu-item-height, 30px)}td{vertical-align:middle}.void,.owner-mask{position:absolute}.void{top:-100vh;right:-100vw;bottom:-100vh;left:-100vw;z-index:-2}\n"] }] }], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: i1.DomSanitizer }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [MAT_DIALOG_DATA] }] }, { type: i1$1.MatDialog, decorators: [{ type: Optional }] }, { type: i1$1.MatDialogRef, decorators: [{ type: Optional }] }], propDecorators: { data: [{ type: Input }], items: [{ type: Input }], config: [{ type: Input }], id: [{ type: Input }], overlayOverlap: [{ type: Input }], hoverDelay: [{ type: Input }], showDebugOverlay: [{ type: Input }], targetBounds: [{ type: Input }], ownerCords: [{ type: Input }], selfCords: [{ type: Input }], parentItem: [{ type: Input }], parentContext: [{ type: Input }], isLockedOpen: [{ type: Input }], onResize: [{ type: HostListener, args: ["window:resize"] }] } }); 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(() => {