├── .gitignore
├── .jshintrc
├── LICENSE
├── README.md
├── autocompleted-select.sublime-project
├── demo
├── index.html
└── index.js
├── dist
└── tests.js
├── gulp
├── build
│ ├── css.js
│ └── js.js
├── config.js
├── lint.js
├── test
│ ├── build-watch.js
│ ├── build.js
│ ├── lint.js
│ └── run.js
└── web-server.js
├── gulpfile.js
├── karma.conf.js
├── package.json
├── shims
└── x-tag.js
├── src
├── css
│ └── styles.css
└── js
│ ├── create-component.js
│ ├── index.js
│ ├── intent.js
│ ├── model.js
│ ├── spec
│ ├── intent.spec.js
│ ├── mock.js
│ ├── model.spec.js
│ └── view.spec.js
│ └── view.js
└── test
└── phantomjs-extensions.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 | *.bundle.js
27 |
28 | # SublimeText files
29 | *.sublime-workspace
30 |
31 | # Temp files
32 | test/browserify-seed.js
33 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": { },
3 | "node": true,
4 | "regexdash": true,
5 | "browser": true,
6 | "sub": true,
7 | "curly": true,
8 | "camelcase": true,
9 | "eqeqeq": true,
10 | "forin": true,
11 | "newcap": true,
12 | "immed": true,
13 | "indent": false,
14 | "latedef": "nofunc",
15 | "noarg": true,
16 | "plusplus": false,
17 | "quotmark": "single",
18 | "undef": true,
19 | "unused": true,
20 | "strict": false,
21 | "trailing": false,
22 | "maxparams": false,
23 | "maxdepth": false,
24 | "maxstatements": false,
25 | "maxcomplexity": false,
26 | "maxlen": 200,
27 | "asi": false,
28 | "boss": false,
29 | "debug": true,
30 | "eqnull": false,
31 | "esnext": false,
32 | "evil": false,
33 | "funcscope": false,
34 | "smarttabs": false,
35 | "shadow": true,
36 | "supernew": true,
37 | "devel": true,
38 | "node": true,
39 | "white": false,
40 | "laxbreak": true,
41 | "validthis": true,
42 | "esnext": true
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Eryk Napierała
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | autocompleted-select
2 | ====================
3 |
4 | Select Web Component with autocompletion. Based on RxJS and VirtualDOM.
--------------------------------------------------------------------------------
/autocompleted-select.sublime-project:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/erykpiast/autocompleted-select/25dee28d02413a1522e70dea44b0e7e2b7da7e7e/autocompleted-select.sublime-project
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | autocompleted-select example
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | require('../src/js/index');
2 |
3 | var autocompletedSelect = document.createElement('autocompleted-select');
4 | autocompletedSelect.value = 'option1';
5 | autocompletedSelect.datalist = [ 'option1', 'option2' ].map((val) => [val]);
6 |
7 | document.body.appendChild(autocompletedSelect);
--------------------------------------------------------------------------------
/gulp/build/css.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var sass = require('gulp-sass');
6 | var sourcemaps = require('gulp-sourcemaps');
7 | var connect = require('gulp-connect');
8 | var autoprefixer = require('gulp-autoprefixer');
9 |
10 | var config = require('../config');
11 |
12 | function buildCssTask() {
13 | return gulp.src(config.src.css.files)
14 | .pipe(sourcemaps.init())
15 | .pipe(sass({
16 | errLogToConsole: true
17 | }))
18 | .on('error', function(err) {
19 | gutil.log('SASS error:', err);
20 | })
21 | .pipe(autoprefixer({
22 | browsers: [ '> 1%', 'last 2 versions' ],
23 | cascade: false
24 | }))
25 | .pipe(sourcemaps.write())
26 | .pipe(gulp.dest(config.dist.css.dir))
27 | .pipe(connect.reload());
28 | };
29 |
30 | module.exports = buildCssTask;
31 |
--------------------------------------------------------------------------------
/gulp/build/js.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var extend = require('extend');
6 | var connect = require('gulp-connect');
7 | var source = require('vinyl-source-stream');
8 | var watchify = require('watchify');
9 | var browserify = require('browserify');
10 | var babelify = require('babelify');
11 | var aliasify = require('aliasify');
12 | var brfs = require('brfs');
13 |
14 | var config = require('../config');
15 |
16 | module.exports = function buildJsTask(before) {
17 | var bundler = watchify(browserify(config.src.js.main, extend(watchify.args, {
18 | debug: true,
19 | entry: true
20 | })))
21 | .transform(brfs)
22 | .transform(babelify.configure({
23 | only: /^(?!.*node_modules)+.+\.js$/,
24 | sourceMap: 'inline',
25 | sourceMapRelative: __dirname
26 | }))
27 | .transform(aliasify.configure({
28 | aliases: {
29 | 'x-tag': '../../shims/x-tag.js'
30 | },
31 | configDir: __dirname
32 | }))
33 | .on('update', _build);
34 |
35 | function _build(changedFiles) {
36 | gutil.log('building is starting...');
37 |
38 | if(changedFiles) {
39 | gutil.log([ 'files to rebuild:' ].concat(changedFiles).join('\n'));
40 | }
41 |
42 | return before(changedFiles)
43 | .pipe(bundler.bundle())
44 | .on('error', function(err) {
45 | gutil.log('Browserify error:', err.message);
46 | })
47 | .pipe(source(config.dist.js.bundleName))
48 | .pipe(gulp.dest(config.dist.js.dir))
49 | .on('finish', function() {
50 | gutil.log('building finished!');
51 | })
52 | .pipe(connect.reload());
53 | };
54 |
55 |
56 |
57 | return function() {
58 | return _build();
59 | };
60 | }
--------------------------------------------------------------------------------
/gulp/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | src: {
5 | js: {
6 | files: [ './demo/index.js', './src/js/**/*.js', '!./src/js/**/spec/**/*.js' ],
7 | main: './demo/index.js'
8 | },
9 | css: {
10 | files: './src/css/**/*.scss'
11 | }
12 | },
13 | dist: {
14 | dir: './demo',
15 | js: {
16 | dir: './demo',
17 | bundleName: 'demo.bundle.js'
18 | },
19 | css: {
20 | dir: './demo'
21 | }
22 | },
23 | test: {
24 | files: './src/js/**/spec/**/*.spec.js',
25 | bundle: {
26 | name: 'tests.js',
27 | dir: './dist'
28 | },
29 | runtimeFiles: [ './test/phantomjs-extensions.js' ],
30 | runnerConfig: './karma.conf.js'
31 | }
32 | }
--------------------------------------------------------------------------------
/gulp/lint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 |
6 | var jshint = require('gulp-jshint');
7 | var stylish = require('jshint-stylish');
8 |
9 | var config = require('./config');
10 |
11 | module.exports = function lintTask(cb, files) {
12 | gutil.log('linting is starting...');
13 |
14 | return gulp.src(files || config.src.js.files)
15 | .pipe(jshint())
16 | .pipe(jshint.reporter(stylish))
17 | .pipe(jshint.reporter('fail'))
18 | .on('error', function(err) {
19 | gutil.log('linter error:', err.message);
20 | })
21 | .on('finish', function() {
22 | gutil.log('linting finished!');
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/gulp/test/build-watch.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var source = require('vinyl-source-stream');
6 | var watchify = require('watchify');
7 | var glob = require('glob');
8 | var browserify = require('browserify');
9 | var babelify = require('babelify');
10 | var aliasify = require('aliasify');
11 | var extend = require('extend');
12 | var brfs = require('brfs');
13 |
14 | var config = require('../config');
15 |
16 |
17 | module.exports = function buildTestsTask(before, after) {
18 | var bundler = watchify(browserify(extend(watchify.args, {
19 | debug: true,
20 | entry: true
21 | })));
22 |
23 | glob.sync(config.test.files).forEach(function(filePath) {
24 | bundler = bundler.add(filePath);
25 | });
26 |
27 | bundler = bundler
28 | .transform(brfs)
29 | .transform(babelify.configure({
30 | only: /^(?!.*node_modules)+.+\.js$/,
31 | sourceMap: 'inline',
32 | sourceMapRelative: __dirname
33 | }))
34 | .transform(aliasify.configure({
35 | aliases: {
36 | 'x-tag': '../../shims/x-tag.js'
37 | },
38 | configDir: __dirname
39 | }))
40 | .on('update', _build);
41 |
42 | var building = false;
43 | function _build(changedFiles) {
44 | gutil.log('building is starting...');
45 |
46 | if(changedFiles) {
47 | gutil.log([ 'files to rebuild:' ].concat(changedFiles).join('\n'));
48 | }
49 |
50 |
51 | if(!building) {
52 | building = true;
53 |
54 | return before(changedFiles)
55 | .pipe(bundler.bundle())
56 | .pipe(source(config.test.bundle.name))
57 | .pipe(gulp.dest(config.test.bundle.dir))
58 | .pipe(after(changedFiles))
59 | .on('error', function(err) {
60 | building = false;
61 |
62 | gutil.log('Building error', err.message);
63 | })
64 | .on('end', function() {
65 | building = false;
66 |
67 | gutil.log('Building finished!');
68 | });
69 | } else {
70 | return null;
71 | }
72 | };
73 |
74 | return function() {
75 | return _build();
76 | };
77 | };
78 |
--------------------------------------------------------------------------------
/gulp/test/build.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var source = require('vinyl-source-stream');
6 | var glob = require('glob');
7 | var browserify = require('browserify');
8 | var babelify = require('babelify');
9 | var aliasify = require('aliasify');
10 | var brfs = require('brfs');
11 |
12 | var config = require('../config');
13 |
14 | var bundler = (function createBundler() {
15 | var bundler = browserify({
16 | debug: true,
17 | entry: true
18 | });
19 |
20 | glob.sync(config.test.files).forEach(function(filePath) {
21 | bundler = bundler.add(filePath);
22 | });
23 |
24 | bundler = bundler
25 | .transform(brfs).transform(babelify.configure({
26 | only: /^(?!.*node_modules)+.+\.js$/,
27 | sourceMap: 'inline',
28 | sourceMapRelative: __dirname
29 | }))
30 | .transform(aliasify.configure({
31 | aliases: {
32 | 'x-tag': '../../shims/x-tag.js'
33 | },
34 | configDir: __dirname
35 | }));
36 |
37 | return bundler;
38 | })();
39 |
40 | function buildTestsTask() {
41 | return bundler.bundle()
42 | .on('error', function(err) {
43 | gutil.log('Browserify error:', err.message);
44 | })
45 | .pipe(source(config.test.bundle.name))
46 | .pipe(gulp.dest(config.test.bundle.dir));
47 | }
48 |
49 | module.exports = buildTestsTask;
50 |
--------------------------------------------------------------------------------
/gulp/test/lint.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 |
6 | var jshint = require('gulp-jshint');
7 | var stylish = require('jshint-stylish');
8 |
9 | var config = require('../config');
10 |
11 | module.exports = function lintTestsTask(cb, files) {
12 | gutil.log('linting is starting...');
13 |
14 | return gulp.src(files || config.test.files)
15 | .pipe(jshint())
16 | .pipe(jshint.reporter(stylish))
17 | .pipe(jshint.reporter('fail'))
18 | .on('error', function(err) {
19 | gutil.log('linter error:', err.message);
20 | })
21 | .on('finish', function() {
22 | gutil.log('linting finished!');
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/gulp/test/run.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var karma = require('gulp-karma');
6 |
7 | var config = require('../config');
8 |
9 | function runTestsTask() {
10 | return gulp.src([].concat(
11 | config.test.runtimeFiles,
12 | [ config.test.bundle.dir + '/' + config.test.bundle.name ]
13 | ))
14 | .pipe(karma({
15 | configFile: config.test.runnerConfig,
16 | action: 'run'
17 | }))
18 | .on('error', function(err) {
19 | gutil.log('Karma error:', err.message);
20 | });
21 | };
22 |
23 | module.exports = runTestsTask;
24 |
--------------------------------------------------------------------------------
/gulp/web-server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var connect = require('gulp-connect');
5 |
6 | var config = require('./config');
7 |
8 | module.exports = function webServerTask() {
9 | connect.server({
10 | root: [ config.dist.dir ],
11 | livereload: true
12 | });
13 | };
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp');
4 | var gutil = require('gulp-util');
5 | var runSequence = require('run-sequence');
6 |
7 | var config = require('./gulp/config');
8 |
9 | gulp.task('lint', require('./gulp/lint'));
10 | gulp.task('webserver', require('./gulp/web-server'));
11 |
12 | gulp.task('build:js', require('./gulp/build/js')(
13 | require('./gulp/lint').bind(null, null)
14 | ));
15 | gulp.task('build:css', require('./gulp/build/css'));
16 | gulp.task('_build', [ 'build:js', 'build:css' ]);
17 | gulp.task('build', function() {
18 | gulp.watch(config.src.css.files, [ 'build:css' ]);
19 | gulp.start([ '_build' ]);
20 | });
21 |
22 | gulp.task('test:lint', require('./gulp/test/lint'));
23 | gulp.task('test:build', require('./gulp/test/build'));
24 | gulp.task('test:build-watch', require('./gulp/test/build-watch')(
25 | require('./gulp/test/lint').bind(null, null),
26 | require('./gulp/test/run').bind(null, null)
27 | ));
28 | gulp.task('test:run', require('./gulp/test/run'));
29 | gulp.task('test', function(cb) {
30 | runSequence(
31 | [ 'lint', 'test:lint' ],
32 | 'test:build',
33 | 'test:run',
34 | function() {
35 | gutil.log('test task finished');
36 |
37 | cb();
38 | }
39 | );
40 | });
41 | gulp.task('test-dev', [ 'test:build-watch' ]);
42 |
43 | gulp.task('default', [ 'build', 'webserver' ]);
44 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | basePath: '.',
4 |
5 | frameworks: [ 'mocha' ],
6 |
7 | client: {
8 | mocha: {
9 | ui: 'tdd'
10 | }
11 | },
12 |
13 | files: [ /* definition in gulpfile */ ],
14 |
15 | customLaunchers: {
16 | Chrome_no_sandbox: {
17 | base: 'Chrome',
18 | flags: [ '--no-sandbox' ] // with sandbox it fails under Docker
19 | }
20 | },
21 |
22 | reporters: [ 'mocha' ],
23 | colors: true,
24 | logLevel: config.LOG_INFO,
25 | autoWatch: false,
26 |
27 | //*
28 | singleRun: true,
29 | port: 9876,
30 | browsers: [ 'PhantomJS' ]
31 | /*/
32 | singleRun: false,
33 | port: process.env.PORT,
34 | browsers: [ ]
35 | //*/
36 | });
37 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "autocompleted-select",
3 | "version": "0.1.0",
4 | "description": "Select Web Component with autocompletion. Based on RxJS and VirtualDOM.",
5 | "main": "src/js/index.js",
6 | "scripts": {
7 | "test": "gulp test"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git://github.com/erykpiast/autocompleted-select.git"
12 | },
13 | "keywords": [
14 | "browserify",
15 | "require",
16 | "seed",
17 | "boilerplate"
18 | ],
19 | "author": "Eryk Napierała ",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/erykpiast/autocompleted-select/issues"
23 | },
24 | "homepage": "https://github.com/erykpiast/autocompleted-select",
25 | "dependencies": {
26 | "cyclejs": "~0.20",
27 | "cyclejs-group": "~0.3",
28 | "foreach": "~2.0",
29 | "map-values": "~1.0",
30 | "ramda": "~0.13"
31 | },
32 | "devDependencies": {
33 | "aliasify": "~1.7",
34 | "babelify": "~6.0",
35 | "brfs": "~1.4",
36 | "browserify": "9.0.4",
37 | "chai": "~2.1",
38 | "chai-equal-collection": "erykpiast/chai-equal-collection",
39 | "cyclejs": "~0.20",
40 | "cyclejs-mock": "~0.1",
41 | "extend": "~2.0",
42 | "glob": "~5.0",
43 | "gulp": "~3.8",
44 | "gulp-autoprefixer": "~2.1",
45 | "gulp-connect": "~2.2",
46 | "gulp-copy": "~0.0",
47 | "gulp-jshint": "~1.10",
48 | "gulp-karma": "~0.0",
49 | "gulp-sass": "~1.3",
50 | "gulp-sourcemaps": "~1.3",
51 | "gulp-util": "~3.0",
52 | "jshint-stylish": "~1.0",
53 | "karma": "~0.12",
54 | "karma-chrome-launcher": "~0.1",
55 | "karma-mocha": "~0.1",
56 | "karma-mocha-reporter": "~1.0",
57 | "karma-phantomjs-launcher": "~0.1",
58 | "lodash.assign": "^3.0.0",
59 | "lodash.foreach": "~3.0",
60 | "mocha": "~2.2",
61 | "proxyquireify": "~1.2",
62 | "run-sequence": "~1.0",
63 | "vinyl-source-stream": "~1.1",
64 | "watchify": "~3.1"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/shims/x-tag.js:
--------------------------------------------------------------------------------
1 | module.exports = window.xtag;
--------------------------------------------------------------------------------
/src/css/styles.css:
--------------------------------------------------------------------------------
1 | div {
2 | display: inline-block;
3 | position: relative;
4 | }
5 |
6 | ul {
7 | background-color: #FFF;
8 | border: 1px solid rgb(169, 169, 169);
9 |
10 | list-style-type: none;
11 |
12 | box-sizing: border-box;
13 | max-height: calc(20 * 18px);
14 | padding: 0;
15 | margin: 0;
16 | width: max-content;
17 | width: -webkit-max-content;
18 | width: -moz-max-content;
19 | min-width: 100%;
20 | overflow-y: auto;
21 | overflow-x: hidden;
22 | position: absolute;
23 |
24 | display: none;
25 |
26 | z-index: 1;
27 | }
28 |
29 | ul.is-visible {
30 | display: block;
31 | }
32 |
33 | li {
34 | font: normal normal normal 13.3333330154419px/normal sans-serif;
35 | height: 18px;
36 | float: left;
37 | clear: left;
38 | width: 100%;
39 | overflow: hidden;
40 | white-space: nowrap;
41 | padding-right: .5em;
42 | }
43 |
44 | li:hover {
45 | cursor: pointer;
46 | }
47 |
48 | li.is-selected {
49 | background-color: rgb(200, 200, 200);
50 | }
51 |
52 | input:focus ~ ul > li.is-selected {
53 | background-color: rgb(30, 144, 255);
54 | }
55 |
56 | input[type=text].is-invalid {
57 | border-color: red;
58 | border-style: solid;
59 | outline-color: red;
60 |
61 | transition: border-color .2s ease-out, outline-color .2s ease-out;
62 | }
--------------------------------------------------------------------------------
/src/js/create-component.js:
--------------------------------------------------------------------------------
1 | import { Rx, createStream, render } from 'cyclejs';
2 | import mapValues from 'map-values';
3 |
4 |
5 | export default function createComponent(name, definitionFn, propsNames = []) {
6 | let proto = Object.create(HTMLElement.prototype);
7 | proto.createdCallback = function() {
8 | let props = {};
9 | propsNames.forEach((name) => {
10 | let value$ = props[name] = new Rx.Subject();
11 | let value = this[name];
12 |
13 | Object.defineProperty(this, name, {
14 | set(newValue) {
15 | value = newValue;
16 |
17 | value$.onNext(value);
18 |
19 | return value;
20 | },
21 | get() {
22 | return value;
23 | }
24 | });
25 | });
26 |
27 | let children$ = new Rx.Subject();
28 | (new MutationObserver((mutations) => {
29 | mutations
30 | .filter((mutation) => mutation.type === 'attributes')
31 | .filter((mutation) => mutation.target === this)
32 | .forEach((mutation) => {
33 | let name = mutation.attributeName;
34 |
35 | this[name] = this.getAttribute(name);
36 | });
37 |
38 | mutations
39 | .filter((mutation) => mutation.type !== 'attributes')
40 | .forEach(() => {
41 | children$.onNext(this.children);
42 | });
43 | })).observe(this, {
44 | subtree: true,
45 | attributes: true,
46 | attributeFilter: Object.keys(props)
47 | });
48 |
49 |
50 | this.shadowRoot = this.createShadowRoot();
51 | let interaction$ = createStream((vtree) => render(vtree, this.shadowRoot).interaction$);
52 |
53 | definitionFn.call(
54 | this,
55 | interaction$,
56 | mapValues(props, (prop$) => prop$.shareReplay(1)),
57 | children$.shareReplay(1)
58 | );
59 | };
60 |
61 | document.registerElement(name, {
62 | prototype: proto
63 | });
64 |
65 | }
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import createComponent from './create-component';
2 | import createGroup from 'cyclejs-group';
3 |
4 | import view from './view';
5 | import intent from './intent';
6 | import model from './model';
7 |
8 |
9 | createComponent('autocompleted-select', function(interaction$, props) {
10 | let view$$ = createGroup({ view$: view });
11 | let intent$$ = createGroup(intent);
12 | let model$$ = createGroup(model);
13 |
14 | interaction$.inject(view$$.view$);
15 | view$$.inject(model$$);
16 | model$$.inject(intent$$, {
17 | datalistAttr$: props.datalist
18 | }, model$$);
19 | intent$$.inject({ interaction$,
20 | valueAttr$: props.value
21 | }, intent$$);
22 |
23 | model$$.value$.skip(1).subscribe((value) => {
24 | this.value = value;
25 |
26 | this.dispatchEvent(new Event('change'));
27 | });
28 |
29 | }, [ 'value', 'datalist' ]);
30 |
--------------------------------------------------------------------------------
/src/js/intent.js:
--------------------------------------------------------------------------------
1 | import { Rx } from 'cyclejs';
2 |
3 | const UP = 38;
4 | const DOWN = 40;
5 | const ENTER = 13;
6 |
7 |
8 | export default {
9 | focusOnField$: (interaction$) => interaction$.choose('#field', 'focus'),
10 | blurOnField$: (interaction$) => interaction$.choose('#field', 'blur'),
11 | inputOnField$: (interaction$) => interaction$.choose('#field', 'input'),
12 | mouseenterOnAutocompletion$: (interaction$) => interaction$.choose('.autocompletion', 'mouseenter'),
13 | mousedownOnAutocompletion$: (interaction$) => interaction$.choose('.autocompletion', 'mousedown'),
14 | keydownOnField$: (interaction$) => interaction$.choose('#field', 'keydown'),
15 | up$: (keydownOnField$) => keydownOnField$.filter(({ keyCode }) => (keyCode === UP)),
16 | down$: (keydownOnField$) => keydownOnField$.filter(({ keyCode }) => (keyCode === DOWN)),
17 | enter$: (keydownOnField$) => keydownOnField$.filter(({ keyCode }) => (keyCode === ENTER)),
18 | notEnter$: (keydownOnField$) => keydownOnField$.filter(({ keyCode }) => (keyCode !== ENTER)),
19 | valueChange$: (inputOnField$, valueAttr$) =>
20 | Rx.Observable.merge(
21 | inputOnField$
22 | .map(({ target }) => target.value),
23 | valueAttr$
24 | ),
25 | selectedAutocompletionInput$: (mouseenterOnAutocompletion$, up$, down$) =>
26 | Rx.Observable.merge(
27 | mouseenterOnAutocompletion$
28 | .map(({ target }) => ({
29 | direct: target.index
30 | })),
31 | Rx.Observable.merge(
32 | up$.map(() => -1),
33 | down$.map(() => 1)
34 | ).map((modifier) => ({ modifier }))
35 | ),
36 | selectedAutocompletionChange$: (mousedownOnAutocompletion$, enter$) =>
37 | Rx.Observable.merge(
38 | enter$,
39 | mousedownOnAutocompletion$
40 | ).map(() => true),
41 | showAutocompletions$: (focusOnField$, notEnter$) =>
42 | Rx.Observable.merge(
43 | notEnter$,
44 | focusOnField$
45 | ).map(() => true),
46 | hideAutocompletions$: (blurOnField$, enter$) =>
47 | Rx.Observable.merge(
48 | enter$,
49 | blurOnField$
50 | ).map(() => true),
51 | finish$: (blurOnField$) =>
52 | blurOnField$.map(() => true)
53 | };
54 |
--------------------------------------------------------------------------------
/src/js/model.js:
--------------------------------------------------------------------------------
1 | import { Rx } from 'cyclejs';
2 |
3 | export default {
4 | // available autocompletions for current text value
5 | // autocompletions are sorted based on ranking, which is
6 | // calculated based on strict string matching and position of current field value in autocompletion
7 | // autocompetion "Car" will be higher than "Carusel" when text is "ca"
8 | // autocompletion "the best of all" will be higher than "he is the best" when text is "best"
9 | // autocompletions are case-insensitive
10 | autocompletions$: (textFieldValue$, datalistAttr$) =>
11 | Rx.Observable.combineLatest(
12 | textFieldValue$,
13 | datalistAttr$,
14 | (value, datalist) =>
15 | value.length ?
16 | datalist // choose keywords matching to value and sort them by ranking
17 | .map((keywords) => ({
18 | value: keywords[0], // show only the first keyword
19 | // but match all of them and choose one with the highest ranking
20 | score: Math.max.apply(Math, keywords.map(function(keyword, index) {
21 | var index = keyword.toLowerCase().indexOf(value.toLowerCase());
22 |
23 | if(index === -1) {
24 | return -Infinity;
25 | }
26 |
27 | return (100 - index * Math.abs(keyword.length - value.length));
28 | }))
29 | }))
30 | .filter(({ score }) => (score >= 0))
31 | .sort((a, b) => b.score - a.score)
32 | .map(({ value }) => value) :
33 | datalist // or show all and sort alphabetically if value is empty
34 | .map((keywords) => keywords[0])
35 | .sort()
36 | ).distinctUntilChanged((autocompletions) => JSON.stringify(autocompletions)),
37 |
38 | // autocompletions shouldn't be visible when text field is not focused, list is empty
39 | // and right after when autocompletion was choosen
40 | areAutocompletionsVisible$: (autocompletions$, showAutocompletions$, hideAutocompletions$) =>
41 | Rx.Observable.merge(
42 | autocompletions$
43 | .filter((autocompletions) => autocompletions.length === 0)
44 | .map(() => false),
45 | showAutocompletions$
46 | .withLatestFrom(
47 | autocompletions$,
48 | (show, autocompletions) => autocompletions.length !== 0
49 | ),
50 | hideAutocompletions$
51 | .map(() => false)
52 | )
53 | .startWith(false)
54 | .distinctUntilChanged(),
55 |
56 | // index of autocompletion selected on the list
57 | highlightedAutocompletionIndex$: (autocompletions$, valueChange$, hideAutocompletions$, selectedAutocompletionInput$) =>
58 | Rx.Observable.combineLatest(
59 | Rx.Observable.merge(
60 | Rx.Observable.merge( // reset position on text and when autocompletions list hides
61 | valueChange$,
62 | hideAutocompletions$
63 | )
64 | .map(() => ({ direct: 0 }))
65 | .delay(1), // reset after fetching value from autocompletions
66 | selectedAutocompletionInput$
67 | ),
68 | autocompletions$,
69 | (positionModifier, autocompletions) => ({ positionModifier, autocompletions })
70 | ).scan(0, (position, { positionModifier, autocompletions }) => {
71 | if(positionModifier.hasOwnProperty('modifier')) {
72 | position = position + positionModifier.modifier;
73 | } else if(positionModifier.hasOwnProperty('direct')) {
74 | position = positionModifier.direct;
75 | } else {
76 | return;
77 | }
78 |
79 | if(position < 0) {
80 | position = 0;
81 | } else if(position > (autocompletions.length - 1)) {
82 | position = autocompletions.length - 1;
83 | }
84 |
85 | return position;
86 | }).distinctUntilChanged(),
87 |
88 | // autocompletion that was highlighted and applied
89 | selectedAutocompletion$: (selectedAutocompletionChange$, highlightedAutocompletionIndex$, autocompletions$) =>
90 | selectedAutocompletionChange$
91 | .withLatestFrom(
92 | highlightedAutocompletionIndex$,
93 | (( enter, position ) => position)
94 | )
95 | .withLatestFrom(
96 | autocompletions$,
97 | ((position, autocompletions) => autocompletions[position])
98 | )
99 | .filter((value) => 'undefined' !== typeof value)
100 | .distinctUntilChanged(),
101 |
102 | // value is invalid when no autocompletions available
103 | isValueInvalid$: (autocompletions$, finish$) =>
104 | Rx.Observable.merge(
105 | autocompletions$
106 | .map((autocompletions) =>
107 | autocompletions.length === 0
108 | ),
109 | finish$
110 | .map(() => false)
111 | )
112 | .startWith(false)
113 | .distinctUntilChanged(),
114 |
115 | // text entered to field or propagated from component attribute
116 | notValidatedTextFieldValue$: (valueChange$) => valueChange$
117 | .distinctUntilChanged(),
118 |
119 | // current text in field, can be entered directly or by choosing autocompletion
120 | // when text field looses focus and value is not valid, previous valid value
121 | textFieldValue$: (value$, notValidatedTextFieldValue$, finish$, selectedAutocompletion$) =>
122 | Rx.Observable.merge(
123 | notValidatedTextFieldValue$,
124 | selectedAutocompletion$,
125 | finish$
126 | .withLatestFrom(
127 | value$,
128 | (finish, value) => value
129 | )
130 | )
131 | .startWith('')
132 | .distinctUntilChanged(),
133 |
134 | // value that will be exported to attribute and emitted with change event of the component
135 | // it has to match exactly with some autocompletion
136 | value$: (autocompletions$, selectedAutocompletion$, finish$, notValidatedTextFieldValue$) =>
137 | Rx.Observable.merge(
138 | selectedAutocompletion$,
139 | Rx.Observable.combineLatest(
140 | notValidatedTextFieldValue$.take(1),
141 | autocompletions$.take(1),
142 | (value, autocompletions) => value === autocompletions[0] ? value : null
143 | ),
144 | finish$
145 | .withLatestFrom(
146 | notValidatedTextFieldValue$,
147 | autocompletions$,
148 | (finish, value, autocompletions) => value === autocompletions[0] ? value : null
149 | )
150 | .filter((value) => value !== null)
151 | ).distinctUntilChanged()
152 | };
153 |
154 |
--------------------------------------------------------------------------------
/src/js/spec/intent.spec.js:
--------------------------------------------------------------------------------
1 | /* global suite, test */
2 |
3 | import chai from 'chai';
4 | import { assert } from 'chai';
5 |
6 | // has to be imported before Rx
7 | import inject from 'cyclejs-mock';
8 |
9 | import { Rx } from 'cyclejs';
10 | import each from 'foreach';
11 |
12 | import intent from '../intent';
13 |
14 | chai.use(require('chai-equal-collection')(Rx.internals.isEqual));
15 |
16 |
17 | suite('intent', () => {
18 |
19 | suite('API', () => {
20 |
21 | test('Should be an object with functions as properties', () => {
22 |
23 | assert.isObject(intent);
24 |
25 | each(intent, (prop) => assert.isFunction(prop));
26 | });
27 |
28 | });
29 |
30 |
31 | suite('events mapping', () => {
32 |
33 | suite('focusOnField$', () => {
34 |
35 | test('should listen on focus event on field',
36 | inject((createObservable, onNext, mockInteractions, getValues) => {
37 | let focusOnField$ = intent.focusOnField$(mockInteractions({
38 | '#field@focus': createObservable(
39 | onNext(100, 'something'),
40 | onNext(200, 'anything')
41 | )
42 | }));
43 |
44 | let results = getValues(focusOnField$);
45 |
46 | assert.equalCollection(
47 | results,
48 | [ 'something', 'anything' ]
49 | );
50 |
51 | }));
52 |
53 | });
54 |
55 |
56 | suite('blurOnField$', () => {
57 |
58 | test('should listen on focus event on field',
59 | inject((createObservable, onNext, mockInteractions, getValues) => {
60 | let blurOnField$ = intent.blurOnField$(mockInteractions({
61 | '#field@blur': createObservable(
62 | onNext(100, 'something'),
63 | onNext(200, 'anything')
64 | )
65 | }));
66 |
67 | let results = getValues(blurOnField$);
68 |
69 | assert.equalCollection(
70 | results,
71 | [ 'something', 'anything' ]
72 | );
73 |
74 | }));
75 |
76 | });
77 |
78 |
79 | suite('inputOnField$', () => {
80 |
81 | test('should listen on focus event on field',
82 | inject((createObservable, onNext, mockInteractions, getValues) => {
83 | let inputOnField$ = intent.inputOnField$(mockInteractions({
84 | '#field@input': createObservable(
85 | onNext(100, 'something'),
86 | onNext(200, 'anything')
87 | )
88 | }));
89 |
90 | let results = getValues(inputOnField$);
91 |
92 | assert.equalCollection(
93 | results,
94 | [ 'something', 'anything' ]
95 | );
96 |
97 | }));
98 |
99 | });
100 |
101 |
102 | suite('mouseenterOnAutocompletion$', () => {
103 |
104 | test('should listen on focus event on field',
105 | inject((createObservable, onNext, mockInteractions, getValues) => {
106 | let mouseenterOnAutocompletion$ = intent.mouseenterOnAutocompletion$(mockInteractions({
107 | '.autocompletion@mouseenter': createObservable(
108 | onNext(100, 'something'),
109 | onNext(200, 'anything')
110 | )
111 | }));
112 |
113 | let results = getValues(mouseenterOnAutocompletion$);
114 |
115 | assert.equalCollection(
116 | results,
117 | [ 'something', 'anything' ]
118 | );
119 |
120 | }));
121 |
122 | });
123 |
124 |
125 | suite('mousedownOnAutocompletion$', () => {
126 |
127 | test('should listen on focus event on field',
128 | inject((createObservable, onNext, mockInteractions, getValues) => {
129 | let mousedownOnAutocompletion$ = intent.mousedownOnAutocompletion$(mockInteractions({
130 | '.autocompletion@mousedown': createObservable(
131 | onNext(100, 'something'),
132 | onNext(200, 'anything')
133 | )
134 | }));
135 |
136 | let results = getValues(mousedownOnAutocompletion$);
137 |
138 | assert.equalCollection(
139 | results,
140 | [ 'something', 'anything' ]
141 | );
142 |
143 | }));
144 |
145 | });
146 |
147 |
148 | suite('keydownOnField$', () => {
149 |
150 | test('should listen on focus event on field',
151 | inject((createObservable, onNext, mockInteractions, getValues) => {
152 | let keydownOnField$ = intent.keydownOnField$(mockInteractions({
153 | '#field@keydown': createObservable(
154 | onNext(100, 'something'),
155 | onNext(200, 'anything')
156 | )
157 | }));
158 |
159 | let results = getValues(keydownOnField$);
160 |
161 | assert.equalCollection(
162 | results,
163 | [ 'something', 'anything' ]
164 | );
165 |
166 | }));
167 |
168 | });
169 |
170 | });
171 |
172 |
173 | suite('I/O', () => {
174 |
175 | suite('valueChange$', () => {
176 |
177 | test('should change value every time value attribute changes',
178 | inject((createObservable, onNext, callWithObservables, getValues) => {
179 | let valueChange$ = callWithObservables(intent.valueChange$, {
180 | valueAttr$: createObservable(
181 | onNext(100, 'abc'),
182 | onNext(200, 'def'),
183 | onNext(250, 'ghi')
184 | )
185 | });
186 |
187 | let results = getValues(valueChange$);
188 |
189 | assert.equalCollection(
190 | results,
191 | [ 'abc', 'def', 'ghi' ]
192 | );
193 |
194 | }));
195 |
196 |
197 | test('should change value every time input event on field is emitted',
198 | inject((createObservable, onNext, callWithObservables, getValues) => {
199 | let valueChange$ = callWithObservables(intent.valueChange$, {
200 | inputOnField$: createObservable(
201 | onNext(100, { target: { value: 'abc' } }),
202 | onNext(200, { target: { value: 'def' } }),
203 | onNext(250, { target: { value: 'ghi' } })
204 | )
205 | });
206 |
207 | let results = getValues(valueChange$);
208 |
209 | assert.equalCollection(
210 | results,
211 | [ 'abc', 'def', 'ghi' ]
212 | );
213 |
214 | }));
215 |
216 | });
217 |
218 |
219 | suite('selectedAutocompletionInput$', () => {
220 |
221 | test('should change index directly every time mouse is over autocompletion',
222 | inject((createObservable, onNext, callWithObservables, getValues) => {
223 | let selectedAutocompletionInput$ = callWithObservables(intent.selectedAutocompletionInput$, {
224 | mouseenterOnAutocompletion$: createObservable(
225 | onNext(100, { target: { index: 10 } }),
226 | onNext(200, { target: { index: 11 } }),
227 | onNext(250, { target: { index: 12 } })
228 | )
229 | });
230 |
231 | let results = getValues(selectedAutocompletionInput$);
232 |
233 | assert.equalCollection(
234 | results,
235 | [ { direct: 10 }, { direct: 11 }, { direct: 12 } ]
236 | );
237 |
238 | }));
239 |
240 |
241 | test('should modify index every time up or down arrow key is pressed',
242 | inject((createObservable, onNext, callWithObservables, getValues) => {
243 | let selectedAutocompletionInput$ = callWithObservables(intent.selectedAutocompletionInput$, {
244 | up$: createObservable(
245 | onNext(100, 'something'),
246 | onNext(200, 'anything'),
247 | onNext(250, 'what else')
248 | ),
249 | down$: createObservable(
250 | onNext(120, 'something'),
251 | onNext(180, 'anything'),
252 | onNext(280, 'what else')
253 | )
254 | });
255 |
256 | let results = getValues(selectedAutocompletionInput$);
257 |
258 | assert.equalCollection(
259 | results,
260 | [ { modifier: -1 }, { modifier: 1 }, { modifier: 1 }, { modifier: -1 }, { modifier: -1 }, { modifier: 1 } ]
261 | );
262 |
263 | }));
264 |
265 | });
266 |
267 |
268 | suite('selectedAutocompletionChange$', () => {
269 |
270 | test('should trigger change every time mouse button is pressed over autocompletion',
271 | inject((createObservable, onNext, callWithObservables, getValues) => {
272 | let selectedAutocompletionChange$ = callWithObservables(intent.selectedAutocompletionChange$, {
273 | mousedownOnAutocompletion$: createObservable(
274 | onNext(100, 'something'),
275 | onNext(200, 'anything')
276 | )
277 | });
278 |
279 | let results = getValues(selectedAutocompletionChange$);
280 |
281 | assert.equalCollection(
282 | results,
283 | [ true, true ]
284 | );
285 |
286 | }));
287 |
288 |
289 | test('should trigger change every time enter key is pressed',
290 | inject((createObservable, onNext, callWithObservables, getValues) => {
291 | let selectedAutocompletionChange$ = callWithObservables(intent.selectedAutocompletionChange$, {
292 | enter$: createObservable(
293 | onNext(100, 'something'),
294 | onNext(200, 'anything')
295 | )
296 | });
297 |
298 | let results = getValues(selectedAutocompletionChange$);
299 |
300 | assert.equalCollection(
301 | results,
302 | [ true, true ]
303 | );
304 |
305 | }));
306 |
307 | });
308 |
309 |
310 | suite('showAutocompletions$', () => {
311 |
312 | test('should show autocompletion every time any key except enter is pressed',
313 | inject((createObservable, onNext, callWithObservables, getValues) => {
314 | let showAutocompletions$ = callWithObservables(intent.showAutocompletions$, {
315 | notEnter$: createObservable(
316 | onNext(100, 'something'),
317 | onNext(200, 'anything')
318 | )
319 | });
320 |
321 | let results = getValues(showAutocompletions$);
322 |
323 | assert.equalCollection(
324 | results,
325 | [ true, true ]
326 | );
327 |
328 | }));
329 |
330 |
331 | test('should hide autocompletions every time field is focused',
332 | inject((createObservable, onNext, callWithObservables, getValues) => {
333 | let showAutocompletions$ = callWithObservables(intent.showAutocompletions$, {
334 | focusOnField$: createObservable(
335 | onNext(100, 'something'),
336 | onNext(200, 'anything')
337 | )
338 | });
339 |
340 | let results = getValues(showAutocompletions$);
341 |
342 | assert.equalCollection(
343 | results,
344 | [ true, true ]
345 | );
346 |
347 | }));
348 |
349 | });
350 |
351 |
352 | suite('hideAutocompletions$', () => {
353 |
354 | test('should hide autocompletions every time enter key is pressed',
355 | inject((createObservable, onNext, callWithObservables, getValues) => {
356 | let hideAutocompletions$ = callWithObservables(intent.hideAutocompletions$, {
357 | enter$: createObservable(
358 | onNext(100, 'something'),
359 | onNext(200, 'anything')
360 | )
361 | });
362 |
363 | let results = getValues(hideAutocompletions$);
364 |
365 | assert.equalCollection(
366 | results,
367 | [ true, true ]
368 | );
369 |
370 | }));
371 |
372 |
373 | test('should hide autocompletions every time field is blured',
374 | inject((createObservable, onNext, callWithObservables, getValues) => {
375 | let hideAutocompletions$ = callWithObservables(intent.hideAutocompletions$, {
376 | blurOnField$: createObservable(
377 | onNext(100, 'something'),
378 | onNext(200, 'anything')
379 | )
380 | });
381 |
382 | let results = getValues(hideAutocompletions$);
383 |
384 | assert.equalCollection(
385 | results,
386 | [ true, true ]
387 | );
388 |
389 | }));
390 |
391 | });
392 |
393 |
394 | suite('finish$', () => {
395 |
396 | test('should trigger editing finish every time field is blured',
397 | inject((createObservable, onNext, callWithObservables, getValues) => {
398 | let finish$ = callWithObservables(intent.finish$, {
399 | blurOnField$: createObservable(
400 | onNext(100, 'something'),
401 | onNext(200, 'anything')
402 | )
403 | });
404 |
405 | let results = getValues(finish$);
406 |
407 | assert.equalCollection(
408 | results,
409 | [ true, true ]
410 | );
411 |
412 | }));
413 |
414 | });
415 |
416 | });
417 |
418 | });
--------------------------------------------------------------------------------
/src/js/spec/mock.js:
--------------------------------------------------------------------------------
1 | // it's the same instance that Cycle uses
2 | import { Rx } from 'cyclejs';
3 | // this call extends Rx above with VirtualTime class and HAS TO be included BEFORE rx.testing
4 | import 'cyclejs/node_modules/rx/dist/rx.virtualtime';
5 | import 'cyclejs/node_modules/rx/dist/rx.testing';
6 |
7 | import createElement from 'cyclejs/node_modules/virtual-dom/create-element';
8 |
9 |
10 |
11 | import getParametersNames from 'get-parameter-names';
12 |
13 | import Depender from 'depender';
14 |
15 |
16 | export default function injectTestingUtils(fn) {
17 | var scheduler = new Rx.TestScheduler();
18 |
19 | /**
20 | * @function callWithObservables - calls function with arguments specified as keys of the object
21 | * @param {object} [observables={}] - collection of observables to use as the function arguments
22 | * @property {*} observables[observableName] - definition of Observable mock for given name
23 | * if value other than observable is provided, hot Observable starting with this value is created
24 | * if any of function argument is missing, empty hot Observable is created
25 | * @returns {Observable} value returned from the function
26 | */
27 | function callWithObservables(fn, observables = {}, ctx = null) {
28 | var args = { };
29 |
30 | Object.keys(observables).forEach((name) => {
31 | if(observables[name] instanceof Rx.Observable) {
32 | args[name] = observables[name];
33 | } else {
34 | args[name] = createObservable(
35 | Rx.ReactiveTest.onNext(2, observables[name])
36 | );
37 | }
38 | });
39 |
40 | var fnArgs = getParametersNames(fn);
41 |
42 | fnArgs.forEach((name) => {
43 | if(!(args[name] instanceof Rx.Observable)) {
44 | args[name] = createObservable();
45 | }
46 | });
47 |
48 | return fn.apply(ctx, fnArgs.map((name) =>
49 | args[name].tap(console.log.bind(console, name))
50 | )
51 | );
52 | }
53 |
54 |
55 | function mockInteractions(definitionObj = {}) {
56 | var eventsMap = { };
57 |
58 | Object.keys(definitionObj).forEach((name) => {
59 | var [ selector, event ] = name.split('@');
60 |
61 | if(!eventsMap.hasOwnProperty(selector)) {
62 | eventsMap[selector] = { };
63 | }
64 |
65 | eventsMap[selector][event] = definitionObj[name];
66 | });
67 |
68 | return {
69 | choose: (selector, event) => {
70 | if(!eventsMap.hasOwnProperty(selector)) {
71 | eventsMap[selector] = { };
72 | }
73 |
74 | if(!eventsMap[selector].hasOwnProperty(event)) {
75 | eventsMap[selector][event] = createObservable();
76 | }
77 |
78 | return eventsMap[selector][event]
79 | .tap(console.log.bind(console, selector, event));
80 | }
81 | };
82 | }
83 |
84 |
85 | function getValues(observable) {
86 | return scheduler.startWithTiming(
87 | () => observable,
88 | 1,
89 | 10,
90 | 100000
91 | ).messages.map((message) => message.value.value);
92 | }
93 |
94 |
95 | function createObservable(...args) {
96 | return scheduler.createColdObservable(...args).shareReplay(1);
97 | }
98 |
99 |
100 | var injector = new Depender();
101 |
102 | injector.define('mockInteractions', mockInteractions);
103 | injector.define('callWithObservables', callWithObservables);
104 | injector.define('createObservable', createObservable);
105 | injector.define('render', createElement);
106 | injector.define('getValues', getValues);
107 | injector.define('onNext', (...args) => Rx.ReactiveTest.onNext(...args));
108 |
109 | return function() {
110 | return injector.use(fn);
111 | };
112 | }
--------------------------------------------------------------------------------
/src/js/spec/model.spec.js:
--------------------------------------------------------------------------------
1 | /* global suite, test */
2 |
3 | import chai from 'chai';
4 | import { assert } from 'chai';
5 |
6 | // has to be imported before Rx
7 | import inject from 'cyclejs-mock';
8 |
9 | import { Rx } from 'cyclejs';
10 | import each from 'foreach';
11 |
12 | import model from '../model';
13 |
14 | chai.use(require('chai-equal-collection')(Rx.internals.isEqual));
15 |
16 |
17 | suite('model', () => {
18 |
19 | suite('API', () => {
20 |
21 | test('Should be an object with functions as properties', () => {
22 |
23 | assert.isObject(model);
24 |
25 | each(model, (prop) => assert.isFunction(prop));
26 | });
27 |
28 | });
29 |
30 |
31 | suite('I/O', () => {
32 |
33 | suite('autocompletions$', () => {
34 |
35 | test('should change value every time value changes',
36 | inject((createObservable, onNext, callWithObservables, getValues) => {
37 | let autocompletions$ = callWithObservables(model.autocompletions$, {
38 | textFieldValue$: createObservable(
39 | onNext(100, 'abc'),
40 | onNext(200, 'def'),
41 | onNext(250, 'ghi')
42 | ),
43 | datalistAttr$: [ [ 'abc1' ], [ 'abc2' ], [ 'def' ], [ 'xyz' ] ]
44 | });
45 |
46 | let results = getValues(autocompletions$);
47 |
48 | assert.equal(results.length, 3);
49 |
50 | }));
51 |
52 |
53 | test('should change value every time datalist changes',
54 | inject((createObservable, onNext, callWithObservables, getValues) => {
55 | let autocompletions$ = callWithObservables(model.autocompletions$, {
56 | textFieldValue$: 'abc',
57 | datalistAttr$: createObservable(
58 | onNext(100, [ [ 'abc1' ], [ 'abc2' ], [ 'def' ], [ 'xyz' ] ]),
59 | onNext(200, [ [ 'abc2' ], [ 'def' ] ]),
60 | onNext(250, [ [ 'abc1' ], [ 'xyz' ] ])
61 | )
62 | });
63 |
64 | let results = getValues(autocompletions$);
65 |
66 | assert.equal(results.length, 3);
67 |
68 | }));
69 |
70 |
71 | test('should show only autocompletions matching with current value',
72 | inject((createObservable, onNext, callWithObservables, getValues) => {
73 | let autocompletions$ = callWithObservables(model.autocompletions$, {
74 | textFieldValue$: createObservable(
75 | onNext(100, 'abc'),
76 | onNext(200, 'def'),
77 | onNext(250, 'ghi')
78 | ),
79 | datalistAttr$: [ [ 'abc1' ], [ 'abc2' ], [ 'definition' ], [ 'undefined' ], [ 'xyz' ] ]
80 | });
81 |
82 | let results = getValues(autocompletions$);
83 |
84 | assert.equalCollection(
85 | results,
86 | [
87 | [ 'abc1', 'abc2' ],
88 | [ 'definition', 'undefined' ],
89 | [ ]
90 | ]
91 | );
92 |
93 | }));
94 |
95 | });
96 |
97 |
98 | suite('areAutocompletionsVisible$', () => {
99 |
100 | test('should be hidden by default',
101 | inject((createObservable, onNext, callWithObservables, getValues) => {
102 | let areAutocompletionsVisible$ = callWithObservables(model.areAutocompletionsVisible$, { });
103 |
104 | let results = getValues(areAutocompletionsVisible$);
105 |
106 | assert.equalCollection(
107 | results,
108 | [ false ]
109 | );
110 |
111 | }));
112 |
113 | test('should hide autocompletions when there is nothing to show',
114 | inject((createObservable, onNext, callWithObservables, getValues) => {
115 | let areAutocompletionsVisible$ = callWithObservables(model.areAutocompletionsVisible$, {
116 | autocompletions$: createObservable(
117 | onNext(100, [ 'abc' ]),
118 | onNext(200, [ ])
119 | ),
120 | showAutocompletions$: createObservable(
121 | onNext(101, 'something')
122 | )
123 | });
124 |
125 | let results = getValues(areAutocompletionsVisible$);
126 |
127 | assert.equalCollection(
128 | results,
129 | [ false, true, false ]
130 | );
131 |
132 | }));
133 |
134 |
135 | test('should hide autocompletions when show request is performed and hide when hide request is performed',
136 | inject((createObservable, onNext, callWithObservables, getValues) => {
137 | let areAutocompletionsVisible$ = callWithObservables(model.areAutocompletionsVisible$, {
138 | showAutocompletions$: createObservable(
139 | onNext(101, [ 'something' ]),
140 | onNext(201, [ 'anything' ])
141 | ),
142 | hideAutocompletions$: createObservable(
143 | onNext(150, [ 'something' ]),
144 | onNext(250, [ 'anything' ])
145 | ),
146 | autocompletions$: createObservable(
147 | onNext(100, [ 'abc' ]),
148 | onNext(200, [ 'def' ])
149 | )
150 | });
151 |
152 | let results = getValues(areAutocompletionsVisible$);
153 |
154 | assert.equalCollection(
155 | results,
156 | [ false, true, false, true, false ]
157 | );
158 |
159 | }));
160 |
161 |
162 | test('should not show autocompletions when show request is performed but there is no autocompletion',
163 | inject((createObservable, onNext, callWithObservables, getValues) => {
164 | let areAutocompletionsVisible$ = callWithObservables(model.areAutocompletionsVisible$, {
165 | showAutocompletions$: createObservable(
166 | onNext(101, [ 'something' ]),
167 | onNext(201, [ 'anything' ])
168 | ),
169 | autocompletions$: createObservable(
170 | onNext(100, [ ]),
171 | onNext(150, [ 'abc' ]),
172 | onNext(200, [ ])
173 | )
174 | });
175 |
176 | let results = getValues(areAutocompletionsVisible$);
177 |
178 | assert.equalCollection(
179 | results,
180 | [ false ]
181 | );
182 |
183 | }));
184 |
185 |
186 | test('should emit value only if it is different than previous one',
187 | inject((createObservable, onNext, callWithObservables, getValues) => {
188 | let areAutocompletionsVisible$ = callWithObservables(model.areAutocompletionsVisible$, {
189 | autocompletions$: createObservable(
190 | onNext(99, [ 'def' ]),
191 | onNext(101, [ 'definition', 'undefined' ]),
192 | onNext(199, [ ]),
193 | onNext(200, [ 'xyz' ]),
194 | onNext(250, [ 'abc' ]),
195 | onNext(251, [ ])
196 | ),
197 | showAutocompletions$: createObservable(
198 | onNext(100, [ 'something' ]),
199 | onNext(201, [ 'anything' ])
200 | ),
201 | hideAutocompletions$: createObservable(
202 | onNext(150, [ 'something' ]),
203 | onNext(250, [ 'anything' ])
204 | )
205 | });
206 |
207 | let results = getValues(areAutocompletionsVisible$);
208 |
209 | assert.equalCollection(
210 | results,
211 | [ false, true, false, true, false ]
212 | );
213 |
214 | }));
215 |
216 | });
217 |
218 |
219 | suite('highlightedAutocompletionIndex$', () => {
220 |
221 | test('should change index value when it changes directly',
222 | inject((createObservable, onNext, callWithObservables, getValues) => {
223 | let highlightedAutocompletionIndex$ = callWithObservables(model.highlightedAutocompletionIndex$, {
224 | selectedAutocompletionInput$: createObservable(
225 | onNext(100, { direct: 2 }),
226 | onNext(200, { direct: 1 }),
227 | onNext(300, { direct: 3 }),
228 | onNext(400, { direct: 0 }),
229 | onNext(500, { direct: 2 })
230 | ),
231 | autocompletions$: [ 'abc1', 'abc2', 'adef', 'axyz' ]
232 | });
233 |
234 | let results = getValues(highlightedAutocompletionIndex$);
235 |
236 | assert.equalCollection(
237 | results,
238 | [ 2, 1, 3, 0, 2 ]
239 | );
240 |
241 | }));
242 |
243 |
244 | test('should change index value when it changes by modifier',
245 | inject((createObservable, onNext, callWithObservables, getValues) => {
246 | let highlightedAutocompletionIndex$ = callWithObservables(model.highlightedAutocompletionIndex$, {
247 | selectedAutocompletionInput$: createObservable(
248 | onNext(100, { modifier: 1 }),
249 | onNext(200, { modifier: 1 }),
250 | onNext(300, { modifier: -1 }),
251 | onNext(400, { modifier: -1 }),
252 | onNext(500, { modifier: 1 })
253 | ),
254 | autocompletions$: [ 'abc1', 'abc2', 'adef', 'axyz' ]
255 | });
256 |
257 | let results = getValues(highlightedAutocompletionIndex$);
258 |
259 | assert.equalCollection(
260 | results,
261 | [ 1, 2, 1, 0, 1 ]
262 | );
263 |
264 | }));
265 |
266 | });
267 |
268 |
269 | suite('selectedAutocompletion$', () => {
270 |
271 | test('should change selected autocompletion on change intent',
272 | inject((createObservable, onNext, callWithObservables, getValues) => {
273 | let selectedAutocompletion$ = callWithObservables(model.selectedAutocompletion$, {
274 | selectedAutocompletionChange$: createObservable(
275 | onNext(101, 'something'),
276 | onNext(201, 'anything')
277 | ),
278 | highlightedAutocompletionIndex$: createObservable(
279 | onNext(100, 1),
280 | onNext(200, 2)
281 | ),
282 | autocompletions$: [ 'abc', 'abc1', 'abc2' ]
283 | });
284 |
285 | let results = getValues(selectedAutocompletion$);
286 |
287 | assert.equalCollection(
288 | results,
289 | [ 'abc1', 'abc2' ]
290 | );
291 |
292 | }));
293 |
294 |
295 | test('should change selected autocompletion only if it is different than previous one',
296 | inject((createObservable, onNext, callWithObservables, getValues) => {
297 | let selectedAutocompletion$ = callWithObservables(model.selectedAutocompletion$, {
298 | selectedAutocompletionChange$: createObservable(
299 | onNext(101, 'something'),
300 | onNext(201, 'anything')
301 | ),
302 | highlightedAutocompletionIndex$: createObservable(
303 | onNext(100, 1)
304 | ),
305 | autocompletions$: [ 'abc', 'abc1', 'abc2' ]
306 | });
307 |
308 | let results = getValues(selectedAutocompletion$);
309 |
310 | assert.equalCollection(
311 | results,
312 | [ 'abc1' ]
313 | );
314 |
315 | }));
316 |
317 | });
318 |
319 |
320 | suite('isValueInvalid$', () => {
321 |
322 | test('should be value invalid if there is no autocompletion and valid when there is some',
323 | inject((createObservable, onNext, callWithObservables, getValues) => {
324 | let isValueInvalid$ = callWithObservables(model.isValueInvalid$, {
325 | autocompletions$: createObservable(
326 | onNext(100, [ 'abc', 'abc1' ]),
327 | onNext(200, [ ]),
328 | onNext(300, [ 'xyz' ])
329 | )
330 | });
331 |
332 | let results = getValues(isValueInvalid$);
333 |
334 | assert.equalCollection(
335 | results,
336 | [ false, true, false ]
337 | );
338 |
339 | }));
340 |
341 |
342 | test('should be value not invalid when editing is finished',
343 | inject((createObservable, onNext, callWithObservables, getValues) => {
344 | let isValueInvalid$ = callWithObservables(model.isValueInvalid$, {
345 | finish$: createObservable(
346 | onNext(101, 'something'),
347 | onNext(201, 'anything')
348 | ),
349 | autocompletions$: createObservable(
350 | onNext(100, [ ]),
351 | onNext(200, [ ])
352 | )
353 | });
354 |
355 | let results = getValues(isValueInvalid$);
356 |
357 | assert.equalCollection(
358 | results,
359 | [ false, true, false, true, false ]
360 | );
361 |
362 | }));
363 |
364 | });
365 |
366 |
367 | suite('notValidatedTextFieldValue$', () => {
368 |
369 | test('should pass every value from valueChange$',
370 | inject((createObservable, onNext, callWithObservables, getValues) => {
371 | let notValidatedTextFieldValue$ = callWithObservables(model.notValidatedTextFieldValue$, {
372 | valueChange$: createObservable(
373 | onNext(100, 'something'),
374 | onNext(200, 'anything'),
375 | onNext(300, 'xxx')
376 | )
377 | });
378 |
379 | let results = getValues(notValidatedTextFieldValue$);
380 |
381 | assert.equalCollection(
382 | results,
383 | [ 'something', 'anything', 'xxx' ]
384 | );
385 |
386 | }));
387 |
388 | });
389 |
390 |
391 | suite('textFieldValue$', () => {
392 |
393 | test('should start with empty value',
394 | inject((createObservable, onNext, callWithObservables, getValues) => {
395 | let textFieldValue$ = callWithObservables(model.textFieldValue$, { });
396 |
397 | let results = getValues(textFieldValue$);
398 |
399 | assert.equalCollection(
400 | results,
401 | [ '' ]
402 | );
403 |
404 | }));
405 |
406 | test('should change every time text field value changes',
407 | inject((createObservable, onNext, callWithObservables, getValues) => {
408 | let textFieldValue$ = callWithObservables(model.textFieldValue$, {
409 | notValidatedTextFieldValue$: createObservable(
410 | onNext(101, 'abc'),
411 | onNext(201, '123'),
412 | onNext(301, 'xyz')
413 | )
414 | });
415 |
416 | let results = getValues(textFieldValue$);
417 |
418 | assert.equalCollection(
419 | results,
420 | [ '', 'abc', '123', 'xyz' ]
421 | );
422 |
423 | }));
424 |
425 |
426 | test('should change every time selected autocompletion changes',
427 | inject((createObservable, onNext, callWithObservables, getValues) => {
428 | let textFieldValue$ = callWithObservables(model.textFieldValue$, {
429 | selectedAutocompletion$: createObservable(
430 | onNext(101, 'abc'),
431 | onNext(201, '123'),
432 | onNext(301, 'xyz')
433 | )
434 | });
435 |
436 | let results = getValues(textFieldValue$);
437 |
438 | assert.equalCollection(
439 | results,
440 | [ '', 'abc', '123', 'xyz' ]
441 | );
442 |
443 | }));
444 |
445 |
446 | test('should change every time edition finishes with latest distinct value from value$',
447 | inject((createObservable, onNext, callWithObservables, getValues) => {
448 | let textFieldValue$ = callWithObservables(model.textFieldValue$, {
449 | finish$: createObservable(
450 | onNext(101, 'something'),
451 | onNext(201, 'anything'),
452 | onNext(301, 'whatever')
453 | ),
454 | value$: createObservable(
455 | onNext(100, 'abc'),
456 | onNext(300, 'xyz')
457 | )
458 | });
459 |
460 | let results = getValues(textFieldValue$);
461 |
462 | assert.equalCollection(
463 | results,
464 | [ '', 'abc', 'xyz' ]
465 | );
466 |
467 | }));
468 |
469 | });
470 |
471 |
472 | suite('value$', () => {
473 |
474 | test('should value change every time selected autocompletion changes',
475 | inject((createObservable, onNext, callWithObservables, getValues) => {
476 | let value$ = callWithObservables(model.value$, {
477 | selectedAutocompletion$: createObservable(
478 | onNext(100, 'abc1'),
479 | onNext(200, 'abc2')
480 | )
481 | });
482 |
483 | let results = getValues(value$);
484 |
485 | assert.equalCollection(
486 | results,
487 | [ 'abc1', 'abc2' ]
488 | );
489 |
490 | }));
491 |
492 |
493 | test('should value change every time editing finishes and there is some autocompletion for current text field value',
494 | inject((createObservable, onNext, callWithObservables, getValues) => {
495 | let value$ = callWithObservables(model.value$, {
496 | finish$: createObservable(
497 | onNext(101, 'something'),
498 | onNext(201, 'anything'),
499 | onNext(301, 'anything')
500 | ),
501 | notValidatedTextFieldValue$: createObservable(
502 | onNext(100, 'abc'),
503 | onNext(200, 'xyz'),
504 | onNext(300, 'def')
505 | ),
506 | autocompletions$: createObservable(
507 | onNext(100, [ 'abc', 'abc1', 'abc2' ]),
508 | onNext(200, [ ]),
509 | onNext(300, [ 'def', 'definition', 'undefined' ])
510 | )
511 | });
512 |
513 | let results = getValues(value$);
514 |
515 | assert.equalCollection(
516 | results,
517 | [ 'abc', 'def' ]
518 | );
519 |
520 | }));
521 |
522 |
523 | test('should value change if previous value was different',
524 | inject((createObservable, onNext, callWithObservables, getValues) => {
525 | let value$ = callWithObservables(model.value$, {
526 | selectedAutocompletion$: createObservable(
527 | onNext(100, 'abc'),
528 | onNext(200, 'def')
529 | ),
530 | finish$: createObservable(
531 | onNext(101, 'something'),
532 | onNext(201, 'anything'),
533 | onNext(301, 'anything')
534 | ),
535 | notValidatedTextFieldValue$: createObservable(
536 | onNext(100, 'abc'),
537 | onNext(200, 'xyz'),
538 | onNext(300, 'def')
539 | ),
540 | autocompletions$: createObservable(
541 | onNext(100, [ 'abc', 'abc1', 'abc2' ]),
542 | onNext(200, [ ]),
543 | onNext(300, [ 'def', 'definition', 'undefined' ])
544 | )
545 | });
546 |
547 | let results = getValues(value$);
548 |
549 | assert.equalCollection(
550 | results,
551 | [ 'abc', 'def' ]
552 | );
553 |
554 | }));
555 |
556 | });
557 |
558 | });
559 |
560 | });
--------------------------------------------------------------------------------
/src/js/spec/view.spec.js:
--------------------------------------------------------------------------------
1 | /* global suite, test */
2 |
3 | import chai from 'chai';
4 | import { assert } from 'chai';
5 |
6 | // has to be imported before Rx
7 | import inject from 'cyclejs-mock';
8 |
9 | import { Rx } from 'cyclejs';
10 |
11 | import view from '../view';
12 |
13 | chai.use(require('chai-equal-collection')(Rx.internals.isEqual));
14 |
15 |
16 | suite('view', () => {
17 |
18 | suite('API', () => {
19 |
20 | test('Should be a function', () => {
21 |
22 | assert.isFunction(view);
23 |
24 | });
25 |
26 | });
27 |
28 | suite('I/O', () => {
29 |
30 | test('should rerender input with id field and value',
31 | inject((createObservable, onNext, render, getValues, callWithObservables) => {
32 | let vtree$ = callWithObservables(view, {
33 | textFieldValue$: createObservable(
34 | onNext(100, 'abc')
35 | ),
36 | autocompletions$: [ 'abc', 'def', 'ghi' ],
37 | areAutocompletionsVisible$: false,
38 | highlightedAutocompletionIndex$: 0,
39 | isValueInvalid$: false
40 | });
41 |
42 | let results = getValues(vtree$.map(render));
43 |
44 | let input = results[0].querySelector('input#field');
45 | assert.isNotNull(input);
46 |
47 | assert.equal(input.value, 'abc');
48 | }));
49 |
50 | test('should rerender view every time value attribute changes',
51 | inject((createObservable, onNext, render, getValues, callWithObservables) => {
52 | let vtree$ = callWithObservables(view, {
53 | textFieldValue$: createObservable(
54 | onNext(100, 'abc'),
55 | onNext(200, 'def'),
56 | onNext(250, 'ghi')
57 | ),
58 | autocompletions$: [ 'abc', 'def', 'ghi' ],
59 | areAutocompletionsVisible$: false,
60 | highlightedAutocompletionIndex$: 0,
61 | isValueInvalid$: false
62 | });
63 |
64 | let results = getValues(vtree$.map(render));
65 |
66 | assert.equal(results.length, 3);
67 |
68 | assert.equalCollection(
69 | results.map((element) => element.querySelector('#field').value),
70 | [ 'abc', 'def', 'ghi' ]
71 | );
72 | }));
73 |
74 | });
75 |
76 | });
--------------------------------------------------------------------------------
/src/js/view.js:
--------------------------------------------------------------------------------
1 | import { Rx, h, vdomPropHook } from 'cyclejs';
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 | var stylesheet = fs.readFileSync(path.resolve(__dirname, '../css/styles.css'), 'utf8');
6 |
7 | export default function view(
8 | textFieldValue$,
9 | autocompletions$,
10 | areAutocompletionsVisible$,
11 | highlightedAutocompletionIndex$,
12 | isValueInvalid$
13 | ) {
14 | return Rx.Observable.combineLatest(
15 | textFieldValue$,
16 | autocompletions$,
17 | areAutocompletionsVisible$,
18 | highlightedAutocompletionIndex$,
19 | isValueInvalid$,
20 | (textFieldValue, autocompletions, areAutocompletionsVisible, highlightedAutocompletionIndex, isValueInvalid) =>
21 | h('div', [
22 | h('style', stylesheet),
23 | h('input#field', {
24 | type: 'text',
25 | value: textFieldValue,
26 | className: isValueInvalid ? 'is-invalid' : ''
27 | }),
28 | h('ul', {
29 | scrollTop: vdomPropHook((element, property) => {
30 | var singleOptionHeight = element.children[0] ? element.children[0].offsetHeight : 18;
31 | var selectedAutocompletionTop = highlightedAutocompletionIndex * singleOptionHeight;
32 | var selectedAutocompletionBottom = (highlightedAutocompletionIndex + 1) * singleOptionHeight;
33 |
34 | var visibleViewport = {
35 | top: element[property],
36 | bottom: element[property] + element.offsetHeight
37 | };
38 |
39 | if(selectedAutocompletionTop < visibleViewport.top) {
40 | element[property] = selectedAutocompletionTop;
41 | } else if(selectedAutocompletionBottom > visibleViewport.bottom) {
42 | element[property] = selectedAutocompletionTop + singleOptionHeight - element.offsetHeight;
43 | }
44 | }),
45 | className: areAutocompletionsVisible ? 'is-visible' : ''
46 | }, autocompletions.map((keyword, index) => h('li.autocompletion', {
47 | key: index,
48 | index: index,
49 | className: highlightedAutocompletionIndex === index ? 'is-selected' : ''
50 | }, keyword)))
51 | ])
52 | )
53 | .shareReplay(1);
54 | }
55 |
--------------------------------------------------------------------------------
/test/phantomjs-extensions.js:
--------------------------------------------------------------------------------
1 | if (!Function.prototype.bind) {
2 | Function.prototype.bind = function (oThis) {
3 | if (typeof this !== 'function') {
4 | // closest thing possible to the ECMAScript 5
5 | // internal IsCallable function
6 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
7 | }
8 |
9 | var FNOP = function () { };
10 | var instanceOfFNOP; try { instanceOfFNOP = this instanceof FNOP; } catch(e) { }
11 | var aArgs = Array.prototype.slice.call(arguments, 1),
12 | fToBind = this,
13 | fBound = function () {
14 | return fToBind.apply(instanceOfFNOP && oThis
15 | ? this
16 | : oThis,
17 | aArgs.concat(Array.prototype.slice.call(arguments)));
18 | };
19 |
20 | FNOP.prototype = this.prototype;
21 | fBound.prototype = new FNOP();
22 |
23 | return fBound;
24 | };
25 | }
26 |
27 |
28 | if(!window.CustomEvent) {
29 | window.CustomEvent = function(event, params) {
30 | params = params || { bubbles: false, cancelable: false, detail: undefined };
31 |
32 | var evt = document.createEvent('CustomEvent');
33 |
34 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
35 |
36 | return evt;
37 | };
38 |
39 | window.CustomEvent.prototype = window.Event.prototype;
40 | }
41 |
--------------------------------------------------------------------------------