UI - Visual Selector now supports Shift+Click for multiple selections!

pull/2475/head^2
dgtlmoon 6 months ago committed by GitHub
parent e09ee7da97
commit 1af342ef64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,267 +2,239 @@
// All rights reserved. // All rights reserved.
// yes - this is really a hack, if you are a front-ender and want to help, please get in touch! // yes - this is really a hack, if you are a front-ender and want to help, please get in touch!
$(document).ready(function () { let runInClearMode = false;
var current_selected_i; $(document).ready(() => {
var state_clicked = false; let currentSelections = [];
let currentSelection = null;
var c; let appendToList = false;
let c, xctx, ctx;
// greyed out fill context let xScale = 1, yScale = 1;
var xctx; let selectorImage, selectorImageRect, selectorData;
// redline highlight context
var ctx;
// Global jQuery selectors with "Elem" appended
var current_default_xpath = []; const $selectorCanvasElem = $('#selector-canvas');
var x_scale = 1; const $includeFiltersElem = $("#include_filters");
var y_scale = 1; const $selectorBackgroundElem = $("img#selector-background");
var selector_image; const $selectorCurrentXpathElem = $("#selector-current-xpath span");
var selector_image_rect; const $fetchingUpdateNoticeElem = $('.fetching-update-notice');
var selector_data; const $selectorWrapperElem = $("#selector-wrapper");
$('#visualselector-tab').click(function () { // Color constants
$("img#selector-background").off('load'); const FILL_STYLE_HIGHLIGHT = 'rgba(205,0,0,0.35)';
state_clicked = false; const FILL_STYLE_GREYED_OUT = 'rgba(205,205,205,0.95)';
current_selected_i = false; const STROKE_STYLE_HIGHLIGHT = 'rgba(255,0,0, 0.9)';
bootstrap_visualselector(); const FILL_STYLE_REDLINE = 'rgba(255,0,0, 0.1)';
const STROKE_STYLE_REDLINE = 'rgba(225,0,0,0.9)';
$('#visualselector-tab').click(() => {
$selectorBackgroundElem.off('load');
currentSelections = [];
bootstrapVisualSelector();
}); });
function clear_reset() { function clearReset() {
state_clicked = false;
ctx.clearRect(0, 0, c.width, c.height); ctx.clearRect(0, 0, c.width, c.height);
if($("#include_filters").val().length) { if ($includeFiltersElem.val().length) {
alert("Existing filters under the 'Filters & Triggers' tab were cleared."); alert("Existing filters under the 'Filters & Triggers' tab were cleared.");
} }
$("#include_filters").val(''); $includeFiltersElem.val('');
currentSelections = [];
// Means we ignore the xpaths from the scraper marked as sel.highlight_as_custom_filter (it matched a previous selector)
runInClearMode = true;
highlightCurrentSelected();
}
function splitToList(v) {
return v.split('\n').map(line => line.trim()).filter(line => line.length > 0);
}
function sortScrapedElementsBySize() {
// Sort the currentSelections array by area (width * height) in descending order
selectorData['size_pos'].sort((a, b) => {
const areaA = a.width * a.height;
const areaB = b.width * b.height;
return areaB - areaA;
});
} }
$(document).on('keydown', function (event) { $(document).on('keydown keyup', (event) => {
if ($("img#selector-background").is(":visible")) { if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') {
if (event.key == "Escape") { appendToList = event.type === 'keydown';
clear_reset(); }
if (event.type === 'keydown') {
if ($selectorBackgroundElem.is(":visible") && event.key === "Escape") {
clearReset();
} }
} }
}); });
// Handle clearing button/link $('#clear-selector').on('click', () => {
$('#clear-selector').on('click', function (event) { clearReset();
clear_reset(); });
// So if they start switching between visualSelector and manual filters, stop it from rendering old filters
$('li.tab a').on('click', () => {
runInClearMode = true;
}); });
// For when the page loads if (!window.location.hash || window.location.hash !== '#visualselector') {
if (!window.location.hash || window.location.hash != '#visualselector') { $selectorBackgroundElem.attr('src', '');
$("img#selector-background").attr('src', '');
return; return;
} }
bootstrapVisualSelector();
bootstrap_visualselector(); function bootstrapVisualSelector() {
$selectorBackgroundElem
.on("error", () => {
function bootstrap_visualselector() { $fetchingUpdateNoticeElem.html("<strong>Ooops!</strong> The VisualSelector tool needs at least one fetched page, please unpause the watch and/or wait for the watch to complete fetching and then reload this page.")
if (1) { .css('color', '#bb0000');
// bootstrap it, this will trigger everything else $('#selector-current-xpath, #clear-selector').hide();
$("img#selector-background").on("error", function () { })
$('.fetching-update-notice').html("<strong>Ooops!</strong> The VisualSelector tool needs atleast one fetched page, please unpause the watch and/or wait for the watch to complete fetching and then reload this page."); .on('load', () => {
$('.fetching-update-notice').css('color','#bb0000');
$('#selector-current-xpath').hide();
$('#clear-selector').hide();
}).bind('load', function () {
console.log("Loaded background..."); console.log("Loaded background...");
c = document.getElementById("selector-canvas"); c = document.getElementById("selector-canvas");
// greyed out fill context
xctx = c.getContext("2d"); xctx = c.getContext("2d");
// redline highlight context
ctx = c.getContext("2d"); ctx = c.getContext("2d");
if ($("#include_filters").val().trim().length) { fetchData();
current_default_xpath = $("#include_filters").val().split(/\r?\n/g); $selectorCanvasElem.off("mousemove mousedown");
} else { })
current_default_xpath = []; .attr("src", screenshot_url);
}
fetch_data(); let s = `${$selectorBackgroundElem.attr('src')}?${new Date().getTime()}`;
$('#selector-canvas').off("mousemove mousedown"); $selectorBackgroundElem.attr('src', s);
// screenshot_url defined in the edit.html template
}).attr("src", screenshot_url);
}
// Tell visualSelector that the image should update
var s = $("img#selector-background").attr('src') + "?" + new Date().getTime();
$("img#selector-background").attr('src', s)
} }
// This is fired once the img src is loaded in bootstrap_visualselector() function fetchData() {
function fetch_data() { $fetchingUpdateNoticeElem.html("Fetching element data..");
// Image is ready
$('.fetching-update-notice').html("Fetching element data..");
$.ajax({ $.ajax({
url: watch_visual_selector_data_url, url: watch_visual_selector_data_url,
context: document.body context: document.body
}).done(function (data) { }).done((data) => {
$('.fetching-update-notice').html("Rendering.."); $fetchingUpdateNoticeElem.html("Rendering..");
selector_data = data; selectorData = data;
sortScrapedElementsBySize();
console.log("Reported browser width from backend: " + data['browser_width']); console.log("Reported browser width from backend: " + data['browser_width']);
state_clicked = false; setScale();
set_scale(); reflowSelector();
reflow_selector(); $fetchingUpdateNoticeElem.fadeOut();
$('.fetching-update-notice').fadeOut();
}); });
} }
function updateFiltersText() {
// Assuming currentSelections is already defined and contains the selections
let uniqueSelections = new Set(currentSelections.map(sel => (sel[0] === '/' ? `xpath:${sel.xpath}` : sel.xpath)));
function set_scale() { // Convert the Set back to an array and join with newline characters
let textboxFilterText = Array.from(uniqueSelections).join("\n");
// some things to check if the scaling doesnt work $includeFiltersElem.val(textboxFilterText);
// - that the widths/sizes really are about the actual screen size cat elements.json |grep -o width......|sort|uniq }
$("#selector-wrapper").show();
selector_image = $("img#selector-background")[0];
selector_image_rect = selector_image.getBoundingClientRect();
// Make the overlayed canvas the same size as the image function setScale() {
$('#selector-canvas').attr('height', selector_image_rect.height).attr('width', selector_image_rect.width); $selectorWrapperElem.show();
$('#selector-wrapper').attr('width', selector_image_rect.width); selectorImage = $selectorBackgroundElem[0];
selectorImageRect = selectorImage.getBoundingClientRect();
x_scale = selector_image_rect.width / selector_image.naturalWidth; $selectorCanvasElem.attr({
y_scale = selector_image_rect.height / selector_image.naturalHeight; 'height': selectorImageRect.height,
'width': selectorImageRect.width
});
$selectorWrapperElem.attr('width', selectorImageRect.width);
$('#visual-selector-heading').css('max-width', selectorImageRect.width + "px")
ctx.strokeStyle = 'rgba(255,0,0, 0.9)'; xScale = selectorImageRect.width / selectorImage.naturalWidth;
ctx.fillStyle = 'rgba(255,0,0, 0.1)'; yScale = selectorImageRect.height / selectorImage.naturalHeight;
ctx.strokeStyle = STROKE_STYLE_HIGHLIGHT;
ctx.fillStyle = FILL_STYLE_REDLINE;
ctx.lineWidth = 3; ctx.lineWidth = 3;
console.log("scaling set x: " + x_scale + " by y:" + y_scale); console.log("Scaling set x: " + xScale + " by y:" + yScale);
$("#selector-current-xpath").css('max-width', selector_image_rect.width); $("#selector-current-xpath").css('max-width', selectorImageRect.width);
} }
function reflow_selector() { function reflowSelector() {
$(window).resize(function () { $(window).resize(() => {
set_scale(); setScale();
highlight_current_selected_i(); highlightCurrentSelected();
}); });
var selector_currnt_xpath_text = $("#selector-current-xpath span"); setScale();
set_scale(); console.log(selectorData['size_pos'].length + " selectors found");
console.log(selector_data['size_pos'].length + " selectors found");
// highlight the default one if we can find it in the xPath list
// or the xpath matches the default one
found = false;
if (current_default_xpath.length) {
// Find the first one that matches
// @todo In the future paint all that match
for (const c of current_default_xpath) {
for (var i = selector_data['size_pos'].length; i !== 0; i--) {
if (selector_data['size_pos'][i - 1].xpath.trim() === c.trim()) {
console.log("highlighting " + c);
current_selected_i = i - 1;
highlight_current_selected_i();
found = true;
break;
}
}
if (found) {
break;
}
}
if (!found) {
alert("Unfortunately your existing CSS/xPath Filter was no longer found!");
}
highlight_matching_filters();
}
let existingFilters = splitToList($includeFiltersElem.val());
$('#selector-canvas').bind('mousemove', function (e) { selectorData['size_pos'].forEach(sel => {
if (state_clicked) { if ((!runInClearMode && sel.highlight_as_custom_filter) || existingFilters.includes(sel.xpath)) {
return; console.log("highlighting " + c);
currentSelections.push(sel);
} }
ctx.clearRect(0, 0, c.width, c.height); });
current_selected_i = null;
highlightCurrentSelected();
updateFiltersText();
// Add in offset $selectorCanvasElem.bind('mousemove', handleMouseMove.debounce(5));
if ((typeof e.offsetX === "undefined" || typeof e.offsetY === "undefined") || (e.offsetX === 0 && e.offsetY === 0)) { $selectorCanvasElem.bind('mousedown', handleMouseDown.debounce(5));
var targetOffset = $(e.target).offset(); $selectorCanvasElem.bind('mouseleave', highlightCurrentSelected.debounce(5));
function handleMouseMove(e) {
if (!e.offsetX && !e.offsetY) {
const targetOffset = $(e.target).offset();
e.offsetX = e.pageX - targetOffset.left; e.offsetX = e.pageX - targetOffset.left;
e.offsetY = e.pageY - targetOffset.top; e.offsetY = e.pageY - targetOffset.top;
} }
// Reverse order - the most specific one should be deeper/"laster" ctx.fillStyle = FILL_STYLE_HIGHLIGHT;
// Basically, find the most 'deepest'
var found = 0; selectorData['size_pos'].forEach(sel => {
ctx.fillStyle = 'rgba(205,0,0,0.35)'; if (e.offsetY > sel.top * yScale && e.offsetY < sel.top * yScale + sel.height * yScale &&
// Will be sorted by smallest width*height first e.offsetX > sel.left * yScale && e.offsetX < sel.left * yScale + sel.width * yScale) {
for (var i = 0; i <= selector_data['size_pos'].length; i++) { setCurrentSelectedText(sel.xpath);
// draw all of them? let them choose somehow? drawHighlight(sel);
var sel = selector_data['size_pos'][i]; currentSelections.push(sel);
// If we are in a bounding-box currentSelection = sel;
if (e.offsetY > sel.top * y_scale && e.offsetY < sel.top * y_scale + sel.height * y_scale highlightCurrentSelected();
&& currentSelections.pop();
e.offsetX > sel.left * y_scale && e.offsetX < sel.left * y_scale + sel.width * y_scale
) {
// FOUND ONE
set_current_selected_text(sel.xpath);
ctx.strokeRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
ctx.fillRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
// no need to keep digging
// @todo or, O to go out/up, I to go in
// or double click to go up/out the selector?
current_selected_i = i;
found += 1;
break;
} }
} })
}
}.debounce(5));
function set_current_selected_text(s) { function setCurrentSelectedText(s) {
selector_currnt_xpath_text[0].innerHTML = s; $selectorCurrentXpathElem[0].innerHTML = s;
} }
function highlight_current_selected_i() { function drawHighlight(sel) {
if (state_clicked) { ctx.strokeRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale);
state_clicked = false; ctx.fillRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale);
xctx.clearRect(0, 0, c.width, c.height); }
return;
}
var sel = selector_data['size_pos'][current_selected_i];
if (sel[0] == '/') {
// @todo - not sure just checking / is right
$("#include_filters").val('xpath:' + sel.xpath);
} else {
$("#include_filters").val(sel.xpath);
}
xctx.fillStyle = 'rgba(205,205,205,0.95)';
xctx.strokeStyle = 'rgba(225,0,0,0.9)';
xctx.lineWidth = 3;
xctx.fillRect(0, 0, c.width, c.height);
// Clear out what only should be seen (make a clear/clean spot)
xctx.clearRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
xctx.strokeRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
state_clicked = true;
set_current_selected_text(sel.xpath);
function handleMouseDown() {
// If we are in 'appendToList' mode, grow the list, if not, just 1
currentSelections = appendToList ? [...currentSelections, currentSelection] : [currentSelection];
highlightCurrentSelected();
updateFiltersText();
} }
}
function highlight_matching_filters() { function highlightCurrentSelected() {
selector_data['size_pos'].forEach(sel => { xctx.fillStyle = FILL_STYLE_GREYED_OUT;
if (sel.highlight_as_custom_filter) { xctx.strokeStyle = STROKE_STYLE_REDLINE;
xctx.fillStyle = 'rgba(205,205,205,0.95)'; xctx.lineWidth = 3;
xctx.strokeStyle = 'rgba(225,0,0,0.95)'; xctx.clearRect(0, 0, c.width, c.height);
xctx.lineWidth = 1;
xctx.clearRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
xctx.strokeRect(sel.left * x_scale, sel.top * y_scale, sel.width * x_scale, sel.height * y_scale);
}
});
}
$('#selector-canvas').bind('mousedown', function (e) { currentSelections.forEach(sel => {
highlight_current_selected_i(); //xctx.clearRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale);
xctx.strokeRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale);
}); });
} }
}); });

@ -432,9 +432,8 @@ Unavailable") }}
<fieldset> <fieldset>
<div class="pure-control-group"> <div class="pure-control-group">
{% if visualselector_enabled %} {% if visualselector_enabled %}
<span class="pure-form-message-inline"> <span class="pure-form-message-inline" id="visual-selector-heading">
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection &dash; after the <i>Browser Steps</i> has completed.<br> The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection. It automatically fills-in the filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab. Use <strong>Shift+Click</strong> to select multiple items.
This tool is a helper to manage filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab.
</span> </span>
<div id="selector-header"> <div id="selector-header">

Loading…
Cancel
Save