Ни для кого, думаю, не секрет, что touch-устройства обрабатывают «мышиные» события несколько иначе, не так, как это происходит на десктоп-браузерах…
Самый яркий для меня пример, это обработка псевдокласса :hover
. Для начала iOS7, например, не будет реагировать на hover если только на элемент, или его родителя, не навешена обработка события click. Это хорошо видно вот на этом примере: jsfiddle.net/H8EmG/ — сколько не тыкай пальцем в текст — никаких подчеркиваний не увидишь. А в этом примере jsfiddle.net/H8EmG/1/ «тычок» пальцем в текст будет приводить к его подчеркиванию. Интересный факт — пока не ткнем в другой элемент, текст так и будет сидеть под ховером…
Другой интересный пример, это обработка появления элементов «по-наведению»: jsfiddle.net/ASRm9/1/ Попробуйте нажать на текст. Сперва вы увидите текст «HOVER!», появившийся внутри строки, а вот второе нажатие уже вызовет alert('click')
. Это происходит потому, что iOS понимает что за :hover
что-то скрыто, и старается не сломать поведение, заложенное автором сайта.
Но однажды мы столкнулись с такой багой, объяснить которую мы не смогли до сих пор, а на ее локализацию потребовался не один отладки на iPad… Желающие подробностей, а также хитрого, как мне кажется, способа решения, наверное, всех проблем с :hover разом — прошу под кат…
ВНЕЗАПНО, после очередного обновления сервиса, разработчиком «платформы» которого я являюсь, вскрылась неприятная проблема — на iPad нельзя выбрать ни одну строку практически во всех «таблицах», которые есть на сервисе. «Клик» просто не срабатывает! Надо заметить что «таблица» это не просто строчки и столбцы. В нашем случае это довольно «богатый» UI-элемент c отметками записей, сортировками, группировками, фильтрами, всякими «лесенками» выводом на печать и экспортом в PDF и Excel…
После долгой и нудной локализации проблемы мы выделили изолированный, простой кусок HTML+CSS который давал схожий результат…
- HTML-таблица, несколько строк, несколько столбцов
- В одном из столбцов есть «чекбокс» —
div
который скрыт по-умолчанию и показывается при наведении на строку. Реализован через:hover
- На строку навешен
click
- Таблица имеет размер больше, чем ее контейнер
- Контейнер имеет фиксированный размер и у него включен
overflow: scroll
Вот пример: jsfiddle.net/822eG/4/. Попробуйте понажимать по строкам таблицы. Hover будет срабатывать (вы увидите «чекбокс») а вот click
(и alert
) вы не увидите как не старайтесь наживать на строчки.
На эту тему я даже завел пост на SO stackoverflow.com/questions/21786375/ios-7-hover-click-issue-no-click-triggered-in-some-cases которй не принес особого профита, кроме предложения включить (непонятно почему) -webkit-overflow-scrolling: touch
на контейнере таблички который реально помогал на примере из jsFiddle, но не помогал на реальном приложении.
В процессе обдумывани этого бардака пришло следующее решение (мой собственный ответ на вопрос на SO) — а что, если :hover
заменить на CSS-класс, который «накидывать» кодом, отлавливая mouseenter
/mouseleave
? Этот простой фикс на самом деле все решает. Даже работать начинает «веселее» — не надо больше кликать два раза. От первого же нажатия получаем и alert и «чекбокс»: jsfiddle.net/822eG/10/
За неимением лучшего варианта стали обдумывать этот… На самом деле у нас очень большой code base. Много как «платформенного» кода, так и «прикладного», на этой платформе основанного. И кто его знает, кто, где и когда, при каких условиях захочет использовать :hover и захочет ли он при это он что-то скрыть или показать. В общем нужно чтобы было «все само (с)» а среднестатиcтический разработчик не думал о проблемах на iOS.
В итоге получилось следующее решение:
- С помощью
MutationObserver
(который есть в iOS 6-7) мониторим вставку тэговlink
вhead
документа — мы это можем себе позволить, т.к. все стили у нас заведомо подключаются с помощью require.js и в Safari это гарантированно будет новыйlink
- При добавлении новых
link
пробежимся поdocument.styleSheets
и проанализируем их... - Переберем все правила и найдем среди них те, в селекторе которых присутствует
:hover
- Посмотрим на стили для таких селекторов, проверим нет ли там
display
отличного отnone
иvisibility
, отличного отvisible
- Если таковые найдутся — перепишем селектор, заменим
:hover
на.hover
(т.е. псевдокласс на обычный класс)... - А на body навесим через delegate обработку
mouseenter
/mouseleave
для найденного селектора, точнее для той его части, которая расположена до:hover
К счастью сделать это оказалось совсем просто… Каждый styleSheet
содержит коллекцию rules
, в которой лежат собственно правила. Каждое правило обладает свойством selectorText
которое можно менять на ходу. А также обладает коллекцией style
где во-первых содержится набор свойств, заданных в данном стиле — они хранятся в виде «массива». У style
есть .length
, перебирая из по длине получим все свойства, измененные в данном стиле. Во-вторых в style
содержатся значения измененных свойств. По индексу, равному имени свойства хранится значение свойства.
То есть если у нас, скажем, есть CSS-код:
.myClass:hover .block, .myItem:hover .element {
color: red;
display: block;
}
то у данного правила selectorText == '.myClass:hover .block, .myItem:hover .element'
, style.length == 2
, style[0] == 'color'
, style[1] == 'display'
, style.color == 'red'
а style.display == 'block'
.
Все остальное — дело техники…
К сожалению выяснилось, что первичны обход правил работает (на наших объемах стилей и link-тэгов) не очень быстро… Профилирование показало, что обращение к rules
занимает львиную долю времени. Возможно, WebKit инициализирует данное свойство лениво и первое обращение инициирует какой-то глубинный парсинг стилей в набор объектов.
Вот что в итоге получилось:
$(document).ready(function(){
// константа, в которой мы определяем, под чем мы работаем
if (!$ws._const.browser.isMobileSafari) {
return;
}
var $body = $('body');
// добавляем класс при наведении
function addPseudoHover() {
this.classList.add('ws-pseudo-hover');
}
// удаляем класс при уходе "мыши"
function removePseudoHover() {
this.classList.remove('ws-pseudo-hover');
}
// Используем в [].filter(...)
function uniq(item, index, array) {
return array.indexOf(item, index + 1) == -1;
}
function trimHoverBase(selector) {
return selector.substr(0, selector.indexOf(':hover')).trim();
}
function filterHoverSelectors(selector) {
return selector.indexOf(':hover') != -1;
}
function createBodyDelegate(hoverSelector){
$body.delegate(hoverSelector, 'mouseenter', addPseudoHover);
$body.delegate(hoverSelector, 'mouseleave', removePseudoHover);
}
function processMutationRecord(mutationRecord) {
var needRefresh = false;
if (mutationRecord.addedNodes) {
for(var i = 0, l = mutationRecord.addedNodes.length; i < l; i++) {
if (mutationRecord.addedNodes[i].nodeName == 'LINK') {
needRefresh = true;
break;
}
}
}
if (needRefresh) {
checkStylesheetSetDebonuced(); // Не будем делать обработку слишком часто
}
}
function checkStylesheetSet() {
var
allHoverSelectors = [],
allRules = [],
sheet, sheetCheckResult;
for(var i = 0, l = document.styleSheets.length; i < l; i++) {
sheet = document.styleSheets[i];
// Проверим, что в стиле есть правила
if (sheet.processed || sheet.rules.length === 0) {
continue;
}
sheetCheckResult = checkCSSRuleSet(sheet);
if (sheetCheckResult.rules.length > 0 && sheetCheckResult.selectors.length > 0) {
Array.prototype.push.apply(allHoverSelectors, sheetCheckResult.selectors);
Array.prototype.push.apply(allRules, sheetCheckResult.rules);
}
// чтобы не обрабатывать один и тот же блок несколько раз
sheet.processed = true;
}
// замена селектора
allRules.forEach(function(aRule){
aRule.selectorText = aRule.selectorText.replace(':hover', '.ws-pseudo-hover');
});
// фильтруем уникальные селекторы, сорздаем делегатов на body
allHoverSelectors.map(trimHoverBase).filter(uniq).forEach(createBodyDelegate);
}
var checkStylesheetSetDebonuced = checkStylesheetSet.debounce(420);
function checkCSSRuleSet(sheet) {
var result = {
selectors: [],
rules: []
};
for(var i = 0, l = sheet.rules.length; i < l; i++) {
var rule = sheet.rules[i];
if (rule.styleSheet && rule.href /* instanceof CSSImportRule*/) {
// Не забываем про @import
checkCSSRuleSet(rule.styleSheet);
} else if (rule.selectorText /* instanceof CSSStyleRule*/) {
var hoverSelectors = getHoverSelectors(rule);
if (hoverSelectors.length > 0) {
if (checkStyles(rule)) {
Array.prototype.push.apply(result.selectors, hoverSelectors);
result.rules.push(rule);
}
}
}
}
return result;
}
function checkStyles(rule) {
for(var i = 0, l = rule.style.length; i < l; i++) {
var styleItem = rule.style[i];
if (styleItem == 'display' && rule.style.display !== 'none') {
return true;
}
if (styleItem == 'visibility' && rule.style.visibility !== 'hidden') {
return true;
}
}
return false;
}
function getHoverSelectors(rule) {
return rule.selectorText.split(',').filter(filterHoverSelectors);
}
// мониторим вставку новых детей в head
new MutationObserver(function(mutationRecords){
mutationRecords.forEach(processMutationRecord);
}).observe(document.getElementsByTagName('head')[0], {
childList: true
});
});
Немного ссылок:
- Поддержка MutationObserver — caniuse.com/#feat=mutationobserver
- Полезное свойство classList — caniuse.com/#feat=classlist
Автор: Olegas