├── app ├── .gitkeep └── components │ ├── ember-range-slider.js │ └── ember-range-slider-handle.js ├── addon ├── .gitkeep ├── components │ ├── ember-range-slider-handle.js │ └── ember-range-slider.js ├── templates │ └── components │ │ ├── ember-range-slider-handle.hbs │ │ └── ember-range-slider.hbs ├── styles │ └── ember-range-slider.css └── utils │ └── scale-strategies.js ├── tests ├── unit │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── views │ │ │ └── .gitkeep │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── custom-end-handle.js │ │ │ └── custom-start-handle.js │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── templates │ │ │ ├── components │ │ │ │ └── .gitkeep │ │ │ └── application.hbs │ │ ├── router.js │ │ ├── app.js │ │ ├── styles │ │ │ └── app.css │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ └── crossdomain.xml │ └── config │ │ └── environment.js ├── test-helper.js ├── helpers │ ├── resolver.js │ ├── start-app.js │ ├── raw-events │ │ ├── register-acceptance-test-helpers.js │ │ ├── async.js │ │ └── utils │ │ │ └── raw-events.js │ └── helpers.js ├── .jshintrc ├── index.html └── acceptance │ └── range-slider-test.js ├── .watchmanconfig ├── CHANGELOG.md ├── .bowerrc ├── config ├── environment.js └── ember-try.js ├── .npmignore ├── bower.json ├── .ember-cli ├── .gitignore ├── blueprints └── ember-range-slider │ └── index.js ├── testem.json ├── ember-cli-build.js ├── index.js ├── .jshintrc ├── .editorconfig ├── .travis.yml ├── LICENSE.md ├── package.json └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.1.0 4 | 5 | - Initial implementation 6 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/components/ember-range-slider.js: -------------------------------------------------------------------------------- 1 | export { default } from '@upsilon/ember-range-slider/components/ember-range-slider'; 2 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /app/components/ember-range-slider-handle.js: -------------------------------------------------------------------------------- 1 | export { default } from '@upsilon/ember-range-slider/components/ember-range-slider-handle'; 2 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { setResolver } from 'ember-qunit'; 3 | import { start } from 'ember-cli-qunit'; 4 | 5 | setResolver(resolver); 6 | start(); 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | tests/ 3 | tmp/ 4 | dist/ 5 | 6 | .bowerrc 7 | .editorconfig 8 | .ember-cli 9 | .travis.yml 10 | .npmignore 11 | **/.gitkeep 12 | bower.json 13 | ember-cli-build.js 14 | Brocfile.js 15 | testem.json 16 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-end-handle.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['CustomHandle', 'CustomHandle--end'], 5 | classNameBindings: ['isSliding'], 6 | attributeBindings: ['style'], 7 | isSliding: false 8 | }); 9 | -------------------------------------------------------------------------------- /addon/components/ember-range-slider-handle.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import layout from '../templates/components/ember-range-slider-handle'; 3 | 4 | export default Ember.Component.extend({ 5 | layout, 6 | attributeBindings: ['style'], 7 | classNameBindings: ['isSliding'] 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-start-handle.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | classNames: ['CustomHandle', 'CustomHandle--start'], 5 | classNameBindings: ['isSliding'], 6 | attributeBindings: ['style'], 7 | isSliding: false 8 | }); 9 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | var Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-wormhole", 3 | "dependencies": { 4 | "ember": "2.16.2", 5 | "ember-cli-test-loader": "0.2.2", 6 | "ember-qunit": "1.0.0", 7 | "jquery": "3.2.1", 8 | "qunit": "1.23.1" 9 | }, 10 | "resolutions": { 11 | "ember": "2.16.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | import config from '../../config/environment'; 3 | 4 | var resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /blueprints/ember-range-slider/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: '@upsilon/ember-range-slider', 3 | 4 | normalizeEntityName: function() {}, 5 | 6 | afterInstall: function() { 7 | var bowerPackages = [ 8 | { name: 'hammerjs', target: '^2.0.8' } 9 | ]; 10 | 11 | return this.addPackagesToProject(bowerPackages); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed&devmode", 4 | "disable_watching": true, 5 | "launch_in_ci": ["Chrome"], 6 | "launch_in_dev": ["Chrome"], 7 | "browser_args": { 8 | "Chrome": [ 9 | "--no-sandbox", 10 | "--headless", 11 | "--disable-gpu", 12 | "--remote-debugging-port=9222" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /addon/templates/components/ember-range-slider-handle.hbs: -------------------------------------------------------------------------------- 1 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import Resolver from "ember-resolver"; 3 | import loadInitializers from "ember-load-initializers"; 4 | import config from "./config/environment"; 5 | 6 | var App; 7 | 8 | App = Ember.Application.extend({ 9 | modulePrefix: config.modulePrefix, 10 | podModulePrefix: config.podModulePrefix, 11 | Resolver: Resolver, 12 | }); 13 | 14 | loadInitializers(App, config.modulePrefix); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /addon/templates/components/ember-range-slider.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{component startHandleComponentKey 4 | percentage=currentStartPercentage 5 | class=startHandleFullClassName 6 | isSliding=isSlidingStartHandle 7 | style=startHandleStyle}} 8 | {{component endHandleComponentKey 9 | percentage=currentEndPercentage 10 | class=endHandleFullClassName 11 | isSliding=isSlidingEndHandle 12 | style=endHandleStyle}} 13 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var EmberApp = require('ember-cli/lib/broccoli/ember-addon'); 3 | 4 | module.exports = function(defaults) { 5 | var app = new EmberApp(defaults, { 6 | snippetSearchPaths: ['tests/dummy/app'] 7 | }); 8 | 9 | /* 10 | This build file specifes the options for the dummy test app of this 11 | addon, located in `/tests/dummy` 12 | This build file does *not* influence how the addon or the app using it 13 | behave. You most likely want to be modifying `./index.js` or app's build file 14 | */ 15 | 16 | return app.toTree(); 17 | }; 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | "use strict"; 3 | 4 | const FASTBOOT_TRANSFORMATION_OPTION = { 5 | using: [ 6 | { 7 | transformation: 'fastbootShim', 8 | }, 9 | ], 10 | }; 11 | 12 | module.exports = { 13 | name: "@upsilon/ember-range-slider", 14 | 15 | included(app) { 16 | this._super.included(app); 17 | 18 | let hasFastboot = this.project.findAddonByName('ember-cli-fastboot'); 19 | let importOptions = hasFastboot ? FASTBOOT_TRANSFORMATION_OPTION : {}; 20 | app.import( 21 | 'node_modules/hammerjs/hammer.js', 22 | importOptions 23 | ); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Trebuchet', 'Helvetica', sans-serif; 3 | font-size: 14px; 4 | line-height: 1.4em; 5 | } 6 | .example { 7 | border: 1px dotted #ccc; 8 | margin-right: 240px; 9 | margin-top: 50px; 10 | max-width:675px; 11 | padding: 10px 20px; 12 | } 13 | .CustomHandle { 14 | border-radius: 10px; 15 | background-color: green; 16 | height: 20px; 17 | width: 20px; 18 | margin-left: -10px; 19 | } 20 | .CustomHandle--end { 21 | background-color: blue; 22 | border-radius: 0px; 23 | transform: rotate(45deg); 24 | } 25 | .CustomHandle.is-sliding { 26 | box-shadow: 0 0 20px red; 27 | } 28 | -------------------------------------------------------------------------------- /addon/styles/ember-range-slider.css: -------------------------------------------------------------------------------- 1 | .EmberRangeSlider { 2 | height: 20px; 3 | position: relative; 4 | } 5 | 6 | .EmberRangeSlider-base { 7 | background-color: #d0d0d0; 8 | border-radius: 5px; 9 | height: 4px; 10 | position: absolute; 11 | top: 9px; 12 | width: 100%; 13 | } 14 | 15 | .EmberRangeSlider-active { 16 | background-color: #676767; 17 | height: 4px; 18 | left: 0%; /* dynamic */ 19 | position: absolute; 20 | right: 0%; /* dynamic */ 21 | top: 9px; 22 | } 23 | 24 | .EmberRangeSlider-handle { 25 | cursor: pointer; 26 | left: 0%; /* dynamic */ 27 | margin-left: -5px; 28 | position: absolute; 29 | width: 10px; 30 | } 31 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import registerAcceptanceTestHelpers from "./raw-events/register-acceptance-test-helpers"; 3 | import Application from "../../app"; 4 | import config from "../../config/environment"; 5 | 6 | export default function startApp(attrs) { 7 | var application; 8 | 9 | var attributes = Ember.merge({}, config.APP); 10 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 11 | 12 | Ember.run(function () { 13 | application = Application.create(attributes); 14 | application.setupForTesting(); 15 | registerAcceptanceTestHelpers(); 16 | application.injectTestHelpers(); 17 | }); 18 | 19 | return application; 20 | } 21 | -------------------------------------------------------------------------------- /tests/helpers/raw-events/register-acceptance-test-helpers.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | import { 4 | triggerTapHelper, 5 | rawTapHelper, 6 | rawPanHelper, 7 | rawMouseDownHelper, 8 | rawMouseUpHelper, 9 | rawMouseMoveHelper, 10 | } from './async'; 11 | 12 | export default function(){ 13 | Ember.Test.registerAsyncHelper('triggerTap', triggerTapHelper); 14 | Ember.Test.registerAsyncHelper('rawTap', rawTapHelper); 15 | Ember.Test.registerAsyncHelper('rawPan', rawPanHelper); 16 | Ember.Test.registerAsyncHelper('rawMouseDown', rawMouseDownHelper); 17 | Ember.Test.registerAsyncHelper('rawMouseUp', rawMouseUpHelper); 18 | Ember.Test.registerAsyncHelper('rawMouseMove', rawMouseMoveHelper); 19 | } 20 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | 12 | 13 | 14 | 15 | {{content-for 'head-footer'}} 16 | 17 | 18 | {{content-for 'body'}} 19 | 20 | 21 | 22 | 23 | {{content-for 'body-footer'}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "10" 5 | 6 | sudo: false 7 | 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | 13 | env: 14 | - EMBER_TRY_SCENARIO=default 15 | - EMBER_TRY_SCENARIO=2.13.3 16 | - EMBER_TRY_SCENARIO=2.16.2 17 | - EMBER_TRY_SCENARIO=ember-release 18 | - EMBER_TRY_SCENARIO=ember-beta 19 | - EMBER_TRY_SCENARIO=ember-canary 20 | 21 | matrix: 22 | allow_failures: 23 | - env: EMBER_TRY_SCENARIO=ember-beta 24 | - env: EMBER_TRY_SCENARIO=ember-canary 25 | 26 | dist: xenial 27 | 28 | addons: 29 | chrome: stable 30 | apt: 31 | packages: 32 | - chromium-chromedriver 33 | 34 | before_script: 35 | - ln --symbolic /usr/lib/chromium-browser/chromedriver "${HOME}/bin/chromedriver" 36 | - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 37 | 38 | install: 39 | - npm install -g bower 40 | - npm install -g yarn 41 | - yarn install 42 | - bower install 43 | 44 | script: 45 | - ember try:each 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 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. 10 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | module.exports = function () { 4 | return { 5 | useYarn: true, 6 | scenarios: [ 7 | { 8 | name: "default", 9 | dependencies: {}, 10 | }, 11 | { 12 | name: "2.13.3", 13 | dependencies: { 14 | ember: "2.13.3", 15 | }, 16 | }, 17 | { 18 | name: "2.16.2", 19 | dependencies: { 20 | ember: "2.16.2", 21 | }, 22 | }, 23 | { 24 | name: "ember-release", 25 | dependencies: { 26 | ember: "components/ember#release", 27 | }, 28 | resolutions: { 29 | ember: "release", 30 | }, 31 | }, 32 | { 33 | name: "ember-beta", 34 | dependencies: { 35 | ember: "components/ember#beta", 36 | }, 37 | resolutions: { 38 | ember: "beta", 39 | }, 40 | }, 41 | { 42 | name: "ember-canary", 43 | dependencies: { 44 | ember: "components/ember#canary", 45 | }, 46 | resolutions: { 47 | ember: "canary", 48 | }, 49 | }, 50 | ], 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "triggerTap", 4 | "rawTap", 5 | "rawPan", 6 | "rawMouseDown", 7 | "rawMouseUp", 8 | "rawMouseMove", 9 | "document", 10 | "window", 11 | "location", 12 | "setTimeout", 13 | "$", 14 | "-Promise", 15 | "define", 16 | "console", 17 | "visit", 18 | "exists", 19 | "fillIn", 20 | "click", 21 | "keyEvent", 22 | "triggerEvent", 23 | "find", 24 | "findWithAssert", 25 | "wait", 26 | "DS", 27 | "andThen", 28 | "currentURL", 29 | "currentPath", 30 | "currentRouteName" 31 | ], 32 | "node": false, 33 | "browser": false, 34 | "boss": true, 35 | "curly": true, 36 | "debug": false, 37 | "devel": false, 38 | "eqeqeq": true, 39 | "evil": true, 40 | "forin": false, 41 | "immed": false, 42 | "laxbreak": false, 43 | "newcap": true, 44 | "noarg": true, 45 | "noempty": false, 46 | "nonew": false, 47 | "nomen": false, 48 | "onevar": false, 49 | "plusplus": false, 50 | "regexp": false, 51 | "undef": true, 52 | "sub": true, 53 | "strict": false, 54 | "white": false, 55 | "eqnull": true, 56 | "esnext": true, 57 | "unused": true 58 | } 59 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | {{content-for 'test-head'}} 12 | 13 | 14 | 15 | 16 | 21 | {{content-for 'head-footer'}} 22 | {{content-for 'test-head-footer'}} 23 | 24 | 25 | 26 | {{content-for 'body'}} 27 | {{content-for 'test-body'}} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{content-for 'body-footer'}} 36 | {{content-for 'test-body-footer'}} 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | } 20 | }; 21 | 22 | if (environment === 'production') { 23 | ENV.rootURL = '/ember-range-slider'; // for gh-pages live demo 24 | } 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | } 44 | 45 | if (environment === 'production') { 46 | 47 | } 48 | 49 | return ENV; 50 | }; 51 | -------------------------------------------------------------------------------- /tests/helpers/raw-events/async.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import { 3 | rawTap, 4 | rawPan, 5 | rawMouseDown, 6 | rawMouseUp, 7 | rawMouseMove 8 | } from './utils/raw-events'; 9 | 10 | // Send a tap or a click depending on if we think we're mobile or not. 11 | 12 | export function rawTapHelper(app, parentElement, selector, offsetFromCenter = null) { 13 | Ember.assert('helper must be given a selector', !!selector); 14 | findWithAssert(selector); 15 | rawTap(parentElement, selector, offsetFromCenter); 16 | return app.testHelpers.wait(); 17 | } 18 | 19 | export function rawPanHelper(app,parentElement, selector, deltaX, deltaY) { 20 | Ember.assert('helper must be given a selector', !!selector); 21 | findWithAssert(selector); 22 | rawPan(parentElement,selector, deltaX, deltaY); 23 | return app.testHelpers.wait(); 24 | } 25 | 26 | export function rawMouseDownHelper(app,parentElement, selector) { 27 | Ember.assert('helper must be given a selector', !!selector); 28 | findWithAssert(selector); 29 | rawMouseDown(parentElement,selector); 30 | return app.testHelpers.wait(); 31 | } 32 | 33 | export function rawMouseUpHelper(app, parentElement, selector) { 34 | Ember.assert('helper must be given a selector', !!selector); 35 | findWithAssert(selector); 36 | rawMouseUp(parentElement, selector); 37 | return app.testHelpers.wait(); 38 | } 39 | 40 | export function rawMouseMoveHelper(app, parentElement, selector, deltaX, deltaY) { 41 | Ember.assert('helper must be given a selector', !!selector); 42 | findWithAssert(selector); 43 | rawMouseMove(parentElement, selector, deltaX, deltaY); 44 | return app.testHelpers.wait(); 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@upsilon/ember-range-slider", 3 | "version": "1.1.0", 4 | "description": "A horizontal slider component with two handles that allows for visually selecting a range from within a specified min and max.", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "ember server", 11 | "build": "ember build", 12 | "test": "ember test" 13 | }, 14 | "repository": "https://github.com/collectrium/ember-range-slider.git", 15 | "author": "Yapp Labs", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "broccoli-asset-rev": "^3.0.0", 19 | "ember-cli": "3.1.3", 20 | "ember-cli-app-version": "3.1.0", 21 | "ember-cli-dependency-checker": "2.0.1", 22 | "ember-cli-github-pages": "0.2.0", 23 | "ember-cli-ic-ajax": "0.2.5", 24 | "ember-cli-inject-live-reload": "2.1.0", 25 | "ember-cli-qunit": "4.0.0", 26 | "ember-cli-release": "0.2.9", 27 | "ember-cli-uglify": "2.0.0", 28 | "ember-code-snippet": "^2.0.0", 29 | "ember-disable-prototype-extensions": "1.1.2", 30 | "ember-disable-proxy-controllers": "1.0.2", 31 | "ember-export-application-global": "2.0.0", 32 | "ember-load-initializers": "^2.1.2", 33 | "ember-try": "1.4.0" 34 | }, 35 | "keywords": [ 36 | "ember-addon" 37 | ], 38 | "dependencies": { 39 | "bower": "^1.8.12", 40 | "ember-cli-babel": "^7.26.6", 41 | "ember-cli-htmlbars": "2.0.2", 42 | "ember-cli-shims": "1.1.0", 43 | "ember-cli-test-loader": "^3.0.0", 44 | "ember-one-way-controls": "^3.1.0", 45 | "ember-resolver": "4.2.4", 46 | "hammerjs": "^2.0.8", 47 | "loader.js": "^4.7.0", 48 | "qunit-notifications": "^1.0.0" 49 | }, 50 | "ember-addon": { 51 | "configPath": "tests/dummy/config", 52 | "demoURL": "http://collectrium.github.io/ember-range-slider/" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | example1RangeStart: 3000, 5 | example1RangeEnd: 6000, 6 | 7 | example2RangeStart: 3000, 8 | example2RangeEnd: 6000, 9 | example2IsSliding: false, 10 | 11 | example3RangeStart: 25, 12 | example3RangeEnd: 75, 13 | 14 | example4RangeStart: 3000, 15 | example4RangeEnd: 6000, 16 | 17 | example5RangeStart: -100, 18 | example5RangeEnd: 100000, 19 | 20 | example6RangeStart: 3000, 21 | example6RangeEnd: 6000, 22 | 23 | actions: { 24 | example1RangeSliderChanging(range) { 25 | this.set('example1RangeStart', range.start); 26 | this.set('example1RangeEnd', range.end); 27 | }, 28 | example2RangeSliderChanged(range) { 29 | this.set('example2RangeStart', range.start); 30 | this.set('example2RangeEnd', range.end); 31 | }, 32 | example3RangeSliderChanging(range) { 33 | this.set('example3RangeStart', range.start); 34 | this.set('example3RangeEnd', range.end); 35 | }, 36 | example4RangeSliderChanging(range) { 37 | this.set('example4RangeStart', Math.round(range.start)); 38 | this.set('example4RangeEnd', Math.round(range.end)); 39 | }, 40 | example5RangeSliderChanging(range) { 41 | this.set('example5RangeStart', Math.round(range.start)); 42 | this.set('example5RangeEnd', Math.round(range.end)); 43 | }, 44 | example6RangeSliderChanging(range) { 45 | this.set('example6RangeStart', Math.round(range.start)); 46 | this.set('example6RangeEnd', Math.round(range.end)); 47 | }, 48 | example4UpdateStart(val) { 49 | const endRange = this.get('example4RangeEnd') 50 | 51 | if (val >= 1200 && val <= endRange) { 52 | this.set('example4RangeStart', Math.min(Math.max(1200, val), endRange)); 53 | } 54 | }, 55 | example4UpdateEnd(val) { 56 | const startRange = this.get('example4RangeStart') 57 | 58 | if (val >= startRange) { 59 | this.set('example4RangeEnd', Math.min(Math.max(val, startRange), 8000)); 60 | } 61 | }, 62 | example6RoundingHandle(val) { 63 | return parseInt((val / 1000)) * 1000; 64 | } 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /tests/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const coordinates = (scope) => { 4 | return { 5 | emberRangeSliderWidth: Ember.$(scope) 6 | .find(".EmberRangeSlider") 7 | .css("width") 8 | .replace("px", ""), 9 | emberRangeSliderStartLeft: Ember.$(scope) 10 | .find(".EmberRangeSlider-handle--start") 11 | .css("left") 12 | .replace("px", ""), 13 | emberRangeSliderEndLeft: Ember.$(scope) 14 | .find(".EmberRangeSlider-handle--end") 15 | .css("left") 16 | .replace("px", ""), 17 | emberRangeSliderActiveWidth: Ember.$(scope) 18 | .find(".EmberRangeSlider-active") 19 | .css("width") 20 | .replace("px", ""), 21 | emberRangeSliderActiveLeft: Ember.$(scope) 22 | .find(".EmberRangeSlider-active") 23 | .css("left") 24 | .replace("px", ""), 25 | }; 26 | }; 27 | 28 | export function startHandlePositionRounded(scope) { 29 | return Math.round( 30 | (+coordinates(scope).emberRangeSliderStartLeft / 31 | +coordinates(scope).emberRangeSliderWidth) * 32 | 100 33 | ); 34 | } 35 | 36 | export function endHandlePositionRounded(scope) { 37 | return Math.round( 38 | (+coordinates(scope).emberRangeSliderEndLeft / 39 | +coordinates(scope).emberRangeSliderWidth) * 40 | 100 41 | ); 42 | } 43 | 44 | export function activeRangeRounded(scope) { 45 | let left = +coordinates(scope).emberRangeSliderActiveLeft; 46 | let width = +coordinates(scope).emberRangeSliderActiveWidth; 47 | return { 48 | start: Math.round((left / +coordinates(scope).emberRangeSliderWidth) * 100), 49 | end: Math.round( 50 | ((left + width) / +coordinates(scope).emberRangeSliderWidth) * 100 51 | ), 52 | }; 53 | } 54 | 55 | export function expectWithin(assert, expected, actual, tolerance, description) { 56 | assert.ok( 57 | Math.abs(expected - actual) <= tolerance, 58 | `Expected ${description} to be within ${tolerance} of ${expected} but was ${actual}` 59 | ); 60 | } 61 | 62 | export function boundStartTextValue(scope) { 63 | let text = find(scope).text(); 64 | let match = /start: ([0-9.]+)/.exec(text); 65 | return parseFloat(match[1]); 66 | } 67 | 68 | export function boundEndTextValue(scope) { 69 | let text = find(scope).text(); 70 | let match = /end: ([0-9.]+)/.exec(text); 71 | return parseFloat(match[1]); 72 | } 73 | 74 | export function boundStartInputValue(scope) { 75 | let value = find(scope).find("input:eq(0)").val(); 76 | return parseFloat(value); 77 | } 78 | 79 | export function boundEndInputValue(scope) { 80 | let value = find(scope).find("input:eq(1)").val(); 81 | return parseFloat(value); 82 | } 83 | 84 | export function expectIsSliding(assert, scope, expectedText, description) { 85 | let text = find(scope).text(); 86 | let match = /isSliding: (\w+)/.exec(text); 87 | assert.equal( 88 | match[1], 89 | expectedText, 90 | `Expected ${description} isSliding to be ${expectedText} but was ${match[1]}` 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Range Slider [![Build Status](https://travis-ci.org/collectrium/ember-range-slider.svg?branch=master)](https://travis-ci.org/collectrium/ember-range-slider) [![Ember Observer Score](http://emberobserver.com/badges/ember-range-slider.svg)](http://emberobserver.com/addons/ember-range-slider) 2 | 3 | Provides a horizontal slider component with two handles that allows for visually selecting a range from within a specified min and max. 4 | 5 | ![Animated GIF of ember-range-slider in action](http://f.cl.ly/items/2E3B3d44330C0S3J0H31/range-slider.gif) 6 | 7 | Supported interactions include pressing (or touching) and dragging either handle, as well as tapping on the slider track to adjust the nearest slider handle to the new location. 8 | 9 | The component follows a data down / actions up approach and uses hammer.js to handle the touch and mouse interactions. 10 | 11 | ## Live Demo 12 | 13 | View a live demo here: [http://collectrium.github.io/ember-range-slider/](http://collectrium.github.io/ember-range-slider/) 14 | 15 | ## Example 16 | 17 | ```hbs 18 | {{ember-range-slider 19 | min=rangeMin 20 | max=rangeMax 21 | start=start 22 | end=end 23 | rangeChanging=(action 'rangeSliderChanging') 24 | rangeChanged=(action 'rangeChanged') 25 | roundingHandler=(action 'roundingNearestInteger') 26 | isSlidingChanged=(action (mut isSliding)) 27 | }} 28 | ``` 29 | 30 | ## Using 31 | 32 | import EmberRangeSlider from '@upsilon/ember-range-slider/components/ember-range-slider'; 33 | 34 | ## Data down 35 | 36 | `min` and `max` are numeric values that define the values associated with the left and right edge of the slider. 37 | 38 | `start` and `end` are numeric values assigned to the left and right handle. 39 | 40 | ## Actions Up 41 | 42 | The `rangeChanging` action is fired with start and end values continuously as the user slides a handle along the range. 43 | 44 | The `rangeChanged` action is fired with start and end values when a range change is "committed", i.e. when the user releases the slider handle. 45 | 46 | The `isSlidingChanged` action is fired with `true` as the user begins sliding a handle and again with `false` when the user releases the handle. 47 | 48 | The `roundingHandler` action is executed with a start and end value, continuously enclosing it in a rounding function and returning a new value as the user moves the handle over the range. 49 | 50 | ## Development Setup 51 | 52 | ### Installation 53 | 54 | * `git clone` this repository 55 | * `yarn install` 56 | * `bower install` 57 | 58 | ### Running Tests 59 | 60 | * `ember try:each` 61 | * `ember test` 62 | * `ember test --server` 63 | 64 | ### Running the dummy app 65 | 66 | * `ember server` 67 | * Visit your app at http://localhost:4200. 68 | 69 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 70 | 71 | ## Credits 72 | 73 | This addon was extracted from a prototype project at Collectrium. Contributions from @lukemelia, @chrislopresto and @leahdungo. 74 | -------------------------------------------------------------------------------- /addon/utils/scale-strategies.js: -------------------------------------------------------------------------------- 1 | const { abs, round, floor, log10, pow } = Math; 2 | 3 | const scaleStrategies = { 4 | 5 | linear: { 6 | getPercentage(min, max, value) { 7 | return (value - min) / (max - min) * 100; 8 | }, 9 | 10 | getValue(min, max, percentage) { 11 | return (max - min) * (percentage / 100) + min; 12 | }, 13 | }, 14 | 15 | logarithmic: { 16 | getPercentage(_min, _max, _value) { 17 | const max = _max || 1; 18 | const min = _min || 1; 19 | const value = _value || 1; 20 | 21 | if (min >= 0 && max >= 0) { 22 | const power = log10(value / min); 23 | const range = log10(max / min); 24 | const closestIntegerPower = floor(power); 25 | 26 | return 100 * (closestIntegerPower / range) + 100 * (power % 1 / range); 27 | } else if (min < 0 && max >= 0) { 28 | const absoluteMin = abs(min); 29 | const absoluteMax = abs(max); 30 | const distance = absoluteMin + absoluteMax; 31 | const zeroPointCoefficient = absoluteMin / distance; 32 | const zeroPointPercentage = zeroPointCoefficient * 100; 33 | 34 | if (value >= 0) { 35 | const valueAtRightHalf = 100 * (log10(value) / log10(max)) * (max / distance); 36 | return zeroPointPercentage + valueAtRightHalf; 37 | } else { 38 | const progress = log10(-value) / log10(absoluteMin); 39 | const minPartProportion = (absoluteMin / distance); 40 | const valueAtLeftHalf = 100 * progress * minPartProportion; 41 | return zeroPointPercentage - valueAtLeftHalf; 42 | } 43 | } else { 44 | return 100 - scaleStrategies.logarithmic.getPercentage(-min, -max, -value); 45 | } 46 | }, 47 | 48 | getValue(_min, _max, percentage) { 49 | const max = _max || 1; 50 | const min = _min || 1; 51 | 52 | if (min >= 0 && max >= 0) { 53 | const range = log10(max / min); 54 | const offset = log10(min); 55 | const power = range * (percentage / 100) + offset; 56 | const closestIntegerPower = floor(power); 57 | 58 | const value = pow(10, power); 59 | return round(value / pow(10, closestIntegerPower)) * pow(10, closestIntegerPower); 60 | } else if (min < 0 && max >= 0) { 61 | const absoluteMin = abs(min); 62 | const absoluteMax = abs(max); 63 | const distance = absoluteMin + absoluteMax; 64 | const zeroPointCoefficient = absoluteMin / distance; 65 | const zeroPointPercentage = zeroPointCoefficient * 100; 66 | 67 | const absolutePercentage = percentage - zeroPointPercentage; 68 | if (absolutePercentage >= 0) { 69 | let relativePercentage = absolutePercentage / (absoluteMax / distance); 70 | return scaleStrategies.logarithmic.getValue(0, absoluteMax, relativePercentage); 71 | } else { 72 | let relativePercentage = -absolutePercentage / (absoluteMin / distance); 73 | return -scaleStrategies.logarithmic.getValue(0, absoluteMin, relativePercentage); 74 | } 75 | } else { 76 | return -scaleStrategies.logarithmic.getValue(-max, -min, percentage); 77 | } 78 | }, 79 | }, 80 | 81 | }; 82 | 83 | export default scaleStrategies; 84 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

ember-range-slider

2 | 3 | {{outlet}} 4 | 5 |
6 |

Example using "rangeChanging" action, which fires continuously as sliders move.

7 | {{code-snippet name='example1.hbs'}} 8 | {{!-- BEGIN-SNIPPET example1 --}} 9 | {{ember-range-slider 10 | min=1200 11 | max=8000 12 | start=example1RangeStart 13 | end=example1RangeEnd 14 | rangeChanging=(action 'example1RangeSliderChanging') 15 | }} 16 | {{!-- END-SNIPPET --}} 17 | 21 |
22 | 23 |
24 |

Example using "rangeChanged" action, which fires once slider is released. 25 | This example also demonstrated the "isSlidingChanged" action, which fires 26 | when the user begins sliding with false, and when then complete sliding, 27 | with true.

28 | {{code-snippet name='example2.hbs'}} 29 | {{!-- BEGIN-SNIPPET example2 --}} 30 | {{ember-range-slider 31 | min=1200 32 | max=8000 33 | start=example2RangeStart 34 | end=example2RangeEnd 35 | rangeChanged=(action 'example2RangeSliderChanged') 36 | isSlidingChanged=(action (mut example2IsSliding)) 37 | }} 38 | {{!-- END-SNIPPET --}} 39 | 44 |
45 | 46 |
47 |

Example of supplying your own component for the slider handles.

48 | {{code-snippet name='example3.hbs'}} 49 | {{!-- BEGIN-SNIPPET example3 --}} 50 | {{ember-range-slider 51 | min=0 52 | max=100 53 | start=example3RangeStart 54 | end=example3RangeEnd 55 | startHandleComponentKey="custom-start-handle" 56 | endHandleComponentKey="custom-end-handle" 57 | rangeChanging=(action 'example3RangeSliderChanging') 58 | }} 59 | {{!-- END-SNIPPET --}} 60 | 64 |
65 | 66 |
67 |

Example of text fields working in concert with slider. Press enter after editing a text field value.

68 | {{code-snippet name='example4.hbs'}} 69 | {{!-- BEGIN-SNIPPET example4 --}} 70 | {{ember-range-slider 71 | min=1200 72 | max=8000 73 | start=example4RangeStart 74 | end=example4RangeEnd 75 | rangeChanging=(action 'example4RangeSliderChanging') 76 | }} 77 |
78 | 79 | - 80 | 81 |
82 | 83 | {{!-- END-SNIPPET --}} 84 | 88 |
89 | 90 |
91 |

You can specify a 'scale' property to set a non-linear scale.

92 | {{code-snippet name='example1.hbs'}} 93 | {{!-- BEGIN-SNIPPET example1 --}} 94 | {{ember-range-slider 95 | min=-1000000 96 | max=1000000 97 | start=example5RangeStart 98 | end=example5RangeEnd 99 | scale='logarithmic' 100 | rangeChanging=(action 'example5RangeSliderChanging') 101 | }} 102 | {{!-- END-SNIPPET --}} 103 | 107 |
108 | 109 |
110 |

Example using "roundingHandler" action to pass a rounding function.

111 | {{code-snippet name='example6.hbs'}} 112 | {{!-- BEGIN-SNIPPET example6 --}} 113 | {{ember-range-slider 114 | min=1000 115 | max=10000 116 | start=example6RangeStart 117 | end=example6RangeEnd 118 | rangeChanging=(action 'example6RangeSliderChanging') 119 | roundingHandler=(action 'example6RoundingHandle') 120 | }} 121 | {{!-- END-SNIPPET --}} 122 | 126 |
127 | -------------------------------------------------------------------------------- /tests/helpers/raw-events/utils/raw-events.js: -------------------------------------------------------------------------------- 1 | /* globals PointerEvent */ 2 | 3 | import Ember from "ember"; 4 | 5 | function createTouchEvent(name, x, y /*, identifier*/) { 6 | const event = new PointerEvent(name, { 7 | view: window, 8 | bubbles: true, 9 | cancelable: true, 10 | button: 0, 11 | clientX: x, 12 | clientY: y, 13 | screenX: x, 14 | screenY: y, 15 | }); 16 | return event; 17 | } 18 | 19 | function dispatchTouchEvent(el, name, x, y) { 20 | const event = createTouchEvent(name, x, y); 21 | el.dispatchEvent(event); 22 | } 23 | 24 | function elementCoordinates($element, $parentElement) { 25 | const width = $element.width(); 26 | const height = $element.height(); 27 | const offset = $element.offset(); 28 | const sliderWidth = +$parentElement.css("width").replace("px", ""); 29 | return { 30 | x: offset.left + width / 2 - 0.4, 31 | y: offset.top + height / 2, 32 | sliderWidth, 33 | }; 34 | } 35 | 36 | export function rawTap(parentElement, selector, offsetFromCenter = null) { 37 | return new Ember.RSVP.Promise(function (resolve) { 38 | const $parent = Ember.$(parentElement); 39 | const $element = Ember.$(selector); 40 | const center = elementCoordinates($element, $parent); 41 | if (offsetFromCenter) { 42 | center.x = center.x + (center.sliderWidth * offsetFromCenter.x) / 100; 43 | center.y = center.y + offsetFromCenter.y; 44 | } 45 | dispatchTouchEvent($element[0], "pointerdown", center.x, center.y); 46 | dispatchTouchEvent($element[0], "pointerup", center.x, center.y); 47 | Ember.run.later(() => { 48 | resolve(); 49 | }, 100); 50 | }); 51 | } 52 | 53 | export function rawPan(parentElement, selector, deltaX, deltaY) { 54 | return new Ember.RSVP.Promise(function (resolve) { 55 | const $parent = Ember.$(parentElement); 56 | const $element = Ember.$(selector); 57 | const begin = elementCoordinates($element, $parent); 58 | const end = { 59 | x: begin.x + (begin.sliderWidth * deltaX) / 100, 60 | y: begin.y + deltaY, 61 | }; 62 | 63 | dispatchTouchEvent($element[0], "pointerdown", begin.x, begin.y); 64 | dispatchTouchEvent($element[0], "pointermove", begin.x, begin.y); 65 | 66 | Ember.run.later(() => { 67 | dispatchTouchEvent($element[0], "pointermove", end.x, end.y); 68 | 69 | Ember.run.later(() => { 70 | dispatchTouchEvent($element[0], "pointermove", end.x, end.y); 71 | dispatchTouchEvent($element[0], "pointerup", end.x, end.y); 72 | 73 | Ember.run.later(() => { 74 | resolve(); 75 | }, 100); 76 | }, 100); 77 | }, 100); 78 | }); 79 | } 80 | 81 | export function rawMouseDown(parentElement, selector) { 82 | return new Ember.RSVP.Promise(function (resolve) { 83 | const $parent = Ember.$(parentElement); 84 | const $element = Ember.$(selector); 85 | const coords = elementCoordinates($element, $parent); 86 | dispatchTouchEvent($element[0], "pointerdown", coords.x, coords.y); 87 | Ember.run.later(() => { 88 | resolve(); 89 | }, 100); 90 | }); 91 | } 92 | 93 | export function rawMouseUp(parentElement, selector) { 94 | return new Ember.RSVP.Promise(function (resolve) { 95 | const $parent = Ember.$(parentElement); 96 | const $element = Ember.$(selector); 97 | const coords = elementCoordinates($element, $parent); 98 | dispatchTouchEvent($element[0], "pointerup", coords.x, coords.y); 99 | Ember.run.later(() => { 100 | resolve(); 101 | }, 100); 102 | }); 103 | } 104 | 105 | export function rawMouseMove(parentElement, selector, deltaX, deltaY) { 106 | return new Ember.RSVP.Promise(function (resolve) { 107 | const $parent = Ember.$(parentElement); 108 | const $element = Ember.$(selector); 109 | const begin = elementCoordinates($element, $parent); 110 | const end = { 111 | x: begin.x + (begin.sliderWidth * deltaX) / 100, 112 | y: begin.y + deltaY, 113 | }; 114 | 115 | dispatchTouchEvent($element[0], "pointermove", begin.x, begin.y); 116 | Ember.run.later(() => { 117 | dispatchTouchEvent($element[0], "pointermove", end.x, end.y); 118 | Ember.run.later(() => { 119 | resolve(); 120 | }, 100); 121 | }, 100); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /addon/components/ember-range-slider.js: -------------------------------------------------------------------------------- 1 | /* global Hammer */ 2 | import Ember from 'ember'; 3 | import layout from '../templates/components/ember-range-slider'; 4 | import scaleStrategies from '../utils/scale-strategies'; 5 | 6 | const { computed, isBlank, run, get, set } = Ember; 7 | const { htmlSafe } = Ember.String; 8 | 9 | function constrainToBetween(value, min, max) { 10 | value = Math.max(min, value); 11 | return Math.min(value, max); 12 | } 13 | 14 | export default Ember.Component.extend({ 15 | layout, 16 | classNames: ['EmberRangeSlider'], 17 | 18 | /* Required input properties. These are not updated by this component per the 19 | * "data-down" portion of the data-down / actions-up approach. 20 | */ 21 | min: null, 22 | max: null, 23 | start: null, 24 | end: null, 25 | 26 | scale: 'linear', 27 | scaleStrategies, 28 | 29 | roundingHandler: ( value ) => value, 30 | onlyIntegers: false, 31 | 32 | baseClassName: 'EmberRangeSlider-base', 33 | activeRegionClassName: 'EmberRangeSlider-active', 34 | handleClassName: 'EmberRangeSlider-handle', 35 | startHandleClassName: 'EmberRangeSlider-handle--start', 36 | endHandleClassName: 'EmberRangeSlider-handle--end', 37 | 38 | startHandleFullClassName: computed('handleClassName', 'startHandleClassName', function () { 39 | return `${this.get('handleClassName')} ${this.get('startHandleClassName')}`; 40 | }), 41 | 42 | endHandleFullClassName: computed('handleClassName', 'endHandleClassName', function () { 43 | return `${this.get('handleClassName')} ${this.get('endHandleClassName')}`; 44 | }), 45 | 46 | /* Set these properties to use another component for the 47 | * start and/or end slider handles 48 | */ 49 | startHandleComponentKey: 'ember-range-slider-handle', 50 | endHandleComponentKey: 'ember-range-slider-handle', 51 | 52 | /* These properties are bound down to the handle components 53 | * so that they are aware when they are sliding. 54 | */ 55 | isSlidingStartHandle: false, 56 | isSlidingEndHandle: false, 57 | 58 | /* Handle positioning is done based on percentages, and the startPercentage 59 | * and endPercentage CPs are responsible for converting between the provided 60 | * min/max/start/end values and the percentages used for layout. Their 61 | * setters also invoke the rangeChanging action, which should be used for 62 | * providing immediate feedback. 63 | */ 64 | startPercentage: computed('min', 'max', 'start', { 65 | get() { 66 | const scale = get(this, 'scale'); 67 | const strategy = get(this, 'scaleStrategies')[scale]; 68 | 69 | const min = get(this, 'min'); 70 | const max = get(this, 'max'); 71 | const start = get(this, 'start'); 72 | const value = isBlank(start) ? min : start; 73 | 74 | const percentage = strategy.getPercentage(min, max, value); 75 | const limitedPercentage = constrainToBetween(percentage, 0, 100); 76 | 77 | if (!get(this, 'isSlidingStartHandle')) { 78 | set(this, 'mockStartPercentage', limitedPercentage); 79 | } 80 | 81 | return limitedPercentage; 82 | }, 83 | set(key, percentage) { 84 | const updatedStart = this.getValueFromPercentage(percentage); 85 | set(this, 'mockStartPercentage', percentage); 86 | this.sendAction('rangeChanging', { 87 | start: updatedStart, 88 | end: get(this, 'end') 89 | }); 90 | return percentage; 91 | } 92 | }), 93 | endPercentage: computed('min', 'max', 'end', { 94 | get() { 95 | const scale = get(this, 'scale'); 96 | const strategy = get(this, 'scaleStrategies')[scale]; 97 | 98 | const min = get(this, 'min'); 99 | const max = get(this, 'max'); 100 | const end = get(this, 'end'); 101 | const value = isBlank(end) ? max : end; 102 | 103 | const percentage = strategy.getPercentage(min, max, value); 104 | const limitedPercentage = constrainToBetween(percentage, 0, 100); 105 | 106 | if (!get(this, 'isSlidingEndHandle')) { 107 | set(this, 'mockEndPercentage', limitedPercentage); 108 | } 109 | 110 | return limitedPercentage; 111 | }, 112 | set(key, percentage) { 113 | const updatedEnd = this.getValueFromPercentage(percentage); 114 | set(this, 'mockEndPercentage', percentage); 115 | this.sendAction('rangeChanging', { 116 | start: get(this, 'start'), 117 | end: updatedEnd 118 | }); 119 | return percentage; 120 | } 121 | }), 122 | 123 | /** 124 | * These values is showing when handle is sliding. 125 | * Used to prevent yanking of handle. 126 | */ 127 | mockStartPercentage: null, 128 | mockEndPercentage: null, 129 | 130 | currentStartPercentage: computed('isSlidingStartHandle', 'startPercentage', 'mockStartPercentage', function() { 131 | const isSlidingStartHandle = get(this, 'isSlidingStartHandle'); 132 | 133 | if (!isSlidingStartHandle) { 134 | const startPercentage = get(this, 'startPercentage'); 135 | set(this, 'mockStartPercentage', startPercentage); 136 | return startPercentage; 137 | } else { 138 | return get(this, 'mockStartPercentage'); 139 | } 140 | }), 141 | currentEndPercentage: computed('isSlidingEndHandle', 'endPercentage', 'mockEndPercentage', function() { 142 | const isSlidingEndHandle = get(this, 'isSlidingEndHandle'); 143 | 144 | if (!isSlidingEndHandle) { 145 | const endPercentage = get(this, 'endPercentage'); 146 | set(this, 'mockEndPercentage', endPercentage); 147 | return endPercentage; 148 | } else { 149 | return get(this, 'mockEndPercentage'); 150 | } 151 | }), 152 | 153 | /* These three CPs are used for dynamic binding in the handlebars template. 154 | */ 155 | activeRangeStyle: computed('currentStartPercentage', 'currentEndPercentage', function() { 156 | let startPercentage = this.get('currentStartPercentage'); 157 | let endPercentage = this.get('currentEndPercentage'); 158 | return htmlSafe(`left: ${startPercentage}%; right: ${100 - endPercentage}%`); 159 | }), 160 | startHandleStyle: computed('currentStartPercentage', function() { 161 | let startPercentage = this.get('currentStartPercentage'), 162 | endPercentage = this.get('currentEndPercentage'); 163 | startPercentage = Math.round(startPercentage * 1000) / 1000; 164 | endPercentage = Math.round(endPercentage * 1000) / 1000; 165 | if (+startPercentage === 100 && +endPercentage === 100) { 166 | return htmlSafe(`left: ${startPercentage}%; z-index: 1`); 167 | } else { 168 | return htmlSafe(`left: ${startPercentage}%`); 169 | } 170 | }), 171 | endHandleStyle: computed('currentEndPercentage', function() { 172 | let endPercentage = this.get('currentEndPercentage'), 173 | startPercentage = this.get('currentStartPercentage'); 174 | startPercentage = Math.round(startPercentage * 1000) / 1000; 175 | endPercentage = Math.round(endPercentage * 1000) / 1000; 176 | if (+startPercentage === 0 && +endPercentage === 0) { 177 | return htmlSafe(`left: ${endPercentage}%; z-index: 1`); 178 | } else { 179 | return htmlSafe(`left: ${endPercentage}%`); 180 | } 181 | }), 182 | 183 | didInsertElement() { 184 | this._super(...arguments); 185 | this.setupHammerEvents(); 186 | }, 187 | 188 | /* Set up a Hammer.js manager to handle touch and mouse events. 189 | * Hammer is a global that is available by virtue of this plugin 190 | * app.import'ing the hammer.js bower_component 191 | */ 192 | setupHammerEvents() { 193 | this._hammer = new Hammer.Manager(this.get('element'), { 194 | recognizers: [ 195 | [Hammer.Tap], 196 | [Hammer.Press, { time: 1 }], 197 | [Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL, threshold: 1 }] 198 | ] 199 | }); 200 | this.wirePress(); 201 | this.wirePan(); 202 | this.wireTap(); 203 | }, 204 | 205 | /* Tear down the Hammer.js manager on destroy. 206 | */ 207 | willDestroyElement() { 208 | this._super(...arguments); 209 | this._hammer.destroy(); 210 | }, 211 | 212 | /* This next set of methods contain all the references to css class names 213 | * for this component. If you subclass and provide your own template with 214 | * different class names, override these methods. 215 | */ 216 | getSliderHandleFromEventTarget(eventTarget) { 217 | return Ember.$(eventTarget).closest(`.${this.get('handleClassName')}`); 218 | }, 219 | 220 | isStartHandle($sliderHandle) { 221 | return $sliderHandle.hasClass(this.get('startHandleClassName')); 222 | }, 223 | 224 | isEndHandle($sliderHandle) { 225 | return $sliderHandle.hasClass(this.get('endHandleClassName')); 226 | }, 227 | 228 | isSliderBase($element) { 229 | return $element.hasClass(this.get('baseClassName')); 230 | }, 231 | 232 | isSliderActiveRegion($element) { 233 | return $element.hasClass(this.get('activeRegionClassName')); 234 | }, 235 | 236 | getPercentageFromX(x) { 237 | let $sliderBase = this.$(`.${this.get('baseClassName')}`); 238 | let baseLeft = $sliderBase.offset().left; 239 | let baseRight = baseLeft + $sliderBase.width(); 240 | return ((x - baseLeft) / (baseRight - baseLeft)) * 100; 241 | }, 242 | 243 | /* Convert from percentage position to the numeric value in 244 | * terms of the passed in range min and max. 245 | */ 246 | 247 | getValueFromPercentage(percentage) { 248 | const min = get(this, 'min'); 249 | const max = get(this, 'max'); 250 | const onlyIntegers = get(this, 'onlyIntegers'); 251 | const roundingHandler = get(this, 'roundingHandler'); 252 | 253 | const scale = get(this, 'scale'); 254 | const strategy = get(this, 'scaleStrategies')[scale]; 255 | 256 | const value = roundingHandler(strategy.getValue(min, max, percentage)); 257 | return (onlyIntegers) ? Math.round(value) : value; 258 | }, 259 | 260 | /* press events precede pan events, so we can use this to keep track of 261 | * which handle is being interacted with, start or end 262 | */ 263 | wirePress() { 264 | this._hammer.on('press', (ev) => { 265 | let $sliderHandle = this.getSliderHandleFromEventTarget(ev.target); 266 | if (this.isStartHandle($sliderHandle)) { 267 | this._isMovingStartHandle = true; 268 | this.startSliding(); 269 | } 270 | if (this.isEndHandle($sliderHandle)) { 271 | this._isMovingEndHandle = true; 272 | this.startSliding(); 273 | } 274 | }); 275 | 276 | this._hammer.on('pressup', () => { 277 | this.stopSliding(); 278 | }); 279 | }, 280 | 281 | /* pan events get translated to a percentage position when a handle is 282 | * being dragged around 283 | */ 284 | wirePan() { 285 | this._hammer.on('pan', (ev) => { 286 | if (this._isMovingStartHandle) { 287 | let startHandlePercent = this.getPercentageFromX(ev.center.x); 288 | let endPercentage = this.get('endPercentage'); 289 | startHandlePercent = constrainToBetween(startHandlePercent, 0, endPercentage); 290 | run(() => { 291 | this.set('startPercentage', startHandlePercent); 292 | }); 293 | } 294 | if (this._isMovingEndHandle) { 295 | let endHandlePercent = this.getPercentageFromX(ev.center.x); 296 | let startPercentage = this.get('startPercentage'); 297 | endHandlePercent = constrainToBetween(endHandlePercent, startPercentage, 100); 298 | run(() => { 299 | this.set('endPercentage', endHandlePercent); 300 | }); 301 | } 302 | }); 303 | this._hammer.on('panend', () => { 304 | this.stopSliding(); 305 | }); 306 | }, 307 | 308 | /* tap events are translated to a percentage position when a handle is 309 | * being dragged around 310 | */ 311 | wireTap() { 312 | this._hammer.on('tap', (ev) => { 313 | let $target = Ember.$(ev.target); 314 | let tapShouldPositionHandle = this.isSliderBase($target) || this.isSliderActiveRegion($target); 315 | if (tapShouldPositionHandle) { 316 | let tappablePercent = this.getPercentageFromX(ev.center.x); 317 | let startPercentage = this.get('startPercentage'); 318 | let endPercentage = this.get('endPercentage'); 319 | let distanceFromStart = Math.abs(tappablePercent - startPercentage); 320 | let distanceFromEnd = Math.abs(tappablePercent - endPercentage); 321 | run(() => { 322 | if (distanceFromStart > distanceFromEnd) { 323 | this.set('endPercentage', tappablePercent); 324 | } else { 325 | this.set('startPercentage', tappablePercent); 326 | } 327 | this.sendRangeChanged(); 328 | }); 329 | return; 330 | } 331 | if (this.getSliderHandleFromEventTarget(ev.target).length === 1) { 332 | this.stopSliding(); 333 | } 334 | }); 335 | }, 336 | 337 | sendRangeChanged() { 338 | let startPercentage = this.get('startPercentage'); 339 | let endPercentage = this.get('endPercentage'); 340 | let start = this.getValueFromPercentage(startPercentage); 341 | let end = this.getValueFromPercentage(endPercentage); 342 | this.sendAction('rangeChanged', { start, end }); 343 | }, 344 | 345 | startSliding() { 346 | run(() => { 347 | this.set('isSlidingStartHandle', this._isMovingStartHandle); 348 | this.set('isSlidingEndHandle', this._isMovingEndHandle); 349 | this.sendAction('isSlidingChanged', true); 350 | }); 351 | }, 352 | 353 | stopSliding() { 354 | if (!this._isMovingStartHandle && !this._isMovingEndHandle) { 355 | return; 356 | } 357 | this._isMovingStartHandle = false; 358 | this._isMovingEndHandle = false; 359 | run(() => { 360 | this.set('isSlidingStartHandle', false); 361 | this.set('isSlidingEndHandle', false); 362 | this.sendAction('isSlidingChanged', false); 363 | this.sendRangeChanged(); 364 | }); 365 | } 366 | }); 367 | -------------------------------------------------------------------------------- /tests/acceptance/range-slider-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { 3 | module, 4 | test 5 | } from 'qunit'; 6 | import startApp from 'dummy/tests/helpers/start-app'; 7 | import { 8 | startHandlePositionRounded, 9 | endHandlePositionRounded, 10 | activeRangeRounded, 11 | expectWithin, 12 | boundStartTextValue, 13 | boundEndTextValue, 14 | boundStartInputValue, 15 | boundEndInputValue, 16 | expectIsSliding 17 | } from '../helpers/helpers'; 18 | 19 | var application; 20 | 21 | module('Acceptance: Range Slider', { 22 | beforeEach: function() { 23 | application = startApp(); 24 | }, 25 | 26 | afterEach: function() { 27 | Ember.run(application, 'destroy'); 28 | } 29 | }); 30 | 31 | test('initial rendering', function(assert) { 32 | visit('/'); 33 | andThen(function() { 34 | assert.equal(currentPath(), 'index'); 35 | 36 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'start handle is positioned (Example 1)'); 37 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'end handle is positioned (Example 1)'); 38 | expectWithin(assert, 26, activeRangeRounded('.example-1').start, 1, 'active range start is positioned (Example 1)'); 39 | expectWithin(assert, 70, activeRangeRounded('.example-1').end, 1, 'active range end is positioned (Example 1)'); 40 | 41 | expectWithin(assert, 26, startHandlePositionRounded('.example-2'), 1, 'start handle is positioned (Example 2)'); 42 | expectWithin(assert, 70, endHandlePositionRounded('.example-2'), 1, 'end handle is positioned (Example 2)'); 43 | expectWithin(assert, 26, activeRangeRounded('.example-2').start, 1, 'active range start is positioned (Example 2)'); 44 | expectWithin(assert, 70, activeRangeRounded('.example-2').end, 1, 'active range end is positioned (Example 2)'); 45 | 46 | expectWithin(assert, 25, startHandlePositionRounded('.example-3'), 1, 'start handle is positioned (Example 3)'); 47 | expectWithin(assert, 75, endHandlePositionRounded('.example-3'), 1, 'end handle is positioned (Example 3)'); 48 | expectWithin(assert, 25, activeRangeRounded('.example-3').start, 1, 'active range start is positioned (Example 3)'); 49 | expectWithin(assert, 75, activeRangeRounded('.example-3').end, 1, 'active range end is positioned (Example 3)'); 50 | 51 | expectWithin(assert, 26, startHandlePositionRounded('.example-4'), 1, 'start handle is positioned (Example 4)'); 52 | expectWithin(assert, 70, endHandlePositionRounded('.example-4'), 1, 'end handle is positioned (Example 4)'); 53 | expectWithin(assert, 26, activeRangeRounded('.example-4').start, 1, 'active range start is positioned (Example 4)'); 54 | expectWithin(assert, 70, activeRangeRounded('.example-4').end, 1, 'active range end is positioned (Example 4)'); 55 | }); 56 | }); 57 | 58 | test('slide start handle interaction with rangeChanging action', function(assert) { 59 | visit('/'); 60 | andThen(function() { 61 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 62 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 63 | }); 64 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 65 | andThen(function() { 66 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'after mousedown start handle position'); 67 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 50, 'after mousedown bound start text'); 68 | }); 69 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", -4, 0); 70 | andThen(function() { 71 | expectWithin(assert, 23, startHandlePositionRounded('.example-1'), 1, 'after mousemove lower start handle position'); 72 | expectWithin(assert, 2765, boundStartTextValue('.example-1'), 50, 'after mousemove lower bound start text'); 73 | }); 74 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", 6, 0); 75 | andThen(function() { 76 | expectWithin(assert, 29, startHandlePositionRounded('.example-1'), 1, 'after mousemove higher start handle position'); 77 | expectWithin(assert, 3150, boundStartTextValue('.example-1'), 50, 'after mousemove higher bound start text'); 78 | }); 79 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", 0, -5); 80 | andThen(function() { 81 | expectWithin(assert, 29, startHandlePositionRounded('.example-1'), 1, 'after mousemove vertical start handle position'); 82 | expectWithin(assert, 3150, boundStartTextValue('.example-1'), 50, 'after mousemove vertical bound start text'); 83 | }); 84 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 85 | andThen(function() { 86 | expectWithin(assert, 29, startHandlePositionRounded('.example-1'), 1, 'after mouseup start handle position'); 87 | expectWithin(assert, 3150, boundStartTextValue('.example-1'), 50, 'after mouseup bound start text'); 88 | }); 89 | }); 90 | 91 | test('slide end handle interaction with rangeChanging action', function(assert) { 92 | visit('/'); 93 | andThen(function() { 94 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'before interaction end handle position'); 95 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 1, 'before interaction bound end text'); 96 | }); 97 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 98 | andThen(function() { 99 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'after mousedown end handle position'); 100 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 50, 'after mousedown bound end text'); 101 | }); 102 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", 4.5, 0); 103 | andThen(function() { 104 | expectWithin(assert, 74, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 105 | expectWithin(assert, 6295, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 106 | }); 107 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", -6, 0); 108 | andThen(function() { 109 | expectWithin(assert, 69, endHandlePositionRounded('.example-1'), 1, 'after mousemove lower end handle position'); 110 | expectWithin(assert, 5878, boundEndTextValue('.example-1'), 50, 'after mousemove lower bound end text'); 111 | }); 112 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", 0, 10); 113 | andThen(function() { 114 | expectWithin(assert, 69, endHandlePositionRounded('.example-1'), 1, 'after mousemove vertical end handle position'); 115 | expectWithin(assert, 5878, boundEndTextValue('.example-1'), 50, 'after mousemove vertical bound end text'); 116 | }); 117 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 118 | andThen(function() { 119 | expectWithin(assert, 69, endHandlePositionRounded('.example-1'), 1, 'after mouseup end handle position'); 120 | expectWithin(assert, 5878, boundEndTextValue('.example-1'), 50, 'after mouseup bound end text'); 121 | }); 122 | }); 123 | 124 | test('check when two ranges in end positions', function(assert) { 125 | visit('/'); 126 | andThen(function() { 127 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 128 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'before interaction end handle position'); 129 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 130 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 1, 'before interaction bound end text'); 131 | }); 132 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 133 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", 30, 0); 134 | andThen(function() { 135 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 136 | expectWithin(assert, 100, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 137 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 138 | expectWithin(assert, 8000, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 139 | }); 140 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 141 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 142 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", 74, 0); 143 | andThen(function() { 144 | expectWithin(assert, 100, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 145 | expectWithin(assert, 100, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 146 | expectWithin(assert, 8000, boundStartTextValue('.example-1'), 50, 'before interaction bound start text'); 147 | expectWithin(assert, 8000, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 148 | }); 149 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 150 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 151 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", -73.5, 0); 152 | andThen(function() { 153 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 154 | expectWithin(assert, 100, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 155 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 50, 'before interaction bound start text'); 156 | expectWithin(assert, 8000, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 157 | }); 158 | }); 159 | 160 | test('check when two ranges in start positions', function(assert) { 161 | visit('/'); 162 | andThen(function() { 163 | expectWithin(assert, 27, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 164 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'before interaction end handle position'); 165 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 166 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 1, 'before interaction bound end text'); 167 | }); 168 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 169 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start", -27, 0); 170 | andThen(function() { 171 | expectWithin(assert, 0, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 172 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 173 | expectWithin(assert, 1200, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 174 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 175 | }); 176 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--start"); 177 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 178 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", -71, 0); 179 | andThen(function() { 180 | expectWithin(assert, 0, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 181 | expectWithin(assert, 0, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 182 | expectWithin(assert, 1200, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 183 | expectWithin(assert, 1200, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 184 | }); 185 | rawMouseUp(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 186 | rawMouseDown(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end"); 187 | rawMouseMove(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-handle--end", 70.5, 0); 188 | andThen(function() { 189 | expectWithin(assert, 0, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 190 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'after mousemove higher end handle position'); 191 | expectWithin(assert, 1200, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 192 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 50, 'after mousemove higher bound end text'); 193 | }); 194 | 195 | }); 196 | 197 | test('tap interaction', function(assert) { 198 | visit('/'); 199 | andThen(function() { 200 | expectWithin(assert, 26, startHandlePositionRounded('.example-1'), 1, 'before interaction start handle position'); 201 | expectWithin(assert, 3000, boundStartTextValue('.example-1'), 1, 'before interaction bound start text'); 202 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'before interaction end handle position'); 203 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 1, 'before interaction bound end text'); 204 | }); 205 | rawTap(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-base", { x: -5, y: 0}); 206 | andThen(function() { 207 | expectWithin(assert, 44, startHandlePositionRounded('.example-1'), 1, 'after lower tap start handle position'); 208 | expectWithin(assert, 4250, boundStartTextValue('.example-1'), 50, 'after lower tap bound start text'); 209 | expectWithin(assert, 70, endHandlePositionRounded('.example-1'), 1, 'after lower tap end handle position'); 210 | expectWithin(assert, 6000, boundEndTextValue('.example-1'), 1, 'after lower tap bound end text'); 211 | }); 212 | rawTap(".example-1 .EmberRangeSlider", ".example-1 .EmberRangeSlider-base", { x: 23.5, y: 0}); 213 | andThen(function() { 214 | expectWithin(assert, 44, startHandlePositionRounded('.example-1'), 1, 'after higher tap start handle position'); 215 | expectWithin(assert, 4250, boundStartTextValue('.example-1'), 50, 'after higher tap bound start text'); 216 | expectWithin(assert, 73, endHandlePositionRounded('.example-1'), 1, 'after higher tap end handle position'); 217 | expectWithin(assert, 6185, boundEndTextValue('.example-1'), 50, 'after higher tap bound end text'); 218 | }); 219 | }); 220 | 221 | test('slide start handle interaction with rangeChanged action', function(assert) { 222 | visit('/'); 223 | andThen(function() { 224 | expectWithin(assert, 26, startHandlePositionRounded('.example-2'), 1, 'before interaction start handle position'); 225 | expectWithin(assert, 3000, boundStartTextValue('.example-2'), 1, 'before interaction bound start text'); 226 | expectIsSliding(assert, '.example-2', 'false', 'before interaction'); 227 | }); 228 | rawMouseDown(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--start"); 229 | andThen(function() { 230 | expectWithin(assert, 26, startHandlePositionRounded('.example-2'), 1, 'after mousedown start handle position'); 231 | expectWithin(assert, 3000, boundStartTextValue('.example-2'), 50, 'after mousedown bound start text'); 232 | expectIsSliding(assert, '.example-2', 'true', 'after mousedown'); 233 | }); 234 | rawMouseMove(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--start", -10, 0); 235 | andThen(function() { 236 | expectWithin(assert, 16, startHandlePositionRounded('.example-2'), 1, 'after mousemove lower start handle position'); 237 | expectWithin(assert, 3000, boundStartTextValue('.example-2'), 50, 'after mousemove lower bound start text'); 238 | expectIsSliding(assert, '.example-2', 'true', 'after mousemove lower'); 239 | }); 240 | rawMouseMove(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--start", 13, 0); 241 | andThen(function() { 242 | expectWithin(assert, 28, startHandlePositionRounded('.example-2'), 1, 'after mousemove higher start handle position'); 243 | expectWithin(assert, 3000, boundStartTextValue('.example-2'), 50, 'after mousemove higher bound start text'); 244 | expectIsSliding(assert, '.example-2', 'true', 'after mousemove higher'); 245 | }); 246 | rawMouseUp(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--start"); 247 | andThen(function() { 248 | expectWithin(assert, 28, startHandlePositionRounded('.example-2'), 1, 'after mouseup start handle position'); 249 | expectWithin(assert, 3150, boundStartTextValue('.example-2'), 50, 'after mouseup bound start text'); 250 | expectIsSliding(assert, '.example-2', 'false', 'after mouseup'); 251 | }); 252 | }); 253 | 254 | test('slide end handle interaction with rangeChanged action', function(assert) { 255 | visit('/'); 256 | andThen(function() { 257 | expectWithin(assert, 70, endHandlePositionRounded('.example-2'), 1, 'before interaction end handle position'); 258 | expectWithin(assert, 6000, boundEndTextValue('.example-2'), 1, 'before interaction bound end text'); 259 | expectIsSliding(assert, '.example-2', 'false', 'before interaction'); 260 | }); 261 | rawMouseDown(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--end"); 262 | andThen(function() { 263 | expectWithin(assert, 70, endHandlePositionRounded('.example-2'), 1, 'after mousedown end handle position'); 264 | expectWithin(assert, 6000, boundEndTextValue('.example-2'), 50, 'after mousedown bound end text'); 265 | expectIsSliding(assert, '.example-2', 'true', 'after mousedown'); 266 | }); 267 | rawMouseMove(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--end", -10, 0); 268 | andThen(function() { 269 | expectWithin(assert, 60, endHandlePositionRounded('.example-2'), 1, 'after mousemove lower end handle position'); 270 | expectWithin(assert, 6000, boundEndTextValue('.example-2'), 50, 'after mousemove lower bound end text'); 271 | expectIsSliding(assert, '.example-2', 'true', 'after mousemove lower'); 272 | }); 273 | rawMouseMove(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--end", 13, 0); 274 | andThen(function() { 275 | expectWithin(assert, 73, endHandlePositionRounded('.example-2'), 1, 'after mousemove higher end handle position'); 276 | expectWithin(assert, 6000, boundEndTextValue('.example-2'), 50, 'after mousemove higher bound end text'); 277 | expectIsSliding(assert, '.example-2', 'true', 'after mousemove higher'); 278 | }); 279 | rawMouseUp(".example-2 .EmberRangeSlider", ".example-2 .EmberRangeSlider-handle--end"); 280 | andThen(function() { 281 | expectWithin(assert, 73, endHandlePositionRounded('.example-2'), 1, 'after mouseup end handle position'); 282 | expectWithin(assert, 6150, boundEndTextValue('.example-2'), 50, 'after mouseup bound end text'); 283 | expectIsSliding(assert, '.example-2', 'false', 'after mouseup'); 284 | }); 285 | }); 286 | 287 | 288 | test('slide start handle interaction with roundingHandler action', function(assert) { 289 | visit('/'); 290 | andThen(function() { 291 | expectWithin(assert, 22, startHandlePositionRounded('.example-6'), 1, 'before interaction start handle position'); 292 | expectWithin(assert, 3000, boundStartTextValue('.example-6'), 1, 'before interaction bound start text'); 293 | }); 294 | rawMouseDown(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--start"); 295 | andThen(function() { 296 | expectWithin(assert, 22, startHandlePositionRounded('.example-6'), 1, 'after mousedown start handle position'); 297 | expectWithin(assert, 3000, boundStartTextValue('.example-6'), 1, 'after mousedown bound start text'); 298 | }); 299 | rawMouseMove(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--start", 10, 0); 300 | andThen(function() { 301 | expectWithin(assert, 32, startHandlePositionRounded('.example-6'), 1, 'after mousemove lower start handle position'); 302 | expectWithin(assert, 3000, boundStartTextValue('.example-6'), 1, 'after mousemove lower bound start text'); 303 | }); 304 | rawMouseMove(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--start", 10, 0); 305 | andThen(function() { 306 | expectWithin(assert, 42, startHandlePositionRounded('.example-6'), 1, 'after mousemove lower start handle position'); 307 | expectWithin(assert, 4000, boundStartTextValue('.example-6'), 1, 'after mousemove lower bound start text'); 308 | }); 309 | rawMouseUp(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--start"); 310 | andThen(function() { 311 | expectWithin(assert, 42, startHandlePositionRounded('.example-6'), 1, 'after mouseup start handle position'); 312 | expectWithin(assert, 4000, boundStartTextValue('.example-6'), 1, 'after mouseup bound start text'); 313 | }); 314 | }); 315 | 316 | test('slide end handle interaction with roundingHandler action', function(assert) { 317 | visit('/'); 318 | andThen(function() { 319 | expectWithin(assert, 56, endHandlePositionRounded('.example-6'), 1, 'before interaction end handle position'); 320 | expectWithin(assert, 6000, boundEndTextValue('.example-6'), 1, 'before interaction bound end text'); 321 | }); 322 | rawMouseDown(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--end"); 323 | andThen(function() { 324 | expectWithin(assert, 56, endHandlePositionRounded('.example-6'), 1, 'after mousedown end handle position'); 325 | expectWithin(assert, 6000, boundEndTextValue('.example-6'), 1, 'after mousedown bound end text'); 326 | }); 327 | rawMouseMove(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--end", 4, 0); 328 | andThen(function() { 329 | expectWithin(assert, 60, endHandlePositionRounded('.example-6'), 1, 'after mousemove lower end handle position'); 330 | expectWithin(assert, 6000, boundEndTextValue('.example-6'), 1, 'after mousemove lower bound end text'); 331 | }); 332 | rawMouseMove(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--end", 15, 0); 333 | andThen(function() { 334 | expectWithin(assert, 75, endHandlePositionRounded('.example-6'), 1, 'after mousemove higher end handle position'); 335 | expectWithin(assert, 7000, boundEndTextValue('.example-6'), 1, 'after mousemove higher bound end text'); 336 | }); 337 | rawMouseUp(".example-6 .EmberRangeSlider", ".example-6 .EmberRangeSlider-handle--end"); 338 | andThen(function() { 339 | expectWithin(assert, 75, endHandlePositionRounded('.example-6'), 1, 'after mouseup end handle position'); 340 | expectWithin(assert, 7000, boundEndTextValue('.example-6'), 1, 'after mouseup bound end text'); 341 | }); 342 | }); 343 | 344 | 345 | test('custom components for handles', function(assert) { 346 | visit('/'); 347 | andThen(function() { 348 | assert.equal(Ember.$('.example-3 .EmberRangeSlider-handle--start').css('background-color'), 'rgb(0, 128, 0)', 'start handle is green'); 349 | assert.equal(Ember.$('.example-3 .EmberRangeSlider-handle--end').css('background-color'), 'rgb(0, 0, 255)', 'end handle is blue'); 350 | }); 351 | }); 352 | 353 | test('bound to text fields', function(assert) { 354 | visit('/'); 355 | andThen(function() { 356 | expectWithin(assert, 26, startHandlePositionRounded('.example-4'), 1, 'before interaction start handle position'); 357 | expectWithin(assert, 3000, boundStartTextValue('.example-4'), 1, 'before interaction bound start text'); 358 | expectWithin(assert, 3000, boundStartInputValue('.example-4'), 1, 'before interaction bound start input'); 359 | expectWithin(assert, 70, endHandlePositionRounded('.example-4'), 1, 'before interaction end handle position'); 360 | expectWithin(assert, 6000, boundEndTextValue('.example-4'), 1, 'before interaction bound end text'); 361 | expectWithin(assert, 6000, boundEndInputValue('.example-4'), 1, 'before interaction bound end input'); 362 | }); 363 | fillIn('.example-4 input:eq(0)', '3200'); 364 | andThen(function() { 365 | expectWithin(assert, 29, startHandlePositionRounded('.example-4'), 1, 'after update start text field, start handle position'); 366 | expectWithin(assert, 3200, boundStartTextValue('.example-4'), 1, 'after update start text field, bound start text'); 367 | expectWithin(assert, 3200, boundStartInputValue('.example-4'), 1, 'after update start text field, bound start input'); 368 | expectWithin(assert, 70, endHandlePositionRounded('.example-4'), 1, 'after update start text field, end handle position'); 369 | expectWithin(assert, 6000, boundEndTextValue('.example-4'), 1, 'after update start text field, bound end text'); 370 | expectWithin(assert, 6000, boundEndInputValue('.example-4'), 1, 'after update start text field, bound end input'); 371 | }); 372 | fillIn('.example-4 input:eq(1)', '5000'); 373 | andThen(function() { 374 | expectWithin(assert, 29, startHandlePositionRounded('.example-4'), 1, 'after update end text field, start handle position'); 375 | expectWithin(assert, 3200, boundStartTextValue('.example-4'), 1, 'after update end text field, bound start text'); 376 | expectWithin(assert, 3200, boundStartInputValue('.example-4'), 1, 'after update end text field, bound start input'); 377 | expectWithin(assert, 55, endHandlePositionRounded('.example-4'), 1, 'after update end text field, end handle position'); 378 | expectWithin(assert, 5000, boundEndTextValue('.example-4'), 1, 'after update end text field, bound end text'); 379 | expectWithin(assert, 5000, boundEndInputValue('.example-4'), 1, 'after update end text field, bound end input'); 380 | }); 381 | rawPan(".example-4 .EmberRangeSlider", '.example-4 .EmberRangeSlider-handle--start', -6, 0); 382 | andThen(function() { 383 | expectWithin(assert, 23, startHandlePositionRounded('.example-4'), 1, 'after slide start handle, start handle position'); 384 | expectWithin(assert, 2796, boundStartTextValue('.example-4'), 50, 'after slide start handle, bound start text'); 385 | expectWithin(assert, 2796, boundStartInputValue('.example-4'), 50, 'after slide start handle, bound start input'); 386 | expectWithin(assert, 55, endHandlePositionRounded('.example-4'), 1, 'after slide start handle, end handle position'); 387 | expectWithin(assert, 5000, boundEndTextValue('.example-4'), 1, 'after slide start handle, bound end text'); 388 | expectWithin(assert, 5000, boundEndInputValue('.example-4'), 50, 'after slide start handle, bound end input'); 389 | }); 390 | rawPan(".example-4 .EmberRangeSlider", '.example-4 .EmberRangeSlider-handle--end', -5.5, 0); 391 | andThen(function() { 392 | expectWithin(assert, 23, startHandlePositionRounded('.example-4'), 1, 'after slide end handle, start handle position'); 393 | expectWithin(assert, 2796, boundStartTextValue('.example-4'), 50, 'after slide end handle, bound start text'); 394 | expectWithin(assert, 2796, boundStartInputValue('.example-4'), 50, 'after slide end handle, bound start input'); 395 | expectWithin(assert, 50, endHandlePositionRounded('.example-4'), 1, 'after slide end handle, end handle position'); 396 | expectWithin(assert, 4600, boundEndTextValue('.example-4'), 50, 'after slide end handle, bound end text'); 397 | expectWithin(assert, 4600, boundEndInputValue('.example-4'), 50, 'after slide end handle, bound end input'); 398 | }); 399 | }); 400 | --------------------------------------------------------------------------------