Howto prompt before accidentally discarding unsaved changes with JavaScript

Ask before leaving an unsaved CKEditor

Vanilla JavaScript way, but removes any other onbeforeunload handlers:

  $(function(){
    document.body.onbeforeunload = function() {
      for(editorName in CKEDITOR.instances) {
        if (CKEDITOR.instances[editorName].checkDirty()) {
          return "Unsaved changes present!"
        }
      }
    }
  }

A robuster implementation example

Note: Don't forget to mark the 'search as you type' forms with the skip_pending_changes_warning class.

var WarnBeforeAccidentallyDiscardUnsavedChanges = (function () {

  var anyDirtyInputFields = false;
  var unloadTriggeredBySubmit = false;
  var warningMessage = "Ungespeicherte Änderungen vorhanden!";
  var confirmationMessage = "Möchten Sie diese Seite wirklich verlassen?";
  var skipWarningClass = "skip_pending_changes_warning";

  function init() {
    skipFormsWithOverwrittenSubmit();
    observeInputFieldsForChanges();
    observeSubmitEvents();
    bindAlertMethod();
  }

  // API method for others which need to know for unsaved changes
  function isUnsavedChangeConfirmed() {
    if (isUnsavedChangePresent()) {
      return confirm(warningMessage + "\n\n" + confirmationMessage);
    } else {
      return true;
    }
  }

  // opens a confirmation dialog if unsaved changes are present
  function bindAlertMethod() {
    $(window).on('beforeunload', function() {
      if (isUnsavedChangePresent() && !unloadTriggeredBySubmit) {
        return warningMessage;
      }
    });
  }

  function isUnsavedChangePresent() {
    var unsavedChanges = false;

    // objects providing an isDirty() method
    unsavedChanges = unsavedChanges || anyDirtyCkeditors();
    // elements which have to be observed manually
    unsavedChanges = unsavedChanges || anyDirtyInputFields;

    return unsavedChanges;
  }


  // dirty check helper methods

  function anyDirtyCkeditors() {
    var anyDirty = false;
    if(typeof CKEDITOR != 'undefined') {
      _.each(CKEDITOR.instances, function(instance) {
        anyDirty = anyDirty || instance.checkDirty();
      });
    }
    return anyDirty;
  }


  // observer helpers

  // Using a css class as marker may make developers awere of the
  // special form behavior when inspecting the form classes
  function skipFormsWithOverwrittenSubmit() {
    _.each($('form input[type=submit]'), function(submitInput) {
      if (!!$(submitInput).attr('onclick')) {
        $(submitInput).closest('form').addClass(skipWarningClass);
      }
    });
  }

  function observeInputFieldsForChanges() {
    var $forms = $('form:not(.' + skipWarningClass + ')');
    var $inputs = $forms.find(
      'input:not(.' + skipWarningClass + '), ' +
        'textarea:not(.' + skipWarningClass + '), ' +
        'select:not(.' + skipWarningClass + ')');
    $inputs.on('change', function() {
      anyDirtyInputFields = true;
    });
  }

  function observeSubmitEvents() {
    $('form').submit(function() {
      unloadTriggeredBySubmit = true;
    });
  }

  return {
    init: init,
    isUnsavedChangeConfirmed: isUnsavedChangeConfirmed
  };

}());

$(WarnBeforeAccidentallyDiscardUnsavedChanges.init);

Martin Straub About 10 years ago