├── app ├── .gitkeep ├── components │ └── pop-over.js ├── initializers │ └── pop-over.js └── templates │ └── components │ └── pop-over.hbs ├── addon ├── .gitkeep ├── computed │ ├── stringify.js │ ├── nearest-parent.js │ ├── w.js │ └── nearest-child.js ├── system │ ├── flow.js │ ├── orientation.js │ ├── rectangle.js │ ├── constraint.js │ └── target.js ├── mixins │ └── scroll_sandbox.js └── components │ └── pop-over.js ├── vendor ├── .gitkeep └── styles │ └── ember-popup-menu.css ├── tests ├── unit │ ├── .gitkeep │ ├── system │ │ ├── rectangle-test.js │ │ ├── target-test.js │ │ └── constraint-test.js │ └── components │ │ └── pop-over-test.js ├── dummy │ ├── public │ │ ├── .gitkeep │ │ ├── robots.txt │ │ └── crossdomain.xml │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ ├── .gitkeep │ │ │ └── app.css │ │ ├── views │ │ │ └── .gitkeep │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── templates │ │ │ ├── .gitkeep │ │ │ ├── components │ │ │ │ └── .gitkeep │ │ │ └── application.hbs │ │ ├── router.js │ │ ├── app.js │ │ ├── index.html │ │ └── flows.js │ ├── .jshintrc │ └── config │ │ └── environment.js ├── helpers │ ├── blur.js │ ├── focus.js │ ├── mouse-up.js │ ├── mouse-down.js │ ├── mouse-out.js │ ├── touch-end.js │ ├── mouse-enter.js │ ├── mouse-leave.js │ ├── touch-start.js │ ├── resolver.js │ ├── start-app.js │ └── simple-click.js ├── test-helper.js ├── index.html ├── .jshintrc └── acceptance │ └── events-test.js ├── .watchmanconfig ├── .bowerrc ├── config ├── environment.js └── ember-try.js ├── testem.json ├── ember-cli-build.js ├── .npmignore ├── .ember-cli ├── .gitignore ├── index.js ├── bower.json ├── .jshintrc ├── .editorconfig ├── .travis.yml ├── LICENSE.md ├── package.json ├── blueprints └── ember-pop-over │ └── files │ └── app │ └── flows.js └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/helpers/blur.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "focusout"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/focus.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "focusin"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/mouse-up.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "mouseup"); 3 | } 4 | -------------------------------------------------------------------------------- /app/components/pop-over.js: -------------------------------------------------------------------------------- 1 | import PopOver from "ember-pop-over/components/pop-over"; 2 | export default PopOver; 3 | -------------------------------------------------------------------------------- /tests/helpers/mouse-down.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "mousedown"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/mouse-out.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "mouseout"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/touch-end.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "touchend"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/mouse-enter.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "mouseenter"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/mouse-leave.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "mouseleave"); 3 | } 4 | -------------------------------------------------------------------------------- /tests/helpers/touch-start.js: -------------------------------------------------------------------------------- 1 | export default function (selector) { 2 | triggerEvent(selector, "touchstart"); 3 | } 4 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "PhantomJS" 7 | ], 8 | "launch_in_dev": [ 9 | "PhantomJS", 10 | "Chrome" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /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 | }); 7 | 8 | Router.map(function() { 9 | }); 10 | 11 | export default Router; 12 | -------------------------------------------------------------------------------- /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 | // Add options here 7 | }); 8 | 9 | return app.toTree(); 10 | }; 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | tests/ 3 | public/ 4 | tmp/ 5 | dist/ 6 | config/ember-try.js 7 | 8 | .bowerrc 9 | .editorconfig 10 | .ember-cli 11 | .travis.yml 12 | .npmignore 13 | **/.gitkeep 14 | bower.json 15 | ember-cli-build.js 16 | Brocfile.js 17 | testem.json 18 | *~ 19 | *.tgz 20 | -------------------------------------------------------------------------------- /addon/computed/stringify.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const computed = Ember.computed; 4 | const get = Ember.get; 5 | 6 | export default function(property) { 7 | return computed(property, { 8 | get() { 9 | return String(get(this, property)); 10 | } 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /.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 | *~ -------------------------------------------------------------------------------- /addon/computed/nearest-parent.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const getOwner = Ember.getOwner; 4 | const computed = Ember.computed; 5 | 6 | export default function(type) { 7 | return computed({ 8 | get() { 9 | var typeClass = getOwner(this).factoryFor('component:' + type) || 10 | getOwner(this).factoryFor('view:' + type); 11 | return this.nearestOfType(typeClass); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /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 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver: Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global node: true */ 2 | module.exports = { 3 | name: 'ember-pop-over', 4 | options: { 5 | nodeAssets: { 6 | 'dom-ruler': { 7 | srcDir: 'dist/amd', 8 | import: ['dom-ruler.js'] 9 | } 10 | } 11 | }, 12 | 13 | included: function (app) { 14 | this._super.included.apply(this, arguments); 15 | app.import('vendor/dom-ruler/dom-ruler.js', { 16 | exports: { 17 | 'dom-ruler': ['default'] 18 | } 19 | }); 20 | app.import("vendor/styles/ember-popup-menu.css"); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | var application; 7 | 8 | var attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(function() { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /addon/computed/w.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const computed = Ember.computed; 4 | const w = Ember.String.w; 5 | 6 | const toArray = function (value) { 7 | if (typeof value === "string") { 8 | value = w(value); 9 | } 10 | return value; 11 | }; 12 | 13 | export default function(defaultValue) { 14 | defaultValue = defaultValue || []; 15 | return computed({ 16 | get() { 17 | return Ember.A(toArray(defaultValue)); 18 | }, 19 | set(key, value) { 20 | value = toArray(value); 21 | return Ember.A(value || toArray(defaultValue)); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-pop-over", 3 | "dependencies": { 4 | "ember": "2.1.0", 5 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", 6 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", 7 | "ember-data": "2.1.0", 8 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.5", 9 | "ember-qunit": "0.4.9", 10 | "ember-qunit-notifications": "0.0.7", 11 | "ember-resolver": "~0.1.18", 12 | "jquery": "^1.11.3", 13 | "loader.js": "ember-cli/loader.js#3.2.1", 14 | "qunit": "~1.18.0" 15 | }, 16 | "resolutions": { 17 | "ember": "2.1.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/initializers/pop-over.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import Flow from "ember-pop-over/system/flow"; 3 | import * as flows from "../flows"; 4 | 5 | const get = Ember.get; 6 | const keys = Object.keys; 7 | 8 | export var initialize = function (app) { 9 | Ember.A(keys(flows)).forEach(function (flowName) { 10 | if (flowName == 'default') { return; } 11 | let constraints = get(flows[flowName].call(Flow.create()), 'constraints'); 12 | app.register(`pop-over-constraint:${flowName}`, constraints, { instantiate: false }); 13 | }); 14 | }; 15 | 16 | export default { 17 | name: "register-pop-over-flows", 18 | initialize: initialize 19 | }; 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /app/templates/components/pop-over.hbs: -------------------------------------------------------------------------------- 1 | {{#if supportsLiquidFire}} 2 |
3 | {{#liquid-if active class="liquid-pop-over"}} 4 |
5 | 6 | {{yield}} 7 |
8 | {{/liquid-if}} 9 |
10 | {{/if}} 11 | {{#if active}} 12 |
13 |
14 | 15 | {{yield}} 16 |
17 |
18 | {{/if}} 19 | -------------------------------------------------------------------------------- /tests/dummy/.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 | -------------------------------------------------------------------------------- /addon/system/flow.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import Orientation from "./orientation"; 3 | 4 | const on = Ember.on; 5 | 6 | export default Ember.Object.extend({ 7 | setupOrienters: on('init', function() { 8 | this.orientAbove = Orientation.create({ orientation: 'above' }); 9 | this.orientBelow = Orientation.create({ orientation: 'below' }); 10 | this.orientRight = Orientation.create({ orientation: 'right' }); 11 | this.orientLeft = Orientation.create({ orientation: 'left' }); 12 | }), 13 | 14 | topEdge: 'top-edge', 15 | bottomEdge: 'bottom-edge', 16 | leftEdge: 'left-edge', 17 | rightEdge: 'right-edge', 18 | center: 'center' 19 | }); 20 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | - EMBER_TRY_SCENARIO=ember-release 15 | - EMBER_TRY_SCENARIO=ember-beta 16 | - EMBER_TRY_SCENARIO=ember-canary 17 | 18 | matrix: 19 | fast_finish: true 20 | allow_failures: 21 | - env: EMBER_TRY_SCENARIO=ember-canary 22 | 23 | before_install: 24 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH 25 | - "npm config set spin false" 26 | - "npm install -g npm@^2" 27 | 28 | install: 29 | - npm install -g bower 30 | - npm install 31 | - bower install 32 | 33 | script: 34 | - ember try $EMBER_TRY_SCENARIO test 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | scenarios: [ 3 | { 4 | name: 'default', 5 | dependencies: { } 6 | }, 7 | { 8 | name: 'ember-release', 9 | dependencies: { 10 | 'ember': 'components/ember#release' 11 | }, 12 | resolutions: { 13 | 'ember': 'release' 14 | } 15 | }, 16 | { 17 | name: 'ember-beta', 18 | dependencies: { 19 | 'ember': 'components/ember#beta' 20 | }, 21 | resolutions: { 22 | 'ember': 'beta' 23 | } 24 | }, 25 | { 26 | name: 'ember-canary', 27 | dependencies: { 28 | 'ember': 'components/ember#canary' 29 | }, 30 | resolutions: { 31 | 'ember': 'canary' 32 | } 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tests/helpers/simple-click.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | var run = Ember.run; 4 | 5 | export default function (selector) { 6 | andThen(function () { 7 | var $element = find(selector); 8 | run($element, 'mousedown'); 9 | 10 | if ($element.is(':input')) { 11 | var type = $element.prop('type'); 12 | if (type !== 'checkbox' && type !== 'radio' && type !== 'hidden') { 13 | run($element, function(){ 14 | // Firefox does not trigger the `focusin` event if the window 15 | // does not have focus. If the document doesn't have focus just 16 | // use trigger('focusin') instead. 17 | if (!document.hasFocus || document.hasFocus()) { 18 | this.focus(); 19 | } else { 20 | this.trigger('focusin'); 21 | } 22 | }); 23 | } 24 | } 25 | 26 | run($element, 'mouseup'); 27 | }); 28 | triggerEvent(selector, "click", { which: 1 }); 29 | } 30 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
Click
4 | {{#pop-over for="click" on="click"}} 5 | I have been clicked. 6 | {{/pop-over}} 7 | 8 |
Click + Hold
9 | {{#pop-over for="click-hold" on="click hold"}} 10 | I have been clicked / held. 11 | {{/pop-over}} 12 | 13 | Hover 14 | {{#pop-over for="hover" on="hover"}} 15 | I have been hovered. 16 | {{/pop-over}} 17 | 18 | Hover + Hold 19 | {{#pop-over id="hover-hold-menu" for="hover-hold" on="hover hold"}} 20 | I have been hovered / held. 21 | {{/pop-over}} 22 | 23 |
Focus
24 | {{#pop-over for="focus" on="focus"}} 25 | I have been focused. 26 | {{/pop-over}} 27 | 28 |
Hover + Focus
29 | {{#pop-over for="hover-focus" on="focus hover"}} 30 | I have been focused / hovered. 31 | {{/pop-over}} 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 17 | {{content-for 'head-footer'}} 18 | {{content-for 'test-head-footer'}} 19 | 20 | 21 | 22 | {{content-for 'body'}} 23 | {{content-for 'test-body'}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for 'body-footer'}} 31 | {{content-for 'test-body-footer'}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/unit/system/rectangle-test.js: -------------------------------------------------------------------------------- 1 | import Rectangle from "ember-pop-over/system/rectangle"; 2 | import { test } from "ember-qunit"; 3 | 4 | module("Rectangle"); 5 | 6 | test("intersecting two overlapping rectangles", function () { 7 | var a = new Rectangle(0, 0, 15, 15); 8 | var b = new Rectangle(5, 10, 15, 15); 9 | 10 | var intersection = Rectangle.intersection(a, b); 11 | equal(intersection.x, 5); 12 | equal(intersection.width, 10); 13 | 14 | equal(intersection.y, 10); 15 | equal(intersection.height, 5); 16 | 17 | ok(a.intersects(b)); 18 | }); 19 | 20 | test("intersecting two non-overlapping rectangles", function () { 21 | var a = new Rectangle(0, 0, 5, 10); 22 | var b = new Rectangle(5, 10, 15, 15); 23 | 24 | var intersection = Rectangle.intersection(a, b); 25 | equal(intersection.x, 0); 26 | equal(intersection.width, 0); 27 | 28 | equal(intersection.y, 0); 29 | equal(intersection.height, 0); 30 | 31 | ok(!a.intersects(b)); 32 | }); 33 | 34 | test("whether one rectangle contains another", function () { 35 | var a = new Rectangle(0, 0, 100, 100); 36 | var b = new Rectangle(5, 10, 20, 20); 37 | 38 | ok(a.contains(b)); 39 | ok(!b.contains(a)); 40 | }); 41 | -------------------------------------------------------------------------------- /vendor/styles/ember-popup-menu.css: -------------------------------------------------------------------------------- 1 | .pop-over { 2 | position: relative; 3 | } 4 | 5 | .pop-over-compass { 6 | position: absolute; 7 | z-index: 100; 8 | } 9 | 10 | .pop-over-compass-invisible { 11 | visibility: hidden; 12 | } 13 | 14 | .liquid-pop-over { 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | .liquid-pop-over .liquid-child { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | .pop-over-container { 25 | background-color: #333; 26 | color: white; 27 | } 28 | 29 | .pop-over-pointer { 30 | position: absolute; 31 | display: block; 32 | border-style: solid; 33 | border-width: 10px; 34 | height: 0; 35 | width: 0; 36 | z-index: 2; 37 | pointer-events: none; 38 | } 39 | 40 | .pop-over-pointer.orient-above { 41 | border-color: #333 transparent transparent transparent; 42 | bottom: -20px; 43 | } 44 | 45 | .pop-over-pointer.orient-below { 46 | border-color: transparent transparent #333 transparent; 47 | top: -20px; 48 | } 49 | 50 | .pop-over-pointer.orient-left { 51 | border-color: transparent transparent transparent #333; 52 | right: -20px; 53 | } 54 | 55 | .pop-over-pointer.orient-right { 56 | border-color: transparent #333 transparent transparent; 57 | left: -20px; 58 | } 59 | -------------------------------------------------------------------------------- /tests/unit/system/target-test.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import Target from "ember-pop-over/system/target"; 3 | import { test } from "ember-qunit"; 4 | 5 | var get = Ember.get; 6 | var run = Ember.run; 7 | 8 | module("Event Target"); 9 | 10 | test('"for" takes an string id', function() { 11 | expect(1); 12 | 13 | var target = Target.create({ 14 | target: "ember-testing-container", 15 | on: 'click' 16 | }); 17 | target.attach(); 18 | equal(target.element, document.getElementById("ember-testing-container")); 19 | target.detach(); 20 | }); 21 | 22 | test('"for" takes an element', function() { 23 | expect(1); 24 | 25 | var element = document.getElementById("ember-testing-container"); 26 | var target = Target.create({ 27 | target: element, 28 | on: 'click' 29 | }); 30 | target.attach(); 31 | equal(target.element, element); 32 | target.detach(); 33 | }); 34 | 35 | test('"for" takes a component', function() { 36 | expect(1); 37 | 38 | var component = Ember.Component.create(); 39 | run(function () { 40 | component.appendTo("#qunit-fixture"); 41 | }); 42 | var target = Target.create({ 43 | target: component, 44 | on: 'click' 45 | }); 46 | target.attach(); 47 | 48 | equal(target.element, get(component, 'element')); 49 | 50 | run(function () { 51 | component.destroy(); 52 | }); 53 | target.detach(); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "QUnit", 10 | "define", 11 | "console", 12 | "equal", 13 | "notEqual", 14 | "notStrictEqual", 15 | "test", 16 | "asyncTest", 17 | "testBoth", 18 | "testWithDefault", 19 | "raises", 20 | "throws", 21 | "deepEqual", 22 | "start", 23 | "stop", 24 | "ok", 25 | "strictEqual", 26 | "module", 27 | "moduleFor", 28 | "moduleForComponent", 29 | "moduleForModel", 30 | "process", 31 | "expect", 32 | "visit", 33 | "exists", 34 | "fillIn", 35 | "click", 36 | "keyEvent", 37 | "triggerEvent", 38 | "find", 39 | "findWithAssert", 40 | "wait", 41 | "DS", 42 | "isolatedContainer", 43 | "startApp", 44 | "andThen", 45 | "currentURL", 46 | "currentPath", 47 | "currentRouteName" 48 | ], 49 | "node": false, 50 | "browser": false, 51 | "boss": true, 52 | "curly": false, 53 | "debug": false, 54 | "devel": false, 55 | "eqeqeq": true, 56 | "evil": true, 57 | "forin": false, 58 | "immed": false, 59 | "laxbreak": false, 60 | "newcap": true, 61 | "noarg": true, 62 | "noempty": false, 63 | "nonew": false, 64 | "nomen": false, 65 | "onevar": false, 66 | "plusplus": false, 67 | "regexp": false, 68 | "undef": true, 69 | "sub": true, 70 | "strict": false, 71 | "white": false, 72 | "eqnull": true, 73 | "esnext": true 74 | } 75 | -------------------------------------------------------------------------------- /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 | baseURL: '/', 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 === 'development') { 23 | // ENV.APP.LOG_RESOLVER = true; 24 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 25 | // ENV.APP.LOG_TRANSITIONS = true; 26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 27 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 28 | ENV.contentSecurityPolicy = { 29 | 'default-src': "'none'", 30 | 'script-src': "'self'", 31 | 'font-src': "'self'", 32 | 'connect-src': "'self'", 33 | 'img-src': "'self'", 34 | 'style-src': "'self' 'unsafe-inline'", 35 | 'media-src': "'self'" 36 | } 37 | } 38 | 39 | if (environment === 'test') { 40 | // Testem prefers this... 41 | ENV.baseURL = '/'; 42 | ENV.locationType = 'none'; 43 | 44 | // keep test console output quieter 45 | ENV.APP.LOG_ACTIVE_GENERATION = false; 46 | ENV.APP.LOG_VIEW_LOOKUPS = false; 47 | 48 | ENV.APP.rootElement = '#ember-testing'; 49 | } 50 | 51 | if (environment === 'production') { 52 | 53 | } 54 | 55 | return ENV; 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-pop-over", 3 | "version": "0.1.31", 4 | "description": "A constraint-based pop-over component for ember apps", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "ember server", 11 | "build": "ember build", 12 | "test": "ember try:testall" 13 | }, 14 | "repository": "https://github.com/tim-evans/ember-pop-over", 15 | "engines": { 16 | "node": ">= 0.10.0" 17 | }, 18 | "author": "Tim Evans ", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "broccoli-asset-rev": "^2.1.2", 22 | "ember-cli": "1.13.8", 23 | "ember-cli-app-version": "0.5.0", 24 | "ember-cli-content-security-policy": "0.4.0", 25 | "ember-cli-dependency-checker": "^1.0.1", 26 | "ember-cli-htmlbars": "0.7.9", 27 | "ember-cli-htmlbars-inline-precompile": "^0.2.0", 28 | "ember-cli-ic-ajax": "0.2.1", 29 | "ember-cli-inject-live-reload": "^1.3.1", 30 | "ember-cli-qunit": "^1.0.0", 31 | "ember-cli-release": "0.2.3", 32 | "ember-cli-sri": "^1.0.3", 33 | "ember-cli-uglify": "^1.2.0", 34 | "ember-data": "1.13.8", 35 | "ember-disable-proxy-controllers": "^1.0.0", 36 | "ember-export-application-global": "^1.0.3", 37 | "ember-disable-prototype-extensions": "^1.0.0", 38 | "ember-try": "0.0.6" 39 | }, 40 | "keywords": [ 41 | "ember-addon", 42 | "pop-over" 43 | ], 44 | "dependencies": { 45 | "dom-ruler": "^0.2.4", 46 | "ember-cli-babel": "^5.1.3", 47 | "ember-cli-node-assets": "^0.1.6" 48 | }, 49 | "ember-addon": { 50 | "configPath": "tests/dummy/config" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/dummy/app/flows.js: -------------------------------------------------------------------------------- 1 | export function around() { 2 | return this.orientAbove.andSnapTo(this.center, this.leftEdge, this.rightEdge) 3 | .then(this.orientRight.andSlideBetween(this.bottomEdge, this.topEdge)) 4 | .then(this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge)) 5 | .then(this.orientLeft .andSlideBetween(this.topEdge, this.bottomEdge)) 6 | .then(this.orientAbove.andSnapTo(this.center)); 7 | } 8 | 9 | export function dropdown() { 10 | return this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge) 11 | .then(this.orientLeft.andSnapTo(this.topEdge, this.bottomEdge)) 12 | .then(this.orientRight.andSnapTo(this.topEdge)) 13 | .then(this.orientBelow.andSnapTo(this.center)); 14 | } 15 | 16 | export function flip() { 17 | return this.orientAbove.andSnapTo(this.center, this.leftEdge, this.rightEdge) 18 | .where(function (boundingRect, _, targetRect) { 19 | var centerY = targetRect.height / 2 + targetRect.y, 20 | halfway = boundingRect.height / 2; 21 | return centerY > halfway; 22 | }) 23 | .then(this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge) 24 | .where(function (boundingRect, _, targetRect) { 25 | var centerY = targetRect.height / 2 + targetRect.y, 26 | halfway = boundingRect.height / 2; 27 | return centerY < halfway; 28 | }) 29 | ) 30 | .then(this.orientAbove.andSnapTo(this.center)); 31 | } 32 | 33 | export function popup() { 34 | return this.orientAbove.andSnapTo(this.center, this.rightEdge, this.leftEdge, this.center); 35 | } 36 | -------------------------------------------------------------------------------- /blueprints/ember-pop-over/files/app/flows.js: -------------------------------------------------------------------------------- 1 | export function around() { 2 | return this.orientAbove.andSnapTo(this.center, this.leftEdge, this.rightEdge) 3 | .then(this.orientRight.andSlideBetween(this.bottomEdge, this.topEdge)) 4 | .then(this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge)) 5 | .then(this.orientLeft .andSlideBetween(this.topEdge, this.bottomEdge)) 6 | .then(this.orientAbove.andSnapTo(this.center)); 7 | } 8 | 9 | export function dropdown() { 10 | return this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge) 11 | .then(this.orientLeft.andSnapTo(this.topEdge, this.bottomEdge)) 12 | .then(this.orientRight.andSnapTo(this.topEdge)) 13 | .then(this.orientBelow.andSnapTo(this.center)); 14 | } 15 | 16 | export function flip() { 17 | return this.orientAbove.andSnapTo(this.center, this.leftEdge, this.rightEdge) 18 | .where(function (boundingRect, _, targetRect) { 19 | var centerY = targetRect.height / 2 + targetRect.y, 20 | halfway = boundingRect.height / 2; 21 | return centerY > halfway; 22 | }) 23 | .then(this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge) 24 | .where(function (boundingRect, _, targetRect) { 25 | var centerY = targetRect.height / 2 + targetRect.y, 26 | halfway = boundingRect.height / 2; 27 | return centerY < halfway; 28 | }) 29 | ) 30 | .then(this.orientAbove.andSnapTo(this.center)); 31 | } 32 | 33 | export function popup() { 34 | return this.orientAbove.andSnapTo(this.center, this.rightEdge, this.leftEdge, this.center); 35 | } 36 | -------------------------------------------------------------------------------- /addon/computed/nearest-child.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const computed = Ember.computed; 4 | const bind = Ember.run.bind; 5 | const get = Ember.get; 6 | const getOwner = Ember.getOwner; 7 | 8 | function flatten(array) { 9 | return Ember.A(array).reduce(function (a, b) { 10 | return a.concat(b); 11 | }, Ember.A()); 12 | } 13 | 14 | function recursivelyFindByType(typeClass, children) { 15 | let view = Ember.A(children).find(function (view) { 16 | return typeClass.detectInstance(view); 17 | }); 18 | 19 | if (view) { 20 | return view; 21 | } 22 | 23 | let childrenOfChildren = flatten(Ember.A(children).getEach('childViews')); 24 | if (childrenOfChildren.length === 0) { 25 | return null; 26 | } 27 | return recursivelyFindByType(typeClass, childrenOfChildren); 28 | } 29 | 30 | export default function(type) { 31 | var tracking = Ember.Map.create(); 32 | var deleteItem; 33 | if (tracking.delete) { 34 | deleteItem = bind(tracking, 'delete'); 35 | } else { 36 | deleteItem = bind(tracking, 'remove'); 37 | } 38 | 39 | return computed('childViews.[]', { 40 | get(key) { 41 | var typeClass = getOwner(this).factoryFor('component:' + type) || 42 | getOwner(this).factoryFor('view:' + type); 43 | 44 | var children = Ember.A(get(this, 'childViews')); 45 | var appendedChildren = children.filterBy('_state', 'inDOM'); 46 | var detachedChildren = children.filter(function (child) { 47 | return ['inBuffer', 'hasElement', 'preRender'].indexOf(child._state) !== -1; 48 | }); 49 | 50 | appendedChildren.forEach(function (child) { 51 | deleteItem(child); 52 | }); 53 | 54 | var notifyChildrenChanged = bind(this, 'notifyPropertyChange', key); 55 | detachedChildren.forEach(function (child) { 56 | if (!tracking.has(child)) { 57 | child.one('didInsertElement', this, notifyChildrenChanged); 58 | tracking.set(child, true); 59 | } 60 | }); 61 | 62 | return recursivelyFindByType(typeClass, appendedChildren); 63 | } 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /tests/unit/components/pop-over-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForComponent, 3 | test 4 | } from 'ember-qunit'; 5 | import Ember from "ember"; 6 | 7 | const get = Ember.get; 8 | const set = Ember.set; 9 | const run = Ember.run; 10 | 11 | moduleForComponent('pop-over', 'PopOverComponent', { 12 | 13 | }); 14 | 15 | test('"retile" is called when will-change properties change', function() { 16 | expect(4); 17 | 18 | var RETILE_CALLED = false; 19 | 20 | var component; 21 | run(this, function () { 22 | component = this.subject({ 23 | on: "click", 24 | retile: function () { 25 | RETILE_CALLED = true; 26 | } 27 | }); 28 | this.render(); 29 | }); 30 | 31 | run(function () { 32 | set(component, 'willChange', "text"); 33 | }); 34 | ok(RETILE_CALLED); 35 | 36 | RETILE_CALLED = false; 37 | run(function () { 38 | set(component, 'text', "Hello"); 39 | }); 40 | ok(RETILE_CALLED); 41 | 42 | RETILE_CALLED = false; 43 | run(function () { 44 | set(component, 'willChange', null); 45 | }); 46 | ok(RETILE_CALLED); 47 | 48 | RETILE_CALLED = false; 49 | run(function () { 50 | set(component, 'text', "Hello"); 51 | }); 52 | ok(!RETILE_CALLED); 53 | }); 54 | 55 | test('classNames are applied when pointer and orientation are set', function() { 56 | expect(5); 57 | 58 | var component; 59 | run(this, function () { 60 | component = this.subject({ 61 | on: "click" 62 | }); 63 | this.render(); 64 | component.show(); 65 | }); 66 | 67 | let $ = component.$(); 68 | equal($.prop('class'), 'ember-view pop-over'); 69 | 70 | run(function () { 71 | set(component, 'orientation', 'above'); 72 | }); 73 | equal($.prop('class'), 'ember-view pop-over orient-above'); 74 | 75 | run(function () { 76 | set(component, 'orientation', 'below'); 77 | set(component, 'pointer', 'center'); 78 | }); 79 | equal($.prop('class'), "ember-view pop-over orient-below pointer-center"); 80 | 81 | run(function () { 82 | set(component, 'orientation', null); 83 | set(component, 'pointer', 'left'); 84 | }); 85 | equal($.prop('class'), "ember-view pop-over pointer-left"); 86 | 87 | run(function () { 88 | set(component, 'pointer', null); 89 | }); 90 | equal($.prop('class'), "ember-view pop-over"); 91 | }); 92 | -------------------------------------------------------------------------------- /addon/system/orientation.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import Constraint from "./constraint"; 3 | 4 | const reads = Ember.computed.reads; 5 | const slice = Array.prototype.slice; 6 | const get = Ember.get; 7 | const set = Ember.set; 8 | const isArray = Ember.isArray; 9 | 10 | export default Ember.Object.extend({ 11 | 12 | init: function () { 13 | this._super(); 14 | 15 | this._constraints = Ember.A(); 16 | set(this, 'defaultConstraint', { 17 | orientation: get(this, 'orientation') 18 | }); 19 | }, 20 | 21 | orientation: null, 22 | 23 | defaultConstraint: null, 24 | 25 | constraints: reads('defaultConstraint'), 26 | 27 | andSnapTo: function (snapGuidelines) { 28 | var constraints = Ember.A(); 29 | var guideline; 30 | var orientation = get(this, 'orientation'); 31 | 32 | snapGuidelines = slice.call(arguments); 33 | 34 | for (var i = 0, len = snapGuidelines.length; i < len; i++) { 35 | guideline = snapGuidelines[i]; 36 | 37 | constraints.push( 38 | new Constraint({ 39 | orientation: orientation, 40 | behavior: 'snap', 41 | guideline: guideline 42 | }) 43 | ); 44 | } 45 | 46 | if (!isArray(get(this, 'constraints'))) { 47 | set(this, 'constraints', Ember.A()); 48 | } 49 | 50 | this._constraints.pushObjects(constraints); 51 | get(this, 'constraints').pushObjects(constraints); 52 | 53 | return this; 54 | }, 55 | 56 | andSlideBetween: function () { 57 | let constraint = new Constraint({ 58 | orientation: get(this, 'orientation'), 59 | behavior: 'slide', 60 | guideline: slice.call(arguments) 61 | }); 62 | 63 | if (!isArray(get(this, 'constraints'))) { 64 | set(this, 'constraints', Ember.A()); 65 | } 66 | 67 | this._constraints.pushObject(constraint); 68 | 69 | // Always unshift slide constraints, 70 | // since they should be handled first 71 | get(this, 'constraints').unshiftObject(constraint); 72 | 73 | return this; 74 | }, 75 | 76 | where: function (condition) { 77 | this._constraints.forEach(function (constraint) { 78 | constraint.condition = condition; 79 | }); 80 | 81 | return this; 82 | }, 83 | 84 | then: function (guideline) { 85 | if (guideline !== this) { 86 | get(this, 'constraints').pushObjects(get(guideline, 'constraints')); 87 | } 88 | 89 | return this; 90 | } 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /addon/system/rectangle.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | import { getLayout } from "dom-ruler"; 3 | 4 | const get = Ember.get; 5 | const $ = Ember.$; 6 | 7 | var Rectangle = function (x, y, width, height) { 8 | this.x = this.left = x; 9 | this.y = this.top = y; 10 | this.right = x + width; 11 | this.bottom = y + height; 12 | this.width = width; 13 | this.height = height; 14 | this.area = width * height; 15 | }; 16 | 17 | Rectangle.prototype = { 18 | intersects: function (rect) { 19 | return Rectangle.intersection(this, rect).area > 0; 20 | }, 21 | 22 | contains: function (rect) { 23 | return Rectangle.intersection(this, rect).area === rect.area; 24 | }, 25 | 26 | translateX: function (dX) { 27 | this.x = this.left = this.x + dX; 28 | this.right += dX; 29 | }, 30 | 31 | translateY: function (dY) { 32 | this.y = this.top = this.y + dY; 33 | this.bottom += dY; 34 | }, 35 | 36 | translate: function (dX, dY) { 37 | this.translateX(dX); 38 | this.translateY(dY); 39 | }, 40 | 41 | setX: function (x) { 42 | this.translateX(x - this.x); 43 | }, 44 | 45 | setY: function (y) { 46 | this.translateY(y - this.y); 47 | } 48 | }; 49 | 50 | Rectangle.intersection = function (rectA, rectB) { 51 | // Find the edges 52 | var x = Math.max(rectA.x, rectB.x); 53 | var y = Math.max(rectA.y, rectB.y); 54 | var right = Math.min(rectA.right, rectB.right); 55 | var bottom = Math.min(rectA.bottom, rectB.bottom); 56 | var width, height; 57 | 58 | if (rectA.right <= rectB.left || 59 | rectB.right <= rectA.left || 60 | rectA.bottom <= rectB.top || 61 | rectB.bottom <= rectA.top) { 62 | x = y = width = height = 0; 63 | } else { 64 | width = Math.max(0, right - x); 65 | height = Math.max(0, bottom - y); 66 | } 67 | 68 | return new Rectangle(x, y, width, height); 69 | }; 70 | 71 | Rectangle.ofView = function (view, boxModel) { 72 | return this.ofElement(get(view, 'element'), boxModel); 73 | }; 74 | 75 | Rectangle.ofElement = function (element, boxModel) { 76 | var size = getLayout(element); 77 | if (boxModel) { 78 | size = size[boxModel]; 79 | } 80 | 81 | var offset; 82 | if (element === document || element === window) { 83 | offset = { top: $(element).scrollTop(), left: $(element).scrollLeft() }; 84 | } else { 85 | offset = $(element).offset(); 86 | } 87 | 88 | return new Rectangle(offset.left, offset.top, size.width, size.height); 89 | }; 90 | 91 | export default Rectangle; 92 | -------------------------------------------------------------------------------- /addon/mixins/scroll_sandbox.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const debounce = Ember.run.debounce; 4 | const set = Ember.set; 5 | const get = Ember.get; 6 | const bind = Ember.run.bind; 7 | 8 | const on = Ember.on; 9 | 10 | // Normalize mouseWheel events 11 | function mouseWheel(evt) { 12 | let oevt = evt.originalEvent; 13 | let delta = 0; 14 | let deltaY = 0; 15 | let deltaX = 0; 16 | 17 | if (oevt.wheelDelta) { 18 | delta = oevt.wheelDelta / 120; 19 | } 20 | if (oevt.detail) { 21 | delta = oevt.detail / -3; 22 | } 23 | 24 | deltaY = delta; 25 | 26 | if (oevt.hasOwnProperty) { 27 | // Gecko 28 | if (oevt.hasOwnProperty('axis') && oevt.axis === oevt.HORIZONTAL_AXIS) { 29 | deltaY = 0; 30 | deltaX = -1 * delta; 31 | } 32 | 33 | // Webkit 34 | if (oevt.hasOwnProperty('wheelDeltaY')) { 35 | deltaY = oevt.wheelDeltaY / +120; 36 | } 37 | if (oevt.hasOwnProperty('wheelDeltaX')) { 38 | deltaX = oevt.wheelDeltaX / -120; 39 | } 40 | } 41 | 42 | evt.wheelDeltaX = deltaX; 43 | evt.wheelDeltaY = deltaY; 44 | 45 | return this.mouseWheel(evt); 46 | } 47 | 48 | /** 49 | Adding this mixin to a view will add scroll behavior that bounds 50 | the scrolling to the contents of the box. 51 | 52 | When the user has stopped scrolling and they are at an edge of the 53 | box, then it will relinquish control to the parent scroll container. 54 | 55 | This is useful when designing custom pop overs that scroll 56 | that should behave like native controls. 57 | 58 | @class ScrollSandbox 59 | @extends Ember.Mixin 60 | */ 61 | export default Ember.Mixin.create({ 62 | 63 | setupScrollHandlers: on('didInsertElement', function () { 64 | this._mouseWheelHandler = bind(this, mouseWheel); 65 | this.$().on('mousewheel DOMMouseScroll', this._mouseWheelHandler); 66 | }), 67 | 68 | scrollingHasStopped: function () { 69 | set(this, 'isScrolling', false); 70 | }, 71 | 72 | /** @private 73 | Prevent scrolling the result list from scrolling 74 | the window. 75 | */ 76 | mouseWheel: function (evt) { 77 | const $element = this.$(); 78 | const scrollTop = $element.scrollTop(); 79 | const maximumScrollTop = $element.prop('scrollHeight') - 80 | $element.outerHeight(); 81 | var isAtScrollEdge; 82 | 83 | if (evt.wheelDeltaY > 0) { 84 | isAtScrollEdge = scrollTop === 0; 85 | } else if (evt.wheelDeltaY < 0) { 86 | isAtScrollEdge = scrollTop === maximumScrollTop; 87 | } 88 | 89 | if (get(this, 'isScrolling') && isAtScrollEdge) { 90 | evt.preventDefault(); 91 | evt.stopPropagation(); 92 | } else if (!isAtScrollEdge) { 93 | set(this, 'isScrolling', true); 94 | } 95 | debounce(this, this.scrollingHasStopped, 75); 96 | }, 97 | 98 | teardownScrollHandlers: on('willDestroyElement', function () { 99 | this.$().off('mousewheel DOMMouseScroll', this._mouseWheelHandler); 100 | }) 101 | }); 102 | -------------------------------------------------------------------------------- /tests/acceptance/events-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import startApp from '../helpers/start-app'; 3 | import mouseUp from '../helpers/mouse-up'; 4 | import simpleClick from '../helpers/simple-click'; 5 | import mouseDown from '../helpers/mouse-down'; 6 | import touchStart from '../helpers/touch-start'; 7 | import touchEnd from '../helpers/touch-end'; 8 | import mouseEnter from '../helpers/mouse-enter'; 9 | import mouseLeave from '../helpers/mouse-leave'; 10 | import focus from '../helpers/focus'; 11 | import blur from '../helpers/blur'; 12 | 13 | var App; 14 | var later = Ember.run.later; 15 | 16 | module('Acceptance: Events', { 17 | setup: function() { 18 | App = startApp(); 19 | }, 20 | teardown: function() { 21 | Ember.run(App, 'destroy'); 22 | } 23 | }); 24 | 25 | test('on="click"', function() { 26 | expect(9); 27 | visit('/'); 28 | 29 | simpleClick("#click"); 30 | andThen(function () { 31 | ok(find(".pop-over-container:visible").length === 1); 32 | }); 33 | 34 | simpleClick("#click"); 35 | andThen(function () { 36 | ok(find(".pop-over-container:visible").length === 0); 37 | }); 38 | 39 | simpleClick("#click span"); 40 | andThen(function () { 41 | ok(find(".pop-over-container:visible").length === 1); 42 | }); 43 | 44 | simpleClick(".other", null, { which: 1 }); 45 | andThen(function () { 46 | ok(find(".pop-over-container:visible").length === 0); 47 | }); 48 | 49 | mouseDown("#click"); 50 | andThen(function () { 51 | ok(find(".pop-over-container:visible").length === 1); 52 | }); 53 | 54 | andThen(function () { 55 | var defer = Ember.RSVP.defer(); 56 | later(defer, 'resolve', 400); 57 | return defer.promise; 58 | }); 59 | 60 | mouseUp("#click"); 61 | andThen(function () { 62 | ok(find(".pop-over-container:visible").length === 1); 63 | }); 64 | 65 | simpleClick("#click"); 66 | andThen(function () { 67 | ok(find(".pop-over-container:visible").length === 0); 68 | }); 69 | 70 | touchStart("#click"); 71 | andThen(function () { 72 | ok(find(".pop-over-container:visible").length === 1); 73 | }); 74 | 75 | andThen(function () { 76 | var defer = Ember.RSVP.defer(); 77 | later(defer, 'resolve', 400); 78 | return defer.promise; 79 | }); 80 | 81 | touchEnd("#click"); 82 | andThen(function () { 83 | ok(find(".pop-over-container:visible").length === 1); 84 | }); 85 | 86 | 87 | }); 88 | 89 | test('on="click hold"', function() { 90 | expect(6); 91 | visit('/'); 92 | 93 | mouseDown("#click-hold"); 94 | andThen(function () { 95 | ok(find(".pop-over-container:visible").length === 1); 96 | }); 97 | 98 | andThen(function () { 99 | var defer = Ember.RSVP.defer(); 100 | later(defer, 'resolve', 400); 101 | return defer.promise; 102 | }); 103 | 104 | mouseUp("#click-hold"); 105 | andThen(function () { 106 | ok(find(".pop-over-container:visible").length === 0); 107 | }); 108 | 109 | touchStart("#click-hold"); 110 | andThen(function () { 111 | ok(find(".pop-over-container:visible").length === 1); 112 | }); 113 | 114 | andThen(function () { 115 | var defer = Ember.RSVP.defer(); 116 | later(defer, 'resolve', 400); 117 | return defer.promise; 118 | }); 119 | 120 | touchStop("#click-hold"); 121 | andThen(function () { 122 | ok(find(".pop-over-container:visible").length === 0); 123 | }); 124 | 125 | simpleClick("#click-hold"); 126 | andThen(function () { 127 | ok(find(".pop-over-container:visible").length === 1); 128 | }); 129 | 130 | simpleClick("#click-hold"); 131 | andThen(function () { 132 | ok(find(".pop-over-container:visible").length === 0); 133 | }); 134 | }); 135 | 136 | test('on="hover"', function() { 137 | expect(2); 138 | visit('/'); 139 | 140 | mouseEnter("#hover"); 141 | andThen(function () { 142 | ok(find(".pop-over-container:visible").length === 1); 143 | }); 144 | 145 | mouseLeave("#hover"); 146 | andThen(function () { 147 | ok(find(".pop-over-container:visible").length === 0); 148 | }); 149 | }); 150 | 151 | test('on="hover hold"', function() { 152 | expect(4); 153 | visit('/'); 154 | 155 | mouseEnter("#hover-hold"); 156 | andThen(function () { 157 | ok(find(".pop-over-container:visible").length === 1); 158 | }); 159 | 160 | mouseLeave("#hover-hold"); 161 | mouseEnter("#hover-hold-menu"); 162 | andThen(function () { 163 | ok(find(".pop-over-container:visible").length === 1); 164 | }); 165 | 166 | mouseEnter("#hover-hold-menu .inner"); 167 | andThen(function () { 168 | ok(find(".pop-over-container:visible").length === 1); 169 | }); 170 | 171 | mouseLeave("#hover-hold-menu"); 172 | andThen(function () { 173 | ok(find(".pop-over-container:visible").length === 0); 174 | }); 175 | }); 176 | 177 | test('on="focus"', function() { 178 | expect(2); 179 | visit('/'); 180 | 181 | focus("#focus"); 182 | andThen(function () { 183 | ok(find(".pop-over-container:visible").length === 1); 184 | }); 185 | 186 | blur("#focus"); 187 | andThen(function () { 188 | ok(find(".pop-over-container:visible").length === 0); 189 | }); 190 | }); 191 | 192 | test('on="hover focus"', function() { 193 | expect(3); 194 | visit('/'); 195 | 196 | focus("#hover-focus"); 197 | andThen(function () { 198 | ok(find(".pop-over-container:visible").length === 1); 199 | }); 200 | 201 | blur("#hover-focus"); 202 | mouseEnter("#hover-focus"); 203 | andThen(function () { 204 | ok(find(".pop-over-container:visible").length === 1); 205 | }); 206 | 207 | mouseLeave("#hover-focus"); 208 | andThen(function () { 209 | ok(find(".pop-over-container:visible").length === 0); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /addon/system/constraint.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const keys = Object.keys; 4 | const compare = Ember.compare; 5 | const mixin = Ember.mixin; 6 | 7 | function orientAbove(target, popover, pointer) { 8 | popover.setY(target.top - pointer.height - popover.height); 9 | pointer.setY(popover.height); 10 | } 11 | 12 | function orientBelow(target, popover, pointer) { 13 | popover.setY(target.bottom + pointer.height); 14 | pointer.setY(pointer.height * -1); 15 | } 16 | 17 | function orientLeft(target, popover, pointer) { 18 | popover.setX(target.left - pointer.width - popover.width); 19 | pointer.setX(popover.width); 20 | } 21 | 22 | function orientRight(target, popover, pointer) { 23 | popover.setX(target.right + pointer.width); 24 | pointer.setX(pointer.width * -1); 25 | } 26 | 27 | function horizontallyCenter(target, popover, pointer) { 28 | popover.setX(target.left + target.width / 2 - popover.width / 2); 29 | pointer.setX(popover.width / 2 - pointer.width / 2); 30 | } 31 | 32 | function verticallyCenter(target, popover, pointer) { 33 | popover.setY(target.top + target.height / 2 - popover.height / 2); 34 | pointer.setY(popover.height / 2 - pointer.height / 2); 35 | } 36 | 37 | function snapLeft(target, popover, pointer) { 38 | const offsetLeft = Math.min(target.width / 2 - (pointer.width * 1.5), 0); 39 | popover.setX(target.left + offsetLeft); 40 | pointer.setX(pointer.width); 41 | } 42 | 43 | function snapRight(target, popover, pointer) { 44 | const offsetRight = Math.min(target.width / 2 - (pointer.width * 1.5), 0); 45 | popover.setX(target.right - offsetRight - popover.width); 46 | pointer.setX(popover.width - pointer.width * 2); 47 | } 48 | 49 | function snapAbove(target, popover, pointer) { 50 | const offsetTop = Math.min(target.height / 2 - (pointer.height * 1.5), 0); 51 | popover.setY(target.top + offsetTop); 52 | pointer.setY(pointer.height); 53 | } 54 | 55 | function snapBelow(target, popover, pointer) { 56 | const offsetBottom = Math.min(target.height / 2 - (pointer.height * 1.5), 0); 57 | popover.setY(target.bottom - offsetBottom - popover.height); 58 | pointer.setY(popover.height - pointer.height * 2); 59 | } 60 | 61 | function slideHorizontally(guidelines, boundary, target, popover, pointer) { 62 | var edges = { 63 | 'left-edge': Math.min(target.width / 2 - (pointer.width * 1.5), 0), 64 | 'center': (target.width / 2 - popover.width / 2), 65 | 'right-edge': target.width - popover.width 66 | }; 67 | var range = Ember.A(guidelines).map(function (guideline) { 68 | return edges.hasOwnProperty(guideline) ? edges[guideline]: -1; 69 | }); 70 | 71 | var left = target.x + range[0]; 72 | var right = left + popover.width; 73 | 74 | range = range.sort(function (a, b) { 75 | return compare(a, b); 76 | }); 77 | var minX = target.x + range[0]; 78 | var maxX = target.x + range[1]; 79 | 80 | var padding = pointer.width; 81 | 82 | // Adjust the popover so it remains in view 83 | if (left < boundary.left ) { 84 | left = boundary.left; 85 | } else if (right > boundary.right) { 86 | left = Math.max(boundary.right - popover.width, boundary.left); 87 | } 88 | 89 | var valid = left >= minX && left <= maxX; 90 | left = Math.max(Math.min(left, maxX), minX); 91 | 92 | popover.setX(left); 93 | 94 | var dX = target.left - left; 95 | var oneThird = (edges['left-edge'] - edges['right-edge']) / 3; 96 | var pointerClassName; 97 | 98 | if (dX < oneThird) { 99 | pointer.setX(dX + Math.min(pointer.width, target.width / 2 - pointer.width * 1.5)); 100 | pointerClassName = 'left-edge'; 101 | } else if (dX < oneThird * 2) { 102 | pointer.setX(dX + target.width / 2 - pointer.width / 2); 103 | pointerClassName = 'center'; 104 | } else { 105 | pointer.setX(dX + target.width - pointer.width * 1.5); 106 | pointerClassName = 'right-edge'; 107 | } 108 | 109 | return { 110 | valid: valid, 111 | pointer: pointerClassName 112 | }; 113 | } 114 | 115 | function slideVertically(guidelines, boundary, target, popover, pointer) { 116 | var edges = { 117 | 'top-edge': Math.min(target.height / 2 - (pointer.height * 1.5), 0), 118 | 'center': (target.height / 2 - popover.height / 2), 119 | 'bottom-edge': target.height - popover.height 120 | }; 121 | var range = Ember.A(guidelines).map(function (guideline) { 122 | return edges[guideline]; 123 | }); 124 | 125 | var top = target.y + range[0]; 126 | var bottom = top + popover.height; 127 | 128 | range = range.sort(function (a, b) { 129 | return compare(a, b); 130 | }); 131 | var minY = target.y + range[0]; 132 | var maxY = target.y + range[1]; 133 | 134 | var padding = pointer.height; 135 | 136 | // Adjust the popover so it remains in view 137 | if (top < boundary.top + padding) { 138 | top = boundary.top + padding; 139 | } else if (bottom > boundary.bottom - padding) { 140 | top = boundary.bottom - popover.height - padding; 141 | } 142 | 143 | var valid = top >= minY && top <= maxY; 144 | top = Math.max(Math.min(top, maxY), minY + padding); 145 | 146 | popover.setY(top); 147 | 148 | var dY = target.top - top; 149 | var oneThird = (edges['top-edge'] - edges['bottom-edge']) / 3; 150 | var pointerClassName; 151 | 152 | if (dY < oneThird) { 153 | pointer.setY(dY + pointer.height + Math.min(target.height / 2 - (pointer.height * 1.5), 0)); 154 | pointerClassName = 'top-edge'; 155 | } else if (dY < oneThird * 2) { 156 | pointer.setY(dY + target.height / 2 - pointer.height / 2); 157 | pointerClassName = 'center'; 158 | } else { 159 | pointer.setY(dY - Math.min(target.height + (pointer.height * 1.5), 0)); 160 | pointerClassName = 'bottom-edge'; 161 | } 162 | 163 | return { 164 | valid: valid, 165 | pointer: pointerClassName 166 | }; 167 | } 168 | 169 | function Constraint(object) { 170 | keys(object).forEach(function (key) { 171 | this[key] = object[key]; 172 | }, this); 173 | } 174 | 175 | Constraint.prototype.solveFor = function (boundingRect, targetRect, popoverRect, pointerRect) { 176 | var orientation = this.orientation; 177 | var result = { 178 | orientation: orientation, 179 | valid: true 180 | }; 181 | 182 | // Orient the pane 183 | switch (orientation) { 184 | case 'above': orientAbove(targetRect, popoverRect, pointerRect); break; 185 | case 'below': orientBelow(targetRect, popoverRect, pointerRect); break; 186 | case 'left': orientLeft(targetRect, popoverRect, pointerRect); break; 187 | case 'right': orientRight(targetRect, popoverRect, pointerRect); break; 188 | } 189 | 190 | // The pane should slide in the direction specified by the flow 191 | if (this.behavior === 'slide') { 192 | switch (orientation) { 193 | case 'above': 194 | case 'below': 195 | mixin(result, slideHorizontally(this.guideline, boundingRect, targetRect, popoverRect, pointerRect)); 196 | break; 197 | case 'left': 198 | case 'right': 199 | mixin(result, slideVertically(this.guideline, boundingRect, targetRect, popoverRect, pointerRect)); 200 | break; 201 | } 202 | 203 | } else if (this.behavior === 'snap') { 204 | result.pointer = this.guideline; 205 | switch (this.guideline) { 206 | case 'center': 207 | switch (this.orientation) { 208 | case 'above': 209 | case 'below': horizontallyCenter(targetRect, popoverRect, pointerRect); break; 210 | case 'left': 211 | case 'right': verticallyCenter(targetRect, popoverRect, pointerRect); break; 212 | } 213 | break; 214 | case 'top-edge': snapAbove(targetRect, popoverRect, pointerRect); break; 215 | case 'bottom-edge': snapBelow(targetRect, popoverRect, pointerRect); break; 216 | case 'right-edge': snapRight(targetRect, popoverRect, pointerRect); break; 217 | case 'left-edge': snapLeft(targetRect, popoverRect, pointerRect); break; 218 | } 219 | } 220 | 221 | result.valid = result.valid && boundingRect.contains(popoverRect); 222 | return result; 223 | }; 224 | 225 | export default Constraint; 226 | -------------------------------------------------------------------------------- /tests/unit/system/constraint-test.js: -------------------------------------------------------------------------------- 1 | import Constraint from "ember-pop-over/system/constraint"; 2 | import Rectangle from "ember-pop-over/system/rectangle"; 3 | import { test } from "ember-qunit"; 4 | 5 | module("Constraint Solver"); 6 | 7 | test("above and below solutions (with no pointer)", function () { 8 | var bounds = new Rectangle(0, 0, 100, 100); 9 | var target = new Rectangle(45, 45, 10, 10); 10 | // Solves for above and below; not left nor right 11 | var popover = new Rectangle(0, 0, 50, 20); 12 | var pointer = new Rectangle(0, 0, 0, 0); 13 | 14 | var constraint = new Constraint({ 15 | orientation: 'above', 16 | behavior: 'snap', 17 | guideline: 'center' 18 | }); 19 | var solution = constraint.solveFor(bounds, target, popover, pointer); 20 | equal(solution.orientation, 'above'); 21 | equal(solution.pointer, 'center'); 22 | ok(solution.valid); 23 | 24 | equal(popover.x, 25); 25 | equal(popover.y, 25); 26 | 27 | 28 | constraint = new Constraint({ 29 | orientation: 'below', 30 | behavior: 'snap', 31 | guideline: 'center' 32 | }); 33 | solution = constraint.solveFor(bounds, target, popover, pointer); 34 | equal(solution.orientation, 'below'); 35 | equal(solution.pointer, 'center'); 36 | ok(solution.valid); 37 | 38 | equal(popover.x, 25); 39 | equal(popover.y, 55); 40 | }); 41 | 42 | test("vertical slide", function () { 43 | var bounds = new Rectangle(0, 0, 100, 100); 44 | var target = new Rectangle(45, 45, 10, 10); 45 | var popover = new Rectangle(0, 0, 40, 90); 46 | var pointer = new Rectangle(0, 0, 0, 0); 47 | 48 | ['right', 'left'].forEach(function (orientation) { 49 | var constraint = new Constraint({ 50 | orientation: orientation, 51 | behavior: 'slide', 52 | guideline: ['bottom-edge', 'top-edge'] 53 | }); 54 | 55 | var solution; 56 | var left = orientation === 'right' ? 55 : 5; 57 | 58 | for (var y = 0; y < 27; y++) { 59 | target = new Rectangle(45, y, 10, 10); 60 | solution = constraint.solveFor(bounds, target, popover, pointer); 61 | equal(solution.orientation, orientation); 62 | equal(solution.pointer, 'top-edge'); 63 | ok(solution.valid); 64 | 65 | equal(popover.top, 0); 66 | equal(popover.left, left); 67 | } 68 | 69 | for (; y < 54; y++) { 70 | target = new Rectangle(45, y, 10, 10); 71 | solution = constraint.solveFor(bounds, target, popover, pointer); 72 | equal(solution.orientation, orientation); 73 | equal(solution.pointer, 'center'); 74 | ok(solution.valid); 75 | 76 | equal(popover.top, 0); 77 | equal(popover.left, left); 78 | } 79 | 80 | for (; y <= 90; y++) { 81 | target = new Rectangle(45, y, 10, 10); 82 | solution = constraint.solveFor(bounds, target, popover, pointer); 83 | equal(solution.orientation, orientation); 84 | equal(solution.pointer, 'bottom-edge'); 85 | ok(solution.valid); 86 | 87 | if (y < 80) { 88 | equal(popover.top, 0); 89 | } else { 90 | equal(popover.top, y - 80); 91 | } 92 | equal(popover.left, left); 93 | } 94 | 95 | target = new Rectangle(45, y, 10, 10); 96 | solution = constraint.solveFor(bounds, target, popover, pointer); 97 | equal(solution.orientation, orientation); 98 | equal(solution.pointer, 'bottom-edge'); 99 | ok(!solution.valid); 100 | }); 101 | }); 102 | 103 | test("vertical slide from center -> bottom", function () { 104 | var bounds = new Rectangle(0, 0, 100, 100); 105 | var target = new Rectangle(45, 45, 10, 10); 106 | var popover = new Rectangle(0, 0, 40, 70); 107 | var pointer = new Rectangle(0, 0, 0, 0); 108 | 109 | ['right', 'left'].forEach(function (orientation) { 110 | var constraint = new Constraint({ 111 | orientation: orientation, 112 | behavior: 'slide', 113 | guideline: ['center', 'top-edge'] 114 | }); 115 | 116 | var solution; 117 | var left = orientation === 'right' ? 55 : 5; 118 | 119 | for (var y = 0; y < 20; y++) { 120 | target = new Rectangle(45, y, 10, 10); 121 | solution = constraint.solveFor(bounds, target, popover, pointer); 122 | equal(solution.orientation, orientation); 123 | equal(solution.pointer, 'top-edge'); 124 | ok(solution.valid); 125 | 126 | equal(popover.top, 0); 127 | equal(popover.left, left); 128 | } 129 | 130 | for (; y <= 60; y++) { 131 | target = new Rectangle(45, y, 10, 10); 132 | solution = constraint.solveFor(bounds, target, popover, pointer); 133 | equal(solution.orientation, orientation); 134 | equal(solution.pointer, 'center'); 135 | ok(solution.valid); 136 | 137 | if (y < 30) { 138 | equal(popover.top, 0); 139 | } else { 140 | equal(popover.top, y - 30); 141 | } 142 | equal(popover.left, left); 143 | } 144 | 145 | target = new Rectangle(45, y, 10, 10); 146 | solution = constraint.solveFor(bounds, target, popover, pointer); 147 | equal(solution.orientation, orientation); 148 | equal(solution.pointer, 'center'); 149 | ok(!solution.valid); 150 | 151 | equal(popover.top, 31); 152 | equal(popover.left, left); 153 | }); 154 | }); 155 | 156 | test("vertical slide from top -> center", function () { 157 | var bounds = new Rectangle(0, 0, 100, 100); 158 | var target = new Rectangle(45, 45, 10, 10); 159 | var popover = new Rectangle(0, 0, 40, 70); 160 | var pointer = new Rectangle(0, 0, 0, 0); 161 | 162 | ['right', 'left'].forEach(function (orientation) { 163 | var constraint = new Constraint({ 164 | orientation: orientation, 165 | behavior: 'slide', 166 | guideline: ['bottom-edge', 'center'] 167 | }); 168 | 169 | var left = orientation === 'right' ? 55 : 5; 170 | 171 | var y = 29; 172 | target = new Rectangle(45, y++, 10, 10); 173 | var solution = constraint.solveFor(bounds, target, popover, pointer); 174 | ok(!solution.valid); 175 | 176 | equal(popover.top, -1); 177 | equal(popover.left, left); 178 | 179 | for (y; y < 40; y++) { 180 | target = new Rectangle(45, y, 10, 10); 181 | solution = constraint.solveFor(bounds, target, popover, pointer); 182 | equal(solution.orientation, orientation); 183 | equal(solution.pointer, 'center'); 184 | ok(solution.valid); 185 | 186 | equal(popover.top, 0); 187 | equal(popover.left, left); 188 | } 189 | 190 | for (; y <= 90; y++) { 191 | target = new Rectangle(45, y, 10, 10); 192 | solution = constraint.solveFor(bounds, target, popover, pointer); 193 | equal(solution.orientation, orientation); 194 | equal(solution.pointer, 'bottom-edge'); 195 | ok(solution.valid); 196 | 197 | if (y <= 60) { 198 | equal(popover.top, 0); 199 | } else { 200 | equal(popover.top, y - 60); 201 | } 202 | equal(popover.left, left); 203 | } 204 | 205 | target = new Rectangle(45, y, 10, 10); 206 | solution = constraint.solveFor(bounds, target, popover, pointer); 207 | ok(!solution.valid); 208 | 209 | equal(popover.top, 31); 210 | equal(popover.left, left); 211 | }); 212 | }); 213 | 214 | test("vertical slide from bottom -> center", function () { 215 | var bounds = new Rectangle(0, 0, 100, 100); 216 | var target = new Rectangle(45, 45, 10, 10); 217 | var popover = new Rectangle(0, 0, 40, 70); 218 | var pointer = new Rectangle(0, 0, 0, 0); 219 | 220 | ['right', 'left'].forEach(function (orientation) { 221 | var constraint = new Constraint({ 222 | orientation: orientation, 223 | behavior: 'slide', 224 | guideline: ['top-edge', 'center'] 225 | }); 226 | 227 | var left = orientation === 'right' ? 55 : 5; 228 | 229 | var y = -1; 230 | target = new Rectangle(45, y++, 10, 10); 231 | var solution = constraint.solveFor(bounds, target, popover, pointer); 232 | ok(!solution.valid); 233 | 234 | equal(popover.top, -1); 235 | equal(popover.left, left); 236 | 237 | for (; y < 50; y++) { 238 | target = new Rectangle(45, y, 10, 10); 239 | solution = constraint.solveFor(bounds, target, popover, pointer); 240 | equal(solution.orientation, orientation); 241 | equal(solution.pointer, 'top-edge'); 242 | ok(solution.valid); 243 | 244 | if (y < 30) { 245 | equal(popover.top, y); 246 | } else { 247 | equal(popover.top, 30); 248 | } 249 | equal(popover.left, left); 250 | } 251 | 252 | for (; y <= 60; y++) { 253 | target = new Rectangle(45, y, 10, 10); 254 | solution = constraint.solveFor(bounds, target, popover, pointer); 255 | equal(solution.orientation, orientation); 256 | equal(solution.pointer, 'center'); 257 | ok(solution.valid); 258 | 259 | equal(popover.top, 30); 260 | equal(popover.left, left); 261 | } 262 | 263 | target = new Rectangle(45, y, 10, 10); 264 | solution = constraint.solveFor(bounds, target, popover, pointer); 265 | ok(!solution.valid); 266 | 267 | equal(popover.top, 31); 268 | equal(popover.left, left); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /addon/system/target.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | const keys = Object.keys; 4 | const copy = Ember.copy; 5 | const get = Ember.get; 6 | const set = Ember.set; 7 | 8 | const computed = Ember.computed; 9 | 10 | const generateGuid = Ember.generateGuid; 11 | 12 | const w = Ember.String.w; 13 | 14 | const bind = Ember.run.bind; 15 | const next = Ember.run.next; 16 | 17 | const isSimpleClick = Ember.ViewUtils.isSimpleClick; 18 | const $ = Ember.$; 19 | 20 | function includes(haystack, needle) { 21 | if (haystack.includes) { 22 | return haystack.includes(needle); 23 | } else { 24 | return haystack.contains(needle); 25 | } 26 | } 27 | 28 | function guard (fn) { 29 | return function (evt) { 30 | if (get(this, 'component.disabled')) { return; } 31 | fn.call(this, evt); 32 | }; 33 | } 34 | 35 | function getElementForTarget(target) { 36 | if (typeof target === 'string') { 37 | return document.getElementById(target); 38 | } else if (get(target, 'element')) { 39 | return get(target, 'element'); 40 | } else { 41 | return target; 42 | } 43 | } 44 | 45 | function getLabelSelector($element) { 46 | var id = $element.attr('id'); 47 | if (id) { 48 | return `label[for="${id}"]`; 49 | } 50 | } 51 | 52 | function getNearestComponentForElement(registry, element) { 53 | var $target = $(element); 54 | if (!$target.hasClass('ember-view')) { 55 | $target = $target.parents('ember-view'); 56 | } 57 | return registry[$target.attr('id')]; 58 | } 59 | 60 | function labelForEvent(evt) { 61 | var $target = $(evt.target); 62 | if ($target[0].tagName.toLowerCase() === 'label') { 63 | return $target; 64 | } else { 65 | return $target.parents('label'); 66 | } 67 | } 68 | 69 | function isLabelClicked(target, label) { 70 | if (label == null) { 71 | return false; 72 | } 73 | return $(label).attr('for') === $(target).attr('id'); 74 | } 75 | 76 | const VALID_ACTIVATORS = ["focus", "hover", "click", "hold"]; 77 | function parseActivators(value) { 78 | if (value) { 79 | var activators = value; 80 | if (typeof value === "string") { 81 | activators = Ember.A(w(value)); 82 | } 83 | Ember.assert( 84 | `${value} are not valid activators. 85 | Valid events are ${VALID_ACTIVATORS.join(', ')}`, 86 | Ember.A(copy(activators)).removeObjects(VALID_ACTIVATORS).length === 0 87 | ); 88 | return activators; 89 | } 90 | 91 | Ember.assert( 92 | `You must provide an event name to the {{pop-over}}. 93 | Valid events are ${VALID_ACTIVATORS.join(', ')}`, 94 | false 95 | ); 96 | } 97 | 98 | function poll(target, scope, fn) { 99 | if (getElementForTarget(target)) { 100 | scope[fn](); 101 | } else { 102 | next(null, poll, target, scope, fn); 103 | } 104 | } 105 | 106 | 107 | var Target = Ember.Object.extend(Ember.Evented, { 108 | 109 | init: function () { 110 | var target = get(this, 'target'); 111 | Ember.assert("You cannot make the {{pop-over}} a target of itself.", get(this, 'component') !== target); 112 | 113 | this.eventManager = { 114 | focusin: bind(this, 'focus'), 115 | focusout: bind(this, 'blur'), 116 | mouseenter: bind(this, 'mouseEnter'), 117 | mouseleave: bind(this, 'mouseLeave'), 118 | mousedown: bind(this, 'mouseDown'), 119 | touchstart: bind(this, 'mouseDown') 120 | }; 121 | 122 | if (get(target, 'element')) { 123 | this.attach(); 124 | } else if (target.one) { 125 | target.one('didInsertElement', this, 'attach'); 126 | } else if (typeof target === 'string') { 127 | poll(target, this, 'attach'); 128 | } 129 | }, 130 | 131 | attach: function () { 132 | var element = getElementForTarget(this.target); 133 | var $element = $(element); 134 | var $document = $(document); 135 | 136 | // Already attached or awaiting an element to exist 137 | if (get(this, 'attached') || element == null) { return; } 138 | 139 | set(this, 'attached', true); 140 | set(this, 'element', element); 141 | 142 | var id = $element.attr('id'); 143 | if (id == null) { 144 | id = generateGuid(); 145 | $element.attr('id', id); 146 | } 147 | 148 | var eventManager = this.eventManager; 149 | var events = keys(eventManager); 150 | var labelSelector = getLabelSelector($element); 151 | 152 | if(this.attachToDocument) { 153 | events.forEach(function (event) { 154 | $document.on(event, `#${id}`, eventManager[event]); 155 | }); 156 | 157 | if (labelSelector) { 158 | events.forEach(function (event) { 159 | $document.on(event, labelSelector, eventManager[event]); 160 | }); 161 | } 162 | } else { 163 | events.forEach(function (event) { 164 | $element.on(event, eventManager[event]); 165 | }); 166 | 167 | if (labelSelector) { 168 | events.forEach(function (event) { 169 | $(labelSelector).on(event, eventManager[event]); 170 | }); 171 | } 172 | } 173 | 174 | 175 | }, 176 | 177 | detach: function () { 178 | var element = this.element; 179 | var $element = $(element); 180 | var $document = $(document); 181 | 182 | var eventManager = this.eventManager; 183 | var events = keys(eventManager); 184 | var labelSelector = getLabelSelector($element); 185 | 186 | var id = $element.attr('id'); 187 | 188 | if(this.attachToDocument) { 189 | events.forEach(function (event) { 190 | $document.off(event, `#${id}`, eventManager[event]); 191 | }); 192 | 193 | if (labelSelector) { 194 | events.forEach(function (event) { 195 | $document.off(event, labelSelector, eventManager[event]); 196 | }); 197 | } 198 | } else { 199 | events.forEach(function (event) { 200 | $element.off(event, eventManager[event]); 201 | }); 202 | 203 | if (labelSelector) { 204 | events.forEach(function (event) { 205 | $(labelSelector).off(event, eventManager[event]); 206 | }); 207 | } 208 | } 209 | 210 | // Remove references for GC 211 | this.eventManager = null; 212 | set(this, 'element', null); 213 | set(this, 'target', null); 214 | set(this, 'component', null); 215 | }, 216 | 217 | on: computed({ 218 | set(key, value) { 219 | return parseActivators(value); 220 | } 221 | }), 222 | 223 | isClicked: function (evt) { 224 | if (isSimpleClick(evt)) { 225 | var label = labelForEvent(evt); 226 | var element = this.element; 227 | return evt.target === element || $.contains(element, evt.target) || 228 | isLabelClicked(element, label); 229 | } 230 | return false; 231 | }, 232 | 233 | active: computed('focused', 'hovered', 'pressed', 'component.hovered', 'component.pressed', { 234 | set(key, value) { 235 | var activators = get(this, 'on'); 236 | if (value) { 237 | if (includes(activators, 'focus')) { 238 | set(this, 'focused', true); 239 | } else if (includes(activators, 'hover')) { 240 | set(this, 'hovered', true); 241 | } else if (includes(activators, 'click')) { 242 | set(this, 'pressed', true); 243 | } 244 | } else { 245 | set(this, 'focused', false); 246 | set(this, 'hovered', false); 247 | set(this, 'pressed', false); 248 | } 249 | return value; 250 | }, 251 | 252 | get() { 253 | var activators = get(this, 'on'); 254 | var active = false; 255 | 256 | if (includes(activators, 'focus')) { 257 | active = active || get(this, 'focused'); 258 | if (includes(activators, 'hold')) { 259 | active = active || get(this, 'component.pressed'); 260 | } 261 | } 262 | 263 | if (includes(activators, 'hover')) { 264 | active = active || get(this, 'hovered'); 265 | if (includes(activators, 'hold')) { 266 | active = active || get(this, 'component.hovered'); 267 | } 268 | } 269 | 270 | if (includes(activators, 'click') || includes(activators, 'hold')) { 271 | active = active || get(this, 'pressed'); 272 | } 273 | 274 | return !!active; 275 | } 276 | }), 277 | 278 | focus: guard(function () { 279 | set(this, 'focused', true); 280 | }), 281 | 282 | blur: guard(function () { 283 | set(this, 'focused', false); 284 | }), 285 | 286 | mouseEnter: guard(function () { 287 | set(this, 'hovered', true); 288 | }), 289 | 290 | mouseLeave: guard(function () { 291 | set(this, 'hovered', false); 292 | }), 293 | 294 | mouseDown: guard(function (evt) { 295 | if (!this.isClicked(evt)) { 296 | return false; 297 | } 298 | 299 | var element = this.element; 300 | var active = !get(this, 'active'); 301 | set(this, 'pressed', active); 302 | 303 | if (active) { 304 | this.holdStart = new Date().getTime(); 305 | 306 | var eventManager = this.eventManager; 307 | eventManager.mouseup = bind(this, 'mouseUp'); 308 | $(document).on('mouseup touchend', eventManager.mouseup); 309 | 310 | evt.preventDefault(); 311 | } 312 | 313 | $(element).focus(); 314 | if (evt.type === 'touchstart') { 315 | // don't allow touch devices to trigger mouseDown 316 | evt.stopPropagation(); 317 | evt.preventDefault(); 318 | } 319 | return true; 320 | }), 321 | 322 | mouseUp: function (evt) { 323 | // Remove mouseup event 324 | var eventManager = this.eventManager; 325 | $(document).off('mouseup touchend', eventManager.mouseup); 326 | eventManager.mouseup = null; 327 | 328 | var label = labelForEvent(evt); 329 | 330 | // Treat clicks on