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

Четверг, 14 января 2010 года, 15:29. Напечатано в категориях Events, JavaScript, Rools, Оптимизация8

Привет, привет, привет!
Недавно мне понадобилась кроссбраузерная работа с JavaScript-событиями DOM-объектов.
Мне нужен был только этот функционал без ничего лишнего. Различные библиотеки не подходили и я взялся за реализацию своего варианта :)

Сначала прошёлся по гуглу и взял за основу 2 заметки:

  1. Кроссбраузерная работа с событиями
    Автор: Дмитрий Ищенко
    Адрес: http://www.jstoolbox.com/2008/01/25/krossbrauzernaya-rabota-s-sobytiyami/
  2. Flexible Javascript Events
    Автор John Resig
    Адрес: http://ejohn.org/blog/flexible-javascript-events/

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

/*
 * RoolsEvent - JavaScript-инструмент для полноценной кроссбраузерной работы с событиями
 *   Версия 0.0.1
 *
 * Copyright (c) 2010 Александр Кузнецов aka Regent(http://vl.vg/)
 * Идея и описание - 2010 Александр Кузнецов aka Regent - http://vl.vg/14.01.2010/cross-events/
 * Прототип кода:
 *   Дмитрий Ищенко - http://www.jstoolbox.com/2008/01/25/krossbrauzernaya-rabota-s-sobytiyami/
 *   John Resig     - http://ejohn.org/blog/flexible-javascript-events/
 * Дата создания: 14.01.2010
 * Лицензия:
 *   Dual licensed under the MIT or GPL Version 2 licenses.
 *   http://docs.jquery.com/License
 */

(
  function(){
    var
      // Быстрая ссылка на window
      w = this,
      // Быстрая ссылка на document
      d = w.document,
      // Временные переменные
      tmp, i, length,
      // События DOM-объектов
      objectEvents = [ 'blur', 'change', 'click', 'dblclick', 'error', 'focus', 'keydown', 'keypress', 'keyup', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'select', 'submit' ],
      // События window
      windowEvents = [ 'load', 'unload', 'scroll', 'resize' ],
      // Создаём временные span для тестов
      span = d.createElement( 'span' ),
      // Internet Explorer
      IE = !!span.attachEvent,
      // DOM уровень 1
      DOM1 = !IE && !span.addEventListener,
      // DOM2 совместимый браузер
      standartBrowser = !IE && !DOM1,
      // Функция добавления события для DOM2-совместимого браузера
      addEvent = standartBrowser ? function( obj, type, fn ){
          obj.addEventListener( type, fn, false );
        // Функция добавления события для Internet Explorer
        } : IE ? function( obj, type, fn ){
          obj.attachEvent( 'on' + type, fn );
        // Функция добавления события для браузера с DOM level 1
        } : function( obj, type, fn ){
          obj[ 'on' + type ] = !obj[ 'on' + type ] ? fn : ( fn.prevfn = obj[ 'on' + type ] ) && function(){
            fn.prevfn();
            fn();
          };
        },
      // Функция удаления события для DOM2-совместимого браузера
      removeEvent = standartBrowser ? function( obj, type, fn ){
          obj.removeEventListener( type, fn, false );
        // Функция удаления события для Internet Explorer
        } : IE ? function( obj, type, fn ){
          obj.detachEvent( 'on' + type, fn );
        // Функция удаления события для браузера с DOM level 1
        } : function( obj, type, fn ){
          obj[ 'on' + type ] = null;
        },
      // Главная управляющая функция event(Менеджер событий)
      event = function( obj, type, fn, remove ){
        // Если DOM-объект не передан или не указан тип события - конец функции
        if( !obj || !type )
          return false;
        // Если передан только DOM-объект и тип события - инициализация события
        else if( arguments.length == 2 && obj[ type ] ){
          try{
            obj[ type ]();
          }
          catch( e ){};
        }
        // Если не указано удаление события - производим установку события
        else if( remove != true ){
          // Функция-посредник для
          var browserFn = function( e ){
            e = e || w.event;
            // Исправления объекта event в Internet Explorer
            if( IE ){
              e.charCode = e.type == 'keypress' ? e.keyCode : 0;
              e.eventPhase = 2;
              e.isChar = e.charCode > 0;
              e.pageX = e.clientX + document.body.scrollLeft;
              e.pageY = e.clientY + document.body.scrollTop;
              e.type == 'mouseout' ? e.relatedTarget = e.toElement : e.type == 'mouseover' ? e.relatedTarget = e.fromElement : '';
              e.preventDefault = function(){
                this.returnValue = false;
              };
              e.stopPropagation = function(){
                this.cancelBubble = true;
              };
              e.target = e.srcElement;
              e.time = ( new Date() ).getTime();
            };
            // Инициализация переданной функции с установкой внутри неё this - ссылки на DOM-объект и передачей event-объекта
            // Если функция возвращает false, то вызывает событие event-объекта preventDefault
            if( fn.call( obj, e ) == false )
              return e.preventDefault() && false;
          };
          // Сохранение всех данных в массив данных по событиям
          event.steak[ event.steak.length ] = {
            'obj'       : obj,
            'type'      : type,
            'fn'        : fn,
            'browserFn' : browserFn,
            'timestamp' : ( new Date() ).getTime()
          };
          // Добавление события
          addEvent(
            obj,
            type,
            browserFn
          );
        }
        // Удаления события, если четвёртый переданный в функцию параметр равен true
        else if( remove == true )
          // Поиск и сравнение функции события
          for( var i = 0, length = event.steak.length; i < length; i++ )
            if( event.steak[ i ].fn == fn )
              removeEvent( obj, type, event.steak[ i ].browserFn );
        return true;
      };
      // Массив данных по событиям
      event.steak = [];
      // Назначение window событий load, unload, scroll, resize
      for( var i = 0, length = windowEvents.length; i < length; i++ )
        event[ windowEvents[ i ] ] = (
          function( type ){
            return function( fn ){
              return !fn ? w[ 'on' + type ]() : !w[ 'on' + type ] ? w[ 'on' + type ] = fn
                : ( fn.prevfn = w[ 'on' + type ] ) &&
                (
                  w[ 'on' + type ] = function(){
                    fn.prevfn();
                    fn();
                  }
                );
            };
          }
        )( windowEvents[ i ] );
      // Назначение быстрых ссылок на события(Пример event.click( obj, fn );)
      for( var i = 0, length = objectEvents.length; i < length; i++ )
        event[ objectEvents[ i ] ] = (
          function( type ){
            return function( obj, fn, remove ){
              event( obj, type, fn, remove );
            };
          }
        )( objectEvents[ i ] );
      // Функция загрузки DOM-дерева
      event.ready = function( fn ){
        !event.ready.fns && ( event.ready.fns = [] );
        fn && event.ready.fns.push( fn );
        if( event.ready.is == true ){
          event.ready.interval && clearInterval( event.ready.interval );
          for( var i = 0, length = event.ready.fns.length; i < length; i++ )
            event.ready.fns[ i ].call( w );
        }
        else if( !event.ready.interval )
          event.ready.interval = setInterval(
            function(){
              if( document && document.getElementsByTagName && document.getElementById && document.body )
                ( event.ready.is = true ) && event.ready();
            },
            50
          );
        return event.ready.is;
      };
      // Удаление всех обработчиков событий при уходе со страницы для избежания утечек памяти
      event.unload(
        function(){
          for( var i = 0, length = event.steak.length; i < length; i++ )
            removeEvent( event.steak[ i ].obj, event.steak[ i ].type, event.steak[ i ].browserFn );
        }
      );
      // Expose RoolsEvent to the global object
      w.Event = event;
  }
)();

В коде старался делать пояснения. Но пояснения на мой взгляд не сильно идеальны :)

Итак, сейчас я расскажу вам об основных моментах ;)
Минимизированная версия скрипта весит 3,02 КБ(без копирайтов 2.27 КБ).
Внутри скрипта собирается объект event, в последствии зеркалом которого становится window.Event.

Немного об API. В RoolsEvent есть одна единственная главная функция Event(является глобальным объектом. Т. е. window.Event == Event). В неё передаётся DOM-объект, тип назначаемого события(click, focus, etc…) и функция, которая должна выполнятся по наступлению события. На событие одного DOM-объекта можно повесить бесконечное количество функций. Четвёртый параметр, передаваемый в функцию Event – удаление данной функции из события объекта.
Сразу приведу пример:

var obj = document.getElementById( 'pr1' );
var fn = function(){ alert( 'Работает!' ); return false; };
Event( obj, 'click', fn );

Кликните для обзора результата – пример!
Ура, мы назначили обработчик события динамически! Что равносильно этому:

<a href="#" id="pr1" onclick="alert( 'Работает!' ); return false;">Кликните</a>

Кстати! Если функция, переданная в обработчик события возвращает false, то отменяются стандартные действия, которые должны были быть вызваны при данном событии(например переход по ссылки при клике).
Теперь же я покажу пример удаления функции из обработчика события + мы повесим на DOM-объект две функции при наступлении события!

var obj1 = document.getElementById( 'pr2' );
var obj1_fn = function(){ alert( 'Работает!' ); return false; };
var obj1_fn1 = function(){
  if( obj1_fn != null ){
    Event( obj1, 'click', obj1_fn, true/*true означает удаление*/ );
    // obj1_fn = null - не обязательно, просто для примера
    obj1_fn = null;
    alert( 'Успешно удалили первую функцию!' );
  };
  alert( 'Работает вторая функция!' );
  return false;
};
Event( obj1, 'click', obj1_fn );
Event( obj1, 'click', obj1_fn1 );

Кликните для обзора результата – пример!
При первом клике срабатывает первая функция и вторая функция.
Вторая функция удаляет с объекта первую функцию. И при следующих кликах на ссылку будет работать только вторая функция!

Именованные функции. Можно также назначать/удалять функции на обработчики события посредством именованных функций в объекте Event. Сразу же пример для события click:

// Назначение
Event.click( object, myFunction )
// Удаление
Event.click( object, myFunction, true )

На данный момент поддерживаются события: blur, change, click, dblclick, error, focus, keydown, keypress, keyup, mousedown, mousemove, mouseout, mouseover, mouseup, select, submit.

События window. Event имеет функции для назначения событий глобальному объекту window. Поддерживаемые события:

  1. load – вызывается при полной загрузке страницы
  2. unload – вызывается при удалении страницы из окна
  3. scroll – вызывается при скроле страницы
  4. resize – вызывается при изменении размеров страницы

Например назначаем событие при уходе со страницы:

Event.unload(
  function(){
    alert( 'Event говорит вам пока!)' );
  }
);

Event.ready. Событие Event.load вызывается при полной загрузки страницы, включая картинки и всё остальное. Обычно это долгий процесс. Когда нам, как разработчику, это совершенно не нужно, а в частности нужно событие, которое запускалось бы тогда, когда DOM-структура была бы вся загружена. Это событие наступает намного раньше Event.load.
И у нас есть решение этой проблемы! Прототип следующего кода вы можете увидеть по ссылке на статью “Проверка загрузки DOM” и там же прочитать ещё теории на эту тему.
Вот изящный исходный код моей функции Event.ready:

event.ready = function( fn ){
  !event.ready.fns && ( event.ready.fns = [] );
  fn && event.ready.fns.push( fn );
  if( event.ready.is == true ){
    event.ready.interval && clearInterval( event.ready.interval );
    for( var i = 0, length = event.ready.fns.length; i < length; i++ )
      event.ready.fns[ i ].call( w );
  }
  else if( !event.ready.interval )
    event.ready.interval = setInterval(
      function(){
          if( document && document.getElementsByTagName && document.getElementById && document.body )
          ( event.ready.is = true ) && event.ready();
      },
      50
    );
  return event.ready.is;
};

Кстати!. При уходе со страницы все обработчики событий удаляются автоматически, чтобы не вызывать всевозможных утечек памяти!

Я бы с удовольствием рассказал Вам о внутреннем устройство RoolsEvent, но чтец теории JavaScript из меня не очень :(
По этому поводу оставляю вам исходный код RoolsEvent на самостоятельное изучение!)

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

Итак, если Вам нужна кроссбраузерная работа с событиями размером в 3.02 КБ, вперёд! :)

Скачать архив
Спасибо! С уважение, Regent.


Комментарии(8)

Чистяков Денис пишет в субботу, 16.01.2010 в 0:08 Ответить
Спасибо за очень интересную статью, и полезную библиотеку.
Для минимального функционала без селекторов -- отлично.
Код очень изящный, но разобраться не просто ) Буду разбираться )
Продолжайте писать, первые статьи просто отличные!
Вопрос, а где наловчились такому JS'у? Книги, блоги, ковыряния в «сердцах» jQuery и еже сними?

P.S. галочка об уведомлениях с бордером рядом с кнопкой добавления -- как то не очень удобно, так и хочется по ней кликнуть что бы отправить?
Может убрать бордер?

P.P.S. Если пытаться отправить комментарий не зарегистрировавшись / авторизовавшись -- выдает ошибку что это бот ((( Это явно бага -- и мыло не указано, даже не написать никак )
Чистяков Денис пишет в субботу, 16.01.2010 в 0:10 Ответить
Странно почему то получилось добавить комментарий только в Опере, в FF3.5 даже после авторизации не хотел добавлять.
Regent пишет в субботу, 16.01.2010 в 9:02 Ответить
@Чистяков Денис, спаисбо!
На счёт jQuery, буквально вчера писал ссылки здесь: эммм... форум тот не работает Позже скину ссылку на него. Лучшая практика - это изучение исходного кода библиотеки.

На счёт добавления комментария - форма использует JavaScript. Если JS в браузере не работает, либо ошибка какая-то произошла, форма не отправит комментарий.

P. S. Скоро пересмотрим дизайн
Чистяков Денис пишет в субботу, 16.01.2010 в 9:45 Ответить
Несколько раз пытался )
JS точно работает, иначе не мог бы запускать код примеров кнопочкой ;) Кстати очень классно придумано, ты первый блогер у которого такое заметил -- очень удобно и наглядно.
Пробовал добавлять из лисы несколько раз, и страницу обновлял, и заново открывал, и регистрировался / авторизовался (авторизация прошла, и появились ссылки на выход и профиль) и все равно ничего.
Из Оперы добавилось с первого раза.
Даже сейчас добавляю из оперы, т.к. из лисы не хочет.
Regent пишет в субботу, 16.01.2010 в 13:29 Ответить
@Чистяков Денис, спасибо!)

На счёт выполнения примеров - там всего навсего 10 строчек кода
На сколько я знаю, такое есть на данный момент на javascript.ru и больше пока нигде не видел
Кстати, всё никак не могу решить, оповещать ли тогда, когда код примера при выполнении создаёт ошибку... Пока же такое не сделал.
Ещё хотел сделать просмотр примеров HTML-кода в всплывающем окне, но руки никак не дойдут

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

Кстати, вот ссылка, как обещал, на вчерашнюю тему изучения jQuery:
http://forum.sape.ru/showthread.php?t=45304
Там в конце страницы я привёл ссылки на интересные сайты
Чистяков Денис пишет в субботу, 16.01.2010 в 13:38 Ответить
Спасибо за ссылки, обязательно посещу.
Пробую из лисы, авторизованным )
Чистяков Денис пишет в субботу, 16.01.2010 в 13:38 Ответить
Ура, теперь -- получилось ;)
Regent пишет в субботу, 16.01.2010 в 13:59 Ответить
@Денис, спасибо ;)
Я выкинул из головы тот не несущий пользы метод
Уведомлять меня о новых комментариях по почте
Для корректной работы отправки комментариев необходимо включить JavaScript
Либо скачать новый нормальный браузер