Статьи

Исправление деталей Элемент

Элемент HTML5 <details> — это очень аккуратная конструкция, но он также имеет довольно серьезную проблему с удобством использования — что произойдет, если вы перейдете по хеш-ссылке, которая нацелена на свернутый элемент <details> ? Ответ ничто. Это как если бы цель была скрыта. Но мы можем решить эту проблему с помощью немного улучшенного JavaScript и доступного полифилла для браузеров без встроенной поддержки.

Представляем <подробности>

Если вы еще не знакомы с деталями и элементами резюме , вот небольшой пример:

 <details open="open"> <summary>This is the summary element</summary> <p> This is the expanding content </p> </details> 

Элемент <summary> , если присутствует, должен быть первым или последним дочерним элементом. Все остальное считается контентом. По умолчанию содержимое свернуто, если не определен атрибут open . Собственные реализации обновляют этот атрибут, когда пользователь щелкает сводку, чтобы открыть и закрыть ее. В настоящее время только Chrome поддерживает <details> . На следующем рисунке показано, как Chrome отображает предыдущий пример.

Подробности и сводный элемент в Chrome

Подробности и сводный элемент в Chrome

Он ничем не отличается от обычного текста, за исключением маленького треугольника, называемого треугольником размытия. Пользователи могут открывать и закрывать его, щелкая треугольник или в любом месте внутри элемента <summary> . Вы также можете перейти к сводке и нажать Enter .

Создание Polyfill

Довольно просто реализовать простой полифил для эмуляции <details> . Polyfill идентифицирует собственные реализации по наличию свойства open — DOM-сопоставления атрибута open . В нативных реализациях нам не нужно вручную обновлять атрибут open , но нам все равно нужно обновлять его атрибуты ARIA , которые основаны на следующей структуре.

 <details open="open"> <summary>This is the summary element</summary> <div> <p> This is the expanding content </p> </div> </details> 

Внутренний <div> — это свертываемое содержимое. Сценарий связывает aria-expanded атрибут aria-expanded с этим элементом, который переключается между true и false когда элемент открывается и закрывается. Атрибут также используется в качестве селектора CSS (показан ниже), который визуально сворачивает содержимое с помощью display .

 details > div[aria-expanded="false"] { display:none; } 

Теперь нам на самом деле не нужен обертывающий элемент контента, но без этого нам пришлось бы устанавливать aria-expanded и display на каждом внутреннем элементе индивидуально — это больше работы и может быть довольно неудобно, если элементы имеют разные свойства отображения. Это особенно верно в IE7! По какой-то причине IE7 не применяет изменение отображения, когда пользователь вручную открывает и закрывает его. Тем не менее, он применяет его по умолчанию (что доказывает, что он понимает селектор), и изменение значения атрибута можно увидеть в DOM. Как будто он может применить селектор, но не отменить его снова. По этой причине мы также должны определить изменение style.display , что делает особенно удобным наличие элемента содержимого; и так как мы должны сделать это для IE7, мы получаем поддержку IE6 бесплатно!

Единственная важная вещь, которую следует отметить в полифилле, — это абстракция addClickEvent , которая обрабатывает разницу между браузерами, которые addClickEvent события click клавиатуры, и теми, которые не делают:

 function addClickEvent(node, callback) { var keydown = false; addEvent(node, 'keydown', function() { keydown = true; }); addEvent(node, 'keyup', function(e, target) { keydown = false; if(e.keyCode == 13) { callback(e, target); } }); addEvent(node, 'click', function(e, target) { if(!keydown) { callback(e, target); } }); } 

Для таких элементов, как ссылки и кнопки, которые изначально принимают фокус клавиатуры, все браузеры click событие click при нажатии клавиши Enter . Но наши элементы <summary> принимают фокус только потому, что мы добавили tabindex , и здесь ситуация зависит от браузера.

На самом деле проблема заключается только в том, что если бы все браузеры вели себя так или иначе, все было бы просто. Но, поскольку есть разные способы поведения, мы должны использовать немного хитрости. Итак, мы определяем события keydown и keyup для обработки клавиши Enter . События также устанавливают и сбрасывают флаг, к которому затем относится событие click , так что он может игнорировать повторяющиеся события клавиатуры при обработке событий мыши и касания.

Выделение проблемы хеша

Итак, теперь у нас есть функциональный полифил, давайте снова свяжемся с этим примером, но на этот раз включим идентификатор фрагмента (то есть хэш-ссылку), который указывает на идентификатор содержимого первого элемента:

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

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

  • Если хеш совпадает с идентификатором элемента на этой странице, и этот элемент находится внутри (или является) элементом <details> , тогда автоматически раскрывается элемент и любые идентичные предки

После того, как мы это реализовали, мы получим намного лучшее поведение, так как область деталей автоматически расширяется, чтобы раскрыть цель расположения:

Исправление проблемы хэша

Мы можем исправить проблему хеширования с помощью следующей рекурсивной функции.

 function autostate(target, expanded, ancestor) { if(typeof(ancestor) == 'undefined') { if(!(target = getAncestor(target, 'details'))) { return null; } ancestor = target; } else { if(!(ancestor = getAncestor(ancestor, 'details'))) { return target; } } statechange(ancestor.__summary, expanded); return autostate(target, expanded, ancestor.parentNode); } 

Функция принимает target элемент и флаг состояния expanded=false и определяет, находится ли цель внутри элемента <details> . Если это так, он передает свой элемент <summary> (сохраненный как локальное свойство __summary ) в функцию смены statechange , которая применяет необходимые изменения для расширения элемента. Затем вернемся к DOM, чтобы сделать то же самое с любыми предками, чтобы мы могли обрабатывать вложенные экземпляры. Нам нужно иметь отдельные аргументы для исходной цели и последующих предков, чтобы мы могли вернуть исходную цель в конце всех рекурсий, т. Е. Если входная цель находилась внутри сжатой области, возвращается та же самая цель, в противном случае возвращается значение null .

Затем мы можем вызвать autostate из событий click по внутренним ссылкам страницы, а также вызвать его при загрузке страницы для элемента, соответствующего location.hash :

 if(location.hash) { autostate(document.getElementById(location.hash.substr(1)), false); } 

Первоначально я хотел, чтобы это было все, что делает функция — получить цель, развернуть ее контейнеры, а затем позволить браузеру перейти к его местоположению. Но на практике это было ненадежно, потому что для того, чтобы заставить его работать, элементы должны были быть расширены до того, как была нажата ссылка, иначе браузер не перешел бы в целевое местоположение. Я попытался исправить это, touchstart действие ссылки, используя отдельные события mousedown , touchstart и touchstart , чтобы цель уже была расширена до touchstart по ссылке. К сожалению, это было очень запутанно, и это все еще не было надежно!

Итак, в конце концов я обнаружил, что лучшим подходом была автоматическая прокрутка браузера с помощью функции window.scrollBy , прежде чем все еще возвращать true по ссылке, чтобы обновить адресную строку. Вот где нам нужна целевая ссылка (или ее отсутствие), возвращаемая функцией autostate — если она возвращает цель, прокрутите ее до позиции цели:

 if(target = autostate(document.getElementById('hash'), false)) { window.scrollBy(0, target.getBoundingClientRect().top); } 

Использование функции getBoundingClientRect обеспечивает идеальные данные, так как она сообщает нам положение целевого элемента относительно области просмотра (то есть относительно части документа, которую вы можете видеть в окне браузера). Это означает, что он прокручивает только столько, сколько необходимо, чтобы найти цель, и именно поэтому мы используем scrollBy вместо scrollTo . Но мы этого не делаем при обработке стандартного location.hash , чтобы отразить поведение собственного браузера с помощью обычных хеш-ссылок — когда вы обновляете страницу с помощью хеша местоположения, браузер не возвращается к целевому расположению, это происходит только при первой загрузке страницы.

Таким образом, чтобы получить такое поведение, мы не должны автоматически прокручивать для целей определения местоположения. Вместо этого мы должны позволить родному прыжку произойти в подходящее время. Мы достигаем этого путем отсрочки инициализации сценария с помощью DOMContentLoaded (плюс резервная onload для старых браузеров), что означает, что страница уже переместилась в целевое местоположение, прежде чем сценарий в первую очередь DOMContentLoaded содержащиеся в ней области.

Вывод

Я думаю о сценариях, как это как omnifill. Это больше, чем просто заполнение для браузеров без новейших функций, поскольку оно также повышает удобство использования и доступность самих функций, даже в браузерах, которые их уже поддерживают. Файлы загрузки для примеров в этой статье перечислены ниже.