diff --git a/changedetectionio/static/js/visual-selector.js b/changedetectionio/static/js/visual-selector.js index 24d1ee18..9cde7350 100644 --- a/changedetectionio/static/js/visual-selector.js +++ b/changedetectionio/static/js/visual-selector.js @@ -2,267 +2,239 @@ // All rights reserved. // yes - this is really a hack, if you are a front-ender and want to help, please get in touch! -$(document).ready(function () { - - var current_selected_i; - var state_clicked = false; - - var c; - - // greyed out fill context - var xctx; - // redline highlight context - var ctx; - - var current_default_xpath = []; - var x_scale = 1; - var y_scale = 1; - var selector_image; - var selector_image_rect; - var selector_data; - - $('#visualselector-tab').click(function () { - $("img#selector-background").off('load'); - state_clicked = false; - current_selected_i = false; - bootstrap_visualselector(); +let runInClearMode = false; + +$(document).ready(() => { + let currentSelections = []; + let currentSelection = null; + let appendToList = false; + let c, xctx, ctx; + let xScale = 1, yScale = 1; + let selectorImage, selectorImageRect, selectorData; + + + // Global jQuery selectors with "Elem" appended + const $selectorCanvasElem = $('#selector-canvas'); + const $includeFiltersElem = $("#include_filters"); + const $selectorBackgroundElem = $("img#selector-background"); + const $selectorCurrentXpathElem = $("#selector-current-xpath span"); + const $fetchingUpdateNoticeElem = $('.fetching-update-notice'); + const $selectorWrapperElem = $("#selector-wrapper"); + + // Color constants + const FILL_STYLE_HIGHLIGHT = 'rgba(205,0,0,0.35)'; + const FILL_STYLE_GREYED_OUT = 'rgba(205,205,205,0.95)'; + const STROKE_STYLE_HIGHLIGHT = 'rgba(255,0,0, 0.9)'; + 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() { - state_clicked = false; + function clearReset() { 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."); } - $("#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) { - if ($("img#selector-background").is(":visible")) { - if (event.key == "Escape") { - clear_reset(); + $(document).on('keydown keyup', (event) => { + if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') { + appendToList = event.type === 'keydown'; + } + + if (event.type === 'keydown') { + if ($selectorBackgroundElem.is(":visible") && event.key === "Escape") { + clearReset(); } } }); - // Handle clearing button/link - $('#clear-selector').on('click', function (event) { - clear_reset(); + $('#clear-selector').on('click', () => { + clearReset(); + }); + // 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') { - $("img#selector-background").attr('src', ''); + if (!window.location.hash || window.location.hash !== '#visualselector') { + $selectorBackgroundElem.attr('src', ''); return; } + bootstrapVisualSelector(); - bootstrap_visualselector(); - - - function bootstrap_visualselector() { - if (1) { - // bootstrap it, this will trigger everything else - $("img#selector-background").on("error", function () { - $('.fetching-update-notice').html("Ooops! 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."); - $('.fetching-update-notice').css('color','#bb0000'); - $('#selector-current-xpath').hide(); - $('#clear-selector').hide(); - }).bind('load', function () { + function bootstrapVisualSelector() { + $selectorBackgroundElem + .on("error", () => { + $fetchingUpdateNoticeElem.html("Ooops! 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.") + .css('color', '#bb0000'); + $('#selector-current-xpath, #clear-selector').hide(); + }) + .on('load', () => { console.log("Loaded background..."); c = document.getElementById("selector-canvas"); - // greyed out fill context xctx = c.getContext("2d"); - // redline highlight context ctx = c.getContext("2d"); - if ($("#include_filters").val().trim().length) { - current_default_xpath = $("#include_filters").val().split(/\r?\n/g); - } else { - current_default_xpath = []; - } - fetch_data(); - $('#selector-canvas').off("mousemove mousedown"); - // 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) + fetchData(); + $selectorCanvasElem.off("mousemove mousedown"); + }) + .attr("src", screenshot_url); + + let s = `${$selectorBackgroundElem.attr('src')}?${new Date().getTime()}`; + $selectorBackgroundElem.attr('src', s); } - // This is fired once the img src is loaded in bootstrap_visualselector() - function fetch_data() { - // Image is ready - $('.fetching-update-notice').html("Fetching element data.."); + function fetchData() { + $fetchingUpdateNoticeElem.html("Fetching element data.."); $.ajax({ url: watch_visual_selector_data_url, context: document.body - }).done(function (data) { - $('.fetching-update-notice').html("Rendering.."); - selector_data = data; + }).done((data) => { + $fetchingUpdateNoticeElem.html("Rendering.."); + selectorData = data; + sortScrapedElementsBySize(); console.log("Reported browser width from backend: " + data['browser_width']); - state_clicked = false; - set_scale(); - reflow_selector(); - $('.fetching-update-notice').fadeOut(); + setScale(); + reflowSelector(); + $fetchingUpdateNoticeElem.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 - // - 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(); + $includeFiltersElem.val(textboxFilterText); + } - // Make the overlayed canvas the same size as the image - $('#selector-canvas').attr('height', selector_image_rect.height).attr('width', selector_image_rect.width); - $('#selector-wrapper').attr('width', selector_image_rect.width); + function setScale() { + $selectorWrapperElem.show(); + selectorImage = $selectorBackgroundElem[0]; + selectorImageRect = selectorImage.getBoundingClientRect(); - x_scale = selector_image_rect.width / selector_image.naturalWidth; - y_scale = selector_image_rect.height / selector_image.naturalHeight; + $selectorCanvasElem.attr({ + '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)'; - ctx.fillStyle = 'rgba(255,0,0, 0.1)'; + xScale = selectorImageRect.width / selectorImage.naturalWidth; + yScale = selectorImageRect.height / selectorImage.naturalHeight; + + ctx.strokeStyle = STROKE_STYLE_HIGHLIGHT; + ctx.fillStyle = FILL_STYLE_REDLINE; ctx.lineWidth = 3; - console.log("scaling set x: " + x_scale + " by y:" + y_scale); - $("#selector-current-xpath").css('max-width', selector_image_rect.width); + console.log("Scaling set x: " + xScale + " by y:" + yScale); + $("#selector-current-xpath").css('max-width', selectorImageRect.width); } - function reflow_selector() { - $(window).resize(function () { - set_scale(); - highlight_current_selected_i(); + function reflowSelector() { + $(window).resize(() => { + setScale(); + highlightCurrentSelected(); }); - - var selector_currnt_xpath_text = $("#selector-current-xpath span"); - - set_scale(); - - 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(); - } + setScale(); + + console.log(selectorData['size_pos'].length + " selectors found"); - $('#selector-canvas').bind('mousemove', function (e) { - if (state_clicked) { - return; + let existingFilters = splitToList($includeFiltersElem.val()); + + selectorData['size_pos'].forEach(sel => { + if ((!runInClearMode && sel.highlight_as_custom_filter) || existingFilters.includes(sel.xpath)) { + console.log("highlighting " + c); + currentSelections.push(sel); } - ctx.clearRect(0, 0, c.width, c.height); - current_selected_i = null; + }); + + + highlightCurrentSelected(); + updateFiltersText(); + + $selectorCanvasElem.bind('mousemove', handleMouseMove.debounce(5)); + $selectorCanvasElem.bind('mousedown', handleMouseDown.debounce(5)); + $selectorCanvasElem.bind('mouseleave', highlightCurrentSelected.debounce(5)); - // Add in offset - if ((typeof e.offsetX === "undefined" || typeof e.offsetY === "undefined") || (e.offsetX === 0 && e.offsetY === 0)) { - var targetOffset = $(e.target).offset(); + function handleMouseMove(e) { + if (!e.offsetX && !e.offsetY) { + const targetOffset = $(e.target).offset(); e.offsetX = e.pageX - targetOffset.left; e.offsetY = e.pageY - targetOffset.top; } - // Reverse order - the most specific one should be deeper/"laster" - // Basically, find the most 'deepest' - var found = 0; - ctx.fillStyle = 'rgba(205,0,0,0.35)'; - // Will be sorted by smallest width*height first - for (var i = 0; i <= selector_data['size_pos'].length; i++) { - // draw all of them? let them choose somehow? - var sel = selector_data['size_pos'][i]; - // If we are in a bounding-box - if (e.offsetY > sel.top * y_scale && e.offsetY < sel.top * y_scale + sel.height * y_scale - && - 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; + ctx.fillStyle = FILL_STYLE_HIGHLIGHT; + + selectorData['size_pos'].forEach(sel => { + if (e.offsetY > sel.top * yScale && e.offsetY < sel.top * yScale + sel.height * yScale && + e.offsetX > sel.left * yScale && e.offsetX < sel.left * yScale + sel.width * yScale) { + setCurrentSelectedText(sel.xpath); + drawHighlight(sel); + currentSelections.push(sel); + currentSelection = sel; + highlightCurrentSelected(); + currentSelections.pop(); } - } + }) + } - }.debounce(5)); - function set_current_selected_text(s) { - selector_currnt_xpath_text[0].innerHTML = s; + function setCurrentSelectedText(s) { + $selectorCurrentXpathElem[0].innerHTML = s; } - function highlight_current_selected_i() { - if (state_clicked) { - state_clicked = false; - 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 drawHighlight(sel) { + ctx.strokeRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale); + ctx.fillRect(sel.left * xScale, sel.top * yScale, sel.width * xScale, sel.height * yScale); + } + 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() { - selector_data['size_pos'].forEach(sel => { - if (sel.highlight_as_custom_filter) { - xctx.fillStyle = 'rgba(205,205,205,0.95)'; - xctx.strokeStyle = 'rgba(225,0,0,0.95)'; - 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); - } - }); - } + function highlightCurrentSelected() { + xctx.fillStyle = FILL_STYLE_GREYED_OUT; + xctx.strokeStyle = STROKE_STYLE_REDLINE; + xctx.lineWidth = 3; + xctx.clearRect(0, 0, c.width, c.height); - $('#selector-canvas').bind('mousedown', function (e) { - highlight_current_selected_i(); + currentSelections.forEach(sel => { + //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); }); } - -}); \ No newline at end of file +}); diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index 15cb3fd5..69fed0aa 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -432,9 +432,8 @@ Unavailable") }}