Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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

* Fix Safari rendering of high-traffic link glows using native SVG filters
Expand Down
80 changes: 76 additions & 4 deletions js/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -761,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') {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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({
Expand Down Expand Up @@ -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').attr('tabindex', '-1').trigger('focus');
}
}
},
open: function() {
$(this).closest('.ui-dialog').addClass('wm-editor-dialog');
$(this).find('.ui-autocomplete-input').each(function() {
Expand All @@ -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');
Expand Down Expand Up @@ -2071,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};
}
30 changes: 30 additions & 0 deletions tests/Browser/ConfigCancelTest.cjs
Original file line number Diff line number Diff line change
@@ -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('<input id="action"><input id="mapname" value="test.conf"><div id="dlgLinkProperties"><input id="unsaved" value="draft"><a id="link_edit">Edit</a></div><div id="dlgNodeProperties"><a id="node_edit">Edit</a></div><div id="dlgTextEdit"><textarea id="item_configtext"></textarea><button id="tb_textedit_cancel">Cancel</button></div>',{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();
24 changes: 23 additions & 1 deletion tests/Browser/EditorContextMenuTest.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div id="dlgNodeProperties"></div><div id="dlgLinkProperties"></div>');
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');
Expand All @@ -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('<input class="focused-delete-test">').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');
25 changes: 25 additions & 0 deletions tests/Browser/EditorHotspotTest.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const {JSDOM}=require('jsdom'), jquery=require('jquery'), fs=require('node:fs'), path=require('node:path'), assert=require('node:assert/strict');
const dom=new JSDOM('<input id="action"><img id="existingdata" data-width="1000" data-height="500" usemap="#weathermap_imap"><map id="weathermap_imap"><area id="NODE:N1:0" shape="rect" coords="100,50,140,80"><area id="LINK:L1:0" shape="poly" coords="200,100,300,100,250,150"></map>',{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');
$('#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.');
27 changes: 27 additions & 0 deletions tests/Browser/EditorReloadTest.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const assert = require('node:assert/strict');
// 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 = global.window = {location: {
href: 'https://example.test/editor.php?action=nothing&mapname=map.conf' + fragment,
reload() { calls.push('reload'); },
replace(url) { calls.push(url); }
}};
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;
navigation.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.');
2 changes: 1 addition & 1 deletion tests/Support/EditorNotesEngineRegression.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<input id='xycapture'"); $end = strpos($controller, "<input id='x'", $start); $markup = substr($controller, $start, $end - $start);
$editor_width = 200; $editor_height = 100; $imageurl = 'fixture.png'; ob_start(); eval('?>' . $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');
Expand Down
6 changes: 2 additions & 4 deletions weathermap-cacti-plugin-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,10 @@
</ul>
</div>
<form id='frmMain' action='<?php print $editor_name ?>' method='post'>
<div class='mainArea'>
<div class='mainArea mapData'>
<input id='xycapture' name='xycapture' width='<?php print html_escape($editor_width); ?>' height='<?php print html_escape($editor_height); ?>' data-width='<?php print html_escape($editor_width); ?>' data-height='<?php print html_escape($editor_height); ?>' style='display:none' type='image' src='<?php print html_escape($imageurl); ?>' />
<img id='existingdata' name='existingdata' width='<?php print html_escape($editor_width); ?>' height='<?php print html_escape($editor_height); ?>' data-width='<?php print html_escape($editor_width); ?>' data-height='<?php print html_escape($editor_height); ?>' src='<?php print html_escape($imageurl); ?>' usemap='#weathermap_imap' />
<?php getMapAreaData($mapfile); ?>
<input id='x' name='x' type='hidden' />
<input id='y' name='y' type='hidden' />
<div class='debug' style='display:none'><p><strong><?php print __('Debug', 'weathermap'); ?></strong>
Expand Down Expand Up @@ -390,9 +391,6 @@
<?php print 'var infoUrlStyle=' . intval(read_config_option('weathermap_infourl_style')) . ';'; ?>
</script>
</div>
<div class='mapData'>
<?php getMapAreaData($mapfile); ?>
</div>
<!-- End Data for overlay and selection -->


Expand Down