@deepkit/desktop-ui
Version:
Library for desktop UI widgets in Angular 10+
292 lines (237 loc) • 8.21 kB
text/typescript
/*
* Deepkit Framework
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
*
* You should have received a copy of the MIT License along with this program.
*/
/**
* @reflection never
*/
import { Subscription } from 'rxjs';
import { ChangeDetectorRef, EventEmitter } from '@angular/core';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { nextTick } from '@deepkit/core';
import type Hammer from 'hammerjs';
const electron = 'undefined' === typeof window ? undefined : (window as any).electron || ((window as any).require ? (window as any).require('electron') : undefined);
export async function getHammer(): Promise<typeof Hammer | undefined> {
if ('undefined' === typeof window) return;
//@ts-ignore
const hammer = await import('hammerjs');
return hammer.default;
}
export class Electron {
public static getRemote(): any {
if (!electron) {
throw new Error('No Electron available.');
}
return electron.remote;
}
public static getIpc(): any {
if (!electron) {
throw new Error('No Electron available.');
}
return electron.ipcRenderer;
}
public static isAvailable(): any {
return !!electron;
}
public static getRemoteOrUndefined(): any {
return electron ? electron.remote : undefined;
}
public static getProcess() {
return Electron.getRemote().process;
}
}
export class AsyncEventEmitter<T> extends EventEmitter<T> {
emit(value?: T): void {
super.emit(value);
}
subscribe(generatorOrNext?: any, error?: any, complete?: any): Subscription {
return super.subscribe(generatorOrNext, error, complete);
}
}
export class ExecutionState {
public running = false;
public error: string = '';
constructor(
protected readonly cd: ChangeDetectorRef,
protected readonly func: (...args: any[]) => Promise<any> | any,
) {
}
public async execute(...args: any[]) {
if (this.running) {
throw new Error('Executor still running');
}
this.running = true;
this.error = '';
this.cd.detectChanges();
try {
return await this.func(...args);
} catch (error: any) {
this.error = error.message || error.toString();
throw error;
} finally {
this.running = false;
this.cd.detectChanges();
}
}
}
/**
* Checks if `target` is children of `parent` or if `target` is `parent`.
*/
export function isTargetChildOf(target: HTMLElement | EventTarget | null, parent: HTMLElement): boolean {
if (!target) return false;
if (target === parent) return true;
return parent.contains(target as Node);
}
export function isMacOs() {
if ('undefined' === typeof navigator) return false;
return navigator.platform.indexOf('Mac') > -1;
}
export function isWindows() {
if ('undefined' === typeof navigator) return false;
return navigator.platform.indexOf('Win') > -1;
}
/**
* Checks if `target` is children of `parent` or if `target` is `parent`.
*/
export function findParentWithClass(start: HTMLElement, className: string): HTMLElement | undefined {
let current: HTMLElement | null = start;
do {
if (current.classList.contains(className)) return current;
current = current.parentElement;
} while (current);
return undefined;
}
export function triggerResize() {
if ('undefined' === typeof window) return;
nextTick(() => {
window.dispatchEvent(new Event('resize'));
});
}
type FocusWatcherUnsubscribe = () => void;
/**
* Observes focus changes on target elements and emits when focus is lost.
*
* This is used to track multi-element focus changes, such as when a user clicks from a dropdown toggle into the dropdown menu.
*/
export function focusWatcher(
target: HTMLElement, allowedFocuses: HTMLElement[] = [],
onBlur: () => void,
customChecker?: (currentlyFocused: HTMLElement | null) => boolean,
): FocusWatcherUnsubscribe {
const doc = target.ownerDocument;
if (doc.body.tabIndex === -1) doc.body.tabIndex = 1;
let currentlyFocused: HTMLElement | null = target;
let subscribed = true;
function isFocusAllowed() {
if (!currentlyFocused) {
return false;
}
if (currentlyFocused === target || target.contains(currentlyFocused)) {
return true;
}
for (const focus of allowedFocuses) {
if (focus && currentlyFocused === focus || focus.contains(currentlyFocused)) {
return true;
}
}
return customChecker ? customChecker(currentlyFocused) : false;
}
function emitBlurIfNeeded() {
if (!currentlyFocused) {
// Shouldn't be possible to have no element at all with focus.
// This means usually that the item that had previously focus was deleted.
currentlyFocused = target;
}
if (subscribed && !isFocusAllowed()) {
onBlur();
unsubscribe();
return true;
}
return false;
}
function onFocusOut() {
currentlyFocused = null;
emitBlurIfNeeded();
}
function onFocusIn(event: FocusEvent) {
currentlyFocused = event.target as any;
emitBlurIfNeeded();
}
function onMouseDown(event: FocusEvent) {
currentlyFocused = event.target as any;
if (emitBlurIfNeeded()) {
event.stopImmediatePropagation();
event.preventDefault();
}
}
doc.addEventListener('mousedown', onMouseDown, true);
doc.addEventListener('focusin', onFocusIn);
doc.addEventListener('focusout', onFocusOut);
function unsubscribe() {
if (!subscribed) return;
subscribed = false;
doc.removeEventListener('mousedown', onMouseDown, true);
doc.removeEventListener('focusin', onFocusIn);
doc.removeEventListener('focusout', onFocusOut);
}
return unsubscribe;
}
interface RouteLike {
routerLink?: string | UrlTree | any[];
routerLinkExact?: boolean;
router?: Router,
activatedRoute?: ActivatedRoute;
queryParams?: { [name: string]: any };
}
export function isRouteActive(route: RouteLike): boolean {
if (!route.router) return false;
if ('string' === typeof route.routerLink) {
return route.router.isActive(route.routerLink, route.routerLinkExact === true);
} else if (Array.isArray(route.routerLink)) {
return route.router.isActive(route.router.createUrlTree(route.routerLink, {
queryParams: route.queryParams,
relativeTo: route.activatedRoute,
}), route.routerLinkExact === true);
} else {
return route.router.isActive(route.routerLink!, route.routerLinkExact === true);
}
}
export function redirectScrollableParentsToWindowResize(node: Element, passive = true) {
const parents = getScrollableParents(node);
function redirect() {
window.dispatchEvent(new Event('resize'));
}
for (const parent of parents) {
parent.addEventListener('scroll', redirect, { passive });
}
return () => {
for (const parent of parents) {
parent.removeEventListener('scroll', redirect);
}
};
}
export function getScrollableParents(node: Element): Element[] {
const scrollableParents: Element[] = [];
let parent = node.parentNode;
while (parent) {
if (!(parent instanceof Element)) {
parent = parent.parentNode;
continue;
}
const computedStyle = window.getComputedStyle(parent);
const overflow = computedStyle.getPropertyValue('overflow');
if (overflow === 'overlay' || overflow === 'scroll' || overflow === 'auto') {
scrollableParents.push(parent);
}
parent = parent.parentNode;
}
return scrollableParents;
}
export function trackByIndex(index: number) {
return index;
}