HTML5 INSIGHT


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

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

canvas.bind("mousedown", function(e) {
    isPainting = true;
    lastPoint = {x: e.offsetX, y: e.offsetY};
});

canvas.bind("mousemove", function(e) {
    if (isPainting) {
        var line = {x1:lastPoint.x, y1: lastPoint.y, x2: e.offsetX, y2: e.offsetY};
        drawLine(...);

        lastPoint = {x: e.offsetX, y: e.offsetY};
    }
});

canvas.bind("mouseup", function(e) {
    isPainting = false;
});

Запускаем. IE — ок, Chrome — ok, Opera — ok, Firefox — fail. Ууупс..

Если вы внимательно прочитали заголовок, то уже должны были догадаться, в чем проблема. Проблема в том, что Firefox не поддерживает свойства offsetX/offsetY у события MouseEvent, хотя все остальные браузеры это делают.

Казалось бы, чего тут обсуждать… но проблема имеет давние корни и интересную историю.

Надо начать с того, что offsetX/offsetY изначально появились именно в IE, причем не будучи никак стандартизованными. В Firefox (Netscape), тем временем, был реализован схожий по задачам, но другой механизм — layerX/layerY (и тоже ни разу не стандартный).

Интересно, что наиболее частое использование этих свойств как раз заключается в моделировании offsetX/offsetY. Например, в багтрекере jQuery можно найти такой пример кода:

var offX, offY;
if (!(e.offsetX || e.offsetY)) {
	offX = e.layerX - $(e.target).position().left;
	offY = e.layerY - $(e.target).position().top;
}
else {
	offX = e.offsetX;
	offY = e.offsetY;
}

Со временем многие другие браузеры начали адаптировать удобный механизм из Internet Explorer, который сегодня уже описывается в стандарте CSSOM View Module (черновик). Но, нельзя не сказать, что и layerX/layerY также до недавних пор присутствовал в Webkit (это важно для понимания последующей истории).

Ну и качество реализации на сегодняшний день — тоже отдельный вопрос. Так в IE отступ считается в соответствии со спецификацией от начала “padding edge”, а в Chrome вы сегодня еще получите добавленной величину границы.

История

Отматываем на далекий 2001 год. В строгом согласии с истиной, в багтрекере Mozilla появляется баг 69787 “DOM Spec for events is lacking offset Support (as it is/was lacking offsetParent etc)”. В комментариях поднимается вопрос о поддержке в Netscape событий из IE4+ вроде offsetTop, offsetLeft и offsetParent, но это кажется не прописано явно в спецификации DOM, поэтому в IE одна реализация, а в NS другая — layerX/layerY. Обе реализации не предусмотрены существующим на тот момент стандартом, хотя и явно востребованы.

В 2005 Gérard Talbot возвращается к обсуждению этого вопроса (а баг переименовывается в “Implement MSIE’s event.offsetX, event.offsetY as mouse coordinates inside target element”):

OTOH, offsetX and offsetY are MSIE event properties and useful properties for, say, easily and quickly determining mouse coordinates inside the target element: eg. mouse (x, y) coordinates in an <img> in a click event.
// Gérard Talbot, 2005
We still don’t provide an equivalent to offsetX/offsetY on Gecko events.
// Boris Zbarsky, 2005
Mozilla-based browsers can provide the equivalent to MSIE’s evt.offsetX/evt.offsetY as explained in bug 122665 comment 3. It is not a direct, immediate and straightforward equivalent though.
If Mozilla should provide an equivalent to offsetX/offsetY, then it should be with the same name and same functioning/implementation as in MSIE. Already Opera 7+ has bugs with that implementation.
// Gérard Talbot, 2005

На этом дискуссия замолкает, а у разработчиков отсутствие поддержки offsetX/offsetY по-прежнему “вызывает боль”, заставляя писать примерно такие куски кода:

var x=0, y=0, fatalerror=0;

if (typeof event.offsetX != 'undefined' && typeof event.offsetY != 'undefined') {	// Browser provides the co-ords for us easily (zero-indexed)
    x = event.offsetX;
    y = event.offsetY;
}
else if (event.target) {		// If we have the 'target' of the (click) event - in this case, the image
    var elem = event.target;
    do {						// Calc x and y of 'target' element (ie. the image)
        x += elem.offsetLeft;
        y += elem.offsetTop;
    } while (elem = elem.offsetParent);
    x = (window.pageXOffset + event.clientX) - x;
    y = (window.pageYOffset + event.clientY) - y;
}
else {	// Fatal error trying to determine click co-ords!
    fatalerror = 1;
}

// x and y are still zero-indexed...
if (!fatalerror) {x++; y++;}

alert('We think x:y co-ords are: ' + x + ':' + y);

Идет время. В 2008 John Resig снова поднимает вопрос о реализации offsetX/offsetY:

We should strongly consider this. Currently we’re the only browser that doesn’t support this: http://www.quirksmode.org/dom/w3c_cssom.html#elementviewm
Also, this is part of the W3C CSSOM working draft: http://www.w3.org/TR/cssom-view/
// John Resig, 2008
I just noticed that http://www.w3.org/TR/cssom-view/#the-mouseeventview-interface defines offsetX and offsetY. So, we may need to resummarize this bug again… so that we can meet the CSSOM View specification instead of MSIE’s model.
// Gérard Talbot, 2008

Дело пошло? Никак нет.

Спустя несколько месяцев, в ноябре того же 2008г., шевеление началось в стане Webkit. В багтрекере появился баг 21868 “layerX and layerY on mouse events are very probably wrong”. Simon Fraser из Apple пытается понять, как должны вести себя layerX/layerY.

Наши дни

Спустя почти три года (в июле 2011), к багу в багтрекере Webkit возвращаются:

Proposed fix: Match Firefox’s behavior.
// Julien Chaffraix, 2011
I’d prefer to just remove layerX/layerY. Can you mail mozilla folks and see if they’d agree with this?
// Simon Fraser, 2011

Мячик снова возвращается в лагерь Firefox.

Per discussion on #content, this is a request to drop support for MouseEvent’s layerX and layerY. It is currently supported only by Gecko and WebKit (context for WebKit -> https://bugs.webkit.org/show_bug.cgi?id=21868).
WebKit is considering removing those 2 properties as our code looks extremely old and we had zero bug about that.
// Julien Chaffraix, 2011
Simon, you suggested dropping layerX/Y. May I ask why? Just because they are rarely used, or do you see something badly wrong with them?
// Olli Pettay, 2011
They are not specified anywhere, the ‘layer’ name seems to be a holdover from Netscape 4 “layers”, and WebKit’s implementation was very probably wrong.
// Simon Fraser, 2011

В конечном счете, все соглашаются, что хорошо бы реализовать поддержку offsetX/offsetY в соответствии со спецификацией, хотя выкидывать layerX/layerY тоже так просто нельзя.

Впрочем, в Webkit именно так и поступили: в свежем Chrome свойства layerX/layerY уже не доступны.

Заключение

Вы наверное, уже подумали, что я тут взялся поругать разработчиков Firefox? Да! Дайте мне уже наконец Firefox c поддержкой offsetX/offsetY! Но не только…

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

Наконец, интересный момент заключается в легкости, с которой иногда расстаются со старыми свойствами, хотя и нестандартными, но давно поддерживаемыми. Я надеюсь, все сайты, которые рассчитывают на layerX/layerY, правильно определяют его наличие до использования и не поломаются, когда его окончательно не станет.

Да, удивительно, но это история почти даже не про IE.