Рассказ о “странном” баге и о том, как влияет верстка работу всего приложения.

При работе с ASP.NET Webforms мы постоянно сталкиваемся с формами. По умолчанию, это одна серверная форма <form id=”aspnetForm” runat=”sever”>, расположенная сразу же за тегом <body>. Но это, можно сказать, классический пример - такую заготовку делает нам Visual Studio при создании другого проекта. На практике же всё может сильно отличаться.

 

Следует отметить, что на странице вы не можете создать более одной серверной формы (с атрибутом runat=”server”). Такое уж ограничение архитектуры ASP.NET. Почему так - догадаться не сложно, но...

 

Но в жизни в проекте бывают ситуации, когда просто необходимо добавить ещё одну форму на страницу. Из достаточно распространённых примеров это: 

 

когда в шапке сайта где-то рядом с меню нужно разместить форму поиска, при этом, для увеличения скорости загрузки, сама шапка сайта находится вне серверной формы. Такой случай достаточно распространённый и уже практически стал стандартом де-факто для разработчиков.

 

Решение данной задачи достаточно простое - добавление второй формы (<form>) на страницу, но уже без атрибута runat=”server”. Следует отметить, что с точки зрения html - несколько форм на одной странице являются абсолютно нормальным и работающем решением. Если только не наступать та те же грабли, на которые наступил я...

 

А допустил я достаточно “детскую” ошибку - т.к. было ограничение на одну серверную форму, то я добавил вторую, клиентскую, внутрь серверной. На первый взгляд всё работало хорошо, обе формы успешно отправлялись на сервер и отправляли все необходимые данные. Вот только работало это в браузерах Google Chrome, Safari и FireFox. Проблемы начались в Opera и Internet Explorer. Выглядело это, на мой взгляд, действительно потрясающе: 

 

Форма фидбека с UpdatePanel двумя TextBox и LinkButton на странице. Обработчик OnClick у кнопки успешно отрабатывал, за исключением того, что значения текстбоксов были пустыми. Дело было вечером и я подумал что проблема в UpdatePanel. Вот только странно было что в IE это тоже не работало. Под нож попала UpdatePanel, что не дало никакого положительного результата. “Странно” - подумал я и проверил это всё ещё раз в разных браузерах. Баг был на месте. Тут пришлось вплотную взяться дебаггером за эту страницу и через некоторое время, внимательно посмотрев на Request.Params причина была обнаружена - сабмитилась не та форма.

 

Дальнейшие танцы с бубном возникшие идеи результата не принесли я на помощь пришел Google:

 

 

Обрадовало меня сразу две вещи:

 

  • такая проблема была не только у меня;
  • причина всего этого безобразия стала ясна уже только при беглом просмотре результатов выдачи гугла - 3-я ссылка вела на сайт W3C, на которой черным по белому было написано: “the FORM element can’t be nested”. (http://www.w3.org/MarkUp/html3/forms.html)

 

 

После этого проблема была быстро решена способом, описанным в начале поста, но мне захотелось ещё немного копнуть внутрь.

 

Тут же был создан небольшой html файл (forms1.html)  и был дан валидатору http://validator.w3.org/. К моему удивлению, валидатор был уверен в том, что html код является полностью валидным. После смены doctype с Transitional на Strict (forms2.html) страница уже оказывалась невалидной. И тут всё окончательно прояснилось и стало на свои места.

 

С точки зрения html, как и с точки зрения здравого смысла - ничего хорошего не принесут. Всё дело лишь в том, что с годами некоторые браузеры стали настолько умными, что уже понимают практически все общеизвестные ошибки в вёрстке и делают это так, как, по их мнению, задумывалось. Отсюда и различное поведение одних страниц в разных браузерах, и попытки обойти проблемы одного браузера, не поломав ничего в других и т.д.

 

В который раз убеждаюсь в необходимости грамотного верстальщика на любом проекте и необходимости соблюдать html-стандарты на своих сайтах. Только не фанатично, у всех правил есть исключения...

 

Исходники форм: https://github.com/e0ne/BlogSamples/tree/master/NestedForms

 


Продолжение предыдущего поста: http://blog.e0ne.info/post/Modal-popup-with-HTML.aspx

Часть 2. Практика

Если есть проблема, то, обязательно, должно быть решение. Так как готового я не нашел (возможно, плохо искал), пришлось изобретать свое.  Решений, на самом деле, нашлось аж целых два, но так как я остановился лишь на одном, то это решение я рассмотрю более подробно и с примером.

Для начала рассмотрю тот вариант, который я не стал реализовывать.

Решение проблемы #1: манипуляции с свойством tabIndex.

Те, кто сталкивался с необходимостью навигацией по сайту с помощью клавиатуры непременно сталкивались с таким свойством элементов, как tabIndex. По умолчанию, для всех элементов <input /> (кроме <input type=”hidden” />) и <option /> tabIndex равен 0. Для других элементов свойство или не определено, или задано значение “-1” (минус еденица) В таком случае переход по элементам осуществляется в порядке их расположения в DOM-моделе. Если необходимо сделать так, чтоб при навигации по странице с помощью кнопки tab фокус на элемент не попадал, то значение tabIndex необходимо сделать -1.

Исходя из вышесказанного, можно сделать такой алгоритм создания модального окна:

  • реализовать диалоговое окно, как описано в предыдущем посте;
  • при вызове функции show() для всех элементов запоминаем текущее состояние tabIndex и ставим новое значение, равное “-1”;
  • в вызове функции hide() восстанавливаем прежние значения tabIndex.

Так как этот способ на практике я не реализовывал, то не могу сказать какие проблемы он может вызвать.

Решение проблемы #2: манипуляции с DOM.

Мой диалог работает следующим образом: базовая часть отображения остается без изменений, все изменения касаются только функций show и hide и некоторыми манипуляциями в DOM-модели.

Работает это следующим образом:

  • все содержимое тега body обрамляем контейнером:
    $(document.body).wrapInner("<div id='body-container' />");
  • делаем метку, где находится наш диалог:
    $("<div id='dlg-marker'/>").insertBefore($("#dlg-content"));
  • перемещаем содержимое диалога в начало тега body:
    $(document.body).prepend($("#dlg-content"));
  • ставим атрибут disabled=”true” для всех элементов, которые не находятся в диалоге:
    $("#body-container").attr('disabled', true);
    // for webkit based browsers
    $("#body-container > input, option").each(function(){
        $(this).attr('disabled', true);
    });
  • показываем диалог.

Соответственно, в методе hide() необходимо все вернуть на своё место:

  • убираем атрибут disabled:
    $('#body-container').removeAttr('disabled');
  • ставим диалог на то место, где он находился изначально; для этого нам и нужна была метка:
    $("#dlg-content").insertAfter($("#dlg-marker"));
  • удаляем ранее созданную метку:
    $("#dlg-marker").detach();
  • удаляем элемент, в который мы поместили содерживое body:
    var html = $('#body-container').html();
    $('#body-container').detach();
    $(document.body).html(html);
  • прячем диалог.

Этот код является слишком неоптимизированным - от него можно избавится, если элемент <div id='body-container' /> будет находится на странице всегда.

Манипуляции с возвращением диалога на прежнее место, прежде всего, необходимы для корректной работы приложений, написанных с помощью ASP.NET. Если этого не сделать, то будут проблемы c UpdatePanel и кнопками, которые находятся вне формы.

В файле advanced-popup-with-input.html я привожу базовую реализацию такого способа. Для использования в production его необходимо немного доработать напильником. А именно:
оформить это всё в виде плагина для jQuery или виджета jQueryUi;
решить проблему с производительностью и обработчиками событий при возвращении элементов в начальное положение относительно DOM-модели.

Сейчас похожая реализация успешно работает в production коде, единственное отличие состоит в том, что элемент <div id='body-container' /> всегда находится на странице и нет необходимости его добавлять и удалять.

Все примеры доступны на GitHub: https://github.com/e0ne/BlogSamples/tree/master/ModalDialog


Часть 1. Теория


Сейчас использование модальных и не только диалогов на веб-сайтах является вполне нормальным явлением, в следствии чего нам доступна масса уже готовых контролов как для asp.net/.... (подставьте сюда ту технологию, которую вы используете в своих проектах), так и множество плагинов для популярных javascript-фреймворком, таких как jquery, moo tools, prototype и других. Для упрощения договоримся, что здесь под контролом я буду иметь в виду все что, что в итоге превращается в html-код и выглядит как привычное нам модальное диалоговое окно.

Реализация диалогового окна на html выглядит следующим образом:

  • в какой-то элемент (например, <div id=”container”>) помещается весь контент диалогового окна и ставится ему свойства display:none; z-index:100; position:absolute;
  • создается “фоновый” элемент для создания эффекта модальности с такими свойствами: background-color:gray; position: absolute; width: 100%; height: 100%;     z-index: 90; display:block; opacity:.32; top:0; left:0;display:none; (например, <div id=”background”>).  - этот пункт нужен только для создания модального диалога;
  • при отображении диалогового окна мы меняем у нашего контейнера и фона свойства display на значение block и видим эффект (модального) диалогового окна.

Пример простого pop-up’а находится в файле simple-popup.html

Все модальные окна, реализованные на HTML, которые встречал работали по описанному алгоритму. Изменения были лишь в удобстве работы с ними и набором дополнительных фич, таких как: drag-n-drop, поддержка тем, обработчики событий и др.

Я всегда использовал этот алгоритм до того момента, как мне не пришлось делать возможность навигации по кнопкам внутри модального диалога с помощью клавиатуры. Для примера, рассмотрим работу диалого, реализованного в файле simple-popup-with-input.html.

При нажатии на кнопку show показывается наш модальный диалог. При этом, мы не можем мышкой перейти на поле ввода вне диалога или кликнуть по кнопке show еще раз до закрытия текущего. С первого взгляда все кажется вполне работоспособным. Но кажется это только до нажатия “магической” (magic!) кнопки tab. И тут мы попадаем на наши поля ввода в таком порядке:

  • кнопка show;
  • поле ввода за кнопкой show;
  • поле ввода на диалоге
  • кнопка hide;
  • элементы управления браузера;
  • и так по кругу...

Здесь мы видим, что модальный диалог никакой не модальный, а только кажется таковым.

Как это работает?

Диалоги на HTML реализуются с помощью слоев отображения элементов. По умолчанию, у нас есть только один слой, т.е. свойство z-index у всех элементов одинаково и равно 0. Для создания нового слоя необходимо выставить свойство z-index в 1, 2, и т.д. В нашем примере, у элементов, не входящих в диалог, z-index равен 0, у фона диалога z-index равен 90, а его высота и ширина равняется 100%, что соответствует ширине и высоте окна браузера, из-за чего он визуально перекрывает элементы, у которых z-index < 90 и складывается впечатление модальности. Последним у нас идет элемент с содержимым диалога, у которого z-index равен 100 (максимальное, в нашем случае, значение), что позволяет нам отобразить его на “переднем” слоем.

Из-за слоев, мы не можешь попасть мышкой на кнопку show, но прекрасно можем добраться до нее с помощью клавиатуры (кнопка tab), а также вводить данные в текстовое поле вне диалога. Получается такой себе полу-модальный диалог с неправильной навигацией с клавиатуры.

В следующей части я расскажу как можно обойти данную проблему и покажу прототип модального диалога, в котором корректно работает tab order.

popup.zip (25,42 kb)