четверг, 26 марта 2009 г.

Универсальное решение для борьбы с «просвечивающими» SELECT-ами в Internet Explorer


UPD [08.06.2009] ie-iframe-substrate 1.0.2
UPD [10.04.2009] ie-iframe-substrate 1.0.1

Введение

Как известно, Internet Explorer версий 5.5 и 6.0 содержит ошибку (aka "z-index issue"), которая заключается в том, что элементы SELECT "пробивают" любые другие элементы (например, позиционированные DIV-ы), размещённые над ними.



Единственный элемент, с которым этого не происходит — IFRAME. Именно на использовании его в качестве "прокладки" и основываются все методы устранения описанной выше ошибки IE [^]. Как правило, в них применяется один из двух вариантов. Либо, IFRAME вставляется внутрь (первым потомком) необходимого элемента, делается прозрачным, и стиль z-index устанавливается в -1. Либо, IFRAME добавляется в DOM, позиционируется непосредственно под необходимым элементом и делается равным ему по размеру.

При кажущейся простоте, у первого варианта два серьёзных недостатка. Во-первых, изменяются "внутренности" элемента, что может привести к конфликтам при динамической обработке контента javascript-ом. Во-вторых, "прикрыть", например, элементы с overflow:auto или overflow:scroll весьма проблематично. Второй вариант не имеет данных недостатков, т.к. IFRAME ни коим образом не связан с элементом, который он "прикрывает". Однако, существует проблема изменения позиции или размера элемента, которым IFRAME постоянно должен соответствовать.

В описываемом ниже решении для борьбы с «просвечивающими» SELECT-ами используется второй вариант размещения IFRAME.

"IFRAME-подложка"

Теперь собственно о главном. Предлагаемое решение основано на применении javascript-expressions в CSS стилях. Используется оно следующим образом:
  1. Скачиваем последнюю версию ie-iframe-substrate.css (~1.3 KB)
  2. Подключаем его на страницу при помощи тэга LINK или в любой CSS-файл при помощи @import
  3. Прямо в этом же файле переписываем селекторы для элементов, которым необходима "подложка" (обязательно с * html).
    Например: * html div.some-layer, * html #someLayer {...}
Всё.

Протестировать можно вот на этих страницах: тест 1, тест 2, тест 3.
К сведению: протестированный для сравнения bgIframe (плагин для jQuery) часть приведённых выше тестов пройти не смог.

Некоторые ограничения и особенности:
  1. "Прикрываемый" элемент не должен использовать стиль behavior
  2. У каждого "прикрываемого" элемента создаётся и используется свойство "_i" (element._i)
  3. Для "IFRAME-подложки" используется CSS-класс "ie-substrate" (iframe.ie-substrate)
Как это работает (в версии 1.0.0):

/**
 * Селектор слоёв (позиционированных элементов),
 * которым нужно создать "IFRAME-подложку".
 */
* html div.layer { /* Скрываем от IE 7+ в "standards-compliant" режиме */
    behavior: expression( 
        /* Если документ загружен и распаршен */
        document.readyState=='complete' ? (
            /* и браузер не IE 7+ (в "quirks" режиме) */
            parseFloat(navigator.appVersion.split('MSIE')[1])<7
            /* и "IFRAME-подложка" ещё не создана */
            && this.iframe==null ? (
                /* создаём IFRAME и вставляем его прямо перед данным элементом (слоем) */
                this.iframe = this.parentNode.insertBefore(
                    document.createElement('iframe'), this
                ),
                /* устанавливаем свойство "src" (важно для HTTPS соединения) */
                this.iframe.src = 'javascript:false',
                /* устанавливаем свойство "id" (для функции обновления) */
                this.iframe.id = '_ie_sub' + new Date().valueOf(),
                /* устанавливаем свойство "layer" для ссылки на данный слой */
                this.iframe.layer = this,
                /* создаём функцию обновления (важно для IE 6.0) */
                this.iframe.func = new Function(
                    "var iframe = document.getElementById('" + this.iframe.id +
                    "'); iframe.allowTransparency=true; iframe.allowTransparency=false;"
                ),
                /* устанавливаем соответствующее имя класса */
                this.iframe.className = 'ie-substrate',
                /* сбрасываем ссылку на IFRAME и устанавливаем флажок */
                this.iframe = true,
                /* отключаем "behavior" для данного элемента */
                this.style.behavior = 'none'
            /* иначе - отключаем "behavior" для данного элемента */
            ) : this.style.behavior = 'none'
        /* иначе - ничего не делаем */
        ) : null
    );
}

/**
 * Стили для "IFRAME-подложки"
 */
iframe.ie-substrate {
    position: absolute;
    /* Делаем IFRAME прозрачным */
    filter: Mask();

    /* Отслеживаем позицию и размеры "прикрываемого" слоя */
    behavior: expression( 
        /* Если IFRAME в DOM-е (т.е. не удален) */
        this.parentNode ? (
           /* Если "прикрываемый" слой в DOM-е (т.е. не удален) */
            this.layer.parentNode ? (
                /* и IFRAME вставлен в DOM-е перед "прикрываемым" слоем */
                this.nextSibling==this.layer ? (
                    /* перехватываем свойства "прикрываемого" слоя */
                    this.style.pixelWidth = this.layer.offsetWidth,
                    this.style.pixelHeight = this.layer.offsetHeight,
                    this.style.pixelLeft = this.layer.offsetLeft,
                    this.style.pixelTop = this.layer.offsetTop,
                    this.offsetTop>0 ? ( /* только если IFRAME виден */
                        this.style.zIndex = this.layer.currentStyle.zIndex,
                        this.style.visibility = this.layer.currentStyle.visibility,
                        this.style.display = this.layer.currentStyle.display
                    ) : null
                /* иначе */
                ) : (
                    /* вставляем IFRAME перед "прикрываемым" слоем в DOM-е */
                    this.layer.parentNode.insertBefore(this, this.layer)
                )
            /* иначе */
            ) : (
                /* удаляем IFRAME */
                this.removeNode(true)
            )
        /* иначе - ничего не делаем */
        ) : null
    );

    /* Обновляем IFRAME-ы периодически, чтобы решить проблему в IE 6.0
     * (SELECT-ы всё ещё могут перекрывать слои после скроллинга). */
    refresh: e\xpression( /* Скрываем от IE 5.5 */
        /* Если IFRAME виден */
        this.offsetTop>0 ? (
            /* включить обновление */
            this.timer ? null : this.timer = window.setInterval(this.func, 300)
        /* иначе */
        ) : (
            /* выключить обновление */
            this.timer ? this.timer = window.clearInterval(this.timer) : null
        )
    );
}

[Скачать этот код в виде файла: ie-iframe-substrate.1.0.0.src.ru.css]

Достоинства:
  1. Простота и прозрачность в использовании
  2. Корректно работает с любыми элементами как в "standards-compliant", так и в "quirks" режимах, а также при любых динамических манипуляциях с "прикрытыми" элементами
  3. Не требует внесения изменений в ваш CSS- или javascript-код
  4. Небольшой объём библиотечного файла (всего 1 KB без комментариев)
  5. Нет привязки к каким-либо фрэймворкам
  6. Не оказывает никакого влияния на другие браузеры (в том числе IE 7+)
  7. Легко отключается не оставляя следов (например, при отказе от поддержки IE 6 или от использования SELECT-ов)
Недостатки:
  1. Снижение быстродействия при увеличении количества "прикрытых" элементов
  2. Утечка памяти в IE 5.5 при большом количестве "прикрытых" элементов и при интенсивных манипуляциях с DOM-ом
  3. Некорректное позиционирование "IFRAME-подложки", если "прикрываемый" элемент имеет относительное позиционирование и расположен внутри элемента TD со значением стиля vertical-align отличным от top
Насколько эти недостатки критичны — нужно рассматривать для каждого конкретного проекта отдельно.


Ссылки по теме: