class PhysicalPositionMenu { // 1) Server-side: provide the option groups getAllMenuData() { return { positionGroups: [ { group: 'Positional', options: [ { value: 'upstream', label: '← Upstream', icon: '←'}, { 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;