preventDefault
i stopPropagation
: kiedy używać poszczególnych metod i jak dokładnie działają.
Event.stopPropagation() i Event.preventDefault()
Obsługa zdarzeń JavaScript jest często prosta. Dotyczy to zwłaszcza prostej (względnie płaskiej) struktury HTML. Sytuacja komplikuje się, gdy zdarzenia przemieszczają się (lub rozprzestrzeniają) w hierarchii elementów. Zwykle ma to miejsce, gdy deweloperzy sięgają po stopPropagation()
lub preventDefault()
, by rozwiązać napotkane problemy. Jeśli kiedykolwiek pomyślisz „Spróbuję preventDefault()
, a jeśli to nie zadziała, spróbuję stopPropagation()
, a jeśli to też nie zadziała, spróbuję jedno i drugie”, ten artykuł jest dla Ciebie. Wyjaśnię, na czym polega każda z nich, kiedy jej używać, i przedstawię różne przykłady jej zastosowania. Chcę raz na zawsze rozwiać Twoje wątpliwości.
Zanim jednak przejdziemy do szczegółów, warto krótko omówić 2 rodzaje obsługi zdarzeń w języku JavaScript (w przypadku wszystkich nowoczesnych przeglądarek – Internet Explorer w wersji 9 i starszych wcale nie obsługiwał rejestrowania zdarzeń).
Style WKD (przechwytywanie i dymkowanie)
Wszystkie nowoczesne przeglądarki obsługują rejestrowanie zdarzeń, ale jest ono bardzo rzadko używane przez programistów.
Co ciekawe, była to jedyna forma zdarzeń obsługiwana przez Netscape. Największy konkurent Netscape, Microsoft Internet Explorer, nie obsługiwał w ogóle rejestrowania zdarzeń, ale tylko inny styl zdarzeń zwany propagowaniem zdarzeń. Po utworzeniu W3C uznano, że należy używać obu stylów zdarzeń, i zadeklarował, że przeglądarki powinny je obsługiwać za pomocą trzeciego parametru w metodzie addEventListener
. Pierwotnie był to tylko parametr logiczny, ale wszystkie współczesne przeglądarki obsługują obiekt options
jako trzeci parametr, za pomocą którego możesz określić (między innymi) czy chcesz korzystać z rejestrowania zdarzeń:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Obiekt options
i jego właściwość capture
są opcjonalne. Jeśli pominiesz jedną z tych wartości, domyślna wartość capture
to false
, co oznacza, że używane będą dymki zdarzeń.
rejestrowanie zdarzeń,
Co to znaczy, że moduł obsługi zdarzeń „nasłuchuje na etapie nagrywania”? Aby to zrozumieć, musimy wiedzieć, jak powstają zdarzenia i jak się przemieszczają. Ta zasada obowiązuje w odniesieniu do wszystkich zdarzeń, nawet jeśli Ty jako deweloper nie korzystasz z niej, nie interesujesz się tym ani nie o niej myślisz.
Wszystkie zdarzenia zaczynają się w oknie i najpierw przechodzą przez etap rejestrowania. Oznacza to, że gdy zdarzenie zostanie wysłane, rozpoczyna okno i przechodzi „w dół” do elementu docelowego najpierw. Dzieje się tak nawet wtedy, gdy słuchasz tylko w fazie przenoszenia. Rozważ ten przykładowy kod znaczników i JavaScriptu:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
Gdy użytkownik kliknie element #C
, wysyłane jest zdarzenie pochodzące z elementu window
. To zdarzenie będzie się rozprzestrzeniać na swoich potomkach w ten sposób:
window
=> document
=> <html>
=> <body>
=> itd., aż do osiągnięcia wartości docelowej.
Nie ma znaczenia, czy nic nie nasłuchuje zdarzenia kliknięcia w elemencie window
, document
, <html>
ani <body>
(ani w żadnym innym elemencie na drodze do celu). Zdarzenie nadal rozpoczyna się w miejscu window
i przebiega w sposób opisany powyżej.
W naszym przykładzie zdarzenie kliknięcia będzie zastępowane (jest to ważne słowo, ponieważ odpowiada bezpośrednio za działanie metody stopPropagation()
i zostanie wyjaśnione w dalszej części tego dokumentu) od window
do elementu docelowego (w tym przypadku #C
) za pomocą każdego elementu między window
a #C
.
Oznacza to, że zdarzenie kliknięcia rozpocznie się o window
, a przeglądarka zada Ci te pytania:
„Czy w fazie przechwytywania jakieś komponenty nasłuchują zdarzenia kliknięcia na window
?” W takim przypadku zostaną uruchomione odpowiednie moduły obsługi zdarzeń. W naszym przykładzie nic nie jest zaznaczone, więc żadne metody obsługi nie zostaną wywołane.
Następnie zdarzenie rozprzestrzeni się na document
, a przeglądarka zapyta: „Czy w fazie przechwytywania jest coś, co nasłuchuje zdarzenia kliknięcia na document
?”. Jeśli tak, zostaną wywołane odpowiednie procedury obsługi zdarzeń.
Następnie zdarzenie zostanie rozpoczęte do elementu <html>
, a przeglądarka zapyta: „Czy na etapie przechwytywania coś nasłuchuje kliknięcia elementu <html>
?”. Jeśli tak, uruchomią się
odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element <body>
, a przeglądarka zapyta: „Czy w fazie przechwytywania coś czeka na zdarzenie kliknięcia elementu <body>
?”. Jeśli tak, zostaną uruchomione odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element #A
. Ponownie przeglądarka zapyta: „Czy w fazie rejestrowania coś nasłuchuje zdarzenia kliknięcia na #A
? Jeśli tak, zostaną wywołane odpowiednie moduły obsługi zdarzeń.
Następnie zdarzenie rozprzestrzenia się na element #B
(i zostanie zadane to samo pytanie).
Na koniec zdarzenie osiągnie cel, a przeglądarka zapyta: „Czy na etapie przechwytywania jest coś wykrywającego zdarzenie kliknięcia elementu #C
?”. Tym razem odpowiedź brzmi „tak”. Ten krótki okres czasu, w którym zdarzenie jest nastawione na cel, nazywa się „fazą docelową”. W tym momencie zostanie wywołany event handler, przeglądarka wygeneruje komunikat „#C was clicked” w console.log i to wszystko.
Błąd! To jeszcze nie koniec. Proces jest kontynuowany, ale teraz przechodzi w fazę przenoszenia.
Dymki dotyczące wydarzeń
Przeglądarka zapyta:
„Czy w fazie przenoszenia jest coś, co nasłuchuje zdarzenia kliknięcia na stronie #C
?” Zwróć na to uwagę.
Możliwe jest śledzenie kliknięć (lub dowolnego typu zdarzenia) zarówno w fazie przechwytywania, jak i w fazie przenoszenia. A jeśli masz połączone moduły obsługi zdarzeń w obu fazach (np. przez dwukrotne wywołanie .addEventListener()
, raz przy użyciu capture = true
i drugie przy użyciu capture = false
), to tak, oba moduły obsługi zdarzeń bezwzględnie uruchomiłyby się dla tego samego elementu. Pamiętaj też, że te reguły są wywoływane w różnych fazach (jedna w fazie przechwytywania, a druga w fazie propagowania).
Następnie zdarzenie zostanie rozpoczęte (bardziej nazywane „dymkiem”, ponieważ wygląda na to, że zdarzenie przesuwa się „w górę” drzewa DOM) do elementu nadrzędnego (#B
), a przeglądarka zapyta: „Czy coś nasłuchuje zdarzeń kliknięcia w #B
w fazie błyszczania?”. W naszym przykładzie nic nie jest, więc nie zostaną wywołane żadne metody obsługi.
Następnie zdarzenie będzie się rozprzestrzeniać do poziomu #A
, a przeglądarka zapyta: „Czy w fazie rozprzestrzeniania jest coś, co nasłuchuje zdarzeń kliknięcia na poziomie #A
?”.
Następnie zdarzenie wyświetli się jako dymek z komunikatem <body>
: „Czy w elemencie <body>
w fazie migania jakieś zdarzenia wykrywają zdarzenia kliknięcia?”.
Następnie element <html>
: „Czy w fazie przenoszenia jest coś, co nasłuchuje zdarzeń kliknięcia w elemencie <html>
?
Następnie document
: „Czy w etapie propagowania jest coś, co nasłuchuje zdarzeń kliknięcia w komponencie document
?”.
Na koniec window
: „Czy w oknie w fazie przenoszenia jest coś, co nasłuchuje zdarzeń kliknięcia?”.
Uff... To była długa podróż, a nasze wydarzenie jest pewnie już bardzo zmęczone, ale czy wierzysz, czy nie, taka jest droga każdego wydarzenia. W większości przypadków nie jest to zauważalne, ponieważ deweloperów zwykle interesuje tylko jedna faza zdarzenia (zwykle jest to faza przenoszenia).
Warto poświęcić trochę czasu na zabawę z rejestracją i przekazywaniem zdarzeń oraz dodawaniem notatek do konsoli podczas uruchamiania obsługi. Warto zobaczyć ścieżkę, jaką pokonuje zdarzenie. Oto przykład, który sprawdza każdy element na obu etapach.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
Dane wyjściowe konsoli zależą od tego, który element klikniesz. Jeśli klikniesz element „najgłębiej” w drzewie DOM (element #C
), zobaczysz, że wszystkie te przetwarzacze zdarzeń są wywoływane. Oto element konsoli #C
(oraz zrzut ekranu) z nieco większym stylizowaniem CSS, aby było łatwiej odróżnić poszczególne elementy:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
Możesz wypróbować tę funkcję w poniżej prezentowanym demonstracyjnym filmie. Kliknij element #C
i zobacz dane wyjściowe konsoli.
event.stopPropagation()
Znając miejsce pochodzenia zdarzeń i sposób ich przemieszczania się (tj. propagowania) w DOM (zarówno na etapie przechwytywania, jak i błyskawicy), możemy teraz zwrócić uwagę na event.stopPropagation()
.
Metodę stopPropagation()
można wywołać w przypadku (większości) natywnych zdarzeń DOM. Piszę „w większości”, ponieważ w niektórych przypadkach wywołanie tej metody nie powoduje żadnych działań (ponieważ samo zdarzenie się nie rozprzestrzenia). Do tej kategorii należą wydarzenia takie jak focus
, blur
, load
, scroll
i kilka innych. Możesz wywołać funkcję stopPropagation()
, ale nic ciekawego się nie stanie, ponieważ te zdarzenia nie są propagowane.
Co jednak robi stopPropagation
?
Robi dokładnie to, co jest napisane. Gdy go wywołasz, przestaje się ono rozprzestrzeniać na elementy, do których w przeciwnym razie by dotarło. Dotyczy to obu kierunków (przechwytywania i przekazywania). Jeśli więc wywołasz funkcję stopPropagation()
w dowolnym miejscu w fazie rejestrowania, zdarzenie nigdy nie dotrze do fazy docelowej ani fazy przenoszenia. Jeśli wywołasz go w fazie przenoszenia, będzie on już w fazie przechwytywania, ale przestanie „przechodzić” od momentu wywołania.
Wracając do naszego przykładowego znacznika, co się stanie, jeśli wywołamy funkcję stopPropagation()
w fazie przechwytywania w elemencie #B
?
Powinien pojawić się taki wynik:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
Możesz wypróbować tę funkcję w prezentacji na żywo poniżej. Kliknij element #C
w demonstracji na żywo i obserwuj dane w konsoli.
Czy można zatrzymać propagowanie na etapie #A
w ramach propagacji? Efektem będzie taki wynik:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
Możesz wypróbować tę funkcję w poniżej prezentowanym demonstracyjnym filmie. Kliknij element #C
w demonstracji na żywo i obserwuj dane w konsoli.
Jeszcze jedno, dla zabawy. Co się stanie, jeśli wywołamy funkcję stopPropagation()
w fazie docelowej w przypadku #C
?
Pamiętaj, że „faza docelowa” to nazwa nadana okresowi, w którym zdarzenie osiągnęło swój cel. Zostaną wyświetlone takie dane wyjściowe:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
Zwróć uwagę, że moduł obsługi zdarzeń #C
, w którym rejestrujemy „kliknięcie #C w fazie przechwytywania” nadal jest wykonywany, ale ten, w którym rejestrujemy „kliknięcie #C w fazie błyszczania” już nie. To powinno mieć sens. Nazwa stopPropagation()
nazwaliśmy od pierwszego, więc to moment, w którym zakończy się propagacja zdarzenia.
Możesz wypróbować tę funkcję w poniżej prezentowanym demonstracyjnym filmie. Kliknij element #C
w wersji demonstracyjnej na żywo i zobacz dane wyjściowe konsoli.
Zachęcamy do wypróbowania każdej z tych demonstracji. Spróbuj kliknąć tylko element #A
lub tylko element body
. Spróbuj przewidzieć, co się stanie, a następnie sprawdź, czy masz rację. W tym momencie powinieneś/powinnaś być w stanie dość dokładnie przewidywać wyniki.
event.stopImmediatePropagation()
Na czym polega ta dziwna i rzadko używana metoda? Jest to podobne do stopPropagation
, ale zamiast blokowania przekazywania zdarzenia do potomków (przechwytywanie) lub przodków (przekazywanie dalej), ta metoda ma zastosowanie tylko wtedy, gdy masz więcej niż 1 obsługę zdarzenia podłączoną do pojedynczego elementu. Ponieważ addEventListener()
obsługuje zdarzenia w stylu multicast, można wielokrotnie podłączać do jednego elementu więcej niż 1 obsługę zdarzeń. W takim przypadku (w większości przeglądarek) moduły obsługi zdarzeń są wykonywane w kolejności podłączenia przewodów. Wywołanie stopImmediatePropagation()
uniemożliwia wywołanie kolejnych przetwarzaczy. Na przykład:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
W przykładzie powyżej w konsoli pojawią się takie dane wyjściowe:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Pamiętaj, że trzeci moduł obsługi zdarzeń nigdy nie jest uruchamiany, ponieważ drugi moduł obsługi zdarzeń wywołuje funkcję e.stopImmediatePropagation()
. Gdybyśmy zamiast tego wywołali funkcję e.stopPropagation()
, trzecia obróbka nadal by się wykonała.
event.preventDefault()
Co robi preventDefault()
, jeśli stopPropagation()
uniemożliwia zdarzeniem poruszanie się „w dół” (przechwytywanie) lub „w górę” (bąbelki)? Wygląda na to, że działa podobnie. Czy to działa?
Nie bardzo Chociaż te dwa terminy są często mylone, w istocie nie mają ze sobą wiele wspólnego.
Gdy zobaczysz preventDefault()
, w głowie dodaj słowo „działanie”. Pamiętaj, że „zapobieganie” oznacza „zapobieganie domyślnemu działaniu”.
Jakie jest domyślne działanie, o które możesz zapytać? Odpowiedź na to pytanie nie jest jednoznaczna, ponieważ zależy od danej kombinacji elementu i zdarzenia. Co więcej, czasami nie ma w ogóle żadnego działania domyślnego.
Zacznijmy od bardzo prostego przykładu. Co się spodziewasz po kliknięciu
linku na stronie internetowej? Oczywiście oczekujesz, że przeglądarka przejdzie na adres URL podany w tym linku.
W tym przypadku element to tag kotwicy, a zdarzenie to zdarzenie kliknięcia. Ta kombinacja (<a>
+
click
) ma „domyślne działanie” polegające na przejściu do adresu URL linku. Co zrobić, jeśli chcesz zapobiec wykonywaniu przez przeglądarkę tego domyślnego działania? Załóżmy, że chcesz uniemożliwić przeglądarce przechodzenie do adresu URL podanego w atribute href
elementu <a>
. Oto, co preventDefault()
zrobi dla Ciebie. Przeanalizuj ten przykład:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
Możesz wypróbować tę funkcję w poniżej prezentowanym demonstracyjnym filmie. Kliknij link The Avett Brothers i obserwuj dane w konsoli (zwróć uwagę, że nie nastąpi przekierowanie do witryny Avett Brothers).
Kliknięcie linku o nazwie The Avett Brothers spowoduje przejście do strony www.theavettbrothers.com
. W tym przypadku do elementu <a>
podłączyliśmy jednak element obsługi zdarzenia kliknięcia i określiliśmy, że działanie domyślne powinno zostać zablokowane. Gdy użytkownik kliknie ten link, nie przejdzie nigdzie indziej, a konsola wyświetli komunikat „Być może powinniśmy tutaj po prostu odtworzyć jego muzykę?”.
Jakie inne kombinacje elementów i zdarzeń umożliwiają zablokowanie domyślnego działania? Nie sposób wymienić wszystkich, a czasami trzeba po prostu eksperymentować. Oto kilka z nich:
Element
<form>
+ zdarzenie „submit”:preventDefault()
ta kombinacja uniemożliwi przesłanie formularza. Jest to przydatne, gdy chcesz przeprowadzić walidację. Jeśli coś się nie uda, możesz warunkowo wywołać metodę preventDefault, aby zatrzymać przesyłanie formularza.Element
<a>
+ zdarzenie „kliknięcie”:preventDefault()
w przypadku tej kombinacji uniemożliwia przeglądarce przejście do adresu URL określonego w atrybucie href elementu<a>
.document
+ zdarzenie „mousewheel”:preventDefault()
w przypadku tej kombinacji kolumna nie będzie się przewijać za pomocą kółka myszy (przewijanie za pomocą klawiatury będzie jednak działać).
↜ W tym celu należy wywołać funkcjęaddEventListener()
z parametrem{ passive: false }
.document
+ zdarzenie „keydown”:preventDefault()
w tej kombinacji jest śmiertelne. Strona staje się wtedy praktycznie bezużyteczna, ponieważ nie można jej przewijać, używać tabulatorów ani wyróżniać elementów za pomocą klawiatury.document
+ zdarzenie „mousedown”:preventDefault()
w przypadku tej kombinacji zablokujesz wyróżnianie tekstu za pomocą myszy i wszystkie inne „domyślne” działania, które można wywołać po naciśnięciu myszy.Element
<input>
+ zdarzenie „keypress”: ta kombinacjapreventDefault()
uniemożliwi docieranie znaków wpisywanych przez użytkownika do elementu wejściowego (ale nie rób tego; rzadko, jeśli w ogóle, istnieje ku temu ważny powód).document
+ zdarzenie „contextmenu”:preventDefault()
ta kombinacja uniemożliwia wyświetlanie menu kontekstowego przeglądarki, gdy użytkownik kliknie prawym przyciskiem myszy lub naciśnie i przytrzyma przycisk (lub w dowolny inny sposób wywoła menu kontekstowe).
Ta lista pod żadnym pozorem nie jest wyczerpująca, ale mamy nadzieję, że dzięki niej dowiesz się, jak korzystać z narzędzia preventDefault()
.
To zabawny żart?
Co się stanie, jeśli stopPropagation()
i preventDefault()
na etapie przechwytywania, zaczynając od dokumentu? Śmiech gwarantowany! Następujący fragment kodu spowoduje, że każda strona internetowa będzie praktycznie bezużyteczna:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
Nie wiem, dlaczego chcesz to zrobić (chyba że chcesz kogoś w jakiś sposób oszukać), ale warto zastanowić się, co się tu dzieje i dlaczego to powoduje taką sytuację.
Wszystkie zdarzenia pochodzą z window
, więc w tym fragmencie kodu całkowicie blokujemy zdarzenia click
, keydown
, mousedown
, contextmenu
i mousewheel
, aby nie docierały do żadnych elementów, które mogłyby je przechwycić. Wykonujemy też wywołanie stopImmediatePropagation
, aby wszystkie moduły obsługiwane przez ten dokument po tym wywołaniu również zostały zablokowane.
Pamiętaj, że stopPropagation()
i stopImmediatePropagation()
nie są (przynajmniej w większości przypadków) powodem, dla którego strona jest bezużyteczna. Po prostu zapobiegają one dotarciu zdarzeń do miejsca, do którego normalnie by trafiły.
Ale wywołujemy też preventDefault()
, który jak pamiętasz zapobiega domyślnemu działaniu. W związku z tym wszystkie domyślne działania (np. przewijanie za pomocą kółka myszy, przewijanie za pomocą klawiatury, wyróżnianie lub przełączanie za pomocą klawiszy tabulacji, klikanie linków, wyświetlanie menu kontekstowego) są zablokowane, co sprawia, że strona jest praktycznie bezużyteczna.
Prezentacje na żywo
Aby ponownie przejrzeć wszystkie przykłady z tego artykułu w jednym miejscu, obejrzyj umieszczoną poniżej prezentację.
Podziękowania
Obraz główny autorstwa Tom Wilson na Unsplash.