├── .gitignore
├── LICENSE
├── README.md
├── bower.json
├── dist
├── ion-gallery.css
├── ion-gallery.js
└── ion-gallery.min.js
├── gulpfile.js
├── package.json
└── src
├── js
├── gallery.js
├── galleryConfig.js
├── galleryHelper.js
├── imageScale.js
├── rowHeight.js
├── slideAction.js
├── slider.js
└── sliderHelper.js
├── scss
└── ion-gallery.scss
└── templates
├── gallery.html
└── slider.html
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | bower_components/
4 | tests/
5 | .tmp/
6 | .idea
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Pedro Abreu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ion-gallery
2 | Ionic gallery with slider
3 |
4 | Demo availabe in Ionic View with id 150745FE
5 |
6 | $ bower install --save ion-gallery
7 |
8 | # Features
9 |
10 | - Define number of collums to present (1 to array length)
11 | - Pinch and double tap do zoom on picture
12 |
13 | # Usage
14 |
15 | Load script and css on the html
16 |
17 |
18 | ...
19 |
20 |
21 | Add ion-gallery as dependency to your project
22 |
23 | angular
24 | .module('starter', ['ionic','ion-gallery'])
25 |
26 | Add gallery directive with array of photos:
27 |
28 |
29 |
30 | Data source example
31 |
32 | $scope.items = [
33 | {
34 | src:'http://www.wired.com/images_blogs/rawfile/2013/11/offset_WaterHouseMarineImages_62652-2-660x440.jpg',
35 | sub: 'This is a subtitle'
36 | },
37 | {
38 | src:'http://www.gettyimages.co.uk/CMS/StaticContent/1391099215267_hero2.jpg',
39 | sub: '' /* Not showed */
40 | },
41 | {
42 | src:'http://www.hdwallpapersimages.com/wp-content/uploads/2014/01/Winter-Tiger-Wild-Cat-Images.jpg',
43 | thumb:'http://www.gettyimages.co.uk/CMS/StaticContent/1391099215267_hero2.jpg'
44 | }
45 | ]
46 |
47 | Thumbnail property is also optional. If no thumbnail, the source content will be used
48 |
49 | Subtitle property is optional. If no property present, nothing is showed (Same for empty string).
50 | Supports html tags.
51 |
52 | UI will reflect changes on the content object passed to the directive. Example of adding and removing pictures can be seen in the ionic view app.
53 |
54 | # Config
55 |
56 | - Via provider:
57 |
58 | Default values in example.
59 |
60 | ```
61 | app.config(function(ionGalleryConfigProvider) {
62 | ionGalleryConfigProvider.setGalleryConfig({
63 | action_label: 'Close',
64 | template_gallery: 'gallery.html',
65 | template_slider: 'slider.html',
66 | toggle: false,
67 | row_size: 3,
68 | fixed_row_size: true
69 | });
70 | });
71 | ```
72 |
73 | ```
74 | Default values
75 | action_label - 'Close' (String)
76 | template_gallery - 'gallery.html' (String)
77 | template_slider - 'slider.html' (String)
78 | toggle - false (Boolean)
79 | row_size - 3 (Int)
80 | fixed_row_size - true (boolean). If true, thumbnails in gallery will always be sized as if there are "row_size" number of images in a row (even if there aren't). If set to false, the row_size will be dynamic until it reaches the set row_size (Ex: if only 1 image it will be rendered in the entire row, if 2 images, both will be rendered in the entire row)
81 | zoom_events - true (Boolean)
82 | ```
83 |
84 | - Via markup:
85 |
86 | Markup overrides provider definitions
87 |
88 | - ion-gallery-row: Defines size of the row. Default to 3 images per row
89 |
90 |
91 |
92 | - ion-gallery-toggle: Sets one tap action on slideshow to hide/show subtitles and "Done" button. Default: true
93 |
94 |
95 |
96 | - ion-item-action: Overrides the default action when a gallery item is tapped. Default: opens the slider modal
97 |
98 |
99 |
100 | - ion-zoom-events: Enable/Disable all zoom events in slider (pinchToZoom, tap and double tap). Default: true
101 |
102 |
103 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ion-gallery",
3 | "version": "0.2",
4 | "description": "Ionic gallery directive",
5 | "main": "dist/ion-gallery.min.js",
6 | "keywords": [
7 | "ionic",
8 | "gallery",
9 | "slider"
10 | ],
11 | "authors": [
12 | "Pedro Abreu "
13 | ],
14 | "license": "MIT",
15 | "ignore": [
16 | "**/.*",
17 | "node_modules",
18 | "bower_components",
19 | "test",
20 | "tests"
21 | ],
22 | "devDependencies": {
23 | "angular-mocks": "~1.4.0",
24 | "ionic": "~1.1.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/dist/ion-gallery.css:
--------------------------------------------------------------------------------
1 | .gallery-view .image-container{position:relative;overflow:hidden;border:2px solid white}.gallery-view .image-container img{position:absolute;top:-9999px;bottom:-9999px;left:-9999px;right:-9999px;margin:auto}.imageView .has-no-header{top:0px !important}.imageView .close-btn{font-weight:900;border:2px solid;position:absolute;right:5px;border-radius:5px}.imageView .headerView{background-image:none;background-color:black}.imageView .gallery-slide-view{width:98%;background-color:transparent}.imageView .image-subtitle{color:white;position:absolute;bottom:0px;left:10px;width:95%;height:15%;z-index:100}.imageView .listContainer{width:100%;height:100%;background-color:black}.imageView .hideAll{display:none}.imageView img{display:block;width:100%;height:auto}.imageView .scroll-view{position:absolute;width:100%;height:100%}.imageView .scroll-view .scroll{min-height:100%;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-box-direction:normal;-moz-box-direction:normal;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:center;-moz-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:stretch;-ms-flex-line-pack:stretch;align-content:stretch;-webkit-box-align:center;-moz-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}
2 |
--------------------------------------------------------------------------------
/dist/ion-gallery.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery', ['templates'])
6 | .directive('ionGallery', ionGallery);
7 |
8 | ionGallery.$inject = ['$ionicPlatform', 'ionGalleryHelper', 'ionGalleryConfig'];
9 |
10 | function ionGallery($ionicPlatform, ionGalleryHelper, ionGalleryConfig) {
11 | controller.$inject = ["$scope"];
12 | return {
13 | restrict: 'AE',
14 | scope: {
15 | ionGalleryItems: '=ionGalleryItems',
16 | ionGalleryRowSize: '=?ionGalleryRow',
17 | ionItemAction: '&?ionItemAction',
18 | ionZoomEvents: '=?ionZoomEvents'
19 | },
20 | controller: controller,
21 | link: link,
22 | replace: true,
23 | templateUrl: ionGalleryConfig.template_gallery
24 | };
25 |
26 | function controller($scope) {
27 | var _rowSize = parseInt($scope.ionGalleryRowSize);
28 |
29 | var _drawGallery = function () {
30 | $scope.ionGalleryRowSize = ionGalleryHelper.getRowSize(_rowSize || ionGalleryConfig.row_size, $scope.ionGalleryItems.length);
31 | $scope.actionLabel = ionGalleryConfig.action_label;
32 | $scope.items = ionGalleryHelper.buildGallery($scope.ionGalleryItems, $scope.ionGalleryRowSize);
33 | $scope.responsiveGrid = parseInt((1 / $scope.ionGalleryRowSize) * 100);
34 | };
35 |
36 | _drawGallery();
37 |
38 | (function () {
39 | $scope.$watch(function () {
40 | return $scope.ionGalleryItems.length;
41 | }, function (newVal, oldVal) {
42 | if (newVal !== oldVal) {
43 | _drawGallery();
44 | }
45 | });
46 | }());
47 |
48 | }
49 |
50 | function link(scope, element, attrs) {
51 | scope.customItemAction = angular.isFunction(scope.ionItemAction) && attrs.hasOwnProperty('ionItemAction');
52 | scope.ionSliderToggle = attrs.ionGalleryToggle === 'false' ? false : ionGalleryConfig.toggle;
53 | }
54 | }
55 | })();
56 |
57 | (function(){
58 | 'use strict';
59 |
60 | angular
61 | .module('ion-gallery')
62 | .provider('ionGalleryConfig',ionGalleryConfig);
63 |
64 | ionGalleryConfig.$inject = [];
65 |
66 | function ionGalleryConfig(){
67 | this.config = {
68 | action_label: 'Done',
69 | template_gallery: 'gallery.html',
70 | template_slider: 'slider.html',
71 | toggle: true,
72 | row_size: 3,
73 | fixed_row_size: true,
74 | zoom_events: true
75 | };
76 |
77 | this.$get = function() {
78 | return this.config;
79 | };
80 |
81 | this.setGalleryConfig = function(config) {
82 | angular.extend(this.config, this.config, config);
83 | };
84 | }
85 |
86 | })();
87 |
88 | (function(){
89 | 'use strict';
90 |
91 | angular
92 | .module('ion-gallery')
93 | .service('ionGalleryHelper',ionGalleryHelper);
94 |
95 | ionGalleryHelper.$inject = ['ionGalleryConfig'];
96 |
97 | function ionGalleryHelper(ionGalleryConfig) {
98 |
99 | this.getRowSize = function(size,length){
100 | var rowSize;
101 |
102 | if(isNaN(size) === true || size <= 0){
103 | rowSize = ionGalleryConfig.row_size;
104 | }
105 | else if(size > length && !ionGalleryConfig.fixed_row_size){
106 | rowSize = length;
107 | }
108 | else{
109 | rowSize = size;
110 | }
111 |
112 | return rowSize;
113 |
114 | };
115 |
116 | this.buildGallery = function(items,rowSize){
117 | var _gallery = [];
118 | var row = -1;
119 | var col = 0;
120 |
121 | for(var i=0;i0){
160 | if(context.naturalHeight >= context.naturalWidth){
161 | element.attr('width','100%');
162 | }
163 | else{
164 | element.attr('height',element.parent()[0].offsetHeight+'px');
165 | }
166 | }
167 | };
168 |
169 | element.bind("load" , function(e){
170 | var _this = this;
171 | if(element.parent()[0].offsetHeight > 0){
172 | scaleImage(this,element.parent()[0].offsetHeight);
173 | }
174 |
175 | scope.$watch(function(){
176 | return element.parent()[0].offsetHeight;
177 | },function(newValue){
178 | scaleImage(_this,newValue);
179 | });
180 | });
181 | }
182 | }
183 | })();
184 | (function(){
185 | 'use strict';
186 |
187 | angular
188 | .module('ion-gallery')
189 | .directive('ionRowHeight',ionRowHeight);
190 |
191 | ionRowHeight.$inject = ['ionGalleryConfig'];
192 |
193 | function ionRowHeight(ionGalleryConfig){
194 |
195 | return {
196 | restrict: 'A',
197 | link : link
198 | };
199 |
200 | function link(scope, element, attrs) {
201 | scope.$watch(
202 | function(){
203 | return scope.ionGalleryRowSize;
204 | },
205 | function(newValue,oldValue){
206 | if(newValue > 0){
207 | element.css('height',element[0].offsetWidth * parseInt(scope.responsiveGrid)/100 + 'px');
208 | }
209 | });
210 | }
211 | }
212 | })();
213 | (function(){
214 | 'use strict';
215 |
216 | angular
217 | .module('ion-gallery')
218 | .directive('ionSlideAction',ionSlideAction);
219 |
220 | ionSlideAction.$inject = ['$ionicGesture','$timeout'];
221 |
222 | function ionSlideAction($ionicGesture, $timeout){
223 |
224 | return {
225 | restrict: 'A',
226 | link : link
227 | };
228 |
229 | function link(scope, element, attrs) {
230 | var isDoubleTapAction = false;
231 |
232 | var pinchZoom = function pinchZoom(){
233 | scope.$emit('ZoomStarted');
234 | };
235 |
236 | var imageDoubleTapGesture = function imageDoubleTapGesture(event) {
237 |
238 | isDoubleTapAction = true;
239 |
240 | $timeout(function(){
241 | isDoubleTapAction = false;
242 | scope.$emit('DoubleTapEvent',{ 'x': event.gesture.touches[0].pageX, 'y': event.gesture.touches[0].pageY});
243 | },200);
244 | };
245 |
246 | var imageTapGesture = function imageTapGesture(event) {
247 |
248 | if(isDoubleTapAction === true){
249 | return;
250 | }
251 | else{
252 | $timeout(function(){
253 | if(isDoubleTapAction === true){
254 | return;
255 | }
256 | else{
257 | scope.$emit('TapEvent');
258 | }
259 | },200);
260 | }
261 | };
262 |
263 | var pinchEvent = $ionicGesture.on('pinch',pinchZoom,element);
264 | var doubleTapEvent = $ionicGesture.on('doubletap', function(e){imageDoubleTapGesture(e);}, element);
265 | var tapEvent = $ionicGesture.on('tap', imageTapGesture, element);
266 |
267 | scope.$on('$destroy', function() {
268 | $ionicGesture.off(doubleTapEvent, 'doubletap', imageDoubleTapGesture);
269 | $ionicGesture.off(tapEvent, 'tap', imageTapGesture);
270 | $ionicGesture.off(pinchEvent, 'pinch', pinchZoom);
271 | });
272 | }
273 | }
274 | })();
275 |
276 | (function(){
277 | 'use strict';
278 |
279 | angular
280 | .module('ion-gallery')
281 | .directive('ionSlider',ionSlider);
282 |
283 | ionSlider.$inject = ['$ionicModal','$timeout','$ionicScrollDelegate','ionSliderHelper','ionGalleryConfig'];
284 |
285 | function ionSlider($ionicModal,$timeout,$ionicScrollDelegate,ionSliderHelper,ionGalleryConfig){
286 |
287 | controller.$inject = ["$scope"];
288 | return {
289 | restrict: 'A',
290 | controller: controller,
291 | link : link
292 | };
293 |
294 | function controller($scope){
295 | var lastSlideIndex;
296 | var currentImage;
297 |
298 | var rowSize = $scope.ionGalleryRowSize;
299 | var zoomStart = false;
300 |
301 | $scope.selectedSlide = 1;
302 | $scope.hideAll = false;
303 | $scope.ionZoomEvents = ionSliderHelper.setZoomEvents($scope.ionZoomEvents)
304 |
305 | $scope.openSlider = function(index) {
306 | $scope.slides = [];
307 | currentImage = index;
308 |
309 | var galleryLength = $scope.ionGalleryItems.length;
310 | var previndex = index - 1 < 0 ? galleryLength - 1 : index - 1;
311 | var nextindex = index + 1 >= galleryLength ? 0 : index + 1;
312 |
313 | $scope.slides[0] = $scope.ionGalleryItems[previndex];
314 | $scope.slides[1] = $scope.ionGalleryItems[index];
315 | $scope.slides[2] = $scope.ionGalleryItems[nextindex];
316 |
317 | lastSlideIndex = 1;
318 | $scope.openModal();
319 | };
320 |
321 | $scope.slideChanged = function(currentSlideIndex) {
322 |
323 | if(currentSlideIndex === lastSlideIndex){
324 | return;
325 | }
326 |
327 | var slideToLoad = $scope.slides.length - lastSlideIndex - currentSlideIndex;
328 | var galleryLength = $scope.ionGalleryItems.length;
329 | var imageToLoad;
330 | var slidePosition = lastSlideIndex + '>' + currentSlideIndex;
331 |
332 | if(slidePosition === '0>1' || slidePosition === '1>2' || slidePosition === '2>0'){
333 | currentImage++;
334 |
335 | if(currentImage >= galleryLength){
336 | currentImage = 0;
337 | }
338 |
339 | imageToLoad = currentImage + 1;
340 |
341 | if( imageToLoad >= galleryLength){
342 | imageToLoad = 0;
343 | }
344 | }
345 | else if(slidePosition === '0>2' || slidePosition === '1>0' || slidePosition === '2>1'){
346 | currentImage--;
347 |
348 | if(currentImage < 0){
349 | currentImage = galleryLength - 1 ;
350 | }
351 |
352 | imageToLoad = currentImage - 1;
353 |
354 | if(imageToLoad < 0){
355 | imageToLoad = galleryLength - 1;
356 | }
357 | }
358 |
359 | if($scope.ionZoomEvents === true){
360 | //Clear zoom
361 | $ionicScrollDelegate.$getByHandle('slide-' + slideToLoad).zoomTo(1);
362 | }
363 |
364 | $scope.slides[slideToLoad] = $scope.ionGalleryItems[imageToLoad];
365 |
366 | lastSlideIndex = currentSlideIndex;
367 | };
368 |
369 | $scope.$on('ZoomStarted', function(e){
370 | $timeout(function () {
371 | zoomStart = true;
372 | $scope.hideAll = true;
373 | });
374 |
375 | });
376 |
377 | $scope.$on('TapEvent', function(e){
378 | $timeout(function () {
379 | _onTap();
380 | });
381 |
382 | });
383 |
384 | $scope.$on('DoubleTapEvent', function(event,position){
385 | $timeout(function () {
386 | _onDoubleTap(position);
387 | });
388 |
389 | });
390 |
391 | var _onTap = function _onTap(){
392 | if(zoomStart === true){
393 | if($scope.ionZoomEvents === true){
394 | $ionicScrollDelegate.$getByHandle('slide-'+lastSlideIndex).zoomTo(1,true);
395 | }
396 |
397 | $timeout(function () {
398 | _isOriginalSize();
399 | },300);
400 |
401 | return;
402 | }
403 |
404 | if(($scope.hasOwnProperty('ionSliderToggle') && $scope.ionSliderToggle === false && $scope.hideAll === false) || zoomStart === true){
405 | return;
406 | }
407 |
408 | $scope.hideAll = !$scope.hideAll;
409 | };
410 |
411 | var _onDoubleTap = function _onDoubleTap(position){
412 | if(zoomStart === false){
413 | if($scope.ionZoomEvents === true){
414 | $ionicScrollDelegate.$getByHandle('slide-'+lastSlideIndex).zoomTo(3,true,position.x,position.y);
415 | }
416 |
417 | zoomStart = true;
418 | $scope.hideAll = true;
419 | }
420 | else{
421 | _onTap();
422 | }
423 | };
424 |
425 | function _isOriginalSize(){
426 | zoomStart = false;
427 | _onTap();
428 | }
429 |
430 | }
431 |
432 | function link(scope, element, attrs) {
433 | var _modal;
434 |
435 | $ionicModal.fromTemplateUrl(ionGalleryConfig.template_slider, {
436 | scope: scope,
437 | animation: 'fade-in'
438 | }).then(function(modal){
439 | _modal = modal;
440 | });
441 |
442 | scope.openModal = function() {
443 | _modal.show();
444 | };
445 |
446 | scope.closeModal = function() {
447 | _modal.hide();
448 | };
449 |
450 | scope.$on('$destroy', function() {
451 | try{
452 | _modal.remove();
453 | } catch(err) {
454 | console.log(err.message);
455 | }
456 | });
457 | }
458 | }
459 | })();
460 |
461 | (function(){
462 | 'use strict';
463 |
464 | angular
465 | .module('ion-gallery')
466 | .service('ionSliderHelper',ionSliderHelper);
467 |
468 | ionSliderHelper.$inject = ['ionGalleryConfig'];
469 |
470 | function ionSliderHelper(ionGalleryConfig) {
471 |
472 | this.setZoomEvents = function setZoomEvents(zoomEvents){
473 | if (zoomEvents === false){
474 | ionGalleryConfig.zoom_events = false;
475 | }
476 |
477 | return ionGalleryConfig.zoom_events;
478 | }
479 |
480 | }
481 | })();
482 |
483 | angular.module("templates", []).run(["$templateCache", function($templateCache) {$templateCache.put("gallery.html","\n
\n
\n\n
![]()
\n\n
\n
\n
\n
\n");
484 | $templateCache.put("slider.html","\n \n\n \n\n");}]);
--------------------------------------------------------------------------------
/dist/ion-gallery.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";function n(n,e,i){function o(n){var o=parseInt(n.ionGalleryRowSize),t=function(){n.ionGalleryRowSize=e.getRowSize(o||i.row_size,n.ionGalleryItems.length),n.actionLabel=i.action_label,n.items=e.buildGallery(n.ionGalleryItems,n.ionGalleryRowSize),n.responsiveGrid=parseInt(1/n.ionGalleryRowSize*100)};t(),function(){n.$watch(function(){return n.ionGalleryItems.length},function(n,e){n!==e&&t()})}()}function t(n,e,o){n.customItemAction=angular.isFunction(n.ionItemAction)&&o.hasOwnProperty("ionItemAction"),n.ionSliderToggle="false"===o.ionGalleryToggle?!1:i.toggle}return o.$inject=["$scope"],{restrict:"AE",scope:{ionGalleryItems:"=ionGalleryItems",ionGalleryRowSize:"=?ionGalleryRow",ionItemAction:"&?ionItemAction",ionZoomEvents:"=?ionZoomEvents"},controller:o,link:t,replace:!0,templateUrl:i.template_gallery}}angular.module("ion-gallery",["templates"]).directive("ionGallery",n),n.$inject=["$ionicPlatform","ionGalleryHelper","ionGalleryConfig"]}(),function(){"use strict";function n(){this.config={action_label:"Done",template_gallery:"gallery.html",template_slider:"slider.html",toggle:!0,row_size:3,fixed_row_size:!0,zoom_events:!0},this.$get=function(){return this.config},this.setGalleryConfig=function(n){angular.extend(this.config,this.config,n)}}angular.module("ion-gallery").provider("ionGalleryConfig",n),n.$inject=[]}(),function(){"use strict";function n(n){this.getRowSize=function(e,i){var o;return o=isNaN(e)===!0||0>=e?n.row_size:e>i&&!n.fixed_row_size?i:e},this.buildGallery=function(n,e){for(var i=[],o=-1,t=0,l=0;l0&&(n.naturalHeight>=n.naturalWidth?e.attr("width","100%"):e.attr("height",e.parent()[0].offsetHeight+"px"))};e.bind("load",function(i){var t=this;e.parent()[0].offsetHeight>0&&o(this,e.parent()[0].offsetHeight),n.$watch(function(){return e.parent()[0].offsetHeight},function(n){o(t,n)})})}return{restrict:"A",link:n}}angular.module("ion-gallery").directive("ionImageScale",n),n.$inject=[]}(),function(){"use strict";function n(n){function e(n,e,i){n.$watch(function(){return n.ionGalleryRowSize},function(i,o){i>0&&e.css("height",e[0].offsetWidth*parseInt(n.responsiveGrid)/100+"px")})}return{restrict:"A",link:e}}angular.module("ion-gallery").directive("ionRowHeight",n),n.$inject=["ionGalleryConfig"]}(),function(){"use strict";function n(n,e){function i(i,o,t){var l=!1,r=function(){i.$emit("ZoomStarted")},s=function(n){l=!0,e(function(){l=!1,i.$emit("DoubleTapEvent",{x:n.gesture.touches[0].pageX,y:n.gesture.touches[0].pageY})},200)},a=function(n){l!==!0&&e(function(){l!==!0&&i.$emit("TapEvent")},200)},c=n.on("pinch",r,o),u=n.on("doubletap",function(n){s(n)},o),d=n.on("tap",a,o);i.$on("$destroy",function(){n.off(u,"doubletap",s),n.off(d,"tap",a),n.off(c,"pinch",r)})}return{restrict:"A",link:i}}angular.module("ion-gallery").directive("ionSlideAction",n),n.$inject=["$ionicGesture","$timeout"]}(),function(){"use strict";function n(n,e,i,o,t){function l(n){function t(){s=!1,a()}var l,r,s=(n.ionGalleryRowSize,!1);n.selectedSlide=1,n.hideAll=!1,n.ionZoomEvents=o.setZoomEvents(n.ionZoomEvents),n.openSlider=function(e){n.slides=[],r=e;var i=n.ionGalleryItems.length,o=0>e-1?i-1:e-1,t=e+1>=i?0:e+1;n.slides[0]=n.ionGalleryItems[o],n.slides[1]=n.ionGalleryItems[e],n.slides[2]=n.ionGalleryItems[t],l=1,n.openModal()},n.slideChanged=function(e){if(e!==l){var o,t=n.slides.length-l-e,s=n.ionGalleryItems.length,a=l+">"+e;"0>1"===a||"1>2"===a||"2>0"===a?(r++,r>=s&&(r=0),o=r+1,o>=s&&(o=0)):"0>2"!==a&&"1>0"!==a&&"2>1"!==a||(r--,0>r&&(r=s-1),o=r-1,0>o&&(o=s-1)),n.ionZoomEvents===!0&&i.$getByHandle("slide-"+t).zoomTo(1),n.slides[t]=n.ionGalleryItems[o],l=e}},n.$on("ZoomStarted",function(i){e(function(){s=!0,n.hideAll=!0})}),n.$on("TapEvent",function(n){e(function(){a()})}),n.$on("DoubleTapEvent",function(n,i){e(function(){c(i)})});var a=function(){return s===!0?(n.ionZoomEvents===!0&&i.$getByHandle("slide-"+l).zoomTo(1,!0),void e(function(){t()},300)):void(n.hasOwnProperty("ionSliderToggle")&&n.ionSliderToggle===!1&&n.hideAll===!1||s===!0||(n.hideAll=!n.hideAll))},c=function(e){s===!1?(n.ionZoomEvents===!0&&i.$getByHandle("slide-"+l).zoomTo(3,!0,e.x,e.y),s=!0,n.hideAll=!0):a()}}function r(e,i,o){var l;n.fromTemplateUrl(t.template_slider,{scope:e,animation:"fade-in"}).then(function(n){l=n}),e.openModal=function(){l.show()},e.closeModal=function(){l.hide()},e.$on("$destroy",function(){try{l.remove()}catch(n){}})}return l.$inject=["$scope"],{restrict:"A",controller:l,link:r}}angular.module("ion-gallery").directive("ionSlider",n),n.$inject=["$ionicModal","$timeout","$ionicScrollDelegate","ionSliderHelper","ionGalleryConfig"]}(),function(){"use strict";function n(n){this.setZoomEvents=function(e){return e===!1&&(n.zoom_events=!1),n.zoom_events}}angular.module("ion-gallery").service("ionSliderHelper",n),n.$inject=["ionGalleryConfig"]}(),angular.module("templates",[]).run(["$templateCache",function(n){n.put("gallery.html",'\n
\n
\n\n
![]()
\n\n
\n
\n
\n
\n'),n.put("slider.html",'\n \n\n \n\n')}]);
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var jshint = require('gulp-jshint');
3 | var templateCache = require('gulp-angular-templatecache');
4 | var concat = require('gulp-concat');
5 | var uglify = require('gulp-uglify');
6 | var rename = require('gulp-rename');
7 | var sass = require('gulp-sass');
8 | var ngAnnotate = require('gulp-ng-annotate');
9 | var stripDebug = require('gulp-strip-debug');
10 | var del = require('del');
11 |
12 | gulp.task('default', ['compress','sass'], function() {
13 | del(['.tmp/'], function (err, paths) {
14 | console.log('Deleted files/folders:\n', paths.join('\n'));
15 | });
16 | });
17 |
18 | gulp.task('lint', function() {
19 | return gulp.src('./src/js/*.js')
20 | .pipe(jshint())
21 | .pipe(jshint.reporter('default'));
22 | });
23 |
24 | gulp.task('templatecache',['lint'], function () {
25 | return gulp.src('./src/templates/*.html')
26 | .pipe(templateCache({standalone:true}))
27 | .pipe(gulp.dest('./.tmp/'));
28 | });
29 |
30 | gulp.task('scripts', ['templatecache'], function() {
31 | return gulp.src('./src/js/*.js')
32 | .pipe(ngAnnotate())
33 | .pipe(concat('ion-gallery.js'))
34 | .pipe(gulp.dest('./.tmp/'));
35 | });
36 |
37 | gulp.task('compress',['scripts'], function() {
38 | return gulp.src('./.tmp/*.js')
39 | .pipe(concat('ion-gallery.js'))
40 | .pipe(gulp.dest('./dist'))
41 | .pipe(stripDebug())
42 | .pipe(uglify())
43 | .pipe(rename('ion-gallery.min.js'))
44 | .pipe(gulp.dest('./dist'));
45 | });
46 |
47 | gulp.task('sass', function () {
48 | gulp.src('./src/scss/*.scss')
49 | .pipe(sass({outputStyle: 'compressed'}))
50 | .pipe(gulp.dest('./dist'));
51 | });
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ion-gallery",
3 | "version": "0.2",
4 | "description": "Ionic gallery directive",
5 | "main": "gulpfile.js",
6 | "dependencies": {},
7 | "devDependencies": {
8 | "del": "^1.2.0",
9 | "gulp": "^3.8.11",
10 | "gulp-angular-templatecache": "^1.6.0",
11 | "gulp-concat": "^2.5.2",
12 | "gulp-jshint": "^1.10.0",
13 | "gulp-uglify": "^1.2.0",
14 | "gulp-ng-annotate": "^0.5.3",
15 | "gulp-rename": "^1.2.2",
16 | "gulp-sass": "^2.0.1",
17 | "gulp-strip-debug": "^1.0.2"
18 | },
19 | "keywords": [
20 | "ionic",
21 | "gallery",
22 | "slider"
23 | ],
24 | "author": "Pedro Abreu ",
25 | "license": "MIT"
26 | }
27 |
--------------------------------------------------------------------------------
/src/js/gallery.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery', ['templates'])
6 | .directive('ionGallery', ionGallery);
7 |
8 | ionGallery.$inject = ['$ionicPlatform', 'ionGalleryHelper', 'ionGalleryConfig'];
9 |
10 | function ionGallery($ionicPlatform, ionGalleryHelper, ionGalleryConfig) {
11 | return {
12 | restrict: 'AE',
13 | scope: {
14 | ionGalleryItems: '=ionGalleryItems',
15 | ionGalleryRowSize: '=?ionGalleryRow',
16 | ionItemAction: '&?ionItemAction',
17 | ionZoomEvents: '=?ionZoomEvents'
18 | },
19 | controller: controller,
20 | link: link,
21 | replace: true,
22 | templateUrl: ionGalleryConfig.template_gallery
23 | };
24 |
25 | function controller($scope) {
26 | var _rowSize = parseInt($scope.ionGalleryRowSize);
27 |
28 | var _drawGallery = function () {
29 | $scope.ionGalleryRowSize = ionGalleryHelper.getRowSize(_rowSize || ionGalleryConfig.row_size, $scope.ionGalleryItems.length);
30 | $scope.actionLabel = ionGalleryConfig.action_label;
31 | $scope.items = ionGalleryHelper.buildGallery($scope.ionGalleryItems, $scope.ionGalleryRowSize);
32 | $scope.responsiveGrid = parseInt((1 / $scope.ionGalleryRowSize) * 100);
33 | };
34 |
35 | _drawGallery();
36 |
37 | (function () {
38 | $scope.$watch(function () {
39 | return $scope.ionGalleryItems.length;
40 | }, function (newVal, oldVal) {
41 | if (newVal !== oldVal) {
42 | _drawGallery();
43 | }
44 | });
45 | }());
46 |
47 | }
48 |
49 | function link(scope, element, attrs) {
50 | scope.customItemAction = angular.isFunction(scope.ionItemAction) && attrs.hasOwnProperty('ionItemAction');
51 | scope.ionSliderToggle = attrs.ionGalleryToggle === 'false' ? false : ionGalleryConfig.toggle;
52 | }
53 | }
54 | })();
55 |
--------------------------------------------------------------------------------
/src/js/galleryConfig.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .provider('ionGalleryConfig',ionGalleryConfig);
7 |
8 | ionGalleryConfig.$inject = [];
9 |
10 | function ionGalleryConfig(){
11 | this.config = {
12 | action_label: 'Done',
13 | template_gallery: 'gallery.html',
14 | template_slider: 'slider.html',
15 | toggle: true,
16 | row_size: 3,
17 | fixed_row_size: true,
18 | zoom_events: true
19 | };
20 |
21 | this.$get = function() {
22 | return this.config;
23 | };
24 |
25 | this.setGalleryConfig = function(config) {
26 | angular.extend(this.config, this.config, config);
27 | };
28 | }
29 |
30 | })();
31 |
--------------------------------------------------------------------------------
/src/js/galleryHelper.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .service('ionGalleryHelper',ionGalleryHelper);
7 |
8 | ionGalleryHelper.$inject = ['ionGalleryConfig'];
9 |
10 | function ionGalleryHelper(ionGalleryConfig) {
11 |
12 | this.getRowSize = function(size,length){
13 | var rowSize;
14 |
15 | if(isNaN(size) === true || size <= 0){
16 | rowSize = ionGalleryConfig.row_size;
17 | }
18 | else if(size > length && !ionGalleryConfig.fixed_row_size){
19 | rowSize = length;
20 | }
21 | else{
22 | rowSize = size;
23 | }
24 |
25 | return rowSize;
26 |
27 | };
28 |
29 | this.buildGallery = function(items,rowSize){
30 | var _gallery = [];
31 | var row = -1;
32 | var col = 0;
33 |
34 | for(var i=0;i0){
21 | if(context.naturalHeight >= context.naturalWidth){
22 | element.attr('width','100%');
23 | }
24 | else{
25 | element.attr('height',element.parent()[0].offsetHeight+'px');
26 | }
27 | }
28 | };
29 |
30 | element.bind("load" , function(e){
31 | var _this = this;
32 | if(element.parent()[0].offsetHeight > 0){
33 | scaleImage(this,element.parent()[0].offsetHeight);
34 | }
35 |
36 | scope.$watch(function(){
37 | return element.parent()[0].offsetHeight;
38 | },function(newValue){
39 | scaleImage(_this,newValue);
40 | });
41 | });
42 | }
43 | }
44 | })();
--------------------------------------------------------------------------------
/src/js/rowHeight.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .directive('ionRowHeight',ionRowHeight);
7 |
8 | ionRowHeight.$inject = ['ionGalleryConfig'];
9 |
10 | function ionRowHeight(ionGalleryConfig){
11 |
12 | return {
13 | restrict: 'A',
14 | link : link
15 | };
16 |
17 | function link(scope, element, attrs) {
18 | scope.$watch(
19 | function(){
20 | return scope.ionGalleryRowSize;
21 | },
22 | function(newValue,oldValue){
23 | if(newValue > 0){
24 | element.css('height',element[0].offsetWidth * parseInt(scope.responsiveGrid)/100 + 'px');
25 | }
26 | });
27 | }
28 | }
29 | })();
--------------------------------------------------------------------------------
/src/js/slideAction.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .directive('ionSlideAction',ionSlideAction);
7 |
8 | ionSlideAction.$inject = ['$ionicGesture','$timeout'];
9 |
10 | function ionSlideAction($ionicGesture, $timeout){
11 |
12 | return {
13 | restrict: 'A',
14 | link : link
15 | };
16 |
17 | function link(scope, element, attrs) {
18 | var isDoubleTapAction = false;
19 |
20 | var pinchZoom = function pinchZoom(){
21 | scope.$emit('ZoomStarted');
22 | };
23 |
24 | var imageDoubleTapGesture = function imageDoubleTapGesture(event) {
25 |
26 | isDoubleTapAction = true;
27 |
28 | $timeout(function(){
29 | isDoubleTapAction = false;
30 | scope.$emit('DoubleTapEvent',{ 'x': event.gesture.touches[0].pageX, 'y': event.gesture.touches[0].pageY});
31 | },200);
32 | };
33 |
34 | var imageTapGesture = function imageTapGesture(event) {
35 |
36 | if(isDoubleTapAction === true){
37 | return;
38 | }
39 | else{
40 | $timeout(function(){
41 | if(isDoubleTapAction === true){
42 | return;
43 | }
44 | else{
45 | scope.$emit('TapEvent');
46 | }
47 | },200);
48 | }
49 | };
50 |
51 | var pinchEvent = $ionicGesture.on('pinch',pinchZoom,element);
52 | var doubleTapEvent = $ionicGesture.on('doubletap', function(e){imageDoubleTapGesture(e);}, element);
53 | var tapEvent = $ionicGesture.on('tap', imageTapGesture, element);
54 |
55 | scope.$on('$destroy', function() {
56 | $ionicGesture.off(doubleTapEvent, 'doubletap', imageDoubleTapGesture);
57 | $ionicGesture.off(tapEvent, 'tap', imageTapGesture);
58 | $ionicGesture.off(pinchEvent, 'pinch', pinchZoom);
59 | });
60 | }
61 | }
62 | })();
63 |
--------------------------------------------------------------------------------
/src/js/slider.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .directive('ionSlider',ionSlider);
7 |
8 | ionSlider.$inject = ['$ionicModal','$timeout','$ionicScrollDelegate','ionSliderHelper','ionGalleryConfig'];
9 |
10 | function ionSlider($ionicModal,$timeout,$ionicScrollDelegate,ionSliderHelper,ionGalleryConfig){
11 |
12 | return {
13 | restrict: 'A',
14 | controller: controller,
15 | link : link
16 | };
17 |
18 | function controller($scope){
19 | var lastSlideIndex;
20 | var currentImage;
21 |
22 | var rowSize = $scope.ionGalleryRowSize;
23 | var zoomStart = false;
24 |
25 | $scope.selectedSlide = 1;
26 | $scope.hideAll = false;
27 | $scope.ionZoomEvents = ionSliderHelper.setZoomEvents($scope.ionZoomEvents)
28 |
29 | $scope.openSlider = function(index) {
30 | $scope.slides = [];
31 | currentImage = index;
32 |
33 | var galleryLength = $scope.ionGalleryItems.length;
34 | var previndex = index - 1 < 0 ? galleryLength - 1 : index - 1;
35 | var nextindex = index + 1 >= galleryLength ? 0 : index + 1;
36 |
37 | $scope.slides[0] = $scope.ionGalleryItems[previndex];
38 | $scope.slides[1] = $scope.ionGalleryItems[index];
39 | $scope.slides[2] = $scope.ionGalleryItems[nextindex];
40 |
41 | lastSlideIndex = 1;
42 | $scope.openModal();
43 | };
44 |
45 | $scope.slideChanged = function(currentSlideIndex) {
46 |
47 | if(currentSlideIndex === lastSlideIndex){
48 | return;
49 | }
50 |
51 | var slideToLoad = $scope.slides.length - lastSlideIndex - currentSlideIndex;
52 | var galleryLength = $scope.ionGalleryItems.length;
53 | var imageToLoad;
54 | var slidePosition = lastSlideIndex + '>' + currentSlideIndex;
55 |
56 | if(slidePosition === '0>1' || slidePosition === '1>2' || slidePosition === '2>0'){
57 | currentImage++;
58 |
59 | if(currentImage >= galleryLength){
60 | currentImage = 0;
61 | }
62 |
63 | imageToLoad = currentImage + 1;
64 |
65 | if( imageToLoad >= galleryLength){
66 | imageToLoad = 0;
67 | }
68 | }
69 | else if(slidePosition === '0>2' || slidePosition === '1>0' || slidePosition === '2>1'){
70 | currentImage--;
71 |
72 | if(currentImage < 0){
73 | currentImage = galleryLength - 1 ;
74 | }
75 |
76 | imageToLoad = currentImage - 1;
77 |
78 | if(imageToLoad < 0){
79 | imageToLoad = galleryLength - 1;
80 | }
81 | }
82 |
83 | if($scope.ionZoomEvents === true){
84 | //Clear zoom
85 | $ionicScrollDelegate.$getByHandle('slide-' + slideToLoad).zoomTo(1);
86 | }
87 |
88 | $scope.slides[slideToLoad] = $scope.ionGalleryItems[imageToLoad];
89 |
90 | lastSlideIndex = currentSlideIndex;
91 | };
92 |
93 | $scope.$on('ZoomStarted', function(e){
94 | $timeout(function () {
95 | zoomStart = true;
96 | $scope.hideAll = true;
97 | });
98 |
99 | });
100 |
101 | $scope.$on('TapEvent', function(e){
102 | $timeout(function () {
103 | _onTap();
104 | });
105 |
106 | });
107 |
108 | $scope.$on('DoubleTapEvent', function(event,position){
109 | $timeout(function () {
110 | _onDoubleTap(position);
111 | });
112 |
113 | });
114 |
115 | var _onTap = function _onTap(){
116 | if(zoomStart === true){
117 | if($scope.ionZoomEvents === true){
118 | $ionicScrollDelegate.$getByHandle('slide-'+lastSlideIndex).zoomTo(1,true);
119 | }
120 |
121 | $timeout(function () {
122 | _isOriginalSize();
123 | },300);
124 |
125 | return;
126 | }
127 |
128 | if(($scope.hasOwnProperty('ionSliderToggle') && $scope.ionSliderToggle === false && $scope.hideAll === false) || zoomStart === true){
129 | return;
130 | }
131 |
132 | $scope.hideAll = !$scope.hideAll;
133 | };
134 |
135 | var _onDoubleTap = function _onDoubleTap(position){
136 | if(zoomStart === false){
137 | if($scope.ionZoomEvents === true){
138 | $ionicScrollDelegate.$getByHandle('slide-'+lastSlideIndex).zoomTo(3,true,position.x,position.y);
139 | }
140 |
141 | zoomStart = true;
142 | $scope.hideAll = true;
143 | }
144 | else{
145 | _onTap();
146 | }
147 | };
148 |
149 | function _isOriginalSize(){
150 | zoomStart = false;
151 | _onTap();
152 | }
153 |
154 | }
155 |
156 | function link(scope, element, attrs) {
157 | var _modal;
158 |
159 | $ionicModal.fromTemplateUrl(ionGalleryConfig.template_slider, {
160 | scope: scope,
161 | animation: 'fade-in'
162 | }).then(function(modal){
163 | _modal = modal;
164 | });
165 |
166 | scope.openModal = function() {
167 | _modal.show();
168 | };
169 |
170 | scope.closeModal = function() {
171 | _modal.hide();
172 | };
173 |
174 | scope.$on('$destroy', function() {
175 | try{
176 | _modal.remove();
177 | } catch(err) {
178 | console.log(err.message);
179 | }
180 | });
181 | }
182 | }
183 | })();
184 |
--------------------------------------------------------------------------------
/src/js/sliderHelper.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | 'use strict';
3 |
4 | angular
5 | .module('ion-gallery')
6 | .service('ionSliderHelper',ionSliderHelper);
7 |
8 | ionSliderHelper.$inject = ['ionGalleryConfig'];
9 |
10 | function ionSliderHelper(ionGalleryConfig) {
11 |
12 | this.setZoomEvents = function setZoomEvents(zoomEvents){
13 | if (zoomEvents === false){
14 | ionGalleryConfig.zoom_events = false;
15 | }
16 |
17 | return ionGalleryConfig.zoom_events;
18 | }
19 |
20 | }
21 | })();
22 |
--------------------------------------------------------------------------------
/src/scss/ion-gallery.scss:
--------------------------------------------------------------------------------
1 | .gallery-view{
2 |
3 | .image-container{
4 | position: relative;
5 | overflow: hidden;
6 | border: 2px solid white;
7 |
8 | img{
9 | position: absolute;
10 | top: -9999px;
11 | bottom: -9999px;
12 | left: -9999px;
13 | right: -9999px;
14 | margin: auto;
15 | }
16 | }
17 | }
18 |
19 | .imageView{
20 |
21 | .has-no-header{
22 | top:0px !important;
23 | }
24 |
25 | .close-btn{
26 | font-weight: 900;
27 | border:2px solid;
28 | position:absolute;
29 | right:5px;
30 | border-radius: 5px;
31 | }
32 |
33 | .headerView{
34 | background-image:none;
35 | background-color: black;
36 | }
37 |
38 | .gallery-slide-view{
39 | width: 98%;
40 | background-color: transparent;
41 | }
42 |
43 | .image-subtitle{
44 | color: white;
45 | position: absolute;
46 | bottom: 0px;
47 | left: 10px;
48 | width: 95%;
49 | height:15%;
50 | z-index: 100;
51 | }
52 |
53 | .listContainer {
54 | width: 100%;
55 | height: 100%;
56 | background-color:black;
57 | }
58 |
59 | .hideAll{
60 | display:none;
61 | }
62 |
63 | img {
64 | display: block;
65 | width: 100%;
66 | height: auto;
67 | }
68 |
69 | .scroll-view {
70 | position: absolute;
71 | width: 100%;
72 | height:100%;
73 | }
74 |
75 | .scroll-view .scroll {
76 | min-height: 100%;
77 | display: -webkit-box;
78 | display: -moz-box;
79 | display: -ms-flexbox;
80 | display: -webkit-flex;
81 | display: flex;
82 | -webkit-box-direction: normal;
83 | -moz-box-direction: normal;
84 | -webkit-box-orient: horizontal;
85 | -moz-box-orient: horizontal;
86 | -webkit-flex-direction: row;
87 | -ms-flex-direction: row;
88 | flex-direction: row;
89 | -webkit-flex-wrap: nowrap;
90 | -ms-flex-wrap: nowrap;
91 | flex-wrap: nowrap;
92 | -webkit-box-pack: center;
93 | -moz-box-pack: center;
94 | -webkit-justify-content: center;
95 | -ms-flex-pack: center;
96 | justify-content: center;
97 | -webkit-align-content: stretch;
98 | -ms-flex-line-pack: stretch;
99 | align-content: stretch;
100 | -webkit-box-align: center;
101 | -moz-box-align: center;
102 | -webkit-align-items: center;
103 | -ms-flex-align: center;
104 | align-items: center;
105 | }
106 | }
--------------------------------------------------------------------------------
/src/templates/gallery.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
![]()
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/templates/slider.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
29 |
30 |
--------------------------------------------------------------------------------