-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.js
382 lines (352 loc) · 15.7 KB
/
main.js
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
// CWSE-overlay-initial: not visible but which will, when display:block, block all user events not associated with this script
// CWSE-overlay-primary: visible to the user
var markup = $('<div id="CWSE-overlay-initial"></div>\
<div id="CWSE-overlay-primary"><div class="optimizer-container">\
<i id="optimizer-close">X</i>\
<div class="content"></div>\
</div></div>');
$("body").append(markup);
var path, selector, middleElement, lastElement;
var potentialSelectors = []; // all available selectors, grouped by level // excludes root and element clicked-on
var chosenSelectors = []; // selectors chosen from the potentialSelectors array
var numberOfDOMLevels = ''; // if there is more than 1 DOM level between the element clicked and the root, this is the total number (including the element clicked)
var root = $("#CWSE-overlay-primary"); // root element which all other elements are inside of
// load middleElement selector(s) into chosenSelectors array because they are default values
var loadME = function() {
// find elements within potentialSelectors with one of the last two indexes // add to chosenSelectors
var last = '';
var almostLast = '';
if (potentialSelectors[potentialSelectors.length-1] !== undefined) {
last = potentialSelectors[potentialSelectors.length-1].index;
almostLast = last - 1;
}
for(var i=0; i < potentialSelectors.length; i++){
if (potentialSelectors[i].index == last || potentialSelectors[i].index == almostLast) {
var newEntry = {};
newEntry.index = potentialSelectors[i].index;
newEntry.content = potentialSelectors[i].class;
chosenSelectors.push(newEntry);
}
}
}
var getCountText = function(path) {
// count number of times this element is called
var count = eval(path).length;
var countText = "<span class='once'>Occurs: once</span>";
if (count > 1) {
countText = "<span>Occurs: " + count + " times</span>";
}
return countText;
}
var copyPath = function(firstTime) {
var copyTextarea = document.querySelector('.js-copytextarea');
copyTextarea.select();
var successful = document.execCommand('copy'); // copy path to clipboard
var msg = successful ? 'Path copied to your clipboard.' : 'Unfortunately, path was not copied to your clipboard. Please try again.';
if (firstTime) {
root.find(".optimizer-container .secondary").after('<p class="msg">'+msg+'</p>');
} else {
root
.find(".optimizer-container .msg")
.fadeOut('100').fadeIn('100')
.siblings('.secondary')
.fadeOut('100').fadeIn('100');
}
copyTextarea.selectionStart = copyTextarea.selectionEnd = -1; // deselect path // un-highlights text in input field
root.find("p.secondary").focus(); // pulls cursor out of textarea
// remove text from textarea and move to div that overlays the textarea
root.find('.js-copytextarea').val("");
root.find('.textAreaDisplayed').text(path); //applies the first time only
}
// open overlay, build content, display content
var main = function(e) {
$(document).off("click", main); // allows user to immediately close the overlay // otherwise the main function would fire first
$("#CWSE-overlay-initial").hide(); // remove invisible overlay so that the "element" variable actualy gets the right element
// get mouse position, so can get element user clicked on located directly beneath the CWSE-overlay-initialal
var x = e.clientX;
var y = e.clientY;
var element = document.elementFromPoint(x, y);
// mark element for future use
var elementMarker = Date.now();
$(element).attr("data-selected", elementMarker);
var id = $(element).closest('[id]');
path = "";
// position overlay correctly // followed Pinterest example
$("body, html").css("overflow", "hidden");
// takes string of classes, returns string where each class is joined by a "." (if there are multiple classes)
var multipleClasses = function(classes) {
if (classes.indexOf(" ") > -1) {
classes = classes.trim().split(' ').join('.');
}
return classes;
}
// in path, in the .find() section, this is the second element from the right
var getMiddleElement = function(elementPath, includeTag) {
var middleElement = $(elementPath).parent().attr("class");
if (middleElement === undefined || $.trim(middleElement).length === 0) { // if no class or class is empty
middleElement = $(elementPath).parent().prop("tagName").toLowerCase();
} else { // if has classes
middleElement = "." + multipleClasses(middleElement);
if (includeTag == true) {
middleElement = $(elementPath).prop("tagName").toLowerCase() + middleElement;
}
}
return middleElement;
}
// get last element
var lastElementTag = $(element).prop("tagName").toLowerCase(); // start with the tag name
lastElement = lastElementTag;
// if class(es), add those to the tag name
if ($(element).attr("class") !== undefined && $.trim($(element).attr("class")).length > 0) {
var lastElementClass = $(element).attr("class");
lastElementClass = multipleClasses(lastElementClass);
lastElement = lastElementTag +'.'+ lastElementClass;
}
// contstruct path
var extraSelectors = ''; // contains markup for extra selectors
var disabled = "disabled";
var endPoint = ""; // end element in selector path // typically, this is the ID
root.find(".optimizer-container").removeClass('show');
if (id.length > 0) { // if ID exists, start with that
var idName = endPoint = id.attr("id"); // setting both variables equal to this value
selector = '$("#'+idName+'")';
var buildElementChart = function() {
root.find(".optimizer-container").addClass('show');
// set buttons to be clickable
disabled = "";
extraSelectors += '<div class="parent"><div class="uneditable root">#' + idName + '</div></div>';
var parentIndex = 0
// get all parents // include self
var parents = eval(selector + '.find("'+lastElement+'[data-selected=\''+ elementMarker + '\']")').parentsUntil("#" + idName).addBack();
numberOfDOMLevels = parents.length;
// from parents, construct list of all classes (or tags), per DOM level, in order
parents.each(function(index) {
parentIndex++;
var classes = $(this).attr("class");
// if no class(es), add tag to potentialSelectors array, along with its DOM level
if (classes === undefined || $(this).attr("class").trim().length === 0) {
var element = {};
element.index = parentIndex;
element.class = (this.nodeName).toLowerCase();
potentialSelectors.push(element);
} else { // if class(es), add class(es)
var classes = "." + (this.className).trim().replace(/ +/g, " .");
if (classes.indexOf(".", 1) > 0) { // if classes contains more than one "."
var elementSplit = classes.split("."); // array of all classes
// add each class to potentialSelectors array, along with its DOM level
$(elementSplit).each(function(index){
if (index !== 0) {
var element = {};
element.index = parentIndex;
element.class = "." + elementSplit[index].trim();
potentialSelectors.push(element);
}
})
} else { // if only one class, add to potentialSelectors array
var element = {};
element.index = parentIndex;
element.class = "." + (this.className).trim();
potentialSelectors.push(element);
}
}
})
// if last element has class(es), add tag into appropriate spot in array
var lastInParents = parents.slice(-1);
if (lastInParents[0] !== undefined) {
if (lastInParents[0].className !== undefined && lastInParents[0].className) {
var element = {};
element.index = parents.length;
element.class = lastInParents[0].nodeName.toLowerCase();
// get index of first object which has an internal index of parents.length (i.e. it's the last group or DOM level)
var indexOfFirst = $.map(potentialSelectors, function(obj, index) {
if(obj.index == parents.length) {
return index;
}
})
// insert tag into this spot
potentialSelectors.splice(indexOfFirst[0], 0, element);
}
}
var prevIndex = 0; // max index in the array
// build markup // populate extraSelectors with all classes/tags from potentialSelectors
var first = true; // first object in the last set of objects
$(potentialSelectors).each(function(index) {
var last = ""; // class needed only for the last group of elements
if (this.index == (parents.length) || this.index == (parents.length - 1)) {
last = "added";
}
// add "uneditable" class to tag // only occurs once
var primaryClass = 'editable';
if (this.index == (parents.length) && first) { // last set of objects in array, first object inside // marked uneditable
first = false;
primaryClass = 'uneditable';
}
if (prevIndex !== this.index) { // start new group
var closeDiv = '</div>';
if (index == 0) {
closeDiv = '';
}
extraSelectors += closeDiv + '<div class="parent"><div class="'+primaryClass + ' ' +last+'" data-index='+ this.index +'>'+this.class+'</div>';
} else { // continue with previous group
extraSelectors += '<div class="'+primaryClass + ' ' +last+'" data-index='+ this.index +'>'+this.class+'</div>';
}
prevIndex = this.index;
})
extraSelectors += '</div>';
}
if ($(element).is(id)) { // if ID is from current element
path = selector;
} else { // if ID is from a parent, grandparent, etc.
if ($(element).parent().is(id)) { // 2 elements
path = selector + '.find("'+lastElement+'")';
} else { // 3+ elements
middleElement = getMiddleElement(element);
path = selector + '.find("'+middleElement+ ' ' +lastElement+'")';
// 4+ elemenets // so, if there is more than one DOM level between the selected element and the root (or ID), make a list for user to select from
if (!$(element).parent().parent().is(id)) {
// buildElementChart();
}
}
buildElementChart();
}
} else {
// get middle element + class
// add "end" element here // set in each if
if ($(element).parent().length > 0) {
middleElement = getMiddleElement(element, true);
path = '$("' + middleElement + ' ' + lastElement +'")'; // 2 elements
// endPoint = '$("' + middleElement + ')';
// get "grandparent" element + class
if ($(element).parent().parent().length > 0) {
var firstElement = getMiddleElement($(element).parent(), true);
path = '$("' + firstElement + ' ' + middleElement + ' ' + lastElement +'")'; // 3 elements
// endPoint = '$("' + firstElement + ')';
}
} else {
path = '$("' + lastElement + '")'; // 1 element
// endPoint = '$("' + lastElement + ')';
}
// buildElementChart();
}
var countText = getCountText(path);
// build/display message
var message = '<section class="viewPath"><textarea class="js-copytextarea">'+path+'</textarea>'; // hidden
message += '<div class="textAreaDisplayed">path will go here path will go here</div>'; // displayed
message += '<p class="secondary" tabindex="0">'+countText+'</p></section>';
message += '<section class="editPath"><h1>Edit Path</h1>';
message += '<button class="revert" '+disabled+' title="This button is available when the Edit button is available.">Clear Changes</button>';
message += '<div class="extraSelectors">'+extraSelectors+'</div></section>';
root
.show()
.find(".content").html(message);
$('.optimizer-container').addClass('stat-show');
var lockUrl = chrome.extension.getURL('images/lock.png'); // image used in first and last displayed selectors
$(".uneditable").css("background-image", "url("+lockUrl+")");
copyPath(true);
// add default selectors to "potentialSelectors" array, because they are default
loadME();
}
var closeOverlay = function() {
root.hide();
// style page now that overlay is gone
$("body, html").css("overflow", "auto");
// empty potentialSelectors and chosenSelectors arrays
potentialSelectors.splice(0,potentialSelectors.length);
chosenSelectors.splice(0,chosenSelectors.length);
}
// user action // triggers main function
$(document)
.on("keydown", function(e) {
if (e.altKey) { // shift key pressed
$("#CWSE-overlay-initial").show(); // overlay page so that click does not trigger any event other than the one below
$(document).on("click", main); // run main function
setTimeout(function() { // later...
$("#CWSE-overlay-initial").hide();
$(document).off("click", main); // unattach this function so user can click without triggering the overlay
}, 1000) // gives the user 1 second after alt to click
}
})
// user closes the lightbox
.on("click", "#CWSE-overlay-primary", function(event) {
var target = $(event.target);
if (target.is($("#CWSE-overlay-primary")) || target.is($("#optimizer-close"))) {
closeOverlay();
}
})
// user closes lightbox by hitting escape key
.keyup(function(e) {
if (e.keyCode == 27) { // escape key maps to keycode `27`
closeOverlay();
}
})
// revert path to original path
.on("click", ".revert", function() {
$(".js-copytextarea").val(path);
// empty chosenSelectors array
var arrayCount = chosenSelectors.length;
chosenSelectors.splice(0,arrayCount);
// load default selectors into chosenSelectors because they're default
loadME();
// revert visible selectors to default classes
root
.find("div.editable").removeClass('added')
.end()
.find("div.parent").eq(numberOfDOMLevels).children().addClass('added') // last DOM level
root.find("div.parent").eq(numberOfDOMLevels-1).children().addClass('added'); // second to last DOM level
// update count and copy path
var countText = getCountText(path);
root.find(".secondary").html(countText);
copyPath();
})
.on("click", ".editable", function() {
// if this element has not been added, add it to the array
var thisIndex = $(this).attr("data-index");
var thisContent = $(this).text();
var chosenSelectorsOutput = "";
if (!$(this).hasClass('added')) {
$(this).addClass('added');
var group = {};
group.index = thisIndex;
group.content = $(this).text();
chosenSelectors.push(group);
} else {
$(this).removeClass('added');
// get index of item I need to remove
var indexInArray = $.map(chosenSelectors, function(obj, index) {
if(obj.content == thisContent) {
return index;
}
})
// remove from array
chosenSelectors.splice(indexInArray,1);
}
// sort elements in correct order // this is based off their data-index
chosenSelectors.sort(function(a,b) {
return parseFloat(a.index) - parseFloat(b.index);
});
// stringify, i.e. construct the string which will display as part of the path
var prevIndex = "";
$(chosenSelectors).each(function(index) {
if (prevIndex == this.index) { // same group, no space
chosenSelectorsOutput += this.content;
} else { // new group
// I honestly don't know why the below works, but 1) it does and 2) I knew at one time
// if it's the highest data-index, i.e. it's visually furthest on the left, remove the space
var maxIndex = chosenSelectors[0].index; // lowest index in the array, closest to root element
if (this.index == maxIndex) {
chosenSelectorsOutput += this.content;
} else {
chosenSelectorsOutput += " " + this.content;
}
}
prevIndex = this.index;
})
var newPath = selector + '.find("' +chosenSelectorsOutput + '")';
// display new path
$(".js-copytextarea").val(newPath); // update hidden element // used in copyPath() function
// update count and copy path
var countText = getCountText(newPath);
root.find(".secondary").html(countText);
copyPath();
$(".textAreaDisplayed").text(newPath); // update visible element for user to see
})