mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
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.
106 lines
3.6 KiB
JavaScript
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;
|
|
}
|
|
}
|