From 8dba641bbb337b3f4aa250025fecad7bfd35083e Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:45:06 +1030 Subject: [PATCH 01/12] Fix editor action state, delete controls and theme contrast --- css/editor.css | 31 +++++++++++- js/editor.js | 43 +++++++++++----- lib/editor.actions.php | 3 ++ lib/editor.inc.php | 9 ++-- locales/po/cacti.pot | 54 +++++++++++++++----- tests/Browser/EditorContextMenuTest.cjs | 59 ++++++++++++++++++++++ tests/Browser/EditorPickerLayeringTest.cjs | 1 + tests/Unit/EditorJsStringsTest.php | 2 +- weathermap-cacti-plugin-editor.php | 20 ++++---- weathermap-cacti-plugin-mgmt.php | 1 + 10 files changed, 184 insertions(+), 39 deletions(-) create mode 100644 tests/Browser/EditorContextMenuTest.cjs diff --git a/css/editor.css b/css/editor.css index 4f8571b1..112bccd6 100644 --- a/css/editor.css +++ b/css/editor.css @@ -26,9 +26,12 @@ input { margin-right: auto; border: 0px; padding: 0px; - cursor: crosshair; + cursor: auto; } +#xycapture, #existingdata.wm-placement-mode { cursor: crosshair; } +#existingdata:not(.wm-placement-mode) + map area { cursor: pointer; } + #frmMain { position: relative; top: 0; @@ -86,6 +89,12 @@ input { padding: 2px; } +#tb_help { + font-size: 13px; + font-weight: normal; + font-style: italic; +} + #toolbar ul li { //display:table; } @@ -316,3 +325,23 @@ body#mainView #wm-node-delete-bar { min-height: 32px; } .ui-dialog.wm-editor-dialog { overflow: visible; } +/* Use Cacti's own theme heading colours in every grouped editor dialog. */ +.dlgProperties .wm-editor-section { display: table; width: 100%; box-sizing: border-box; margin: 10px 0 6px; } +.dlgProperties table .wm-editor-section { margin: 0; } +/* Destructive node/link controls share readable red styling in every theme. */ +#node_delete, #link_delete, #note_delete, +.wm-delete-action, .wm-delete-action .ui-menu-item-wrapper { + color: #c62828 !important; + background: #fff !important; +} +#node_delete, #link_delete, #note_delete, button.wm-delete-action { + border: 1px solid #c62828 !important; +} + +/* Standalone dialogs need a matching text/surface pair in legacy dark themes. */ +body#mainView .ui-dialog .ui-button:not(.wm-asset-delete):not(.wm-image-delete):not(#note_delete):not(.wm-delete-action):not(#node_delete):not(#link_delete):not(.wm-icon-choice):not(.wm-note-colour), +body#mainView .ui-dialog input.ui-state-default, +body#mainView .ui-dialog textarea.ui-state-default { + background: var(--background-content, #e6e6e6) !important; + color: var(--text-color-lighter, #333) !important; +} diff --git a/js/editor.js b/js/editor.js index dad82c1a..0fc2e3da 100644 --- a/js/editor.js +++ b/js/editor.js @@ -379,6 +379,7 @@ $(function() { function initJS() { // if the xycapture element is there, then we are in the main edit screen if ($('#xycapture').length) { + if ($('#tb_help').data('idle-help') === undefined) $('#tb_help').data('idle-help', $('#tb_help').text()); attach_click_events(); attach_help_events(); //show_context_help('node_label', 'node_help'); @@ -433,7 +434,7 @@ function initContextMenu() { {title: txtMove, cmd: 'move', uiIcon: 'ui-icon-arrow-4'}, {title: txtClone, cmd: 'clone', uiIcon: 'ui-icon-copy'}, {title: txtEdit, cmd: 'edit', uiIcon: 'ui-icon-pencil'}, - {title: txtDelete, cmd: 'delete', uiIcon: 'ui-icon-trash'}, + {title: txtDelNode, cmd: 'delete', uiIcon: 'ui-icon-trash', addClass: 'wm-delete-action'}, {title: "----"}, {title: txtProperties, cmd: 'properties', uiIcon: 'ui-icon-gear'} ]; @@ -443,7 +444,7 @@ function initContextMenu() { {title: txtTidy, cmd: 'tidy', uiIcon: 'ui-icon-arrow-4'}, {title: txtVia, cmd: 'via', uiIcon: 'ui-icon-copy'}, {title: txtEdit, cmd: 'edit', uiIcon: 'ui-icon-pencil'}, - {title: txtDelete, cmd: 'delete', uiIcon: 'ui-icon-trash'}, + {title: txtDelLink, cmd: 'delete', uiIcon: 'ui-icon-trash', addClass: 'wm-delete-action'}, {title: "----"}, {title: txtProperties, cmd: 'properties', uiIcon: 'ui-icon-gear'} ]; @@ -461,6 +462,10 @@ function initContextMenu() { }, beforeOpen: function(event, ui) { var target = ui.target[0].id; + if (target.startsWith('LINK') || target.startsWith('NODE')) { + $('#action').val(''); + mapmode('existing'); + } if (target.startsWith('LINK')) { $(this).contextmenu('replaceMenu', linkMenu); @@ -658,7 +663,7 @@ function click_execute(event, alt) { if (objecttype == 'NODE' && $('#action').val() == 'add_link') { $('#param').val(NodeIDs[objectid]); $('#action').val('add_link2'); - $('#tb_help').text('Click on the second node for the end of the link.'); + $('#tb_help').addClass('wm-action-help').text('Click on the second node for the end of the link.'); } else if (objecttype == 'NODE' && $('#action').val() == 'add_link2') { $('#param2').val(NodeIDs[objectid]); form_submit(); @@ -714,7 +719,15 @@ function new_file() { self.location = '?action=newfile'; } +function wmResetActionHelp() { + $('#tb_help').removeClass('wm-action-help').text($('#tb_help').data('idle-help')); +} + function mapmode(m) { + var placing = m == 'xy' || $('#action').val() == 'add_link'; + $('#existingdata').toggleClass('wm-placement-mode', placing); + $('#tb_help').toggleClass('wm-action-help', placing); + if (!placing) wmResetActionHelp(); if (m == 'xy') { $('#debug').val('xy'); $('#xycapture').show(); @@ -738,7 +751,7 @@ function setCanvasSize(element) { } function add_node() { - $('#tb_help').text(addNodeHelp); + $('#tb_help').addClass('wm-action-help').text(addNodeHelp); $('#action').val('add_node'); mapmode('xy'); @@ -753,13 +766,14 @@ function delete_node() { var connected = Object.keys(Links).filter(function(key) { return Links[key].a === name || Links[key].b === name; }).length; var message = delNodePrompt.replace('%s', function() { return wmNodeDisplayName(name); }); if (connected) { - message += ' ' + delNodeConnected.replace('%s', String(connected)); + message += ' ' + (connected === 1 && typeof delNodeConnectedSingle !== 'undefined' ? delNodeConnectedSingle : delNodeConnected).replace('%s', String(connected)); } $('.dlgConfirm').text(message + ' ' + delNodeKeepDevice); - mapmode('xy'); + mapmode('existing'); $('.dlgConfirm').dialog({ + close: function() { mapmode('existing'); }, resizable: false, title: delNodeTitle, height: 'auto', @@ -775,6 +789,7 @@ function delete_node() { }, { text: txtDelNode, + 'class': 'wm-delete-action', click: function() { $(this).dialog('close'); hide_all_dialogs(); @@ -807,7 +822,7 @@ function edit_link() { function move_node() { hide_dialog('dlgNodeProperties'); - $('#tb_help').text(moveNodeHelp); + $('#tb_help').addClass('wm-action-help').text(moveNodeHelp); $('#action').val('move_node'); mapmode('xy'); @@ -816,14 +831,14 @@ function move_node() { function via_link() { hide_dialog('dlgLinkProperties'); - $('#tb_help').text(viaLinkHelp); + $('#tb_help').addClass('wm-action-help').text(viaLinkHelp); $('#action').val('via_link'); mapmode('xy'); } function add_link() { - $('#tb_help').text(addLinkHelp); + $('#tb_help').addClass('wm-action-help').text(addLinkHelp); $('#action').val('add_link'); mapmode('existing'); @@ -841,9 +856,10 @@ function delete_link() { }); $('.dlgConfirm').text(prompt + ' ' + wmEditorText.keepLinkNodes); - mapmode('xy'); + mapmode('existing'); $('.dlgConfirm').dialog({ + close: function() { mapmode('existing'); }, resizable: false, title: delLinkTitle, height: 'auto', @@ -859,6 +875,7 @@ function delete_link() { }, { text: txtDelLink, + 'class': 'wm-delete-action', click: function() { $(this).dialog('close'); hide_all_dialogs(); @@ -924,7 +941,7 @@ function map_style() { } function position_timestamp() { - $('#tb_help').text(timeStHelp); + $('#tb_help').addClass('wm-action-help').text(timeStHelp); $('#action').val('place_stamp'); mapmode('xy'); @@ -960,7 +977,7 @@ function position_legend(event) { } function real_position_legend(scalename) { - $('#tb_help').text(posLegendHelp); + $('#tb_help').addClass('wm-action-help').text(posLegendHelp); $('#action').val('place_legend'); $('#param').val(scalename); @@ -1139,6 +1156,8 @@ show_dialog('dlgLinkProperties'); } function show_dialog(dlg) { + mapmode('existing'); + wmResetActionHelp(); if (dlg == 'dlgMapProperties') { var selectedNode = $('#map_bgfile').val(); diff --git a/lib/editor.actions.php b/lib/editor.actions.php index bf976588..8117a40b 100644 --- a/lib/editor.actions.php +++ b/lib/editor.actions.php @@ -952,6 +952,9 @@ function editorSettings($mapfile) { $use_overlay = (isset_request_var('editorsettings_showvias') ? intval(get_nfilter_request_var('editorsettings_showvias')) : false); $use_relative_overlay = (isset_request_var('editorsettings_showrelative') ? intval(get_nfilter_request_var('editorsettings_showrelative')) : false); $grid_snap_value = (isset_request_var('editorsettings_gridsnap') ? intval(get_nfilter_request_var('editorsettings_gridsnap')) : 0); + + // This AJAX action exits before the entry page's cookie write. + cacti_cookie_set('wmeditor', ($use_overlay ? '1' : '0') . ':' . ($use_relative_overlay ? '1' : '0') . ':' . intval($grid_snap_value)); } /** diff --git a/lib/editor.inc.php b/lib/editor.inc.php index ba03d20b..094f0507 100644 --- a/lib/editor.inc.php +++ b/lib/editor.inc.php @@ -1051,9 +1051,10 @@ function getEditorJs() { 'deleteLinkPrompt' => __('Remove the link between %1$s and %2$s?', 'weathermap'), 'keepLinkNodes' => __('The nodes and Cacti graphs will remain.', 'weathermap'), ], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>; - var delNodePrompt = ; - var delNodeConnected = ; - var delNodeKeepDevice = ; + var delNodePrompt = ; + var delNodeConnected = ; + var delNodeConnectedSingle = ; + var delNodeKeepDevice = ; var delNodeWarning = ''; var delNodeTitle = ''; @@ -1068,7 +1069,7 @@ function getEditorJs() { var txtLinkActions = ''; var txtMove = ''; var txtClone = ''; - var txtEdit = ''; + var txtEdit = ''; var txtDelete = ''; var txtVia = ''; var txtTidy = ''; diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index 6963759a..cdeed42f 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Cacti\n" +"Project-Id-Version: Cacti 1.7\n" "Report-Msgid-Bugs-To: developers@cacti.net\n" -"POT-Creation-Date: 2026-10-11 10:30+1030\n" +"POT-Creation-Date: 2026-10-11 20:34+1030\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Cacti Developers \n" "Language-Team: Cacti Developers \n" @@ -89,6 +89,16 @@ msgstr "" msgid "Link Comment Colour" msgstr "" +#: lib/editor.config-delete.php +#, php-format +msgid "Delete configuration file %s" +msgstr "" + +#: lib/editor.config-delete.php +#, php-format +msgid "Are you sure you want to permanently delete the configuration file \"%s\"?" +msgstr "" + #: lib/editor.inc.php msgid "Next results" msgstr "" @@ -328,16 +338,21 @@ msgstr "" #: lib/editor.inc.php #, php-format -msgid "Remove %s from this map?" +msgid "Delete %s from this map?" +msgstr "" + +#: lib/editor.inc.php +#, php-format +msgid "This will also remove %s connected links." msgstr "" #: lib/editor.inc.php #, php-format -msgid "Connected links to remove: %s." +msgid "This will also remove %s connected link." msgstr "" #: lib/editor.inc.php -msgid "The Cacti device and graphs will remain." +msgid "The Cacti device and its graphs will remain." msgstr "" #: lib/editor.inc.php @@ -385,9 +400,8 @@ msgstr "" msgid "Clone" msgstr "" -#: lib/editor.inc.php weathermap-cacti-plugin-editor.php -#: weathermap-cacti-plugin-mgmt.php -msgid "Edit" +#: lib/editor.inc.php +msgid "Edit Configuration" msgstr "" #: lib/editor.inc.php weathermap-cacti-plugin-editor.php @@ -1046,6 +1060,10 @@ msgstr "" msgid "Graph Selector" msgstr "" +#: weathermap-cacti-plugin-editor.php weathermap-cacti-plugin-mgmt.php +msgid "Edit" +msgstr "" + #: weathermap-cacti-plugin-editor.php msgid "Save" msgstr "" @@ -1282,6 +1300,18 @@ msgstr "" msgid "Link Hover Height (pixels)" msgstr "" +#: weathermap-cacti-plugin-editor.php +msgid "Edit Object Configuration" +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "Edit the raw configuration for this map object." +msgstr "" + +#: weathermap-cacti-plugin-editor.php +msgid "Advanced: changes are saved directly to the map configuration without syntax validation. Use Properties for the normal editing controls." +msgstr "" + #: weathermap-cacti-plugin-mgmt.php msgid "Duplicate" msgstr "" @@ -2009,6 +2039,10 @@ msgstr "" msgid "Unable to delete the last remaining Map Group." msgstr "" +#: weathermap-cacti-plugin-mgmt.php +msgid "Map title is too long. Use at most 4088 bytes after escaping." +msgstr "" + #: weathermap-cacti-plugin-mgmt.php #, php-format msgid "The New Map File name %s already exists!" @@ -2142,7 +2176,3 @@ msgstr "" #: weathermap-cacti-plugin.php msgid "Paused" msgstr "" - -#: weathermap-cacti-plugin-mgmt.php -msgid "Map title is too long. Use at most 4088 bytes after escaping." -msgstr "" diff --git a/tests/Browser/EditorContextMenuTest.cjs b/tests/Browser/EditorContextMenuTest.cjs new file mode 100644 index 00000000..822bef04 --- /dev/null +++ b/tests/Browser/EditorContextMenuTest.cjs @@ -0,0 +1,59 @@ +// Exercise real editor mode and confirmation callbacks with jQuery UI. +const { JSDOM } = require('jsdom'); +const jquery = require('jquery'); +const fs = require('node:fs'); +const path = require('node:path'); +const assert = require('node:assert/strict'); +const dom = new JSDOM('
', {runScripts:'outside-only',pretendToBeVisual:true}); +const w = dom.window; +const $ = jquery(w); +w.$ = w.jQuery = $; +w.eval(fs.readFileSync(path.join(process.env.CACTI_JS_DIR, 'jquery-ui.js'),'utf8')); +const src=fs.readFileSync(path.join(__dirname,'../../js/editor.js'),'utf8'); +w.eval(src.slice(src.indexOf('function wmResetActionHelp('),src.indexOf('function add_node('))); +w.eval(src.slice(src.indexOf('function delete_node('),src.indexOf('function clone_node('))); +w.eval(src.slice(src.indexOf('function delete_link('),src.indexOf('function form_submit('))); +Object.assign(w,{Links:{uplink:{a:'dc',b:'office'}},delNodePrompt:'Delete %s from this map?',delNodeConnectedSingle:'This will also remove %s connected link.',delNodeConnected:'This will also remove %s connected links.',delNodeKeepDevice:'The Cacti device and its graphs will remain.',delNodeTitle:'Delete Node Confirmation',txtCancel:'Cancel',txtDelNode:'Delete Node',delLinkTitle:'Delete Link Confirmation',txtDelLink:'Delete Link',wmEditorText:{deleteLinkPrompt:'Remove %1$s to %2$s?',keepLinkNodes:'Nodes remain.'},wmNodeDisplayName:n=>n==='dc'?'Datacentre':n}); +$('#action').val('move_node');w.mapmode('xy'); +assert.ok($('#existingdata').hasClass('wm-placement-mode')); +$('#action').val('');w.mapmode('existing'); +assert.ok(!$('#existingdata').hasClass('wm-placement-mode')); +$('#action').val('add_link');w.mapmode('existing'); +assert.ok($('#existingdata').hasClass('wm-placement-mode')); +$('#action').val(''); +w.delete_node(); +assert.equal($('#existingdata').css('display')==='none',false); +assert.equal($('#xycapture').css('display'),'none'); +assert.ok(!$('#existingdata').hasClass('wm-placement-mode')); +assert.match($('.dlgConfirm').text(),/remove 1 connected link\./); +assert.equal($('.dlgConfirm').dialog('widget').find('button.wm-delete-action').text(),'Delete Node'); +// Title-bar X and Escape both run jQuery UI's close callback. +$('.dlgConfirm').dialog('close'); +assert.ok(!$('#existingdata').hasClass('wm-placement-mode')); +delete w.delNodeConnectedSingle; +w.delete_node(); +assert.match($('.dlgConfirm').text(),/connected links/); +let submitted; +w.hide_all_dialogs=()=>$('#action').val(''); +w.form_submit=()=>{submitted=$('input').serializeArray();}; +$('.dlgConfirm').dialog('option','buttons')[1].click.call($('.dlgConfirm')[0]); +assert.equal(submitted.find(x=>x.name==='action').value,'delete_node'); +assert.equal(submitted.find(x=>x.name==='param').value,'dc'); +assert.equal($('#action').val(),'delete_node'); +assert.equal($('#param').val(),'dc'); +w.delete_link(); +assert.equal($('.dlgConfirm').dialog('widget').find('button.wm-delete-action').text(),'Delete Link'); +assert.equal($('#xycapture').css('display'),'none'); +$('.dlgConfirm').dialog('close'); +assert.ok(!$('#existingdata').hasClass('wm-placement-mode')); +let contextOptions; +$.fn.contextmenu=function(options){contextOptions=options;return this;}; +for(const key of ['txtNodeActions','txtMove','txtClone','txtEdit','txtDelete','txtProperties','txtLinkActions','txtTidy','txtVia'])w[key]=key; +w.eval(src.slice(src.indexOf('function initContextMenu('),src.indexOf('function contextAction('))); +w.initContextMenu(); +$('#action').val('add_link');w.mapmode('existing'); +contextOptions.beforeOpen.call($('map')[0],{}, {target:$('#NODE\\:1\\:0')}); +assert.equal($('#action').val(),''); +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/EditorPickerLayeringTest.cjs b/tests/Browser/EditorPickerLayeringTest.cjs index d79ee40d..d8ec38c3 100644 --- a/tests/Browser/EditorPickerLayeringTest.cjs +++ b/tests/Browser/EditorPickerLayeringTest.cjs @@ -11,6 +11,7 @@ w.$ = w.jQuery = $; const cactiJs = process.env.CACTI_JS_DIR || path.join(__dirname, '../../../../include/js'); w.eval(fs.readFileSync(path.join(cactiJs, 'jquery-ui.js'), 'utf8')); const source = fs.readFileSync(path.join(__dirname, '../../js/editor.js'), 'utf8'); +w.eval(source.slice(source.indexOf('function wmResetActionHelp('), source.indexOf('function add_node('))); w.eval(source.slice(source.indexOf('function show_dialog('), source.indexOf('function hide_dialog('))); $('#picker').autocomplete({source: ['Interface A'], appendTo: w.document.body}); const menu = $('#picker').autocomplete('widget'); diff --git a/tests/Unit/EditorJsStringsTest.php b/tests/Unit/EditorJsStringsTest.php index c082e0f4..28d8b47e 100644 --- a/tests/Unit/EditorJsStringsTest.php +++ b/tests/Unit/EditorJsStringsTest.php @@ -39,7 +39,7 @@ expect($strings['nodeTitle'])->toBe('Node: %s'); expect($strings['hoverSummary'])->toBe('Hover graphs: %s'); expect($strings['graphSelected'])->toContain('%s'); - expect($output)->toContain('Connected links to remove: %s.', 'Remove %s from this map?'); + expect($output)->toContain('This will also remove %s connected links.', 'This will also remove %s connected link.', 'Delete %s from this map?'); expect($output)->toContain('currentInterface'); expect($output)->toContain('deleteLinkPrompt'); }); diff --git a/weathermap-cacti-plugin-editor.php b/weathermap-cacti-plugin-editor.php index a3ee49bc..7c7512fe 100644 --- a/weathermap-cacti-plugin-editor.php +++ b/weathermap-cacti-plugin-editor.php @@ -320,7 +320,7 @@
  • Settings', 'weathermap'); ?>
  • ---, ---', 'weathermap'); ?>
  • - +
  • @@ -700,7 +700,7 @@
    @@ -736,7 +736,7 @@ @@ -745,7 +745,7 @@ @@ -878,14 +878,14 @@ -
    +
    '>
    -

    You can edit the map items directly here.

    +

    - From this form, you can edit the Mapfile component directly. No syntax checking is done. So, make changes with care. +
    @@ -902,7 +902,8 @@
    -
    +
    keyfont); ?>
    -
    +
    -
    +
    - + + - + +
    Overlays
    Show VIAs overlay
    Placement
    Snap To Grid - + - +
    Overlays
    Show VIAs overlay @@ -921,7 +921,7 @@
    Placement
    Snap To Grid From 77f27d03dabd039359efa7a6606f6e8370c23056 Mon Sep 17 00:00:00 2001 From: alcatron <27321443+alcatron@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:56:05 +1030 Subject: [PATCH 03/12] Apply placement cursor state to real map hotspots --- css/editor.css | 4 ++-- js/editor.js | 2 +- tests/Browser/EditorContextMenuTest.cjs | 5 ++++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/css/editor.css b/css/editor.css index 4cc5b6db..1bb3c9c8 100644 --- a/css/editor.css +++ b/css/editor.css @@ -29,8 +29,8 @@ input { cursor: auto; } -#xycapture, #existingdata.wm-placement-mode { cursor: crosshair; } -.mapData map area { cursor: pointer; } +#xycapture, #existingdata.wm-placement-mode, .mapData.wm-placement-mode map area { cursor: crosshair; } +.mapData:not(.wm-placement-mode) map area { cursor: pointer; } #frmMain { position: relative; diff --git a/js/editor.js b/js/editor.js index 0fc2e3da..f5054fe5 100644 --- a/js/editor.js +++ b/js/editor.js @@ -725,7 +725,7 @@ function wmResetActionHelp() { function mapmode(m) { var placing = m == 'xy' || $('#action').val() == 'add_link'; - $('#existingdata').toggleClass('wm-placement-mode', placing); + $('#existingdata, .mapData').toggleClass('wm-placement-mode', placing); $('#tb_help').toggleClass('wm-action-help', placing); if (!placing) wmResetActionHelp(); if (m == 'xy') { diff --git a/tests/Browser/EditorContextMenuTest.cjs b/tests/Browser/EditorContextMenuTest.cjs index 822bef04..6a0d3d11 100644 --- a/tests/Browser/EditorContextMenuTest.cjs +++ b/tests/Browser/EditorContextMenuTest.cjs @@ -4,11 +4,12 @@ const jquery = require('jquery'); const fs = require('node:fs'); const path = require('node:path'); const 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; const $ = jquery(w); w.$ = w.jQuery = $; w.eval(fs.readFileSync(path.join(process.env.CACTI_JS_DIR, 'jquery-ui.js'),'utf8')); +$('head').append($('