Chrome devtools elements tab support for Android (#4351)

* Enable chrome-devtools elemets tab

* Trigger updates when property is chaned form native

* Tslint fixes

* Don't run dom-elemet tests in IOS

* fix tests

* Create package.json

* Update package.json

* domNode changed to field for performance
This commit is contained in:
Alexander Vakrilov
2017-06-12 16:48:27 +03:00
committed by GitHub
parent b7c61cad96
commit f2462158fb
16 changed files with 896 additions and 8 deletions

View File

@@ -0,0 +1,81 @@
/*
On element select in the inspector the following are requested:
- Inline styles -> CSSStyle
- Attributes styles -> CSSStyle (Appears as 'Stacklayout[Attributes style]` - unsure of its purpose) irrelevant?
- Style matches -> RuleMatch[]
- Inherited styles -> InheritedStyleEntry[]
- Pseudo Element matches -> PseudoElementMatches[]
- Computed Styles for node -> CSSComputedStyleProperty[]
- Element Fonts -> PlatformFontUsage
*/
export interface CSSProperty {
name: string
value: string
disabled: boolean // strikes out the disabled property
}
export interface ShorthandEntry { // seems irrelevant - feel free to leave empty for now
name: string
value: string
}
export interface CSSStyle {
cssProperties: CSSProperty[]
shorthandEntries: ShorthandEntry[] // doesn't seem to display anywhere
cssText?: string
}
export interface Value {
text: string
}
export interface SelectorList { // e.g. [".btn", "Button", "Label"]
selectors: Value[]
text: string // doesn't seem to display anywhere
}
export interface CSSRule {
selectorList: SelectorList
origin: string // a constant - "regular"
style: CSSStyle
styleSheetId?: string // associated stylesheet
}
export interface RuleMatch {
rule: CSSRule
matchingSelectors: number[] // index-based - the index of the selector that the node currently inspected matches
}
export interface InheritedStyleEntry {
matchedCSSRules: RuleMatch[]
inlineStyle?: CSSStyle
}
export interface CSSComputedStyleProperty {
name: string
value: string
}
export interface PlatformFontUsage {
familyName: string
glyphCount: number // number of characters in text of element
isCustomFont: boolean
}
export interface CSSStyleSheetHeader {
styleSheetId: string // a unique identifier - file name/path should do
frameId: string // constant
sourceUrl: string
origin: string // constant
title: string // the same as the id?
disabled: boolean // false - if the css has been invalidated/disabled
isInLine: boolean // false
startLine: number // constant - 1
startColumn: number // constant - 1
}
export interface PseudoElementMatches {
pseudoType: string // e.g. last-child
matches: RuleMatch[]
}

View File

@@ -0,0 +1,97 @@
import { unsetValue } from "../ui/core/properties";
import { ViewBase } from "../ui/core/view-base";
import { topmost } from "../ui/frame";
import { getNodeById } from "./dom-node";
export interface Inspector {
// DevTools -> Application communication. Methods that devtools calls when needed.
getDocument(): string;
removeNode(nodeId: number): void;
getComputedStylesForNode(nodeId: number): string;
setAttributeAsText(nodeId: number, text: string, name: string): void;
// Application -> DevTools communication. Methods that the app should call when needed.
childNodeInserted(parentId: number, lastId: number, nodeStr: string): void;
childNodeRemoved(parentId: number, nodeId: number): void;
attributeModified(nodeId: number, attrName: string, attrValue: string): void;
attributeRemoved(nodeId: number, attrName: string): void;
}
function getViewById(nodeId: number): ViewBase {
const node = getNodeById(nodeId);
let view;
if (node) {
view = node.viewRef.get();
}
return view;
}
export function attachInspectorCallbacks(inspector: Inspector) {
inspector.getDocument = function () {
const topMostFrame = topmost();
topMostFrame.ensureDomNode();
return topMostFrame.domNode.toJSON();
}
inspector.getComputedStylesForNode = function (nodeId) {
const view = getViewById(nodeId);
if (view) {
return JSON.stringify(view.domNode.getComputedProperties());
}
return "[]";
}
inspector.removeNode = function (nodeId) {
const view = getViewById(nodeId);
if (view) {
// Avoid importing layout and content view
let parent = <any>view.parent;
if (parent.removeChild) {
parent.removeChild(view);
} else if (parent.content === view) {
parent.content = null;
}
else {
console.log("Can't remove child from " + parent);
}
}
}
inspector.setAttributeAsText = function (nodeId, text, name) {
const view = getViewById(nodeId);
if (view) {
// attribute is registered for the view instance
let hasOriginalAttribute = !!name.trim();
if (text) {
let textParts = text.split("=");
if (textParts.length === 2) {
let attrName = textParts[0];
let attrValue = textParts[1].replace(/['"]+/g, '');
// if attr name is being replaced with another
if (name !== attrName && hasOriginalAttribute) {
view[name] = unsetValue;
view[attrName] = attrValue;
} else {
view[hasOriginalAttribute ? name : attrName] = attrValue;
}
}
} else {
// delete attribute
view[name] = unsetValue;
}
view.domNode.loadAttributes();
}
}
}
// Automatically attach callbacks if there is __inspector
if (global && global.__inspector) {
attachInspectorCallbacks(global.__inspector)
}

View File

@@ -0,0 +1,197 @@
import { getSetProperties, getComputedCssValues } from "../ui/core/properties";
import { PercentLength } from "../ui/styling/style-properties";
import { ViewBase } from "../ui/core/view";
import { Color } from "../color";
import { CSSComputedStyleProperty } from "./css-agent";
import { Inspector } from "./devtools-elements";
const registeredDomNodes = {};
const ELEMENT_NODE_TYPE = 1;
const ROOT_NODE_TYPE = 9;
const propertyBlacklist = [
"effectivePaddingLeft",
"effectivePaddingBottom",
"effectivePaddingRight",
"effectivePaddingTop",
"effectiveBorderTopWidth",
"effectiveBorderRightWidth",
"effectiveBorderBottomWidth",
"effectiveBorderLeftWidth",
"effectiveMinWidth",
"nodeName",
"nodeType",
"decodeWidth",
"decodeHeight"
]
function notifyInspector(callback: (inspector: Inspector) => void) {
const ins = (<any>global).__inspector
if (ins) {
callback(ins);
}
}
function valueToString(value: any): string {
if (typeof value === "undefined" || value === null) {
return "";
} else if (value instanceof Color) {
return value.toString()
} else if (typeof value === "object") {
return PercentLength.convertToString(value)
} else {
return value + "";
}
}
function propertyFilter([name, value]: [string, any]): boolean {
if (name[0] === "_") {
return false;
}
if (value !== null && typeof value === "object") {
return false;
}
if (propertyBlacklist.indexOf(name) >= 0) {
return false;
}
return true;
}
function registerNode(domNode: DOMNode) {
registeredDomNodes[domNode.nodeId] = domNode;
}
function unregisterNode(domNode: DOMNode) {
delete registeredDomNodes[domNode.nodeId];
}
export function getNodeById(id: number): DOMNode {
return registeredDomNodes[id];
}
export class DOMNode {
nodeId;
nodeType;
nodeName;
localName;
nodeValue = '';
attributes: string[] = [];
viewRef: WeakRef<ViewBase>;
constructor(view: ViewBase) {
this.viewRef = new WeakRef(view);
this.nodeType = view.typeName === "Frame" ? ROOT_NODE_TYPE : ELEMENT_NODE_TYPE;
this.nodeId = view._domId;
this.nodeName = view.typeName;
this.localName = this.nodeName;
// Load all attributes
this.loadAttributes();
registerNode(this);
}
public loadAttributes() {
this.attributes = [];
getSetProperties(this.viewRef.get())
.filter(propertyFilter)
.forEach(pair => this.attributes.push(pair[0], pair[1] + ""));
}
get children(): DOMNode[] {
const view = this.viewRef.get();
if (!view) {
return [];
}
const res = [];
view.eachChild((child) => {
child.ensureDomNode();
res.push(child.domNode);
return true;
});
return res;
}
onChildAdded(childView: ViewBase): void {
notifyInspector((ins) => {
const view = this.viewRef.get();
childView.ensureDomNode();
let previousChild: ViewBase;
view.eachChild((child) => {
if (child === childView) {
return false;
}
previousChild = child;
return true;
});
const index = !!previousChild ? previousChild._domId : 0;
ins.childNodeInserted(this.nodeId, index, childView.domNode.toJSON());
});
}
onChildRemoved(view: ViewBase): void {
notifyInspector((ins) => {
ins.childNodeRemoved(this.nodeId, view.domNode.nodeId);
});
}
attributeModified(name: string, value: any) {
notifyInspector((ins) => {
ins.attributeModified(this.nodeId, name, valueToString(value));
});
}
attributeRemoved(name: string) {
notifyInspector((ins) => {
ins.attributeRemoved(this.nodeId, name);
});
}
getComputedProperties(): CSSComputedStyleProperty[] {
const view = this.viewRef.get();
if (!view) {
return [];
}
const result = getComputedCssValues(view)
.filter(pair => pair[0][0] !== "_")
.map((pair) => {
return {
name: pair[0],
value: valueToString(pair[1])
};
});
return result;
}
dispose() {
unregisterNode(this);
this.viewRef.clear();
}
public toJSON() {
return JSON.stringify(this.toObject());
}
private toObject() {
return {
nodeId: this.nodeId,
nodeType: this.nodeType,
nodeName: this.nodeName,
localName: this.localName,
nodeValue: this.nodeValue,
children: this.children.map(c => c.toObject()),
attributes: this.attributes
};
};
}