├── .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 | --------------------------------------------------------------------------------