Files
openfoodnetwork/app/webpacker/controllers/bulk_form_controller.js
David Cook 7fe6f3fe89 Disable form elements in a disabled-section
I chose to use the 'elements' collection rather than choosing which elements to include (ie this supports inputs, textareas, buttons and anything else I didn't think of). It could be a bit simpler if we assume the element is a form. Even simpler if it's a fieldset (that has a disabled property). But I didn't want to limit it too much.

Unfortunately JS is quite ugly compared to Ruby. And 'prettier' made it uglier in my opinion.
2023-11-03 14:32:58 +11:00

106 lines
3.6 KiB
JavaScript

import { Controller } from "stimulus";
// Manages "changed" state for a form with multiple records
export default class BulkFormController extends Controller {
static targets = ["actions", "changedSummary"];
static values = {
disableSelector: String,
error: Boolean,
};
recordElements = {};
connect() {
this.form = this.element;
// Start listening for any changes within the form
// this.element.addEventListener('change', this.toggleChanged.bind(this)); // dunno why this doesn't work
for (const element of this.form.elements) {
element.addEventListener("keyup", this.toggleChanged.bind(this)); // instant response
element.addEventListener("change", this.toggleChanged.bind(this)); // just in case (eg right-click paste)
// Set up a tree of fields according to their associated record
const recordContainer = element.closest("[data-record-id]"); // The JS could be more efficient if this data was added to each element. But I didn't want to pollute the HTML too much.
const recordId = recordContainer && recordContainer.dataset.recordId;
if (recordId) {
this.recordElements[recordId] ||= [];
this.recordElements[recordId].push(element);
}
}
this.toggleFormChanged();
}
disconnect() {
// Make sure to clean up anything that happened outside
this.#disableOtherElements(false);
window.removeEventListener("beforeunload", this.preventLeavingBulkForm);
}
toggleChanged(e) {
const element = e.target;
element.classList.toggle("changed", this.#isChanged(element));
this.toggleFormChanged();
}
toggleFormChanged() {
// For each record, check if any fields are changed
const changedRecordCount = Object.values(this.recordElements).filter((elements) =>
elements.some(this.#isChanged)
).length;
const formChanged = changedRecordCount > 0 || this.errorValue;
// Show actions
this.actionsTarget.classList.toggle("hidden", !formChanged);
this.#disableOtherElements(formChanged); // like filters and sorting
// Display number of records changed
const key = this.hasChangedSummaryTarget && this.changedSummaryTarget.dataset.translationKey;
if (key) {
// TODO: save processing and only run if changedRecordCount has changed.
this.changedSummaryTarget.textContent = I18n.t(key, { count: changedRecordCount });
}
// Prevent accidental data loss
if (formChanged) {
window.addEventListener("beforeunload", this.preventLeavingBulkForm);
} else {
window.removeEventListener("beforeunload", this.preventLeavingBulkForm);
}
}
preventLeavingBulkForm(e) {
// Cancel the event
e.preventDefault();
// Chrome requires returnValue to be set. Other browsers may display this if provided, but let's
// not create a new translation key, and keep the behaviour consistent.
e.returnValue = "";
}
// private
#disableOtherElements(disable) {
if (!this.hasDisableSelectorValue) return;
this.disableElements ||= document.querySelectorAll(this.disableSelectorValue);
if (!this.disableElements) return;
this.disableElements.forEach((element) => {
element.classList.toggle("disabled-section", disable);
// Also disable any form elements
let forms = element.tagName == "FORM" ? [element] : element.querySelectorAll("form");
forms &&
forms.forEach((form) =>
Array.from(form.elements).forEach((formElement) => (formElement.disabled = disable))
);
});
}
#isChanged(element) {
return element.value != element.defaultValue;
}
}