2020-03-20 23:02:11 +00:00
|
|
|
import { deeplySerializeSelectedProperties, serializeDirectiveState } from './state-serializer/state-serializer';
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2020-02-24 17:38:22 +00:00
|
|
|
import {
|
|
|
|
|
DevToolsNode,
|
|
|
|
|
ElementPosition,
|
|
|
|
|
ComponentExplorerViewQuery,
|
|
|
|
|
DirectivesProperties,
|
|
|
|
|
UpdatedStateData,
|
2020-03-20 23:02:11 +00:00
|
|
|
PropertyQueryTypes,
|
2020-02-24 17:38:22 +00:00
|
|
|
} from 'protocol';
|
2020-02-19 03:04:34 +00:00
|
|
|
import { DebuggingAPI } from './interfaces';
|
2020-02-19 22:39:50 +00:00
|
|
|
import { IndexedNode } from './observer/identity-tracker';
|
2020-03-11 03:08:56 +00:00
|
|
|
import { buildDirectiveTree, getLViewFromDirectiveOrElementInstance } from './lview-transform';
|
2020-01-27 18:40:18 +00:00
|
|
|
|
2020-03-11 15:40:42 +00:00
|
|
|
const ngDebug = () => (window as any).ng;
|
2020-02-24 17:38:22 +00:00
|
|
|
|
2020-02-20 01:33:54 +00:00
|
|
|
export interface DirectiveInstanceType {
|
2020-01-27 18:40:18 +00:00
|
|
|
instance: any;
|
2020-02-20 01:33:54 +00:00
|
|
|
name: string;
|
2020-01-27 18:40:18 +00:00
|
|
|
}
|
|
|
|
|
|
2020-02-20 01:33:54 +00:00
|
|
|
export interface ComponentInstanceType {
|
2020-01-27 18:40:18 +00:00
|
|
|
instance: any;
|
2020-02-20 01:33:54 +00:00
|
|
|
name: string;
|
2020-03-09 23:13:34 +00:00
|
|
|
isElement: boolean;
|
2020-01-27 18:40:18 +00:00
|
|
|
}
|
|
|
|
|
|
2020-02-21 00:34:52 +00:00
|
|
|
export interface ComponentTreeNode extends DevToolsNode<DirectiveInstanceType, ComponentInstanceType> {
|
2020-01-27 18:40:18 +00:00
|
|
|
children: ComponentTreeNode[];
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-29 00:29:23 +00:00
|
|
|
export const getLatestComponentState = (query: ComponentExplorerViewQuery): DirectivesProperties | undefined => {
|
2020-03-20 23:02:11 +00:00
|
|
|
const node = queryDirectiveForest(query.selectedElement, buildDirectiveForest((window as any).ng));
|
|
|
|
|
if (!node) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-02-19 18:28:41 +00:00
|
|
|
|
2020-03-20 23:02:11 +00:00
|
|
|
const result: DirectivesProperties = {};
|
|
|
|
|
|
|
|
|
|
const populateResultSet = (dir: DirectiveInstanceType | ComponentInstanceType) => {
|
|
|
|
|
if (query.propertyQuery.type === PropertyQueryTypes.All) {
|
2020-01-27 18:40:18 +00:00
|
|
|
result[dir.name] = {
|
2020-03-20 23:02:11 +00:00
|
|
|
props: serializeDirectiveState(dir.instance),
|
2020-01-27 18:40:18 +00:00
|
|
|
};
|
2020-03-20 23:02:11 +00:00
|
|
|
}
|
|
|
|
|
if (query.propertyQuery.type === PropertyQueryTypes.Specified) {
|
|
|
|
|
result[dir.name] = {
|
2020-03-21 00:19:27 +00:00
|
|
|
props: deeplySerializeSelectedProperties(dir.instance, query.propertyQuery.properties[dir.name] || []),
|
2020-01-27 18:40:18 +00:00
|
|
|
};
|
|
|
|
|
}
|
2020-03-20 23:02:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
node.directives.forEach(populateResultSet);
|
|
|
|
|
if (node.component) {
|
|
|
|
|
populateResultSet(node.component);
|
2020-01-27 18:40:18 +00:00
|
|
|
}
|
2020-03-20 23:02:11 +00:00
|
|
|
|
2020-01-27 18:40:18 +00:00
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-12 15:47:40 +00:00
|
|
|
const getRootLViewsHelper = (element: Element, rootLViews = new Set<any>()): Set<any> => {
|
2020-03-11 03:08:56 +00:00
|
|
|
if (!(element instanceof HTMLElement)) {
|
2020-03-12 15:47:40 +00:00
|
|
|
return rootLViews;
|
2020-03-11 03:08:56 +00:00
|
|
|
}
|
|
|
|
|
const lView = getLViewFromDirectiveOrElementInstance(element);
|
|
|
|
|
if (lView) {
|
2020-03-12 01:13:06 +00:00
|
|
|
rootLViews.add(lView);
|
2020-03-12 15:47:40 +00:00
|
|
|
return rootLViews;
|
2020-03-11 03:08:56 +00:00
|
|
|
}
|
|
|
|
|
// tslint:disable-next-line: prefer-for-of
|
|
|
|
|
for (let i = 0; i < element.children.length; i++) {
|
|
|
|
|
getRootLViewsHelper(element.children[i], rootLViews);
|
|
|
|
|
}
|
|
|
|
|
return rootLViews;
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-12 15:47:40 +00:00
|
|
|
// To get all roots, we first get all elements with ng-version attribute.
|
|
|
|
|
// This includes all app roots plus Angular Elements.
|
|
|
|
|
// We may also have overlays which are on the same level as the top-level
|
|
|
|
|
// app. We get these by traversing the DOM starting from the root DOM
|
|
|
|
|
// element and stopping once we hit a node which is not HTMLElement or
|
|
|
|
|
// has lView data associated with it.
|
|
|
|
|
const getRootLViews = (element: Element): Set<any> => {
|
2020-03-12 01:13:06 +00:00
|
|
|
const roots = element.querySelectorAll('[ng-version]');
|
|
|
|
|
return getRootLViewsHelper(element, new Set(Array.from(roots).map(getLViewFromDirectiveOrElementInstance)));
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-08 18:51:01 +00:00
|
|
|
export const buildDirectiveForest = (ngd: DebuggingAPI): ComponentTreeNode[] => {
|
2020-03-12 01:13:06 +00:00
|
|
|
const roots = getRootLViews(document.documentElement);
|
|
|
|
|
const result = Array.prototype.concat.apply([], [...roots].map(buildDirectiveTree));
|
2020-03-11 03:08:56 +00:00
|
|
|
return result;
|
2020-01-27 18:40:18 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Based on an ElementID we return a specific component node.
|
|
|
|
|
// If we can't find any, we return null.
|
2020-03-08 18:51:01 +00:00
|
|
|
export const queryDirectiveForest = (
|
2020-02-19 21:39:32 +00:00
|
|
|
position: ElementPosition,
|
|
|
|
|
forest: ComponentTreeNode[]
|
|
|
|
|
): ComponentTreeNode | null => {
|
|
|
|
|
if (!position.length) {
|
2020-01-27 18:40:18 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
let node: null | ComponentTreeNode = null;
|
2020-02-19 21:39:32 +00:00
|
|
|
for (const i of position) {
|
2020-01-27 18:40:18 +00:00
|
|
|
node = forest[i];
|
|
|
|
|
if (!node) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
forest = node.children;
|
|
|
|
|
}
|
|
|
|
|
return node;
|
|
|
|
|
};
|
2020-02-07 19:43:49 +00:00
|
|
|
|
2020-02-19 21:39:32 +00:00
|
|
|
export const findNodeInForest = (position: ElementPosition, forest: ComponentTreeNode[]): HTMLElement | null => {
|
2020-03-20 18:54:37 +00:00
|
|
|
const foundComponent: ComponentTreeNode | null = queryDirectiveForest(position, forest);
|
2020-02-07 19:43:49 +00:00
|
|
|
return foundComponent ? (foundComponent.nativeElement as HTMLElement) : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getIndexForNativeElementInForest = (
|
|
|
|
|
nativeElement: HTMLElement,
|
|
|
|
|
forest: IndexedNode[]
|
2020-02-19 21:39:32 +00:00
|
|
|
): ElementPosition | null => {
|
2020-03-20 18:54:37 +00:00
|
|
|
const foundElementPosition: ElementPosition | null = findElementIDFromNativeElementInForest(forest, nativeElement);
|
2020-02-19 21:39:32 +00:00
|
|
|
return foundElementPosition || null;
|
2020-02-07 19:43:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const findElementIDFromNativeElementInForest = (
|
|
|
|
|
forest: IndexedNode[],
|
|
|
|
|
nativeElement: HTMLElement
|
2020-02-19 21:39:32 +00:00
|
|
|
): ElementPosition | null => {
|
2020-02-19 03:04:34 +00:00
|
|
|
for (const el of forest) {
|
|
|
|
|
if (el.nativeElement === nativeElement) {
|
2020-02-19 21:39:32 +00:00
|
|
|
return el.position;
|
2020-02-07 19:43:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
2020-02-19 03:04:34 +00:00
|
|
|
|
|
|
|
|
for (const el of forest) {
|
|
|
|
|
if (el.children.length) {
|
|
|
|
|
return findElementIDFromNativeElementInForest(el.children, nativeElement);
|
2020-02-07 19:43:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
2020-02-07 21:09:36 +00:00
|
|
|
|
2020-02-19 21:39:32 +00:00
|
|
|
export const findNodeFromSerializedPosition = (serializedPosition: string) => {
|
|
|
|
|
const position: number[] = serializedPosition.split(',').map(index => parseInt(index, 10));
|
2020-03-11 15:40:42 +00:00
|
|
|
return queryDirectiveForest(position, buildDirectiveForest(ngDebug()));
|
2020-02-07 21:09:36 +00:00
|
|
|
};
|
2020-02-19 20:07:01 +00:00
|
|
|
|
2020-03-20 18:54:37 +00:00
|
|
|
export const updateState = (updatedStateData: UpdatedStateData): void => {
|
2020-03-11 15:40:42 +00:00
|
|
|
const ngd = ngDebug();
|
|
|
|
|
const node = queryDirectiveForest(updatedStateData.directiveId.element, buildDirectiveForest(ngd));
|
2020-03-20 18:54:37 +00:00
|
|
|
if (!node) {
|
|
|
|
|
console.warn('Could not update the state of component', updateState, 'because the component was not found');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (updatedStateData.directiveId.directive !== undefined) {
|
2020-02-19 20:07:01 +00:00
|
|
|
const directive = node.directives[updatedStateData.directiveId.directive].instance;
|
|
|
|
|
mutateComponentOrDirective(updatedStateData, directive);
|
2020-03-11 15:40:42 +00:00
|
|
|
ngd.applyChanges(ngd.getOwningComponent(directive));
|
2020-03-20 18:54:37 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (node.component) {
|
|
|
|
|
const comp = node.component.instance;
|
|
|
|
|
mutateComponentOrDirective(updatedStateData, comp);
|
|
|
|
|
ngd.applyChanges(comp);
|
|
|
|
|
return;
|
2020-02-19 20:07:01 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-20 18:54:37 +00:00
|
|
|
const mutateComponentOrDirective = (updatedStateData: UpdatedStateData, compOrDirective: any) => {
|
2020-02-19 20:07:01 +00:00
|
|
|
const valueKey = updatedStateData.keyPath.pop();
|
2020-03-20 18:54:37 +00:00
|
|
|
if (valueKey === undefined) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-02-19 20:07:01 +00:00
|
|
|
|
|
|
|
|
let parentObjectOfValueToUpdate = compOrDirective;
|
|
|
|
|
updatedStateData.keyPath.forEach(key => {
|
|
|
|
|
parentObjectOfValueToUpdate = parentObjectOfValueToUpdate[key];
|
|
|
|
|
});
|
2020-02-24 17:38:22 +00:00
|
|
|
|
2020-02-19 20:07:01 +00:00
|
|
|
parentObjectOfValueToUpdate[valueKey] = updatedStateData.newValue;
|
|
|
|
|
};
|