Коммит
dabd604bae
37
Makefile
37
Makefile
|
@ -7,7 +7,7 @@
|
||||||
# @author Raimund Schlüßler
|
# @author Raimund Schlüßler
|
||||||
# @copyright 2017 Raimund Schlüßler <raimund.schluessler@googlemail.com>
|
# @copyright 2017 Raimund Schlüßler <raimund.schluessler@googlemail.com>
|
||||||
|
|
||||||
# Generic Makefile for building and packaging a Nextcloud app which uses npm and
|
# Generic Makefile for building and packaging a Nextcloud app which uses yarn and
|
||||||
# Composer.
|
# Composer.
|
||||||
#
|
#
|
||||||
# Dependencies:
|
# Dependencies:
|
||||||
|
@ -15,31 +15,31 @@
|
||||||
# * which
|
# * which
|
||||||
# * curl: used if phpunit and composer are not installed to fetch them from the web
|
# * curl: used if phpunit and composer are not installed to fetch them from the web
|
||||||
# * tar: for building the archive
|
# * tar: for building the archive
|
||||||
# * npm: for building and testing everything JS
|
# * yarn: for building and testing everything JS
|
||||||
#
|
#
|
||||||
# If no composer.json is in the app root directory, the Composer step
|
# If no composer.json is in the app root directory, the Composer step
|
||||||
# will be skipped. The same goes for the package.json which can be located in
|
# will be skipped. The same goes for the package.json which can be located in
|
||||||
# the app root or the js/ directory.
|
# the app root or the js/ directory.
|
||||||
#
|
#
|
||||||
# The npm command by launches the npm build script:
|
# The yarn command by launches the yarn build script:
|
||||||
#
|
#
|
||||||
# npm run build
|
# yarn run build
|
||||||
#
|
#
|
||||||
# The npm test command launches the npm test script:
|
# The yarn test command launches the yarn test script:
|
||||||
#
|
#
|
||||||
# npm run test
|
# yarn run test
|
||||||
#
|
#
|
||||||
# The idea behind this is to be completely testing and build tool agnostic. All
|
# The idea behind this is to be completely testing and build tool agnostic. All
|
||||||
# build tools and additional package managers should be installed locally in
|
# build tools and additional package managers should be installed locally in
|
||||||
# your project, since this won't pollute people's global namespace.
|
# your project, since this won't pollute people's global namespace.
|
||||||
#
|
#
|
||||||
# The following npm scripts in your package.json install and update the bower
|
# The following yarn scripts in your package.json install and update the
|
||||||
# and npm dependencies and use gulp as build system (notice how everything is
|
# yarn dependencies and use gulp as build system (notice how everything is
|
||||||
# run from the node_modules folder):
|
# run from the node_modules folder):
|
||||||
#
|
#
|
||||||
# "scripts": {
|
# "scripts": {
|
||||||
# "test": "node node_modules/gulp-cli/bin/gulp.js karma",
|
# "test": "node node_modules/gulp-cli/bin/gulp.js karma",
|
||||||
# "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
|
# "prebuild": "yarn install && yarn upgrade",
|
||||||
# "build": "node node_modules/gulp-cli/bin/gulp.js"
|
# "build": "node node_modules/gulp-cli/bin/gulp.js"
|
||||||
# },
|
# },
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ source_package_name=$(source_artifact_directory)/$(app_name)
|
||||||
appstore_build_directory=$(CURDIR)/build/appstore/tasks
|
appstore_build_directory=$(CURDIR)/build/appstore/tasks
|
||||||
appstore_artifact_directory=$(CURDIR)/build/artifacts/appstore
|
appstore_artifact_directory=$(CURDIR)/build/artifacts/appstore
|
||||||
appstore_package_name=$(appstore_artifact_directory)/$(app_name)
|
appstore_package_name=$(appstore_artifact_directory)/$(app_name)
|
||||||
npm=$(shell which npm 2> /dev/null)
|
yarn=$(shell which yarn 2> /dev/null)
|
||||||
gcp=$(shell which gcp 2> /dev/null)
|
gcp=$(shell which gcp 2> /dev/null)
|
||||||
|
|
||||||
ifeq (, $(gcp))
|
ifeq (, $(gcp))
|
||||||
|
@ -83,23 +83,22 @@ all: build
|
||||||
|
|
||||||
# Fetches the PHP and JS dependencies and compiles the JS. If no composer.json
|
# Fetches the PHP and JS dependencies and compiles the JS. If no composer.json
|
||||||
# is present, the composer step is skipped, if no package.json or js/package.json
|
# is present, the composer step is skipped, if no package.json or js/package.json
|
||||||
# is present, the npm step is skipped
|
# is present, the yarn step is skipped
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build:
|
||||||
make npm
|
make yarn
|
||||||
|
|
||||||
# Installs npm dependencies
|
# Installs yarn dependencies
|
||||||
.PHONY: npm
|
.PHONY: yarn
|
||||||
npm:
|
yarn:
|
||||||
cd js && $(npm) run build
|
cd js && $(yarn) run build
|
||||||
|
|
||||||
# Removes the appstore build
|
# Removes the appstore build
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf ./build
|
rm -rf ./build
|
||||||
|
|
||||||
# Same as clean but also removes dependencies installed by composer, bower and
|
# Same as clean but also removes dependencies installed by yarn
|
||||||
# npm
|
|
||||||
.PHONY: distclean
|
.PHONY: distclean
|
||||||
distclean: clean
|
distclean: clean
|
||||||
rm -rf vendor
|
rm -rf vendor
|
||||||
|
@ -218,7 +217,7 @@ endif
|
||||||
# from the internet
|
# from the internet
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
cd js && $(npm) run test
|
cd js && $(yarn) run test
|
||||||
ifeq (, $(shell which phpunit 2> /dev/null))
|
ifeq (, $(shell which phpunit 2> /dev/null))
|
||||||
@echo "No phpunit command available, downloading a copy from the web"
|
@echo "No phpunit command available, downloading a copy from the web"
|
||||||
mkdir -p $(build_tools_directory)
|
mkdir -p $(build_tools_directory)
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"directory": "vendor",
|
|
||||||
"ignoredDependencies": [
|
|
||||||
"jquery",
|
|
||||||
"moment"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -38,6 +38,7 @@
|
||||||
"exports": true,
|
"exports": true,
|
||||||
"escapeHTML": true,
|
"escapeHTML": true,
|
||||||
"possible": true,
|
"possible": true,
|
||||||
"dav": true
|
"dav": true,
|
||||||
|
"OCA": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "stylelint-config-standard",
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-scss"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indentation": "tab",
|
||||||
|
"number-leading-zero": "never",
|
||||||
|
"comment-empty-line-before": ["always", {
|
||||||
|
"except": ["first-nested"]
|
||||||
|
}],
|
||||||
|
"at-rule-no-unknown": null,
|
||||||
|
"scss/at-rule-no-unknown": true
|
||||||
|
}
|
||||||
|
}
|
130
js/Gruntfile.js
130
js/Gruntfile.js
|
@ -1,130 +0,0 @@
|
||||||
/**
|
|
||||||
* Nextcloud - Tasks
|
|
||||||
*
|
|
||||||
* @author Raimund Schlüßler
|
|
||||||
* @copyright 2017 Raimund Schlüßler <raimund.schluessler@googlemail.com>
|
|
||||||
*
|
|
||||||
* This library is free software; you can redistribute it and/or
|
|
||||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
* License as published by the Free Software Foundation; either
|
|
||||||
* version 3 of the License, or any later version.
|
|
||||||
*
|
|
||||||
* This library is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public
|
|
||||||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = function(grunt) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
|
||||||
grunt.loadNpmTasks('grunt-wrap');
|
|
||||||
// grunt.loadNpmTasks('grunt-karma');
|
|
||||||
grunt.loadNpmTasks('grunt-svg-sprite');
|
|
||||||
|
|
||||||
grunt.initConfig({
|
|
||||||
meta: {
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
version: '<%= meta.pkg.version %>',
|
|
||||||
banner: '/**\n' + ' * <%= meta.pkg.description %> - v<%= meta.version %>\n' + ' *\n' + ' * Copyright (c) <%= grunt.template.today("yyyy") %> - ' + '<%= meta.pkg.author.name %> <<%= meta.pkg.author.email %>>\n' + ' *\n' + ' * This file is licensed under the Affero\
|
|
||||||
General Public License version 3 or later.\n' + ' * See the COPYING file\n' + ' *\n' + ' */\n\n',
|
|
||||||
build: 'app/',
|
|
||||||
production: 'public/'
|
|
||||||
},
|
|
||||||
concat: {
|
|
||||||
"default": {
|
|
||||||
options: {
|
|
||||||
banner: '<%= meta.banner %>\n',
|
|
||||||
stripBanners: {
|
|
||||||
options: 'block'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
src: '<%= meta.build %>/**/*.js',
|
|
||||||
dest: '<%= meta.production %>app.js'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wrap: {
|
|
||||||
"default": {
|
|
||||||
src: '<%= meta.production %>app.js',
|
|
||||||
dest: '',
|
|
||||||
wrapper: ['(function(angular, $, oc_requesttoken, undefined){\n\n', '\n})(window.angular, window.jQuery, oc_requesttoken);']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
jshint: {
|
|
||||||
files: [
|
|
||||||
'Gruntfile.js',
|
|
||||||
'<%= meta.build %>**/*.js'
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
jshintrc: '.jshintrc',
|
|
||||||
reporter: require('jshint-stylish')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
concat: {
|
|
||||||
files: [
|
|
||||||
'<%= meta.build %>**/*.js'
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
livereload: true
|
|
||||||
},
|
|
||||||
tasks: ['js']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
karma: {
|
|
||||||
unit: {
|
|
||||||
configFile: 'config/karma.js'
|
|
||||||
},
|
|
||||||
continuous: {
|
|
||||||
configFile: 'config/karma.js',
|
|
||||||
singleRun: true,
|
|
||||||
reporters: ['progress', 'junit'],
|
|
||||||
junitReporter: {
|
|
||||||
outputFile: 'test-results.xml'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
svg_sprite: {
|
|
||||||
basic: {
|
|
||||||
|
|
||||||
// Target basics
|
|
||||||
expand: true,
|
|
||||||
cwd: '../img/src',
|
|
||||||
src: ['**/*.svg'],
|
|
||||||
dest: '..',
|
|
||||||
|
|
||||||
// Target options
|
|
||||||
options: {
|
|
||||||
shape: {
|
|
||||||
transform: []
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
css: { // Activate the «css» mode
|
|
||||||
bust: false,
|
|
||||||
common: 'icon',
|
|
||||||
dimensions: '',
|
|
||||||
prefix: '.ico-%s',
|
|
||||||
sprite: "../img/sprites.svg",
|
|
||||||
render: {
|
|
||||||
css: true // Activate CSS output (with default options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// grunt.registerTask('ci', ['karma:continuous']);
|
|
||||||
grunt.registerTask('js', ['concat']);
|
|
||||||
grunt.registerTask('default', 'js');
|
|
||||||
grunt.registerTask('build', ['concat', 'svg_sprite']);
|
|
||||||
};
|
|
20
js/README.md
20
js/README.md
|
@ -1,20 +0,0 @@
|
||||||
# JS & CSS development
|
|
||||||
You need grunt installed to compile the coffeescript and less files.
|
|
||||||
To compile all files run `grunt dev`.
|
|
||||||
|
|
||||||
## JS
|
|
||||||
To compile the coffeescript run:
|
|
||||||
`grunt js`
|
|
||||||
|
|
||||||
## CSS
|
|
||||||
To compile the less files run:
|
|
||||||
`grunt css`
|
|
||||||
|
|
||||||
## Watch Tasks
|
|
||||||
Instead of executing the command everytime you change a file, grunt can watch for changes.
|
|
||||||
Use the following commands instead:
|
|
||||||
```
|
|
||||||
grunt watch:dev
|
|
||||||
grunt watch:js
|
|
||||||
grunt watch:css
|
|
||||||
```
|
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Nextcloud - Tasks",
|
|
||||||
"version": "0.9.5",
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "1.5.5",
|
|
||||||
"angular-route": "1.5.5",
|
|
||||||
"angular-animate": "1.5.5",
|
|
||||||
"angular-sanitize": "1.5.5",
|
|
||||||
"angular-ui-select": "https://github.com/angular-ui/ui-select.git#v0.19.8",
|
|
||||||
"angular-draganddrop": "https://github.com/marceljuenemann/angular-drag-and-drop-lists.git#v2.1.0",
|
|
||||||
"jquery-timepicker": "",
|
|
||||||
"ical.js": "~1.2.2",
|
|
||||||
"jstzdetect": ""
|
|
||||||
},
|
|
||||||
"devDependencies": {},
|
|
||||||
"homepage": "https://github.com/nextcloud/tasks",
|
|
||||||
"authors": [
|
|
||||||
"Raimund Schlüßler <raimund.schluessler@googlemail.com>"
|
|
||||||
],
|
|
||||||
"description": "",
|
|
||||||
"main": "",
|
|
||||||
"moduleType": [],
|
|
||||||
"license": "AGPLv3",
|
|
||||||
"private": true,
|
|
||||||
"ignore": [
|
|
||||||
"**/.*",
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
"core/vendor",
|
|
||||||
"test",
|
|
||||||
"tests"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* Nextcloud - Inventory
|
||||||
|
*
|
||||||
|
* This file is licensed under the Affero General Public License version 3 or
|
||||||
|
* later. See the COPYING file.
|
||||||
|
*
|
||||||
|
* @author Bernhard Posselt <dev@bernhard-posselt.com>
|
||||||
|
* @copyright Bernhard Posselt 2012, 2014
|
||||||
|
*
|
||||||
|
* @author Georg Ehrke
|
||||||
|
* @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
|
||||||
|
*
|
||||||
|
* @author Raimund Schlüßler
|
||||||
|
* @copyright 2017 Raimund Schlüßler <raimund.schluessler@googlemail.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*jslint node: true */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// get plugins
|
||||||
|
const gulp = require('gulp'),
|
||||||
|
uglify = require('gulp-uglify'),
|
||||||
|
jshint = require('gulp-jshint'),
|
||||||
|
KarmaServer = require('karma').Server,
|
||||||
|
concat = require('gulp-concat'),
|
||||||
|
wrap = require('gulp-wrap'),
|
||||||
|
strip = require('gulp-strip-banner'),
|
||||||
|
babel = require('gulp-babel'),
|
||||||
|
stylelint = require('gulp-stylelint'),
|
||||||
|
sourcemaps = require('gulp-sourcemaps'),
|
||||||
|
svgSprite = require('gulp-svg-sprite'),
|
||||||
|
npmFiles = require('gulp-npm-files');
|
||||||
|
|
||||||
|
// configure
|
||||||
|
const buildTarget = 'app.min.js';
|
||||||
|
const scssBuildTarget = 'style.scss';
|
||||||
|
const karmaConfig = __dirname + '/../tests/js/config/karma.js';
|
||||||
|
const destinationFolder = __dirname + '/public/';
|
||||||
|
const scssDestinationFolder = '../css/';
|
||||||
|
|
||||||
|
const jsSources = [
|
||||||
|
'app/**/*.js'
|
||||||
|
];
|
||||||
|
const scssSources = [
|
||||||
|
'../css/src/*.scss'
|
||||||
|
];
|
||||||
|
|
||||||
|
const testSources = ['../tests/js/unit/**/*.js'];
|
||||||
|
const lintSources = jsSources.concat(testSources).concat(['*.js']);
|
||||||
|
const watchSources = lintSources;
|
||||||
|
|
||||||
|
const svgConfig = {
|
||||||
|
shape: {
|
||||||
|
transform: []
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
css: { // Activate the «css» mode
|
||||||
|
bust: false,
|
||||||
|
common: 'icon',
|
||||||
|
dimensions: '',
|
||||||
|
prefix: '.ico-%s',
|
||||||
|
sprite: "../img/sprites.svg",
|
||||||
|
render: {
|
||||||
|
css: {
|
||||||
|
dest: "../css/sprite.css"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// tasks
|
||||||
|
gulp.task('build', ['lint'], () => {
|
||||||
|
|
||||||
|
return gulp.src(jsSources)
|
||||||
|
.pipe(babel({
|
||||||
|
presets: ['es2015'],
|
||||||
|
compact: false,
|
||||||
|
babelrc: false,
|
||||||
|
ast: false
|
||||||
|
}))
|
||||||
|
.pipe(strip())
|
||||||
|
.pipe(sourcemaps.init({identityMap: true}))
|
||||||
|
.pipe(concat(buildTarget))
|
||||||
|
.pipe(wrap(`(function($, oc_requesttoken, undefined){
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
<%= contents %>
|
||||||
|
})(jQuery, oc_requesttoken);`))
|
||||||
|
.pipe(uglify())
|
||||||
|
.pipe(sourcemaps.write('./'))
|
||||||
|
.pipe(gulp.dest(destinationFolder));
|
||||||
|
});
|
||||||
|
|
||||||
|
// gulp.task('default', ['build', 'vendor', 'scsslint', 'scssConcat']);
|
||||||
|
gulp.task('default', ['build', 'vendor', 'svg_sprite']);
|
||||||
|
|
||||||
|
gulp.task('lint', () => {
|
||||||
|
return gulp.src(lintSources)
|
||||||
|
.pipe(jshint('.jshintrc'))
|
||||||
|
.pipe(jshint.reporter('default'));
|
||||||
|
// .pipe(jshint.reporter('fail'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('scsslint', () => {
|
||||||
|
return gulp.src(scssSources)
|
||||||
|
.pipe(stylelint ({
|
||||||
|
reporters: [{
|
||||||
|
formatter: 'string',
|
||||||
|
console: true
|
||||||
|
}]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('scssConcat', ['svg_sprite'], () => {
|
||||||
|
return gulp.src(scssSources)
|
||||||
|
.pipe(concat(scssBuildTarget))
|
||||||
|
.pipe(gulp.dest(scssDestinationFolder));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('scssConcatWatch', () => {
|
||||||
|
return gulp.src(scssSources)
|
||||||
|
.pipe(concat(scssBuildTarget))
|
||||||
|
.pipe(gulp.dest(scssDestinationFolder));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('watch', () => {
|
||||||
|
gulp.watch(watchSources, ['build']);
|
||||||
|
gulp.watch(scssSources, ['scssConcatWatch']);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('karma', (done) => {
|
||||||
|
new KarmaServer({
|
||||||
|
configFile: karmaConfig,
|
||||||
|
singleRun: true,
|
||||||
|
browsers: ['Firefox'],
|
||||||
|
reporters: ['progress', 'coverage']
|
||||||
|
}, done).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('watch-karma', (done) => {
|
||||||
|
new KarmaServer({
|
||||||
|
configFile: karmaConfig,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Firefox'],
|
||||||
|
reporters: ['progress', 'coverage']
|
||||||
|
}, done).start();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('svg_sprite', () => {
|
||||||
|
return gulp.src('**/*.svg', {cwd: '../img/src'})
|
||||||
|
.pipe(svgSprite(svgConfig))
|
||||||
|
.pipe(gulp.dest('..'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("vendor", () => {
|
||||||
|
return gulp.src(npmFiles(), { base: "./node_modules" })
|
||||||
|
.pipe(gulp.dest('vendor'));
|
||||||
|
});
|
|
@ -6,12 +6,13 @@
|
||||||
"name": "Raimund Schlüßler",
|
"name": "Raimund Schlüßler",
|
||||||
"email": "raimund.schluessler@googlemail.com"
|
"email": "raimund.schluessler@googlemail.com"
|
||||||
},
|
},
|
||||||
|
"license": "AGPLv3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "",
|
"homepage": "https://github.com/nextcloud/tasks",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node node_modules/grunt-cli/bin/grunt karma",
|
"test": "node node_modules/gulp-cli/bin/gulp.js karma",
|
||||||
"prebuild": "npm install && npm update && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
|
"prebuild": "yarn install && yarn upgrade",
|
||||||
"build": "node node_modules/grunt-cli/bin/grunt build"
|
"build": "node node_modules/gulp-cli/bin/gulp.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -19,19 +20,36 @@
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/nextcloud/tasks/issues",
|
"bugs": "https://github.com/nextcloud/tasks/issues",
|
||||||
"contributors": [],
|
"contributors": [],
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"angular": "1.5.5",
|
||||||
|
"angular-animate": "1.5.5",
|
||||||
|
"angular-draganddrop": "https://github.com/marceljuenemann/angular-drag-and-drop-lists.git#v2.1.0",
|
||||||
|
"angular-route": "1.5.5",
|
||||||
|
"angular-sanitize": "1.5.5",
|
||||||
|
"angular-ui-select": "https://github.com/angular-ui/ui-select.git#v0.19.8",
|
||||||
|
"ical.js": "~1.2.2",
|
||||||
|
"jstimezonedetect": ""
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bower": "*",
|
"babel-preset-es2015": "^6.14.0",
|
||||||
"grunt": "^1.0.1",
|
"gulp": "^3.9.1",
|
||||||
"grunt-cli": "^1.2.0",
|
"gulp-babel": "^6.1.2",
|
||||||
"grunt-contrib-jshint": "^1.0.0",
|
"gulp-cli": "^1.2.2",
|
||||||
"jshint-stylish": "^2.2.1",
|
"gulp-concat": "^2.6.1",
|
||||||
"grunt-concurrent": "^2.3.1",
|
"gulp-jshint": "^2.0.4",
|
||||||
"grunt-contrib-concat": "^1.0.1",
|
"gulp-ng-annotate": "^2.0.0",
|
||||||
"grunt-contrib-watch": "^1.0.0",
|
"gulp-npm-files": "^0.1.3",
|
||||||
"grunt-karma": "^2.0.0",
|
"gulp-sourcemaps": "^2.4.0",
|
||||||
"grunt-wrap": "^0.3.0",
|
"gulp-strip-banner": "0.0.2",
|
||||||
"grunt-svg-sprite": "1.3.7"
|
"gulp-stylelint": "^4.0.0",
|
||||||
|
"gulp-svg-sprite": "1.3.7",
|
||||||
|
"gulp-uglify": "^2.0.0",
|
||||||
|
"gulp-wrap": "^0.13.0",
|
||||||
|
"jshint": "^2.9.4",
|
||||||
|
"karma": "^1.3.0",
|
||||||
|
"stylelint": "^8.0.0",
|
||||||
|
"stylelint-config-standard": "^17.0.0",
|
||||||
|
"stylelint-scss": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
|
4962
js/public/app.js
4962
js/public/app.js
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular-animate",
|
|
||||||
"version": "1.5.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./angular-animate.js",
|
|
||||||
"ignore": [],
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "1.5.5"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/angular/bower-angular-animate",
|
|
||||||
"_release": "1.5.5",
|
|
||||||
"_resolution": {
|
|
||||||
"type": "version",
|
|
||||||
"tag": "v1.5.5",
|
|
||||||
"commit": "39c4ea7a81ed05b09229f5961e31e1d9dc251bf8"
|
|
||||||
},
|
|
||||||
"_source": "https://github.com/angular/bower-angular-animate.git",
|
|
||||||
"_target": "1.5.5",
|
|
||||||
"_originalSource": "angular-animate"
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular-drag-and-drop-lists",
|
|
||||||
"main": "angular-drag-and-drop-lists.js",
|
|
||||||
"version": "2.1.0",
|
|
||||||
"homepage": "https://github.com/marceljuenemann/angular-drag-and-drop-lists",
|
|
||||||
"authors": [
|
|
||||||
"Marcel Juenemann <marcel@juenemann.cc>"
|
|
||||||
],
|
|
||||||
"description": "Angular directives for sorting nested lists using the HTML5 Drag & Drop API",
|
|
||||||
"keywords": [
|
|
||||||
"angular",
|
|
||||||
"drag",
|
|
||||||
"drop",
|
|
||||||
"dnd",
|
|
||||||
"nested",
|
|
||||||
"sortable",
|
|
||||||
"lists",
|
|
||||||
"html5"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"ignore": [
|
|
||||||
"**/.*",
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
"demo",
|
|
||||||
"*.json",
|
|
||||||
"test",
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"_release": "2.1.0",
|
|
||||||
"_resolution": {
|
|
||||||
"type": "version",
|
|
||||||
"tag": "2.1.0",
|
|
||||||
"commit": "7e98e194a8d66fa735bf6dc7c3e39886287b914e"
|
|
||||||
},
|
|
||||||
"_source": "https://github.com/marceljuenemann/angular-drag-and-drop-lists.git",
|
|
||||||
"_target": "v2.1.0",
|
|
||||||
"_originalSource": "https://github.com/marceljuenemann/angular-drag-and-drop-lists.git"
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
<h1>Demo: Advanced Features</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>dnd-effect-allowed:</strong> This demo shows how to use dnd-effect-allowed to control which drop effects are allowed.
|
||||||
|
If the source and target elements have no drop effect that is allowed on both, then a drop is not possible. If there are multiple
|
||||||
|
possible drop effects, then the user can control the drop effect using modifier keys (Ctrl and Alt).</li>
|
||||||
|
<li><strong>dnd-external-sources:</strong> Allows to drag and drop elements accross browser windows, which you can test in this
|
||||||
|
example. The downside to this is that the lists will accept arbitrary text to be dropped. To prevent that, the dnd-drop callback
|
||||||
|
verifies that the dropped element is of the desired format.</li>
|
||||||
|
<li><strong>dnd-allowed-types in nested lists:</strong> We are using the dnd-allowed-types attribute to ensure that Containers
|
||||||
|
only accept items, but not other containers.</li>
|
||||||
|
<li><strong>dnd-horizontal-list:</strong> This attribute tells the positioning algorithm to drop incoming elements left or right
|
||||||
|
of the existing elements, instead of above or below.</li>
|
||||||
|
<li><strong>Callbacks:</strong> The directives offer various callbacks, which in this example will log the events to the console.
|
||||||
|
Additionally, the callbacks on the dnd-list can prevent an element from being dropped. In this example <strong>you can't drop elements
|
||||||
|
after the 10th position</strong>, because we are preventing that in the dnd-dragover callback.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="advancedDemo row">
|
||||||
|
<div ng-repeat="containers in model" class="col-md-6">
|
||||||
|
<div class="dropzone box box-yellow" ng-include="'advanced/advanced.html'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div view-source="advanced"></div>
|
||||||
|
|
||||||
|
<h2>Generated Model</h2>
|
||||||
|
<pre>{{modelAsJson}}</pre>
|
|
@ -0,0 +1,66 @@
|
||||||
|
/***************************** Dropzone Styling *****************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dnd-list should always have a min-height,
|
||||||
|
* otherwise you can't drop to it once it's empty
|
||||||
|
*/
|
||||||
|
.advancedDemo .dropzone ul[dnd-list] {
|
||||||
|
min-height: 42px;
|
||||||
|
margin: 0px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advancedDemo .dropzone li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce opacity of elements during the drag operation. This allows the user
|
||||||
|
* to see where he is dropping his element, even if the element is huge. The
|
||||||
|
* .dndDragging class is automatically set during the drag operation.
|
||||||
|
*/
|
||||||
|
.advancedDemo .dropzone .dndDragging {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dndDraggingSource class will be applied to the source element of a drag
|
||||||
|
* operation.
|
||||||
|
*/
|
||||||
|
.advancedDemo .dropzone .dndDraggingSource {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element with .dndPlaceholder class will be added as child of the dnd-list
|
||||||
|
* while the user is dragging over it.
|
||||||
|
*/
|
||||||
|
.advancedDemo .dropzone .dndPlaceholder {
|
||||||
|
background-color: #ddd !important;
|
||||||
|
display: block;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** Element type specific styles *****************************/
|
||||||
|
|
||||||
|
.advancedDemo .dropzone .itemlist {
|
||||||
|
min-height: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advancedDemo .dropzone .itemlist > li {
|
||||||
|
background-color: #337ab7;
|
||||||
|
border: none;
|
||||||
|
border-radius: .25em;
|
||||||
|
color: #fff;
|
||||||
|
float: left;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 50px;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 3px;
|
||||||
|
text-align: center;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advancedDemo .dropzone .container-element {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<h3>Dropzone {{$index + 1}}</h3>
|
||||||
|
<ul dnd-list="containers"
|
||||||
|
dnd-allowed-types="['container']"
|
||||||
|
dnd-external-sources="true"
|
||||||
|
dnd-dragover="dragoverCallback(index, external, type, callback)"
|
||||||
|
dnd-drop="dropCallback(index, item, external, type)">
|
||||||
|
<li ng-repeat="container in containers"
|
||||||
|
dnd-draggable="container"
|
||||||
|
dnd-type="'container'"
|
||||||
|
dnd-effect-allowed="copyMove"
|
||||||
|
dnd-moved="containers.splice($index, 1)"
|
||||||
|
dnd-callback="container.items.length">
|
||||||
|
<div class="container-element box box-blue">
|
||||||
|
<h3>Container (effects allowed: {{container.effectAllowed}})</h3>
|
||||||
|
<ul dnd-list="container.items"
|
||||||
|
dnd-allowed-types="['item']"
|
||||||
|
dnd-horizontal-list="true"
|
||||||
|
dnd-external-sources="true"
|
||||||
|
dnd-effect-allowed="{{container.effectAllowed}}"
|
||||||
|
dnd-dragover="dragoverCallback(index, external, type)"
|
||||||
|
dnd-drop="dropCallback(index, item, external, type)"
|
||||||
|
dnd-inserted="logListEvent('inserted at', index, external, type)"
|
||||||
|
class="itemlist">
|
||||||
|
<li ng-repeat="item in container.items"
|
||||||
|
dnd-draggable="item"
|
||||||
|
dnd-type="'item'"
|
||||||
|
dnd-effect-allowed="{{item.effectAllowed}}"
|
||||||
|
dnd-dragstart="logEvent('Started to drag an item')"
|
||||||
|
dnd-moved="container.items.splice($index, 1)"
|
||||||
|
dnd-dragend="logEvent('Drag operation ended. Drop effect: ' + dropEffect)">
|
||||||
|
{{item.label}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,43 @@
|
||||||
|
angular.module("demo").controller("AdvancedDemoController", function($scope) {
|
||||||
|
|
||||||
|
$scope.dragoverCallback = function(index, external, type, callback) {
|
||||||
|
$scope.logListEvent('dragged over', index, external, type);
|
||||||
|
// Invoke callback to origin for container types.
|
||||||
|
if (type == 'container' && !external) {
|
||||||
|
console.log('Container being dragged contains ' + callback() + ' items');
|
||||||
|
}
|
||||||
|
return index < 10; // Disallow dropping in the third row.
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.dropCallback = function(index, item, external, type) {
|
||||||
|
$scope.logListEvent('dropped at', index, external, type);
|
||||||
|
// Return false here to cancel drop. Return true if you insert the item yourself.
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.logEvent = function(message) {
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.logListEvent = function(action, index, external, type) {
|
||||||
|
var message = external ? 'External ' : '';
|
||||||
|
message += type + ' element was ' + action + ' position ' + index;
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize model
|
||||||
|
$scope.model = [[], []];
|
||||||
|
var id = 10;
|
||||||
|
angular.forEach(['all', 'move', 'copy', 'link', 'copyLink', 'copyMove'], function(effect, i) {
|
||||||
|
var container = {items: [], effectAllowed: effect};
|
||||||
|
for (var k = 0; k < 7; ++k) {
|
||||||
|
container.items.push({label: effect + ' ' + id++, effectAllowed: effect});
|
||||||
|
}
|
||||||
|
$scope.model[i % $scope.model.length].push(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('model', function(model) {
|
||||||
|
$scope.modelAsJson = angular.toJson(model, true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,70 @@
|
||||||
|
body {
|
||||||
|
padding-top: 70px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box > h3 {
|
||||||
|
color: #333;
|
||||||
|
border-color: #ddd;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-padding {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-padding > h3 {
|
||||||
|
margin: -15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-grey {
|
||||||
|
border-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-grey > h3 {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);
|
||||||
|
background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-blue {
|
||||||
|
border-color: #bce8f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-blue > h3 {
|
||||||
|
color: #31708f;
|
||||||
|
background-color: #d9edf7;
|
||||||
|
border-color: #bce8f1;
|
||||||
|
background-image: -webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);
|
||||||
|
background-image: linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-yellow {
|
||||||
|
border-color: #faebcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-yellow > h3 {
|
||||||
|
color: #8a6d3b;
|
||||||
|
background-color: #fcf8e3;
|
||||||
|
border-color: #faebcc;
|
||||||
|
background-image: -webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);
|
||||||
|
background-image: linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
angular.module("demo", ["ngRoute", "dndLists"])
|
||||||
|
.config(function($routeProvider) {
|
||||||
|
$routeProvider
|
||||||
|
.when('/simple', {
|
||||||
|
templateUrl: 'simple/simple-frame.html',
|
||||||
|
controller: 'SimpleDemoController'
|
||||||
|
})
|
||||||
|
.when('/nested', {
|
||||||
|
templateUrl: 'nested/nested-frame.html',
|
||||||
|
controller: 'NestedListsDemoController'
|
||||||
|
})
|
||||||
|
.when('/types', {
|
||||||
|
templateUrl: 'types/types-frame.html',
|
||||||
|
controller: 'TypesDemoController'
|
||||||
|
})
|
||||||
|
.when('/advanced', {
|
||||||
|
templateUrl: 'advanced/advanced-frame.html',
|
||||||
|
controller: 'AdvancedDemoController'
|
||||||
|
})
|
||||||
|
.when('/multi', {
|
||||||
|
templateUrl: 'multi/multi-frame.html',
|
||||||
|
controller: 'MultiDemoController'
|
||||||
|
})
|
||||||
|
.otherwise({redirectTo: '/nested'});
|
||||||
|
})
|
||||||
|
|
||||||
|
.directive('navigation', function($rootScope, $location) {
|
||||||
|
return {
|
||||||
|
template: '<li ng-repeat="option in options" ng-class="{active: isActive(option)}">' +
|
||||||
|
' <a ng-href="{{option.href}}">{{option.label}}</a>' +
|
||||||
|
'</li>',
|
||||||
|
link: function (scope, element, attr) {
|
||||||
|
scope.options = [
|
||||||
|
{label: "Nested Containers", href: "#/nested"},
|
||||||
|
{label: "Simple Demo", href: "#/simple"},
|
||||||
|
{label: "Item Types", href: "#/types"},
|
||||||
|
{label: "Advanced Demo", href: "#/advanced"},
|
||||||
|
{label: "Multiselection", href: "#/multi"},
|
||||||
|
{label: "Github", href: "https://github.com/marceljuenemann/angular-drag-and-drop-lists"}
|
||||||
|
];
|
||||||
|
|
||||||
|
scope.isActive = function(option) {
|
||||||
|
return option.href.indexOf(scope.location) === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
$rootScope.$on("$locationChangeSuccess", function(event, next, current) {
|
||||||
|
scope.location = $location.path();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
7
js/vendor/angular-draganddrop/demo/framework/vendor/bootstrap-theme.min.css
поставляемый
Normal file
7
js/vendor/angular-draganddrop/demo/framework/vendor/bootstrap-theme.min.css
поставляемый
Normal file
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Двоичные данные
js/vendor/angular-draganddrop/demo/framework/vendor/ic_content_copy_black_24dp_2x.png
поставляемый
Normal file
Двоичные данные
js/vendor/angular-draganddrop/demo/framework/vendor/ic_content_copy_black_24dp_2x.png
поставляемый
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 180 B |
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* prism.js default theme for JavaScript, CSS and HTML
|
||||||
|
* Based on dabblet (http://dabblet.com)
|
||||||
|
* @author Lea Verou
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: black;
|
||||||
|
text-shadow: 0 1px white;
|
||||||
|
font-family: Consolas, Monaco, 'Andale Mono', monospace;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection, code[class*="language-"] ::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #f5f2f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: slategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol {
|
||||||
|
color: #905;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.builtin {
|
||||||
|
color: #690;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.variable {
|
||||||
|
color: #a67f59;
|
||||||
|
background: hsla(0,0%,100%,.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #07a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[data-line] {
|
||||||
|
position: relative;
|
||||||
|
padding: 1em 0 1em 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-highlight {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: inherit 0;
|
||||||
|
margin-top: 1em; /* Same as .prism’s padding-top */
|
||||||
|
|
||||||
|
background: hsla(24, 20%, 50%,.08);
|
||||||
|
background: -moz-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||||
|
background: -webkit-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||||
|
background: -o-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||||
|
background: linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0));
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-highlight:before,
|
||||||
|
.line-highlight[data-end]:after {
|
||||||
|
content: attr(data-start);
|
||||||
|
position: absolute;
|
||||||
|
top: .4em;
|
||||||
|
left: .6em;
|
||||||
|
min-width: 1em;
|
||||||
|
padding: 0 .5em;
|
||||||
|
background-color: hsla(24, 20%, 50%,.4);
|
||||||
|
color: hsl(24, 20%, 95%);
|
||||||
|
font: bold 65%/1.5 sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: .3em;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-shadow: none;
|
||||||
|
box-shadow: 0 1px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-highlight[data-end]:after {
|
||||||
|
content: attr(data-end);
|
||||||
|
top: auto;
|
||||||
|
bottom: .4em;
|
||||||
|
}
|
||||||
|
pre.line-numbers {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 3.8em;
|
||||||
|
counter-reset: linenumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.line-numbers > code {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-numbers-rows {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
left: -3.8em;
|
||||||
|
width: 3em; /* works for line-numbers below 1000 lines */
|
||||||
|
letter-spacing: -1px;
|
||||||
|
border-right: 1px solid #999;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers-rows > span {
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
counter-increment: linenumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers-rows > span:before {
|
||||||
|
content: counter(linenumber);
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
padding-right: 0.8em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Prism: Lightweight, robust, elegant syntax highlighting
|
||||||
|
* MIT license http://www.opensource.org/licenses/mit-license.php/
|
||||||
|
* @author Lea Verou http://lea.verou.me
|
||||||
|
*/(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ");var l={element:r,language:o,grammar:u,code:f};t.hooks.run("before-highlight",l);if(i&&self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){l.highlightedCode=n.stringify(JSON.parse(e.data),o);t.hooks.run("before-insert",l);l.element.innerHTML=l.highlightedCode;s&&s.call(l.element);t.hooks.run("after-highlight",l)};c.postMessage(JSON.stringify({language:l.language,code:l.code}))}else{l.highlightedCode=t.highlight(l.code,l.grammar,l.language);t.hooks.run("before-insert",l);l.element.innerHTML=l.highlightedCode;s&&s.call(r);t.hooks.run("after-highlight",l)}},highlight:function(e,r,i){return n.stringify(t.tokenize(e,r),i)},tokenize:function(e,n,r){var i=t.Token,s=[e],o=n.rest;if(o){for(var u in o)n[u]=o[u];delete n.rest}e:for(var u in n){if(!n.hasOwnProperty(u)||!n[u])continue;var a=n[u],f=a.inside,l=!!a.lookbehind,c=0;a=a.pattern||a;for(var h=0;h<s.length;h++){var p=s[h];if(s.length>e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+"</"+s.tag+">"};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();;
|
||||||
|
Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});;
|
||||||
|
Prism.languages.css={comment:/\/\*[\w\W]*?\*\//g,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*{))/gi,inside:{punctuation:/[;:]/g}},url:/url\((["']?).*?\1\)/gi,selector:/[^\{\}\s][^\{\};]*(?=\s*\{)/g,property:/(\b|\B)[\w-]+(?=\s*:)/ig,string:/("|')(\\?.)*?\1/g,important:/\B!important\b/gi,ignore:/&(lt|gt|amp);/gi,punctuation:/[\{\};:]/g};Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{style:{pattern:/(<|<)style[\w\W]*?(>|>)[\w\W]*?(<|<)\/style(>|>)/ig,inside:{tag:{pattern:/(<|<)style[\w\W]*?(>|>)|(<|<)\/style(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css}}});;
|
||||||
|
Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g};
|
||||||
|
;
|
||||||
|
Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});;
|
||||||
|
(function(){if(!window.Prism){return}function $$(a,b){return Array.prototype.slice.call((b||document).querySelectorAll(a))}function hasClass(a,b){b=" "+b+" ";return(" "+a.className+" ").replace(/[\n\t]/g," ").indexOf(b)>-1}var h=crlf=/\r?\n|\r/g;function highlightLines(a,b,c){var d=b.replace(/\s+/g,'').split(','),offset=+a.getAttribute('data-line-offset')||0;var e=parseFloat(getComputedStyle(a).lineHeight);for(var i=0,range;range=d[i++];){range=range.split('-');var f=+range[0],end=+range[1]||f;var g=document.createElement('div');g.textContent=Array(end-f+2).join(' \r\n');g.className=(c||'')+' line-highlight';if(!hasClass(a,'line-numbers')){g.setAttribute('data-start',f);if(end>f){g.setAttribute('data-end',end)}}g.style.top=(f-offset-1)*e+'px';if(hasClass(a,'line-numbers')){a.appendChild(g)}else{(a.querySelector('code')||a).appendChild(g)}}}function applyHash(){var b=location.hash.slice(1);$$('.temporary.line-highlight').forEach(function(a){a.parentNode.removeChild(a)});var c=(b.match(/\.([\d,-]+)$/)||[,''])[1];if(!c||document.getElementById(b)){return}var d=b.slice(0,b.lastIndexOf('.')),pre=document.getElementById(d);if(!pre){return}if(!pre.hasAttribute('data-line')){pre.setAttribute('data-line','')}highlightLines(pre,c,'temporary ');document.querySelector('.temporary.line-highlight').scrollIntoView()}var j=0;Prism.hooks.add('after-highlight',function(b){var c=b.element.parentNode;var d=c&&c.getAttribute('data-line');if(!c||!d||!/pre/i.test(c.nodeName)){return}clearTimeout(j);$$('.line-highlight',c).forEach(function(a){a.parentNode.removeChild(a)});highlightLines(c,d);j=setTimeout(applyHash,1)});addEventListener('hashchange',applyHash)})();
|
||||||
|
;
|
||||||
|
Prism.hooks.add("after-highlight",function(e){var t=e.element.parentNode;if(!t||!/pre/i.test(t.nodeName)||t.className.indexOf("line-numbers")===-1){return}var n=1+e.code.split("\n").length;var r;lines=new Array(n);lines=lines.join("<span></span>");r=document.createElement("span");r.className="line-numbers-rows";r.innerHTML=lines;if(t.hasAttribute("data-start")){t.style.counterReset="linenumber "+(parseInt(t.getAttribute("data-start"),10)-1)}e.element.appendChild(r)})
|
||||||
|
;
|
|
@ -0,0 +1,17 @@
|
||||||
|
<h2>Source Code</h2>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li ng-repeat="type in models.types"
|
||||||
|
ng-click="models.activeTab = type.language"
|
||||||
|
ng-class="{active: models.activeTab === type.language}">
|
||||||
|
<a style="cursor:pointer;">{{type.label}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div ng-repeat="type in models.types"
|
||||||
|
ng-class="{active: models.activeTab === type.language}"
|
||||||
|
class="tab-pane" >
|
||||||
|
<pre class="line-numbers" data-line="{{highlightLines[type.language]}}"><code class="language-{{type.language}}">{{type.source}}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,29 @@
|
||||||
|
angular.module("demo").directive('viewSource', function($http, $timeout) {
|
||||||
|
return {
|
||||||
|
scope: {
|
||||||
|
demoName: "@viewSource",
|
||||||
|
highlightLines: "="
|
||||||
|
},
|
||||||
|
templateUrl: 'framework/view-source.html',
|
||||||
|
link: function (scope, element, attr) {
|
||||||
|
|
||||||
|
scope.models = {
|
||||||
|
types: [
|
||||||
|
{extension: "html", language: "markup", label: "Markup"},
|
||||||
|
{extension: "css", language: "css", label: "CSS"},
|
||||||
|
{extension: "js", language: "javascript", label: "Javascript"},
|
||||||
|
],
|
||||||
|
activeTab: "markup"
|
||||||
|
};
|
||||||
|
|
||||||
|
angular.forEach(scope.models.types, function(type) {
|
||||||
|
$http.get(scope.demoName + '/' + scope.demoName + '.' + type.extension)
|
||||||
|
.success(function (data) {
|
||||||
|
type.source = data;
|
||||||
|
$timeout(Prism.highlightAll, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
|
@ -0,0 +1,65 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Drag & Drop Lists for angular.js</title>
|
||||||
|
|
||||||
|
<!-- jQuery is not required -->
|
||||||
|
<!-- <script src="//code.jquery.com/jquery-1.11.0.min.js"></script> -->
|
||||||
|
|
||||||
|
<!-- angular is the only dependency! -->
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
|
||||||
|
<script src="../angular-drag-and-drop-lists.js"></script>
|
||||||
|
|
||||||
|
<!-- Stuff that is only required in this demo (no need to copy) -->
|
||||||
|
<link rel="stylesheet" href="framework/vendor/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="framework/vendor/bootstrap-theme.min.css">
|
||||||
|
<link rel="stylesheet" href="framework/vendor/prism.css">
|
||||||
|
<link rel="stylesheet" href="framework/demo-framework.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="simple/simple.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="nested/nested.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="types/types.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="advanced/advanced.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="multi/multi.css" />
|
||||||
|
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular-route.min.js"></script>
|
||||||
|
<script src="framework/vendor/prism.js"></script>
|
||||||
|
<script src="framework/demo-framework.js"></script>
|
||||||
|
<script src="framework/view-source.js"></script>
|
||||||
|
<script src="simple/simple.js"></script>
|
||||||
|
<script src="nested/nested.js"></script>
|
||||||
|
<script src="types/types.js"></script>
|
||||||
|
<script src="advanced/advanced.js"></script>
|
||||||
|
<script src="multi/multi.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body ng-app="demo">
|
||||||
|
|
||||||
|
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<a class="navbar-brand" href="#">angular-drag-and-drop-lists</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-collapse collapse">
|
||||||
|
<ul class="nav navbar-nav" navigation></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>Angular drag & drop with HTML5</h1>
|
||||||
|
<p>
|
||||||
|
Directives for modifying lists with the HTML5 drag & drop API.
|
||||||
|
Supports nested lists for building trees and other fancy structures.
|
||||||
|
No boilerplate code, no jQuery, just a few KB!
|
||||||
|
</p>
|
||||||
|
<p><a href="https://github.com/marceljuenemann/angular-drag-and-drop-lists" class="btn btn-success btn-lg" role="button">Code on github »</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-view></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<h1>Demo: Multiselect Lists</h1>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong>Instructions:</strong>
|
||||||
|
Click on items to select/unselect them. When dragging, all selected items will be moved at once.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="multiDemo row">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="row">
|
||||||
|
<div ng-repeat="list in models" class="col-md-6">
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">List {{list.listName}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" ng-include="'multi/multi.html'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div view-source="multi"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Generated Model</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<pre class="code">{{modelAsJson}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* The dnd-list should always have a min-height,
|
||||||
|
* otherwise you can't drop into it once it's empty
|
||||||
|
*/
|
||||||
|
.multiDemo ul[dnd-list] {
|
||||||
|
min-height: 42px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element with .dndPlaceholder class will be
|
||||||
|
* added to the dnd-list while the user is dragging
|
||||||
|
* over it.
|
||||||
|
*/
|
||||||
|
.multiDemo ul[dnd-list] .dndPlaceholder {
|
||||||
|
background-color: #ddd;
|
||||||
|
display: block;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiDemo ul[dnd-list] li {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show selected elements in green
|
||||||
|
*/
|
||||||
|
.multiDemo ul[dnd-list] li.selected {
|
||||||
|
background-color: #dff0d8;
|
||||||
|
color: #3c763d;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
<ul dnd-list dnd-drop="onDrop(list, item, index)">
|
||||||
|
<li ng-repeat="item in list.items"
|
||||||
|
dnd-draggable="getSelectedItemsIncluding(list, item)"
|
||||||
|
dnd-dragstart="onDragstart(list, event)"
|
||||||
|
dnd-moved="onMoved(list)"
|
||||||
|
dnd-dragend="list.dragging = false"
|
||||||
|
dnd-selected="item.selected = !item.selected"
|
||||||
|
ng-class="{'selected': item.selected}"
|
||||||
|
ng-hide="list.dragging && item.selected"
|
||||||
|
>
|
||||||
|
{{item.label}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,66 @@
|
||||||
|
angular.module("demo").controller("MultiDemoController", function($scope) {
|
||||||
|
|
||||||
|
$scope.models = [
|
||||||
|
{listName: "A", items: [], dragging: false},
|
||||||
|
{listName: "B", items: [], dragging: false}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dnd-dragging determines what data gets serialized and send to the receiver
|
||||||
|
* of the drop. While we usually just send a single object, we send the array
|
||||||
|
* of all selected items here.
|
||||||
|
*/
|
||||||
|
$scope.getSelectedItemsIncluding = function(list, item) {
|
||||||
|
item.selected = true;
|
||||||
|
return list.items.filter(function(item) { return item.selected; });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We set the list into dragging state, meaning the items that are being
|
||||||
|
* dragged are hidden. We also use the HTML5 API directly to set a custom
|
||||||
|
* image, since otherwise only the one item that the user actually dragged
|
||||||
|
* would be shown as drag image.
|
||||||
|
*/
|
||||||
|
$scope.onDragstart = function(list, event) {
|
||||||
|
list.dragging = true;
|
||||||
|
if (event.dataTransfer.setDragImage) {
|
||||||
|
var img = new Image();
|
||||||
|
img.src = 'framework/vendor/ic_content_copy_black_24dp_2x.png';
|
||||||
|
event.dataTransfer.setDragImage(img, 0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the dnd-drop callback, we now have to handle the data array that we
|
||||||
|
* sent above. We handle the insertion into the list ourselves. By returning
|
||||||
|
* true, the dnd-list directive won't do the insertion itself.
|
||||||
|
*/
|
||||||
|
$scope.onDrop = function(list, items, index) {
|
||||||
|
angular.forEach(items, function(item) { item.selected = false; });
|
||||||
|
list.items = list.items.slice(0, index)
|
||||||
|
.concat(items)
|
||||||
|
.concat(list.items.slice(index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last but not least, we have to remove the previously dragged items in the
|
||||||
|
* dnd-moved callback.
|
||||||
|
*/
|
||||||
|
$scope.onMoved = function(list) {
|
||||||
|
list.items = list.items.filter(function(item) { return !item.selected; });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the initial model
|
||||||
|
angular.forEach($scope.models, function(list) {
|
||||||
|
for (var i = 1; i <= 4; ++i) {
|
||||||
|
list.items.push({label: "Item " + list.listName + i});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Model to JSON for demo purpose
|
||||||
|
$scope.$watch('models', function(model) {
|
||||||
|
$scope.modelAsJson = angular.toJson(model, true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
<h1>Demo: Nested Containers</h1>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong>Instructions:</strong>
|
||||||
|
In this demo you can not only drag & drop list items, but also containers, which
|
||||||
|
can contain list items or other containers themselves. To create new elements, use the toolbar on the right.
|
||||||
|
If this is more than you need, check out the <a href="#/simple">simple list demo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="nestedDemo row" ng-include="'nested/nested.html'"></div>
|
|
@ -0,0 +1,126 @@
|
||||||
|
/***************************** Dropzone Styling *****************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dnd-list should always have a min-height,
|
||||||
|
* otherwise you can't drop to it once it's empty
|
||||||
|
*/
|
||||||
|
.nestedDemo .dropzone ul[dnd-list] {
|
||||||
|
margin: 0px;
|
||||||
|
min-height: 42px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .dropzone li {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
display: block;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce opacity of elements during the drag operation. This allows the user
|
||||||
|
* to see where he is dropping his element, even if the element is huge. The
|
||||||
|
* .dndDragging class is automatically set during the drag operation.
|
||||||
|
*/
|
||||||
|
.nestedDemo .dropzone .dndDragging {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dndDraggingSource class will be applied to the source element of a drag
|
||||||
|
* operation. It makes sense to hide it to give the user the feeling that he's
|
||||||
|
* actually moving it. Note that the source element has also .dndDragging class.
|
||||||
|
*/
|
||||||
|
.nestedDemo .dropzone .dndDraggingSource {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element with .dndPlaceholder class will be added as child of the dnd-list
|
||||||
|
* while the user is dragging over it.
|
||||||
|
*/
|
||||||
|
.nestedDemo .dropzone .dndPlaceholder {
|
||||||
|
background-color: #ddd;
|
||||||
|
display: block;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** Element Selection *****************************/
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .selected .item {
|
||||||
|
color: #3c763d;
|
||||||
|
background-color: #dff0d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .selected .box {
|
||||||
|
border-color: #d6e9c6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .selected .box > h3 {
|
||||||
|
background-color: #dff0d8;
|
||||||
|
background-image: linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);
|
||||||
|
border-color: #d6e9c6;
|
||||||
|
color: #3c763d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** Element type specific styles *****************************/
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .container-element {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .dropzone .container-element .column {
|
||||||
|
float: left;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** Toolbox *****************************/
|
||||||
|
|
||||||
|
.nestedDemo .toolbox ul {
|
||||||
|
cursor: move;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .toolbox button {
|
||||||
|
margin: 5px;
|
||||||
|
opacity: 1.0;
|
||||||
|
width: 123px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .toolbox .dndDragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .toolbox .dndDraggingSource {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************** Trashcan *****************************/
|
||||||
|
|
||||||
|
.nestedDemo .trashcan ul {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .trashcan img {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-filter: grayscale(100%);
|
||||||
|
-moz-filter: grayscale(100%);
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .trashcan .dndDragover img {
|
||||||
|
width: 100%;
|
||||||
|
-webkit-filter: none;
|
||||||
|
-moz-filter: none;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestedDemo .trashcan .dndPlaceholder {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
<!-- Markup for lists inside the dropzone. It's inside a seperate template
|
||||||
|
because it will be used recursively. The dnd-list directive enables
|
||||||
|
to drop elements into the referenced array. The dnd-draggable directive
|
||||||
|
makes an element draggable and will transfer the object that was
|
||||||
|
assigned to it. If an element was dragged away, you have to remove
|
||||||
|
it from the original list yourself using the dnd-moved attribute -->
|
||||||
|
<script type="text/ng-template" id="list.html">
|
||||||
|
<ul dnd-list="list">
|
||||||
|
<li ng-repeat="item in list"
|
||||||
|
dnd-draggable="item"
|
||||||
|
dnd-effect-allowed="move"
|
||||||
|
dnd-moved="list.splice($index, 1)"
|
||||||
|
dnd-selected="models.selected = item"
|
||||||
|
ng-class="{selected: models.selected === item}"
|
||||||
|
ng-include="item.type + '.html'">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- This template is responsible for rendering a container element. It uses
|
||||||
|
the above list template to render each container column -->
|
||||||
|
<script type="text/ng-template" id="container.html">
|
||||||
|
<div class="container-element box box-blue">
|
||||||
|
<h3>Container {{item.id}}</h3>
|
||||||
|
<div class="column" ng-repeat="list in item.columns" ng-include="'list.html'"></div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Template for a normal list item -->
|
||||||
|
<script type="text/ng-template" id="item.html">
|
||||||
|
<div class="item">Item {{item.id}}</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Main area with dropzones and source code -->
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="row">
|
||||||
|
<div ng-repeat="(zone, list) in models.dropzones" class="col-md-6">
|
||||||
|
<div class="dropzone box box-yellow">
|
||||||
|
<!-- The dropzone also uses the list template -->
|
||||||
|
<h3>Dropzone {{zone}}</h3>
|
||||||
|
<div ng-include="'list.html'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div view-source="nested"></div>
|
||||||
|
|
||||||
|
<h2>Generated Model</h2>
|
||||||
|
<pre>{{modelAsJson}}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="col-md-2">
|
||||||
|
|
||||||
|
<div class="toolbox box box-grey box-padding">
|
||||||
|
<h3>New Elements</h3>
|
||||||
|
<ul>
|
||||||
|
<!-- The toolbox only allows to copy objects, not move it. After a new
|
||||||
|
element was created, dnd-copied is invoked and we generate the next id -->
|
||||||
|
<li ng-repeat="item in models.templates"
|
||||||
|
dnd-draggable="item"
|
||||||
|
dnd-effect-allowed="copy"
|
||||||
|
dnd-copied="item.id = item.id + 1"
|
||||||
|
>
|
||||||
|
<button type="button" class="btn btn-default btn-lg" disabled="disabled">{{item.type}}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="models.selected" class="box box-grey box-padding">
|
||||||
|
<h3>Selected</h3>
|
||||||
|
<strong>Type: </strong> {{models.selected.type}}<br>
|
||||||
|
<input type="text" ng-model="models.selected.id" class="form-control" style="margin-top: 5px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="trashcan box box-grey box-padding">
|
||||||
|
<!-- If you use [] as referenced list, the dropped elements will be lost -->
|
||||||
|
<h3>Trashcan</h3>
|
||||||
|
<ul dnd-list="[]">
|
||||||
|
<li><img src="nested/trashcan.jpg"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* The controller doesn't do much more than setting the initial data model
|
||||||
|
*/
|
||||||
|
angular.module("demo").controller("NestedListsDemoController", function($scope) {
|
||||||
|
|
||||||
|
$scope.models = {
|
||||||
|
selected: null,
|
||||||
|
templates: [
|
||||||
|
{type: "item", id: 2},
|
||||||
|
{type: "container", id: 1, columns: [[], []]}
|
||||||
|
],
|
||||||
|
dropzones: {
|
||||||
|
"A": [
|
||||||
|
{
|
||||||
|
"type": "container",
|
||||||
|
"id": 1,
|
||||||
|
"columns": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "6"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"B": [
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "container",
|
||||||
|
"id": "2",
|
||||||
|
"columns": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "11"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "container",
|
||||||
|
"id": "3",
|
||||||
|
"columns": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "13"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "14"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": "16"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "item",
|
||||||
|
"id": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('models.dropzones', function(model) {
|
||||||
|
$scope.modelAsJson = angular.toJson(model, true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
});
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 28 KiB |
|
@ -0,0 +1,38 @@
|
||||||
|
<h1>Demo: Simple Lists</h1>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong>Instructions:</strong>
|
||||||
|
Drag & drop the list items to move them around, or just click to select them.
|
||||||
|
If that's too boring, check out the <a href="#/nested">nested container demo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="simpleDemo row">
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="row">
|
||||||
|
<div ng-repeat="(listName, list) in models.lists" class="col-md-6">
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">List {{listName}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" ng-include="'simple/simple.html'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div view-source="simple"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Generated Model</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<pre class="code">{{modelAsJson}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* The dnd-list should always have a min-height,
|
||||||
|
* otherwise you can't drop to it once it's empty
|
||||||
|
*/
|
||||||
|
.simpleDemo ul[dnd-list] {
|
||||||
|
min-height: 42px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dndDraggingSource class will be applied to
|
||||||
|
* the source element of a drag operation. It makes
|
||||||
|
* sense to hide it to give the user the feeling
|
||||||
|
* that he's actually moving it.
|
||||||
|
*/
|
||||||
|
.simpleDemo ul[dnd-list] .dndDraggingSource {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element with .dndPlaceholder class will be
|
||||||
|
* added to the dnd-list while the user is dragging
|
||||||
|
* over it.
|
||||||
|
*/
|
||||||
|
.simpleDemo ul[dnd-list] .dndPlaceholder {
|
||||||
|
background-color: #ddd;
|
||||||
|
display: block;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simpleDemo ul[dnd-list] li {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show selected elements in green
|
||||||
|
*/
|
||||||
|
.simpleDemo ul[dnd-list] li.selected {
|
||||||
|
background-color: #dff0d8;
|
||||||
|
color: #3c763d;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!-- The dnd-list directive allows to drop elements into it.
|
||||||
|
The dropped data will be added to the referenced list -->
|
||||||
|
<ul dnd-list="list">
|
||||||
|
<!-- The dnd-draggable directive makes an element draggable and will
|
||||||
|
transfer the object that was assigned to it. If an element was
|
||||||
|
dragged away, you have to remove it from the original list
|
||||||
|
yourself using the dnd-moved attribute -->
|
||||||
|
<li ng-repeat="item in list"
|
||||||
|
dnd-draggable="item"
|
||||||
|
dnd-moved="list.splice($index, 1)"
|
||||||
|
dnd-effect-allowed="move"
|
||||||
|
dnd-selected="models.selected = item"
|
||||||
|
ng-class="{'selected': models.selected === item}"
|
||||||
|
>
|
||||||
|
{{item.label}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,19 @@
|
||||||
|
angular.module("demo").controller("SimpleDemoController", function($scope) {
|
||||||
|
|
||||||
|
$scope.models = {
|
||||||
|
selected: null,
|
||||||
|
lists: {"A": [], "B": []}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate initial model
|
||||||
|
for (var i = 1; i <= 3; ++i) {
|
||||||
|
$scope.models.lists.A.push({label: "Item A" + i});
|
||||||
|
$scope.models.lists.B.push({label: "Item B" + i});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model to JSON for demo purpose
|
||||||
|
$scope.$watch('models', function(model) {
|
||||||
|
$scope.modelAsJson = angular.toJson(model, true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
<h1>Demo: Item Types</h1>
|
||||||
|
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong>Instructions:</strong>
|
||||||
|
Drag & drop the names to move them around. Note that the names can not be
|
||||||
|
dropped in the list for the wrong gender. This is achieved with the dnd-type and
|
||||||
|
dnd-allowed-types attributes.
|
||||||
|
This demo also shows the use of the dnd-disable-if attribute, which is used here
|
||||||
|
to limit the number of names per list, as well as fix Alex' position.
|
||||||
|
You can combine these functions with <a href="#/nested">nested lists</a>
|
||||||
|
to build very powerful UIs.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="typesDemo row">
|
||||||
|
|
||||||
|
<div ng-repeat="list in lists" class="col-md-4">
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">List of {{list.label}} (max. {{list.max}})</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" ng-include="'types/types.html'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8" view-source="types"></div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">List Models</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<pre class="code">{{modelAsJson}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,102 @@
|
||||||
|
/**
|
||||||
|
* For the correct positioning of the placeholder element, the dnd-list and
|
||||||
|
* it's children must have position: relative
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list],
|
||||||
|
.typesDemo ul[dnd-list] > li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dnd-list should always have a min-height,
|
||||||
|
* otherwise you can't drop to it once it's empty
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list] {
|
||||||
|
min-height: 42px;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dndDraggingSource class will be applied to
|
||||||
|
* the source element of a drag operation. It makes
|
||||||
|
* sense to hide it to give the user the feeling
|
||||||
|
* that he's actually moving it.
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list] .dndDraggingSource {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An element with .dndPlaceholder class will be
|
||||||
|
* added to the dnd-list while the user is dragging
|
||||||
|
* over it.
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list] .dndPlaceholder {
|
||||||
|
display: block;
|
||||||
|
background-color: #ddd;
|
||||||
|
padding: 10px 15px;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dnd-lists's child elements currently MUST have
|
||||||
|
* position: relative. Otherwise we can not determine
|
||||||
|
* whether the mouse pointer is in the upper or lower
|
||||||
|
* half of the element we are dragging over. In other
|
||||||
|
* browsers we can use event.offsetY for this.
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list] li {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
|
||||||
|
/* Disable text selection if item is not draggable */
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-khtml-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typesDemo ul[dnd-list] li dnd-nodrag {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gender-specifc background
|
||||||
|
*/
|
||||||
|
.typesDemo ul[dnd-list] li.background-man {
|
||||||
|
background-color: #CAE0FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typesDemo ul[dnd-list] li.background-woman {
|
||||||
|
background-color: #FFE2F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typesDemo ul[dnd-list] input.background-man {
|
||||||
|
background-color: #D8E9FF;
|
||||||
|
color: #2F4D99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typesDemo ul[dnd-list] input.background-woman {
|
||||||
|
background-color: #FFF0FA;
|
||||||
|
color: #D84FA7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle positioning
|
||||||
|
*/
|
||||||
|
.typesDemo .handle {
|
||||||
|
cursor: move;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typesDemo .name {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<ul dnd-list="list.people"
|
||||||
|
dnd-allowed-types="list.allowedTypes"
|
||||||
|
dnd-disable-if="list.people.length >= list.max">
|
||||||
|
|
||||||
|
<li ng-repeat="person in list.people"
|
||||||
|
dnd-draggable="person"
|
||||||
|
dnd-type="person.type"
|
||||||
|
dnd-disable-if="person.type == 'unknown'"
|
||||||
|
dnd-moved="list.people.splice($index, 1)"
|
||||||
|
class="background-{{person.type}}"
|
||||||
|
>
|
||||||
|
<dnd-nodrag>
|
||||||
|
<div dnd-handle class="handle">:::</div>
|
||||||
|
<div class="name">
|
||||||
|
<input type="text" ng-model="person.name" class="background-{{person.type}} form-control input-sm">
|
||||||
|
</div>
|
||||||
|
</dnd-nodrag>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="dndPlaceholder">
|
||||||
|
Drop any <strong>{{list.allowedTypes.join(' or ')}}</strong> here
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
|
@ -0,0 +1,43 @@
|
||||||
|
angular.module("demo").controller("TypesDemoController", function($scope) {
|
||||||
|
|
||||||
|
$scope.lists = [
|
||||||
|
{
|
||||||
|
label: "Men",
|
||||||
|
allowedTypes: ['man'],
|
||||||
|
max: 4,
|
||||||
|
people: [
|
||||||
|
{name: "Bob", type: "man"},
|
||||||
|
{name: "Charlie", type: "man"},
|
||||||
|
{name: "Dave", type: "man"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Women",
|
||||||
|
allowedTypes: ['woman'],
|
||||||
|
max: 4,
|
||||||
|
people: [
|
||||||
|
{name: "Alice", type: "woman"},
|
||||||
|
{name: "Eve", type: "woman"},
|
||||||
|
{name: "Peggy", type: "woman"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "People",
|
||||||
|
allowedTypes: ['man', 'woman'],
|
||||||
|
max: 6,
|
||||||
|
people: [
|
||||||
|
{name: "Frank", type: "man"},
|
||||||
|
{name: "Mallory", type: "woman"},
|
||||||
|
{name: "Alex", type: "unknown"},
|
||||||
|
{name: "Oscar", type: "man"},
|
||||||
|
{name: "Wendy", type: "woman"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Model to JSON for demo purpose
|
||||||
|
$scope.$watch('lists', function(lists) {
|
||||||
|
$scope.modelAsJson = angular.toJson(lists, true);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "angular-drag-and-drop-lists",
|
||||||
|
"main": "angular-drag-and-drop-lists.js",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"description": "Angular directives for sorting nested lists using the HTML5 Drag and Drop API",
|
||||||
|
"repository": "https://github.com/marceljuenemann/angular-drag-and-drop-lists",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "angular-drag-and-drop-lists.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"angular": "~1.4.9",
|
||||||
|
"angular-mocks": "~1.4.9",
|
||||||
|
"http-server": "~0.6.1",
|
||||||
|
"minifier": "~0.7.1",
|
||||||
|
"jasmine-core": "~2.4.1",
|
||||||
|
"jquery": "~2.2.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"minify": "minify angular-drag-and-drop-lists.js",
|
||||||
|
"start": "http-server -p 8000"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
describe('dndDraggable', function() {
|
||||||
|
|
||||||
|
var SIMPLE_HTML = '<div dnd-draggable="{hello: \'world\'}"></div>';
|
||||||
|
|
||||||
|
describe('constructor', function() {
|
||||||
|
it('sets the draggable attribute', function() {
|
||||||
|
var element = compileAndLink(SIMPLE_HTML);
|
||||||
|
expect(element.attr('draggable')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('watches and handles the dnd-disabled-if expression', function() {
|
||||||
|
var element = compileAndLink('<div dnd-draggable dnd-disable-if="disabled"></div>');
|
||||||
|
expect(element.attr('draggable')).toBe('true');
|
||||||
|
|
||||||
|
element.scope().disabled = true;
|
||||||
|
element.scope().$digest();
|
||||||
|
expect(element.attr('draggable')).toBe('false');
|
||||||
|
|
||||||
|
element.scope().disabled = false;
|
||||||
|
element.scope().$digest();
|
||||||
|
expect(element.attr('draggable')).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dragstart handler', function() {
|
||||||
|
var element;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
element = compileAndLink(SIMPLE_HTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setData with serialized data', function() {
|
||||||
|
expect(Dragstart.on(element).data).toEqual({'application/x-dnd': '{"hello":"world"}'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the dnd-type in the mime type', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable="{}" dnd-type="\'foo\'"></div>');
|
||||||
|
expect(Dragstart.on(element).data).toEqual({'application/x-dnd-foo': '{}'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts the dnd-type to lower case', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable="{}" dnd-type="\'Foo\'"></div>');
|
||||||
|
expect(Dragstart.on(element).data).toEqual({'application/x-dnd-foo': '{}'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses application/json mime type if custom types are not allowed', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable="[1]"></div>');
|
||||||
|
var dragstart = Dragstart.on(element, {allowedMimeTypes: ['Text', 'application/json']});
|
||||||
|
expect(dragstart.data).toEqual({'application/json': '{"item":[1]}'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses Text mime type in Internet Explorer', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable="{}" dnd-type="\'Foo\'"></div>');
|
||||||
|
var dragstart = Dragstart.on(element, {allowedMimeTypes: ['URL', 'Text']});
|
||||||
|
expect(dragstart.data).toEqual({
|
||||||
|
'Text': '{"item":{},"type":"foo"}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops propagation', function() {
|
||||||
|
expect(Dragstart.on(element).propagationStopped).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets effectAllowed to move by default', function() {
|
||||||
|
expect(Dragstart.on(element).effectAllowed).toBe('move');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets effectAllowed from dnd-effect-allowed', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable dnd-effect-allowed="copyMove"></div>');
|
||||||
|
expect(Dragstart.on(element).effectAllowed).toBe('copyMove');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets effectAllowed to single effect in IE', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable dnd-effect-allowed="copyLink"></div>');
|
||||||
|
expect(Dragstart.on(element, {allowedMimeTypes: ['Text']}).effectAllowed).toBe('copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds CSS classes to element', inject(function($timeout) {
|
||||||
|
Dragstart.on(element);
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(true);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(false);
|
||||||
|
|
||||||
|
$timeout.flush(0);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('invokes dnd-dragstart callback', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable dnd-dragstart="ev = event"></div>');
|
||||||
|
Dragstart.on(element);
|
||||||
|
expect(element.scope().ev).toEqual(jasmine.any(DragEventMock));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not start dragging if dnd-disable-if is true', function() {
|
||||||
|
element = compileAndLink('<div dnd-draggable dnd-disable-if="true"></div>');
|
||||||
|
var dragstart = Dragstart.on(element);
|
||||||
|
expect(dragstart.returnValue).toBe(true);
|
||||||
|
expect(dragstart.defaultPrevented).toBe(false);
|
||||||
|
expect(dragstart.propagationStopped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the dragImage if event was triggered on a dnd-handle', function() {
|
||||||
|
var dragstart = Dragstart.on(element, {allowSetDragImage: true, dndHandle: true});
|
||||||
|
expect(dragstart.dragImage).toBe(element[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dragend handler', function() {
|
||||||
|
var element, dragstart;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
element = compileAndLink(SIMPLE_HTML);
|
||||||
|
dragstart = Dragstart.on(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops propagation', function() {
|
||||||
|
expect(dragstart.dragend(element).propagationStopped).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes CSS classes from element', inject(function($timeout) {
|
||||||
|
$timeout.flush(0);
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(true);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(true);
|
||||||
|
|
||||||
|
dragstart.dragend(element);
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(false);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('removes dndDraggingSource after a timeout', inject(function($timeout) {
|
||||||
|
// IE 9 might not flush the $timeout before invoking the dragend handler.
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(true);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(false);
|
||||||
|
|
||||||
|
dragstart.dragend(element);
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(false);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(false);
|
||||||
|
|
||||||
|
$timeout.flush(0);
|
||||||
|
expect(element.hasClass('dndDragging')).toBe(false);
|
||||||
|
expect(element.hasClass('dndDraggingSource')).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
var dropEffects = {move: 'moved', copy: 'copied', link: 'linked', none: 'canceled'};
|
||||||
|
angular.forEach(dropEffects, function(callback, dropEffect) {
|
||||||
|
it('calls callbacks for dropEffect ' + dropEffect, function() {
|
||||||
|
var html = '<div dnd-draggable="{}" dnd-effect-allowed="' + dropEffect + '" '
|
||||||
|
+ 'dnd-dragend="returnedDropEffect = dropEffect" '
|
||||||
|
+ 'dnd-' + callback + '="returnedEvent = event"></div>';
|
||||||
|
var element = compileAndLink(html);
|
||||||
|
var target = compileAndLink('<div dnd-list="[]"></div>');
|
||||||
|
Dragstart.on(element).dragover(target).drop(target).dragend(element);
|
||||||
|
|
||||||
|
expect(element.scope().returnedEvent).toEqual(jasmine.any(DragEventMock));
|
||||||
|
expect(element.scope().returnedDropEffect).toBe(dropEffect);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('click handler', function() {
|
||||||
|
it('does nothing if dnd-selected is not set', function() {
|
||||||
|
var element = compileAndLink(SIMPLE_HTML);
|
||||||
|
var click = new DragEventResult(element, 'click', new DataTransferMock(), {});
|
||||||
|
expect(click.propagationStopped).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-selected callback and stops propagation', function() {
|
||||||
|
var element = compileAndLink('<div dnd-draggable dnd-selected="selected = true"></div>');
|
||||||
|
var click = new DragEventResult(element, 'click', new DataTransferMock(), {});
|
||||||
|
expect(click.propagationStopped).toBe(true);
|
||||||
|
expect(element.scope().selected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,576 @@
|
||||||
|
describe('dndList', function() {
|
||||||
|
|
||||||
|
it('hides the placeholder element', function() {
|
||||||
|
var element = compileAndLink('<dnd-list><img class="dndPlaceholder"></dnd-list>');
|
||||||
|
expect(element.children().length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows dropping if dnd-disable-if is true', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-disable-if="disabled"></div>');
|
||||||
|
element.scope().disabled = true;
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows drop if dnd-disable-if is false', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-disable-if="disabled"></div>');
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows dropping from external sources', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]"></div>');
|
||||||
|
var dragenter = Dragenter.validExternalOn(element);
|
||||||
|
forAllHandlers(dragenter, element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dropping from external sources if dnd-external-sources is set', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-external-sources="true"></div>');
|
||||||
|
var dragenter = Dragenter.validExternalOn(element);
|
||||||
|
forAllHandlers(dragenter, element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows drop without valid mime types', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-external-sources="true"></div>');
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'text/plain': '{}'});
|
||||||
|
forAllHandlers(dragenter, element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Old Internet Explorer versions don't have dataTransfer.types.
|
||||||
|
it('allows drop if dataTransfer.types is undefined', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-external-sources="true"></div>');
|
||||||
|
var data = angular.toJson({item: {}, type: 'mytype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'Text': data}, {undefinedTypes: true});
|
||||||
|
forAllHandlers(dragenter, element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows drop if dataTransfer.types contains "Text"', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-external-sources="true"></div>');
|
||||||
|
var data = angular.toJson({item: {}, type: 'mytype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'Text': data});
|
||||||
|
forAllHandlers(dragenter, element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows drop if dataTransfer.types contains "application/json"', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-external-sources="true"></div>');
|
||||||
|
var data = angular.toJson({item: {}, type: 'mytype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'x-pdf': '{}', 'application/json': data});
|
||||||
|
forAllHandlers(dragenter, element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows dropping untyped elements if dnd-allowed-types is set', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'mytype\']"></div>');
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dropping typed elements if dnd-allowed-types is not set', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'sometype\'"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]"></div>');
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows dropping elements of the wrong type', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'othertype\'"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'mytype\']"></div>');
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dropping elements of the correct type', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'MyType\']"></div>');
|
||||||
|
forAllHandlers(Dragstart.on(source).dragenter(element), element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows dropping elements of the wrong type (test for Edge)', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'othertype\'"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'mytype\']"></div>');
|
||||||
|
var dragstart = Dragstart.on(source, {allowedMimeTypes: ['text/plain', 'application/json']});
|
||||||
|
forAllHandlers(dragstart.dragenter(element), element, verifyDropCancelled);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dropping elements of the correct type (test for Edge)', function() {
|
||||||
|
var source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'mytype\']"></div>');
|
||||||
|
var dragstart = Dragstart.on(source, {allowedMimeTypes: ['text/plain', 'application/json']});
|
||||||
|
forAllHandlers(dragstart.dragenter(element), element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows dropping external elements if correct type is encoded inside', function() {
|
||||||
|
var element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'myType\']" ' +
|
||||||
|
'dnd-external-sources="true"></div>');
|
||||||
|
var data = angular.toJson({item: {}, type: 'mytype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'application/json': data});
|
||||||
|
forAllHandlers(dragenter, element, verifyDropAccepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dragover handler', function() {
|
||||||
|
var source, element;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}"></div>');
|
||||||
|
element = compileAndLink('<div dnd-list="list"></div>');
|
||||||
|
element.scope().list = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds dndDragover CSS class', function() {
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.hasClass('dndDragover')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds placeholder element', function() {
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.children().length).toBe(1);
|
||||||
|
expect(element.children()[0].tagName).toBe('LI');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reuses custom placeholder element if it exists', function() {
|
||||||
|
element = compileAndLink('<dnd-list><img class="dndPlaceholder"></dnd-list>');
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.children().length).toBe(1);
|
||||||
|
expect(element.children()[0].tagName).toBe('IMG');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover callback', function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.scope().dragover.event).toEqual(jasmine.any(DragEventMock));
|
||||||
|
expect(element.scope().dragover.index).toBe(3);
|
||||||
|
expect(element.scope().dragover.external).toBe(false);
|
||||||
|
expect(element.scope().dragover.type).toBeUndefined();
|
||||||
|
expect(element.scope().dragover.item).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with correct type', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.scope().dragover.type).toBe('mytype');
|
||||||
|
expect(element.scope().dragover.external).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with correct type (test for IE)', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(source, {allowedMimeTypes: ['Text']}).dragover(element);
|
||||||
|
expect(element.scope().dragover.type).toBe('mytype');
|
||||||
|
expect(element.scope().dragover.external).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with correct type for external drops', function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragenter.externalOn(element, {'application/x-dnd-mytype': {}}).dragover(element);
|
||||||
|
expect(element.scope().dragover.type).toBe('mytype');
|
||||||
|
expect(element.scope().dragover.external).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with null type for external drops from IE', function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragenter.externalOn(element, {'Text': 'unaccessible'}).dragover(element);
|
||||||
|
expect(element.scope().dragover.type).toBeNull();
|
||||||
|
expect(element.scope().dragover.external).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with undefined callback', function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.scope().dragover.callback).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-dragover with callback set on dragstart', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-callback="a*b"></div>');
|
||||||
|
source.scope().a = 2;
|
||||||
|
element = compileAndLink('<ul dnd-list="[]" dnd-dragover="result = callback({b: 3});"></ul>');
|
||||||
|
Dragstart.on(source).dragover(element);
|
||||||
|
expect(element.scope().result).toBe(6)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dnd-dragover callback can cancel the drop', function() {
|
||||||
|
element = compileAndLink('<div dnd-list="list" dnd-dragover="false"></div>');
|
||||||
|
verifyDropCancelled(Dragstart.on(source).dragover(element), element);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows all external drops with Text mime type', function() {
|
||||||
|
element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'myType\']" ' +
|
||||||
|
'dnd-external-sources="true"></div>');
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'Text': 'unaccessible'});
|
||||||
|
verifyDropAccepted(dragenter.dragover(element), element);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placeholder positioning (vertical)', positioningTests(false, false));
|
||||||
|
describe('placeholder positioning (horizontal)', positioningTests(true, false));
|
||||||
|
|
||||||
|
function positioningTests(horizontal) {
|
||||||
|
return function() {
|
||||||
|
var clientYField = 'client' + (horizontal ? 'X' : 'Y');
|
||||||
|
var heightField = horizontal ? 'width' : 'height';
|
||||||
|
var topField = horizontal ? 'left' : 'top';
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
element = createListWithItemsAndCallbacks(horizontal);
|
||||||
|
angular.element(document.body).append(element);
|
||||||
|
if (horizontal) {
|
||||||
|
element.children().css('float','left');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds actual placeholder element', function() {
|
||||||
|
var options = {target: element.children()[0]};
|
||||||
|
options[clientYField] = 1;
|
||||||
|
Dragstart.on(source).dragover(element, options);
|
||||||
|
expect(element.scope().dragover.index).toBe(0);
|
||||||
|
expect(angular.element(element.children()[0]).hasClass('dndPlaceholder')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts before element if mouse is in first half', function() {
|
||||||
|
var options = {target: element.children()[1]};
|
||||||
|
var rect = options.target.getBoundingClientRect();
|
||||||
|
options[clientYField] = rect[topField] + rect[heightField] / 2 - 1;
|
||||||
|
Dragstart.on(source).dragover(element, options);
|
||||||
|
expect(element.scope().dragover.index).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts after element if mouse is in second half', function() {
|
||||||
|
var options = {target: element.children()[1]};
|
||||||
|
var rect = options.target.getBoundingClientRect();
|
||||||
|
options[clientYField] = rect[topField] + rect[heightField] / 2 + 1;
|
||||||
|
Dragstart.on(source).dragover(element, options);
|
||||||
|
expect(element.scope().dragover.index).toBe(2);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('drop handler', function() {
|
||||||
|
var source, element;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{example: \'data\'}"></div>');
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts into the list and removes dndDragover class', function() {
|
||||||
|
var dragover = Dragstart.on(source).dragover(element, {target: element.children()[0]});
|
||||||
|
expect(element.hasClass("dndDragover")).toBe(true);
|
||||||
|
dragover.drop(element);
|
||||||
|
expect(element.scope().list).toEqual([1, {example: 'data'}, 2, 3]);
|
||||||
|
expect(element.hasClass("dndDragover")).toBe(false);
|
||||||
|
expect(element.children().length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts in correct position', function() {
|
||||||
|
Dragstart.on(source).dragover(element, {target: element.children()[1]}).drop(element);
|
||||||
|
expect(element.scope().list).toEqual([1, 2, {example: 'data'}, 3]);
|
||||||
|
expect(element.scope().inserted.index).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes the dnd-inserted callback', function() {
|
||||||
|
Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().inserted.event).toEqual(jasmine.any(DragEventMock));
|
||||||
|
expect(element.scope().inserted.index).toBe(3);
|
||||||
|
expect(element.scope().inserted.external).toBe(false);
|
||||||
|
expect(element.scope().inserted.type).toBeUndefined();
|
||||||
|
expect(element.scope().inserted.item).toBe(element.scope().list[3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dnd-drop can transform the object', function() {
|
||||||
|
var testObject = {transformed: true};
|
||||||
|
element.scope().dropHandler = function(params) {
|
||||||
|
expect(params.event).toEqual(jasmine.any(DragEventMock));
|
||||||
|
expect(params.index).toBe(3);
|
||||||
|
expect(params.external).toBe(false);
|
||||||
|
expect(params.type).toBeUndefined();
|
||||||
|
expect(params.item).toEqual({example: 'data'});
|
||||||
|
return testObject;
|
||||||
|
};
|
||||||
|
Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().list[3]).toBe(testObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dnd-drop can cancel the drop', function() {
|
||||||
|
element.scope().dropHandler = function() { return false; };
|
||||||
|
var drop = Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().list).toEqual([1, 2, 3]);
|
||||||
|
expect(element.scope().inserted).toBeUndefined();
|
||||||
|
verifyDropCancelled(drop, element, true, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dnd-drop can take care of inserting the element', function() {
|
||||||
|
element.scope().dropHandler = function() { return true; };
|
||||||
|
verifyDropAccepted(Dragstart.on(source).dragover(element).drop(element), element);
|
||||||
|
expect(element.scope().list).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-drop with undefined callback', function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().drop.callback).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes dnd-drop with callback set on dragstart', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-callback="a*b"></div>');
|
||||||
|
source.scope().a = 2;
|
||||||
|
element = compileAndLink('<ul dnd-list="list" dnd-drop="callback({b: 3});"></ul>');
|
||||||
|
element.scope().list = [];
|
||||||
|
Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().list).toEqual([6])
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callbacks with correct type', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
Dragstart.on(source).dragover(element).drop(element);
|
||||||
|
expect(element.scope().drop.type).toBe('mytype');
|
||||||
|
expect(element.scope().drop.external).toBe(false);
|
||||||
|
expect(element.scope().inserted.type).toBe('mytype');
|
||||||
|
expect(element.scope().inserted.external).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callbacks with correct type for Edge', function() {
|
||||||
|
source = compileAndLink('<div dnd-draggable="{}" dnd-type="\'mytype\'"></div>');
|
||||||
|
Dragstart.on(source, {allowedMimeTypes: ['application/json']}).dragover(element).drop(element);
|
||||||
|
expect(element.scope().drop.type).toBe('mytype');
|
||||||
|
expect(element.scope().drop.external).toBe(false);
|
||||||
|
expect(element.scope().inserted.type).toBe('mytype');
|
||||||
|
expect(element.scope().inserted.external).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callbacks with correct type for external elements', function() {
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'application/x-dnd-mytype': '{}'});
|
||||||
|
verifyDropAccepted(dragenter.dragover(element).drop(element), element);
|
||||||
|
expect(element.scope().drop.type).toBe('mytype');
|
||||||
|
expect(element.scope().drop.external).toBe(true);
|
||||||
|
expect(element.scope().inserted.type).toBe('mytype');
|
||||||
|
expect(element.scope().inserted.external).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invokes callbacks with correct type for external elements (test for Edge)', function() {
|
||||||
|
var data = angular.toJson({item: [1, 2, 3], type: 'mytype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'application/json': data});
|
||||||
|
verifyDropAccepted(dragenter.dragover(element).drop(element), element);
|
||||||
|
expect(element.scope().drop.type).toBe('mytype');
|
||||||
|
expect(element.scope().drop.external).toBe(true);
|
||||||
|
expect(element.scope().inserted.type).toBe('mytype');
|
||||||
|
expect(element.scope().inserted.external).toBe(true);
|
||||||
|
expect(element.scope().inserted.item).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows drops with wrong type encoded inside (test for Edge)', function() {
|
||||||
|
element = compileAndLink('<div dnd-list="[]" dnd-allowed-types="[\'myType\']" ' +
|
||||||
|
'dnd-external-sources="true"></div>');
|
||||||
|
var data = angular.toJson({item: [], type: 'othertype'});
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'application/json': data});
|
||||||
|
verifyDropCancelled(dragenter.dragover(element).drop(element), element, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels drop when JSON is invalid', function() {
|
||||||
|
var dragenter = Dragenter.externalOn(element, {'application/x-dnd': 'Lorem ipsum'});
|
||||||
|
verifyDropCancelled(dragenter.dragover(element).drop(element), element, true, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dragleave handler', function() {
|
||||||
|
var element, dragover;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
element = createListWithItemsAndCallbacks();
|
||||||
|
angular.element(document.body).append(element);
|
||||||
|
|
||||||
|
dragover = Dragstart.on(compileAndLink('<div dnd-draggable="{}"></div>')).dragover(element);
|
||||||
|
expect(element.hasClass('dndDragover')).toBe(true);
|
||||||
|
expect(element.children().length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the placeholder and dndDragover class', function() {
|
||||||
|
var rect = element[0].getBoundingClientRect();
|
||||||
|
dragover.dragleave(element, {clientX: rect.left - 2, clientY: rect.top - 2});
|
||||||
|
expect(element.hasClass('dndDragover')).toBe(false);
|
||||||
|
expect(element.children().length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the placeholder and dndDragover if child placeholder is already set', function() {
|
||||||
|
var rect = element[0].getBoundingClientRect();
|
||||||
|
dragover.dragleave(element, {clientX: rect.left + 2, clientY: rect.top + 2, phShown: true});
|
||||||
|
expect(element.hasClass('dndDragover')).toBe(false);
|
||||||
|
expect(element.children().length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets _dndPhShown if mouse is still inside', function() {
|
||||||
|
var rect = element[0].getBoundingClientRect();
|
||||||
|
var result = dragover.dragleave(element, {clientX: rect.left + 2, clientY: rect.top + 2});
|
||||||
|
expect(element.hasClass('dndDragover')).toBe(true);
|
||||||
|
expect(element.children().length).toBe(4);
|
||||||
|
expect(result.dndPhShownSet).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dropEffect', function() {
|
||||||
|
// This matrix shows the expected drop effect, given two effectAllowed values.
|
||||||
|
var ALL = [ 'all', 'move', 'copy', 'link', 'copyLink', 'copyMove', 'linkMove'];
|
||||||
|
var EXPECTED_MATRIX = {
|
||||||
|
move: ['move', 'move', 'none', 'none', 'none', 'move', 'move'],
|
||||||
|
copy: ['copy', 'none', 'copy', 'none', 'copy', 'copy', 'none'],
|
||||||
|
link: ['link', 'none', 'none', 'link', 'link', 'none', 'link'],
|
||||||
|
copyLink: ['copy', 'none', 'copy', 'link', 'copy', 'copy', 'link'],
|
||||||
|
copyMove: ['move', 'move', 'copy', 'none', 'copy', 'move', 'move'],
|
||||||
|
linkMove: ['move', 'move', 'none', 'link', 'link', 'move', 'move'],
|
||||||
|
all: ['move', 'move', 'copy', 'link', 'copy', 'move', 'move'],
|
||||||
|
'': ['move', 'move', 'copy', 'link', 'copy', 'move', 'move'],
|
||||||
|
};
|
||||||
|
angular.forEach(ALL, function(sourceEffectAllowed, index) {
|
||||||
|
angular.forEach(EXPECTED_MATRIX, function(expected, targetEffectAllowed) {
|
||||||
|
expected = expected[index];
|
||||||
|
it('is ' + expected + ' for effect-allowed ' + sourceEffectAllowed
|
||||||
|
+ ' and ' + targetEffectAllowed, function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-dragend="result = dropEffect" '
|
||||||
|
+ 'dnd-effect-allowed="' + sourceEffectAllowed + '"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks(false, targetEffectAllowed);
|
||||||
|
expect(Dragstart.on(src).effectAllowed).toBe(sourceEffectAllowed);
|
||||||
|
if (expected != 'none') {
|
||||||
|
// Verify dragover.
|
||||||
|
expect(Dragstart.on(src).dragover(target).dropEffect).toBe(expected);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe(expected);
|
||||||
|
// Verify drop.
|
||||||
|
expect(Dragstart.on(src).dragover(target).drop(target).dropEffect).toBe(expected);
|
||||||
|
expect(target.scope().drop.dropEffect).toBe(expected);
|
||||||
|
// Verify dragend.
|
||||||
|
Dragstart.on(src).dragover(target).drop(target).dragend(src);
|
||||||
|
expect(src.scope().result).toBe(expected);
|
||||||
|
} else {
|
||||||
|
verifyDropCancelled(Dragstart.on(src).dragover(target), target, false, 3);
|
||||||
|
verifyDropCancelled(Dragstart.on(src).dragover(target).drop(target), target, true, 3);
|
||||||
|
Dragstart.on(src).dragend(src);
|
||||||
|
expect(src.scope().result).toBe('none');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// In Safari dataTransfer.effectAllowed is always 'all', ignoring the value set in dragstart.
|
||||||
|
it('is determined from internal state in Safari', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="link"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks(false, 'copyLink');
|
||||||
|
var options = {effectAllowed: 'all'};
|
||||||
|
Dragstart.on(src).dragover(target, options).drop(target, options);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('link');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('link');
|
||||||
|
});
|
||||||
|
|
||||||
|
// On MacOS, modifiers automatically limit the effectAllowed passed to dragover and drop.
|
||||||
|
it('is limited by modifier keys on MacOS', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="all"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(src).dragover(target, {effectAllowed: 'copyLink'}).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('copy');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is copy if Ctrl key is pressed', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="all"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(src).dragover(target, {ctrlKey: true}).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('copy');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is link if Alt key is pressed', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="all"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(src).dragover(target, {altKey: true}).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('link');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('link');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores Ctrl key if copy is not possible', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="linkMove"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(src).dragover(target, {ctrlKey: true}).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('move');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('move');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects effectAllowed from external drops', function() {
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragenter.validExternalOn(target, {effectAllowed: 'copyLink'}).dragover(target).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('copy');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects effectAllowed from external drops in IE', function() {
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragenter.externalOn(target, {'Text': '{}'}, {effectAllowed: 'copyLink'})
|
||||||
|
.dragover(target).drop(target);
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('move');
|
||||||
|
expect(target.scope().drop.dropEffect).toBe('move');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores effectAllowed from internal drops in IE', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="copyLink"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
Dragstart.on(src, {allowedMimeTypes: ['Text']}).dragover(target, {altKey: true});
|
||||||
|
expect(target.scope().dragover.dropEffect).toBe('link');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set dropEffect in IE', function() {
|
||||||
|
var src = compileAndLink('<div dnd-draggable="{}" dnd-effect-allowed="copyLink"></div>');
|
||||||
|
var target = createListWithItemsAndCallbacks();
|
||||||
|
var dragover = Dragstart.on(src, {allowedMimeTypes: ['Text']}).dragover(target);
|
||||||
|
expect(dragover.dropEffect).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function verifyDropAccepted(result) {
|
||||||
|
expect(result.defaultPrevented).toBe(true);
|
||||||
|
if (result.type == 'dragenter') {
|
||||||
|
expect(result.returnValue).toBeUndefined();
|
||||||
|
expect(result.propagationStopped).toBe(false);
|
||||||
|
} else {
|
||||||
|
expect(result.returnValue).toBe(false);
|
||||||
|
expect(result.propagationStopped).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyDropCancelled(result, element, opt_defaultPrevented, opt_children) {
|
||||||
|
expect(result.returnValue).toBe(true);
|
||||||
|
expect(result.propagationStopped).toBe(false);
|
||||||
|
expect(result.defaultPrevented).toBe(opt_defaultPrevented || false);
|
||||||
|
expect(result.dropEffect).toBeUndefined();
|
||||||
|
expect(element.hasClass("dndDragover")).toBe(false);
|
||||||
|
expect(element.children().length).toBe(opt_children || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forAllHandlers(dragenter, element, verify) {
|
||||||
|
verify(dragenter, element);
|
||||||
|
var dragover = dragenter.dragover(element);
|
||||||
|
verify(dragover, element);
|
||||||
|
var dragover2 = dragover.dragover(element);
|
||||||
|
verify(dragover2, element);
|
||||||
|
var drop = dragover2.drop(element);
|
||||||
|
verify(drop, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createListWithItemsAndCallbacks(horizontal, effectAllowed) {
|
||||||
|
var params = '{event: event, dropEffect: dropEffect, index: index, '
|
||||||
|
+ 'item: item, external: external, type: type, callback: callback}';
|
||||||
|
var element = compileAndLink('<ul dnd-list="list" dnd-external-sources="true" ' +
|
||||||
|
'dnd-horizontal-list="' + (horizontal || 'false') + '" ' +
|
||||||
|
(effectAllowed ? 'dnd-effect-allowed="' + effectAllowed + '" ' : '') +
|
||||||
|
'dnd-dragover="dragover = ' + params + '" ' +
|
||||||
|
'dnd-drop="dropHandler(' + params + ')" ' +
|
||||||
|
'dnd-inserted="inserted = ' + params + '">' +
|
||||||
|
'<li>A</li><li>B</li><li>C</li></ul>');
|
||||||
|
element.scope().dropHandler = function(params) {
|
||||||
|
element.scope().drop = params;
|
||||||
|
return params.item;
|
||||||
|
};
|
||||||
|
element.scope().list = [1, 2, 3];
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
describe('dndNodrag', function() {
|
||||||
|
var element;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
element = compileAndLink('<div dnd-nodrag></div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the draggable attribute', function() {
|
||||||
|
expect(element.attr('draggable')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops propagation and prevents default for dragstart events', function() {
|
||||||
|
var dragstart = Dragstart.on(element);
|
||||||
|
expect(dragstart.propagationStopped).toBe(true);
|
||||||
|
expect(dragstart.defaultPrevented).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call preventDefault if dataTransfer is already set', function() {
|
||||||
|
var dragstart = Dragstart.on(element, {presetTypes: ['text/plain']});
|
||||||
|
expect(dragstart.propagationStopped).toBe(true);
|
||||||
|
expect(dragstart.defaultPrevented).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing in dragstart if the event was triggered on a dnd-handle', function() {
|
||||||
|
var dragstart = Dragstart.on(element, {dndHandle: true});
|
||||||
|
expect(dragstart.propagationStopped).toBe(false);
|
||||||
|
expect(dragstart.defaultPrevented).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops propagation of dragend events', function() {
|
||||||
|
var dragend = Dragend.on(element);
|
||||||
|
expect(dragend.propagationStopped).toBe(true);
|
||||||
|
expect(dragend.defaultPrevented).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing in dragend if the event was triggered on a dnd-handle', function() {
|
||||||
|
var dragend = Dragend.on(element, {dndHandle: true});
|
||||||
|
expect(dragend.propagationStopped).toBe(false);
|
||||||
|
expect(dragend.defaultPrevented).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Testing angular-drag-and-drop-lists...</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
|
||||||
|
|
||||||
|
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
|
||||||
|
<script src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
|
||||||
|
<script src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
|
||||||
|
|
||||||
|
<script src="../node_modules/jquery/dist/jquery.js"></script>
|
||||||
|
<script src="../node_modules/angular/angular.js"></script>
|
||||||
|
<script src="../node_modules/angular-mocks/angular-mocks.js"></script>
|
||||||
|
|
||||||
|
<script src="../angular-drag-and-drop-lists.js"></script>
|
||||||
|
|
||||||
|
<!-- Spec files -->
|
||||||
|
<script src="mocks.js"></script>
|
||||||
|
<script src="init.js"></script>
|
||||||
|
<script src="dndDraggableSpec.js"></script>
|
||||||
|
<script src="dndListSpec.js"></script>
|
||||||
|
<script src="dndNodragSpec.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
var $compile,
|
||||||
|
$rootScope;
|
||||||
|
|
||||||
|
beforeEach(module('dndLists'));
|
||||||
|
|
||||||
|
beforeEach(inject(function(_$compile_, _$rootScope_){
|
||||||
|
$compile = _$compile_;
|
||||||
|
$rootScope = _$rootScope_;
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
// Reset internal dndState in case dragend was not called.
|
||||||
|
Dragend.on(compileAndLink('<div dnd-draggable="{}"></div>'));
|
||||||
|
});
|
||||||
|
|
||||||
|
function compileAndLink(html) {
|
||||||
|
var scope = $rootScope.$new();
|
||||||
|
var element = $compile(html)(scope);
|
||||||
|
scope.$digest();
|
||||||
|
return element;
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
class DataTransferMock {
|
||||||
|
constructor() { this.$results = {}; }
|
||||||
|
get dropEffect() { throw "Unexcepted dropEffect getter invocation"; }
|
||||||
|
set dropEffect(value) { throw "Unexcepted dropEffect setter invocation"; }
|
||||||
|
get effectAllowed() { throw "Unexcepted effectAllowed getter invocation"; }
|
||||||
|
set effectAllowed(value) { throw "Unexcepted effectAllowed setter invocation"; }
|
||||||
|
get types() { throw "Unexcepted types getter invocation"; }
|
||||||
|
set types(value) { throw "Unexcepted types setter invocation"; }
|
||||||
|
getData() { throw "Unexcepted getData invocation"; }
|
||||||
|
setData() { throw "Unexcepted setData invocation"; }
|
||||||
|
setDragImage() { throw "Unexcepted setDragImage invocation"; }
|
||||||
|
getResults() { return this.$results; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragstartDataTransfer extends DataTransferMock {
|
||||||
|
constructor(options) {
|
||||||
|
super();
|
||||||
|
this.$allowSetDragImage = options.allowSetDragImage || false;
|
||||||
|
this.$allowedMimeTypes = options.allowedMimeTypes || null;
|
||||||
|
this.$presetTypes = options.presetTypes || [];
|
||||||
|
this.$results.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get effectAllowed() { throw "Unexcepted effectAllowed getter invocation"; }
|
||||||
|
set effectAllowed(value) { this.$results.effectAllowed = value; }
|
||||||
|
get types() { return this.$presetTypes; }
|
||||||
|
set types(value) { throw "Unexcepted types setter invocation"; }
|
||||||
|
|
||||||
|
setData(format, data) {
|
||||||
|
if (this.$allowedMimeTypes && !this.$allowedMimeTypes.includes(format)) {
|
||||||
|
throw "Invalid mime type " + format;
|
||||||
|
}
|
||||||
|
this.$results.data[format] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragImage(img) {
|
||||||
|
if (!this.$allowSetDragImage) throw "Unexcepted setDragImage invocation";
|
||||||
|
this.$results.dragImage = img;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DropzoneDataTransfer extends DataTransferMock {
|
||||||
|
constructor(data, options) {
|
||||||
|
super();
|
||||||
|
this.$data = data;
|
||||||
|
this.$dropEffect = options.dropEffect || 'move';
|
||||||
|
this.$effectAllowed = options.effectAllowed || 'move';
|
||||||
|
this.$types = options.undefinedTypes ? undefined : Object.keys(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
get dropEffect() { throw "Unexcepted dropEffect getter invocation"; }
|
||||||
|
set dropEffect(value) { this.$results.dropEffect = value; }
|
||||||
|
get effectAllowed() { return this.$effectAllowed; }
|
||||||
|
set effectAllowed(value) { throw "Unexcepted effectAllowed setter invocation"; }
|
||||||
|
get types() { return this.$types; }
|
||||||
|
set types(value) { throw "Unexcepted types setter invocation"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DropDataTransfer extends DropzoneDataTransfer {
|
||||||
|
getData(format) { return this.$data[format]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragEventMock {
|
||||||
|
constructor(type, dataTransfer, options) {
|
||||||
|
this.$type = type;
|
||||||
|
this.$dataTransfer = dataTransfer;
|
||||||
|
this.$options = options;
|
||||||
|
this.$results = {dataTransfer: dataTransfer.getResults()};
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientX() { return this.$options.clientX || 0; }
|
||||||
|
get clientY() { return this.$options.clientY || 0; }
|
||||||
|
get ctrlKey() { return this.$options.ctrlKey || false; }
|
||||||
|
get altKey() { return this.$options.altKey || false; }
|
||||||
|
get dataTransfer() { return this.$dataTransfer; }
|
||||||
|
get originalEvent() { return this; }
|
||||||
|
get target() { return this.$options.target || undefined; }
|
||||||
|
get type() { return this.$type; }
|
||||||
|
get _dndHandle() { return this.$options.dndHandle || undefined; }
|
||||||
|
get _dndPhShown() { return this.$options.phShown || undefined; }
|
||||||
|
set _dndPhShown(value) { this.$results.setDndPhShown = value; }
|
||||||
|
|
||||||
|
preventDefault() { this.$results.invokedPreventDefault = true; }
|
||||||
|
stopPropagation() { this.$results.invokedStopPropagation = true; }
|
||||||
|
getResults() { return this.$results; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragEventResult {
|
||||||
|
constructor(element, type, dataTransfer, opt_eventOptions) {
|
||||||
|
let handler = $._data($(element).get(0), "events")[type][0].handler;
|
||||||
|
let event = new DragEventMock(type, dataTransfer, opt_eventOptions || {});
|
||||||
|
this.$results = event.getResults();
|
||||||
|
this.$results.returnValue = handler(event);
|
||||||
|
this.$type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get propagationStopped() { return !!this.$results.invokedStopPropagation; }
|
||||||
|
get defaultPrevented() { return !!this.$results.invokedPreventDefault; }
|
||||||
|
get dndPhShownSet() { return this.$results.setDndPhShown || false; }
|
||||||
|
get returnValue() { return this.$results.returnValue; }
|
||||||
|
get dropEffect() { return this.$results.dataTransfer.dropEffect; }
|
||||||
|
get type() { return this.$type; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragstart extends DragEventResult {
|
||||||
|
constructor(element, options) {
|
||||||
|
super(element, 'dragstart', new DragstartDataTransfer(options), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() { return this.$results.dataTransfer.data; }
|
||||||
|
get dragImage() { return this.$results.dataTransfer.dragImage; }
|
||||||
|
get effectAllowed() { return this.$results.dataTransfer.effectAllowed; }
|
||||||
|
|
||||||
|
dragenter(element, opt_options) {
|
||||||
|
var options = $.extend({effectAllowed: this.effectAllowed}, opt_options);
|
||||||
|
return new Dragenter(element, this.$results.dataTransfer.data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragover(element, opt_options) {
|
||||||
|
return this.dragenter(element, opt_options).dragover(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragend(element) {
|
||||||
|
return Dragend.on(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
static on(element, opt_options) {
|
||||||
|
return new Dragstart(element, opt_options || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragend extends DragEventResult {
|
||||||
|
constructor(element, options) {
|
||||||
|
super(element, 'dragend', new DataTransferMock(), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static on(element, opt_options) {
|
||||||
|
return new Dragend(element, opt_options || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DropzoneEventResult extends DragEventResult {
|
||||||
|
constructor(element, type, data, dataTransfer, options) {
|
||||||
|
options.target = options.target || element[0];
|
||||||
|
super(element, type, dataTransfer, options);
|
||||||
|
this.$originalData = $.extend({}, data);
|
||||||
|
this.$options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragover(element, opt_options) {
|
||||||
|
return new Dragover(element, this.$originalData, $.extend({}, this.$options, opt_options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragenter extends DropzoneEventResult {
|
||||||
|
constructor(element, data, options) {
|
||||||
|
super(element, 'dragenter', data, new DropzoneDataTransfer(data, options), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static externalOn(element, data, opt_options) {
|
||||||
|
return new Dragenter(element, data, opt_options || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static validExternalOn(element, opt_options) {
|
||||||
|
return Dragenter.externalOn(element, {'application/x-dnd': '{"hello":"world"}'}, opt_options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragover extends DropzoneEventResult {
|
||||||
|
constructor(element, data, options) {
|
||||||
|
super(element, 'dragover', data, new DropzoneDataTransfer(data, options), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragleave(element, opt_options) {
|
||||||
|
return new Dragleave(element, this.$originalData, $.extend({}, this.$options, opt_options));
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(element, opt_options) {
|
||||||
|
return new Drop(element, this.$originalData, $.extend({}, this.$options, opt_options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragleave extends DropzoneEventResult {
|
||||||
|
constructor(element, data, options) {
|
||||||
|
super(element, 'dragleave', data, new DataTransferMock(), options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Drop extends DropzoneEventResult {
|
||||||
|
constructor(element, data, options) {
|
||||||
|
super(element, 'drop', data, new DropDataTransfer(data, options), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
dragend(element) {
|
||||||
|
return Dragend.on(element);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular-route",
|
|
||||||
"version": "1.5.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./angular-route.js",
|
|
||||||
"ignore": [],
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "1.5.5"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/angular/bower-angular-route",
|
|
||||||
"_release": "1.5.5",
|
|
||||||
"_resolution": {
|
|
||||||
"type": "version",
|
|
||||||
"tag": "v1.5.5",
|
|
||||||
"commit": "642a2dfc05f869f4a56733e182393536184ed4fa"
|
|
||||||
},
|
|
||||||
"_source": "https://github.com/angular/bower-angular-route.git",
|
|
||||||
"_target": "1.5.5",
|
|
||||||
"_originalSource": "angular-route"
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular-sanitize",
|
|
||||||
"version": "1.5.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./angular-sanitize.js",
|
|
||||||
"ignore": [],
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "1.5.5"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/angular/bower-angular-sanitize",
|
|
||||||
"_release": "1.5.5",
|
|
||||||
"_resolution": {
|
|
||||||
"type": "version",
|
|
||||||
"tag": "v1.5.5",
|
|
||||||
"commit": "84a69853f2a591b1b9d000984e6219924aaff016"
|
|
||||||
},
|
|
||||||
"_source": "https://github.com/angular/bower-angular-sanitize.git",
|
|
||||||
"_target": "1.5.5",
|
|
||||||
"_originalSource": "angular-sanitize"
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"name": "angular-ui-select",
|
|
||||||
"homepage": "https://github.com/angular-ui/ui-select",
|
|
||||||
"authors": [
|
|
||||||
"AngularUI"
|
|
||||||
],
|
|
||||||
"description": "AngularJS ui-select",
|
|
||||||
"main": [
|
|
||||||
"dist/select.js",
|
|
||||||
"dist/select.css"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"ignore": [
|
|
||||||
"**/.*",
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
"src",
|
|
||||||
"test",
|
|
||||||
"gulpfile.js",
|
|
||||||
"karma.conf.js",
|
|
||||||
"examples"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"angular": ">=1.2.18"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"jquery": "~1.11",
|
|
||||||
"angular-sanitize": ">=1.2.18",
|
|
||||||
"angular-mocks": ">=1.2.18"
|
|
||||||
},
|
|
||||||
"version": "0.19.8",
|
|
||||||
"_release": "0.19.8",
|
|
||||||
"_resolution": {
|
|
||||||
"type": "version",
|
|
||||||
"tag": "v0.19.8",
|
|
||||||
"commit": "fc6502179847b41f864be042d432eec32e8530c4"
|
|
||||||
},
|
|
||||||
"_source": "https://github.com/angular-ui/ui-select.git",
|
|
||||||
"_target": "v0.19.8",
|
|
||||||
"_originalSource": "https://github.com/angular-ui/ui-select.git"
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.appendToBodyDemo.startToggleTimer()"
|
||||||
|
ng-disabled="ctrl.appendToBodyDemo.remainingTime">
|
||||||
|
{{ ctrl.appendToBodyDemo.remainingTime ? 'Toggling in ' + (ctrl.appendToBodyDemo.remainingTime / 1000) + ' seconds' : 'Toggle ui-select presence' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<div class="select-box" ng-show="ctrl.appendToBodyDemo.present">
|
||||||
|
<h3>Bootstrap theme</h3>
|
||||||
|
<p>Selected: {{ctrl.address.selected.formatted_address}}</p>
|
||||||
|
<ui-select ng-model="ctrl.address.selected"
|
||||||
|
theme="bootstrap"
|
||||||
|
ng-disabled="ctrl.disabled"
|
||||||
|
reset-search-input="false"
|
||||||
|
style="width: 600px;"
|
||||||
|
title="Choose an address"
|
||||||
|
append-to-body="true">
|
||||||
|
<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="address in ctrl.addresses track by $index"
|
||||||
|
refresh="ctrl.refreshAddresses($select.search)"
|
||||||
|
refresh-delay="0">
|
||||||
|
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-box" ng-if="ctrl.appendToBodyDemo.present">
|
||||||
|
<h3>Select2 theme</h3>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person" append-to-body="true">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-box" ng-if="ctrl.appendToBodyDemo.present">
|
||||||
|
<h3>Selectize theme</h3>
|
||||||
|
<p>Selected: {{ctrl.country.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.country.selected" theme="selectize" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country" append-to-body="true">
|
||||||
|
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search">
|
||||||
|
<span ng-bind-html="country.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="country.code | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
|
||||||
|
</div>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h3>Bootstrap theme <small>(remote data source)</small></h3>
|
||||||
|
<p>Selected: {{ctrl.address.selected.formatted_address}}</p>
|
||||||
|
<ui-select ng-model="ctrl.address.selected"
|
||||||
|
theme="bootstrap"
|
||||||
|
ng-disabled="ctrl.disabled"
|
||||||
|
reset-search-input="false"
|
||||||
|
style="width: 600px;"
|
||||||
|
title="Choose an address">
|
||||||
|
<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="address in ctrl.addresses track by $index"
|
||||||
|
refresh="ctrl.refreshAddresses($select.search)"
|
||||||
|
refresh-delay="0">
|
||||||
|
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Select2 theme</h3>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Selectize theme</h3>
|
||||||
|
<p>Selected: {{ctrl.country.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.country.selected" theme="selectize" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
|
||||||
|
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search">
|
||||||
|
<span ng-bind-html="country.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="country.code | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
21
js/vendor/angular-ui-select/docs/examples/demo-bind-to-single-property-async.html
поставляемый
Normal file
21
js/vendor/angular-ui-select/docs/examples/demo-bind-to-single-property-async.html
поставляемый
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h3>Select2 theme</h3>
|
||||||
|
<h4>Single property binding with async data</h4>
|
||||||
|
|
||||||
|
<p>Selected: <code> {{ctrl.personAsync.selected | json }}</code></p>
|
||||||
|
<p>List Count: <code>{{ (ctrl.peopleAsync || []).length }}</code></p>
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.personAsync.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Single property binding with async data">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name || $select.selected}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.email as person in ctrl.peopleAsync | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p><small><em>Data will be populated in 3 secs</em></small></p>
|
22
js/vendor/angular-ui-select/docs/examples/demo-bind-to-single-property.html
поставляемый
Normal file
22
js/vendor/angular-ui-select/docs/examples/demo-bind-to-single-property.html
поставляемый
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h3>Select2 theme</h3>
|
||||||
|
<p>Selected: <code>{{ctrl.person.selected}}</code></p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.email as person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<button class="btn btn-primary" ng-click="ctrl.person.selected = 'wladimir@email.com'">Change Model</button>
|
||||||
|
<p>Runs <code>ctrl.person.selected = 'wladimir@email.com'</code></p>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<p>Selected: {{ctrl.person.selected.name}}</p>
|
||||||
|
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
<legend>ui-select inside a Bootstrap form</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Default</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="bootstrap">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Grouped</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="bootstrap">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="'country'" repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">With a clear button</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group">
|
||||||
|
|
||||||
|
<ui-select allow-clear ng-model="ctrl.person.selected" theme="bootstrap">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="button" ng-click="ctrl.person.selected = undefined" class="btn btn-default">
|
||||||
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Disabled</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="bootstrap" ng-disabled="true">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<code>ctrl.disabled {{ctrl.disabled | json}}</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enableSearch()">Enable Search</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disableSearch()">Disable Search</button>
|
||||||
|
<code>ctrl.searchEnabled {{ ctrl.searchEnabled | json }}</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
</p>
|
||||||
|
<h3>Bootstrap theme</h3>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="bootstrap" search-enabled="ctrl.searchEnabled" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<hr />
|
||||||
|
<p>Number Selected: {{ctrl.multipleDemo.selectedPeople.length}}</p>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeople" theme="bootstrap" search-enabled="ctrl.searchEnabled" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Select2 theme</h3>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" search-enabled="ctrl.searchEnabled" ng-disabled="ctrl.disabled" style="min-width: 300px;">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<hr />
|
||||||
|
<p>Number Selected: {{ctrl.multipleDemo.selectedPeople.length}}</p>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeople" theme="select2" search-enabled="ctrl.searchEnabled" ng-disabled="ctrl.disabled" style="min-width: 300px;">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<h3>Selectize theme</h3>
|
||||||
|
<p>Selected: {{ctrl.country.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.country.selected" theme="selectize" search-enabled="ctrl.searchEnabled" ng-disabled="ctrl.disabled" style="width: 300px;">
|
||||||
|
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="country in ctrl.countries | filter: $select.search">
|
||||||
|
<span ng-bind-html="country.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="country.code | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<h3>Dropdown Position</h3>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<pre>default value can be changed at <strong class="text-warning">uiSelectConfig</strong></pre>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Always UP</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}" position='up'>
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Always DOWN</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}" position='down'>
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
<p>AUTO depending on available space (default)</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}" position='auto'>
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h3>Event on Select</h3>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" on-select="ctrl.onSelectCallback($item, $model)" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.email as person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<h3>Event Result</h3>
|
||||||
|
<p>Called <code>{{ctrl.counter+0}}</code> times</p>
|
||||||
|
<p>Parameters</p>
|
||||||
|
<pre>
|
||||||
|
$model = {{ ctrl.eventResult.model | json }}
|
||||||
|
$item = {{ ctrl.eventResult.item | json }}
|
||||||
|
</pre>
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Focus</h1>
|
||||||
|
<p>Regular uiSelect</p>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Autofocus</h3>
|
||||||
|
<p>Use the <code>autofocus</code> attribute to automatically focus ui-select when the page is loaded</p>
|
||||||
|
<ui-select autofocus ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Focus on Demand</h3>
|
||||||
|
<p>Use the <code>focus-on</code> attribute, defining a scope event name (<code>UiSelectDemo1</code>) to listen for, to manually trigger focussing of ui-select.</p>
|
||||||
|
<ui-select focus-on="UiSelectDemo1" ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p></p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-default" ng-click="ctrl.setInputFocus()">Set Focus</button>
|
||||||
|
(<code>$scope.$broadcast('UiSelectDemo1');</code>)
|
||||||
|
</p>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Group By</h1>
|
||||||
|
<p>Selected: {{ctrl.person.selected}}</p>
|
||||||
|
|
||||||
|
<h3>Grouped using a string <small><code>group-by="'country'"</code></small></h3>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="'country'" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Grouped using a function <small><code>group-by="ctrl.someGroupFn"</code></small></h3>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="ctrl.someGroupFn" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Regular</h3>
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Group Filtering</h1>
|
||||||
|
<p>You can filter which groups are displayed by ui-select, by using the <code>group-filter</code> attribute. The provided value should either be an array or function.</p>
|
||||||
|
<p>Selected: {{country.selected}}</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h3> Filter groups by array <small><code>group-filter="['Z','B','C']"</code></small></h3>
|
||||||
|
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
|
||||||
|
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="ctrl.firstLetterGroupFn" group-filter="['Z','B','C']" repeat="country in ctrl.countries | filter: $select.search">
|
||||||
|
<span ng-bind-html="country.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="country.code | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Filter groups using a function <small><code>group-filter="ctrl.reverseOrderFilterFn"</code></small></h3>
|
||||||
|
<ui-select ng-model="ctrl.country.selected" theme="select2" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a country">
|
||||||
|
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="ctrl.firstLetterGroupFn" group-filter="ctrl.reverseOrderFilterFn" repeat="country in ctrl.countries | filter: $select.search">
|
||||||
|
<span ng-bind-html="country.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="country.code | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Multiple Selection</h1>
|
||||||
|
|
||||||
|
<h3>Array of strings</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.colors" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 300px;" title="Choose a color">
|
||||||
|
<ui-select-match placeholder="Select colors...">{{$item}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="color in ctrl.availableColors | filter:$select.search">
|
||||||
|
{{color}}
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.colors}}</p>
|
||||||
|
<hr>
|
||||||
|
<h3>Array of objects (sorting enabled)</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeople" theme="bootstrap" ng-disabled="ctrl.disabled" sortable="true" close-on-select="false" style="width: 800px;">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.selectedPeople}}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Deselect callback with single property binding</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.deSelectedPeople" on-remove="ctrl.removed($item, $model)" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.email as person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Last Removed:</p>
|
||||||
|
<pre>
|
||||||
|
$item = {{ctrl.lastRemoved.item}}
|
||||||
|
$model = {{ctrl.lastRemoved.model}}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Array of objects with single property binding</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeopleSimple" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.email as person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.selectedPeopleSimple}}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Array of objects (with groupBy)</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.selectedPeopleWithGroupBy" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices group-by="ctrl.someGroupFn" repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.selectedPeopleWithGroupBy}}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h3>Disabling instead of removing selected items</h3>
|
||||||
|
<ui-select multiple ng-model="ctrl.multipleDemo.removeSelectIsFalse" theme="bootstrap" ng-disabled="ctrl.disabled" close-on-select="false" style="width: 800px;" title="Choose a person" remove-selected="false">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-bind-html="person.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.removeSelectIsFalse}}</p>
|
||||||
|
|
||||||
|
<div style="height:500px"></div>
|
|
@ -0,0 +1,47 @@
|
||||||
|
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Object as Item Source</h1>
|
||||||
|
<p>uiSelect supports providing choices using the <code>(key, value)</code> format, similar to that used by <code>ng-repeat</code></p>
|
||||||
|
|
||||||
|
<h3>Using value for binding</h3>
|
||||||
|
|
||||||
|
<p>Selected: <code>{{ctrl.person.selectedValue}}</code></p>
|
||||||
|
<ui-select ng-model="ctrl.person.selectedValue" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.value as (key, person) in ctrl.peopleObj | filter: { value: { name: $select.search }}">
|
||||||
|
<div ng-bind-html="person.value.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.value.email}}
|
||||||
|
age: {{person.value.age}}
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Using single property for binding</h3>
|
||||||
|
<p>Selected: <code>{{ctrl.person.selectedSingle}}</code></p>
|
||||||
|
<ui-select ng-model="ctrl.person.selectedSingle" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.value.name as (key, person) in ctrl.peopleObj | filter: { value: { name: $select.search }}">
|
||||||
|
<div ng-bind-html="person.value.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.value.email}}
|
||||||
|
age: {{person.value.age}}
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<h3>Using key for binding</h3>
|
||||||
|
<p>Selected: <code>{{ctrl.person.selectedSingleKey}}</code></p>
|
||||||
|
<ui-select ng-model="ctrl.person.selectedSingleKey" theme="select2" ng-disabled="ctrl.disabled" style="min-width: 300px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="person.key as (key, person) in ctrl.peopleObj | filter: { value: { name: $select.search }}">
|
||||||
|
<div ng-bind-html="person.value.name | highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.value.email}}
|
||||||
|
age: {{person.value.age}}
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
91
js/vendor/angular-ui-select/docs/examples/demo-select2-with-bootstrap.html
поставляемый
Normal file
91
js/vendor/angular-ui-select/docs/examples/demo-select2-with-bootstrap.html
поставляемый
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<p>Selected: {{ctrl.person.selected.name}}</p>
|
||||||
|
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
<legend>ui-select inside a Bootstrap form</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Default</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" class="form-control" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Multiple</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select multiple sortable="true" ng-model="ctrl.multipleDemo.selectedPeople" theme="select2" class="form-control" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$item.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Grouped</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" class="form-control" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="'country'" repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">With a clear button</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group select2-bootstrap-append">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" class="form-control" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="button" ng-click="ctrl.person.selected = undefined" class="btn btn-default">
|
||||||
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Disabled</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="select2" class="form-control" ng-disabled="true" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
111
js/vendor/angular-ui-select/docs/examples/demo-selectize-with-bootstrap.html
поставляемый
Normal file
111
js/vendor/angular-ui-select/docs/examples/demo-selectize-with-bootstrap.html
поставляемый
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectize-control {
|
||||||
|
/* Align Selectize with input-group-btn */
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectize-control > .selectize-dropdown {
|
||||||
|
top: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset right rounded corners, see Bootstrap input-groups.less */
|
||||||
|
.input-group > .selectize-control > .selectize-input {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>Selected: {{ctrl.person.selected.name}}</p>
|
||||||
|
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<fieldset>
|
||||||
|
<legend>ui-select inside a Bootstrap form</legend>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Default</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="selectize" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Grouped</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="selectize" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices group-by="'country'" repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">With a clear button</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="input-group">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="selectize">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<span ng-bind-html="item.name | highlight: $select.search"></span>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button type="button" ng-click="ctrl.person.selected = undefined" class="btn btn-default">
|
||||||
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Multiple</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" multiple theme="selectize" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$item.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label">Disabled</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
|
||||||
|
<ui-select ng-model="ctrl.person.selected" theme="selectize" class="form-control" ng-disabled="true" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select or search a person in the list...">{{$select.selected.name}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="item in ctrl.people | filter: $select.search">
|
||||||
|
<div ng-bind-html="item.name | highlight: $select.search"></div>
|
||||||
|
<small ng-bind-html="item.email | highlight: $select.search"></small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.enable()">Enable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.disable()">Disable ui-select</button>
|
||||||
|
<button class="btn btn-default btn-xs" ng-click="ctrl.clear()">Clear ng-model</button>
|
||||||
|
|
||||||
|
<h1>Tagging</h1>
|
||||||
|
<p>Tagging allows the creation of new items not present in the item list</p>
|
||||||
|
|
||||||
|
<h3>Simple String Tags <small>(With Custom Tag Label & Sort Enabled)</small></h3>
|
||||||
|
|
||||||
|
<ui-select multiple tagging tagging-label="(custom 'new' label)" ng-model="ctrl.multipleDemo.colors" theme="bootstrap" sortable="true" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a color">
|
||||||
|
<ui-select-match placeholder="Select colors...">{{$item}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="color in ctrl.availableColors | filter:$select.search">
|
||||||
|
{{color}}
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.colors}}</p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Simple String Tags <small>(Predictive Search Model & No Labels)</small></h3>
|
||||||
|
|
||||||
|
<ui-select multiple tagging tagging-label="false" ng-model="ctrl.multipleDemo.colors2" theme="bootstrap" ng-disabled="ctrl.disabled" style="width: 300px;" title="Choose a color">
|
||||||
|
<ui-select-match placeholder="Select colors...">{{$item}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="color in ctrl.availableColors | filter:$select.search">
|
||||||
|
{{color}}
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: {{ctrl.multipleDemo.colors2}}</p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Object Tags <small>(with grouping)</small></h3>
|
||||||
|
<ui-select multiple tagging="ctrl.tagTransform" ng-model="ctrl.multipleDemo.selectedPeople" theme="bootstrap" ng-disabled="ctrl.disabled" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}" group-by="'country'">
|
||||||
|
<div ng-if="person.isTag" ng-bind-html="(person.name | highlight: $select.search) +' (new)'"></div>
|
||||||
|
<div ng-if="!person.isTag" ng-bind-html="person.name + person.isTag| highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: </p>
|
||||||
|
<pre>selectedPeople = {{ctrl.multipleDemo.selectedPeople | json}}</pre>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Object Tags with Tokenization <small>("<code> </code>", <code>/</code>, <code>,</code>)</small></h3>
|
||||||
|
<strong>Note that the Space character can't be used literally, use the keyword SPACE - <code>tagging-tokens="SPACE|,|/"</code></strong>
|
||||||
|
<ui-select multiple tagging="ctrl.tagTransform" tagging-tokens="SPACE|,|/" ng-model="ctrl.multipleDemo.selectedPeople2" theme="bootstrap" ng-disabled="ctrl.disabled" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$item.name}} <{{$item.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-if="person.isTag" ng-bind-html="person.name + ' ' + $select.taggingLabel | highlight: $select.search"></div>
|
||||||
|
<div ng-if="!person.isTag" ng-bind-html="person.name| highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: </p>
|
||||||
|
<pre>selectedPeople2 = {{ctrl.multipleDemo.selectedPeople2 | json}}</pre>
|
||||||
|
|
||||||
|
<h3>Tagging in Single Select mode</h3>(NOT WORKING)
|
||||||
|
<ui-select tagging="ctrl.tagTransform" ng-model="ctrl.person.selected" theme="bootstrap" ng-disabled="ctrl.disabled" style="width: 800px;" title="Choose a person">
|
||||||
|
<ui-select-match placeholder="Select person...">{{$select.selected.name}} <{{$select.selected.email}}></ui-select-match>
|
||||||
|
<ui-select-choices repeat="person in ctrl.people | propsFilter: {name: $select.search, age: $select.search}">
|
||||||
|
<div ng-if="person.isTag" ng-bind-html="(person.name | highlight: $select.search) +' (new)'"></div>
|
||||||
|
<div ng-if="!person.isTag" ng-bind-html="person.name + person.isTag| highlight: $select.search"></div>
|
||||||
|
<small>
|
||||||
|
email: {{person.email}}
|
||||||
|
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
|
||||||
|
</small>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected:</p>
|
||||||
|
<pre>ctrl.person.selected = {{ctrl.person.selected | json }}</pre>
|
||||||
|
|
||||||
|
<h3>Tagging in Single select mode, with simple strings</h3>(NOT WORKING)
|
||||||
|
<ui-select tagging tagging-label="('new')" ng-model="ctrl.singleDemo.color" theme="bootstrap" style="width: 800px;" title="Choose a color">
|
||||||
|
<ui-select-match placeholder="Select color...">{{$select.selected}}</ui-select-match>
|
||||||
|
<ui-select-choices repeat="color in ctrl.availableColors | filter: $select.search">
|
||||||
|
<div ng-bind-html="color | highlight: $select.search"></div>
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
<p>Selected: <code>{{ctrl.singleDemo.color}}</code></p>
|
||||||
|
|
||||||
|
<div style="height:500px"></div>
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" ng-app="ui.select.pages">
|
<html lang="en" ng-app="ui.select.pages">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" ng-app="demo">
|
<html lang="en" ng-app="demo">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
var fs = require('fs');
|
||||||
|
var del = require('del');
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var streamqueue = require('streamqueue');
|
||||||
|
var karma = require('karma').server;
|
||||||
|
var $ = require('gulp-load-plugins')();
|
||||||
|
var runSequence = require('run-sequence');
|
||||||
|
var conventionalRecommendedBump = require('conventional-recommended-bump');
|
||||||
|
var titleCase = require('title-case');
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
pkg : JSON.parse(fs.readFileSync('./package.json')),
|
||||||
|
banner:
|
||||||
|
'/*!\n' +
|
||||||
|
' * <%= pkg.name %>\n' +
|
||||||
|
' * <%= pkg.homepage %>\n' +
|
||||||
|
' * Version: <%= pkg.version %> - <%= timestamp %>\n' +
|
||||||
|
' * License: <%= pkg.license %>\n' +
|
||||||
|
' */\n\n\n'
|
||||||
|
};
|
||||||
|
|
||||||
|
gulp.task('default', ['build','test']);
|
||||||
|
gulp.task('build', ['scripts', 'styles']);
|
||||||
|
gulp.task('test', ['build', 'karma']);
|
||||||
|
|
||||||
|
gulp.task('watch', ['build','karma-watch'], function() {
|
||||||
|
gulp.watch(['src/**/*.{js,html}'], ['build']);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('clean', function(cb) {
|
||||||
|
del(['dist', 'temp'], cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('scripts', ['clean'], function() {
|
||||||
|
|
||||||
|
var buildTemplates = function () {
|
||||||
|
return gulp.src('src/**/*.html')
|
||||||
|
.pipe($.minifyHtml({
|
||||||
|
empty: true,
|
||||||
|
spare: true,
|
||||||
|
quotes: true
|
||||||
|
}))
|
||||||
|
.pipe($.angularTemplatecache({module: 'ui.select'}));
|
||||||
|
};
|
||||||
|
|
||||||
|
var buildLib = function(){
|
||||||
|
return gulp.src(['src/common.js','src/*.js'])
|
||||||
|
.pipe($.plumber({
|
||||||
|
errorHandler: handleError
|
||||||
|
}))
|
||||||
|
.pipe($.concat('select_without_templates.js'))
|
||||||
|
.pipe($.header('(function () { \n"use strict";\n'))
|
||||||
|
.pipe($.footer('\n}());'))
|
||||||
|
.pipe(gulp.dest('temp'))
|
||||||
|
.pipe($.jshint())
|
||||||
|
.pipe($.jshint.reporter('jshint-stylish'))
|
||||||
|
.pipe($.jshint.reporter('fail'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return streamqueue({objectMode: true }, buildLib(), buildTemplates())
|
||||||
|
.pipe($.plumber({
|
||||||
|
errorHandler: handleError
|
||||||
|
}))
|
||||||
|
.pipe($.concat('select.js'))
|
||||||
|
.pipe($.header(config.banner, {
|
||||||
|
timestamp: (new Date()).toISOString(), pkg: config.pkg
|
||||||
|
}))
|
||||||
|
.pipe(gulp.dest('dist'))
|
||||||
|
.pipe($.sourcemaps.init())
|
||||||
|
.pipe($.uglify({preserveComments: 'some'}))
|
||||||
|
.pipe($.concat('select.min.js'))
|
||||||
|
.pipe($.sourcemaps.write('./'))
|
||||||
|
.pipe(gulp.dest('dist'));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('styles', ['clean'], function() {
|
||||||
|
|
||||||
|
return gulp.src(['src/common.css'], {base: 'src'})
|
||||||
|
.pipe($.sourcemaps.init())
|
||||||
|
.pipe($.header(config.banner, {
|
||||||
|
timestamp: (new Date()).toISOString(), pkg: config.pkg
|
||||||
|
}))
|
||||||
|
.pipe($.concat('select.css'))
|
||||||
|
.pipe(gulp.dest('dist'))
|
||||||
|
.pipe($.minifyCss())
|
||||||
|
.pipe($.concat('select.min.css'))
|
||||||
|
.pipe($.sourcemaps.write('../dist', {debug: true}))
|
||||||
|
.pipe(gulp.dest('dist'));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('karma', ['build'], function() {
|
||||||
|
karma.start({configFile : __dirname +'/karma.conf.js', singleRun: true});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('karma-watch', ['build'], function() {
|
||||||
|
karma.start({configFile : __dirname +'/karma.conf.js', singleRun: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('pull', function(done) {
|
||||||
|
$.git.pull();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('add', function(done) {
|
||||||
|
$.git.add();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('recommendedBump', function(done) {
|
||||||
|
/**
|
||||||
|
* Bumping version number and tagging the repository with it.
|
||||||
|
* Please read http://semver.org/
|
||||||
|
*
|
||||||
|
* To bump the version numbers accordingly after you did a patch,
|
||||||
|
* introduced a feature or made a backwards-incompatible release.
|
||||||
|
*/
|
||||||
|
|
||||||
|
conventionalRecommendedBump({preset: 'angular'}, function(err, importance) {
|
||||||
|
// Get all the files to bump version in
|
||||||
|
gulp.src(['./package.json'])
|
||||||
|
.pipe($.bump({type: importance}))
|
||||||
|
.pipe(gulp.dest('./'));
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('changelog', function() {
|
||||||
|
|
||||||
|
return gulp.src('CHANGELOG.md', {buffer: false})
|
||||||
|
.pipe($.conventionalChangelog({preset: 'angular'}))
|
||||||
|
.pipe(gulp.dest('./'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('push', function(done) {
|
||||||
|
$.git.push('origin', 'master', {args: '--follow-tags'});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('commit', function() {
|
||||||
|
return gulp.src('./')
|
||||||
|
.pipe($.git.commit('chore(release): bump package version and update changelog', {emitData: true}))
|
||||||
|
.on('data', function(data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('tag', function() {
|
||||||
|
return gulp.src('package.json')
|
||||||
|
.pipe($.tagVersion());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('bump', function(done) {
|
||||||
|
runSequence('recommendedBump', 'changelog', 'add', 'commit', 'tag', 'push', done);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs', function (cb) {
|
||||||
|
runSequence('docs:clean', 'docs:examples', 'docs:assets', 'docs:index', cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs:clean', function (cb) {
|
||||||
|
del(['docs-built'], cb)
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs:assets', function () {
|
||||||
|
gulp.src('./dist/*').pipe(gulp.dest('./docs-built/dist'));
|
||||||
|
return gulp.src('docs/assets/*').pipe(gulp.dest('./docs-built/assets'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs:examples', function () {
|
||||||
|
// Need a way to reset filename list: $.filenames('exampleFiles',{overrideMode:true});
|
||||||
|
return gulp.src(['docs/examples/*.html'])
|
||||||
|
.pipe($.header(fs.readFileSync('docs/partials/_header.html')))
|
||||||
|
.pipe($.footer(fs.readFileSync('docs/partials/_footer.html')))
|
||||||
|
.pipe($.filenames('exampleFiles'))
|
||||||
|
.pipe(gulp.dest('./docs-built/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs:index', function () {
|
||||||
|
|
||||||
|
var exampleFiles = $.filenames.get('exampleFiles');
|
||||||
|
exampleFiles = exampleFiles.map(function (filename) {
|
||||||
|
var cleaned = titleCase(filename.replace('demo-', '').replace('.html', ''));
|
||||||
|
return '<h4><a href="./' + filename + '">' + cleaned + '</a> <plnkr-opener example-path="' + filename + '"></plnkr-opener></h4>';
|
||||||
|
});
|
||||||
|
|
||||||
|
return gulp.src('docs/index.html')
|
||||||
|
.pipe($.replace('<!-- INSERT EXAMPLES HERE -->', exampleFiles.join("\n")))
|
||||||
|
.pipe(gulp.dest('./docs-built/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('docs:watch', ['docs'], function() {
|
||||||
|
gulp.watch(['docs/**/*.{js,html}'], ['docs']);
|
||||||
|
});
|
||||||
|
|
||||||
|
var handleError = function (err) {
|
||||||
|
console.log(err.toString());
|
||||||
|
this.emit('end');
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
|
||||||
|
// Base path, that will be used to resolve files and exclude
|
||||||
|
basePath: '',
|
||||||
|
|
||||||
|
// Frameworks to use
|
||||||
|
frameworks: ['jasmine'],
|
||||||
|
|
||||||
|
// List of files / patterns to load in the browser
|
||||||
|
files: [
|
||||||
|
'node_modules/jquery/dist/jquery.js',
|
||||||
|
'node_modules/angular/angular.js',
|
||||||
|
'node_modules/angular-sanitize/angular-sanitize.js',
|
||||||
|
'node_modules/angular-mocks/angular-mocks.js',
|
||||||
|
|
||||||
|
'dist/select.js',
|
||||||
|
'test/helpers.js',
|
||||||
|
'test/**/*.spec.js'
|
||||||
|
],
|
||||||
|
|
||||||
|
// List of files to exclude
|
||||||
|
exclude: ['./index.js'],
|
||||||
|
|
||||||
|
// Web server port
|
||||||
|
port: 9876,
|
||||||
|
|
||||||
|
// Level of logging
|
||||||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
|
||||||
|
// Enable / disable watching file and executing tests whenever any file changes
|
||||||
|
autoWatch: true,
|
||||||
|
|
||||||
|
// Start these browsers, currently available:
|
||||||
|
// - Chrome
|
||||||
|
// - ChromeCanary
|
||||||
|
// - Firefox
|
||||||
|
// - Opera
|
||||||
|
// - Safari (only Mac)
|
||||||
|
// - PhantomJS
|
||||||
|
// - IE (only Windows)
|
||||||
|
browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'],
|
||||||
|
|
||||||
|
// Continuous Integration mode
|
||||||
|
// if true, it capture browsers, run tests and exit
|
||||||
|
singleRun: false
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
<ul class="ui-select-choices ui-select-choices-content ui-select-dropdown dropdown-menu"
|
||||||
|
ng-show="$select.open && $select.items.length > 0">
|
||||||
|
<li class="ui-select-choices-group" id="ui-select-choices-{{ $select.generatedId }}" >
|
||||||
|
<div class="divider" ng-show="$select.isGrouped && $index > 0"></div>
|
||||||
|
<div ng-show="$select.isGrouped" class="ui-select-choices-group-label dropdown-header" ng-bind="$group.name"></div>
|
||||||
|
<div ng-attr-id="ui-select-choices-row-{{ $select.generatedId }}-{{$index}}" class="ui-select-choices-row"
|
||||||
|
ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" role="option">
|
||||||
|
<span class="ui-select-choices-row-inner"></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<span class="ui-select-match">
|
||||||
|
<span ng-repeat="$item in $select.selected track by $index">
|
||||||
|
<span
|
||||||
|
class="ui-select-match-item btn btn-default btn-xs"
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
ng-click="$selectMultiple.activeMatchIndex = $index;"
|
||||||
|
ng-class="{'btn-primary':$selectMultiple.activeMatchIndex === $index, 'select-locked':$select.isLocked(this, $index)}"
|
||||||
|
ui-select-sort="$select.selected">
|
||||||
|
<span class="close ui-select-match-close" ng-hide="$select.disabled" ng-click="$selectMultiple.removeChoice($index)"> ×</span>
|
||||||
|
<span uis-transclude-append></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="ui-select-match" ng-hide="$select.open && $select.searchEnabled" ng-disabled="$select.disabled" ng-class="{'btn-default-focus':$select.focus}">
|
||||||
|
<span tabindex="-1"
|
||||||
|
class="btn btn-default form-control ui-select-toggle"
|
||||||
|
aria-label="{{ $select.baseTitle }} activate"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
ng-click="$select.activate()"
|
||||||
|
style="outline: 0;">
|
||||||
|
<span ng-show="$select.isEmpty()" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span>
|
||||||
|
<span ng-hide="$select.isEmpty()" class="ui-select-match-text pull-left" ng-class="{'ui-select-allow-clear': $select.allowClear && !$select.isEmpty()}" ng-transclude=""></span>
|
||||||
|
<i class="caret pull-right" ng-click="$select.toggle($event)"></i>
|
||||||
|
<a ng-show="$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)" aria-label="{{ $select.baseTitle }} clear" style="margin-right: 10px"
|
||||||
|
ng-click="$select.clear($event)" class="btn btn-xs btn-link pull-right">
|
||||||
|
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<ul class="ui-select-no-choice dropdown-menu"
|
||||||
|
ng-show="$select.items.length == 0">
|
||||||
|
<li ng-transclude>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="ui-select-container ui-select-multiple ui-select-bootstrap dropdown form-control" ng-class="{open: $select.open}">
|
||||||
|
<div>
|
||||||
|
<div class="ui-select-match"></div>
|
||||||
|
<input type="search"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
class="ui-select-search input-xs"
|
||||||
|
placeholder="{{$selectMultiple.getPlaceholder()}}"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
ng-click="$select.activate()"
|
||||||
|
ng-model="$select.search"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="{{$select.open}}"
|
||||||
|
aria-label="{{$select.baseTitle}}"
|
||||||
|
ng-class="{'spinner': $select.refreshing}"
|
||||||
|
ondrop="return false;">
|
||||||
|
</div>
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
<div class="ui-select-no-choice"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="ui-select-container ui-select-bootstrap dropdown" ng-class="{open: $select.open}">
|
||||||
|
<div class="ui-select-match"></div>
|
||||||
|
<span ng-show="$select.open && $select.refreshing && $select.spinnerEnabled" class="ui-select-refreshing {{$select.spinnerClass}}"></span>
|
||||||
|
<input type="search" autocomplete="off" tabindex="-1"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-label="{{ $select.baseTitle }}"
|
||||||
|
aria-owns="ui-select-choices-{{ $select.generatedId }}"
|
||||||
|
class="form-control ui-select-search"
|
||||||
|
ng-class="{ 'ui-select-search-hidden' : !$select.searchEnabled }"
|
||||||
|
placeholder="{{$select.placeholder}}"
|
||||||
|
ng-model="$select.search"
|
||||||
|
ng-show="$select.open">
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
<div class="ui-select-no-choice"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,354 @@
|
||||||
|
/* Style when highlighting a search. */
|
||||||
|
.ui-select-highlight {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-offscreen {
|
||||||
|
clip: rect(0 0 0 0) !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
border: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
position: absolute !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
left: 0px !important;
|
||||||
|
top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ui-select-choices-row:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select2 theme */
|
||||||
|
|
||||||
|
/* Mark invalid Select2 */
|
||||||
|
.ng-dirty.ng-invalid > a.select2-choice {
|
||||||
|
border-color: #D44950;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-result-single {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-locked > .select2-search-choice-close{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-locked > .ui-select-match-close{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > .select2-container.open {
|
||||||
|
z-index: 9999; /* The z-index Select2 applies to the select2-drop */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle up direction Select2 */
|
||||||
|
.ui-select-container[theme="select2"].direction-up .ui-select-match,
|
||||||
|
.ui-select-container.select2.direction-up .ui-select-match {
|
||||||
|
border-radius: 4px; /* FIXME hardcoded value :-/ */
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown,
|
||||||
|
.ui-select-container.select2.direction-up .ui-select-dropdown {
|
||||||
|
border-radius: 4px; /* FIXME hardcoded value :-/ */
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
|
border-top-width: 1px; /* FIXME hardcoded value :-/ */
|
||||||
|
border-top-style: solid;
|
||||||
|
|
||||||
|
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
margin-top: -4px; /* FIXME hardcoded value :-/ */
|
||||||
|
}
|
||||||
|
.ui-select-container[theme="select2"].direction-up .ui-select-dropdown .select2-search,
|
||||||
|
.ui-select-container.select2.direction-up .ui-select-dropdown .select2-search {
|
||||||
|
margin-top: 4px; /* FIXME hardcoded value :-/ */
|
||||||
|
}
|
||||||
|
.ui-select-container[theme="select2"].direction-up.select2-dropdown-open .ui-select-match,
|
||||||
|
.ui-select-container.select2.direction-up.select2-dropdown-open .ui-select-match {
|
||||||
|
border-bottom-color: #5897fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden,
|
||||||
|
.ui-select-container[theme="select2"] .ui-select-dropdown .ui-select-search-hidden input{
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selectize theme */
|
||||||
|
|
||||||
|
/* Helper class to show styles when focus */
|
||||||
|
.selectize-input.selectize-focus{
|
||||||
|
border-color: #007FBB !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix input width for Selectize theme */
|
||||||
|
.selectize-control.single > .selectize-input > input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix line break when there's at least one item selected with the Selectize theme */
|
||||||
|
.selectize-control.multi > .selectize-input > input {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix dropdown width for Selectize theme */
|
||||||
|
.selectize-control > .selectize-dropdown {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mark invalid Selectize */
|
||||||
|
.ng-dirty.ng-invalid > div.selectize-input {
|
||||||
|
border-color: #D44950;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle up direction Selectize */
|
||||||
|
.ui-select-container[theme="selectize"].direction-up .ui-select-dropdown {
|
||||||
|
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
margin-top: -2px; /* FIXME hardcoded value :-/ */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-container[theme="selectize"] input.ui-select-search-hidden{
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border:0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bootstrap theme */
|
||||||
|
|
||||||
|
/* Helper class to show styles when focus */
|
||||||
|
.btn-default-focus {
|
||||||
|
color: #333;
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
border-color: #ADADAD;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 5px auto -webkit-focus-ring-color;
|
||||||
|
outline-offset: -2px;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-toggle {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-toggle > .caret {
|
||||||
|
position: absolute;
|
||||||
|
height: 10px;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix Bootstrap dropdown position when inside a input-group */
|
||||||
|
.input-group > .ui-select-bootstrap.dropdown {
|
||||||
|
/* Instead of relative */
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control {
|
||||||
|
border-radius: 4px; /* FIXME hardcoded value :-/ */
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up {
|
||||||
|
border-radius: 4px !important; /* FIXME hardcoded value :-/ */
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-search-hidden{
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap > .ui-select-match > .btn{
|
||||||
|
/* Instead of center because of .btn */
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap > .ui-select-match > .caret {
|
||||||
|
position: absolute;
|
||||||
|
top: 45%;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */
|
||||||
|
.ui-select-bootstrap > .ui-select-choices ,.ui-select-bootstrap > .ui-select-no-choice {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > .ui-select-bootstrap.open {
|
||||||
|
z-index: 1000; /* Standard Bootstrap dropdown z-index */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple.ui-select-bootstrap {
|
||||||
|
height: auto;
|
||||||
|
padding: 3px 3px 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple.ui-select-bootstrap input.ui-select-search {
|
||||||
|
background-color: transparent !important; /* To prevent double background when disabled */
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
height: 1.666666em;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple.ui-select-bootstrap .ui-select-match .close {
|
||||||
|
font-size: 1.6em;
|
||||||
|
line-height: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple.ui-select-bootstrap .ui-select-match-item {
|
||||||
|
outline: 0;
|
||||||
|
margin: 0 3px 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple .ui-select-match-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple .ui-select-match-item.dropping .ui-select-match-close {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple:hover .ui-select-match-item.dropping-before:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 2px;
|
||||||
|
border-left: 1px solid #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-multiple:hover .ui-select-match-item.dropping-after:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 2px;
|
||||||
|
border-right: 1px solid #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-choices-row>span {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: 3px 20px;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #333;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-choices-row>span:hover, .ui-select-bootstrap .ui-select-choices-row>span:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #262626;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-choices-row.active>span {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 0;
|
||||||
|
background-color: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-choices-row.disabled>span,
|
||||||
|
.ui-select-bootstrap .ui-select-choices-row.active.disabled>span {
|
||||||
|
color: #777;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fix hide/show angular animation */
|
||||||
|
.ui-select-match.ng-hide-add,
|
||||||
|
.ui-select-search.ng-hide-add {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mark invalid Bootstrap */
|
||||||
|
.ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match {
|
||||||
|
border-color: #D44950;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle up direction Bootstrap */
|
||||||
|
.ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown {
|
||||||
|
box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-bootstrap .ui-select-match-text {
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
.ui-select-bootstrap .ui-select-match-text span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ui-select-bootstrap .ui-select-toggle > a.btn {
|
||||||
|
position: absolute;
|
||||||
|
height: 10px;
|
||||||
|
right: 10px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.ui-select-refreshing.glyphicon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
padding: 8px 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes ui-select-spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(359deg);
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes ui-select-spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(359deg);
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-spin {
|
||||||
|
-webkit-animation: ui-select-spin 2s infinite linear;
|
||||||
|
animation: ui-select-spin 2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-refreshing.ng-animate {
|
||||||
|
-webkit-animation: none 0s;
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
var KEY = {
|
||||||
|
TAB: 9,
|
||||||
|
ENTER: 13,
|
||||||
|
ESC: 27,
|
||||||
|
SPACE: 32,
|
||||||
|
LEFT: 37,
|
||||||
|
UP: 38,
|
||||||
|
RIGHT: 39,
|
||||||
|
DOWN: 40,
|
||||||
|
SHIFT: 16,
|
||||||
|
CTRL: 17,
|
||||||
|
ALT: 18,
|
||||||
|
PAGE_UP: 33,
|
||||||
|
PAGE_DOWN: 34,
|
||||||
|
HOME: 36,
|
||||||
|
END: 35,
|
||||||
|
BACKSPACE: 8,
|
||||||
|
DELETE: 46,
|
||||||
|
COMMAND: 91,
|
||||||
|
|
||||||
|
MAP: { 91 : "COMMAND", 8 : "BACKSPACE" , 9 : "TAB" , 13 : "ENTER" , 16 : "SHIFT" , 17 : "CTRL" , 18 : "ALT" , 19 : "PAUSEBREAK" , 20 : "CAPSLOCK" , 27 : "ESC" , 32 : "SPACE" , 33 : "PAGE_UP", 34 : "PAGE_DOWN" , 35 : "END" , 36 : "HOME" , 37 : "LEFT" , 38 : "UP" , 39 : "RIGHT" , 40 : "DOWN" , 43 : "+" , 44 : "PRINTSCREEN" , 45 : "INSERT" , 46 : "DELETE", 48 : "0" , 49 : "1" , 50 : "2" , 51 : "3" , 52 : "4" , 53 : "5" , 54 : "6" , 55 : "7" , 56 : "8" , 57 : "9" , 59 : ";", 61 : "=" , 65 : "A" , 66 : "B" , 67 : "C" , 68 : "D" , 69 : "E" , 70 : "F" , 71 : "G" , 72 : "H" , 73 : "I" , 74 : "J" , 75 : "K" , 76 : "L", 77 : "M" , 78 : "N" , 79 : "O" , 80 : "P" , 81 : "Q" , 82 : "R" , 83 : "S" , 84 : "T" , 85 : "U" , 86 : "V" , 87 : "W" , 88 : "X" , 89 : "Y" , 90 : "Z", 96 : "0" , 97 : "1" , 98 : "2" , 99 : "3" , 100 : "4" , 101 : "5" , 102 : "6" , 103 : "7" , 104 : "8" , 105 : "9", 106 : "*" , 107 : "+" , 109 : "-" , 110 : "." , 111 : "/", 112 : "F1" , 113 : "F2" , 114 : "F3" , 115 : "F4" , 116 : "F5" , 117 : "F6" , 118 : "F7" , 119 : "F8" , 120 : "F9" , 121 : "F10" , 122 : "F11" , 123 : "F12", 144 : "NUMLOCK" , 145 : "SCROLLLOCK" , 186 : ";" , 187 : "=" , 188 : "," , 189 : "-" , 190 : "." , 191 : "/" , 192 : "`" , 219 : "[" , 220 : "\\" , 221 : "]" , 222 : "'"
|
||||||
|
},
|
||||||
|
|
||||||
|
isControl: function (e) {
|
||||||
|
var k = e.which;
|
||||||
|
switch (k) {
|
||||||
|
case KEY.COMMAND:
|
||||||
|
case KEY.SHIFT:
|
||||||
|
case KEY.CTRL:
|
||||||
|
case KEY.ALT:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.metaKey || e.ctrlKey || e.altKey) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isFunctionKey: function (k) {
|
||||||
|
k = k.which ? k.which : k;
|
||||||
|
return k >= 112 && k <= 123;
|
||||||
|
},
|
||||||
|
isVerticalMovement: function (k){
|
||||||
|
return ~[KEY.UP, KEY.DOWN].indexOf(k);
|
||||||
|
},
|
||||||
|
isHorizontalMovement: function (k){
|
||||||
|
return ~[KEY.LEFT,KEY.RIGHT,KEY.BACKSPACE,KEY.DELETE].indexOf(k);
|
||||||
|
},
|
||||||
|
toSeparator: function (k) {
|
||||||
|
var sep = {ENTER:"\n",TAB:"\t",SPACE:" "}[k];
|
||||||
|
if (sep) return sep;
|
||||||
|
// return undefined for special keys other than enter, tab or space.
|
||||||
|
// no way to use them to cut strings.
|
||||||
|
return KEY[k] ? undefined : k;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function isNil(value) {
|
||||||
|
return angular.isUndefined(value) || value === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add querySelectorAll() to jqLite.
|
||||||
|
*
|
||||||
|
* jqLite find() is limited to lookups by tag name.
|
||||||
|
* TODO This will change with future versions of AngularJS, to be removed when this happens
|
||||||
|
*
|
||||||
|
* See jqLite.find - why not use querySelectorAll? https://github.com/angular/angular.js/issues/3586
|
||||||
|
* See feat(jqLite): use querySelectorAll instead of getElementsByTagName in jqLite.find https://github.com/angular/angular.js/pull/3598
|
||||||
|
*/
|
||||||
|
if (angular.element.prototype.querySelectorAll === undefined) {
|
||||||
|
angular.element.prototype.querySelectorAll = function(selector) {
|
||||||
|
return angular.element(this[0].querySelectorAll(selector));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add closest() to jqLite.
|
||||||
|
*/
|
||||||
|
if (angular.element.prototype.closest === undefined) {
|
||||||
|
angular.element.prototype.closest = function( selector) {
|
||||||
|
var elem = this[0];
|
||||||
|
var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
|
||||||
|
|
||||||
|
while (elem) {
|
||||||
|
if (matchesSelector.bind(elem)(selector)) {
|
||||||
|
return elem;
|
||||||
|
} else {
|
||||||
|
elem = elem.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestId = 0;
|
||||||
|
|
||||||
|
var uis = angular.module('ui.select', [])
|
||||||
|
|
||||||
|
.constant('uiSelectConfig', {
|
||||||
|
theme: 'bootstrap',
|
||||||
|
searchEnabled: true,
|
||||||
|
sortable: false,
|
||||||
|
placeholder: '', // Empty by default, like HTML tag <select>
|
||||||
|
refreshDelay: 1000, // In milliseconds
|
||||||
|
closeOnSelect: true,
|
||||||
|
skipFocusser: false,
|
||||||
|
dropdownPosition: 'auto',
|
||||||
|
removeSelected: true,
|
||||||
|
resetSearchInput: true,
|
||||||
|
generateId: function() {
|
||||||
|
return latestId++;
|
||||||
|
},
|
||||||
|
appendToBody: false,
|
||||||
|
spinnerEnabled: false,
|
||||||
|
spinnerClass: 'glyphicon glyphicon-refresh ui-select-spin',
|
||||||
|
backspaceReset: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913
|
||||||
|
.service('uiSelectMinErr', function() {
|
||||||
|
var minErr = angular.$$minErr('ui.select');
|
||||||
|
return function() {
|
||||||
|
var error = minErr.apply(this, arguments);
|
||||||
|
var message = error.message.replace(new RegExp('\nhttp://errors.angularjs.org/.*'), '');
|
||||||
|
return new Error(message);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
// Recreates old behavior of ng-transclude. Used internally.
|
||||||
|
.directive('uisTranscludeAppend', function () {
|
||||||
|
return {
|
||||||
|
link: function (scope, element, attrs, ctrl, transclude) {
|
||||||
|
transclude(scope, function (clone) {
|
||||||
|
element.append(clone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights text that matches $select.search.
|
||||||
|
*
|
||||||
|
* Taken from AngularUI Bootstrap Typeahead
|
||||||
|
* See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L340
|
||||||
|
*/
|
||||||
|
.filter('highlight', function() {
|
||||||
|
function escapeRegexp(queryToEscape) {
|
||||||
|
return ('' + queryToEscape).replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(matchItem, query) {
|
||||||
|
return query && matchItem ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A read-only equivalent of jQuery's offset function: http://api.jquery.com/offset/
|
||||||
|
*
|
||||||
|
* Taken from AngularUI Bootstrap Position:
|
||||||
|
* See https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js#L70
|
||||||
|
*/
|
||||||
|
.factory('uisOffset',
|
||||||
|
['$document', '$window',
|
||||||
|
function ($document, $window) {
|
||||||
|
|
||||||
|
return function(element) {
|
||||||
|
var boundingClientRect = element[0].getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
width: boundingClientRect.width || element.prop('offsetWidth'),
|
||||||
|
height: boundingClientRect.height || element.prop('offsetHeight'),
|
||||||
|
top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
|
||||||
|
left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,11 @@
|
||||||
|
<ul tabindex="-1" class="ui-select-choices ui-select-choices-content select2-results">
|
||||||
|
<li class="ui-select-choices-group" ng-class="{'select2-result-with-children': $select.choiceGrouped($group) }">
|
||||||
|
<div ng-show="$select.choiceGrouped($group)" class="ui-select-choices-group-label select2-result-label" ng-bind="$group.name"></div>
|
||||||
|
<ul
|
||||||
|
id="ui-select-choices-{{ $select.generatedId }}" ng-class="{'select2-result-sub': $select.choiceGrouped($group), 'select2-result-single': !$select.choiceGrouped($group) }">
|
||||||
|
<li role="option" ng-attr-id="ui-select-choices-row-{{ $select.generatedId }}-{{$index}}" class="ui-select-choices-row" ng-class="{'select2-highlighted': $select.isActive(this), 'select2-disabled': $select.isDisabled(this)}">
|
||||||
|
<div class="select2-result-label ui-select-choices-row-inner"></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!--
|
||||||
|
select2-choice needs to be before ui-select-multiple
|
||||||
|
otherwise CSS rules from https://github.com/fk/select2-bootstrap-css
|
||||||
|
do not work: [class^="select2-choice"]
|
||||||
|
-->
|
||||||
|
<span class="ui-select-match">
|
||||||
|
<li class="ui-select-match-item select2-search-choice" ng-repeat="$item in $select.selected track by $index"
|
||||||
|
ng-class="{'select2-search-choice-focus':$selectMultiple.activeMatchIndex === $index, 'select2-locked':$select.isLocked(this, $index)}"
|
||||||
|
ui-select-sort="$select.selected">
|
||||||
|
<span uis-transclude-append></span>
|
||||||
|
<a href="javascript:;" class="ui-select-match-close select2-search-choice-close" ng-click="$selectMultiple.removeChoice($index)" tabindex="-1"></a>
|
||||||
|
</li>
|
||||||
|
</span>
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!--
|
||||||
|
select2-choice needs to be before ui-select-match
|
||||||
|
otherwise CSS rules from https://github.com/fk/select2-bootstrap-css
|
||||||
|
do not work: [class^="select2-choice"]
|
||||||
|
-->
|
||||||
|
<a class="select2-choice ui-select-match"
|
||||||
|
ng-class="{'select2-default': $select.isEmpty()}"
|
||||||
|
ng-click="$select.toggle($event)" aria-label="{{ $select.baseTitle }} select">
|
||||||
|
<span ng-show="$select.isEmpty()" class="select2-chosen">{{$select.placeholder}}</span>
|
||||||
|
<span ng-hide="$select.isEmpty()" class="select2-chosen" ng-transclude></span>
|
||||||
|
<abbr ng-if="$select.allowClear && !$select.isEmpty()" class="select2-search-choice-close" ng-click="$select.clear($event)"></abbr>
|
||||||
|
<span class="select2-arrow ui-select-toggle"><b></b></span>
|
||||||
|
</a>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="ui-select-no-choice dropdown"
|
||||||
|
ng-show="$select.items.length == 0">
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<div data-selectable="" ng-transclude></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<div class="ui-select-container ui-select-multiple select2 select2-container select2-container-multi"
|
||||||
|
ng-class="{'select2-container-active select2-dropdown-open open': $select.open,
|
||||||
|
'select2-container-disabled': $select.disabled}">
|
||||||
|
<ul class="select2-choices">
|
||||||
|
<span class="ui-select-match"></span>
|
||||||
|
<li class="select2-search-field">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
spellcheck="false"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-owns="ui-select-choices-{{ $select.generatedId }}"
|
||||||
|
aria-label="{{ $select.baseTitle }}"
|
||||||
|
aria-activedescendant="ui-select-choices-row-{{ $select.generatedId }}-{{ $select.activeIndex }}"
|
||||||
|
class="select2-input ui-select-search"
|
||||||
|
placeholder="{{$selectMultiple.getPlaceholder()}}"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
ng-hide="$select.disabled"
|
||||||
|
ng-model="$select.search"
|
||||||
|
ng-click="$select.activate()"
|
||||||
|
style="width: 34px;"
|
||||||
|
ondrop="return false;">
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="ui-select-dropdown select2-drop select2-with-searchbox select2-drop-active"
|
||||||
|
ng-class="{'select2-display-none': !$select.open || $select.items.length === 0}">
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="ui-select-container select2 select2-container"
|
||||||
|
ng-class="{'select2-container-active select2-dropdown-open open': $select.open,
|
||||||
|
'select2-container-disabled': $select.disabled,
|
||||||
|
'select2-container-active': $select.focus,
|
||||||
|
'select2-allowclear': $select.allowClear && !$select.isEmpty()}">
|
||||||
|
<div class="ui-select-match"></div>
|
||||||
|
<div class="ui-select-dropdown select2-drop select2-with-searchbox select2-drop-active"
|
||||||
|
ng-class="{'select2-display-none': !$select.open}">
|
||||||
|
<div class="search-container" ng-class="{'ui-select-search-hidden':!$select.searchEnabled, 'select2-search':$select.searchEnabled}">
|
||||||
|
<input type="search" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||||
|
ng-class="{'select2-active': $select.refreshing}"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-owns="ui-select-choices-{{ $select.generatedId }}"
|
||||||
|
aria-label="{{ $select.baseTitle }}"
|
||||||
|
class="ui-select-search select2-input"
|
||||||
|
ng-model="$select.search">
|
||||||
|
</div>
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
<div class="ui-select-no-choice"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div ng-show="$select.open" class="ui-select-choices ui-select-dropdown selectize-dropdown"
|
||||||
|
ng-class="{'single': !$select.multiple, 'multi': $select.multiple}">
|
||||||
|
<div class="ui-select-choices-content selectize-dropdown-content">
|
||||||
|
<div class="ui-select-choices-group optgroup">
|
||||||
|
<div ng-show="$select.isGrouped" class="ui-select-choices-group-label optgroup-header" ng-bind="$group.name"></div>
|
||||||
|
<div role="option" class="ui-select-choices-row" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}">
|
||||||
|
<div class="option ui-select-choices-row-inner" data-selectable></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="ui-select-match" data-value
|
||||||
|
ng-repeat="$item in $select.selected track by $index"
|
||||||
|
ng-click="$selectMultiple.activeMatchIndex = $index;"
|
||||||
|
ng-class="{'active':$selectMultiple.activeMatchIndex === $index}"
|
||||||
|
ui-select-sort="$select.selected">
|
||||||
|
<span class="ui-select-match-item"
|
||||||
|
ng-class="{'select-locked':$select.isLocked(this, $index)}">
|
||||||
|
<span uis-transclude-append></span>
|
||||||
|
<span class="remove ui-select-match-close" ng-hide="$select.disabled" ng-click="$selectMultiple.removeChoice($index)">×</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div ng-hide="$select.searchEnabled && ($select.open || $select.isEmpty())" class="ui-select-match">
|
||||||
|
<span ng-show="!$select.searchEnabled && ($select.isEmpty() || $select.open)" class="ui-select-placeholder text-muted">{{$select.placeholder}}</span>
|
||||||
|
<span ng-hide="$select.isEmpty() || $select.open" ng-transclude></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="ui-select-no-choice selectize-dropdown"
|
||||||
|
ng-show="$select.items.length == 0">
|
||||||
|
<div class="selectize-dropdown-content">
|
||||||
|
<div data-selectable="" ng-transclude></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<div class="ui-select-container selectize-control multi plugin-remove_button" ng-class="{'open': $select.open}">
|
||||||
|
<div class="selectize-input"
|
||||||
|
ng-class="{'focus': $select.open, 'disabled': $select.disabled, 'selectize-focus' : $select.focus}"
|
||||||
|
ng-click="$select.open && !$select.searchEnabled ? $select.toggle($event) : $select.activate()">
|
||||||
|
<div class="ui-select-match"></div>
|
||||||
|
<input type="search" autocomplete="off" tabindex="-1"
|
||||||
|
class="ui-select-search"
|
||||||
|
ng-class="{'ui-select-search-hidden':!$select.searchEnabled}"
|
||||||
|
placeholder="{{$selectMultiple.getPlaceholder()}}"
|
||||||
|
ng-model="$select.search"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
aria-expanded="{{$select.open}}"
|
||||||
|
aria-label="{{ $select.baseTitle }}"
|
||||||
|
ondrop="return false;">
|
||||||
|
</div>
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
<div class="ui-select-no-choice"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<div class="ui-select-container selectize-control single" ng-class="{'open': $select.open}">
|
||||||
|
<div class="selectize-input"
|
||||||
|
ng-class="{'focus': $select.open, 'disabled': $select.disabled, 'selectize-focus' : $select.focus}"
|
||||||
|
ng-click="$select.open && !$select.searchEnabled ? $select.toggle($event) : $select.activate()">
|
||||||
|
<div class="ui-select-match"></div>
|
||||||
|
<input type="search" autocomplete="off" tabindex="-1"
|
||||||
|
class="ui-select-search ui-select-toggle"
|
||||||
|
ng-class="{'ui-select-search-hidden':!$select.searchEnabled}"
|
||||||
|
ng-click="$select.toggle($event)"
|
||||||
|
placeholder="{{$select.placeholder}}"
|
||||||
|
ng-model="$select.search"
|
||||||
|
ng-hide="!$select.isEmpty() && !$select.open"
|
||||||
|
ng-disabled="$select.disabled"
|
||||||
|
aria-label="{{ $select.baseTitle }}">
|
||||||
|
</div>
|
||||||
|
<div class="ui-select-choices"></div>
|
||||||
|
<div class="ui-select-no-choice"></div>
|
||||||
|
</div>
|
|
@ -0,0 +1,90 @@
|
||||||
|
uis.directive('uiSelectChoices',
|
||||||
|
['uiSelectConfig', 'uisRepeatParser', 'uiSelectMinErr', '$compile', '$window',
|
||||||
|
function(uiSelectConfig, RepeatParser, uiSelectMinErr, $compile, $window) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
require: '^uiSelect',
|
||||||
|
replace: true,
|
||||||
|
transclude: true,
|
||||||
|
templateUrl: function(tElement) {
|
||||||
|
// Needed so the uiSelect can detect the transcluded content
|
||||||
|
tElement.addClass('ui-select-choices');
|
||||||
|
|
||||||
|
// Gets theme attribute from parent (ui-select)
|
||||||
|
var theme = tElement.parent().attr('theme') || uiSelectConfig.theme;
|
||||||
|
return theme + '/choices.tpl.html';
|
||||||
|
},
|
||||||
|
|
||||||
|
compile: function(tElement, tAttrs) {
|
||||||
|
|
||||||
|
if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
|
||||||
|
|
||||||
|
// var repeat = RepeatParser.parse(attrs.repeat);
|
||||||
|
var groupByExp = tAttrs.groupBy;
|
||||||
|
var groupFilterExp = tAttrs.groupFilter;
|
||||||
|
|
||||||
|
if (groupByExp) {
|
||||||
|
var groups = tElement.querySelectorAll('.ui-select-choices-group');
|
||||||
|
if (groups.length !== 1) throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-group but got '{0}'.", groups.length);
|
||||||
|
groups.attr('ng-repeat', RepeatParser.getGroupNgRepeatExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
var parserResult = RepeatParser.parse(tAttrs.repeat);
|
||||||
|
|
||||||
|
var choices = tElement.querySelectorAll('.ui-select-choices-row');
|
||||||
|
if (choices.length !== 1) {
|
||||||
|
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
choices.attr('ng-repeat', parserResult.repeatExpression(groupByExp))
|
||||||
|
.attr('ng-if', '$select.open'); //Prevent unnecessary watches when dropdown is closed
|
||||||
|
|
||||||
|
|
||||||
|
var rowsInner = tElement.querySelectorAll('.ui-select-choices-row-inner');
|
||||||
|
if (rowsInner.length !== 1) {
|
||||||
|
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row-inner but got '{0}'.", rowsInner.length);
|
||||||
|
}
|
||||||
|
rowsInner.attr('uis-transclude-append', ''); //Adding uisTranscludeAppend directive to row element after choices element has ngRepeat
|
||||||
|
|
||||||
|
// If IE8 then need to target rowsInner to apply the ng-click attr as choices will not capture the event.
|
||||||
|
var clickTarget = $window.document.addEventListener ? choices : rowsInner;
|
||||||
|
clickTarget.attr('ng-click', '$select.select(' + parserResult.itemName + ',$select.skipFocusser,$event)');
|
||||||
|
|
||||||
|
return function link(scope, element, attrs, $select) {
|
||||||
|
|
||||||
|
|
||||||
|
$select.parseRepeatAttr(attrs.repeat, groupByExp, groupFilterExp); //Result ready at $select.parserResult
|
||||||
|
$select.disableChoiceExpression = attrs.uiDisableChoice;
|
||||||
|
$select.onHighlightCallback = attrs.onHighlight;
|
||||||
|
$select.minimumInputLength = parseInt(attrs.minimumInputLength) || 0;
|
||||||
|
$select.dropdownPosition = attrs.position ? attrs.position.toLowerCase() : uiSelectConfig.dropdownPosition;
|
||||||
|
|
||||||
|
scope.$watch('$select.search', function(newValue) {
|
||||||
|
if(newValue && !$select.open && $select.multiple) $select.activate(false, true);
|
||||||
|
$select.activeIndex = $select.tagging.isActivated ? -1 : 0;
|
||||||
|
if (!attrs.minimumInputLength || $select.search.length >= attrs.minimumInputLength) {
|
||||||
|
$select.refresh(attrs.refresh);
|
||||||
|
} else {
|
||||||
|
$select.items = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('refreshDelay', function() {
|
||||||
|
// $eval() is needed otherwise we get a string instead of a number
|
||||||
|
var refreshDelay = scope.$eval(attrs.refreshDelay);
|
||||||
|
$select.refreshDelay = refreshDelay !== undefined ? refreshDelay : uiSelectConfig.refreshDelay;
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watch('$select.open', function(open) {
|
||||||
|
if (open) {
|
||||||
|
tElement.attr('role', 'listbox');
|
||||||
|
$select.refresh(attrs.refresh);
|
||||||
|
} else {
|
||||||
|
element.removeAttr('role');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,756 @@
|
||||||
|
/**
|
||||||
|
* Contains ui-select "intelligence".
|
||||||
|
*
|
||||||
|
* The goal is to limit dependency on the DOM whenever possible and
|
||||||
|
* put as much logic in the controller (instead of the link functions) as possible so it can be easily tested.
|
||||||
|
*/
|
||||||
|
uis.controller('uiSelectCtrl',
|
||||||
|
['$scope', '$element', '$timeout', '$filter', '$$uisDebounce', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig', '$parse', '$injector', '$window',
|
||||||
|
function($scope, $element, $timeout, $filter, $$uisDebounce, RepeatParser, uiSelectMinErr, uiSelectConfig, $parse, $injector, $window) {
|
||||||
|
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
var EMPTY_SEARCH = '';
|
||||||
|
|
||||||
|
ctrl.placeholder = uiSelectConfig.placeholder;
|
||||||
|
ctrl.searchEnabled = uiSelectConfig.searchEnabled;
|
||||||
|
ctrl.sortable = uiSelectConfig.sortable;
|
||||||
|
ctrl.refreshDelay = uiSelectConfig.refreshDelay;
|
||||||
|
ctrl.paste = uiSelectConfig.paste;
|
||||||
|
ctrl.resetSearchInput = uiSelectConfig.resetSearchInput;
|
||||||
|
ctrl.refreshing = false;
|
||||||
|
ctrl.spinnerEnabled = uiSelectConfig.spinnerEnabled;
|
||||||
|
ctrl.spinnerClass = uiSelectConfig.spinnerClass;
|
||||||
|
ctrl.removeSelected = uiSelectConfig.removeSelected; //If selected item(s) should be removed from dropdown list
|
||||||
|
ctrl.closeOnSelect = true; //Initialized inside uiSelect directive link function
|
||||||
|
ctrl.skipFocusser = false; //Set to true to avoid returning focus to ctrl when item is selected
|
||||||
|
ctrl.search = EMPTY_SEARCH;
|
||||||
|
|
||||||
|
ctrl.activeIndex = 0; //Dropdown of choices
|
||||||
|
ctrl.items = []; //All available choices
|
||||||
|
|
||||||
|
ctrl.open = false;
|
||||||
|
ctrl.focus = false;
|
||||||
|
ctrl.disabled = false;
|
||||||
|
ctrl.selected = undefined;
|
||||||
|
|
||||||
|
ctrl.dropdownPosition = 'auto';
|
||||||
|
|
||||||
|
ctrl.focusser = undefined; //Reference to input element used to handle focus events
|
||||||
|
ctrl.multiple = undefined; // Initialized inside uiSelect directive link function
|
||||||
|
ctrl.disableChoiceExpression = undefined; // Initialized inside uiSelectChoices directive link function
|
||||||
|
ctrl.tagging = {isActivated: false, fct: undefined};
|
||||||
|
ctrl.taggingTokens = {isActivated: false, tokens: undefined};
|
||||||
|
ctrl.lockChoiceExpression = undefined; // Initialized inside uiSelectMatch directive link function
|
||||||
|
ctrl.clickTriggeredSelect = false;
|
||||||
|
ctrl.$filter = $filter;
|
||||||
|
ctrl.$element = $element;
|
||||||
|
|
||||||
|
// Use $injector to check for $animate and store a reference to it
|
||||||
|
ctrl.$animate = (function () {
|
||||||
|
try {
|
||||||
|
return $injector.get('$animate');
|
||||||
|
} catch (err) {
|
||||||
|
// $animate does not exist
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
ctrl.searchInput = $element.querySelectorAll('input.ui-select-search');
|
||||||
|
if (ctrl.searchInput.length !== 1) {
|
||||||
|
throw uiSelectMinErr('searchInput', "Expected 1 input.ui-select-search but got '{0}'.", ctrl.searchInput.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.isEmpty = function() {
|
||||||
|
return isNil(ctrl.selected) || ctrl.selected === '' || (ctrl.multiple && ctrl.selected.length === 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
function _findIndex(collection, predicate, thisArg){
|
||||||
|
if (collection.findIndex){
|
||||||
|
return collection.findIndex(predicate, thisArg);
|
||||||
|
} else {
|
||||||
|
var list = Object(collection);
|
||||||
|
var length = list.length >>> 0;
|
||||||
|
var value;
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
value = list[i];
|
||||||
|
if (predicate.call(thisArg, value, i, list)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most of the time the user does not want to empty the search input when in typeahead mode
|
||||||
|
function _resetSearchInput() {
|
||||||
|
if (ctrl.resetSearchInput) {
|
||||||
|
ctrl.search = EMPTY_SEARCH;
|
||||||
|
//reset activeIndex
|
||||||
|
if (ctrl.selected && ctrl.items.length && !ctrl.multiple) {
|
||||||
|
ctrl.activeIndex = _findIndex(ctrl.items, function(item){
|
||||||
|
return angular.equals(this, item);
|
||||||
|
}, ctrl.selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _groupsFilter(groups, groupNames) {
|
||||||
|
var i, j, result = [];
|
||||||
|
for(i = 0; i < groupNames.length ;i++){
|
||||||
|
for(j = 0; j < groups.length ;j++){
|
||||||
|
if(groups[j].name == [groupNames[i]]){
|
||||||
|
result.push(groups[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the user clicks on ui-select, displays the dropdown list
|
||||||
|
ctrl.activate = function(initSearchValue, avoidReset) {
|
||||||
|
if (!ctrl.disabled && !ctrl.open) {
|
||||||
|
if(!avoidReset) _resetSearchInput();
|
||||||
|
|
||||||
|
$scope.$broadcast('uis:activate');
|
||||||
|
ctrl.open = true;
|
||||||
|
ctrl.activeIndex = ctrl.activeIndex >= ctrl.items.length ? 0 : ctrl.activeIndex;
|
||||||
|
// ensure that the index is set to zero for tagging variants
|
||||||
|
// that where first option is auto-selected
|
||||||
|
if ( ctrl.activeIndex === -1 && ctrl.taggingLabel !== false ) {
|
||||||
|
ctrl.activeIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = $element.querySelectorAll('.ui-select-choices-content');
|
||||||
|
var searchInput = $element.querySelectorAll('.ui-select-search');
|
||||||
|
if (ctrl.$animate && ctrl.$animate.on && ctrl.$animate.enabled(container[0])) {
|
||||||
|
var animateHandler = function(elem, phase) {
|
||||||
|
if (phase === 'start' && ctrl.items.length === 0) {
|
||||||
|
// Only focus input after the animation has finished
|
||||||
|
ctrl.$animate.off('removeClass', searchInput[0], animateHandler);
|
||||||
|
$timeout(function () {
|
||||||
|
ctrl.focusSearchInput(initSearchValue);
|
||||||
|
});
|
||||||
|
} else if (phase === 'close') {
|
||||||
|
// Only focus input after the animation has finished
|
||||||
|
ctrl.$animate.off('enter', container[0], animateHandler);
|
||||||
|
$timeout(function () {
|
||||||
|
ctrl.focusSearchInput(initSearchValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ctrl.items.length > 0) {
|
||||||
|
ctrl.$animate.on('enter', container[0], animateHandler);
|
||||||
|
} else {
|
||||||
|
ctrl.$animate.on('removeClass', searchInput[0], animateHandler);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$timeout(function () {
|
||||||
|
ctrl.focusSearchInput(initSearchValue);
|
||||||
|
if(!ctrl.tagging.isActivated && ctrl.items.length > 1) {
|
||||||
|
_ensureHighlightVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ctrl.open && !ctrl.searchEnabled) {
|
||||||
|
// Close the selection if we don't have search enabled, and we click on the select again
|
||||||
|
ctrl.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.focusSearchInput = function (initSearchValue) {
|
||||||
|
ctrl.search = initSearchValue || ctrl.search;
|
||||||
|
ctrl.searchInput[0].focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.findGroupByName = function(name) {
|
||||||
|
return ctrl.groups && ctrl.groups.filter(function(group) {
|
||||||
|
return group.name === name;
|
||||||
|
})[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.parseRepeatAttr = function(repeatAttr, groupByExp, groupFilterExp) {
|
||||||
|
function updateGroups(items) {
|
||||||
|
var groupFn = $scope.$eval(groupByExp);
|
||||||
|
ctrl.groups = [];
|
||||||
|
angular.forEach(items, function(item) {
|
||||||
|
var groupName = angular.isFunction(groupFn) ? groupFn(item) : item[groupFn];
|
||||||
|
var group = ctrl.findGroupByName(groupName);
|
||||||
|
if(group) {
|
||||||
|
group.items.push(item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ctrl.groups.push({name: groupName, items: [item]});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(groupFilterExp){
|
||||||
|
var groupFilterFn = $scope.$eval(groupFilterExp);
|
||||||
|
if( angular.isFunction(groupFilterFn)){
|
||||||
|
ctrl.groups = groupFilterFn(ctrl.groups);
|
||||||
|
} else if(angular.isArray(groupFilterFn)){
|
||||||
|
ctrl.groups = _groupsFilter(ctrl.groups, groupFilterFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctrl.items = [];
|
||||||
|
ctrl.groups.forEach(function(group) {
|
||||||
|
ctrl.items = ctrl.items.concat(group.items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlainItems(items) {
|
||||||
|
ctrl.items = items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.setItemsFn = groupByExp ? updateGroups : setPlainItems;
|
||||||
|
|
||||||
|
ctrl.parserResult = RepeatParser.parse(repeatAttr);
|
||||||
|
|
||||||
|
ctrl.isGrouped = !!groupByExp;
|
||||||
|
ctrl.itemProperty = ctrl.parserResult.itemName;
|
||||||
|
|
||||||
|
//If collection is an Object, convert it to Array
|
||||||
|
|
||||||
|
var originalSource = ctrl.parserResult.source;
|
||||||
|
|
||||||
|
//When an object is used as source, we better create an array and use it as 'source'
|
||||||
|
var createArrayFromObject = function(){
|
||||||
|
var origSrc = originalSource($scope);
|
||||||
|
$scope.$uisSource = Object.keys(origSrc).map(function(v){
|
||||||
|
var result = {};
|
||||||
|
result[ctrl.parserResult.keyName] = v;
|
||||||
|
result.value = origSrc[v];
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ctrl.parserResult.keyName){ // Check for (key,value) syntax
|
||||||
|
createArrayFromObject();
|
||||||
|
ctrl.parserResult.source = $parse('$uisSource' + ctrl.parserResult.filters);
|
||||||
|
$scope.$watch(originalSource, function(newVal, oldVal){
|
||||||
|
if (newVal !== oldVal) createArrayFromObject();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.refreshItems = function (data){
|
||||||
|
data = data || ctrl.parserResult.source($scope);
|
||||||
|
var selectedItems = ctrl.selected;
|
||||||
|
//TODO should implement for single mode removeSelected
|
||||||
|
if (ctrl.isEmpty() || (angular.isArray(selectedItems) && !selectedItems.length) || !ctrl.multiple || !ctrl.removeSelected) {
|
||||||
|
ctrl.setItemsFn(data);
|
||||||
|
}else{
|
||||||
|
if ( data !== undefined && data !== null ) {
|
||||||
|
var filteredItems = data.filter(function(i) {
|
||||||
|
return angular.isArray(selectedItems) ? selectedItems.every(function(selectedItem) {
|
||||||
|
return !angular.equals(i, selectedItem);
|
||||||
|
}) : !angular.equals(i, selectedItems);
|
||||||
|
});
|
||||||
|
ctrl.setItemsFn(filteredItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctrl.dropdownPosition === 'auto' || ctrl.dropdownPosition === 'up'){
|
||||||
|
$scope.calculateDropdownPos();
|
||||||
|
}
|
||||||
|
$scope.$broadcast('uis:refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
|
||||||
|
$scope.$watchCollection(ctrl.parserResult.source, function(items) {
|
||||||
|
if (items === undefined || items === null) {
|
||||||
|
// If the user specifies undefined or null => reset the collection
|
||||||
|
// Special case: items can be undefined if the user did not initialized the collection on the scope
|
||||||
|
// i.e $scope.addresses = [] is missing
|
||||||
|
ctrl.items = [];
|
||||||
|
} else {
|
||||||
|
if (!angular.isArray(items)) {
|
||||||
|
throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items);
|
||||||
|
} else {
|
||||||
|
//Remove already selected items (ex: while searching)
|
||||||
|
//TODO Should add a test
|
||||||
|
ctrl.refreshItems(items);
|
||||||
|
|
||||||
|
//update the view value with fresh data from items, if there is a valid model value
|
||||||
|
if(angular.isDefined(ctrl.ngModel.$modelValue)) {
|
||||||
|
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var _refreshDelayPromise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typeahead mode: lets the user refresh the collection using his own function.
|
||||||
|
*
|
||||||
|
* See Expose $select.search for external / remote filtering https://github.com/angular-ui/ui-select/pull/31
|
||||||
|
*/
|
||||||
|
ctrl.refresh = function(refreshAttr) {
|
||||||
|
if (refreshAttr !== undefined) {
|
||||||
|
// Debounce
|
||||||
|
// See https://github.com/angular-ui/bootstrap/blob/0.10.0/src/typeahead/typeahead.js#L155
|
||||||
|
// FYI AngularStrap typeahead does not have debouncing: https://github.com/mgcrea/angular-strap/blob/v2.0.0-rc.4/src/typeahead/typeahead.js#L177
|
||||||
|
if (_refreshDelayPromise) {
|
||||||
|
$timeout.cancel(_refreshDelayPromise);
|
||||||
|
}
|
||||||
|
_refreshDelayPromise = $timeout(function() {
|
||||||
|
if ($scope.$select.search.length >= $scope.$select.minimumInputLength) {
|
||||||
|
var refreshPromise = $scope.$eval(refreshAttr);
|
||||||
|
if (refreshPromise && angular.isFunction(refreshPromise.then) && !ctrl.refreshing) {
|
||||||
|
ctrl.refreshing = true;
|
||||||
|
refreshPromise.finally(function() {
|
||||||
|
ctrl.refreshing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, ctrl.refreshDelay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.isActive = function(itemScope) {
|
||||||
|
if ( !ctrl.open ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var itemIndex = ctrl.items.indexOf(itemScope[ctrl.itemProperty]);
|
||||||
|
var isActive = itemIndex == ctrl.activeIndex;
|
||||||
|
|
||||||
|
if ( !isActive || itemIndex < 0 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive && !angular.isUndefined(ctrl.onHighlightCallback)) {
|
||||||
|
itemScope.$eval(ctrl.onHighlightCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isActive;
|
||||||
|
};
|
||||||
|
|
||||||
|
var _isItemSelected = function (item) {
|
||||||
|
return (ctrl.selected && angular.isArray(ctrl.selected) &&
|
||||||
|
ctrl.selected.filter(function (selection) { return angular.equals(selection, item); }).length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
var disabledItems = [];
|
||||||
|
|
||||||
|
function _updateItemDisabled(item, isDisabled) {
|
||||||
|
var disabledItemIndex = disabledItems.indexOf(item);
|
||||||
|
if (isDisabled && disabledItemIndex === -1) {
|
||||||
|
disabledItems.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDisabled && disabledItemIndex > -1) {
|
||||||
|
disabledItems.splice(disabledItemIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isItemDisabled(item) {
|
||||||
|
return disabledItems.indexOf(item) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.isDisabled = function(itemScope) {
|
||||||
|
|
||||||
|
if (!ctrl.open) return;
|
||||||
|
|
||||||
|
var item = itemScope[ctrl.itemProperty];
|
||||||
|
var itemIndex = ctrl.items.indexOf(item);
|
||||||
|
var isDisabled = false;
|
||||||
|
|
||||||
|
if (itemIndex >= 0 && (angular.isDefined(ctrl.disableChoiceExpression) || ctrl.multiple)) {
|
||||||
|
|
||||||
|
if (item.isTag) return false;
|
||||||
|
|
||||||
|
if (ctrl.multiple) {
|
||||||
|
isDisabled = _isItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDisabled && angular.isDefined(ctrl.disableChoiceExpression)) {
|
||||||
|
isDisabled = !!(itemScope.$eval(ctrl.disableChoiceExpression));
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateItemDisabled(item, isDisabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDisabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// When the user selects an item with ENTER or clicks the dropdown
|
||||||
|
ctrl.select = function(item, skipFocusser, $event) {
|
||||||
|
if (isNil(item) || !_isItemDisabled(item)) {
|
||||||
|
|
||||||
|
if ( ! ctrl.items && ! ctrl.search && ! ctrl.tagging.isActivated) return;
|
||||||
|
|
||||||
|
if (!item || !_isItemDisabled(item)) {
|
||||||
|
// if click is made on existing item, prevent from tagging, ctrl.search does not matter
|
||||||
|
ctrl.clickTriggeredSelect = false;
|
||||||
|
if($event && ($event.type === 'click' || $event.type === 'touchend') && item)
|
||||||
|
ctrl.clickTriggeredSelect = true;
|
||||||
|
|
||||||
|
if(ctrl.tagging.isActivated && ctrl.clickTriggeredSelect === false) {
|
||||||
|
// if taggingLabel is disabled and item is undefined we pull from ctrl.search
|
||||||
|
if ( ctrl.taggingLabel === false ) {
|
||||||
|
if ( ctrl.activeIndex < 0 ) {
|
||||||
|
if (item === undefined) {
|
||||||
|
item = ctrl.tagging.fct !== undefined ? ctrl.tagging.fct(ctrl.search) : ctrl.search;
|
||||||
|
}
|
||||||
|
if (!item || angular.equals( ctrl.items[0], item ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// keyboard nav happened first, user selected from dropdown
|
||||||
|
item = ctrl.items[ctrl.activeIndex];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// tagging always operates at index zero, taggingLabel === false pushes
|
||||||
|
// the ctrl.search value without having it injected
|
||||||
|
if ( ctrl.activeIndex === 0 ) {
|
||||||
|
// ctrl.tagging pushes items to ctrl.items, so we only have empty val
|
||||||
|
// for `item` if it is a detected duplicate
|
||||||
|
if ( item === undefined ) return;
|
||||||
|
|
||||||
|
// create new item on the fly if we don't already have one;
|
||||||
|
// use tagging function if we have one
|
||||||
|
if ( ctrl.tagging.fct !== undefined && typeof item === 'string' ) {
|
||||||
|
item = ctrl.tagging.fct(item);
|
||||||
|
if (!item) return;
|
||||||
|
// if item type is 'string', apply the tagging label
|
||||||
|
} else if ( typeof item === 'string' ) {
|
||||||
|
// trim the trailing space
|
||||||
|
item = item.replace(ctrl.taggingLabel,'').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// search ctrl.selected for dupes potentially caused by tagging and return early if found
|
||||||
|
if (_isItemSelected(item)) {
|
||||||
|
ctrl.close(skipFocusser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_resetSearchInput();
|
||||||
|
$scope.$broadcast('uis:select', item);
|
||||||
|
|
||||||
|
if (ctrl.closeOnSelect) {
|
||||||
|
ctrl.close(skipFocusser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Closes the dropdown
|
||||||
|
ctrl.close = function(skipFocusser) {
|
||||||
|
if (!ctrl.open) return;
|
||||||
|
if (ctrl.ngModel && ctrl.ngModel.$setTouched) ctrl.ngModel.$setTouched();
|
||||||
|
ctrl.open = false;
|
||||||
|
_resetSearchInput();
|
||||||
|
$scope.$broadcast('uis:close', skipFocusser);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.setFocus = function(){
|
||||||
|
if (!ctrl.focus) ctrl.focusInput[0].focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.clear = function($event) {
|
||||||
|
ctrl.select(null);
|
||||||
|
$event.stopPropagation();
|
||||||
|
$timeout(function() {
|
||||||
|
ctrl.focusser[0].focus();
|
||||||
|
}, 0, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle dropdown
|
||||||
|
ctrl.toggle = function(e) {
|
||||||
|
if (ctrl.open) {
|
||||||
|
ctrl.close();
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
ctrl.activate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set default function for locked choices - avoids unnecessary
|
||||||
|
// logic if functionality is not being used
|
||||||
|
ctrl.isLocked = function () {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch(function () {
|
||||||
|
return angular.isDefined(ctrl.lockChoiceExpression) && ctrl.lockChoiceExpression !== "";
|
||||||
|
}, _initaliseLockedChoices);
|
||||||
|
|
||||||
|
function _initaliseLockedChoices(doInitalise) {
|
||||||
|
if(!doInitalise) return;
|
||||||
|
|
||||||
|
var lockedItems = [];
|
||||||
|
|
||||||
|
function _updateItemLocked(item, isLocked) {
|
||||||
|
var lockedItemIndex = lockedItems.indexOf(item);
|
||||||
|
if (isLocked && lockedItemIndex === -1) {
|
||||||
|
lockedItems.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLocked && lockedItemIndex > -1) {
|
||||||
|
lockedItems.splice(lockedItemIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isItemlocked(item) {
|
||||||
|
return lockedItems.indexOf(item) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl.isLocked = function (itemScope, itemIndex) {
|
||||||
|
var isLocked = false,
|
||||||
|
item = ctrl.selected[itemIndex];
|
||||||
|
|
||||||
|
if(item) {
|
||||||
|
if (itemScope) {
|
||||||
|
isLocked = !!(itemScope.$eval(ctrl.lockChoiceExpression));
|
||||||
|
_updateItemLocked(item, isLocked);
|
||||||
|
} else {
|
||||||
|
isLocked = _isItemlocked(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLocked;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var sizeWatch = null;
|
||||||
|
var updaterScheduled = false;
|
||||||
|
ctrl.sizeSearchInput = function() {
|
||||||
|
|
||||||
|
var input = ctrl.searchInput[0],
|
||||||
|
container = ctrl.$element[0],
|
||||||
|
calculateContainerWidth = function() {
|
||||||
|
// Return the container width only if the search input is visible
|
||||||
|
return container.clientWidth * !!input.offsetParent;
|
||||||
|
},
|
||||||
|
updateIfVisible = function(containerWidth) {
|
||||||
|
if (containerWidth === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var inputWidth = containerWidth - input.offsetLeft;
|
||||||
|
if (inputWidth < 50) inputWidth = containerWidth;
|
||||||
|
ctrl.searchInput.css('width', inputWidth+'px');
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.searchInput.css('width', '10px');
|
||||||
|
$timeout(function() { //Give tags time to render correctly
|
||||||
|
if (sizeWatch === null && !updateIfVisible(calculateContainerWidth())) {
|
||||||
|
sizeWatch = $scope.$watch(function() {
|
||||||
|
if (!updaterScheduled) {
|
||||||
|
updaterScheduled = true;
|
||||||
|
$scope.$$postDigest(function() {
|
||||||
|
updaterScheduled = false;
|
||||||
|
if (updateIfVisible(calculateContainerWidth())) {
|
||||||
|
sizeWatch();
|
||||||
|
sizeWatch = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, angular.noop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function _handleDropDownSelection(key) {
|
||||||
|
var processed = true;
|
||||||
|
switch (key) {
|
||||||
|
case KEY.DOWN:
|
||||||
|
if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
|
||||||
|
else if (ctrl.activeIndex < ctrl.items.length - 1) {
|
||||||
|
var idx = ++ctrl.activeIndex;
|
||||||
|
while(_isItemDisabled(ctrl.items[idx]) && idx < ctrl.items.length) {
|
||||||
|
ctrl.activeIndex = ++idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KEY.UP:
|
||||||
|
var minActiveIndex = (ctrl.search.length === 0 && ctrl.tagging.isActivated) ? -1 : 0;
|
||||||
|
if (!ctrl.open && ctrl.multiple) ctrl.activate(false, true); //In case its the search input in 'multiple' mode
|
||||||
|
else if (ctrl.activeIndex > minActiveIndex) {
|
||||||
|
var idxmin = --ctrl.activeIndex;
|
||||||
|
while(_isItemDisabled(ctrl.items[idxmin]) && idxmin > minActiveIndex) {
|
||||||
|
ctrl.activeIndex = --idxmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KEY.TAB:
|
||||||
|
if (!ctrl.multiple || ctrl.open) ctrl.select(ctrl.items[ctrl.activeIndex], true);
|
||||||
|
break;
|
||||||
|
case KEY.ENTER:
|
||||||
|
if(ctrl.open && (ctrl.tagging.isActivated || ctrl.activeIndex >= 0)){
|
||||||
|
ctrl.select(ctrl.items[ctrl.activeIndex], ctrl.skipFocusser); // Make sure at least one dropdown item is highlighted before adding if not in tagging mode
|
||||||
|
} else {
|
||||||
|
ctrl.activate(false, true); //In case its the search input in 'multiple' mode
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KEY.ESC:
|
||||||
|
ctrl.close();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
processed = false;
|
||||||
|
}
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind to keyboard shortcuts
|
||||||
|
ctrl.searchInput.on('keydown', function(e) {
|
||||||
|
|
||||||
|
var key = e.which;
|
||||||
|
|
||||||
|
if (~[KEY.ENTER,KEY.ESC].indexOf(key)){
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$apply(function() {
|
||||||
|
|
||||||
|
var tagged = false;
|
||||||
|
|
||||||
|
if (ctrl.items.length > 0 || ctrl.tagging.isActivated) {
|
||||||
|
if(!_handleDropDownSelection(key) && !ctrl.searchEnabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
if ( ctrl.taggingTokens.isActivated ) {
|
||||||
|
for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) {
|
||||||
|
if ( ctrl.taggingTokens.tokens[i] === KEY.MAP[e.keyCode] ) {
|
||||||
|
// make sure there is a new value to push via tagging
|
||||||
|
if ( ctrl.search.length > 0 ) {
|
||||||
|
tagged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( tagged ) {
|
||||||
|
$timeout(function() {
|
||||||
|
ctrl.searchInput.triggerHandler('tagged');
|
||||||
|
var newItem = ctrl.search.replace(KEY.MAP[e.keyCode],'').trim();
|
||||||
|
if ( ctrl.tagging.fct ) {
|
||||||
|
newItem = ctrl.tagging.fct( newItem );
|
||||||
|
}
|
||||||
|
if (newItem) ctrl.select(newItem, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if(KEY.isVerticalMovement(key) && ctrl.items.length > 0){
|
||||||
|
_ensureHighlightVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === KEY.ENTER || key === KEY.ESC) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrl.searchInput.on('paste', function (e) {
|
||||||
|
var data;
|
||||||
|
|
||||||
|
if (window.clipboardData && window.clipboardData.getData) { // IE
|
||||||
|
data = window.clipboardData.getData('Text');
|
||||||
|
} else {
|
||||||
|
data = (e.originalEvent || e).clipboardData.getData('text/plain');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend the current input field text to the paste buffer.
|
||||||
|
data = ctrl.search + data;
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
// If tagging try to split by tokens and add items
|
||||||
|
if (ctrl.taggingTokens.isActivated) {
|
||||||
|
var items = [];
|
||||||
|
for (var i = 0; i < ctrl.taggingTokens.tokens.length; i++) { // split by first token that is contained in data
|
||||||
|
var separator = KEY.toSeparator(ctrl.taggingTokens.tokens[i]) || ctrl.taggingTokens.tokens[i];
|
||||||
|
if (data.indexOf(separator) > -1) {
|
||||||
|
items = data.split(separator);
|
||||||
|
break; // only split by one token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.length === 0) {
|
||||||
|
items = [data];
|
||||||
|
}
|
||||||
|
var oldsearch = ctrl.search;
|
||||||
|
angular.forEach(items, function (item) {
|
||||||
|
var newItem = ctrl.tagging.fct ? ctrl.tagging.fct(item) : item;
|
||||||
|
if (newItem) {
|
||||||
|
ctrl.select(newItem, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctrl.search = oldsearch || EMPTY_SEARCH;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
} else if (ctrl.paste) {
|
||||||
|
ctrl.paste(data);
|
||||||
|
ctrl.search = EMPTY_SEARCH;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrl.searchInput.on('tagged', function() {
|
||||||
|
$timeout(function() {
|
||||||
|
_resetSearchInput();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// See https://github.com/ivaynberg/select2/blob/3.4.6/select2.js#L1431
|
||||||
|
function _ensureHighlightVisible() {
|
||||||
|
var container = $element.querySelectorAll('.ui-select-choices-content');
|
||||||
|
var choices = container.querySelectorAll('.ui-select-choices-row');
|
||||||
|
if (choices.length < 1) {
|
||||||
|
throw uiSelectMinErr('choices', "Expected multiple .ui-select-choices-row but got '{0}'.", choices.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrl.activeIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var highlighted = choices[ctrl.activeIndex];
|
||||||
|
var posY = highlighted.offsetTop + highlighted.clientHeight - container[0].scrollTop;
|
||||||
|
var height = container[0].offsetHeight;
|
||||||
|
|
||||||
|
if (posY > height) {
|
||||||
|
container[0].scrollTop += posY - height;
|
||||||
|
} else if (posY < highlighted.clientHeight) {
|
||||||
|
if (ctrl.isGrouped && ctrl.activeIndex === 0)
|
||||||
|
container[0].scrollTop = 0; //To make group header visible when going all the way up
|
||||||
|
else
|
||||||
|
container[0].scrollTop -= highlighted.clientHeight - posY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onResize = $$uisDebounce(function() {
|
||||||
|
ctrl.sizeSearchInput();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
angular.element($window).bind('resize', onResize);
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
ctrl.searchInput.off('keyup keydown tagged blur paste');
|
||||||
|
angular.element($window).off('resize', onResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('$select.activeIndex', function(activeIndex) {
|
||||||
|
if (activeIndex)
|
||||||
|
$element.find('input').attr(
|
||||||
|
'aria-activedescendant',
|
||||||
|
'ui-select-choices-row-' + ctrl.generatedId + '-' + activeIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch('$select.open', function(open) {
|
||||||
|
if (!open)
|
||||||
|
$element.find('input').removeAttr('aria-activedescendant');
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -0,0 +1,423 @@
|
||||||
|
uis.directive('uiSelect',
|
||||||
|
['$document', 'uiSelectConfig', 'uiSelectMinErr', 'uisOffset', '$compile', '$parse', '$timeout',
|
||||||
|
function($document, uiSelectConfig, uiSelectMinErr, uisOffset, $compile, $parse, $timeout) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
templateUrl: function(tElement, tAttrs) {
|
||||||
|
var theme = tAttrs.theme || uiSelectConfig.theme;
|
||||||
|
return theme + (angular.isDefined(tAttrs.multiple) ? '/select-multiple.tpl.html' : '/select.tpl.html');
|
||||||
|
},
|
||||||
|
replace: true,
|
||||||
|
transclude: true,
|
||||||
|
require: ['uiSelect', '^ngModel'],
|
||||||
|
scope: true,
|
||||||
|
|
||||||
|
controller: 'uiSelectCtrl',
|
||||||
|
controllerAs: '$select',
|
||||||
|
compile: function(tElement, tAttrs) {
|
||||||
|
|
||||||
|
// Allow setting ngClass on uiSelect
|
||||||
|
var match = /{(.*)}\s*{(.*)}/.exec(tAttrs.ngClass);
|
||||||
|
if(match) {
|
||||||
|
var combined = '{'+ match[1] +', '+ match[2] +'}';
|
||||||
|
tAttrs.ngClass = combined;
|
||||||
|
tElement.attr('ng-class', combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Multiple or Single depending if multiple attribute presence
|
||||||
|
if (angular.isDefined(tAttrs.multiple))
|
||||||
|
tElement.append('<ui-select-multiple/>').removeAttr('multiple');
|
||||||
|
else
|
||||||
|
tElement.append('<ui-select-single/>');
|
||||||
|
|
||||||
|
if (tAttrs.inputId)
|
||||||
|
tElement.querySelectorAll('input.ui-select-search')[0].id = tAttrs.inputId;
|
||||||
|
|
||||||
|
return function(scope, element, attrs, ctrls, transcludeFn) {
|
||||||
|
|
||||||
|
var $select = ctrls[0];
|
||||||
|
var ngModel = ctrls[1];
|
||||||
|
|
||||||
|
$select.generatedId = uiSelectConfig.generateId();
|
||||||
|
$select.baseTitle = attrs.title || 'Select box';
|
||||||
|
$select.focusserTitle = $select.baseTitle + ' focus';
|
||||||
|
$select.focusserId = 'focusser-' + $select.generatedId;
|
||||||
|
|
||||||
|
$select.closeOnSelect = function() {
|
||||||
|
if (angular.isDefined(attrs.closeOnSelect)) {
|
||||||
|
return $parse(attrs.closeOnSelect)();
|
||||||
|
} else {
|
||||||
|
return uiSelectConfig.closeOnSelect;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
scope.$watch('skipFocusser', function() {
|
||||||
|
var skipFocusser = scope.$eval(attrs.skipFocusser);
|
||||||
|
$select.skipFocusser = skipFocusser !== undefined ? skipFocusser : uiSelectConfig.skipFocusser;
|
||||||
|
});
|
||||||
|
|
||||||
|
$select.onSelectCallback = $parse(attrs.onSelect);
|
||||||
|
$select.onRemoveCallback = $parse(attrs.onRemove);
|
||||||
|
|
||||||
|
//Set reference to ngModel from uiSelectCtrl
|
||||||
|
$select.ngModel = ngModel;
|
||||||
|
|
||||||
|
$select.choiceGrouped = function(group){
|
||||||
|
return $select.isGrouped && group && group.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(attrs.tabindex){
|
||||||
|
attrs.$observe('tabindex', function(value) {
|
||||||
|
$select.focusInput.attr('tabindex', value);
|
||||||
|
element.removeAttr('tabindex');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$watch(function () { return scope.$eval(attrs.searchEnabled); }, function(newVal) {
|
||||||
|
$select.searchEnabled = newVal !== undefined ? newVal : uiSelectConfig.searchEnabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watch('sortable', function() {
|
||||||
|
var sortable = scope.$eval(attrs.sortable);
|
||||||
|
$select.sortable = sortable !== undefined ? sortable : uiSelectConfig.sortable;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('backspaceReset', function() {
|
||||||
|
// $eval() is needed otherwise we get a string instead of a boolean
|
||||||
|
var backspaceReset = scope.$eval(attrs.backspaceReset);
|
||||||
|
$select.backspaceReset = backspaceReset !== undefined ? backspaceReset : true;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('limit', function() {
|
||||||
|
//Limit the number of selections allowed
|
||||||
|
$select.limit = (angular.isDefined(attrs.limit)) ? parseInt(attrs.limit, 10) : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watch('removeSelected', function() {
|
||||||
|
var removeSelected = scope.$eval(attrs.removeSelected);
|
||||||
|
$select.removeSelected = removeSelected !== undefined ? removeSelected : uiSelectConfig.removeSelected;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('disabled', function() {
|
||||||
|
// No need to use $eval() (thanks to ng-disabled) since we already get a boolean instead of a string
|
||||||
|
$select.disabled = attrs.disabled !== undefined ? attrs.disabled : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('resetSearchInput', function() {
|
||||||
|
// $eval() is needed otherwise we get a string instead of a boolean
|
||||||
|
var resetSearchInput = scope.$eval(attrs.resetSearchInput);
|
||||||
|
$select.resetSearchInput = resetSearchInput !== undefined ? resetSearchInput : true;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('paste', function() {
|
||||||
|
$select.paste = scope.$eval(attrs.paste);
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('tagging', function() {
|
||||||
|
if(attrs.tagging !== undefined)
|
||||||
|
{
|
||||||
|
// $eval() is needed otherwise we get a string instead of a boolean
|
||||||
|
var taggingEval = scope.$eval(attrs.tagging);
|
||||||
|
$select.tagging = {isActivated: true, fct: taggingEval !== true ? taggingEval : undefined};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$select.tagging = {isActivated: false, fct: undefined};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('taggingLabel', function() {
|
||||||
|
if(attrs.tagging !== undefined )
|
||||||
|
{
|
||||||
|
// check eval for FALSE, in this case, we disable the labels
|
||||||
|
// associated with tagging
|
||||||
|
if ( attrs.taggingLabel === 'false' ) {
|
||||||
|
$select.taggingLabel = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$select.taggingLabel = attrs.taggingLabel !== undefined ? attrs.taggingLabel : '(new)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('taggingTokens', function() {
|
||||||
|
if (attrs.tagging !== undefined) {
|
||||||
|
var tokens = attrs.taggingTokens !== undefined ? attrs.taggingTokens.split('|') : [',','ENTER'];
|
||||||
|
$select.taggingTokens = {isActivated: true, tokens: tokens };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('spinnerEnabled', function() {
|
||||||
|
// $eval() is needed otherwise we get a string instead of a boolean
|
||||||
|
var spinnerEnabled = scope.$eval(attrs.spinnerEnabled);
|
||||||
|
$select.spinnerEnabled = spinnerEnabled !== undefined ? spinnerEnabled : uiSelectConfig.spinnerEnabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
attrs.$observe('spinnerClass', function() {
|
||||||
|
var spinnerClass = attrs.spinnerClass;
|
||||||
|
$select.spinnerClass = spinnerClass !== undefined ? attrs.spinnerClass : uiSelectConfig.spinnerClass;
|
||||||
|
});
|
||||||
|
|
||||||
|
//Automatically gets focus when loaded
|
||||||
|
if (angular.isDefined(attrs.autofocus)){
|
||||||
|
$timeout(function(){
|
||||||
|
$select.setFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Gets focus based on scope event name (e.g. focus-on='SomeEventName')
|
||||||
|
if (angular.isDefined(attrs.focusOn)){
|
||||||
|
scope.$on(attrs.focusOn, function() {
|
||||||
|
$timeout(function(){
|
||||||
|
$select.setFocus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDocumentClick(e) {
|
||||||
|
if (!$select.open) return; //Skip it if dropdown is close
|
||||||
|
|
||||||
|
var contains = false;
|
||||||
|
|
||||||
|
if (window.jQuery) {
|
||||||
|
// Firefox 3.6 does not support element.contains()
|
||||||
|
// See Node.contains https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
|
||||||
|
contains = window.jQuery.contains(element[0], e.target);
|
||||||
|
} else {
|
||||||
|
contains = element[0].contains(e.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contains && !$select.clickTriggeredSelect) {
|
||||||
|
var skipFocusser;
|
||||||
|
if (!$select.skipFocusser) {
|
||||||
|
//Will lose focus only with certain targets
|
||||||
|
var focusableControls = ['input','button','textarea','select'];
|
||||||
|
var targetController = angular.element(e.target).controller('uiSelect'); //To check if target is other ui-select
|
||||||
|
skipFocusser = targetController && targetController !== $select; //To check if target is other ui-select
|
||||||
|
if (!skipFocusser) skipFocusser = ~focusableControls.indexOf(e.target.tagName.toLowerCase()); //Check if target is input, button or textarea
|
||||||
|
} else {
|
||||||
|
skipFocusser = true;
|
||||||
|
}
|
||||||
|
$select.close(skipFocusser);
|
||||||
|
scope.$digest();
|
||||||
|
}
|
||||||
|
$select.clickTriggeredSelect = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See Click everywhere but here event http://stackoverflow.com/questions/12931369
|
||||||
|
$document.on('click', onDocumentClick);
|
||||||
|
|
||||||
|
scope.$on('$destroy', function() {
|
||||||
|
$document.off('click', onDocumentClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move transcluded elements to their correct position in main template
|
||||||
|
transcludeFn(scope, function(clone) {
|
||||||
|
// See Transclude in AngularJS http://blog.omkarpatil.com/2012/11/transclude-in-angularjs.html
|
||||||
|
|
||||||
|
// One day jqLite will be replaced by jQuery and we will be able to write:
|
||||||
|
// var transcludedElement = clone.filter('.my-class')
|
||||||
|
// instead of creating a hackish DOM element:
|
||||||
|
var transcluded = angular.element('<div>').append(clone);
|
||||||
|
|
||||||
|
var transcludedMatch = transcluded.querySelectorAll('.ui-select-match');
|
||||||
|
transcludedMatch.removeAttr('ui-select-match'); //To avoid loop in case directive as attr
|
||||||
|
transcludedMatch.removeAttr('data-ui-select-match'); // Properly handle HTML5 data-attributes
|
||||||
|
if (transcludedMatch.length !== 1) {
|
||||||
|
throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-match but got '{0}'.", transcludedMatch.length);
|
||||||
|
}
|
||||||
|
element.querySelectorAll('.ui-select-match').replaceWith(transcludedMatch);
|
||||||
|
|
||||||
|
var transcludedChoices = transcluded.querySelectorAll('.ui-select-choices');
|
||||||
|
transcludedChoices.removeAttr('ui-select-choices'); //To avoid loop in case directive as attr
|
||||||
|
transcludedChoices.removeAttr('data-ui-select-choices'); // Properly handle HTML5 data-attributes
|
||||||
|
if (transcludedChoices.length !== 1) {
|
||||||
|
throw uiSelectMinErr('transcluded', "Expected 1 .ui-select-choices but got '{0}'.", transcludedChoices.length);
|
||||||
|
}
|
||||||
|
element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
|
||||||
|
|
||||||
|
var transcludedNoChoice = transcluded.querySelectorAll('.ui-select-no-choice');
|
||||||
|
transcludedNoChoice.removeAttr('ui-select-no-choice'); //To avoid loop in case directive as attr
|
||||||
|
transcludedNoChoice.removeAttr('data-ui-select-no-choice'); // Properly handle HTML5 data-attributes
|
||||||
|
if (transcludedNoChoice.length == 1) {
|
||||||
|
element.querySelectorAll('.ui-select-no-choice').replaceWith(transcludedNoChoice);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Support for appending the select field to the body when its open
|
||||||
|
var appendToBody = scope.$eval(attrs.appendToBody);
|
||||||
|
if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
|
||||||
|
scope.$watch('$select.open', function(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
positionDropdown();
|
||||||
|
} else {
|
||||||
|
resetDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move the dropdown back to its original location when the scope is destroyed. Otherwise
|
||||||
|
// it might stick around when the user routes away or the select field is otherwise removed
|
||||||
|
scope.$on('$destroy', function() {
|
||||||
|
resetDropdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hold on to a reference to the .ui-select-container element for appendToBody support
|
||||||
|
var placeholder = null,
|
||||||
|
originalWidth = '';
|
||||||
|
|
||||||
|
function positionDropdown() {
|
||||||
|
// Remember the absolute position of the element
|
||||||
|
var offset = uisOffset(element);
|
||||||
|
|
||||||
|
// Clone the element into a placeholder element to take its original place in the DOM
|
||||||
|
placeholder = angular.element('<div class="ui-select-placeholder"></div>');
|
||||||
|
placeholder[0].style.width = offset.width + 'px';
|
||||||
|
placeholder[0].style.height = offset.height + 'px';
|
||||||
|
element.after(placeholder);
|
||||||
|
|
||||||
|
// Remember the original value of the element width inline style, so it can be restored
|
||||||
|
// when the dropdown is closed
|
||||||
|
originalWidth = element[0].style.width;
|
||||||
|
|
||||||
|
// Now move the actual dropdown element to the end of the body
|
||||||
|
$document.find('body').append(element);
|
||||||
|
|
||||||
|
element[0].style.position = 'absolute';
|
||||||
|
element[0].style.left = offset.left + 'px';
|
||||||
|
element[0].style.top = offset.top + 'px';
|
||||||
|
element[0].style.width = offset.width + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDropdown() {
|
||||||
|
if (placeholder === null) {
|
||||||
|
// The dropdown has not actually been display yet, so there's nothing to reset
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the dropdown element back to its original location in the DOM
|
||||||
|
placeholder.replaceWith(element);
|
||||||
|
placeholder = null;
|
||||||
|
|
||||||
|
element[0].style.position = '';
|
||||||
|
element[0].style.left = '';
|
||||||
|
element[0].style.top = '';
|
||||||
|
element[0].style.width = originalWidth;
|
||||||
|
|
||||||
|
// Set focus back on to the moved element
|
||||||
|
$select.setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hold on to a reference to the .ui-select-dropdown element for direction support.
|
||||||
|
var dropdown = null,
|
||||||
|
directionUpClassName = 'direction-up';
|
||||||
|
|
||||||
|
// Support changing the direction of the dropdown if there isn't enough space to render it.
|
||||||
|
scope.$watch('$select.open', function() {
|
||||||
|
|
||||||
|
if ($select.dropdownPosition === 'auto' || $select.dropdownPosition === 'up'){
|
||||||
|
scope.calculateDropdownPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var setDropdownPosUp = function(offset, offsetDropdown){
|
||||||
|
|
||||||
|
offset = offset || uisOffset(element);
|
||||||
|
offsetDropdown = offsetDropdown || uisOffset(dropdown);
|
||||||
|
|
||||||
|
dropdown[0].style.position = 'absolute';
|
||||||
|
dropdown[0].style.top = (offsetDropdown.height * -1) + 'px';
|
||||||
|
element.addClass(directionUpClassName);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var setDropdownPosDown = function(offset, offsetDropdown){
|
||||||
|
|
||||||
|
element.removeClass(directionUpClassName);
|
||||||
|
|
||||||
|
offset = offset || uisOffset(element);
|
||||||
|
offsetDropdown = offsetDropdown || uisOffset(dropdown);
|
||||||
|
|
||||||
|
dropdown[0].style.position = '';
|
||||||
|
dropdown[0].style.top = '';
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var calculateDropdownPosAfterAnimation = function() {
|
||||||
|
// Delay positioning the dropdown until all choices have been added so its height is correct.
|
||||||
|
$timeout(function() {
|
||||||
|
if ($select.dropdownPosition === 'up') {
|
||||||
|
//Go UP
|
||||||
|
setDropdownPosUp();
|
||||||
|
} else {
|
||||||
|
//AUTO
|
||||||
|
element.removeClass(directionUpClassName);
|
||||||
|
|
||||||
|
var offset = uisOffset(element);
|
||||||
|
var offsetDropdown = uisOffset(dropdown);
|
||||||
|
|
||||||
|
//https://code.google.com/p/chromium/issues/detail?id=342307#c4
|
||||||
|
var scrollTop = $document[0].documentElement.scrollTop || $document[0].body.scrollTop; //To make it cross browser (blink, webkit, IE, Firefox).
|
||||||
|
|
||||||
|
// Determine if the direction of the dropdown needs to be changed.
|
||||||
|
if (offset.top + offset.height + offsetDropdown.height > scrollTop + $document[0].documentElement.clientHeight) {
|
||||||
|
//Go UP
|
||||||
|
setDropdownPosUp(offset, offsetDropdown);
|
||||||
|
}else{
|
||||||
|
//Go DOWN
|
||||||
|
setDropdownPosDown(offset, offsetDropdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the dropdown once it has been positioned.
|
||||||
|
dropdown[0].style.opacity = 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var opened = false;
|
||||||
|
|
||||||
|
scope.calculateDropdownPos = function() {
|
||||||
|
if ($select.open) {
|
||||||
|
dropdown = angular.element(element).querySelectorAll('.ui-select-dropdown');
|
||||||
|
|
||||||
|
if (dropdown.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the dropdown so there is no flicker until $timeout is done executing.
|
||||||
|
if ($select.search === '' && !opened) {
|
||||||
|
dropdown[0].style.opacity = 0;
|
||||||
|
opened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uisOffset(dropdown).height && $select.$animate && $select.$animate.on && $select.$animate.enabled(dropdown)) {
|
||||||
|
var needsCalculated = true;
|
||||||
|
|
||||||
|
$select.$animate.on('enter', dropdown, function (elem, phase) {
|
||||||
|
if (phase === 'close' && needsCalculated) {
|
||||||
|
calculateDropdownPosAfterAnimation();
|
||||||
|
needsCalculated = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
calculateDropdownPosAfterAnimation();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dropdown === null || dropdown.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the position of the dropdown.
|
||||||
|
dropdown[0].style.opacity = 0;
|
||||||
|
dropdown[0].style.position = '';
|
||||||
|
dropdown[0].style.top = '';
|
||||||
|
element.removeClass(directionUpClassName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,48 @@
|
||||||
|
uis.directive('uiSelectMatch', ['uiSelectConfig', function(uiSelectConfig) {
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
require: '^uiSelect',
|
||||||
|
replace: true,
|
||||||
|
transclude: true,
|
||||||
|
templateUrl: function(tElement) {
|
||||||
|
// Needed so the uiSelect can detect the transcluded content
|
||||||
|
tElement.addClass('ui-select-match');
|
||||||
|
|
||||||
|
var parent = tElement.parent();
|
||||||
|
// Gets theme attribute from parent (ui-select)
|
||||||
|
var theme = getAttribute(parent, 'theme') || uiSelectConfig.theme;
|
||||||
|
var multi = angular.isDefined(getAttribute(parent, 'multiple'));
|
||||||
|
|
||||||
|
return theme + (multi ? '/match-multiple.tpl.html' : '/match.tpl.html');
|
||||||
|
},
|
||||||
|
link: function(scope, element, attrs, $select) {
|
||||||
|
$select.lockChoiceExpression = attrs.uiLockChoice;
|
||||||
|
attrs.$observe('placeholder', function(placeholder) {
|
||||||
|
$select.placeholder = placeholder !== undefined ? placeholder : uiSelectConfig.placeholder;
|
||||||
|
});
|
||||||
|
|
||||||
|
function setAllowClear(allow) {
|
||||||
|
$select.allowClear = (angular.isDefined(allow)) ? (allow === '') ? true : (allow.toLowerCase() === 'true') : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs.$observe('allowClear', setAllowClear);
|
||||||
|
setAllowClear(attrs.allowClear);
|
||||||
|
|
||||||
|
if($select.multiple){
|
||||||
|
$select.sizeSearchInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAttribute(elem, attribute) {
|
||||||
|
if (elem[0].hasAttribute(attribute))
|
||||||
|
return elem.attr(attribute);
|
||||||
|
|
||||||
|
if (elem[0].hasAttribute('data-' + attribute))
|
||||||
|
return elem.attr('data-' + attribute);
|
||||||
|
|
||||||
|
if (elem[0].hasAttribute('x-' + attribute))
|
||||||
|
return elem.attr('x-' + attribute);
|
||||||
|
}
|
||||||
|
}]);
|
|
@ -0,0 +1,460 @@
|
||||||
|
uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelectMinErr, $timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'EA',
|
||||||
|
require: ['^uiSelect', '^ngModel'],
|
||||||
|
|
||||||
|
controller: ['$scope','$timeout', function($scope, $timeout){
|
||||||
|
|
||||||
|
var ctrl = this,
|
||||||
|
$select = $scope.$select,
|
||||||
|
ngModel;
|
||||||
|
|
||||||
|
if (angular.isUndefined($select.selected))
|
||||||
|
$select.selected = [];
|
||||||
|
|
||||||
|
//Wait for link fn to inject it
|
||||||
|
$scope.$evalAsync(function(){ ngModel = $scope.ngModel; });
|
||||||
|
|
||||||
|
ctrl.activeMatchIndex = -1;
|
||||||
|
|
||||||
|
ctrl.updateModel = function(){
|
||||||
|
ngModel.$setViewValue(Date.now()); //Set timestamp as a unique string to force changes
|
||||||
|
ctrl.refreshComponent();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.refreshComponent = function(){
|
||||||
|
//Remove already selected items
|
||||||
|
//e.g. When user clicks on a selection, the selected array changes and
|
||||||
|
//the dropdown should remove that item
|
||||||
|
if($select.refreshItems){
|
||||||
|
$select.refreshItems();
|
||||||
|
}
|
||||||
|
if($select.sizeSearchInput){
|
||||||
|
$select.sizeSearchInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove item from multiple select
|
||||||
|
ctrl.removeChoice = function(index){
|
||||||
|
|
||||||
|
// if the choice is locked, don't remove it
|
||||||
|
if($select.isLocked(null, index)) return false;
|
||||||
|
|
||||||
|
var removedChoice = $select.selected[index];
|
||||||
|
|
||||||
|
var locals = {};
|
||||||
|
locals[$select.parserResult.itemName] = removedChoice;
|
||||||
|
|
||||||
|
$select.selected.splice(index, 1);
|
||||||
|
ctrl.activeMatchIndex = -1;
|
||||||
|
$select.sizeSearchInput();
|
||||||
|
|
||||||
|
// Give some time for scope propagation.
|
||||||
|
$timeout(function(){
|
||||||
|
$select.onRemoveCallback($scope, {
|
||||||
|
$item: removedChoice,
|
||||||
|
$model: $select.parserResult.modelMapper($scope, locals)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrl.updateModel();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.getPlaceholder = function(){
|
||||||
|
//Refactor single?
|
||||||
|
if($select.selected && $select.selected.length) return;
|
||||||
|
return $select.placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}],
|
||||||
|
controllerAs: '$selectMultiple',
|
||||||
|
|
||||||
|
link: function(scope, element, attrs, ctrls) {
|
||||||
|
|
||||||
|
var $select = ctrls[0];
|
||||||
|
var ngModel = scope.ngModel = ctrls[1];
|
||||||
|
var $selectMultiple = scope.$selectMultiple;
|
||||||
|
|
||||||
|
//$select.selected = raw selected objects (ignoring any property binding)
|
||||||
|
|
||||||
|
$select.multiple = true;
|
||||||
|
|
||||||
|
//Input that will handle focus
|
||||||
|
$select.focusInput = $select.searchInput;
|
||||||
|
|
||||||
|
//Properly check for empty if set to multiple
|
||||||
|
ngModel.$isEmpty = function(value) {
|
||||||
|
return !value || value.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
//From view --> model
|
||||||
|
ngModel.$parsers.unshift(function () {
|
||||||
|
var locals = {},
|
||||||
|
result,
|
||||||
|
resultMultiple = [];
|
||||||
|
for (var j = $select.selected.length - 1; j >= 0; j--) {
|
||||||
|
locals = {};
|
||||||
|
locals[$select.parserResult.itemName] = $select.selected[j];
|
||||||
|
result = $select.parserResult.modelMapper(scope, locals);
|
||||||
|
resultMultiple.unshift(result);
|
||||||
|
}
|
||||||
|
return resultMultiple;
|
||||||
|
});
|
||||||
|
|
||||||
|
// From model --> view
|
||||||
|
ngModel.$formatters.unshift(function (inputValue) {
|
||||||
|
var data = $select.parserResult && $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search
|
||||||
|
locals = {},
|
||||||
|
result;
|
||||||
|
if (!data) return inputValue;
|
||||||
|
var resultMultiple = [];
|
||||||
|
var checkFnMultiple = function(list, value){
|
||||||
|
if (!list || !list.length) return;
|
||||||
|
for (var p = list.length - 1; p >= 0; p--) {
|
||||||
|
locals[$select.parserResult.itemName] = list[p];
|
||||||
|
result = $select.parserResult.modelMapper(scope, locals);
|
||||||
|
if($select.parserResult.trackByExp){
|
||||||
|
var propsItemNameMatches = /(\w*)\./.exec($select.parserResult.trackByExp);
|
||||||
|
var matches = /\.([^\s]+)/.exec($select.parserResult.trackByExp);
|
||||||
|
if(propsItemNameMatches && propsItemNameMatches.length > 0 && propsItemNameMatches[1] == $select.parserResult.itemName){
|
||||||
|
if(matches && matches.length>0 && result[matches[1]] == value[matches[1]]){
|
||||||
|
resultMultiple.unshift(list[p]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (angular.equals(result,value)){
|
||||||
|
resultMultiple.unshift(list[p]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (!inputValue) return resultMultiple; //If ngModel was undefined
|
||||||
|
for (var k = inputValue.length - 1; k >= 0; k--) {
|
||||||
|
//Check model array of currently selected items
|
||||||
|
if (!checkFnMultiple($select.selected, inputValue[k])){
|
||||||
|
//Check model array of all items available
|
||||||
|
if (!checkFnMultiple(data, inputValue[k])){
|
||||||
|
//If not found on previous lists, just add it directly to resultMultiple
|
||||||
|
resultMultiple.unshift(inputValue[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultMultiple;
|
||||||
|
});
|
||||||
|
|
||||||
|
//Watch for external model changes
|
||||||
|
scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
|
||||||
|
if (oldValue != newValue){
|
||||||
|
//update the view value with fresh data from items, if there is a valid model value
|
||||||
|
if(angular.isDefined(ngModel.$modelValue)) {
|
||||||
|
ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
|
||||||
|
}
|
||||||
|
$selectMultiple.refreshComponent();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ngModel.$render = function() {
|
||||||
|
// Make sure that model value is array
|
||||||
|
if(!angular.isArray(ngModel.$viewValue)){
|
||||||
|
// Have tolerance for null or undefined values
|
||||||
|
if (isNil(ngModel.$viewValue)){
|
||||||
|
ngModel.$viewValue = [];
|
||||||
|
} else {
|
||||||
|
throw uiSelectMinErr('multiarr', "Expected model value to be array but got '{0}'", ngModel.$viewValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$select.selected = ngModel.$viewValue;
|
||||||
|
$selectMultiple.refreshComponent();
|
||||||
|
scope.$evalAsync(); //To force $digest
|
||||||
|
};
|
||||||
|
|
||||||
|
scope.$on('uis:select', function (event, item) {
|
||||||
|
if($select.selected.length >= $select.limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$select.selected.push(item);
|
||||||
|
var locals = {};
|
||||||
|
locals[$select.parserResult.itemName] = item;
|
||||||
|
|
||||||
|
$timeout(function(){
|
||||||
|
$select.onSelectCallback(scope, {
|
||||||
|
$item: item,
|
||||||
|
$model: $select.parserResult.modelMapper(scope, locals)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$selectMultiple.updateModel();
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$on('uis:activate', function () {
|
||||||
|
$selectMultiple.activeMatchIndex = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$watch('$select.disabled', function(newValue, oldValue) {
|
||||||
|
// As the search input field may now become visible, it may be necessary to recompute its size
|
||||||
|
if (oldValue && !newValue) $select.sizeSearchInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
$select.searchInput.on('keydown', function(e) {
|
||||||
|
var key = e.which;
|
||||||
|
scope.$apply(function() {
|
||||||
|
var processed = false;
|
||||||
|
// var tagged = false; //Checkme
|
||||||
|
if(KEY.isHorizontalMovement(key)){
|
||||||
|
processed = _handleMatchSelection(key);
|
||||||
|
}
|
||||||
|
if (processed && key != KEY.TAB) {
|
||||||
|
//TODO Check si el tab selecciona aun correctamente
|
||||||
|
//Crear test
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function _getCaretPosition(el) {
|
||||||
|
if(angular.isNumber(el.selectionStart)) return el.selectionStart;
|
||||||
|
// selectionStart is not supported in IE8 and we don't want hacky workarounds so we compromise
|
||||||
|
else return el.value.length;
|
||||||
|
}
|
||||||
|
// Handles selected options in "multiple" mode
|
||||||
|
function _handleMatchSelection(key){
|
||||||
|
var caretPosition = _getCaretPosition($select.searchInput[0]),
|
||||||
|
length = $select.selected.length,
|
||||||
|
// none = -1,
|
||||||
|
first = 0,
|
||||||
|
last = length-1,
|
||||||
|
curr = $selectMultiple.activeMatchIndex,
|
||||||
|
next = $selectMultiple.activeMatchIndex+1,
|
||||||
|
prev = $selectMultiple.activeMatchIndex-1,
|
||||||
|
newIndex = curr;
|
||||||
|
|
||||||
|
if(caretPosition > 0 || ($select.search.length && key == KEY.RIGHT)) return false;
|
||||||
|
|
||||||
|
$select.close();
|
||||||
|
|
||||||
|
function getNewActiveMatchIndex(){
|
||||||
|
switch(key){
|
||||||
|
case KEY.LEFT:
|
||||||
|
// Select previous/first item
|
||||||
|
if(~$selectMultiple.activeMatchIndex) return prev;
|
||||||
|
// Select last item
|
||||||
|
else return last;
|
||||||
|
break;
|
||||||
|
case KEY.RIGHT:
|
||||||
|
// Open drop-down
|
||||||
|
if(!~$selectMultiple.activeMatchIndex || curr === last){
|
||||||
|
$select.activate();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Select next/last item
|
||||||
|
else return next;
|
||||||
|
break;
|
||||||
|
case KEY.BACKSPACE:
|
||||||
|
// Remove selected item and select previous/first
|
||||||
|
if(~$selectMultiple.activeMatchIndex){
|
||||||
|
if($selectMultiple.removeChoice(curr)) {
|
||||||
|
return prev;
|
||||||
|
} else {
|
||||||
|
return curr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// If nothing yet selected, select last item
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KEY.DELETE:
|
||||||
|
// Remove selected item and select next item
|
||||||
|
if(~$selectMultiple.activeMatchIndex){
|
||||||
|
$selectMultiple.removeChoice($selectMultiple.activeMatchIndex);
|
||||||
|
return curr;
|
||||||
|
}
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newIndex = getNewActiveMatchIndex();
|
||||||
|
|
||||||
|
if(!$select.selected.length || newIndex === false) $selectMultiple.activeMatchIndex = -1;
|
||||||
|
else $selectMultiple.activeMatchIndex = Math.min(last,Math.max(first,newIndex));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$select.searchInput.on('keyup', function(e) {
|
||||||
|
|
||||||
|
if ( ! KEY.isVerticalMovement(e.which) ) {
|
||||||
|
scope.$evalAsync( function () {
|
||||||
|
$select.activeIndex = $select.taggingLabel === false ? -1 : 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Push a "create new" item into array if there is a search string
|
||||||
|
if ( $select.tagging.isActivated && $select.search.length > 0 ) {
|
||||||
|
|
||||||
|
// return early with these keys
|
||||||
|
if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC || KEY.isVerticalMovement(e.which) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// always reset the activeIndex to the first item when tagging
|
||||||
|
$select.activeIndex = $select.taggingLabel === false ? -1 : 0;
|
||||||
|
// taggingLabel === false bypasses all of this
|
||||||
|
if ($select.taggingLabel === false) return;
|
||||||
|
|
||||||
|
var items = angular.copy( $select.items );
|
||||||
|
var stashArr = angular.copy( $select.items );
|
||||||
|
var newItem;
|
||||||
|
var item;
|
||||||
|
var hasTag = false;
|
||||||
|
var dupeIndex = -1;
|
||||||
|
var tagItems;
|
||||||
|
var tagItem;
|
||||||
|
|
||||||
|
// case for object tagging via transform `$select.tagging.fct` function
|
||||||
|
if ( $select.tagging.fct !== undefined) {
|
||||||
|
tagItems = $select.$filter('filter')(items,{'isTag': true});
|
||||||
|
if ( tagItems.length > 0 ) {
|
||||||
|
tagItem = tagItems[0];
|
||||||
|
}
|
||||||
|
// remove the first element, if it has the `isTag` prop we generate a new one with each keyup, shaving the previous
|
||||||
|
if ( items.length > 0 && tagItem ) {
|
||||||
|
hasTag = true;
|
||||||
|
items = items.slice(1,items.length);
|
||||||
|
stashArr = stashArr.slice(1,stashArr.length);
|
||||||
|
}
|
||||||
|
newItem = $select.tagging.fct($select.search);
|
||||||
|
// verify the new tag doesn't match the value of a possible selection choice or an already selected item.
|
||||||
|
if (
|
||||||
|
stashArr.some(function (origItem) {
|
||||||
|
return angular.equals(origItem, newItem);
|
||||||
|
}) ||
|
||||||
|
$select.selected.some(function (origItem) {
|
||||||
|
return angular.equals(origItem, newItem);
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
scope.$evalAsync(function () {
|
||||||
|
$select.activeIndex = 0;
|
||||||
|
$select.items = items;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (newItem) newItem.isTag = true;
|
||||||
|
// handle newItem string and stripping dupes in tagging string context
|
||||||
|
} else {
|
||||||
|
// find any tagging items already in the $select.items array and store them
|
||||||
|
tagItems = $select.$filter('filter')(items,function (item) {
|
||||||
|
return item.match($select.taggingLabel);
|
||||||
|
});
|
||||||
|
if ( tagItems.length > 0 ) {
|
||||||
|
tagItem = tagItems[0];
|
||||||
|
}
|
||||||
|
item = items[0];
|
||||||
|
// remove existing tag item if found (should only ever be one tag item)
|
||||||
|
if ( item !== undefined && items.length > 0 && tagItem ) {
|
||||||
|
hasTag = true;
|
||||||
|
items = items.slice(1,items.length);
|
||||||
|
stashArr = stashArr.slice(1,stashArr.length);
|
||||||
|
}
|
||||||
|
newItem = $select.search+' '+$select.taggingLabel;
|
||||||
|
if ( _findApproxDupe($select.selected, $select.search) > -1 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// verify the the tag doesn't match the value of an existing item from
|
||||||
|
// the searched data set or the items already selected
|
||||||
|
if ( _findCaseInsensitiveDupe(stashArr.concat($select.selected)) ) {
|
||||||
|
// if there is a tag from prev iteration, strip it / queue the change
|
||||||
|
// and return early
|
||||||
|
if ( hasTag ) {
|
||||||
|
items = stashArr;
|
||||||
|
scope.$evalAsync( function () {
|
||||||
|
$select.activeIndex = 0;
|
||||||
|
$select.items = items;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( _findCaseInsensitiveDupe(stashArr) ) {
|
||||||
|
// if there is a tag from prev iteration, strip it
|
||||||
|
if ( hasTag ) {
|
||||||
|
$select.items = stashArr.slice(1,stashArr.length);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( hasTag ) dupeIndex = _findApproxDupe($select.selected, newItem);
|
||||||
|
// dupe found, shave the first item
|
||||||
|
if ( dupeIndex > -1 ) {
|
||||||
|
items = items.slice(dupeIndex+1,items.length-1);
|
||||||
|
} else {
|
||||||
|
items = [];
|
||||||
|
if (newItem) items.push(newItem);
|
||||||
|
items = items.concat(stashArr);
|
||||||
|
}
|
||||||
|
scope.$evalAsync( function () {
|
||||||
|
$select.activeIndex = 0;
|
||||||
|
$select.items = items;
|
||||||
|
|
||||||
|
if ($select.isGrouped) {
|
||||||
|
// update item references in groups, so that indexOf will work after angular.copy
|
||||||
|
var itemsWithoutTag = newItem ? items.slice(1) : items;
|
||||||
|
$select.setItemsFn(itemsWithoutTag);
|
||||||
|
if (newItem) {
|
||||||
|
// add tag item as a new group
|
||||||
|
$select.items.unshift(newItem);
|
||||||
|
$select.groups.unshift({name: '', items: [newItem], tagging: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function _findCaseInsensitiveDupe(arr) {
|
||||||
|
if ( arr === undefined || $select.search === undefined ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var hasDupe = arr.filter( function (origItem) {
|
||||||
|
if ( $select.search.toUpperCase() === undefined || origItem === undefined ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return origItem.toUpperCase() === $select.search.toUpperCase();
|
||||||
|
}).length > 0;
|
||||||
|
|
||||||
|
return hasDupe;
|
||||||
|
}
|
||||||
|
function _findApproxDupe(haystack, needle) {
|
||||||
|
var dupeIndex = -1;
|
||||||
|
if(angular.isArray(haystack)) {
|
||||||
|
var tempArr = angular.copy(haystack);
|
||||||
|
for (var i = 0; i <tempArr.length; i++) {
|
||||||
|
// handle the simple string version of tagging
|
||||||
|
if ( $select.tagging.fct === undefined ) {
|
||||||
|
// search the array for the match
|
||||||
|
if ( tempArr[i]+' '+$select.taggingLabel === needle ) {
|
||||||
|
dupeIndex = i;
|
||||||
|
}
|
||||||
|
// handle the object tagging implementation
|
||||||
|
} else {
|
||||||
|
var mockObj = tempArr[i];
|
||||||
|
if (angular.isObject(mockObj)) {
|
||||||
|
mockObj.isTag = true;
|
||||||
|
}
|
||||||
|
if ( angular.equals(mockObj, needle) ) {
|
||||||
|
dupeIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dupeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$select.searchInput.on('blur', function() {
|
||||||
|
$timeout(function() {
|
||||||
|
$selectMultiple.activeMatchIndex = -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче