news/plugins/admin/themes/grav/app/forms/state.js

150 lines
4.8 KiB
JavaScript

import $ from 'jquery';
import Immutable from 'immutable';
import immutablediff from 'immutablediff';
import '../utils/jquery-utils';
let FormLoadState = {};
const DOMBehaviors = {
attach() {
this.preventUnload();
this.preventClickAway();
},
preventUnload() {
let selector = '[name="task"][value^="save"], [data-delete-action], [data-flex-safe-action]';
if ($._data(window, 'events') && ($._data(window, 'events').beforeunload || []).filter((event) => event.namespace === '_grav').length) {
return;
}
// Allow some elements to leave the page without native confirmation
$(selector).on('click._grav', function(event) {
$(global).off('beforeunload');
});
// Catch browser uri change / refresh attempt and stop it if the form state is dirty
$(global).on('beforeunload._grav', () => {
if (Instance.equals() === false) {
return 'You have made changes on this page that you have not yet confirmed. If you navigate away from this page you will lose your unsaved changes.';
}
});
},
preventClickAway() {
let selector = 'a[href]:not([href^="#"]):not([target="_blank"]):not([href^="javascript:"])';
if ($._data($(selector).get(0), 'events') && ($._data($(selector).get(0), 'events').click || []).filter((event) => event.namespace === '_grav')) {
return;
}
// Prevent clicking away if the form state is dirty
// instead, display a confirmation before continuing
$(selector).on('click._grav', function(event) {
let isClean = Instance.equals();
if (isClean === null || isClean) { return true; }
event.preventDefault();
let destination = $(this).attr('href');
let modal = $('[data-remodal-id="changes"]');
let lookup = $.remodal.lookup[modal.data('remodal')];
let buttons = $('a.button', modal);
let handler = function(event) {
event.preventDefault();
let action = $(this).data('leave-action');
buttons.off('click', handler);
lookup.close();
if (action === 'continue') {
$(global).off('beforeunload');
global.location.href = destination;
}
};
buttons.on('click', handler);
lookup.open();
});
}
};
export default class FormState {
constructor(options = {
ignore: [],
form_id: 'blueprints'
}) {
this.options = options;
this.refresh();
if (!this.form || !this.fields.length) { return; }
FormLoadState = this.collect();
this.loadState = FormLoadState;
DOMBehaviors.attach();
}
refresh() {
this.form = $(`form#${this.options.form_id}`).filter(':noparents(.remodal)');
this.fields = $(`form#${this.options.form_id} *, [form="${this.options.form_id}"]`).filter(':input:not(.no-form)').filter(':noparents(.remodal)');
return this;
}
collect() {
if (!this.form || !this.fields.length) { return; }
let values = {};
this.refresh().fields.each((index, field) => {
field = $(field);
let name = field.prop('name');
let type = field.prop('type');
let tag = field.prop('tagName').toLowerCase();
let value;
if (name.startsWith('toggleable_') || name === 'data[lang]' || name === 'data[redirect]') {
return;
}
switch (type) {
case 'checkbox':
value = field.is(':checked');
break;
case 'radio':
if (!field.is(':checked')) { return; }
value = field.val();
break;
default:
value = field.val();
}
if (tag === 'select' && value === null) {
value = '';
}
if (Array.isArray(value)) {
value = value.join('|');
}
if (name && !~this.options.ignore.indexOf(name)) {
values[name] = value;
}
});
return Immutable.OrderedMap(values);
}
diff() {
return immutablediff(FormLoadState, this.collect());
}
// When the form doesn't exist or there are no fields, `equals` returns `null`
// for this reason, _NEVER_ check with !Instance.equals(), use Instance.equals() === false
equals() {
if (!this.form || !this.fields.length) { return null; }
return Immutable.is(FormLoadState, this.collect());
}
};
export let Instance = new FormState();
export { DOMBehaviors };