Заметки о вёрстке сайтов  ·  Александр Шуркаев об HTML, CSS и JavaScript (скрипты, справочники и примеры по сайтостроению)

Событие onbeforeunload избавляет от потери данных

25 апреля 2003 г.

Предотвращение потери данных в админских интерфейсах и любых других HTML формах.

Постановка задачи

Недавно Дима Смирнов высказал такую идею:

Есть админский интерфейс — или любая форма — и в ней есть Критичные Поля (КП). После того, как содержание КП изменилось (т.е. было отредактировано) пользователь не может уйти со страницы по любой ссылке, потому что выскакивает диалог «Вы не сохранили изменения. Действительно хотите уйти отсюда?». А то у многих такая проблема: либо при редактировании случайно уходишь по ссылке, либо забываешь — постил ты изменения или не постил.

В комментариях к вышеприведённому посту предлагается несколько вариантов решения. Кто-то предлагает только проверять модификацию КП, не учитывая, были ли реально внесены изменения. Кто-то ратует за сохранение первичных значений либо в hidden полях, либо в переменных.

На мой взгляд, правильным будет первый подход. Прежде всего потому, что КП могут быть не только текстовыми. Это может быть, например, checkbox, а у него, как вы понимаете, нужно проверять не значение атрибута value, а состояние свойства checked. Можно, конечно, учитывать тип КП, но скрипт тогда станет неповоротлив. Да и вообще, большинство программ не заморачиваются проверкой на реальные изменения, а ограничиваются установкой некого флага modified, что обычно визуально выражается в появлении звездочки (*) после названия файла в заголовке программы.

Решение

Исходя из приведённых рассуждений, родилось следующее решение:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta content="text/html; charset=windows-1251" http-equiv="Content-Type" />
<title>check_modified</title>
<style type="text/css">
<!--
.modified{
border:1px solid red
}
-->
</style>
<script type="text/javascript">
<!--
/*
written by alexander shurkayev <alshur@ya.ru> | http://htmlcssjs.ru
*/

var root = window.addEventListener || window.attachEvent ? window : document.addEventListener ? document : null;
var cf_modified = false;
var WIN_CLOSE_MSG = "\nВы не сохранили изменения. Действительно хотите уйти отсюда?\n";

function set_modified(e){
  var el = window.event ? window.event.srcElement : e.currentTarget;
  el.className = "modified";
  cf_modified = true;
}

function ignore_modified(){
  if (typeof(root.onbeforeunload) != "undefined") root.onbeforeunload = null;
}

function check_cf(){
  if (cf_modified) return WIN_CLOSE_MSG;
}

function init(){
  if (typeof(root.onbeforeunload) != "undefined") root.onbeforeunload = check_cf;
  else return;

  for (var i = 0; oCurrForm = document.forms[i]; i++){
    for (var j = 0; oCurrFormElem = oCurrForm.elements[j]; j++){
      if (oCurrFormElem.getAttribute("cf")){
        if (oCurrFormElem.addEventListener) oCurrFormElem.addEventListener("change", set_modified, false);
        else if (oCurrFormElem.attachEvent) oCurrFormElem.attachEvent("onchange", set_modified);
      }
    }
    if (oCurrForm.addEventListener) oCurrForm.addEventListener("submit", ignore_modified, false);
    else if (oCurrForm.attachEvent) oCurrForm.attachEvent("onsubmit", ignore_modified);
  }
}

if (root){
  if (root.addEventListener) root.addEventListener("load", init, false);
  else if (root.attachEvent) root.attachEvent("onload", init);
}
//-->
</script>
</head>

<body>
<form style="width:350px">
<fieldset>
<legend>Форма</legend>
<div style="padding:20px">
<label for="textfield"><strong>Textfield</strong></label>
<br />
<input name="textfield" id="textfield" type="text" value="textfield" cf="true" />
<br />
<label for="textarea"><strong>Textarea</strong></label>
<br />
<textarea name="textarea" id="textarea" wrap="virtual" cf="true">textarea</textarea>
<br />
<label for="checkbox"><strong>Checkbox</strong></label>
<br />
<input type="checkbox" name="checkbox" id="checkbox" value="checkbox" cf="true" />
checkbox<br /><br />
<input type="submit" value="Submit!" tabindex="3" />
</div>
</fieldset>
</form>
<a href="/">link1</a><br />
<a href="javascript:self.close()">link2</a>
</body>
</html>

Вот как это дело выглядит (не обращайте внимание на убогость формы, я намеренно исключил дополнительное форматирование):

Форма





checkbox

link1
link2

Краткое описание работы…

Прежде всего, мы определили CSS стиль .modified. Он служит для индикации изменённого КП. Определим этот стиль, например, таким образом:

.modified{
border:1px solid red
}

Затем мы указываем во всех КП придуманный атрибут cf (critical field). Например:

<input type="checkbox" name="checkbox" id="checkbox" value="checkbox" cf="true" />

В плане HTML и CSS больше ничего для работы скрипта указывать не нужно. Перейдём к самому скрипту.

В функции инициализации (init()) мы вешаем на элемент root (это либо window, либо document, зависит от браузера) обработчик события onbeforeunload (функция check_cf()). При возникновении события и если у нас изменилось какое-либо КП, этот обработчик вызывает диалоговое окно с вопросом, указанным в переменной WIN_CLOSE_MSG.

Затем в функции init() мы пробегаем все формы на странице, и если очередной элемент очередной формы имеет придуманный нами атрибут cf, то мы вешаем на данный элемент обработчик события onchange (функция set_modified(e)).

…и немного теории

Событие onbeforeunload возникает в следующих случаях:

  1. Закрытие текущего окна браузера.
  2. Переход к другой странице, набрав её URL в адресной строке или через закладки.
  3. Нажатие кнопки Back, Forward, Refresh или Home.
  4. Нажатие на ссылку, ведущую к новой странице.
  5. Вызов события click у тега a.
  6. Вызов метода document.write.
  7. Вызов метода document.open.
  8. Вызов метода document.close.
  9. Вызов метода window.close.
  10. Вызов метода window.open с указанием в качестве имени нового окна значения _self.
  11. Вызов метода window.navigate или window.NavigateAndFind.
  12. Вызов метода location.replace.
  13. Вызов метода location.reload.
  14. Указание нового значения свойства location.href.
  15. Отправка значений формы через кнопку input type=submit или вызовом метода form.submit.

Обратите внимание на последний пункт. Поскольку при отправке значений формы нам не нужно проверять изменились поля или же нет, мы отключаем обработчик события onbeforeunload (функция ignore_modified()).

Пункт четыре в списке указывает, что событие onbeforeunload возникает также при нажатии на ссылку. Но в моём примере (<a href="/">link1</a>) обработчик иногда почему-то вызывается два раза. Спишем это на неустойчивую психику IE.

Ах да, я же забыл самое главное. Событие onbeforeunload поддерживается на данный момент только в Internet Explorer 4+. С начала 2001 года идут разговоры о реализации этого события в Mozilla, но разработчики браузера упорно отрицают необходимость поддержки данного события, аргументируя это возможным злоупотреблением.

На самом деле, злоупотребления никакого не может быть, если действовать по тому принципу, как реализована поддержка события onbeforeunload в IE.

Во-первых, разработчик сайта никогда не сможет запретить закрытие окна (указав в обработчике return false), так как булево значение false будет расценено IE как строка "false" и она появится в диалоговом окне закрытия текущего окна браузера.

Кроме того, в этом диалоговом окне вы можете указать только часть сообщения, которое будет показано пользователю. Перед вашей частью сообщения будет написано (английская версия IE) Are you sure you want to navigate away from this page?, а после — Press OK to continue, or Cancel to stay on the current page. Так что подменить кнопки, например, не получится.

Возвращаясь к браузерной совместимости, стоит упомянуть событие onunload, которое, казалось бы, может подойти для нашей задачи ввиду его лучшей поддержке в других браузерах. Но увы, это событие не подходит, так как оно не позволяет отменить закрытие окна, оно лишь предназначено для сборки мусора и тому подобных задач.

Заключение

Учитывая, что я старался сделать скрипт простым и forward-compatible, я не стал заморачиваться эмуляцией кликов по ссылкам и прочей чепухой, которая бы позволила скрипту работать в Opera, например. Кому нужно, тот сможет, основываясь на моей программке, написать свой вариант.

Всем пока. По всей видимости, это последняя заметка в старом формате. Скоро будет новая версия сайта, она вам понравится.

P. S. Начиная с версии 1.7 браузер Mozilla поддерживает событие onbeforeunload!

Хитовые статьи про разработку сайтов

Рассылка новостей и новых статей

Сообщения будут приходить пару раз в неделю, не чаще

Объявления

LiveInternet