-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdevtools.js
More file actions
183 lines (166 loc) · 6.49 KB
/
devtools.js
File metadata and controls
183 lines (166 loc) · 6.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
* Add a sidepanel to the Element Inspector main panel, which shows generated
* cssSelector paths for elements protected by our LWC components' Synthetic Shadow DOM,
* and generated Java queryStrings for updating obsolete Selenium selectors.
*/
let generateShadowJSPath = function() {
/*
* Add classes to element tagname, for use in selectors.
*/
function getSelector(elem, long=true) {
if (!elem.tagName)
return elem.nodeName;
let name = elem.tagName.toLowerCase();
if (long) {
elem.classList.forEach((cls) => {
// Might need to add more to escape list.
// Might be simpler to just drop class names with werid characters.
name += '.' + cls.replace(/([{}])/g, '\\\\$1');
});
}
return name;
}
/*
* Is the element at the root of a components synthetic Shadow DOM tree?
*/
function isShadowRoot(node) {
return node instanceof ShadowRoot;
}
/*
* Find the path from the element up to the document.
* The path will include all LWC shadow boundaries.
*/
function findPath(node) {
if (!node) return;
const path = [];
path.push({
elem: node,
selector: getSelector(node)
});
while (node !== null && node !== document) {
let parent = node.parentNode;
if (isShadowRoot(parent)) {
parent = parent.host;
path.push({
elem: parent,
selector: getSelector(parent, false)
});
}
node = parent;
}
return path;
}
function jsPathElementsToQuerySelector(jsPath) {
console.log(jsPath);
let expanded = [];
jsPath.forEach(function(elem) {
if(elem[1] == 0) {
expanded.push(".querySelector('" + elem[0] + "')");
} else if(elem[1] == '*') {
expanded.push(".querySelectorAll('" + elem[0] + "')");
} else {
expanded.push(".querySelectorAll('" + elem[0] + "')[" + elem[1] + "]");
}
});
return "document" + expanded.join(".shadowRoot");
}
function executeJSPath(jsPath) {
let currentElement = document;
for (let i = 0; i < jsPath.length; i++) {
const [selector, index] = jsPath[i];
if (index === 0) {
currentElement = currentElement.querySelector(selector);
} else if (index === '*') {
// Return all elements for this selector - used for finding possibilities
const elements = currentElement.querySelectorAll(selector);
return Array.from(elements);
} else {
const elements = currentElement.querySelectorAll(selector);
currentElement = elements[index];
}
if (!currentElement) {
return null;
}
// Move to shadow root if not the last element
if (i < jsPath.length - 1 && currentElement.shadowRoot) {
currentElement = currentElement.shadowRoot;
}
}
return currentElement;
}
function jsPathElementsToShadowPath(jsPath) {
let selectors = [];
jsPath.forEach(function(elem) {
let selector = elem[0];
if(elem[1] > 0) {
selector += "[" + elem[1] + "]";
}
selectors.push(selector);
});
return selectors.join(" => ");
}
/**
* Turn potential path into JavaScript selector.
* Use safe DOM traversal instead of eval.
*/
function jsPathFromPath(path) {
if (!path || path.length === 0) return;
let jsPath = [];
for (let i = path.length - 1; i >= 0; i--) {
const node = path[i];
const selector = node.selector;
// Execute the path built so far to get current context
const testPath = jsPath.concat([[selector, '*']]);
const possibilities = executeJSPath(testPath);
if (!possibilities || (Array.isArray(possibilities) && possibilities.length === 0)) {
console.log("Error: Lost my way. No valid paths for selector: " + selector);
throw "Lost my way. No valid paths for selector: " + selector;
} else if (!Array.isArray(possibilities)) {
// Single element returned by querySelector
jsPath.push([selector, 0]);
} else if (possibilities.length === 1) {
// Single element in array from querySelectorAll
jsPath.push([selector, 0]);
} else {
// Multiple elements, find the correct index
let found = false;
for (let p = 0; p < possibilities.length; p++) {
if (possibilities[p] === node.elem) {
jsPath.push([selector, p]);
found = true;
break;
}
}
if (!found) {
console.log("Error: Could not find way to " + selector);
throw "Could not find way to " + selector;
}
}
}
return jsPath;
}
// Find the optimal selector for currently selected element.
const jsPath = jsPathFromPath(findPath($0));
const jsPathSelector = jsPathElementsToQuerySelector(jsPath);
let data = Object.create(null);
data.tagname = getSelector($0, true);
data.jsPath = jsPathSelector;
// data.java = 'String queryString = "return ' + jspath.replace(/"/g, '\\"') + '";';
data.java = 'String queryString = "return ' + jsPathSelector.replace(/"/g, "'") + '";';
data.shadowPath = jsPathElementsToShadowPath(jsPath);
return data;
};
/*
* Add a sidepanel to the Element Inspector main panel.
* Inspired by: https://chromium.googlesource.com/chromium/src/+/master/chrome/common/extensions/docs/examples/api/devtools/panels/chrome-query
*/
chrome.devtools.panels.elements.createSidebarPane(
"LWC ShadowPath",
function(sidebar) {
function updateElementProperties() {
sidebar.setExpression("(" + generateShadowJSPath.toString() + ")()");
}
updateElementProperties();
chrome.devtools.panels.elements.onSelectionChanged.addListener(updateElementProperties);
}
);