diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..3b7c28f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,11 @@
+* **I'm submitting a ...**
+[ ] bug report
+[ ] feature request
+[ ] question about the decisions made in the repository
+[ ] question about how to use this project
+
+* **Summary**
+
+
+
+* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..caae6b1
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+* **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...)
+
+
+
+* **What is the current behavior?** (You can also link to an open issue here)
+
+
+
+* **What is the new behavior (if this is a feature change)?**
+
+
+
+* **Other information**:
diff --git a/.gitignore b/.gitignore
index 50b8fae..230a87a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.idea
+build
dist
w11k-select.iml
node_modules
@@ -7,3 +8,11 @@ node_modules
.sass-cache
bower_components
temp
+coverage
+.nyc_output
+*.log
+
+/lib/
+/lib-esm/
+/src/w11k-select.css
+stats.html
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index f5ae818..0000000
--- a/.jshintrc
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- // Settings
- "passfail" : false, // Stop on first error.
- "maxerr" : 100, // Maximum errors before stopping.
-
-
- // Predefined globals whom JSHint will ignore.
- "browser" : false, // Standard browser globals e.g. `window`, `document`.
- "node" : false,
- "jquery" : true,
-
- "predef" : [ // Extra globals.
- // application code
- "angular",
- "_",
- "lodash",
-
- // test code
- "beforeEach",
- "afterEach",
- "module",
- "describe",
- "expect",
- "it",
- "inject",
- "jasmine",
- "spyOn",
- "browser",
- "element"
- ],
-
- // Development.
- "debug" : false, // Allow debugger statements e.g. browser breakpoints.
- "devel" : false, // Allow development statements e.g. `console.log();`.
-
-
- // EcmaScript 5.
- "strict" : true, // Require `use strict` pragma in every file.
- "globalstrict" : true, // Allow global `use strict` syntax because build-system wraps all application code into a function
-
-
- // The Good Parts.
- "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
- "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
- "laxcomma" : true, // Tolerate line breaking before ','
- "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.).
- "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
- "curly" : true, // Require {} for every new block or scope.
- "eqeqeq" : true, // Require triple equals i.e. `===`.
- "eqnull" : false, // Tolerate use of `== null`.
- "evil" : false, // Tolerate use of `eval`.
- "expr" : false, // Tolerate `ExpressionStatement` as Programs.
- "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`.
- "immed" : true, // Require immediate invocations to be wrapped in parents e.g. `( function(){}() );`
- "latedef" : true, // Prohibit variable use before definition.
- "loopfunc" : false, // Allow functions to be defined within loops.
- "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
- "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions.
- "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
- "scripturl" : true, // Tolerate script-targeted URLs.
- "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
- "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
- "undef" : true, // Require all non-global variables be declared before they are used.
-
-
- // Personal styling preferences.
- "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
- "noempty" : true, // Prohibit use of empty blocks.
- "nonew" : true, // Prohibit use of constructors for side-effects.
- "nomen" : true, // Prohibit use of initial or trailing underbars in names.
- "onevar" : false, // Allow only one `var` statement per function.
- "plusplus" : false, // Prohibit use of `++` & `--`.
- "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
- "trailing" : true, // Prohibit trailing whitespaces.
- "white" : true, // Check against strict whitespace and indentation rules.
-
- "indent": 2,
- "camelcase": true,
- "quotmark": "single",
- "unused": true,
- "smarttabs": false
-}
-
-
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..940ce63
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,11 @@
+src
+config
+examples
+tsconfig.json
+tslint.json
+.travis.yml
+.github
+
+coverage
+.nyc_output
+*.log
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f8194bf
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+sudo: false
+language: node_js
+node_js:
+ - 6
+ - 4
+after_success:
+ - yarn send-coverage
diff --git a/Gruntfile.js b/Gruntfile.js
deleted file mode 100644
index afeb4dc..0000000
--- a/Gruntfile.js
+++ /dev/null
@@ -1,153 +0,0 @@
-'use strict';
-
-module.exports = function (grunt) {
-
- var pkg = require('./package.json');
-
- grunt.loadNpmTasks('grunt-contrib-clean');
- grunt.loadNpmTasks('grunt-contrib-jshint');
- grunt.loadNpmTasks('grunt-contrib-sass');
- grunt.loadNpmTasks('grunt-contrib-less');
- grunt.loadNpmTasks('grunt-contrib-copy');
- grunt.loadNpmTasks('grunt-contrib-uglify');
- grunt.loadNpmTasks('grunt-contrib-concat');
- grunt.loadNpmTasks('grunt-html2js');
- grunt.loadNpmTasks('grunt-bump');
- grunt.loadNpmTasks('grunt-conventional-changelog');
-
- var bowerrc = grunt.file.exists('./.bowerrc') ? grunt.file.readJSON('./.bowerrc') : { 'json': 'bower.json' };
-
- var bumpFiles = [ 'package.json', '../w11k-select-bower/package.json' ];
- if (grunt.file.exists(bowerrc.json)) {
- bumpFiles.push(bowerrc.json);
- }
-
- grunt.initConfig({
- pkg: pkg,
- meta: {
- banner:
- '/**\n' +
- ' * <%= pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
- ' * <%= pkg.homepage %>\n' +
- ' *\n' +
- ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' +
- ' */\n'
- },
-
- clean: {
- dist: ['dist/*', 'temp/*']
- },
- jshint: {
- src: {
- options: {
- jshintrc: '.jshintrc'
- },
- files: [{
- expand: true,
- cwd: 'src',
- src: '**.js'
- }]
- }
- },
- sass: {
- dist: {
- options: {
- style: 'expanded'
- },
- files: {
- 'dist/w11k-select.css': 'src/w11k-select.scss'
- }
- }
- },
- less: {
- dist: {
- options: {
- paths: ['src']
- },
- files: {
- 'temp/w11k-select.less.css': 'src/w11k-select.less'
- }
- }
- },
- copy: {
- template: {
- src: 'src/w11k-select.tpl.html',
- dest: 'dist/w11k-select.tpl.html'
- },
- sass: {
- src: 'src/w11k-select.scss',
- dest: 'dist/w11k-select.scss'
- },
- less: {
- src: 'src/w11k-select.less',
- dest: 'dist/w11k-select.less'
- },
- release: {
- files: [
- {
- expand: true,
- cwd: 'dist/',
- src: '*',
- dest: '../w11k-select-bower/dist/'
- },
- {
- src: 'bower.json',
- dest: '../w11k-select-bower/'
- }
- ]
- }
- },
- concat: {
- code: {
- options: {
- banner: '<%= meta.banner %>'
- },
- src: 'src/w11k-select.js',
- dest: 'dist/w11k-select.js'
- }
- },
- html2js: {
- template: {
- options: {
- base: 'src/',
- module: 'w11k.select.template',
- quoteChar: '\'',
- htmlmin: {
- collapseWhitespace: true
- }
- },
- files: {
- 'dist/w11k-select.tpl.js': 'src/w11k-select.tpl.html'
- }
- }
- },
- uglify: {
- options: {
- banner: '<%= meta.banner %>'
- },
- code: {
- files: [{
- 'dist/w11k-select.min.js': 'src/w11k-select.js'
- }]
- }
- },
- bump: {
- options: {
- files: bumpFiles,
- commit: true,
- commitMessage: 'chore(project): bump version to %VERSION%',
- commitFiles: ['-a'],
- createTag: false,
- push: false
- }
- }
- });
-
-
- grunt.registerTask('default', ['build']);
-
- grunt.registerTask('build', ['clean', 'jshint', 'sass', 'less', 'copy:template', 'copy:sass', 'copy:less', 'html2js', 'concat', 'uglify']);
-
- grunt.registerTask('release', ['build', 'copy:release']);
-
-};
diff --git a/README.md b/README.md
index 68b2d0e..e326e18 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,24 @@ This is a sass example to reproduce the default icons. Notice: You don't need th
}
+## Release
+1. project
+ 1. create release branch
+ 1. `npm run package`
+ 1. bump bower version
+ 1. bump npm version
+ 1. update changelog
+ 1. merge release into develop and master branch
+ 1. `git push --tags`
+ 1. update [demo page](http://w11k.github.io/w11k-select/)
+1. npm package
+ 1. npm publish
+1. bower package
+ 1. copy release to [bower repo](https://github.com/w11k/w11k-select-bower)
+ 1. bump version in bower repo
+ 1. create tag in bower repo
+ 1. `git push --tags`
+
## Roadmap
see milestones and issues at https://github.com/w11k/w11k-select/issues
diff --git a/bower.json b/bower.json
index 88b5ba2..2919df5 100644
--- a/bower.json
+++ b/bower.json
@@ -2,20 +2,11 @@
"name": "w11k-select",
"version": "0.9.0",
"main": [
- "dist/w11k-select.js",
- "dist/w11k-select.tpl.js",
- "dist/w11k-select.css"
+ "build/index.js"
],
"ignore": [],
"dependencies": {
- "angular": ">= 1.2.0 < 1.7.0",
- "w11k-dropdownToggle": "1.0.x",
- "angular-bindonce": "0.3.x"
- },
- "devDependencies": {
- "font-awesome": "4.0.x",
- "bootstrap": "3.1.0",
- "es5-shim": "2.3.0",
- "jquery": "1.11.0"
+ "angular": ">= 1.3.0 < 1.7.0",
+ "w11k-dropdownToggle": "1.0.x"
}
}
diff --git a/config/tsconfig.flexible.json b/config/tsconfig.flexible.json
new file mode 100644
index 0000000..9647394
--- /dev/null
+++ b/config/tsconfig.flexible.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "strictNullChecks": true
+ }
+}
diff --git a/config/tsconfig.module.json b/config/tsconfig.module.json
new file mode 100644
index 0000000..1853415
--- /dev/null
+++ b/config/tsconfig.module.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../tsconfig",
+ "compilerOptions": {
+ "outDir": "../build/module",
+ "module": "es6",
+ "declaration": false
+ },
+ "exclude": [
+ "../node_modules/**",
+ "../src/**/*.spec.ts"
+ ]
+}
diff --git a/config/tsconfig.strict.json b/config/tsconfig.strict.json
new file mode 100644
index 0000000..b82f64d
--- /dev/null
+++ b/config/tsconfig.strict.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "strictNullChecks": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true
+ }
+}
diff --git a/package.json b/package.json
index 22ccebc..d3c2e2a 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,60 @@
{
"name": "w11k-select",
"version": "0.9.0",
-
"description": "single- and multi-select directive for angularjs",
- "keywords": [ "angular", "angularjs", "directive", "select", "multi-select", "bootstrap" ],
+ "keywords": [
+ "angular",
+ "angularjs",
+ "directive",
+ "select",
+ "multi-select",
+ "bootstrap"
+ ],
"homepage": "https://github.com/w11k/w11k-select",
"bugs": "https://github.com/w11k/w11k-select/issues",
-
+ "scripts": {
+ "info": "npm-scripts-info",
+ "sass:compile": "node-sass src/w11k-select.scss dist/w11k-select.css",
+ "copy:sass": "cd src && cp --parents -R **/*.scss ./../dist/ && cp -R *.scss ./../dist/ ",
+ "copy:less": "cd src && cp --parents -R **/*.less ./../dist/ && cp -R *.less ./../dist/ ",
+ "copy:html": "cd src && cp --parents -R **/*.html ./../dist/ && cp -R *.html ./../dist/ ",
+ "lint": "tslint src/**/*.ts",
+ "unit": "yarn build && nyc ava",
+ "check-coverage": "nyc check-coverage --lines 100 --functions 100 --branches 100",
+ "test": "yarn lint && yarn unit && yarn check-coverage",
+ "watch:build": "tsc -p tsconfig.json -w",
+ "watch:unit": "tsc -p tsconfig.json && ava --watch --verbose",
+ "cov": "yarn unit && yarn html-coverage && opn coverage/index.html",
+ "html-coverage": "nyc report --reporter=html",
+ "send-coverage": "nyc report --reporter=lcov > coverage.lcov && codecov",
+ "docs": "typedoc src/index.ts --excludePrivate --mode file --theme minimal --out dist/docs && opn dist/docs/index.html",
+ "docs:json": "typedoc --mode file --json dist/docs/typedoc.json src/index.ts",
+ "release": "standard-version",
+ "ng:annotate": "find dist/ -iname '*.js' -exec sh -c 'ng-annotate -a {} -o ./{}' \\;",
+ "clean": "shx rm -rf lib lib-esm build dist *.log",
+ "build": "npm run scss && tsc && tsc -m es6 --outDir lib-esm && npm run fixdts",
+ "fixdts": "dts-downlevel 'lib/**/*.d.ts' 'lib-esm/**/*.d.ts'",
+ "package": "npm run clean && npm run build && npm run bundle && npm run ng:annotate",
+ "bundle": "rollup -c && rollup -c --environment MINIFY",
+ "prepare": "npm run package",
+ "scss": "node-sass --include-path scss src/w11k-select.scss src/w11k-select.css"
+ },
+ "scripts-info": {
+ "info": "Display information about the scripts [Warning]: Some where updated, info might be old",
+ "build": "(Trash and re)build the library",
+ "lint": "Lint all typescript source files",
+ "unit": "Run unit tests",
+ "test": "Lint and test the library",
+ "watch": "Watch source files, rebuild library on changes, rerun relevant tests",
+ "watch:build": "Watch source files, rebuild library on changes",
+ "watch:unit": "Watch the build, rerun relevant tests on changes",
+ "cov": "Run tests, generate the HTML coverage report, and open it in a browser",
+ "html-coverage": "Output HTML test coverage report",
+ "send-coverage": "Output lcov test coverage report and send it to codecov",
+ "docs": "Generate API documentation and open it in a browser",
+ "docs:json": "Generate API documentation in typedoc JSON format",
+ "release": "Bump package.json version, update CHANGELOG.md, tag a release"
+ },
"author": {
"name": "w11k GmbH",
"url": "http://www.w11k.de"
@@ -14,39 +62,53 @@
"contributors": [
"Philipp Burgmer"
],
-
"repository": {
"type": "git",
"url": "https://github.com/w11k/w11k-select.git"
},
-
- "licenses": [{
+ "licenses": [
+ {
"type": "MIT"
- }],
-
- "main": "src/w11k-select.js",
- "files": [
- "src/**"
+ }
],
-
+ "engines": {
+ "node": ">=4"
+ },
+ "main": "lib/index.js",
+ "jsnext:main": "lib-esm/index.js",
+ "typings": "lib/index.d.ts",
"dependencies": {
"angular": ">= 1.2.0 < 1.7.0",
- "w11k-dropdownToggle": "1.0.x",
- "angular-bindonce": "0.3.x"
+ "angular-bindonce": "0.3.x",
+ "tslib": "^1.5.0",
+ "w11k-dropdownToggle": "1.0.x"
},
-
"devDependencies": {
- "grunt": "0.4.5",
- "grunt-contrib-clean": "0.5.0",
- "grunt-contrib-jshint": "0.8.0",
- "grunt-contrib-sass": "0.7.2",
- "grunt-contrib-less": "0.12.0",
- "grunt-contrib-copy": "0.5.0",
- "grunt-contrib-uglify": "0.4.0",
- "grunt-contrib-concat": "0.4.0",
- "grunt-html2js": "0.2.4",
-
- "grunt-bump": "0.0.13",
- "grunt-conventional-changelog": "1.1.0"
+ "@types/angular": "^1.6.6",
+ "@types/node": "^7.0.5",
+ "ava": "^0.18.1",
+ "codecov": "^1.0.1",
+ "dts-downlevel": "^0.3.0",
+ "multiview": "^2.3.1",
+ "ng-annotate": "^1.2.1",
+ "node-sass": "^4.5.0",
+ "npm-scripts-info": "^0.3.6",
+ "nyc": "^10.0.0",
+ "opn-cli": "^3.1.0",
+ "rollup": "^0.38.0",
+ "rollup-plugin-copy": "^0.2.3",
+ "rollup-plugin-node-resolve": "^2.0.0",
+ "rollup-plugin-progress": "^0.1.0",
+ "rollup-plugin-sass": "^0.5.1",
+ "rollup-plugin-sourcemaps": "^0.4.1",
+ "rollup-plugin-uglify": "^1.0.1",
+ "rollup-plugin-visualizer": "^0.1.5",
+ "shx": "^0.2.2",
+ "standard-version": "^4.0.0",
+ "trash-cli": "^1.4.0",
+ "tslint": "^4.0.2",
+ "tslint-config-standard": "^4.0.0",
+ "typedoc": "^0.5.5",
+ "typescript": "^2.2.0"
}
}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..1adb39e
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,74 @@
+import nodeResolve from "rollup-plugin-node-resolve";
+import uglify from "rollup-plugin-uglify";
+import progress from "rollup-plugin-progress";
+import sourcemaps from "rollup-plugin-sourcemaps";
+import visualizer from "rollup-plugin-visualizer";
+import copy from "rollup-plugin-copy";
+import sass from "rollup-plugin-sass";
+
+sass({
+ output: 'dist/bundle.css',
+ options: {
+ file: "src/w11k-select.scss"
+ }
+});
+
+
+var MINIFY = process.env.MINIFY;
+
+var pkg = require('./package.json');
+var banner = "/**";
+banner += `
+ * @version v${pkg.version}
+ * @link ${pkg.homepage}
+ * @license MIT License, http://www.opensource.org/licenses/MIT
+ */`;
+
+var uglifyOpts = {output: {}};
+// retain multiline comment with @license
+uglifyOpts.output.comments = (node, comment) =>
+comment.type === 'comment2' && /@license/i.test(comment.value);
+
+var plugins = [
+ nodeResolve({jsnext: true}),
+ progress({clearLine: false}),
+ sourcemaps(),
+ copy({
+ "src/w11k-select.tpl.html": "dist/w11k-select.tpl.html",
+ "src/w11k-select-option/w11k-select-option.tpl.html": "dist/w11k-select-option.tpl.html",
+ "src/w11k-select.css": "dist/w11k-select.css",
+ "src/w11k-select.scss": "dist/w11k-select.scss",
+ "src/w11k-select.less": "dist/w11k-select.less",
+ "src/w11k-select-option/_w11k-select-options.scss": "dist/w11k-select-option/_w11k-select-options.scss",
+ "src/w11k-select-option/_w11k-select-options.less": "dist/w11k-select-option/_w11k-select-options.less",
+ "src/w11k-select-checkbox/_w11k-checkbox.directive.scss": "dist/w11k-select-checkbox/_w11k-checkbox.directive.scss",
+ "src/w11k-select-checkbox/_w11k-checkbox.directive.less": "dist/w11k-select-checkbox/_w11k-checkbox.directive.less",
+ verbose: true
+ }),
+ sass
+
+];
+
+if (MINIFY) plugins.push(uglify(uglifyOpts));
+if (MINIFY) plugins.push(visualizer({sourcemap: true}));
+
+var extension = MINIFY ? ".min.js" : ".js";
+
+const BASE_CONFIG = {
+ sourceMap: true,
+ format: 'umd',
+ exports: 'named',
+ plugins: plugins,
+ banner: banner,
+};
+
+const CONFIG = Object.assign({
+ moduleName: 'w11k-select',
+ entry: 'lib-esm/index.js',
+ dest: 'dist/w11k-select' + extension,
+ globals: {angular: 'angular'},
+ external: ['angular'],
+}, BASE_CONFIG);
+
+
+export default CONFIG;
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..ff9168f
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,2 @@
+export {module} from './w11k-select';
+export {Config} from './model/config.model';
diff --git a/src/lib/collect-active-labels.ts b/src/lib/collect-active-labels.ts
new file mode 100644
index 0000000..d7d5d58
--- /dev/null
+++ b/src/lib/collect-active-labels.ts
@@ -0,0 +1,9 @@
+import {InternalOption} from '../model/internal-option.model';
+import {OptionState} from '../model/option-state.enum';
+
+export function collectActiveLabels(option: InternalOption, labelArray: string[]): void {
+ if (option.state === OptionState.selected) {
+ labelArray.push(option.label)
+ }
+ option.children.forEach(option => collectActiveLabels(option, labelArray))
+}
diff --git a/src/lib/external-option-2-label.ts b/src/lib/external-option-2-label.ts
new file mode 100644
index 0000000..a4fe3b1
--- /dev/null
+++ b/src/lib/external-option-2-label.ts
@@ -0,0 +1,6 @@
+export function externalOption2label(option, optionsExpParsed) {
+ let context = {};
+ context[optionsExpParsed.item] = option;
+
+ return optionsExpParsed.label(context);
+}
diff --git a/src/lib/external-option-2-value.ts b/src/lib/external-option-2-value.ts
new file mode 100644
index 0000000..ed64dde
--- /dev/null
+++ b/src/lib/external-option-2-value.ts
@@ -0,0 +1,6 @@
+export function externalOption2value(option, optionsExpParsed) {
+ let context = {};
+ context[optionsExpParsed.item] = option;
+
+ return optionsExpParsed.value(context);
+}
diff --git a/src/lib/external-options-2-internal-options.ts b/src/lib/external-options-2-internal-options.ts
new file mode 100644
index 0000000..79f3a94
--- /dev/null
+++ b/src/lib/external-options-2-internal-options.ts
@@ -0,0 +1,40 @@
+import {externalOption2value} from './external-option-2-value';
+import {externalOption2label} from './external-option-2-label';
+import {value2trackingId} from './value-2-tracking-id';
+import {OptionState} from '../model/option-state.enum';
+import {ConfigInstance} from '../model/config.model';
+import {InternalOption} from '../model/internal-option.model';
+
+export function externalOptions2internalOptions(externalOptions, viewValue, w11kSelectHelper, optionsExpParsed, config: ConfigInstance): InternalOption[] {
+ let viewValueIDs = {};
+
+ let i = viewValue.length;
+
+ while (i--) {
+ let trackingId = value2trackingId(viewValue[i], w11kSelectHelper, optionsExpParsed);
+ viewValueIDs[trackingId] = true;
+ }
+
+ function prepareOptions(externalOption, parent?: string) {
+ let value = externalOption2value(externalOption, optionsExpParsed);
+ let trackingId = value2trackingId(value, w11kSelectHelper, optionsExpParsed);
+ let label = externalOption2label(externalOption, optionsExpParsed);
+
+ let internalOption = new InternalOption(
+ trackingId,
+ label,
+ externalOption,
+ !!viewValueIDs[trackingId],
+ viewValueIDs[trackingId] ? OptionState.selected : OptionState.unselected,
+ [],
+ parent || null
+ );
+
+ if (externalOption[config.children]) {
+ internalOption.children = externalOption[config.children].map(child => prepareOptions(child, trackingId))
+ }
+ return internalOption;
+ }
+
+ return externalOptions.map(prepareOptions);
+}
diff --git a/src/lib/internal-option-2-value.ts b/src/lib/internal-option-2-value.ts
new file mode 100644
index 0000000..ae1ef0f
--- /dev/null
+++ b/src/lib/internal-option-2-value.ts
@@ -0,0 +1,4 @@
+import {externalOption2value} from './external-option-2-value';
+export function internalOption2value(option, optionsExpParsed) {
+ return externalOption2value(option.model, optionsExpParsed);
+}
diff --git a/src/lib/internal-options-2-external-model.ts b/src/lib/internal-options-2-external-model.ts
new file mode 100644
index 0000000..012bcbe
--- /dev/null
+++ b/src/lib/internal-options-2-external-model.ts
@@ -0,0 +1,19 @@
+import {internalOption2value} from './internal-option-2-value';
+import {Config} from '../model/config.model';
+import {InternalOption} from '../model/internal-option.model';
+import {OptionState} from '../model/option-state.enum';
+
+export function internalOptions2externalModel(options, optionsExpParsed, config: Config) {
+ let arr = [];
+ options.forEach(option => traverse(option, arr, optionsExpParsed));
+ return arr;
+}
+
+
+function traverse(option: InternalOption, arr, optionsExpParsed) {
+ if (option.state === OptionState.selected) {
+ arr.push(internalOption2value(option, optionsExpParsed))
+ }
+ option.children.forEach(option => traverse(option, arr, optionsExpParsed))
+
+}
diff --git a/src/lib/key-listener.ts b/src/lib/key-listener.ts
new file mode 100644
index 0000000..b16d5f8
--- /dev/null
+++ b/src/lib/key-listener.ts
@@ -0,0 +1,18 @@
+export function keyListener () {
+ return function (scope, elm, attrs) {
+ // prevent scroll on space click
+ elm.bind("keydown", function (event) {
+ if (event.keyCode === 32) {
+ event.preventDefault();
+ }
+ });
+
+ // trigger click on spacer || enter
+ elm.bind("keyup", function (event) {
+ if (event.keyCode === 32 || event.keyCode === 13) {
+ scope.$apply(attrs.keyListener);
+ }
+ });
+
+ };
+}
diff --git a/src/lib/set-selected.ts b/src/lib/set-selected.ts
new file mode 100644
index 0000000..6c02a21
--- /dev/null
+++ b/src/lib/set-selected.ts
@@ -0,0 +1,10 @@
+import {InternalOption} from '../model/internal-option.model';
+import {OptionState} from '../model/option-state.enum';
+export function setSelected(options: InternalOption[], selected: boolean) {
+ let i = options.length;
+ while (i--) {
+ options[i].selected = selected;
+ options[i].state = selected ? OptionState.selected : OptionState.unselected;
+ setSelected(options[i].children || [], selected)
+ }
+}
diff --git a/src/lib/value-2-tracking-id.ts b/src/lib/value-2-tracking-id.ts
new file mode 100644
index 0000000..e2e81be
--- /dev/null
+++ b/src/lib/value-2-tracking-id.ts
@@ -0,0 +1,19 @@
+export function value2trackingId(value, w11kSelectHelper, optionsExpParsed) {
+ if (optionsExpParsed.tracking !== undefined) {
+ let context = {};
+ let assignValueFn = optionsExpParsed.value.assign;
+ assignValueFn(context, value);
+
+ let trackingValue = optionsExpParsed.tracking(context);
+
+ if (trackingValue === undefined) {
+ throw new Error('Couldn\'t get \'track by\' value. Please make sure to only use something in \'track by’ part of w11kSelectOptions expression, accessible from result of value part. (\'option.data\' and \'option.data.unique\' but not \'option.unique\')');
+ }
+
+ return trackingValue.toString();
+ }
+ else {
+ return w11kSelectHelper.hashCode(value);
+ }
+
+}
diff --git a/src/model/config.model.ts b/src/model/config.model.ts
new file mode 100644
index 0000000..0fb781b
--- /dev/null
+++ b/src/model/config.model.ts
@@ -0,0 +1,101 @@
+export class ConfigCommon {
+ templateUrl: string = 'w11k-select.tpl.html';
+ templateUrlOptions: string = 'w11k-select-option.tpl.html';
+}
+export interface StyleConfig {
+ marginBottom: '10px';
+ maxHeight: undefined | string;
+}
+
+export interface FilterConfig {
+ active: boolean;
+ placeholder: string;
+ select: {
+ active: boolean;
+ text: undefined | string;
+ }
+ deselect: {
+ active: true,
+ text: undefined | string;
+ }
+}
+
+export class ConfigInstance {
+ /** for form validation */
+ required: boolean = false;
+ /** Hide checkboxes during single selection */
+ hideCheckboxes: boolean = false;
+ /** single or multiple select */
+ multiple: boolean = true;
+ /** disable user interaction */
+ disabled: boolean = false;
+ /** all the configuration for the header (visible if dropdown closed) */
+ header = {
+ /** text to show if no item selected (plain text, no evaluation, no data-binding) */
+ placeholder: '',
+ /**
+ * text to show if item(s) selected (expression, evaluated against user scope)
+ * make sure to enclose your expression withing quotes, otherwise it will be evaluated too early
+ * default: undefined evaluates to a comma separated representation of selected items
+ * example: ng-model='options.selected' w11k-select-config='{header: {placeholder: 'options.selected.length'}}'
+ */
+ text: undefined
+ };
+ dropdown: {
+ onOpen: (() => void) | undefined,
+ onClose: (() => void) | undefined
+ } = {
+ onOpen: undefined,
+ onClose: undefined
+ };
+ /** all the configuration for the filter section within the dropdown */
+ filter: FilterConfig = {
+ /** activate filter input to search for options */
+ active: true,
+ /** text to show if no filter is applied */
+ placeholder: 'Filter',
+ /** 'select all filtered options' button */
+ select: {
+ /** show select all button */
+ active: true,
+ /**
+ * label for select all button
+ * default: undefined evaluates to 'all'
+ */
+ text: undefined
+ },
+ /** 'deselect all filtered options' button */
+ deselect: {
+ /** show deselect all button */
+ active: true,
+ /**
+ * label for deselect all button
+ * default: undefined evaluates to 'none'
+ */
+ text: undefined
+ }
+ };
+
+ /** values for dynamically calculated styling of dropdown */
+ style: StyleConfig = {
+ /** margin-bottom for automatic height adjust */
+ marginBottom: '10px',
+ /** static or manually calculated max height (disables internal height calculation) */
+ maxHeight: undefined
+ };
+ /** when set to true, the clear-button is always visible. */
+ showClearAlways: boolean = false;
+ children: string;
+
+}
+
+export class Config {
+ common: ConfigCommon;
+ instance: ConfigInstance;
+
+
+ constructor(common?: ConfigCommon, instance?: ConfigInstance) {
+ this.common = common || new ConfigCommon();
+ this.instance = instance || new ConfigInstance();
+ }
+}
diff --git a/src/model/internal-option.model.ts b/src/model/internal-option.model.ts
new file mode 100644
index 0000000..4cc31b6
--- /dev/null
+++ b/src/model/internal-option.model.ts
@@ -0,0 +1,21 @@
+import {OptionState} from './option-state.enum';
+export class InternalOption {
+
+ trackingId: string;
+ label: string;
+ model: any;
+ selected: boolean;
+ state: OptionState;
+ children: any[];
+ parent: any;
+
+ constructor(trackingId: string, label: string, model: any, selected: boolean, state: OptionState, children: any[], parent: any) {
+ this.trackingId = trackingId;
+ this.label = label;
+ this.model = model;
+ this.selected = selected;
+ this.state = state;
+ this.children = children;
+ this.parent = parent;
+ }
+}
diff --git a/src/model/option-state.enum.ts b/src/model/option-state.enum.ts
new file mode 100644
index 0000000..ac6ef49
--- /dev/null
+++ b/src/model/option-state.enum.ts
@@ -0,0 +1,5 @@
+export enum OptionState {
+ unselected,
+ selected,
+ childsSelected
+}
diff --git a/src/w11k-select-checkbox/_w11k-checkbox.directive.less b/src/w11k-select-checkbox/_w11k-checkbox.directive.less
new file mode 100644
index 0000000..2cd4072
--- /dev/null
+++ b/src/w11k-select-checkbox/_w11k-checkbox.directive.less
@@ -0,0 +1,48 @@
+a.w11k-checkbox {
+
+ width: 12px;
+ height: 12px;
+ display: inline-block;
+ border-radius: 2px;
+ border: #989898 1px solid;
+ background: #f4f4f4;
+ padding: 0;
+ position: relative;
+ transform: translate(1px, 1px);
+ margin-right: 8px;
+ margin-left: 8px;
+
+ &:before {
+ position: absolute;
+ left: 0;
+ top: -3px;
+ font-weight: bold;
+ display: inline-block;
+ content: ' ';
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ }
+
+ &.selected {
+ &:before {
+ content: '✓';
+ font-size: 13px;
+ }
+ }
+
+ &.unselected {
+ &:before {
+ }
+ }
+
+ &.childsSelected {
+ &:before {
+ content: ' ';
+ width: 6px;
+ height: 6px;
+ background: rgba(blue, .3);
+ top: 2px;
+ left: 2px;
+ border-radius: 1px;
+ }
+ }
+}
diff --git a/src/w11k-select-checkbox/_w11k-checkbox.directive.scss b/src/w11k-select-checkbox/_w11k-checkbox.directive.scss
new file mode 100644
index 0000000..2cd4072
--- /dev/null
+++ b/src/w11k-select-checkbox/_w11k-checkbox.directive.scss
@@ -0,0 +1,48 @@
+a.w11k-checkbox {
+
+ width: 12px;
+ height: 12px;
+ display: inline-block;
+ border-radius: 2px;
+ border: #989898 1px solid;
+ background: #f4f4f4;
+ padding: 0;
+ position: relative;
+ transform: translate(1px, 1px);
+ margin-right: 8px;
+ margin-left: 8px;
+
+ &:before {
+ position: absolute;
+ left: 0;
+ top: -3px;
+ font-weight: bold;
+ display: inline-block;
+ content: ' ';
+ font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
+ }
+
+ &.selected {
+ &:before {
+ content: '✓';
+ font-size: 13px;
+ }
+ }
+
+ &.unselected {
+ &:before {
+ }
+ }
+
+ &.childsSelected {
+ &:before {
+ content: ' ';
+ width: 6px;
+ height: 6px;
+ background: rgba(blue, .3);
+ top: 2px;
+ left: 2px;
+ border-radius: 1px;
+ }
+ }
+}
diff --git a/src/w11k-select-checkbox/w11k-checkbox.directive.ts b/src/w11k-select-checkbox/w11k-checkbox.directive.ts
new file mode 100644
index 0000000..d68637d
--- /dev/null
+++ b/src/w11k-select-checkbox/w11k-checkbox.directive.ts
@@ -0,0 +1,26 @@
+import {OptionState} from '../model/option-state.enum';
+
+
+export class W11KSelectCheckbox {
+ state: OptionState;
+ toggle: Function;
+
+ getClass(state: OptionState): string {
+ return OptionState[state];
+ }
+
+}
+
+export function w11kSelectCheckboxDirective(): ng.IDirective {
+ return {
+ scope: {
+ 'state': '=',
+ },
+ bindToController: true,
+ controllerAs: '$ctrl',
+ template: ``,
+ restrict: 'E',
+ controller: W11KSelectCheckbox
+ }
+
+}
diff --git a/src/w11k-select-helper.factory.ts b/src/w11k-select-helper.factory.ts
new file mode 100644
index 0000000..98b16cc
--- /dev/null
+++ b/src/w11k-select-helper.factory.ts
@@ -0,0 +1,111 @@
+/** @internal */
+import * as angular from 'angular';
+import {IDocumentService, IParseService} from 'angular';
+
+export class W11KSelectHelper {
+
+ // value as label for item in collection | filter track by tracking
+ OPTIONS_EXP = /^([a-zA-Z][\w\.]*)(?:\s+as\s+([a-zA-Z][\w\.]*))?\s+for\s+(?:([a-zA-Z][\w]*))\s+in\s+([$_a-zA-Z][\w\.\(\)]*(?:\s+\|\s[a-zA-Z][\w\:_\{\}']*)*)(?:\s+track\sby\s+([a-zA-Z][\w\.]*))?$/;
+
+
+ constructor(public readonly $parse: IParseService,
+ public readonly $document: IDocumentService) {
+ 'ngInject';
+
+ }
+
+ extendDeep = (dst, ...otherArgs) => {
+ angular.forEach(otherArgs, (obj) => {
+ if (obj !== dst) {
+ angular.forEach(obj, (value, key) => {
+ if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
+ this.extendDeep(dst[key], value);
+ } else {
+ dst[key] = value;
+ }
+ });
+ }
+ });
+ return dst;
+ };
+
+ hashCode(value) {
+ let string;
+ if (typeof value === 'object') {
+ string = angular.toJson(value);
+ }
+ else {
+ string = value.toString();
+ }
+
+ let hash = 0;
+ let length = string.length;
+ for (let i = 0; i < length; i++) {
+ hash = string.charCodeAt(i) + (hash << 6) + (hash << 16) - hash;
+ }
+
+ return hash.toString(36);
+ }
+
+ parseOptions(input) {
+
+ let match = input.match(this.OPTIONS_EXP);
+ if (!match) {
+ let expected = '"item.value" [as "item.label"] for "item" in "collection [ | filter ] [track by item.value.unique]"';
+ throw new Error('Expected options in form of \'' + expected + '\' but got "' + input + '".');
+ }
+
+ let result: any = {
+ value: this.$parse(match[1]),
+ label: this.$parse(match[2] || match[1]),
+ item: match[3],
+ collection: this.$parse(match[4])
+ };
+
+ if (match[5] !== undefined) {
+ result.tracking = this.$parse(match[5]);
+ }
+
+ return result;
+ }
+
+ getParent(element, selector) {
+ // with jQuery
+ if (angular.isFunction(element.parents)) {
+ let container = element.parents(selector);
+ if (container.length > 0) {
+ return container[0];
+ }
+
+ return;
+ }
+
+ // without jQuery
+ let matchesSelector = 'MatchesSelector';
+ let matchFunctions = [
+ 'matches',
+ 'matchesSelector',
+ 'moz' + matchesSelector,
+ 'webkit' + matchesSelector,
+ 'ms' + matchesSelector,
+ 'o' + matchesSelector
+ ];
+
+ for (let index in matchFunctions) {
+ let matchFunction = matchFunctions[index];
+ if (angular.isFunction(element[0][matchFunction])) {
+ let parent1 = element[0].parentNode;
+ while (parent1 !== this.$document[0]) {
+ if (parent1[matchFunction](selector)) {
+ return parent1;
+ }
+ parent1 = parent1.parentNode;
+ }
+
+ return;
+ }
+ }
+
+ return;
+ }
+}
diff --git a/src/w11k-select-infinite-scroll.directive.ts b/src/w11k-select-infinite-scroll.directive.ts
new file mode 100644
index 0000000..eb289f8
--- /dev/null
+++ b/src/w11k-select-infinite-scroll.directive.ts
@@ -0,0 +1,69 @@
+export function w11kSelectInfiniteScroll($timeout) {
+ 'ngInject';
+ return {
+ link: function (scope, element, attrs) {
+ let scrollDistance = 0;
+ let scrollEnabled = true;
+ let checkImmediatelyWhenEnabled = false;
+
+ let onDomScrollHandler = function () {
+ onScrollHandler(true);
+ };
+
+ let scrollContainer = element[0];
+
+ if (scrollContainer.children.length !== 1) {
+ throw new Error('scroll container has to have exactly one child!');
+ }
+
+ let content = scrollContainer.children[0];
+
+ let onScrollHandler = function (apply?) {
+
+ let distanceToBottom = content.clientHeight - scrollContainer.scrollTop;
+ let shouldScroll = distanceToBottom <= scrollContainer.clientHeight * (scrollDistance + 1);
+
+ if (shouldScroll && scrollEnabled) {
+ if (apply) {
+ scope.$apply(function () {
+ scope.$eval(attrs.w11kSelectInfiniteScroll);
+ });
+ }
+ else {
+ scope.$eval(attrs.w11kSelectInfiniteScroll);
+ }
+ }
+ else if (shouldScroll) {
+ checkImmediatelyWhenEnabled = true;
+ }
+ };
+
+ attrs.$observe('w11kSelectInfiniteScrollDistance', function (value: any) {
+ scrollDistance = parseFloat(value);
+ });
+
+
+ attrs.$observe('w11kSelectInfiniteScrollDisabled', function (value: any) {
+ scrollEnabled = !value;
+
+ if (scrollEnabled && checkImmediatelyWhenEnabled) {
+ checkImmediatelyWhenEnabled = false;
+ onScrollHandler();
+ }
+ });
+
+ element.on('scroll', onDomScrollHandler);
+ scope.$on('$destroy', function () {
+ element.off('scroll', onDomScrollHandler);
+ });
+
+ return $timeout(function () {
+ if (attrs.w11kSelectInfiniteScrollImmediateCheck) {
+ if (scope.$eval(attrs.w11kSelectInfiniteScrollImmediateCheck)) {
+ onScrollHandler();
+ }
+ }
+ });
+ }
+ };
+}
diff --git a/src/w11k-select-option/_w11k-select-options.less b/src/w11k-select-option/_w11k-select-options.less
new file mode 100644
index 0000000..ea5d154
--- /dev/null
+++ b/src/w11k-select-option/_w11k-select-options.less
@@ -0,0 +1,79 @@
+*[w11k-select-option] {
+ .table-row {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ cursor: pointer;
+
+ > * {
+ display: table-cell;
+ }
+ &:hover {
+ background-color: gray;
+ }
+ }
+
+ .table-row.selected {
+ background-color: gray;
+ }
+
+}
+
+/* sorry about this - couldn't get it to work otherwise
+ especially IE with hover background and indent */
+
+@padding: 19px;
+ul > li {
+ > div {
+ padding-left: 0;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 1;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 2;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 3;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 4;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 5;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 6;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 7;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 8;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 9;
+ }
+ ul > li {
+ > div {
+ padding-left: @padding * 10;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/w11k-select-option/_w11k-select-options.scss b/src/w11k-select-option/_w11k-select-options.scss
new file mode 100644
index 0000000..33c9597
--- /dev/null
+++ b/src/w11k-select-option/_w11k-select-options.scss
@@ -0,0 +1,79 @@
+*[w11k-select-option] {
+ .table-row {
+ padding-top: 4px;
+ padding-bottom: 4px;
+ cursor: pointer;
+
+ > * {
+ display: table-cell;
+ }
+ &:hover {
+ background-color: gray;
+ }
+ }
+
+ .table-row.selected {
+ background-color: gray;
+ }
+
+}
+
+/* sorry about this - couldn't get it to work otherwise
+ especially IE with hover background and indent */
+
+$paddig: 19px;
+ul > li {
+ > div {
+ padding-left: 0;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 1;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 2;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 3;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 4;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 5;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 6;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 7;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 8;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 9;
+ }
+ ul > li {
+ > div {
+ padding-left: $paddig * 10;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/w11k-select-option/w11k-select-option.directive.ts b/src/w11k-select-option/w11k-select-option.directive.ts
new file mode 100644
index 0000000..aa3e3b1
--- /dev/null
+++ b/src/w11k-select-option/w11k-select-option.directive.ts
@@ -0,0 +1,122 @@
+import {OptionState} from '../model/option-state.enum';
+import {InternalOption} from '../model/internal-option.model';
+
+class Result {
+ selected: number = 0;
+ unselected: number = 0;
+ childsSelected: number = 0;
+ length: number;
+
+ constructor(length: number) {
+ this.length = length;
+ }
+}
+
+export function w11kSelectOptionDirektive(w11kSelectConfig) {
+ 'ngInject';
+
+ return {
+ restrict: 'A',
+ replace: false,
+ templateUrl: w11kSelectConfig.common.templateUrlOptions,
+ scope: {
+ 'options': '=',
+ 'parent': '=',
+ 'select': '&',
+ },
+ require: 'ngModel',
+ controller: function ($scope, $attrs, $parse) {
+ if ($scope.$parent.childsMap) {
+ $scope.$parent.addChild($scope, $scope.parent)
+ }
+ $scope.childsMap = {};
+
+
+ $scope.upwardsClick = function (clickedOption: InternalOption, res: Result) {
+ let fatherOption: InternalOption = $scope.options.find(option => option.trackingId === clickedOption.parent);
+ if (res.selected === 0 && res.childsSelected === 0) {
+ setSelected(fatherOption, OptionState.unselected, $scope);
+ } else if (res.selected === res.length) {
+ setSelected(fatherOption, OptionState.selected, $scope);
+ } else {
+ setSelected(fatherOption, OptionState.childsSelected, $scope);
+
+ }
+
+ if ($scope.$parent.upwardsClick) {
+ let res = calcRes($scope.options);
+ $scope.$parent.upwardsClick(fatherOption, res);
+ }
+
+
+ };
+
+ $scope.addChild = function (childScope, father) {
+ $scope.childsMap[father.trackingId] = childScope;
+ };
+ $scope.onOptionStateClick = function ($event, option: InternalOption) {
+
+ switch (option.state) {
+ case OptionState.unselected:
+ setSelected(option, OptionState.selected, $scope);
+ break;
+ case OptionState.selected:
+ setSelected(option, OptionState.unselected, $scope);
+ break;
+ case OptionState.childsSelected:
+ setSelected(option, OptionState.selected, $scope);
+ break;
+ }
+
+ // upwards Click
+ if ($scope.$parent.upwardsClick) {
+ let res = calcRes($scope.options);
+ $scope.$parent.upwardsClick(option, res);
+ }
+
+ $scope.childsMap[option.trackingId].downWardstoggleAll(option.state);
+ };
+
+ $scope.downWardstoggleAll = function (toSetState: OptionState) {
+ $scope.options = toggleDownWards($scope.options, toSetState, $scope);
+ }
+ },
+ };
+}
+
+
+function toggleDownWards(options: InternalOption[], toSetState: OptionState, $scope): InternalOption[] {
+ return options.map(
+ option => {
+ option.children = toggleDownWards(option.children, toSetState, $scope);
+ setSelected(option, toSetState, $scope);
+ return option;
+ }
+ )
+}
+
+
+function calcRes(options: InternalOption[]): Result {
+ return options.reduce(
+ (prev: Result, next: InternalOption) => {
+ if (next.state === OptionState.selected) {
+ prev.selected++;
+ }
+ if (next.state === OptionState.unselected) {
+ prev.unselected++;
+ }
+ if (next.state === OptionState.childsSelected) {
+ prev.childsSelected++;
+ }
+
+ return prev
+
+ },
+ new Result(options.length))
+}
+
+
+function setSelected(option: InternalOption, optionState: OptionState, $scope) {
+ option.state = optionState;
+ $scope.select({option: option});
+}
diff --git a/src/w11k-select-option/w11k-select-option.tpl.html b/src/w11k-select-option/w11k-select-option.tpl.html
new file mode 100644
index 0000000..5bc356c
--- /dev/null
+++ b/src/w11k-select-option/w11k-select-option.tpl.html
@@ -0,0 +1,11 @@
+
+
+
+ {{::option.label}}
+
+
+
+
+
diff --git a/src/w11k-select.directive.ts b/src/w11k-select.directive.ts
new file mode 100644
index 0000000..3738119
--- /dev/null
+++ b/src/w11k-select.directive.ts
@@ -0,0 +1,594 @@
+/** @internal */
+import * as angular from 'angular';
+import {setSelected} from './lib/set-selected';
+import {internalOptions2externalModel} from './lib/internal-options-2-external-model';
+import {value2trackingId} from './lib/value-2-tracking-id';
+import {externalOptions2internalOptions} from './lib/external-options-2-internal-options';
+import {InternalOption} from './model/internal-option.model';
+import {OptionState} from './model/option-state.enum';
+import {ConfigInstance} from './model/config.model';
+import {collectActiveLabels} from './lib/collect-active-labels';
+
+
+export interface Scope extends ng.IScope {
+ config: ConfigInstance;
+ style: any; // only required once?
+ onKeyPressedOnDropDownToggle: (event: any) => void;
+ showMoreOptions: () => void;
+ onFilterValueChanged: () => void;
+ clearFilter: () => void;
+ onKeyPressedInFilter: ($event: any) => void;
+ selectFiltered: ($event?: any) => void
+ deselectFiltered: ($event: any) => void
+ deselectAll: ($event: any) => void
+ select: ($event: any) => void;
+ isEmpty: () => boolean;
+
+ options: {
+ visible: any[]
+ };
+ filter: {
+ values: any
+ };
+
+ dropdown: {
+ onOpen?: ($event: any) => void | undefined;
+ onClose?: () => void | undefined;
+ close?: () => void;
+ toggle?: any
+ }
+
+}
+
+export function w11kSelect(w11kSelectConfig, $parse, $document, w11kSelectHelper, $filter, $timeout, $window, $q) {
+ 'ngInject';
+
+ let jqWindow = angular.element($window);
+
+ return {
+ restrict: 'A',
+ replace: false,
+ templateUrl: w11kSelectConfig.common.templateUrl,
+ scope: {},
+ require: 'ngModel',
+ controller: function ($scope, $attrs, $parse) {
+ if ($attrs.w11kSelect && $attrs.w11kSelect.length > 0) {
+ let exposeExpression = $parse($attrs.w11kSelect);
+
+ if (exposeExpression.assign) {
+ exposeExpression.assign($scope.$parent, this);
+ }
+ }
+
+ this.open = function () {
+ $scope.dropdown.open();
+ };
+
+ this.close = function () {
+ $scope.dropdown.close();
+ };
+
+ this.toggle = function () {
+ $scope.dropdown.toggle();
+ };
+ },
+ compile: function (tElement, tAttrs) {
+ let configExpParsed = $parse(tAttrs.w11kSelectConfig);
+ let optionsExpParsed = w11kSelectHelper.parseOptions(tAttrs.w11kSelectOptions);
+
+ let ngModelSetter = $parse(tAttrs.ngModel).assign;
+ let assignValueFn = optionsExpParsed.value.assign;
+
+ if (optionsExpParsed.tracking !== undefined && assignValueFn === undefined) {
+ throw new Error('value part of w11kSelectOptions expression must be assignable if \'track by\' is used');
+ }
+
+ return function (scope: Scope, iElement: any, iAttrs: any, controller: any) {
+ let domElement = iElement[0];
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * internal model
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ let hasBeenOpened = false;
+ let internalOptions: InternalOption[] = [];
+ let internalOptionsMap = {};
+ let optionsFiltered = [];
+
+ scope.options = {
+ visible: []
+ };
+
+ scope.filter = {
+ values: {}
+ };
+
+ scope.config = angular.copy(w11kSelectConfig.instance);
+
+ // marker to read some parts of the config only once
+ let configRead = false;
+
+ scope.$watch(
+ function () {
+ return configExpParsed(scope.$parent);
+ },
+ function (newConfig) {
+ if (angular.isArray(newConfig)) {
+ w11kSelectHelper.extendDeep.apply(null, [scope.config].concat(newConfig));
+ applyConfig();
+ }
+ else if (angular.isObject(newConfig)) {
+ w11kSelectHelper.extendDeep(scope.config, newConfig);
+ applyConfig();
+ }
+ },
+ true
+ );
+
+ function applyConfig() {
+ optionsAlreadyRead.then(function () {
+ checkSelection();
+ updateNgModel();
+ checkConfig(scope.config, setViewValue);
+ });
+
+ if (!configRead) {
+ updateStaticTexts();
+ configRead = true;
+ }
+ }
+
+ function updateStaticTexts() {
+ if (scope.config.filter.select.active && scope.config.filter.select.text) {
+ let selectFilteredButton = domElement.querySelector('.select-filtered-text');
+ selectFilteredButton.textContent = scope.config.filter.select.text;
+ }
+
+ if (scope.config.filter.deselect.active && scope.config.filter.deselect.text) {
+ let deselectFilteredButton = domElement.querySelector('.deselect-filtered-text');
+ deselectFilteredButton.textContent = scope.config.filter.deselect.text;
+ }
+
+ if (scope.config.header.placeholder) {
+ let headerPlaceholder = domElement.querySelector('.header-placeholder');
+ headerPlaceholder.textContent = scope.config.header.placeholder;
+ }
+ }
+
+ function checkSelection() {
+ if (scope.config.multiple === false) {
+ let selectedOptions: InternalOption[] = internalOptions.filter(
+ (option) => option.state === OptionState.selected
+ );
+
+ if (selectedOptions.length > 0) {
+ setSelected(selectedOptions, false);
+ selectedOptions[0].state = OptionState.selected;
+ }
+ }
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * dropdown
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+ function onEscPressed(event) {
+ if (event.keyCode === 27) {
+ if (scope.dropdown.close) { // check is for ts only
+ scope.dropdown.close();
+ }
+ }
+ }
+
+ function adjustHeight() {
+ if (angular.isDefined(scope.config.style.maxHeight)) {
+ domDropDownContent.style.maxHeight = scope.style.maxHeight;
+ }
+ else {
+ let maxHeight = calculateDynamicMaxHeight();
+ domDropDownContent.style.maxHeight = maxHeight + 'px';
+
+ }
+ }
+
+ function resetHeight() {
+ domDropDownContent.style.maxHeight = '';
+ }
+
+ function calculateDynamicMaxHeight() {
+ let maxHeight;
+
+ let contentOffset = domDropDownContent.getBoundingClientRect().top;
+
+ let windowHeight = $window.innerHeight || $window.document.documentElement.clientHeight;
+
+ let containerHeight;
+ let containerOffset;
+
+ if (angular.isDefined(domHeightAdjustContainer)) {
+ containerHeight = domHeightAdjustContainer.innerHeight || domHeightAdjustContainer.clientHeight;
+ containerOffset = domHeightAdjustContainer.getBoundingClientRect().top;
+ }
+ else {
+ containerHeight = $window.innerHeight || $window.document.documentElement.clientHeight;
+ containerOffset = 0;
+ }
+
+ if (scope.config.style.marginBottom.indexOf('px') < 0) {
+ throw new Error('Illegal Value for w11kSelectStyle.marginBottom');
+ }
+ let marginBottom = parseFloat(scope.config.style.marginBottom.slice(0, -2));
+
+ let referenceHeight;
+ let referenceOffset;
+
+ if (containerHeight + containerOffset > windowHeight) {
+ referenceHeight = windowHeight;
+ referenceOffset = 0;
+ }
+ else {
+ referenceHeight = containerHeight;
+ referenceOffset = containerOffset;
+ }
+
+ maxHeight = referenceHeight - (contentOffset - referenceOffset) - marginBottom;
+
+ let minHeightFor3Elements = 93;
+ if (maxHeight < minHeightFor3Elements) {
+ maxHeight = minHeightFor3Elements;
+ }
+
+ return maxHeight;
+ }
+
+ let domDropDownMenu = domElement.querySelector('.dropdown-menu');
+ let domDropDownContent = domElement.querySelector('.dropdown-menu .content');
+ let domHeightAdjustContainer = w11kSelectHelper.getParent(iElement, '.w11k-select-adjust-height-to');
+ let domHeaderText = domElement.querySelector('.header-text');
+
+ scope.dropdown = {
+ onOpen: function ($event) {
+ if (scope.config.disabled) {
+ $event.prevent();
+ return;
+ }
+
+ if (hasBeenOpened === false) {
+ hasBeenOpened = true;
+ }
+ filterOptions();
+
+ $document.on('keyup', onEscPressed);
+
+ domDropDownMenu.style.visibility = 'hidden';
+ $timeout(function () {
+ adjustHeight();
+ domDropDownMenu.style.visibility = 'visible';
+
+ if (scope.config.filter.active) {
+ // use timeout to open dropdown first and then set the focus,
+ // otherwise focus won't be set because iElement is not visible
+ $timeout(function () {
+ iElement[0].querySelector('.dropdown-menu input').focus();
+ });
+ }
+ });
+ jqWindow.on('resize', adjustHeight);
+
+ if (angular.isFunction(scope.config.dropdown.onOpen)) {
+ (scope.config.dropdown.onOpen as any)();
+ }
+ },
+ onClose: function () {
+ // important: set properties of filter.values to empty strings not to null,
+ // otherwise angular's filter won't work
+ scope.filter.values.label = '';
+
+ $timeout(function () {
+ resetHeight();
+ });
+ $document.off('keyup', onEscPressed);
+ jqWindow.off('resize', adjustHeight);
+
+ if (angular.isFunction(scope.config.dropdown.onClose)) {
+ (scope.config.dropdown.onClose as any)();
+ }
+ }
+ };
+
+ scope.$on('$destroy', function () {
+ $document.off('keyup', onEscPressed);
+ jqWindow.off('resize', adjustHeight);
+ });
+
+ scope.onKeyPressedOnDropDownToggle = function ($event) {
+ // enter or space
+ if ($event.keyCode === 13 || $event.keyCode === 32) {
+ $event.preventDefault();
+ $event.stopPropagation();
+
+ scope.dropdown.toggle();
+ }
+ };
+
+ function updateHeader() {
+ if (angular.isDefined(scope.config.header.text)) {
+ domHeaderText.textContent = scope.$parent.$eval(scope.config.header.text as any);
+ }
+ else {
+ let arr = [];
+ internalOptions.forEach(option => collectActiveLabels(option, arr));
+ domHeaderText.textContent = arr.join(', ');
+ }
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * filter
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ let filter = $filter('filter');
+ let initialLimitTo = 80;
+ let increaseLimitTo = initialLimitTo * 0.5;
+
+ function filterOptions() {
+ if (hasBeenOpened) {
+ // false as third parameter: use contains to compare
+ optionsFiltered = filter(internalOptions, scope.filter.values, false);
+ scope.options.visible = optionsFiltered.slice(0, initialLimitTo);
+ }
+ }
+
+ scope.showMoreOptions = function () {
+ scope.options.visible = optionsFiltered.slice(0, scope.options.visible.length + increaseLimitTo);
+ };
+
+ scope.onFilterValueChanged = function () {
+ filterOptions();
+ };
+
+ scope.clearFilter = function () {
+ scope.filter.values = {};
+ filterOptions();
+ };
+
+ scope.onKeyPressedInFilter = function ($event) {
+ // on enter
+ if ($event.keyCode === 13) {
+ $event.preventDefault();
+ $event.stopPropagation();
+
+ scope.selectFiltered();
+ }
+ };
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * buttons
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ scope.selectFiltered = function ($event?) {
+ if (angular.isDefined($event)) {
+ $event.preventDefault();
+ $event.stopPropagation();
+ }
+
+ if (scope.config.multiple) {
+ setSelected(optionsFiltered, true);
+ }
+ else if (optionsFiltered.length === 1) {
+ scope.select(optionsFiltered[0]); // behaves like if the option was clicked using the mouse
+ }
+
+ setViewValue();
+ };
+
+ scope.deselectFiltered = function ($event) {
+ if (angular.isDefined($event)) {
+ $event.preventDefault();
+ $event.stopPropagation();
+ }
+
+ setSelected(optionsFiltered, false);
+ setViewValue();
+ };
+
+ scope.deselectAll = function ($event) {
+ if (angular.isDefined($event)) {
+ $event.preventDefault();
+ $event.stopPropagation();
+ }
+
+ setSelected(internalOptions, false);
+ setViewValue();
+ };
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * options
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+
+ let optionsAlreadyRead;
+
+ let updateOptions = (function () {
+ let deferred = $q.defer();
+ optionsAlreadyRead = deferred.promise;
+
+ return function updateOptions() {
+ let externalOptions = optionsExpParsed.collection(scope.$parent);
+ let viewValue = controller.$viewValue;
+
+ if (angular.isArray(externalOptions)) {
+ internalOptions = externalOptions2internalOptions(externalOptions, viewValue, w11kSelectHelper, optionsExpParsed, scope.config);
+ internalOptionsMap = {};
+ let i = internalOptions.length;
+ while (i--) {
+ let option: any = internalOptions[i];
+ if (internalOptionsMap[option.trackingId]) {
+ throw new Error('Duplicate hash value for options ' + option.label + ' and ' + internalOptionsMap[option.trackingId].label);
+ }
+ internalOptionsMap[option.trackingId] = option;
+ }
+
+ filterOptions();
+
+ if (ngModelAlreadyRead) {
+ updateNgModel();
+ }
+ deferred.resolve();
+ }
+ };
+ })();
+
+ // watch for changes of options collection made outside
+ scope.$watchCollection(
+ function externalOptionsWatch() {
+ return optionsExpParsed.collection(scope.$parent);
+ },
+ function externalOptionsWatchAction(newVal) {
+ if (angular.isDefined(newVal)) {
+ updateOptions();
+ }
+ }
+ );
+
+ scope.select = function select(option: InternalOption) {
+ // runs only if hierarchy is flat and multiple false
+
+ if (scope.config.multiple) {
+ setViewValue();
+ return
+ }
+
+ // disable all others:
+ setSelected(internalOptions, false);
+ option.state = OptionState.selected;
+ setViewValue();
+ (scope.dropdown.close as any)();
+ };
+
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ngModel
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ function setViewValue() {
+ let selectedValues = internalOptions2externalModel(internalOptions, optionsExpParsed, w11kSelectConfig);
+
+ controller.$setViewValue(selectedValues);
+ updateHeader();
+ }
+
+ function updateNgModel() {
+ let value = internalOptions2externalModel(internalOptions, optionsExpParsed, w11kSelectConfig);
+ angular.forEach(controller.$parsers, function (parser) {
+ value = parser(value);
+ });
+
+ ngModelSetter(scope.$parent, value);
+ }
+
+
+ let ngModelAlreadyRead;
+
+ function render() {
+ optionsAlreadyRead.then(function () {
+ ngModelAlreadyRead = true;
+
+ let viewValue = controller.$viewValue;
+
+ setSelected(internalOptions, false);
+
+ let i = viewValue.length;
+ while (i--) {
+ let trackingId = value2trackingId(viewValue[i], w11kSelectHelper, optionsExpParsed);
+ let option = internalOptionsMap[trackingId];
+
+ if (option) {
+ option.state = OptionState.selected;
+ }
+ }
+
+ updateHeader();
+ });
+ }
+
+
+ function external2internal(modelValue) {
+ let viewValue;
+
+ if (angular.isArray(modelValue)) {
+ viewValue = modelValue;
+ }
+ else if (angular.isDefined(modelValue)) {
+ viewValue = [modelValue];
+ }
+ else {
+ viewValue = [];
+ }
+
+ return viewValue;
+ }
+
+ function internal2external(viewValue) {
+ if (angular.isUndefined(viewValue)) {
+ return;
+ }
+
+ let modelValue;
+
+ if (scope.config.multiple) {
+ modelValue = viewValue;
+ }
+ else {
+ modelValue = viewValue[0];
+ }
+
+ return modelValue;
+ }
+
+ function validateRequired(viewValue) {
+ if (scope.config.multiple === true && scope.config.required === true && viewValue.length === 0) {
+ return false;
+ }
+ if (scope.config.multiple === false && scope.config.required === true && viewValue === undefined) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function isEmpty() {
+ let value = controller.$viewValue;
+ return !(angular.isArray(value) && value.length > 0);
+ }
+
+ scope.isEmpty = isEmpty;
+
+ controller.$isEmpty = isEmpty;
+
+ controller.$render = render;
+ controller.$formatters.push(external2internal);
+ controller.$validators.required = validateRequired;
+ controller.$parsers.push(internal2external);
+
+ };
+ }
+ };
+}
+
+
+let checkConfig = (config: ConfigInstance, setViewValue) => {
+ /**
+ * Currently there is a bug if multiple = false and required = true.
+ * Then the validator runs only once, before the config is present
+ * and returns a wrong validation state.
+ * might be fixed by calling updateNgModel() here
+ * */
+ // throw error if multiple is false and childrenKey is present
+ if (config.children && !config.multiple) {
+ throw new Error('Multiple must be enabled when displaying hierarchically structure');
+ }
+ if (config.children) {
+ setViewValue();
+ }
+};
diff --git a/src/w11k-select.js b/src/w11k-select.js
deleted file mode 100644
index ada3645..0000000
--- a/src/w11k-select.js
+++ /dev/null
@@ -1,918 +0,0 @@
-'use strict';
-
-angular.module('w11k.select', [
- 'pasvaz.bindonce',
- 'w11k.dropdownToggle',
- 'w11k.select.template'
-]);
-
-angular.module('w11k.select').constant('w11kSelectConfig', {
- common: {
- /**
- * path to template
- * do not change if you're using w11k-select.tpl.js
- * adjust if you want to use your own template or
- */
- templateUrl: 'w11k-select.tpl.html'
- },
- instance: {
- /** for form validation */
- required: false,
- /** Hide checkboxes during single selection */
- hideCheckboxes: false,
- /** single or multiple select */
- multiple: true,
- /** disable user interaction */
- disabled: false,
- /** all the configuration for the header (visible if dropdown closed) */
- header: {
- /** text to show if no item selected (plain text, no evaluation, no data-binding) */
- placeholder: '',
- /**
- * text to show if item(s) selected (expression, evaluated against user scope)
- * make sure to enclose your expression withing quotes, otherwise it will be evaluated too early
- * default: undefined evaluates to a comma separated representation of selected items
- * example: ng-model="options.selected" w11k-select-config="{header: {placeholder: 'options.selected.length'}}"
- */
- text: undefined
- },
- dropdown: {
- onOpen: undefined,
- onClose: undefined
- },
- /** all the configuration for the filter section within the dropdown */
- filter: {
- /** activate filter input to search for options */
- active: true,
- /** text to show if no filter is applied */
- placeholder: 'Filter',
- /** 'select all filtered options' button */
- select: {
- /** show select all button */
- active: true,
- /**
- * label for select all button
- * default: undefined evaluates to 'all'
- */
- text: undefined
- },
- /** 'deselect all filtered options' button */
- deselect: {
- /** show deselect all button */
- active: true,
- /**
- * label for deselect all button
- * default: undefined evaluates to 'none'
- */
- text: undefined
- }
- },
- /** values for dynamically calculated styling of dropdown */
- style: {
- /** margin-bottom for automatic height adjust */
- marginBottom: '10px',
- /** static or manually calculated max height (disables internal height calculation) */
- maxHeight: undefined
- },
- /** when set to true, the clear-button is always visible. */
- showClearAlways: false
- }
-});
-
-angular.module('w11k.select').factory('w11kSelectHelper', ['$parse', '$document', function ($parse, $document) {
-
- // value as label for item in collection | filter track by tracking
- var OPTIONS_EXP = /^([a-zA-Z][\w\.]*)(?:\s+as\s+([a-zA-Z][\w\.]*))?\s+for\s+(?:([a-zA-Z][\w]*))\s+in\s+([$_a-zA-Z][\w\.\(\)]*(?:\s+\|\s[a-zA-Z][\w\:_\{\}']*)*)(?:\s+track\sby\s+([a-zA-Z][\w\.]*))?$/;
-
- function extendDeep(dst) {
- angular.forEach(arguments, function (obj) {
- if (obj !== dst) {
- angular.forEach(obj, function (value, key) {
- if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
- extendDeep(dst[key], value);
- } else {
- dst[key] = value;
- }
- });
- }
- });
- return dst;
- }
-
- function hashCode(value) {
- var string;
- if (typeof value === 'object') {
- string = angular.toJson(value);
- }
- else {
- string = value.toString();
- }
-
- var hash = 0;
- var length = string.length;
- for (var i = 0; i < length; i++) {
- hash = string.charCodeAt(i) + (hash << 6) + (hash << 16) - hash;
- }
-
- return hash.toString(36);
- }
-
- function parseOptions(input) {
-
- var match = input.match(OPTIONS_EXP);
- if (!match) {
- var expected = '"item.value" [as "item.label"] for "item" in "collection [ | filter ] [track by item.value.unique]"';
- throw new Error('Expected options in form of \'' + expected + '\' but got "' + input + '".');
- }
-
- var result = {
- value: $parse(match[1]),
- label: $parse(match[2] || match[1]),
- item: match[3],
- collection: $parse(match[4])
- };
-
- if (match[5] !== undefined) {
- result.tracking = $parse(match[5]);
- }
-
- return result;
- }
-
- function getParent(element, selector) {
- // with jQuery
- if (angular.isFunction(element.parents)) {
- var container = element.parents(selector);
- if (container.length > 0) {
- return container[0];
- }
-
- return;
- }
-
- // without jQuery
- var matchesSelector = 'MatchesSelector';
- var matchFunctions = [
- 'matches',
- 'matchesSelector',
- 'moz' + matchesSelector,
- 'webkit' + matchesSelector,
- 'ms' + matchesSelector,
- 'o' + matchesSelector
- ];
-
- for (var index in matchFunctions) {
- var matchFunction = matchFunctions[index];
- if (angular.isFunction(element[0][matchFunction])) {
- var parent1 = element[0].parentNode;
- while (parent1 !== $document[0]) {
- if (parent1[matchFunction](selector)) {
- return parent1;
- }
- parent1 = parent1.parentNode;
- }
-
- return;
- }
- }
-
- return;
- }
-
-
- return {
- parseOptions: parseOptions,
- hashCode: hashCode,
- extendDeep: extendDeep,
- getParent: getParent
- };
-}]);
-
-angular.module('w11k.select').directive('w11kSelect', [
- 'w11kSelectConfig', '$parse', '$document', 'w11kSelectHelper', '$filter', '$timeout', '$window', '$q',
- function (w11kSelectConfig, $parse, $document, w11kSelectHelper, $filter, $timeout, $window, $q) {
-
- var jqWindow = angular.element($window);
-
- return {
- restrict: 'A',
- replace: false,
- templateUrl: w11kSelectConfig.common.templateUrl,
- scope: {},
- require: 'ngModel',
- controller: ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
- if ($attrs.w11kSelect && $attrs.w11kSelect.length > 0) {
- var exposeExpression = $parse($attrs.w11kSelect);
-
- if (exposeExpression.assign) {
- exposeExpression.assign($scope.$parent, this);
- }
- }
-
- this.open = function () {
- $scope.dropdown.open();
- };
-
- this.close = function () {
- $scope.dropdown.close();
- };
-
- this.toggle = function () {
- $scope.dropdown.toggle();
- };
- }],
- compile: function (tElement, tAttrs) {
- var configExpParsed = $parse(tAttrs.w11kSelectConfig);
- var optionsExpParsed = w11kSelectHelper.parseOptions(tAttrs.w11kSelectOptions);
-
- var ngModelSetter = $parse(tAttrs.ngModel).assign;
- var assignValueFn = optionsExpParsed.value.assign;
-
- if (optionsExpParsed.tracking !== undefined && assignValueFn === undefined) {
- throw new Error('value part of w11kSelectOptions expression must be assignable if \'track by\' is used');
- }
-
- return function (scope, iElement, iAttrs, controller) {
- var domElement = iElement[0];
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * internal model
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- var hasBeenOpened = false;
- var internalOptions = [];
- var internalOptionsMap = {};
- var optionsFiltered = [];
-
- scope.options = {
- visible: []
- };
-
- scope.filter = {
- values: {}
- };
-
- scope.config = angular.copy(w11kSelectConfig.instance);
-
- // marker to read some parts of the config only once
- var configRead = false;
-
- scope.$watch(
- function () {
- return configExpParsed(scope.$parent);
- },
- function (newConfig) {
- if (angular.isArray(newConfig)) {
- w11kSelectHelper.extendDeep.apply(null, [scope.config].concat(newConfig));
- applyConfig();
- }
- else if (angular.isObject(newConfig)) {
- w11kSelectHelper.extendDeep(scope.config, newConfig);
- applyConfig();
- }
- },
- true
- );
-
- function applyConfig() {
- optionsAlreadyRead.then(function () {
- checkSelection();
- updateNgModel();
- });
-
- if (!configRead) {
- updateStaticTexts();
- configRead = true;
- }
- }
-
- function updateStaticTexts() {
- if (scope.config.filter.select.active && scope.config.filter.select.text) {
- var selectFilteredButton = domElement.querySelector('.select-filtered-text');
- selectFilteredButton.textContent = scope.config.filter.select.text;
- }
-
- if (scope.config.filter.deselect.active && scope.config.filter.deselect.text) {
- var deselectFilteredButton = domElement.querySelector('.deselect-filtered-text');
- deselectFilteredButton.textContent = scope.config.filter.deselect.text;
- }
-
- if (scope.config.header.placeholder) {
- var headerPlaceholder = domElement.querySelector('.header-placeholder');
- headerPlaceholder.textContent = scope.config.header.placeholder;
- }
- }
-
- function checkSelection() {
- if (scope.config.multiple === false) {
- var selectedOptions = internalOptions.filter(function (option) {
- return option.selected;
- });
-
- if (selectedOptions.length > 0) {
- setSelected(selectedOptions, false);
- selectedOptions[0].selected = true;
- }
- }
- }
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * dropdown
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
-
- function onEscPressed(event) {
- if (event.keyCode === 27) {
- scope.dropdown.close();
- }
- }
-
- function adjustHeight() {
- if (angular.isDefined(scope.config.style.maxHeight)) {
- domDropDownContent.style.maxHeight = scope.style.maxHeight;
- }
- else {
- var maxHeight = calculateDynamicMaxHeight();
- domDropDownContent.style.maxHeight = maxHeight + 'px';
-
- }
- }
-
- function resetHeight() {
- domDropDownContent.style.maxHeight = '';
- }
-
- function calculateDynamicMaxHeight() {
- var maxHeight;
-
- var contentOffset = domDropDownContent.getBoundingClientRect().top;
-
- var windowHeight = $window.innerHeight || $window.document.documentElement.clientHeight;
-
- var containerHeight;
- var containerOffset;
-
- if (angular.isDefined(domHeightAdjustContainer)) {
- containerHeight = domHeightAdjustContainer.innerHeight || domHeightAdjustContainer.clientHeight;
- containerOffset = domHeightAdjustContainer.getBoundingClientRect().top;
- }
- else {
- containerHeight = $window.innerHeight || $window.document.documentElement.clientHeight;
- containerOffset = 0;
- }
-
- if (scope.config.style.marginBottom.indexOf('px') < 0) {
- throw new Error('Illegal Value for w11kSelectStyle.marginBottom');
- }
- var marginBottom = parseFloat(scope.config.style.marginBottom.slice(0, -2));
-
- var referenceHeight;
- var referenceOffset;
-
- if (containerHeight + containerOffset > windowHeight) {
- referenceHeight = windowHeight;
- referenceOffset = 0;
- }
- else {
- referenceHeight = containerHeight;
- referenceOffset = containerOffset;
- }
-
- maxHeight = referenceHeight - (contentOffset - referenceOffset) - marginBottom;
-
- var minHeightFor3Elements = 93;
- if (maxHeight < minHeightFor3Elements) {
- maxHeight = minHeightFor3Elements;
- }
-
- return maxHeight;
- }
-
- var domDropDownMenu = domElement.querySelector('.dropdown-menu');
- var domDropDownContent = domElement.querySelector('.dropdown-menu .content');
- var domHeightAdjustContainer = w11kSelectHelper.getParent(iElement, '.w11k-select-adjust-height-to');
- var domHeaderText = domElement.querySelector('.header-text');
-
- scope.dropdown = {
- onOpen: function ($event) {
- if (scope.config.disabled) {
- $event.prevent();
- return;
- }
-
- if (hasBeenOpened === false) {
- hasBeenOpened = true;
- }
- filterOptions();
-
- $document.on('keyup', onEscPressed);
-
- domDropDownMenu.style.visibility = 'hidden';
- $timeout(function () {
- adjustHeight();
- domDropDownMenu.style.visibility = 'visible';
-
- if (scope.config.filter.active) {
- // use timeout to open dropdown first and then set the focus,
- // otherwise focus won't be set because iElement is not visible
- $timeout(function () {
- iElement[0].querySelector('.dropdown-menu input').focus();
- });
- }
- });
- jqWindow.on('resize', adjustHeight);
-
- if (angular.isFunction(scope.config.dropdown.onOpen)) {
- scope.config.dropdown.onOpen();
- }
- },
- onClose: function () {
- // important: set properties of filter.values to empty strings not to null,
- // otherwise angular's filter won't work
- scope.filter.values.label = '';
-
- $timeout(function () {
- resetHeight();
- });
- $document.off('keyup', onEscPressed);
- jqWindow.off('resize', adjustHeight);
-
- if (angular.isFunction(scope.config.dropdown.onClose)) {
- scope.config.dropdown.onClose();
- }
- }
- };
-
- scope.$on('$destroy', function () {
- $document.off('keyup', onEscPressed);
- jqWindow.off('resize', adjustHeight);
- });
-
- scope.onKeyPressedOnDropDownToggle = function ($event) {
- // enter or space
- if ($event.keyCode === 13 || $event.keyCode === 32) {
- $event.preventDefault();
- $event.stopPropagation();
-
- scope.dropdown.toggle();
- }
- };
-
- function updateHeader() {
- if (angular.isDefined(scope.config.header.text)) {
- domHeaderText.textContent = scope.$parent.$eval(scope.config.header.text);
- }
- else {
- var optionsSelected = internalOptions.filter(function (option) {
- return option.selected;
- });
-
- var selectedOptionsLabels = optionsSelected.map(function (option) {
- return option.label;
- });
-
- domHeaderText.textContent = selectedOptionsLabels.join(', ');
- }
- }
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * filter
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- var filter = $filter('filter');
- var initialLimitTo = 80;
- var increaseLimitTo = initialLimitTo * 0.5;
-
- function filterOptions() {
- if (hasBeenOpened) {
- // false as third parameter: use contains to compare
- optionsFiltered = filter(internalOptions, scope.filter.values, false);
- scope.options.visible = optionsFiltered.slice(0, initialLimitTo);
- }
- }
-
- scope.showMoreOptions = function () {
- scope.options.visible = optionsFiltered.slice(0, scope.options.visible.length + increaseLimitTo);
- };
-
- scope.onFilterValueChanged = function () {
- filterOptions();
- };
-
- scope.clearFilter = function () {
- scope.filter.values = {};
- filterOptions();
- };
-
- scope.onKeyPressedInFilter = function ($event) {
- // on enter
- if ($event.keyCode === 13) {
- $event.preventDefault();
- $event.stopPropagation();
-
- scope.selectFiltered();
- }
- };
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * buttons
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- scope.selectFiltered = function ($event) {
- if (angular.isDefined($event)) {
- $event.preventDefault();
- $event.stopPropagation();
- }
-
- if (scope.config.multiple) {
- setSelected(optionsFiltered, true);
- }
- else if (optionsFiltered.length === 1) {
- scope.select(optionsFiltered[0]); //behaves like if the option was clicked using the mouse
- }
-
- setViewValue();
- };
-
- scope.deselectFiltered = function ($event) {
- if (angular.isDefined($event)) {
- $event.preventDefault();
- $event.stopPropagation();
- }
-
- setSelected(optionsFiltered, false);
- setViewValue();
- };
-
- scope.deselectAll = function ($event) {
- if (angular.isDefined($event)) {
- $event.preventDefault();
- $event.stopPropagation();
- }
-
- setSelected(internalOptions, false);
- setViewValue();
- };
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * options
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- function externalOptions2internalOptions(externalOptions, viewValue) {
- var viewValueIDs = {};
-
- var i = viewValue.length;
- while (i--) {
- var trackingId = value2trackingId(viewValue[i]);
- viewValueIDs[trackingId] = true;
- }
-
- var internalOptions = externalOptions.map(function (externalOption) {
- var value = externalOption2value(externalOption);
- var trackingId = value2trackingId(value);
- var label = externalOption2label(externalOption);
-
- var selected;
- if (viewValueIDs[trackingId]) {
- selected = true;
- }
- else {
- selected = false;
- }
-
- var internalOption = {
- trackingId: trackingId,
- label: label,
- model: externalOption,
- selected: selected
- };
-
- return internalOption;
- });
-
- return internalOptions;
- }
-
- var optionsAlreadyRead;
-
- var updateOptions = (function () {
- var deferred = $q.defer();
- optionsAlreadyRead = deferred.promise;
-
- return function updateOptions() {
- var externalOptions = optionsExpParsed.collection(scope.$parent);
- var viewValue = controller.$viewValue;
-
- if (angular.isArray(externalOptions)) {
- internalOptions = externalOptions2internalOptions(externalOptions, viewValue);
-
- internalOptionsMap = {};
- var i = internalOptions.length;
- while (i--) {
- var option = internalOptions[i];
- if (internalOptionsMap[option.trackingId]) {
- throw new Error('Duplicate hash value for options ' + option.label + ' and ' + internalOptionsMap[option.trackingId].label);
- }
- internalOptionsMap[option.trackingId] = option;
- }
-
- filterOptions();
-
- if (ngModelAlreadyRead) {
- updateNgModel();
- }
- deferred.resolve();
- }
- };
- })();
-
- // watch for changes of options collection made outside
- scope.$watchCollection(
- function externalOptionsWatch() {
- return optionsExpParsed.collection(scope.$parent);
- },
- function externalOptionsWatchAction(newVal) {
- if (angular.isDefined(newVal)) {
- updateOptions();
- }
- }
- );
-
- scope.select = function select(option) {
- if (option.selected === false && scope.config.multiple === false) {
- setSelected(internalOptions, false);
- option.selected = true;
-
- scope.dropdown.close();
- setViewValue();
- }
- else if (option.selected && scope.config.required === false && scope.config.multiple === false) {
- option.selected = false;
- scope.dropdown.close();
- setViewValue();
- }
- else if (scope.config.multiple) {
- option.selected = !option.selected;
- setViewValue();
- }
- };
-
- // called on click to a checkbox of an option
- scope.onOptionStateClick = function onOptionStateClick($event, option) {
- // we have to stop propagation, otherwise selected state will be toggled twice
- // because of click handler of list element
- $event.stopPropagation();
-
- if (option.selected && scope.config.required && scope.config.multiple === false) {
- $event.preventDefault();
- }
-
- scope.select(option);
- };
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * ngModel
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- function setViewValue() {
- var selectedValues = internalOptions2externalModel(internalOptions);
-
- controller.$setViewValue(selectedValues);
- updateHeader();
- }
-
- function updateNgModel() {
- var value = internalOptions2externalModel(internalOptions);
-
- angular.forEach(controller.$parsers, function (parser) {
- value = parser(value);
- });
-
- ngModelSetter(scope.$parent, value);
- }
-
-
- var ngModelAlreadyRead;
-
- function render() {
- optionsAlreadyRead.then(function () {
- ngModelAlreadyRead = true;
-
- var viewValue = controller.$viewValue;
-
- setSelected(internalOptions, false);
-
- var i = viewValue.length;
- while (i--) {
- var trackingId = value2trackingId(viewValue[i]);
- var option = internalOptionsMap[trackingId];
-
- if (option) {
- option.selected = true;
- }
- }
-
- updateHeader();
- });
- }
-
- function value2trackingId(value) {
- if (optionsExpParsed.tracking !== undefined) {
- var context = {};
- assignValueFn(context, value);
-
- var trackingValue = optionsExpParsed.tracking(context);
-
- if (trackingValue === undefined) {
- throw new Error('Couldn\'t get \'track by\' value. Please make sure to only use something in \'track by’ part of w11kSelectOptions expression, accessible from result of value part. (\'option.data\' and \'option.data.unique\' but not \'option.unique\')');
- }
-
- return trackingValue.toString();
- }
- else {
- return w11kSelectHelper.hashCode(value);
- }
-
- }
-
- function external2internal(modelValue) {
- var viewValue;
-
- if (angular.isArray(modelValue)) {
- viewValue = modelValue;
- }
- else if (angular.isDefined(modelValue)) {
- viewValue = [modelValue];
- }
- else {
- viewValue = [];
- }
-
- return viewValue;
- }
-
- function internal2external(viewValue) {
- if (angular.isUndefined(viewValue)) {
- return;
- }
-
- var modelValue;
-
- if (scope.config.multiple) {
- modelValue = viewValue;
- }
- else {
- modelValue = viewValue[0];
- }
-
- return modelValue;
- }
-
- function validateRequired(viewValue) {
-
- if (scope.config.multiple === true && scope.config.required === true && viewValue.length === 0) {
- return false;
- }
-
- if (scope.config.multiple === false && scope.config.required === true && viewValue === undefined) {
- return false;
- }
-
- return true;
- }
-
- function isEmpty() {
- var value = controller.$viewValue;
- return !(angular.isArray(value) && value.length > 0);
- }
-
- scope.isEmpty = isEmpty;
-
- controller.$isEmpty = isEmpty;
-
- controller.$render = render;
- controller.$formatters.push(external2internal);
-
- if (angular.version.major === 1 && angular.version.minor < 3) {
- controller.$parsers.push(function (viewValue) {
- controller.$setValidity('required', validateRequired(viewValue));
- });
- } else {
- controller.$validators.required = validateRequired;
- }
-
- controller.$parsers.push(internal2external);
-
-
- /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- * helper functions
- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-
- function setSelected(options, selected) {
- var i = options.length;
- while (i--) {
- options[i].selected = selected;
- }
- }
-
- function internalOptions2externalModel(options) {
- var selectedOptions = options.filter(function (option) {
- return option.selected;
- });
-
- var selectedValues = selectedOptions.map(internalOption2value);
-
- return selectedValues;
- }
-
- function internalOption2value(option) {
- return externalOption2value(option.model);
- }
-
- function externalOption2value(option) {
- var context = {};
- context[optionsExpParsed.item] = option;
-
- return optionsExpParsed.value(context);
- }
-
- function externalOption2label(option) {
- var context = {};
- context[optionsExpParsed.item] = option;
-
- return optionsExpParsed.label(context);
- }
- };
- }
- };
- }
-]);
-
-angular.module('w11k.select').directive('w11kSelectInfiniteScroll', ['$timeout', function ($timeout) {
- return {
- link: function (scope, element, attrs) {
- var scrollDistance = 0;
- var scrollEnabled = true;
- var checkImmediatelyWhenEnabled = false;
-
- var onDomScrollHandler = function () {
- onScrollHandler(true);
- };
-
- var scrollContainer = element[0];
-
- if (scrollContainer.children.length !== 1) {
- throw new Error('scroll container has to have exactly one child!');
- }
-
- var content = scrollContainer.children[0];
-
- var onScrollHandler = function (apply) {
-
- var distanceToBottom = content.clientHeight - scrollContainer.scrollTop;
- var shouldScroll = distanceToBottom <= scrollContainer.clientHeight * (scrollDistance + 1);
-
- if (shouldScroll && scrollEnabled) {
- if (apply) {
- scope.$apply(function () {
- scope.$eval(attrs.w11kSelectInfiniteScroll);
- });
- }
- else {
- scope.$eval(attrs.w11kSelectInfiniteScroll);
- }
- }
- else if (shouldScroll) {
- checkImmediatelyWhenEnabled = true;
- }
- };
-
- attrs.$observe('w11kSelectInfiniteScrollDistance', function (value) {
- scrollDistance = parseFloat(value);
- });
-
-
- attrs.$observe('w11kSelectInfiniteScrollDisabled', function (value) {
- scrollEnabled = !value;
-
- if (scrollEnabled && checkImmediatelyWhenEnabled) {
- checkImmediatelyWhenEnabled = false;
- onScrollHandler();
- }
- });
-
- element.on('scroll', onDomScrollHandler);
- scope.$on('$destroy', function () {
- element.off('scroll', onDomScrollHandler);
- });
-
- return $timeout(function () {
- if (attrs.w11kSelectInfiniteScrollImmediateCheck) {
- if (scope.$eval(attrs.w11kSelectInfiniteScrollImmediateCheck)) {
- onScrollHandler();
- }
- }
- });
- }
- };
-}]);
diff --git a/src/w11k-select.less b/src/w11k-select.less
index ed9820e..b163990 100644
--- a/src/w11k-select.less
+++ b/src/w11k-select.less
@@ -1,3 +1,6 @@
+@import "/w11k-select-option/_w11k-select-options";
+@import "w11k-select-checkbox/_w11k-checkbox.directive";
+
.w11k-select {
a:hover {
text-decoration: none;
@@ -65,48 +68,16 @@
overflow-x: hidden;
max-height: 500px;
width: 100%;
+ padding-top: 4px;
ul.items {
- padding: 6px 0;
margin: 0;
- display: table;
width: 100%;
> li {
margin: 0;
padding: 0;
- display: table-row;
-
- > div {
- display: table-cell;
- padding: 4px;
- }
-
- > div.state {
- padding-left: 12px;
-
- input {
- position: relative;
- top: -1px;
- cursor: pointer;
- }
- }
-
- > div.name {
- padding-right: 24px;
- width: 100%;
- }
}
-
- > li.selected {
- background-color: gray;
- }
-
- > li:hover {
- background-color: gray;
- cursor: pointer;
- }
-
}
&.hidden-checkboxes ul li input[type="checkbox"] {
diff --git a/src/w11k-select.scss b/src/w11k-select.scss
index ed9820e..6683a80 100644
--- a/src/w11k-select.scss
+++ b/src/w11k-select.scss
@@ -1,3 +1,6 @@
+@import "w11k-select-option/w11k-select-options";
+@import "w11k-select-checkbox/w11k-checkbox.directive";
+
.w11k-select {
a:hover {
text-decoration: none;
@@ -65,48 +68,16 @@
overflow-x: hidden;
max-height: 500px;
width: 100%;
+ padding-top: 4px;
ul.items {
- padding: 6px 0;
margin: 0;
- display: table;
width: 100%;
> li {
margin: 0;
padding: 0;
- display: table-row;
-
- > div {
- display: table-cell;
- padding: 4px;
- }
-
- > div.state {
- padding-left: 12px;
-
- input {
- position: relative;
- top: -1px;
- cursor: pointer;
- }
- }
-
- > div.name {
- padding-right: 24px;
- width: 100%;
- }
}
-
- > li.selected {
- background-color: gray;
- }
-
- > li:hover {
- background-color: gray;
- cursor: pointer;
- }
-
}
&.hidden-checkboxes ul li input[type="checkbox"] {
diff --git a/src/w11k-select.tpl.html b/src/w11k-select.tpl.html
index c76ac3b..dc8f64b 100644
--- a/src/w11k-select.tpl.html
+++ b/src/w11k-select.tpl.html
@@ -1,56 +1,54 @@