Kopiowanie wydarzeń w Javascript - fun with events

Dzisiaj odtwarzam fragment pewnego zadania, które utkwiło mi w głowie. Sam pomysł nie jest mój, padł zarówno na githubie jak i stackoverflow, istnieją różne implementacje, oto szkic mojej :)

Problem: Przeniesienie wydarzeń z jednego elementu na drugi w czystym Javascriptcie.

Na czym polega problem? Jak wiecie - albo i nie - oficjalnie przeglądarki nie udostępniają żadnego interfejsu do odczytywania zdarzeń zarówno globalnie jak i na elemencie. Nie istnieje element.getEventsListeners() albo element.events poza debuggerem Chrome'a.

Chociaż pewnie drugi zapis rozpoznajecie, pochodzi z JQuery i większość odpowiedzi na powyżej zadany problem brzmi 'użyj jQuery'. Dzisiaj zrobimy to sami :)

Koncepcja rozwiązania

Nie ma element.getEventsListeners()? Napiszmy go sobie. Po prostu przechwycimy je w miejscu, którym jest to najprostsze - przy dodawaniu zdarzenia i zapiszemy :)

Przede wszystkim musimy zacząć od WebAPI. Wiele ludzi myli język Javascript z WebAPI, czyli napisanymi w Javascript (EMCAScript w sumie) interfejsami, które umożliwiają wprowadzanie dynamicznych zmian na stronie internetowej. Tutaj można zobaczyć pełną listę Web API.

Tak więc mamy HTMLElement, który implementuje GlobalEventHandler - brzmi perfekcyjnie :)

Teraz najdelikatniejsza część. Ogólną zasadą jest, że nie zmienia się prototypów natywnych obiektów, ale dzisiaj zrobimy wyjątek :) Zadbamy o kompatybilność.

HTMLElement.prototype.addEventListner = function() {
    console.log('Żaden event nie będzie już działał, ale mamy fajnego console.loga w zamian');
}

Możliwość techniczną już mamy, a teraz koncepcja: będziemy zbierać eventy do jakieś właściwości. Ciekawym rozwiązaniem, które podpatrzyłam (nie jestem w stanie znaleźć tego repo na githubie, jeśli ktoś znajdzie, proszę o kontakt, piszę z pamięci), to forma składowania elementów:

element._listeners = {
    'click': [
        {
            'func': jakasTamFunkcja,
            'useCapture': true
        },
        {
            'func': kolejna funkcja,
            'useCapture': false
        }
    ],
    'moveover': [
        {
            'func': jakasTamFunkcja,
            'useCapture': true
        },
        {
            'func': kolejna funkcja,
            'useCapture': false
        }
    ]
}

Bardzo porządkuje eventy, prawda? I pozwoli na prostsze zwracanie funkcji na konkretnym wydarzeniu: element.getEventsListeners('click'). To napiszmy pierwszą funkcję zbierającą eventy.


//do _listners będziemy zbierać eventy
HTMLElement.prototype._listeners = {};

//będziemy nadpisywać addEventListener oraz removeEventListener, ale nie możemy ich stracić, gdyż są niezbędne do działania zdarzeń
HTMLElement.prototype._addEventListener = HTMLElement.prototype.addEventListener;
HTMLElement.prototype._removeEventListener = HTMLElement.prototype.removeEventListener;

//nadpisywanie dodawania zdarzeń; ważne aby przyjmowały te same parametry co natywne funkcje
HTMLElement.prototype.addEventListener = function(type, func, useCapture = false) {
    if (typeof type !== 'undefined' && typeof func !== 'undefined' && type && func) {

        this._listeners[type] = this._listeners[type] ? this._listeners[type] : new Array();

        this._listeners[type].push({
            func: func,
            capture: useCapture
        });
        this._addEventListener(type, func, useCapture);
    }
}

Gdy załączycie ten skrypt pierwszy, a potem dołączycie inne (zawierając dodawanie zdarzeń) wszystko powinno być w porządku. Poza tym, że po dodaniu eventu i wywołaniu console.log(element._listeners) pokaże się nam w konsoli obiekt zawierający dodane zdarzenie.

Aby zakończyć potrzebne są nam jeszcze dwie funkcje: removeEventListener i getEventListeners.

HTMLElement.prototype.removeEventListener = function(type, func, useCapture = false) {

    if (typeof type !== 'undefined' && typeof func !== 'undefined' && type && func) {
        if (Array.isArray(this._listeners[type])) {

            this._listeners[type] = this._listeners[type].filter(event => {
                event.func = func && event.capture == useCapture
            });

        }
        this._removeEventListener(type, func, useCapture);
    }
}

HTMLElement.prototype.getEventListeners = function(type) {
    return type ? this._listeners[type] || [] : this._listeners || {};
}

Kluczem jest spójność, czyli nie zmienianie listeners bezpośrednio, z pomięciem użycia naszych nowych funkcji.

Teraz fun with events, czyli będziemy kopiować zdarzenia z jednego elementu na drugi. W tym celu napiszemy funkcję copyEventListeners():

HTMLElement.prototype.copyEventListenersFrom = function(element) {
    let eventTypes = Object.keys(element.getEventListeners());
    eventTypes.forEach((eventType) => {
        events[eventType].forEach((event) => {
            this.addEventListener(eventType, event.func, event.capture);
        });
    });
}

O czym trzeba pamiętać?

  • to szkic napisany w trakcie tworzenia tego artykułu, nie używaj go na produkcji. Ze względu na elementy ES6, trzeba by go przetestować, przepuścić przez Babela oraz zminifikować.
  • ta wersja nie obsługuje IE w wersji mniejszych od 8, pollyfill by się przydał, fajny pomysł na rozbudowę zadania :)
  • tę funkcję można rozszerzyć o sprawdzanie typów albo pilnowanie spójności listeners. W chwili obecnej nawet gdy dodamy event poprzez natywny _addEventListeners(), nasz removeEventListenes również go usunie. Trudno powiedzieć czy to bug czy feature :)

Podsumowanie

Mam nadzieję, że się podobało :) Jeśli macie inne pomysły lub uwagi, zapraszam do komentowania.

https://github.com/KamilaBrylewska/events

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)