@o3r/components
Version:
This module contains component-related features (Component replacement, CMS compatibility, helpers, pipes, debugging developer tools...) It comes with an integrated ng builder to help you generate components compatible with Otter features (CMS integration
1 lines • 135 kB
Source Map (JSON)
{"version":3,"file":"o3r-components.mjs","sources":["../../src/core/rendering/helpers.ts","../../src/devkit/components-devkit.interface.ts","../../src/stores/placeholder-request/placeholder-request.actions.ts","../../src/stores/placeholder-request/placeholder-request.reducer.ts","../../src/stores/placeholder-request/placeholder-request.state.ts","../../src/stores/placeholder-request/placeholder-request.module.ts","../../src/stores/placeholder-request/placeholder-request.selectors.ts","../../src/stores/placeholder-request/placeholder-request.sync.ts","../../src/stores/placeholder-template/placeholder-template.actions.ts","../../src/stores/placeholder-template/placeholder-template.reducer.ts","../../src/stores/placeholder-template/placeholder-template.state.ts","../../src/stores/placeholder-template/placeholder-template.module.ts","../../src/stores/placeholder-template/placeholder-template.selectors.ts","../../src/stores/placeholder-template/placeholder-template.sync.ts","../../src/devkit/components-devtools.token.ts","../../src/devkit/highlight/constants.ts","../../src/devkit/highlight/helpers.ts","../../src/devkit/highlight/highlight.service.ts","../../src/devkit/inspector/otter-inspector.helpers.ts","../../src/devkit/inspector/otter-inspector.service.ts","../../src/devkit/components-devtools.message.service.ts","../../src/devkit/components-devtools.module.ts","../../src/tools/component-replacement/c11n.directive.ts","../../src/tools/component-replacement/c11n.helpers.ts","../../src/tools/component-replacement/c11n.token.ts","../../src/tools/component-replacement/c11n.service.ts","../../src/tools/component-replacement/c11n.mock.module.ts","../../src/tools/component-replacement/c11n.module.ts","../../src/tools/pipes/capitalize/capitalize.pipe.ts","../../src/tools/pipes/duration/duration.model.ts","../../src/tools/pipes/duration/duration.pipe.ts","../../src/tools/pipes/keep-white-space/keep-white-space.pipe.ts","../../src/tools/pipes/replace-with-bold/replace-with-bold.pipe.ts","../../src/tools/placeholder/placeholder.component.ts","../../src/tools/placeholder/placeholder.template.html","../../src/tools/placeholder/placeholder.module.ts","../../src/o3r-components.ts"],"sourcesContent":["import {\n animationFrameScheduler,\n from,\n Observable,\n observeOn,\n of,\n} from 'rxjs';\nimport {\n bufferCount,\n concatMap,\n delay,\n mergeMap,\n scan,\n tap,\n} from 'rxjs/operators';\n\n/**\n * Buffers and emits data for lazy/progressive rendering of big lists\n * That could solve issues with long-running tasks when trying to render an array\n * of similar components.\n * @param delayMs Delay between data emits\n * @param concurrency Amount of elements that should be emitted at once\n */\nexport function lazyArray<T>(delayMs = 0, concurrency = 2) {\n let isFirstEmission = true;\n return (source$: Observable<T[]>) => {\n return source$.pipe(\n mergeMap((items) => {\n if (!isFirstEmission) {\n return of(items);\n }\n\n const items$ = from(items);\n\n return items$.pipe(\n bufferCount(concurrency),\n concatMap((value, index) => {\n return of(value).pipe(\n observeOn(animationFrameScheduler),\n delay(index * delayMs)\n );\n }),\n scan((acc: T[], steps: T[]) => {\n return [...acc, ...steps];\n }, []),\n tap((scannedItems: T[]) => {\n const scanDidComplete = scannedItems.length === items.length;\n\n if (scanDidComplete) {\n isFirstEmission = false;\n }\n })\n );\n })\n );\n };\n}\n","import type {\n ConnectContentMessage,\n DevtoolsCommonOptions,\n MessageDataTypes,\n OtterMessageContent,\n RequestMessagesContentMessage,\n} from '@o3r/core';\nimport type {\n PlaceholderMode,\n} from '../stores';\nimport type {\n GroupInfo,\n} from './highlight/models';\nimport {\n OtterLikeComponentInfo,\n} from './inspector';\n\n/**\n * Component Devtools options\n */\nexport interface ComponentsDevtoolsServiceOptions extends DevtoolsCommonOptions {\n}\n\n/**\n * Message to give the selected component information\n */\nexport interface SelectedComponentInfoMessage extends OtterLikeComponentInfo, OtterMessageContent<'selectedComponentInfo'> {\n}\n\n/**\n * Message to toggle the inspector\n */\nexport interface ToggleInspectorMessage extends OtterMessageContent<'toggleInspector'> {\n /** Is the inspector running */\n isRunning: boolean;\n}\n\n/**\n * Message to toggle the highlight\n */\nexport interface ToggleHighlightMessage extends OtterMessageContent<'toggleHighlight'> {\n /** Is the highlight displayed */\n isRunning: boolean;\n}\n\n/**\n * Message the change the configuration of the `HighlightService`\n */\nexport interface ChangeHighlightConfiguration extends OtterMessageContent<'changeHighlightConfiguration'> {\n /**\n * Minimum width of HTMLElement to be considered\n */\n elementMinWidth?: number;\n /**\n * Minimum height of HTMLElement to be considered\n */\n elementMinHeight?: number;\n /**\n * Throttle interval\n */\n throttleInterval?: number;\n /**\n * Group information to detect elements\n */\n groupsInfo?: Record<string, GroupInfo>;\n /**\n * Maximum number of ancestors\n */\n maxDepth?: number;\n /**\n * Opacity of the chips\n */\n chipsOpacity?: number;\n /**\n * Auto refresh\n */\n autoRefresh?: boolean;\n}\n\n/**\n * Message to toggle the placeholder mode\n */\nexport interface PlaceholderModeMessage extends OtterMessageContent<'placeholderMode'> {\n /** Placeholder mode */\n mode: PlaceholderMode;\n}\n\n/**\n * Message to know the component selection availability\n */\nexport interface IsComponentSelectionAvailableMessage extends OtterMessageContent<'isComponentSelectionAvailable'> {\n available: boolean;\n}\n\ntype ComponentsMessageContents = IsComponentSelectionAvailableMessage\n | SelectedComponentInfoMessage\n | ToggleInspectorMessage\n | ToggleHighlightMessage\n | ChangeHighlightConfiguration\n | PlaceholderModeMessage;\n\n/** List of possible DataTypes for Components messages */\nexport type ComponentsMessageDataTypes = MessageDataTypes<ComponentsMessageContents>;\n\n/** List of all messages for Components purpose */\nexport type AvailableComponentsMessageContents = ComponentsMessageContents\n | ConnectContentMessage\n | RequestMessagesContentMessage<ComponentsMessageDataTypes>;\n\n/**\n * Determine if the given message is a Components message\n * @param message message to check\n */\nexport const isComponentsMessage = (message: any): message is AvailableComponentsMessageContents => {\n return message && (\n message.dataType === 'requestMessages'\n || message.dataType === 'connect'\n || message.dataType === 'selectedComponentInfo'\n || message.dataType === 'isComponentSelectionAvailable'\n || message.dataType === 'placeholderMode'\n || message.dataType === 'toggleInspector'\n || message.dataType === 'toggleHighlight'\n || message.dataType === 'changeHighlightConfiguration'\n );\n};\n","import {\n createAction,\n props,\n} from '@ngrx/store';\nimport {\n asyncProps,\n AsyncRequest,\n FailAsyncStoreItemEntitiesActionPayload,\n FromApiActionPayload,\n UpdateAsyncStoreItemEntityActionPayloadWithId,\n UpdateEntityActionPayloadWithId,\n} from '@o3r/core';\nimport {\n PlaceholderRequestModel,\n PlaceholderRequestReply,\n} from './placeholder-request.state';\n\nconst ACTION_FAIL_ENTITIES = '[PlaceholderRequest] fail entities';\nconst ACTION_SET_ENTITY_FROM_URL = '[PlaceholderRequest] set entity from url';\nconst ACTION_CANCEL_REQUEST = '[PlaceholderRequest] cancel request';\nconst ACTION_UPDATE_ENTITY = '[PlaceholderRequest] update entity';\nconst ACTION_UPDATE_ENTITY_SYNC = '[PlaceholderRequest] update entity sync';\n\n/** Action to cancel a Request ID registered in the store. Can happen from effect based on a switchMap for instance */\nexport const cancelPlaceholderRequest = createAction(ACTION_CANCEL_REQUEST, props<AsyncRequest & { id: string }>());\n\n/** Action to update failureStatus for PlaceholderRequestModels */\nexport const failPlaceholderRequestEntity = createAction(ACTION_FAIL_ENTITIES, props<FailAsyncStoreItemEntitiesActionPayload<any>>());\n\n/** Action to update an entity */\nexport const updatePlaceholderRequestEntity = createAction(ACTION_UPDATE_ENTITY, props<UpdateAsyncStoreItemEntityActionPayloadWithId<PlaceholderRequestModel>>());\n\n/** Action to update an entity without impact on request id */\nexport const updatePlaceholderRequestEntitySync = createAction(ACTION_UPDATE_ENTITY_SYNC, props<UpdateEntityActionPayloadWithId<PlaceholderRequestModel>>());\n\n/** Action to update PlaceholderRequest with known IDs, will create the entity with only the url, the call will be created in the effect */\nexport const setPlaceholderRequestEntityFromUrl = createAction(ACTION_SET_ENTITY_FROM_URL, asyncProps<FromApiActionPayload<PlaceholderRequestReply> & { resolvedUrl: string; id: string }>());\n","import {\n createEntityAdapter,\n} from '@ngrx/entity';\nimport {\n ActionCreator,\n createReducer,\n on,\n ReducerTypes,\n} from '@ngrx/store';\nimport {\n asyncStoreItemAdapter,\n createEntityAsyncRequestAdapter,\n} from '@o3r/core';\nimport * as actions from './placeholder-request.actions';\nimport {\n PlaceholderRequestModel,\n PlaceholderRequestState,\n} from './placeholder-request.state';\n\n/**\n * PlaceholderRequest Store adapter\n */\nexport const placeholderRequestAdapter = createEntityAsyncRequestAdapter(createEntityAdapter<PlaceholderRequestModel>({\n selectId: (model) => model.id\n}));\n\n/**\n * PlaceholderRequest Store initial value\n */\nexport const placeholderRequestInitialState = placeholderRequestAdapter.getInitialState<PlaceholderRequestState>({\n requestIds: []\n});\n\n/**\n * Reducers of Placeholder request store that handles the call to the placeholder template URL\n */\nexport const placeholderRequestReducerFeatures: ReducerTypes<PlaceholderRequestState, ActionCreator[]>[] = [\n on(actions.cancelPlaceholderRequest, (state, action) => {\n const id = action.id;\n if (!id || !state.entities[id]) {\n return state;\n }\n return placeholderRequestAdapter.updateOne({\n id: action.id,\n changes: asyncStoreItemAdapter.resolveRequest(state.entities[id], action.requestId)\n }, asyncStoreItemAdapter.resolveRequest(state, action.requestId));\n }),\n on(actions.updatePlaceholderRequestEntity, (state, action) => {\n const currentEntity = state.entities[action.entity.id]!;\n const newEntity = asyncStoreItemAdapter.resolveRequest({ ...action.entity, ...asyncStoreItemAdapter.extractAsyncStoreItem(currentEntity) }, action.requestId);\n return placeholderRequestAdapter.updateOne({\n id: newEntity.id,\n changes: newEntity\n }, asyncStoreItemAdapter.resolveRequest(state, action.requestId));\n }),\n on(actions.updatePlaceholderRequestEntitySync, (state, action) => {\n return placeholderRequestAdapter.updateOne({\n id: action.entity.id,\n changes: {\n ...action.entity\n }\n }, state);\n }),\n on(actions.setPlaceholderRequestEntityFromUrl, (state, payload) => {\n const currentEntity = state.entities[payload.id];\n // Nothing to update if resolved URLs already match\n if (currentEntity && currentEntity.resolvedUrl === payload.resolvedUrl) {\n return state;\n }\n let newEntity = {\n id: payload.id,\n resolvedUrl: payload.resolvedUrl,\n used: true\n };\n if (currentEntity) {\n newEntity = { ...asyncStoreItemAdapter.extractAsyncStoreItem(currentEntity), ...newEntity };\n }\n return placeholderRequestAdapter.addOne(\n asyncStoreItemAdapter.addRequest(\n asyncStoreItemAdapter.initialize(newEntity),\n payload.requestId),\n asyncStoreItemAdapter.addRequest(state, payload.requestId)\n );\n }),\n on(actions.failPlaceholderRequestEntity, (state, payload) => {\n return placeholderRequestAdapter.failRequestMany(asyncStoreItemAdapter.resolveRequest(state, payload.requestId), payload && payload.ids, payload.requestId);\n })\n];\n\n/**\n * PlaceholderRequest Store reducer\n */\nexport const placeholderRequestReducer = createReducer(\n placeholderRequestInitialState,\n ...placeholderRequestReducerFeatures\n);\n","import {\n EntityState,\n} from '@ngrx/entity';\nimport {\n AsyncStoreItem,\n} from '@o3r/core';\n\n/**\n * Variable model from the placeholder reply\n */\nexport interface PlaceholderVariable {\n type: 'fact' | 'fullUrl' | 'relativeUrl' | 'localisation';\n value: string;\n parameters?: Record<string, string>;\n path?: string;\n}\n\n/**\n * Raw JSON template coming back from the CMS or any other source\n */\nexport interface PlaceholderRequestReply {\n template?: string;\n vars?: Record<string, PlaceholderVariable>;\n}\n\n/**\n * PlaceholderRequest model\n */\nexport interface PlaceholderRequestModel extends AsyncStoreItem, PlaceholderRequestReply {\n /** Raw URL that is not localized, ex: my_url/[LANGUAGE]/my_placeholder.json */\n id: string;\n /** Resolved URL that is localized, ex: my_url/en-GB/my_placeholder.json */\n resolvedUrl: string;\n /** Rendered template associated to the resolved URL, can be dynamic */\n renderedTemplate?: string;\n /** Unknown type found in the reply */\n unknownTypeFound?: boolean;\n\n /** A mechanism to cache previous request results for a given language. This boolean disables the dynamic rendering when it is set to false */\n used?: boolean;\n}\n\n/**\n * PlaceholderRequest state details\n */\nexport interface PlaceholderRequestStateDetails extends AsyncStoreItem {}\n\n/**\n * PlaceholderRequest store state\n */\nexport interface PlaceholderRequestState extends EntityState<PlaceholderRequestModel>, PlaceholderRequestStateDetails {\n}\n\n/**\n * Name of the PlaceholderRequest Store\n */\nexport const PLACEHOLDER_REQUEST_STORE_NAME = 'placeholderRequest';\n\n/**\n * PlaceholderRequest Store Interface\n */\nexport interface PlaceholderRequestStore {\n /** PlaceholderRequest state */\n [PLACEHOLDER_REQUEST_STORE_NAME]: PlaceholderRequestState;\n}\n","import {\n InjectionToken,\n ModuleWithProviders,\n NgModule,\n} from '@angular/core';\nimport {\n Action,\n ActionReducer,\n StoreModule,\n} from '@ngrx/store';\nimport {\n placeholderRequestReducer,\n} from './placeholder-request.reducer';\nimport {\n PLACEHOLDER_REQUEST_STORE_NAME,\n PlaceholderRequestState,\n} from './placeholder-request.state';\n\n/** Token of the PlaceholderRequest reducer */\nexport const PLACEHOLDER_REQUEST_REDUCER_TOKEN = new InjectionToken<ActionReducer<PlaceholderRequestState, Action>>('Feature PlaceholderRequest Reducer');\n\n/** Provide default reducer for PlaceholderRequest store */\nexport function getDefaultplaceholderRequestReducer() {\n return placeholderRequestReducer;\n}\n\n@NgModule({\n imports: [\n StoreModule.forFeature(PLACEHOLDER_REQUEST_STORE_NAME, PLACEHOLDER_REQUEST_REDUCER_TOKEN)\n ],\n providers: [\n { provide: PLACEHOLDER_REQUEST_REDUCER_TOKEN, useFactory: getDefaultplaceholderRequestReducer }\n ]\n})\nexport class PlaceholderRequestStoreModule {\n public static forRoot<T extends PlaceholderRequestState>(reducerFactory: () => ActionReducer<T, Action>): ModuleWithProviders<PlaceholderRequestStoreModule> {\n return {\n ngModule: PlaceholderRequestStoreModule,\n providers: [\n { provide: PLACEHOLDER_REQUEST_REDUCER_TOKEN, useFactory: reducerFactory }\n ]\n };\n }\n}\n","import {\n createFeatureSelector,\n createSelector,\n} from '@ngrx/store';\nimport {\n placeholderRequestAdapter,\n} from './placeholder-request.reducer';\nimport {\n PLACEHOLDER_REQUEST_STORE_NAME,\n PlaceholderRequestState,\n} from './placeholder-request.state';\n\nexport const selectPlaceholderRequestState = createFeatureSelector<PlaceholderRequestState>(PLACEHOLDER_REQUEST_STORE_NAME);\n\nconst { selectEntities } = placeholderRequestAdapter.getSelectors();\n\n/** Select the dictionary of PlaceholderRequest entities */\nexport const selectPlaceholderRequestEntities = createSelector(selectPlaceholderRequestState, (state) => state && selectEntities(state));\n\n/**\n * Select a specific PlaceholderRequest entity using a raw url as id\n * @param rawUrl\n */\nexport const selectPlaceholderRequestEntityUsage = (rawUrl: string) => createSelector(\n selectPlaceholderRequestState,\n (state) => {\n return state?.entities[rawUrl] ? state.entities[rawUrl].used : undefined;\n });\n","import {\n asyncEntitySerializer,\n Serializer,\n} from '@o3r/core';\nimport {\n placeholderRequestAdapter,\n placeholderRequestInitialState,\n} from './placeholder-request.reducer';\nimport {\n PlaceholderRequestModel,\n PlaceholderRequestState,\n} from './placeholder-request.state';\n\nexport const placeholderRequestStorageSerializer = asyncEntitySerializer;\n\nexport const placeholderRequestStorageDeserializer = (rawObject: any) => {\n if (!rawObject || !rawObject.ids) {\n return placeholderRequestInitialState;\n }\n const storeObject = placeholderRequestAdapter.getInitialState<PlaceholderRequestState>(rawObject);\n for (const id of rawObject.ids) {\n storeObject.entities[id] = rawObject.entities[id] as PlaceholderRequestModel;\n }\n return storeObject;\n};\n\nexport const placeholderRequestStorageSync: Readonly<Serializer<PlaceholderRequestState>> = {\n serialize: placeholderRequestStorageSerializer,\n deserialize: placeholderRequestStorageDeserializer\n} as const;\n","import {\n createAction,\n props,\n} from '@ngrx/store';\nimport {\n SetEntityActionPayload,\n} from '@o3r/core';\nimport {\n type PlaceholderMode,\n PlaceholderTemplateModel,\n} from './placeholder-template.state';\n\nconst ACTION_DELETE_ENTITY = '[PlaceholderTemplate] delete entity';\nconst ACTION_SET_ENTITY = '[PlaceholderTemplate] set entity';\nconst ACTION_TOGGLE_MODE = '[PlaceholderTemplate] toggle mode';\n\n/** Action to delete a specific entity */\nexport const deletePlaceholderTemplateEntity = createAction(ACTION_DELETE_ENTITY, props<{ id: string }>());\n\n/** Action to clear all placeholderTemplate and fill the store with the payload */\nexport const setPlaceholderTemplateEntity = createAction(ACTION_SET_ENTITY, props<SetEntityActionPayload<PlaceholderTemplateModel>>());\n\nexport const togglePlaceholderModeTemplate = createAction(ACTION_TOGGLE_MODE, props<{ mode: PlaceholderMode }>());\n","import {\n createEntityAdapter,\n} from '@ngrx/entity';\nimport {\n ActionCreator,\n createReducer,\n on,\n ReducerTypes,\n} from '@ngrx/store';\nimport * as actions from './placeholder-template.actions';\nimport {\n PlaceholderTemplateModel,\n PlaceholderTemplateState,\n} from './placeholder-template.state';\n\n/**\n * PlaceholderTemplate Store adapter\n */\nexport const placeholderTemplateAdapter = createEntityAdapter<PlaceholderTemplateModel>({\n selectId: (model) => model.id\n});\n\n/**\n * PlaceholderTemplate Store initial value\n */\nexport const placeholderTemplateInitialState = placeholderTemplateAdapter.getInitialState<PlaceholderTemplateState>({\n mode: 'normal'\n});\n\n/**\n * List of basic actions for PlaceholderTemplate Store\n */\nexport const placeholderTemplateReducerFeatures: ReducerTypes<PlaceholderTemplateState, ActionCreator[]>[] = [\n on(actions.setPlaceholderTemplateEntity, (state, payload) =>\n placeholderTemplateAdapter.addOne(payload.entity, placeholderTemplateAdapter.removeOne(payload.entity.id, state))),\n on(actions.deletePlaceholderTemplateEntity, (state, payload) => {\n const id = payload.id;\n if (!id || !state.entities[id]) {\n return state;\n }\n return placeholderTemplateAdapter.removeOne(id, state);\n }),\n on(actions.togglePlaceholderModeTemplate, (state, payload) => {\n return {\n ...state,\n mode: payload.mode\n };\n })\n];\n\n/**\n * PlaceholderTemplate Store reducer\n */\nexport const placeholderTemplateReducer = createReducer(\n placeholderTemplateInitialState,\n ...placeholderTemplateReducerFeatures\n);\n","import {\n EntityState,\n} from '@ngrx/entity';\n\n/**\n * PlaceholderTemplate model\n */\nexport interface PlaceholderTemplateModel {\n /** Placeholder id that is unique*/\n id: string;\n /** Urls to the templates to be fetched, and priority for rendering order */\n urlsWithPriority: { rawUrl: string; priority: number }[];\n}\n\n/** Possible placeholder mode */\nexport type PlaceholderMode = 'normal' | 'debug' | 'pending';\n\n/**\n * PlaceholderTemplate store state\n */\nexport interface PlaceholderTemplateState extends EntityState<PlaceholderTemplateModel> {\n mode: PlaceholderMode;\n}\n\n/**\n * Name of the PlaceholderTemplate Store\n */\nexport const PLACEHOLDER_TEMPLATE_STORE_NAME = 'placeholderTemplate';\n\n/**\n * PlaceholderTemplate Store Interface\n */\nexport interface PlaceholderTemplateStore {\n /** PlaceholderTemplate state */\n [PLACEHOLDER_TEMPLATE_STORE_NAME]: PlaceholderTemplateState;\n}\n","import {\n InjectionToken,\n ModuleWithProviders,\n NgModule,\n} from '@angular/core';\nimport {\n Action,\n ActionReducer,\n StoreModule,\n} from '@ngrx/store';\nimport {\n placeholderTemplateReducer,\n} from './placeholder-template.reducer';\nimport {\n PLACEHOLDER_TEMPLATE_STORE_NAME,\n PlaceholderTemplateState,\n} from './placeholder-template.state';\n\n/** Token of the PlaceholderTemplate reducer */\nexport const PLACEHOLDER_TEMPLATE_REDUCER_TOKEN = new InjectionToken<ActionReducer<PlaceholderTemplateState, Action>>('Feature PlaceholderTemplate Reducer');\n\n/** Provide default reducer for PlaceholderTemplate store */\nexport function getDefaultPlaceholderTemplateReducer() {\n return placeholderTemplateReducer;\n}\n\n@NgModule({\n imports: [\n StoreModule.forFeature(PLACEHOLDER_TEMPLATE_STORE_NAME, PLACEHOLDER_TEMPLATE_REDUCER_TOKEN)\n ],\n providers: [\n { provide: PLACEHOLDER_TEMPLATE_REDUCER_TOKEN, useFactory: getDefaultPlaceholderTemplateReducer }\n ]\n})\nexport class PlaceholderTemplateStoreModule {\n public static forRoot<T extends PlaceholderTemplateState>(reducerFactory: () => ActionReducer<T, Action>): ModuleWithProviders<PlaceholderTemplateStoreModule> {\n return {\n ngModule: PlaceholderTemplateStoreModule,\n providers: [\n { provide: PLACEHOLDER_TEMPLATE_REDUCER_TOKEN, useFactory: reducerFactory }\n ]\n };\n }\n}\n","import {\n createFeatureSelector,\n createSelector,\n} from '@ngrx/store';\nimport {\n selectPlaceholderRequestState,\n} from '../placeholder-request';\nimport {\n placeholderTemplateAdapter,\n} from './placeholder-template.reducer';\nimport {\n PLACEHOLDER_TEMPLATE_STORE_NAME,\n PlaceholderTemplateState,\n} from './placeholder-template.state';\n\nconst { selectEntities } = placeholderTemplateAdapter.getSelectors();\n\nexport const selectPlaceholderTemplateState = createFeatureSelector<PlaceholderTemplateState>(PLACEHOLDER_TEMPLATE_STORE_NAME);\n\n/** Select the dictionary of PlaceholderTemplate entities */\nexport const selectPlaceholderTemplateEntities = createSelector(selectPlaceholderTemplateState, (state) => state && selectEntities(state));\n\n/**\n * Select a specific PlaceholderTemplate\n * @param placeholderId\n */\nexport const selectPlaceholderTemplateEntity = (placeholderId: string) =>\n createSelector(selectPlaceholderTemplateState, (state) => state?.entities[placeholderId]);\n\n/**\n * Select the ordered rendered placeholder template full data (url, priority etc.) for a given placeholderId\n * Return undefined if the placeholder is not found\n * Returns {orderedRenderedTemplates: undefined, isPending: true} if any of the request is still pending\n * @param placeholderId\n */\nexport const selectSortedTemplates = (placeholderId: string) => createSelector(\n selectPlaceholderTemplateEntity(placeholderId),\n selectPlaceholderRequestState,\n (placeholderTemplate, placeholderRequestState) => {\n if (!placeholderTemplate || !placeholderRequestState) {\n return;\n }\n // The isPending will be considered true if any of the Url is still pending\n let isPending: boolean | undefined = false;\n const templates: { rawUrl: string; priority: number; renderedTemplate?: string; resolvedUrl: string }[] = [];\n placeholderTemplate.urlsWithPriority.forEach((urlWithPriority) => {\n const placeholderRequest = placeholderRequestState.entities[urlWithPriority.rawUrl];\n if (placeholderRequest) {\n // If one of the items is pending, we will wait to display all contents at the same time\n isPending = isPending || placeholderRequest.isPending;\n // Templates in failure will be ignored from the list\n if (!placeholderRequest.isFailure) {\n templates.push({\n rawUrl: urlWithPriority.rawUrl,\n resolvedUrl: placeholderRequest.resolvedUrl,\n priority: urlWithPriority.priority,\n renderedTemplate: placeholderRequest.renderedTemplate\n });\n }\n }\n });\n // No need to perform sorting if still pending\n if (isPending) {\n return { orderedTemplates: undefined, isPending };\n }\n // Sort templates by priority\n const orderedTemplates = templates.sort((template1, template2) => {\n return (template2.priority - template1.priority) || 1;\n }).filter((templateData) => !!templateData.renderedTemplate);\n\n return { orderedTemplates, isPending };\n });\n\nexport const selectPlaceholderTemplateMode = createSelector(selectPlaceholderTemplateState, (state) => state.mode);\n","import {\n Serializer,\n} from '@o3r/core';\nimport {\n placeholderTemplateAdapter,\n placeholderTemplateInitialState,\n} from './placeholder-template.reducer';\nimport {\n PlaceholderTemplateModel,\n PlaceholderTemplateState,\n} from './placeholder-template.state';\n\nexport const placeholderTemplateStorageDeserializer = (rawObject: any) => {\n if (!rawObject || !rawObject.ids) {\n return placeholderTemplateInitialState;\n }\n const storeObject = placeholderTemplateAdapter.getInitialState<PlaceholderTemplateState>(rawObject);\n for (const id of rawObject.ids) {\n storeObject.entities[id] = rawObject.entities[id] as PlaceholderTemplateModel;\n }\n return storeObject;\n};\n\nexport const placeholderTemplateStorageSync: Readonly<Serializer<PlaceholderTemplateState>> = {\n deserialize: placeholderTemplateStorageDeserializer\n} as const;\n","import {\n InjectionToken,\n} from '@angular/core';\nimport {\n ComponentsDevtoolsServiceOptions,\n} from './components-devkit.interface';\n\nexport const OTTER_COMPONENTS_DEVTOOLS_DEFAULT_OPTIONS: Readonly<ComponentsDevtoolsServiceOptions> = {\n isActivatedOnBootstrap: false\n} as const;\n\nexport const OTTER_COMPONENTS_DEVTOOLS_OPTIONS = new InjectionToken<ComponentsDevtoolsServiceOptions>('Otter Components Devtools options');\n","/**\n * Class applied on the wrapper of highlight elements\n */\nexport const HIGHLIGHT_WRAPPER_CLASS = 'highlight-wrapper';\n\n/**\n * Class applied on the overlay elements\n */\nexport const HIGHLIGHT_OVERLAY_CLASS = 'highlight-overlay';\n\n/**\n * Class applied on the chip elements\n */\nexport const HIGHLIGHT_CHIP_CLASS = 'highlight-chip';\n\n/**\n * Default value for maximum number of ancestors\n */\nexport const DEFAULT_MAX_DEPTH = 10;\n\n/**\n * Default value for element min height\n */\nexport const DEFAULT_ELEMENT_MIN_HEIGHT = 30;\n/**\n * Default value for element min width\n */\nexport const DEFAULT_ELEMENT_MIN_WIDTH = 60;\n/**\n * Default value for throttle interval\n */\nexport const DEFAULT_THROTTLE_INTERVAL = 500;\n/**\n * Default value for chips opacity\n */\nexport const DEFAULT_CHIPS_OPACITY = 1;\n/**\n * Default value for auto refresh activation\n */\nexport const DEFAULT_AUTO_REFRESH = true;\n","import {\n HIGHLIGHT_CHIP_CLASS,\n HIGHLIGHT_OVERLAY_CLASS,\n HIGHLIGHT_WRAPPER_CLASS,\n} from './constants';\nimport type {\n ElementWithGroupInfo,\n GroupInfo,\n} from './models';\n\n/**\n * Retrieve the identifier of the element\n * @param element\n */\nexport function getIdentifier(element: ElementWithGroupInfo): string {\n const { tagName, attributes, classList } = element.htmlElement;\n const regexp = new RegExp(element.regexp, 'i');\n if (!regexp.test(tagName)) {\n const attribute = Array.from(attributes).find((attr) => regexp.test(attr.name));\n if (attribute) {\n return `${attribute.name}${attribute.value ? `=\"${attribute.value}\"` : ''}`;\n }\n const className = Array.from(classList).find((cName) => regexp.test(cName));\n if (className) {\n return className;\n }\n }\n return tagName;\n}\n\n/**\n * Filters a list of HTML elements and returns those that match specific group information.\n *\n * Each element is checked against a set of criteria:\n * - The element's dimensions must meet the minimum height and width requirements.\n * - The element's tag name, attributes, or class names must match a regular expression defined in the group information.\n * @param elements An array of HTML elements to filter.\n * @param elementMinHeight The min height required for each element to be considered in the computation\n * @param elementMinWidth The min width required for each element to be considered in the computation\n * @param groupsInfo The config that describes the HTML tags to check\n * @returns An array of objects containing the matching elements and their associated group information\n */\nexport function filterElementsWithInfo(elements: HTMLElement[], elementMinHeight: number, elementMinWidth: number, groupsInfo: Record<string, GroupInfo>): ElementWithGroupInfo[] {\n return elements.reduce((acc: ElementWithGroupInfo[], element) => {\n const { height, width } = element.getBoundingClientRect();\n\n if (height < elementMinHeight || width < elementMinWidth) {\n return acc;\n }\n const elementInfo = Object.values(groupsInfo).find((info) => {\n const regexp = new RegExp(info.regexp, 'i');\n\n return regexp.test(element.tagName)\n || Array.from(element.attributes).some((attr) => regexp.test(attr.name))\n || Array.from(element.classList).some((cName) => regexp.test(cName));\n });\n if (elementInfo) {\n return acc.concat({ ...elementInfo, htmlElement: element });\n }\n return acc;\n }, []);\n}\n\n/**\n * Compute the number of ancestors of a given element based on a list of elements\n * @param element\n * @param elementList\n */\nexport function computeNumberOfAncestors(element: HTMLElement, elementList: HTMLElement[]) {\n return elementList.filter((el: HTMLElement) => el.contains(element)).length;\n}\n\n/**\n * Throttle {@link fn} with a {@link delay}\n * @param fn method to run\n * @param delay given in ms\n */\nexport function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {\n let timerFlag: ReturnType<typeof setTimeout> | null = null;\n\n const throttleFn = (...args: Parameters<T>) => {\n if (timerFlag === null) {\n fn(...args);\n timerFlag = setTimeout(() => {\n timerFlag = null;\n }, delay);\n }\n };\n return throttleFn;\n}\n\n/**\n * Run {@link refreshFn} if {@link mutations} implies to refresh elements inside {@link highlightWrapper}\n * @param mutations\n * @param highlightWrapper\n * @param refreshFn\n */\nexport function runRefreshIfNeeded(mutations: MutationRecord[], highlightWrapper: Element | null, refreshFn: () => void) {\n if (\n mutations.some((mutation) =>\n mutation.target !== highlightWrapper\n || (\n mutation.target === document.body\n && Array.from<HTMLElement>(mutation.addedNodes.values() as any)\n .concat(...mutation.removedNodes.values() as any)\n .some((node) => !node.classList.contains(HIGHLIGHT_WRAPPER_CLASS))\n )\n )\n ) {\n refreshFn();\n }\n}\n\n/**\n * Options to create an overlay element\n */\nexport interface CreateOverlayOptions {\n top: string;\n left: string;\n position: string;\n width: string;\n height: string;\n backgroundColor: string;\n}\n\n/**\n * Create an overlay element\n * @param doc HTML Document\n * @param opts\n * @param depth\n */\nexport function createOverlay(doc: Document, opts: CreateOverlayOptions, depth: number) {\n const overlay = doc.createElement('div');\n overlay.classList.add(HIGHLIGHT_OVERLAY_CLASS);\n // All static style could be moved in a <style>\n overlay.style.top = opts.top;\n overlay.style.left = opts.left;\n overlay.style.width = opts.width;\n overlay.style.height = opts.height;\n overlay.style.border = `2px ${depth % 2 === 0 ? 'solid' : 'dotted'} ${opts.backgroundColor}`;\n overlay.style.zIndex = '10000';\n overlay.style.position = opts.position;\n overlay.style.pointerEvents = 'none';\n return overlay;\n}\n\n/**\n * Options to create a chip element\n */\nexport interface CreateChipOptions {\n displayName: string;\n depth: number;\n top: string;\n left: string;\n position: string;\n backgroundColor: string;\n color?: string;\n name: string;\n opacity?: number;\n}\n\n/**\n * Create a chip element\n * @param doc HTML Document\n * @param opts\n * @param overlay\n */\nexport function createChip(doc: Document, opts: CreateChipOptions, overlay: HTMLDivElement) {\n const chip = doc.createElement('div');\n chip.classList.add(HIGHLIGHT_CHIP_CLASS);\n chip.textContent = `${opts.displayName} ${opts.depth}`;\n // All static style could be moved in a <style>\n chip.style.top = opts.top;\n chip.style.left = opts.left;\n chip.style.backgroundColor = opts.backgroundColor;\n chip.style.color = opts.color ?? '#FFF';\n chip.style.position = opts.position;\n chip.style.display = 'inline-block';\n chip.style.padding = '2px 4px';\n chip.style.borderRadius = '0 0 4px';\n chip.style.cursor = 'pointer';\n chip.style.zIndex = '10000';\n chip.style.textWrap = 'no-wrap';\n chip.style.opacity = opts.opacity?.toString() ?? '1';\n chip.title = opts.name;\n chip.addEventListener('click', () => {\n // Should we log in the console as well ?\n void navigator.clipboard.writeText(opts.name);\n });\n chip.addEventListener('mouseover', () => {\n chip.style.opacity = '1';\n overlay.style.boxShadow = `0 0 10px 3px ${opts.backgroundColor}`;\n });\n chip.addEventListener('mouseout', () => {\n chip.style.opacity = opts.opacity?.toString() ?? '1';\n overlay.style.boxShadow = 'none';\n });\n overlay.style.transition = 'box-shadow 0.3s ease-in-out';\n\n return chip;\n}\n","import {\n DOCUMENT,\n} from '@angular/common';\nimport {\n DestroyRef,\n inject,\n Injectable,\n} from '@angular/core';\nimport {\n DEFAULT_AUTO_REFRESH,\n DEFAULT_CHIPS_OPACITY,\n DEFAULT_ELEMENT_MIN_HEIGHT,\n DEFAULT_ELEMENT_MIN_WIDTH,\n DEFAULT_MAX_DEPTH,\n DEFAULT_THROTTLE_INTERVAL,\n HIGHLIGHT_WRAPPER_CLASS,\n} from './constants';\nimport {\n computeNumberOfAncestors,\n createChip,\n createOverlay,\n filterElementsWithInfo,\n getIdentifier,\n runRefreshIfNeeded,\n throttle,\n} from './helpers';\nimport type {\n ElementWithGroupInfo,\n ElementWithGroupInfoAndDepth,\n GroupInfo,\n} from './models';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class HighlightService {\n /**\n * Group information\n * Value could be changed through chrome extension options\n */\n public groupsInfo: Record<string, GroupInfo> = {};\n\n /**\n * Maximum number of components ancestor\n * Value could be changed through chrome extension view\n */\n public maxDepth = DEFAULT_MAX_DEPTH;\n\n /**\n * Element min height to be considered\n * Value could be changed through chrome extension options\n */\n public elementMinHeight = DEFAULT_ELEMENT_MIN_HEIGHT;\n\n /**\n * Element min width to be considered\n * Value could be changed through chrome extension options\n */\n public elementMinWidth = DEFAULT_ELEMENT_MIN_WIDTH;\n\n /**\n * Throttle interval to refresh the highlight elements\n * Value could be changed through chrome extension options\n */\n public throttleInterval = DEFAULT_THROTTLE_INTERVAL;\n\n /**\n * Opacity of the chips\n * Value could be changed through chrome extension options\n */\n public chipsOpacity = DEFAULT_CHIPS_OPACITY;\n\n /**\n * Whether to activate the auto refresh of the highlight\n * Value could be changed through chrome extension view\n */\n public autoRefresh = DEFAULT_AUTO_REFRESH;\n\n private throttleRun: (() => void) | undefined;\n private singleRun = false;\n\n private readonly document = inject(DOCUMENT);\n\n private readonly mutationObserver = new MutationObserver((mutations) =>\n runRefreshIfNeeded(\n mutations,\n this.getHighlightWrapper(),\n () => this.throttleRun?.()\n )\n );\n\n private readonly resizeObserver = new ResizeObserver(() => this.throttleRun?.());\n\n constructor() {\n inject(DestroyRef).onDestroy(() => this.stop());\n }\n\n private getHighlightWrapper() {\n return this.document.body.querySelector(`.${HIGHLIGHT_WRAPPER_CLASS}`);\n }\n\n private cleanHighlightWrapper() {\n this.getHighlightWrapper()?.querySelectorAll('*').forEach((node) => node.remove());\n }\n\n private initializeHighlightWrapper() {\n let wrapper = this.getHighlightWrapper();\n if (!wrapper) {\n wrapper = this.document.createElement('div');\n wrapper.classList.add(HIGHLIGHT_WRAPPER_CLASS);\n this.document.body.append(wrapper);\n }\n this.cleanHighlightWrapper();\n }\n\n private run() {\n this.initializeHighlightWrapper();\n const wrapper = this.getHighlightWrapper()!;\n\n // We have to select all elements from document because\n // with CSS Selector it's impossible to select element\n // by regex on their `tagName`, attribute name or attribute value.\n const allHTMLElements: HTMLElement[] = Array.from(this.document.body.querySelectorAll<HTMLElement>('*'));\n const elementsWithInfo: ElementWithGroupInfo[] = filterElementsWithInfo(allHTMLElements, this.elementMinHeight, this.elementMinWidth, this.groupsInfo);\n\n const elementsWithInfoAndDepth = elementsWithInfo\n .reduce((acc: ElementWithGroupInfoAndDepth[], elementWithInfo, _, array) => {\n const depth = computeNumberOfAncestors(elementWithInfo.htmlElement, array.map((e) => e.htmlElement));\n if (depth <= this.maxDepth) {\n return acc.concat({\n ...elementWithInfo,\n depth\n });\n }\n return acc;\n }, []);\n\n const overlayData: Record<string, { chip: HTMLElement; overlay: HTMLElement; depth: number }[]> = {};\n\n elementsWithInfoAndDepth.forEach((item) => {\n const { htmlElement: element, backgroundColor, color, displayName, depth } = item;\n const rect = element.getBoundingClientRect();\n const position = element.computedStyleMap().get('position')?.toString() === 'fixed' ? 'fixed' : 'absolute';\n const top = `${position === 'fixed' ? rect.top : (rect.top + window.scrollY)}px`;\n const left = `${position === 'fixed' ? rect.left : (rect.left + window.scrollX)}px`;\n\n const overlay = createOverlay(this.document, {\n top, left, width: `${rect.width}px`, height: `${rect.height}px`, position, backgroundColor\n }, depth);\n\n const chip = createChip(this.document, {\n displayName,\n depth,\n top,\n left,\n backgroundColor,\n color,\n position,\n name: getIdentifier(item),\n opacity: this.chipsOpacity\n }, overlay);\n\n wrapper.append(overlay);\n wrapper.append(chip);\n\n const positionKey = `${top};${left}`;\n if (!overlayData[positionKey]) {\n overlayData[positionKey] = [];\n }\n overlayData[positionKey].push({ chip, overlay, depth });\n });\n\n Object.values(overlayData).forEach((chips) => {\n chips\n .sort(({ depth: depthA }, { depth: depthB }) => depthA - depthB)\n .forEach(({ chip, overlay }, index, array) => {\n if (index !== 0) {\n // In case of overlap,\n // we should translate the chip to have it visible\n // and reduce the size of the overlay.\n const translateX = array.slice(0, index).reduce((sum, e) => sum + e.chip.getBoundingClientRect().width, 0);\n chip.style.transform = `translateX(${translateX}px)`;\n overlay.style.margin = `${index}px 0 0 ${index}px`;\n overlay.style.width = `${+overlay.style.width.replace('px', '') - 2 * index}px`;\n overlay.style.height = `${+overlay.style.height.replace('px', '') - 2 * index}px`;\n overlay.style.zIndex = `${+overlay.style.zIndex - index}`;\n }\n });\n });\n }\n\n /**\n * Returns true if the highlight is displayed\n */\n public isRunning() {\n return !!this.throttleRun || this.singleRun;\n }\n\n /**\n * Start the highlight of elements\n */\n public start() {\n this.stop();\n if (!this.autoRefresh) {\n this.run();\n this.singleRun = true;\n return;\n }\n\n this.throttleRun = throttle(() => this.run(), this.throttleInterval);\n this.throttleRun();\n this.document.addEventListener('scroll', this.throttleRun, true);\n this.resizeObserver.observe(this.document.body);\n this.mutationObserver.observe(this.document.body, { childList: true, subtree: true });\n }\n\n /**\n * Stop the highlight of elements\n */\n public stop() {\n this.cleanHighlightWrapper();\n if (this.throttleRun) {\n this.document.removeEventListener('scroll', this.throttleRun, true);\n this.resizeObserver.disconnect();\n this.mutationObserver.disconnect();\n this.throttleRun = undefined;\n this.singleRun = false;\n }\n }\n}\n","import {\n otterComponentInfoPropertyName,\n} from '@o3r/core';\n\nexport interface OtterLikeComponentInfo {\n /** Container information */\n container?: OtterLikeComponentInfo;\n /** Configuration ID */\n configId?: string;\n /** Component name */\n componentName: string;\n /** Component translation */\n translations?: Record<string, string[]>;\n /** Component analytics */\n analytics?: Record<string, string[]>;\n}\n\n/**\n * Otter inspector css class\n */\nexport const INSPECTOR_CLASS = 'otter-devtools-inspector';\n\n/**\n * Determine if a node is an Otter container\n * @param node Element to check\n * @returns true if the node is an Otter container\n */\nexport const isContainer = (node: Element | undefined | null): node is Element => {\n return !!node?.tagName.toLowerCase().endsWith('cont');\n};\n\n/**\n * Determine the config id of a component instance\n * @param instance component instance\n * @returns the config id of the component instance\n */\nexport const getConfigId = (instance: any) => {\n return instance[otterComponentInfoPropertyName]?.configId;\n};\n\n/**\n * Recursive method to determin the translations of a node\n * @param node HTMLElement to check\n * @param rec recursive method\n * @returns the trasnslations associated to their component name\n */\nexport function getTranslationsRec(node: Element | null, rec: typeof getTranslationsRec): Record<string, string[]> | undefined {\n const angularDevTools = (window as any).ng;\n const o3rInfoProperty: typeof otterComponentInfoPropertyName = '__otter-info__';\n if (!node || !angularDevTools) {\n return;\n }\n const component = angularDevTools.getComponent(node);\n const subTranslations: (Record<string, string[]> | undefined)[] = Array.from(node.children).map((child) => rec(child, rec));\n const translations: Record<string, string[]> = {};\n subTranslations.forEach((s) => {\n Object.entries(s || {}).forEach(([k, v]) => {\n if (v.length > 0) {\n translations[k] = v;\n }\n });\n });\n if (component) {\n const componentName: string = component.constructor.name;\n const componentTranslations = Object.values<string>(component[o3rInfoProperty]?.translations || {}).filter((t) => typeof t === 'string');\n if (componentTranslations.length > 0) {\n translations[componentName] = componentTranslations;\n }\n }\n return Object.keys(translations).length > 0 ? translations : undefined;\n}\n\n/**\n * Determine the translations of a node\n * @param node HTMLElement to check\n * @returns the translations associated to their component name\n */\nexport const getTranslations = (node: Element | null): Record<string, string[]> | undefined => getTranslationsRec(node, getTranslations);\n\n/**\n * Recursive method to determine the analytics of a node\n * @param node Element to check\n * @param rec recursive method\n * @returns the analytics associated to their component name\n */\nexport function getAnalyticEventsRec(node: Element | null, rec: typeof getAnalyticEventsRec): Record<string, string[]> | undefined {\n const angularDevTools = (window as any).ng;\n const o3rInfoProperty: typeof otterComponentInfoPropertyName = '__otter-info__';\n if (!node || !angularDevTools) {\n return;\n }\n const component = angularDevTools.getComponent(node);\n const subEvents = Array.from(node.children).map((child) => rec(child, rec));\n const events: Record<string, string[]> = {};\n subEvents.forEach((s) => {\n Object.entries(s || {}).forEach(([k, v]) => {\n if (v.length > 0) {\n events[k] = v;\n }\n });\n });\n if (component && component[o3rInfoProperty]) {\n const componentName: string = component.constructor.name;\n const componentEvents: string[] = Object.values<any>(component.analyticsEvents || {}).map((eventConstructor) => eventConstructor.name);\n if (componentEvents.length > 0) {\n events[componentName] = componentEvents;\n }\n }\n return Object.keys(events).length > 0 ? events : undefined;\n}\n\n/**\n * Determine the analytics of a node\n * @param node Element to check\n * @returns the analytics associated to their component name\n */\nexport const getAnalyticEvents = (node: Element | null): Record<string, string[]> | undefined => getAnalyticEventsRec(node, getAnalyticEvents);\n\n/**\n * Determine all info from an Otter component\n * @param componentClassInstance component instance\n * @param host HTML element hosting the component\n * @returns all info from an Otter component\n */\nexport const getOtterLikeComponentInfo = (componentClassInstance: any, host: Element): OtterLikeComponentInfo => {\n const configId = getConfigId(componentClassInstance);\n const translations = getTranslations(host);\n const analytics = getAnalyticEvents(host);\n return {\n // Cannot use anymore `constructor.name` else all components are named `_a`\n componentName: componentClassInstance.constructor.ɵcmp?.debugInfo?.className || componentClassInstance.constructor.name,\n configId,\n translations,\n analytics\n };\n};\n","import {\n BehaviorSubject,\n Observable,\n} from 'rxjs';\nimport {\n Ng,\n} from './ng';\nimport {\n getOtterLikeComponentInfo,\n INSPECTOR_CLASS,\n isContainer,\n OtterLikeComponentInfo,\n} from './otter-inspector.helpers';\n\ninterface ComponentInfo extends OtterLikeComponentInfo {\n component: any;\n host: Element;\n}\n\n/**\n * Service to handle the custom inspector for the Otter Devtools Chrome extension.\n */\nexport class OtterInspectorService {\n private readonly angularDevTools: Ng | undefined;\n private readonly elementMouseOverCallback = this.elementMouseOver.bind(this);\n private readonly elementClickCallback = this.elementClick.bind(this);\n private readonly cancelEventCallback = this.cancelEvent.bind(this);\n private selectedComponent: ComponentInfo | undefined;\n private inspectorDiv: HTMLDivElement | null = null;\n private readonly otterLikeComponentInfoToBeSent = new BehaviorSubject<OtterLikeComponentInfo | undefined>(undefined);\n\n /**\n * Stream of component info to be sent to the extension app.\n */\n public otterLikeComponentInfoToBeSent$: Observable<OtterLikeComponentInfo | undefined> = this.otterLikeComponentInfoToBeSent.asObservable();\n\n constructor() {\n this.angularDevTools = (window as any).ng as Ng | undefined;\n }\n\n private startInspecting() {\n window.addEventListener('mouseover', this.elementMouseOverCallback, true);\n window.addEventListener('click', this.elementClickCallback, true);\n window.addEventListener('mouseout', this.cancelEventCallback, true);\n }\n\n private elementClick(e: MouseEvent) {\n e.stopImmediatePropagation();\n e.stopPropagation();\n e.preventDefault();\n if (!this.selectedComponent || !this.angularDevTools) {\n return;\n }\n const parentElement = this.selectedComponent.host.parentElement;\n const parent = parentElement && (this.angularDevTools.getComponent(parentElement) || this.angularDevTools.getOwningComponent(parentElement));\n const parentHost = parent && this.angularDevTools.getHostElement(parent);\n const container = isContainer(parentHost)\n ? getOtterLikeComponentInfo(parent, parentHost)\n : undefined;\n\n const { component, host, ...infoToBeSent } = this.selectedComponent;\n\n this.otterLikeComponentInfoToBeSent.next({\n ...infoToBeSent,\n container\n });\n }\n\n private isOtterLikeComponent(info: OtterLikeComponentInfo) {\n const hasConfigId = !!info.configId;\n const hasTranslations = !!info.translations?.length;\n const hasAnalytics = Object.keys(info.analytics || {}).length > 0;\n return hasConfigId || hasTranslations || hasAnalytics;\n }\n\n private findComponentInfo(node: Element): ComponentInfo | undefined {\n if (!this.angularDevTools) {\n return;\n }\n let componentClassInstance = this.angularDevTools.getComponent(node) || this.angularDevTools.getOwningComponent(node);\n let o3rLikeComponentInfo: OtterLikeComponentInf