import { Objects } from '@ephox/boulder'; import { Arr, Cell, Fun, Global, Obj, Optional } from '@ephox/katamari'; import type { SugarElement } from '@ephox/sugar'; import type { AlloyComponent } from '../api/component/ComponentApi'; import * as SystemEvents from '../api/events/SystemEvents'; import type { GuiSystem } from '../api/system/Gui'; import * as AlloyLogger from '../log/AlloyLogger'; export interface DebuggerLogger { logEventCut: (eventName: string, target: SugarElement, purpose: string) => void; logEventStopped: (eventName: string, target: SugarElement, purpose: string) => void; logNoParent: (eventName: string, target: SugarElement, purpose: string) => void; logEventNoHandlers: (eventName: string, target: SugarElement) => void; logEventResponse: (eventName: string, target: SugarElement, purpose: string) => void; write: () => void; } export interface InspectorInfo { '(original.spec)': any; '(dom.ref)': Node; '(element)': string; '(initComponents)': InspectorInfo[]; '(components)': InspectorInfo[]; '(bound.events)': string; '(behaviours)': string | Record; } type LookupInfo = { [key: string]: InspectorInfo } | { error: string }; export interface Inspector { systems: Record; lookup: (uid: string) => Optional; events: { setToNormal: (eventName: string) => void; setToLogging: (eventName: string) => void; setToStop: (eventName: string) => void; }; } interface AlloyGlobal { __CHROME_INSPECTOR_CONNECTION_TO_ALLOY__: Inspector; } const unknown = 'unknown'; /* typescipt qwerk: const debugging: boolean = true; if (boolean === false) { -> this throws a type error! // TS2365:Operator '===' cannot be applied to types 'false' and 'true' https://www.typescriptlang.org/play/#src=const%20foo%3A%20boolean%20%3D%20true%3B%0D%0A%0D%0Aif%20(foo%20%3D%3D%3D%20false)%20%7B%0D%0A%20%20%20%20%0D%0A%7D } */ const debugging: any = true; const CHROME_INSPECTOR_GLOBAL = '__CHROME_INSPECTOR_CONNECTION_TO_ALLOY__'; enum EventConfiguration { STOP, NORMAL, LOGGING } const eventConfig = Cell>({ }); export type EventProcessor = (logger: DebuggerLogger) => boolean; const makeEventLogger = (eventName: string, initialTarget: SugarElement): DebuggerLogger => { const sequence: Array<{ outcome: string; target: SugarElement; purpose?: string }> = [ ]; const startTime = new Date().getTime(); return { logEventCut: (_name: string, target: SugarElement, purpose: string) => { sequence.push({ outcome: 'cut', target, purpose }); }, logEventStopped: (_name: string, target: SugarElement, purpose: string) => { sequence.push({ outcome: 'stopped', target, purpose }); }, logNoParent: (_name: string, target: SugarElement, purpose: string) => { sequence.push({ outcome: 'no-parent', target, purpose }); }, logEventNoHandlers: (_name: string, target: SugarElement) => { sequence.push({ outcome: 'no-handlers-left', target }); }, logEventResponse: (_name: string, target: SugarElement, purpose: string) => { sequence.push({ outcome: 'response', purpose, target }); }, write: () => { const finishTime = new Date().getTime(); if (Arr.contains([ 'mousemove', 'mouseover', 'mouseout', SystemEvents.systemInit() ], eventName)) { return; } // eslint-disable-next-line no-console console.log(eventName, { event: eventName, time: finishTime - startTime, target: initialTarget.dom, sequence: Arr.map(sequence, (s) => { if (!Arr.contains([ 'cut', 'stopped', 'response' ], s.outcome)) { return s.outcome; } else { return '{' + s.purpose + '} ' + s.outcome + ' at (' + AlloyLogger.element(s.target) + ')'; } }) }); } }; }; const processEvent = (eventName: string, initialTarget: SugarElement, f: EventProcessor) => { const status = Obj.get(eventConfig.get(), eventName).orThunk(() => { const patterns = Obj.keys(eventConfig.get()); return Arr.findMap(patterns, (p) => eventName.indexOf(p) > -1 ? Optional.some(eventConfig.get()[p]) : Optional.none()); }).getOr( EventConfiguration.NORMAL ); switch (status) { case EventConfiguration.NORMAL: return f(noLogger()); case EventConfiguration.LOGGING: { const logger = makeEventLogger(eventName, initialTarget); const output = f(logger); logger.write(); return output; } case EventConfiguration.STOP: // Does not even run the function to trigger event and listen to handlers return true; } }; // Ignore these files in the error stack const path = [ 'alloy/data/Fields', 'alloy/debugging/Debugging' ]; const getTrace = (): string => { if (debugging === false) { return unknown; } const err = new Error(); if (err.stack !== undefined) { const lines = err.stack.split('\n'); return Arr.find(lines, (line) => line.indexOf('alloy') > 0 && !Arr.exists(path, (p) => line.indexOf(p) > -1)).getOr(unknown); } else { return unknown; } }; const logHandler = (_label: string, _handlerName: string, _trace: any): void => { // if (debugging) console.log(label + ' [' + handlerName + ']', trace); }; const ignoreEvent = { logEventCut: Fun.noop, logEventStopped: Fun.noop, logNoParent: Fun.noop, logEventNoHandlers: Fun.noop, logEventResponse: Fun.noop, write: Fun.noop }; const monitorEvent = (eventName: string, initialTarget: SugarElement, f: EventProcessor): boolean => processEvent(eventName, initialTarget, f); const inspectorInfo = (comp: AlloyComponent) => { const go = (c: AlloyComponent): InspectorInfo => { const cSpec = c.spec; return { '(original.spec)': cSpec, '(dom.ref)': c.element.dom, '(element)': AlloyLogger.element(c.element), '(initComponents)': Arr.map(cSpec.components !== undefined ? cSpec.components : [ ], go), '(components)': Arr.map(c.components(), go), '(bound.events)': Obj.mapToArray(c.events, (_v, k) => [ k ]).join(', '), '(behaviours)': cSpec.behaviours !== undefined ? Obj.map(cSpec.behaviours, (v, k) => v === undefined ? '--revoked--' : { 'config': v.configAsRaw(), 'original-config': v.initialConfig, 'state': c.readState(k) }) : 'none' }; }; return go(comp); }; const getOrInitConnection = (): Inspector => { const win: AlloyGlobal = Global; // The format of the global is going to be: // lookup(uid) -> Optional { name => data } // systems: Set AlloyRoots if (win[CHROME_INSPECTOR_GLOBAL] !== undefined) { return win[CHROME_INSPECTOR_GLOBAL]; } else { const setEventStatus = (eventName: string, status: EventConfiguration) => { const evs = eventConfig.get(); evs[eventName] = status; eventConfig.set(evs); }; win[CHROME_INSPECTOR_GLOBAL] = { systems: { }, lookup: (uid: string) => { const systems = win[CHROME_INSPECTOR_GLOBAL].systems; const connections: string[] = Obj.keys(systems); return Arr.findMap(connections, (conn) => { const connGui = systems[conn]; return connGui.getByUid(uid).toOptional().map((comp): LookupInfo => Objects.wrap(AlloyLogger.element(comp.element), inspectorInfo(comp))); }).orThunk(() => Optional.some({ error: 'Systems (' + connections.join(', ') + ') did not contain uid: ' + uid })); }, events: { setToNormal: (eventName: string) => { setEventStatus(eventName, EventConfiguration.NORMAL); }, setToLogging: (eventName: string) => { setEventStatus(eventName, EventConfiguration.LOGGING); }, setToStop: (eventName: string) => { setEventStatus(eventName, EventConfiguration.STOP); } } }; return win[CHROME_INSPECTOR_GLOBAL]; } }; const registerInspector = (name: string, gui: GuiSystem): void => { const connection = getOrInitConnection(); connection.systems[name] = gui; }; const noLogger = Fun.constant(ignoreEvent); const isDebugging = Fun.constant(debugging); export { logHandler, noLogger, getTrace, monitorEvent, isDebugging, registerInspector };