Для написания плагина будут использоваться материалы из статьи Мастер класс по созданию плагина галереи на jQuery, а также для понимания материала рекомендую ознакомится со статьей Как написать плагин на jQuery
Что я хотел получить в результате
На страницу в произвольном порядке выводятся несколько блоков, содержащих фотографии. Этими блоками могут быть, как обычные <div> так и ссылки <a>. Кроме них к странице подключаются два файла, сам плагин и его стили. Вызвав соответствующий названию плагина метод объекта $, с некими параметрами мы бы получали прокручивающуюся галерею фото. Фотографии должны прокручиваться, как с помощью мыши так и соответствующими кнопками по бокам галереи - назад-вперед. Кроме этих средств навигации в плагине должна быть возможность прокрутки к любой фотографии.
В дополнении ко всему у плагина должна быть возможность выбора ориентации, горизонтальная либо вертикальная, смена скорости и шага прокрутки.
Если Вам лень читать, как это все работает то Вы можете скачать готовый плагин
Как яхту назовешь, так она и поплывет
Назвать плагин надо так, чтобы во первых, название не пересекалось с плагинами jQuery из под коробки, т.е. сразу забываем про названия типа html, css, appendTo и т.д. И желательно, чтобы не пересекало названия других популярных плагинов. Достаточно пару минут погуглить, и Вы поймете, используется ли Ваше название еще где-либо. Если нет, смело вперед.
Я решил назвать свое детище xdGallery. Наверно потому, что мой блог называется XDan, а может потому, что мои инициалы ЧВ в латинской раскладке XD. Название нигде не встречается, так что я приступаю к написанию плагина.
Создаем рабочий каркас
Есть некий стандарт в написании плагинов на jQuery. Мы уже определились с именем, теперь перейдем к файлам. Файл плагина должен называться jquery.<Название плагина>.js, так же должен называться и css файл. Т.е. в моем случае нужно создать два файла jquery.xdGallery.js и jquery.xdGallery.css
Если файл плагина занимает много места, то его сжимают различными упаковщиками. К примеру packer'ом. Сжатый файл, в котором удалены лишние пробелы, отступы и переносы строк называют jquery.<Название плагина>.min.js, если файл был сжат алгоритмами обфускации кода, или в само распаковывающийся js, то такой файл называют jquery.<Название плагина>.pack.js. Практика показывает, что наиболее оптимален первый вариант, когда из js просто удаляются лишние символы.
Однако, не стоит поставлять Ваш плагин лишь с сжатой версией файлов, программист, который будет использовать плагин в своем проекте, вполне возможно захочет что-то поменять. Поэтому оригинальный несжатый файл, также должен присутствовать в архиве.
Так же есть ряд правил, которым нужно следовать, чтобы плагин сохранил свою работоспособность после сжатия. Для каждого упаковщика они свои, и вполне возможно, что Вы напишите код, который вопреки всем выполненным требованиям не запустится, поэтому изучайте требования к Вашему упаковщику.
Упаковщикам с которыми я имел дело, было важно чтобы в конце каждой функции стояла ";"
Кроме js и css с плагином часто идут сопутствующие файлы, картинки, другие плагины. Картинки, если есть возможность лучше собрать в одну, и кинуть рядом с плагином. В противном случае, обычно создают папку с названием плагина, и кидают все файлы, кроме js туда. Да, да, файл jquery.xdGallery.css тоже можно закинуть туда.
Для работы мне потребуется 3 картинки назад, вперед и кнопка навигации. Я закину их в папку xdGallery/img/.
jquery.xdGallery.css закинем в xdGallery/
С файлами мы определились, теперь перейдем непосредственно к коду.
jquery.xdGallery.js
Создадим каркас плагина в файле jquery.xdGallery.js
(function($){ $.fn.xdGallery = function( galleryOptions ){ var defaultOptions = { 'shag':200 }; var options = $.extend({}, defaultOptions, galleryOptions); var step = function(){} var init = function(){} if (this.length > 0){ this.each(function(){ // делаем для всех найденных галерей init(this); }); } return this; } })(jQuery);
Еще одно правило: весь код заключается в само вызывающееся замыкание, на вход которого подается объект jQuery. Это сделано для того, чтобы внутри этого замыкания, независимо от того, включен ли noConflict режим, работать с объектом jQuery посредством его более короткого алиаса $. Проиллюстрирую на следующем примере:
// где-то выше был подключен jQuery // затем переопределяем стандартный $ на свой function $(id){ return document.getElementById(id); } // такое случается когда к странице подключаются другие библиотеки(например:Prototype) // в этом случае объект $ будет не валидным и далее нужно работать с объектом jQuery // понятно, что это не совсем удобно, поэтому делают так (function($){ // а уже тут работаем в $ как с обычным jQuery объектом $('a.myclass').css('background','red') })(jQuery);
Далее взглянем на этот код
var defaultOptions = { 'shag':200 }; var options = $.extend({}, defaultOptions, galleryOptions);
здесь мы просто, заполняем все опции дефолтными значениями, ведь не каждому программисту захочется переопределять их все. Но, мы также должны оставить возможность более продвинутым программистам вводить свои параметры, поэтому дефолтные настройки объединяются с настройками, которые программист подал при инициализации плагина, по средствам метода jQuery.extend. При этом приоритет имеют кастомные настройки.
var custom = { 'shag':100 } var default = { 'shag':300, 'speed':150 }; var results = $.extend({}, default, custom); // в результате в объекте results будет {shag:100,speed:150}
Далее по коду. Еще два правила:
1)плагин по возможности, если не предусмотрено обратное, должен возвращать объект this, который в нем является алиасом jQuery. Т.е. каждый плагин jQuery должен возвращать jQuery.
2)так как галерей, на странице может быть несколько, то плагин должен обработать все объекты к которым был применен, при помощи метода jQuery.each
jquery.xdGallery.css
css файл будет содержать все стили галереи. Главное требование здесь то же, что и при выборе названия галереи: названия селекторов должны быть не общеупотребительными. Желательно сделать их с некими префиксами, кроме того, так как плагинов на странице может быть несколько, то работаем только с селекторами по классу.
Моим префиксом будет xdgallery
.xdgallery{overflow:hidden;position:relative;border:1px solid #eee; width:700px;} .xdgallery .carusel{position:relative;width:5000px;} .xdgallery .carusel a{float:left;display:block;margin-right:5px;} .xdgallery .prev, .xdgallery .next, .xdgallery .navigation{ position:absolute; left:0px; z-index:99; width:8%; background:#eee url(img/prev.png) no-repeat; background-position: 50% 50%; height:100%; opacity:0.5; cursor:pointer; } .xdgallery .next{ left:92%; background-image:url(img/next.png); } .xdgallery .navigation{ left:50%; width:auto; bottom:5px; height:16px; background:#eee; padding:3px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; opacity:0.7; } .xdgallery .navigation div{ height:16px; width:16px; float:left; background:url(img/tab.png); margin-right:3px; } .xdgallery .navigation div.active{ background-position:0px 16px; } .xdgallery.vertical{ height:600px;width:200px;} .xdgallery.vertical .carusel{ width:auto; height:5000px;} .xdgallery.vertical .carusel a{float:none;display:block;margin-bottom:5px;} .xdgallery.vertical .prev, .xdgallery.vertical .next{ top:0px; left:auto; height:8%; background:#eee url(img/up.png) no-repeat; background-position: 50% 50%; width:100%; } .xdgallery.vertical .next{ top:92%; left:auto; background-image:url(img/down.png); } .xdgallery.vertical .navigation{ top:50%; width:16px; left:5px; height:auto; bottom:auto; } .xdgallery.vertical .navigation div{ margin-bottom:3px; float:none; }
Как Вы можете видеть, в стилях предусмотрены два варианта: основной - для горизонтальной ориентации, и вертикальный. По сути, чтобы поменять ориентацию на вертикальную, к классу галереи xdgallery надо дописать vertical. Как работают эти стили я подробно расписал в приведенном выше мастер классе, тут я не буду на этом останавливаться.
Перейдем непосредственно к программной части
Опции
Для работы плагина я решил выбрать следующий набор опций:
var defaultOptions = { 'shag':200,// px за один шаг прокрутки 'speed':200, // время за которое карусель прокрутится на один шаг(shag) 'itemClass':'itemXDGalery',// пометка каждого item'а классом 'easingWheel':'swing', // характер прокрутки при вращении колесиком 'easingNext':'linear', // характер прокрутки при прокрутке через next,prev 'easingDrag':'linear', // характер прокрутки при перетаскивании, эффект инерции 'invertWheel':1,// инвертировать прокрутку при вращении колесика мышки (1 или -1) 'invertNext':-1, // инвертировать прокрутку при нажатии кнопок next и prev (1 или -1) 'horizontal':true,// горизонтальная ориентация галереи, по умолчанию true, если false то вертикальная ориентация 'onHoverNext':false, // прокручивать карусель при наведении на кнопку next или prev 'navigation':false // навигация, если false, то выключена. Если >0, то равно на сколько табов будет делиться карусель, если указать = 0, то будет автоматически подбирать под количество items };
я думаю назначение каждой опции объяснять не нужно, все ясно из комментариев.
Инициализация
Как мы выяснили в мастер классе, для работы галереи нужно, чтобы поданные программистом items'ы( т.е. фото) были обернуты в 2 div бокса. Напомню тут эту структуру
<div class="xdgallery"> <div class="prev"></div><!--кнопка прокрутки назад--> <div class="next"></div><!--кнопка прокрутки вперед--> <div class="carusel"> <!--здесь будет фото--> <div style="clear:both"></div> </div> </div>
Но, мы уже говорили о том, что плагин не должен требовать от программиста создание такой структуры. Он должен создавать ее сам. Т.е. нужно на входе получить
<div id="gal"> <a href="examples/gallery/img/big_photo1.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo1.jpg" /> </a> <a href="examples/gallery/img/big_photo2.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo2.jpg" /> </a> <a href="examples/gallery/img/big_photo3.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo3.jpg" /> </a> </div>
а на выходе получим
<div id="gal" class="xdgallery"> <div class="prev"></div><!--кнопка прокрутки назад--> <div class="next"></div><!--кнопка прокрутки вперед--> <div class="carusel"> <a href="examples/gallery/img/big_photo1.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo1.jpg" /> </a> <a href="examples/gallery/img/big_photo2.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo2.jpg" /> </a> <a href="examples/gallery/img/big_photo3.jpg" rel="lightbox-cats" target="_blanc"> <img src="examples/gallery/img/photo3.jpg" /> </a> <div style="clear:both"></div> </div> </div>
Для этого напишем функцию инициализации
var init = function(obj){ var $gallery ={}; $gallery._oldgallery = $(obj);// запоминаем нашу галерею if( !$gallery._oldgallery.children().length )return false; $gallery._itemTag = $gallery._oldgallery.children()[0].tagName; $gallery._oldgallery.children().addClass( options.itemClass );// помечаем все элементы галлереи // оборачиваем галерею в бокс $gallery._gallery = $('<div class="xdgallery '+(options.horizontal?'horizontal':'vertical')+'">'+ '<div class="prev"></div><!--кнопка прокрутки назад-->'+ '<div class="next"></div><!--кнопка прокрутки вперед-->'+ '<div class="navigation"></div><!--навигация-->'+ '<div class="carusel">'+$gallery._oldgallery.html()+ '<div style="clear:both"></div>'+ '</div>'+ '</div>'); $gallery._oldgallery.replaceWith($gallery._gallery); $gallery._carusel = $gallery._gallery.find('.carusel'); // находим в ней карусель $gallery._items = $gallery._carusel.find( this._itemTag ); // находим все элементы карусели this._galleryWidth = this._gallery[extent]() // ширина(высота) галлереи // при необходимости добавляем навигацию } /*******************************/ // далее код по навешиванию событий, который мы дополним позже }
Функция init вызывается для каждой найденной галереи. Весь фокус в том, что мы создаем новую DOM структуру this._gallery = $('<div... а затем методом replaceWith заменяем исходные items на обернутые. Далее работа уже ведется с this._gallery. После оборачивания, находим у галереи карусель this._carusel - это движущийся элемент, самая главная рабочая лошадка галереи. А также определяем ширину(высоту) получившейся галереи. В этом нам поможет методы $().width() и $().height(). А вот какой из них вызывать подскажет переменная extent. Ее мы определим там, где мы объединяли наши настройки:
var options = $.extend({}, defaultOptions, galleryOptions); var hor = options.horizontal = Boolean(options.horizontal) ; var extent = hor?'width':'height'; // в зависимости от ориентации, нужно замерять либо ширину либо высоту var axis = hor?'marginLeft':'marginTop'; // в зависимости от ориентации, нужно отсчитывать отступ либо сверху либо слева var clientAxis = hor?'clientX':'clientY'; // в зависимости от ориентации, нужно отсчитывать отступ либо сверху либо слева
Работает это очень просто. Любой метод в JS можно вызывать, либо непосредственно, к примеру $('a').attr('href','http://xdan.ru'), либо как элемент массива $('a')['attr']('href','http://xdan.ru'). Это не касается только jQuery, к примеру вызов сообщения window['alert']('Hello world');
Кстати очень неплохой метод для обфускации кода. Вызывая так методы Вы очень осложните жизнь тому, кто без спроса попытается разобрать Ваш код на кирпичики.
Далее на управляющие элементы нашей галерейки нужно повесить события. К примеру на кнопки вперед, назад и на прокрутку мыши. Так как эти события уникальны для каждой галереи, то и прописывать их нужно в той же функции инициализации init
$gallery._gallery.find('div.next,div.prev').hover(function(){ $(this).stop().animate({'opacity':'0.8'},400); if( options.onHoverNext ){ step((this.className=='next'?1:-1)*options.invertNext,options.shag*1000,options.speed*20,options.easingNext,$gallery); } },function(){ $(this).stop().animate({'opacity':'0.5'},400); if( options.onHoverNext ){ $gallery._carusel.stop(); } }).click(function(event){ var evnt = event || window.event; step((this.className=='next'?1:-1)*options.invertNext,options.shag,options.speed,options.easingNext,$gallery); evnt.stopPropagation&&evnt.stopPropagation(); // если поддерживается, выполняем evnt.preventDefault&&evnt.preventDefault(); }).dblclick(function(event){ var evnt = event || window.event; evnt.stopPropagation&&evnt.stopPropagation(); // если поддерживается, выполняем evnt.preventDefault&&evnt.preventDefault(); }); if( $gallery._gallery.mousewheel ){ $gallery._gallery.mousewheel(function(event,delta){ var evnt = event || window.event; if( step(-delta*options.invertWheel,options.shag,options.speed,options.easingWheel,$gallery) ){ evnt.stopPropagation&&evnt.stopPropagation(); // если поддерживается, выполняем evnt.preventDefault&&evnt.preventDefault(); } }) }
чтобы понять, как это все работает и для чего написано, снова посылаю Вас на мой мастер класс. Замечу лишь ряд моментов.
При включенной опции options.onHoverNext, при наведении мыши на элементы управления next и prev, должен срабатывать механизм прокрутки.
Присутствие this._gallery.mousewheel проверяется для того, чтобы узнать подключен ли плагин jquery.mousewheel.js к странице.
Чтобы не срабатывали стандартные события items'ов(к примеру для тегов <a> это будет переход по ссылке) при клике на next и prev вызываются методы evnt.stopPropagation(); и evnt.preventDefault(); подробнее о этих методах можете почитать тут
Ну и наконец за всю анимацию карусели, отвечает функция step, которая не является уникальной для каждой галереи, поэтому ее мы опишем позже, не в функции init. На вход она получает направление движения, шаг, скорость, характер движения и саму галерею.
Сначала напишу зачем подавать на вход шаг и скорость, учитывая, что объект options будет глобальным и для step. Дело в том, что мы будем использовать step для эффекта инерции при перетаскивании мышкой, и там шаг прокрутки и скорость будут выбираться в зависимости от скорости мышки в момент отпускания.
Подавать саму галерею на вход необходимо потому что, функция step будет одна для всех найденных на странице галерей. Поэтому ей нужно знать с какой галереей она работает в данный момент.
Drag&Drop и эффект инерции
Туда же, в init нужно добавить еще несколько событий. Для перетаскивания применяются методы mousedown, mousemove и mouseup( нажатие кнопки мыши, ее перетаскивание и отпускание)
var oldx =0 ,oldy = 0; var startDrag = false; var oldMargin = 0; $gallery._carusel.mousedown(function(event){ var evnt = event || window.event; if (evnt.preventDefault) evnt.preventDefault() }).click(function(){ return false; }) var oldmovex = 1; var oldmovetime = 1; var carWidth = 0; var curSpeedObj = {'x':0,'t':0}; var measureSpeed = function( clientX ){ var curtime = new Date().getTime(); curSpeedObj.x = Math.abs(clientX-oldmovex); curSpeedObj.t = curtime-oldmovetime; oldmovex = clientX; oldmovetime = curtime; } $gallery._carusel.mousedown(function(event){ var evnt = event || window.event; oldx = evnt.clientX; carWidth = caruselWidth($gallery); oldMargin = parseInt( $gallery._carusel.css(axis) ) startDrag = true; }) $( window ).mousemove(function(event){ if( startDrag ){ var evnt = event || window.event; measureSpeed(evnt[clientAxis]); var shg = evnt[clientAxis]-oldx; naviStep(Math.abs(Math.abs(oldMargin) - shg ),carWidth - $gallery._galleryWidth,$gallery); if( Math.abs(oldMargin) - shg <= carWidth - $gallery._galleryWidth && Math.abs(oldMargin) - shg >= 0) $gallery._carusel.css(axis,oldMargin+shg); else{ if(Math.abs(oldMargin) - shg<=0)$gallery._carusel.css(axis,'0px');else $gallery._carusel.css(axis,'-'+(carWidth-$gallery._galleryWidth)+'px'); } } }).mouseup(function(event){ if(startDrag){ var evnt = event || window.event; startDrag = false; if( curSpeedObj.x/curSpeedObj.t > 0.5 ){ step(-(evnt[clientAxis]-oldx),curSpeedObj.x*20,curSpeedObj.t*20,options.easingDrag,$gallery); curSpeedObj.x = curSpeedObj.t = 0; } } });
Подробно останавливаться здесь на том, как это работает я так же не буду. Вы можете это узнать из мастер класса. Однако, расскажу про эффект инерции.
Дело в том, что метод mousemove вызывается не постоянно, а лишь через некоторые промежутки времени. Т.е. можно реально измерить сколько времени прошло, когда мышь прошла на n пиксел. За измерение скорости отвечает функция measureSpeed, точнее она не измеряет скорость, а лишь запоминает приращение времени и расстояния. Затем, когда кнопка мыши отпускается, проверяем больше ли скорость некой константы curSpeedObj.x/curSpeedObj.t > 0.5. Если больше то прокручиваем галерею, функцией step на шаг curSpeedObj.x*20 со скоростью curSpeedObj.t*20, при этом характер движения должен быть затухающим. ( 0.5 и 20- это числа выбранные эмпирически, возможно их надо вынести в настройки)
С инициализацией покончено, теперь наши галерейки живут своей жизнью, а для этого им нужны еще несколько функций.
step
Функция step, является лошадкой, которая двигает нашу тележку(карусель). Ее нужно добавить там же, где была прописана функция init.
// основная функция прокрутки, работает для прокрутки next и prev, а также для mousewheel var step = function(direct,shag,speed,easing,$gallery){ var marginLeft = parseInt( $gallery._carusel.css( axis ) );// текущее положение карусели var wdth = caruselWidth($gallery) - $gallery._galleryWidth; // максимальная прокрутка влево // срабатывает после окончания любой прокрутки, просто запускает функцию naviStep var callback = function(){ naviStep(Math.abs(parseInt( $gallery._carusel.css( axis ) )),wdth,$gallery); } ; if( direct > 0 ){ // прокрутка влево // если текущая прокрутка + shag не превышает максимально допустимую if( wdth >= Math.abs(marginLeft)+shag ){ // то просто прокручиваем на shag $gallery._carusel.stop().animate( margin('-='+shag+'px') ,speed,easing,callback) // иначе докручиваем до края, и все }else{ if(wdth == Math.abs(marginLeft))return false; $gallery._carusel.stop().animate( margin('-'+wdth+'px') ,speed,easing,callback); } }else{ // аналогично для прокрутки вправо, но тут крутим до нуля if( 0 <= Math.abs(marginLeft)- shag ){ $gallery._carusel.stop().animate( margin('+='+ shag+'px'),speed,easing,callback) }else{ if( 0 == Math.abs(marginLeft) )return false; $gallery._carusel.stop().animate( margin('0px'),speed,easing,callback); } } return true; }
Про функцию naviStep я пока рассказывать не буду. А про детали работы функции step, вы уже наверно прочитали в мастер классе. Расскажу лишь про функцию margin.
Дело в том, что плагин animate, первым своим параметром получает объект, с настройками, в каком направлении ему двигать карусель. Для горизонтальной галереи это должно быть css свойство margin-left, для вертикальной margin-top (точнее их js аналоги marginLeft и marginTop), но в js нельзя создавать объект, с переменными полями, т.е. вот так
var a = {'a':111} // можно //но var b = 'a'; var c = {b:111} // нельзя
поэтому была написана нехитрая функция margin, которую добавим до step
// утилита возвращающая объект вида {marginTop:st} var margin = function(st){ var a = {}; a[axis] = st; return a; };
где axis это либо marginTop либо marginLeft. Все просто.
Еще одна утилитная функция caruselWidth - функция для измерения длины(высоты) карусели. Вы спросите меня почему нельзя сделать так this._carusel.width()?! Отвечу. Можно! Но, работает это не во всех браузерах, к примеру Хром вернет длину галереи = 0, и будет возвращать разные данные по мере загрузки всех items'ов. Поэтому придется считать длину галереи при каждом вызове step. Не волнуйтесь, это не затратная операция.
// функция для измерения длины(высоты) галереи var caruselWidth = function( $gallery ){ var w = 0; $gallery._items.each(function(){ w+=$(this)[extent]()+5; }) return w; };
Функция naviStep переключает текущую кнопку навигации, и вызывается каждый раз при окончании движения карусели. Приводить ее здесь я не буду. Предлагаю Вам самим ознакомится с ней изучив исходники.
Как пользоваться
Для того чтобы плагин работал к проекту нужно подключить следующие файлы
<script type="text/javascript" src="js/jquery.mousewheel.min.js"></script> <script type="text/javascript" src="js/jquery.xdGallery.js"></script> <link rel="stylesheet" href="js/xdGallery/jquery.xdGallery.css" type="text/css" /
Сами items'ы подаются в произвольном виде, главное они должны быть в неком боксе
<div id="myGallery"> <img alt="xdGallery на jQuery" src="tmp/images/jquery/xdGallery/photo1.jpeg" style="width: 300px; height: 199px" /> <img alt="xdGallery на jQuery" src="tmp/images/jquery/xdGallery/photo2.jpeg" style="width: 300px; height: 199px" /> <img alt="xdGallery на jQuery" src="tmp/images/jquery/xdGallery/photo3.jpeg" style="width: 300px; height: 199px" /> </div>
ну и непосредственно вызов плагина нужно делать в событии onDomReady
$(function(){ $('#myGallery').xdGallery(); });
Теперь посмотрим, что у нас получилось
и вертикальный вариант
Всего Вам самого наилучшего!!! =)
Комментарии
В событии mousedown замените в строке 23
oldx = evnt.clientX;
на
oldx = evnt[clientAxis];
И тогда при вертикальном расположении галереи при перетаскивании мышкой не будет этих скачков (как у вас в примере).
Потому что сейчас oldx берется из положения курсора по оси X на странице, а будет, как и надо, по оси Y.
А вообще статья классная, помогла
Спасибо!