Ни для кого, думаю, не секрет, что touch-устройства обрабатывают «мышиные» события несколько иначе, не так, как это происходит на десктоп-браузерах…
Самый яркий для меня пример, это обработка псевдокласса
Другой интересный пример, это обработка появления элементов «по-наведению»: jsfiddle.net/ASRm9/1 Попробуйте нажать на текст. Сперва вы увидите текст «HOVER!», появившийся внутри строки, а вот второе нажатие уже вызовет
Но однажды мы столкнулись с такой багой, объяснить которую мы не смогли до сих пор, а на ее локализацию потребовался не один день отладки на iPad… Желающие подробностей, а также хитрого, как мне кажется, способа решения, наверное, всех проблем с :hover разом — прошу под кат…
ВНЕЗАПНО, после очередного обновления сервиса, разработчиком «платформы» которого я являюсь, вскрылась неприятная проблема — на iPad нельзя выбрать ни одну строку практически во всех «таблицах», которые есть на сервисе. «Клик» просто не срабатывает! Надо заметить что «таблица» это не просто строчки и столбцы. В нашем случае это довольно «богатый» UI-элемент c отметками записей, сортировками, группировками, фильтрами, всякими «лесенками» выводом на печать и экспортом в PDF и Excel…
После долгой и нудной локализации проблемы мы выделили изолированный, простой кусок HTML+CSS который давал схожий результат…
Вот пример: jsfiddle.net/822eG/4. Попробуйте понажимать по строкам таблицы. Hover будет срабатывать (вы увидите «чекбокс») а вот
На эту тему я даже завел пост на SO stackoverflow.com/questions/21786375/ios-7-hover-click-issue-no-click-triggered-in-some-cases который не принес особого профита, кроме предложения включить (непонятно почему)
В процессе обдумывания этого бардака пришло следующее решение (мой собственный ответ на вопрос на SO) — а что, если
За неимением лучшего варианта стали обдумывать этот… На самом деле у нас очень большой code base. Много как «платформенного» кода, так и «прикладного», на этой платформе основанного. И кто его знает, кто, где и когда, при каких условиях захочет использовать :hover и захочет ли он при это он что-то скрыть или показать. В общем нужно чтобы было «все само (с)» а среднестатистический разработчик не думал о проблемах на iOS.
В итоге получилось следующее решение:
К счастью сделать это оказалось совсем просто… Каждый
То есть если у нас, скажем, есть CSS-код:
то у данного правила
Все остальное — дело техники…
К сожалению выяснилось, что первичны обход правил работает (на наших объемах стилей и link-тэгов) не очень быстро… Профилирование показало, что обращение к
Вот что в итоге получилось:
Немного ссылок:
Самый яркий для меня пример, это обработка псевдокласса
: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 и захочет ли он при это он что-то скрыть или показать. В общем нужно чтобы было «все само (с)» а среднестатистический разработчик не думал о проблемах на 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