繁体   English   中英

在离开 web 页面之前警告用户未保存的更改

[英]Warn user before leaving web page with unsaved changes

我的应用程序中有一些带有 forms 的页面。

如何以这样一种方式保护表单,如果有人离开或关闭浏览器选项卡,应提示他们确认他们确实想要留下未保存数据的表单?

简短的错误答案:

您可以通过处理beforeunload事件并返回一个非空字符串来做到这一点:

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.
});

这种方法的问题是提交表单也会触发 unload 事件 通过添加您正在提交表单的标志可以轻松解决此问题:

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.
    });
};

然后在提交时调用setter:

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

但是请继续阅读...

长而正确的答案:

当用户没有更改表单上的任何内容时,您也不想显示此消息。 一种解决方案是将beforeunload事件与“脏”标志结合使用,该标志仅在确实相关时才会触发提示。

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.
    });
};
    

现在要实现isDirty方法,有多种方法。

您可以使用jQuery 和表单序列化,但这种方法有一些缺陷。 首先,您必须更改代码以在任何表单上工作( $("form").each()可以),但最大的问题是 jQuery 的serialize()只能在命名的、非禁用的元素上工作,所以改变任何禁用或未命名的元素都不会触发脏标志。 有一些解决方法,例如将控件设为只读而不是启用、序列化然后再次禁用控件。

所以事件似乎是要走的路。 您可以尝试监听按键 本次活动有几个问题:

  • 不会在复选框、单选按钮或其他通过鼠标输入更改的元素上触发。
  • 将触发不相关的按键,例如Ctrl键。
  • 不会触发通过 JavaScript 代码设置的值。
  • 通过上下文菜单剪切或粘贴文本时不会触发。
  • 不适用于日期选择器或复选框/单选按钮美化器等虚拟输入,它们通过 JavaScript 将其值保存在隐藏输入中。

change事件不会触发从 JavaScript 代码设置的值,因此也不适用于虚拟输入。

input事件绑定到页面上的所有input (以及textareaselect在旧版浏览器上不起作用,并且像上面提到的所有事件处理解决方案一样,不支持撤消。 当用户更改文本框然后撤消它,或者选中和取消选中复选框时,表单仍然被认为是脏的。

而当您想要实现更多行为时,例如忽略某些元素,您将有更多工作要做。

不要重新发明轮子:

因此,在您考虑实施这些解决方案和所有必需的解决方法之前,请意识到您正在重新发明轮子,并且您很容易遇到其他人已经为您解决的问题。

如果您的应用程序已经使用 jQuery,那么您最好使用经过测试、维护的代码而不是自己编写代码,并使用第三方库来完成所有这些工作。

jquery.dirty (由@troseman 在评论中建议)提供了正确检测表单是否已更改以及防止用户在显示提示时离开页面的功能。 它还具有其他有用的功能,例如重置表单,以及将表单的当前状态设置为“干净”状态。 示例用法:

$("#myForm").dirty({preventLeaving: true});

jQuery 的 Are You Sure?是一个较旧的、目前被放弃的项目插件,也很好用; 查看他们的演示页面 示例用法:

<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>

并非所有地方都支持自定义消息

请注意, 自 2011 年以来,Firefox 4不支持此对话框中的自定义消息。 截至 2016 年 4 月,Chrome 51 正在推出,其中自定义消息也被删除

这个网站的其他地方有一些替代方案,但我认为这样的对话足够清楚:

你想离开这个网站吗?

您所做的更改可能不会保存。

离开停留

查看 JavaScript onbeforeunload 事件 它是 Microsoft 引入的非标准 JavaScript,但它适用于大多数浏览器,并且其 onbeforeunload 文档有更多信息和示例。

通过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.
});

您可以使用 Google JQuery Form Serialize 函数,这将收集所有表单输入并将其保存在数组中。 我想这个解释就足够了:)

无需配置即可自动检测所有输入修改的通用解决方案,包括 contenteditable 元素:

"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);
    }
});
// clear modified inputs upon form submission
addEventListener("submit", (evt) => {
    modified_inputs.clear();
    // to prevent the warning from happening, it is advisable
    // that you clear your form controls back to their default
    // state with evt.target.reset() or form.reset() after submission
});
// warn before closing if any inputs are modified
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;
    }
});
})();

建立在Wasim A. 使用序列化的绝妙想法之上。 问题是在提交表单时也显示了警告。 这已在此处修复。

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.'
        }
    });
})

它已经在 Chrome 和 IE 11 中进行了测试。

根据之前的答案,并从堆栈溢出的各个地方拼凑而成,这是我想出的解决方案,它可以处理您实际想要提交更改的情况:

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;

值得注意的是,IE11 似乎要求closeEditorWarning函数返回undefined以使其不显示警报。

以下单线对我有用。

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

只需根据应用程序的状态将modified设置为truefalse

您可以使用 serialize() 通过序列化表单值来创建 URL 编码的文本字符串,并在卸载之前检查表单是否已更改

$(document).ready(function(){
    var form = $('#some-form'),
        original = form.serialize()

    form.submit(function(){
        window.onbeforeunload = null
    })

    window.onbeforeunload = function(){
        if (form.serialize() != original)
            return 'Are you sure you want to leave?'
    }
})

请参阅此链接https://coderwall.com/p/gny70a/alert-when-leaving-page-with-unsaved-form由 Vladimir Sidorenko 撰写

以下代码效果很好。 您需要通过 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.
            });
        });
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;

测试了 Eli Grey 的通用解决方案,仅在我将代码简化为

  '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;
      }
    });

  })();

对他的修改删除了target[defaultValue]的用法,只使用target.dataset[defaultValue]来存储真正的默认值。

我添加了一个“已保存”事件侦听器,其中“已保存”事件将在您的保存操作成功时由您自己触发。

但是这种“通用”解决方案只适用于浏览器,不适用于应用程序的 webview,例如微信浏览器。

为了使其在微信浏览器(部分)中也能正常工作,再次进行了另一项改进:

  '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");
    }
  })();

简短的回答:

let pageModified = true

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

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.'
        }
    });
})

...在一个复杂的面向对象的环境中自发地为我完成了这项工作,没有任何必要的改变。

我应用的唯一更改是引用名为“formForm”('form' -> '#formForm')的具体表单(每个文件只有一个表单):

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

做得特别好的事实是提交按钮被“单独留下”。

此外,它也适用于最新版本的 Firefox(截至 2019 年 2 月 7 日)。

添加到@codecaster 的想法,您可以将其添加到带有表单的每个页面(在我的情况下,我以全局方式使用它,因此只有在表单上才会有此警告)将他的功能更改为

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

还放置表单提交,包括登录和取消按钮链接,因此当人们按下取消或提交表单时,也不会在没有表单的每个页面中触发警告......

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

您可以在此处查看详细说明: http ://techinvestigations.redexp.in/comparison-of-form-values-on-load-and-before-close/comparison-of-form-values-on-load-and - 关闭前

主要代码:

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
    }

我做了不同的事情,在这里分享,以便有人可以得到帮助,仅使用 Chrome 进行测试。

仅当有一些更改时,我才想在关闭选项卡之前警告用户。

<input type="text" name="field" value="" class="onchange" />

var ischanged = false;

$('.onchange').change(function () {
    ischanged = true;
});

window.onbeforeunload = function (e) {
    if (ischanged) {
        return "Make sure to save all changes.";
    }        
};

效果很好,但是遇到了另一个问题,当我提交表单时,我收到了不需要的警告,我看到了很多解决方法,这是因为 onbeforeunload 在 onsubmit 之前触发,这就是为什么我们无法在 onsubmit 事件中处理它,比如onbeforeunload = null ,但提交按钮的 onclick 事件在这两个事件之前触发,所以我更新了代码

var isChanged = false;
var isSubmit = false;

window.onbeforeunload = function (e) {
    if (isChanged && (!isSubmit)) {
        return "Make sure to save all changes.";
    }        
};

$('#submitbutton').click(function () {
    isSubmit = true;
});

$('.onchange').change(function () {
    isChanged = true;
});

 onbeforeunload=confirm /* Put this line in your JavaScript code */

运行此代码片段后,请单击/点击代码下方的白色区域

非常感谢

我做了以下代码。 它可以比较所有字段(标有 .ignoreDirty 类的字段除外)的更改,也可以选择仅比较当前可见字段的更改。 它可以为 Javascript 添加的新字段重新初始化。 因此,我保存的不是表单状态,而是每个控件的状态。

/* Dirty warning for forms */
dirty = (skipHiddenOrNullToInit) => {
    /*  will return True if there are changes in form(s)
        for first initialization you can use both: .dirty(null) or .dirty() (ignore its result)
            .dirty(null) will (re)initialize all controls - in addititon use it after Save if you stay on same page
            .dirty() will initialize new controls - in addititon use it if you add new fields with JavaScript
        then
            .dirty() (or: .dirty(false)) says if data are changed without regard to hidden fields
            .dirty(true) says if data are changed with regard to hidden fields (ie. fields with .d-none or .hidden class)
        controls with .ignoreDirty class will be skipped always
        previous about .d-none, .hidden, .ignoreDirty applies to the control itself and all its ancestors
    */
    let isDirty = false;
    let skipSelectors = '.ignoreDirty';
    if (skipHiddenOrNullToInit) {
        skipSelectors += ', .d-none, .hidden'
    } else if (skipHiddenOrNullToInit === undefined) {
        skipHiddenOrNullToInit = false;
    }
    $('input, select').each(
    function(_idx, el) {
        if ($(el).prop('type') !== 'hidden') {
            let dirtyInit = $(el).data('dirty-init');
            if (skipHiddenOrNullToInit === null || dirtyInit === undefined) {
                try {
                    isChromeAutofillEl = $(el).is(":-webkit-autofill");
                } catch (error) {
                    isChromeAutofillEl = false;
                }
                if (isChromeAutofillEl && $(el).data('dirty-init') === undefined) {
                    setTimeout(function() {  // otherwise problem with Chrome autofilled controls
                        $(el).data('dirty-init', $(el).val());
                    }, 200)
                } else {
                    $(el).data('dirty-init', $(el).val());
                }
            } else if ($(el).closest(skipSelectors).length === 0 && dirtyInit !== $(el).val()) {
                isDirty = true;
                return false; // breaks jQuery .each
            }
        }
    }
    );
    return isDirty;
}

我在使用 Chrome 自动填充值时遇到了额外的麻烦,因为它很难初始化并已经加载它们。 所以我不在页面加载时初始化,而是在任何焦点事件中初始化。 (但是:JavaScript 更改的控制值可能仍然存在问题。)我使用以下代码在页面加载时调用:

let init_dirty = (ifStayFunc) => {
    /*  ifStayFunc: optional callback when user decides to stay on page
    use .clearDirty class to avoid warning on some button, however:
        if the button fires JavaScript do't use .clearDirty class and instead
            use directly dirty(null) in code - to be sure it will run before window.location */
    $('input, select').on('focusin', function(evt) {
        if (!$('body').data('dirty_initialized')) {
            dirty();
            $('body').data('dirty_initialized', true);
        }
    });
    window.addEventListener('beforeunload', (evt) => {
        if (dirty(true)) {
            if (ifStayFunc) {
                ifStayFunc();
            }
            evt.preventDefault();
            evt.returnValue = '';  // at least Google Chrome requires this
        }
    });
    $('.clearDirty').on('click', function(evt) {
        dirty(null);
    });
};

因此,我将 .clearDirty 类添加到提供 Save 的按钮中,这样我就可以防止在这种情况下发出警告。 如果用户在收到警告时留在页面上,则回调 ifStayFunc 允许我做某事。 通常我可以显示额外的保存按钮(如果我仍然只看到一些默认/主按钮,这使得 Safe+SomethingMore - 我希望允许保存而不使用这个“SomethingMore”)。

首先,大部分浏览器默认都有这个功能。 为什么你需要这个? 为什么不保持表单同步? 我的意思是,将其保存在任何更改上,而无需等待用户提交任何内容。 就像 Google 通讯录一样。 当然,如果只有表单中的所有字段都是强制性的。 用户不喜欢强行填满东西而没有机会离开去思考他们是否需要它。 :)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM