Add jquery.dirty to warn user about unsaved changes

Adds jquery.dirty v0.8.3 from: https://github.com/simon-reynolds/jquery.dirty/releases/tag/0.8.3

Show a warning if the user attempts to navigate away from the form with pending changes.

Sadly jquery.dirty doesn't work correctly for tagulous fields, see:

* https://github.com/simon-reynolds/jquery.dirty/issues/71
* https://github.com/radiac/django-tagulous/issues/156
pull/88/head
JensDiemer 2022-02-05 12:01:21 +01:00
rodzic 37be07778c
commit 2977c9dfba
7 zmienionych plików z 356 dodań i 0 usunięć

1
.gitignore vendored
Wyświetl plik

@ -13,6 +13,7 @@
# from test projects:
**/static/*
!src/inventory/static/*
**/media/*
*.sqlite3

Wyświetl plik

@ -0,0 +1,16 @@
(function ($) {
'use strict';
$(function () {
const form_object = $(form_selector);
form_object.dirty({
preventLeaving: true,
onDirty: function () {
console.log('form is dirty');
var dirty_fields = form_object.dirty("showDirtyFields");
dirty_fields.each(function (index, element) {
console.log(index + ' - ' + element.value);
});
},
});
});
})(django.jQuery);

Wyświetl plik

@ -0,0 +1,308 @@
/*
* Dirty
* jquery plugin to detect when a form is modified
* (c) 2016 Simon Taite - https://github.com/simon-reynolds/jquery.dirty
* originally based on jquery.dirrty by Ruben Torres - https://github.com/rubentd/dirrty
* Released under the MIT license
*/
(function($) {
//Save dirty instances
var singleDs = [];
var dirty = "dirty";
var clean = "clean";
var dataInitialValue = "dirtyInitialValue";
var dataIsDirty = "isDirty";
var getSingleton = function(id) {
var result;
singleDs.forEach(function(e) {
if (e.id === id) {
result = e;
}
});
return result;
};
var setSubmitEvents = function(d) {
d.form.on("submit", function() {
d.submitting = true;
});
if (d.options.preventLeaving) {
$(window).on("beforeunload", function(event) {
if (d.isDirty && !d.submitting) {
event.preventDefault();
return d.options.leavingMessage;
}
});
}
};
var setNamespacedEvents = function(d) {
d.form.find("input, select, textarea").on("change.dirty click.dirty keyup.dirty keydown.dirty blur.dirty", function(e) {
d.checkValues(e);
});
d.form.on("dirty", function() {
d.options.onDirty();
});
d.form.on("clean", function() {
d.options.onClean();
});
};
var clearNamespacedEvents = function(d) {
d.form.find("input, select, textarea").off("change.dirty click.dirty keyup.dirty keydown.dirty blur.dirty");
d.form.off("dirty");
d.form.off("clean");
};
var Dirty = function(form, options) {
this.form = form;
this.isDirty = false;
this.options = options;
this.history = [clean, clean]; //Keep track of last statuses
this.id = $(form).attr("id");
singleDs.push(this);
};
Dirty.prototype = {
init: function() {
this.saveInitialValues();
this.setEvents();
},
isRadioOrCheckbox: function(el){
return $(el).is(":radio, :checkbox");
},
isFileInput: function(el){
return $(el).is(":file")
},
saveInitialValues: function() {
var d = this;
this.form.find("input, select, textarea").each(function(_, e) {
var isRadioOrCheckbox = d.isRadioOrCheckbox(e);
var isFile = d.isFileInput(e);
if (isRadioOrCheckbox) {
var isChecked = $(e).is(":checked") ? "checked" : "unchecked";
$(e).data(dataInitialValue, isChecked);
} else if(isFile){
$(e).data(dataInitialValue, JSON.stringify(e.files))
} else {
$(e).data(dataInitialValue, $(e).val() || '');
}
});
},
refreshEvents: function () {
var d = this;
clearNamespacedEvents(d);
setNamespacedEvents(d);
},
showDirtyFields: function() {
var d = this;
return d.form.find("input, select, textarea").filter(function(_, e){
return $(e).data("isDirty");
});
},
setEvents: function() {
var d = this;
setSubmitEvents(d);
setNamespacedEvents(d);
},
isFieldDirty: function($field) {
var initialValue = $field.data(dataInitialValue);
// Explicitly check for null/undefined here as value may be `false`, so ($field.data(dataInitialValue) || '') would not work
if (initialValue == null) { initialValue = ''; }
var currentValue = $field.val();
if (currentValue == null) { currentValue = ''; }
// Boolean values can be encoded as "true/false" or "True/False" depending on underlying frameworks so we need a case insensitive comparison
var boolRegex = /^(true|false)$/i;
var isBoolValue = boolRegex.test(initialValue) && boolRegex.test(currentValue);
if (isBoolValue) {
var regex = new RegExp("^" + initialValue + "$", "i");
return !regex.test(currentValue);
}
return currentValue !== initialValue;
},
isFileInputDirty: function($field) {
var initialValue = $field.data(dataInitialValue);
var plainField = $field[0];
var currentValue = JSON.stringify(plainField.files);
return currentValue !== initialValue;
},
isCheckboxDirty: function($field) {
var initialValue = $field.data(dataInitialValue);
var currentValue = $field.is(":checked") ? "checked" : "unchecked";
return initialValue !== currentValue;
},
checkValues: function(e) {
var d = this;
var formIsDirty = false;
this.form.find("input, select, textarea").each(function(_, el) {
var isRadioOrCheckbox = d.isRadioOrCheckbox(el);
var isFile = d.isFileInput(el);
var $el = $(el);
var thisIsDirty;
if (isRadioOrCheckbox) {
thisIsDirty = d.isCheckboxDirty($el);
} else if (isFile) {
thisIsDirty = d.isFileInputDirty($el);
} else {
thisIsDirty = d.isFieldDirty($el);
}
$el.data(dataIsDirty, thisIsDirty);
formIsDirty |= thisIsDirty;
});
if (formIsDirty) {
d.setDirty();
} else {
d.setClean();
}
},
setDirty: function() {
this.isDirty = true;
this.history[0] = this.history[1];
this.history[1] = dirty;
if (this.options.fireEventsOnEachChange || this.wasJustClean()) {
this.form.trigger("dirty");
}
},
setClean: function() {
this.isDirty = false;
this.history[0] = this.history[1];
this.history[1] = clean;
if (this.options.fireEventsOnEachChange || this.wasJustDirty()) {
this.form.trigger("clean");
}
},
//Lets me know if the previous status of the form was dirty
wasJustDirty: function() {
return (this.history[0] === dirty);
},
//Lets me know if the previous status of the form was clean
wasJustClean: function() {
return (this.history[0] === clean);
},
setAsClean: function(){
this.saveInitialValues();
this.setClean();
},
setAsDirty: function(){
this.saveInitialValues();
this.setDirty();
},
resetForm: function(){
var d = this;
this.form.find("input, select, textarea").each(function(_, e) {
var $e = $(e);
var isRadioOrCheckbox = d.isRadioOrCheckbox(e);
var isFile = d.isFileInput(e);
if (isRadioOrCheckbox) {
var initialCheckedState = $e.data(dataInitialValue);
var isChecked = initialCheckedState === "checked";
$e.prop("checked", isChecked);
} if(isFile) {
e.value = "";
$(e).data(dataInitialValue, JSON.stringify(e.files))
} else {
var value = $e.data(dataInitialValue);
$e.val(value);
}
});
this.checkValues();
}
};
$.fn.dirty = function(options) {
if (typeof options === "string" && /^(isDirty|isClean|refreshEvents|resetForm|setAsClean|setAsDirty|showDirtyFields)$/i.test(options)) {
//Check if we have an instance of dirty for this form
// TODO: check if this is DOM or jQuery object
var d = getSingleton($(this).attr("id"));
if (!d) {
d = new Dirty($(this), options);
d.init();
}
var optionsLowerCase = options.toLowerCase();
switch (optionsLowerCase) {
case "isclean":
return !d.isDirty;
case "isdirty":
return d.isDirty;
case "refreshevents":
d.refreshEvents();
case "resetform":
d.resetForm();
case "setasclean":
return d.setAsClean();
case "setasdirty":
return d.setAsDirty();
case "showdirtyfields":
return d.showDirtyFields();
}
} else if (typeof options === "object" || !options) {
return this.each(function(_, e) {
options = $.extend({}, $.fn.dirty.defaults, options);
var dirty = new Dirty($(e), options);
dirty.init();
});
}
};
$.fn.dirty.defaults = {
preventLeaving: false,
leavingMessage: "There are unsaved changes on this page which will be discarded if you continue.",
onDirty: $.noop, //This function is fired when the form gets dirty
onClean: $.noop, //This funciton is fired when the form gets clean again
fireEventsOnEachChange: false, // Fire onDirty/onClean on each modification of the form
};
})(jQuery);

Wyświetl plik

@ -0,0 +1,10 @@
{% extends "admin/change_form.html" %}
{% load static %}
{% block extrahead %}{{ block.super }}
<script src="{% static "jquery.dirty.js" %}"></script>
<script src="{% static "inventory.js" %}"></script>
<script>
const form_selector="#{{ opts.model_name }}_form";
</script>
{% endblock %}

Wyświetl plik

@ -71,6 +71,13 @@
</script>
<script src="/static/adminsortable2/js/inline-tabular.js">
</script>
<script src="/static/jquery.dirty.js">
</script>
<script src="/static/inventory.js">
</script>
<script>
const form_selector="#itemmodel_form";
</script>
<meta content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport"/>
<link href="/static/admin/css/responsive.css" rel="stylesheet" type="text/css"/>
<meta content="NONE,NOARCHIVE" name="robots"/>

Wyświetl plik

@ -71,6 +71,13 @@
</script>
<script src="/static/adminsortable2/js/inline-tabular.js">
</script>
<script src="/static/jquery.dirty.js">
</script>
<script src="/static/inventory.js">
</script>
<script>
const form_selector="#itemmodel_form";
</script>
<meta content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport"/>
<link href="/static/admin/css/responsive.css" rel="stylesheet" type="text/css"/>
<meta content="NONE,NOARCHIVE" name="robots"/>

Wyświetl plik

@ -67,6 +67,13 @@
</script>
<script src="/static/adminsortable2/js/inline-tabular.js">
</script>
<script src="/static/jquery.dirty.js">
</script>
<script src="/static/inventory.js">
</script>
<script>
const form_selector="#memomodel_form";
</script>
<meta content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport"/>
<link href="/static/admin/css/responsive.css" rel="stylesheet" type="text/css"/>
<meta content="NONE,NOARCHIVE" name="robots"/>