UNPKG

@web-atoms/core

Version:
421 lines (361 loc) • 14.6 kB
import { App } from "../App"; import Command from "../core/Command"; import EventScope from "../core/EventScope"; import FormattedString from "../core/FormattedString"; import { StringHelper } from "../core/StringHelper"; import { CancelToken, errorHandled } from "../core/types"; import XNode from "../core/XNode"; import JsonError from "../services/http/JsonError"; import { NavigationService, NotifyType } from "../services/NavigationService"; import type { AtomControl } from "../web/controls/AtomControl"; import PopupService from "../web/services/PopupService"; export type onEventSetBusyTypes = "target" | "current-target" | "till-current-target" | "ancestors" | "button"; export interface IActionOptions { /** * Execute current action when the specified event will be fired. The benefit is, * the element which has fired this event will have `[data-busy=true]` set so * you can use CSS to disable the button and prevent further executions. */ onEvent?: string | string[] | EventScope | EventScope[] | Command | Command[]; /** * By default event is listened on current element, however some events are only sent globally * and might end up on parent or window. You can change the target by overriding this. */ onEventTarget?: EventTarget; /** * Set busy will be set to only target of the event. You can change this behavior by providing * any of target, current-target, ancestors, button. Ancestors will set all ancestors to busy. * `button` will only set busy if target is button or any ancestor is button. */ onEventSetBusy?: MarkBusySet; /** * Defer the execution for given milliseconds, each execution request made before the defer * will cancel previous request and will enqueue the new request. This is helpful in * preventing multiple simultaneous executions. */ defer?: number; /** * When action is set to automatically execute on the given event fired, * if this is set to true, simultaneous executions will be blocked. Default is true. */ blockMultipleExecution?: boolean; /** * Display success message after method successfully executes, * if method returns promise, success will display after promise * has finished, pass null to not display message. * @default null */ success?: string | FormattedString | XNode; /** * Title for success message * @default Done */ successTitle?: string; successMode?: "alert" | "notify"; /** * By default 2000 milliseconds, the success/error notification will hide in given milliseconds */ notifyDelay?: number; /** * Ask for confirmation before invoking this method * @default null */ confirm?: string | XNode; /** * Title for confirm message * @default Confirm */ confirmTitle?: string; /** * Validate the view model before execution and report to user * @default false */ validate?: boolean | string | FormattedString; /** * Title for validation * @default Error */ validateTitle?: string; /** * dispatch event after successful execution. */ dispatchEvent?: string | EventScope; /** * Closes the current popup/window by calling viewModel.close, returned result will be sent in close */ close?: boolean; /** * Authorize user, if not empty role */ authorize?: string[] | boolean; } export interface IAuthorize { authorize: string[] | boolean; authorized: boolean; } export class MarkBusySet { public static none = new MarkBusySet(function*() { }) public static target = new MarkBusySet(function* (t, ct) { yield t; }); public static currentTarget = new MarkBusySet(function* (t, ct) { yield ct; }); public static tillCurrentTarget = new MarkBusySet(function*(target, currentTarget) { let start = target; do { yield start; start = start.parentElement; } while (start !== currentTarget); yield currentTarget; }); public static button = new MarkBusySet(function *(target, currentTarget) { let start = target; while (start) { if (start.tagName === "BUTTON") { yield start; break; } start = start.parentElement; } }); public static buttonOrAnchor = new MarkBusySet(function *(target, currentTarget) { let start = target; while (start) { if (start.tagName === "BUTTON" || start.tagName === "A") { yield start; break; } start = start.parentElement; } }); public static selector(selector: string) { return new MarkBusySet(function *(target, currentTarget) { let start = target; while (start) { if (start.matches(selector)) { yield start; break; } start = start.parentElement; } }); } public static allAncestors = new MarkBusySet(function*(target, currentTarget) { do { yield target; target = target.parentElement; } while (target); }); private constructor(private set: (target: HTMLElement, currentTarget: HTMLElement) => Iterable<HTMLElement>) { } public *find(event: Event) { yield *this.set(event.target as HTMLElement, event.currentTarget as HTMLElement); } } const onEventHandler = (owner, blockMultipleExecution, key, busyKey: symbol, onEventSetBusy: MarkBusySet) => async (ce: Event) => { const element = ce.currentTarget as HTMLElement; if (owner[busyKey]) { if (blockMultipleExecution) { return; } } owner[busyKey] = true; try { if (onEventSetBusy) { for (const iterator of onEventSetBusy.find(ce)) { iterator.setAttribute("data-busy", "true"); } } const detail = (ce as any).detail; return await owner[key](detail, ce); } finally { delete owner[busyKey]; if(onEventSetBusy) { for (const iterator of onEventSetBusy.find(ce)) { iterator.removeAttribute("data-busy"); } } } }; /** * Reports an alert to user when an error has occurred * or validation has failed. * If you set success message, it will display an alert with success message. * If you set confirm message, it will ask form confirmation before executing this method. * You can configure options to enable/disable certain * alerts. * @param reportOptions */ export default function Action( { onEvent = void 0, onEventTarget = void 0, onEventSetBusy, blockMultipleExecution = true, dispatchEvent, authorize = void 0, defer = void 0, success = null, successTitle = "Done", successMode = "notify", confirm = null, confirmTitle = null, validate = false, validateTitle = null, close = false, notifyDelay = 2000, }: IActionOptions = {}) { return (target, key: string | symbol, descriptor: any): any => { const getEventNames = (names: string | Command | EventScope | (string | Command | EventScope)[]): string | string[] => { if (names === null || names === void 0) { return; } if (Array.isArray(names)) { return names.map(getEventNames) as any; } if(names instanceof Command) { onEventTarget ??= window; return names.eventScope.eventType; } if (names instanceof EventScope) { onEventTarget ??= window; return names.eventType; } return names; } onEvent = getEventNames(onEvent); if (onEvent?.length > 0 ) { const oldCreate = target.beginEdit as Function; if(oldCreate) { const onEventName = Array.isArray(onEvent) ? onEvent.map(StringHelper.fromHyphenToCamel) : StringHelper.fromHyphenToCamel(onEvent); const busyKey = Symbol.for("isBusy" + key.toString()); target.beginEdit = function() { const result = oldCreate.apply(this, arguments); // initialize here... const c = this as AtomControl; let element = this.element; if (element) { if (onEventTarget) { element = onEventTarget; } const handler = onEventHandler(c, blockMultipleExecution, key, busyKey, onEventSetBusy); if (typeof onEventName === "string") { c.bindEvent(element, onEventName, handler); } else { for (const eventName of onEventName) { c.bindEvent(element, eventName, handler); } } } return result; }; } } const { value } = descriptor; const deferSymbol = defer ? Symbol.for(`${key.toString()}Defer`) : void 0; return { get: function(){ const vm = this; // tslint:disable-next-line: ban-types const oldMethod = value; // tslint:disable-next-line:only-arrow-functions const fx = async function( ... a: any[]) { const vm = this; if (defer) { const previous = vm[deferSymbol]; if (previous !== 0) { if (previous > 0) { clearTimeout(previous); } vm[deferSymbol] = setTimeout(() => { // this will force us to execute method... vm[deferSymbol] = 0; const r = vm[key](... a); // we need to delete symbol to restart deferring delete vm[deferSymbol]; return r; }, defer); return; } } const app = vm.app as App; const ns = app.resolve(NavigationService) as NavigationService; try { if (authorize && !App.authorize()) { return; } if (validate) { if (!vm.isValid) { const vMsg = typeof validate === "boolean" ? "Please enter correct information" : validate; await ns.alert(vMsg, validateTitle || "Error"); return; } } if (confirm) { if (! await ns.confirm(confirm as any, confirmTitle || "Confirm")) { return; } } let result = oldMethod.apply(vm, a); if (result?.then) { result = await result; } if (success) { if (successMode === "notify") { await ns.notify(success as any, successTitle, NotifyType.Information, notifyDelay); } else { await ns.alert(success as any, successTitle); } } if (close) { vm.close?.(result); } if (dispatchEvent) { const element = (vm.element ?? document.body) as HTMLElement; if (typeof dispatchEvent !== "string") { dispatchEvent = dispatchEvent.eventType; } element.dispatchEvent(new CustomEvent(dispatchEvent, { detail: result, bubbles: true })); } return result; } catch (e) { if (CancelToken.isCancelled(e)) { throw e; } e[errorHandled] = true; if (/^timeout$/i.test(e.toString().trim())) { // tslint:disable-next-line: no-console console.warn(e); throw e; } if (e instanceof JsonError) { if (e.details) { await PopupService.alert({ message: e.message, title: "Error", detail: e.details }); throw e; } } await ns.alert(e, "Error"); throw e; } }; Object.defineProperty(vm, key, { value: fx, writable: true, enumerable: false } ); return fx; }, configurable: true }; }; }