/**
* Presenters for managing consumption and policy stanzas via the UI editor.
*
* Presenters for UI components which manage consumption and policy stanzas via
* the UI editor. These are the locations where action commands are actually held.
*
* @license BSD, see LICENSE.md.
*/
import {ParsedYear, YearMatcher} from "duration";
import {EngineNumber} from "engine_number";
import {MetaSerializer, MetaChangeApplier} from "meta_serialization";
import {GwpLookupPresenter} from "known_substance";
import {NumberParseUtil} from "number_parse_util";
import {
Application,
Command,
DefinitionalStanza,
RetireCommand,
SubstanceBuilder,
} from "ui_translator_components";
import {
NameConflictResolution,
resolveNameConflict,
resolveSubstanceNameConflict,
DuplicateEntityPresenter,
} from "duplicate_util";
import {
ALWAYS_ON_STREAMS,
ENABLEABLE_STREAMS,
STREAM_TARGET_SELECTORS,
} from "ui_editor_const";
import {
buildSetupListButton,
buildUpdateCount,
getEngineNumberValue,
getFieldValue,
getListInput,
getSanitizedFieldValue,
setDuring,
setEngineNumberValue,
setFieldValue,
setListInput,
setupDialogInternalLinks,
setupDurationSelector,
updateDurationSelector,
validateNumericInputs,
} from "ui_editor_util";
import {
initSetCommandUi,
readSetCommandUi,
initChangeCommandUi,
readChangeCommandUi,
initLimitCommandUi,
readLimitCommandUi,
initRechargeCommandUi,
readRechargeCommandUi,
initRecycleCommandUi,
readRecycleCommandUi,
initReplaceCommandUi,
readReplaceCommandUi,
readDurationUi,
} from "ui_editor_strategy";
/**
* Regular expression for matching consumption/policy object identifiers.
* Matches format: "substance" for "application"
*/
const OBJ_IDENTIFIER_REGEX = /"([^"]+)" for "([^"]+)"/;
/**
* Regular expression for matching resolved substance names with optional suffix.
* Matches format: "substance" for "application" (optional suffix)
*/
const RESOLVED_FULL_NAME_REGEX = /^"([^"]+)" for "[^"]+"(\s*\([^)]+\))?$/;
/**
* Manages stream selection availability based on context.
*
* This sub-presenter is responsible for updating the availability of stream options in
* select dropdowns based on which streams are enabled in the current context.
* It handles both consumption context (where streams are determined by checkboxes)
* and policy context (where streams are determined by existing code).
*/
class StreamSelectionAvailabilityUpdater {
/**
* Creates a new StreamSelectionAvailabilityUpdater.
*
* @param {HTMLElement|Document} container - The DOM container element to
* search within for stream controls.
* @param {string|null} context - The context for stream availability
* ('consumption' or 'policy').
*/
constructor(container, context) {
const self = this;
self._container = container;
self._context = context;
}
/**
* Gets enabled streams for the current context.
*
* @param {Object} codeObj - The code object containing substances data.
* @param {string} substanceName - The name of the substance to check.
* @param {string} context - Optional context override. If not provided, uses
* the context provided at construction.
* @returns {Array<string>} Array of enabled stream names.
*/
getEnabledStreamsForCurrentContext(codeObj, substanceName, context = null) {
const actualContext = context || this._context;
if (actualContext === "consumption") {
return this._getCurrentEnabledStreamsFromCheckboxes();
} else if (actualContext === "policy") {
return this._getCurrentEnabledStreamsFromCode(codeObj, substanceName);
}
return [];
}
/**
* Gets the first enabled option from a select element.
*
* @param {HTMLSelectElement} selectElement - The select element to check.
* @returns {string} Value of the first enabled option, or 'sales' as fallback.
*/
getFirstEnabledOption(selectElement) {
const options = selectElement.querySelectorAll("option:not([disabled])");
return options.length > 0 ? options[0].value : "sales";
}
/**
* Updates stream option disabled states based on enabled streams.
*
* @param {HTMLSelectElement} selectElement - The select element to update.
* @param {Array<string>} enabledStreams - Array of enabled stream names.
*/
updateStreamOptionStates(selectElement, enabledStreams) {
const options = selectElement.querySelectorAll("option");
options.forEach((option) => {
const value = option.value;
if (ALWAYS_ON_STREAMS.includes(value)) {
option.removeAttribute("disabled");
} else if (ENABLEABLE_STREAMS.includes(value)) {
if (enabledStreams.includes(value)) {
option.removeAttribute("disabled");
} else {
option.setAttribute("disabled", "disabled");
}
}
});
}
/**
* Updates all stream target dropdowns within the container.
*
* @param {Array<string>} enabledStreams - Array of enabled stream names.
* @param {Function} filterFn - Optional filter function (deprecated).
*/
updateAllStreamTargetDropdowns(enabledStreams, filterFn = null) {
STREAM_TARGET_SELECTORS.forEach((selector) => {
const dropdowns = this._container.querySelectorAll(selector);
dropdowns.forEach((dropdown) => {
this.updateStreamOptionStates(dropdown, enabledStreams);
});
});
}
/**
* Refreshes all stream target dropdowns for a specific substance.
*
* @param {Object} codeObj - The code object containing substances data.
* @param {string} substanceName - The name of the substance whose streams changed.
*/
refreshAllStreamTargetDropdowns(codeObj, substanceName) {
const enabledStreams = this._getEnabledStreamsForSubstance(codeObj, substanceName);
this.updateAllStreamTargetDropdowns(enabledStreams);
}
/**
* Get enabled streams for a substance from code objects.
*
* @param {Object} codeObj - The code object containing substances data.
* @param {string} substanceName - The name of the substance to check.
* @returns {Array<string>} Array of enabled stream names.
* @private
*/
_getEnabledStreamsForSubstance(codeObj, substanceName) {
const substances = codeObj.getSubstances();
const substance = substances.find((s) => s.getName() === substanceName);
if (!substance) {
throw new Error(`Substance "${substanceName}" not found`);
}
const enableCommands = substance.getEnables();
return enableCommands
.map((cmd) => cmd.getTarget())
.filter((x) => ENABLEABLE_STREAMS.includes(x));
}
/**
* Get currently enabled streams from checkboxes.
*
* @returns {Array<string>} Array of enabled stream names.
* @private
*/
_getCurrentEnabledStreamsFromCheckboxes() {
const enabledStreams = [];
const container = this._container;
const enableDomestic = container.querySelector(".enable-domestic-checkbox");
const enableImport = container.querySelector(".enable-import-checkbox");
const enableExport = container.querySelector(".enable-export-checkbox");
const addToEnabledIfChecked = (checkbox, streamName) => {
if (checkbox.checked) {
enabledStreams.push(streamName);
}
};
addToEnabledIfChecked(enableDomestic, "domestic");
addToEnabledIfChecked(enableImport, "import");
addToEnabledIfChecked(enableExport, "export");
return enabledStreams;
}
/**
* Get currently enabled streams from code objects.
*
* @param {Object} codeObj - The code object containing substances data.
* @param {string} substanceName - The name of the substance to check.
* @returns {Array<string>} Array of enabled stream names.
* @private
*/
_getCurrentEnabledStreamsFromCode(codeObj, substanceName) {
const policySubstanceInput = this._container.querySelector(".edit-policy-substance-input");
if (!policySubstanceInput) {
return [];
}
const firstName = policySubstanceInput.options[0].value;
const policySubstanceNameCandidate = policySubstanceInput.value;
const noneSelected = this._hasNoSubstanceSelected(policySubstanceInput);
const policySubstanceName = noneSelected ? firstName : policySubstanceNameCandidate;
return this._getEnabledStreamsForSubstance(codeObj, policySubstanceName);
}
/**
* Determines if a select element has no value selected but has options available.
*
* This occurs in the initial dialog state before user interaction, when the
* select element has options but no value has been explicitly chosen. In such
* cases, we need to default to the first option for stream availability calculation.
*
* @param {HTMLSelectElement} selectElement - The select element to check.
* @returns {boolean} True if no selection with available options, false otherwise.
* @private
*/
_hasNoSubstanceSelected(selectElement) {
const valueSelected = selectElement.value;
if (valueSelected) {
return false;
}
const optionsEnabled = selectElement.options;
if (!optionsEnabled) {
return false;
}
const atLeastOneOption = selectElement.options.length > 0;
return atLeastOneOption;
}
}
/**
* Manages the UI for displaying reminders about current substance and application.
*
* Displays the current substance and application being edited in the UI,
* updating these reminders when selections change. Also provides links to
* return to editing the base properties.
*/
class ReminderPresenter {
/**
* Creates a new ReminderPresenter.
*
* @param {HTMLElement} root - Root DOM element for the reminder UI.
* @param {string} appInputSelector - CSS selector for application input element.
* @param {string} substanceInputSelector - CSS selector for substance input element.
* @param {string} baseTabSelector - CSS selector for base tab element.
* @param {Object} tabs - Tabby tabs instance.
*/
constructor(root, appInputSelector, substanceInputSelector, baseTabSelector, tabs) {
const self = this;
self._root = root;
self._appInputSelector = appInputSelector;
self._substanceInputSelector = substanceInputSelector;
self._baseTabSelector = baseTabSelector;
self._tabs = tabs;
self._setupEvents();
}
/**
* Update the reminder UI elements with the current application and substance values.
*
* This function updates the innerHTML of reminder elements to display
* the current values for application and substance. It ensures that changes
* in the inputs are reflected in the corresponding reminder display fields.
*/
update() {
const self = this;
const applicationInput = self._root.querySelector(self._appInputSelector);
const substanceInput = self._root.querySelector(self._substanceInputSelector);
const substanceDisplays = self._root.querySelectorAll(".reminder-substance");
const appDisplays = self._root.querySelectorAll(".reminder-app");
const embedSubstanceDisplays = self._root.querySelectorAll(".embed-substance-label");
const embedAppDisplays = self._root.querySelectorAll(".embed-app-label");
/**
* Updates a display element with the current substance value.
*
* @param {HTMLElement} display - The display element to update.
*/
const updateSubstance = (display) => {
display.innerHTML = "";
const textNode = document.createTextNode(substanceInput.value);
display.appendChild(textNode);
};
/**
* Updates a display element with the current application value.
*
* @param {HTMLElement} display - The display element to update.
*/
const updateApp = (display) => {
display.innerHTML = "";
const textNode = document.createTextNode(applicationInput.value);
display.appendChild(textNode);
};
appDisplays.forEach(updateApp);
embedAppDisplays.forEach(updateApp);
substanceDisplays.forEach(updateSubstance);
embedSubstanceDisplays.forEach(updateSubstance);
}
/**
* Setup event listeners for the reminder UI.
*
* This method binds change events for the application and substance inputs that update their
* respective displays within the reminder interface. It also binds click events to the edit
* reminder links, managing the tab switch to the base tab selector when activated.
*
* @private
*/
_setupEvents() {
const self = this;
const applicationInput = self._root.querySelector(self._appInputSelector);
const substanceInput = self._root.querySelector(self._substanceInputSelector);
substanceInput.addEventListener("change", () => self.update());
applicationInput.addEventListener("change", () => self.update());
self.update();
const links = self._root.querySelectorAll(".edit-reminder-link");
links.forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
self._tabs.toggle(self._baseTabSelector);
});
});
}
}
/**
* Manages the UI for listing and editing consumption logic.
*
* Manages the UI for listing and editing consumption logic where substances
* are consumed for different applications.
*/
class ConsumptionListPresenter {
/**
* Creates a new ConsumptionListPresenter.
*
* @param {HTMLElement} root - The root DOM element for the consumption list.
* @param {Function} getCodeObj - Callback to get the current code object.
* @param {Function} onCodeObjUpdate - Callback when the code object is
* updated.
*/
constructor(root, getCodeObj, onCodeObjUpdate) {
const self = this;
self._root = root;
self._dialog = self._root.querySelector(".dialog");
self._getCodeObj = getCodeObj;
self._onCodeObjUpdate = onCodeObjUpdate;
self._editingName = null;
self._streamUpdater = new StreamSelectionAvailabilityUpdater(
self._dialog,
"consumption",
);
self._setupDialog();
self.refresh();
}
/**
* Visually and functionally enables the consumption list interface.
*/
enable() {
const self = this;
self._root.classList.remove("inactive");
}
/**
* Visually and functionally disables the consumption list interface.
*/
disable() {
const self = this;
self._root.classList.add("inactive");
}
/**
* Updates the consumption list display with new data.
*
* @param {Object} codeObj - Current code object to display.
*/
refresh(codeObj) {
const self = this;
self._refreshList(codeObj);
}
/**
* Refreshes the consumption list display.
*
* @param {Object} codeObj - The current code object.
* @private
*/
_refreshList(codeObj) {
const self = this;
const consumptionNames = self._getConsumptionNames();
const itemList = d3.select(self._root).select(".item-list");
itemList.html("");
const newItems = itemList.selectAll("li").data(consumptionNames).enter().append("li");
newItems.attr("aria-label", (x) => x);
const buttonsPane = newItems.append("div").classed("list-buttons", true);
newItems
.append("div")
.classed("list-label", true)
.text((x) => x);
buttonsPane
.append("a")
.attr("href", "#")
.on("click", (event, x) => {
event.preventDefault();
self._showDialogFor(x);
})
.text("edit")
.attr("aria-label", (x) => "edit " + x);
buttonsPane.append("span").text(" | ");
buttonsPane
.append("a")
.attr("href", "#")
.on("click", (event, x) => {
event.preventDefault();
self._handleDeleteConsumption(x);
})
.text("delete")
.attr("aria-label", (x) => "delete " + x);
}
/**
* Handles deletion of a consumption record.
*
* Prompts the user for confirmation before deleting the specified consumption.
* Parses the consumption name to extract substance and application, then
* removes it from the code object.
*
* @param {string} consumptionName - The full name of the consumption to delete.
* @private
*/
_handleDeleteConsumption(consumptionName) {
const self = this;
const message = "Are you sure you want to delete " + consumptionName + "?";
const isConfirmed = confirm(message);
if (!isConfirmed) {
return;
}
const codeObj = self._getCodeObj();
const match = consumptionName.match(OBJ_IDENTIFIER_REGEX);
const substance = match[1];
const application = match[2];
codeObj.deleteSubstance(application, substance);
self._onCodeObjUpdate(codeObj);
}
/**
* Sets up the dialog for adding/editing consumption records.
*
* Orchestrates initialization of all dialog components including tabs,
* buttons, command lists, reminders, GWP lookup, and stream checkboxes.
*
* @private
*/
_setupDialog() {
const self = this;
self._tabs = new Tabby("#" + self._dialog.querySelector(".tabs").id);
self._setupDialogButtons();
self._setupDialogCommandLists();
self._setupDialogReminders();
self._setupDialogGwpLookup();
self._setupDialogStreamCheckboxes();
}
/**
* Sets up dialog buttons for add, close, and save actions.
*
* Initializes event handlers for the add link, close button, and save button
* within the consumption dialog.
*
* @private
*/
_setupDialogButtons() {
const self = this;
const addLink = self._root.querySelector(".add-link");
addLink.addEventListener("click", (event) => {
self._showDialogFor(null);
event.preventDefault();
});
const closeButton = self._root.querySelector(".cancel-button");
closeButton.addEventListener("click", (event) => {
self._dialog.close();
event.preventDefault();
});
const saveButton = self._root.querySelector(".save-button");
saveButton.addEventListener("click", (event) => {
event.preventDefault();
if (self._save()) {
self._dialog.close();
}
});
}
/**
* Sets up command list buttons for set, change, limit, and recharge.
*
* Initializes all command list buttons with their respective templates and
* initialization functions. Configures the update callback to hide reminders
* and refresh counts.
*
* @private
*/
_setupDialogCommandLists() {
const self = this;
const updateHints = () => {
const embedReminders = self._root.querySelectorAll(".embed-reminder");
embedReminders.forEach((reminder) => {
reminder.style.display = "none";
});
self._updateCounts();
};
const setupListButton = buildSetupListButton(updateHints);
const addLevelButton = self._root.querySelector(".add-start-button");
const levelList = self._root.querySelector(".level-list");
setupListButton(
addLevelButton,
levelList,
"set-command-template",
(item, root, context) => {
initSetCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"consumption",
);
const addChangeButton = self._root.querySelector(".add-change-button");
const changeList = self._root.querySelector(".change-list");
setupListButton(
addChangeButton,
changeList,
"change-command-template",
(item, root, context) => {
initChangeCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"consumption",
);
const addLimitButton = self._root.querySelector(".add-limit-button");
const limitList = self._root.querySelector(".limit-list");
setupListButton(
addLimitButton,
limitList,
"limit-command-template",
(item, root, context) => {
initLimitCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"consumption",
);
const addRechargeButton = self._root.querySelector(".add-recharge-button");
const rechargeList = self._root.querySelector(".recharge-list");
setupListButton(addRechargeButton, rechargeList, "recharge-command-template", (item, root) =>
initRechargeCommandUi(item, root, self._getCodeObj()),
);
}
/**
* Sets up reminder presenter and internal navigation links.
*
* Initializes the ReminderPresenter to display current application and
* substance context, and configures internal dialog navigation links.
*
* @private
*/
_setupDialogReminders() {
const self = this;
self._reminderPresenter = new ReminderPresenter(
self._dialog,
".edit-consumption-application-input",
".edit-consumption-substance-input",
"#consumption-general",
self._tabs,
);
setupDialogInternalLinks(self._root, self._tabs);
}
/**
* Sets up GWP lookup functionality.
*
* Initializes the GwpLookupPresenter to enable substance GWP value lookup
* from the known_gwp.json database via the lookup link.
*
* @private
*/
_setupDialogGwpLookup() {
const self = this;
const lookupLink = self._root.querySelector("#lookup-gwp");
const substanceInput = self._dialog.querySelector(".edit-consumption-substance-input");
const ghgInput = self._dialog.querySelector(".edit-consumption-ghg-input");
const ghgUnitsInput = self._dialog.querySelector(".edit-consumption-ghg-units-input");
const gwpPathInput = document.getElementById("known-gwp-path");
const jsonPath = gwpPathInput ? gwpPathInput.value : "json/known_gwp.json";
self._gwpLookupPresenter = new GwpLookupPresenter(
lookupLink,
substanceInput,
ghgInput,
ghgUnitsInput,
jsonPath,
);
}
/**
* Sets up enable stream checkbox listeners.
*
* Attaches change event listeners to the domestic, import, and export enable
* checkboxes to trigger visibility and validation updates.
*
* @private
*/
_setupDialogStreamCheckboxes() {
const self = this;
const enableImport = self._dialog.querySelector(".enable-import-checkbox");
const enableDomestic = self._dialog.querySelector(".enable-domestic-checkbox");
const enableExport = self._dialog.querySelector(".enable-export-checkbox");
enableImport.addEventListener("change", () => self._updateSource());
enableDomestic.addEventListener("change", () => self._updateSource());
enableExport.addEventListener("change", () => self._updateSource());
}
/**
* Parses a full substance name into substance and equipment parts.
*
* Splits a compound substance name on the " - " separator to extract the
* base substance name and equipment model. Returns empty strings if the
* input is null or undefined.
*
* @param {string} fullSubstanceName - The full substance name potentially
* containing equipment model (e.g., "R-410A - Model X").
* @returns {{substance: string, equipment: string}} Object with parsed parts.
* @private
*/
_getSubstanceAndEquipment(fullSubstanceName) {
if (!fullSubstanceName) {
return {substance: "", equipment: ""};
}
const parts = fullSubstanceName.split(" - ");
return {
substance: parts[0] || "",
equipment: parts.slice(1).join(" - ") || "",
};
}
/**
* Gets effective substance and equipment from object or returns defaults.
*
* When an existing object is provided, parses its name to extract substance
* and equipment parts. Returns empty strings for both if object is null.
*
* @param {Object|null} objToShow - The substance object to extract from.
* @returns {{substance: string, equipment: string}} Parsed substance and equipment.
* @private
*/
_getEffectiveSubstanceAndEquipment(objToShow) {
if (objToShow) {
return this._getSubstanceAndEquipment(objToShow.getName());
} else {
return {substance: "", equipment: ""};
}
}
/**
* Gets value from target or returns fallback if target is null.
*
* Null-safe accessor that calls getValue() on the target object if available,
* otherwise returns the provided fallback value.
*
* @param {Object|null} target - The target object with getValue() method.
* @param {*} fallback - The fallback value to return if target is null.
* @returns {*} The target's value or the fallback.
* @private
*/
_getValueOrDefault(target, fallback) {
const needDefault = target === null;
return needDefault ? fallback : target.getValue();
}
/**
* Shows the dialog for editing a consumption record.
*
* @param {string|null} name - Name of consumption to edit. Pass null for a
* new record.
* @private
*/
_showDialogFor(name) {
const self = this;
self._editingName = name;
self._tabs.toggle("#consumption-general");
if (name === null) {
self._dialog.querySelector(".action-title").innerHTML = "Add";
} else {
self._dialog.querySelector(".action-title").innerHTML = "Edit";
}
const codeObj = self._getCodeObj();
const getObjToShow = () => {
if (name === null) {
return {obj: null, application: ""};
}
const match = name.match(OBJ_IDENTIFIER_REGEX);
const substance = match[1];
const application = match[2];
const substanceObj = codeObj.getApplication(application).getSubstance(substance);
return {obj: substanceObj, application: application};
};
const objToShowInfo = getObjToShow();
const objToShow = objToShowInfo["obj"];
const applicationName = objToShowInfo["application"];
const applicationNames = codeObj.getApplications().map((x) => x.getName());
const applicationSelect = self._dialog.querySelector(".application-select");
d3.select(applicationSelect)
.html("")
.selectAll("option")
.data(applicationNames)
.enter()
.append("option")
.attr("value", (x) => x)
.text((x) => x);
const substanceAndEquipment = self._getEffectiveSubstanceAndEquipment(objToShow);
setFieldValue(
self._dialog.querySelector(".edit-consumption-substance-input"),
objToShow,
"",
(x) => substanceAndEquipment.substance,
);
setFieldValue(
self._dialog.querySelector(".edit-consumption-application-input"),
objToShow,
applicationNames[0],
(x) => applicationName,
);
setFieldValue(
self._dialog.querySelector(".edit-consumption-equipment-input"),
objToShow,
"",
(x) => substanceAndEquipment.equipment,
);
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-ghg-input"),
self._dialog.querySelector(".edit-consumption-ghg-units-input"),
objToShow,
new EngineNumber(1, "kgCO2e / kg"),
(x) => (x.getEqualsGhg() ? x.getEqualsGhg().getValue() : null),
);
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-energy-input"),
self._dialog.querySelector(".edit-consumption-energy-units-input"),
objToShow,
new EngineNumber(1, "kwh / unit"),
(x) => (x.getEqualsKwh() ? x.getEqualsKwh().getValue() : null),
);
const domesticFallback = new EngineNumber(1, "kg / unit");
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-domestic-input"),
self._dialog.querySelector(".initial-charge-domestic-units-input"),
objToShow,
domesticFallback,
(x) => self._getValueOrDefault(x.getInitialCharge("domestic"), domesticFallback),
);
const importFallback = new EngineNumber(2, "kg / unit");
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-import-input"),
self._dialog.querySelector(".initial-charge-import-units-input"),
objToShow,
importFallback,
(x) => self._getValueOrDefault(x.getInitialCharge("import"), importFallback),
);
const exportFallback = new EngineNumber(1, "kg / unit");
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-export-input"),
self._dialog.querySelector(".initial-charge-export-units-input"),
objToShow,
exportFallback,
(x) => self._getValueOrDefault(x.getInitialCharge("export"), exportFallback),
);
setEngineNumberValue(
self._dialog.querySelector(".edit-consumption-retirement-input"),
self._dialog.querySelector(".retirement-units-input"),
objToShow,
new EngineNumber(5, "% / year"),
(x) => x.getRetire() ? x.getRetire().getValue() : null,
);
/**
* Sets up retirement UI controls including input and checkbox.
*
* Initializes the retirement reduces equipment checkbox based on whether
* the substance has a retire command with the withReplacement flag.
* The checkbox logic is inverted: checked means normal retirement that
* reduces equipment population, unchecked means retirement with replacement
* that maintains equipment population.
*/
const setupRetirementUI = () => {
const retirementReducesCheckbox = self._dialog.querySelector(
".retirement-reduces-equipment-checkbox",
);
if (objToShow !== null && objToShow.getRetire()) {
const withReplacement = objToShow.getRetire().getWithReplacement();
retirementReducesCheckbox.checked = !withReplacement;
} else {
retirementReducesCheckbox.checked = true;
}
};
setupRetirementUI();
const removeCallback = () => self._updateCounts();
setListInput(
self._dialog.querySelector(".level-list"),
document.getElementById("set-command-template").innerHTML,
objToShow === null ? [] : objToShow.getSetVals(),
(item, root) => initSetCommandUi(
item, root, self._getCodeObj(), "consumption", self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".change-list"),
document.getElementById("change-command-template").innerHTML,
objToShow === null ? [] : objToShow.getChanges(),
(item, root) => initChangeCommandUi(
item, root, self._getCodeObj(), "consumption", self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".limit-list"),
document.getElementById("limit-command-template").innerHTML,
objToShow === null ? [] : objToShow.getLimits(),
(item, root) => initLimitCommandUi(
item,
root,
self._getCodeObj(),
"consumption",
self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".recharge-list"),
document.getElementById("recharge-command-template").innerHTML,
objToShow === null ? [] : objToShow.getRecharges(),
(item, root) => initRechargeCommandUi(item, root, self._getCodeObj()),
removeCallback,
);
/**
* Sets up enable checkboxes based on substance's enable commands.
*
* Initializes the domestic, import, and export enable checkboxes.
* For existing substances, checks if the substance has enable commands
* for each stream. For new substances, defaults all checkboxes to unchecked
* since the enable command must be explicitly added when a stream is used.
*/
const setupEnableCheckboxes = () => {
const enableImport = self._dialog.querySelector(".enable-import-checkbox");
const enableDomestic = self._dialog.querySelector(".enable-domestic-checkbox");
const enableExport = self._dialog.querySelector(".enable-export-checkbox");
if (objToShow !== null) {
const enableCommands = objToShow.getEnables();
enableDomestic.checked = enableCommands.some((cmd) => cmd.getTarget() === "domestic");
enableImport.checked = enableCommands.some((cmd) => cmd.getTarget() === "import");
enableExport.checked = enableCommands.some((cmd) => cmd.getTarget() === "export");
} else {
enableDomestic.checked = false;
enableImport.checked = false;
enableExport.checked = false;
}
};
setupEnableCheckboxes();
/**
* Sets up the sales assumption dropdown.
*
* Initializes the sales assumption dropdown with the current assume mode.
* Defaults to "continued" for new substances or when no assume mode is set.
* The assume mode controls whether sales continue from previous year,
* are zeroed out, or only cover recharge needs.
*/
const setupSalesAssumption = () => {
const salesAssumptionInput = self._dialog.querySelector(".sales-assumption-input");
if (objToShow === null) {
salesAssumptionInput.value = "continued";
} else {
const assumeMode = objToShow.getAssumeMode();
salesAssumptionInput.value = assumeMode === null ? "continued" : assumeMode;
}
};
setupSalesAssumption();
self._updateSource();
self._dialog.showModal();
self._reminderPresenter.update();
self._updateCounts();
}
/**
* Update the source inputs value and visibility.
*
* Shows or hides initial charge inputs based on enable checkbox states.
* When a stream is enabled, shows its input and sets default value if needed.
* When disabled, hides the input and sets value to 0. Updates stream target
* dropdowns to reflect current enabled streams.
*
* @private
*/
_updateSource() {
const self = this;
const enableImport = self._dialog.querySelector(".enable-import-checkbox");
const enableDomestic = self._dialog.querySelector(".enable-domestic-checkbox");
const enableExport = self._dialog.querySelector(".enable-export-checkbox");
const domesticInput = self._dialog.querySelector(
".edit-consumption-initial-charge-domestic-input",
);
const domesticInputOuter = self._dialog.querySelector(
".edit-consumption-initial-charge-domestic-input-outer",
);
const importInput = self._dialog.querySelector(".edit-consumption-initial-charge-import-input");
const importInputOuter = self._dialog.querySelector(
".edit-consumption-initial-charge-import-input-outer",
);
const exportInput = self._dialog.querySelector(".edit-consumption-initial-charge-export-input");
const exportInputOuter = self._dialog.querySelector(
".edit-consumption-initial-charge-export-input-outer",
);
/**
* Updates visibility and value for a stream input based on checkbox state.
*
* When checkbox is checked, shows the input field and sets default value
* (1 kg/unit) if current value is 0 or empty. When unchecked, hides the
* input and sets value to 0.
*
* @param {HTMLInputElement} checkbox - The enable checkbox for this stream.
* @param {HTMLInputElement} input - The value input element.
* @param {HTMLElement} inputOuter - The container element for visibility.
* @param {string} unitsInputSelector - CSS selector for the units dropdown.
* @param {string} unitsValue - Default units value to set.
*/
const updateStreamInput = (checkbox, input, inputOuter, unitsInputSelector, unitsValue) => {
if (checkbox.checked) {
inputOuter.style.display = "block";
if (input.value === "0" || input.value === "") {
input.value = 1;
const unitsInput = self._dialog.querySelector(unitsInputSelector);
if (unitsInput) {
unitsInput.value = unitsValue;
}
}
} else {
inputOuter.style.display = "none";
input.value = 0;
}
};
updateStreamInput(
enableDomestic,
domesticInput,
domesticInputOuter,
".initial-charge-domestic-units-input",
"kg / unit",
);
updateStreamInput(
enableImport,
importInput,
importInputOuter,
".initial-charge-import-units-input",
"kg / unit",
);
updateStreamInput(
enableExport,
exportInput,
exportInputOuter,
".initial-charge-export-units-input",
"kg / unit",
);
const tempEnabledStreams = [];
if (enableDomestic.checked) {
tempEnabledStreams.push("domestic");
}
if (enableImport.checked) {
tempEnabledStreams.push("import");
}
if (enableExport.checked) {
tempEnabledStreams.push("export");
}
self._streamUpdater.updateAllStreamTargetDropdowns(tempEnabledStreams);
}
/**
* Gets list of all consuming substances.
*
* @returns {string[]} Array of substances.
* @private
*/
_getConsumptionNames() {
const self = this;
const codeObj = self._getCodeObj();
const applications = codeObj.getApplications();
const consumptionsNested = applications.map((x) => {
const appName = x.getName();
const substances = x.getSubstances();
return substances.map((substance) => {
const metadata = substance.getMeta(appName);
return metadata.getKey();
});
});
const consumptions = consumptionsNested.flat();
return consumptions.sort();
}
/**
* Saves the current consumption data.
*
* Validates inputs and delegates to either _saveNew or _saveUpdate based on
* whether a new substance is being created or an existing one is being modified.
*
* @returns {boolean} True if save succeeded, false if user cancelled.
* @private
*/
_save() {
const self = this;
if (!validateNumericInputs(self._dialog, "substance")) {
return false;
}
const substance = self._parseObj();
const codeObj = self._getCodeObj();
const newSubstance = self._editingName === null;
if (newSubstance) {
return self._saveNew(substance, codeObj);
} else {
return self._saveUpdate(substance, codeObj);
}
}
/**
* Saves a new consumption substance.
*
* Handles name conflict resolution for new substances. If a duplicate name
* is detected, appends a numeric suffix and updates the UI accordingly.
* Delegates to specialized handlers based on conflict status.
*
* @param {Object} substance - The parsed substance object.
* @param {Object} codeObj - The code object to update.
* @returns {boolean} True indicating save succeeded.
* @private
*/
_saveNew(substance, codeObj) {
const self = this;
const applicationName = getFieldValue(
self._dialog.querySelector(".edit-consumption-application-input"),
);
const baseName = substance.getName();
const priorNames = new Set(self._getConsumptionNames());
const fullBaseName = `"${baseName}" for "${applicationName}"`;
const resolution = resolveSubstanceNameConflict(fullBaseName, priorNames);
if (resolution.getNameChanged()) {
return self._saveNewConflict(substance, applicationName, resolution);
} else {
return self._saveNewNoConflict(substance, applicationName, codeObj);
}
}
/**
* Saves a new substance after resolving name conflict.
*
* Updates the substance input field with the resolved name and re-parses
* the form data before inserting into the code object.
*
* @param {Object} substance - The original parsed substance object.
* @param {string} applicationName - The application name.
* @param {Object} resolution - The name conflict resolution result.
* @returns {boolean} True indicating save succeeded.
* @private
*/
_saveNewConflict(substance, applicationName, resolution) {
const self = this;
const resolvedFullName = resolution.getNewName();
const fullNameMatch = resolvedFullName.match(RESOLVED_FULL_NAME_REGEX);
if (fullNameMatch) {
const baseSubstanceName = fullNameMatch[1];
const suffix = fullNameMatch[2] || "";
const resolvedSubstanceName = baseSubstanceName + suffix;
const substanceInput = self._dialog.querySelector(".edit-consumption-substance-input");
const equipmentInput = self._dialog.querySelector(".edit-consumption-equipment-input");
const equipmentModel = getFieldValue(equipmentInput);
if (equipmentModel && equipmentModel.trim() !== "") {
const baseResolved = resolvedSubstanceName.replace(` - ${equipmentModel.trim()}`, "");
substanceInput.value = baseResolved;
} else {
substanceInput.value = resolvedSubstanceName;
}
substance = self._parseObj();
}
const codeObj = self._getCodeObj();
codeObj.insertSubstance(null, applicationName, null, substance);
self._onCodeObjUpdate(codeObj);
return true;
}
/**
* Saves a new substance without name conflicts.
*
* Directly inserts the substance into the code object without modification.
*
* @param {Object} substance - The parsed substance object.
* @param {string} applicationName - The application name.
* @param {Object} codeObj - The code object to update.
* @returns {boolean} True indicating save succeeded.
* @private
*/
_saveNewNoConflict(substance, applicationName, codeObj) {
const self = this;
codeObj.insertSubstance(null, applicationName, null, substance);
self._onCodeObjUpdate(codeObj);
return true;
}
/**
* Updates an existing consumption substance.
*
* Handles renaming of substances within the same application, ensuring
* that policy references are updated accordingly. Uses standard insertion
* logic for application changes or when the substance name remains unchanged.
*
* @param {Object} substance - The parsed substance object.
* @param {Object} codeObj - The code object to update.
* @returns {boolean} True indicating save succeeded.
* @private
*/
_saveUpdate(substance, codeObj) {
const self = this;
const match = self._editingName.match(OBJ_IDENTIFIER_REGEX);
const oldSubstanceName = match[1];
const oldApplicationName = match[2];
const newApplicationName = getFieldValue(
self._dialog.querySelector(".edit-consumption-application-input"),
);
const newSubstanceName = substance.getName();
const appNameSame = oldApplicationName === newApplicationName;
const substanceNameChanged = oldSubstanceName !== newSubstanceName;
if (appNameSame && substanceNameChanged) {
codeObj.renameSubstanceInApplication(
oldApplicationName,
oldSubstanceName,
newSubstanceName,
);
codeObj.insertSubstance(
oldApplicationName,
newApplicationName,
newSubstanceName,
substance,
);
} else {
codeObj.insertSubstance(
oldApplicationName,
newApplicationName,
oldSubstanceName,
substance,
);
}
self._onCodeObjUpdate(codeObj);
return true;
}
/**
* Parses the dialog form data into a substance object.
*
* @returns {Object} The parsed substance object.
* @private
*/
_parseObj() {
const self = this;
// Helper function to combine substance and equipment model
const getEffectiveSubstanceName = () => {
const baseSubstance = getSanitizedFieldValue(
self._dialog.querySelector(".edit-consumption-substance-input"),
);
const equipmentModel = getFieldValue(
self._dialog.querySelector(".edit-consumption-equipment-input"),
);
if (equipmentModel && equipmentModel.trim() !== "") {
return baseSubstance + " - " + equipmentModel.trim();
}
return baseSubstance;
};
const substanceName = getEffectiveSubstanceName();
const substanceBuilder = new SubstanceBuilder(substanceName, false);
/**
* Adds an enable command for a stream if its checkbox is checked.
*
* @param {string} streamName - The stream name (domestic, import, export).
* @param {HTMLInputElement} checkbox - The enable checkbox element.
*/
const addEnableCommand = (streamName, checkbox) => {
if (checkbox.checked) {
substanceBuilder.addCommand(new Command("enable", streamName, null, null));
}
};
const enableImport = self._dialog.querySelector(".enable-import-checkbox");
const enableDomestic = self._dialog.querySelector(".enable-domestic-checkbox");
const enableExport = self._dialog.querySelector(".enable-export-checkbox");
addEnableCommand("domestic", enableDomestic);
addEnableCommand("import", enableImport);
addEnableCommand("export", enableExport);
const assumeModeDropdown = self._dialog.querySelector(".sales-assumption-input");
const assumeMode = assumeModeDropdown.value;
const ghgValue = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-ghg-input"),
self._dialog.querySelector(".edit-consumption-ghg-units-input"),
);
substanceBuilder.addCommand(new Command("equals", null, ghgValue, null));
const energyValue = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-energy-input"),
self._dialog.querySelector(".edit-consumption-energy-units-input"),
);
substanceBuilder.addCommand(new Command("equals", null, energyValue, null));
const initialChargeDomestic = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-domestic-input"),
self._dialog.querySelector(".initial-charge-domestic-units-input"),
);
const initialChargeDomesticCommand = new Command(
"initial charge",
"domestic",
initialChargeDomestic,
null,
);
substanceBuilder.addCommand(initialChargeDomesticCommand);
const initialChargeImport = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-import-input"),
self._dialog.querySelector(".initial-charge-import-units-input"),
);
const initialChargeImportCommand = new Command(
"initial charge",
"import",
initialChargeImport,
null,
);
substanceBuilder.addCommand(initialChargeImportCommand);
const initialChargeExport = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-initial-charge-export-input"),
self._dialog.querySelector(".initial-charge-export-units-input"),
);
const initialChargeExportCommand = new Command(
"initial charge",
"export",
initialChargeExport,
null,
);
substanceBuilder.addCommand(initialChargeExportCommand);
const retirement = getEngineNumberValue(
self._dialog.querySelector(".edit-consumption-retirement-input"),
self._dialog.querySelector(".retirement-units-input"),
);
// Get checkbox state - inverted logic (checked = reduces, unchecked = with replacement)
const retirementReducesCheckbox = self._dialog.querySelector(
".retirement-reduces-equipment-checkbox",
);
const withReplacement = !retirementReducesCheckbox.checked;
// Create retire command with withReplacement flag
const retireCommand = new RetireCommand(retirement, null, withReplacement);
substanceBuilder.addCommand(retireCommand);
const levels = getListInput(self._dialog.querySelector(".level-list"), readSetCommandUi);
levels.forEach((x) => substanceBuilder.addCommand(x));
const changes = getListInput(self._dialog.querySelector(".change-list"), readChangeCommandUi);
changes.forEach((x) => substanceBuilder.addCommand(x));
const limits = getListInput(self._dialog.querySelector(".limit-list"), readLimitCommandUi);
limits.forEach((x) => substanceBuilder.addCommand(x));
const recharges = getListInput(
self._dialog.querySelector(".recharge-list"),
readRechargeCommandUi,
);
recharges.forEach((x) => substanceBuilder.addCommand(x));
substanceBuilder.setAssumeMode(assumeMode);
const substance = substanceBuilder.build(true);
return substance;
}
/**
* Updates the UI to display the count of commands in each list.
*
* @private
*/
_updateCounts() {
const self = this;
const updateCount = buildUpdateCount(self._dialog);
updateCount(".level-list", "#consumption-set-count");
updateCount(".change-list", "#consumption-change-count");
updateCount(".limit-list", "#consumption-limit-count");
updateCount(".recharge-list", "#consumption-servicing-count");
}
}
/**
* Manages the UI for listing and editing policies.
*
* Manages the UI for listing and editing policies that define recycling,
* replacement, level changes, limits, etc on substances.
*/
class PolicyListPresenter {
/**
* Creates a new PolicyListPresenter.
*
* @param {HTMLElement} root - The root DOM element for the policy list.
* @param {Function} getCodeObj - Callback to get the current code object.
* @param {Function} onCodeObjUpdate - Callback when the code object is updated.
*/
constructor(root, getCodeObj, onCodeObjUpdate) {
const self = this;
self._root = root;
self._dialog = self._root.querySelector(".dialog");
self._getCodeObj = getCodeObj;
self._onCodeObjUpdate = onCodeObjUpdate;
self._editingName = null;
self._streamUpdater = new StreamSelectionAvailabilityUpdater(
self._dialog,
"policy",
);
self._setupDialog();
self.refresh();
}
/**
* Visually and functionally enables the policy list interface.
*/
enable() {
const self = this;
self._root.classList.remove("inactive");
}
/**
* Visually and functionally sisables the policy list interface.
*/
disable() {
const self = this;
self._root.classList.add("inactive");
}
/**
* Updates the policy list to visualize new policies.
*
* @param {Object} codeObj - Current code object to display.
*/
refresh(codeObj) {
const self = this;
self._refreshList(codeObj);
}
/**
* Updates the policy list UI with current logic.
*
* @param {Object} codeObj - Current code object from which to extract policies.
* @private
*/
_refreshList(codeObj) {
const self = this;
const policyNames = self._getPolicyNames();
const itemList = d3.select(self._root).select(".item-list");
itemList.html("");
const newItems = itemList.selectAll("li").data(policyNames).enter().append("li");
newItems.attr("aria-label", (x) => x);
const buttonsPane = newItems.append("div").classed("list-buttons", true);
newItems
.append("div")
.classed("list-label", true)
.text((x) => x);
buttonsPane
.append("a")
.attr("href", "#")
.on("click", (event, x) => {
event.preventDefault();
self._showDialogFor(x);
})
.text("edit")
.attr("aria-label", (x) => "edit " + x);
buttonsPane.append("span").text(" | ");
buttonsPane
.append("a")
.attr("href", "#")
.on("click", (event, x) => {
event.preventDefault();
const message = "Are you sure you want to delete " + x + "?";
const isConfirmed = confirm(message);
if (isConfirmed) {
const codeObj = self._getCodeObj();
codeObj.deletePolicy(x);
self._onCodeObjUpdate(codeObj);
}
})
.text("delete")
.attr("aria-label", (x) => "delete " + x);
}
/**
* Creates a listener function for substance input changes.
*
* Returns a function that updates stream target dropdowns when the substance
* selection changes. The listener retrieves enabled streams for the selected
* substance and updates all stream target dropdowns in the policy dialog to
* reflect which streams are available.
*
* @param {HTMLSelectElement} substanceInput - The substance selection input element.
* @returns {Function} Event listener function for the change event.
* @private
*/
_createSubstanceInputListener(substanceInput) {
const self = this;
return () => {
const selectedSubstance = substanceInput.value;
const codeObj = self._getCodeObj();
const enabledStreams = self._streamUpdater._getEnabledStreamsForSubstance(
codeObj,
selectedSubstance,
);
self._streamUpdater.updateAllStreamTargetDropdowns(enabledStreams);
};
}
/**
* Sets up the dialog for adding/editing policies.
*
* Sets up the dialog for adding/editing policies, initializing tabs and
* event handlers for recycling, replacement, level changes, limits, etc.
*
* @private
*/
_setupDialog() {
const self = this;
self._tabs = new Tabby("#" + self._dialog.querySelector(".tabs").id);
self._reminderPresenter = new ReminderPresenter(
self._dialog,
".edit-policy-application-input",
".edit-policy-substance-input",
"#policy-general",
self._tabs,
);
const addLink = self._root.querySelector(".add-link");
addLink.addEventListener("click", (event) => {
self._showDialogFor(null);
event.preventDefault();
});
const closeButton = self._root.querySelector(".cancel-button");
closeButton.addEventListener("click", (event) => {
self._dialog.close();
event.preventDefault();
});
const saveButton = self._root.querySelector(".save-button");
saveButton.addEventListener("click", (event) => {
event.preventDefault();
if (self._save()) {
self._dialog.close();
}
});
const updateHints = () => {
self._reminderPresenter.update();
self._updateCounts();
};
const setupListButton = buildSetupListButton(updateHints);
const substanceInput = self._dialog.querySelector(".edit-policy-substance-input");
substanceInput.addEventListener("change", self._createSubstanceInputListener(substanceInput));
const addRecyclingButton = self._root.querySelector(".add-recycling-button");
const recyclingList = self._root.querySelector(".recycling-list");
setupListButton(
addRecyclingButton,
recyclingList,
"recycle-command-template",
(item, root, context) => {
initRecycleCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"policy",
);
const addReplaceButton = self._root.querySelector(".add-replace-button");
const replaceList = self._root.querySelector(".replace-list");
setupListButton(
addReplaceButton,
replaceList,
"replace-command-template",
(item, root, context) => {
initReplaceCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"policy",
);
const addLevelButton = self._root.querySelector(".add-level-button");
const levelList = self._root.querySelector(".level-list");
setupListButton(
addLevelButton,
levelList,
"set-command-template",
(item, root, context) => {
initSetCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"policy",
);
const addChangeButton = self._root.querySelector(".add-change-button");
const changeList = self._root.querySelector(".change-list");
setupListButton(
addChangeButton,
changeList,
"change-command-template",
(item, root, context) => {
initChangeCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"policy",
);
const addLimitButton = self._root.querySelector(".add-limit-button");
const limitList = self._root.querySelector(".limit-list");
setupListButton(
addLimitButton,
limitList,
"limit-command-template",
(item, root, context) => {
initLimitCommandUi(item, root, self._getCodeObj(), context, self._streamUpdater);
},
"policy",
);
const addRechargeButton = self._root.querySelector(".add-recharge-button");
const rechargeList = self._root.querySelector(".recharge-list");
setupListButton(
addRechargeButton,
rechargeList,
"recharge-command-template",
(item, root, context) => {
initRechargeCommandUi(item, root, self._getCodeObj());
},
"policy",
);
setupDialogInternalLinks(self._root, self._tabs);
}
/**
* Shows the dialog for adding or editing a policy.
*
* @param {string|null} name - Name of policy to edit, or null for new policy.
* @private
*/
_showDialogFor(name) {
const self = this;
self._editingName = name;
const codeObj = self._getCodeObj();
self._tabs.toggle("#policy-general");
const isArrayEmpty = (x) => x === null || x.length == 0;
const targetPolicy = name === null ? null : codeObj.getPolicy(name);
const targetApplications = targetPolicy === null ? null : targetPolicy.getApplications();
const targetApplication = isArrayEmpty(targetApplications) ? null : targetApplications[0];
const targetSubstances = targetApplication === null ? null : targetApplication.getSubstances();
const targetSubstance = isArrayEmpty(targetSubstances) ? null : targetSubstances[0];
if (name === null) {
self._dialog.querySelector(".action-title").innerHTML = "Add";
} else {
self._dialog.querySelector(".action-title").innerHTML = "Edit";
}
setFieldValue(self._dialog.querySelector(".edit-policy-name-input"), targetPolicy, "", (x) =>
x.getName(),
);
const applicationNames = codeObj.getApplications().map((x) => x.getName());
const applicationSelect = self._dialog.querySelector(".application-select");
const targetAppName = targetApplication === null ? "" : targetApplication.getName();
d3.select(applicationSelect)
.html("")
.selectAll("option")
.data(applicationNames)
.enter()
.append("option")
.attr("value", (x) => x)
.text((x) => x)
.property("selected", (x) => x === targetAppName);
const substances = codeObj.getSubstances();
const substanceNamesDup = substances.map((x) => x.getName());
const substanceNames = Array.of(...new Set(substanceNamesDup));
const substanceSelect = d3.select(self._dialog.querySelector(".substances-select"));
const substanceName = targetSubstance === null ? "" : targetSubstance.getName();
substanceSelect.html("");
substanceSelect
.selectAll("option")
.data(substanceNames)
.enter()
.append("option")
.attr("value", (x) => x)
.text((x) => x)
.property("selected", (x) => x === substanceName);
const removeCallback = () => self._updateCounts();
setListInput(
self._dialog.querySelector(".recycling-list"),
document.getElementById("recycle-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getRecycles(),
(item, root) => initRecycleCommandUi(
item,
root,
self._getCodeObj(),
"policy",
self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".replace-list"),
document.getElementById("replace-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getReplaces(),
(item, root) => initReplaceCommandUi(
item,
root,
self._getCodeObj(),
"policy",
self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".level-list"),
document.getElementById("set-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getSetVals(),
(item, root) => initSetCommandUi(
item, root, self._getCodeObj(), "policy", self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".change-list"),
document.getElementById("change-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getChanges(),
(item, root) => initChangeCommandUi(
item, root, self._getCodeObj(), "policy", self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".limit-list"),
document.getElementById("limit-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getLimits(),
(item, root) => initLimitCommandUi(
item,
root,
self._getCodeObj(),
"policy",
self._streamUpdater,
),
removeCallback,
);
setListInput(
self._dialog.querySelector(".recharge-list"),
document.getElementById("recharge-command-template").innerHTML,
targetSubstance === null ? [] : targetSubstance.getRecharges(),
(item, root) => initRechargeCommandUi(item, root, self._getCodeObj()),
removeCallback,
);
self._dialog.showModal();
self._reminderPresenter.update();
self._updateCounts();
// Initialize stream target dropdown states based on selected substance
const substanceInput = self._dialog.querySelector(".edit-policy-substance-input");
const selectedSubstance = substanceInput.value;
if (selectedSubstance) {
const enabledStreams = self._streamUpdater._getEnabledStreamsForSubstance(
self._getCodeObj(), selectedSubstance,
);
// Update all stream target dropdowns in the policy dialog
self._streamUpdater.updateAllStreamTargetDropdowns(enabledStreams);
}
}
/**
* Gets list of all policy names.
*
* @returns {string[]} Array of policy names.
* @private
*/
_getPolicyNames() {
const self = this;
const codeObj = self._getCodeObj();
const policies = codeObj.getPolicies();
return policies.map((x) => x.getName()).sort();
}
/**
* Parses the dialog form data into a policy object.
*
* @returns {Object} The parsed policy object.
* @private
*/
_parseObj() {
const self = this;
const policyName = getSanitizedFieldValue(
self._dialog.querySelector(".edit-policy-name-input"),
);
const applicationName = getFieldValue(
self._dialog.querySelector(".edit-policy-application-input"),
);
const substanceName = getFieldValue(self._dialog.querySelector(".edit-policy-substance-input"));
const builder = new SubstanceBuilder(substanceName, true);
const recycles = getListInput(
self._dialog.querySelector(".recycling-list"),
readRecycleCommandUi,
);
recycles.forEach((command) => builder.addCommand(command));
const replaces = getListInput(
self._dialog.querySelector(".replace-list"),
readReplaceCommandUi,
);
replaces.forEach((command) => builder.addCommand(command));
const levels = getListInput(self._dialog.querySelector(".level-list"), readSetCommandUi);
levels.forEach((command) => builder.addCommand(command));
const changes = getListInput(self._dialog.querySelector(".change-list"), readChangeCommandUi);
changes.forEach((command) => builder.addCommand(command));
const limits = getListInput(self._dialog.querySelector(".limit-list"), readLimitCommandUi);
limits.forEach((command) => builder.addCommand(command));
const recharges = getListInput(
self._dialog.querySelector(".recharge-list"),
readRechargeCommandUi,
);
recharges.forEach((command) => builder.addCommand(command));
const substance = builder.build(true);
const application = new Application(applicationName, [substance], true, true);
const policy = new DefinitionalStanza(policyName, [application], true, true);
return policy;
}
/**
* Saves the current policy data.
*
* @private
*/
_save() {
const self = this;
if (!validateNumericInputs(self._dialog, "policy")) {
return false;
}
let policy = self._parseObj();
const newPolicy = self._editingName === null;
if (newPolicy) {
const baseName = policy.getName();
const priorNames = new Set(self._getPolicyNames());
const resolution = resolveNameConflict(baseName, priorNames);
if (resolution.getNameChanged()) {
const nameInput = self._dialog.querySelector(".edit-policy-name-input");
nameInput.value = resolution.getNewName();
policy = self._parseObj();
}
}
const codeObj = self._getCodeObj();
codeObj.insertPolicy(self._editingName, policy);
self._onCodeObjUpdate(codeObj);
return true;
}
/**
* Updates the UI to display the count of commands in each list.
*
* @private
*/
_updateCounts() {
const self = this;
const updateCount = buildUpdateCount(self._dialog);
updateCount(".recycling-list", "#policy-recycle-count");
updateCount(".replace-list", "#policy-replace-count");
updateCount(".level-list", "#policy-set-count");
updateCount(".change-list", "#policy-change-count");
updateCount(".limit-list", "#policy-limit-count");
updateCount(".recharge-list", "#policy-servicing-count");
}
}
export {
StreamSelectionAvailabilityUpdater,
ReminderPresenter,
ConsumptionListPresenter,
PolicyListPresenter,
};