UNPKG

@dotglitch/ngx-common

Version:

Angular components and utilities that are commonly used.

1 lines 833 kB
{"version":3,"file":"dotglitch-ngx-common.mjs","sources":["../../../packages/common/src/directives/utils.ts","../../../packages/common/src/components/menu/menu.component.ts","../../../packages/common/src/components/menu/menu.component.html","../../../packages/common/src/components/tooltip/tooltip.component.ts","../../../packages/common/src/components/tooltip/tooltip.component.html","../../../packages/common/src/directives/tooltip.directive.ts","../../../packages/common/src/directives/menu.directive.ts","../../../packages/common/src/directives/image-cache.directive.ts","../../../packages/common/src/pipes/html-bypass.pipe.ts","../../../packages/common/src/pipes/resource-bypass.pipe.ts","../../../packages/common/src/pipes/script-bypass.pipe.ts","../../../packages/common/src/pipes/style-bypass.pipe.ts","../../../packages/common/src/pipes/url-bypass.pipe.ts","../../../packages/common/src/utils/index.ts","../../../packages/common/src/services/dependency.service.ts","../../../packages/common/src/components/lazy-loader/types.ts","../../../packages/common/src/components/lazy-loader/lazy-loader.service.ts","../../../packages/common/src/components/lazy-loader/lazy-loader.component.ts","../../../packages/common/src/components/lazy-loader/lazy-loader.component.html","../../../packages/common/src/services/dialog.service.ts","../../../packages/common/src/services/fetch.service.ts","../../../packages/common/src/services/keyboard.service.ts","../../../packages/common/src/services/file.service.ts","../../../packages/common/src/services/theme.service.ts","../../../packages/common/src/services/navigation.service.ts","../../../packages/common/src/components/command-palette/shortcut/shortcut.component.ts","../../../packages/common/src/components/command-palette/shortcut/shortcut.component.html","../../../packages/common/src/components/command-palette/breadcrumb/breadcrumb.component.ts","../../../packages/common/src/components/command-palette/breadcrumb/breadcrumb.component.html","../../../packages/common/src/components/command-palette/command-palette.component.ts","../../../packages/common/src/components/command-palette/command-palette.component.html","../../../packages/common/src/services/command-palette.service.ts","../../../packages/common/src/components/lazy-loader/lazy-loader.module.ts","../../../packages/common/src/components/dynamic-html/types.ts","../../../packages/common/src/components/dynamic-html/dynamic-html.service.ts","../../../packages/common/src/components/dynamic-html/dynamic-html.component.ts","../../../packages/common/src/components/dynamic-html/dynamic-html.module.ts","../../../packages/common/src/components/filemanager/textextensions.ts","../../../packages/common/src/assets/mat-icons.ts","../../../packages/common/src/components/filemanager/icon-resolver.ts","../../../packages/common/src/components/tabulator/tabulator.component.ts","../../../packages/common/src/components/tabulator/tabulator.component.html","../../../packages/common/src/components/filemanager/helpers.ts","../../../packages/common/src/components/filemanager/file-grid/file-grid.component.ts","../../../packages/common/src/components/filemanager/file-grid/file-grid.component.html","../../../packages/common/src/components/filemanager/toolbar/icon-button/icon-button.component.ts","../../../packages/common/src/components/filemanager/toolbar/icon-button/icon-button.component.html","../../../packages/common/src/components/filemanager/toolbar/breadcrumb/breadcrumb.component.ts","../../../packages/common/src/components/filemanager/toolbar/breadcrumb/breadcrumb.component.html","../../../packages/common/src/components/filemanager/toolbar/toolbar.component.ts","../../../packages/common/src/components/filemanager/toolbar/toolbar.component.html","../../../packages/common/src/components/filemanager/tree-view/tree-view.component.ts","../../../packages/common/src/components/filemanager/tree-view/tree-view.component.html","../../../packages/common/src/components/types.ts","../../../packages/common/src/components/filemanager/filemanager.component.ts","../../../packages/common/src/components/filemanager/filemanager.component.html","../../../packages/common/src/components/vscode/ts-type-resolver/dependency-parser.ts","../../../packages/common/src/components/vscode/ts-type-resolver/update-emitter.ts","../../../packages/common/src/components/vscode/ts-type-resolver/recursion-depth.ts","../../../packages/common/src/components/vscode/ts-type-resolver/types.ts","../../../packages/common/src/components/vscode/ts-type-resolver/unpkg-source-resolver.ts","../../../packages/common/src/components/vscode/ts-type-resolver/import-resolver.ts","../../../packages/common/src/components/vscode/ts-type-resolver/dummy-source-cache.ts","../../../packages/common/src/components/vscode/ts-type-resolver/main.ts","../../../packages/common/src/components/vscode/vscode.component.ts","../../../packages/common/src/components/parallax-card/parallax-card.component.ts","../../../packages/common/src/components/parallax-card/parallax-card.component.html","../../../packages/common/src/components/react-magic-wrapper/react-magic-wrapper.component.ts","../../../packages/common/src/public-api.ts","../../../packages/common/src/dotglitch-ngx-common.ts"],"sourcesContent":["/**\n * This utils file exists outside of the strict angular DI zone\n * This enables opening popups without requiring absolute DI bindings.\n */\n\nexport const getPosition = (el: HTMLElement | PointerEvent, config: any = {}, bounds: DOMRect) => {\n // Bounds of the popup owner\n const src: DOMRect = !!el['nodeName']\n ? (el as HTMLElement).getBoundingClientRect()\n : {\n // It's a pointer event, so we'll take the X and Y from the pointer.\n x: el['clientX'],\n y: el['clientY'],\n // Set a default tiny size, so we don't divide by zero.\n width: 0.0001,\n height: 0.0001\n } as DOMRect;\n\n // Popup bounds\n const { width, height } = bounds;\n\n const winh = window.innerHeight;\n const winw = window.innerWidth;\n\n const cords = {\n top: null,\n left: null\n };\n\n if (config?.position == \"left\" || config?.position == \"right\" || !config?.position) {\n switch (config?.alignment) {\n\n case \"end\": {\n // vertically bind to bottom\n cords.top = src.y + src.height - height;\n break;\n }\n case \"afterend\": {\n // vertically bind below bottom\n cords.top = src.y + src.height;\n break;\n }\n case \"beforestart\": {\n // vertically bind above top\n cords.top = src.y - height;\n break;\n }\n case \"start\": {\n // vertically bind to top\n cords.top = src.y;\n break;\n }\n case \"center\":\n default: {\n // vertically center\n cords.top = (src.y + (src.height / 2)) - (height / 2);\n break;\n }\n }\n\n // Apply bounds to prevent the dialog from being cut-off screen\n // Lower bound\n cords.top = Math.max(config?.edgePadding || 0, cords.top);\n // Upper bound\n cords.top = Math.min(winh - height, cords.top);\n\n if (config?.position == \"left\") {\n cords.left = src.x - (width + (config?.arrowSize || 0) + (config?.arrowPadding || 0));\n }\n if (config?.position == \"right\" || !config?.position) {\n cords.left = src.x + (src.width + (config?.arrowSize || 0) + (config?.arrowPadding || 0));\n }\n\n // Lower bound\n cords.left = Math.max(config?.edgePadding || 0, cords.left);\n // Upper bound\n cords.left = Math.min(winw - width, cords.left);\n }\n else if (config?.position == \"top\" || config?.position == \"bottom\") {\n switch (config?.alignment) {\n case \"end\": {\n // vertically bind to right\n cords.left = src.x + src.width - width;\n break;\n }\n case \"afterend\": {\n // vertically bind past right\n cords.left = src.x + src.width;\n break;\n }\n case \"beforestart\": {\n // vertically bind before left\n cords.left = src.x - width;\n break;\n }\n case \"start\": {\n // vertically bind to left\n cords.left = src.x;\n break;\n }\n case \"center\":\n default: {\n // vertically center\n cords.left = (src.x + (src.width / 2)) - (width / 2);\n break;\n }\n }\n\n // Apply bounds to prevent the dialog from being cut-off screen\n // Lower bound\n cords.left = Math.max(config?.edgePadding || 0, cords.left);\n // Upper bound\n cords.left = Math.min(winw - width, cords.left);\n\n\n if (config?.position == \"top\") {\n cords.top = src.y - (height + (config?.arrowSize || 0) + (config?.arrowPadding || 0));\n }\n if (config?.position == \"bottom\") {\n cords.top = src.y + (src.height + (config?.arrowSize || 0) + (config?.arrowPadding || 0));\n }\n\n // Lower bound\n cords.top = Math.max(config?.edgePadding || 0, cords.top);\n // Upper bound\n cords.top = Math.min(winh - height, cords.top);\n }\n\n // Assign unit\n cords.top = cords.top + 'px';\n cords.left = cords.left + 'px';\n\n return cords;\n}\n","import { NgTemplateOutlet } from '@angular/common';\nimport { Component, HostListener, Inject, Input, Optional, TemplateRef, Type, ViewContainerRef } from '@angular/core';\nimport { DomSanitizer, createApplication } from '@angular/platform-browser';\nimport { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressSpinnerModule } from '@angular/material/progress-spinner';\n\nimport { ComponentPortal, PortalModule } from '@angular/cdk/portal';\nimport { firstValueFrom } from 'rxjs';\nimport { MenuItem, MenuOptions } from '../../types/menu';\n\ndeclare const Zone: any;\nconst zone = new Zone(Zone.current, { name: \"@dotglitch_menu\", properties: {} });\n\nexport const calcMenuItemBounds = async (menuItems: MenuItem[], dataObj: any) => {\n const data = {\n data: dataObj,\n items: menuItems,\n config: {},\n selfCords: { left: \"0px\", top: \"0px\" },\n ownerCords: { x: 0, y: 0, width: 0, height: 0 },\n id: null\n }\n\n return calcComponentBounds(MenuComponent, data);\n}\n\nconst calcComponentBounds = async (component: Type<any>, data: any) => {\n return new Promise<DOMRect>((res, rej) => {\n zone.run(async () => {\n const app = await createApplication({\n providers: [\n { provide: MAT_DIALOG_DATA, useValue: data }\n ]\n });\n\n const del = document.createElement(\"div\");\n del.classList.add(\"ngx-menu\");\n del.style.position = \"absolute\";\n del.style.left = '-1000vw';\n document.body.append(del);\n\n const base = app.bootstrap(component, del);\n const { instance } = base;\n\n await firstValueFrom(app.isStable);\n\n const el: HTMLElement = instance.viewContainer?.element?.nativeElement;\n\n const rect = el.getBoundingClientRect();\n app.destroy();\n del.remove();\n\n res(rect);\n });\n })\n}\n\nconst $data = Symbol(\"data\");\nconst $hover = Symbol(\"hover\");\n\n@Component({\n selector: 'ngx-menu',\n templateUrl: './menu.component.html',\n styleUrls: ['./menu.component.scss'],\n imports: [\n NgTemplateOutlet,\n PortalModule,\n MatIconModule,\n MatProgressSpinnerModule\n ],\n standalone: true,\n host: {\n \"[attr.tx]\": \"targetBounds?.x\",\n \"[attr.ty]\": \"targetBounds?.y\",\n \"[attr.th]\": \"targetBounds?.height\",\n \"[attr.tw]\": \"targetBounds?.width\",\n }\n})\nexport class MenuComponent {\n\n @Input() public data: any;\n @Input() public items: MenuItem[];\n @Input() public config: MenuOptions;\n @Input() public id: string;\n @Input() public overlayOverlap = 32;\n @Input() public hoverDelay = 400;\n @Input() public showDebugOverlay = false;\n @Input() public targetBounds: DOMRect;\n\n @Input() ownerCords: DOMRect;\n @Input() selfCords;\n @Input() parentItem;\n @Input() parentContext;\n @Input() isLockedOpen = false;\n\n public hasBootstrapped = false;\n public pointerIsOnVoid = false;\n public pointerHasBeenOverMask = false;\n parentIsNgxMenu = false;\n\n coverRectCords = {\n top: 0,\n left: 0,\n height: 0,\n width: 0\n }\n\n // Check if there are any slashes or dots -- that will clearly exclude it from being a mat icon\n public readonly matIconRx = /[\\/\\.]/i;\n showIconColumn = true;\n showShortcutColumn = true;\n\n template: TemplateRef<any>;\n templateType: \"template\" | \"component\";\n componentPortal: ComponentPortal<any>;\n private childDialogs: MatDialogRef<any>[] = [];\n\n constructor(\n public viewContainer: ViewContainerRef,\n public sanitizer: DomSanitizer,\n @Optional() @Inject(MAT_DIALOG_DATA) private _data: any,\n @Optional() public dialog: MatDialog, // optional only for the purpose of estimating dimensions\n @Optional() public dialogRef: MatDialogRef<any>\n ) {\n // Defaults are set before @Input() hooks evaluate\n this.dialog = this.dialog || this._data?.dialog;\n this.data = this._data?.data;\n this.ownerCords = this._data?.ownerCords;\n this.selfCords = this._data?.selfCords;\n this.items = this._data?.items;\n this.config = this._data?.config;\n this.id = this._data?.id;\n this.parentItem = this._data?.parentItem;\n this.parentContext = this._data?.parentContext;\n this.isLockedOpen = this.isLockedOpen || this._data?.config?.['_isLockedOpen'];\n this.parentIsNgxMenu = this._data?.parentIsNgxMenu;\n this.targetBounds = this._data?.targetBounds;\n\n this.template = this._data?.template;\n\n this.templateType = this.template instanceof TemplateRef ? \"template\" : \"component\";\n\n if (this.templateType == \"component\") {\n this.componentPortal = new ComponentPortal(this.template as any);\n }\n }\n\n ngOnInit() {\n\n this.items?.forEach(i => {\n if (typeof i == \"string\") return;\n\n // Set defaults\n i['_disabled'] = false;\n i['_visible'] = true;\n i['_context'] = (typeof i.context == \"function\")\n ? i.context(this.data)\n : i.context;\n\n if (i.label)\n try { i['_formattedLabel'] = this.formatLabel(i.label); } catch (e) { console.warn(e) }\n\n if (typeof i.isDisabled == \"function\")\n try { i['_disabled'] = i.isDisabled(this.data || {}, i['_context']); } catch(e) { console.warn(e) }\n\n if (typeof i.isVisible == \"function\")\n try { i['_visible'] = i.isVisible(this.data || {}, i['_context']); } catch (e) { console.warn(e) }\n\n if (typeof i.linkTemplate == \"function\")\n try { i['_link'] = i.linkTemplate(this.data || {}, i['_context']); } catch (e) { console.warn(e) }\n\n if (typeof i.iconTemplate == \"function\")\n try { i['_icon'] = i.iconTemplate(this.data || {}, i['_context']); } catch (e) { console.warn(e); }\n });\n\n // Show the icon column if there are any items with an icon\n this.showIconColumn = !!this.items?.find(i =>\n typeof i == \"object\" &&\n typeof i['icon'] == \"string\" &&\n i['icon'].length > 2\n );\n\n this.showShortcutColumn = !!this.items?.find(i =>\n typeof i == \"object\" &&\n typeof i['shortcut'] == \"string\" &&\n i['shortcut'].length > 2\n );\n\n if (this.ownerCords) {\n const selfY = parseInt(this.selfCords.top?.replace('px', '') || '0');\n const selfX = parseInt(this.selfCords.left?.replace('px', '') || '0');\n\n this.coverRectCords = {\n top: this.ownerCords.y - selfY - (this.overlayOverlap/2),\n left: this.ownerCords.x - selfX - (this.overlayOverlap/2),\n height: this.ownerCords.height + this.overlayOverlap,\n width: this.ownerCords.width + this.overlayOverlap\n }\n }\n\n if (this.config?.stayOpen)\n this.isLockedOpen = true;\n\n setTimeout(() => {\n this.hasBootstrapped = true;\n }, 200);\n }\n\n ngAfterViewInit() {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n el.addEventListener(\"keydown\", evt => {\n this.isLockedOpen = true;\n });\n el.addEventListener(\"pointerdown\", evt => {\n this.isLockedOpen = true;\n });\n el.addEventListener(\"touch\", evt => {\n this.isLockedOpen = true;\n });\n }\n\n ngOnDestroy() {\n //\n this.childDialogs.forEach(d => d.close({[$data]: true}))\n }\n\n /**\n *\n */\n async onMenuItemClick(item: MenuItem, row: HTMLTableRowElement, keepOpen = false) {\n if (typeof item == 'string') return null;\n if (item.separator) return null;\n\n const context = await item['_context'];\n\n // If cache is enabled, only load if we don't have any children.\n const forceLoad = (item.cacheResolvedChildren ? !item.children : true);\n\n if (item.childrenResolver && forceLoad) {\n item['_isResolving'] = true;\n item['_children'] = await item.childrenResolver(this.data, context);\n item['_isResolving'] = false;\n }\n else if (typeof item.children == \"function\" && forceLoad) {\n item['_isResolving'] = true;\n item['_children'] = await item.children(this.data, context);\n item['_isResolving'] = false;\n }\n else {\n item['_children'] = item.children;\n }\n\n if (item['_children'] || item.childTemplate)\n row['_open'] = true;\n\n if (!item.childTemplate && !item.children) {\n if (typeof item.action == \"function\") {\n const res = await item.action(this.data, context)\n this.close(res === undefined ? true : res);\n return res;\n }\n\n // If no action, this is simply a text item.\n return null;\n }\n\n // Need X pos, Y pos, width and height\n const bounds = row.getBoundingClientRect();\n\n const cords = {\n top: null,\n left: null,\n bottom: null,\n // right: null\n };\n\n // Set position coordinates\n const targetBounds = await (item.childTemplate\n ? calcComponentBounds(MenuComponent, { template: item.childTemplate })\n : calcMenuItemBounds(item['_children'], this.data));\n const { width, height } = targetBounds;\n\n if (bounds.y + height > window.innerHeight)\n cords.bottom = \"0px\";\n if (bounds.x + bounds.width + width > window.innerWidth)\n cords.left = ((bounds.x - width)) + \"px\";\n\n if (!cords.bottom) cords.top = bounds.y + \"px\";\n if (!cords.left) cords.left = bounds.x + bounds.width + \"px\";\n\n\n const config = structuredClone(this.config)\n config['_isLockedOpen'] = keepOpen;\n\n // Do not project in the top left corner -- this scenario\n // happens when a dialog opens as the parent is killed.\n if (cords.left == '0px' && cords.top == '0px')\n return;\n\n const dialogRef = this.dialog.open(MenuComponent, {\n position: cords,\n panelClass: [\"ngx-menu\"].concat(this.config?.customClass || []),\n backdropClass: \"ngx-menu-backdrop\",\n hasBackdrop: false,\n data: {\n data: this.data,\n ownerCords: row.getBoundingClientRect(),\n selfCords: cords,\n parentItem: item,\n parentContext: context,\n items: item['_children'],\n template: item.childTemplate,\n config: config,\n parentIsNgxMenu: true,\n targetBounds\n }\n });\n\n let _s = dialogRef\n .afterClosed()\n .subscribe(async (result) => {\n // Clicked \"void\" on a submenu\n if (typeof result == \"object\" && result[$data] == true) {\n this.close(result);\n }\n // Went back to parent menu -- do not close (same as result == null)\n else if (typeof result == \"object\" && result[$data] == false) {\n\n }\n // Got some other result value\n else if (result != null) {\n // Perform action callback\n if (typeof item.action == 'function') {\n this.close(await item.action(result, context));\n }\n // Just close.\n else {\n this.close();\n }\n }\n\n row['_open'] = false;\n\n this.childDialogs.splice(this.childDialogs.indexOf(dialogRef), 1);\n\n _s.unsubscribe();\n });\n\n this.childDialogs.push(dialogRef);\n return dialogRef;\n }\n\n /**\n *\n * @param label\n * @returns\n */\n formatLabel(label: string): string {\n return label.replace(/_([a-z0-9])_/i, (match, group) => `<u>${group}</u>`);\n }\n\n /**\n * Close the context menu under these circumstances\n */\n // @HostListener(\"window:resize\", ['event'])\n // @HostListener(\"window:blur\", ['event'])\n close(result?) {\n this.childDialogs.forEach(d => d.close())\n this.dialogRef?.close(result);\n }\n\n closeOnVoid(force = false) {\n if (!this.isLockedOpen || force) {\n this.close({[$data]: force});\n }\n }\n\n startHoverTimer(item, row) {\n\n // Invert check to make the logic simpler\n // TL;DR: if (any) of these are true, we will do the hover action\n if (!(\n Array.isArray(item.children) && item.children.length > 0 ||\n typeof item.children == \"function\" ||\n item.childTemplate ||\n item.childrenResolver\n ))\n return;\n\n item[$hover] = setTimeout(() => {\n delete item[$hover];\n\n if (!this.pointerIsOnVoid) {\n this.childDialogs.forEach(cd => cd.close());\n row['_open'] = true;\n this.onMenuItemClick(item, row);\n }\n }, this.hoverDelay);\n }\n\n stopHoverTimer(item) {\n item[$hover] && clearTimeout(item[$hover]);\n delete item[$hover];\n }\n\n private closeTimer: number;\n startCloseTimer() {\n this.closeTimer = setTimeout(() => {\n this.closeOnVoid();\n }, 500) as any;\n }\n stopCloseTimer() {\n clearTimeout(this.closeTimer);\n }\n\n /**\n * Check if the dialog is clipping offscreen\n * if so, move it back into view.\n */\n @HostListener(\"window:resize\")\n private onResize() {\n const el = this.viewContainer?.element?.nativeElement as HTMLElement;\n if (!el) return;\n\n const { width, height, x, y } = el.getBoundingClientRect();\n\n const target = document.querySelector(\".ngx-menu\") as HTMLElement;\n if (!target) return;\n\n // Move back into view if we're clipping outside of the bottom\n if (y + height > window.innerHeight) {\n const newTop = (window.innerHeight - (height + (this.config.edgePadding || 12))) + \"px\";\n target.style['margin-top'] = newTop;\n }\n\n // Move back into view if we're clipping off the right\n if (x + width > window.innerWidth) {\n const newLeft = (window.innerWidth - (width + (this.config.edgePadding || 12))) + \"px\"\n target.style['margin-left'] = newLeft;\n }\n }\n\n // If the void element gets stuck open, make wheel events pass through.\n onWheel(evt: WheelEvent) {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n el.style.display = \"none\";\n const target = document.elementFromPoint(evt.clientX, evt.clientY);\n el.style.display = \"block\";\n\n target.scroll({\n top: evt.deltaY + target.scrollTop,\n left: evt.deltaX + target.scrollLeft,\n behavior: \"smooth\"\n })\n }\n}\n","<!-- 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","import { NgComponentOutlet, NgTemplateOutlet } from '@angular/common';\nimport { Component, HostListener, Inject, Input, TemplateRef, Type, ViewContainerRef } from '@angular/core';\nimport { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogConfig } from '@angular/material/dialog';\nimport { Optional } from '@angular/core';\nimport { createApplication } from '@angular/platform-browser';\nimport { firstValueFrom } from 'rxjs';\nimport { TooltipOptions } from '../../types/tooltip';\nimport { MenuItem } from '../../types/menu';\nimport { MenuComponent } from '../menu/menu.component';\n\ndeclare const Zone;\nconst zone = new Zone(Zone.current, { name: \"@dotglitch_menu\", properties: {} });\n\nexport const calcTooltipBounds = async (template: TemplateRef<any> | Type<any>, data: any, matDialogConfig: MatDialogConfig) => {\n\n const args = {\n data: data || {},\n template,\n config: {},\n selfCords: { left: \"0px\", top: \"0px\" },\n ownerCords: { x: 0, y: 0, width: 0, height: 0 },\n id: null\n }\n\n // dimensions should be in px... Might need to handle vw/v\n if (matDialogConfig?.width && matDialogConfig?.height) {\n return {\n width: parseInt(matDialogConfig.width),\n height: parseInt(matDialogConfig.height),\n top: 0,\n left: 0,\n right: 0,\n bottom: 0\n } as DOMRect;\n }\n\n return new Promise<DOMRect>((res, rej) => {\n zone.run(async () => {\n // Forcibly bootstrap the ctx menu outside of the client application's zone.\n const app = await createApplication({\n providers: [\n { provide: MAT_DIALOG_DATA, useValue: args }\n ]\n });\n\n const del = document.createElement(\"div\");\n del.style.position = \"absolute\";\n del.style.left = '-1000vw';\n document.body.append(del);\n\n const base = app.bootstrap(TooltipComponent, del);\n const { instance } = base;\n\n await firstValueFrom(app.isStable);\n\n const el: HTMLElement = instance.viewContainer?.element?.nativeElement;\n\n const rect = el.getBoundingClientRect();\n app.destroy();\n del.remove();\n\n res(rect)\n });\n })\n}\n\n@Component({\n selector: 'ngx-tooltip',\n templateUrl: './tooltip.component.html',\n styleUrls: ['./tooltip.component.scss'],\n imports: [\n NgTemplateOutlet,\n NgComponentOutlet,\n MenuComponent\n ],\n standalone: true\n})\nexport class TooltipComponent {\n @Input() data: any;\n @Input() config: TooltipOptions;\n @Input() ownerCords: DOMRect;\n @Input() selfCords;\n @Input() template: TemplateRef<any> | Type<any> | MenuItem[];\n\n public isTemplate = false;\n public isMenu = false;\n public hasBootstrapped = false;\n public pointerIsOnVoid = false;\n public isLockedOpen = false;\n\n clientWidth = window.innerWidth;\n clientHeight = window.innerHeight;\n\n coverRectCords = {\n top: 0,\n left: 0,\n height: 0,\n width: 0\n }\n\n constructor(\n public viewContainer: ViewContainerRef,\n @Optional() @Inject(MAT_DIALOG_DATA) private _data: any,\n @Optional() public dialog: MatDialog, // optional only for the purpose of estimating dimensions\n @Optional() public dialogRef: MatDialogRef<any>,\n ) {\n // Defaults are set before @Input() hooks evaluate\n this.data = this.data || this._data?.data || {};\n this.config = this.config || this._data?.config;\n this.dialog = this.dialog || this._data?.dialog;\n this.template = this.template || this._data?.template;\n this.ownerCords = this.ownerCords || this._data?.ownerCords;\n this.selfCords = this.selfCords || this._data?.selfCords;\n this.isLockedOpen = this._data?.isLockedOpen || this.config?.stayOpen;\n }\n\n ngOnInit() {\n\n const selfY = parseInt(this.selfCords.top.replace('px', ''));\n const selfX = parseInt(this.selfCords.left.replace('px', ''));\n\n this.coverRectCords = {\n top: this.ownerCords.y - selfY - 16,\n left: this.ownerCords.x - selfX - 16,\n height: this.ownerCords.height + 32,\n width: this.ownerCords.width + 32\n }\n\n if (Array.isArray(this.template))\n this.isMenu = true;\n else if (this.template instanceof TemplateRef)\n this.isTemplate = true;\n else if (typeof this.template == \"function\")\n this.isTemplate = false;\n else\n throw new Error(\"Unrecognized template object provided.\");\n\n // TODO: resolve the event hook with the .void element\n setTimeout(() => {\n this.hasBootstrapped = true;\n if (this.pointerIsOnVoid && !this.isLockedOpen)\n this.dialogRef.close();\n }, 200);\n }\n\n ngAfterViewInit() {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n\n el.addEventListener(\"keydown\", evt => {\n this.isLockedOpen = true;\n });\n\n el.addEventListener(\"pointerdown\", evt => {\n this.isLockedOpen = true;\n });\n\n el.addEventListener(\"touch\", evt => {\n this.isLockedOpen = true;\n });\n }\n\n @HostListener(\"window:keydown\", ['$event'])\n onKeyDown(evt: KeyboardEvent) {\n if (this.config?.freezeOnKeyCode) {\n if (evt.code == this.config.freezeOnKeyCode)\n this.isLockedOpen = true;\n }\n }\n\n onVoidPointerDown(evt: PointerEvent) {\n if (!this.isLockedOpen) {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n el.querySelector(\".void\").remove();\n\n setTimeout(() => {\n const clonedEvt = new PointerEvent(\"pointerdown\", evt);\n const target = document.elementFromPoint(evt.clientX, evt.clientY) as HTMLElement;\n\n console.log(\"DEBUG EVENTS\", {evt, clonedEvt});\n target.dispatchEvent(clonedEvt);\n }, 15)\n }\n\n this.closeOnVoid(true)\n }\n\n // If the void element gets stuck open, make wheel events pass through.\n onWheel(evt: WheelEvent) {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n el.style.display = \"none\";\n const target = document.elementFromPoint(evt.clientX, evt.clientY);\n el.style.display = \"block\";\n\n target.scroll({\n top: evt.deltaY + target.scrollTop,\n left: evt.deltaX + target.scrollLeft,\n behavior: \"smooth\"\n });\n }\n\n /**\n * Close the tooltip if these actions occur\n */\n @HostListener(\"window:resize\")\n @HostListener(\"window:blur\")\n @HostListener(\"pointerleave\")\n private onClose() {\n if (!this.isLockedOpen)\n this.dialogRef?.close();\n\n this.clientWidth = window.innerWidth;\n this.clientHeight = window.innerHeight;\n }\n\n closeOnVoid(force = false) {\n if (!this.isLockedOpen || force)\n this.dialogRef.close();\n }\n}\n","<!-- 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","import { Directive, Input, HostListener, TemplateRef, Type, ViewContainerRef } from '@angular/core';\nimport { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';\nimport { getPosition } from './utils';\nimport { TooltipComponent, calcTooltipBounds } from '../components/tooltip/tooltip.component';\nimport { TooltipOptions } from '../types/tooltip';\nimport { MenuItem, MenuOptions } from '../types/menu';\nimport { ulid } from 'ulidx';\nimport { firstValueFrom } from 'rxjs';\nimport { MenuComponent } from '../components/menu/menu.component';\n\n@Directive({\n selector: '[ngx-tooltip]',\n providers: [\n MatDialog\n ],\n standalone: true\n})\nexport class TooltipDirective {\n\n /**\n */\n @Input(\"ngx-tooltip\") template: TemplateRef<any> | Type<any> | MenuItem[];\n\n /**\n * Configuration for opening the app menu\n */\n @Input(\"ngx-tooltip-config\") config: TooltipOptions = {};\n\n /**\n * Arbitrary data to pass into the template\n */\n @Input(\"ngx-tooltip-context\") data: any = {};\n\n private isCursorOverTarget = false;\n private dialogIsOpen = false;\n\n constructor(\n private dialog: MatDialog,\n private viewContainer: ViewContainerRef\n ) {\n }\n\n ngAfterViewInit() {\n const el = this.viewContainer.element.nativeElement as HTMLElement;\n\n this.config?.triggers?.forEach(t => {\n el.addEventListener(t, () => {\n if (t == \"click\")\n this.config.stayOpen = true;\n\n this.open();\n })\n })\n }\n\n async open() {\n if (!this.dialogIsOpen) {\n const el = this.viewContainer.element.nativeElement;\n this.dialogIsOpen = true;\n await openTooltip(this.dialog, this.template, this.data, el, this.config);\n this.dialogIsOpen = false;\n }\n }\n\n @HostListener('pointerenter', ['$event'])\n public async onPointerEnter(evt: PointerEvent) {\n // If the template is not a template ref, do nothing.\n if (!(this.template instanceof TemplateRef))\n return;\n\n if (Array.isArray(this.config?.triggers) && !this.config.triggers.includes(\"hover\")) {\n return;\n }\n\n this.isCursorOverTarget = true;\n\n setTimeout(async () => {\n // If the cursor moved away in the time\n if (!this.isCursorOverTarget)\n return;\n\n this.open();\n }, this.config.delay ?? 250);\n }\n\n @HostListener('pointerleave', ['$event'])\n public async onPointerLeave(evt: PointerEvent) {\n this.isCursorOverTarget = false;\n }\n}\n\n// Helper to open the context menu without using the directive.\nexport const openTooltip = async (\n dialog: MatDialog,\n template: TemplateRef<any> | Type<any> | MenuItem[],\n data: any,\n el: HTMLElement,\n config?: TooltipOptions,\n focusTrap = false,\n matPopupOptions?: MatDialogConfig<any>\n) => {\n\n const component = Array.isArray(template) ? MenuComponent : template;\n const rect = await calcTooltipBounds(component, data, matPopupOptions);\n const ownerCords = el.getBoundingClientRect();\n const cords = getPosition(el, config, rect);\n const specificId = ulid();\n\n return firstValueFrom(\n dialog.open(TooltipComponent, {\n autoFocus: focusTrap,\n restoreFocus: focusTrap,\n data: {\n dialog,\n data: data,\n template: template,\n config: config,\n matPopupOptions,\n ownerCords: ownerCords,\n selfCords: cords,\n id: specificId\n },\n panelClass: [\"ngx-tooltip\", 'ngx-' + specificId].concat(config?.customClass || []),\n position: cords,\n hasBackdrop: false,\n ...matPopupOptions\n })\n .afterClosed()\n );\n};\n\n@Directive({\n selector: '[ngx-dropdown],[ngx-dropdown-config]',\n providers: [\n MatDialog\n ],\n standalone: true\n})\nexport class DropdownDirective extends TooltipDirective {\n /**\n * The items that will be bound to the menu that pops\n * up when the user clicks the element.\n */\n @Input(\"ngx-dropdown\") override template: TemplateRef<any> | Type<any> | MenuItem[];\n\n /**\n * Configuration for opening the app menu\n */\n @Input(\"ngx-dropdown-config\") _config: TooltipOptions = {};\n\n ngOnInit() {\n // Set default values\n this._config.position = this._config.position ?? \"bottom\";\n this._config.alignment = this._config.alignment ?? \"start\";\n this._config.stayOpen = this._config.stayOpen ?? true;\n\n this.config = this._config;\n }\n}\n","import { Directive, HostListener, Input, ViewContainerRef } from '@angular/core';\nimport { MatDialog } from '@angular/material/dialog';\nimport { getPosition } from './utils';\nimport { MenuItem, MenuOptions } from '../types/menu';\nimport { MenuComponent, calcMenuItemBounds } from '../components/menu/menu.component';\nimport { ulid } from 'ulidx';\nimport { firstValueFrom } from 'rxjs'