From 5c4bfbcbd15a63524768eb6065f03ca9d1117427 Mon Sep 17 00:00:00 2001 From: tzmijewski Date: Thu, 31 Jul 2025 14:20:13 -0400 Subject: [PATCH] [tz-321] Implement panel page interactions for DOM events. --- modules/detour/src/lib/detour.factories.ts | 75 ++++++- modules/detour/src/lib/detour.module.ts | 12 +- .../lib/models/interaction-event.models.ts | 12 +- .../lib/models/interaction-handler.models.ts | 7 +- .../panel-page/panel-page.component.ts | 185 +++++++++++++++++- modules/render/src/lib/render.factories.ts | 4 +- 6 files changed, 283 insertions(+), 12 deletions(-) diff --git a/modules/detour/src/lib/detour.factories.ts b/modules/detour/src/lib/detour.factories.ts index d17ba578..43138335 100644 --- a/modules/detour/src/lib/detour.factories.ts +++ b/modules/detour/src/lib/detour.factories.ts @@ -1,5 +1,74 @@ +import { Param, ParamEvaluatorService } from "@rollthecloudinc/dparam"; import { InteractionEventPlugin } from "./models/interaction-event.models"; +import { InteractionHandlerPlugin } from "./models/interaction-handler.models"; +import { map } from "rxjs/operators"; +import { Renderer2 } from "@angular/core"; +import { Observable } from "rxjs"; -export const interactionEventDomFactory = () => { - return new InteractionEventPlugin({ title: 'DOM', id: 'dom' }); -}; \ No newline at end of file +export const interactionEventDomFactory = (paramEvaluatorService: ParamEvaluatorService) => { + return new InteractionEventPlugin({ + title: 'DOM', + id: 'dom', + connect: ({ filteredListeners, listenerParams, renderer, callback }) => new Observable(obs => { + const mapTypes = new Map>(); + const len = filteredListeners.length; + for (let i = 0; i < len; i++) { + const type = (listenerParams[i] as any).type; + if (mapTypes.has(type)) { + const targets = mapTypes.get(type); + targets.push(i); + mapTypes.set(type, targets); + } else { + mapTypes.set(type, [i]); + } + } + const eventDelegtionHandler = (m => e => { + if (m.has(e.type)) { + const targets = m.get(e.type); + const len = targets.length; + targets.forEach((__, i) => { + const expectedTarget = (listenerParams[targets[i]] as any).target; + if (e.target.matches(expectedTarget)) { + console.log(`delegated target match ${expectedTarget}`); + if(filteredListeners[i].handler.settings.params) { + const paramNames = filteredListeners[i].handler.settings.paramsString ? filteredListeners[i].handler.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : []; + paramEvaluatorService.paramValues( + filteredListeners[i].handler.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map()) + ).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + ).subscribe((handlerParams) => { + // plugin call and pass params + // console.log('handler original event and params', e, filteredListeners[i].handler.plugin, handlerParams); + callback({ handlerParams, plugin: filteredListeners[i].handler.plugin, index: i, evt: e }); + }) + } else { + // plugin call and pass params + // console.log('handler original event and params', filteredListeners[i].handler.plugin, e); + callback({ handlerParams: {}, plugin: filteredListeners[i].handler.plugin, index: i, evt: e }); + } + } + }); + } + })(mapTypes) + const keys = Array.from(mapTypes); + for (let i = 0; i < keys.length; i++) { + const type = keys[i][0]; + renderer.listen('document', type, e => { + eventDelegtionHandler(e); + }); + } + obs.next({}); + obs.complete(); + }) + }); +}; + +export const interactionHandlerHelloWorldFactory = () => { + return new InteractionHandlerPlugin({ + title: 'Hello World', + id: 'hello_world', + handle: ({}) => { + console.log("Hello World"); + } + }) +} \ No newline at end of file diff --git a/modules/detour/src/lib/detour.module.ts b/modules/detour/src/lib/detour.module.ts index bb394334..3a554c28 100644 --- a/modules/detour/src/lib/detour.module.ts +++ b/modules/detour/src/lib/detour.module.ts @@ -2,12 +2,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MaterialModule } from '@rollthecloudinc/material'; -import { DparamModule } from '@rollthecloudinc/dparam'; +import { DparamModule, ParamEvaluatorService } from '@rollthecloudinc/dparam'; import { InteractionListenerComponent } from './components/interaction-listener/interaction-listener.component'; import { InteractionsDialogComponent } from './components/interactions-dialog/interactions-dialog.component'; import { InteractionsFormComponent } from './components/interactions-form/interactions-form.component'; import { InteractionEventPluginManager } from './services/interaction-event-plugin-manager.service'; -import { interactionEventDomFactory } from './detour.factories'; +import { interactionEventDomFactory, interactionHandlerHelloWorldFactory } from './detour.factories'; +import { InteractionHandlerPluginManager } from './services/interaction-handler-plugin-manager.service'; @NgModule({ declarations: [ @@ -30,8 +31,11 @@ import { interactionEventDomFactory } from './detour.factories'; }) export class DetourModule { constructor( - iepm: InteractionEventPluginManager + iepm: InteractionEventPluginManager, + ihpm: InteractionHandlerPluginManager, + paramEvaluatorService: ParamEvaluatorService ) { - iepm.register(interactionEventDomFactory()); + iepm.register(interactionEventDomFactory(paramEvaluatorService)); + ihpm.register(interactionHandlerHelloWorldFactory()); } } diff --git a/modules/detour/src/lib/models/interaction-event.models.ts b/modules/detour/src/lib/models/interaction-event.models.ts index be230081..9ea2461e 100644 --- a/modules/detour/src/lib/models/interaction-event.models.ts +++ b/modules/detour/src/lib/models/interaction-event.models.ts @@ -1,13 +1,23 @@ -import { Type } from '@angular/core'; +import { Renderer2, Type } from '@angular/core'; import { Plugin } from '@rollthecloudinc/plugin'; +import { Observable } from 'rxjs'; +import { InteractionListener } from './interaction.models'; + +export type InteractionEventCallbackInput = { handlerParams: {}, plugin: string, index: number, evt: any }; +export type InteractionEventCallback = ({ handlerParams, plugin, index, evt }) => void +export type InteractionEventOutput = { }; +export type InteractionEventInput = { filteredListeners: Array, listenerParams: {}, renderer: Renderer2, callback: InteractionEventCallback }; export class InteractionEventPlugin extends Plugin { // editor: Type; // errorMessage: string; // builder: ({ v, serialized }: { v: ValidationValidator, serialized: boolean }) => Observable; + connect: ({ filteredListeners, listenerParams, renderer, callback }: InteractionEventInput) => Observable; constructor(data?: InteractionEventPlugin) { super(data) if(data) { + // Probably should be required to have a connection. + this.connect = data.connect; // this.editor = data.editor; // this.errorMessage = data.errorMessage; // this.builder = data.builder; diff --git a/modules/detour/src/lib/models/interaction-handler.models.ts b/modules/detour/src/lib/models/interaction-handler.models.ts index 80eebc1b..2f3389d7 100644 --- a/modules/detour/src/lib/models/interaction-handler.models.ts +++ b/modules/detour/src/lib/models/interaction-handler.models.ts @@ -1,14 +1,19 @@ -import { Type } from '@angular/core'; +import { Type, Renderer2 } from '@angular/core'; import { Plugin } from '@rollthecloudinc/plugin'; +import { InteractionListener } from './interaction.models'; + +export type InteractionHandlerInput = { handlerParams: {}, plugin: string, index: number, evt: any, listener: InteractionListener, renderer: Renderer2 } export class InteractionHandlerPlugin extends Plugin { // editor: Type; // editor: Type; // errorMessage: string; // builder: ({ v, serialized }: { v: ValidationValidator, serialized: boolean }) => Observable; + handle: ({ handlerParams, plugin, index, evt, listener, renderer } : InteractionHandlerInput) => void; constructor(data?: InteractionHandlerPlugin) { super(data) if(data) { + this.handle = data.handle; //this.editor = data.editor; // this.editor = data.editor; // this.errorMessage = data.errorMessage; diff --git a/modules/render/src/lib/components/panel-page/panel-page.component.ts b/modules/render/src/lib/components/panel-page/panel-page.component.ts index 00251e2a..826b40f4 100644 --- a/modules/render/src/lib/components/panel-page/panel-page.component.ts +++ b/modules/render/src/lib/components/panel-page/panel-page.component.ts @@ -6,7 +6,7 @@ import { CONTENT_PLUGIN, ContentPlugin, ContentPluginManager } from '@rolltheclo import { GridLayoutComponent, LayoutPluginManager } from '@rollthecloudinc/layout'; import { AsyncApiCallHelperService, StyleLoaderService } from '@rollthecloudinc/utils'; import { FilesService, MediaSettings, MEDIA_SETTINGS } from '@rollthecloudinc/media'; -import { InteractionListener } from '@rollthecloudinc/detour'; +import { InteractionEventPluginManager, InteractionHandlerPluginManager, InteractionListener } from '@rollthecloudinc/detour'; import { /*ContextManagerService, */ InlineContext, ContextPluginManager, InlineContextResolverService } from '@rollthecloudinc/context'; import { PanelPage, Pane, LayoutSetting, CssHelperService, PanelsContextService, PageBuilderFacade, FormService, PanelPageForm, PanelPageState, PanelContentHandler, PaneStateService, Panel, StylePlugin, PanelResolverService, StylePluginManager, StyleResolverService } from '@rollthecloudinc/panels'; import { DisplayGrid, GridsterConfig, GridType, GridsterItem } from 'angular-gridster2'; @@ -29,6 +29,7 @@ import { camelize } from 'inflected'; import merge from 'deepmerge-json'; import { DOCUMENT } from '@angular/common'; import { AuthFacade } from '@rollthecloudinc/auth'; +import { Param, ParamEvaluatorService } from '@rollthecloudinc/dparam'; @Component({ selector: 'classifieds-ui-panel-page', @@ -377,6 +378,140 @@ export class PanelPageComponent implements OnInit, AfterViewInit, AfterContentIn tap(() => this.isStable = false) ).subscribe(); + readonly wireListenersSub = combineLatest([ + this.listeners$, + this.renderLayout$, + this.afterContentInit$, + ]).pipe( + delay(1), + switchMap(() => forkJoin(this.filteredListeners.map(l => of({}).pipe( + map(() => ({ paramNames: l.event.settings.paramsString ? l.event.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : [] })), + switchMap(({ paramNames }) => this.paramEvaluatorService.paramValues(l.event.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map())).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + )), + defaultIfEmpty([]) + ) + ))), + switchMap(listenerParams => this.iepm.getPlugin('dom').pipe( + map(p => ({ p, listenerParams })) + )), + switchMap(({ p, listenerParams }) => p.connect({ + filteredListeners: this.filteredListeners, + listenerParams, + renderer: this.renderer, + callback: ({ handlerParams, plugin, index, evt }) => { + // console.log(`The handler was called`, handlerParams, plugin, index, this.filteredListeners[index], evt ); + this.ihpm.getPlugin(plugin).pipe( + tap(p => { + p.handle({ + handlerParams, + plugin, + index, + listener: this.filteredListeners[index], + evt, + renderer: this.renderer }); + }) + ).subscribe(); + } + })), + tap((listenerParams) => { + console.log('listener info', this.filteredListeners, listenerParams); + + /*this.iepm.getPlugin('dom').subscribe(p => { + p.connect({ + filteredListeners: this.filteredListeners, + listenerParams, + renderer: this.renderer, + callback: ({ handlerParams, plugin, index, evt }) => { + console.log(`The handler was called`, handlerParams, plugin, index, this.filteredListeners[index], evt ); + } + }).subscribe(); + });*/ + + // The hard way to handle events using our own delegation algorithm + // since nodes are constantly changing underneath and simple way + // doesn't seem to work. + + // This is all going to be part of the plugin function anyway. + + /*const mapTypes = new Map>(); + const len = this.filteredListeners.length; + for (let i = 0; i < len; i++) { + const type = (listenerParams[i] as any).type; + if (mapTypes.has(type)) { + const targets = mapTypes.get(type); + targets.push(i); + mapTypes.set(type, targets); + } else { + mapTypes.set(type, [i]); + } + } + const eventDelegtionHandler = (m => e => { + if (m.has(e.type)) { + const targets = m.get(e.type); + const len = targets.length; + targets.forEach((__, i) => { + const expectedTarget = (listenerParams[targets[i]] as any).target; + if (e.target.matches(expectedTarget)) { + console.log(`delegated target match ${expectedTarget}`); + if(this.filteredListeners[i].handler.settings.params) { + const paramNames = this.filteredListeners[i].handler.settings.paramsString ? this.filteredListeners[i].handler.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : []; + this.paramEvaluatorService.paramValues( + this.filteredListeners[i].handler.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map()) + ).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + ).subscribe((handlerParams) => { + // plugin call and pass params + console.log('handler original event and params', e, this.filteredListeners[i].handler.plugin, handlerParams); + }) + } else { + // plugin call and pass params + console.log('handler original event and params', this.filteredListeners[i].handler.plugin, e); + } + } + }); + } + })(mapTypes) + const keys = Array.from(mapTypes); + for (let i = 0; i < keys.length; i++) { + const type = keys[i][0]; + this.renderer.listen('document', type, e => { + eventDelegtionHandler(e); + }); + }*/ + + /*this.renderer.listen('document', 'click', e => { + console.log('delegated target'); + if (e.target.matches('.open-dialog')) { + console.log('delegated target match'); + } + });*/ + + /*const listenerLen = this.filteredListeners.length; + for (let i = 0; i < listenerLen; i++) { + // Assumption is made herre that would be responsibility of plugin instead ie. target is required for DOM event. + // For now though just to get things spinning again hard code expectation. + const targets =(this.el.nativeElement as Element).querySelectorAll((listenerParams[i] as any).target); + console.log('listener target', targets); + targets.forEach(t => this.renderer.listen(t, (listenerParams[i] as any).type, e => { + console.log('listener fired'); + if(this.filteredListeners[i].handler.settings.params) { + const paramNames = this.filteredListeners[i].handler.settings.paramsString ? this.filteredListeners[i].handler.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : []; + this.paramEvaluatorService.paramValues( + this.filteredListeners[i].handler.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map()) + ).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + ).subscribe((handlerParams) => { + console.log('handler original event and params',e, handlerParams); + }); + } else { + console.log('handler original event and params', e); + } + })); + }*/ + }) + ).subscribe() + get panelsArray(): UntypedFormArray { return this.pageForm.get('panels') as UntypedFormArray; } @@ -412,6 +547,10 @@ export class PanelPageComponent implements OnInit, AfterViewInit, AfterContentIn private classifyService: ClassifyService, private fileService: FilesService, private authFacade: AuthFacade, + private paramEvaluatorService: ParamEvaluatorService, + private renderer: Renderer2, + private iepm: InteractionEventPluginManager, + private ihpm: InteractionHandlerPluginManager, es: EntityServices, ) { this.panelPageService = es.getEntityCollectionService('PanelPage'); @@ -761,10 +900,52 @@ export class RenderPaneComponent implements OnInit, OnChanges, ControlValueAcces ]).pipe( map(([l]) => l), tap(listeners => { + this.filteredListeners = listeners console.log('pane listeners', listeners); }) ).subscribe(); + readonly wireListenersSub = combineLatest([ + this.listeners$, + this.afterContentInit$, + ]).pipe( + delay(1), + switchMap(() => forkJoin(this.filteredListeners.map(l => of({}).pipe( + map(() => ({ paramNames: l.event.settings.paramsString ? l.event.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : [] })), + switchMap(({ paramNames }) => this.paramEvaluatorService.paramValues(l.event.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map())).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + )), + defaultIfEmpty([]) + ) + ))), + tap((listenerParams) => { + console.log('listener info', this.filteredListeners, listenerParams); + const listenerLen = this.filteredListeners.length; + for (let i = 0; i < listenerLen; i++) { + // Assumption is made herre that would be responsibility of plugin instead ie. target is required for DOM event. + // For now though just to get things spinning again hard code expectation. + const targets =(this.el.nativeElement as Element).querySelectorAll((listenerParams[i] as any).target); + console.log('listener target', targets); + targets.forEach(t => this.renderer.listen(t, (listenerParams[i] as any).type, e => { + console.log('listener fired'); + if(this.filteredListeners[i].handler.settings.params) { + const paramNames = this.filteredListeners[i].handler.settings.paramsString ? this.filteredListeners[i].handler.settings.paramsString.split('&').filter(v => v.indexOf('=:') !== -1).map(v => v.split('=', 2)[1].substr(1)) : []; + this.paramEvaluatorService.paramValues( + this.filteredListeners[i].handler.settings.params.reduce((p, c, i) => new Map([ ...p, [ paramNames[i], c ] ]), new Map()) + ).pipe( + map(params => Array.from(params).reduce((p, [k, v]) => ({ ...p, [k]: v }), {})) + ).subscribe((handlerParams) => { + console.log('handler original event and params',e, handlerParams); + }); + } else { + console.log('handler original event and params', e); + } + })); + + } + }) + ).subscribe() + paneForm = this.fb.group({ contentPlugin: this.fb.control('', Validators.required), name: this.fb.control(''), @@ -832,6 +1013,8 @@ export class RenderPaneComponent implements OnInit, OnChanges, ControlValueAcces private cpm: ContentPluginManager, private cssHelper: CssHelperService, private paneStateService: PaneStateService, + private paramEvaluatorService: ParamEvaluatorService, + private renderer: Renderer2, es: EntityServices ) { this.panelPageStateService = es.getEntityCollectionService('PanelPageState'); diff --git a/modules/render/src/lib/render.factories.ts b/modules/render/src/lib/render.factories.ts index 188d8afe..a6aec93a 100644 --- a/modules/render/src/lib/render.factories.ts +++ b/modules/render/src/lib/render.factories.ts @@ -1,9 +1,9 @@ import { InteractionHandlerPlugin } from '@rollthecloudinc/detour'; export const interationHandlerFormSubmit = () => { - return new InteractionHandlerPlugin({ id: 'panels_form_submit', title: 'Submit Panels Form' }); + return new InteractionHandlerPlugin({ id: 'panels_form_submit', title: 'Submit Panels Form', handle: () => {} }); }; export const interationHandlerDialog = () => { - return new InteractionHandlerPlugin({ id: 'panels_dialog', title: 'Open Panels Dialog' }); + return new InteractionHandlerPlugin({ id: 'panels_dialog', title: 'Open Panels Dialog', handle: () => {} }); }; \ No newline at end of file