Added a "unsaved changed" warning dialog for part, entity edits and system settings

This fixes issue #1368
This commit is contained in:
Jan Böhmer 2026-05-25 21:29:10 +02:00
parent ad0c60f766
commit 79c36494ea
21 changed files with 556 additions and 11 deletions

View file

@ -25,9 +25,11 @@ import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
_tomSelect;
@ -82,7 +84,8 @@ export default class extends Controller {
'autoselect_typed': {},
'click_to_edit': {},
'clear_button': {},
"restore_on_backspace": {}
'restore_on_backspace': {},
'form_reset_handler': {}
}
};

View file

@ -25,9 +25,11 @@ import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
_tomSelect;
@ -64,7 +66,8 @@ export default class extends Controller {
'autoselect_typed': {},
'click_to_edit': {},
'clear_button': {},
"restore_on_backspace": {}
'restore_on_backspace': {},
'form_reset_handler': {}
}
};

View file

@ -91,6 +91,9 @@ export default class extends Controller {
config.translations = [window.CKEDITOR_TRANSLATIONS, translations];
}
//Apply the default value of the source element as data attribute, so that dirty-form-controller can detect changes
this.element.dataset.defaultValue = this.element.defaultValue;
const watchdog = new EditorWatchdog();
watchdog.setCreator((elementOrData, editorConfig) => {
return EDITOR_TYPE.create(elementOrData, editorConfig)
@ -111,10 +114,21 @@ export default class extends Controller {
editor.updateSourceElement();
// Dispatch the input event for further treatment
const event = new Event("input");
this.element.dispatchEvent(event);
this.element.dispatchEvent(new Event("input", { bubbles: true }));
});
//Set an reset listener to update the editor if the source element is reset (e.g. by a reset button)
if (this.element.form && this.element.name) {
this.element.form.addEventListener("reset", () => {
if (editor.isReadOnly) {
return;
}
if (this.element.dataset.defaultValue !== undefined) {
editor.setData(this.element.dataset.defaultValue);
}
});
}
//This return is important! Otherwise we get mysterious errors in the console
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
return editor;

View file

@ -4,6 +4,9 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import {marked} from "marked";
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
_tomSelect;
@ -18,7 +21,7 @@ export default class extends Controller {
let settings = {
allowEmptyOption: true,
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
plugins: ['dropdown_input', this.element.required ? null : 'clear_button', 'form_reset_handler'],
searchField: ["name", "description", "category", "footprint"],
valueField: "id",
labelField: "name",

View file

@ -21,6 +21,9 @@ import {Controller} from "@hotwired/stimulus";
import "tom-select/dist/css/tom-select.bootstrap5.css";
import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
@ -44,7 +47,7 @@ export default class extends Controller {
}
let settings = {
plugins: ["clear_button"],
plugins: ["clear_button", "form_reset_handler"],
allowEmptyOption: true,
selectOnTab: true,
maxOptions: null,

View file

@ -25,9 +25,11 @@ import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
/**
* This is the frontend controller for StaticFileAutocompleteType form element.
@ -64,7 +66,8 @@ export default class extends Controller {
'autoselect_typed': {},
'click_to_edit': {},
'clear_button': {},
'restore_on_backspace': {}
'restore_on_backspace': {},
'form_reset_handler': {}
}
};

View file

@ -23,9 +23,11 @@ import TomSelect from "tom-select";
import {Controller} from "@hotwired/stimulus";
import {trans} from '../../translator.js'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
_tomSelect;
@ -96,6 +98,7 @@ export default class extends Controller {
plugins: {
"autoselect_typed": {},
"form_reset_handler": {},
}
};
@ -105,6 +108,7 @@ export default class extends Controller {
}
this._tomSelect = new TomSelect(this.element, settings);
//Do not do a sync here as this breaks the initial rendering of the empty option
//this._tomSelect.sync();
}

View file

@ -25,9 +25,11 @@ import TomSelect from "tom-select";
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
export default class extends Controller {
_tomSelect;
@ -43,6 +45,7 @@ export default class extends Controller {
remove_button:{},
'autoselect_typed': {},
'click_to_edit': {},
'form_reset_handler': {},
},
persistent: false,
selectOnTab: true,

View file

@ -102,7 +102,18 @@ export default class extends Controller {
onNodeSelected: (event) => {
const node = event.detail.node;
if (node.href) {
window.Turbo.visit(node.href, {action: "advance", frame: this._frame});
const url = node.href;
// Turbo.visit with a frame target bypasses turbo:before-visit, so dispatch it
// manually so that dirty-form guards can intercept it.
const beforeVisitEvent = new CustomEvent('turbo:before-visit', {
bubbles: true,
cancelable: true,
detail: { url, frame: this._frame },
});
document.dispatchEvent(beforeVisitEvent);
if (!beforeVisitEvent.defaultPrevented) {
window.Turbo.visit(url, {action: "advance", frame: this._frame});
}
}
},
}, [BS5Theme, BS53Theme, FAIconTheme]);