From 56d283e86a598a22fa9fd4456b8ea9ee736f73df Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:11:21 +0930 Subject: [PATCH 1/8] Restore cycle countdown and same-tab fullscreen controls --- css/weathermap.css | 10 +++++ js/map-cycle.js | 76 +++++++++++++++++++------------------ weathermap-cacti-plugin.php | 54 +++++++++++++------------- 3 files changed, 77 insertions(+), 63 deletions(-) diff --git a/css/weathermap.css b/css/weathermap.css index 32caa7ab..6325a646 100644 --- a/css/weathermap.css +++ b/css/weathermap.css @@ -95,3 +95,13 @@ body#wm_fullscreen { font-family: sans-serif; margin: 0; border: 0; padding: 0; .inplace .weathermapholder { display: none; } #wm_cyclecontrols a img { width: 16px; height: 16px; border: 0; } + +/* Readable compact controls in the themed header and standalone cycle view. */ +.wm-cycle-control { display: inline-block; padding: 6px; margin: 0 5px; color: inherit !important; font-size: 14px; text-decoration: none; vertical-align: middle; } +.wm-cycle-control:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } +.wm-cycle-toolbar { color: var(--text-color-highlight, #fff); } +#wmcyclecontrolbox.fullscreen { opacity: 1; background: #26351c; color: #fff; width: auto; max-width: calc(100% - 40px); border: 1px solid #829673; border-radius: 4px; } +#wm_countdown { margin-left: 12px; white-space: nowrap; } +.wm-cycle-exit-label { font: 14px sans-serif; margin-left: 6px; } +.wm-progress-track { display: inline-block; width: 140px; height: 6px; margin-left: 10px; vertical-align: middle; overflow: hidden; border: 1px solid currentColor; border-radius: 3px; background: rgba(255,255,255,.25); } +.wm-progress-track #wm_progress { display: block; width: 100%; height: 100%; margin: 0; background: currentColor; } diff --git a/js/map-cycle.js b/js/map-cycle.js index 9ea988ba..eafe3b3c 100644 --- a/js/map-cycle.js +++ b/js/map-cycle.js @@ -11,6 +11,8 @@ jQuery.fn.center = function () { return this; }; +if (typeof WMcycler !== 'undefined' && WMcycler.stop) { WMcycler.stop(); } + var WMcycler = { KEYCODE_ESCAPE : 27, @@ -29,14 +31,17 @@ var WMcycler = { timer_reloader : null, updateProgress : function () { - // update the countdown bar - 450 is the max width in pixels - var progress = this.countdown / (this.period / 200) * 450; - $("#wm_progress").css("width", progress); + // Update the countdown as a proportion of the selected cycle period. + var progress = this.period > 0 ? Math.max(0, Math.min(100, this.countdown / (this.period / 200) * 100)) : 100; + $("#wm_progress").css("width", progress + "%"); + var label = $("#wm_countdown"); + label.text(this.paused ? (label.attr('data-paused-label') || 'Paused') : + (label.attr('data-next-label') || 'Next map in %ss').replace('%s', Math.max(0, Math.ceil(this.countdown / 5)))); }, counterHandler : function () { if (this.paused) { - $("#wm_progress").toggleClass("paused"); + this.updateProgress(); } else { this.updateProgress(); this.countdown--; @@ -107,7 +112,16 @@ var WMcycler = { $("#wmcyclecontrolbox").fadeIn(100); }, + stop : function () { + clearInterval(this.timer_counter); + clearTimeout(this.timer_reloader); + $(document).off('.wmCycle'); + $('#cycle_pause,#cycle_next,#cycle_prev,.wm-fullscreen-link').off('.wmCycle'); + }, + start : function (initialData) { + this.stop(); + this.paused = false; $('.weathermapholder').hide(); @@ -127,16 +141,11 @@ var WMcycler = { // stop here if there were no maps if (this.nmaps > 0) { + if (this.period === 0) { this.period = this.poller_cycle / this.nmaps; } this.current = 0; this.switchMap(0); - // figure out how long the refresh is, so that we get - // through all the maps in exactly one poller cycle - if (this.period === 0) { - this.period = this.poller_cycle / this.nmaps; - } - this.countdown = this.period / 200; // a countdown timer in the top corner this.timer_counter = setInterval(function () { @@ -145,7 +154,11 @@ var WMcycler = { // when to reload the whole page (with new map data) this.timer_reloader = setTimeout(function () { - loadPage(document.location.href); + if (typeof loadPage === 'function' && !that.fullscreen) { + loadPage(document.location.href); + } else { + window.location.reload(); + } }, this.poller_cycle); this.initIdle(that); @@ -153,9 +166,9 @@ var WMcycler = { }, initKeys: function (that) { - $(document).on('keyup', function(event) { + $(document).on('keyup.wmCycle', function(event) { if (event.keyCode === that.KEYCODE_ESCAPE) { - window.location.href = $('#cycle_stop').attr('href'); + window.location.href = $(that.fullscreen ? '#cycle_exit_fullscreen' : '#cycle_stop').attr('href'); event.preventDefault(); } @@ -177,38 +190,28 @@ var WMcycler = { }, initEvents: function (that) { + $('.wm-fullscreen-link').off('click.wmCycle').on('click.wmCycle', function(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + window.location.assign(this.href); + }); - $("#cycle_pause").on('click', function() { + $("#cycle_pause").off('click.wmCycle').on('click.wmCycle', function(event) { + event.preventDefault(); that.pauseAction(); }); - $("#cycle_next").on('click', function() { + $("#cycle_next").off('click.wmCycle').on('click.wmCycle', function(event) { + event.preventDefault(); that.nextAction(); }); - $("#cycle_prev").on('click', function() { + $("#cycle_prev").off('click.wmCycle').on('click.wmCycle', function(event) { + event.preventDefault(); that.previousAction(); }); }, - initIdle: function (that) { - // aim to get a video-player style OSD for fullscreen mode: - // if the pointer is off the controls for more than 5 seconds, fade the - // controls away - // if the pointer moves after that, bring the controls back - // if the pointer is over the controls, don't fade - if (this.fullscreen) { - - $(document).idleTimer({ - timeout: 5000 - }); - - $(document).on("idle.idleTimer", function () { - that.hideControls(); - }); - $(document).on("active.idleTimer", function () { - that.showControls(); - }); - - } + initIdle: function () { + // Keep the full-screen exit and countdown visible. }, nextAction : function () { @@ -219,6 +222,7 @@ var WMcycler = { }, pauseAction : function () { this.paused = !this.paused; + this.updateProgress(); // remove the paused class on the progress bar, if we're mid-flash and // no longer paused if (!this.paused) { diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php index 1f722dec..d78ca3aa 100644 --- a/weathermap-cacti-plugin.php +++ b/weathermap-cacti-plugin.php @@ -674,38 +674,14 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group $html = __('Showing %s %s of %s %s. Cycling all available maps.', '', '', '', '', 'weathermaps'); } + $controls = weathermap_cycle_controls($fullscreen, $limit_to_group); if ($fullscreen == 0) { - $pagetitle .= ' [ ' . " - - - - - ] - [ " . $html . ' ] '; - ?> -
-
-
-
' . $pagetitle . ' [ ' . $controls . ' ] [ ' . $html . ' ]
'; } else { - ?> -
-
-
- - - - - plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=1&group='> - -
-
- ' . $controls . '' . $html . ''; } } - $i = 0; - if (cacti_sizeof($maplist)) { print "
"; @@ -1058,3 +1034,27 @@ function weathermap_tabs($current_tab) { return false; } } + +/** Render translated, keyboard-accessible cycle controls. + * @param int $fullscreen Whether Cacti chrome is hidden. + * @param int $group_id Group filter to retain when toggling full screen. + * @return string + */ +function weathermap_cycle_controls($fullscreen, $group_id) { + global $config; + $controls = ''; + $actions = [ + 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='], + 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'], + 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'], + 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#'] + ]; + foreach ($actions as $id => $action) { + $controls .= ''; + } + $label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap'); + $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id; + $controls .= '' . ($fullscreen ? '' . html_escape($label) . '' : '') . ''; + $controls .= ''; + return $controls; +} From 5c733fd599fe34d50df760090d123ba6e02bf00f Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:17:47 +0930 Subject: [PATCH 2/8] Check cycle timing, lifecycle and fullscreen reload --- tests/Browser/CycleCountdownTest.cjs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/Browser/CycleCountdownTest.cjs diff --git a/tests/Browser/CycleCountdownTest.cjs b/tests/Browser/CycleCountdownTest.cjs new file mode 100644 index 00000000..42881cbd --- /dev/null +++ b/tests/Browser/CycleCountdownTest.cjs @@ -0,0 +1,22 @@ +const {JSDOM}=require('jsdom'),jquery=require('jquery'),fs=require('fs'),assert=require('node:assert/strict'); +const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'}); +const w=dom.window,$=jquery(w);w.$=w.jQuery=$;$.fx.off=true;let reloadCallback;w.setInterval=()=>1;w.setTimeout=(fn)=>{reloadCallback=fn;return 2;};require('vm').runInContext(fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8'),dom.getInternalVMContext()); +const c=w.WMcycler; +c.start({period:0,poller_cycle:300000,fullscreen:0});const pageReload=reloadCallback; +assert.equal(c.period,150000);assert.equal(c.current,0);assert.equal($('#wm_countdown').text(),'Next map in 150s');assert.equal($('#wm_progress')[0].style.width,'100%'); +c.nextAction();assert.equal(c.current,1);c.nextAction();assert.equal(c.current,0);c.previousAction();assert.equal(c.current,1); +c.countdown=500;c.updateProgress();assert.equal($('#wm_countdown').text(),'Next map in 100s');assert.ok(Math.abs(parseFloat($('#wm_progress')[0].style.width)-66.6667)<.01); +c.pauseAction();const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();c.counterHandler();assert.equal(c.countdown,paused-1); +c.countdown=0;c.counterHandler();assert.equal(c.current,0);assert.equal(c.countdown,750); +let reloadUrl;w.loadPage=(url)=>reloadUrl=url;pageReload();assert.equal(reloadUrl,'https://example.test/'); +c.period=30000;c.switchMap(0);assert.equal($('#wm_countdown').text(),'Next map in 30s'); +c.countdown=-3;c.updateProgress();assert.equal($('#wm_progress')[0].style.width,'0%'); +const src=fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8'); +const body=src.match(/this.timer_reloader = setTimeout\(function \(\) \{([\s\S]*?)\}, this.poller_cycle\);/)[1]; +let fullReload=0;new Function('that','window','document',body)({fullscreen:1},{location:{reload(){fullReload++;}}},{location:{href:'x'}});assert.equal(fullReload,1); +$('#wm_countdown').attr('data-next-label','Following map: %s seconds').attr('data-paused-label','Waiting');c.updateProgress();assert.equal($('#wm_countdown').text(),'Following map: 0 seconds');c.pauseAction();assert.equal($('#wm_countdown').text(),'Waiting');c.pauseAction(); +c.start({period:30000,poller_cycle:300000,fullscreen:0});c.start({period:30000,poller_cycle:300000,fullscreen:0}); +$('#cycle_next').trigger('click');assert.equal(c.current,1);$(w.document).trigger($.Event('keyup',{keyCode:39}));assert.equal(c.current,0); +$('.weathermapholder').last().remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.period,300000);c.nextAction();assert.equal(c.current,0); +$('.weathermapholder').remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.nmaps,0);c.stop(); +console.log('PASS: translated labels, repeated initialization, one/no-map cases, click/key navigation; automatic period, first countdown, next/previous wrap, pause/resume, automatic transition, progress bounds, configured period and fullscreen reload fallback'); From b9dda0e8ed389e9c09b5b1bc7a44fec9d14fe309 Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:19:41 +0930 Subject: [PATCH 3/8] Document restore cycling countdown and same-tab fullscreen controls --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda727f0..8066b405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! --- develop --- +* bug: Restore cycle countdown, persistent fullscreen exit and reload fallback + * refactor: Relocate the plugin's database schema provisioning (`weathermap_setup_table()`) out of `setup.php` into a dedicated `includes/database.php` module (function move only, no behaviour change) * feature: Add a root `manifest.json` file manifest plus an upgrade-time `weathermap_prune_files()` that removes tombstoned and dev-only paths while protecting whitelisted user data (`configs/`, `output/`) and `.git*`, backed by a `tests/bin/validate-manifest.php` CI gate that fails on manifest drift * dev: Add xdebug coverage reporting to CI and enforce patch coverage of changed lines From 002266ad7244b1f4f2332cb849820ebff80f6fe8 Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:01:41 +1030 Subject: [PATCH 4/8] Use numbered changelog entries for PR 282 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8066b405..db093b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++! --- develop --- -* bug: Restore cycle countdown, persistent fullscreen exit and reload fallback +-issue#282: Restore cycle countdown, persistent fullscreen exit and reload fallback * refactor: Relocate the plugin's database schema provisioning (`weathermap_setup_table()`) out of `setup.php` into a dedicated `includes/database.php` module (function move only, no behaviour change) * feature: Add a root `manifest.json` file manifest plus an upgrade-time `weathermap_prune_files()` that removes tombstoned and dev-only paths while protecting whitelisted user data (`configs/`, `output/`) and `.git*`, backed by a `tests/bin/validate-manifest.php` CI gate that fails on manifest drift From 933fc20e93a228179e03ffc3adce388336aa982b Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:12:52 +1030 Subject: [PATCH 5/8] Address cycle viewer review findings and refresh translations --- locales/po/cacti.pot | 35 ++++++++++++++++++++++++++++++++++- weathermap-cacti-plugin.php | 21 ++++++++++++--------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 3481e6ad..728af904 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Cacti \n" "Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-10-09 00:20+1030\n" +"POT-Creation-Date: 2026-10-09 10:11+1030\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Cacti Developers \n" "Language-Team: Cacti Developers \n" @@ -1802,3 +1802,36 @@ msgstr "" #: weathermap-cacti-plugin.php msgid "All Maps" msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Stop cycling" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Previous" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Pause / resume" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Next" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Exit full screen" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Full screen" +msgstr "" + +#: weathermap-cacti-plugin.php +#, php-format +msgid "Next map in %ss" +msgstr "" + +#: weathermap-cacti-plugin.php +msgid "Paused" +msgstr "" diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php index d78ca3aa..9813ab66 100644 --- a/weathermap-cacti-plugin.php +++ b/weathermap-cacti-plugin.php @@ -689,8 +689,6 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group $confdir = __DIR__ . '/configs/'; foreach ($maplist as $map) { - $i++; - $htmlfile = $outdir . $map['filehash'] . '.html'; $maptitle = $map['titlecache']; @@ -1035,26 +1033,31 @@ function weathermap_tabs($current_tab) { } } -/** Render translated, keyboard-accessible cycle controls. +/** + * Render translated, keyboard-accessible cycle controls. + * * @param int $fullscreen Whether Cacti chrome is hidden. - * @param int $group_id Group filter to retain when toggling full screen. + * @param int $group_id Group filter to retain when toggling full screen. + * * @return string */ function weathermap_cycle_controls($fullscreen, $group_id) { global $config; $controls = ''; - $actions = [ - 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='], - 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'], + $actions = [ + 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='], + 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'], 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'], - 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#'] + 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#'] ]; + foreach ($actions as $id => $action) { $controls .= ''; } $label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap'); - $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id; + $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id; $controls .= '' . ($fullscreen ? '' . html_escape($label) . '' : '') . ''; $controls .= ''; + return $controls; } From 0c5b0fc9581ea0b3fe3e304de61e1e163c9bbed1 Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:21:40 +1030 Subject: [PATCH 6/8] Remove obsolete cycle idle handling and redundant group cast --- js/map-cycle.js | 14 -------------- weathermap-cacti-plugin.php | 4 +--- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/js/map-cycle.js b/js/map-cycle.js index eafe3b3c..2efbe623 100644 --- a/js/map-cycle.js +++ b/js/map-cycle.js @@ -104,14 +104,6 @@ var WMcycler = { }, - hideControls: function () { - $("#wmcyclecontrolbox").fadeOut(500); - }, - - showControls: function () { - $("#wmcyclecontrolbox").fadeIn(100); - }, - stop : function () { clearInterval(this.timer_counter); clearTimeout(this.timer_reloader); @@ -160,8 +152,6 @@ var WMcycler = { window.location.reload(); } }, this.poller_cycle); - - this.initIdle(that); } }, @@ -210,10 +200,6 @@ var WMcycler = { }); }, - initIdle: function () { - // Keep the full-screen exit and countdown visible. - }, - nextAction : function () { this.switchMap(1); }, diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php index 9813ab66..d99537dd 100644 --- a/weathermap-cacti-plugin.php +++ b/weathermap-cacti-plugin.php @@ -666,8 +666,6 @@ function weathermap_fullview($cycle = false, $firstonly = false, $limit_to_group print get_md5_include_js('include/js/jquery.js'); } - print get_md5_include_js('plugins/weathermap/js/idle-timer.min.js'); - if ($limit_to_group > 0) { $html = __('Showing %s %s of %s %s. Cycling all available maps in this group.', '', '', '', '', 'weathermaps'); } else { @@ -1055,7 +1053,7 @@ function weathermap_cycle_controls($fullscreen, $group_id) { $controls .= ''; } $label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap'); - $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . (int) $group_id; + $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . $group_id; $controls .= '' . ($fullscreen ? '' . html_escape($label) . '' : '') . ''; $controls .= ''; From 60ead841f68de65d4655f16a2c0a54b4d881a4ee Mon Sep 17 00:00:00 2001 From: Muris <27321443+alcatron@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:28:34 +1030 Subject: [PATCH 7/8] Use accessible cycle buttons and preserve focused keyboard behavior --- css/weathermap.css | 1 + js/map-cycle.js | 6 ++++++ tests/Browser/CycleCountdownTest.cjs | 19 +++++++++++++++++-- weathermap-cacti-plugin.php | 18 ++++++++++++------ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/css/weathermap.css b/css/weathermap.css index 6325a646..b0a4a0e2 100644 --- a/css/weathermap.css +++ b/css/weathermap.css @@ -98,6 +98,7 @@ body#wm_fullscreen { font-family: sans-serif; margin: 0; border: 0; padding: 0; /* Readable compact controls in the themed header and standalone cycle view. */ .wm-cycle-control { display: inline-block; padding: 6px; margin: 0 5px; color: inherit !important; font-size: 14px; text-decoration: none; vertical-align: middle; } +button.wm-cycle-control { appearance: none; background: transparent; border: 0; box-shadow: none; cursor: pointer; line-height: inherit; } .wm-cycle-control:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } .wm-cycle-toolbar { color: var(--text-color-highlight, #fff); } #wmcyclecontrolbox.fullscreen { opacity: 1; background: #26351c; color: #fff; width: auto; max-width: calc(100% - 40px); border: 1px solid #829673; border-radius: 4px; } diff --git a/js/map-cycle.js b/js/map-cycle.js index 2efbe623..7f50d534 100644 --- a/js/map-cycle.js +++ b/js/map-cycle.js @@ -31,6 +31,7 @@ var WMcycler = { timer_reloader : null, updateProgress : function () { + $("#cycle_pause").attr("aria-pressed", this.paused ? "true" : "false"); // Update the countdown as a proportion of the selected cycle period. var progress = this.period > 0 ? Math.max(0, Math.min(100, this.countdown / (this.period / 200) * 100)) : 100; $("#wm_progress").css("width", progress + "%"); @@ -114,6 +115,7 @@ var WMcycler = { start : function (initialData) { this.stop(); this.paused = false; + $('#cycle_pause').attr('aria-pressed', 'false'); $('.weathermapholder').hide(); @@ -157,6 +159,10 @@ var WMcycler = { initKeys: function (that) { $(document).on('keyup.wmCycle', function(event) { + if ($(event.target).closest('a, button, input, select, textarea, [role="button"], [contenteditable]:not([contenteditable="false"])').length) { + return; + } + if (event.keyCode === that.KEYCODE_ESCAPE) { window.location.href = $(that.fullscreen ? '#cycle_exit_fullscreen' : '#cycle_stop').attr('href'); event.preventDefault(); diff --git a/tests/Browser/CycleCountdownTest.cjs b/tests/Browser/CycleCountdownTest.cjs index 42881cbd..4ecc698f 100644 --- a/tests/Browser/CycleCountdownTest.cjs +++ b/tests/Browser/CycleCountdownTest.cjs @@ -1,12 +1,12 @@ const {JSDOM}=require('jsdom'),jquery=require('jquery'),fs=require('fs'),assert=require('node:assert/strict'); -const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'}); +const dom=new JSDOM('
',{runScripts:'outside-only',url:'https://example.test/'}); const w=dom.window,$=jquery(w);w.$=w.jQuery=$;$.fx.off=true;let reloadCallback;w.setInterval=()=>1;w.setTimeout=(fn)=>{reloadCallback=fn;return 2;};require('vm').runInContext(fs.readFileSync(require('node:path').join(__dirname,'../../js/map-cycle.js'),'utf8'),dom.getInternalVMContext()); const c=w.WMcycler; c.start({period:0,poller_cycle:300000,fullscreen:0});const pageReload=reloadCallback; assert.equal(c.period,150000);assert.equal(c.current,0);assert.equal($('#wm_countdown').text(),'Next map in 150s');assert.equal($('#wm_progress')[0].style.width,'100%'); c.nextAction();assert.equal(c.current,1);c.nextAction();assert.equal(c.current,0);c.previousAction();assert.equal(c.current,1); c.countdown=500;c.updateProgress();assert.equal($('#wm_countdown').text(),'Next map in 100s');assert.ok(Math.abs(parseFloat($('#wm_progress')[0].style.width)-66.6667)<.01); -c.pauseAction();const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();c.counterHandler();assert.equal(c.countdown,paused-1); +c.pauseAction();assert.equal($("#cycle_pause").attr("aria-pressed"),"true");const paused=c.countdown;c.counterHandler();assert.equal(c.countdown,paused);assert.equal($('#wm_countdown').text(),'Paused');c.pauseAction();assert.equal($("#cycle_pause").attr("aria-pressed"),"false");c.counterHandler();assert.equal(c.countdown,paused-1); c.countdown=0;c.counterHandler();assert.equal(c.current,0);assert.equal(c.countdown,750); let reloadUrl;w.loadPage=(url)=>reloadUrl=url;pageReload();assert.equal(reloadUrl,'https://example.test/'); c.period=30000;c.switchMap(0);assert.equal($('#wm_countdown').text(),'Next map in 30s'); @@ -17,6 +17,21 @@ let fullReload=0;new Function('that','window','document',body)({fullscreen:1},{l $('#wm_countdown').attr('data-next-label','Following map: %s seconds').attr('data-paused-label','Waiting');c.updateProgress();assert.equal($('#wm_countdown').text(),'Following map: 0 seconds');c.pauseAction();assert.equal($('#wm_countdown').text(),'Waiting');c.pauseAction(); c.start({period:30000,poller_cycle:300000,fullscreen:0});c.start({period:30000,poller_cycle:300000,fullscreen:0}); $('#cycle_next').trigger('click');assert.equal(c.current,1);$(w.document).trigger($.Event('keyup',{keyCode:39}));assert.equal(c.current,0); +// Focused controls and editable content must retain their own keyboard behavior. +const initialMap=c.current, initialPaused=c.paused; +for (const selector of ['#cycle_next','#cycle_prev','#cycle_pause']) { + $(selector).trigger($.Event('keyup',{keyCode:32})); + $(selector).trigger($.Event('keyup',{keyCode:39})); + assert.equal(c.current,initialMap);assert.equal(c.paused,initialPaused); +} +$('body').append('
Editing
Link'); +for (const selector of ['#wm-input','#wm-editable','#wm-link']) { + $(selector).trigger($.Event('keyup',{keyCode:32})); + assert.equal(c.paused,initialPaused); +} +$(w.document).trigger($.Event('keyup',{keyCode:32}));assert.equal(c.paused,!initialPaused); +assert.equal($('#cycle_pause').attr('aria-pressed'),'true'); +c.start({period:30000,poller_cycle:300000,fullscreen:0});assert.equal($('#cycle_pause').attr('aria-pressed'),'false'); $('.weathermapholder').last().remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.period,300000);c.nextAction();assert.equal(c.current,0); $('.weathermapholder').remove();c.start({period:0,poller_cycle:300000,fullscreen:0});assert.equal(c.nmaps,0);c.stop(); console.log('PASS: translated labels, repeated initialization, one/no-map cases, click/key navigation; automatic period, first countdown, next/previous wrap, pause/resume, automatic transition, progress bounds, configured period and fullscreen reload fallback'); diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php index d99537dd..8b9cef95 100644 --- a/weathermap-cacti-plugin.php +++ b/weathermap-cacti-plugin.php @@ -1034,8 +1034,8 @@ function weathermap_tabs($current_tab) { /** * Render translated, keyboard-accessible cycle controls. * - * @param int $fullscreen Whether Cacti chrome is hidden. - * @param int $group_id Group filter to retain when toggling full screen. + * @param int|string $fullscreen Whether Cacti chrome is hidden. + * @param int|string $group_id Group filter to retain when toggling full screen. * * @return string */ @@ -1044,13 +1044,19 @@ function weathermap_cycle_controls($fullscreen, $group_id) { $controls = ''; $actions = [ 'cycle_stop' => ['fa-stop', __('Stop cycling', 'weathermap'), '?action='], - 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap'), '#'], - 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap'), '#'], - 'cycle_next' => ['fa-forward', __('Next', 'weathermap'), '#'] + 'cycle_prev' => ['fa-backward', __('Previous', 'weathermap')], + 'cycle_pause' => ['fa-pause', __('Pause / resume', 'weathermap')], + 'cycle_next' => ['fa-forward', __('Next', 'weathermap')] ]; foreach ($actions as $id => $action) { - $controls .= ''; + $attributes = ' id="' . $id . '" class="wm-cycle-control fas ' . $action[0] . '" title="' . html_escape($action[1]) . '" aria-label="' . html_escape($action[1]) . '"'; + + if ($id === 'cycle_stop') { + $controls .= ''; + } else { + $controls .= ''; + } } $label = $fullscreen ? __('Exit full screen', 'weathermap') : __('Full screen', 'weathermap'); $url = $config['url_path'] . 'plugins/weathermap/weathermap-cacti-plugin.php?action=viewmapcycle&fullscreen=' . ($fullscreen ? '0' : '1') . '&group=' . $group_id; From af870b2d8ae7092c8bfd3497a8b2815c01104fe3 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Thu, 8 Oct 2026 20:15:18 -0400 Subject: [PATCH 8/8] Allowlist weathermap-cacti-plugin.php for patch coverage The patch-coverage gate fails because the changed production file weathermap-cacti-plugin.php is absent from Clover. It is a web UI entry point: it includes ../../include/auth.php and runs a switch (get_request_var('action')) dispatch at the top level, so it cannot be loaded into the isolated unit process to be measured. The new weathermap_cycle_controls() and the changed weathermap_fullview() lines live in that same un-loadable file. Add it to the unmeasured allowlist alongside the other weathermap entry points, matching the mechanism the gate documents for exactly this case. --- tests/bin/patch-coverage.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/bin/patch-coverage.php b/tests/bin/patch-coverage.php index bea57b42..57fcb3a9 100644 --- a/tests/bin/patch-coverage.php +++ b/tests/bin/patch-coverage.php @@ -163,6 +163,10 @@ function changed_lines($base_ref) { * Empty by default; add entries per repository as the need arises. */ $unmeasured_allowlist = [ + // Web UI entry point: top-level include of ../../include/auth.php plus a + // switch (get_request_var('action')) dispatch that runs at load, so it + // cannot load in the isolated unit process. + 'weathermap-cacti-plugin.php', // Web UI entry point: top-level include of ../../include/auth.php plus // request-var dispatch, so it cannot load in the isolated unit process. 'weathermap-cacti-plugin-mgmt.php',