diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 1d7e223..0000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "vendor/bower" -} diff --git a/.gitignore b/.gitignore index dbe06f8..6a25c97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,9 @@ -lib-cov -*.seed -*.log -*.csv -*.dat -*.out -*.pid -*.gz - -pids -logs -results - -npm-debug.log node_modules +bower_components +npm-debug.log +coverage +test/runner/*.js +.publish +.token .DS_Store -.sass-cache -.grunt -.bundle -vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9a0c606 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - stable +install: + - npm install +after_script: + - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2cca5cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ + +## V2.1.0 - 2016-07-27 + +* Rewrite the whole component. +* Add multiple select supports for `select[multiple]` element. +* Add remote filter mode for huge options situation. diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 1b27ec0..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gem 'sass', '>= 3.4.0' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 77e2bf6..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,13 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - sass (3.4.10) - -PLATFORMS - ruby - -DEPENDENCIES - sass (>= 3.4.0) - -BUNDLED WITH - 1.10.6 diff --git a/Gruntfile.coffee b/Gruntfile.coffee deleted file mode 100644 index 4c2a36b..0000000 --- a/Gruntfile.coffee +++ /dev/null @@ -1,75 +0,0 @@ -module.exports = (grunt) -> - - grunt.initConfig - - pkg: grunt.file.readJSON 'package.json' - - sass: - select: - options: - bundleExec: true - style: 'expanded' - sourcemap: 'none' - files: - 'styles/select.css': 'styles/select.scss' - - coffee: - src: - options: - bare: true - files: - 'lib/select.js': 'src/select.coffee' - spec: - files: - 'spec/select-spec.js': 'spec/select-spec.coffee' - - umd: - all: - src: 'lib/select.js' - template: 'umd.hbs' - amdModuleId: 'simple-select' - objectToExport: 'select' - globalAlias: 'select' - deps: - 'default': ['$', 'SimpleModule'] - amd: ['jquery', 'simple-module'] - cjs: ['jquery', 'simple-module'] - global: - items: ['jQuery', 'SimpleModule'] - prefix: '' - - watch: - styles: - files: ['styles/*.scss'] - tasks: ['sass'] - spec: - files: ['spec/**/*.coffee'] - tasks: ['coffee:spec'] - src: - files: ['src/**/*.coffee'] - tasks: ['coffee:src', 'umd'] - jasmine: - files: ['lib/**/*.js', 'specs/**/*.js'] - tasks: 'jasmine:test:build' - - jasmine: - test: - src: ['lib/**/*.js'] - options: - outfile: 'spec/index.html' - styles: 'styles/select.css' - specs: 'spec/select-spec.js' - vendor: [ - 'vendor/bower/jquery/dist/jquery.min.js' - 'vendor/bower/jquery-mousewheel/jquery.mousewheel.min.js' - 'vendor/bower/simple-module/lib/module.js' - 'vendor/bower/simple-util/lib/util.js' - ] - - grunt.loadNpmTasks 'grunt-contrib-sass' - grunt.loadNpmTasks 'grunt-contrib-coffee' - grunt.loadNpmTasks 'grunt-contrib-watch' - grunt.loadNpmTasks 'grunt-contrib-jasmine' - grunt.loadNpmTasks 'grunt-umd' - - grunt.registerTask 'default', ['sass', 'coffee', 'umd', 'jasmine', 'watch'] diff --git a/LICENSE b/LICENSE index 8e0b9f2..ce3e9a9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +## The MIT License (MIT) -Copyright (c) 2014 彩程设计 +Copyright (c) 2016 Mycolorway Design Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md index b2d13fa..f39e4b0 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,176 @@ -simple-select +Simple Select ============= -一个基于 [Simple Module](https://github.com/mycolorway/simple-module) 的快速选择组件。 +[![Latest Version](https://img.shields.io/npm/v/simple-select.svg)](https://www.npmjs.com/package/simple-select) +[![Build Status](https://img.shields.io/travis/mycolorway/simple-select.svg)](https://travis-ci.org/mycolorway/simple-select) +[![Coveralls](https://img.shields.io/coveralls/mycolorway/simple-select.svg)](https://coveralls.io/github/mycolorway/simple-select) +[![David](https://img.shields.io/david/mycolorway/simple-select.svg)](https://david-dm.org/mycolorway/simple-select) +[![David](https://img.shields.io/david/dev/mycolorway/simple-select.svg)](https://david-dm.org/mycolorway/simple-select#info=devDependencies) +[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/mycolorway/simple-select) -![Demo Gif](https://raw.githubusercontent.com/mycolorway/simple-select/master/demo.gif) -### 如何使用 +Autocomplete select component, supports multiple select mode and ajax remote data source. -#### 下载并引用 +## Installation -通过 `bower install` 下载依赖的第三方库,然后在页面中引入这些文件: +Install via npm: + +```bash +npm install --save simple-select +``` + +Install via bower: + +```bash +bower install --save simple-select +``` + +## Usage ```html - - - -``` - -#### 初始化配置 -在使用 simple-select 的 HTML 页面里应该有一个对应的 select 元素,例如: - -```html - + ``` -我们需要在这个页面的脚本里初始化 simple-select: - -```javascript +```js simple.select({ - el: $('select'), // * 必须 - cls: "", // 额外的 class - onItemRender: $.noop, // 渲染列表每个元素后调用的函数 - placeholder: "", // input 元素的 placeholder 属性 - multiline: false // input 元素是否可换行,默认为true + el: '.name-list' }); ``` -组件会通过 ` - - - - - -``` +## Options -### 方法和事件 +__el__ -simple-select 初始化之后,select 实例会暴露一些公共方法供调用: +Selector/Element/jQuery Object, Required, specify the select element to be initialized with. -```javascript -// 初始化 simple-select -var select = simple.select({ - el: $('select') -}); +__remote__ + +false/Hash, set a hash to enable remote data source mode. The hash may contain three key/value pairs: -// 调用 selectItem 方法选择第三个元素 -select.selectItem(2); +```js +{ + url: 'xxx', // ajax api url, required + searchKey: 'name', // param key for the user input search value, required + params: {} // extra params passing to the server, optional +} ``` -#### 公共方法 - -**setItems(items)** - -设置 simple-select 列表元素,`label` `key` 为必须属性,所有属性都保存在对应 item 的 data 属性里: - -```javascript -select.setItems([{ - label: "张三", - hint: '#1', - key: "zhangsan zs 张三", - id: "1" -},{ - label: "李四", - hint: '#2', - key: "lisi ls 李四", - id: "2" -},{ - label: "王麻子", - hint: '#3', - key: "wangmazi wmz 王麻子", - id: "3" -}]); + This option is required unless `el` option is present. + +__cls__ + +String, extra html class to be added to wrapper element for style customization. + +__onItemRender__ + +Function, callback function to be called when item renders in dropdown list with two params: item element and item data. + +__placeholder__ + +String, set placeholder for input element. The default placeholder is the text of blank option if it exists. + +__allowInput__ + +false/Selector/Element/jQuery Object, false by default, set an `input:text` element to allow submit custom value outside select options. If you pass a selector, the `allowInput` element need be sibling of select element. + +__noWrap__ + +Boolean, false by default, set true to allow word wrap in text field. + +__locales__ + +Hash, set custom locale texts for a single instance. If you want to set default locales for all simple-select instances, use `simple.select.locales` instead. + + +## Methods + +__selectItem__ + +(`String` value), set selected option by value. + +__unselectItem__ + +(`String` value), remove selected option in multiple select mode. + +__clear__ + +clear selected option and . + +__disable__ + +Disable component, cannot make changes. + +__enable__ + +Enable component. + +__destroy__ + +Destroy component, restore element to original state. + +## Events + +__change__ + +Triggered when the selection is changed with selection data as param. + +## Development + +Clone repository from github: + +```bash +git clone https://github.com/mycolorway/simple-select.git ``` -**selectItem(index)** +Install npm dependencies: + +```bash +npm install +``` -选择对应的列表元素,返回该元素的属性: +Run default gulp task to build project, which will compile source files, run test and watch file changes for you: -```javascript -select.selectItem(2); -// 返回 -// { -// label: "王麻子", -// hint: '#2', -// key: "wangmazi wmz 王麻子", -// id: "3" -// } +```bash +gulp ``` -**clearSelection()** +Now, you are ready to go. -清除输入内容和选择的元素。 +## Publish -**destroy()** +If you want to publish new version to npm and bower, please make sure all tests have passed before you publish new version, and you need do these preparations: -恢复到初始化之前的状态。 +* Add new release information in `CHANGELOG.md`. The format of markdown contents will matter, because build scripts will get version and release content from this file by regular expression. You can follow the format of the older release information. +* Put your [personal API tokens](https://github.com/blog/1509-personal-api-tokens) in `/.token.json`, which is required by the build scripts to request [Github API](https://developer.github.com/v3/) for creating new release: -#### 事件 +```json +{ + "github": "[your github personal access token]" +} +``` + +Now you can run `gulp publish` task, which will do these work for you: -**select** +* Get version number from `CHANGELOG.md` and bump it into `package.json` and `bower.json`. +* Get release information from `CHANGELOG.md` and request Github API to create new release. -触发条件:选择某个列表元素。返回该元素的属性。 +If everything goes fine, you can see your release at [https://github.com/mycolorway/simple-select/releases](https://github.com/mycolorway/simple-select/releases). At the End you can publish new version to npm with the command: -**clear** +```bash +npm publish +``` -触发条件:清除输入内容和选择的元素。 +Please be careful with the last step, because you cannot delete or republish a release on npm. diff --git a/bower.json b/bower.json index 1d7a6c0..9c77f3e 100644 --- a/bower.json +++ b/bower.json @@ -1,31 +1,22 @@ { - "name": "simple-select", - "version": "2.0.14", + "name": "simple_select", + "version": "2.1.0", "homepage": "https://github.com/mycolorway/simple-select", "authors": [ - "kshift " - ], - "description": "a simple select plugin based on Simple Module", - "main": "lib/select.js", - "keywords": [ - "sample", - "select" + "farthinker " ], + "description": "Autocomplete select component", + "main": "dist/simple-select.js", "license": "MIT", "ignore": [ "**/.*", - "node_modules", - "vendor", - "Gemfile", - "Gemfile.lock", - "Gruntfile.coffee", - "package.json", - "README.md", - "umd.hbs", - "demo.gif" + "test", + "build", + "gulpfile.coffee", + "package.json" ], "dependencies": { - "jquery": "2.x", - "simple-module": "~2.0.5" + "jquery": "~2.2.4", + "simple-module": "~2.0.6" } } diff --git a/build/compile.coffee b/build/compile.coffee new file mode 100644 index 0000000..566a3af --- /dev/null +++ b/build/compile.coffee @@ -0,0 +1,51 @@ +gulp = require 'gulp' +_ = require 'lodash' +coffeelint = require './helpers/coffeelint' +browserify = require './helpers/browserify' +sass = require './helpers/sass' +header = require './helpers/header' +rename = require './helpers/rename' +uglify = require './helpers/uglify' +umd = require './helpers/umd' + +compileSass = -> + gulp.src 'styles/**/*.scss' + .pipe sass() + .pipe header() + .pipe gulp.dest('styles/') +compileSass.displayName = 'compile-sass' + +checkCoffee = -> + gulp.src 'src/**/*.coffee' + .pipe coffeelint() +checkCoffee.displayName = 'coffeelint' + +compileCoffee = -> + gulp.src 'src/simple-select.coffee' + .pipe browserify() + .pipe umd() + .pipe header() + .pipe gulp.dest('dist/') +compileCoffee.displayName = 'compile-coffee' + +compileUglify = -> + gulp.src ['dist/**/*.js', '!dist/**/*.min.js'] + .pipe uglify() + .pipe header('simple') + .pipe rename + suffix: '.min' + .pipe gulp.dest('dist/') +compileUglify.displayName = 'compile-uglify' + +compileAssets = gulp.parallel compileCoffee, compileSass, (done) -> + done() + +compile = gulp.series checkCoffee, compileAssets, compileUglify, (done) -> + done() + +gulp.task 'compile', compile + +module.exports = _.extend compile, + sass: compileSass + coffee: compileCoffee + uglify: compileUglify diff --git a/build/helpers/browserify.coffee b/build/helpers/browserify.coffee new file mode 100644 index 0000000..a83cde4 --- /dev/null +++ b/build/helpers/browserify.coffee @@ -0,0 +1,51 @@ +gutil = require 'gulp-util' +_ = require 'lodash' +through = require 'through2' +coffee = require 'coffee-script' +browserify = require 'browserify' +handleError = require './error' + +module.exports = (opts) -> + b = browserify _.extend + transform: [coffeeify] + bundleExternal: false + , opts + + through.obj (file, encoding, done) -> + + try + b.require file.path, + expose: file.stem + b.bundle (error, buffer) => + handleError(error, @) if error + file.contents = buffer + file.path = gutil.replaceExtension file.path, '.js' + @push file + done() + catch e + handleError e, @ + done() + +coffeeify = (filename, opts = {}) -> + return through() unless /\.coffee$/.test(filename) + + opts = _.extend + inline: true + bare: true + header: false + , opts + + chunks = [] + through (chunk, encoding, done) -> + chunks.push chunk + done() + , (done) -> + str = Buffer.concat(chunks).toString() + + try + result = coffee.compile str, opts + catch e + handleError e, @ + + @push result + done() diff --git a/build/helpers/changelogs.coffee b/build/helpers/changelogs.coffee new file mode 100644 index 0000000..9ba3951 --- /dev/null +++ b/build/helpers/changelogs.coffee @@ -0,0 +1,25 @@ +fs = require 'fs' + +changelogs = fs.readFileSync('CHANGELOG.md').toString() + +lastestVersion = do -> + result = changelogs.match /## V(\d+\.\d+\.\d+)/ + + if result and result.length > 1 + result[1] + else + null + +latestContent = do -> + re = new RegExp "## V#{lastestVersion.replace('.', '\\.')}\ + .+\\n\\n((?:\\* .*\\n)+)" + result = changelogs.match re + + if result and result.length > 1 + result[1] + else + null + +module.exports = + lastestVersion: lastestVersion + latestContent: latestContent diff --git a/build/helpers/coffee.coffee b/build/helpers/coffee.coffee new file mode 100644 index 0000000..d2e3057 --- /dev/null +++ b/build/helpers/coffee.coffee @@ -0,0 +1,18 @@ +gutil = require 'gulp-util' +through = require 'through2' +coffee = require 'coffee-script' +handleError = require './error' + +module.exports = (opts) -> + through.obj (file, encoding, done) -> + str = file.contents.toString() + + try + result = coffee.compile str, opts + catch e + handleError e, @ + + file.contents = new Buffer result + file.path = gutil.replaceExtension file.path, '.js' + @push file + done() diff --git a/build/helpers/coffeelint.coffee b/build/helpers/coffeelint.coffee new file mode 100644 index 0000000..3e897c9 --- /dev/null +++ b/build/helpers/coffeelint.coffee @@ -0,0 +1,21 @@ +through = require 'through2' +coffeelint = require 'coffeelint' +Reporter = require 'coffeelint/lib/reporters/default' +configFinder = require 'coffeelint/lib/configfinder' +handleError = require './error' + +module.exports = -> + through.obj (file, encoding, done) -> + opts = configFinder.getConfig() + errorReport = coffeelint.getErrorReport() + errorReport.lint file.relative, file.contents.toString(), opts + + summary = errorReport.getSummary() + if summary.errorCount > 0 || summary.warningCount > 0 + reporter = new Reporter errorReport + reporter.publish() + if summary.errorCount > 0 + handleError 'coffeelint failed with errors or warnings', @ + + @push file + done() diff --git a/build/helpers/data.coffee b/build/helpers/data.coffee new file mode 100644 index 0000000..702ce04 --- /dev/null +++ b/build/helpers/data.coffee @@ -0,0 +1,8 @@ +through = require 'through2' +_ = require 'lodash' + +module.exports = (data) -> + through.obj (file, encoding, done) -> + file.data = if _.isFunction(data) then data(file) else data + @push file + done() diff --git a/build/helpers/error.coffee b/build/helpers/error.coffee new file mode 100644 index 0000000..f28c7b8 --- /dev/null +++ b/build/helpers/error.coffee @@ -0,0 +1,13 @@ +gutil = require 'gulp-util' + +module.exports = (error, stream) -> + if stream + opts = if typeof error == 'string' + {} + else + stack: error.stack + showStack: !!error.stack + + stream.emit 'error', new gutil.PluginError 'gulp-build', error, opts + else + gutil.log gutil.colors.red("gulp-build error: #{error.message || error}") diff --git a/build/helpers/header.coffee b/build/helpers/header.coffee new file mode 100644 index 0000000..ca031dc --- /dev/null +++ b/build/helpers/header.coffee @@ -0,0 +1,22 @@ +fs = require 'fs' +through = require 'through2' +_ = require 'lodash' +pkg = require '../../package' + +module.exports = (type = 'full') -> + now = new Date() + year = now.getFullYear() + month = _.padStart(now.getMonth() + 1, 2, '0') + date = now.getDate() + tpl = fs.readFileSync("build/templates/#{type}-header.txt").toString() + header = _.template(tpl) + name: pkg.name + version: pkg.version + homepage: pkg.homepage + date: "#{year}-#{month}-#{date}" + + through.obj (file, encoding, done) -> + headerBuffer = new Buffer header + file.contents = Buffer.concat [headerBuffer, file.contents] + @push file + done() diff --git a/build/helpers/remove-dir.coffee b/build/helpers/remove-dir.coffee new file mode 100644 index 0000000..88ea768 --- /dev/null +++ b/build/helpers/remove-dir.coffee @@ -0,0 +1,13 @@ +fs = require 'fs' + +module.exports = removeDir = (dirPath) -> + return unless fs.existsSync dirPath + + fs.readdirSync(dirPath).forEach (file, index) -> + filePath = "#{dirPath}/#{file}" + if fs.lstatSync(filePath).isDirectory() + removeDir filePath + else + fs.unlinkSync filePath + + fs.rmdirSync dirPath diff --git a/build/helpers/rename.coffee b/build/helpers/rename.coffee new file mode 100644 index 0000000..eb8ea97 --- /dev/null +++ b/build/helpers/rename.coffee @@ -0,0 +1,25 @@ +_ = require 'lodash' +through = require 'through2' +path = require 'path' + +module.exports = (opts) -> + opts = _.extend + prefix: '' + suffix: '' + dirname: null + basename: null + extname: null + , opts + + through.obj (file, encoding, done) -> + dirname = path.dirname file.relative + extname = path.extname file.relative + basename = path.basename file.relative, extname + newDirName = if _.isNull(opts.dirname) then dirname else opts.dirname + newExtName = if _.isNull(opts.extname) then extname else opts.extname + newBaseName = if _.isNull(opts.basename) then basename else opts.basename + + filename = "#{opts.prefix}#{newBaseName}#{opts.suffix}#{newExtName}" + file.path = path.join file.base, newDirName, filename + @push file + done() diff --git a/build/helpers/sass.coffee b/build/helpers/sass.coffee new file mode 100644 index 0000000..7fe43af --- /dev/null +++ b/build/helpers/sass.coffee @@ -0,0 +1,23 @@ +gutil = require 'gulp-util' +through = require 'through2' +path = require 'path' +sass = require 'node-sass' +_ = require 'lodash' +handleError = require './error' + +module.exports = (opts) -> + through.obj (file, encoding, done) -> + opts = _.extend + data: file.contents.toString() + includePath: [path.dirname(file.path)] + , opts + + try + result = sass.renderSync opts + catch e + handleError e, @ + + file.contents = new Buffer result.css + file.path = gutil.replaceExtension file.path, '.css' + @push file + done() diff --git a/build/helpers/uglify.coffee b/build/helpers/uglify.coffee new file mode 100644 index 0000000..5eb43b7 --- /dev/null +++ b/build/helpers/uglify.coffee @@ -0,0 +1,17 @@ +through = require 'through2' +uglify = require 'uglify-js' +_ = require 'lodash' +handleError = require './error' + +module.exports = (opts) -> + through.obj (file, encoding, done) -> + opts = _.extend {fromString: true}, opts + + try + result = uglify.minify file.contents.toString(), opts + catch e + handleError e, @ + + file.contents = new Buffer result.code + @push file + done() diff --git a/build/helpers/umd.coffee b/build/helpers/umd.coffee new file mode 100644 index 0000000..f3c8d41 --- /dev/null +++ b/build/helpers/umd.coffee @@ -0,0 +1,27 @@ +fs = require 'fs' +through = require 'through2' +_ = require 'lodash' +pkg = require '../../package' + +module.exports = (opts) -> + umdConfig = _.cloneDeep pkg.umd + opts = _.extend umdConfig, opts + + opts.dependencies.cjs = opts.dependencies.cjs.map (name) -> + "require('#{name}')" + .join ',' + + opts.dependencies.global = opts.dependencies.global.map (name) -> + "root.#{name}" + .join ',' + + opts.dependencies.params = opts.dependencies.params.join ',' + + tpl = _.template fs.readFileSync('build/templates/umd.js').toString() + + through.obj (file, encoding, done) -> + opts.contents = file.contents.toString() + opts.filename = file.stem + file.contents = new Buffer tpl opts + @push file + done() diff --git a/build/publish.coffee b/build/publish.coffee new file mode 100644 index 0000000..3ef7f96 --- /dev/null +++ b/build/publish.coffee @@ -0,0 +1,76 @@ +gulp = require 'gulp' +gutil = require 'gulp-util' +fs = require 'fs' +request = require 'request' +changelogs = require './helpers/changelogs' +handleError = require './helpers/error' +compile = require './compile' +test = require './test' +_ = require 'lodash' + +bumpVersion = (done) -> + newVersion = changelogs.lastestVersion + unless newVersion + throw new Error('Publish: Invalid version in CHANGELOG.md') + return + + pkg = require '../package' + pkg.version = newVersion + fs.writeFileSync './package.json', JSON.stringify(pkg, null, 2) + + bowerConfig = require '../bower.json' + bowerConfig.version = newVersion + fs.writeFileSync './bower.json', JSON.stringify(bowerConfig, null, 2) + + done() +bumpVersion.displayName = 'bump-version' + +createRelease = (done) -> + try + token = _.trim fs.readFileSync('.token').toString() + catch e + throw new Error 'Publish: Need github access token for creating release.' + return + + pkg = require '../package' + content = changelogs.latestContent + unless content + throw new Error('Publish: Invalid release content in CHANGELOG.md') + return + + request + uri: "https://api.github.com/repos/#{pkg.githubOwner}/#{pkg.name}/releases" + method: 'POST' + json: true + body: + tag_name: "v#{pkg.version}", + name: "v#{pkg.version}", + body: content, + draft: false, + prerelease: false + headers: + Authorization: "token #{token}", + 'User-Agent': 'Mycolorway Release' + , (error, response, body) -> + if error + handleError error + else if response.statusCode.toString().search(/2\d\d/) > -1 + message = "#{pkg.name} v#{pkg.version} released on github!" + gutil.log gutil.colors.green message + else + message = "#{response.statusCode} #{JSON.stringify response.body}" + handleError gutil.colors.red message + done() +createRelease.displayName = 'create-release' + +publish = gulp.series [ + compile, + test, + bumpVersion, + createRelease +]..., (done) -> + done() + +gulp.task 'publish', publish + +module.exports = publish diff --git a/build/templates/full-header.txt b/build/templates/full-header.txt new file mode 100644 index 0000000..6b2170d --- /dev/null +++ b/build/templates/full-header.txt @@ -0,0 +1,10 @@ +/** + * <%= name %> v<%= version %> + * <%= homepage %> + * + * Copyright Mycolorway Design + * Released under the MIT license + * <%= homepage %>/license.html + * + * Date: <%= date %> + */ diff --git a/build/templates/mocha-phantomjs-hooks.js b/build/templates/mocha-phantomjs-hooks.js new file mode 100644 index 0000000..6be0428 --- /dev/null +++ b/build/templates/mocha-phantomjs-hooks.js @@ -0,0 +1,21 @@ +var fs = require('fs'); + +function collectCoverage(page) { + var coverage = page.evaluate(function() { + return window.__coverage__; + }); + + if (!coverage) { + return; + } + + var json = JSON.stringify(coverage); + fs.write('coverage/coverage.json', json); +} + +// beforeStart and afterEnd hooks for mocha-phantomjs +module.exports = { + afterEnd: function(data) { + collectCoverage(data.page); + } +}; diff --git a/build/templates/simple-header.txt b/build/templates/simple-header.txt new file mode 100644 index 0000000..5af8953 --- /dev/null +++ b/build/templates/simple-header.txt @@ -0,0 +1 @@ +/* <%= name %> v<%= version %> | (c) Mycolorway Design | MIT License */ diff --git a/build/templates/umd.js b/build/templates/umd.js new file mode 100644 index 0000000..6dc61a4 --- /dev/null +++ b/build/templates/umd.js @@ -0,0 +1,16 @@ +;(function(root, factory) { + if (typeof module === 'object' && module.exports) { + module.exports = factory(<%= dependencies.cjs %>); + } else { + root.<%= name %> = factory(<%= dependencies.global %>); + root.simple = root.simple || {}; + root.simple.select = function (opts) { + return new root.<%= name %>(opts); + } + root.simple.select.locales = root.<%= name %>.locales; + } +}(this, function (<%= dependencies.params %>) { +var define, module, exports; +var b = <%= contents %> +return b('<%= filename %>'); +})); diff --git a/build/test.coffee b/build/test.coffee new file mode 100644 index 0000000..eb908e3 --- /dev/null +++ b/build/test.coffee @@ -0,0 +1,18 @@ +gulp = require 'gulp' +karma = require 'karma' +fs = require 'fs' +handleError = require './helpers/error' + +test = (done) -> + server = new karma.Server + configFile: "#{process.cwd()}/karma.coffee" + , (code) -> + fs.unlinkSync 'test/coverage-init.js' + if code != 0 + handleError "karma exit with code: #{code}" + done() + + server.start() + +gulp.task 'test', test +module.exports = test diff --git a/coffeelint.json b/coffeelint.json new file mode 100644 index 0000000..86422e3 --- /dev/null +++ b/coffeelint.json @@ -0,0 +1,129 @@ +{ + "arrow_spacing": { + "level": "ignore" + }, + "braces_spacing": { + "level": "ignore", + "spaces": 0, + "empty_object_spaces": 0 + }, + "camel_case_classes": { + "level": "error" + }, + "coffeescript_error": { + "level": "error" + }, + "colon_assignment_spacing": { + "level": "ignore", + "spacing": { + "left": 0, + "right": 0 + } + }, + "cyclomatic_complexity": { + "value": 10, + "level": "ignore" + }, + "duplicate_key": { + "level": "error" + }, + "empty_constructor_needs_parens": { + "level": "ignore" + }, + "ensure_comprehensions": { + "level": "warn" + }, + "eol_last": { + "level": "ignore" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "line_endings": { + "level": "ignore", + "value": "unix" + }, + "max_line_length": { + "value": 80, + "level": "error", + "limitComments": true + }, + "missing_fat_arrows": { + "level": "ignore", + "is_strict": false + }, + "newlines_after_classes": { + "value": 3, + "level": "ignore" + }, + "no_backticks": { + "level": "error" + }, + "no_debugger": { + "level": "warn", + "console": false + }, + "no_empty_functions": { + "level": "ignore" + }, + "no_empty_param_list": { + "level": "ignore" + }, + "no_implicit_braces": { + "level": "ignore", + "strict": true + }, + "no_implicit_parens": { + "strict": true, + "level": "ignore" + }, + "no_interpolation_in_single_quotes": { + "level": "ignore" + }, + "no_plusplus": { + "level": "ignore" + }, + "no_stand_alone_at": { + "level": "ignore" + }, + "no_tabs": { + "level": "error" + }, + "no_this": { + "level": "ignore" + }, + "no_throwing_strings": { + "level": "error" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_trailing_whitespace": { + "level": "error", + "allowed_in_comments": false, + "allowed_in_empty_lines": true + }, + "no_unnecessary_double_quotes": { + "level": "ignore" + }, + "no_unnecessary_fat_arrows": { + "level": "warn" + }, + "non_empty_constructor_needs_parens": { + "level": "ignore" + }, + "prefer_english_operator": { + "level": "ignore", + "doubleNotLevel": "ignore" + }, + "space_operators": { + "level": "ignore" + }, + "spacing_after_comma": { + "level": "ignore" + }, + "transform_messes_up_line_numbers": { + "level": "warn" + } +} diff --git a/demo.gif b/demo.gif deleted file mode 100644 index dd01d7e..0000000 Binary files a/demo.gif and /dev/null differ diff --git a/demo.html b/demo.html index 45e4f2b..14f2c14 100644 --- a/demo.html +++ b/demo.html @@ -4,7 +4,7 @@ A simple select plugin - + - - - - - + + +
-

Demo one: multiline as false

+

Demo one: single select

+
-

Demo two: without non value option

- - -
- -
-

Demo three: with non value option

- - + - - -
- -
-

Demo Four: call setItems with object

-
diff --git a/dist/simple-select.js b/dist/simple-select.js new file mode 100644 index 0000000..c1db113 --- /dev/null +++ b/dist/simple-select.js @@ -0,0 +1,1318 @@ +/** + * simple-select v2.1.0 + * http://mycolorway.github.io/simple-select + * + * Copyright Mycolorway Design + * Released under the MIT license + * http://mycolorway.github.io/simple-select/license.html + * + * Date: 2016-07-27 + */ +;(function(root, factory) { + if (typeof module === 'object' && module.exports) { + module.exports = factory(require('jquery'),require('simple-module')); + } else { + root.SimpleSelect = factory(root.jQuery,root.SimpleModule); + root.simple = root.simple || {}; + root.simple.select = function (opts) { + return new root.SimpleSelect(opts); + } + root.simple.select.locales = root.SimpleSelect.locales; + } +}(this, function ($,SimpleModule) { +var define, module, exports; +var b = require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o', { + text: item.name, + value: item.value, + data: item.data + }).appendTo($parent); + }; + + HtmlSelect.prototype._render = function() { + this.el.empty(); + if (this.groups.length === 0) { + this.el.append('", { + label: group.name + }); + $.each(group.items, function(i, item) { + return _this._renderOption(item, $group); + }); + return _this.el.append($group); + }; + })(this)); + } + return this.el; + }; + + HtmlSelect.prototype.setGroups = function(groups) { + this.groups = groups; + return this._render(); + }; + + HtmlSelect.prototype.getValue = function() { + return this.el.val(); + }; + + HtmlSelect.prototype.setValue = function(value) { + return this.el.val(value); + }; + + HtmlSelect.prototype.getBlankOption = function() { + var $blankOption; + $blankOption = this.el.find('option:not([value]), option[value=""]'); + if ($blankOption.length > 0) { + return $blankOption; + } else { + return false; + } + }; + + return HtmlSelect; + +})(SimpleModule); + +module.exports = HtmlSelect; + +},{"./models/group.coffee":4}],2:[function(require,module,exports){ +var DataProvider, Input, Item, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +DataProvider = require('./models/data-provider.coffee'); + +Item = require('./models/item.coffee'); + +Input = (function(superClass) { + extend(Input, superClass); + + function Input() { + return Input.__super__.constructor.apply(this, arguments); + } + + Input.prototype.opts = { + el: null, + noWrap: false, + placeholder: '', + selected: false + }; + + Input.prototype._init = function() { + this.el = $(this.opts.el); + this.dataProvider = DataProvider.getInstance(); + this._render(); + return this._bind(); + }; + + Input.prototype._render = function() { + this.el.append('\n\n\n \n\n\n \n'); + this.el.find(this.opts.noWrap ? 'textarea' : 'input:text').remove(); + this.textField = this.el.find('.text-field'); + this.textField.attr('placeholder', this.opts.placeholder); + this.setSelected(this.opts.selected); + return this.el; + }; + + Input.prototype._bind = function() { + this.el.on('mousedown', (function(_this) { + return function(e) { + _this.textField.focus(); + return false; + }; + })(this)); + this.el.find(".link-expand").on("mousedown", (function(_this) { + return function(e) { + if (_this.disabled) { + return; + } + if (!_this.focused) { + _this.focus(); + } + _this.trigger('expandClick'); + return false; + }; + })(this)); + this.el.find(".link-clear").on("mousedown", (function(_this) { + return function(e) { + if (_this.disabled) { + return; + } + _this.trigger('clearClick'); + return false; + }; + })(this)); + return this.textField.on("keydown.simple-select", (function(_this) { + return function(e) { + var direction; + if (e.which === 40 || e.which === 38) { + e.preventDefault(); + direction = e.which === 40 ? 'down' : 'up'; + return _this.triggerHandler('arrowPress', [direction]); + } else if (e.which === 13) { + e.preventDefault(); + return _this.triggerHandler('enterPress'); + } else if (e.which === 27) { + e.preventDefault(); + return _this.blur(); + } else if (e.which === 8) { + return _this._onBackspacePress(e); + } + }; + })(this)).on("input.simple-select", (function(_this) { + return function(e) { + if (_this._inputTimer) { + clearTimeout(_this._inputTimer); + _this._inputTimer = null; + } + return _this._inputTimer = setTimeout(function() { + return _this._onInputChange(); + }, 200); + }; + })(this)).on("blur.simple-select", (function(_this) { + return function(e) { + _this.focused = false; + return _this.triggerHandler('blur'); + }; + })(this)).on("focus.simple-select", (function(_this) { + return function(e) { + _this.focused = true; + return _this.triggerHandler('focus'); + }; + })(this)); + }; + + Input.prototype._onBackspacePress = function(e) { + if (this.selected) { + e.preventDefault(); + return this.clear(); + } + }; + + Input.prototype._onInputChange = function() { + this._autoresize(); + this.setSelected(false); + return this.triggerHandler('change', [this.getValue()]); + }; + + Input.prototype._autoresize = function() { + var borderBottom, borderTop, scrollHeight; + if (this.opts.noWrap) { + return; + } + this.textField.css("height", 0); + scrollHeight = parseFloat(this.textField[0].scrollHeight); + borderTop = parseFloat(this.textField.css("border-top-width")); + borderBottom = parseFloat(this.textField.css("border-bottom-width")); + return this.textField.css("height", scrollHeight + borderTop + borderBottom); + }; + + Input.prototype.setValue = function(value) { + this.textField.val(value); + return this._onInputChange(); + }; + + Input.prototype.getValue = function() { + return this.textField.val(); + }; + + Input.prototype.setSelected = function(selected) { + if (selected == null) { + selected = false; + } + if (selected) { + if (!(selected instanceof Item)) { + selected = this.dataProvider.getItem(selected); + } + this.textField.val(selected.name); + this._autoresize(); + this.el.addClass('selected'); + } else { + this.el.removeClass('selected'); + } + this.selected = selected; + return selected; + }; + + Input.prototype.setDisabled = function(disabled) { + if (disabled == null) { + disabled = false; + } + this.disabled = disabled; + this.textField.prop('disabled', disabled); + this.el.toggleClass('disabled', disabled); + return disabled; + }; + + Input.prototype.focus = function() { + return this.textField.focus(); + }; + + Input.prototype.blur = function() { + return this.textField.blur(); + }; + + Input.prototype.clear = function() { + return this.setValue(''); + }; + + return Input; + +})(SimpleModule); + +module.exports = Input; + +},{"./models/data-provider.coffee":3,"./models/item.coffee":5}],3:[function(require,module,exports){ +var DataProvider, Group, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +Group = require('./group.coffee'); + +DataProvider = (function(superClass) { + extend(DataProvider, superClass); + + function DataProvider() { + return DataProvider.__super__.constructor.apply(this, arguments); + } + + DataProvider.getInstance = function() { + return this.instance; + }; + + DataProvider.prototype.opts = { + remote: false, + groups: null, + selectEl: null + }; + + DataProvider.prototype._init = function() { + this.remote = this.opts.remote; + if (this.opts.groups) { + this.setGroupsFromJson(this.opts.groups); + } else if (this.opts.selectEl) { + this.setGroupsFromHtml(this.opts.selectEl); + } + return DataProvider.instance = this; + }; + + DataProvider.prototype._fetch = function(value, callback) { + var obj, onFetch; + if (!this.remote || this.triggerHandler('beforeFetch') === false) { + return; + } + onFetch = (function(_this) { + return function(groups) { + _this.setGroupsFromJson(groups); + _this.triggerHandler('fetch', [_this.groups]); + return typeof callback === "function" ? callback(_this.groups) : void 0; + }; + })(this); + if (!value) { + onFetch([]); + return; + } + return $.ajax({ + url: this.remote.url, + data: $.extend({}, this.remote.params, ( + obj = {}, + obj["" + this.remote.searchKey] = value, + obj + )), + dataType: 'json' + }).done(function(groups) { + return onFetch(groups); + }); + }; + + DataProvider.prototype.setGroupsFromJson = function(groups) { + if (!groups) { + return; + } + this.groups = []; + if ($.isArray(groups)) { + this.groups.push(new Group({ + items: groups + })); + } else if ($.isPlainObject(groups)) { + $.each(groups, (function(_this) { + return function(groupName, groupItems) { + return _this.groups.push(new Group({ + name: groupName, + items: groupItems + })); + }; + })(this)); + } + this.triggerHandler('change', [this.groups]); + return this.groups; + }; + + DataProvider.prototype.setGroupsFromHtml = function(selectEl) { + var $groups, $select, itemsFromOptions; + $select = $(selectEl); + if (!($select && $select.length > 0)) { + return; + } + this.groups = []; + itemsFromOptions = function($options) { + var items; + items = []; + $options.each(function(i, option) { + var $option, value; + $option = $(option); + value = $option.val(); + if (!value) { + return; + } + return items.push([$option.text(), value, $option.data()]); + }); + return items; + }; + if (($groups = $select.find('optgroup')).length > 0) { + $groups.each((function(_this) { + return function(i, groupEl) { + var $group; + $group = $(groupEl); + return _this.groups.push(new Group({ + name: $group.attr('label'), + items: itemsFromOptions($group.find('option')) + })); + }; + })(this)); + } else { + this.groups.push(new Group({ + items: itemsFromOptions($select.find('option')) + })); + } + this.triggerHandler('change', [this.groups]); + return this.groups; + }; + + DataProvider.prototype.getGroups = function() { + return this.groups; + }; + + DataProvider.prototype.getItem = function(value) { + var result; + result = null; + $.each(this.groups, function(i, group) { + result = group.getItem(value); + if (result) { + return false; + } + }); + return result; + }; + + DataProvider.prototype.getItemByName = function(name) { + var result; + result = null; + $.each(this.groups, function(i, group) { + result = group.getItemByName(name); + if (result) { + return false; + } + }); + return result; + }; + + DataProvider.prototype.filter = function(value, callback) { + var groups; + if (this.remote) { + this._fetch(value, (function(_this) { + return function() { + if (typeof callback === "function") { + callback(_this.groups, value); + } + return _this.triggerHandler('filter', [_this.groups, value]); + }; + })(this)); + } else { + groups = []; + $.each(this.groups, function(i, group) { + var filteredGroup; + filteredGroup = group.filterItems(value); + if (filteredGroup.items.length > 0) { + return groups.push(filteredGroup); + } + }); + if (typeof callback === "function") { + callback(groups, value); + } + this.triggerHandler('filter', [groups, value]); + } + return null; + }; + + DataProvider.prototype.excludeItems = function(items, groups) { + var results; + if (items == null) { + items = []; + } + if (groups == null) { + groups = this.groups; + } + results = []; + $.each(groups, function(i, group) { + var excludedGroup; + excludedGroup = group.excludeItems(items); + if (excludedGroup.items.length > 0) { + return results.push(excludedGroup); + } + }); + return results; + }; + + return DataProvider; + +})(SimpleModule); + +module.exports = DataProvider; + +},{"./group.coffee":4}],4:[function(require,module,exports){ +var Group, Item; + +Item = require('./item.coffee'); + +Group = (function() { + Group.defaultName = '__default__'; + + function Group(opts) { + this.name = opts.name || Group.defaultName; + this.items = []; + if ($.isArray(opts.items) && opts.items.length > 0) { + $.each(opts.items, (function(_this) { + return function(i, item) { + if ($.isArray(item)) { + item = { + name: item[0], + value: item[1], + data: item.length > 2 ? item[2] : null + }; + } + return _this.items.push(new Item(item)); + }; + })(this)); + } + } + + Group.prototype.filterItems = function(value) { + var group; + group = new Group({ + name: this.name + }); + $.each(this.items, function(i, item) { + if (item.match(value)) { + return group.items.push(item); + } + }); + return group; + }; + + Group.prototype.excludeItems = function(items) { + var group; + items = items.map(function(item) { + return item.value; + }); + group = new Group({ + name: this.name + }); + $.each(this.items, function(i, item) { + if (items.indexOf(item.value) === -1) { + return group.items.push(item); + } + }); + return group; + }; + + Group.prototype.getItem = function(value) { + var result; + result = null; + $.each(this.items, function(i, item) { + if (item.value === value) { + result = item; + } + if (result) { + return false; + } + }); + return result; + }; + + Group.prototype.getItemByName = function(name) { + var result; + result = null; + $.each(this.items, function(i, item) { + if (item.name === name) { + result = item; + } + if (result) { + return false; + } + }); + return result; + }; + + return Group; + +})(); + +module.exports = Group; + +},{"./item.coffee":5}],5:[function(require,module,exports){ +var Item, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +Item = (function(superClass) { + extend(Item, superClass); + + function Item(opts) { + this.name = opts.name; + this.value = opts.value.toString(); + this.data = {}; + if ($.isPlainObject(opts.data)) { + $.each(opts.data, (function(_this) { + return function(key, value) { + key = key.replace(/^data-/, '').split('-'); + $.each(key, function(i, part) { + if (i > 0) { + return key[i] = part.charAt(0).toUpperCase() + part.slice(1); + } + }); + _this.data[key.join()] = value; + return null; + }; + })(this)); + } + } + + Item.prototype.match = function(value) { + var e, error, filterKey, re; + try { + re = new RegExp("(^|\\s)" + value, "i"); + } catch (error) { + e = error; + re = new RegExp("", "i"); + } + filterKey = this.data.key || this.name; + return re.test(filterKey); + }; + + return Item; + +})(SimpleModule); + +module.exports = Item; + +},{}],6:[function(require,module,exports){ +var Input, Item, MultipleInput, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +Item = require('./models/item.coffee'); + +Input = require('./input.coffee'); + +MultipleInput = (function(superClass) { + extend(MultipleInput, superClass); + + function MultipleInput() { + return MultipleInput.__super__.constructor.apply(this, arguments); + } + + MultipleInput.prototype.opts = { + el: null, + placeholder: '', + selected: false + }; + + MultipleInput._itemTpl = '
\n \n \n
'; + + MultipleInput.prototype._render = function() { + this.el.append('').addClass('multiple'); + this.textField = this.el.find('textarea'); + this.textField.attr('placeholder', this.opts.placeholder); + if ($.isArray(this.opts.selected)) { + $.each(this.opts.selected, (function(_this) { + return function(i, item) { + return _this.addSelected(item); + }; + })(this)); + } + return this.el; + }; + + MultipleInput.prototype._bind = function() { + MultipleInput.__super__._bind.call(this); + return this.el.on('mousedown', '.selected-item', (function(_this) { + return function(e) { + var $item; + $item = $(e.currentTarget); + _this.triggerHandler('itemClick', [$item, $item.data('item')]); + return false; + }; + })(this)); + }; + + MultipleInput.prototype._onBackspacePress = function(e) { + if (!this.getValue()) { + e.preventDefault(); + return this.el.find('.selected-item:last').mousedown(); + } + }; + + MultipleInput.prototype._onInputChange = function() { + return this.triggerHandler('change', [this.getValue()]); + }; + + MultipleInput.prototype._autoresize = function() {}; + + MultipleInput.prototype._setPlaceholder = function(show) { + if (show == null) { + show = true; + } + if (show) { + return this.textField.attr('placeholder', this.opts.placeholder); + } else { + return this.textField.removeAttr('placeholder'); + } + }; + + MultipleInput.prototype.setSelected = function(item) { + if (item == null) { + item = false; + } + if (item) { + return this.addSelected(selected); + } else { + return this.clear(); + } + }; + + MultipleInput.prototype.addSelected = function(item) { + var $item; + if (!(item instanceof Item)) { + item = this.dataProvider.getItem(item); + } + if (!item) { + return; + } + this.selected || (this.selected = []); + this.selected.push(item); + $item = $(MultipleInput._itemTpl).attr('data-value', item.value).data('item', item); + $item.find('.item-label').text(item.name); + $item.insertBefore(this.textField); + this.setValue(''); + this._setPlaceholder(false); + return item; + }; + + MultipleInput.prototype.removeSelected = function(item) { + if (!(item instanceof Item)) { + item = this.dataProvider.getItem(item); + } + if (!item) { + return; + } + if (this.selected) { + $.each(this.selected, (function(_this) { + return function(i, _item) { + if (_item.value === item.value) { + _this.selected.splice(i, 1); + return false; + } + }; + })(this)); + if (this.selected.length === 0) { + this.selected = false; + this._setPlaceholder(true); + } + } + this.el.find(".selected-item[data-value='" + item.value + "']").remove(); + this.setValue(''); + return item; + }; + + MultipleInput.prototype.clear = function() { + this.setValue(''); + this.selected = false; + this._setPlaceholder(true); + return this.el.find('.selected-item').remove(); + }; + + return MultipleInput; + +})(Input); + +module.exports = MultipleInput; + +},{"./input.coffee":2,"./models/item.coffee":5}],7:[function(require,module,exports){ +var DataProvider, Group, Item, Popover, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +DataProvider = require('./models/data-provider.coffee'); + +Group = require('./models/group.coffee'); + +Item = require('./models/item.coffee'); + +Popover = (function(superClass) { + extend(Popover, superClass); + + function Popover() { + return Popover.__super__.constructor.apply(this, arguments); + } + + Popover._itemTpl = "
\n \n \n
"; + + Popover.prototype.opts = { + el: null, + groups: [], + onItemRender: null, + localse: {} + }; + + Popover.prototype._init = function() { + this.el = $(this.opts.el); + this.groups = this.opts.groups; + this._render(); + return this._bind(); + }; + + Popover.prototype._renderItem = function(item) { + var $itemEl; + $itemEl = $(Popover._itemTpl).data('item', item); + $itemEl.find('.label').text(item.name); + if (item.data.hint) { + $itemEl.find('.hint').text(item.data.hint); + } + $itemEl.attr('data-value', item.value); + this.el.append($itemEl); + if ($.isFunction(this.opts.onItemRender)) { + this.opts.onItemRender.call(this, $itemEl, item); + } + return $itemEl; + }; + + Popover.prototype._render = function() { + var noGroup; + this.el.empty(); + noGroup = this.groups.length === 1 && this.groups[0].name === Group.defaultName; + if (this.groups.length === 0 || (noGroup && this.groups[0].items.length === 0)) { + $('
').text(this.opts.locales.noResults).appendTo(this.el); + } else if (noGroup) { + $.each(this.groups[0].items, (function(_this) { + return function(i, item) { + return _this._renderItem(item); + }; + })(this)); + } else { + $.each(this.groups, (function(_this) { + return function(i, group) { + $('
').text(group.name).appendTo(_this.el); + return $.each(group.items, function(i, item) { + return _this._renderItem(item); + }); + }; + })(this)); + } + return this.el; + }; + + Popover.prototype._bind = function() { + return this.el.on('mousedown', '.select-item', (function(_this) { + return function(e) { + var $item; + $item = $(e.currentTarget); + _this.triggerHandler('itemClick', [$item, $item.data('item')]); + return false; + }; + })(this)); + }; + + Popover.prototype._scrollToHighlighted = function() { + var $item; + $item = this.el.find('.select-item.highlighted'); + if ($item.length > 0) { + return this.el.scrollTop($item.position().top); + } + }; + + Popover.prototype.setGroups = function(groups) { + this.setHighlighted(false); + this.setLoading(false); + this.setActive(false); + this.groups = groups; + this._render(); + return groups; + }; + + Popover.prototype.setHighlighted = function(highlighted) { + if (highlighted == null) { + highlighted = false; + } + if (highlighted) { + if (!(highlighted instanceof Item)) { + highlighted = this.dataProvider.getItem(highlighted); + } + this.el.find(".select-item[data-value='" + highlighted.value + "']").addClass('highlighted').siblings().removeClass('highlighted'); + } else { + this.el.find('.select-item.highlighted').removeClass('highlighted'); + } + this.highlighted = highlighted; + return highlighted; + }; + + Popover.prototype.highlightNextItem = function() { + var $item; + if (this.highlighted) { + $item = this.el.find(".select-item[data-value='" + this.highlighted.value + "']").nextAll('.select-item:first'); + } else { + $item = this.el.find('.select-item:first'); + } + if ($item.length > 0) { + return this.setHighlighted($item.data('item')); + } + }; + + Popover.prototype.highlightPrevItem = function() { + var $item; + if (this.highlighted) { + $item = this.el.find(".select-item[data-value='" + this.highlighted.value + "']").prevAll('.select-item:first'); + } else { + $item = this.el.find('.select-item:first'); + } + if ($item.length > 0) { + return this.setHighlighted($item.data('item')); + } + }; + + Popover.prototype.setLoading = function(loading) { + if (loading == null) { + loading = true; + } + this.loading = loading; + if (loading) { + if (!(this.el.find('.loading').length > 0)) { + $('
').text(this.opts.locales.loading).appendTo(this.el); + } + this.el.addClass('loading'); + } else { + this.el.removeClass('loading'); + this.el.find('.loading').remove(); + } + this.setActive(loading); + return loading; + }; + + Popover.prototype.setActive = function(active) { + if (active == null) { + active = true; + } + this.active = active; + this.el.toggleClass('active', active); + if (active) { + this._scrollToHighlighted(); + this.triggerHandler('show'); + } else { + this.triggerHandler('hide'); + } + return active; + }; + + Popover.prototype.setPosition = function(position) { + if (position) { + this.el.css(position); + } + return this; + }; + + return Popover; + +})(SimpleModule); + +module.exports = Popover; + +},{"./models/data-provider.coffee":3,"./models/group.coffee":4,"./models/item.coffee":5}],"simple-select":[function(require,module,exports){ +var DataProvider, Group, HtmlSelect, Input, Item, MultipleInput, Popover, SimpleSelect, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +DataProvider = require('./models/data-provider.coffee'); + +Group = require('./models/group.coffee'); + +Item = require('./models/item.coffee'); + +HtmlSelect = require('./html-select.coffee'); + +Input = require('./input.coffee'); + +MultipleInput = require('./multiple-input.coffee'); + +Popover = require('./popover.coffee'); + +SimpleSelect = (function(superClass) { + extend(SimpleSelect, superClass); + + function SimpleSelect() { + return SimpleSelect.__super__.constructor.apply(this, arguments); + } + + SimpleSelect.prototype.opts = { + el: null, + remote: false, + cls: "", + onItemRender: null, + placeholder: "", + allowInput: false, + noWrap: false, + locales: null + }; + + SimpleSelect.locales = { + loading: 'Loading...', + noResults: 'No results found' + }; + + SimpleSelect._tpl = "
\n
\n
\n
"; + + SimpleSelect.prototype._init = function() { + var $blankOption, groups, placeholder, ref; + this.el = $(this.opts.el); + if (!(this.el.length > 0)) { + throw new Error("simple select: option el is required"); + return; + } + if ((ref = this.el.data("simpleSelect")) != null) { + ref.destroy(); + } + this.locales = $.extend({}, SimpleSelect.locales, this.opts.locales); + this.multiple = this.el.is('[multiple]'); + this._render(); + this.dataProvider = new DataProvider({ + remote: this.opts.remote, + selectEl: this.el + }); + this.htmlSelect = new HtmlSelect({ + el: this.el + }); + placeholder = this.opts.placeholder ? this.opts.placeholder : ($blankOption = this.htmlSelect.getBlankOption()) ? $blankOption.text() : ''; + if (this.multiple) { + this.input = new MultipleInput({ + el: this.wrapper.find('.input'), + placeholder: placeholder, + selected: this.htmlSelect.getValue() + }); + groups = this.dataProvider.excludeItems(this.input.selected); + } else { + this.input = new Input({ + el: this.wrapper.find('.input'), + placeholder: placeholder, + noWrap: this.opts.noWrap, + selected: this.htmlSelect.getValue() + }); + groups = this.dataProvider.groups; + } + this.popover = new Popover({ + el: this.wrapper.find('.popover'), + groups: groups, + onItemRender: this.opts.onItemRender, + locales: this.locales + }); + this._bind(); + if (this.el.prop('disabled')) { + return this.disable(); + } + }; + + SimpleSelect.prototype._render = function() { + this.el.hide().data("simpleSelect", this); + return this.wrapper = $(SimpleSelect._tpl).data("simpleSelect", this).addClass(this.opts.cls).insertBefore(this.el); + }; + + SimpleSelect.prototype._bind = function() { + this.dataProvider.on('filter', (function(_this) { + return function(e, groups, value) { + if (_this.multiple && _this.input.selected) { + groups = _this.dataProvider.excludeItems(_this.input.selected, groups); + } + _this.popover.setGroups(groups); + return _this.popover.setActive(!!(!_this.dataProvider.remote || value)); + }; + })(this)); + this.dataProvider.on('beforeFetch', (function(_this) { + return function(e) { + return _this.popover.setLoading(true); + }; + })(this)).on('fetch', (function(_this) { + return function(e) { + return _this.popover.setLoading(false); + }; + })(this)); + this.popover.on('itemClick', (function(_this) { + return function(e, $item, item) { + return _this.selectItem(item); + }; + })(this)); + this.popover.on('show', (function(_this) { + return function(e) { + _this._setPopoverPosition(); + if (!_this.multiple && _this.input.selected) { + return _this.popover.setHighlighted(_this.input.selected); + } else { + return _this.popover.highlightNextItem(); + } + }; + })(this)); + this.input.on('itemClick', (function(_this) { + return function(e, $item, item) { + return _this.unselectItem(item); + }; + })(this)); + this.input.on('clearClick', (function(_this) { + return function(e) { + return _this.clear(); + }; + })(this)); + this.input.on('expandClick', (function(_this) { + return function(e) { + return _this.popover.setActive(true); + }; + })(this)); + this.input.on('arrowPress', (function(_this) { + return function(e, direction) { + if (!_this.popover.active) { + return; + } + if (direction === 'up') { + return _this.popover.highlightPrevItem(); + } else { + return _this.popover.highlightNextItem(); + } + }; + })(this)); + this.input.on('enterPress', (function(_this) { + return function(e) { + if (_this.popover.active) { + if (_this.popover.highlighted) { + _this.selectItem(_this.popover.highlighted); + } else if (!_this.multiple) { + _this._setUserInput(); + } + return _this.popover.setActive(false); + } else { + return _this.el.closest('form').submit(); + } + }; + })(this)); + this.input.on('change', (function(_this) { + return function(e, value) { + if (!_this.multiple) { + _this._syncValue(); + } + _this.dataProvider.filter(value); + return _this._setPopoverPosition(); + }; + })(this)); + this.input.on('focus', (function(_this) { + return function(e) { + if (!(_this.dataProvider.remote && (!_this.input.getValue() || _this.input.selected))) { + return _this.popover.setActive(true); + } + }; + })(this)); + return this.input.on('blur', (function(_this) { + return function(e) { + var item, value; + if (!_this.multiple && !_this.input.selected) { + value = _this.input.getValue(); + if (item = _this.dataProvider.getItemByName(value)) { + _this.selectItem(item); + } else { + _this._setUserInput(value); + } + } + return _this.popover.setActive(false); + }; + })(this)); + }; + + SimpleSelect.prototype._setUserInput = function(value) { + if (value == null) { + value = this.input.getValue(); + } + if (this.opts.allowInput && !this.multiple) { + return this.wrapper.siblings(this.opts.allowInput).val(value); + } + }; + + SimpleSelect.prototype._setPopoverPosition = function() { + return this.popover.setPosition({ + top: this.input.el.outerHeight() + 2, + left: 0 + }); + }; + + SimpleSelect.prototype._syncValue = function() { + var currentValue, group, items, values; + if (this.multiple) { + items = this.input.selected || []; + } else { + items = this.input.selected ? [this.input.selected] : []; + } + currentValue = this.htmlSelect.getValue() || []; + if (!$.isArray(currentValue)) { + currentValue = [currentValue]; + } + if (this.dataProvider.remote) { + group = new Group({ + items: items + }); + this.htmlSelect.setGroups([group]); + } + values = items.map(function(item) { + return item.value; + }); + if (items.length > 0) { + this.htmlSelect.setValue(values); + } else { + this.htmlSelect.setValue(''); + } + if (currentValue.join(',') !== values.join(',')) { + this.triggerHandler('change', [this.input.selected]); + } + return items; + }; + + SimpleSelect.prototype.selectItem = function(item) { + if (!(item instanceof Item)) { + item = this.dataProvider.getItem(item); + } + if (!item) { + return; + } + if (this.multiple) { + this.input.addSelected(item); + } else { + this.input.setSelected(item); + } + this.popover.setActive(false); + if (this.opts.remote) { + this.popover.setGroups([]); + } else if (!this.multiple) { + this.popover.setGroups(this.dataProvider.getGroups()); + this.popover.setHighlighted(item); + } + this._setUserInput(''); + this._syncValue(); + return item; + }; + + SimpleSelect.prototype.unselectItem = function(item) { + if (!this.multiple) { + return; + } + if (!(item instanceof Item)) { + item = this.dataProvider.getItem(item); + } + if (!item) { + return; + } + this.input.removeSelected(item); + this._syncValue(); + return item; + }; + + SimpleSelect.prototype.clear = function() { + this.input.clear(); + this.popover.setActive(false); + this._setUserInput(''); + return this; + }; + + SimpleSelect.prototype.focus = function() { + return this.input.focus(); + }; + + SimpleSelect.prototype.blur = function() { + return this.input.blur(); + }; + + SimpleSelect.prototype.disable = function() { + this.input.setDisabled(true); + this.htmlSelect.setDisabled(true); + this.wrapper.addClass('disabled'); + return this; + }; + + SimpleSelect.prototype.enable = function() { + this.input.setDisabled(false); + this.htmlSelect.setDisabled(false); + this.wrapper.removeClass('disabled'); + return this; + }; + + SimpleSelect.prototype.destroy = function() { + this.el.removeData('simpleSelect').insertAfter(this.wrapper).show(); + this.wrapper.remove(); + return this; + }; + + return SimpleSelect; + +})(SimpleModule); + +module.exports = SimpleSelect; + +},{"./html-select.coffee":1,"./input.coffee":2,"./models/data-provider.coffee":3,"./models/group.coffee":4,"./models/item.coffee":5,"./multiple-input.coffee":6,"./popover.coffee":7}]},{},[]); + +return b('simple-select'); +})); diff --git a/dist/simple-select.min.js b/dist/simple-select.min.js new file mode 100644 index 0000000..5df16fd --- /dev/null +++ b/dist/simple-select.min.js @@ -0,0 +1,2 @@ +/* simple-select v2.1.0 | (c) Mycolorway Design | MIT License */ +!function(t,e){"object"==typeof module&&module.exports?module.exports=e(require("jquery"),require("simple-module")):(t.SimpleSelect=e(t.jQuery,t.SimpleModule),t.simple=t.simple||{},t.simple.select=function(e){return new t.SimpleSelect(e)},t.simple.select.locales=t.SimpleSelect.locales)}(this,function(t,e){var i=require=function t(e,i,r){function n(s,l){if(!i[s]){if(!e[s]){var u="function"==typeof require&&require;if(!l&&u)return u(s,!0);if(o)return o(s,!0);var p=new Error("Cannot find module '"+s+"'");throw p.code="MODULE_NOT_FOUND",p}var a=i[s]={exports:{}};e[s][0].call(a.exports,function(t){var i=e[s][1][t];return n(i?i:t)},a,a.exports,t,e,i,r)}return i[s].exports}for(var o="function"==typeof require&&require,s=0;s",{text:e.name,value:e.value,data:e.data}).appendTo(i)},i.prototype._render=function(){return this.el.empty(),0===this.groups.length?this.el.append("",{label:r.name}),t.each(r.items,function(t,i){return e._renderOption(i,n)}),e.el.append(n)}}(this)),this.el},i.prototype.setGroups=function(t){return this.groups=t,this._render()},i.prototype.getValue=function(){return this.el.val()},i.prototype.setValue=function(t){return this.el.val(t)},i.prototype.getBlankOption=function(){var t;return t=this.el.find('option:not([value]), option[value=""]'),t.length>0&&t},i}(e),r.exports=s},{"./models/group.coffee":4}],2:[function(i,r,n){var o,s,l,u=function(t,e){function i(){this.constructor=t}for(var r in e)p.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},p={}.hasOwnProperty;o=i("./models/data-provider.coffee"),l=i("./models/item.coffee"),s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return u(i,e),i.prototype.opts={el:null,noWrap:!1,placeholder:"",selected:!1},i.prototype._init=function(){return this.el=t(this.opts.el),this.dataProvider=o.getInstance(),this._render(),this._bind()},i.prototype._render=function(){return this.el.append('\n\n\n \n\n\n \n'),this.el.find(this.opts.noWrap?"textarea":"input:text").remove(),this.textField=this.el.find(".text-field"),this.textField.attr("placeholder",this.opts.placeholder),this.setSelected(this.opts.selected),this.el},i.prototype._bind=function(){return this.el.on("mousedown",function(t){return function(e){return t.textField.focus(),!1}}(this)),this.el.find(".link-expand").on("mousedown",function(t){return function(e){if(!t.disabled)return t.focused||t.focus(),t.trigger("expandClick"),!1}}(this)),this.el.find(".link-clear").on("mousedown",function(t){return function(e){if(!t.disabled)return t.trigger("clearClick"),!1}}(this)),this.textField.on("keydown.simple-select",function(t){return function(e){var i;return 40===e.which||38===e.which?(e.preventDefault(),i=40===e.which?"down":"up",t.triggerHandler("arrowPress",[i])):13===e.which?(e.preventDefault(),t.triggerHandler("enterPress")):27===e.which?(e.preventDefault(),t.blur()):8===e.which?t._onBackspacePress(e):void 0}}(this)).on("input.simple-select",function(t){return function(e){return t._inputTimer&&(clearTimeout(t._inputTimer),t._inputTimer=null),t._inputTimer=setTimeout(function(){return t._onInputChange()},200)}}(this)).on("blur.simple-select",function(t){return function(e){return t.focused=!1,t.triggerHandler("blur")}}(this)).on("focus.simple-select",function(t){return function(e){return t.focused=!0,t.triggerHandler("focus")}}(this))},i.prototype._onBackspacePress=function(t){if(this.selected)return t.preventDefault(),this.clear()},i.prototype._onInputChange=function(){return this._autoresize(),this.setSelected(!1),this.triggerHandler("change",[this.getValue()])},i.prototype._autoresize=function(){var t,e,i;if(!this.opts.noWrap)return this.textField.css("height",0),i=parseFloat(this.textField[0].scrollHeight),e=parseFloat(this.textField.css("border-top-width")),t=parseFloat(this.textField.css("border-bottom-width")),this.textField.css("height",i+e+t)},i.prototype.setValue=function(t){return this.textField.val(t),this._onInputChange()},i.prototype.getValue=function(){return this.textField.val()},i.prototype.setSelected=function(t){return null==t&&(t=!1),t?(t instanceof l||(t=this.dataProvider.getItem(t)),this.textField.val(t.name),this._autoresize(),this.el.addClass("selected")):this.el.removeClass("selected"),this.selected=t,t},i.prototype.setDisabled=function(t){return null==t&&(t=!1),this.disabled=t,this.textField.prop("disabled",t),this.el.toggleClass("disabled",t),t},i.prototype.focus=function(){return this.textField.focus()},i.prototype.blur=function(){return this.textField.blur()},i.prototype.clear=function(){return this.setValue("")},i}(e),r.exports=s},{"./models/data-provider.coffee":3,"./models/item.coffee":5}],3:[function(i,r,n){var o,s,l=function(t,e){function i(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},u={}.hasOwnProperty;s=i("./group.coffee"),o=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return l(i,e),i.getInstance=function(){return this.instance},i.prototype.opts={remote:!1,groups:null,selectEl:null},i.prototype._init=function(){return this.remote=this.opts.remote,this.opts.groups?this.setGroupsFromJson(this.opts.groups):this.opts.selectEl&&this.setGroupsFromHtml(this.opts.selectEl),i.instance=this},i.prototype._fetch=function(e,i){var r,n;if(this.remote&&this.triggerHandler("beforeFetch")!==!1)return n=function(t){return function(e){return t.setGroupsFromJson(e),t.triggerHandler("fetch",[t.groups]),"function"==typeof i?i(t.groups):void 0}}(this),e?t.ajax({url:this.remote.url,data:t.extend({},this.remote.params,(r={},r[""+this.remote.searchKey]=e,r)),dataType:"json"}).done(function(t){return n(t)}):void n([])},i.prototype.setGroupsFromJson=function(e){if(e)return this.groups=[],t.isArray(e)?this.groups.push(new s({items:e})):t.isPlainObject(e)&&t.each(e,function(t){return function(e,i){return t.groups.push(new s({name:e,items:i}))}}(this)),this.triggerHandler("change",[this.groups]),this.groups},i.prototype.setGroupsFromHtml=function(e){var i,r,n;if(r=t(e),r&&r.length>0)return this.groups=[],n=function(e){var i;return i=[],e.each(function(e,r){var n,o;if(n=t(r),o=n.val())return i.push([n.text(),o,n.data()])}),i},(i=r.find("optgroup")).length>0?i.each(function(e){return function(i,r){var o;return o=t(r),e.groups.push(new s({name:o.attr("label"),items:n(o.find("option"))}))}}(this)):this.groups.push(new s({items:n(r.find("option"))})),this.triggerHandler("change",[this.groups]),this.groups},i.prototype.getGroups=function(){return this.groups},i.prototype.getItem=function(e){var i;return i=null,t.each(this.groups,function(t,r){if(i=r.getItem(e))return!1}),i},i.prototype.getItemByName=function(e){var i;return i=null,t.each(this.groups,function(t,r){if(i=r.getItemByName(e))return!1}),i},i.prototype.filter=function(e,i){var r;return this.remote?this._fetch(e,function(t){return function(){return"function"==typeof i&&i(t.groups,e),t.triggerHandler("filter",[t.groups,e])}}(this)):(r=[],t.each(this.groups,function(t,i){var n;if(n=i.filterItems(e),n.items.length>0)return r.push(n)}),"function"==typeof i&&i(r,e),this.triggerHandler("filter",[r,e])),null},i.prototype.excludeItems=function(e,i){var r;return null==e&&(e=[]),null==i&&(i=this.groups),r=[],t.each(i,function(t,i){var n;if(n=i.excludeItems(e),n.items.length>0)return r.push(n)}),r},i}(e),r.exports=o},{"./group.coffee":4}],4:[function(e,i,r){var n,o;o=e("./item.coffee"),n=function(){function e(i){this.name=i.name||e.defaultName,this.items=[],t.isArray(i.items)&&i.items.length>0&&t.each(i.items,function(e){return function(i,r){return t.isArray(r)&&(r={name:r[0],value:r[1],data:r.length>2?r[2]:null}),e.items.push(new o(r))}}(this))}return e.defaultName="__default__",e.prototype.filterItems=function(i){var r;return r=new e({name:this.name}),t.each(this.items,function(t,e){if(e.match(i))return r.items.push(e)}),r},e.prototype.excludeItems=function(i){var r;return i=i.map(function(t){return t.value}),r=new e({name:this.name}),t.each(this.items,function(t,e){if(i.indexOf(e.value)===-1)return r.items.push(e)}),r},e.prototype.getItem=function(e){var i;return i=null,t.each(this.items,function(t,r){if(r.value===e&&(i=r),i)return!1}),i},e.prototype.getItemByName=function(e){var i;return i=null,t.each(this.items,function(t,r){if(r.name===e&&(i=r),i)return!1}),i},e}(),i.exports=n},{"./item.coffee":5}],5:[function(i,r,n){var o,s=function(t,e){function i(){this.constructor=t}for(var r in e)l.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},l={}.hasOwnProperty;o=function(e){function i(e){this.name=e.name,this.value=e.value.toString(),this.data={},t.isPlainObject(e.data)&&t.each(e.data,function(e){return function(i,r){return i=i.replace(/^data-/,"").split("-"),t.each(i,function(t,e){if(t>0)return i[t]=e.charAt(0).toUpperCase()+e.slice(1)}),e.data[i.join()]=r,null}}(this))}return s(i,e),i.prototype.match=function(t){var e,i,r;try{r=new RegExp("(^|\\s)"+t,"i")}catch(t){e=t,r=new RegExp("","i")}return i=this.data.key||this.name,r.test(i)},i}(e),r.exports=o},{}],6:[function(e,i,r){var n,o,s,l=function(t,e){function i(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},u={}.hasOwnProperty;o=e("./models/item.coffee"),n=e("./input.coffee"),s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return l(i,e),i.prototype.opts={el:null,placeholder:"",selected:!1},i._itemTpl='
\n \n \n
',i.prototype._render=function(){return this.el.append('').addClass("multiple"),this.textField=this.el.find("textarea"),this.textField.attr("placeholder",this.opts.placeholder),t.isArray(this.opts.selected)&&t.each(this.opts.selected,function(t){return function(e,i){return t.addSelected(i)}}(this)),this.el},i.prototype._bind=function(){return i.__super__._bind.call(this),this.el.on("mousedown",".selected-item",function(e){return function(i){var r;return r=t(i.currentTarget),e.triggerHandler("itemClick",[r,r.data("item")]),!1}}(this))},i.prototype._onBackspacePress=function(t){if(!this.getValue())return t.preventDefault(),this.el.find(".selected-item:last").mousedown()},i.prototype._onInputChange=function(){return this.triggerHandler("change",[this.getValue()])},i.prototype._autoresize=function(){},i.prototype._setPlaceholder=function(t){return null==t&&(t=!0),t?this.textField.attr("placeholder",this.opts.placeholder):this.textField.removeAttr("placeholder")},i.prototype.setSelected=function(t){return null==t&&(t=!1),t?this.addSelected(selected):this.clear()},i.prototype.addSelected=function(e){var r;if(e instanceof o||(e=this.dataProvider.getItem(e)),e)return this.selected||(this.selected=[]),this.selected.push(e),r=t(i._itemTpl).attr("data-value",e.value).data("item",e),r.find(".item-label").text(e.name),r.insertBefore(this.textField),this.setValue(""),this._setPlaceholder(!1),e},i.prototype.removeSelected=function(e){if(e instanceof o||(e=this.dataProvider.getItem(e)),e)return this.selected&&(t.each(this.selected,function(t){return function(i,r){if(r.value===e.value)return t.selected.splice(i,1),!1}}(this)),0===this.selected.length&&(this.selected=!1,this._setPlaceholder(!0))),this.el.find(".selected-item[data-value='"+e.value+"']").remove(),this.setValue(""),e},i.prototype.clear=function(){return this.setValue(""),this.selected=!1,this._setPlaceholder(!0),this.el.find(".selected-item").remove()},i}(n),i.exports=s},{"./input.coffee":2,"./models/item.coffee":5}],7:[function(i,r,n){var o,s,l,u,p=function(t,e){function i(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=i("./models/data-provider.coffee"),s=i("./models/group.coffee"),l=i("./models/item.coffee"),u=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return p(i,e),i._itemTpl='
\n \n \n
',i.prototype.opts={el:null,groups:[],onItemRender:null,localse:{}},i.prototype._init=function(){return this.el=t(this.opts.el),this.groups=this.opts.groups,this._render(),this._bind()},i.prototype._renderItem=function(e){var r;return r=t(i._itemTpl).data("item",e),r.find(".label").text(e.name),e.data.hint&&r.find(".hint").text(e.data.hint),r.attr("data-value",e.value),this.el.append(r),t.isFunction(this.opts.onItemRender)&&this.opts.onItemRender.call(this,r,e),r},i.prototype._render=function(){var e;return this.el.empty(),e=1===this.groups.length&&this.groups[0].name===s.defaultName,0===this.groups.length||e&&0===this.groups[0].items.length?t('
').text(this.opts.locales.noResults).appendTo(this.el):e?t.each(this.groups[0].items,function(t){return function(e,i){return t._renderItem(i)}}(this)):t.each(this.groups,function(e){return function(i,r){return t('
').text(r.name).appendTo(e.el),t.each(r.items,function(t,i){return e._renderItem(i)})}}(this)),this.el},i.prototype._bind=function(){return this.el.on("mousedown",".select-item",function(e){return function(i){var r;return r=t(i.currentTarget),e.triggerHandler("itemClick",[r,r.data("item")]),!1}}(this))},i.prototype._scrollToHighlighted=function(){var t;if(t=this.el.find(".select-item.highlighted"),t.length>0)return this.el.scrollTop(t.position().top)},i.prototype.setGroups=function(t){return this.setHighlighted(!1),this.setLoading(!1),this.setActive(!1),this.groups=t,this._render(),t},i.prototype.setHighlighted=function(t){return null==t&&(t=!1),t?(t instanceof l||(t=this.dataProvider.getItem(t)),this.el.find(".select-item[data-value='"+t.value+"']").addClass("highlighted").siblings().removeClass("highlighted")):this.el.find(".select-item.highlighted").removeClass("highlighted"),this.highlighted=t,t},i.prototype.highlightNextItem=function(){var t;if(t=this.highlighted?this.el.find(".select-item[data-value='"+this.highlighted.value+"']").nextAll(".select-item:first"):this.el.find(".select-item:first"),t.length>0)return this.setHighlighted(t.data("item"))},i.prototype.highlightPrevItem=function(){var t;if(t=this.highlighted?this.el.find(".select-item[data-value='"+this.highlighted.value+"']").prevAll(".select-item:first"):this.el.find(".select-item:first"),t.length>0)return this.setHighlighted(t.data("item"))},i.prototype.setLoading=function(e){return null==e&&(e=!0),this.loading=e,e?(this.el.find(".loading").length>0||t('
').text(this.opts.locales.loading).appendTo(this.el),this.el.addClass("loading")):(this.el.removeClass("loading"),this.el.find(".loading").remove()),this.setActive(e),e},i.prototype.setActive=function(t){return null==t&&(t=!0),this.active=t,this.el.toggleClass("active",t),t?(this._scrollToHighlighted(),this.triggerHandler("show")):this.triggerHandler("hide"),t},i.prototype.setPosition=function(t){return t&&this.el.css(t),this},i}(e),r.exports=u},{"./models/data-provider.coffee":3,"./models/group.coffee":4,"./models/item.coffee":5}],"simple-select":[function(i,r,n){var o,s,l,u,p,a,h,c,d=function(t,e){function i(){this.constructor=t}for(var r in e)f.call(e,r)&&(t[r]=e[r]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},f={}.hasOwnProperty;o=i("./models/data-provider.coffee"),s=i("./models/group.coffee"),p=i("./models/item.coffee"),l=i("./html-select.coffee"),u=i("./input.coffee"),a=i("./multiple-input.coffee"),h=i("./popover.coffee"),c=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return d(i,e),i.prototype.opts={el:null,remote:!1,cls:"",onItemRender:null,placeholder:"",allowInput:!1,noWrap:!1,locales:null},i.locales={loading:"Loading...",noResults:"No results found"},i._tpl='
\n
\n
\n
',i.prototype._init=function(){var e,r,n,s;if(this.el=t(this.opts.el),!(this.el.length>0))throw new Error("simple select: option el is required");if(null!=(s=this.el.data("simpleSelect"))&&s.destroy(),this.locales=t.extend({},i.locales,this.opts.locales),this.multiple=this.el.is("[multiple]"),this._render(),this.dataProvider=new o({remote:this.opts.remote,selectEl:this.el}),this.htmlSelect=new l({el:this.el}),n=this.opts.placeholder?this.opts.placeholder:(e=this.htmlSelect.getBlankOption())?e.text():"",this.multiple?(this.input=new a({el:this.wrapper.find(".input"),placeholder:n,selected:this.htmlSelect.getValue()}),r=this.dataProvider.excludeItems(this.input.selected)):(this.input=new u({el:this.wrapper.find(".input"),placeholder:n,noWrap:this.opts.noWrap,selected:this.htmlSelect.getValue()}),r=this.dataProvider.groups),this.popover=new h({el:this.wrapper.find(".popover"),groups:r,onItemRender:this.opts.onItemRender,locales:this.locales}),this._bind(),this.el.prop("disabled"))return this.disable()},i.prototype._render=function(){return this.el.hide().data("simpleSelect",this),this.wrapper=t(i._tpl).data("simpleSelect",this).addClass(this.opts.cls).insertBefore(this.el)},i.prototype._bind=function(){return this.dataProvider.on("filter",function(t){return function(e,i,r){return t.multiple&&t.input.selected&&(i=t.dataProvider.excludeItems(t.input.selected,i)),t.popover.setGroups(i),t.popover.setActive(!(t.dataProvider.remote&&!r))}}(this)),this.dataProvider.on("beforeFetch",function(t){return function(e){return t.popover.setLoading(!0)}}(this)).on("fetch",function(t){return function(e){return t.popover.setLoading(!1)}}(this)),this.popover.on("itemClick",function(t){return function(e,i,r){return t.selectItem(r)}}(this)),this.popover.on("show",function(t){return function(e){return t._setPopoverPosition(),!t.multiple&&t.input.selected?t.popover.setHighlighted(t.input.selected):t.popover.highlightNextItem()}}(this)),this.input.on("itemClick",function(t){return function(e,i,r){return t.unselectItem(r)}}(this)),this.input.on("clearClick",function(t){return function(e){return t.clear()}}(this)),this.input.on("expandClick",function(t){return function(e){return t.popover.setActive(!0)}}(this)),this.input.on("arrowPress",function(t){return function(e,i){if(t.popover.active)return"up"===i?t.popover.highlightPrevItem():t.popover.highlightNextItem()}}(this)),this.input.on("enterPress",function(t){return function(e){return t.popover.active?(t.popover.highlighted?t.selectItem(t.popover.highlighted):t.multiple||t._setUserInput(),t.popover.setActive(!1)):t.el.closest("form").submit()}}(this)),this.input.on("change",function(t){return function(e,i){return t.multiple||t._syncValue(),t.dataProvider.filter(i),t._setPopoverPosition()}}(this)),this.input.on("focus",function(t){return function(e){if(!t.dataProvider.remote||t.input.getValue()&&!t.input.selected)return t.popover.setActive(!0)}}(this)),this.input.on("blur",function(t){return function(e){var i,r;return t.multiple||t.input.selected||(r=t.input.getValue(),(i=t.dataProvider.getItemByName(r))?t.selectItem(i):t._setUserInput(r)),t.popover.setActive(!1)}}(this))},i.prototype._setUserInput=function(t){if(null==t&&(t=this.input.getValue()),this.opts.allowInput&&!this.multiple)return this.wrapper.siblings(this.opts.allowInput).val(t)},i.prototype._setPopoverPosition=function(){return this.popover.setPosition({top:this.input.el.outerHeight()+2,left:0})},i.prototype._syncValue=function(){var e,i,r,n;return r=this.multiple?this.input.selected||[]:this.input.selected?[this.input.selected]:[],e=this.htmlSelect.getValue()||[],t.isArray(e)||(e=[e]),this.dataProvider.remote&&(i=new s({items:r}),this.htmlSelect.setGroups([i])),n=r.map(function(t){return t.value}),r.length>0?this.htmlSelect.setValue(n):this.htmlSelect.setValue(""),e.join(",")!==n.join(",")&&this.triggerHandler("change",[this.input.selected]),r},i.prototype.selectItem=function(t){if(t instanceof p||(t=this.dataProvider.getItem(t)),t)return this.multiple?this.input.addSelected(t):this.input.setSelected(t),this.popover.setActive(!1),this.opts.remote?this.popover.setGroups([]):this.multiple||(this.popover.setGroups(this.dataProvider.getGroups()),this.popover.setHighlighted(t)),this._setUserInput(""),this._syncValue(),t},i.prototype.unselectItem=function(t){if(this.multiple&&(t instanceof p||(t=this.dataProvider.getItem(t)),t))return this.input.removeSelected(t),this._syncValue(),t},i.prototype.clear=function(){return this.input.clear(),this.popover.setActive(!1),this._setUserInput(""),this},i.prototype.focus=function(){return this.input.focus()},i.prototype.blur=function(){return this.input.blur()},i.prototype.disable=function(){return this.input.setDisabled(!0),this.htmlSelect.setDisabled(!0),this.wrapper.addClass("disabled"),this},i.prototype.enable=function(){return this.input.setDisabled(!1),this.htmlSelect.setDisabled(!1),this.wrapper.removeClass("disabled"),this},i.prototype.destroy=function(){return this.el.removeData("simpleSelect").insertAfter(this.wrapper).show(),this.wrapper.remove(),this},i}(e),r.exports=c},{"./html-select.coffee":1,"./input.coffee":2,"./models/data-provider.coffee":3,"./models/group.coffee":4,"./models/item.coffee":5,"./multiple-input.coffee":6,"./popover.coffee":7}]},{},[]);return i("simple-select")}); \ No newline at end of file diff --git a/gulpfile.coffee b/gulpfile.coffee new file mode 100644 index 0000000..84c576e --- /dev/null +++ b/gulpfile.coffee @@ -0,0 +1,18 @@ +gulp = require 'gulp' +compile = require './build/compile.coffee' +test = require './build/test.coffee' +publish = require './build/publish.coffee' +coffeelint = require './build/helpers/coffeelint.coffee' + +lint = -> + gulp.src 'build/**/*.coffee' + .pipe coffeelint() + +gulp.task 'default', gulp.series lint, compile, test, (done) -> + gulp.watch 'build/**/*.coffee', lint + + gulp.watch 'src/**/*.coffee', gulp.series compile.coffee, test + gulp.watch 'styles/**/*.scss', compile.sass + gulp.watch 'test/**/*.coffee', test + + done() diff --git a/karma.coffee b/karma.coffee new file mode 100644 index 0000000..b9fdc72 --- /dev/null +++ b/karma.coffee @@ -0,0 +1,96 @@ +module.exports = (config) -> + config.set + + # base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '' + + + # frameworks to use + # available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['coffee-coverage', 'browserify', 'mocha', 'chai', 'sinon'] + + + # list of files / patterns to load in the browser + files: [ + 'node_modules/jquery/dist/jquery.js' + 'node_modules/simple-module/lib/module.js' + 'test/coverage-init.js' + 'src/simple-select.coffee' + 'test/**/*.coffee' + ] + + + # list of files to exclude + exclude: [ + ] + + + # preprocess matching files before serving them to the browser + # available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: + 'src/simple-select.coffee': ['browserify'] + 'test/**/*.coffee': ['browserify'] + + + browserify: + transform: [['browserify-coffee-coverage', {noInit: true, instrumentor: 'istanbul'}]] + extensions: ['.js', '.coffee'] + + + coffeeCoverage: + framework: + initAllSources: true + sourcesBasePath: 'src' + dest: 'test/coverage-init.js' + instrumentor: 'istanbul' + + + coverageReporter: + dir: 'coverage' + subdir: '.' + reporters: [ + { type: 'lcovonly' } + { type: 'text-summary' } + ] + + + # test results reporter to use + # possible values: 'dots', 'progress' + # available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['coverage', 'mocha'] + + + # web server port + port: 9876 + + + # enable / disable colors in the output (reporters and logs) + colors: true + + + # level of logging + # possible values: + # - config.LOG_DISABLE + # - config.LOG_ERROR + # - config.LOG_WARN + # - config.LOG_INFO + # - config.LOG_DEBUG + logLevel: config.LOG_INFO + + + # enable / disable watching file and executing tests whenever any file changes + autoWatch: false + + + # start these browsers + # available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['PhantomJS'] + + + # Continuous Integration mode + # if true, Karma captures browsers, runs the tests and exits + singleRun: true + + # Concurrency level + # how many browser should be started simultaneous + concurrency: Infinity diff --git a/lib/select.js b/lib/select.js deleted file mode 100644 index fea1def..0000000 --- a/lib/select.js +++ /dev/null @@ -1,549 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define('simple-select', ["jquery","simple-module"], function (a0,b1) { - return (root['select'] = factory(a0,b1)); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(require("jquery"),require("simple-module")); - } else { - root.simple = root.simple || {}; - root.simple['select'] = factory(jQuery,SimpleModule); - } -}(this, function ($, SimpleModule) { - -var Select, select, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - -Select = (function(superClass) { - extend(Select, superClass); - - function Select() { - return Select.__super__.constructor.apply(this, arguments); - } - - Select.prototype.opts = { - el: null, - cls: "", - onItemRender: $.noop, - placeholder: "", - allowInput: false, - multiline: true - }; - - Select.i18n = { - "zh-CN": { - all_options: "所有选项", - clear_selection: "清除选择", - loading: "加载中..." - }, - "en": { - all_options: "All options", - clear_selection: "Clear Selection", - loading: "Loading..." - } - }; - - Select._tpl = { - textarea: "", - input: "", - item: "
\n \n \n
", - group: "
\n
" - }; - - Select.prototype._init = function() { - var ref; - if (!this.opts.el) { - throw "simple select: option el is required"; - return; - } - if ((ref = this.opts.el.data("select")) != null) { - ref.destroy(); - } - this._render(); - this._bind(); - return this.autoresizeInput(); - }; - - Select.prototype._render = function() { - var inputTpl; - Select._tpl.select = "
\n \n \n \n \n \n \n
\n
" + (this._t('loading')) + "
\n
\n
"; - this.el = $(this.opts.el).hide(); - this.el.data("select", this); - this.select = $(Select._tpl.select).data("select", this).addClass(this.opts.cls).insertBefore(this.el); - if (this.opts.multiline) { - this.select.addClass('multiline'); - inputTpl = Select._tpl.textarea; - } else { - inputTpl = Select._tpl.input; - } - this.input = $(inputTpl).attr("placeholder", this.opts.placeholder || this.el.data('placeholder') || "").prependTo(this.select); - this.list = this.select.find(".select-list"); - this.requireSelect = true; - this._setGroupsItems(); - this.generateList(); - return this.select.toggleClass('require-select', this.requireSelect); - }; - - Select.prototype._setGroupsItems = function() { - this.groups = false; - return this.items = this.el.find("option").map((function(_this) { - return function(i, option) { - var $group, $option, groupLabel, item; - $option = $(option); - $group = $option.parent('optgroup'); - item = _this._item($option); - if ($group.length !== 0) { - if (_this.groups === false) { - _this.groups = {}; - } - groupLabel = $group.attr('label'); - if (!$.isArray(_this.groups[groupLabel])) { - _this.groups[groupLabel] = []; - } - _this.groups[groupLabel].push(item); - } - return item; - }; - })(this)).get(); - }; - - Select.prototype._item = function($option) { - var label, value; - value = $option.attr('value'); - label = $option.text().trim(); - if (!value) { - this.requireSelect = false; - return; - } - return $.extend({ - label: label, - _value: value - }, $option.data()); - }; - - Select.prototype._expand = function(expand) { - if (expand === false) { - this.input.removeClass("expanded"); - return this.list.hide(); - } else { - this.input.addClass("expanded"); - if (this.items.length > 0) { - this.list.show(); - } - this.list.css("top", this.input.outerHeight() + 4); - if (this._selectedIndex > -1) { - return this._scrollToSelected(); - } - } - }; - - Select.prototype._scrollToSelected = function() { - var $selectedEl; - if (this._selectedIndex < 0) { - return; - } - $selectedEl = this.list.find(".select-item").eq(this._selectedIndex); - return this.list.scrollTop($selectedEl.position().top); - }; - - Select.prototype._bind = function() { - this.select.find(".link-clear").on("mousedown", (function(_this) { - return function(e) { - _this.clearSelection(); - return false; - }; - })(this)); - this.select.find(".link-expand").on("mousedown", (function(_this) { - return function(e) { - _this._expand(!_this.input.hasClass("expanded")); - if (!_this._focused) { - _this.input.focus(); - } - return false; - }; - })(this)); - this.list.on("mousedown", (function(_this) { - return function(e) { - if (window.navigator.userAgent.toLowerCase().indexOf('msie') > -1) { - _this._scrollMousedown = true; - setTimeout(function() { - return _this.input.focus(); - }, 0); - } - return false; - }; - })(this)).on("mousedown", ".select-item", (function(_this) { - return function(e) { - var index; - index = _this.list.find(".select-item").index($(e.currentTarget)); - _this.selectItem(index); - _this.input.blur(); - return false; - }; - })(this)); - this.input.on("keydown.select", (function(_this) { - return function(e) { - return _this._keydown(e); - }; - })(this)).on("keyup.select", (function(_this) { - return function(e) { - return _this._keyup(e); - }; - })(this)).on("blur.select", (function(_this) { - return function(e) { - return _this._blur(e); - }; - })(this)).on("focus.select", (function(_this) { - return function(e) { - return _this._focus(e); - }; - })(this)); - return this.select.on("mousedown", (function(_this) { - return function(e) { - return _this.input.focus(); - }; - })(this)); - }; - - Select.prototype._keydown = function(e) { - var $itemEls, $nextEl, $prevEl, $selectedEl, index; - if (!(this.items && this.items.length)) { - return; - } - if (this.triggerHandler(e) === false) { - return; - } - if (e.which === 40 || e.which === 38) { - if (!this.input.hasClass("expanded")) { - this._expand(); - $itemEls = this.list.find(".select-item").show(); - if (this._selectedIndex < 0) { - $itemEls.first().addClass("selected"); - } - } else { - $selectedEl = this.list.find(".select-item.selected"); - if (!$selectedEl.length) { - this.list.find(".select-item:first").addClass("selected"); - return; - } - if (e.which === 38) { - $prevEl = $selectedEl.prevAll(".select-item:visible:first"); - if ($prevEl.length) { - $selectedEl.removeClass("selected"); - $prevEl.addClass("selected"); - } - } else if (e.which === 40) { - $nextEl = $selectedEl.nextAll(".select-item:visible:first"); - if ($nextEl.length) { - $selectedEl.removeClass("selected"); - $nextEl.addClass("selected"); - } - } - } - } else if (e.which === 13) { - e.preventDefault(); - if (this.input.hasClass("expanded")) { - $selectedEl = this.list.find(".select-item.selected"); - if ($selectedEl.length) { - index = this.list.find(".select-item").index($selectedEl); - this.selectItem(index); - return false; - } - } else if (this._selectedIndex > -1) { - this.selectItem(this._selectedIndex); - } - if (this.opts.allowInput) { - this.input.blur(); - return false; - } - this.clearSelection(); - return false; - } else if (e.which === 27) { - e.preventDefault(); - this.input.blur(); - } else if (e.which === 8) { - if (this.select.hasClass("selected")) { - this.clearSelection(); - } - if (!this.input.hasClass("expanded")) { - this._expand(); - } - } - return this.autoresizeInput(); - }; - - Select.prototype._keyup = function(e) { - var $itemEls; - if ($.inArray(e.which, [13, 40, 38, 9, 27]) > -1) { - return false; - } - this.autoresizeInput(); - if (this._keydownTimer) { - clearTimeout(this._keydownTimer); - this._keydownTimer = null; - } - $itemEls = this.list.find(".select-item"); - return this._keydownTimer = setTimeout((function(_this) { - return function() { - var re, results, value; - if (!_this.input.hasClass("expanded")) { - _this._expand(); - } - if (_this.select.hasClass("selected")) { - _this.select.removeClass("selected"); - } - value = $.trim(_this.input.val()); - if (!value) { - if (_this.items.length > 0) { - _this.list.show(); - } - $itemEls.show().removeClass("selected"); - return; - } - try { - re = new RegExp("(|\\s)" + value, "i"); - } catch (_error) { - e = _error; - re = new RegExp("", "i"); - } - results = $itemEls.hide().removeClass("selected").filter(function() { - return re.test($(this).data("key")); - }); - if (results.length) { - if (_this.items.length > 0) { - _this.list.show(); - } - return results.show().first().addClass("selected"); - } else { - return _this.list.hide(); - } - }; - })(this), 0); - }; - - Select.prototype._blur = function(e) { - var matchIdx, value; - if (this._scrollMousedown) { - this._scrollMousedown = false; - return false; - } - this.input.removeClass("expanded error"); - this.list.hide().find(".select-item").show().removeClass("selected"); - value = $.trim(this.input.val()); - if (!this.select.hasClass("selected")) { - if (this.opts.allowInput) { - this.el.val(''); - this.trigger('select', [ - { - label: value, - _value: -1 - } - ]); - } else if (value) { - matchIdx = -1; - $.each(this.items, function(i, item) { - if (item.label === value) { - matchIdx = i; - return false; - } - }); - if (matchIdx >= 0) { - this.selectItem(matchIdx); - } else { - this.selectItem(Math.max(this._selectedIndex || -1, 0)); - } - } else if (this.requireSelect && this.items.length > 0) { - this.selectItem(0); - } else { - this.el.val(''); - } - } - return this._focused = false; - }; - - Select.prototype._focus = function(e) { - this._expand(); - if (this._selectedIndex > -1) { - this.list.find(".select-item").eq(this._selectedIndex).addClass("selected"); - } - setTimeout((function(_this) { - return function() { - return _this.input.select(); - }; - })(this), 0); - return this._focused = true; - }; - - Select.prototype.generateList = function() { - var $itemEl, idx, it, item, j, k, len, len1, ref, ref1, results1; - this.list.empty(); - if (this.groups) { - $.each(this.groups, (function(_this) { - return function(groupLabel, items) { - var $groupEl, $itemEl, item, j, len, results1; - $groupEl = $(Select._tpl.group); - $groupEl.text(groupLabel); - _this.list.append($groupEl); - results1 = []; - for (j = 0, len = items.length; j < len; j++) { - item = items[j]; - $itemEl = _this._itemEl(item); - _this.list.append($itemEl); - if ($.isFunction(_this.opts.onItemRender)) { - results1.push(_this.opts.onItemRender.call(_this, $itemEl, item)); - } else { - results1.push(void 0); - } - } - return results1; - }; - })(this)); - } else { - ref = this.items; - for (j = 0, len = ref.length; j < len; j++) { - item = ref[j]; - $itemEl = this._itemEl(item); - this.list.append($itemEl); - if ($.isFunction(this.opts.onItemRender)) { - this.opts.onItemRender.call(this, $itemEl, item); - } - } - } - ref1 = this.items; - results1 = []; - for (idx = k = 0, len1 = ref1.length; k < len1; idx = ++k) { - it = ref1[idx]; - if (it._value === this.el.val()) { - this.selectItem(idx); - break; - } else { - results1.push(void 0); - } - } - return results1; - }; - - Select.prototype._itemEl = function(item) { - var $itemEl; - $itemEl = $(Select._tpl.item).data(item); - $itemEl.find(".label span").text(item.label); - $itemEl.find(".hint").text(item.hint); - return $itemEl; - }; - - Select.prototype.setItems = function(items, requireSelect) { - var item, j, len; - if (requireSelect == null) { - requireSelect = true; - } - this.items = []; - this.clearSelection(); - this.list.empty(); - this.el.empty(); - this.requireSelect = requireSelect; - this.select.toggleClass('require-select', this.requireSelect); - if (!this.requireSelect) { - this.el.prepend(''); - } - if ($.isArray(items) && items.length > 0) { - for (j = 0, len = items.length; j < len; j++) { - item = items[j]; - this.el.append(""); - } - this._setGroupsItems(); - this.generateList(); - } - if ($.type(items) === 'object') { - $.each(items, (function(_this) { - return function(groupLabel, items) { - var $group, k, len1; - $group = $(""); - for (k = 0, len1 = items.length; k < len1; k++) { - item = items[k]; - $group.append(""); - } - return _this.el.append($group); - }; - })(this)); - this._setGroupsItems(); - return this.generateList(); - } - }; - - Select.prototype.selectItem = function(index) { - var item; - if (!this.items) { - return; - } - if ($.isNumeric(index)) { - if (index < 0) { - return; - } - item = this.items[index]; - this.select.addClass("selected"); - this.input.val(item.label).removeClass("expanded error"); - this.list.hide().find(".select-item").eq(index).addClass("selected"); - this._selectedIndex = index; - this.el.val(item._value); - this.trigger("select", [item]); - this.autoresizeInput(); - } - if (this._selectedIndex > -1) { - return this.items[this._selectedIndex]; - } - }; - - Select.prototype.clearSelection = function() { - this.input.val("").removeClass("expanded error"); - this.select.removeClass("selected"); - this.list.hide().find(".select-item").show().removeClass("selected"); - this._selectedIndex = -1; - this.el.val(''); - this.trigger("clear"); - return this.autoresizeInput(); - }; - - Select.prototype.autoresizeInput = function() { - if (!this.opts.multiline) { - return; - } - return setTimeout((function(_this) { - return function() { - _this.input.css("height", 0); - return _this.input.css("height", parseInt(_this.input.get(0).scrollHeight) + parseInt(_this.input.css("border-top-width")) + parseInt(_this.input.css("border-bottom-width"))); - }; - })(this), 0); - }; - - Select.prototype.disable = function() { - this.input.prop("disabled", true); - return this.select.find(".link-expand, .link-clear").hide(); - }; - - Select.prototype.enable = function() { - this.input.prop("disabled", false); - return this.select.find(".link-expand, .link-clear").attr("style", ""); - }; - - Select.prototype.destroy = function() { - this.select.remove(); - this.el.removeData('select'); - return this.el.show(); - }; - - return Select; - -})(SimpleModule); - -select = function(opts) { - return new Select(opts); -}; - -return select; - -})); diff --git a/package.json b/package.json index d306f78..2e19542 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,70 @@ { "name": "simple-select", - "version": "2.0.14", - "description": "a simple select plugin based on Simple Module", - "main": "lib/select.js", + "githubOwner": "mycolorway", + "version": "2.1.0", + "description": "Autocomplete select component", + "homepage": "http://mycolorway.github.io/simple-select", "repository": { "type": "git", - "url": "git@github.com:mycolorway/simple-select.git" + "url": "https://github.com/mycolorway/simple-select.git" }, - "author": "kshift", + "author": "farthinker", "license": "MIT", "bugs": { "url": "https://github.com/mycolorway/simple-select/issues" }, - "homepage": "https://github.com/mycolorway/simple-select", + "main": "dist/simple-select.js", + "scripts": { + "start": "gulp", + "test": "gulp test" + }, + "umd": { + "name": "SimpleSelect", + "dependencies": { + "cjs": [ + "jquery", + "simple-module" + ], + "global": [ + "jQuery", + "SimpleModule" + ], + "params": [ + "$", + "SimpleModule" + ] + } + }, "dependencies": { - "jquery": "2.x", - "simple-module": "~2.0.5" + "jquery": "~2.2.4", + "simple-module": "~2.0.6" }, "devDependencies": { - "grunt": "0.x", - "grunt-contrib-watch": "0.x", - "grunt-contrib-coffee": "0.x", - "grunt-contrib-sass": "0.x", - "grunt-contrib-jasmine": "0.x", - "grunt-umd": "2.x" + "browserify": "^13.0.0", + "browserify-coffee-coverage": "^1.1.1", + "chai": "^3.5.0", + "coffee-script": "^1.10.0", + "coffeelint": "^1.15.0", + "coveralls": "^2.11.8", + "gulp": "github:gulpjs/gulp#4.0", + "gulp-util": "^3.0.7", + "istanbul": "^0.4.2", + "karma": "^1.1.1", + "karma-browserify": "^5.0.5", + "karma-chai": "^0.1.0", + "karma-coffee-coverage": "^1.1.2", + "karma-coverage": "^1.0.0", + "karma-mocha": "^1.0.1", + "karma-mocha-reporter": "^2.0.4", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sinon": "^1.0.5", + "lodash": "^4.13.1", + "mocha": "^2.5.3", + "node-sass": "^3.8.0", + "phantomjs-prebuilt": "^2.1.7", + "sinon": "^1.17.5", + "through2": "^2.0.1", + "uglify-js": "^2.6.2", + "watchify": "^3.7.0" } } diff --git a/spec/select-spec.coffee b/spec/select-spec.coffee deleted file mode 100644 index 6bc023b..0000000 --- a/spec/select-spec.coffee +++ /dev/null @@ -1,181 +0,0 @@ -selectEl = null - -beforeEach -> - selectEl = $(""" - - """) - -afterEach -> - $(".simple-select").each () -> - $(@).data("select").destroy() - $("select").remove() - - -describe 'Simple Select', -> - - it 'should inherit from SimpleModule', -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - expect(select instanceof SimpleModule).toBe(true) - - it "should see select if everything is ok", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - expect($("body .simple-select").length).toBe(1) - - it "should see throw error if no content", -> - expect(simple.select).toThrow() - - it "should have three items if everything is ok", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - expect($("body .simple-select .select-item").length).toBe(3) - - it "should see one item if type some content", (done) -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - $(".select-result").val("John").trigger("keyup") - setTimeout -> - target = $("body .simple-select .select-item:visible") - expect(target.length and target.hasClass("selected")).toBe(true) - done() - , 10 - - it "should set original select form element value according to select item", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - select.selectItem(1) - expect(select.el.val()).toBe('1') - select.clearSelection() - expect(select.el.val()).toBe(null) - - it "should see 'select-list' if click 'link-expand'", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - $(".link-expand").trigger("mousedown") - expect($("body .simple-select .select-list:visible").length).toBe(1) - - it "should have value if select item", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - $(".select-item:first").trigger("mousedown") - expect($(".select-result").val().length).toBeGreaterThan(0) - - it "should remove selected' if click 'link-clear'", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - $(".select-item:first").trigger("mousedown") - $(".link-clear").trigger("mousedown") - expect($(".select-result").val().length).toBe(0) - - it "should work if use setItems to set items", -> - $(""" - - """).appendTo("body") - select = simple.select - el: $("#select-two") - - select.setItems [ - { - label: "张三" - key: "zhangsan zs 张三" - id: "1" - } - { - label: "李四" - key: "lisi ls 李四" - id: "2" - } - { - label: "王麻子" - key: "wangmazi wmz 王麻子" - id: "3" - } - ] - - expect($("body .simple-select .select-item").length).toBe(3) - - it 'should not show option without value', -> - selectEl.append('') - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - $(".link-expand").trigger("mousedown") - expect($("body .simple-select .select-item:visible").length).toBe(3) - - it 'should always select default value if all options with value', -> - selectEl.find('option[value=2]').prop('selected', true) - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - expect(select.el.val()).toBe('2') - - select.clearSelection() - select.input.blur() - - expect(select.el.val()).toBe('0') - - it 'should always select default value if one option without value', -> - selectEl.append('') - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - - select.clearSelection() - select.input.blur() - - expect(select.requireSelect).toBe(false) - expect(select.el.val()).toBe('') - - it "should keep reference in el", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - expect(selectEl.data('select')).toBe(select) - - it "should destroy reference in el after destroy", -> - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - select.destroy() - expect(selectEl.data('select')).not.toBe(select) - - it "should set placeholder by setting data-placeholder", -> - hint = "some hint text" - selectEl.data("placeholder", hint) - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - expect(select.input.attr("placeholder")).toBe(hint) - - it "should override data-placeholder by setting placeholder in opt", -> - hint = "some hint text" - anotherHint = "another hint text" - selectEl.data("placeholder", hint) - selectEl.appendTo("body") - select = simple.select - el: $("#select-one") - placeholder: anotherHint - expect(select.input.attr("placeholder")).toBe(anotherHint) diff --git a/spec/select-spec.js b/spec/select-spec.js deleted file mode 100644 index ec51bb3..0000000 --- a/spec/select-spec.js +++ /dev/null @@ -1,196 +0,0 @@ -(function() { - var selectEl; - - selectEl = null; - - beforeEach(function() { - return selectEl = $(""); - }); - - afterEach(function() { - $(".simple-select").each(function() { - return $(this).data("select").destroy(); - }); - return $("select").remove(); - }); - - describe('Simple Select', function() { - it('should inherit from SimpleModule', function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - return expect(select instanceof SimpleModule).toBe(true); - }); - it("should see select if everything is ok", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - return expect($("body .simple-select").length).toBe(1); - }); - it("should see throw error if no content", function() { - return expect(simple.select).toThrow(); - }); - it("should have three items if everything is ok", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - return expect($("body .simple-select .select-item").length).toBe(3); - }); - it("should see one item if type some content", function(done) { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - $(".select-result").val("John").trigger("keyup"); - return setTimeout(function() { - var target; - target = $("body .simple-select .select-item:visible"); - expect(target.length && target.hasClass("selected")).toBe(true); - return done(); - }, 10); - }); - it("should set original select form element value according to select item", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - select.selectItem(1); - expect(select.el.val()).toBe('1'); - select.clearSelection(); - return expect(select.el.val()).toBe(null); - }); - it("should see 'select-list' if click 'link-expand'", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - $(".link-expand").trigger("mousedown"); - return expect($("body .simple-select .select-list:visible").length).toBe(1); - }); - it("should have value if select item", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - $(".select-item:first").trigger("mousedown"); - return expect($(".select-result").val().length).toBeGreaterThan(0); - }); - it("should remove selected' if click 'link-clear'", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - $(".select-item:first").trigger("mousedown"); - $(".link-clear").trigger("mousedown"); - return expect($(".select-result").val().length).toBe(0); - }); - it("should work if use setItems to set items", function() { - var select; - $("").appendTo("body"); - select = simple.select({ - el: $("#select-two") - }); - select.setItems([ - { - label: "张三", - key: "zhangsan zs 张三", - id: "1" - }, { - label: "李四", - key: "lisi ls 李四", - id: "2" - }, { - label: "王麻子", - key: "wangmazi wmz 王麻子", - id: "3" - } - ]); - return expect($("body .simple-select .select-item").length).toBe(3); - }); - it('should not show option without value', function() { - var select; - selectEl.append(''); - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - $(".link-expand").trigger("mousedown"); - return expect($("body .simple-select .select-item:visible").length).toBe(3); - }); - it('should always select default value if all options with value', function() { - var select; - selectEl.find('option[value=2]').prop('selected', true); - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - expect(select.el.val()).toBe('2'); - select.clearSelection(); - select.input.blur(); - return expect(select.el.val()).toBe('0'); - }); - it('should always select default value if one option without value', function() { - var select; - selectEl.append(''); - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - select.clearSelection(); - select.input.blur(); - expect(select.requireSelect).toBe(false); - return expect(select.el.val()).toBe(''); - }); - it("should keep reference in el", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - return expect(selectEl.data('select')).toBe(select); - }); - it("should destroy reference in el after destroy", function() { - var select; - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - select.destroy(); - return expect(selectEl.data('select')).not.toBe(select); - }); - it("should set placeholder by setting data-placeholder", function() { - var hint, select; - hint = "some hint text"; - selectEl.data("placeholder", hint); - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one") - }); - return expect(select.input.attr("placeholder")).toBe(hint); - }); - return it("should override data-placeholder by setting placeholder in opt", function() { - var anotherHint, hint, select; - hint = "some hint text"; - anotherHint = "another hint text"; - selectEl.data("placeholder", hint); - selectEl.appendTo("body"); - select = simple.select({ - el: $("#select-one"), - placeholder: anotherHint - }); - return expect(select.input.attr("placeholder")).toBe(anotherHint); - }); - }); - -}).call(this); diff --git a/src/html-select.coffee b/src/html-select.coffee new file mode 100644 index 0000000..63d75da --- /dev/null +++ b/src/html-select.coffee @@ -0,0 +1,56 @@ +Group = require './models/group.coffee' + +class HtmlSelect extends SimpleModule + + opts: + el: null + groups: null + + _init: -> + @el = $ @opts.el + @groups = @opts.groups + @_render() if @groups + + _renderOption: (item, $parent = @el) -> + $ '", + label: group.name + $.each group.items, (i, item) => + @_renderOption item, $group + @el.append $group + + @el + + setGroups: (groups) -> + @groups = groups + @_render() + + getValue: -> + @el.val() + + setValue: (value) -> + @el.val value + + getBlankOption: -> + $blankOption = @el.find('option:not([value]), option[value=""]') + if $blankOption.length > 0 then $blankOption else false + +module.exports = HtmlSelect diff --git a/src/input.coffee b/src/input.coffee new file mode 100644 index 0000000..dce6af9 --- /dev/null +++ b/src/input.coffee @@ -0,0 +1,137 @@ +DataProvider = require './models/data-provider.coffee' +Item = require './models/item.coffee' + +class Input extends SimpleModule + + opts: + el: null + noWrap: false + placeholder: '' + selected: false + + _init: -> + @el = $ @opts.el + @dataProvider = DataProvider.getInstance() + @_render() + @_bind() + + _render: -> + @el.append ''' + + + + + + + + + ''' + + @el.find(if @opts.noWrap then 'textarea' else 'input:text').remove() + @textField = @el.find '.text-field' + @textField.attr 'placeholder', @opts.placeholder + @setSelected @opts.selected + @el + + _bind: -> + @el.on 'mousedown', (e) => + @textField.focus() + false + + @el.find(".link-expand").on "mousedown", (e) => + return if @disabled + @focus() unless @focused + @trigger 'expandClick' + false + + @el.find(".link-clear").on "mousedown", (e) => + return if @disabled + @trigger 'clearClick' + false + + @textField.on "keydown.simple-select", (e) => + if e.which == 40 or e.which == 38 # up and down + e.preventDefault() + direction = if e.which == 40 then 'down' else 'up' + @triggerHandler 'arrowPress', [direction] + + else if e.which == 13 # enter + e.preventDefault() + @triggerHandler 'enterPress' + + else if e.which == 27 # esc + e.preventDefault() + @blur() + + else if e.which == 8 + @_onBackspacePress e + + .on "input.simple-select", (e) => + if @_inputTimer + clearTimeout @_inputTimer + @_inputTimer = null + @_inputTimer = setTimeout => + @_onInputChange() + , 200 + .on "blur.simple-select", (e) => + @focused = false + @triggerHandler 'blur' + .on "focus.simple-select", (e) => + @focused = true + @triggerHandler 'focus' + + _onBackspacePress: (e) -> + if @selected + e.preventDefault() + @clear() + + _onInputChange: -> + @_autoresize() + @setSelected false + @triggerHandler 'change', [@getValue()] + + _autoresize: -> + return if @opts.noWrap + @textField.css("height", 0) + scrollHeight = parseFloat(@textField[0].scrollHeight) + borderTop = parseFloat(@textField.css("border-top-width")) + borderBottom = parseFloat(@textField.css("border-bottom-width")) + @textField.css("height", scrollHeight + borderTop + borderBottom) + + setValue: (value) -> + @textField.val value + @_onInputChange() + + getValue: -> + @textField.val() + + setSelected: (selected = false) -> + if selected + unless selected instanceof Item + selected = @dataProvider.getItem selected + @textField.val selected.name + @_autoresize() + @el.addClass 'selected' + else + @el.removeClass 'selected' + + @selected = selected + selected + + setDisabled: (disabled = false) -> + @disabled = disabled + @textField.prop 'disabled', disabled + @el.toggleClass 'disabled', disabled + disabled + + focus: -> + @textField.focus() + + blur: -> + @textField.blur() + + clear: -> + @setValue '' + + +module.exports = Input diff --git a/src/models/data-provider.coffee b/src/models/data-provider.coffee new file mode 100644 index 0000000..e7a0dbb --- /dev/null +++ b/src/models/data-provider.coffee @@ -0,0 +1,125 @@ +Group = require './group.coffee' + +class DataProvider extends SimpleModule + + @getInstance: -> + @instance + + opts: + remote: false + groups: null + selectEl: null + + _init: -> + @remote = @opts.remote + + if @opts.groups + @setGroupsFromJson @opts.groups + else if @opts.selectEl + @setGroupsFromHtml @opts.selectEl + + DataProvider.instance = @ + + _fetch: (value, callback) -> + return if !@remote || @triggerHandler('beforeFetch') == false + + onFetch = (groups) => + @setGroupsFromJson groups + @triggerHandler 'fetch', [@groups] + callback? @groups + + unless value + onFetch [] + return + + $.ajax + url: @remote.url + data: $.extend {}, @remote.params, + "#{@remote.searchKey}": value + dataType: 'json' + .done (groups) -> + onFetch groups + + setGroupsFromJson: (groups) -> + return unless groups + + @groups = [] + if $.isArray groups + @groups.push new Group + items: groups + else if $.isPlainObject groups + $.each groups, (groupName, groupItems) => + @groups.push new Group + name: groupName + items: groupItems + + @triggerHandler 'change', [@groups] + @groups + + setGroupsFromHtml: (selectEl) -> + $select = $ selectEl + return unless $select && $select.length > 0 + + @groups = [] + itemsFromOptions = ($options) -> + items = [] + $options.each (i, option) -> + $option = $ option + value = $option.val() + return unless value + items.push [$option.text(), value, $option.data()] + items + + if ($groups = $select.find('optgroup')).length > 0 + $groups.each (i, groupEl) => + $group = $ groupEl + @groups.push new Group + name: $group.attr('label') + items: itemsFromOptions($group.find('option')) + else + @groups.push new Group + items: itemsFromOptions($select.find('option')) + + @triggerHandler 'change', [@groups] + @groups + + getGroups: -> + @groups + + getItem: (value) -> + result = null + $.each @groups, (i, group) -> + result = group.getItem value + false if result + result + + getItemByName: (name) -> + result = null + $.each @groups, (i, group) -> + result = group.getItemByName name + false if result + result + + filter: (value, callback) -> + if @remote + @_fetch value, => + callback? @groups, value + @triggerHandler 'filter', [@groups, value] + else + groups = [] + $.each @groups, (i, group) -> + filteredGroup = group.filterItems(value) + groups.push(filteredGroup) if filteredGroup.items.length > 0 + + callback? groups, value + @triggerHandler 'filter', [groups, value] + null + + excludeItems: (items = [], groups = @groups) -> + results = [] + $.each groups, (i, group) -> + excludedGroup = group.excludeItems(items) + results.push(excludedGroup) if excludedGroup.items.length > 0 + results + +module.exports = DataProvider diff --git a/src/models/group.coffee b/src/models/group.coffee new file mode 100644 index 0000000..43ff09f --- /dev/null +++ b/src/models/group.coffee @@ -0,0 +1,49 @@ +Item = require './item.coffee' + +class Group + + @defaultName: '__default__' + + constructor: (opts) -> + @name = opts.name || Group.defaultName + @items = [] + + if $.isArray(opts.items) && opts.items.length > 0 + $.each opts.items, (i, item) => + if $.isArray item + item = + name: item[0] + value: item[1] + data: if item.length > 2 then item[2] else null + @items.push new Item item + + filterItems: (value) -> + group = new Group + name: @name + $.each @items, (i, item) -> + group.items.push(item) if item.match(value) + group + + excludeItems: (items) -> + items = items.map (item) -> item.value + group = new Group + name: @name + $.each @items, (i, item) -> + group.items.push(item) if items.indexOf(item.value) == -1 + group + + getItem: (value) -> + result = null + $.each @items, (i, item) -> + result = item if item.value == value + false if result + result + + getItemByName: (name) -> + result = null + $.each @items, (i, item) -> + result = item if item.name == name + false if result + result + +module.exports = Group diff --git a/src/models/item.coffee b/src/models/item.coffee new file mode 100644 index 0000000..65e4d77 --- /dev/null +++ b/src/models/item.coffee @@ -0,0 +1,26 @@ +class Item extends SimpleModule + + constructor: (opts) -> + @name = opts.name + @value = opts.value.toString() + @data = {} + if $.isPlainObject opts.data + $.each opts.data, (key, value) => + key = key.replace(/^data-/, '').split('-') + # camel case format + $.each key, (i, part) -> + # capitalize + key[i] = part.charAt(0).toUpperCase() + part.slice(1) if i > 0 + @data[key.join()] = value + null + + match: (value) -> + try + re = new RegExp("(^|\\s)" + value, "i") + catch e + re = new RegExp("", "i") + + filterKey = @data.key || @name + re.test filterKey + +module.exports = Item diff --git a/src/multiple-input.coffee b/src/multiple-input.coffee new file mode 100644 index 0000000..a75d03e --- /dev/null +++ b/src/multiple-input.coffee @@ -0,0 +1,103 @@ +Item = require './models/item.coffee' +Input = require './input.coffee' + +class MultipleInput extends Input + + opts: + el: null + placeholder: '' + selected: false + + @_itemTpl: ''' +
+ + +
+ ''' + + _render: -> + @el.append ''' + + ''' + .addClass 'multiple' + + @textField = @el.find 'textarea' + @textField.attr 'placeholder', @opts.placeholder + if $.isArray @opts.selected + $.each @opts.selected, (i, item) => + @addSelected item + @el + + _bind: -> + super() + + @el.on 'mousedown', '.selected-item', (e) => + $item = $ e.currentTarget + @triggerHandler 'itemClick', [$item, $item.data('item')] + false + + _onBackspacePress: (e) -> + unless @getValue() + e.preventDefault() + @el.find('.selected-item:last').mousedown() + + _onInputChange: -> + @triggerHandler 'change', [@getValue()] + + _autoresize: -> + # do nothing + + _setPlaceholder: (show = true) -> + if show + @textField.attr 'placeholder', @opts.placeholder + else + @textField.removeAttr 'placeholder' + + setSelected: (item = false) -> + if item + @addSelected selected + else + @clear() + + addSelected: (item) -> + unless item instanceof Item + item = @dataProvider.getItem item + return unless item + + @selected ||= [] + @selected.push item + + $item = $ MultipleInput._itemTpl + .attr 'data-value', item.value + .data 'item', item + $item.find('.item-label').text(item.name) + $item.insertBefore @textField + @setValue '' + @_setPlaceholder false + item + + removeSelected: (item) -> + unless item instanceof Item + item = @dataProvider.getItem item + return unless item + + if @selected + $.each @selected, (i, _item) => + if _item.value == item.value + @selected.splice(i, 1) + false + if @selected.length == 0 + @selected = false + @_setPlaceholder true + + @el.find(".selected-item[data-value='#{item.value}']").remove() + @setValue '' + item + + clear: -> + @setValue '' + @selected = false + @_setPlaceholder true + @el.find('.selected-item').remove() + +module.exports = MultipleInput diff --git a/src/popover.coffee b/src/popover.coffee new file mode 100644 index 0000000..d1c0823 --- /dev/null +++ b/src/popover.coffee @@ -0,0 +1,144 @@ +DataProvider = require './models/data-provider.coffee' +Group = require './models/group.coffee' +Item = require './models/item.coffee' + +class Popover extends SimpleModule + + @_itemTpl: """ +
+ + +
+ """ + + opts: + el: null + groups: [] + onItemRender: null + localse: {} + + _init: -> + @el = $ @opts.el + @groups = @opts.groups + + @_render() + @_bind() + + _renderItem: (item) -> + $itemEl = $(Popover._itemTpl).data('item', item) + $itemEl.find('.label').text(item.name) + $itemEl.find('.hint').text(item.data.hint) if item.data.hint + $itemEl.attr 'data-value', item.value + @el.append $itemEl + + if $.isFunction @opts.onItemRender + @opts.onItemRender.call(@, $itemEl, item) + + $itemEl + + _render: -> + @el.empty() + noGroup = @groups.length == 1 && @groups[0].name == Group.defaultName + + if @groups.length == 0 || (noGroup && @groups[0].items.length == 0) + $('
') + .text @opts.locales.noResults + .appendTo @el + else if noGroup + $.each @groups[0].items, (i, item) => + @_renderItem item + else + $.each @groups, (i, group) => + $ '
' + .text(group.name) + .appendTo @el + $.each group.items, (i, item) => + @_renderItem item + + @el + + _bind: -> + @el.on 'mousedown', '.select-item', (e) => + $item = $ e.currentTarget + @triggerHandler 'itemClick', [$item, $item.data('item')] + false + + _scrollToHighlighted: -> + $item = @el.find('.select-item.highlighted') + if $item.length > 0 + @el.scrollTop $item.position().top + + setGroups: (groups) -> + @setHighlighted false + @setLoading false + @setActive false + @groups = groups + @_render() + groups + + setHighlighted: (highlighted = false) -> + if highlighted + unless highlighted instanceof Item + highlighted = @dataProvider.getItem highlighted + + @el.find(".select-item[data-value='#{highlighted.value}']") + .addClass 'highlighted' + .siblings() + .removeClass 'highlighted' + else + @el.find('.select-item.highlighted').removeClass 'highlighted' + + @highlighted = highlighted + highlighted + + highlightNextItem: -> + if @highlighted + $item = @el.find(".select-item[data-value='#{@highlighted.value}']") + .nextAll('.select-item:first') + else + $item = @el.find('.select-item:first') + + if $item.length > 0 + @setHighlighted $item.data('item') + + highlightPrevItem: -> + if @highlighted + $item = @el.find(".select-item[data-value='#{@highlighted.value}']") + .prevAll('.select-item:first') + else + $item = @el.find('.select-item:first') + + if $item.length > 0 + @setHighlighted $item.data('item') + + setLoading: (loading = true) -> + @loading = loading + + if loading + unless @el.find('.loading').length > 0 + $('
') + .text @opts.locales.loading + .appendTo @el + @el.addClass 'loading' + else + @el.removeClass 'loading' + @el.find('.loading').remove() + + @setActive loading + loading + + setActive: (active = true) -> + @active = active + @el.toggleClass 'active', active + if active + @_scrollToHighlighted() + @triggerHandler 'show' + else + @triggerHandler 'hide' + active + + setPosition: (position) -> + @el.css(position) if position + @ + +module.exports = Popover diff --git a/src/select.coffee b/src/select.coffee deleted file mode 100644 index c1fccc8..0000000 --- a/src/select.coffee +++ /dev/null @@ -1,423 +0,0 @@ -class Select extends SimpleModule - - opts: - el: null - cls: "" - onItemRender: $.noop - placeholder: "" - allowInput: false - multiline: true - - @i18n: - "zh-CN": - all_options: "所有选项" - clear_selection: "清除选择" - loading: "加载中..." - "en": - all_options: "All options" - clear_selection: "Clear Selection" - loading: "Loading..." - - @_tpl: - textarea: """ - - """ - - input: """ - - """ - - item: """ -
- - -
- """ - - group: """ -
-
- """ - - _init: -> - unless @opts.el - throw "simple select: option el is required" - return - - @opts.el.data("select")?.destroy() - @_render() - @_bind() - @autoresizeInput() - - - _render: -> - Select._tpl.select = """ -
- - - - - - -
-
#{@_t('loading')}
-
-
- """ - - @el = $(@opts.el).hide() - @el.data("select", @) - @select = $(Select._tpl.select) - .data("select", @) - .addClass(@opts.cls) - .insertBefore @el - if @opts.multiline - @select.addClass('multiline') - inputTpl = Select._tpl.textarea - else - inputTpl = Select._tpl.input - - @input = $(inputTpl) - .attr("placeholder", @opts.placeholder || @el.data('placeholder') || "") - .prependTo @select - @list = @select.find ".select-list" - - @requireSelect = true - - @_setGroupsItems() - @generateList() - @select.toggleClass 'require-select', @requireSelect - - _setGroupsItems: () -> - @groups = false - - @items = @el.find("option").map (i, option) => - $option = $(option) - $group = $option.parent('optgroup') - item = @_item $option - if $group.length isnt 0 - @groups = {} if @groups is false - groupLabel = $group.attr('label') - @groups[groupLabel] = [] unless $.isArray(@groups[groupLabel]) - @groups[groupLabel].push(item) - item - .get() - - - _item: ($option) -> - value = $option.attr 'value' - label = $option.text().trim() - - unless value - @requireSelect = false - return - - $.extend({ - label: label, - _value: value - }, $option.data()) - - - _expand: (expand) -> - if expand is false - @input.removeClass "expanded" - @list.hide() - else - @input.addClass "expanded" - @list.show() if @items.length > 0 - @list.css("top", @input.outerHeight() + 4) - @_scrollToSelected() if @_selectedIndex > -1 - - - _scrollToSelected: -> - return if @_selectedIndex < 0 - $selectedEl = @list.find(".select-item").eq @_selectedIndex - @list.scrollTop $selectedEl.position().top - - - _bind: -> - @select.find(".link-clear").on "mousedown", (e) => - @clearSelection() - return false - - @select.find(".link-expand").on "mousedown", (e) => - @_expand !@input.hasClass("expanded") - @input.focus() unless @_focused - return false - - @list.on "mousedown", (e) => - if window.navigator.userAgent.toLowerCase().indexOf('msie') > -1 - @_scrollMousedown = true - setTimeout => - @input.focus() - , 0 - return false - .on "mousedown", ".select-item", (e) => - index = @list.find(".select-item").index $(e.currentTarget) - @selectItem index - @input.blur() - return false - - @input.on "keydown.select", (e) => - @_keydown(e) - .on "keyup.select", (e) => - @_keyup(e) - .on "blur.select", (e) => - @_blur(e) - .on "focus.select", (e) => - @_focus(e) - - @select.on "mousedown", (e) => - @input.focus() - - - _keydown: (e) -> - return unless @items and @items.length - return if @triggerHandler(e) is false - - if e.which is 40 or e.which is 38 # up and down - unless @input.hasClass "expanded" - @_expand() - $itemEls = @list.find(".select-item").show() - $itemEls.first().addClass("selected") if @_selectedIndex < 0 - - else - $selectedEl = @list.find ".select-item.selected" - unless $selectedEl.length - @list.find(".select-item:first").addClass "selected" - return - - if e.which is 38 - $prevEl = $selectedEl.prevAll(".select-item:visible:first") - if $prevEl.length - $selectedEl.removeClass "selected" - $prevEl.addClass "selected" - else if e.which is 40 - $nextEl = $selectedEl.nextAll(".select-item:visible:first") - if $nextEl.length - $selectedEl.removeClass "selected" - $nextEl.addClass "selected" - - else if e.which is 13 # enter - e.preventDefault() - if @input.hasClass "expanded" - $selectedEl = @list.find ".select-item.selected" - if $selectedEl.length - index = @list.find(".select-item").index $selectedEl - @selectItem index - return false - else if @_selectedIndex > -1 - @selectItem @_selectedIndex - - if @opts.allowInput - @input.blur() - return false - - @clearSelection() - return false - - else if e.which is 27 # esc - e.preventDefault() - @input.blur() - - else if e.which is 8 # backspace - @clearSelection() if @select.hasClass "selected" - @_expand() unless @input.hasClass "expanded" - @autoresizeInput() - - - _keyup: (e) -> - return false if $.inArray(e.which, [13, 40, 38, 9, 27]) > -1 - @autoresizeInput() - - if @_keydownTimer - clearTimeout(@_keydownTimer) - @_keydownTimer = null - - $itemEls = @list.find ".select-item" - @_keydownTimer = setTimeout => - @_expand() unless @input.hasClass "expanded" - if @select.hasClass "selected" - @select.removeClass "selected" - - value = $.trim @input.val() - unless value - @list.show() if @items.length > 0 - $itemEls.show().removeClass "selected" - return - - try - re = new RegExp("(|\\s)" + value, "i") - catch e - re = new RegExp("", "i") - - results = $itemEls.hide().removeClass("selected").filter -> - return re.test $(@).data("key") - - if results.length - @list.show() if @items.length > 0 - results.show().first().addClass "selected" - else - @list.hide() - , 0 - - - _blur: (e) -> - if @_scrollMousedown - @_scrollMousedown = false - return false - - @input.removeClass "expanded error" - @list.hide() - .find(".select-item") - .show() - .removeClass "selected" - - value = $.trim @input.val() - if !@select.hasClass("selected") - if @opts.allowInput - @el.val '' - @trigger 'select', [{label: value, _value: -1}] - else if value - matchIdx = -1 - $.each @items, (i, item) -> - if (item.label is value) - matchIdx = i - return false - if matchIdx >= 0 - @selectItem matchIdx - else - @selectItem Math.max(@_selectedIndex || -1, 0) - else if @requireSelect and @items.length > 0 - @selectItem 0 - else - @el.val '' - - @_focused = false - - _focus: (e) -> - @_expand() - if @_selectedIndex > -1 - @list.find(".select-item").eq(@_selectedIndex).addClass("selected") - setTimeout => - @input.select() - , 0 - @_focused = true - - - generateList: -> - @list.empty() - if @groups - $.each @groups, (groupLabel, items) => - $groupEl = $(Select._tpl.group) - $groupEl.text(groupLabel) - @list.append($groupEl) - for item in items - $itemEl = @_itemEl(item) - @list.append($itemEl) - @opts.onItemRender.call(@, $itemEl, item) if $.isFunction @opts.onItemRender - else - for item in @items - $itemEl = @_itemEl(item) - @list.append $itemEl - @opts.onItemRender.call(@, $itemEl, item) if $.isFunction @opts.onItemRender - - for it, idx in @items - if it._value is @el.val() - @selectItem idx - break - - _itemEl: (item) -> - $itemEl = $(Select._tpl.item).data(item) - $itemEl.find(".label span").text(item.label) - $itemEl.find(".hint").text(item.hint) - $itemEl - - setItems: (items, requireSelect = true) -> - @items = [] - @clearSelection() - @list.empty() - @el.empty() - - @requireSelect = requireSelect - @select.toggleClass 'require-select', @requireSelect - @el.prepend('') unless @requireSelect - - if $.isArray(items) && items.length > 0 - for item in items - @el.append("") - @_setGroupsItems() - @generateList() - - if $.type(items) is 'object' - $.each items, (groupLabel, items) => - $group = $("") - for item in items - $group.append("") - @el.append($group) - @_setGroupsItems() - @generateList() - - selectItem: (index) -> - return unless @items - - if $.isNumeric index - return if index < 0 - - item = @items[index] - @select.addClass "selected" - @input.val item.label - .removeClass "expanded error" - @list.hide() - .find ".select-item" - .eq index - .addClass "selected" - - @_selectedIndex = index - @el.val item._value - @trigger "select", [item] - @autoresizeInput() - - return @items[@_selectedIndex] if @_selectedIndex > -1 - - - clearSelection: -> - @input.val("").removeClass("expanded error") - @select.removeClass("selected") - @list.hide() - .find(".select-item") - .show() - .removeClass "selected" - - @_selectedIndex = -1 - @el.val '' - @trigger "clear" - @autoresizeInput() - - - autoresizeInput: () -> - return unless @opts.multiline - setTimeout () => - @input.css("height", 0) - @input.css("height", parseInt(@input.get(0).scrollHeight) + parseInt(@input.css("border-top-width")) + parseInt(@input.css("border-bottom-width"))) - , 0 - - - disable: -> - @input.prop "disabled", true - @select.find(".link-expand, .link-clear").hide() - - - enable: -> - @input.prop "disabled", false - @select.find(".link-expand, .link-clear").attr("style", "") - - - destroy: -> - @select.remove() - @el.removeData 'select' - @el.show() - - -select = (opts) -> - new Select(opts) diff --git a/src/simple-select.coffee b/src/simple-select.coffee new file mode 100644 index 0000000..71c8a3e --- /dev/null +++ b/src/simple-select.coffee @@ -0,0 +1,254 @@ +DataProvider = require './models/data-provider.coffee' +Group = require './models/group.coffee' +Item = require './models/item.coffee' +HtmlSelect = require './html-select.coffee' +Input = require './input.coffee' +MultipleInput = require './multiple-input.coffee' +Popover = require './popover.coffee' + +class SimpleSelect extends SimpleModule + + opts: + el: null + remote: false + cls: "" + onItemRender: null + placeholder: "" + allowInput: false + noWrap: false + locales: null + + @locales: + loading: 'Loading...' + noResults: 'No results found' + + @_tpl: """ +
+
+
+
+ """ + + _init: -> + @el = $(@opts.el) + unless @el.length > 0 + throw new Error "simple select: option el is required" + return + + @el.data("simpleSelect")?.destroy() + @locales = $.extend {}, SimpleSelect.locales, @opts.locales + @multiple = @el.is '[multiple]' + + @_render() + + @dataProvider = new DataProvider + remote: @opts.remote + selectEl: @el + + @htmlSelect = new HtmlSelect + el: @el + + placeholder = if @opts.placeholder + @opts.placeholder + else if ($blankOption = @htmlSelect.getBlankOption()) + $blankOption.text() + else + '' + + if @multiple + @input = new MultipleInput + el: @wrapper.find('.input') + placeholder: placeholder + selected: @htmlSelect.getValue() + groups = @dataProvider.excludeItems @input.selected + else + @input = new Input + el: @wrapper.find('.input') + placeholder: placeholder + noWrap: @opts.noWrap + selected: @htmlSelect.getValue() + groups = @dataProvider.groups + + @popover = new Popover + el: @wrapper.find('.popover') + groups: groups + onItemRender: @opts.onItemRender + locales: @locales + + @_bind() + @disable() if @el.prop('disabled') + + _render: -> + @el.hide() + .data("simpleSelect", @) + @wrapper = $(SimpleSelect._tpl) + .data("simpleSelect", @) + .addClass(@opts.cls) + .insertBefore @el + + _bind: -> + # data provider events + @dataProvider.on 'filter', (e, groups, value) => + if @multiple && @input.selected + groups = @dataProvider.excludeItems @input.selected, groups + @popover.setGroups groups + @popover.setActive !!(!@dataProvider.remote || value) + + @dataProvider.on 'beforeFetch', (e) => + @popover.setLoading true + .on 'fetch', (e) => + @popover.setLoading false + + # popover events + @popover.on 'itemClick', (e, $item, item) => + @selectItem item + + @popover.on 'show', (e) => + @_setPopoverPosition() + if !@multiple && @input.selected + @popover.setHighlighted @input.selected + else + @popover.highlightNextItem() + + # input events + @input.on 'itemClick', (e, $item, item) => + @unselectItem item + + @input.on 'clearClick', (e) => + @clear() + + @input.on 'expandClick', (e) => + @popover.setActive true + + @input.on 'arrowPress', (e, direction) => + return unless @popover.active + if direction == 'up' + @popover.highlightPrevItem() + else + @popover.highlightNextItem() + + @input.on 'enterPress', (e) => + if @popover.active + if @popover.highlighted + @selectItem @popover.highlighted + else if !@multiple + @_setUserInput() + @popover.setActive false + else + @el.closest('form').submit() + + @input.on 'change', (e, value) => + @_syncValue() unless @multiple + @dataProvider.filter value + @_setPopoverPosition() + + @input.on 'focus', (e) => + unless @dataProvider.remote && (!@input.getValue() || @input.selected) + @popover.setActive true + + @input.on 'blur', (e) => + if !@multiple && !@input.selected + value = @input.getValue() + if item = @dataProvider.getItemByName(value) + @selectItem item + else + @_setUserInput value + @popover.setActive false + + _setUserInput: (value = @input.getValue()) -> + if @opts.allowInput && !@multiple + @wrapper.siblings(@opts.allowInput).val value + + _setPopoverPosition: -> + @popover.setPosition + top: @input.el.outerHeight() + 2 + left: 0 + + _syncValue: -> + if @multiple + items = @input.selected || [] + else + items = if @input.selected then [@input.selected] else [] + + currentValue = @htmlSelect.getValue() || [] + currentValue = [currentValue] unless $.isArray currentValue + + if @dataProvider.remote + group = new Group + items: items + @htmlSelect.setGroups [group] + + values = items.map (item) -> item.value + if items.length > 0 + @htmlSelect.setValue values + else + @htmlSelect.setValue '' + + unless currentValue.join(',') == values.join(',') + @triggerHandler 'change', [@input.selected] + items + + selectItem: (item) -> + unless item instanceof Item + item = @dataProvider.getItem item + + return unless item + + if @multiple + @input.addSelected item + else + @input.setSelected item + + @popover.setActive false + if @opts.remote + @popover.setGroups [] + else if !@multiple + @popover.setGroups @dataProvider.getGroups() + @popover.setHighlighted item + + @_setUserInput '' + @_syncValue() + item + + unselectItem: (item) -> + return unless @multiple + unless item instanceof Item + item = @dataProvider.getItem item + return unless item + + @input.removeSelected item + @_syncValue() + item + + clear: -> + @input.clear() + @popover.setActive false + @_setUserInput '' + @ + + focus: -> + @input.focus() + + blur: -> + @input.blur() + + disable: -> + @input.setDisabled true + @htmlSelect.setDisabled true + @wrapper.addClass 'disabled' + @ + + enable: -> + @input.setDisabled false + @htmlSelect.setDisabled false + @wrapper.removeClass 'disabled' + @ + + destroy: -> + @el.removeData 'simpleSelect' + .insertAfter @wrapper + .show() + @wrapper.remove() + @ + +module.exports = SimpleSelect diff --git a/styles/select.css b/styles/select.css deleted file mode 100644 index e3a2b2e..0000000 --- a/styles/select.css +++ /dev/null @@ -1,121 +0,0 @@ -.simple-select { - position: relative; - width: 155px; -} -.simple-select .select-result { - width: 100%; - height: 24px; - box-sizing: border-box; - padding: 4px; - padding-right: 24px; -} -.simple-select .link-clear, .simple-select .link-expand { - display: block; - width: 24px; - height: 24px; - line-height: 24px; - text-align: center; - font-size: 13px; - color: #999999; - position: absolute; - top: 1px; - right: 1px; - cursor: pointer; -} -.simple-select .link-clear:hover, .simple-select .link-expand:hover { - opacity: 0.8; -} -.simple-select .link-clear > i span, .simple-select .link-expand > i span { - font-style: normal; -} -.simple-select .link-expand { - font-size: 14px; -} -.simple-select .link-clear { - display: none; -} -.simple-select.selected .link-expand { - display: none; -} -.simple-select.selected .link-clear { - display: block; -} -.simple-select.require-select .link-expand { - display: block !important; -} -.simple-select.require-select .link-clear { - display: none !important; -} -.simple-select .select-list { - display: none; - width: 100%; - max-height: 199px; - overflow-x: hidden; - overflow-y: auto; - background: #ffffff; - border: 1px solid #cccccc; - position: absolute; - top: 30px; - z-index: 10; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} -.simple-select .select-list .loading { - padding: 8px; - font-size: 12px; - line-height: 1; - color: #bbbbbb; -} -.simple-select .select-list .select-item, .simple-select .select-list .select-group { - display: block; - font-size: 12px; - line-height: 24px; - padding: 0 10px; - color: #666666; - border-bottom: 1px solid #dfdfdf; - cursor: pointer; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} -.simple-select .select-list .select-item:first-child, .simple-select .select-list .select-group:first-child { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} -.simple-select .select-list .select-item:last-child, .simple-select .select-list .select-group:last-child { - border-bottom: none; - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} -.simple-select .select-list .select-item:hover, .simple-select .select-list .select-item.selected, .simple-select .select-list .select-group:hover, .simple-select .select-list .select-group.selected { - background: #efefef; -} -.simple-select .select-list .select-item .label, .simple-select .select-list .select-group .label { - color: #666666; - text-decoration: none; -} -.simple-select .select-list .select-item .hint, .simple-select .select-list .select-group .hint { - float: right; - color: #aaa; -} -.simple-select .select-list .select-group { - background: #f6f6f6; - color: #888888; - cursor: default; -} -.simple-select .select-list .select-group:hover { - background: #f6f6f6; -} -.simple-select.multiline .select-result { - resize: none; - overflow: hidden; -} -.simple-select.multiline .select-list .select-item { - white-space: normal; -} diff --git a/styles/select.scss b/styles/select.scss deleted file mode 100644 index cd8cc57..0000000 --- a/styles/select.scss +++ /dev/null @@ -1,150 +0,0 @@ -.simple-select { - position: relative; - width: 155px; - - .select-result { - width: 100%; - height: 24px; - box-sizing: border-box; - padding: 4px; - padding-right: 24px; - } - - .link-clear, .link-expand { - display: block; - width: 24px; - height: 24px; - line-height: 24px; - text-align: center; - font-size: 13px; - color: #999999; - position: absolute; - top: 1px; - right: 1px; - cursor: pointer; - - &:hover { - opacity: 0.8; - } - - > i span { - font-style: normal; - } - } - - .link-expand { - font-size: 14px; - } - - .link-clear { - display: none; - } - - &.selected { - .link-expand { - display: none; - } - - .link-clear { - display: block; - } - - } - - &.require-select { - .link-expand { - display: block!important; - } - - .link-clear { - display: none!important; - } - } - - .select-list { - display: none; - width: 100%; - max-height: 199px; - overflow-x: hidden; - overflow-y: auto; - background: #ffffff; - border: 1px solid #cccccc; - position: absolute; - top: 30px; - z-index: 10; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - - .loading { - padding: 8px; - font-size: 12px; - line-height: 1; - color: #bbbbbb; - } - - .select-item, .select-group { - display: block; - font-size: 12px; - line-height: 24px; - padding: 0 10px; - color: #666666; - border-bottom: 1px solid #dfdfdf; - cursor: pointer; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - &:first-child { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; - } - - &:last-child { - border-bottom: none; - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; - } - - &:hover, &.selected { - background: #efefef; - } - - .label { - color: #666666; - text-decoration: none; - } - - .hint { - float: right; - color: #aaa; - } - } - - .select-group { - background: #f6f6f6; - color: #888888; - cursor: default; - &:hover { - background: #f6f6f6; - } - } - } - - &.multiline { - .select-result { - resize: none; - overflow: hidden; - } - .select-list { - .select-item { - white-space: normal; - } - } - } -} diff --git a/styles/simple-select.css b/styles/simple-select.css new file mode 100644 index 0000000..93948c2 --- /dev/null +++ b/styles/simple-select.css @@ -0,0 +1,142 @@ +/** + * simple-select v2.1.0 + * http://mycolorway.github.io/simple-select + * + * Copyright Mycolorway Design + * Released under the MIT license + * http://mycolorway.github.io/simple-select/license.html + * + * Date: 2016-07-27 + */ +.simple-select { + position: relative; } + .simple-select .input .text-field { + display: block; + width: 100%; + height: 24px; + box-sizing: border-box; + padding: 4px; + padding-right: 24px; + resize: none; + overflow: hidden; + outline: none; } + .simple-select .input .link-clear, + .simple-select .input .link-expand { + display: block; + width: 24px; + height: auto; + line-height: 24px; + text-align: center; + font-size: 14px; + color: #999999; + text-decoration: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; } + .simple-select .input .link-clear:hover, + .simple-select .input .link-expand:hover { + opacity: 0.8; } + .simple-select .input .link-clear i, + .simple-select .input .link-expand i { + font-style: normal; } + .simple-select .input .link-clear { + display: none; + font-size: 12px; } + .simple-select .input.selected .link-expand { + display: none; } + .simple-select .input.selected .link-clear { + display: block; } + .simple-select .input.multiple { + display: flex; + flex-wrap: wrap; + padding: 4px 0 0 4px; + cursor: text; + border: 1px solid #dfdfdf; } + .simple-select .input.multiple .text-field { + width: 40%; + height: 16px; + line-height: 16px; + flex: 1 0 auto; + border: none; + padding: 0; + margin: 0 0 4px 0; } + .simple-select .input.multiple .selected-item { + background: #dfdfdf; + display: block; + margin: 0px 5px 4px 0; + padding: 2px 5px; + font-size: 12px; + line-height: 12px; + border-radius: 2px; + color: #333; + cursor: pointer; } + .simple-select .input.multiple .selected-item:hover { + background: #efefef; } + .simple-select .input.multiple .selected-item .icon-remove { + font-size: 10px; + font-style: normal; + color: #777; + margin: 0 0 0 2px; } + .simple-select .input.multiple .selected-item + .text-field::-webkit-input-placeholder, + .simple-select .input.multiple .selected-item + .text-field::-moz-placeholder, + .simple-select .input.multiple .selected-item + .text-field:-ms-input-placeholder { + color: transparent; } + .simple-select .popover { + display: none; + width: 100%; + max-height: 200px; + overflow-x: hidden; + overflow-y: auto; + background: #ffffff; + border: 1px solid #cccccc; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + z-index: 1; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2); } + .simple-select .popover.active { + display: block; } + .simple-select .popover .loading, + .simple-select .popover .no-results { + padding: 8px; + font-size: 12px; + line-height: 1; + color: #999999; } + .simple-select .popover .select-item { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + font-size: 12px; + line-height: 20px; + padding: 2px 10px; + color: #666666; + border-bottom: 1px solid #dfdfdf; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } + .simple-select .popover .select-item:hover { + background: #f3f3f3; } + .simple-select .popover .select-item.highlighted { + background: #efefef; } + .simple-select .popover .select-item .label { + color: #666666; } + .simple-select .popover .select-item .hint { + color: #aaa; } + .simple-select .popover .select-group { + font-size: 12px; + line-height: 24px; + padding: 0 10px; + border-bottom: 1px solid #dfdfdf; + background: #f6f6f6; + color: #000000; + font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } + .simple-select .popover.loading .no-results, + .simple-select .popover.loading .select-item, + .simple-select .popover.loading .select-group { + display: none; } diff --git a/styles/simple-select.scss b/styles/simple-select.scss new file mode 100644 index 0000000..821446d --- /dev/null +++ b/styles/simple-select.scss @@ -0,0 +1,185 @@ +$height: 24px; + +.simple-select { + position: relative; + + .input { + .text-field { + display: block; + width: 100%; + height: $height; + box-sizing: border-box; + padding: 4px; + padding-right: $height; + resize: none; + overflow: hidden; + outline: none; + } + + .link-clear, + .link-expand { + display: block; + width: $height; + height: auto; + line-height: $height; + text-align: center; + font-size: 14px; + color: #999999; + text-decoration: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + + &:hover { + opacity: 0.8; + } + + i { + font-style: normal; + } + } + + .link-clear { + display: none; + font-size: 12px; + } + + &.selected { + .link-expand { + display: none; + } + + .link-clear { + display: block; + } + } + } + + .input.multiple { + display: flex; + flex-wrap: wrap; + padding: 4px 0 0 4px; + cursor: text; + border: 1px solid #dfdfdf; + + .text-field { + width: 40%; + height: 16px; + line-height: 16px; + flex: 1 0 auto; + border: none; + padding: 0; + margin: 0 0 4px 0; + } + + .selected-item { + background: #dfdfdf; + display: block; + margin: 0px 5px 4px 0; + padding: 2px 5px; + font-size: 12px; + line-height: 12px; + border-radius: 2px; + color: #333; + cursor: pointer; + + &:hover { + background: #efefef; + } + + .icon-remove { + font-size: 10px; + font-style: normal; + color: #777; + margin: 0 0 0 2px; + } + } + + .selected-item + .text-field::-webkit-input-placeholder, + .selected-item + .text-field::-moz-placeholder, + .selected-item + .text-field:-ms-input-placeholder { + color: transparent; + } + } + + .popover { + display: none; + width: 100%; + max-height: 200px; + overflow-x: hidden; + overflow-y: auto; + background: #ffffff; + border: 1px solid #cccccc; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + z-index: 1; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.2); + + &.active { + display: block; + } + + .loading, + .no-results { + padding: 8px; + font-size: 12px; + line-height: 1; + color: #999999; + } + + .select-item { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + font-size: 12px; + line-height: 20px; + padding: 2px 10px; + color: #666666; + border-bottom: 1px solid #dfdfdf; + cursor: pointer; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &:hover { + background: #f3f3f3; + } + + &.highlighted { + background: #efefef; + } + + .label { + color: #666666; + } + + .hint { + color: #aaa; + } + } + + .select-group { + font-size: 12px; + line-height: 24px; + padding: 0 10px; + border-bottom: 1px solid #dfdfdf; + background: #f6f6f6; + color: #000000; + font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + &.loading { + .no-results, + .select-item, + .select-group { + display: none; + } + } + } +} diff --git a/test/models/data-provider.coffee b/test/models/data-provider.coffee new file mode 100644 index 0000000..1180b64 --- /dev/null +++ b/test/models/data-provider.coffee @@ -0,0 +1,116 @@ +DataProvider = require '../../src/models/data-provider' +Group = require '../../src/models/group' +Item = require '../../src/models/item' +expect = chai.expect + +describe 'Data Provider', -> + + dataProvider = null + + beforeEach -> + dataProvider = new DataProvider + groups: { + 'Cat Animals': [ + ['Cat', '1'] + ['Tiger', '2'] + ], + 'Dog Animals': [ + ['Dog', '3'] + ['Wolf', '4'] + ] + } + + afterEach -> + dataProvider = null + + it 'can set groups from json data', -> + groups1 = dataProvider.getGroups() + expect(groups1.length).to.be.equal 2 + expect(groups1[0].name).to.be.equal 'Cat Animals' + expect(groups1[0].items.length).to.be.equal 2 + + dataProvider.setGroupsFromJson [ + ['Cat 1', '3', {key: 'cat1'}] + ['Cat 2', '4', {key: 'cat2'}] + ] + groups2 = dataProvider.getGroups() + expect(groups2.length).to.be.equal 1 + expect(groups2[0].name).to.be.equal Group.defaultName + expect(groups2[0].items.length).to.be.equal 2 + + it 'can set groups from select element', -> + $selectEl = $ ''' + + ''' + dataProvider = new DataProvider + selectEl: $selectEl + + groups = dataProvider.getGroups() + expect(groups.length).to.be.equal 2 + expect(groups[0].name).to.be.equal 'Dog Animals' + expect(groups[0].items.length).to.be.equal 2 + + it 'can get item by value', -> + item = dataProvider.getItem '3' + expect(item instanceof Item).to.be.true + expect(item.name).to.be.equal 'Dog' + expect(item.value).to.be.equal '3' + + it 'can get item by name', -> + item = dataProvider.getItemByName 'Wolf' + expect(item instanceof Item).to.be.true + expect(item.name).to.be.equal 'Wolf' + expect(item.value).to.be.equal '4' + + it 'can filter items without remote option', (done) -> + validate = (groups, value) -> + expect(value).to.be.equal 'ti' + expect(groups.length).to.be.equal 1 + expect(groups[0].items.length).to.be.equal 1 + expect(groups[0].items[0].name).to.be.equal 'Tiger' + expect(groups[0].items[0].value).to.be.equal '2' + + dataProvider.on 'filter', (e, groups, value) -> + validate groups, value + done() + dataProvider.filter 'ti', (groups, value) -> + validate groups, value + + it 'can filter items with remote option', (done) -> + server = sinon.fakeServer.create + respondImmediately: true + server.respondWith [ + 200 + {"Content-Type": "application/json"} + JSON.stringify({'Cat Animals': [['Tiger', '2']]}) + ] + + validate = (groups, value) -> + expect(value).to.be.equal 'ti' + expect(groups.length).to.be.equal 1 + expect(groups[0].items.length).to.be.equal 1 + expect(groups[0].items[0].name).to.be.equal 'Tiger' + expect(groups[0].items[0].value).to.be.equal '2' + + dataProvider = new DataProvider + remote: + url: 'api/url' + searchKey: 'animal_name' + + dataProvider.on 'fetch', (e, groups) -> + expect(groups.length).to.be.equal 1 + dataProvider.on 'filter', (e, groups, value) -> + validate groups, value + server.restore() + done() + dataProvider.filter 'ti', (groups, value) -> + validate groups, value diff --git a/test/models/group.coffee b/test/models/group.coffee new file mode 100644 index 0000000..c242da1 --- /dev/null +++ b/test/models/group.coffee @@ -0,0 +1,57 @@ +Group = require '../../src/models/group' +Item = require '../../src/models/item' +expect = chai.expect + +describe 'Group Model', -> + + group = null + + beforeEach -> + group = new Group + name: 'Test Group' + items: [ + ['Dog 1', '1', {key: 'dog1'}] + ['Dog 2', '2', {key: 'dog2'}] + ['Cat 1', '3', {key: 'cat1'}] + ['Cat 2', '4', {key: 'cat2'}] + ] + + afterEach -> + group = null + + it 'accepts name and items as options', -> + expect(group.name).to.be.equal 'Test Group' + expect(group.items.length).to.be.equal 4 + expect(group.items[0] instanceof Item).to.be.true + + it 'can fitlers items by search key', -> + newGroup = group.filterItems 'dog' + expect(newGroup).to.be.not.equal group + expect(newGroup.name).to.be.equal group.name + expect(newGroup.items.length).to.be.equal 2 + expect(newGroup.items[0].name).to.be.equal 'Dog 1' + expect(newGroup.items[1].name).to.be.equal 'Dog 2' + + it 'can excludes items by values', -> + items = [ + new Item name: 'xxx', value: '1' + new Item name: 'xxx', value: '3' + ] + newGroup = group.excludeItems items + expect(newGroup).to.be.not.equal group + expect(newGroup.name).to.be.equal group.name + expect(newGroup.items.length).to.be.equal 2 + expect(newGroup.items[0].name).to.be.equal 'Dog 2' + expect(newGroup.items[1].name).to.be.equal 'Cat 2' + + it 'can get item by value', -> + item = group.getItem '4' + expect(item instanceof Item).to.be.true + expect(item.name).to.be.equal 'Cat 2' + expect(item.value).to.be.equal '4' + + it 'can get item by Name', -> + item = group.getItemByName 'Cat 1' + expect(item instanceof Item).to.be.true + expect(item.name).to.be.equal 'Cat 1' + expect(item.value).to.be.equal '3' diff --git a/test/models/item.coffee b/test/models/item.coffee new file mode 100644 index 0000000..024a63a --- /dev/null +++ b/test/models/item.coffee @@ -0,0 +1,31 @@ +Item = require '../../src/models/item' +expect = chai.expect + +describe 'Item Model', -> + + item = null + + beforeEach -> + item = new Item + name: 'Tiger' + value: '1' + data: + 'data-key': 'laohu lh' + animal: true + human: false + + afterEach -> + item = null + + it 'accepts name/value/data as options', -> + expect(item.name).to.be.equal 'Tiger' + expect(item.value).to.be.equal '1' + expect(Object.keys(item.data).length).to.be.equal 3 + expect(item.data.key).to.be.equal 'laohu lh' + expect(item.data.animal).to.be.true + expect(item.data.human).to.be.false + + it 'can match value', -> + expect(item.match 'tiger').to.be.false + expect(item.match 'laohu').to.be.true + expect(item.match 'lh').to.be.true diff --git a/test/simple-select.coffee b/test/simple-select.coffee new file mode 100644 index 0000000..03a13bb --- /dev/null +++ b/test/simple-select.coffee @@ -0,0 +1,26 @@ +SimpleSelect = require '../src/simple-select' +expect = chai.expect + +describe 'Simple Select', -> + selectEl = null + + beforeEach -> + selectEl = $(""" + + """) + + afterEach -> + $(".simple-select").each () -> + $(@).data("simpleSelect").destroy() + $("select").remove() + + it 'should inherit from SimpleModule', -> + selectEl.appendTo("body") + select = new SimpleSelect + el: $("#select-one") + + expect(select instanceof SimpleModule).to.be.equal(true) diff --git a/umd.hbs b/umd.hbs deleted file mode 100644 index 1cf155d..0000000 --- a/umd.hbs +++ /dev/null @@ -1,23 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define({{#if amdModuleId}}'{{amdModuleId}}', {{/if}}[{{{amdDependencies.wrapped}}}], function ({{{amdDependencies.params}}}) { - return ({{#if objectToExport}}root['{{{objectToExport}}}'] = {{/if}}factory({{{amdDependencies.params}}})); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory({{{cjsDependencies.wrapped}}}); - } else { - root.simple = root.simple || {}; - {{#if globalAlias}}root.simple['{{{globalAlias}}}'] = {{else}}{{#if objectToExport}}root['{{{objectToExport}}}'] = {{/if}}{{/if}}factory({{{globalDependencies.normal}}}); - } -}(this, function ({{dependencies}}) { - -{{{code}}} -{{#if objectToExport}} -return {{{objectToExport}}}; -{{/if}} - -}));