Материал для начинающих.
Наверное, каждый начинающий web-разработчик должен написать кривую, с кучей костылей, но свою карусель. Вот и настало мое время.
Карусель — элемент web-интерфейса, который поочередно показывает пользователю заранее подготовленные слайды с информацией.
Идея нашей карусели: красиво выстроить карточки на экране в два уровня при помощи css свойства z-index и поочередно менять css положение карточек с анимацией изменений при помощи свойства transition.
CSS файл будет выглядеть следующим образом:
el-carousel { width: 100%; margin: 0; position: relative; z-index: 20; } .el-carousel .el-card { position: absolute; background: rgba(141, 141, 141, 0.5); border:1px #e0e0e0 solid; border-radius:1px; box-shadow: 0 0 0 4px rgba(107, 108, 40, 0.25), 0 0 0 5px rgba(183, 183, 183, 0.6); -webkit-transition: all 1s ease-in-out; -moz-transition: all 1s ease-in-out; -o-transition: all 1s ease-in-out; transition: all 1s ease-in-out; z-index: 5; opacity: 0.2; cursor: pointer; } .el-carousel .sm-el-card-1 { height: 65%; width: 23%; left: 6%; bottom: 1%; z-index: 20; } .el-carousel .sm-el-card-2 { height: 70%; width: 25%; left: 2%; bottom: 7%; opacity: 0; } .el-carousel .sm-el-card-3 { height: 70%; width: 25%; left: 2%; bottom: 7%; z-index: 20; opacity: 0.8; } .el-carousel .sm-el-card-4 { height: 84%; width: 30%; left: 35%; bottom: 14%; z-index: 20; opacity: 1; background: rgba(141, 141, 141, 0.7); } .el-carousel .sm-el-card-5 { height: 70%; width: 25%; left: 73%; bottom: 7%; z-index: 20; opacity: 0.8; } .el-carousel .sm-el-card-6 { height: 70%; width: 25%; left: 73%; bottom: 7%; opacity: 0; } .el-carousel .sm-el-card-7 { height: 65%; width: 23%; left: 70.5%; bottom: 1%; } .el-carousel .sm-el-card-8 { height: 77%; width: 27%; left: 33%; bottom: 8%; } .el-carousel .sm-el-card-hide { height: 77%; width: 27%; left: 33%; bottom: 8%; opacity: 0; } .el-carousel .md-el-card-4 { height: 57%; width: 14%; left: 2%; bottom: 7%; z-index: 20; opacity: 0.6; } .el-carousel .md-el-card-5 { height: 71%; width: 16%; left: 20.5%; bottom: 11%; z-index: 20; opacity: 0.8; } .el-carousel .md-el-card-6 { height: 84%; width: 19%; left: 40.5%; bottom: 14%; z-index: 20; opacity: 1; background: rgba(141, 141, 141, 0.7); } .el-carousel .md-el-card-7 { height: 71%; width: 16%; left: 63.75%; bottom: 11%; z-index: 20; opacity: 0.8; } .el-carousel .md-el-card-8 { height: 57%; width: 14%; left: 84%; bottom: 7%; z-index: 20; opacity: 0.6; } .el-carousel .md-el-card-9 { height: 57%; width: 14%; left: 84%; bottom: 7%; opacity: 0; } .el-carousel .md-el-card-10 { height: 67%; width: 15%; left: 62%; bottom: 7%; } .el-carousel .md-el-card-1 { height: 78%; width: 18%; left: 38.7%; bottom: 9.5%; } .el-carousel .md-el-card-2 { height: 67%; width: 15%; left: 18.7%; bottom: 7%; } .el-carousel .md-el-card-3 { height: 57%; width: 14%; left: 2%; bottom: 7%; opacity: 0; } .el-carousel .md-el-card-hide { height: 78%; width: 18%; left: 38.7%; bottom: 9.5%; opacity: 0; } .el-carousel .lg-el-card-1 { height: 78.7%; width: 15%; left: 40.5%; bottom: 10%; } .el-carousel .lg-el-card-2 { height: 65%; width: 13%; left: 23.5%; bottom: 7%; } .el-carousel .lg-el-card-3 { height: 52%; width: 11%; left: 9%; bottom: 3%; } .el-carousel .lg-el-card-4 { height: 33%; width: 7%; left: 1%; bottom: 4%; z-index: 10; opacity: 0.4; } .el-carousel .lg-el-card-5 { height: 57%; width: 12%; left: 10%; bottom: 7%; z-index: 20; opacity: 0.6; } .el-carousel .lg-el-card-6 { height: 71%; width: 14%; left: 25%; bottom: 11%; z-index: 20; opacity: 0.8; } .el-carousel .lg-el-card-7 { height: 84%; width: 16%; left: 42%; bottom: 14%; z-index: 20; opacity: 1; background: rgba(141, 141, 141, 0.7); } .el-carousel .lg-el-card-8 { height: 71%; width: 14%; left: 61%; bottom: 11%; z-index: 20; opacity: 0.8; } .el-carousel .lg-el-card-9 { height: 57%; width: 12%; left: 78%; bottom: 7%; z-index: 20; opacity: 0.6; } .el-carousel .lg-el-card-10 { height: 33%; width: 7%; left: 91.32%; bottom: 4%; z-index: 10; opacity: 0.4; } .el-carousel .lg-el-card-11 { height: 52%; width: 11%; left: 77%; bottom: 3%; } .el-carousel .lg-el-card-12 { height: 65%; width: 13%; left: 59.5%; bottom: 7%; } .el-carousel .lg-el-card-hide { height: 78.7%; width: 15%; left: 40.5%; bottom: 10%; opacity: 0; }
Так как карусель адаптивная — все параметры размеров и положения определяются в процентах.
Итак, попробуем реализовать нашу карусель angular-way. Чтобы каждая карточка карусели имела уникальный шаблон и действие, воспользуемся директивой и фабрикой.
Рассмотрим фабрику
В замыкании будем хранить два массива:
- list — массив html-шаблонов карточек,
- action — массив действий при клике на карточку.
Фабрика предоставляет два метода addCard и addAction, которые добавляют в массивы html-шаблон и функцию, которая должна выполниться при клике соответственно. Функция addCard принимает на вход второй, необязательный параметр, который при логическом значении true разрешает промис. Это своего рода костыль, о котором я расскажу немного позже.
Отдельно отмечу безопасное подключение зависимостей. Не углубляясь в детали, имена подключаемых зависимостей менять нельзя, так как это полностью ломает код при его минификации. Поэтому в ангуляре предусмотрено безопасное подключение зависимостей — вместо функции фабрики/контроллера/директивы передается массив, первыми элементами которого указываются строковые имена зависимостей, а последним уже сама функция с подключенными зависимостями. В данном случае минификация кода нам уже не страшна, однако нужно не забывать, что порядок перечисления строковых зависимостей в массиве должен совпадать с порядком подключения зависимостей в функцию.
'use strict'; (function () { angular.module('carousel') .factory('card', ['$q', function ($q) { var list = [], action = [], done = $q.defer(); // добавляем новую карточку function addCard(card, last) { if(typeof card === 'object' && card.length > 0) { list.push(card); if(last) { done.resolve(); } } } // добавляем действие при клике на карточку function addAction(foo) { action.push(foo); } // возвращаем методы наполнения и массивы карточек, и действий return { addCard: addCard, addAction: addAction, list: list, action: action, done: done.promise }; }]); })();
Директива html-шаблона карточек
Тут все просто. Для директивы установлен изолируемый скоуп с тремя пробрасываемыми параметрами через атрибуты элемента, на который установлена директива.
Отмечу, что для удобства в директивах можно изменять имена пробрасываемых параметров. Ключом в объекте скоупа указывается удобное для использования имя, а в его поле, после указания типа пробрасывания (= ,@, &) — имя атрибута элемента. Если атрибут элемента двойной, например last-card, то в имени элемента он указывается в виде верблюжьей нотации lastCard.
Параметр action пробрасывается через & — это означает, что на вход принимается выражение, в нашем случае — функция клика по карточке.
Параметры item и last пробрасываются через =, т.е. на вход принимается определенное значение, которое может быть задано как явно в значении атрибута, так и определено переменной скоупа, в котором находится директива, и передано в атрибут элемента переменной.
Данные параметры необходимы в том случае, когда мы не хотим создавать отдельный шаблон для каждой карточки, а делаем один шаблон и клонируем его с помощью директивы ng-repeat. В параметр item передается текущий объект данных, полученный при помощи ng-repeat. В параметр last передается специальное значение $last, которому директива ng-repeat присвоит true, если объект из рассматриваемого массива для клонирования — последний.
Для получения html-шаблона (фактически внутреннего содержимого директивы) необходимо установить параметру transclude значение true. При этом в связывающей функции директивы (link) появится пятый параметр, который представляет из себя функцию трансклюзии. Отмечу, что назначение данной функции более широкое, но в данном случае она используется только для получения внутреннего содержимого директивы.
Настало время обосновать применение костыля с явным определением последнего элемента клонирования $last. Дело в том, что пользовательская директива с transclude в паре с ng-repeat работает специфически. Это связано с последовательностью выполнения операций: первым делом angular клонирует шаблон элемента, затем выполняются остальные директивы с меньшим приоритетом, в том числе пользовательские, и только после этого клонированные шаблоны наполняются значениями из скоупа. Поэтому если явно не указать, что ng-repeat сделал свое дело — карусель не будет отображать карточки с содержимым клонированных элементов.
'use strict'; (function () { angular.module('carousel') .directive('elCard', ['card', function(card) { return { scope: { action: '&cardAction', item: '=elCard', last: '=lastCard' }, restrict: 'A', transclude: true, link: function(scope, elem, attr, ctrl, transclude) { // наполняем массив действий при клике на соответствующую карточку card.addAction(scope.action); // наполняем массив элементов карточек transclude(scope, function(item) { card.addCard(item, scope.last); elem.remove(); }); } }; }]); })();
Директива карусели
Директива строит нашу карусель по имеющимся шаблонам. Карусель имеет три варианта исполнения: на 3, 5 или 7 карт в первом ряду. По умолчанию выбрано 7 карточек, но предусмотрен пробрасываемый параметр elements для определения количества карт вручную. В зависимости от количества карточек определяется коэффициент пересчета высоты элемента карусели heihtCoeff.
Директива построена на трех функциях:
- changeHeight — пересчитывает высоту карусели в зависимости от ширины экрана. Данная функция выполняется не только при первом старте директивы, но и при срабатывании события изменения размера окна браузера.
- makeCards — создает необходимое количество элементов карточек. У данной функции есть два нюанса:
- количество пользовательских карточек меньше требуемого для карусели. В данном случае недостающие карточки наполняются повторяющимися имеющимися шаблонами.
- количество пользовательских карточек больше требуемого. Лишним картам присваивается положение на заднем фоне карусели и устанавливается абсолютная прозрачность. По мере движения карусели эти карточки меняются местами с уже показанными и так по кругу.
- moveCards — каждой карточке присвоена директива ng-class, которая присваивает элементу имя класса из строкового значения переменной. В нашем случае все классы, определяющие местоположение карточек, занесены в массив строковых элементов и функция всего лишь реализует продвижение «очереди» при помощи методов массива shift и push.
Для приведения в действие и остановки предусмотрены вспомогательные функции runCarousel и stopCarousel, которые периодически выполняют moveCards при помощи $interval. При переходе на другую вкладку $interval продолжает свою работу, а css свойство transition — нет, что приводит к сбоям в работе карусели. Поэтому старт и остановка карусели привязаны к событиям смены активного окна. По окончании работы директивы не забываем отвязать всех слушателей.
'use strict'; (function () { angular.module('carousel') .directive('elCarousel', ['$window', '$compile', '$interval', 'card', function($window, $compile, $interval, card) { return { scope: { elements: '=elCarousel' }, restrict: 'A', link: function($scope, elem) { var cards = [], action = [], heightCoeff = ($scope.elements === 3) ? 2 : ($scope.elements === 5) ? 3.3 : 3.96, cardAmount = ($scope.elements === 3) ? 8 : ($scope.elements === 5) ? 10 : 12; $scope.card = []; // присваиваем элементу необходимый для работы класс elem.addClass('el-carousel'); // выполняем подготовительные действия для обычных карточек и созданных при помощи ng-repeat function preStartActions() { if(card.list.length < 1) { return; } cards = card.list; action = card.action; makeCards(); } preStartActions(); card.done.then(preStartActions); // изменяем высоту элемента в зависимости от ширины function changeHeight() { var carouselWidth = elem.width(), carouselHeight = carouselWidth / heightCoeff; elem.css('height', carouselHeight); } angular.element($window).bind('resize', changeHeight); changeHeight(); // создаем DOM элементы карточек из массива function makeCards() { elem.empty(); var k = 0, cardNumber = (cards.length > cardAmount) ? cards.length : cardAmount, numClass = (cardAmount === 8) ? 'sm-' : (cardAmount === 10) ? 'md-' : 'lg-'; for(var i = 0; i < cardNumber; i++) { var div = angular.element('<div ng-click="cardAction' + i + '()" class="el-card" ng-class="card[' + i + ']"></div>'); if(i < cards.length) { div.append(cards[i].clone()); $scope['cardAction' + i] = action[i]; } else { div.append(cards[k].clone()); $scope['cardAction' + i] = action[k]; k = (k > cards.length - 2) ? 0 : k + 1; } $scope.card[i] = (i < cardAmount) ? numClass + 'el-card-' + (i + 1) : numClass + 'el-card-hide'; $compile(div)($scope); elem.append(div); } } // перемещаем карточки в порядке очереди function moveCards() { var lastElem = $scope.card.shift(); $scope.card.push(lastElem); } // старт/стоп карусели в зависимости от активности окна var moveInterval; runCarousel(); angular.element($window).bind('blur', stopCarousel); function stopCarousel() { $interval.cancel(moveInterval); } angular.element($window).bind('focus', runCarousel); function runCarousel() { moveInterval = $interval(moveCards, 2000); } elem.bind('$destroy', function () { $interval.cancel(moveInterval); angular.element($window).unbind('blur', stopCarousel); angular.element($window).unbind('focus', runCarousel); angular.element($window).unbind('resize', changeHeight); }); } }; }]); })();
В результатате наш html код карусели может выглядеть следующим образом
<div el-carousel="7"> <div ng-repeat="card in cardList" el-card="card" card-action="someAction(someParam)" last-card="$last"> <span>{{card.name}}</span> <img class="image image-1" ng-src={{card.img}} alt={{card.alt}}/> </div> <div el-card card-action="otherAction(otherParam)"> <img class="image image-2" src="/app/ru/main/img/js.png" alt="javascript"/> </div> </div>
Можно использовать как индивидуально написанные шаблоны, так и клонированные с помощью ng-repeat, а также комбинировать их вместе.
Спасибо за внимание, всем удачи.
ссылка на оригинал статьи http://habrahabr.ru/post/261847/
Добавить комментарий