Hej! Dziś zajmę się całkiem, moim zdaniem ciekawym tematem jakim jest biblioteka jQuery, a konkretnie, jak zresztą napisałem już w tytule tego posta, dowiemy się jak stworzyć własną wtyczkę jQuery! Myślę, że zacznę od przedstawienia problemu - tym razem wziął się on z życia, a takie przecież lubię najbardziej ;)

Problem

Jeśli zaglądacie od czasu do czasu na tego bloga, zauważyliście na pewno, że niektóre wpisy zawierają obrazek nagłówkowy, który gdy wejdziecie do posta, możecie zobaczyć w pełnych wymiarach, natomiast w stronie głównej wyświetlany jest tylko jego wycinek. Uzyskałem to następująco: po pierwsze tag img w kodzie html otoczony jest dodatkowym elementem div (dodatkowo obrazek jest jednocześnie linkiem ale to nie ma tutaj znaczenia):

<div class="custom-thumb">
    <a rel="bookmark" href="http://burczu-programator.pl/blog/okna-modalne-wtyczce-jquery-bootstrap">
        <img class="attachment-post-thumbnail wp-post-image" alt="Twitter Bootstrap" src="http://nafrontendzie.pl/wp-content/uploads/2014/05/twitter-bootstrap.jpg">
    </a>
</div>

Link oraz obrazek generowane są przez WordPress natomiast dodatkowy div dodałem sam. Aby uzyskać efekt wyświetlania tylko środkowej części obrazka, napisałem taki styl CSS:

.custom-thumb{
    height: 190px

    overflow: hidden;
}

.custom-thumb img {
    max-width: 100%;

    position: relative;
    top: -50%;
    left: 0;
}

Po pierwsze, klasa .custom-thumb ma ustawiony parametr overflow na wartość hidden. Ma też na sztywno zdefiniowaną wysokość. Dzięki temu uzyskujemy swego rodzaju okienko - pokazane zostanie tylko 190px w osi Y każdego elementu znajdującego się wewnątrz tego kontenera, reszta zostanie ukryta. Wszystko fajnie ale to tylko połowiczny efekt ponieważ, widoczny jest górny wycinek obrazka. Dlatego też, w stylu elementu img znalazło się ustawienie top: -50%. To powoduje przesunięcie obrazka wewnątrz kontenera w górę - tym sposobem osiągam porządany efekt. Z pozostałych ustawień stylu dla img, ciekawa może być jeszcze pierwsza linia - max-width: 100%. Dzięki temu obrazek dopasowuje swoją szerokość do szerokości głównego kontenera - jeśli ta szerokość jest mniejsza od szerokości obrazka, zmniejsza on swoje wymiary i na odwrót (wysokość zmienia swoją wartość proporcjonalnie).

Niestety, takie rozwiązanie nie do końca działa. Z dwóch powodów: po pierwsze, jak już wspomiałem, max-width: 100% powoduje, że obrazek dopasowuje się do wielkości okna, jednak top: -50% odnosi się do rzeczywistej wysokości obrazka. Jeśli więc wysokość rzeczywista różni się od wyliczonej przez przeglądarkę podczas dopasowywania obrazka, przesunięcie “nie trafia” w sam środek zdjęcia… Drugim problemem na jaki natrafiłem jest to, że kiedy strona otwierana jest na małym urządzeniu, na przykład smartfonie, obrazek jest tak bardzo zmniejszony, że jego wyliczona wysokość jest mniejsza od 190px ustawionych dla kontenera. To powoduje, wielkie marginesy i dziwne przesunięcie obrazka…

Częściowe rozwiązanie - skrypt JavaScript

W związku z powyższymi problemami postanowiłem napisać skrypt JavaScript, który wyręczy przeglądarkę i odpowiednio dopasuje wszystkie ustawienia. Po pierwsze więc, usunąłem wszystkie ustawienia CSS dla klasy .custom-thumb oraz ustawienie top: -50% z ustawień dla obrazka. Po drugie stworzyłem następujący skrypt:

CustomCropHeaderImage = (function ($) {

	// jQuery variables
	var $customThumb = $('.custom-thumb .attachment-post-thumbnail');

	// other variables
	var requestedHeight;

	function crop() {

		$customThumb.each(function() {
			var top,
				maxHeight = requestedHeight;

			// set max height
			if (this.height < maxHeight) {
				maxHeight = this.height;
			}

			// calculate top
			top = -1 * ((this.height - maxHeight) / 2);

			$(this).parents('.custom-thumb')
				.css('overflow', 'hidden')
				.css('height', maxHeight + 'px');

			$(this)
				.css('top', top + 'px')
				.css('margin', '0px');
		});
	}

	function init(height) {

		// set private variables
		requestedHeight = height;

		$(window).on('load resize', crop);
	}

	return {
		Init: init
	};

} (jQuery));

CustomCropHeaderImage.Init(190);

Myślę, że dla większości z Was wszystko jest tutaj jasne. Funkcja crop uruchamiana jest po załadowaniu strony i podczas zmiany rozmiaru okna (linia 38). Sama funkcja wylicza dwie wartości. Po pierwsze ustala czy użyć wysokości oczekiwanej (190px) czy mniejszej (jeśli wysokość obrazka jest mniejsza niż 190px) - linie 16 - 18. Po drugie wylicza o ile przesunąć obrazek względem kontenera (linia 21). Następnie, na podstawie wyliczonych wartości ustawia odpowiednie wartości stylom elementów HTML.

No i teraz wszystko działa jak należy! Zresztą możecie to sami sprawdzić, bo skrypt ten już działa na moim blogu… Pomyślałem jednak, że fajnie byłoby to jeszcze rozwinąć, a przy okazji się czegoś nauczyć. Stąd pomysł aby przekształcić ten skrypt na wtyczkę jQuery. Na pewno będzie to bardziej eleganckie rozwiązanie, a jeśli trochę je przy okazji rozwinę to będzie się także nadawało do wykorzystania w innych moich projektach tematów WordPress. I tak powstał pomysł na ten wpis - napiszemy tę wtyczkę razem :)

No więc jak stworzyć własną wtyczkę jQuery?

Po tym dość długim wstępie, czas przejść wreszcie do rzeczy czyli tworzenia wtyczki jQuery. Na początek, kilka słów na temat tego jaki efekt chciałbym uzyskać. A więc wygląda to tak, że dla takiego kodu HTML wyświetlającego obrazek:

<img scr="/somepic.png" class="images" id="some-image">

Chcę móc wywołać coś takiego:

$('some-image').setUpCropping(190);

Tworzymy wtyczkę

Ok, wiemy już co chcemy uzyskać. Teraz pytanie jak to zrobić? Okazuje się, że tworzenie wtyczki jQuery **jest bardzo proste. **jQuery zawiera obiekt $.fn, w którym umieszczone są wszystkie funkcje operujące na obiektach elementów DOM. Wystarczy więc rozszerzyć ten obiekt o definicję naszej własnej funkcji:

$.fn.setUpCropping = function(height) { ... }

Prawda, że proste? Czyli jak stworzyć własną wtyczkę jQuery? Spójrzmy jak przerobiłem mój skrypt, tak by można było go używać jako wtyczkę jQuery:

(function($) {
	function crop(element, maxHeight) {
		// variables
		var top;

		// change max height if needed
		if (element.height < maxHeight) {
			maxHeight = element.height;
		}

		// calculate top
		top = -1 * ((element.height - maxHeight) / 2);

		$(element).parents('.custom-thumb')
			.css('overflow', 'hidden')
			.css('height', maxHeight + 'px');

		$(element)
			.css('top', top + 'px')
			.css('margin', '0px');
	}
	$.fn.setUpCropping = function(height) {
		// do for each element
		return this.each(function() {
			// variables
			var self = this;

			// set up event
			$(window).on('load resize', function() {
				crop(self, height);
			});
		});
	};
} (jQuery));

W linii 24 widzimy definicję funkcji setUpCropping, która będzie mogła być wywoływana na obiektach elementów DOM. Warto zwrócić też uwagę na linię 26 - w obiekcie this mamy dostęp do listy obiektów wyselekcjonowanych za pomocą selektora jQuery. Możemy więc teraz na nich wykonywać wszystkie dostępne w jQuery operacje. Warte uwagi jest też, że wynik (w tym przypadku iteracji) jest zwracany jako wynik funkcji. To w celu zachowania “łańcuchowości” czyli czegoś w stylu interfejsu “fluent” znanego z C#. Dzięki temu nasza wtyczka będzie kompatybilna z innymi funkcjami jQuery i będzie mogła być częścią łańcucha wywołań podobnego, na przykład, do tego z linii 15 - 17. Dalej w linii 28 przypisuję obiekt this do zmiennej, ponieważ później, w linii 32 znajdujemy się w innym zakresie zmiennych i this wskazuje już na co innego.

Wtyczka w akcji

Myślę, że na tym poprzestanę wyjaśnianie powyższego kodu. Mówi on sam za siebie :) Jeśli coś jest niejasne - pytajcie w komentarzach. Powyższy kod spełnia już moje główne wymaganie - możliwe jest teraz skonfigurowanie wycinania obrazka w ten sposób:

$('.custom-thumb .attachment-post-thumbnail').setUpCropping(190);

Oczywiście ktoś mógłby się przyczepić, że to zadziała tylko kiedy obrazek jest otoczony kontenerem .custom-thumb… i słusznie, bo tak jest rzeczywiście jednak nie będę tego ograniczenia usuwał w tym wpisie - jest on już wystarczająco długi, a to jeszcze nie koniec… :)

… muszę bowiem napisać jeszcze o jednej ważnej rzeczy. Chodzi mianowicie o przekazywanie ustawień do naszej wtyczki. Do tego celu wykorzystuje się wzorzec obiektu opcji (ang. settings object patter). Jeśli z niego skorzystamy, wywołanie metody setUpCropping mogłoby wyglądać na przykład tak:

$('.custom-thumb .attachment-post-thumbnail').setUpCropping({
    height: 190
});

Z tego punktu widzenia sprawa wygląda prosto, ponieważ po prostu przekazujemy obiekt z ustawionymi odpowiednimi właściwościami. Poniżej natomiast kod metody setUpCropping zawierający obsługę ustawień (z uwzględnieniem ustawień domyślnych):

$.fn.setUpCropping = function(options) {

    // handle options
    var settings = $.extend({
        height: 100, // set default value of height to 100
        width: 50 // another default (not used yet)
    }, options );

    // do for each element
    return this.each(function() {
        // variables
        var self = this;

        // set up event
        $(window).on('load resize', function() {
            crop(self, settings.height);
        });
    });
};

W liniach 4 - 7 zaznaczyłem obsługę opcji. Jak widać, posłużyłem się funkcją extend, dzięki której, najpierw tworzę obiekt zawierający wartości domyślne, a następnie “nakładam” na niego obiekt przekazany jako parametr - jeśli zawiera te same wartości co obiekt domyślny to nadpisuje je i rozszerza je o ewentualne właściwości dodatkowe. W linii 16 widać, że tym razem używam wartości znajdującej się w obiekcie settings.

Podsumowanie

Uff… dobrnęliśmy do końca! Dawno chyba nie było tak długiego wpisu :) Mam nadzieję, że udało mi się jak stworzyć własną wtyczkę jQuery i ogólnie jaka jest idea tworzenia własnych wtyczek. Oczywiście powyższe przykłady należy traktować czysto poglądowo - na pewno samą funkcjonalność wtyczki można zaimplementować lepiej i… taki mam zamiar ;)