Кроссбраузерная работа с событиями.

О проблемах несовместимости браузеров, я думаю, известно каждому. В данной статье я рассмотрю проблемы несовместимости браузеров и их решения при работе с событиями в JavaScript.

В DOM 1 был определён всего лишь один метод работы с событиями - это обработчики событий. Подключение функции обработчика было простым делом - ссылка на функцию или анонимная функция присваивалась соответствующему свойству элемента с префиксом “on”. Т.е. событие щелчка мыши - onclick, событие отправки формы - onsubmit и т.д. Которкий пример:

// присваивание анонимной функции
element.onclick = function(){
	alert("Это событие onclick");
}

// присваивание ссылки на функцию
element.onmouseover = hoverMe;

// присваивание в виде тэга элемента
<div onclick="myfunction()"></div>

При таком методе работы с событиями есть большой недостаток - каждому элементу можно добавлять только один обработчик событий. Поэтому в DOM 2 был добавлен новый метод работы с событиями - это event listeners или "слушатели событий" (в различных источниках этот термин переводится по разному, либо перехватчик событий либо приемник событий, но на мой взгляд перевод “слушатель” более точен). Вот здесь то и начинаются проблемы с совместимостью, и как всегда главным возмутителем спокойствия оказался Internet Explorer, всегда делающий все по своему. Разберемся немного в том, что представляет собой слушатель событий и как он реализован на разных браузерах.

Стандартами DOM level 2 для установки слушателя событий определена функция addEventListener(), для удаления - removeEventListener(). В IE, то же самое выполняют функции attachEvent() и detachEvent(). Здесь напрашивается самый простой метод преодоления этих различий - определять наличие функций и вызывать соответствующую функцию. В случае, если ни addEventListener ни attachEvent не доступны, применяем простые обработчики событий. Ниже приведен код для этого метода:

// добавление и удаление функций-слушателей
// obj - элемент
// type - тип события без префикса "on", т.е. "click", "submit"
// fn - ссылка на функцию, или анонимная функция

// добавление
function addEvent(obj, type, fn){
	if (obj.addEventListener){
		obj.addEventListener( type, fn, false );
	} else if(obj.attachEvent) {
		obj.attachEvent( "on"+type, fn );
	} else {
		obj["on"+type] = fn;
	}
}

// удаление
function removeEvent(obj, type, fn){
	if (obj.removeEventListener) {
		obj.removeEventListener( type, fn, false );
	} else if (obj.detachEvent){
		obj.detachEvent( "on"+type, obj[type+fn] );
	} else {
		obj["on"+type] = null;
	}
}

// **********************
// Пример применения функций

function clickHandler(){
	// первая функция слушатель
	alert("Событие click");
	var oTarget = document.getElementById("testDiv");
	removeEvent(oTarget, "click", clickHandler);
}

function mouseoverHandler(){
	alert("Событие mouseover");
	var oTarget = document.getElementById("testDiv");
	removeEvent(oTarget, "mouseover", mouseoverHandler);
}

// добавляем эти две функции как слушатели к одному элементу DOM
var elem = document.getElemntById("testDiv");

addEvent(elem, "click", clickHandler);
addEvent(elem, "mouseover", mouseoverHandler);

В данном примере назначаются 2 функции слушателя для одного элемента c id=testDiv. Функции слушатели в этом случае выполняются только один раз, поскольку сразу же удаляются функцией removeEvent прямо в теле функций.

Эти функции позволяют унифицировать работу с назначением слушателей на разных браузерах. Но тут есть небольшой нюанс - все броузеры за исключением Internet Explorer правильно устанавливают указатель this в теле функции слушателя, в то время как this в IE указывает на абсолютно бесполезный здесь объект window. Но и это также поправимо. Джон Ресиг (John Resig) в своей статье описал замечательный кроссбраузерный метод работы с событиями, который я привожу здесь:

function addEvent(obj, type, fn){
	if (obj.addEventListener){
		obj.addEventListener( type, fn, false );
	} else {
		obj["e"+type+fn] = fn;
		obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
		obj.attachEvent( "on"+type, obj[type+fn] );
	}
}

function removeEvent(obj, type, fn){
	if (obj.removeEventListener){
		obj.removeEventListener( type, fn, false );
	} else {
		obj.detachEvent( "on"+type, obj[type+fn] );
		obj[type+fn] = null;
		obj["e"+type+fn] = null;
	}
}

Описание ключевых моментов:

obj["e"+type+fn] = fn;

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

obj[type+fn] = function(){ obj["e"+type+fn]( window.event ); }

Эта строка создаёт анонимную функцию, которая, при выполнении вызывает предыдущую, передавая при этом глобальный объект event. Эта функция присваивается свойству type+fn объект, чтобы можно было впоследствии её удалить. Таким образом, мы имеем две универсальные функции для работы со слушателями событий.

Разобравшись с обработчиками и функциями слушателями, рассмотрим объект event. Реализация этого объекта различается в IE и DOM-совместимых браузерах. Например, такие свойства объекта event, как target и relatedTarget заменены в IE свойствами srcElement и fromElement, а методы preventDefault() и stopPropagation() реализуются при помощи свойств returnValue и cancelBubbling. Свойства и методы Internet Explorer можно привести в соответствие с DOM при помощи следующей функции (решает проблему только на IE в Windows):

// Форматирование события для соответствия параметров в IE и других броузерах
// определение IE
var sUserAgent = navigator.userAgent;
var isIE = sUserAgent.indexOf("compatible") > -1 && sUserAgent.indexOf("MSIE") > -1 && sUserAgent.indexOf("Opera") == -1;
var isWin = (navigator.platform == "Win32") || (navigator.paltform == "Windows");

function formatEvent(oEvent){
	if(isIE && isWin){
		oEvent.charCode=(oEvent.type=='keypress')?oEvent.keyCode:0;
		oEvent.eventPhase=2;
		oEvent.isChar=(oEvent.charCode>0);
		oEvent.pageX=oEvent.clientX+document.body.scrollLeft;
		oEvent.pageY=oEvent.clientY+document.body.scrollTop;
		oEvent.preventDefault=function(){
			this.returnValue=false;
		}
		if(oEvent.type=='mouseout')
			oEvent.relatedTarget=oEvent.toElement;
		else if(oEvent.type=='mouseover')
			oEvent.relatedTarget=oEvent.fromElement;
		oEvent.stopPropagation=function(){
			this.cancelBubble=true;
		}
		oEvent.target=oEvent.srcElement;
		oEvent.time=(new Date).getTime();
	}
	return oEvent;
}

Теперь лучше всего объединить все вышеназванные методики в один объект, который будет работать с событиями. Вот что у меня получилось:

// определение IE
var sUserAgent = navigator.userAgent;
var isIE = sUserAgent.indexOf("compatible") > -1 && sUserAgent.indexOf("MSIE") > -1 && sUserAgent.indexOf("Opera") == -1;
var isWin = (navigator.platform == "Win32") || (navigator.paltform == "Windows");

var Event = {
	// добавление слушателя
	add:function(obj,type,fn){
		if (obj.addEventListener){
			obj.addEventListener( type, fn, false );
		} else if (obj.attachEvent){
			obj["e"+type+fn] = fn;
			obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
			obj.attachEvent( "on"+type, obj[type+fn] );
		} else {
			obj["on"+type] = fn;
		}
	},

	// удаление слушателя
	remove:function(obj, type, fn){
		if (obj.removeEventListener){
			obj.removeEventListener( type, fn, false );
		} else if (obj.detachEvent){
			obj.detachEvent( "on"+type, obj[type+fn] );
			obj[type+fn] = null;
			obj["e"+type+fn] = null;
		} else {
			obj["on"+type] = null;
		}
	},

	// форматирование события для соответствия параметров в IE и других броузерах
	format:function(oEvent){
		if(isIE && isWin){
			oEvent.charCode=(oEvent.type=="keypress")?oEvent.keyCode:0;
			oEvent.eventPhase=2;
			oEvent.isChar=(oEvent.charCode>0);
			oEvent.pageX=oEvent.clientX+document.body.scrollLeft;
			oEvent.pageY=oEvent.clientY+document.body.scrollTop;
			oEvent.preventDefault=function(){
				this.returnValue=false;
			}
			if(oEvent.type=="mouseout")
				oEvent.relatedTarget=oEvent.toElement;
			else if(oEvent.type=="mouseover")
				oEvent.relatedTarget=oEvent.fromElement;
			oEvent.stopPropagation=function(){
				this.cancelBubble=true;
			}
			oEvent.target=oEvent.srcElement;
			oEvent.time=(new Date).getTime();
		}
		return oEvent;
	}
}

// ******************
// Пример использования
function handler(oEvent){
	var e = Event.format(oEvent || window.event);

	alert(e.target.id);
	e.stopPropagation();
}

// подключение функции-слушателя handler к элементу targetDiv
function attachEvent(){
	Event.add(document.getElementById("targetDiv"), "click", handler );
}
Добавить в закладки:

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