Javascript.RU

Правильные 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.

click ссылка me

Как видно, прячет оно нормально, а вот показ - некорректный.

Аналогичная проблема будет с ячейками таблицы, у которых по стандарту display: table-cell.

Этой проблемы лишен следующий вариант:

function toggle(el) {
  el.style.display = (el.style.display == 'none') ? '' : 'none'
}

Вместо block свойство display сбрасывается - при этом элемент получает display из CSS по умолчанию, то есть то, которое было изначально:

click ссылка me

Как видите, все работает верно.

Но при этом возникает другая проблема. Свойство display у элемента может отличаться от унаследованного из CSS, например:

<a href="#" style="display:block">...</a>

Или особое значение display могло быть установлено из javascript.

При этом обнуление display сбросит это особое значение.

В DIV ссылка - <a href="#" onclick="return false" style="display:block">. При повторном показе функция toggle сбрасывает display в значение по умолчанию, поэтому элемент будет показан уже с display:inline.

click ссылка me

Для лучшей применимости разобьем 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 такого не скажешь.

  1. Во-первых, вызов show до hide сбросит display. Это поведение - некорректное, его нужно поправить.
  2. Во-вторых, что более важно, если элемент спрятан 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
	}
}
  1. Проверка, показывается ли элемент. Эта строчка нужна для безопасности. Если ее убрать, то (2) обнулит свойство display, а оно могло быть нестандартным. Этот глюк присутствует в ряде javascript-библиотек, например, в jQuery (проверено в 1.4 на момент написания).

    Вы можете захотеть убрать эту строчку в целях оптимизации, чтобы сделать show быстрее.

    Но при этом show станет некорректно работать при вызове без предшествующего hide на элементе с нестандартным display.

  2. Получить старое значение display, если оно сохранено hide и поставить его элементу. Если старого значения нет - на безрыбье и рак рыба, просто обнуляем display.
  3. Показывается ли элемент? Элемент может не показываться, например, из-за того, что в его CSS классе прописано display:none. Если так, то для показа элемента придется найти и применить подходящее значение display.

    Где взять значение display для показа изначально скрытого элемента? Это совсем не обязательно block, т.к. элемент мог быть ссылкой, ячейкой таблицы, да и вообще - "правильный" display для показа элемента зависит от места, времени и настроения программиста.

    В блоке (3.2) функция помещает элемент с таким же тэгом в конец <body> и получает его display, которое кеширует во вспомогательном объекте displayCache. Конечно, это всего лишь догадка, однако в простых случаях она работает.

    Этот display и используется для назначения элементу.

    1. Попробовать получить правильное значение display из кэша.
    2. В кэше нет - добавляем пустой тэг к <body>, затем берем его display.
      1. Если и этот тэг имеет реальный display:none - угадать не получилось. Возьмем block: что еще делать, элемент-то показать надо.
    3. Угаданное значение 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 всегда работает так, как задумано!


Автор: pg sharp (не зарегистрирован), дата: 17 ноября, 2024 - 09:48
#permalink

I am keen on such themes so I will address page where it is cool depicted my blog


Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
14 + 2 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum