├── .bowerrc ├── .gitignore ├── .github └── FUNDING.yml ├── ion-tree-list.less ├── TODO.md ├── tests ├── karma.conf.js └── unit │ └── ion-tree-list.spec.js ├── bower.json ├── ion-tree-list.min.css ├── ion-tree-list.min.js ├── ion-tree-list.tmpl.html ├── LICENSE ├── gulpfile.js ├── package.json ├── ion-tree-list.js └── README.md /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.css.map 2 | bower_components/* 3 | .DS_Store 4 | .idea 5 | /node_modules 6 | gh-pages/* 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [fer] 4 | patreon: ferqwerty 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /ion-tree-list.less: -------------------------------------------------------------------------------- 1 | // Iterate from 1 to 35 starting with padding-right 15px and offset 20px 2 | .mixin-loop(1, 35, 15, 20); 3 | 4 | .mixin-loop(@start, @end, @init, @offset) when (@end >= @start) { 5 | .depth-@{start} { 6 | padding-left: @init * 1px 7 | } 8 | .mixin-loop(@start + 1, @end, @init + @offset, @offset); 9 | } 10 | 11 | .icon { 12 | &.ion-arrow-right-b, 13 | &.ion-arrow-down-b { 14 | padding-right: 5px 15 | } 16 | } -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] collapsedLevel .. test + document… igual usar el json tree para todo 2 | - [ ] https://github.com/awendland/angular-json-tree 3 | - [ ] https://github.com/krispo/json-tree 4 | - [ ] ionic 2 5 | - [ ] sacar el CSS fuera del template? para que lo pille del CDN 6 | - [ ] I just created a function that only opened the next depth of the tree. Thank for checking in! 7 | - [ ] https://github.com/vivlong/ion-tree-list/commit/8cbeebb8f276db6b29314e392399c02ff8ce5538 8 | - [ ] https://github.com/tjgao/ion-tree-list/commit/597a43a2ee31e7453f7fd7cb7d19f5a065931706 9 | - [ ] ion-tree-list - rememberable states! 10 | - [ ] meter CDNJS algun link en el readme 11 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '../', 4 | frameworks: ['jasmine'], 5 | plugins: ['karma-jasmine', 'karma-phantomjs-launcher', 'karma-ng-html2js-preprocessor'], 6 | files: [ 7 | 'bower_components/ionic/release/js/ionic.bundle.js', 8 | 'bower_components/angular-mocks/angular-mocks.js', 9 | '*.html', 10 | 'ion-*.js', 11 | 'tests/unit/*.spec.js' 12 | ], 13 | preprocessors: { 14 | '**/*.tmpl.html': ['ng-html2js'] 15 | }, 16 | browsers: ['PhantomJS'], 17 | singleRun: false 18 | }) 19 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ion-tree-list", 3 | "main": "ion-tree-list.js", 4 | "version": "0.0.11", 5 | "homepage": "https://github.com/fer/ion-tree-list", 6 | "authors": [ 7 | "fer " 8 | ], 9 | "description": "Ionic directive for displaying nested list items.", 10 | "keywords": [ 11 | "ionic", 12 | "ion", 13 | "tree", 14 | "list" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | ".bowerrc", 19 | ".gitignore", 20 | "bower.json", 21 | "gulpfile.js", 22 | "ion-tree-list.less", 23 | "LICENSE", 24 | "package.json", 25 | "README.md", 26 | "tests" 27 | ], 28 | "devDependencies": { 29 | "ionic": "~1.0.1", 30 | "angular-mocks": "1.3.13" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ion-tree-list.min.css: -------------------------------------------------------------------------------- 1 | .depth-1{padding-left:15px}.depth-2{padding-left:35px}.depth-3{padding-left:55px}.depth-4{padding-left:75px}.depth-5{padding-left:95px}.depth-6{padding-left:115px}.depth-7{padding-left:135px}.depth-8{padding-left:155px}.depth-9{padding-left:175px}.depth-10{padding-left:195px}.depth-11{padding-left:215px}.depth-12{padding-left:235px}.depth-13{padding-left:255px}.depth-14{padding-left:275px}.depth-15{padding-left:295px}.depth-16{padding-left:315px}.depth-17{padding-left:335px}.depth-18{padding-left:355px}.depth-19{padding-left:375px}.depth-20{padding-left:395px}.depth-21{padding-left:415px}.depth-22{padding-left:435px}.depth-23{padding-left:455px}.depth-24{padding-left:475px}.depth-25{padding-left:495px}.depth-26{padding-left:515px}.depth-27{padding-left:535px}.depth-28{padding-left:555px}.depth-29{padding-left:575px}.depth-30{padding-left:595px}.depth-31{padding-left:615px}.depth-32{padding-left:635px}.depth-33{padding-left:655px}.depth-34{padding-left:675px}.depth-35{padding-left:695px}.icon.ion-arrow-down-b,.icon.ion-arrow-right-b{padding-right:5px} -------------------------------------------------------------------------------- /ion-tree-list.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function addDepthToTree(e,t,l){for(var o in e)e[o]&&"object"==typeof e[o]&&(e[o].depth=t,e[o].collapsed=l,addDepthToTree(e[o],"tree"===o?++t:t,l));return e}function toggleCollapse(e){for(var t in e)e[t]&&"object"==typeof e[t]&&(e[t].collapsed=!e[t].collapsed,toggleCollapse(e[t]));return e}var CONF={baseUrl:"lib/ion-tree-list",digestTtl:35};angular.module("ion-tree-list",[],["$rootScopeProvider",function(e){e.digestTtl(CONF.digestTtl)}]).directive("ionTreeList",[function(){return{restrict:"E",scope:{items:"=",collapsed:"=",templateUrl:"@",showReorder:"="},templateUrl:CONF.baseUrl+"/ion-tree-list.tmpl.html",controller:["$scope",function(e){e.baseUrl=CONF.baseUrl,e.toggleCollapse=function(e){e&&e.collapsible!==!1&&toggleCollapse(e)},e.emitEvent=function(t){e.$emit("$ionTreeList:ItemClicked",t)},e.moveItem=function(t,l,o){e.items.splice(l,1),e.items.splice(o,0,t)},e.$watch("collapsed",function(){e.toggleCollapse(e.items)}),e.$watch("items",function(){e.items=addDepthToTree(e.items,1,e.collapsed),e.$emit("$ionTreeList:LoadComplete",e.items)})}],compile:function(e,t){t.templateUrl=t.templateUrl?t.templateUrl:"item_default_renderer"}}}]); -------------------------------------------------------------------------------- /ion-tree-list.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 23 | 24 |
25 | 26 |
27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 fer 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 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'), 4 | release = require('gulp-release-tasks')(gulp), 5 | $ = require('gulp-load-plugins')(), 6 | Server = require('karma').Server; 7 | 8 | gulp.task('test', function (done) { 9 | new Server({ 10 | configFile: __dirname + '/tests/karma.conf.js', 11 | singleRun: true 12 | }, done).start() 13 | }); 14 | 15 | gulp.task('less', function () { 16 | return gulp.src('./*.less') 17 | .pipe($.less()) 18 | .pipe($.minifyCss()) 19 | .pipe($.rename({suffix: '.min'})) 20 | .pipe(gulp.dest('.')); 21 | }); 22 | 23 | gulp.task('compress', function() { 24 | return gulp.src('ion-tree-list.js') 25 | .pipe($.minify( 26 | { 27 | ext: { 28 | min: '.min.js' 29 | } 30 | })) 31 | .pipe(gulp.dest('.')) 32 | }); 33 | 34 | gulp.task('updateNpmDependencies', function(){ 35 | return gulp.src('package.json') 36 | .pipe($.david({ update: true })) 37 | .pipe(gulp.dest('.')) 38 | }); 39 | 40 | gulp.task('copy', function() { 41 | return gulp.src(['ion-tree-list.js', 'ion-tree-list.css', 'ion-tree-list-tmpl.html']) 42 | .pipe(gulp.dest('gh-pages/ion-tree-list/lib/ion-tree-list/')); 43 | }); 44 | 45 | gulp.task('watch', function(){ 46 | gulp.watch('./*.less', ['less']); 47 | gulp.watch('**/*.js', ['copy']); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fer/ion-tree-list", 3 | "version": "0.0.11", 4 | "description": "Ionic directive for displaying nested list items.", 5 | "main": "ion-tree-list.js", 6 | "scripts": { 7 | "start": "gulp watch", 8 | "test": "gulp test" 9 | }, 10 | "publishConfig": { "registry": "https://npm.pkg.github.com/" }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/fer/ion-tree-list.git" 14 | }, 15 | "keywords": [ 16 | "ionic", 17 | "ion", 18 | "tree", 19 | "list" 20 | ], 21 | "author": "fer ", 22 | "contributors": [ 23 | { 24 | "name": "Salva Vilarrasa", 25 | "email": "shagy69@gmail.com" 26 | } 27 | ], 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/fer/ion-tree-list/issues" 31 | }, 32 | "homepage": "https://github.com/fer/ion-tree-list#readme", 33 | "devDependencies": { 34 | "gulp": "^3.9.0", 35 | "gulp-david": "^1.0.0", 36 | "gulp-less": "^3.0.3", 37 | "gulp-load-plugins": "^1.0.0-rc.1", 38 | "gulp-minify": "0.0.14", 39 | "gulp-minify-css": "^1.2.0", 40 | "gulp-release-tasks": "0.0.3", 41 | "gulp-rename": "^1.2.2", 42 | "jasmine-core": "^2.3.4", 43 | "karma": "^1.3.0", 44 | "karma-html2js-preprocessor": "^1.0.0", 45 | "karma-jasmine": "^1.0.2", 46 | "karma-ng-html2js-preprocessor": "^1.0.0", 47 | "karma-phantomjs-launcher": "^1.0.0", 48 | "phantomjs-prebuilt": "^2.1.7" 49 | }, 50 | "dependencies": { 51 | "bower": "^1.7.7" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ion-tree-list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* global angular */ 3 | 4 | var CONF = { 5 | baseUrl: 'lib/ion-tree-list', 6 | digestTtl: 35 7 | }; 8 | 9 | function addDepthToTree(obj, depth, collapsed) { 10 | for (var key in obj) { 11 | if (obj[key] && typeof(obj[key]) == 'object') { 12 | obj[key].depth = depth; 13 | obj[key].collapsed = collapsed; 14 | addDepthToTree(obj[key], key === 'tree' ? ++ depth : depth, collapsed) 15 | } 16 | } 17 | return obj 18 | } 19 | 20 | function toggleCollapse(obj) { 21 | if (obj.tree) { 22 | obj.tree.collapsed = !obj.tree.collapsed; 23 | for (var i = 0; i < obj.tree.length; i++) { 24 | obj.tree[i].collapsed = !obj.tree[i].collapsed; 25 | } 26 | } 27 | return obj 28 | } 29 | 30 | angular.module('ion-tree-list', [], ['$rootScopeProvider', function($rootScopeProvider){ 31 | $rootScopeProvider.digestTtl(CONF.digestTtl) 32 | }]) 33 | .directive('ionTreeList', [function() { 34 | return { 35 | restrict: 'E', 36 | scope: { 37 | items: '=', 38 | collapsed: '=', 39 | templateUrl: '@', 40 | showReorder: '=' 41 | }, 42 | templateUrl: CONF.baseUrl + '/ion-tree-list.tmpl.html', 43 | controller: ['$scope', function($scope) { 44 | $scope.baseUrl = CONF.baseUrl; 45 | 46 | $scope.toggleCollapse = function(item) { 47 | if (item && item.collapsible !== false) { 48 | toggleCollapse(item); 49 | } 50 | }; 51 | 52 | $scope.emitEvent = function(item){ 53 | $scope.$emit('$ionTreeList:ItemClicked', item) 54 | }; 55 | 56 | $scope.moveItem = function(item, fromIndex, toIndex) { 57 | $scope.items.splice(fromIndex, 1); 58 | $scope.items.splice(toIndex, 0, item) 59 | }; 60 | 61 | $scope.$watch('collapsed', function() { 62 | $scope.toggleCollapse($scope.items) 63 | }); 64 | 65 | $scope.$watch('items', function() { 66 | $scope.items = addDepthToTree($scope.items, 1, $scope.collapsed); 67 | $scope.$emit('$ionTreeList:LoadComplete', $scope.items) 68 | }) 69 | }], 70 | compile: function(element, attrs){ 71 | attrs.templateUrl = attrs.templateUrl ? attrs.templateUrl : 'item_default_renderer'; 72 | } 73 | } 74 | }]); 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ion-tree-list [![Stories in Ready](https://badge.waffle.io/fer/ion-tree-list.png?label=ready&title=Ready)](https://waffle.io/fer/ion-tree-list) [![devDependency Status](https://david-dm.org/fer/ion-tree-list/dev-status.svg?style=flat)](https://david-dm.org/fer/ion-tree-list#info=devDependencies) [![Bower version](https://badge.fury.io/bo/ion-tree-list.svg)](http://badge.fury.io/bo/ion-tree-list) 2 | 3 | Ionic directive for displaying nested list ionic items. 4 | 5 | Check [demo](http://fer.github.io/ion-tree-list/) link. 6 | 7 | ### Note: 8 | #### I am actively looking for contributors who can port this library to [Ionic 2](http://ionic.io/2). If you are interested in doing so, don't hesitate to [drop me an email](mailto:fer+github@ferqwerty.com). 9 | 10 | ## Installation 11 | 12 | ``` 13 | bower install ion-tree-list --save 14 | ``` 15 | 16 | Add somewhere in your HEAD tag: 17 | 18 | ``` 19 | 20 | ``` 21 | 22 | Or fetch it from the [cdnjs](https://cdnjs.com/about) project : [https://cdnjs.com/libraries/ion-tree-list](https://cdnjs.com/libraries/ion-tree-list). 23 | 24 | You'll need to add ```ion-tree-list``` as a dependency on your Ionic app: 25 | 26 | ``` 27 | angular.module('starter', [ 28 | 'ionic', 29 | 'controllers', 30 | 'services', 31 | 'ion-tree-list' 32 | ]) 33 | ``` 34 | 35 | In your ```controller.js```: 36 | 37 | ``` 38 | $scope.tasks = [ 39 | { 40 | name: 'first task 1', 41 | tree: [ 42 | { 43 | name: 'first task 1.1' 44 | } 45 | ] 46 | }, 47 | { 48 | name: 'first task 2' 49 | } 50 | ]; 51 | ``` 52 | 53 | 54 | In your ```view.html```: 55 | 56 | ``` 57 | 58 | ``` 59 | 60 | Fetch clicked item by listening to ```$ionTreeList:ItemClicked``` in your controller: 61 | 62 | ## Emmited events 63 | 64 | ``` 65 | $scope.$on('$ionTreeList:ItemClicked', function(event, item) { 66 | // process 'item' 67 | console.log(item); 68 | }); 69 | 70 | $scope.$on('$ionTreeList:LoadComplete', function(event, items) { 71 | // process 'items' 72 | console.log(items); 73 | }); 74 | ``` 75 | 76 | ## Custom templates 77 | 78 | Imagine your tasks in ```$scope.tasks``` in your ```controller.js``` has an extra attribute as ```checked```: 79 | 80 | ``` 81 | $scope.tasks = [ 82 | { 83 | name: 'first task 1', 84 | checked: false, 85 | tree: [ 86 | { 87 | name: 'first task 1.1', 88 | checked: true 89 | }, 90 | ] 91 | }, 92 | { 93 | name: 'first task 2', 94 | checked: true 95 | } 96 | ]; 97 | ``` 98 | 99 | In order to consume the ```checked``` value in your view, create a ```ion-item.tmpl.html``` file in 100 | your www folder containing the following: 101 | 102 | ``` 103 | 104 | {{item.name}} 105 | ``` 106 | 107 | Add an extra ```template-url``` attribute for your custom template: 108 | 109 | ``` 110 | 111 | ``` 112 | 113 | ## Contributing 114 | 115 | Developers interested in contributing are very welcomed :) 116 | 117 | Don't hesitate to ping me on my [email](mailto:fer@ferqwerty.com) if you are missing a feature, a fix or you simply want to contribute. 118 | 119 | There's a list of issues right [here](https://github.com/fer/ion-tree-list/issues). 120 | -------------------------------------------------------------------------------- /tests/unit/ion-tree-list.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global angular, inject, spyOn, expect */ 3 | 4 | describe('Directives', function(){ 5 | var templateCache, scope, element, Directive, 6 | templateUrl = 'ion-tree-list.tmpl.html', 7 | items = [ 8 | { 9 | name: 'task 1', 10 | tree: [ 11 | { 12 | name: 'task 1.1' 13 | } 14 | ] 15 | }, 16 | { 17 | name: 'task 2' 18 | }, 19 | { 20 | name: 'task 3' 21 | } 22 | ]; 23 | 24 | beforeEach(module('ion-tree-list')); 25 | beforeEach(module(templateUrl)); 26 | beforeEach(inject( 27 | function($rootScope, $compile, $templateCache, $injector){ 28 | Directive = function(){ 29 | this.directive = $injector.get('ionTreeListDirective'); 30 | this.directive[0].templateUrl = templateUrl; // Override baseUrl for custom templates 31 | this.element = $compile(angular.element(''))(scope); 32 | this.element.scope().$apply(); 33 | this.isolateScope = this.element.isolateScope() 34 | }; 35 | 36 | scope = $rootScope.$new(); 37 | scope.items = items; 38 | scope.$digest(); 39 | templateCache = $templateCache 40 | } 41 | )); 42 | 43 | describe('ion-tree-list', function(){ 44 | 45 | var d; 46 | 47 | beforeEach(function(){ 48 | d = new Directive('ionTreeList') 49 | }); 50 | 51 | it('has the ion tree list directive', function(){ 52 | expect(d.directive).toBeDefined() 53 | }); 54 | 55 | it('logs a template for cart-item.html', function(){ 56 | expect(templateCache.get('ion-tree-list.tmpl.html')).toBeDefined() 57 | }); 58 | 59 | it('has an isolate scope', function() { 60 | expect(d.isolateScope).toBeDefined() 61 | }); 62 | 63 | it('has an isolate scope with a "items" property on it', function() { 64 | expect(d.isolateScope.items).toBeDefined() 65 | }); 66 | 67 | it('has a moveItem method', function(){ 68 | expect(typeof d.isolateScope.moveItem).toBe('function') 69 | }); 70 | 71 | it('items have the same of elements as in scope', function(){ 72 | expect(d.element[0].querySelectorAll('.item').length).toBe(4) 73 | }); 74 | 75 | it('items has the correct className assigned', function(){ 76 | var list = d.element[0].children[3].children[0], 77 | classNameFirst = list.children[0].className, 78 | classNameFirstNested = list.children[1].children[0].className; 79 | 80 | expect(classNameFirst).toBe('item depth-1'); 81 | expect(classNameFirstNested).toBe('item depth-2'); 82 | }); 83 | 84 | it('has a emitEvent method', function(){ 85 | expect(typeof d.isolateScope.emitEvent).toBe('function') 86 | }); 87 | 88 | it('has an emitEvent on item click called $ionTreeList:ItemClicked', function($timeout){ 89 | spyOn(d.isolateScope, '$emit'); 90 | 91 | var items = d.element.find('ion-item'), item0 = items[0]; 92 | item0.click(); 93 | d.isolateScope.$digest(); 94 | $timeout(function(){ 95 | expect(d.isolateScope.$emit).toHaveBeenCalled(); 96 | expect(d.isolateScope.$emit).toHaveBeenCalledWith('$ionTreeList:ItemClicked', item0) 97 | }, 0) 98 | }) 99 | 100 | it('has an emitEvent on list loaded called $ionTreeList:LoadComplete', function($timeout){ 101 | spyOn(d.isolateScope, '$emit'); 102 | d.isolateScope.items.pop(); 103 | d.isolateScope.$digest(); 104 | $timeout(function(){ 105 | expect(d.isolateScope.$emit).toHaveBeenCalled(); 106 | expect(d.isolateScope.$emit).toHaveBeenCalledWith('$ionTreeList:LoadComplete') 107 | }, 0) 108 | }) 109 | }) 110 | }); 111 | --------------------------------------------------------------------------------