mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 19:26:42 +08:00
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:

committed by
GitHub

parent
b7c61cad96
commit
f2462158fb
13
apps/app/devtools-app/app.css
Normal file
13
apps/app/devtools-app/app.css
Normal file
@ -0,0 +1,13 @@
|
||||
.btn1 {
|
||||
background-color: lightgreen;
|
||||
color: coral;
|
||||
font-size: 20;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
background-color: coral;
|
||||
color: lightgreen;
|
||||
font-size: 24;
|
||||
font-family: serif;
|
||||
}
|
6
apps/app/devtools-app/app.ts
Normal file
6
apps/app/devtools-app/app.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import * as application from "tns-core-modules/application";
|
||||
|
||||
// Needed only for build infrastructure
|
||||
application.setCssFileName("devtools-app/app.css");
|
||||
|
||||
application.start({ moduleName: "devtools-app/main-page" });
|
36
apps/app/devtools-app/main-page.ts
Normal file
36
apps/app/devtools-app/main-page.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import * as frame from "tns-core-modules/ui/frame";
|
||||
import { Label } from "tns-core-modules/ui/label";
|
||||
import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout";
|
||||
// import { DOMNode } from "tns-core-modules/debugger/dom-node";
|
||||
|
||||
export function print(args) {
|
||||
// const node = new DOMNode(frame.topmost());
|
||||
// console.dir(node.toJSON());
|
||||
|
||||
// const btn = args.object.page.getViewById("btn");
|
||||
// btn.ensureDomNode();
|
||||
// console.dir(btn.domNode.getComputedProperties());
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
export function add(args) {
|
||||
const container = args.object.page.getViewById("container");
|
||||
const lbl = new Label();
|
||||
lbl.text = "label " + i++;
|
||||
container.addChild(lbl);
|
||||
}
|
||||
|
||||
export function remove(args) {
|
||||
const container = <StackLayout>args.object.page.getViewById("container");
|
||||
const lbl = container.getChildAt(container.getChildrenCount() - 1);
|
||||
container.removeChild(lbl);
|
||||
}
|
||||
|
||||
export function navigate() {
|
||||
frame.topmost().navigate("gallery-app/main-page");
|
||||
}
|
||||
|
||||
export function change(args){
|
||||
args.object.text = "hi " + Math.random();
|
||||
|
||||
}
|
29
apps/app/devtools-app/main-page.xml
Normal file
29
apps/app/devtools-app/main-page.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Page navigatedTo="print">
|
||||
<GridLayout rows="* * *" columns="* * *">
|
||||
|
||||
<Button id="btn"
|
||||
customProp="custom"
|
||||
text="hi there"
|
||||
fontSize="30"
|
||||
row="1"
|
||||
col="1"
|
||||
color="red"
|
||||
backgroundColor="lightgreen"
|
||||
class="btn"
|
||||
tap="change"/>
|
||||
<Button text="class: btn1" class="btn1" row="1" col="0"/>
|
||||
<Button text="class: btn2" class="btn2" row="1" col="2"/>
|
||||
|
||||
<Button text="add" tap="add" />
|
||||
<Button text="remove" tap="remove" col="2" />
|
||||
|
||||
<Button text="move beach" tap="navigate" col="1" />
|
||||
<StackLayout colSpan="3" row="2" id="container">
|
||||
<TextField hint="hint" text="123" />
|
||||
<TextField hint="hint" text="456" />
|
||||
|
||||
|
||||
</StackLayout>
|
||||
</GridLayout>
|
||||
</Page>
|
334
tests/app/debugger/dom-node-tests.ts
Normal file
334
tests/app/debugger/dom-node-tests.ts
Normal file
@ -0,0 +1,334 @@
|
||||
import { assert, assertEqual } from "../TKUnit";
|
||||
import { DOMNode } from "tns-core-modules/debugger/dom-node";
|
||||
import { attachInspectorCallbacks } from "tns-core-modules/debugger/devtools-elements";
|
||||
import { Inspector } from "tns-core-modules/debugger/devtools-elements";
|
||||
import { unsetValue } from "tns-core-modules/ui/core/properties";
|
||||
import { Button } from "tns-core-modules/ui/button";
|
||||
import { Slider } from "tns-core-modules/ui/slider";
|
||||
import { Label } from "tns-core-modules/ui/label";
|
||||
import { textProperty } from "tns-core-modules/ui/text-base";
|
||||
import { TextView } from "tns-core-modules/ui/text-view";
|
||||
import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout";
|
||||
|
||||
let originalInspectorGlobal: Inspector;
|
||||
let currentInspector: Inspector;
|
||||
function getTestInspector(): Inspector {
|
||||
let inspector = {
|
||||
getDocument(): string { return ""; },
|
||||
removeNode(nodeId: number): void { /* */ },
|
||||
getComputedStylesForNode(nodeId: number): string { return ""; },
|
||||
setAttributeAsText(nodeId: number, text: string, name: string): void { /* */},
|
||||
|
||||
childNodeInserted(parentId: number, lastId: number, nodeStr: string): void { /* to be replaced */ },
|
||||
childNodeRemoved(parentId: number, nodeId: number): void { /* to be replaced */ },
|
||||
attributeModified(nodeId: number, attrName: string, attrValue: string) { /* to be replaced */ },
|
||||
attributeRemoved(nodeId: number, attrName: string) { /* to be replaced */ }
|
||||
}
|
||||
|
||||
attachInspectorCallbacks(inspector);
|
||||
|
||||
return inspector;
|
||||
}
|
||||
|
||||
export function setUp(): void {
|
||||
originalInspectorGlobal = global.__inspector;
|
||||
currentInspector = getTestInspector();
|
||||
global.__inspector = currentInspector;
|
||||
}
|
||||
|
||||
export function tearDown(): void {
|
||||
global.__inspector = originalInspectorGlobal;
|
||||
}
|
||||
|
||||
function assertAttribute(domNode: DOMNode, name: string, value: any) {
|
||||
const propIdx = domNode.attributes.indexOf(name);
|
||||
assert(propIdx >= 0, `Attribute ${name} not found`);
|
||||
assertEqual(domNode.attributes[propIdx + 1], value);
|
||||
}
|
||||
|
||||
export function test_custom_attribute_is_reported_in_dom_node() {
|
||||
const btn = new Button();
|
||||
btn["test_prop"] = "test_value";
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
assertAttribute(domNode, "test_prop", "test_value");
|
||||
}
|
||||
|
||||
export function test_custom__falsy_attribute_is_reported_in_dom_node() {
|
||||
const btn = new Button();
|
||||
btn["test_prop_null"] = null;
|
||||
btn["test_prop_0"] = 0;
|
||||
btn["test_prop_undefined"] = undefined;
|
||||
btn["test_prop_empty_string"] = "";
|
||||
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
assertAttribute(domNode, "test_prop_null", null + "");
|
||||
assertAttribute(domNode, "test_prop_0", 0 + "");
|
||||
assertAttribute(domNode, "test_prop_undefined", undefined + "");
|
||||
assertAttribute(domNode, "test_prop_empty_string", "");
|
||||
}
|
||||
|
||||
export function test_property_is_reported_in_dom_node() {
|
||||
const btn = new Button();
|
||||
btn.text = "test_value";
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
assertAttribute(domNode, "text", "test_value");
|
||||
}
|
||||
|
||||
export function test_childNodeInserted_in_dom_node() {
|
||||
let childNodeInsertedCalled = false;
|
||||
let actualParentId = 0;
|
||||
let expectedParentId = 0;
|
||||
|
||||
currentInspector.childNodeInserted = (parentId, lastNodeId, node) => {
|
||||
childNodeInsertedCalled = true;
|
||||
actualParentId = parentId;
|
||||
}
|
||||
|
||||
const stack = new StackLayout();
|
||||
stack.ensureDomNode();
|
||||
expectedParentId = stack._domId;
|
||||
|
||||
const btn1 = new Button();
|
||||
btn1.text = "button1";
|
||||
stack.addChild(btn1);
|
||||
|
||||
assert(childNodeInsertedCalled, "global.__inspector.childNodeInserted not called.");
|
||||
assertEqual(actualParentId, expectedParentId);
|
||||
}
|
||||
|
||||
export function test_childNodeInserted_at_index_in_dom_node() {
|
||||
const stack = new StackLayout();
|
||||
stack.ensureDomNode();
|
||||
|
||||
// child index 0
|
||||
const btn1 = new Button();
|
||||
btn1.text = "button1";
|
||||
stack.addChild(btn1);
|
||||
|
||||
// child index 1
|
||||
const btn2 = new Button();
|
||||
btn2.text = "button2";
|
||||
stack.addChild(btn2);
|
||||
|
||||
// child index 2
|
||||
const btn3 = new Button();
|
||||
btn3.text = "button3";
|
||||
stack.addChild(btn3);
|
||||
|
||||
const lbl = new Label();
|
||||
lbl.text = "label me this";
|
||||
|
||||
let called = false;
|
||||
currentInspector.childNodeInserted = (parentId, lastNodeId, node) => {
|
||||
assertEqual(lastNodeId, btn1._domId, "Child inserted at index 1's previous sibling does not match.");
|
||||
assertEqual(JSON.parse(node).nodeId, lbl._domId, "Child id doesn't match");
|
||||
called = true;
|
||||
}
|
||||
|
||||
stack.insertChild(lbl, 1);
|
||||
assert(called, "childNodeInserted not called");
|
||||
}
|
||||
|
||||
export function test_childNodeRemoved_in_dom_node() {
|
||||
let childNodeRemovedCalled = false;
|
||||
let actualRemovedNodeId = 0;
|
||||
let expectedRemovedNodeId = 0;
|
||||
|
||||
currentInspector.childNodeRemoved = (parentId, nodeId) => {
|
||||
childNodeRemovedCalled = true;
|
||||
actualRemovedNodeId = nodeId;
|
||||
}
|
||||
|
||||
const stack = new StackLayout();
|
||||
stack.ensureDomNode();
|
||||
|
||||
const btn1 = new Button();
|
||||
btn1.text = "button1";
|
||||
expectedRemovedNodeId = btn1._domId;
|
||||
stack.addChild(btn1);
|
||||
|
||||
const btn2 = new Button();
|
||||
btn2.text = "button2";
|
||||
stack.addChild(btn2);
|
||||
|
||||
stack.removeChild(btn1);
|
||||
console.log("btn2: " + btn2);
|
||||
|
||||
assert(childNodeRemovedCalled, "global.__inspector.childNodeRemoved not called.");
|
||||
assertEqual(actualRemovedNodeId, expectedRemovedNodeId);
|
||||
}
|
||||
|
||||
export function test_falsy_property_is_reported_in_dom_node() {
|
||||
const btn = new Button();
|
||||
btn.text = null;
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
assertAttribute(domNode, "text", "null");
|
||||
|
||||
btn.text = undefined;
|
||||
domNode.loadAttributes();
|
||||
assertAttribute(domNode, "text", "undefined");
|
||||
}
|
||||
|
||||
export function test_property_change_calls_attributeModified() {
|
||||
const btn = new Button();
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
|
||||
let callbackCalled = false;
|
||||
currentInspector.attributeModified = (nodeId: number, attrName: string, attrValue: string) => {
|
||||
assertEqual(nodeId, domNode.nodeId, "nodeId");
|
||||
assertEqual(attrName, "text", "attrName");
|
||||
assertEqual(attrValue, "new value", "attrValue");
|
||||
callbackCalled = true;
|
||||
}
|
||||
|
||||
btn.text = "new value";
|
||||
|
||||
assert(callbackCalled, "attributeModified not called");
|
||||
}
|
||||
|
||||
export function test_property_change_from_native_calls_attributeModified() {
|
||||
const tv = new TextView();
|
||||
tv.ensureDomNode();
|
||||
const domNode = tv.domNode;
|
||||
|
||||
let callbackCalled = false;
|
||||
currentInspector.attributeModified = (nodeId: number, attrName: string, attrValue: string) => {
|
||||
assertEqual(nodeId, domNode.nodeId, "nodeId");
|
||||
assertEqual(attrName, "text", "attrName");
|
||||
assertEqual(attrValue, "new value", "attrValue");
|
||||
callbackCalled = true;
|
||||
}
|
||||
|
||||
textProperty.nativeValueChange(tv, "new value");
|
||||
|
||||
assert(callbackCalled, "attributeModified not called");
|
||||
}
|
||||
|
||||
export function test_property_reset_calls_attributeRemoved() {
|
||||
const btn = new Button();
|
||||
btn.text = "some value";
|
||||
btn.ensureDomNode();
|
||||
const domNode = btn.domNode;
|
||||
|
||||
let callbackCalled = false;
|
||||
currentInspector.attributeRemoved = (nodeId: number, attrName: string) => {
|
||||
assertEqual(nodeId, domNode.nodeId, "nodeId");
|
||||
assertEqual(attrName, "text", "attrName");
|
||||
callbackCalled = true;
|
||||
}
|
||||
|
||||
btn.text = unsetValue;
|
||||
|
||||
assert(callbackCalled, "attributeRemoved not called");
|
||||
}
|
||||
|
||||
export function test_coercible_property_change_calls_attributeModified() {
|
||||
const slider = new Slider();
|
||||
slider.ensureDomNode();
|
||||
const domNode = slider.domNode;
|
||||
|
||||
let callbackCalled = false;
|
||||
currentInspector.attributeModified = (nodeId: number, attrName: string, attrValue: string) => {
|
||||
assertEqual(nodeId, domNode.nodeId, "nodeId");
|
||||
assertEqual(attrName, "value", "attrName");
|
||||
assertEqual(attrValue, "10", "attrValue");
|
||||
callbackCalled = true;
|
||||
}
|
||||
|
||||
slider.value = 10;
|
||||
|
||||
assert(callbackCalled, "attributeModified not called");
|
||||
}
|
||||
|
||||
export function test_coercible_property_reset_calls_attributeRemoved() {
|
||||
const slider = new Slider();
|
||||
slider.value = 10;
|
||||
slider.ensureDomNode();
|
||||
const domNode = slider.domNode;
|
||||
|
||||
let callbackCalled = false;
|
||||
currentInspector.attributeRemoved = (nodeId: number, attrName: string) => {
|
||||
assertEqual(nodeId, domNode.nodeId, "nodeId");
|
||||
assertEqual(attrName, "value", "attrName");
|
||||
callbackCalled = true;
|
||||
}
|
||||
|
||||
slider.value = unsetValue;
|
||||
|
||||
assert(callbackCalled, "attributeRemoved not called");
|
||||
}
|
||||
|
||||
export function test_inspector_ui_setAttributeAsText_set_existing_property() {
|
||||
// Arrange
|
||||
const label = new Label();
|
||||
|
||||
label.text = "original label";
|
||||
const expectedValue = "updated label";
|
||||
|
||||
label.ensureDomNode();
|
||||
|
||||
// Act
|
||||
// simulate call from the inspector UI
|
||||
currentInspector.setAttributeAsText(label.domNode.nodeId, "text='" + expectedValue + "'", "text");
|
||||
|
||||
// Assert
|
||||
assertEqual(label.text, expectedValue);
|
||||
}
|
||||
|
||||
export function test_inspector_ui_setAttributeAsText_remove_existing_property() {
|
||||
// Arrange
|
||||
const label = new Label();
|
||||
label.text = "original label";
|
||||
|
||||
label.ensureDomNode();
|
||||
|
||||
// Act
|
||||
// simulate call from the inspector UI
|
||||
currentInspector.setAttributeAsText(label.domNode.nodeId, "" /* empty value - removes the attribute */, "text");
|
||||
|
||||
// Assert
|
||||
assertEqual(label.text, "");
|
||||
}
|
||||
|
||||
export function test_inspector_ui_setAttributeAsText_set_new_property() {
|
||||
// Arrange
|
||||
const label = new Label();
|
||||
const expectedValue = "custom";
|
||||
|
||||
label.ensureDomNode();
|
||||
|
||||
// Act
|
||||
// simulate call from the inspector UI
|
||||
currentInspector.setAttributeAsText(label.domNode.nodeId, "data-attr='" + expectedValue + "'" /* data-attr="custom" */, " " /* empty attr name initially */);
|
||||
|
||||
// Assert
|
||||
assertEqual(label["data-attr"], expectedValue);
|
||||
}
|
||||
|
||||
export function test_inspector_ui_removeNode() {
|
||||
let childNodeRemovedCalled = false;
|
||||
let stack = new StackLayout();
|
||||
let label = new Label();
|
||||
stack.addChild(label);
|
||||
|
||||
stack.ensureDomNode();
|
||||
label.ensureDomNode();
|
||||
|
||||
let expectedParentId = stack.domNode.nodeId;
|
||||
let expectedNodeId = label.domNode.nodeId;
|
||||
|
||||
currentInspector.childNodeRemoved = (parentId, nodeId) => {
|
||||
childNodeRemovedCalled = true;
|
||||
assertEqual(parentId, expectedParentId);
|
||||
assertEqual(nodeId, expectedNodeId);
|
||||
}
|
||||
|
||||
currentInspector.removeNode(label.domNode.nodeId);
|
||||
|
||||
assert(childNodeRemovedCalled, "childNodeRemoved callback not called.");
|
||||
}
|
@ -31,6 +31,9 @@ export function isRunningOnEmulator(): boolean {
|
||||
|
||||
export var allTests = {};
|
||||
|
||||
import * as domNodeTest from "./debugger/dom-node-tests";
|
||||
allTests["DOM-NODE"] = domNodeTest;
|
||||
|
||||
import * as profilingTests from "./profiling/profiling-tests";
|
||||
allTests["PROFILING"] = profilingTests;
|
||||
|
||||
|
81
tns-core-modules/debugger/css-agent.ts
Normal file
81
tns-core-modules/debugger/css-agent.ts
Normal 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[]
|
||||
}
|
97
tns-core-modules/debugger/devtools-elements.ts
Normal file
97
tns-core-modules/debugger/devtools-elements.ts
Normal 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)
|
||||
}
|
197
tns-core-modules/debugger/dom-node.ts
Normal file
197
tns-core-modules/debugger/dom-node.ts
Normal 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
|
||||
};
|
||||
};
|
||||
}
|
@ -108,7 +108,7 @@ export class CssAnimationProperty<T extends Style, U> {
|
||||
|
||||
public register(cls: { prototype: T }): void;
|
||||
public isSet(instance: T): boolean;
|
||||
|
||||
|
||||
public _valueConverter?: (value: string) => any;
|
||||
public static _getByCssName(name: string): CssAnimationProperty<any, any>;
|
||||
}
|
||||
@ -121,4 +121,7 @@ export function propagateInheritableCssProperties(parentStyle: Style, childStyle
|
||||
export function clearInheritedProperties(view: ViewBase): void;
|
||||
|
||||
export function makeValidator<T>(...values: T[]): (value: any) => value is T;
|
||||
export function makeParser<T>(isValid: (value: any) => boolean): (value: any) => T;
|
||||
export function makeParser<T>(isValid: (value: any) => boolean): (value: any) => T;
|
||||
|
||||
export function getSetProperties(view: ViewBase): [string, any][];
|
||||
export function getComputedCssValues(view: ViewBase): [string, any][];
|
@ -12,6 +12,7 @@ export { Style };
|
||||
|
||||
export const unsetValue: any = new Object();
|
||||
|
||||
let cssPropertyNames: string[] = [];
|
||||
let symbolPropertyMap = {};
|
||||
let cssSymbolPropertyMap = {};
|
||||
|
||||
@ -152,6 +153,14 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
|
||||
if (affectsLayout) {
|
||||
this.requestLayout();
|
||||
}
|
||||
|
||||
if (this.domNode) {
|
||||
if (reset) {
|
||||
this.domNode.attributeRemoved(propertyName);
|
||||
} else {
|
||||
this.domNode.attributeModified(propertyName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,6 +188,10 @@ export class Property<T extends ViewBase, U> implements TypedPropertyDescriptor<
|
||||
if (affectsLayout) {
|
||||
owner.requestLayout();
|
||||
}
|
||||
|
||||
if (owner.domNode) {
|
||||
owner.domNode.attributeModified(propertyName, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -299,6 +312,14 @@ export class CoercibleProperty<T extends ViewBase, U> extends Property<T, U> imp
|
||||
if (affectsLayout) {
|
||||
this.requestLayout();
|
||||
}
|
||||
|
||||
if (this.domNode) {
|
||||
if (reset) {
|
||||
this.domNode.attributeRemoved(propertyName);
|
||||
} else {
|
||||
this.domNode.attributeModified(propertyName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -399,6 +420,8 @@ export class CssProperty<T extends Style, U> implements definitions.CssProperty<
|
||||
const propertyName = options.name;
|
||||
this.name = propertyName;
|
||||
|
||||
cssPropertyNames.push(options.cssName);
|
||||
|
||||
this.cssName = `css:${options.cssName}`;
|
||||
this.cssLocalName = options.cssName;
|
||||
|
||||
@ -635,6 +658,8 @@ export class CssAnimationProperty<T extends Style, U> {
|
||||
const propertyName = options.name;
|
||||
this.name = propertyName;
|
||||
|
||||
cssPropertyNames.push(options.cssName);
|
||||
|
||||
CssAnimationProperty.properties[propertyName] = this;
|
||||
if (options.cssName && options.cssName !== propertyName) {
|
||||
CssAnimationProperty.properties[options.cssName] = this;
|
||||
@ -1206,4 +1231,34 @@ export function makeParser<T>(isValid: (value: any) => boolean): (value: any) =>
|
||||
throw new Error("Invalid value: " + value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getSetProperties(view: ViewBase): [string, any][] {
|
||||
const result = [];
|
||||
|
||||
Object.getOwnPropertyNames(view).forEach(prop => {
|
||||
result.push([prop, view[prop]]);
|
||||
});
|
||||
|
||||
let symbols = Object.getOwnPropertySymbols(view);
|
||||
for (let symbol of symbols) {
|
||||
const property = symbolPropertyMap[symbol];
|
||||
if (!property) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = view[property.key];
|
||||
result.push([property.name, value]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getComputedCssValues(view: ViewBase): [string, any][] {
|
||||
const result = [];
|
||||
const style = view.style;
|
||||
for (var prop of cssPropertyNames) {
|
||||
result.push([prop, style[prop]]);
|
||||
}
|
||||
return result;
|
||||
}
|
@ -15,6 +15,7 @@ import { layout } from "../../../utils/utils";
|
||||
import { Color } from "../../../color";
|
||||
import { Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from "../../layouts/flexbox-layout";
|
||||
import { Length } from "../../styling/style-properties";
|
||||
import { DOMNode } from "../../../debugger/dom-node";
|
||||
|
||||
export { isIOS, isAndroid, layout, Color };
|
||||
|
||||
@ -57,6 +58,7 @@ export abstract class ViewBase extends Observable {
|
||||
col: number;
|
||||
rowSpan: number;
|
||||
colSpan: number;
|
||||
domNode: DOMNode;
|
||||
|
||||
order: Order;
|
||||
flexGrow: FlexGrow;
|
||||
@ -277,7 +279,7 @@ export abstract class ViewBase extends Observable {
|
||||
|
||||
/**
|
||||
* Performs the core logic of adding a child view to the native visual tree. Returns true if the view's native representation has been successfully added, false otherwise.
|
||||
*/
|
||||
*/
|
||||
_addViewToNativeVisualTree(view: ViewBase, atIndex?: number): boolean;
|
||||
_removeViewFromNativeVisualTree(view: ViewBase): void;
|
||||
_childIndexToNativeChildIndex(index?: number): number;
|
||||
@ -296,6 +298,12 @@ export abstract class ViewBase extends Observable {
|
||||
*/
|
||||
public deletePseudoClass(name: string): void;
|
||||
|
||||
/**
|
||||
* @unstable
|
||||
* Ensures a dom-node for this view.
|
||||
*/
|
||||
public ensureDomNode();
|
||||
|
||||
//@private
|
||||
public _styleScope: any;
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { Binding, BindingOptions, Observable, WrappedValue, PropertyChangeData,
|
||||
import { isIOS, isAndroid } from "../../../platform";
|
||||
import { layout } from "../../../utils/utils";
|
||||
import { Length, paddingTopProperty, paddingRightProperty, paddingBottomProperty, paddingLeftProperty } from "../../styling/style-properties";
|
||||
import { DOMNode } from "../../../debugger/dom-node";
|
||||
|
||||
// TODO: Remove this import!
|
||||
import * as types from "../../../utils/types";
|
||||
@ -142,6 +143,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
private _visualState: string;
|
||||
private _inlineStyleSelector: SelectorCore;
|
||||
private __nativeView: any;
|
||||
public domNode: DOMNode;
|
||||
|
||||
public bindingContext: any;
|
||||
public nativeView: any;
|
||||
@ -259,7 +261,13 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overriden so we don't raise `poropertyChange`
|
||||
public ensureDomNode() {
|
||||
if (!this.domNode) {
|
||||
this.domNode = new DOMNode(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Overridden so we don't raise `poropertyChange`
|
||||
// The property will raise its own event.
|
||||
public set(name: string, value: any) {
|
||||
this[name] = WrappedValue.unwrap(value);
|
||||
@ -558,6 +566,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
view.parent = this;
|
||||
this._addViewCore(view, atIndex);
|
||||
view._parentChanged(null);
|
||||
|
||||
if (this.domNode) {
|
||||
this.domNode.onChildAdded(view);
|
||||
}
|
||||
}
|
||||
|
||||
@profile
|
||||
@ -590,7 +602,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logic for removing a child view from this instance. Used by the framework to handle lifecycle events more centralized. Do not outside the UI Stack implementation.
|
||||
* Core logic for removing a child view from this instance. Used by the framework to handle lifecycle events more centralized. Do not use outside the UI Stack implementation.
|
||||
*/
|
||||
public _removeView(view: ViewBase) {
|
||||
if (traceEnabled()) {
|
||||
@ -601,6 +613,10 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
throw new Error("View not added to this instance. View: " + view + " CurrentParent: " + view.parent + " ExpectedParent: " + this);
|
||||
}
|
||||
|
||||
if (this.domNode) {
|
||||
this.domNode.onChildRemoved(view);
|
||||
}
|
||||
|
||||
this._removeViewCore(view);
|
||||
view.parent = undefined;
|
||||
view._parentChanged(this);
|
||||
@ -785,6 +801,12 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition
|
||||
// this._iosView = null;
|
||||
|
||||
this._context = null;
|
||||
|
||||
if (this.domNode) {
|
||||
this.domNode.dispose();
|
||||
this.domNode = undefined;
|
||||
}
|
||||
|
||||
traceNotifyEvent(this, "_onContextChanged");
|
||||
traceNotifyEvent(this, "_tearDownUI");
|
||||
}
|
||||
|
@ -34,6 +34,10 @@ function onLivesync(args: EventData): void {
|
||||
}
|
||||
application.on("livesync", onLivesync);
|
||||
|
||||
if (global && global.__inspector) {
|
||||
require("tns-core-modules/debugger/devtools-elements");
|
||||
}
|
||||
|
||||
let frameStack: Array<FrameBase> = [];
|
||||
|
||||
function buildEntryFromArgs(arg: any): NavigationEntry {
|
||||
|
4
tns-core-modules/ui/slider/slider.d.ts
vendored
4
tns-core-modules/ui/slider/slider.d.ts
vendored
@ -38,12 +38,12 @@ export class Slider extends View {
|
||||
/**
|
||||
* Represents the observable property backing the value property of each Slider instance.
|
||||
*/
|
||||
export const valueProperty: Property<Slider, number>;
|
||||
export const valueProperty: CoercibleProperty<Slider, number>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the minValue property of each Slider instance.
|
||||
*/
|
||||
export const minValueProperty: CoercibleProperty<Slider, number>;
|
||||
export const minValueProperty: Property<Slider, number>;
|
||||
|
||||
/**
|
||||
* Represents the observable property backing the maxValue property of each Slider instance.
|
||||
|
@ -129,7 +129,7 @@ export class TextBase extends TextBaseCommon {
|
||||
}
|
||||
}
|
||||
|
||||
// Overriden in TextField becasue setSingleLine(false) will remove methodTransformation.
|
||||
// Overridden in TextField because setSingleLine(false) will remove methodTransformation.
|
||||
// and we don't want to allow TextField to be multiline
|
||||
[whiteSpaceProperty.setNative](value: WhiteSpace) {
|
||||
const nativeView = this.nativeView;
|
||||
|
Reference in New Issue
Block a user