Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add jquery.dirty to warn user about unsaved changes #88

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# from test projects:
**/static/*
!src/inventory/static/*
**/media/*
*.sqlite3

Expand Down
16 changes: 16 additions & 0 deletions src/inventory/static/inventory.js
Original file line number Diff line number Diff line change
@@ -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);
308 changes: 308 additions & 0 deletions src/inventory/static/jquery.dirty.js
Original file line number Diff line number Diff line change
@@ -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);
10 changes: 10 additions & 0 deletions src/inventory_project/templates/admin/change_form.html
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down