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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ IMPORTANT NOTE: This version only works on CACTI 1.x++!

--- develop ---

* Add a searchable node icon picker and bundled enterprise site icons

* issue#299: Keep editor picker results open when browsing and allow changing an existing selection

* feature#298: Fit or stretch background images within the map canvas
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ See CHANGELOG.md for the most recent updates, listed by version.

See COPYING for the license under which php-weathermap is released.

## Bundled building icons

The node icon library in `images/objects/` includes modern corporate building,
branch office, hospital, clinic, data centre and warehouse icons. These
transparent PNGs are 96 × 96 pixels and can be selected from the editor's node
icon picker without uploading additional files.

## Compatibility

This version only works with Cacti 1.2.x onwards. A re-write of the user interface
Expand Down
40 changes: 40 additions & 0 deletions css/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,43 @@ body#mainView #wm-node-delete-bar { min-height: 32px; }
.ui-dialog.wm-editor-dialog {
overflow: visible;
}

/* Searchable node icon gallery; keep backgrounds on their existing selector. */
body#mainView .wm-icon-source { display: none; }
body#mainView .wm-icon-picker { width: 320px; max-width: 100%; }
body#mainView .wm-icon-picker summary {
display: flex; align-items: center; gap: 10px; cursor: pointer;
padding: 8px 12px; min-height: 48px; box-sizing: border-box;
border: 1px solid var(--border-color, #888); border-radius: 4px;
background: var(--background, #fff); color: var(--text-color-lighter, #333);
list-style: none;
}
body#mainView .wm-icon-picker summary::-webkit-details-marker { display: none; }
body#mainView .wm-icon-picker summary::after { content: '▾'; margin-left: auto; }
body#mainView .wm-icon-picker[open] summary::after { content: '▴'; }
body#mainView .wm-icon-picker summary img { width: 40px; height: 40px; object-fit: contain; }
body#mainView .wm-icon-panel {
padding: 8px; border: 1px solid var(--border-color, #888); border-top: 0;
background: var(--background, #fff); color: var(--text-color-lighter, #333); box-sizing: border-box;
}
body#mainView .wm-icon-panel input[type="search"] { width: 100%; box-sizing: border-box; margin: 0; caret-color: auto; }
body#mainView .wm-icon-count { font-size: 12px; margin: 6px 0; }
body#mainView .wm-icon-grid {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px; max-height: 220px; overflow-y: auto; overscroll-behavior: contain;
}
body#mainView .wm-icon-grid .wm-icon-choice {
display: flex; flex-direction: column; align-items: center; gap: 5px;
width: 100%; min-width: 0; min-height: 86px; padding: 6px; margin: 0;
border: 1px solid var(--border-color, #888); border-radius: 4px;
background: var(--background, #fff); color: var(--text-color-lighter, #333);
cursor: pointer; white-space: normal; font-size: 12px; line-height: 1.25;
}
body#mainView .wm-icon-choice[hidden] { display: none !important; }
body#mainView .wm-icon-choice img { width: 48px; height: 48px; object-fit: contain; }
body#mainView .wm-icon-choice span { overflow-wrap: anywhere; }
body#mainView .wm-icon-choice:hover,
body#mainView .wm-icon-choice[aria-pressed="true"] {
background: var(--background-lowlight, #eee);
border-color: var(--text-color-highlight-a, #2874aa);
}
Binary file added images/objects/branch-office.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/objects/clinic-red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/objects/corporate-building.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/objects/data-centre.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/objects/hospital-red.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/objects/warehouse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 66 additions & 8 deletions js/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1050,14 +1050,7 @@ function show_node(name) {
if (success) {
$('#action').val('set_node_properties');

if ($('#node_iconfilename.dd-container').length) {
$('#node_iconfilename').ddslick('destroy');
}

$('#node_iconfilename').val(selectedNode).ddslick({
height:240,
defaultSelectedIndex:selectedNode
});
wmNodeIconPicker();

$('.dd-container').on('click', function() {
$('.ui-dialog').css('z-index', '100');
Expand All @@ -1075,6 +1068,71 @@ function show_node(name) {
}
}

function wmNodeIconPicker() {
var select = $('#node_iconfilename');
if (select.hasClass('dd-container')) {
select.ddslick('destroy');
select = $('#node_iconfilename');
}
select.val(selectedNode).addClass('wm-icon-source');
$('#wm-icon-picker').remove();
var picker = $('<details id="wm-icon-picker" class="wm-icon-picker">').insertAfter(select);
var summary = $('<summary>').appendTo(picker);
var panel = $('<div class="wm-icon-panel">').appendTo(picker);
var search = $('<input type="search">').attr({placeholder: wmEditorText.iconSearchPlaceholder, 'aria-label': wmEditorText.iconSearchLabel}).appendTo(panel);
var count = $('<div class="wm-icon-count" role="status" aria-live="polite">').appendTo(panel);
var grid = $('<div class="wm-icon-grid">').attr({role: 'group', 'aria-label': wmEditorText.iconGroupLabel}).appendTo(panel);
function title(option) {
return option.text.replace(/\.(png|gif|jpe?g)$/i, '').replace(/[-_]+/g, ' ');
}
function content(target, option) {
var src = $(option).attr('data-imagesrc');
if (src) $('<img alt="">').attr('src', src).appendTo(target);
$('<span>').text(title(option)).appendTo(target);
}
function updateSelection() {
var option = select.find('option:selected')[0];
summary.empty();
if (option) content(summary, option);
else summary.text(wmEditorText.iconChoosePrompt);
grid.find('button').each(function() {
$(this).attr('aria-pressed', String($(this).data('value') === select.val()));
});
}
select.find('option').each(function() {
var option = this;
var button = $('<button type="button" class="wm-icon-choice">').attr('title', option.text).data('value', option.value).appendTo(grid);
content(button, option);
button.on('click', function() {
select.val(option.value);
updateSelection();
picker.prop('open', false);
summary.trigger('focus');
});
});
search.on('input', function() {
var query = this.value.trim().toLowerCase();
var visible = 0;
grid.find('button').each(function() {
var matches = ($(this).text() + ' ' + $(this).data('value')).toLowerCase().indexOf(query) !== -1;
$(this).prop('hidden', !matches);
if (matches) visible++;
});
count.text(visible ? wmEditorText.iconMatchCount.replace('%s', String(visible)) : wmEditorText.iconNoMatches);
});
picker.on('keydown', function(event) {
if (event.key === 'Escape' && picker.prop('open')) {
event.stopPropagation();
picker.prop('open', false);
summary.trigger('focus');
} else if (event.key === 'Enter' && event.target === search[0]) {
event.preventDefault();
}
});
search.trigger('input');
updateSelection();
}

function prime_link_form(name) {
var mylink = Links[name];

Expand Down
7 changes: 7 additions & 0 deletions lib/editor.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,13 @@ function getEditorJs() {
var addNodeHelp = '<?php print __esc('Click on the map where you would like to add a new node.', 'weathermap'); ?>';

var wmEditorText = <?php print json_encode([
'iconSearchPlaceholder' => __('Search icons…', 'weathermap'),
'iconSearchLabel' => __('Search node icons', 'weathermap'),
'iconGroupLabel' => __('Node icons', 'weathermap'),
'iconChoosePrompt' => __('Choose an icon', 'weathermap'),
'iconMatchCount' => __('Matching icons: %s', '%s', 'weathermap'),
'iconNoMatches' => __('No matching icons', 'weathermap'),

'currentInterface' => __('Current interface', 'weathermap'),
'advanced' => __('Advanced settings', 'weathermap'),
'hideAdvanced' => __('Hide advanced settings', 'weathermap'),
Expand Down
47 changes: 41 additions & 6 deletions locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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-12 01:30+1030\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Cacti Developers <developers@cacti.net>\n"
"Language-Team: Cacti Developers <developers@cacti.net>\n"
Expand Down Expand Up @@ -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 ""
Expand Down Expand Up @@ -166,6 +176,31 @@ msgstr ""
msgid "Click on the map where you would like to add a new node."
msgstr ""

#: lib/editor.inc.php
msgid "Search icons…"
msgstr ""

#: lib/editor.inc.php
msgid "Search node icons"
msgstr ""

#: lib/editor.inc.php
msgid "Node icons"
msgstr ""

#: lib/editor.inc.php
msgid "Choose an icon"
msgstr ""

#: lib/editor.inc.php
#, php-format
msgid "Matching icons: %s"
msgstr ""

#: lib/editor.inc.php
msgid "No matching icons"
msgstr ""

#: lib/editor.inc.php
msgid "Current interface"
msgstr ""
Expand Down Expand Up @@ -2009,6 +2044,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!"
Expand Down Expand Up @@ -2142,7 +2181,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 ""
37 changes: 37 additions & 0 deletions tests/Browser/EditorIconPickerTest.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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('<form><select name="node_iconfilename" id="node_iconfilename"><option value="--NONE--">Label Only</option><option value="hospital.png" data-imagesrc="images/objects/hospital.png">hospital-red.png</option><option value="switch.png">network-switch.png</option></select></form>',{runScripts:'outside-only',pretendToBeVisual:true});
const w=dom.window,$=jquery(w);w.$=w.jQuery=$;w.selectedNode='hospital.png';
const inc=fs.readFileSync(path.join(__dirname,'../../lib/editor.inc.php'),'utf8');
w.wmEditorText={};
for(const match of inc.matchAll(/'(icon[A-Za-z]+)' => __\('([^']+)'/g))w.wmEditorText[match[1]]=match[2];
w.wmEditorText.iconSearchPlaceholder='Rechercher les icônes';
w.wmEditorText.iconMatchCount='Icônes correspondantes : %s';
const source=fs.readFileSync(path.join(__dirname,'../../js/editor.js'),'utf8');
w.eval(source.slice(source.indexOf('function wmNodeIconPicker('),source.indexOf('function prime_link_form(')));
w.wmNodeIconPicker();
assert.equal($('input[type=search]').attr('placeholder'),'Rechercher les icônes');
assert.equal($('.wm-icon-grid').attr('role'),'group');
assert.equal($('.wm-icon-grid').attr('aria-label'),w.wmEditorText.iconGroupLabel);
assert.equal($('#wm-icon-picker summary').text(),'hospital red');
$('#wm-icon-picker').prop('open',true);
$('input[type=search]').val('switch').trigger('input');
assert.equal($('.wm-icon-choice').filter((i,e)=>!e.hidden).length,1);
assert.equal($('.wm-icon-count').text(),'Icônes correspondantes : 1');
$('.wm-icon-choice').filter((i,e)=>!e.hidden).trigger('click');
assert.equal($('#node_iconfilename').val(),'switch.png');
assert.equal($('form').serialize(),'node_iconfilename=switch.png');
assert.equal($('#wm-icon-picker').prop('open'),false);
w.selectedNode='--NONE--';w.wmNodeIconPicker();
assert.equal($('#wm-icon-picker').length,1);
assert.equal($('#wm-icon-picker summary').text(),'Label Only');
$('#wm-icon-picker').prop('open',true).trigger($.Event('keydown',{key:'Escape'}));
assert.equal($('#wm-icon-picker').prop('open'),false);
let escaped=0;$(w.document).on('keydown',e=>{if(e.key==='Escape')escaped++});
$('#wm-icon-picker summary').trigger($.Event('keydown',{key:'Escape'}));assert.equal(escaped,1,'closed picker lets Escape close its parent dialog');
$('input[type=search]').val('nothing-matches').trigger('input');
assert.equal($('.wm-icon-count').text(),'No matching icons');
console.log('Icon picker selection, search, reopening and Escape PASS');