Правильные show/hide/toggle
Как правильно реализовать универсальные функции show и hide для DOM элемента? Есть несколько распространенных вариантов, с различными граблями, которые мы рассмотрим, и выберем оптимальный.
Самое интересное, что даже самые лучшие show/hide функции из хороших javascript-библиотек не универсальны.
Рассмотрим самую простую функцию toggle :
function toggle(el) {
el.style.display = (el.style.display == 'none') ? 'block' : 'none'
}
Недостатки такого подхода легко видеть. Например, ссылки <a href="..."> имеют по умолчанию display: inline . А этот код поставит display = "block" .
Для проверки - кликните на любом месте этого желтого div , при этом вызовется toggle ссылки. И затем - кликните еще раз для повторного toggle .
Как видно, прячет оно нормально, а вот показ - некорректный.
Аналогичная проблема будет с ячейками таблицы, у которых по стандарту display: table-cell .
Этой проблемы лишен следующий вариант:
function toggle(el) {
el.style.display = (el.style.display == 'none') ? '' : 'none'
}
Вместо block свойство display сбрасывается - при этом элемент получает display из CSS по умолчанию, то есть то, которое было изначально:
Как видите, все работает верно.
Но при этом возникает другая проблема. Свойство display у элемента может отличаться от унаследованного из CSS, например:
<a href="#" style="display:block">...</a>
Или особое значение display могло быть установлено из javascript.
При этом обнуление display сбросит это особое значение.
В DIV ссылка - <a href="#" onclick="return false" style="display:block"> . При повторном показе функция toggle сбрасывает display в значение по умолчанию, поэтому элемент будет показан уже с display:inline.
Для лучшей применимости разобьем toggle на две части: show и hide и отладим их по отдельности.
Чтобы обойти описанную выше проблему с перезаписыванием display - при скрытии элемента будем записывать старое значение display в атрибут el.displayOld , а при показе - восстанавливать.
function hide(el) {
if (!el.hasAttribute('displayOld')) {
el.setAttribute("displayOld", el.style.display)
}
el.style.display = "none"
}
function show(el) {
var old = el.getAttribute("displayOld");
el.style.display = old || "";
}
Теперь show/hide для ссылки со своим display будет работать корректно.
Однако, и этот вариант - для нас не более чем промежуточный.
Функция hide работает отлично. О show такого не скажешь.
- Во-первых, вызов
show до hide сбросит display . Это поведение - некорректное, его нужно поправить.
- Во-вторых, что более важно, если элемент спрятан CSS-классом, то
show его не покажет.
Например, show не покажет ссылку из этого примера:
<style>
.hidden {display:none}
</style>
<a href="#" class="hidden">Ссылка</a>
Чтобы решить эти проблему, функция show должна знать, показывается ли элемент реально или нет. Это нам поможет сделать доступ к Computed Style - вычисленному стилю элемента, доступ к которому делается по-разному, в зависимости от браузера.
Следующая функция берет реальное значение display из Computed Style, то есть такое. которое получается в результате применения всех CSS-классов и свойств.
function getRealDisplay(elem) {
if (elem.currentStyle) {
return elem.currentStyle.display
} else if (window.getComputedStyle) {
var computedStyle = window.getComputedStyle(elem, null)
return computedStyle.getPropertyValue('display')
}
}
Если элемент не показывается из-за CSS-класса, то getRealDisplay вернет для него 'none' .
displayCache = {}
function show(el) {
if (getRealDisplay(el) != 'none') return // (1)
el.style.display = el.getAttribute("displayOld") || "" // (2)
if ( getRealDisplay(el) === "none" ) { // (3)
var nodeName = el.nodeName, body = document.body, display
if ( displayCache[nodeName] ) { // (3.1)
display = displayCache[nodeName]
} else { // (3.2)
var testElem = document.createElement(nodeName)
body.appendChild(testElem)
display = getRealDisplay(testElem)
if (display === "none" ) { // (3.2.1)
display = "block"
}
body.removeChild(testElem)
displayCache[nodeName] = display
}
el.setAttribute('displayOld', display) // (3.3)
el.style.display = display
}
}
- Проверка, показывается ли элемент. Эта строчка нужна для безопасности. Если ее убрать, то (2) обнулит свойство display, а оно могло быть нестандартным. Этот глюк присутствует в ряде javascript-библиотек, например, в jQuery (проверено в 1.4 на момент написания).
Вы можете захотеть убрать эту строчку в целях оптимизации, чтобы сделать show быстрее.
Но при этом show станет некорректно работать при вызове без предшествующего hide на элементе с нестандартным display.
- Получить старое значение
display , если оно сохранено hide и поставить его элементу. Если старого значения нет - на безрыбье и рак рыба, просто обнуляем display .
- Показывается ли элемент? Элемент может не показываться, например, из-за того, что в его CSS классе прописано
display:none . Если так, то для показа элемента придется найти и применить подходящее значение display .
Где взять значение display для показа изначально скрытого элемента? Это совсем не обязательно block , т.к. элемент мог быть ссылкой, ячейкой таблицы, да и вообще - "правильный" display для показа элемента зависит от места, времени и настроения программиста.
В блоке (3.2) функция помещает элемент с таким же тэгом в конец <body> и получает его display , которое кеширует во вспомогательном объекте displayCache . Конечно, это всего лишь догадка, однако в простых случаях она работает.
Этот display и используется для назначения элементу.
- Попробовать получить правильное значение
display из кэша.
- В кэше нет - добавляем пустой тэг к
<body> , затем берем его display .
- Если и этот тэг имеет реальный
display:none - угадать не получилось. Возьмем block : что еще делать, элемент-то показать надо.
- Угаданное значение
display применяем к элементу и сохраняем для дальнейшего использования.
Итак, теперь на основе show и hide можно сделать функцию toggle , которая видимый элемент скрывает, а невидимый - наоборот, показывает. Иначе говоря, переключает состояние элемента.
Функция toggle сама по себе очень проста:
function toggle(el) {
isHidden(el) ? show(el) : hide(el)
}
Для ее работы необходима вспомогательная функция isHidden , которая определяет, виден ли элемент. Само собой, имеется в виду реальная видимость, а не свойство display элемента.
Используем для этого трюк с offsetWidth/offsetHeight :
function isHidden(el) {
var width = el.offsetWidth, height = el.offsetHeight,
tr = el.nodeName.toLowerCase() === "tr"
return width === 0 && height === 0 && !tr ?
true : width > 0 && height > 0 && !tr ? false : getRealDisplay(el)
}
Эта реализация пытается получить ответ, по возможности используя проверку offsetWidth/offsetHeight , т.к. это быстрее, чем getRealDisplay .
Итак, вот итоговый код toggle.js.
function getRealDisplay(elem) {
if (elem.currentStyle) {
return elem.currentStyle.display
} else if (window.getComputedStyle) {
var computedStyle = window.getComputedStyle(elem, null )
return computedStyle.getPropertyValue('display')
}
}
function hide(el) {
if (!el.getAttribute('displayOld')) {
el.setAttribute("displayOld", el.style.display)
}
el.style.display = "none"
}
displayCache = {}
function isHidden(el) {
var width = el.offsetWidth, height = el.offsetHeight,
tr = el.nodeName.toLowerCase() === "tr"
return width === 0 && height === 0 && !tr ?
true : width > 0 && height > 0 && !tr ? false : getRealDisplay(el)
}
function toggle(el) {
isHidden(el) ? show(el) : hide(el)
}
function show(el) {
if (getRealDisplay(el) != 'none') return
var old = el.getAttribute("displayOld");
el.style.display = old || "";
if ( getRealDisplay(el) === "none" ) {
var nodeName = el.nodeName, body = document.body, display
if ( displayCache[nodeName] ) {
display = displayCache[nodeName]
} else {
var testElem = document.createElement(nodeName)
body.appendChild(testElem)
display = getRealDisplay(testElem)
if (display === "none" ) {
display = "block"
}
body.removeChild(testElem)
displayCache[nodeName] = display
}
el.setAttribute('displayOld', display)
el.style.display = display
}
}
Пример работы вы можете увидеть на отдельной страничке.
Открыть пример в новом окне
Эта универсальная функция toggle широко используется в различных библиотеках, в частности, в jQuery.
Теперь вы знаете, что она делает ряд лишних операций, и если вдруг ваш toggle заглючит - представляете, в чем может быть дело.
Пусть ваш toggle всегда работает так, как задумано!
|
1) не мешало бы добавить проверку на not null для el и на соответствующие nodeType
2) Мне кажется, что для хранения старого значения лучше использовать JS поле - в простейшем случае el.displayOld, а лучше отдельный глобальный хэш.
Модифицировать коллекцию аттрибутов - как-то нехорошо.
Правильный hide
А в CSS-классе element_hide уже выставляются нужные свойства, будь то display: none, left: -9999px, visibility: hidden или что-то ещё. Т.е. функции show/hide вообще не нужны, нужны addClass/removeClass.
Не плохо бы проверочку сделать было бы на существование этого класса element_hide у этого элемента.
Хорошее дополнение, как правило класс действительно лучше, да.
Полностью согласен.
Концепт такой:
ЦСС, соответственно, содержит правила, как показывать (инлайн, блок,...) и как прятать (дисплей, визибилити, позишн, опейсити,...) конкретно этот елемент. Как бонус - мы разделяем поведение (js) и вид (css).
ПС: код не проверял, пишу по старой памяти. Если есть ошибки - укажите.
ППС: Лучше навешать эти функции как методы на ДОМ-обьект, тогда можно обойтись без elm (заменить на this).
при отображении вкладок tabs использовал такой вариант
$(document).ready(function(){
$('#tabsTitle li').mouseover(function(){ $('#content4 > div').hide();
$('#' + $(this).attr('class')).toggle(); });
});
Однако как прописать , что бы при переходе на страницу открывалась третья по счету вкладка или любая другая, как это задать подскажи код если время есть
Записывать id или номер текущей вкладки при каждом изменении в location.hash, а при первой загрузке страницы читать location.hash и переключать программно.
Даже более-менее семантично.
кста по такому принципу вконтакте списки с выборкой работают
Абсолютно согласен с Kolyaj. На самом деле, никогда нельзя обращаться из JS к свойству style, если на то нет весомых причин, которой может являться, к примеру, анимация. Во-первых изменения стиля из джаваскрипта - медленная операция, а во-вторых - не кашерно. Я уверен, что если посчитать(в байтах) ваш код и реализацию css classes+js, то в итоге второй вариант выйграет даже здесь.
когда ты пишешь javascript-код для своего сайта, то удобней, может, с классами (тоже кстати, классы использую).
А вот когда ты делаешь универсальный код (библиотеку, например), то тогда приходится так.
если элемент изначально скрыт, ни show(), ни toggle() его не покажет
По моему, в hide не помешает тоже добавить проверку на isHidden(el), иначе если hide будет вызван 2 раза подряд, show поставит ему block вместо изначального значения...
Написал аналог toggle но с запоминание на печеньках. Защиты от дураков (как в статье выше) нет, поэтому желательно заранее знать, к каким элементам мы его будем применять.
Код:
Использование:
При загрузке странички скрипт проверит печеньку и скроет или покажет нужные элементы.
А зачем вообще трогать display?
Это семантически разве верно?
display отвечает за модель отображения контента.
За видимость отвечает visibility.
Разве не проще просто его включать-выключать?
Вот только visibility сохраняет пространство, занимаемое элементом.
Взял скрипт toggle.js с этой страницы.
Код HTML:
Интересующие меня элементы:
таблица нормально скрыта, должна показываться как:
В IE6 и Opera все работает идеально. Firefox ругается:
el.nodeName is undefined на строку tr = el.nodeName.toLowerCase() === "tr"
(width и height в пред. строке тоже не работают)
Как исправить?
Sorry, еще раз код
Если пытаться применить функцию toggle отсюда:
http://javascript.ru/files/toggle/toggle.js
к изначально скрытой таблице, в Опере все работает нормально, а FF 3.6 присваивает переменной el не id-шник интересующей меня таблицы, а объект Window. Как изменить скрипт, чтобы он работал во всех браузерах, не привязываясь к конкретной разметке?
З.Ы. В IE6 тоже не работает, но работоспособность в IE не требуется :-)
Пытаюсь применить данный скрипт
http://javascript.ru/files/toggle/toggle.js
для показа/скрытия таблицы. В Opera работает идеально, а FF 3.6 и IE при вызове функции isHidden() при изначально скрытой таблице выставляют объектом el не интересующую меня таблицу, а объект Window.
Есть ли способ сделать скрипт универсальным для Opera и FF не приписывая отдельно больших кусков под FF ?
или же если нужно скрыть этот элемент
toggle(this) прекрасно скроет сам чекбокс, чего мне не надо.
Мне нужно по чекбоксу показывать/скрывать таблицу с id="content"
Таблица изначально скрыта (display: none) поэтому FF похоже вообще не создает никаких элементов с таким id и toggle(document.getElementById('content')) тоже не отрабатывает.
таблицу изначально лучше не скрывать, придумайте альтернативу, багов не оберетесь..
было бы интересно можно ли без цикла определить скрыт ли элемент типа
Учитывая что скрытым может быть и один из предков
Да, можно, по offsetWidth == offsetHeight == 0. Работает на всех элементах, кроме TR, на них в нек случаях багает.
В процессе написания своего фреймворка, долго думал над этим. На ум пришёл такой вариант:
Код намного короче, работает практически универсально.
Спасибо, помогло =)
Рад))
однако если изначально (поставил в CSS) элемент имеет свойство display:none; то не работает.
в функции show "" не поставит (ff 7.0.1) свойство в изначальное такое как предполагается у элемента (если оно none) в w3c, например:
div - блочный элемент т.е. имеет по w3c значение display:block;
span - display:inline;
так вот как узнать у элемента какое у него по умолчанию w3c свойство display ?
document.defaultView им можно получить? если да, то как? smile
Ребят, ведь здесь не учитывается, что у скрываемывого элемента. например дива, есть есть еще вложенные элементы, которые автоматически унаследуют измененные свойства, а после его отображения унаследуют опять же, но это уже могут быть не совсем те свойства ???
Например внутри дива есть ссылки, таблицы и т.п.
Так display не наследуется.
Я не нахожу идею написания универсальной функции для этой цели здравой. Для каждого конкретного случая универсальная функция окажется избыточной.
Разумнее писать функцию под свой конкретный случай.
Доброго времени суток! может вы сможете мне помочь? мне надо чтобы вот в этом тексте
$(function () {
function makeTabs(contId) {
var tabContainers = $('#'+contId+' div.tabs > div');
tabContainers.hide().filter(':ID').show();
$('#'+contId+' div.tabs ul.tabNavigation a').click(function () {
tabContainers.hide();
tabContainers.filter(this.hash).show();
$('#'+contId+' div.tabs ul.tabNavigation a').removeClass('selected');
$(this).addClass('selected');
return false;
}).filter(':ID').click();
}
});
ID было не названием (сейчас ид (в ксс) материала у меня работает как просто название) а конкретным, уникальным номером задающим, свое, уникальное название-номер для таба.
Если не сложно конечно...
почему-то toggle не работет...
точнее не работает как нужно , срабатует просто как событие onclick ,но по второму клику ничего не происходит..
Ловите:
Здравствуйте.
Помогите пожалуйста, я в JS не силен. Пользуюсь вот таким вариантом:
function toggle(el)
{
myEl = document.getElementById(el);
myEl.style.display = (myEl.style.display == 'block') ? 'none' : 'block';
}
При этом в теле находятся вот такие ссылки:
ссылка 1
ссылка 2
ссылка 3
ссылка 4
Как сделать так, чтобы при смене у div id="one" display:none на display:block другим элементам обязательно задавался параметр display:none и при нажатии на "ссылка 2" открытое "Показать 1" исчезало, а "Показать 2" отображалось?
Почему то код не написался...
Здравствуйте.
Помогите пожалуйста, я в JS не силен. Пользуюсь вот таким вариантом:
При этом в теле находятся вот такие ссылки:
Как сделать так, чтобы при смене у div id="one" display:none на display:block другим элементам обязательно задавался параметр display:none и при нажатии на "ссылка 2" открытое "Показать 1" исчезало, а "Показать 2" отображалось?
Увас небольшая ошибка в html-коде, а именно не закрыт атрибут onclick.
Старые версии IE не поддерживают setAttribute, только setAttributeNode.
Не пойму, почему просто не юзать visibility?
visibility не освобождает место, выделенное под элемент
Вот моя версия простейшего анимированного тоггла:
Применение:
Здравствуйте.
Не подскажите, как скрыть блок div на определенной странице, то есть при переходе по определенному адресу, например на главную страницу?
Спасибо.
А почему работаем с css-свойством display? Есть же универсальный атрибут hidden, с ним все вообще в одну строчку
В функции
isHidden
допущена ошибка:финальная проверка должна выглядеть как:
getRealDisplay(el) === 'none'
Где взять значение display для показа изначально скрытого элемента? cookie clicker