UNPKG

@deepkit/desktop-ui

Version:

Library for desktop UI widgets in Angular 10+

1 lines 702 kB
{"version":3,"file":"deepkit-desktop-ui.mjs","sources":["../../src/browser-text.ts","../../src/components/form/form.component.ts","../../src/core/form.ts","../../src/core/utils.ts","../../src/components/document.ts","../../src/components/app/utils.ts","../../src/components/app/position.ts","../../src/components/app/menu-electron.ts","../../src/components/window/window-menu.ts","../../src/components/window/window-state.ts","../../src/components/app/drag.ts","../../src/components/splitter/splitter.component.ts","../../src/components/window/window-content.component.ts","../../src/components/icon/icon.component.ts","../../src/components/window/window-header.component.ts","../../src/components/app/app.ts","../../src/components/window/window.component.ts","../../src/components/button/button.component.ts","../../src/components/app/reactivate-change-detection.ts","../../src/components/button/dropdown.component.ts","../../src/components/adaptive-container/adaptive-container.component.ts","../../src/components/app/pending-tasks.ts","../../src/components/app/dui-responsive.directive.ts","../../src/components/app/menu.ts","../../src/components/app/pipes.ts","../../src/components/app/state.ts","../../src/components/app/style.component.ts","../../src/components/button/tab-button.component.ts","../../src/components/checkbox/checkbox.component.ts","../../src/components/core/render-component.directive.ts","../../src/components/dialog/dialog.component.ts","../../src/components/input/input.component.ts","../../src/components/dialog/dialog.ts","../../src/components/indicator/indicator.component.ts","../../src/components/layout/label.component.ts","../../src/components/layout/section-header.component.ts","../../src/components/list/list.component.ts","../../src/components/radio/radio-box.component.ts","../../src/components/select/select-box.component.ts","../../src/components/table/table.component.ts","../../src/components/tabs/tab.component.ts","../../src/components/tabs/tabs.component.ts","../../src/components/slider/slider.component.ts","../../src/components/window/external-window.component.ts","../../src/components/window/external-window.ts","../../src/components/window/window-footer.component.ts","../../src/components/window/window-sidebar.component.ts","../../src/index.ts","../../src/deepkit-desktop-ui.ts"],"sourcesContent":["\nexport class BrowserText {\n protected canvas = document.createElement('canvas');\n\n protected context = this.canvas.getContext('2d')!;\n\n constructor(public fontSize: number = 11, public fontFamily: string = getComputedStyle(document.querySelector('.dui-body') || document.body).fontFamily) {\n document.body.appendChild(this.canvas);\n this.canvas.style.display = 'none';\n }\n\n destroy() {\n document.body.removeChild(this.canvas);\n }\n\n getDimensions(text: string) {\n this.context.font = this.fontSize + 'px ' + this.fontFamily;\n const m = this.context.measureText(text);\n return {\n width: m.width,\n height: m.actualBoundingBoxAscent + m.actualBoundingBoxDescent\n };\n }\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\nimport { booleanAttribute, ChangeDetectorRef, Component, ContentChild, HostListener, input, output, signal, SkipSelf } from '@angular/core';\nimport { FormGroup, FormsModule, NgControl, ReactiveFormsModule } from '@angular/forms';\nimport { KeyValuePipe } from '@angular/common';\n\n@Component({\n selector: 'dui-form-row',\n template: `\n <div class=\"label\" [style.width.px]=\"labelWidth()\">{{ label() }}@if (description()) {\n <div class=\"description\">{{ description() }}</div>\n }</div>\n <div class=\"field\">\n <ng-content></ng-content>\n\n @if (ngControl && ngControl.errors && ngControl.touched) {\n <div class=\"error\">\n @for (kv of ngControl.errors|keyvalue; track kv) {\n <div>\n {{ isString(kv.value) ? '' : kv.key }}{{ isString(kv.value) ? kv.value : '' }}\n </div>\n }\n </div>\n }\n </div>`,\n host: {\n '[class.left-aligned]': 'left()',\n },\n styleUrls: ['./form-row.component.scss'],\n imports: [KeyValuePipe],\n})\nexport class FormRowComponent {\n label = input<string>('');\n description = input<string>('');\n\n labelWidth = input<number>();\n left = input(false, { transform: booleanAttribute });\n\n @ContentChild(NgControl, { static: false }) ngControl?: NgControl;\n\n isString(v: any) {\n return 'string' === typeof v;\n }\n}\n\n@Component({\n selector: 'dui-form',\n template: `\n <form [formGroup]=\"formGroup()\" (submit)=\"$event.preventDefault();submitForm()\">\n <ng-content></ng-content>\n @if (errorText(); as text) {\n <div class=\"error\">{{ text }}</div>\n }\n </form>\n `,\n styleUrls: ['./form.component.scss'],\n imports: [FormsModule, ReactiveFormsModule],\n})\nexport class FormComponent {\n formGroup = input<FormGroup>(new FormGroup({}));\n\n disabled = input<boolean>(false);\n\n submit = input<() => Promise<any> | any>();\n\n success = output();\n error = output();\n\n errorText = signal('');\n submitting = signal(false);\n\n constructor(\n protected cd: ChangeDetectorRef,\n @SkipSelf() protected cdParent: ChangeDetectorRef,\n ) {\n }\n\n @HostListener('keyup', ['$event'])\n onEnter(event: KeyboardEvent) {\n if (this.submit() && event.key.toLowerCase() === 'enter'\n && event.target && (event.target as HTMLElement).tagName.toLowerCase() === 'input') {\n this.submitForm();\n }\n }\n\n get invalid() {\n return this.formGroup().invalid;\n }\n\n async submitForm() {\n if (this.disabled()) return;\n if (this.submitting()) return;\n const formGroup = this.formGroup();\n if (formGroup.invalid) return;\n this.errorText.set('');\n\n this.submitting.set(true);\n\n try {\n const submit = this.submit();\n if (submit) {\n try {\n await submit();\n this.success.emit();\n } catch (error: any) {\n this.error.emit(error);\n\n if (error.errors && error.errors[0]) {\n //we got a validation-like error object\n for (const item of error.errors) {\n const control = formGroup.get(item.path);\n if (control) {\n control.setErrors({\n ...control.errors,\n [item.code]: item.message,\n });\n }\n }\n } else {\n this.errorText.set(error.message || error);\n }\n }\n }\n } finally {\n this.submitting.set(false);\n }\n }\n}\n","import { Directive, forwardRef, HostBinding, inject, Injector, Input, input, model, OnDestroy, Type } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';\nimport { FormComponent } from '../components/form/form.component';\n\nexport function ngValueAccessor<T>(clazz: Type<T>) {\n return {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => clazz),\n multi: true,\n };\n}\n\n@Directive()\nexport class ValueAccessorBase<T> implements ControlValueAccessor, OnDestroy {\n private _ngControl?: NgControl;\n private _ngControlFetched = false;\n\n value = model<T | undefined>(undefined);\n\n disabled = model(false);\n\n @Input() valid?: boolean;\n @Input() error?: boolean;\n\n protected formComponent?: FormComponent;\n protected _changedCallback: ((value: T | undefined) => void)[] = [];\n protected _touchedCallback: (() => void)[] = [];\n\n @HostBinding('class.disabled')\n get isDisabled(): boolean {\n if (this.formComponent && this.formComponent.disabled()) return true;\n\n if (this.ngControl) {\n return !!this.ngControl.disabled;\n }\n\n return this.disabled();\n }\n\n @HostBinding('class.valid')\n get isValid() {\n return this.valid === true;\n }\n\n @HostBinding('class.error')\n get isError() {\n if (undefined === this.error && this.ngControl) {\n return (this.ngControl.dirty || this.ngControl.touched) && this.ngControl.invalid;\n }\n\n return this.error;\n }\n\n @HostBinding('class.required')\n required = input(false);\n\n protected injector = inject(Injector);\n\n get ngControl(): NgControl | undefined {\n if (!this._ngControlFetched) {\n try {\n this._ngControl = this.injector.get(NgControl);\n } catch (e) {\n }\n this._ngControlFetched = true;\n }\n\n return this._ngControl;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled.set(isDisabled);\n }\n\n ngOnDestroy(): void {\n }\n\n /**\n * This is called whenever value() needs to update. Either through Angular forms or through setValue().\n * Do not call this method in UI code, use setValue() instead so that angular forms informed about the change.\n *\n * This is a good place to normalize values whenever they change. (like clamping a number to a range)\n */\n writeValue(value?: T) {\n this.value.set(value);\n }\n\n /**\n * Set the value from UI code.\n */\n setValue(value: T | undefined) {\n this.writeValue(value);\n for (const callback of this._changedCallback) {\n callback(this.value());\n }\n }\n\n /**\n * Call this method to signal Angular's form or other users that this widget has been touched.\n */\n touch() {\n for (const callback of this._touchedCallback) {\n callback();\n }\n }\n\n registerOnChange(fn: (value: T | undefined) => void) {\n this._changedCallback.push(fn);\n }\n\n registerOnTouched(fn: () => void) {\n this._touchedCallback.push(fn);\n }\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\n/**\n * @reflection never\n */\nimport { Subscription } from 'rxjs';\nimport { ChangeDetectorRef, EventEmitter, Inject, Injectable } from '@angular/core';\nimport { nextTick } from '@deepkit/core';\nimport { DOCUMENT } from '@angular/common';\n\nconst electron = 'undefined' === typeof window ? undefined : (window as any).electron || ((window as any).require ? (window as any).require('electron') : undefined);\n\nexport type ElectronOrBrowserWindow = Window & {\n setVibrancy?: (vibrancy: string) => void;\n addListener?: (event: string, listener: (...args: any[]) => void) => void;\n removeListener?: (event: string, listener: (...args: any[]) => void) => void;\n};\n\n@Injectable({ providedIn: 'root' })\nexport class BrowserWindow {\n constructor(@Inject(DOCUMENT) private window?: ElectronOrBrowserWindow) {\n }\n\n isElectron() {\n return !!this.window?.setVibrancy;\n }\n\n getWindow(): ElectronOrBrowserWindow | undefined {\n return this.window;\n }\n\n setVibrancy(vibrancy: string): void {\n if (!this.window) return;\n\n if (this.window.setVibrancy) {\n this.window.setVibrancy(vibrancy);\n } else {\n console.warn('setVibrancy is not supported by this window.');\n }\n }\n\n addListener(event: string, listener: (...args: any[]) => void): void {\n if (!this.window) return;\n\n if (this.window.addEventListener) {\n this.window.addEventListener(event, listener);\n } else if (this.window.addListener) {\n this.window.addListener(event, listener);\n }\n }\n\n removeListener(event: string, listener: (...args: any[]) => void): void {\n if (!this.window) return;\n\n if (this.window.removeEventListener) {\n this.window.removeEventListener(event, listener);\n } else if (this.window.removeListener) {\n this.window.removeListener(event, listener);\n }\n }\n}\n\n@Injectable({ providedIn: 'root' })\nexport class Electron {\n public static getRemote(): any {\n if (!electron) {\n throw new Error('No Electron available.');\n }\n\n return electron.remote;\n }\n\n public static getIpc(): any {\n if (!electron) {\n throw new Error('No Electron available.');\n }\n\n return electron.ipcRenderer;\n }\n\n public static isAvailable(): any {\n return !!electron;\n }\n\n public static getRemoteOrUndefined(): any {\n return electron ? electron.remote : undefined;\n }\n\n public static getProcess() {\n return Electron.getRemote().process;\n }\n}\n\nexport class AsyncEventEmitter<T> extends EventEmitter<T> {\n emit(value?: T): void {\n super.emit(value);\n }\n\n subscribe(generatorOrNext?: any, error?: any, complete?: any): Subscription {\n return super.subscribe(generatorOrNext, error, complete);\n }\n}\n\n\nexport class ExecutionState {\n public running = false;\n public error: string = '';\n\n constructor(\n protected readonly cd: ChangeDetectorRef,\n protected readonly func: (...args: any[]) => Promise<any> | any,\n ) {\n }\n\n public async execute(...args: any[]) {\n if (this.running) {\n throw new Error('Executor still running');\n }\n\n this.running = true;\n this.error = '';\n this.cd.detectChanges();\n\n try {\n return await this.func(...args);\n } catch (error: any) {\n this.error = error.message || error.toString();\n throw error;\n } finally {\n this.running = false;\n this.cd.detectChanges();\n }\n\n }\n}\n\n/**\n * Checks if `target` is children of `parent` or if `target` is `parent`.\n */\nexport function isTargetChildOf(target: HTMLElement | EventTarget | null, parent: HTMLElement): boolean {\n if (!target) return false;\n\n if (target === parent) return true;\n\n return parent.contains(target as Node);\n}\n\nexport function isMacOs() {\n if ('undefined' === typeof navigator) return false;\n return navigator.platform.indexOf('Mac') > -1;\n}\n\nexport function isWindows() {\n if ('undefined' === typeof navigator) return false;\n return navigator.platform.indexOf('Win') > -1;\n}\n\n/**\n * Checks if `target` is children of `parent` or if `target` is `parent`.\n */\nexport function findParentWithClass(start: HTMLElement, className: string): HTMLElement | undefined {\n let current: HTMLElement | null = start;\n do {\n if (current.classList.contains(className)) return current;\n current = current.parentElement;\n } while (current);\n\n return undefined;\n}\n\nexport function triggerResize() {\n if ('undefined' === typeof window) return;\n nextTick(() => {\n window.dispatchEvent(new Event('resize'));\n });\n}\n\nexport type FocusWatcherUnsubscribe = () => void;\n\n/**\n * Observes focus changes on target elements and emits when focus is lost.\n *\n * This is used to track multi-element focus changes, such as when a user clicks from a dropdown toggle into the dropdown menu.\n */\nexport function focusWatcher(\n target: Element, allowedFocuses: Element[] = [],\n onBlur: (event: FocusEvent) => void,\n customChecker?: (currentlyFocused: Element | null) => boolean,\n): FocusWatcherUnsubscribe {\n const doc = target.ownerDocument;\n if (doc.body.tabIndex === -1) doc.body.tabIndex = 1;\n\n let currentlyFocused: Element | null = target;\n\n let subscribed = true;\n\n function isFocusAllowed() {\n if (!currentlyFocused) {\n return false;\n }\n\n if (currentlyFocused === target || target.contains(currentlyFocused)) {\n return true;\n }\n\n for (const focus of allowedFocuses) {\n if (focus && currentlyFocused === focus || focus.contains(currentlyFocused)) {\n return true;\n }\n }\n\n return customChecker ? customChecker(currentlyFocused) : false;\n }\n\n function emitBlurIfNeeded(event: FocusEvent) {\n if (!currentlyFocused) {\n // Shouldn't be possible to have no element at all with focus.\n // This means usually that the item that had previously focus was deleted.\n currentlyFocused = target;\n }\n if (subscribed && !isFocusAllowed()) {\n onBlur(event);\n unsubscribe();\n return true;\n }\n return false;\n }\n\n function onFocusOut(event: FocusEvent) {\n currentlyFocused = null;\n emitBlurIfNeeded(event);\n }\n\n function onFocusIn(event: FocusEvent) {\n currentlyFocused = event.target as any;\n emitBlurIfNeeded(event);\n }\n\n function onMouseDown(event: FocusEvent) {\n currentlyFocused = event.target as any;\n if (emitBlurIfNeeded(event)) {\n event.stopImmediatePropagation();\n event.preventDefault();\n }\n }\n\n doc.addEventListener('mousedown', onMouseDown, true);\n doc.addEventListener('focusin', onFocusIn);\n doc.addEventListener('focusout', onFocusOut);\n\n function unsubscribe() {\n if (!subscribed) return;\n subscribed = false;\n doc.removeEventListener('mousedown', onMouseDown, true);\n doc.removeEventListener('focusin', onFocusIn);\n doc.removeEventListener('focusout', onFocusOut);\n }\n\n return unsubscribe;\n}\n\nexport function redirectScrollableParentsToWindowResize(node: Element, passive = true) {\n const parents = getScrollableParents(node);\n\n function redirect() {\n window.dispatchEvent(new Event('resize'));\n }\n\n for (const parent of parents) {\n parent.addEventListener('scroll', redirect, { passive });\n }\n\n return () => {\n for (const parent of parents) {\n parent.removeEventListener('scroll', redirect);\n }\n };\n}\n\nexport function getScrollableParents(node: Element): Element[] {\n const scrollableParents: Element[] = [];\n let parent = node.parentNode;\n\n while (parent) {\n if (!(parent instanceof Element)) {\n parent = parent.parentNode;\n continue;\n }\n const computedStyle = window.getComputedStyle(parent);\n const overflow = computedStyle.getPropertyValue('overflow');\n if (overflow === 'overlay' || overflow === 'scroll' || overflow === 'auto') {\n scrollableParents.push(parent);\n }\n\n parent = parent.parentNode;\n }\n\n return scrollableParents;\n}\n\nexport function trackByIndex(index: number) {\n return index;\n}\n","import { inject, Injectable, OnDestroy, signal } from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\n\n@Injectable({ providedIn: 'root' })\nexport class DuiDocument implements OnDestroy {\n document = inject(DOCUMENT, { optional: true });\n\n activeElement = signal<Element | undefined>(undefined);\n\n onFocus = () => {\n if (!this.document) return;\n this.activeElement.set(this.document.activeElement || undefined);\n };\n\n constructor() {\n this.document?.addEventListener('focusin', this.onFocus);\n this.document?.addEventListener('focusout', this.onFocus);\n }\n\n ngOnDestroy() {\n this.document?.removeEventListener('focusin', this.onFocus);\n this.document?.removeEventListener('focusout', this.onFocus);\n }\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\nimport { Directive, ElementRef, HostListener, inject, Input, input, OnChanges, OnDestroy, OnInit, output } from '@angular/core';\nimport { nextTick } from '@deepkit/core';\nimport { Electron } from '../../core/utils';\nimport { DOCUMENT } from '@angular/common';\n\nexport function injectDocument(): Document | undefined {\n return inject(DOCUMENT, { optional: true }) || undefined;\n}\n\nexport function injectElementRef(): ElementRef<HTMLElement> {\n return inject(ElementRef);\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, value));\n}\n\n@Directive({ selector: '[openExternal], a[href]' })\nexport class OpenExternalDirective implements OnChanges {\n openExternal = input<string>('');\n href = input<string>('');\n\n constructor(private element: ElementRef) {\n // this.element.nativeElement.href = '#';\n }\n\n ngOnChanges(): void {\n // this.element.nativeElement.href = this.getLink();\n if (this.element.nativeElement instanceof HTMLAnchorElement) {\n this.element.nativeElement.setAttribute('href', this.getLink());\n }\n }\n\n getLink() {\n return this.openExternal() || this.href();\n }\n\n @HostListener('click', ['$event'])\n onClick(event: Event) {\n event.stopPropagation();\n event.preventDefault();\n\n if (Electron.isAvailable()) {\n event.preventDefault();\n Electron.getRemote().shell.openExternal(this.getLink());\n } else {\n window.open(this.getLink(), '_blank');\n }\n }\n}\n\nlet lastScheduleResize: any;\n\nexport function scheduleWindowResizeEvent() {\n if (lastScheduleResize) cancelAnimationFrame(lastScheduleResize);\n lastScheduleResize = nextTick(() => {\n window.dispatchEvent(new Event('resize'));\n lastScheduleResize = undefined;\n });\n}\n\n@Directive({ selector: 'ng-template[templateType]' })\nexport class TemplateContextTypeDirective<T> {\n @Input() protected templateType!: T;\n\n public static ngTemplateContextGuard<T>(dir: TemplateContextTypeDirective<T>, ctx: unknown,\n ): ctx is T {\n return true;\n }\n}\n\n\n@Directive({ selector: '[onDomCreation]' })\nexport class OnDomCreationDirective implements OnInit, OnDestroy {\n onDomCreation = output<Element>();\n onDomCreationDestroy = output<Element>();\n private element: ElementRef<HTMLElement> = injectElementRef();\n\n ngOnInit() {\n this.onDomCreation.emit(this.element.nativeElement);\n }\n\n ngOnDestroy() {\n this.onDomCreationDestroy.emit(this.element.nativeElement);\n }\n}\n\nexport type RegisterEventListenerRemove = () => void;\n\nexport function registerEventListener<\n T extends Element | Document | Window,\n K extends keyof KeyMap,\n KeyMap extends GlobalEventHandlersEventMap = T extends Document ? DocumentEventMap : T extends Window ? WindowEventMap : HTMLElementEventMap,\n>(\n element: T,\n type: K,\n listener: (ev: KeyMap[K]) => any,\n options?: AddEventListenerOptions,\n): RegisterEventListenerRemove {\n element.addEventListener(type as string, listener as EventListenerOrEventListenerObject, options);\n return () => element.removeEventListener(type as string, listener as EventListenerOrEventListenerObject, options);\n}\n","import { Directive, OnDestroy, OnInit, output } from '@angular/core';\nimport { injectElementRef } from './utils';\n\nexport type PositionObserverDisconnect = () => void;\n\nexport function observePosition(\n element: Element,\n callback: (entry: DOMRectReadOnly) => void,\n debugPosition = false,\n): PositionObserverDisconnect {\n if ('undefined' === typeof IntersectionObserver) return () => undefined;\n let lastObserver: IntersectionObserver | undefined;\n\n const resizeObserver = new ResizeObserver(() => {\n setupObserver();\n });\n resizeObserver.observe(element);\n const debug = debugPosition ? document.createElement('div') : undefined;\n if (debug) {\n debug.style.position = 'fixed';\n debug.style.zIndex = '999999';\n debug.style.backgroundColor = 'rgba(255, 0, 0, 0.5)';\n debug.style.pointerEvents = 'none';\n document.body.appendChild(debug);\n }\n\n function setupObserver() {\n const box = element.getBoundingClientRect();\n if (box.width === 0 || box.height === 0) {\n return;\n }\n const vw = document.body.clientWidth;\n const vh = window.innerHeight;\n const top = Math.floor(box.top);\n const left = Math.floor(box.left);\n const right = Math.floor(vw - box.right);\n const bottom = Math.floor(vh - box.bottom);\n\n const rootMargin = `${-top}px ${-right}px ${-bottom}px ${-left}px`;\n if (debug) {\n debug.style.top = `${top}px`;\n debug.style.left = `${left}px`;\n debug.style.right = `${right}px`;\n debug.style.bottom = `${bottom}px`;\n }\n\n const size = box.width > box.height ? box.width : box.height;\n const onePixelInPercent = 1 / size;\n\n // To not create a new IntersectionObserver every event, we use several thresholds.\n // To not create too many thresholds (when the element is very big),\n // we generate 100 items, each the size of one pixel in percent.\n // In the callback we rebuild a new observer when intersectionRatio is\n // either bigger than the biggest threshold or smaller than the smallest threshold.\n const thresholdCount = Math.min(size, 100);\n const thresholds = Array.from({ length: thresholdCount }, (_, i) => 1 - ((i + 1) * onePixelInPercent));\n // console.log('rootMargin', rootMargin, box, thresholds);\n const minRatio = thresholds[thresholds.length - 1];\n\n if (lastObserver) lastObserver.disconnect();\n lastObserver = new IntersectionObserver(\n (entries, observer) => {\n for (const entry of entries) {\n callback(entry.boundingClientRect);\n }\n if (entries[0].intersectionRatio <= minRatio) {\n observer.disconnect();\n setupObserver();\n }\n },\n { root: null, rootMargin, threshold: thresholds },\n );\n lastObserver.observe(element);\n }\n\n setupObserver();\n\n return () => {\n lastObserver?.disconnect();\n lastObserver = undefined;\n resizeObserver.disconnect();\n if (debug) {\n debug.remove();\n }\n };\n}\n\n@Directive({\n selector: '[duiPositionChange]',\n})\nexport class PositionChangeDirective implements OnInit, OnDestroy {\n duiPositionChange = output<any>();\n\n elementRef = injectElementRef();\n observer = observePosition(this.elementRef.nativeElement, (rect) => {\n this.duiPositionChange.emit(rect);\n });\n\n ngOnInit() {\n }\n\n ngOnDestroy() {\n this.observer();\n }\n}\n","import type { MenuBase } from './menu';\n\n// Type from electron menu item\nexport type BuiltTemplateItem = { [name: string]: any };\n\n// This is probably broken for current Electron versions, but we keep it for future work\nexport function buildElectronMenuTemplate(menu: MenuBase) {\n const submenu: any[] = [];\n for (const item of menu.children()) {\n if (item === menu) continue;\n if (!item.validOs()) {\n continue;\n }\n submenu.push(buildElectronMenuTemplate(item));\n }\n\n const result: BuiltTemplateItem = {\n click: () => {\n menu.active();\n },\n };\n\n const label = menu.label();\n if (label) result['label'] = label;\n const sublabel = menu.sublabel();\n if (sublabel) result['sublabel'] = sublabel;\n\n if (!menu.enabled()) result['enabled'] = false;\n if (menu.type) result['type'] = menu.type;\n\n const accelerator = menu.accelerator();\n if (accelerator) result['accelerator'] = accelerator;\n const role = menu.role();\n if (role) result['role'] = role;\n if (menu.type) result['type'] = menu.type;\n if (accelerator) result['accelerator'] = accelerator;\n if (submenu.length) result['submenu'] = submenu;\n\n menu.buildTemplate?.(result);\n\n return result;\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\nimport { arrayRemoveItem, nextTick } from '@deepkit/core';\nimport { Injectable } from '@angular/core';\nimport { Electron } from '../../core/utils';\nimport type { MenuComponent } from '../app/menu';\nimport { buildElectronMenuTemplate } from '../app/menu-electron';\n\n@Injectable()\nexport class WindowMenuState {\n menus: MenuComponent[] = [];\n\n addMenu(menu: MenuComponent) {\n this.menus.push(menu);\n\n this.build();\n }\n\n removeMenu(menu: MenuComponent) {\n arrayRemoveItem(this.menus, menu);\n this.build();\n }\n\n build() {\n nextTick(() => {\n this._build();\n });\n }\n\n protected _build() {\n const template: any[] = [];\n\n //todo, merge menus with same id(), id falls back to role+label\n // then we can use fileMenu in sub views and add sub menu items as we want\n for (const menu of this.menus) {\n if (!menu.forApp()) continue;\n for (const child of menu.children()) {\n template.push(buildElectronMenuTemplate(child));\n }\n }\n\n if (!template.length) {\n template.push(...[\n { role: 'appMenu' },\n { role: 'fileMenu' },\n { role: 'editMenu' },\n { role: 'viewMenu' },\n { role: 'windowMenu' },\n ]);\n }\n\n if (Electron.isAvailable()) {\n const remote: any = Electron.getRemote();\n if (remote) {\n try {\n const menu = remote.Menu.buildFromTemplate(template);\n remote.Menu.setApplicationMenu(menu);\n } catch (error) {\n console.error('Could not buildFromTemplate', template);\n console.error(error);\n }\n } else {\n console.warn('Not in electron environment');\n }\n }\n }\n\n focus() {\n //set our electron menu\n //Menu.setApplicationMenu()\n this.build();\n }\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\nimport { Injectable, Signal, signal, TemplateRef, ViewContainerRef } from '@angular/core';\nimport { arrayRemoveItem } from '@deepkit/core';\nimport { WindowMenuState } from './window-menu';\nimport { BrowserWindow } from '../../core/utils';\n\nexport interface WinHeader {\n getBottomPosition(): number;\n}\n\nexport interface Win {\n id: number;\n browserWindow: BrowserWindow;\n\n getClosestNonDialogWindow(): Win | undefined;\n\n header: Signal<WinHeader | undefined>;\n viewContainerRef: ViewContainerRef;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class WindowRegistry {\n id = 0;\n\n registry = new Map<Win, {\n state: WindowState,\n menu: WindowMenuState,\n viewContainerRef: ViewContainerRef\n }>();\n\n windowHistory: Win[] = [];\n activeWindow?: Win;\n\n getAllElectronWindows(): BrowserWindow[] {\n return [...this.registry.keys()].filter(v => v.browserWindow.isElectron()).map(v => v.browserWindow);\n }\n\n register(win: Win, state: WindowState, menu: WindowMenuState, viewContainerRef: ViewContainerRef) {\n this.id++;\n win.id = this.id;\n\n this.registry.set(win, { state, menu, viewContainerRef });\n }\n\n /**\n * Finds the activeWindow and returns its most outer parent.\n */\n getOuterActiveWindow(): Win | undefined {\n if (this.activeWindow) return this.activeWindow.getClosestNonDialogWindow();\n }\n\n getCurrentViewContainerRef(): ViewContainerRef {\n if (this.activeWindow) {\n return this.activeWindow.viewContainerRef;\n // const reg = this.registry.get(this.activeWindow);\n // if (reg) {\n // return reg.viewContainerRef;\n // }\n }\n\n throw new Error('No active window');\n }\n\n focus(win: Win) {\n if (this.activeWindow === win) return;\n\n const reg = this.registry.get(win);\n if (!reg) throw new Error('Window not registered');\n\n this.activeWindow = win;\n\n arrayRemoveItem(this.windowHistory, win);\n this.windowHistory.push(win);\n\n reg.state.focus.set(true);\n reg.menu.focus();\n }\n\n blur(win: Win) {\n const reg = this.registry.get(win);\n if (reg) {\n reg.state.focus.set(false);\n }\n if (this.activeWindow === win) {\n this.activeWindow = undefined;\n }\n }\n\n unregister(win: Win) {\n const reg = this.registry.get(win);\n if (reg) {\n reg.state.focus.set(false);\n }\n\n this.registry.delete(win);\n arrayRemoveItem(this.windowHistory, win);\n if (this.activeWindow === win) {\n this.activeWindow = undefined;\n }\n\n if (this.windowHistory.length) {\n this.focus(this.windowHistory[this.windowHistory.length - 1]);\n }\n }\n}\n\nexport interface AlignedButtonGroup {\n activateOneTimeAnimation: () => void;\n}\n\n@Injectable()\nexport class WindowState {\n public buttonGroupAlignedToSidebar = signal<AlignedButtonGroup | undefined>(undefined);\n public focus = signal(false);\n public disableInputs = signal(false);\n\n public toolbars = signal<{ [name: string]: TemplateRef<any>[] }>({});\n\n public addToolbarContainer(forName: string, template: TemplateRef<any>) {\n let toolbars = this.toolbars()[forName];\n if (!toolbars) {\n toolbars = [];\n }\n\n toolbars.push(template);\n this.toolbars.update(v => ({ ...v, [forName]: toolbars.slice() }));\n }\n\n public removeToolbarContainer(forName: string, template: TemplateRef<any>) {\n const toolbars = this.toolbars()[forName];\n if (!toolbars) return;\n arrayRemoveItem(toolbars, template);\n this.toolbars.update(v => ({ ...v, [forName]: toolbars.slice() }));\n }\n}\n","import { Directive, input, output } from '@angular/core';\nimport { injectElementRef, registerEventListener, RegisterEventListenerRemove } from './utils';\n\nexport interface DuiDragEvent extends PointerEvent {\n id: number;\n deltaX: number;\n deltaY: number;\n}\n\nexport interface DuiDragStartEvent extends DuiDragEvent {\n /**\n * If this is set to false, the drag will not be accepted.\n */\n accept: boolean;\n}\n\n/**\n * A directive that catches pointer events and emits drag events.\n *\n * This won't move the element, it just emits events when the user does a drag gesture.\n *\n * ```html\n * <div (duiDrag)=\"onDrag($event)\" [duiDragThreshold]=\"2\"></div>\n * ```\n */\n@Directive({\n selector: '[duiDrag]',\n})\nexport class DragDirective {\n protected element = injectElementRef();\n protected id = 0;\n\n duiDragThreshold = input(0);\n duiDragAbortOnEscape = input(true);\n\n duiDrag = output<DuiDragEvent>();\n duiDragStart = output<DuiDragStartEvent>();\n duiDragEnd = output<DuiDragEvent>();\n duiDragCancel = output<number>();\n\n protected startX = 0;\n protected startY = 0;\n protected dragging = false;\n protected draggingElement?: DuiDragEvent['target'];\n\n protected destroy: RegisterEventListenerRemove;\n protected removePointerMove?: RegisterEventListenerRemove;\n protected removePointerUp?: RegisterEventListenerRemove;\n protected removeKeyUp?: RegisterEventListenerRemove;\n\n constructor() {\n this.destroy = registerEventListener(this.element.nativeElement, 'pointerdown', (e) => this.onPointerDown(e));\n }\n\n protected onPointerDown(e: PointerEvent) {\n if (e.button !== 0) return;\n e.stopPropagation();\n\n this.startX = e.clientX;\n this.startY = e.clientY;\n this.dragging = false;\n\n const el = this.element.nativeElement;\n el.setPointerCapture(e.pointerId);\n const id = ++this.id;\n\n this.dragging = false;\n const threshold = this.duiDragThreshold() * this.duiDragThreshold();\n this.draggingElement = e.target;\n\n const onMove = (event: PointerEvent) => {\n const dx = event.clientX - this.startX;\n const dy = event.clientY - this.startY;\n\n if (!this.dragging) {\n const start = threshold ? dx * dx + dy * dy >= threshold : true;\n if (start) {\n const startEvent: DuiDragStartEvent = Object.assign(event, {\n id,\n accept: true,\n deltaX: dx,\n deltaY: dy,\n });\n this.duiDragStart.emit(startEvent);\n this.dragging = startEvent.accept;\n if (!startEvent.accept) {\n this.abort(id);\n return;\n }\n }\n }\n\n if (this.dragging) {\n const dragEvent: DuiDragEvent = Object.assign(event, {\n id,\n deltaX: dx,\n deltaY: dy,\n });\n this.duiDrag.emit(dragEvent);\n }\n };\n\n const onUp = (event: PointerEvent) => {\n el.releasePointerCapture(event.pointerId);\n this.release();\n\n if (this.dragging) {\n this.dragging = false;\n event.stopPropagation();\n const dx = event.clientX - this.startX;\n const dy = event.clientY - this.startY;\n const dragEndEvent: DuiDragEvent = Object.assign(event, {\n id,\n deltaX: dx,\n deltaY: dy,\n });\n this.duiDragEnd.emit(dragEndEvent);\n this.element.nativeElement.addEventListener('click', (up) => up.stopPropagation(), { capture: true, once: true });\n }\n };\n\n const onKey = (event: KeyboardEvent) => {\n if (!this.dragging) return;\n event.stopPropagation();\n if (event.key === 'Escape') {\n el.releasePointerCapture(e.pointerId);\n this.dragging = false;\n this.release();\n this.duiDragCancel.emit(id);\n }\n };\n\n this.removePointerMove = registerEventListener(window, 'pointermove', onMove);\n this.removePointerUp = registerEventListener(window, 'pointerup', onUp, { once: true });\n if (this.duiDragAbortOnEscape()) {\n this.removeKeyUp = registerEventListener(window, 'keydown', onKey);\n }\n }\n\n protected release() {\n this.removePointerMove?.();\n this.removePointerUp?.();\n this.removeKeyUp?.();\n }\n\n protected abort(id: number) {\n this.release();\n this.draggingElement = undefined;\n if (this.dragging) {\n this.duiDragCancel.emit(id);\n }\n this.dragging = false;\n }\n\n ngOnDestroy() {\n this.removePointerMove?.();\n this.removePointerUp?.();\n this.removeKeyUp?.();\n this.destroy();\n }\n}\n","import { booleanAttribute, Component, computed, effect, inject, input, model, Renderer2 } from '@angular/core';\nimport { DragDirective, DuiDragEvent, DuiDragStartEvent } from '../app/drag';\nimport { clamp } from '../app/utils';\n\n/**\n * A splitter component that can be used for layout resizing. With an indicator that shows a handle.\n *\n * This is typically used to resize layouts such as sidebars, panels, or other UI elements.\n *\n * Best used in combination with flex-basis CSS property to allow flexible resizing.\n *\n * ```html\n * <div class=\"layout\">\n * <div class=\"sidebar\" [style.flex-basis.px]=\"sidebarSize()\">\n * <dui-splitter position=\"right\" [size]=\"sidebarSize\" (sizeChange)=\"sidebarSize.set($event)\" indicator></dui-splitter>\n * </div>\n * <div class=\"content\"></div>\n * </div>\n * ```\n */\n@Component({\n selector: 'dui-splitter',\n template: '',\n styleUrls: ['./splitter.component.scss'],\n host: {\n '[class.splitter-right]': 'position() === \"right\"',\n '[class.splitter-left]': 'position() === \"left\"',\n '[class.splitter-top]': 'position() === \"top\"',\n '[class.splitter-bottom]': 'position() === \"bottom\"',\n '[class.splitter-with-indicator]': 'indicator()',\n '[class.horizontal]': 'isHorizontal()',\n '[class.vertical]': '!isHorizontal()',\n '(duiDragStart)': 'onDuiDragStart($event)',\n '(duiDrag)': 'onDuiDrag($event)',\n '(duiDragEnd)': 'onDuiDragEnd($event)',\n '(duiDragCancel)': 'onDuiDragCancel()',\n },\n hostDirectives: [\n {\n directive: DragDirective,\n inputs: ['duiDragThreshold'],\n outputs: ['duiDragStart', 'duiDrag', 'duiDragEnd', 'duiDragCancel'],\n },\n ],\n})\nexport class SplitterComponent {\n /**\n * When set, the splitter will show an indicator (handle) to indicate that it can be dragged.\n */\n indicator = input(false, { transform: booleanAttribute });\n\n size = model(0);\n\n inverted = input(false, { transform: booleanAttribute });\n\n /**\n * If set one of these, the splitter will be positioned absolutely in the layout.\n * Make sure to set a parent element with `position: relative;` to allow absolute positioning.\n */\n position = input<'left' | 'right' | 'top' | 'bottom'>();\n\n orientation = input<'horizontal' | 'vertical'>();\n\n /**\n * Per default splitter is vertical (movement left-to-right), meaning vertical line.\n * If set to true, it will be horizontal (movement top-to-bottom).\n */\n horizontal = input(false, { transform: booleanAttribute });\n\n min = input(0);\n max = input(Infinity);\n\n element = input<Element>();\n\n /**\n * When element is set, this CSS property will be used to set the size of the splitter.\n * Default is 'flex-basis', which is typically used in flexbox layouts.\n */\n property = input<'flex-basis' | 'width' | string>('flex-basis');\n\n isHorizontal = computed(() => (this.horizontal() || this.position() === 'top' || this.position() === 'bottom') || this.orientation() === 'horizontal');\n\n protected startSize = 0;\n protected renderer = inject(Renderer2);\n\n constructor() {\n effect(() => {\n const property = this.property();\n const element = this.element();\n if (!element) return;\n\n const size = this.size();\n if (size) {\n this.renderer.setStyle(element, property, `${size}px`);\n }\n });\n }\n\n protected onDuiDragStart($event: DuiDragStartEvent) {\n this.startSize = this.size();\n const element = this.element();\n if (!element) return;\n const rect = element.getBoundingClientRect();\n if (this.isHorizontal()) {\n this.startSize = rect.height;\n } else {\n this.startSize = rect.width;\n }\n }\n\n protected onDuiDragEnd(event: DuiDragEvent) {\n const element = this.element();\n if (!element) return;\n // it's important to reset this.size() to the final real size after the drag ends.\n // If for example dragged way too far, the size might be negative or too large.\n const rect = element.getBoundingClientRect();\n if (this.isHorizontal()) {\n this.size.set(rect.height);\n this.renderer.setStyle(element, this.property(), `${rect.height}px`);\n } else {\n this.size.set(rect.width);\n this.renderer.setStyle(element, this.property(), `${rect.width}px`);\n }\n }\n\n protected onDuiDrag(event: DuiDragEvent) {\n const delta = this.isHorizontal() ? event.deltaY : event.deltaX;\n const factor = this.inverted() ? -1 : 1;\n const value = clamp(this.startSize + (delta * factor), this.min(), this.max());\n this.size.set(value);\n const element = this.element();\n if (element) {\n this.renderer.setStyle(element, this.property(), `${value}px`);\n }\n }\n\n protected onDuiDragCancel() {\n this.size.set(this.startSize);\n }\n}\n","/*\n * Deepkit Framework\n * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the MIT License.\n *\n * You should have received a copy of the MIT License along with this program.\n */\n\nimport { booleanAttribute, Component, effect, ElementRef, input, model, signal, TemplateRef, viewChild } from '@angular/core';\nimport { WindowState } from './window-state';\nimport { triggerResize } from '../../core/utils';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { SplitterComponent } from '../splitter/splitter.component';\nimport { clamp } from '../app/utils';\n\nexport interface WinSidebar {\n template: TemplateRef<any>;\n}\n\n@Component({\n selector: 'dui-window-content',\n template: `\n <div class=\"top-line\"></div>\n\n <div class=\"content {{class()}}\" #content>\n <ng-content></ng-content>\n </div>\n\n @if (toolbar(); as toolbar) {\n <div class=\"sidebar\"\n (transitionend)=\"transitionEnded()\"\n #sidebar [class.hidden]=\"!sidebarVisible() \" [class.with-animation]=\"withAnimation()\"\n [style.min-width.px]=\"sidebarVisible() && !withAnimation() ? sidebarMinWidth() : undefined\"\n [style.max-width.px]=\"sidebarMaxWidth()\"\n [style.width.px]=\"sidebarWidth()\">\n <div class=\"sidebar-container overlay-scrollbar-small\" [style.width.px]=\"sidebarWidth()\" #sidebarContainer>\n <ng-container [ngTemplateOutlet]=\"toolbar.template\" [ngTemplateOutletContext]=\"{}\"></ng-container>\n </div>\n @if (sidebarVisible()) {\n <dui-splitter position=\"right\" [element]=\"sidebar\" property=\"width\" [(size)]=\"sidebarWidth\"></dui-splitter>\n }\n </div>\n }\n `,\n host: {\n '[class.transparent]': 'transparent()',\n },\n styleUrls: ['./window-content.component.scss'],\n imports: [\n NgTemplateOutlet,\n SplitterComponent,\n ],\n})\nexport class WindowContentComponent {\n transparent = input(false, { transform: booleanAttribute });\n\n sidebarVisible = input<boolean>(true);\n\n class = input<string>('');\n\n sidebarWidth = model(250);\n sidebarMaxWidth = input(550);\n sidebarMinWidth = input(100);\n\n toolbar = signal<WinSidebar | undefined>(undefined);\n\n sidebar = viewChild('sidebar', { read: ElementRef });\n sidebarContainer = viewChild('sidebarContainer', { read: ElementRef });\n content = viewChild('content', { read: ElementRef });\n\n withAnimation = signal(false);\n\n constructor(\n private windowState: WindowState,\n ) {\n effect(() => {\n const normalized = clamp(this.sidebarWidth(), this.sidebarMinWidth(), this.sidebarMaxWidth());\n this.sidebarWidth.set(normalized);\n });\n\n let last = this.sidebarVisible();\n effect(() => {\n if (this.sidebar() && this.sidebarContainer()) {\n if (last === this.sidebarVisible()) return;\n last = this.sidebarVisible();\n this.handleSidebarVisibility(true);\n }\n });\n }\n\n protected transitionEnded() {\n if (this.withAnimation()) {\n this.withAnimation.set(false);\n triggerResize();\n }\n }\n\n unregisterSidebar(sidebar: WinSidebar) {\n if (this.toolbar() === sidebar) this.toolbar.set(undefined);\n }\n\n registerSidebar(sidebar: WinSidebar) {\n this.toolbar.set(