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.