From 4bb153ef33e66a106ca19252b630786727a840f0 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:28:00 +1030 Subject: [PATCH 1/6] Fix editor reloads, Safari hotspots and raw-edit cancellation --- CHANGELOG.md | 2 + js/editor.js | 73 ++++++++++++++++++- locales/po/cacti.pot | 14 ++-- tests/Browser/ConfigCancelTest.cjs | 30 ++++++++ tests/Browser/EditorContextMenuTest.cjs | 24 +++++- tests/Browser/EditorHotspotTest.cjs | 16 ++++ tests/Browser/EditorReloadTest.cjs | 29 ++++++++ tests/Support/EditorNotesEngineRegression.php | 2 +- weathermap-cacti-plugin-editor.php | 6 +- 9 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 tests/Browser/ConfigCancelTest.cjs create mode 100644 tests/Browser/EditorHotspotTest.cjs create mode 100644 tests/Browser/EditorReloadTest.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index eae61f01..35414332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! --- develop --- +* issue: Refresh saved editor snapshots reliably, restore Safari node/note selection and return to properties when cancelling raw configuration edits + * Add traffic colour presets and optional high-load link pulsing * Fix Safari rendering of high-traffic link glows using native SVG filters diff --git a/js/editor.js b/js/editor.js index 8a49f886..70415dd6 100644 --- a/js/editor.js +++ b/js/editor.js @@ -544,7 +544,44 @@ function wmEditorReturnUrl(target) { return 'weathermap-cacti-plugin.php' + (match ? '?action=viewmap&id=' + encodeURIComponent(match[1]) : ''); } +// Some WebKit layouts route image-map clicks to the image after scrolling. +// Resolve those clicks against the same ordered, logical-coordinate regions. +function wmEditorAreaAt(image, event) { + var rect = image.getBoundingClientRect(); + if (!rect.width || !rect.height) return null; + var x = (event.clientX - rect.left) * Number(image.getAttribute('data-width')) / rect.width; + var y = (event.clientY - rect.top) * Number(image.getAttribute('data-height')) / rect.height; + var map = document.getElementById((image.getAttribute('usemap') || '').split('#').pop()); + if (!map) return null; + return Array.from(map.querySelectorAll('area')).find(function(area) { + var c = area.coords.split(',').map(Number), inside = false; + if (!c.length || !c.every(Number.isFinite)) return false; + if (area.shape.toLowerCase() === 'rect' && c.length === 4) { + return x >= c[0] && x <= c[2] && y >= c[1] && y <= c[3]; + } + if (area.shape.toLowerCase() === 'circle' && c.length === 3) { + return Math.pow(x - c[0], 2) + Math.pow(y - c[1], 2) <= c[2] * c[2]; + } + if (area.shape.toLowerCase() === 'poly' && c.length >= 6 && c.length % 2 === 0) { + for (var i = 0, j = c.length - 2; i < c.length; j = i, i += 2) { + if ((c[i + 1] > y) !== (c[j + 1] > y) && x < (c[j] - c[i]) * (y - c[i + 1]) / (c[j + 1] - c[i + 1]) + c[i]) inside = !inside; + } + } + return inside; + }) || null; +} + function attach_click_events() { + $('#existingdata').off('.wmhit').on('click.wmhit', function(event) { + if (event.target !== this) return; + var area = wmEditorAreaAt(this, event); + if (area) { + event.preventDefault(); + $(area).triggerHandler('click'); + } + }).on('mousemove.wmhit', function(event) { + if (!$(this).hasClass('wm-placement-mode')) this.style.cursor = wmEditorAreaAt(this, event) ? 'pointer' : ''; + }); $('#xycapture').off('click.wmnote').on('click.wmnote', function(event) { if ($('#action').val() === 'place_note' || ($('#action').val() === 'move_node' && typeof WMNotes !== 'undefined' && WMNotes[$('#param').val()])) coord_update(event); }); @@ -627,7 +664,11 @@ function attach_click_events() { } // used by the cancel button on each of the properties dialogs -function cancel_op() { +function cancel_op(event) { + if (event && event.currentTarget && event.currentTarget.id === 'tb_textedit_cancel') { + $('#dlgTextEdit').dialog('close'); + return; + } hide_all_dialogs(); $('#action').val(''); @@ -934,11 +975,20 @@ function wmEditorReloadUrl() { url.searchParams.set('action', 'nothing'); url.searchParams.delete('config_revision'); url.searchParams.delete('__csrf_magic'); + url.hash = ''; return url.href; } function wmReloadEditor() { - window.location.replace(wmEditorReloadUrl()); + var url = wmEditorReloadUrl(); + var current = new URL(window.location.href); + current.hash = ''; + // Same-document fragment navigation does not fetch a fresh map snapshot. + if (current.href === url) { + window.location.reload(); + } else { + window.location.replace(url); + } } function form_submit(event) { @@ -1073,6 +1123,8 @@ function real_position_legend(scalename) { } function show_itemtext(itemtype,name) { + var parent = itemtype === 'link' ? 'dlgLinkProperties' : 'dlgNodeProperties'; + var returnTo = $('#' + parent).dialog('instance') && $('#' + parent).dialog('isOpen') ? parent : null; mapmode('existing'); hide_all_dialogs(); @@ -1087,7 +1139,7 @@ function show_itemtext(itemtype,name) { $('#action').val('set_link_config'); } - $('#dlgTextEdit').data('wm-config-action', $('#action').val()); + $('#dlgTextEdit').data('wm-config-action', $('#action').val()).data('wm-config-parent', returnTo); show_dialog('dlgTextEdit'); $.ajax({ @@ -1334,6 +1386,19 @@ function show_dialog(dlg) { modal: false, resizable: false, draggable: true, + close: function() { + $('#action').val(''); + mapmode('existing'); + if (dlg === 'dlgTextEdit') { + var parent = $(this).data('wm-config-parent'); + $(this).removeData('wm-config-parent'); + if (parent) { + show_dialog(parent); + $('#action').val(parent === 'dlgLinkProperties' ? 'set_link_properties' : 'set_node_properties'); + $(parent === 'dlgLinkProperties' ? '#link_edit' : '#node_edit').trigger('focus'); + } + } + }, open: function() { $(this).closest('.ui-dialog').addClass('wm-editor-dialog'); $(this).find('.ui-autocomplete-input').each(function() { @@ -1357,6 +1422,8 @@ function hide_dialog(dlg) { } function hide_all_dialogs() { + // Navigation to another task must not reopen the configuration editor's parent. + $('#dlgTextEdit').removeData('wm-config-parent'); hide_dialog('dlgMapProperties'); hide_dialog('dlgMapStyle'); hide_dialog('dlgLinkProperties'); diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 18edb16a..a89d7aa5 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -1,17 +1,17 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR The Cacti Group -# This file is distributed under the same license as the Cacti package. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Cacti 1.7\n" -"Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-10-15 10:19+1030\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-10-16 23:17+1030\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: Cacti Developers \n" -"Language-Team: Cacti Developers \n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/tests/Browser/ConfigCancelTest.cjs b/tests/Browser/ConfigCancelTest.cjs new file mode 100644 index 00000000..46dca290 --- /dev/null +++ b/tests/Browser/ConfigCancelTest.cjs @@ -0,0 +1,30 @@ +const {JSDOM}=require('jsdom'), jquery=require('jquery'), fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict'); +const dom=new JSDOM('
',{runScripts:'outside-only',pretendToBeVisual:true}); +const w=dom.window,$=jquery(w);w.$=w.jQuery=$; +w.eval(fs.readFileSync(path.join(process.env.CACTI_JS_DIR,'jquery-ui.js'),'utf8')); +const source=fs.readFileSync(path.join(__dirname,'../../js/editor.js'),'utf8'); +w.eval(source.slice(source.indexOf('function cancel_op('),source.indexOf('function help_handler('))); +w.eval(source.slice(source.indexOf('function show_itemtext('),source.indexOf('function prime_node_form('))); +w.eval(source.slice(source.indexOf('function show_dialog('),source.indexOf('function coord_capture('))); +w.mapmode=()=>{};w.wmResetActionHelp=()=>{};w.editor_url='/editor';$.ajax=()=>{}; +$('#tb_textedit_cancel').on('click',w.cancel_op); +for(const [type,parent,action,edit] of [['link','dlgLinkProperties','set_link_properties','link_edit'],['node','dlgNodeProperties','set_node_properties','node_edit']]){ + w.hide_all_dialogs();w.show_dialog(parent);w.show_itemtext(type,'test'); + assert.equal($('#'+parent).dialog('isOpen'),false); + $('#item_configtext').val('discard this config draft'); + $('#tb_textedit_cancel').trigger('click'); + assert.equal($('#dlgTextEdit').dialog('isOpen'),false); + assert.equal($('#'+parent).dialog('isOpen'),true); + assert.equal($('#action').val(),action); + assert.equal(w.document.activeElement.id,edit); + assert.equal($('#unsaved').val(),'draft'); + // Title-bar close and Escape use this same close callback. + w.show_itemtext(type,'test');$('#dlgTextEdit').dialog('close'); + assert.equal($('#'+parent).dialog('isOpen'),true); + w.show_itemtext(type,'test');w.hide_all_dialogs(); + assert.equal($('#'+parent).dialog('isOpen'),false,'Navigation must not reopen the parent'); +} +w.show_itemtext('link','test');$('#tb_textedit_cancel').trigger('click'); +assert.equal($('#dlgLinkProperties').dialog('isOpen'),false,'Direct configuration editing has no parent to restore'); +console.log('PASS: Cancel and close return to properties, preserve drafts and action, while navigation stays closed'); +w.close(); diff --git a/tests/Browser/EditorContextMenuTest.cjs b/tests/Browser/EditorContextMenuTest.cjs index a770e7d5..632e38da 100644 --- a/tests/Browser/EditorContextMenuTest.cjs +++ b/tests/Browser/EditorContextMenuTest.cjs @@ -70,7 +70,7 @@ $('#action').val('add_link');w.mapmode('existing');assert.ok($('#existingdata'). w.click_execute({},'LINK:1:0');assert.equal($('#action').val(),'');assert.ok(!$('#existingdata').hasClass('wm-placement-mode'));assert.equal($('area').css('cursor'),'pointer'); w.eval(src.slice(src.indexOf('function form_submit('),src.indexOf('function map_properties('))); $('body').append('
'); -let saveRequest;$.ajax=opts=>saveRequest=opts;w.editor_url='/editor'; +let saveRequest;$.ajax=opts=>saveRequest=opts;w.editor_url='/editor';w.wmSyncLinkHoverParts=()=>{}; for (const kind of ['node','link']) for (const exit of ['cancel','close']) { const dialog=$('#dlg'+(kind==='node'?'Node':'Link')+'Properties');dialog.dialog({modal:false}); $('#action').val('add_link2');$('#param').val('first-endpoint');w.mapmode('existing'); @@ -83,5 +83,27 @@ for (const kind of ['node','link']) for (const exit of ['cancel','close']) { assert.equal(submitted.get('param'),$(kind==='node'?'#node_name':'#link_name').val()); assert.ok(!$('#existingdata').hasClass('wm-placement-mode'));dialog.dialog('close'); } +// Programmatic deletion must not turn into Save when focus returns to a property field. +for (const kind of ['node','link']) { + const dialog=$('#dlg'+(kind==='node'?'Node':'Link')+'Properties'); + dialog.append('').dialog({modal:false}).dialog('open'); + dialog.find('input.focused-delete-test')[0].focus(); + assert.equal($(w.document.activeElement).closest('.ui-dialog-content').attr('id'),dialog.attr('id')); + $('#action').val('delete_'+kind);$('#param').val(kind==='node'?'dc':'uplink'); + w.form_submit(); + const deletion=new URLSearchParams(saveRequest.data); + assert.equal(deletion.get('action'),'delete_'+kind); + assert.equal(deletion.get('param'),kind==='node'?'dc':'uplink'); + dialog.dialog('close'); +} +w.eval(src.slice(src.indexOf('function show_dialog('),src.indexOf('function hide_dialog('))); +for (const kind of ['Node', 'Link']) { + const id = 'dlg' + kind + 'Properties'; + w.show_dialog(id); + $('#action').val('set_' + kind.toLowerCase() + '_properties'); + $('#' + id).dialog('close'); + assert.equal($('#action').val(), '', 'title-bar close must allow selecting the next object'); + assert.ok(!$('#existingdata').hasClass('wm-placement-mode')); +} dom.window.close(); console.log('PASS: context menu cancels placement; node/link confirmations and closing restore normal map mode'); diff --git a/tests/Browser/EditorHotspotTest.cjs b/tests/Browser/EditorHotspotTest.cjs new file mode 100644 index 00000000..b0071714 --- /dev/null +++ b/tests/Browser/EditorHotspotTest.cjs @@ -0,0 +1,16 @@ +const {JSDOM}=require('jsdom'), jquery=require('jquery'), fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict'); +const dom=new JSDOM('',{runScripts:'outside-only'}); +const w=dom.window,$=jquery(w);w.$=w.jQuery=$; +w.eval(fs.readFileSync(path.join(__dirname,'../../js/editor.js'),'utf8')); +const image=$('#existingdata')[0];let rect={left:30,top:-200,width:500,height:250};image.getBoundingClientRect=()=>rect; +assert.equal(w.wmEditorAreaAt(image,{clientX:90,clientY:-170}).id,'NODE:N1:0'); +rect={left:30,top:40,width:500,height:250}; +assert.equal(w.wmEditorAreaAt(image,{clientX:90,clientY:70}).id,'NODE:N1:0'); +assert.equal(w.wmEditorAreaAt(image,{clientX:155,clientY:100}).id,'LINK:L1:0'); +assert.equal(w.wmEditorAreaAt(image,{clientX:195,clientY:100}),null); +w.attach_click_events();let hits=0;$('#NODE\\:N1\\:0').off('click').on('click',()=>hits++); +$('#existingdata').trigger($.Event('click',{clientX:90,clientY:70}));assert.equal(hits,1); +w.attach_click_events();$('#NODE\\:N1\\:0').off('click').on('click',()=>hits++); +$('#existingdata').trigger($.Event('click',{clientX:90,clientY:70}));assert.equal(hits,2,'reattaching handlers must not double-dispatch'); +$('#existingdata').trigger($.Event('click',{clientX:90,clientY:70,target:$('#NODE\\:N1\\:0')[0]}));assert.equal(hits,2,'native area clicks must not dispatch twice'); +dom.window.close();console.log('PASS: scrolled and scaled image clicks select existing hotspots without duplicate native events.'); diff --git a/tests/Browser/EditorReloadTest.cjs b/tests/Browser/EditorReloadTest.cjs new file mode 100644 index 00000000..7582ee02 --- /dev/null +++ b/tests/Browser/EditorReloadTest.cjs @@ -0,0 +1,29 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const assert = require('node:assert/strict'); +const source = fs.readFileSync(path.join(__dirname, '../../js/editor.js'), 'utf8'); +const navigation = source.slice(source.indexOf('function wmEditorReloadUrl()'), source.indexOf('function form_submit(')); +for (const fragment of ['', '#', '#note']) { + const calls = []; + const window = {location: { + href: 'https://example.test/editor.php?action=nothing&mapname=map.conf' + fragment, + reload() { calls.push('reload'); }, + replace(url) { calls.push(url); } + }}; + const context = {URL, window, $: () => ({val: () => 'map.conf'})}; + vm.createContext(context); + vm.runInContext(navigation, context); + context.wmReloadEditor(); + assert.deepEqual(calls, ['reload'], 'saving an already-open map must fetch a fresh document'); + window.location.href = 'https://example.test/editor.php?action=edit&config_revision=old&__csrf_magic=old#note'; + calls.length = 0; + context.wmReloadEditor(); + const target = new URL(calls[0]); + assert.equal(target.hash, ''); + assert.equal(target.searchParams.get('action'), 'nothing'); + assert.equal(target.searchParams.get('mapname'), 'map.conf'); + assert.equal(target.searchParams.has('config_revision'), false); + assert.equal(target.searchParams.has('__csrf_magic'), false); +} +console.log('PASS: successful saves reload the document even at the same URL or fragment.'); diff --git a/tests/Support/EditorNotesEngineRegression.php b/tests/Support/EditorNotesEngineRegression.php index f4421927..53ed3866 100644 --- a/tests/Support/EditorNotesEngineRegression.php +++ b/tests/Support/EditorNotesEngineRegression.php @@ -65,7 +65,7 @@ class DimensionPostProcessor extends WeatherMapPostProcessor { function run(&$ma note_assert(wm_note_effective_dimensions($sized) === [240, 90] && $sized->width === 120, 'postprocessor dimensions are reflected without changing original map'); $controller = file_get_contents($plugin . '/weathermap-cacti-plugin-editor.php'); $start = strpos($controller, "' . $markup); $renderedElements = ob_get_clean(); + $mapfile = $file; $editor_width = 200; $editor_height = 100; $imageurl = 'fixture.png'; ob_start(); eval('?>' . $markup); $renderedElements = ob_get_clean(); note_assert(substr_count($renderedElements, "width='200'") === 4 && substr_count($renderedElements, "height='100'") === 4, 'editor image attributes and data coordinates use effective logical size'); $values = ['name' => '', 'text' => "Main site\n10.0.0.0/24", 'font' => 'bundled:Vera.ttf:11', 'colour' => '#0000ff', 'x' => '155', 'y' => '80']; note_assert(wm_editor_save_note($map, $values), 'create'); diff --git a/weathermap-cacti-plugin-editor.php b/weathermap-cacti-plugin-editor.php index c5dbe434..3d2399cf 100644 --- a/weathermap-cacti-plugin-editor.php +++ b/weathermap-cacti-plugin-editor.php @@ -348,9 +348,10 @@
-
+
+ -
- -
From b652f457dc8ef3b141dbe1946c64e34f8c340313 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:30:13 +1030 Subject: [PATCH 2/6] Reference PR #308 in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35414332..53060f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! --- develop --- -* issue: Refresh saved editor snapshots reliably, restore Safari node/note selection and return to properties when cancelling raw configuration edits +* issue#308: Refresh saved editor snapshots reliably, restore Safari node/note selection and return to properties when cancelling raw configuration edits * Add traffic colour presets and optional high-load link pulsing From 792db4b8040d930c356902019f2539be2c461102 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:40:44 +1030 Subject: [PATCH 3/6] Preserve translation template project metadata --- locales/po/cacti.pot | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index a89d7aa5..18edb16a 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -1,17 +1,17 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. +# Copyright (C) YEAR The Cacti Group +# This file is distributed under the same license as the Cacti package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-16 23:17+1030\n" +"Project-Id-Version: Cacti 1.7\n" +"Report-Msgid-Bugs-To: developers@cacti.net\n" +"POT-Creation-Date: 2026-10-15 10:19+1030\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" +"Last-Translator: Cacti Developers \n" +"Language-Team: Cacti Developers \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" From e43e548e4f45efd27b44cc54b29a803d566c12d6 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:19:45 +1030 Subject: [PATCH 4/6] Clear hotspot cursors when entering placement mode --- CHANGELOG.md | 2 ++ js/editor.js | 2 +- tests/Browser/EditorHotspotTest.cjs | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53060f7d..55a04c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! --- develop --- +-issue#308: Clear hotspot pointer cursors when entering placement mode + * issue#308: Refresh saved editor snapshots reliably, restore Safari node/note selection and return to properties when cancelling raw configuration edits * Add traffic colour presets and optional high-load link pulsing diff --git a/js/editor.js b/js/editor.js index 70415dd6..f812531c 100644 --- a/js/editor.js +++ b/js/editor.js @@ -802,7 +802,7 @@ function wmResetActionHelp() { function mapmode(m) { var placing = m == 'xy' || ['add_link', 'add_link2'].indexOf($('#action').val()) !== -1; - $('#existingdata, .mapData').toggleClass('wm-placement-mode', placing); + $('#existingdata, .mapData').toggleClass('wm-placement-mode', placing).css('cursor', ''); $('#tb_help').toggleClass('wm-action-help', placing); if (!placing) wmResetActionHelp(); if (m == 'xy') { diff --git a/tests/Browser/EditorHotspotTest.cjs b/tests/Browser/EditorHotspotTest.cjs index b0071714..1873d8db 100644 --- a/tests/Browser/EditorHotspotTest.cjs +++ b/tests/Browser/EditorHotspotTest.cjs @@ -13,4 +13,13 @@ $('#existingdata').trigger($.Event('click',{clientX:90,clientY:70}));assert.equa w.attach_click_events();$('#NODE\\:N1\\:0').off('click').on('click',()=>hits++); $('#existingdata').trigger($.Event('click',{clientX:90,clientY:70}));assert.equal(hits,2,'reattaching handlers must not double-dispatch'); $('#existingdata').trigger($.Event('click',{clientX:90,clientY:70,target:$('#NODE\\:N1\\:0')[0]}));assert.equal(hits,2,'native area clicks must not dispatch twice'); +$('#existingdata').trigger($.Event('mousemove',{clientX:90,clientY:70})); +assert.equal(image.style.cursor,'pointer'); +$('#action').val('add_link');w.mapmode('existing'); +assert.equal(image.style.cursor,'','Entering placement clears an existing inline pointer'); +$('#existingdata').trigger($.Event('mousemove',{clientX:90,clientY:70})); +assert.equal(image.style.cursor,'','Placement keeps stylesheet crosshair active'); +$('#action').val('');w.mapmode('existing'); +$('#existingdata').trigger($.Event('mousemove',{clientX:90,clientY:70})); +assert.equal(image.style.cursor,'pointer','Normal hotspot feedback returns after placement'); dom.window.close();console.log('PASS: scrolled and scaled image clicks select existing hotspots without duplicate native events.'); From 7fd9a87b1e96c329635679d0b9c47e0f94cfb6df Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:51:34 +1030 Subject: [PATCH 5/6] Load editor navigation regression without dynamic code execution --- js/editor.js | 5 +++++ tests/Browser/EditorReloadTest.cjs | 20 +++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/js/editor.js b/js/editor.js index f812531c..259ca7b6 100644 --- a/js/editor.js +++ b/js/editor.js @@ -2138,3 +2138,8 @@ function show_note(name, x, y) { $('#note_font,#note_align').each(function() { if ($(this).data('ui-selectmenu')) $(this).selectmenu('refresh'); }); $('#note_text').trigger('focus'); } + +// Allow the navigation regression to load these helpers directly under Node.js. +if (typeof module !== 'undefined' && module.exports) { + module.exports = {wmEditorReloadUrl: wmEditorReloadUrl, wmReloadEditor: wmReloadEditor}; +} diff --git a/tests/Browser/EditorReloadTest.cjs b/tests/Browser/EditorReloadTest.cjs index 7582ee02..18fa30b0 100644 --- a/tests/Browser/EditorReloadTest.cjs +++ b/tests/Browser/EditorReloadTest.cjs @@ -1,24 +1,22 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const vm = require('node:vm'); const assert = require('node:assert/strict'); -const source = fs.readFileSync(path.join(__dirname, '../../js/editor.js'), 'utf8'); -const navigation = source.slice(source.indexOf('function wmEditorReloadUrl()'), source.indexOf('function form_submit(')); +// Load the actual module without evaluating source strings. Defer DOM-ready setup. +global.document = {}; +global.$ = Object.assign(selector => typeof selector === 'function' ? undefined : { + val: () => 'map.conf', on() {return this;} +}, {fn: {}}); +const navigation = require('../../js/editor.js'); for (const fragment of ['', '#', '#note']) { const calls = []; - const window = {location: { + const window = global.window = {location: { href: 'https://example.test/editor.php?action=nothing&mapname=map.conf' + fragment, reload() { calls.push('reload'); }, replace(url) { calls.push(url); } }}; - const context = {URL, window, $: () => ({val: () => 'map.conf'})}; - vm.createContext(context); - vm.runInContext(navigation, context); - context.wmReloadEditor(); + navigation.wmReloadEditor(); assert.deepEqual(calls, ['reload'], 'saving an already-open map must fetch a fresh document'); window.location.href = 'https://example.test/editor.php?action=edit&config_revision=old&__csrf_magic=old#note'; calls.length = 0; - context.wmReloadEditor(); + navigation.wmReloadEditor(); const target = new URL(calls[0]); assert.equal(target.hash, ''); assert.equal(target.searchParams.get('action'), 'nothing'); From 4bc9aeaf2acef9d8c007fb753651290b2b7a3589 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:28:26 +1030 Subject: [PATCH 6/6] Restore raw-edit focus to the production anchor controls --- js/editor.js | 2 +- tests/Browser/ConfigCancelTest.cjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/editor.js b/js/editor.js index 259ca7b6..283325a2 100644 --- a/js/editor.js +++ b/js/editor.js @@ -1395,7 +1395,7 @@ function show_dialog(dlg) { if (parent) { show_dialog(parent); $('#action').val(parent === 'dlgLinkProperties' ? 'set_link_properties' : 'set_node_properties'); - $(parent === 'dlgLinkProperties' ? '#link_edit' : '#node_edit').trigger('focus'); + $(parent === 'dlgLinkProperties' ? '#link_edit' : '#node_edit').attr('tabindex', '-1').trigger('focus'); } } }, diff --git a/tests/Browser/ConfigCancelTest.cjs b/tests/Browser/ConfigCancelTest.cjs index 46dca290..7dd88bbe 100644 --- a/tests/Browser/ConfigCancelTest.cjs +++ b/tests/Browser/ConfigCancelTest.cjs @@ -1,5 +1,5 @@ const {JSDOM}=require('jsdom'), jquery=require('jquery'), fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict'); -const dom=new JSDOM('
',{runScripts:'outside-only',pretendToBeVisual:true}); +const dom=new JSDOM('
',{runScripts:'outside-only',pretendToBeVisual:true}); const w=dom.window,$=jquery(w);w.$=w.jQuery=$; w.eval(fs.readFileSync(path.join(process.env.CACTI_JS_DIR,'jquery-ui.js'),'utf8')); const source=fs.readFileSync(path.join(__dirname,'../../js/editor.js'),'utf8');