web-dev-qa-db-fra.com

Avertir l'utilisateur avant de quitter la page Web avec les modifications non enregistrées

J'ai quelques pages avec des formulaires dans mon application.

Comment puis-je sécuriser le formulaire de telle sorte que si quelqu'un navigue ou ferme l'onglet du navigateur, il devrait être invité à confirmer qu'il souhaite réellement quitter le formulaire avec des données non enregistrées?

298
Khan

Réponse brève et fausse:

Vous pouvez le faire en gérant l'événement beforeunload et renvoyant une chaîne non nulle :

window.addEventListener("beforeunload", function (e) {
    var confirmationMessage = 'It looks like you have been editing something. '
                            + 'If you leave before saving, your changes will be lost.';

    (e || window.event).returnValue = confirmationMessage; //Gecko + IE
    return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});

Le problème avec cette approche est que la soumission d'un formulaire déclenche également l'événement de déchargement . Ceci est facilement résolu en ajoutant le drapeau indiquant que vous soumettez un formulaire:

var formSubmitting = false;
var setFormSubmitting = function() { formSubmitting = true; };

window.onload = function() {
    window.addEventListener("beforeunload", function (e) {
        if (formSubmitting) {
            return undefined;
        }

        var confirmationMessage = 'It looks like you have been editing something. '
                                + 'If you leave before saving, your changes will be lost.';

        (e || window.event).returnValue = confirmationMessage; //Gecko + IE
        return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
    });
};

Puis en appelant le poseur lors de la soumission:

<form method="post" onsubmit="setFormSubmitting()">     
    <input type="submit" />
</form>

Mais lisez la suite ...

Réponse longue et correcte:

Vous ne souhaitez pas non plus afficher ce message lorsque l'utilisateur n'a rien modifié dans vos formulaires . Une solution consiste à utiliser l'événement beforeunload en combinaison avec un indicateur "modifié", qui ne déclenche l'invite que s'il est vraiment pertinent.

var isDirty = function() { return false; }

window.onload = function() {
    window.addEventListener("beforeunload", function (e) {
        if (formSubmitting || !isDirty()) {
            return undefined;
        }

        var confirmationMessage = 'It looks like you have been editing something. '
                                + 'If you leave before saving, your changes will be lost.';

        (e || window.event).returnValue = confirmationMessage; //Gecko + IE
        return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
    });
};

Maintenant, pour implémenter la méthode isDirty, il existe différentes approches.

Vous pouvez utiliser jQuery et la sérialisation de formulaire , mais cette approche présente des défauts. Vous devez d’abord modifier le code pour qu’il fonctionne sur n’importe quel formulaire ($("form").each() fera l'affaire), mais le plus gros problème est que serialize() de jQuery ne fonctionnera que sur des éléments nommés, non désactivés. L'élément non nommé ne déclenchera pas le drapeau sale. Il existe des solutions pour cela , comme créer des contrôles en lecture seule au lieu d'activer, sérialiser puis désactiver à nouveau les contrôles.

Les événements semblent donc être la voie à suivre. Vous pouvez essayer en écoutant les touches . Cet événement a quelques problèmes:

  • Ne se déclenchera pas sur les cases à cocher, les boutons radio ou d'autres éléments en cours de modification via l'entrée de la souris.
  • Déclenchera des pressions de touche non pertinentes comme le Ctrl clé.
  • Ne déclenchera pas sur les valeurs définies par le code JavaScript.
  • Ne se déclenchera pas en coupant ou en collant du texte dans les menus contextuels.
  • Ne fonctionnera pas avec les entrées virtuelles comme les sélecteurs de date ou les fichiers d’embellissement des cases à cocher/radiobutton qui enregistrent leur valeur dans une entrée masquée via JavaScript.

L'événement change) également ne se déclenche pas sur les valeurs définies à partir du code JavaScript , donc ne fonctionnera pas non plus pour les entrées virtuelles.

Liaison de l'événement input à tous les inputs (et textareas et selects) sur votre page ne fonctionnera pas avec les anciens navigateurs et, comme toutes les solutions de gestion d'événements mentionnées ci-dessus, ne prend pas en charge l'annulation. Lorsqu'un utilisateur modifie une zone de texte puis l'annule ou coche et décoche une case, le formulaire est toujours considéré comme étant sale.

Et lorsque vous souhaitez implémenter davantage de comportements, par exemple ignorer certains éléments, vous aurez encore plus de travail à faire.

Ne réinventez pas la roue:

Donc, avant de penser à la mise en œuvre de ces solutions et de toutes les solutions de contournement requises, sachez que vous réinventez la roue et que vous êtes susceptible de rencontrer des problèmes que d'autres ont déjà résolus pour vous.

Si votre application utilise déjà jQuery, vous pouvez également utiliser du code mis à jour et maintenu au lieu de faire rouler le vôtre, et utiliser une bibliothèque tierce pour tout cela. le plugin Are You Sure? de jQuery fonctionne très bien, consultez leur page de démonstration . C'est aussi simple que cela:

<script src="jquery.are-you-sure.js"></script>

<script>
  $(function() {
    $('#myForm').areYouSure(
      {
        message: 'It looks like you have been editing something. '
               + 'If you leave before saving, your changes will be lost.'
      }
    );
  });

</script>

Messages personnalisés non pris en charge partout

Notez que Firefox 4 ne supportait pas les messages personnalisés dans cette boîte de dialogue. Depuis le mois dernier, Chrome 51 est en cours de déploiement dans lequel des messages personnalisés sont également supprimés .

Certaines alternatives existent ailleurs sur ce site, mais je pense qu'un dialogue comme celui-ci est suffisamment clair:

Voulez-vous quitter ce site?

Les modifications que vous avez apportées peuvent ne pas être enregistrées.

LeaveStay

511
CodeCaster

Découvrez le JavaScript événement onbeforeunload . C'est un JavaScript non standard introduit par Microsoft, mais il fonctionne dans la plupart des navigateurs et leur documentation onbeforeunload contient plus d'informations et d'exemples.

75
Paul Annesley

via jquery

$('#form').data('serialize',$('#form').serialize()); // On load save form current state

$(window).bind('beforeunload', function(e){
    if($('#form').serialize()!=$('#form').data('serialize'))return true;
    else e=null; // i.e; if form state change show warning box, else don't show it.
});

Vous pouvez utiliser la fonction de sérialisation des formulaires de Google JQuery. Elle collecte toutes les entrées de formulaire et les enregistre dans un tableau. Je suppose que cette explication est suffisante :)

33
Wasim A.

Solution universelle ne nécessitant aucune configuration qui détecte automatiquement toutes les modifications d’entrée, y compris les éléments contenteditables:

"use strict";
(() => {
const modified_inputs = new Set;
const defaultValue = "defaultValue";
// store default values
addEventListener("beforeinput", (evt) => {
    const target = evt.target;
    if (!(defaultValue in target || defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ("" + (target.value || target.textContent)).trim();
    }
});
// detect input modifications
addEventListener("input", (evt) => {
    const target = evt.target;
    let original;
    if (defaultValue in target) {
        original = target[defaultValue];
    } else {
        original = target.dataset[defaultValue];
    }
    if (original !== ("" + (target.value || target.textContent)).trim()) {
        if (!modified_inputs.has(target)) {
            modified_inputs.add(target);
        }
    } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
    }
});
addEventListener("beforeunload", (evt) => {
    if (modified_inputs.size) {
        const unsaved_changes_warning = "Changes you made may not be saved.";
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
    }
});
})();
12
Eli Grey

Construit sur Wasim A. excellente idée d'utiliser la sérialisation. Le problème était que l'avertissement était également affiché lors de la soumission du formulaire. Ceci a été corrigé ici.

var isSubmitting = false

$(document).ready(function () {
    $('form').submit(function(){
        isSubmitting = true
    })

    $('form').data('initial-state', $('form').serialize());

    $(window).on('beforeunload', function() {
        if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
            return 'You have unsaved changes which will not be saved.'
        }
    });
})

Il a été testé sous Chrome et IE 11.

7
Eerik Sven Puudist

Sur la base des réponses précédentes, et des pavés provenant de divers endroits en cas de débordement de pile, voici la solution proposée par moi qui gère le cas où vous souhaitez réellement soumettre vos modifications:

window.thisPage = window.thisPage || {};
window.thisPage.isDirty = false;

window.thisPage.closeEditorWarning = function (event) {
    if (window.thisPage.isDirty)
        return 'It looks like you have been editing something' +
               ' - if you leave before saving, then your changes will be lost.'
    else
        return undefined;
};

$("form").on('keyup', 'textarea', // You can use input[type=text] here as well.
             function () { 
                 window.thisPage.isDirty = true; 
             });

$("form").submit(function () {
    QC.thisPage.isDirty = false;
});
window.onbeforeunload = window.thisPage.closeEditorWarning;

Il est à noter que IE11 semble nécessiter que la fonction closeEditorWarning renvoie undefined pour ne pas afficher une alerte.

7
Jonathan

Le one-liner suivant a fonctionné pour moi.

window.onbeforeunload = s => modified ? "" : null;

Il suffit de définir modified sur true ou false en fonction de l'état de votre application.

6
spencer.sm

Le code suivant fonctionne très bien. Vous devez atteindre les modifications d’entrée de vos éléments de formulaire via l’attribut id:

var somethingChanged=false;
            $('#managerForm input').change(function() { 
                somethingChanged = true; 
           }); 
            $(window).bind('beforeunload', function(e){
                if(somethingChanged)
                    return "You made some changes and it's not saved?";
                else 
                    e=null; // i.e; if form state change show warning box, else don't show it.
            });
        });
4
metzelder
var unsaved = false;
$(":input").change(function () {         
    unsaved = true;
});

function unloadPage() {         
    if (unsaved) {             
        alert("You have unsaved changes on this page. Do you want to leave this page and discard your changes or stay on this page?");
    }
} 
window.onbeforeunload = unloadPage;
3
Ankit

Vous pouvez vérifier une explication détaillée ici: http://techinvestigations.redexp.in/comparison-of-form-values-on-load-and-before-close/ comparaison-de-forme- valeurs-sur-chargement-et-avant-fermeture

Le code principal:

function formCompare(defaultValues, valuesOnClose) {

    // Create arrays of property names
    var aPropsFormLoad = Object.keys(defaultValues);
    var aPropsFormClose = Object.keys(valuesOnClose);

    // If number of properties is different,
    // objects are not equivalent
    if (aPropsFormLoad.length != aPropsFormClose.length) {
        return false;
    }

    for (var i = 0; i < aPropsFormLoad.length; i++) {
        var propName = aPropsFormLoad[i];

        // If values of same property are not equal,
        // objects are not equivalent
        if (defaultValues[aPropsFormLoad]+"" !== valuesOnClose[aPropsFormLoad]+"") {
            return false;
        }
    }

    // If we made it this far, objects
    // are considered equivalent
    return true;

}

//add polyfill for older browsers, as explained on the link above

//use the block below on load
    for(i=0; i < document.forms[0].elements.length; i++){
    console.log("The field name is: " + document.forms[0].elements[i].name +
        " and it’s value is: " + document.forms[0].elements[i].value );
    aPropsFormLoad[i] = document.forms[0].elements[i].value;
    }

//create a similar array on window unload event.

//and call the utility function
    if (!formCompare(aPropsOnLoad, aPropsOnClose))
    {
    //perform action: 
    //ask user for confirmation or
    //display message about changes made
    }
1
Kanika Sud

En ajoutant à l’idée de @codecaster, vous pouvez ajouter ceci à chaque page avec un formulaire (dans mon cas, je l’utilise de manière globale, de sorte que seuls les formulaires auraient cet avertissement) changer sa fonction en

if ( formSubmitting || document.getElementsByTagName('form').length == 0) 

Mettez également sur les formulaires soumettre y compris les liens de connexion et dans les boutons d'annulation afin que lorsque vous appuyez sur annuler ou soumettre le formulaire ne déclenche pas l'avertissement aussi dans chaque page avec un formulaire ...

<a class="btn btn-danger btn-md" href="back/url" onclick="setFormSubmitting()">Cancel</a>
1
Esteban Fuentealba

Réponse courte:

let pageModified = true

window.addEventListener("beforeunload", 
  () => pageModified ? 'Close page without saving data?' : null
)
1
Daniel Garmoshka

La solution d'Eerik Sven Puudist ...

var isSubmitting = false;

$(document).ready(function () {
    $('form').submit(function(){
        isSubmitting = true
    })

    $('form').data('initial-state', $('form').serialize());

    $(window).on('beforeunload', function() {
        if (!isSubmitting && $('form').serialize() != $('form').data('initial-state')){
            return 'You have unsaved changes which will not be saved.'
        }
    });
})

... a spontanément fait le travail pour moi dans un environnement complexe orienté objet sans qu'aucun changement ne soit nécessaire.

Le seul changement que j'ai appliqué consistait à faire référence à la forme concrète (une seule forme par fichier) appelée "formulaireForm" ("formulaire" -> "#formForm"):

<form ... id="formForm" name="formForm" ...>

Le fait que le bouton de soumission est "laissé seul" est particulièrement bien fait.

De plus, cela fonctionne aussi pour moi avec la dernière version de Firefox (à partir du 7 février 2019).

1
mtjmohr

Testé la solution universelle d'Eli Grey, n'a fonctionné qu'après avoir simplifié le code

  'use strict';
  (() => {
    const modified_inputs = new Set();
    const defaultValue = 'defaultValue';
    // store default values
    addEventListener('beforeinput', evt => {
      const target = evt.target;
      if (!(defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
      }
    });

    // detect input modifications
    addEventListener('input', evt => {
      const target = evt.target;
      let original = target.dataset[defaultValue];

      let current = ('' + (target.value || target.textContent)).trim();

      if (original !== current) {
        if (!modified_inputs.has(target)) {
          modified_inputs.add(target);
        }
      } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
      }
    });

    addEventListener(
      'saved',
      function(e) {
        modified_inputs.clear()
      },
      false
    );

    addEventListener('beforeunload', evt => {
      if (modified_inputs.size) {
        const unsaved_changes_warning = 'Changes you made may not be saved.';
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
      }
    });

  })();

Les modifications apportées à son sont supprimées de l'utilisation de target[defaultValue] et n'utilisent que target.dataset[defaultValue] pour stocker la valeur réelle par défaut.

Et j'ai ajouté un écouteur d'événement 'enregistré' où l'événement 'enregistré' sera déclenché par vous-même lorsque votre action de sauvegarde aura abouti.

Mais cette solution "universelle" ne fonctionne que dans les navigateurs, pas dans la visualisation Web de l'application, par exemple, les navigateurs wechat.

Pour que cela fonctionne (partiellement) dans les navigateurs web également, une autre amélioration est à nouveau:

  'use strict';
  (() => {
    const modified_inputs = new Set();
    const defaultValue = 'defaultValue';
    // store default values
    addEventListener('beforeinput', evt => {
      const target = evt.target;
      if (!(defaultValue in target.dataset)) {
        target.dataset[defaultValue] = ('' + (target.value || target.textContent)).trim();
      }
    });

    // detect input modifications
    addEventListener('input', evt => {
      const target = evt.target;
      let original = target.dataset[defaultValue];

      let current = ('' + (target.value || target.textContent)).trim();

      if (original !== current) {
        if (!modified_inputs.has(target)) {
          modified_inputs.add(target);
        }
      } else if (modified_inputs.has(target)) {
        modified_inputs.delete(target);
      }

      if(modified_inputs.size){
        const event = new Event('needSave')
        window.dispatchEvent(event);
      }
    });

    addEventListener(
      'saved',
      function(e) {
        modified_inputs.clear()
      },
      false
    );

    addEventListener('beforeunload', evt => {
      if (modified_inputs.size) {
        const unsaved_changes_warning = 'Changes you made may not be saved.';
        evt.returnValue = unsaved_changes_warning;
        return unsaved_changes_warning;
      }
    });

    const ua = navigator.userAgent.toLowerCase();

    if(/MicroMessenger/i.test(ua)) {
      let pushed = false

      addEventListener('needSave', evt => {
        if(!pushed) {
          pushHistory();

          window.addEventListener("popstate", function(e) {
            if(modified_inputs.size) {
              var cfi = confirm('确定要离开当前页面嘛?' + JSON.stringify(e));
              if (cfi) {
                modified_inputs.clear()
                history.go(-1)
              }else{
                e.preventDefault();
                e.stopPropagation();
              }
            }
          }, false);
        }

        pushed = true
      });
    }

    function pushHistory() {
      var state = {
        title: document.title,
        url: "#flag"
      };
      window.history.pushState(state, document.title, "#flag");
    }
  })();
0
Jeff Tian