class PhysicalPositionMenu {
// 1) Server-side: provide the option groups
getAllMenuData() {
return {
positionGroups: [
{ group: 'Positional', options: [
{ value: 'upstream', label: '→ Upstream', icon: '→'}, //flow is then typically left to right
{ value: 'atEquipment', label: '⊥ in place' , icon: '⊥' },
{ value: 'downstream', label: '← Downstream' , icon: '←' }
]
}
],
// Distance contexts for each position
distanceContexts: {
upstream: {
description: 'Distance from parent inlet',
placeholder: 'e.g., 2.5 (meters before parent)',
helpText: 'How far upstream from the parent equipment'
},
downstream: {
description: 'Distance from parent outlet',
placeholder: 'e.g., 3.0 (meters after parent)',
helpText: 'How far downstream from the parent equipment'
},
atEquipment: {
description: 'Distance from parent start',
placeholder: 'e.g., 1.2 (meters from start)',
helpText: 'Position within the parent equipment boundaries'
}
}
};
}
// 2) HTML template (pure markup)
getHtmlTemplate() {
return `
Physical Position vs parent
`;
}
// 3) HTML injector
getHtmlInjectionCode(nodeName) {
const tpl = this.getHtmlTemplate()
.replace(/`/g,'\\`').replace(/\$/g,'\\$');
return `
// PhysicalPosition HTML injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.injectHtml = function() {
const ph = document.getElementById('position-fields-placeholder');
if (ph && !ph.hasChildNodes()) {
ph.innerHTML = \`${tpl}\`;
}
};
`;
}
// 4) Data-loader injector
getDataInjectionCode(nodeName) {
return `
// PhysicalPosition data loader for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.loadData = function(node) {
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
const sel = document.getElementById('node-input-positionVsParent');
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
const distanceInput = document.getElementById('node-input-distance');
const distanceSection = document.getElementById('distance-section');
//Load position options
if (sel) {
sel.innerHTML = '';
(data.positionGroups||[]).forEach(grp => {
const optg = document.createElement('optgroup');
optg.label = grp.group;
grp.options.forEach(o=>{
const opt = document.createElement('option');
opt.value = o.value;
opt.textContent = o.label;
opt.setAttribute('data-icon', o.icon);
optg.appendChild(opt);
});
sel.appendChild(optg);
});
sel.value = node.positionVsParent || 'atEquipment';
}
//Load distance values
if (hasDistanceCheck) {
hasDistanceCheck.checked = node.hasDistance || false;
distanceSection.style.display = hasDistanceCheck.checked ? 'block' : 'none';
}
if (distanceInput) {
distanceInput.value = node.distance || '';
}
// Update distance context for current position
this.updateDistanceContext(node.positionVsParent || 'atEquipment', data.distanceContexts);
};
`;
}
// 5) (no special events needed, but stub for symmetry)
getEventInjectionCode(nodeName) {
return `
// PhysicalPosition events for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.wireEvents = function(node) {
const positionSel = document.getElementById('node-input-positionVsParent');
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
const distanceSection = document.getElementById('distance-section');
const data = window.EVOLV.nodes.${nodeName}.menuData.position;
// Toggle distance section visibility
if (hasDistanceCheck && distanceSection) {
hasDistanceCheck.addEventListener('change', function() {
distanceSection.style.display = this.checked ? 'block' : 'none';
// Clear distance if unchecked
if (!this.checked) {
const distanceInput = document.getElementById('node-input-distance');
if (distanceInput) {
distanceInput.value = '';
}
}
});
}
// Update distance context when position changes
if (positionSel) {
positionSel.addEventListener('change', function() {
const position = this.value;
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext(position, data.distanceContexts);
});
}
};
// Helper function to update distance context
window.EVOLV.nodes.${nodeName}.positionMenu.updateDistanceContext = function(position, contexts) {
const distanceInput = document.getElementById('node-input-distance');
const distanceHelp = document.getElementById('distance-help');
const context = contexts && contexts[position];
if (context && distanceInput && distanceHelp) {
distanceInput.placeholder = context.placeholder || '0.0';
distanceHelp.textContent = context.helpText || 'Enter distance in meters';
}
};
`;
}
// 6) Save-logic injector
getSaveInjectionCode(nodeName) {
return `
// PhysicalPosition Save injection for ${nodeName}
window.EVOLV.nodes.${nodeName}.positionMenu.saveEditor = function(node) {
console.log("=== PhysicalPosition Save Debug ===");
const sel = document.getElementById('node-input-positionVsParent');
const hasDistanceCheck = document.getElementById('node-input-hasDistance');
const distanceInput = document.getElementById('node-input-distance');
console.log("→ sel element found:", !!sel);
console.log("→ sel:", sel);
console.log("→ sel.value:", sel ? sel.value : "NO ELEMENT");
console.log("→ sel.selectedIndex:", sel ? sel.selectedIndex : "NO ELEMENT");
console.log("→ sel.options:", sel ? Array.from(sel.options).map(o => ({value: o.value, text: o.textContent})) : "NO OPTIONS");
if (!sel) {
console.error("→ positionMenu.saveEditor FAILED: select element not found!");
return false;
}
// Save existing position data
const positionValue = sel.value;
const selectedOption = sel.options[sel.selectedIndex];
console.log("→ positionValue:", positionValue);
console.log("→ selectedOption:", selectedOption);
console.log("→ selectedOption.textContent:", selectedOption ? selectedOption.textContent : "NO OPTION");
console.log("→ selectedOption data-icon:", selectedOption ? selectedOption.getAttribute('data-icon') : "NO ICON");
node.positionVsParent = positionValue || 'atEquipment';
node.positionLabel = selectedOption ? selectedOption.textContent : 'At Equipment';
node.positionIcon = selectedOption ? selectedOption.getAttribute('data-icon') : 'fa fa-cog';
console.log("→ node.positionVsParent set to:", node.positionVsParent);
console.log("→ node.positionLabel set to:", node.positionLabel);
// Save distance data
console.log("→ hasDistanceCheck found:", !!hasDistanceCheck);
console.log("→ hasDistanceCheck.checked:", hasDistanceCheck ? hasDistanceCheck.checked : "NO ELEMENT");
node.hasDistance = hasDistanceCheck ? hasDistanceCheck.checked : false;
if (node.hasDistance && distanceInput && distanceInput.value) {
console.log("→ distanceInput.value:", distanceInput.value);
node.distance = parseFloat(distanceInput.value) || 0;
node.distanceUnit = 'm'; // Fixed to meters for now
// Generate distance description based on position
const contexts = window.EVOLV.nodes.${nodeName}.menuData.position.distanceContexts;
const context = contexts && contexts[node.positionVsParent];
node.distanceDescription = context ? context.description : 'Distance from parent';
console.log("→ distance set to:", node.distance);
} else {
console.log("→ clearing distance data");
delete node.distance;
delete node.distanceUnit;
delete node.distanceDescription;
}
console.log("→ positionMenu.saveEditor result: SUCCESS");
console.log("→ final node.positionVsParent:", node.positionVsParent);
return true;
};
`;
}
// 7) Compose everything into one client bundle
getClientInitCode(nodeName) {
const htmlCode = this.getHtmlInjectionCode(nodeName);
const dataCode = this.getDataInjectionCode(nodeName);
const eventCode = this.getEventInjectionCode(nodeName);
const saveCode = this.getSaveInjectionCode(nodeName);
return `
// --- PhysicalPositionMenu for ${nodeName} ---
window.EVOLV.nodes.${nodeName}.positionMenu =
window.EVOLV.nodes.${nodeName}.positionMenu || {};
${htmlCode}
${dataCode}
${eventCode}
${saveCode}
// hook into oneditprepare
window.EVOLV.nodes.${nodeName}.positionMenu.initEditor = function(node) {
this.injectHtml();
this.loadData(node);
this.wireEvents(node);
};
`;
}
}
module.exports = PhysicalPositionMenu;