├── .gitignore
├── CHANGELOG.md
├── Gruntfile.js
├── LICENSE.md
├── README.md
├── bower.json
├── demo
├── app.js
└── index.html
├── dist
├── angular-multi-select-tree-0.1.0.css
├── angular-multi-select-tree-0.1.0.js
├── angular-multi-select-tree-0.1.0.min.css
├── angular-multi-select-tree-0.1.0.min.js
└── angular-multi-select-tree-0.1.0.tpl.js
├── karma.conf.js
├── package.json
├── src
├── multi-select-tree-main.js
├── multi-select-tree.js
├── multi-select-tree.less
├── multi-select-tree.tpl.html
├── tree-item.js
└── tree-item.tpl.html
└── test
└── unit
└── componentSpec.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | components
4 | bower_components
5 | build
6 | .idea
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/a5hik/angular-multi-select-tree/6afbba8fff408404df1776ad1dc5fbcd614cd2e3/CHANGELOG.md
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function (grunt) {
2 | 'use strict';
3 |
4 | require('load-grunt-tasks')(grunt);
5 | var _ = require('lodash');
6 |
7 | var karmaConfig = function(configFile, customOptions) {
8 | var options = { configFile: configFile, keepalive: true };
9 | var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' };
10 | return _.extend(options, customOptions, travisOptions);
11 | };
12 |
13 | var mountFolder = function (connect, dir) {
14 | return connect.static(require('path').resolve(dir));
15 | };
16 |
17 | grunt.initConfig({
18 | pkg: grunt.file.readJSON('bower.json'),
19 | meta: {
20 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
21 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
22 | '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' +
23 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
24 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */'
25 | },
26 | watch: {
27 | scripts: {
28 | files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
29 | tasks: ['jshint', 'karma:unit']
30 | }
31 | },
32 | jshint: {
33 | all: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'],
34 | options: {
35 | eqeqeq: true,
36 | globals: {
37 | angular: true
38 | }
39 | }
40 | },
41 | concat: {
42 | js: {
43 | src: ['src/**/*main.js',
44 | 'src/**/*.js'],
45 | dest: 'dist/angular-multi-select-tree-<%= pkg.version %>.js'
46 | },
47 | css: {
48 | src: ['src/**/*.css', 'src/**/*.less'],
49 | dest: 'dist/angular-multi-select-tree-<%= pkg.version %>.css'
50 | }
51 | },
52 | ngtemplates: {
53 | 'multi-select-tree': {
54 | src: 'src/**/*.html',
55 | dest: 'dist/angular-multi-select-tree-<%= pkg.version %>.tpl.js'
56 | }
57 | },
58 | uglify: {
59 | src: {
60 | files: {
61 | 'dist/angular-multi-select-tree-<%= pkg.version %>.min.js': '<%= concat.js.dest %>'
62 | }
63 | }
64 | },
65 |
66 | // connect
67 | connect: {
68 | options: {
69 | port: 3000,
70 | livereload: 93729,
71 | hostname: '0.0.0.0'
72 | },
73 | demo: {
74 | options: {
75 | middleware: function (connect) {
76 | return [
77 | mountFolder(connect, '')
78 | ];
79 | }
80 | }
81 | }
82 | },
83 |
84 | // open
85 | open: {
86 | server: {
87 | path: 'http://localhost:<%= connect.options.port %>/demo/'
88 | }
89 | },
90 |
91 | karma: {
92 | unit: {
93 | options: karmaConfig('karma.conf.js', {
94 | singleRun: true
95 | })
96 | },
97 | server: {
98 | options: karmaConfig('karma.conf.js', {
99 | singleRun: false
100 | })
101 | }
102 | },
103 | changelog: {
104 | options: {
105 | dest: 'CHANGELOG.md'
106 | }
107 | },
108 | ngmin: {
109 | src: {
110 | src: '<%= concat.js.dest %>',
111 | dest: '<%= concat.js.dest %>'
112 | }
113 | },
114 | cssmin: {
115 | css: {
116 | src: '<%= concat.css.dest %>',
117 | dest: 'dist/angular-multi-select-tree-<%= pkg.version %>.min.css'
118 | }
119 | },
120 | clean: ['dist/*']
121 | });
122 |
123 | grunt.registerTask('default', ['jshint', 'karma:unit']);
124 | grunt.registerTask('test', ['karma:unit']);
125 | grunt.registerTask('test-server', ['karma:server']);
126 | grunt.registerTask('server', ['open', 'connect:demo', 'watch']);
127 | grunt.registerTask('build', ['clean', 'jshint', 'concat', 'ngtemplates', 'ngmin', 'cssmin', 'uglify']);
128 | };
129 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Muhammed Ashik
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | angular-multi-select-tree
2 | =============================
3 |
4 | A native Angular multi select tree. No JQuery.
5 | If you use this module you can give it a thumbs up at [http://ngmodules.org/modules/angular-multi-select-tree](http://ngmodules.org/modules/angular-multi-select-tree).
6 |
7 | #### Demo Page:
8 |
9 | [Demo] (http://a5hik.github.io/angular-multi-select-tree)
10 |
11 | #### Features:
12 |
13 | TBD
14 | #### Implementation Details:
15 |
16 | TBD
17 | #### Design details:
18 |
19 | #### Callbacks:
20 |
21 | TBD
22 | ##### Usage:
23 |
24 | Get the binaries of angular-multi-select-tree with any of the following ways.
25 |
26 | ```sh
27 | bower install angular-multi-select-tree
28 | ```
29 | Or for yeoman with bower automatic include:
30 | ```
31 | bower install angular-multi-select-tree -save
32 | ```
33 | Or bower.json
34 | ```
35 | {
36 | "dependencies": [..., "multi-select-tree: "latest_version eg - "1.1.0" ", ...],
37 | }
38 | ```
39 | Make sure to load the scripts in your html.
40 | ```html
41 |
42 |
43 |
44 |
45 | ```
46 |
47 | And Inject the sortable module as dependency.
48 |
49 | ```
50 | angular.module('xyzApp', ['multi-select-tree', '....']);
51 | ```
52 |
53 | ###### Html Structure:
54 |
55 | TBD
56 | Define your callbacks in the invoking controller.
57 |
58 | TBD
59 | That's what all you have to do.
60 |
61 | ##### NG Modules Link:
62 |
63 | If you use this module you can give it a thumbs up at [http://ngmodules.org/modules/angular-multi-select-tree](http://ngmodules.org/modules/angular-multi-select-tree).
64 |
65 | ##### License
66 |
67 | MIT, see [LICENSE.md](./LICENSE.md).
68 |
69 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "a5hik",
3 | "name": "angular-multi-select-tree",
4 | "version": "0.1.0",
5 | "description": "A hierarchical (or tree) selection control for AngularJS",
6 | "main": "index.js",
7 | "homepage": "https://github.com/a5hik/angular-multi-select-tree",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/a5hik/angular-multi-select-tree"
11 | },
12 | "dependencies": {
13 | "angular": "~1.3.x",
14 | "bootstrap-css-only": "~3.2.0"
15 | },
16 | "devDependencies": {
17 | "angular-mocks": "~1.3.x"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | /*jshint undef: false, unused: false, indent: 2*/
2 | /*global angular: false */
3 |
4 | 'use strict';
5 |
6 | var app = angular.module('demoApp', ['multi-select-tree']);
7 |
8 | app.controller('demoAppCtrl', function ($scope) {
9 |
10 | var data1 = [];
11 |
12 | for (var i = 0; i < 7; i++) {
13 | var obj = {
14 | id: i,
15 | name: 'Node ' + i,
16 | children: []
17 | };
18 |
19 | for (var j = 0; j < 3; j++) {
20 | var obj2 = {
21 | id: j,
22 | name: 'Node ' + i + '.' + j,
23 | children: []
24 | };
25 | obj.children.push(obj2);
26 | }
27 |
28 | data1.push(obj);
29 | }
30 |
31 | data1[1].children[0].children.push({
32 | id: j,
33 | name: 'Node sub_sub 1',
34 | children: [],
35 | selected: true
36 | });
37 |
38 | $scope.data = angular.copy(data1);
39 |
40 | var data3 = [];
41 |
42 | for (var i = 0; i < 7; i++) {
43 | var obj3 = {
44 | id: i,
45 | name: 'Node new view ' + i
46 | };
47 | data3.push(obj3);
48 | }
49 |
50 |
51 | $scope.selectOnly1Or2 = function(item, selectedItems) {
52 | if (selectedItems !== undefined && selectedItems.length >= 20) {
53 | return false;
54 | } else {
55 | return true;
56 | }
57 | };
58 |
59 | $scope.switchViewCallback = function(scopeObj) {
60 |
61 | if (scopeObj.switchViewLabel == 'test2') {
62 | scopeObj.switchViewLabel = 'test1';
63 | scopeObj.inputModel = data1;
64 | scopeObj.selectOnlyLeafs = true;
65 | } else {
66 | scopeObj.switchViewLabel = 'test2';
67 | scopeObj.inputModel = data3;
68 | scopeObj.selectOnlyLeafs = false;
69 | }
70 | }
71 | });
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularJS Demo UI Component
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Demo Page for the Angular Multi Select Tree...
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Multi-select
26 |
27 |
28 |
33 |
Selected items: {{selectedItem2}}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/dist/angular-multi-select-tree-0.1.0.css:
--------------------------------------------------------------------------------
1 | .tree-control .tree-input {
2 | position: relative;
3 | display: inline-block;
4 | text-align: center;
5 | cursor: pointer;
6 | border: 1px solid #c6c6c6;
7 | padding: 1px 8px 1px 8px;
8 | font-size: 14px;
9 | min-height : 38px !important;
10 | border-radius: 4px;
11 | color: #555;
12 | -webkit-user-select: none;
13 | -moz-user-select: none;
14 | -ms-user-select: none;
15 | -o-user-select: none;
16 | user-select: none;
17 | white-space:normal;
18 | background-color: #fff;
19 | background-image: linear-gradient(#fff, #f7f7f7);
20 | }
21 |
22 | .tree-control .tree-input:hover {
23 | background-image: linear-gradient(#fff, #e9e9e9);
24 | }
25 |
26 | /* downward pointing arrow */
27 | .tree-control .caret {
28 | display: inline-block;
29 | width: 0;
30 | height: 0;
31 | margin: 0px 0px 1px 12px !important;
32 | vertical-align: middle;
33 | border-top: 4px solid #333;
34 | border-right: 4px solid transparent;
35 | border-left: 4px solid transparent;
36 | border-bottom: 0 dotted;
37 | }
38 |
39 | .tree-control .tree-input span.selected-items .selected-item {
40 | background: #f2f2f2;
41 | border: 1px solid darkgray;
42 | border-radius: 3px;
43 | padding: 3px;
44 | cursor: text;
45 | }
46 |
47 | .tree-control .tree-input span.selected-items .selected-item-close {
48 | width: 20px;
49 | cursor: pointer;
50 | font-weight: bold;
51 | display: inline-block;
52 | padding: 2px;
53 | text-align: center;
54 | }
55 | .tree-control .tree-input span.selected-items .selected-item-close:hover {
56 | background-color: #f2f2f2
57 | }
58 | .tree-control .tree-input span.selected-items .selected-item-close:before {
59 | content: 'x';
60 | }
61 |
62 | .tree-control .tree-view {
63 | background-color: #fff;
64 | position: absolute;
65 | z-index: 999;
66 | border: 1px solid rgba(0, 0, 0, 0.15);
67 | border-radius: 4px;
68 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
69 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
70 | min-width:348px;
71 | margin-right: 30px;
72 | max-height: 300px;
73 | overflow: auto;
74 | padding: 10px 5px;
75 | }
76 | .tree-control .tree-view ul {
77 | padding: 0;
78 | margin: 0;
79 | }
80 | .tree-control .tree-view ul .item-details {
81 | display: inline-block;
82 | margin-left: 5px;
83 | }
84 | .tree-control .tree-view ul .tree-checkbox {
85 | margin-right: 3px;
86 | margin-top: 0;
87 | color: #ddd !important;
88 | cursor: pointer;
89 | }
90 | .tree-control .tree-view .active {
91 | background-color: #f2f2f2;
92 | border-radius: 3px;
93 | }
94 | .tree-control .tree-view .selected.active {
95 | background-color: #46b8da;
96 | }
97 |
98 | /* container of helper elements */
99 | .tree-control .tree-view .helper-container {
100 | border-bottom: 1px solid #ddd;
101 | padding: 8px 8px 0px 8px;
102 | }
103 |
104 | /* container of multi select items */
105 | .tree-control .tree-view .tree-container {
106 | padding: 8px;
107 | }
108 |
109 | .tree-control .tree-view .item-container {
110 | padding: 3px;
111 | color: #444;
112 | white-space: nowrap;
113 | -webkit-user-select: none;
114 | -moz-user-select: none;
115 | -ms-user-select: none;
116 | -o-user-select: none;
117 | user-select: none;
118 | border: 1px solid transparent;
119 | position: relative;
120 | }
121 |
122 | /* item labels focus on mouse hover */
123 | .tree-control .tree-view .item-container:hover {
124 | background-image: linear-gradient( #c1c1c1, #999 ) !important;
125 | color: #fff !important;
126 | cursor: pointer;
127 | border: 1px solid #ccc !important;
128 | }
129 |
130 | .tree-control .tree-view .selected {
131 | background-image: linear-gradient( #e9e9e9, #f1f1f1 );
132 | color: #555;
133 | cursor: pointer;
134 | border-top: 1px solid #e4e4e4;
135 | border-left: 1px solid #e4e4e4;
136 | border-right: 1px solid #d9d9d9;
137 | }
138 |
139 | /* helper buttons (select all, none, reset); */
140 | .tree-control .tree-view .helper-button {
141 | display: inline;
142 | text-align: center;
143 | cursor: pointer;
144 | border: 1px solid #ccc;
145 | height: 26px;
146 | font-size: 13px;
147 | border-radius: 2px;
148 | color: #666;
149 | background-color: #f1f1f1;
150 | line-height: 1.6;
151 | margin: 0px 0px 8px 0px;
152 | }
153 |
154 | /* clear button */
155 | .tree-control .tree-view .clear-button {
156 | position: absolute;
157 | display: inline;
158 | text-align: center;
159 | cursor: pointer;
160 | border: 1px solid #ccc;
161 | height: 22px;
162 | width: 22px;
163 | font-size: 13px;
164 | border-radius: 2px;
165 | color: #666;
166 | background-color: #f1f1f1;
167 | line-height: 1.4;
168 | right : 2px;
169 | top: 2px;
170 | }
171 |
172 | /* filter */
173 | .tree-control .tree-view .input-filter {
174 | border-radius: 2px;
175 | border: 1px solid #ccc;
176 | height: 26px;
177 | font-size: 14px;
178 | width:100%;
179 | padding-left:7px;
180 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
181 | -moz-box-sizing: border-box; /* Firefox, other Gecko */
182 | box-sizing: border-box; /* Opera/IE 8+ */
183 | color: #888;
184 | margin: 0px 0px 8px 0px;
185 | }
186 |
187 | /* helper elements on hover & focus */
188 | .tree-control .tree-view .clear-button:hover,
189 | .tree-control .tree-view .helper-button:hover {
190 | border: 1px solid #ccc;
191 | color: #999;
192 | background-color: #f4f4f4;
193 | }
194 |
195 | .tree-control .tree-view .clear-button:focus,
196 | .tree-control .tree-view .helper-button:focus,
197 | .tree-control .tree-view .input-filter:focus {
198 | border: 1px solid #66AFE9 !important;
199 | box-shadow: inset 0 0px 1px rgba(0,0,0,.035), 0 0 5px rgba(82,168,236,.7) !important;
200 | }
201 |
202 | /* ! create a "row" */
203 | .tree-control .tree-view .line {
204 | max-height: 34px;
205 | overflow: hidden;
206 | position: relative;
207 | }
208 |
209 | .tree-control .tree-view .item-close {
210 | width: 20px;
211 | cursor: pointer;
212 | font-weight: bold;
213 | padding: 5px;
214 | }
215 | .tree-control .tree-view .item-close:hover {
216 | background-color: #f2f2f2
217 | }
218 | .tree-control .tree-view .item-close:before {
219 | content: 'x';
220 | }
221 |
222 | .tree-control .tree-view li {
223 | list-style-type: none;
224 | margin-left: 15px;
225 | }
226 |
227 | .tree-control .tree-view li .expand {
228 | display: inline-block;
229 | width: 0;
230 | height: 0;
231 | border-top: 6px solid transparent;
232 | border-bottom: 6px solid transparent;
233 | border-left: 10px solid #525252;
234 | }
235 | .tree-control .tree-view li .expand-opened {
236 | border: none;
237 | border-left: 6px solid transparent;
238 | border-right: 6px solid transparent;
239 | border-top: 10px solid #525252;
240 | }
241 | .tree-control .tree-view li.top-level {
242 | margin: 0;
243 | }
244 |
--------------------------------------------------------------------------------
/dist/angular-multi-select-tree-0.1.0.js:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2014 Muhammed Ashik
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 | /*jshint indent: 2 */
25 | /*global angular: false */
26 | (function () {
27 | 'use strict';
28 | angular.module('multi-select-tree', []);
29 | }());
30 | /*jshint indent: 2 */
31 | /*global angular: false */
32 | (function () {
33 | 'use strict';
34 | var mainModule = angular.module('multi-select-tree');
35 | /**
36 | * Controller for multi select tree.
37 | */
38 | mainModule.controller('multiSelectTreeCtrl', [
39 | '$scope',
40 | '$document',
41 | function ($scope, $document) {
42 | var activeItem;
43 | $scope.showTree = false;
44 | $scope.selectedItems = [];
45 | $scope.multiSelect = $scope.multiSelect || false;
46 | /**
47 | * Clicking on document will hide the tree.
48 | */
49 | function docClickHide() {
50 | closePopup();
51 | $scope.$apply();
52 | }
53 | /**
54 | * Closes the tree popup.
55 | */
56 | function closePopup() {
57 | $scope.showTree = false;
58 | if (activeItem) {
59 | activeItem.isActive = false;
60 | activeItem = undefined;
61 | }
62 | $document.off('click', docClickHide);
63 | }
64 | /**
65 | * Sets the active item.
66 | *
67 | * @param item the item element.
68 | */
69 | $scope.onActiveItem = function (item) {
70 | if (activeItem !== item) {
71 | if (activeItem) {
72 | activeItem.isActive = false;
73 | }
74 | activeItem = item;
75 | activeItem.isActive = true;
76 | }
77 | };
78 | /**
79 | * Copies the selectedItems in to output model.
80 | */
81 | $scope.refreshOutputModel = function () {
82 | $scope.outputModel = angular.copy($scope.selectedItems);
83 | };
84 | /**
85 | * Refreshes the selected Items model.
86 | */
87 | $scope.refreshSelectedItems = function () {
88 | $scope.selectedItems = [];
89 | if ($scope.inputModel) {
90 | setSelectedChildren($scope.inputModel);
91 | }
92 | };
93 | /**
94 | * Iterates over children and sets the selected items.
95 | *
96 | * @param children the children element.
97 | */
98 | function setSelectedChildren(children) {
99 | for (var i = 0, len = children.length; i < len; i++) {
100 | if (!isItemSelected(children[i]) && children[i].selected === true) {
101 | $scope.selectedItems.push(children[i]);
102 | } else if (isItemSelected(children[i]) && children[i].selected === false) {
103 | children[i].selected = true;
104 | }
105 | if (children[i] && children[i].children) {
106 | setSelectedChildren(children[i].children);
107 | }
108 | }
109 | }
110 | /**
111 | * Checks of the item is already selected.
112 | *
113 | * @param item the item to be checked.
114 | * @return {boolean} if the item is already selected.
115 | */
116 | function isItemSelected(item) {
117 | var isSelected = false;
118 | if ($scope.selectedItems) {
119 | for (var i = 0; i < $scope.selectedItems.length; i++) {
120 | if ($scope.selectedItems[i].id === item.id) {
121 | isSelected = true;
122 | break;
123 | }
124 | }
125 | }
126 | return isSelected;
127 | }
128 | /**
129 | * Deselect the item.
130 | *
131 | * @param item the item element
132 | * @param $event
133 | */
134 | $scope.deselectItem = function (item, $event) {
135 | $event.stopPropagation();
136 | $scope.selectedItems.splice($scope.selectedItems.indexOf(item), 1);
137 | item.selected = false;
138 | this.refreshOutputModel();
139 | };
140 | /**
141 | * Swap the tree popup on control click event.
142 | *
143 | * @param $event the click event.
144 | */
145 | $scope.onControlClicked = function ($event) {
146 | $event.stopPropagation();
147 | $scope.showTree = !$scope.showTree;
148 | if ($scope.showTree) {
149 | $document.on('click', docClickHide);
150 | }
151 | };
152 | /**
153 | * Stop the event on filter clicked.
154 | *
155 | * @param $event the click event
156 | */
157 | $scope.onFilterClicked = function ($event) {
158 | $event.stopPropagation();
159 | };
160 | /**
161 | * Clears the filter text.
162 | *
163 | * @param $event the click event
164 | */
165 | $scope.clearFilter = function ($event) {
166 | $event.stopPropagation();
167 | $scope.filterKeyword = '';
168 | };
169 | /**
170 | * Wrapper function for can select item callback.
171 | *
172 | * @param item the item
173 | */
174 | $scope.canSelectItem = function (item) {
175 | return $scope.callback({
176 | item: item,
177 | selectedItems: $scope.selectedItems
178 | });
179 | };
180 | /**
181 | * The callback is used to switch the views.
182 | * based on the view type.
183 | *
184 | * @param $event the event object.
185 | */
186 | $scope.switchCurrentView = function ($event) {
187 | $event.stopPropagation();
188 | $scope.switchViewCallback({ scopeObj: $scope });
189 | };
190 | /**
191 | * Handles the item select event.
192 | *
193 | * @param item the selected item.
194 | */
195 | $scope.itemSelected = function (item) {
196 | if ($scope.useCallback && $scope.canSelectItem(item) === false || $scope.selectOnlyLeafs && item.children && item.children.length > 0) {
197 | return;
198 | }
199 | if (!$scope.multiSelect) {
200 | closePopup();
201 | for (var i = 0; i < $scope.selectedItems.length; i++) {
202 | $scope.selectedItems[i].selected = false;
203 | }
204 | item.selected = true;
205 | $scope.selectedItems = [];
206 | $scope.selectedItems.push(item);
207 | } else {
208 | item.selected = true;
209 | var indexOfItem = $scope.selectedItems.indexOf(item);
210 | if (isItemSelected(item)) {
211 | item.selected = false;
212 | $scope.selectedItems.splice(indexOfItem, 1);
213 | } else {
214 | $scope.selectedItems.push(item);
215 | }
216 | }
217 | this.refreshOutputModel();
218 | };
219 | }
220 | ]);
221 | /**
222 | * sortableItem directive.
223 | */
224 | mainModule.directive('multiSelectTree', function () {
225 | return {
226 | restrict: 'E',
227 | templateUrl: 'src/multi-select-tree.tpl.html',
228 | scope: {
229 | inputModel: '=',
230 | outputModel: '=?',
231 | multiSelect: '=?',
232 | switchView: '=?',
233 | switchViewLabel: '@',
234 | switchViewCallback: '&',
235 | selectOnlyLeafs: '=?',
236 | callback: '&',
237 | defaultLabel: '@'
238 | },
239 | link: function (scope, element, attrs) {
240 | if (attrs.callback) {
241 | scope.useCallback = true;
242 | }
243 | // watch for changes in input model as a whole
244 | // this on updates the multi-select when a user load a whole new input-model.
245 | scope.$watch('inputModel', function (newVal) {
246 | if (newVal) {
247 | scope.refreshSelectedItems();
248 | scope.refreshOutputModel();
249 | }
250 | });
251 | /**
252 | * Checks whether any of children match the keyword.
253 | *
254 | * @param item the parent item
255 | * @param keyword the filter keyword
256 | * @returns {boolean} false if matches.
257 | */
258 | function isChildrenFiltered(item, keyword) {
259 | var childNodes = getAllChildNodesFromNode(item, []);
260 | for (var i = 0, len = childNodes.length; i < len; i++) {
261 | if (childNodes[i].name.toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
262 | return false;
263 | }
264 | }
265 | return true;
266 | }
267 | /**
268 | * Return all childNodes of a given node (as Array of Nodes)
269 | */
270 | function getAllChildNodesFromNode(node, childNodes) {
271 | for (var i = 0; i < node.children.length; i++) {
272 | childNodes.push(node.children[i]);
273 | // add the childNodes from the children if available
274 | getAllChildNodesFromNode(node.children[i], childNodes);
275 | }
276 | return childNodes;
277 | }
278 | scope.$watch('filterKeyword', function () {
279 | if (scope.filterKeyword !== undefined) {
280 | angular.forEach(scope.inputModel, function (item) {
281 | if (item.name.toLowerCase().indexOf(scope.filterKeyword.toLowerCase()) !== -1) {
282 | item.isFiltered = false;
283 | } else if (!isChildrenFiltered(item, scope.filterKeyword)) {
284 | item.isFiltered = false;
285 | } else {
286 | item.isFiltered = true;
287 | }
288 | });
289 | }
290 | });
291 | },
292 | controller: 'multiSelectTreeCtrl'
293 | };
294 | });
295 | }());
296 | /*jshint indent: 2 */
297 | /*global angular: false */
298 | (function () {
299 | 'use strict';
300 | var mainModule = angular.module('multi-select-tree');
301 | /**
302 | * Controller for sortable item.
303 | *
304 | * @param $scope - drag item scope
305 | */
306 | mainModule.controller('treeItemCtrl', [
307 | '$scope',
308 | function ($scope) {
309 | $scope.item.isExpanded = false;
310 | /**
311 | * Shows the expand option.
312 | *
313 | * @param item the item
314 | * @returns {*|boolean}
315 | */
316 | $scope.showExpand = function (item) {
317 | return item.children && item.children.length > 0;
318 | };
319 | /**
320 | * On expand clicked toggle the option.
321 | *
322 | * @param item the item
323 | * @param $event
324 | */
325 | $scope.onExpandClicked = function (item, $event) {
326 | $event.stopPropagation();
327 | item.isExpanded = !item.isExpanded;
328 | };
329 | /**
330 | * Event on click of select item.
331 | *
332 | * @param item the item
333 | * @param $event
334 | */
335 | $scope.clickSelectItem = function (item, $event) {
336 | $event.stopPropagation();
337 | if ($scope.itemSelected) {
338 | $scope.itemSelected({ item: item });
339 | }
340 | };
341 | /**
342 | * Is leaf selected.
343 | *
344 | * @param item the item
345 | * @param $event
346 | */
347 | $scope.subItemSelected = function (item, $event) {
348 | if ($scope.itemSelected) {
349 | $scope.itemSelected({ item: item });
350 | }
351 | };
352 | /**
353 | * Active sub item.
354 | *
355 | * @param item the item
356 | * @param $event
357 | */
358 | $scope.activeSubItem = function (item, $event) {
359 | if ($scope.onActiveItem) {
360 | $scope.onActiveItem({ item: item });
361 | }
362 | };
363 | /**
364 | * On mouse over event.
365 | *
366 | * @param item the item
367 | * @param $event
368 | */
369 | $scope.onMouseOver = function (item, $event) {
370 | $event.stopPropagation();
371 | if ($scope.onActiveItem) {
372 | $scope.onActiveItem({ item: item });
373 | }
374 | };
375 | /**
376 | * Can select item.
377 | *
378 | * @returns {*}
379 | */
380 | $scope.showCheckbox = function () {
381 | if (!$scope.multiSelect) {
382 | return false;
383 | }
384 | if ($scope.selectOnlyLeafs) {
385 | return false;
386 | }
387 | if ($scope.useCallback) {
388 | return $scope.canSelectItem($scope.item);
389 | }
390 | };
391 | }
392 | ]);
393 | /**
394 | * sortableItem directive.
395 | */
396 | mainModule.directive('treeItem', [
397 | '$compile',
398 | function ($compile) {
399 | return {
400 | restrict: 'E',
401 | templateUrl: 'src/tree-item.tpl.html',
402 | scope: {
403 | item: '=',
404 | itemSelected: '&',
405 | onActiveItem: '&',
406 | multiSelect: '=?',
407 | selectOnlyLeafs: '=?',
408 | isActive: '=',
409 | useCallback: '=',
410 | canSelectItem: '='
411 | },
412 | controller: 'treeItemCtrl',
413 | compile: function (element, attrs, link) {
414 | // Normalize the link parameter
415 | if (angular.isFunction(link)) {
416 | link = { post: link };
417 | }
418 | // Break the recursion loop by removing the contents
419 | var contents = element.contents().remove();
420 | var compiledContents;
421 | return {
422 | pre: link && link.pre ? link.pre : null,
423 | post: function (scope, element, attrs) {
424 | // Compile the contents
425 | if (!compiledContents) {
426 | compiledContents = $compile(contents);
427 | }
428 | // Re-add the compiled contents to the element
429 | compiledContents(scope, function (clone) {
430 | element.append(clone);
431 | });
432 | // Call the post-linking function, if any
433 | if (link && link.post) {
434 | link.post.apply(null, arguments);
435 | }
436 | }
437 | };
438 | }
439 | };
440 | }
441 | ]);
442 | }());
--------------------------------------------------------------------------------
/dist/angular-multi-select-tree-0.1.0.min.css:
--------------------------------------------------------------------------------
1 | .tree-control .tree-input{position:relative;display:inline-block;text-align:center;cursor:pointer;border:1px solid #c6c6c6;padding:1px 8px;font-size:14px;min-height:38px!important;border-radius:4px;color:#555;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;white-space:normal;background-color:#fff;background-image:linear-gradient(#fff,#f7f7f7)}.tree-control .tree-input:hover{background-image:linear-gradient(#fff,#e9e9e9)}.tree-control .caret{display:inline-block;width:0;height:0;margin:0 0 1px 12px!important;vertical-align:middle;border-top:4px solid #333;border-right:4px solid transparent;border-left:4px solid transparent;border-bottom:0 dotted}.tree-control .tree-input span.selected-items .selected-item{background:#f2f2f2;border:1px solid #a9a9a9;border-radius:3px;padding:3px;cursor:text}.tree-control .tree-input span.selected-items .selected-item-close{width:20px;cursor:pointer;font-weight:700;display:inline-block;padding:2px;text-align:center}.tree-control .tree-input span.selected-items .selected-item-close:hover{background-color:#f2f2f2}.tree-control .tree-input span.selected-items .selected-item-close:before{content:'x'}.tree-control .tree-view{background-color:#fff;position:absolute;z-index:999;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);min-width:348px;margin-right:30px;max-height:300px;overflow:auto;padding:10px 5px}.tree-control .tree-view ul{padding:0;margin:0}.tree-control .tree-view ul .item-details{display:inline-block;margin-left:5px}.tree-control .tree-view ul .tree-checkbox{margin-right:3px;margin-top:0;color:#ddd!important;cursor:pointer}.tree-control .tree-view .active{background-color:#f2f2f2;border-radius:3px}.tree-control .tree-view .selected.active{background-color:#46b8da}.tree-control .tree-view .helper-container{border-bottom:1px solid #ddd;padding:8px 8px 0}.tree-control .tree-view .tree-container{padding:8px}.tree-control .tree-view .item-container{padding:3px;color:#444;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;border:1px solid transparent;position:relative}.tree-control .tree-view .item-container:hover{background-image:linear-gradient(#c1c1c1,#999)!important;color:#fff!important;cursor:pointer;border:1px solid #ccc!important}.tree-control .tree-view .selected{background-image:linear-gradient(#e9e9e9,#f1f1f1);color:#555;cursor:pointer;border-top:1px solid #e4e4e4;border-left:1px solid #e4e4e4;border-right:1px solid #d9d9d9}.tree-control .tree-view .helper-button{display:inline;text-align:center;cursor:pointer;border:1px solid #ccc;height:26px;font-size:13px;border-radius:2px;color:#666;background-color:#f1f1f1;line-height:1.6;margin:0 0 8px}.tree-control .tree-view .clear-button{position:absolute;display:inline;text-align:center;cursor:pointer;border:1px solid #ccc;height:22px;width:22px;font-size:13px;border-radius:2px;color:#666;background-color:#f1f1f1;line-height:1.4;right:2px;top:2px}.tree-control .tree-view .input-filter{border-radius:2px;border:1px solid #ccc;height:26px;font-size:14px;width:100%;padding-left:7px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;color:#888;margin:0 0 8px}.tree-control .tree-view .clear-button:hover,.tree-control .tree-view .helper-button:hover{border:1px solid #ccc;color:#999;background-color:#f4f4f4}.tree-control .tree-view .clear-button:focus,.tree-control .tree-view .helper-button:focus,.tree-control .tree-view .input-filter:focus{border:1px solid #66AFE9!important;box-shadow:inset 0 0 1px rgba(0,0,0,.035),0 0 5px rgba(82,168,236,.7)!important}.tree-control .tree-view .line{max-height:34px;overflow:hidden;position:relative}.tree-control .tree-view .item-close{width:20px;cursor:pointer;font-weight:700;padding:5px}.tree-control .tree-view .item-close:hover{background-color:#f2f2f2}.tree-control .tree-view .item-close:before{content:'x'}.tree-control .tree-view li{list-style-type:none;margin-left:15px}.tree-control .tree-view li .expand{display:inline-block;width:0;height:0;border-top:6px solid transparent;border-bottom:6px solid transparent;border-left:10px solid #525252}.tree-control .tree-view li .expand-opened{border:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:10px solid #525252}.tree-control .tree-view li.top-level{margin:0}
--------------------------------------------------------------------------------
/dist/angular-multi-select-tree-0.1.0.min.js:
--------------------------------------------------------------------------------
1 | !function(){"use strict";angular.module("multi-select-tree",[])}(),function(){"use strict";var a=angular.module("multi-select-tree");a.controller("multiSelectTreeCtrl",["$scope","$document",function(a,b){function c(){d(),a.$apply()}function d(){a.showTree=!1,g&&(g.isActive=!1,g=void 0),b.off("click",c)}function e(b){for(var c=0,d=b.length;d>c;c++)f(b[c])||b[c].selected!==!0?f(b[c])&&b[c].selected===!1&&(b[c].selected=!0):a.selectedItems.push(b[c]),b[c]&&b[c].children&&e(b[c].children)}function f(b){var c=!1;if(a.selectedItems)for(var d=0;d0)){if(a.multiSelect){b.selected=!0;var c=a.selectedItems.indexOf(b);f(b)?(b.selected=!1,a.selectedItems.splice(c,1)):a.selectedItems.push(b)}else{d();for(var e=0;ed;d++)if(-1!==c[d].name.toLowerCase().indexOf(b.toLowerCase()))return!1;return!0}function e(a,b){for(var c=0;c0},a.onExpandClicked=function(a,b){b.stopPropagation(),a.isExpanded=!a.isExpanded},a.clickSelectItem=function(b,c){c.stopPropagation(),a.itemSelected&&a.itemSelected({item:b})},a.subItemSelected=function(b){a.itemSelected&&a.itemSelected({item:b})},a.activeSubItem=function(b){a.onActiveItem&&a.onActiveItem({item:b})},a.onMouseOver=function(b,c){c.stopPropagation(),a.onActiveItem&&a.onActiveItem({item:b})},a.showCheckbox=function(){return a.multiSelect?a.selectOnlyLeafs?!1:a.useCallback?a.canSelectItem(a.item):void 0:!1}}]),a.directive("treeItem",["$compile",function(a){return{restrict:"E",templateUrl:"src/tree-item.tpl.html",scope:{item:"=",itemSelected:"&",onActiveItem:"&",multiSelect:"=?",selectOnlyLeafs:"=?",isActive:"=",useCallback:"=",canSelectItem:"="},controller:"treeItemCtrl",compile:function(b,c,d){angular.isFunction(d)&&(d={post:d});var e,f=b.contents().remove();return{pre:d&&d.pre?d.pre:null,post:function(b,c){e||(e=a(f)),e(b,function(a){c.append(a)}),d&&d.post&&d.post.apply(null,arguments)}}}}}])}();
--------------------------------------------------------------------------------
/dist/angular-multi-select-tree-0.1.0.tpl.js:
--------------------------------------------------------------------------------
1 | angular.module('multi-select-tree').run(['$templateCache', function($templateCache) {
2 | 'use strict';
3 |
4 | $templateCache.put('src/multi-select-tree.tpl.html',
5 | "\n" +
6 | "\n" +
7 | "
\n" +
8 | " \n" +
9 | " \n" +
10 | " \n" +
11 | " 0\" class=\"selected-items\">\n" +
12 | " {{selectedItem.name}} \n" +
14 | " \n" +
15 | " \n" +
16 | " \n" +
17 | "
\n" +
18 | "
\n" +
19 | "
\n" +
20 | "
\n" +
21 | " \n" +
22 | "
\n" +
23 | "
\n" +
24 | " \n" +
26 | " \n" +
27 | "
\n" +
28 | "
\n" +
29 | "
\n" +
30 | " \n" +
34 | "
\n" +
35 | "
\n" +
36 | "
\n"
37 | );
38 |
39 |
40 | $templateCache.put('src/tree-item.tpl.html',
41 | "\n" +
42 | " \n" +
44 | "
\n" +
46 | "\n" +
47 | "
{{item.name}}\n" +
49 | "
\n" +
50 | "
\n" +
51 | " \n" +
52 | " \n" +
55 | "
\n" +
56 | "\n"
57 | );
58 |
59 | }]);
60 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | 'use strict';
3 |
4 | config.set({
5 |
6 | // base path, that will be used to resolve files and exclude
7 | basePath: '',
8 |
9 |
10 | // list of files / patterns to load in the browser
11 | files: [
12 | 'bower_components/angular/angular.js',
13 | 'bower_components/angular-mocks/angular-mocks.js',
14 | 'src/**/*.js',
15 | 'test/**/*spec.js'
16 | ],
17 |
18 | frameworks: ['jasmine'],
19 |
20 |
21 | // list of files to exclude
22 | exclude: [
23 |
24 | ],
25 |
26 |
27 | // test results reporter to use
28 | // possible values: 'dots', 'progress', 'junit'
29 | reporters: ['progress'],
30 |
31 | // web server port
32 | port: 9876,
33 |
34 |
35 | // cli runner port
36 | runnerPort: 9100,
37 |
38 |
39 | // enable / disable colors in the output (reporters and logs)
40 | colors: true,
41 |
42 |
43 | // level of logging
44 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
45 | logLevel: config.LOG_INFO,
46 |
47 |
48 | // enable / disable watching file and executing tests whenever any file changes
49 | autoWatch: true,
50 |
51 |
52 | // Start these browsers, currently available:
53 | // - Chrome
54 | // - ChromeCanary
55 | // - Firefox
56 | // - Opera
57 | // - Safari (only Mac)
58 | // - PhantomJS
59 | // - IE (only Windows)
60 | browsers: ['Chrome'],
61 |
62 |
63 | // If browser does not capture in given timeout [ms], kill it
64 | captureTimeout: 60000,
65 |
66 |
67 | // Continuous Integration mode
68 | // if true, it capture browsers, run tests and exit
69 | singleRun: false
70 | });
71 | };
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-multi-select-tree",
3 | "version": "0.1.0",
4 | "description": "A hierarchical (or tree) selection control for AngularJS",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/a5hik/angular-multi-select-tree"
15 | },
16 | "keywords": [
17 | "tree",
18 | "control",
19 | "hierarchical",
20 | "hierarchy",
21 | "selection",
22 | "angualr",
23 | "angularjs"
24 | ],
25 | "author": "Muhammed Ashik",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/a5hik/angular-multi-select-tree/issues"
29 | },
30 | "homepage": "https://github.com/a5hik/angular-multi-select-tree",
31 | "devDependencies": {
32 | "grunt": "~0.4.2",
33 | "grunt-contrib-watch": "~0.5.3",
34 | "grunt-contrib-concat": "~0.3.x",
35 | "grunt-contrib-copy": "~0.4.1",
36 | "grunt-contrib-uglify": "~0.2.x",
37 | "grunt-contrib-jshint": "~0.4.x",
38 | "grunt-contrib-cssmin": "~0.7.0",
39 | "grunt-contrib-clean": "~0.5.0",
40 | "grunt-contrib-connect": "~0.5.0",
41 | "grunt-angular-templates": "~0.5.7",
42 | "grunt-open": "~0.2.3",
43 | "grunt-karma": "~0.7.x",
44 | "karma-jasmine": "~0.2.2",
45 | "karma-chrome-launcher": "~0.1",
46 | "grunt-conventional-changelog": "~1.0.x",
47 | "grunt-ngmin": "~0.0.3",
48 | "load-grunt-tasks": "~0.2.0",
49 | "lodash": "~2.4.x"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/multi-select-tree-main.js:
--------------------------------------------------------------------------------
1 | /*
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2014 Muhammed Ashik
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | */
24 |
25 | /*jshint indent: 2 */
26 | /*global angular: false */
27 |
28 | (function () {
29 | 'use strict';
30 | angular.module('multi-select-tree', []);
31 | }());
32 |
--------------------------------------------------------------------------------
/src/multi-select-tree.js:
--------------------------------------------------------------------------------
1 | /*jshint indent: 2 */
2 | /*global angular: false */
3 |
4 | (function () {
5 |
6 | 'use strict';
7 | var mainModule = angular.module('multi-select-tree');
8 |
9 | /**
10 | * Controller for multi select tree.
11 | */
12 | mainModule.controller('multiSelectTreeCtrl', ['$scope', '$document', function ($scope, $document) {
13 |
14 | var activeItem;
15 |
16 | $scope.showTree = false;
17 | $scope.selectedItems = [];
18 | $scope.multiSelect = $scope.multiSelect || false;
19 |
20 | /**
21 | * Clicking on document will hide the tree.
22 | */
23 | function docClickHide() {
24 | closePopup();
25 | $scope.$apply();
26 | }
27 |
28 | /**
29 | * Closes the tree popup.
30 | */
31 | function closePopup() {
32 | $scope.showTree = false;
33 | if (activeItem) {
34 | activeItem.isActive = false;
35 | activeItem = undefined;
36 | }
37 | $document.off('click', docClickHide);
38 | }
39 |
40 | /**
41 | * Sets the active item.
42 | *
43 | * @param item the item element.
44 | */
45 | $scope.onActiveItem = function (item) {
46 | if (activeItem !== item) {
47 | if (activeItem) {
48 | activeItem.isActive = false;
49 | }
50 | activeItem = item;
51 | activeItem.isActive = true;
52 | }
53 | };
54 |
55 | /**
56 | * Copies the selectedItems in to output model.
57 | */
58 | $scope.refreshOutputModel = function () {
59 | $scope.outputModel = angular.copy($scope.selectedItems);
60 | };
61 |
62 | /**
63 | * Refreshes the selected Items model.
64 | */
65 | $scope.refreshSelectedItems = function () {
66 | $scope.selectedItems = [];
67 | if ($scope.inputModel) {
68 | setSelectedChildren($scope.inputModel);
69 | }
70 | };
71 |
72 | /**
73 | * Iterates over children and sets the selected items.
74 | *
75 | * @param children the children element.
76 | */
77 | function setSelectedChildren(children) {
78 | for (var i = 0, len = children.length; i < len; i++) {
79 | if (!isItemSelected(children[i]) && children[i].selected === true) {
80 | $scope.selectedItems.push(children[i]);
81 | } else if (isItemSelected(children[i]) && children[i].selected === false) {
82 | children[i].selected = true;
83 | }
84 | if (children[i] && children[i].children) {
85 | setSelectedChildren(children[i].children);
86 | }
87 | }
88 | }
89 | /**
90 | * Checks of the item is already selected.
91 | *
92 | * @param item the item to be checked.
93 | * @return {boolean} if the item is already selected.
94 | */
95 | function isItemSelected(item) {
96 | var isSelected = false;
97 | if ($scope.selectedItems) {
98 | for (var i = 0; i < $scope.selectedItems.length; i++) {
99 | if ($scope.selectedItems[i].id === item.id) {
100 | isSelected = true;
101 | break;
102 | }
103 | }
104 | }
105 | return isSelected;
106 | }
107 |
108 | /**
109 | * Deselect the item.
110 | *
111 | * @param item the item element
112 | * @param $event
113 | */
114 | $scope.deselectItem = function (item, $event) {
115 | $event.stopPropagation();
116 | $scope.selectedItems.splice($scope.selectedItems.indexOf(item), 1);
117 | item.selected = false;
118 | this.refreshOutputModel();
119 | };
120 |
121 | /**
122 | * Swap the tree popup on control click event.
123 | *
124 | * @param $event the click event.
125 | */
126 | $scope.onControlClicked = function ($event) {
127 | $event.stopPropagation();
128 | $scope.showTree = !$scope.showTree;
129 | if ($scope.showTree) {
130 | $document.on('click', docClickHide);
131 | }
132 | };
133 |
134 | /**
135 | * Stop the event on filter clicked.
136 | *
137 | * @param $event the click event
138 | */
139 | $scope.onFilterClicked = function ($event) {
140 | $event.stopPropagation();
141 | };
142 |
143 | /**
144 | * Clears the filter text.
145 | *
146 | * @param $event the click event
147 | */
148 | $scope.clearFilter = function ($event) {
149 | $event.stopPropagation();
150 | $scope.filterKeyword = '';
151 | };
152 |
153 | /**
154 | * Wrapper function for can select item callback.
155 | *
156 | * @param item the item
157 | */
158 | $scope.canSelectItem = function (item) {
159 | return $scope.callback({item: item, selectedItems: $scope.selectedItems});
160 | };
161 |
162 | /**
163 | * The callback is used to switch the views.
164 | * based on the view type.
165 | *
166 | * @param $event the event object.
167 | */
168 | $scope.switchCurrentView = function($event) {
169 | $event.stopPropagation();
170 | $scope.switchViewCallback({scopeObj:$scope});
171 | };
172 |
173 | /**
174 | * Handles the item select event.
175 | *
176 | * @param item the selected item.
177 | */
178 | $scope.itemSelected = function (item) {
179 | if (($scope.useCallback && $scope.canSelectItem(item) === false) ||
180 | ($scope.selectOnlyLeafs && item.children && item.children.length > 0)) {
181 | return;
182 | }
183 |
184 | if (!$scope.multiSelect) {
185 | closePopup();
186 | for (var i = 0; i < $scope.selectedItems.length; i++) {
187 | $scope.selectedItems[i].selected = false;
188 | }
189 | item.selected = true;
190 | $scope.selectedItems = [];
191 | $scope.selectedItems.push(item);
192 | } else {
193 | item.selected = true;
194 | var indexOfItem = $scope.selectedItems.indexOf(item);
195 | if (isItemSelected(item)) {
196 | item.selected = false;
197 | $scope.selectedItems.splice(indexOfItem, 1);
198 | } else {
199 | $scope.selectedItems.push(item);
200 | }
201 | }
202 | this.refreshOutputModel();
203 | };
204 |
205 | }]);
206 |
207 | /**
208 | * sortableItem directive.
209 | */
210 | mainModule.directive('multiSelectTree',
211 | function () {
212 | return {
213 | restrict: 'E',
214 | templateUrl: 'src/multi-select-tree.tpl.html',
215 | scope: {
216 | inputModel: '=',
217 | outputModel: '=?',
218 | multiSelect: '=?',
219 | switchView: '=?',
220 | switchViewLabel: '@',
221 | switchViewCallback: '&',
222 | selectOnlyLeafs: '=?',
223 | callback: '&',
224 | defaultLabel: '@'
225 | },
226 | link: function (scope, element, attrs) {
227 | if (attrs.callback) {
228 | scope.useCallback = true;
229 | }
230 |
231 | // watch for changes in input model as a whole
232 | // this on updates the multi-select when a user load a whole new input-model.
233 | scope.$watch('inputModel', function (newVal) {
234 | if (newVal) {
235 | scope.refreshSelectedItems();
236 | scope.refreshOutputModel();
237 | }
238 | });
239 |
240 | /**
241 | * Checks whether any of children match the keyword.
242 | *
243 | * @param item the parent item
244 | * @param keyword the filter keyword
245 | * @returns {boolean} false if matches.
246 | */
247 | function isChildrenFiltered(item, keyword) {
248 | var childNodes = getAllChildNodesFromNode(item, []);
249 | for (var i = 0, len = childNodes.length; i < len; i++) {
250 | if (childNodes[i].name.toLowerCase().indexOf(keyword.toLowerCase()) !== -1) {
251 | return false;
252 | }
253 | }
254 | return true;
255 | }
256 |
257 | /**
258 | * Return all childNodes of a given node (as Array of Nodes)
259 | */
260 | function getAllChildNodesFromNode(node, childNodes) {
261 | for (var i = 0; i < node.children.length; i++) {
262 | childNodes.push(node.children[i]);
263 | // add the childNodes from the children if available
264 | getAllChildNodesFromNode(node.children[i], childNodes);
265 | }
266 | return childNodes;
267 | }
268 |
269 | scope.$watch('filterKeyword', function () {
270 | if (scope.filterKeyword !== undefined) {
271 | angular.forEach(scope.inputModel, function (item) {
272 | if (item.name.toLowerCase().indexOf(scope.filterKeyword.toLowerCase()) !== -1) {
273 | item.isFiltered = false;
274 | } else if (!isChildrenFiltered(item, scope.filterKeyword)) {
275 | item.isFiltered = false;
276 | } else {
277 | item.isFiltered = true;
278 | }
279 | });
280 | }
281 | });
282 | },
283 | controller: 'multiSelectTreeCtrl'
284 | };
285 | });
286 | }());
--------------------------------------------------------------------------------
/src/multi-select-tree.less:
--------------------------------------------------------------------------------
1 | .tree-control .tree-input {
2 | position: relative;
3 | display: inline-block;
4 | text-align: center;
5 | cursor: pointer;
6 | border: 1px solid #c6c6c6;
7 | padding: 1px 8px 1px 8px;
8 | font-size: 14px;
9 | min-height : 38px !important;
10 | border-radius: 4px;
11 | color: #555;
12 | -webkit-user-select: none;
13 | -moz-user-select: none;
14 | -ms-user-select: none;
15 | -o-user-select: none;
16 | user-select: none;
17 | white-space:normal;
18 | background-color: #fff;
19 | background-image: linear-gradient(#fff, #f7f7f7);
20 | }
21 |
22 | .tree-control .tree-input:hover {
23 | background-image: linear-gradient(#fff, #e9e9e9);
24 | }
25 |
26 | /* downward pointing arrow */
27 | .tree-control .caret {
28 | display: inline-block;
29 | width: 0;
30 | height: 0;
31 | margin: 0px 0px 1px 12px !important;
32 | vertical-align: middle;
33 | border-top: 4px solid #333;
34 | border-right: 4px solid transparent;
35 | border-left: 4px solid transparent;
36 | border-bottom: 0 dotted;
37 | }
38 |
39 | .tree-control .tree-input span.selected-items .selected-item {
40 | background: #f2f2f2;
41 | border: 1px solid darkgray;
42 | border-radius: 3px;
43 | padding: 3px;
44 | cursor: text;
45 | }
46 |
47 | .tree-control .tree-input span.selected-items .selected-item-close {
48 | width: 20px;
49 | cursor: pointer;
50 | font-weight: bold;
51 | display: inline-block;
52 | padding: 2px;
53 | text-align: center;
54 | }
55 | .tree-control .tree-input span.selected-items .selected-item-close:hover {
56 | background-color: #f2f2f2
57 | }
58 | .tree-control .tree-input span.selected-items .selected-item-close:before {
59 | content: 'x';
60 | }
61 |
62 | .tree-control .tree-view {
63 | background-color: #fff;
64 | position: absolute;
65 | z-index: 999;
66 | border: 1px solid rgba(0, 0, 0, 0.15);
67 | border-radius: 4px;
68 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
69 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
70 | min-width:348px;
71 | margin-right: 30px;
72 | max-height: 300px;
73 | overflow: auto;
74 | padding: 10px 5px;
75 | }
76 | .tree-control .tree-view ul {
77 | padding: 0;
78 | margin: 0;
79 | }
80 | .tree-control .tree-view ul .item-details {
81 | display: inline-block;
82 | margin-left: 5px;
83 | }
84 | .tree-control .tree-view ul .tree-checkbox {
85 | margin-right: 3px;
86 | margin-top: 0;
87 | color: #ddd !important;
88 | cursor: pointer;
89 | }
90 | .tree-control .tree-view .active {
91 | background-color: #f2f2f2;
92 | border-radius: 3px;
93 | }
94 | .tree-control .tree-view .selected.active {
95 | background-color: #46b8da;
96 | }
97 |
98 | /* container of helper elements */
99 | .tree-control .tree-view .helper-container {
100 | border-bottom: 1px solid #ddd;
101 | padding: 8px 8px 0px 8px;
102 | }
103 |
104 | /* container of multi select items */
105 | .tree-control .tree-view .tree-container {
106 | padding: 8px;
107 | }
108 |
109 | .tree-control .tree-view .item-container {
110 | padding: 3px;
111 | color: #444;
112 | white-space: nowrap;
113 | -webkit-user-select: none;
114 | -moz-user-select: none;
115 | -ms-user-select: none;
116 | -o-user-select: none;
117 | user-select: none;
118 | border: 1px solid transparent;
119 | position: relative;
120 | }
121 |
122 | /* item labels focus on mouse hover */
123 | .tree-control .tree-view .item-container:hover {
124 | background-image: linear-gradient( #c1c1c1, #999 ) !important;
125 | color: #fff !important;
126 | cursor: pointer;
127 | border: 1px solid #ccc !important;
128 | }
129 |
130 | .tree-control .tree-view .selected {
131 | background-image: linear-gradient( #e9e9e9, #f1f1f1 );
132 | color: #555;
133 | cursor: pointer;
134 | border-top: 1px solid #e4e4e4;
135 | border-left: 1px solid #e4e4e4;
136 | border-right: 1px solid #d9d9d9;
137 | }
138 |
139 | /* helper buttons (select all, none, reset); */
140 | .tree-control .tree-view .helper-button {
141 | display: inline;
142 | text-align: center;
143 | cursor: pointer;
144 | border: 1px solid #ccc;
145 | height: 26px;
146 | font-size: 13px;
147 | border-radius: 2px;
148 | color: #666;
149 | background-color: #f1f1f1;
150 | line-height: 1.6;
151 | margin: 0px 0px 8px 0px;
152 | }
153 |
154 | /* clear button */
155 | .tree-control .tree-view .clear-button {
156 | position: absolute;
157 | display: inline;
158 | text-align: center;
159 | cursor: pointer;
160 | border: 1px solid #ccc;
161 | height: 22px;
162 | width: 22px;
163 | font-size: 13px;
164 | border-radius: 2px;
165 | color: #666;
166 | background-color: #f1f1f1;
167 | line-height: 1.4;
168 | right : 2px;
169 | top: 2px;
170 | }
171 |
172 | /* filter */
173 | .tree-control .tree-view .input-filter {
174 | border-radius: 2px;
175 | border: 1px solid #ccc;
176 | height: 26px;
177 | font-size: 14px;
178 | width:100%;
179 | padding-left:7px;
180 | -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
181 | -moz-box-sizing: border-box; /* Firefox, other Gecko */
182 | box-sizing: border-box; /* Opera/IE 8+ */
183 | color: #888;
184 | margin: 0px 0px 8px 0px;
185 | }
186 |
187 | /* helper elements on hover & focus */
188 | .tree-control .tree-view .clear-button:hover,
189 | .tree-control .tree-view .helper-button:hover {
190 | border: 1px solid #ccc;
191 | color: #999;
192 | background-color: #f4f4f4;
193 | }
194 |
195 | .tree-control .tree-view .clear-button:focus,
196 | .tree-control .tree-view .helper-button:focus,
197 | .tree-control .tree-view .input-filter:focus {
198 | border: 1px solid #66AFE9 !important;
199 | box-shadow: inset 0 0px 1px rgba(0,0,0,.035), 0 0 5px rgba(82,168,236,.7) !important;
200 | }
201 |
202 | /* ! create a "row" */
203 | .tree-control .tree-view .line {
204 | max-height: 34px;
205 | overflow: hidden;
206 | position: relative;
207 | }
208 |
209 | .tree-control .tree-view .item-close {
210 | width: 20px;
211 | cursor: pointer;
212 | font-weight: bold;
213 | padding: 5px;
214 | }
215 | .tree-control .tree-view .item-close:hover {
216 | background-color: #f2f2f2
217 | }
218 | .tree-control .tree-view .item-close:before {
219 | content: 'x';
220 | }
221 |
222 | .tree-control .tree-view li {
223 | list-style-type: none;
224 | margin-left: 15px;
225 | }
226 |
227 | .tree-control .tree-view li .expand {
228 | display: inline-block;
229 | width: 0;
230 | height: 0;
231 | border-top: 6px solid transparent;
232 | border-bottom: 6px solid transparent;
233 | border-left: 10px solid #525252;
234 | }
235 | .tree-control .tree-view li .expand-opened {
236 | border: none;
237 | border-left: 6px solid transparent;
238 | border-right: 6px solid transparent;
239 | border-top: 10px solid #525252;
240 | }
241 | .tree-control .tree-view li.top-level {
242 | margin: 0;
243 | }
244 |
--------------------------------------------------------------------------------
/src/multi-select-tree.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{selectedItem.name}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/tree-item.js:
--------------------------------------------------------------------------------
1 | /*jshint indent: 2 */
2 | /*global angular: false */
3 |
4 | (function () {
5 |
6 | 'use strict';
7 | var mainModule = angular.module('multi-select-tree');
8 |
9 | /**
10 | * Controller for sortable item.
11 | *
12 | * @param $scope - drag item scope
13 | */
14 | mainModule.controller('treeItemCtrl', ['$scope', function ($scope) {
15 |
16 | $scope.item.isExpanded = false;
17 |
18 | /**
19 | * Shows the expand option.
20 | *
21 | * @param item the item
22 | * @returns {*|boolean}
23 | */
24 | $scope.showExpand = function (item) {
25 | return item.children && item.children.length > 0;
26 | };
27 |
28 | /**
29 | * On expand clicked toggle the option.
30 | *
31 | * @param item the item
32 | * @param $event
33 | */
34 | $scope.onExpandClicked = function (item, $event) {
35 | $event.stopPropagation();
36 | item.isExpanded = !item.isExpanded;
37 | };
38 |
39 | /**
40 | * Event on click of select item.
41 | *
42 | * @param item the item
43 | * @param $event
44 | */
45 | $scope.clickSelectItem = function (item, $event) {
46 | $event.stopPropagation();
47 | if ($scope.itemSelected) {
48 | $scope.itemSelected({item: item});
49 | }
50 | };
51 |
52 | /**
53 | * Is leaf selected.
54 | *
55 | * @param item the item
56 | * @param $event
57 | */
58 | $scope.subItemSelected = function (item, $event) {
59 | if ($scope.itemSelected) {
60 | $scope.itemSelected({item: item});
61 | }
62 | };
63 |
64 | /**
65 | * Active sub item.
66 | *
67 | * @param item the item
68 | * @param $event
69 | */
70 | $scope.activeSubItem = function (item, $event) {
71 | if ($scope.onActiveItem) {
72 | $scope.onActiveItem({item: item});
73 | }
74 | };
75 |
76 | /**
77 | * On mouse over event.
78 | *
79 | * @param item the item
80 | * @param $event
81 | */
82 | $scope.onMouseOver = function (item, $event) {
83 | $event.stopPropagation();
84 | if ($scope.onActiveItem) {
85 | $scope.onActiveItem({item: item});
86 | }
87 | };
88 |
89 | /**
90 | * Can select item.
91 | *
92 | * @returns {*}
93 | */
94 | $scope.showCheckbox = function () {
95 | if (!$scope.multiSelect) {
96 | return false;
97 | }
98 |
99 | if ($scope.selectOnlyLeafs) {
100 | return false;
101 | }
102 |
103 | if ($scope.useCallback) {
104 | return $scope.canSelectItem($scope.item);
105 | }
106 | };
107 |
108 | }]);
109 |
110 | /**
111 | * sortableItem directive.
112 | */
113 | mainModule.directive('treeItem', ['$compile',
114 | function ($compile) {
115 | return {
116 | restrict: 'E',
117 | templateUrl: 'src/tree-item.tpl.html',
118 | scope: {
119 | item: '=',
120 | itemSelected: '&',
121 | onActiveItem: '&',
122 | multiSelect: '=?',
123 | selectOnlyLeafs: '=?',
124 | isActive: '=', // the item is active - means it is highlighted but not selected
125 | useCallback: '=',
126 | canSelectItem: '=' // reference from the parent control
127 | },
128 | controller: 'treeItemCtrl',
129 | /**
130 | * Manually compiles the element, fixing the recursion loop.
131 | * @param element
132 | * @param [link] A post-link function, or an object with function(s) registered via pre and post properties.
133 | * @returns An object containing the linking functions.
134 | */
135 | compile: function (element, attrs, link) {
136 | // Normalize the link parameter
137 | if (angular.isFunction(link)) {
138 | link = { post: link };
139 | }
140 |
141 | // Break the recursion loop by removing the contents
142 | var contents = element.contents().remove();
143 | var compiledContents;
144 | return {
145 | pre: (link && link.pre) ? link.pre : null,
146 | /**
147 | * Compiles and re-adds the contents
148 | */
149 | post: function (scope, element, attrs) {
150 | // Compile the contents
151 | if (!compiledContents) {
152 | compiledContents = $compile(contents);
153 | }
154 | // Re-add the compiled contents to the element
155 | compiledContents(scope, function (clone) {
156 | element.append(clone);
157 | });
158 |
159 | // Call the post-linking function, if any
160 | if (link && link.post) {
161 | link.post.apply(null, arguments);
162 | }
163 | }
164 | };
165 | }
166 | };
167 | }]);
168 | }());
--------------------------------------------------------------------------------
/src/tree-item.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
{{item.name}}
9 |
10 |
11 |
16 |
17 |
--------------------------------------------------------------------------------
/test/unit/componentSpec.js:
--------------------------------------------------------------------------------
1 | describe('author.component-name', function () {
2 |
3 | beforeEach(module('author.component-name'));
4 |
5 | it('should have thingService', function () {
6 | inject(function (thingService) {
7 | expect(thingService).toBeDefined();
8 | });
9 | });
10 |
11 | describe('thingService', function () {
12 |
13 | var thingService;
14 |
15 | beforeEach(inject(function (_thingService_) {
16 | thingService = _thingService_;
17 | }));
18 |
19 | it('should be an object', function () {
20 | expect(typeof thingService).toBe('object');
21 | });
22 |
23 | it('should have a method sayHello()', function () {
24 | expect(thingService.sayHello).toBeDefined();
25 | });
26 |
27 | describe('sayHello()', function () {
28 |
29 | it('should be a function', function () {
30 | expect(typeof thingService.sayHello).toBe('function');
31 | });
32 |
33 | it('should return a string', function () {
34 | expect(typeof thingService.sayHello()).toBe('string');
35 | });
36 |
37 | it('should return \'Hello!\'', function () {
38 | expect(thingService.sayHello()).toEqual('Hello!');
39 | });
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------