├── .bowerrc
├── .editorconfig
├── .ember-cli
├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── Brocfile.js
├── LICENSE.md
├── README.md
├── addon
├── .gitkeep
├── components
│ └── popup-menu.js
├── computed
│ ├── nearest-child.js
│ ├── nearest-parent.js
│ ├── stringify.js
│ └── w.js
├── mixins
│ └── scroll_sandbox.js
└── system
│ ├── constraint.js
│ ├── flow.js
│ ├── orientation.js
│ ├── rectangle.js
│ └── target.js
├── app
├── .gitkeep
├── components
│ └── popup-menu.js
├── initializers
│ └── popup-menu.js
├── popup-menu
│ ├── animators
│ │ ├── bounce.js
│ │ └── scale.js
│ └── flows
│ │ ├── around.js
│ │ ├── dropdown.js
│ │ ├── flip.js
│ │ └── popup.js
└── templates
│ └── components
│ └── popup-menu.hbs
├── blueprints
└── ember-popup-menu
│ └── index.js
├── bower.json
├── config
├── ember-try.js
└── environment.js
├── index.js
├── package.json
├── testem.json
├── tests
├── .jshintrc
├── acceptance
│ └── events-test.js
├── dummy
│ ├── .jshintrc
│ ├── app
│ │ ├── app.js
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ └── .gitkeep
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── index.html
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── router.js
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ ├── .gitkeep
│ │ │ └── app.css
│ │ ├── templates
│ │ │ ├── .gitkeep
│ │ │ ├── application.hbs
│ │ │ └── components
│ │ │ │ └── .gitkeep
│ │ └── views
│ │ │ └── .gitkeep
│ ├── config
│ │ └── environment.js
│ └── public
│ │ ├── .gitkeep
│ │ ├── crossdomain.xml
│ │ └── robots.txt
├── helpers
│ ├── blur.js
│ ├── focus.js
│ ├── mouse-down.js
│ ├── mouse-enter.js
│ ├── mouse-leave.js
│ ├── mouse-out.js
│ ├── mouse-up.js
│ ├── resolver.js
│ ├── simple-click.js
│ └── start-app.js
├── index.html
├── test-helper.js
└── unit
│ ├── .gitkeep
│ ├── components
│ └── popup-menu-test.js
│ └── system
│ ├── constraint-test.js
│ ├── rectangle-test.js
│ └── target-test.js
└── vendor
├── .gitkeep
└── styles
└── ember-popup-menu.css
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "analytics": false
4 | }
5 |
--------------------------------------------------------------------------------
/.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 | indent_style = space
22 | indent_size = 2
23 |
24 | [*.css]
25 | indent_style = space
26 | indent_size = 2
27 |
28 | [*.html]
29 | indent_style = space
30 | indent_size = 2
31 |
32 | [*.{diff,md}]
33 | trim_trailing_whitespace = false
34 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | *~
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | bower_components/
2 | tests/
3 | public/
4 | tmp/
5 | dist/
6 |
7 | .bowerrc
8 | .editorconfig
9 | .ember-cli
10 | .travis.yml
11 | .npmignore
12 | **/.gitkeep
13 | bower.json
14 | Brocfile.js
15 | testem.json
16 | *~
17 | *.tgz
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 |
4 | sudo: false
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | before_install:
11 | - "npm config set spin false"
12 | - "npm install -g npm@^2"
13 |
14 | install:
15 | - npm install -g bower
16 | - npm install
17 | - bower install
18 |
19 | script:
20 | - npm test
21 |
--------------------------------------------------------------------------------
/Brocfile.js:
--------------------------------------------------------------------------------
1 | /* global require, module */
2 |
3 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
4 |
5 | var app = new EmberAddon();
6 |
7 | app.import('bower_components/dom-ruler/dist/dom-ruler.amd.js', {
8 | exports: {
9 | 'dom-ruler': ['default']
10 | }
11 | });
12 |
13 | module.exports = app.toTree();
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ember {{popup-menu}}
2 |
3 | **NOTE: This is still pre-1.0 software and is subject to change.**
4 |
5 | This popup-menu provides a pluggable interface for dealing with popups around your site. It has an inteface for registering constraint behaviors and animations.
6 |
7 | For use of the popup-menu as a tooltip, the following handlebars will do the trick:
8 |
9 | ```handlebars
10 |
11 | {{#popup-menu for="help-me" on="hover"}}
12 | Hey there!
13 | {{/popup-menu}}
14 | ```
15 |
16 | ## Installation
17 |
18 | * `npm install --save-dev ember-popup-menu`
19 | * `ember g ember-popup-menu`
20 |
21 | ## Flows
22 |
23 | Flows provide a mechanism to programatically define how popups interact with the page.
24 |
25 | The API for flows is designed to provide a clear interface for understanding how a popup will react when the page is scrolled or resized.
26 |
27 | For example, a popup that always opens to the right of the target would look like:
28 |
29 | ```javascript
30 | export default function () {
31 | return this.orientRight.andSnapTo(this.center);
32 | }
33 | ```
34 |
35 | If you would like the popup to slide between the top and bottom edge, you can use the `andSlideBetween` method:
36 |
37 | ```javascript
38 | export default function () {
39 | return this.orientRight.andSlideBetween(this.topEdge, this.bottomEdge);
40 | }
41 | ```
42 |
43 | If no flow satisfies the constraints of the page, then the last flow in the cascade will be picked.
44 |
45 | Orientation | Description
46 | -------------|----------------
47 | orientAbove | Orients the popup menu above the target
48 | orientLeft | Orients the popup menu to the left of the target
49 | orientBelow | Orients the popup menu below the target
50 | orientRight | Orients the popup menu to the right of the target
51 |
52 | Behavior | Description
53 | -----------------|----------------
54 | andSnapTo | Attempts to snap to the locations given (in order)
55 | andSlideBetween | Positions the menu in between the locations given (the gravity of the menu is dictated by the positions of the parameters. For example, if the function is called `andSlideBetween(this.top, this.bottom)`, gravity will be reversed.
56 | where | A generic constraint that can pass / fail a constraint depending on the results of the function. This is hot code, and should be very straightforward.
57 |
58 | The list of possible locations for a flow depend on whether the flow is vertical or horizontal.
59 |
60 | For vertical flows (`orientLeft`, `orientRight`), the possible names are:
61 |
62 | * topEdge
63 | * center
64 | * bottomEdge
65 |
66 | For horizontal flows (`orientTop`, `orientBottom`), the possible names are:
67 |
68 | * leftEdge
69 | * center
70 | * rightEdge
71 |
72 | ## Recipes
73 |
74 | Tooltips:
75 | ```javascript
76 | import PopupMenu from "ember-popup-menu/components/popup-menu";
77 |
78 | var ToolTip = PopupMenu.extend({
79 | classNames: ['tool-tip'],
80 | layoutName: 'components/popup-menu',
81 | on: ['hover', 'focus'],
82 | flow: 'popup'
83 | });
84 |
85 | export default ToolTip;
86 | ```
87 |
88 | ```handlebars
89 |
90 | {{#tool-tip for="help-me"}}
91 | Hey there!
92 | {{/tool-tip}}
93 | ```
94 |
95 | Dropdown menu:
96 | ```javascript
97 | import PopupMenu from "ember-popup-menu/components/popup-menu";
98 |
99 | var DropDown = PopupMenu.extend({
100 | classNames: ['drop-down'],
101 | layoutName: 'components/popup-menu',
102 | on: ['hover', 'focus', 'hold'],
103 | flow: 'dropdown'
104 | });
105 |
106 | export default DropDown;
107 | ```
108 |
109 | ```handlebars
110 |
Me
111 | {{#drop-down for="current-user"}}
112 |
113 | Settings
114 | Billing
115 |
116 | {{/drop-down}}
117 | ```
118 |
119 | ## Writing your own components using {{popup-menu}}
120 |
121 | The {{popup-menu}} component is designed to be used with other components. It provides a programatic API for adding customized targets, and a set of utilities that allow for an easier and more consistent development experience when authoring these addons.
122 |
123 | Let's go through the steps of authoring a component that uses a {{popup-menu}} by making a {{date-picker}} widget. Some of the implementation details will be ignored to make this tutorial clearer to follow.
124 |
125 | First, let's bootstrap the addon:
126 |
127 | ```bash
128 | $ ember addon my-date-picker
129 | ```
130 |
131 | After this we'll add `ember-popup-menu` and `ember-moment` as a dependencies (*not* a development dependency):
132 |
133 | ```bash
134 | $ cd my-date-picker
135 | $ npm install --save ember-popup-menu
136 | $ ember g ember-popup-menu
137 | $ npm install --save ember-moment
138 | $ ember g ember-moment
139 | ```
140 |
141 | Now, we're ready to start authoring the addon. Let's first start by creating the component javascript file.
142 |
143 | ```bash
144 | $ mkdir addon/components
145 | $ touch addon/components/date-picker.js
146 | ```
147 |
148 | Using the editor of your choice, add the following bootstrap code to get started:
149 |
150 | ```javascript
151 | import Ember from "ember";
152 |
153 | var DatePicker = Ember.Component.extend({
154 | classNames: ['date-picker']
155 | });
156 |
157 | export default DatePicker;
158 | ```
159 |
160 | Let's define our public API first. This is what you will use to interface with the component in handlebars:
161 |
162 | ```javascript
163 | import Ember from "ember";
164 |
165 | var DatePicker = Ember.Component.extend({
166 | value: null,
167 | icon: null
168 | });
169 |
170 | export default DatePicker;
171 | ```
172 |
173 | `value` is the date that is picked and `icon` is an icon used to display a calendar icon.
174 |
175 | We're going to make the date picker a combination of a text field and a configurable icon, so let's start hooking them up so the popup-menu knows what will trigger events:
176 |
177 | ```javascript
178 | import Ember from "ember";
179 | import nearestChild from "ember-popup-menu/computed/nearest-child";
180 |
181 | var get = Ember.get;
182 |
183 | var DatePicker = Ember.Component.extend({
184 | classNames: ['date-picker'],
185 |
186 | value: null,
187 | icon: null,
188 |
189 | popup: nearestChild('popup-menu'),
190 |
191 | attachTargets: function () {
192 | var popup = get(this, 'popup');
193 | var icon = get(this, 'icon');
194 |
195 | popup.addTarget(icon, {
196 | on: "click"
197 | });
198 | }.on('didInsertElement')
199 | });
200 |
201 | export default DatePicker;
202 | ```
203 |
204 | Let's walk through the code.
205 |
206 | First, we imported `nearestChild`. This is a computed property that returns the nearest child of a given type. We then use this property to get the popup-menu.
207 |
208 | Then we add the icon as a target for the popup menu that will toggle the menu when clicked.
209 |
210 | For the next step, let's start showing the popup live and doing some iterative development. To do this, we'll need to start fiddling with the app directory.
211 |
212 | Create the `components` and `templates/components` directories under `app` at the root of your addon's project.
213 |
214 | The first thing to do is expose the component as a public interface by making a file called `date-picker.js` under `components`:
215 |
216 | ```javascript
217 | import DatePicker from "my-date-picker/components/date-picker";
218 | export default DatePicker;
219 | ```
220 |
221 | This simply exposes the date picker as a component consumable by the host application.
222 |
223 | Next, let's add a handlebars template for the date picker, under `templates/components/date-picker.hbs`:
224 |
225 | ```handlebars
226 | {{input type=text value=displayValue}}Open
227 | {{#popup-menu flow="dropdown" will-change="month" month=month}}
228 |
233 | {{calendar-month month=firstOfMonth}}
234 | {{/popup-menu}}
235 | ```
236 |
237 | With the template we created, we've solidified a few requirements for the component. Let's go back to `date-picker.js` in the addon directory and suss these out.
238 |
239 | First, let's automatically generate an ID for the icon. This way, the popup-menu has a unique identifier for triggering on. While we're at it, let's implement the details around `month`.
240 |
241 | ```javascript
242 | import Ember from "ember";
243 | import nearestChild from "ember-popup-menu/computed/nearest-child";
244 |
245 | var generateGuid = Ember.generateGuid;
246 |
247 | var get = Ember.get;
248 |
249 | var DatePicker = Ember.Component.extend({
250 | classNames: ['date-picker'],
251 |
252 | value: null,
253 | icon: function () {
254 | return generateGuid();
255 | }.property(),
256 |
257 | popup: nearestChild('popup-menu'),
258 |
259 | attachTargets: function () {
260 | var popup = get(this, 'popup');
261 | var icon = get(this, 'icon');
262 |
263 | popup.addTarget(icon, {
264 | on: "click"
265 | });
266 | }.on('didInsertElement'),
267 |
268 | actions: {
269 | previousMonth: function () {
270 | var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');
271 | set(this, 'month', previousMonth.month());
272 | set(this, 'year', previousMonth.year());
273 | },
274 |
275 | nextMonth: function () {
276 | var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');
277 | set(this, 'month', nextMonth.month());
278 | set(this, 'year', nextMonth.year());
279 | }
280 | },
281 |
282 | month: null,
283 | year: null,
284 |
285 | firstOfMonth: function () {
286 | return moment({ year: get(this, 'year'), month: get(this, 'month') });
287 | }.property('year', 'month')
288 |
289 | });
290 |
291 | export default DatePicker;
292 | ```
293 |
294 | As a default, let's make month be the current month *or* the month of the selected value:
295 |
296 | ```javascript
297 | import Ember from "ember";
298 | import moment from 'moment';
299 | import nearestChild from "ember-popup-menu/computed/nearest-child";
300 |
301 | var generateGuid = Ember.generateGuid;
302 |
303 | var get = Ember.get;
304 |
305 | var reads = Ember.computed.reads;
306 |
307 | var DatePicker = Ember.Component.extend({
308 | classNames: ['date-picker'],
309 |
310 | value: null,
311 | icon: function () {
312 | return generateGuid();
313 | }.property(),
314 |
315 | popup: nearestChild('popup-menu'),
316 |
317 | attachTargets: function () {
318 | var popup = get(this, 'popup');
319 | var icon = get(this, 'icon');
320 |
321 | popup.addTarget(icon, {
322 | on: "click"
323 | });
324 | }.on('didInsertElement'),
325 |
326 | actions: {
327 | previousMonth: function () {
328 | var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');
329 | set(this, 'month', previousMonth.month());
330 | set(this, 'year', previousMonth.year());
331 | },
332 |
333 | nextMonth: function () {
334 | var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');
335 | set(this, 'month', nextMonth.month());
336 | set(this, 'year', nextMonth.year());
337 | }
338 | },
339 |
340 | month: reads('currentMonth'),
341 | year: reads('currentYear'),
342 |
343 | firstOfMonth: function () {
344 | return moment({ year: get(this, 'year'), month: get(this, 'month') });
345 | }.property('year', 'month'),
346 |
347 | currentMonth: function () {
348 | return get(this, 'value') ?
349 | get(this, 'value').getMonth() :
350 | new Date().getMonth();
351 | }.property(),
352 |
353 | currentYear: function () {
354 | return get(this, 'value') ?
355 | get(this, 'value').getFullYear() :
356 | new Date().getFullYear();
357 | }.property(),
358 |
359 | displayValue: function () {
360 | var value = get(this, 'value');
361 | return value ? moment(value).format("MM/DD/YYYY") : null;
362 | }.property('value')
363 | });
364 |
365 | export default DatePicker;
366 | ```
367 |
368 | With this much, we should be able to rotate through a list of months in the calendar year. Let's test this by commenting out the `{{calendar-month}}` component:
369 |
370 | ```handlebars
371 | {{input type=text value=displayValue}}Open
372 | {{#popup-menu flow="dropdown" will-change="month" month=month}}
373 |
378 | {{!calendar-month month=firstOfMonth}}
379 | {{/popup-menu}}
380 | ```
381 |
382 | Now on to the next step! Let's implement the calendar-month component. In `calendar-month.js` in your addon, let's add code to come up with the days of the week and weeks in the given month.
383 |
384 | ```javascript
385 | import Ember from "ember";
386 | import moment from "moment";
387 |
388 | var get = Ember.get;
389 |
390 | var CalendarMonth = Ember.Component.extend({
391 | classNames: ['calendar-month'],
392 | tagName: "table",
393 |
394 | dayNames: function () {
395 | var firstWeek = get(this, 'weeks.firstObject');
396 | return firstWeek.map(function (day) {
397 | return moment(day).format("ddd");
398 | });
399 | }.property('weeks'),
400 |
401 | weeks: function () {
402 | var month = get(this, 'month');
403 | var day = month.clone().startOf('week');
404 | var weeks = [];
405 | var week = [];
406 | for (var iDay = 0; iDay < 7; iDay++) {
407 | week.push(day.clone().toDate());
408 | day.add(1, 'day');
409 | }
410 | weeks.push(week);
411 |
412 | while (day.month() === month.month()) {
413 | week = [];
414 | for (iDay = 0; iDay < 7; iDay++) {
415 | week.push(day.clone().toDate());
416 | day.add(1, 'day');
417 | }
418 | weeks.push(week);
419 | }
420 | return weeks;
421 | }.property('month')
422 | });
423 |
424 | export default CalendarMonth;
425 | ```
426 |
427 | And now let's add the template for that. First, expose the component in the app:
428 |
429 | ```javascript
430 | import CalendarMonth from "my-date-picker/components/calendar-month";
431 | export default CalendarMonth;
432 | ```
433 |
434 | And then add the template for it:
435 |
436 | ```handlebars
437 |
438 |
439 | {{#each dayOfWeek in dayNames}}
440 | {{dayOfWeek}}
441 | {{/each}}
442 |
443 |
444 |
445 | {{#each week in weeks}}
446 |
447 | {{#each day in week}}
448 | {{calendar-day value=day month=month}}
449 | {{/each}}
450 |
451 | {{/each}}
452 |
453 | ```
454 |
455 | Hmm. Looks like we have yet another component to write! Let's finish off with that one, and then pop the stack all the way back to finish off the component.
456 |
457 | ```javascript
458 | import Ember from "ember";
459 | import moment from "moment";
460 | import nearestParent from "ember-popup-menu/computed/nearest-parent";
461 |
462 | var get = Ember.get;
463 |
464 | var reads = Ember.computed.reads;
465 |
466 | var CalendarDay = Ember.Component.extend({
467 | classNames: ['calendar-day'],
468 |
469 | tagName: "td",
470 | classNameBindings: ['isSelected:selected', 'isToday', 'isDisabled:disabled'],
471 |
472 | datePicker: nearestParent('date-picker'),
473 | selection: reads('datePicker.value'),
474 |
475 | isToday: function () {
476 | return moment(get(this, 'value')).isSame(new Date(), 'day');
477 | }.property('value'),
478 |
479 | isSelected: function () {
480 | return moment(get(this, 'value')).isSame(get(this, 'selection'), 'day');
481 | }.property('value', 'selection'),
482 |
483 | isDisabled: function () {
484 | return !moment(get(this, 'value')).isSame(get(this, 'month'), 'month');
485 | }.property('value', 'month'),
486 |
487 | click: function () {
488 | if (get(this, 'isDisabled')) { return; }
489 | get(this, 'datePicker').send('selectDate', get(this, 'value'));
490 | }
491 | });
492 |
493 | export default CalendarDay;
494 | ```
495 |
496 | ```handlebars
497 | {{moment value "D"}}
498 | ```
499 |
500 | Now let's pop our stack and finish by writing a handler for `selectDate` in `date-picker.js`:
501 |
502 | ```javascript
503 | import Ember from "ember";
504 | import moment from 'moment';
505 | import nearestChild from "ember-popup-menu/computed/nearest-child";
506 |
507 | var generateGuid = Ember.generateGuid;
508 |
509 | var get = Ember.get;
510 | var set = Ember.set;
511 |
512 | var reads = Ember.computed.reads;
513 |
514 | var DatePicker = Ember.Component.extend({
515 | classNames: ['date-picker'],
516 | value: null,
517 | icon: function () {
518 | return generateGuid();
519 | }.property(),
520 |
521 | popup: nearestChild('popup-menu'),
522 |
523 | attachTargets: function () {
524 | var popup = get(this, 'popup');
525 | var icon = get(this, 'icon');
526 |
527 | popup.addTarget(icon, {
528 | on: "click"
529 | });
530 | }.on('didInsertElement'),
531 |
532 | actions: {
533 | previousMonth: function () {
534 | var previousMonth = get(this, 'firstOfMonth').clone().subtract(1, 'month');
535 | set(this, 'month', previousMonth.month());
536 | set(this, 'year', previousMonth.year());
537 | },
538 |
539 | nextMonth: function () {
540 | var nextMonth = get(this, 'firstOfMonth').clone().add(1, 'month');
541 | set(this, 'month', nextMonth.month());
542 | set(this, 'year', nextMonth.year());
543 | },
544 |
545 | selectDate: function (date) {
546 | set(this, 'value', date);
547 | get(this, 'popup').deactivate();
548 | }
549 | },
550 |
551 | month: reads('currentMonth'),
552 | year: reads('currentYear'),
553 |
554 | firstOfMonth: function () {
555 | return moment({ year: get(this, 'year'), month: get(this, 'month') });
556 | }.property('year', 'month'),
557 |
558 | currentMonth: function () {
559 | return get(this, 'value') ?
560 | get(this, 'value').getMonth() :
561 | new Date().getMonth();
562 | }.property(),
563 |
564 | currentYear: function () {
565 | return get(this, 'value') ?
566 | get(this, 'value').getFullYear() :
567 | new Date().getFullYear();
568 | }.property(),
569 |
570 | displayValue: function () {
571 | var value = get(this, 'value');
572 | return value ? moment(value).format("MM/DD/YYYY") : null;
573 | }.property('value')
574 | });
575 |
576 | export default DatePicker;
577 | ```
578 |
579 | When we deactivate the popup, we're telling it that all targets are not active anymore. That way, the popup hides.
580 |
581 | To polish it off, let's add styling. Create a file in addons called `styles/my-date-picker.css` and add the following CSS:
582 |
583 | ```css
584 | .date-picker .popup-menu {
585 | padding: 20px;
586 | }
587 |
588 | .date-picker header {
589 | height: 25px;
590 | position: relative;
591 | }
592 | .date-picker .next-month,
593 | .date-picker .previous-month {
594 | cursor: pointer;
595 | position: absolute;
596 | top: 0;
597 | }
598 |
599 | .date-picker .next-month {
600 | right: 0;
601 | }
602 |
603 | .date-picker .previous-month {
604 | left: 0;
605 | }
606 |
607 | .date-picker .month {
608 | text-align: center;
609 | }
610 |
611 | .calendar-month {
612 | border-collapse: collapse;
613 | border-spacing: 0;
614 | }
615 |
616 | .calendar-month th {
617 | font-family: sans-serif;
618 | text-transform: uppercase;
619 | font-size: 12px;
620 | height: 30px;
621 | border-bottom: 1px solid #999;
622 | border-top: 3px solid #FFF;
623 | margin-bottom: 5px;
624 | }
625 |
626 | .calendar-day {
627 | cursor: pointer;
628 | text-align: center;
629 | width: 20px;
630 | height: 20px;
631 | padding: 1px;
632 | }
633 |
634 | .calendar-day span {
635 | display: block;
636 | padding: 5px;
637 | }
638 |
639 | .calendar-day.disabled {
640 | color: #999;
641 | cursor: default;
642 | }
643 |
644 | .calendar-day.is-today span {
645 | border: 1px solid #666;
646 | }
647 |
648 | .calendar-day.selected span {
649 | border: 1px solid #FFF;
650 | }
651 | ```
652 |
653 | If everything went well, you should have a date-picker that behaves like the one here: http://paddle8.github.io/ember-popup-menu/
654 |
655 |
656 | ## Running
657 |
658 | * `ember server`
659 | * Visit your app at http://localhost:4200.
660 |
661 | ## Running Tests
662 |
663 | * `ember test`
664 | * `ember test --server`
665 |
666 | ## Building
667 |
668 | * `ember build`
669 |
670 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/).
671 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/addon/.gitkeep
--------------------------------------------------------------------------------
/addon/components/popup-menu.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 | import Target from "../system/target";
3 | import Rectangle from "../system/rectangle";
4 | import w from "../computed/w";
5 |
6 | const computed = Ember.computed;
7 | const on = Ember.on;
8 | const observer = Ember.observer;
9 |
10 | const bind = Ember.run.bind;
11 | const scheduleOnce = Ember.run.scheduleOnce;
12 | const next = Ember.run.next;
13 | const cancel = Ember.run.cancel;
14 |
15 | const get = Ember.get;
16 | const set = Ember.set;
17 | const fmt = Ember.String.fmt;
18 |
19 | const alias = Ember.computed.alias;
20 | const bool = Ember.computed.bool;
21 | const filterBy = Ember.computed.filterBy;
22 |
23 | const addObserver = Ember.addObserver;
24 | const removeObserver = Ember.removeObserver;
25 |
26 | const RSVP = Ember.RSVP;
27 |
28 | const isSimpleClick = Ember.ViewUtils.isSimpleClick;
29 | const $ = Ember.$;
30 |
31 | export default Ember.Component.extend({
32 |
33 | isVisible: false,
34 |
35 | classNames: ['popup-menu'],
36 |
37 | classNameBindings: ['orientationClassName', 'pointerClassName'],
38 |
39 | orientationClassName: computed('orientation', function () {
40 | var orientation = get(this, 'orientation');
41 | return orientation ? fmt('orient-%@', [orientation]) : null;
42 | }),
43 |
44 | pointerClassName: computed('pointer', function () {
45 | var pointer = get(this, 'pointer');
46 | return pointer ? fmt('pointer-%@', [pointer]) : null;
47 | }),
48 |
49 | disabled: false,
50 |
51 | orientation: null,
52 |
53 | pointer: null,
54 |
55 | flow: 'around',
56 |
57 | /**
58 | The target element of the popup menu.
59 | Can be a view, id, or element.
60 | */
61 | for: null,
62 |
63 | on: null,
64 |
65 | addTarget: function (target, options) {
66 | get(this, 'targets').pushObject(Target.create(options, {
67 | component: this,
68 | target: target
69 | }));
70 | },
71 |
72 | targets: computed(function() {
73 | return Ember.A();
74 | }),
75 |
76 | /**
77 | Property that notifies the popup menu to retile
78 | */
79 | 'will-change': alias('willChange'),
80 | willChange: w(),
81 |
82 | willChangeDidChange: on('init', observer('willChange', function () {
83 | (get(this, '_oldWillChange') || Ember.A()).forEach(function (key) {
84 | removeObserver(this, key, this, 'retile');
85 | }, this);
86 |
87 | get(this, 'willChange').forEach(function (key) {
88 | addObserver(this, key, this, 'retile');
89 | }, this);
90 |
91 | set(this, '_oldWillChange', get(this, 'willChange'));
92 | this.retile();
93 | })),
94 |
95 | // ..............................................
96 | // Event management
97 | //
98 |
99 | attachWindowEvents: on('didInsertElement', function () {
100 | this.retile();
101 |
102 | var retile = this.__retile = bind(this, 'retile');
103 | ['scroll', 'resize'].forEach(function (event) {
104 | $(window).on(event, retile);
105 | });
106 |
107 | addObserver(this, 'isVisible', this, 'retile');
108 | }),
109 |
110 | attachTargets: on('didInsertElement', function () {
111 | // Add implicit target
112 | if (get(this, 'for') && get(this, 'on')) {
113 | this.addTarget(get(this, 'for'), {
114 | on: get(this, 'on')
115 | });
116 | }
117 |
118 | next(this, function () {
119 | get(this, 'targets').invoke('attach');
120 | });
121 | }),
122 |
123 | removeEvents: on('willDestroyElement', function () {
124 | get(this, 'targets').invoke('detach');
125 |
126 | var retile = this.__retile;
127 | ['scroll', 'resize'].forEach(function (event) {
128 | $(window).off(event, retile);
129 | });
130 |
131 | if (this.__documentClick) {
132 | $(document).off('mousedown', this.__documentClick);
133 | this.__documentClick = null;
134 | }
135 |
136 | removeObserver(this, 'isVisible', this, 'retile');
137 | this.__retile = null;
138 | }),
139 |
140 | mouseEnter: function () {
141 | if (get(this, 'disabled')) { return; }
142 | set(this, 'hovered', true);
143 | },
144 |
145 | mouseLeave: function () {
146 | if (get(this, 'disabled')) { return; }
147 | set(this, 'hovered', false);
148 | get(this, 'targets').setEach('hovered', false);
149 | },
150 |
151 | mouseDown: function () {
152 | if (get(this, 'disabled')) { return; }
153 | set(this, 'active', true);
154 | },
155 |
156 | mouseUp: function () {
157 | if (get(this, 'disabled')) { return; }
158 | set(this, 'active', false);
159 | },
160 |
161 | documentClick: function (evt) {
162 | if (get(this, 'disabled')) { return; }
163 |
164 | set(this, 'active', false);
165 | var targets = get(this, 'targets');
166 | var element = get(this, 'element');
167 | var clicked = isSimpleClick(evt) &&
168 | (evt.target === element || $.contains(element, evt.target));
169 | var clickedAnyTarget = targets.any(function (target) {
170 | return target.isClicked(evt);
171 | });
172 |
173 | if (!clicked && !clickedAnyTarget) {
174 | targets.setEach('active', false);
175 | }
176 | },
177 |
178 | isActive: bool('activeTargets.length'),
179 |
180 | activeTargets: filterBy('targets', 'isActive', true),
181 |
182 | activeTarget: computed('activeTargets.[]', function () {
183 | if (get(this, 'isActive')) {
184 | return get(this, 'targets').findBy('anchor', true) ||
185 | get(this, 'activeTargets.firstObject');
186 | }
187 | return null;
188 | }),
189 |
190 | activate: function (target) {
191 | get(this, 'targets').findBy('target', target).set('isActive', true);
192 | },
193 |
194 | deactivate: function (target) {
195 | if (target == null) {
196 | get(this, 'targets').setEach('isActive', false);
197 | } else {
198 | get(this, 'targets').findBy('target', target).set('isActive', false);
199 | }
200 | },
201 |
202 | /**
203 | Before the menu is shown, setup click events
204 | to catch when the user clicks outside the
205 | menu.
206 | */
207 | visibilityDidChange: on('init', observer('isActive', function () {
208 | var component = this;
209 |
210 | if (this._animation) {
211 | this._animation.then(function () {
212 | component.visibilityDidChange();
213 | });
214 | }
215 |
216 | scheduleOnce('afterRender', this, 'animateMenu');
217 | })),
218 |
219 | animateMenu: function () {
220 | var component = this;
221 | var proxy = this.__documentClick = this.__documentClick || bind(this, 'documentClick');
222 | var animation = get(this, 'animation');
223 |
224 | var isActive = get(this, 'isActive');
225 | var isInactive = !isActive;
226 | var isVisible = get(this, 'isVisible');
227 | var isHidden = !isVisible;
228 |
229 | if (isActive && isHidden) {
230 | this._animation = this.show(animation).then(function () {
231 | $(document).on('mousedown', proxy);
232 | component._animation = null;
233 | });
234 |
235 | // Remove click events immediately
236 | } else if (isInactive && isVisible) {
237 | $(document).off('mousedown', proxy);
238 | this._animation = this.hide(animation).then(function () {
239 | component._animation = null;
240 | });
241 | }
242 | },
243 |
244 | hide: function (animationName) {
245 | var deferred = RSVP.defer();
246 | var component = this;
247 | var animation = this.container.lookup('popup-animation:' + animationName);
248 | this._hider = next(this, function () {
249 | if (this.isDestroyed) { return; }
250 |
251 | if (animation) {
252 | var promise = animation.out.call(this);
253 | promise.then(function () {
254 | set(component, 'isVisible', false);
255 | });
256 | deferred.resolve(promise);
257 | } else {
258 | set(component, 'isVisible', false);
259 | deferred.resolve();
260 | }
261 | });
262 | return deferred.promise;
263 | },
264 |
265 | show: function (animationName) {
266 | cancel(this._hider);
267 |
268 | var deferred = RSVP.defer();
269 | var animation = this.container.lookup('popup-animation:' + animationName);
270 | set(this, 'isVisible', true);
271 | scheduleOnce('afterRender', this, function () {
272 | if (animation) {
273 | deferred.resolve(animation['in'].call(this));
274 | } else {
275 | deferred.resolve();
276 | }
277 | });
278 |
279 | return deferred.promise;
280 | },
281 |
282 | retile: function () {
283 | if (get(this, 'isVisible')) {
284 | scheduleOnce('afterRender', this, 'tile');
285 | }
286 | },
287 |
288 | tile: function () {
289 | var target = get(this, 'activeTarget');
290 | // Don't tile if there's nothing to constrain the popup menu around
291 | if (!get(this, 'element') || !target) {
292 | return;
293 | }
294 |
295 | var $popup = this.$();
296 | var $pointer = $popup.children('.popup-menu_pointer');
297 |
298 | var boundingRect = Rectangle.ofElement(window);
299 | var popupRect = Rectangle.ofView(this, 'padding');
300 | var targetRect = Rectangle.ofElement(target.element, 'padding');
301 | var pointerRect = Rectangle.ofElement($pointer[0], 'borders');
302 |
303 | if (boundingRect.intersects(targetRect)) {
304 | var flowName = get(this, 'flow');
305 | var constraints = this.container.lookup('popup-constraint:' + flowName);
306 | Ember.assert(fmt(
307 | ("The flow named '%@1' was not registered with the {{popup-menu}}.\n" +
308 | "Register your flow by creating a file at 'app/popup-menu/flows/%@1.js' with the following function body:\n\nexport default function %@1 () {\n return this.orientBelow().andSnapTo(this.center);\n});"), [flowName]), constraints);
309 | var solution;
310 | for (var i = 0, len = constraints.length; i < len; i++) {
311 | solution = constraints[i].solveFor(boundingRect, targetRect, popupRect, pointerRect);
312 | if (solution.valid) { break; }
313 | }
314 |
315 | this.setProperties({
316 | orientation: solution.orientation,
317 | pointer: solution.pointer
318 | });
319 |
320 | var offset = $popup.offsetParent().offset();
321 | var top = popupRect.top - offset.top;
322 | var left = popupRect.left - offset.left;
323 | $popup.css({
324 | top: top + 'px',
325 | left: left + 'px'
326 | });
327 | $pointer.css({
328 | top: pointerRect.top + 'px',
329 | left: pointerRect.left + 'px'
330 | });
331 | }
332 | }
333 |
334 | });
335 |
--------------------------------------------------------------------------------
/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 |
7 | function flatten(array) {
8 | return Ember.A(array).reduce(function (a, b) {
9 | return a.concat(b);
10 | }, Ember.A());
11 | }
12 |
13 | function recursivelyFindByType(typeClass, children) {
14 | let view = children.find(function (view) {
15 | return typeClass.detectInstance(view);
16 | });
17 |
18 | if (view) {
19 | return view;
20 | }
21 |
22 | let childrenOfChildren = flatten(children.getEach('childViews'));
23 | if (childrenOfChildren.length === 0) {
24 | return null;
25 | }
26 | return recursivelyFindByType(typeClass, childrenOfChildren);
27 | }
28 |
29 | export default function(type) {
30 | var tracking = Ember.Map.create();
31 | var deleteItem;
32 | if (tracking.delete) {
33 | deleteItem = bind(tracking, 'delete');
34 | } else {
35 | deleteItem = bind(tracking, 'remove');
36 | }
37 |
38 | return computed('childViews.[]', function nearestChild(key) {
39 | var typeClass = this.container.lookupFactory('component:' + type) ||
40 | this.container.lookupFactory('view:' + type);
41 |
42 | var children = Ember.A(get(this, 'childViews'));
43 | var appendedChildren = children.filterBy('_state', 'inDOM');
44 | var detachedChildren = children.filter(function (child) {
45 | return ['inBuffer', 'hasElement', 'preRender'].indexOf(child._state) !== -1;
46 | });
47 |
48 | appendedChildren.forEach(function (child) {
49 | deleteItem(child);
50 | });
51 |
52 | var notifyChildrenChanged = bind(this, 'notifyPropertyChange', key);
53 | detachedChildren.forEach(function (child) {
54 | if (!tracking.has(child)) {
55 | child.one('didInsertElement', this, notifyChildrenChanged);
56 | tracking.set(child, true);
57 | }
58 | });
59 |
60 | return recursivelyFindByType(typeClass, appendedChildren);
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/addon/computed/nearest-parent.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 | const computed = Ember.computed;
4 |
5 | export default function(type) {
6 | return computed(function nearestParent() {
7 | var typeClass = this.container.lookupFactory('component:' + type) ||
8 | this.container.lookupFactory('view:' + type);
9 | return this.nearestOfType(typeClass);
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/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, function stringify() {
8 | return String(get(this, property));
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/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(function w(key, value) {
16 | if (arguments.length > 1) {
17 | value = toArray(value);
18 | }
19 | return Ember.A(value || toArray(defaultValue));
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/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 popup components 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 |
--------------------------------------------------------------------------------
/addon/system/constraint.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 | const keys = Ember.keys;
4 | const compare = Ember.compare;
5 | const mixin = Ember.mixin;
6 |
7 | function orientAbove(target, popup, pointer) {
8 | popup.setY(target.top - pointer.height - popup.height);
9 | pointer.setY(popup.height);
10 | }
11 |
12 | function orientBelow(target, popup, pointer) {
13 | popup.setY(target.bottom + pointer.height);
14 | pointer.setY(pointer.height * -1);
15 | }
16 |
17 | function orientLeft(target, popup, pointer) {
18 | popup.setX(target.left - pointer.width - popup.width);
19 | pointer.setX(popup.width);
20 | }
21 |
22 | function orientRight(target, popup, pointer) {
23 | popup.setX(target.right + pointer.width);
24 | pointer.setX(pointer.width * -1);
25 | }
26 |
27 | function horizontallyCenter(target, popup, pointer) {
28 | popup.setX(target.left + target.width / 2 - popup.width / 2);
29 | pointer.setX(popup.width / 2 - pointer.width / 2);
30 | }
31 |
32 | function verticallyCenter(target, popup, pointer) {
33 | popup.setY(target.top + target.height / 2 - popup.height / 2);
34 | pointer.setY(popup.height / 2 - pointer.height / 2);
35 | }
36 |
37 | function snapLeft(target, popup, pointer) {
38 | const offsetLeft = Math.min(target.width / 2 - (pointer.width * 1.5), 0);
39 | popup.setX(target.left + offsetLeft);
40 | pointer.setX(pointer.width);
41 | }
42 |
43 | function snapRight(target, popup, pointer) {
44 | const offsetRight = Math.min(target.width / 2 - (pointer.width * 1.5), 0);
45 | popup.setX(target.right - offsetRight - popup.width);
46 | pointer.setX(popup.width - pointer.width * 2);
47 | }
48 |
49 | function snapAbove(target, popup, pointer) {
50 | const offsetTop = Math.min(target.height / 2 - (pointer.height * 1.5), 0);
51 | popup.setY(target.top + offsetTop);
52 | pointer.setY(pointer.height);
53 | }
54 |
55 | function snapBelow(target, popup, pointer) {
56 | const offsetBottom = Math.min(target.height / 2 - (pointer.height * 1.5), 0);
57 | popup.setY(target.bottom - offsetBottom - popup.height);
58 | pointer.setY(popup.height - pointer.height * 2);
59 | }
60 |
61 | function slideHorizontally(guidelines, boundary, target, popup, pointer) {
62 | var edges = {
63 | 'left-edge': Math.min(target.width / 2 - (pointer.width * 1.5), 0),
64 | 'center': (target.width / 2 - popup.width / 2),
65 | 'right-edge': target.width - popup.width
66 | };
67 | var range = Ember.A(guidelines).map(function (guideline) {
68 | return edges[guideline] || [-1, -1];
69 | });
70 |
71 | var left = target.x + range[0];
72 | var right = left + popup.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 popup so it remains in view
83 | if (left < boundary.left + padding) {
84 | left = boundary.left + padding;
85 | } else if (right > boundary.right - padding) {
86 | left = boundary.right - popup.width - padding;
87 | }
88 |
89 | var valid = left >= minX && left <= maxX;
90 | left = Math.max(Math.min(left, maxX), minX);
91 |
92 | popup.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, popup, pointer) {
116 | var edges = {
117 | 'top-edge': Math.min(target.height / 2 - (pointer.height * 1.5), 0),
118 | 'center': (target.height / 2 - popup.height / 2),
119 | 'bottom-edge': target.height - popup.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 + popup.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 popup 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 - popup.height - padding;
141 | }
142 |
143 | var valid = top >= minY && top <= maxY;
144 | top = Math.max(Math.min(top, maxY), minY + padding);
145 |
146 | popup.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, popupRect, 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, popupRect, pointerRect); break;
185 | case 'below': orientBelow(targetRect, popupRect, pointerRect); break;
186 | case 'left': orientLeft(targetRect, popupRect, pointerRect); break;
187 | case 'right': orientRight(targetRect, popupRect, 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, popupRect, pointerRect));
196 | break;
197 | case 'left':
198 | case 'right':
199 | mixin(result, slideVertically(this.guideline, boundingRect, targetRect, popupRect, 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, popupRect, pointerRect); break;
210 | case 'left':
211 | case 'right': verticallyCenter(targetRect, popupRect, pointerRect); break;
212 | }
213 | break;
214 | case 'top-edge': snapAbove(targetRect, popupRect, pointerRect); break;
215 | case 'bottom-edge': snapBelow(targetRect, popupRect, pointerRect); break;
216 | case 'right-edge': snapRight(targetRect, popupRect, pointerRect); break;
217 | case 'left-edge': snapLeft(targetRect, popupRect, pointerRect); break;
218 | }
219 | }
220 |
221 | result.valid = result.valid && boundingRect.contains(popupRect);
222 | return result;
223 | };
224 |
225 | export default Constraint;
226 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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').unshiftObjects(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 | var offset = $(element).offset() || { top: $(element).scrollTop(), left: $(element).scrollLeft() };
81 |
82 | return new Rectangle(offset.left, offset.top, size.width, size.height);
83 | };
84 |
85 | export default Rectangle;
86 |
--------------------------------------------------------------------------------
/addon/system/target.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 | const keys = Ember.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 fmt = Ember.String.fmt;
13 | const w = Ember.String.w;
14 |
15 | const bind = Ember.run.bind;
16 | const next = Ember.run.next;
17 |
18 | const isSimpleClick = Ember.ViewUtils.isSimpleClick;
19 | const $ = Ember.$;
20 |
21 | function guard (fn) {
22 | return function (evt) {
23 | if (get(this, 'component.disabled')) { return; }
24 | fn.call(this, evt);
25 | };
26 | }
27 |
28 | function getElementForTarget(target) {
29 | if (Ember.View.detectInstance(target)) {
30 | return get(target, 'element');
31 | } else if (typeof target === "string") {
32 | return document.getElementById(target);
33 | } else {
34 | return target;
35 | }
36 | }
37 |
38 | function getLabelSelector($element) {
39 | var id = $element.attr('id');
40 | if (id) {
41 | return fmt("label[for='%@']", [id]);
42 | }
43 | }
44 |
45 | function getNearestViewForElement(element) {
46 | var $target = $(element);
47 | if (!$target.hasClass('ember-view')) {
48 | $target = $target.parents('ember-view');
49 | }
50 | return Ember.View.views[$target.attr('id')];
51 | }
52 |
53 | function labelForEvent(evt) {
54 | var $target = $(evt.target);
55 | if ($target[0].tagName.toLowerCase() === 'label') {
56 | return $target;
57 | } else {
58 | return $target.parents('label');
59 | }
60 | }
61 |
62 | function isLabelClicked(target, label) {
63 | if (label == null) {
64 | return false;
65 | }
66 | return $(label).attr('for') === $(target).attr('id');
67 | }
68 |
69 | const VALID_ACTIVATORS = ["focus", "hover", "click", "hold"];
70 | function parseActivators(value) {
71 | if (value) {
72 | var activators = value;
73 | if (typeof value === "string") {
74 | activators = Ember.A(w(value));
75 | }
76 | Ember.assert(
77 | fmt("%@ are not valid activators.\n" +
78 | "Valid activators are %@", [value, VALID_ACTIVATORS.join(', ')]),
79 | Ember.A(copy(activators)).removeObjects(VALID_ACTIVATORS).length === 0
80 | );
81 | return activators;
82 | }
83 |
84 | Ember.assert(
85 | fmt("You must provide an event name to the {{popup-menu}}.\n" +
86 | "Valid events are %@", [VALID_ACTIVATORS.join(', ')]),
87 | false
88 | );
89 | }
90 |
91 | function poll(target, scope, fn) {
92 | if (getElementForTarget(target)) {
93 | scope[fn]();
94 | } else {
95 | next(null, poll, target, scope, fn);
96 | }
97 | }
98 |
99 |
100 | var Target = Ember.Object.extend(Ember.Evented, {
101 |
102 | init: function () {
103 | var target = get(this, 'target');
104 | Ember.assert("You cannot make the {{popup-menu}} a target of itself.", get(this, 'component') !== target);
105 |
106 | this.eventManager = {
107 | focusin: bind(this, 'focus'),
108 | focusout: bind(this, 'blur'),
109 | mouseenter: bind(this, 'mouseEnter'),
110 | mouseleave: bind(this, 'mouseLeave'),
111 | mousedown: bind(this, 'mouseDown')
112 | };
113 |
114 | if (Ember.View.detectInstance(target)) {
115 | if (get(target, 'element')) {
116 | this.attach();
117 | } else {
118 | target.one('didInsertElement', this, 'attach');
119 | }
120 | } else if (typeof target === 'string') {
121 | poll(target, this, 'attach');
122 | }
123 | },
124 |
125 | attach: function () {
126 | var element = getElementForTarget(this.target);
127 | var $element = $(element);
128 | var $document = $(document);
129 |
130 | // Already attached or awaiting an element to exist
131 | if (get(this, 'attached') || element == null) { return; }
132 |
133 | set(this, 'attached', true);
134 | set(this, 'element', element);
135 |
136 | var id = $element.attr('id');
137 | if (id == null) {
138 | id = generateGuid();
139 | $element.attr('id', id);
140 | }
141 |
142 | var eventManager = this.eventManager;
143 |
144 | keys(eventManager).forEach(function (event) {
145 | $document.on(event, '#' + id, eventManager[event]);
146 | });
147 |
148 | var selector = getLabelSelector($element);
149 | if (selector) {
150 | keys(eventManager).forEach(function (event) {
151 | $document.on(event, selector, eventManager[event]);
152 | });
153 | }
154 | },
155 |
156 | detach: function () {
157 | var element = this.element;
158 | var $element = $(element);
159 | var $document = $(document);
160 |
161 | var eventManager = this.eventManager;
162 |
163 | var id = $element.attr('id');
164 | keys(eventManager).forEach(function (event) {
165 | $document.off(event, '#' + id, eventManager[event]);
166 | });
167 |
168 | var selector = getLabelSelector($element);
169 | if (selector) {
170 | keys(eventManager).forEach(function (event) {
171 | $document.off(event, selector, eventManager[event]);
172 | });
173 | }
174 |
175 | // Remove references for GC
176 | this.eventManager = null;
177 | set(this, 'element', null);
178 | set(this, 'target', null);
179 | set(this, 'component', null);
180 | },
181 |
182 | on: computed(function (key, value) {
183 | return parseActivators(value);
184 | }),
185 |
186 | isClicked: function (evt) {
187 | if (isSimpleClick(evt)) {
188 | var label = labelForEvent(evt);
189 | var element = this.element;
190 | return evt.target === element || $.contains(element, evt.target) ||
191 | isLabelClicked(element, label);
192 | }
193 | return false;
194 | },
195 |
196 | isActive: computed('focused', 'hovered', 'active', 'component.hovered', 'component.active', function (key, value) {
197 | var activators = get(this, 'on');
198 | // Set
199 | if (arguments.length > 1) {
200 | if (value) {
201 | if (activators.contains('focus')) {
202 | set(this, 'focused', true);
203 | } else if (activators.contains('hover')) {
204 | set(this, 'hovered', true);
205 | } else if (activators.contains('click')) {
206 | set(this, 'active', true);
207 | }
208 | } else {
209 | set(this, 'focused', false);
210 | set(this, 'hovered', false);
211 | set(this, 'active', false);
212 | }
213 | return value;
214 | }
215 |
216 | // Get
217 | var isActive = false;
218 |
219 | if (activators.contains('focus')) {
220 | isActive = isActive || get(this, 'focused');
221 | if (activators.contains('hold')) {
222 | isActive = isActive || get(this, 'component.active');
223 | }
224 | }
225 |
226 | if (activators.contains('hover')) {
227 | isActive = isActive || get(this, 'hovered');
228 | if (activators.contains('hold')) {
229 | isActive = isActive || get(this, 'component.hovered');
230 | }
231 | }
232 |
233 | if (activators.contains('click') || activators.contains('hold')) {
234 | isActive = isActive || get(this, 'active');
235 | }
236 |
237 | return !!isActive;
238 | }),
239 |
240 | focus: guard(function () {
241 | set(this, 'focused', true);
242 | }),
243 |
244 | blur: guard(function () {
245 | set(this, 'focused', false);
246 | }),
247 |
248 | mouseEnter: guard(function () {
249 | set(this, 'hovered', true);
250 | }),
251 |
252 | mouseLeave: guard(function () {
253 | set(this, 'hovered', false);
254 | }),
255 |
256 | mouseDown: guard(function (evt) {
257 | if (!this.isClicked(evt)) {
258 | return false;
259 | }
260 |
261 | var element = this.element;
262 | var isActive = !get(this, 'isActive');
263 | set(this, 'active', isActive);
264 |
265 | if (isActive) {
266 | this.holdStart = new Date().getTime();
267 |
268 | var eventManager = this.eventManager;
269 | eventManager.mouseup = bind(this, 'mouseUp');
270 | $(document).on('mouseup', eventManager.mouseup);
271 |
272 | evt.preventDefault();
273 | }
274 |
275 | $(element).focus();
276 | return true;
277 | }),
278 |
279 | mouseUp: function (evt) {
280 | // Remove mouseup event
281 | var eventManager = this.eventManager;
282 | $(document).off('mouseup', eventManager.mouseup);
283 | eventManager.mouseup = null;
284 |
285 | var label = labelForEvent(evt);
286 |
287 | // Treat clicks on elements as triggers to
288 | // open the menu
289 | if (isLabelClicked(this.element, label)) {
290 | return true;
291 | }
292 |
293 | var view = getNearestViewForElement(evt.target);
294 | var activators = get(this, 'on');
295 |
296 | // Manually trigger a click on internal elements
297 | if (view && view.nearestOfType(get(this, 'component').constructor)) {
298 | view.trigger('click');
299 |
300 | } else if (activators.contains('click') && activators.contains('hold')) {
301 | // If the user waits more than 400ms between mouseDown and mouseUp,
302 | // we can assume that they are clicking and dragging to the menu item,
303 | // and we should close the menu if they mouseup anywhere not inside
304 | // the menu.
305 | if (new Date().getTime() - this.holdStart > 400) {
306 | set(this, 'active', false);
307 | }
308 | }
309 | return true;
310 | }
311 |
312 | });
313 |
314 | export default Target;
315 |
--------------------------------------------------------------------------------
/app/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/app/.gitkeep
--------------------------------------------------------------------------------
/app/components/popup-menu.js:
--------------------------------------------------------------------------------
1 | import PopupMenu from "ember-popup-menu/components/popup-menu";
2 | export default PopupMenu;
3 |
--------------------------------------------------------------------------------
/app/initializers/popup-menu.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 | import Flow from "ember-popup-menu/system/flow";
3 | import config from "../config/environment";
4 |
5 | var get = Ember.get;
6 | var keys = Ember.keys;
7 |
8 | export var initialize = function (container) {
9 | var matcher = new RegExp(config.modulePrefix + '/popup-menu/flows/.*');
10 | var entries = keys(window.requirejs.entries);
11 |
12 | entries.filter(function (path) {
13 | return matcher.test(path);
14 | }).forEach(function (path) {
15 | var flowName = path.replace(config.modulePrefix + '/popup-menu/flows/', '');
16 | var generator = window.require(path)['default'];
17 | var constraints = get(generator.call(Flow.create()), 'constraints');
18 | container.register('popup-constraint:' + flowName, constraints, { instantiate: false });
19 | });
20 |
21 | matcher = new RegExp(config.modulePrefix + '/popup-menu/animators/.*');
22 |
23 | entries.filter(function (path) {
24 | return matcher.test(path);
25 | }).forEach(function (path) {
26 | var animationName = path.replace(config.modulePrefix + '/popup-menu/animators/', '');
27 | var animation = window.require(path)['default'];
28 |
29 | container.register('popup-animation:' + animationName, animation, { instantiate: false });
30 | });
31 | };
32 |
33 | export default {
34 | name: "register-popup-menu-extensions",
35 | initialize: initialize
36 | };
37 |
--------------------------------------------------------------------------------
/app/popup-menu/animators/bounce.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 | var RSVP = Ember.RSVP;
4 | var get = Ember.get;
5 | var scheduleOnce = Ember.run.scheduleOnce;
6 |
7 | export default {
8 | in: function () {
9 | var $element = this.$();
10 |
11 | var marginName;
12 | switch (get(this, 'orientation')) {
13 | case 'above': marginName = 'marginBottom'; break;
14 | case 'below': marginName = 'marginTop'; break;
15 | case 'left': marginName = 'marginRight'; break;
16 | case 'right': marginName = 'marginLeft'; break;
17 | default: return RSVP.resolve();
18 | }
19 |
20 | return new RSVP.Promise(function (resolve) {
21 | scheduleOnce('afterRender', function () {
22 | var css = { scale: 0.9, opacity: 0 };
23 | css[marginName] = '-10px';
24 | $element.css(css);
25 |
26 | var transition = { scale: 1, opacity: 1 };
27 | transition[marginName] = 0;
28 | $element.transition(transition, 200, 'easeOutBack', resolve);
29 | });
30 | });
31 | },
32 |
33 | out: function () {
34 | var $element = this.$();
35 | var self = this;
36 |
37 | var marginName;
38 | switch (get(this, 'orientation')) {
39 | case 'above': marginName = 'marginBottom'; break;
40 | case 'below': marginName = 'marginTop'; break;
41 | case 'left': marginName = 'marginRight'; break;
42 | case 'right': marginName = 'marginLeft'; break;
43 | default: return RSVP.resolve();
44 | }
45 |
46 | return new RSVP.Promise(function (resolve, reject) {
47 | var transition = { scale: 0.9, opacity: 0 };
48 | transition[marginName] = '-20px';
49 | $element.transition(transition, 200, 'easeInBack', function () {
50 | if (!self.isDestroyed) {
51 | $element.css(marginName, '-10px');
52 | resolve();
53 | } else {
54 | reject();
55 | }
56 | });
57 | });
58 | }
59 | };
60 |
--------------------------------------------------------------------------------
/app/popup-menu/animators/scale.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 |
3 | var RSVP = Ember.RSVP;
4 | var scheduleOnce = Ember.run.scheduleOnce;
5 |
6 | export default {
7 | in: function () {
8 | var $element = this.$();
9 |
10 | return new RSVP.Promise(function (resolve) {
11 | scheduleOnce('afterRender', function () {
12 | $element.css({ scale: 0.9, opacity: 0 });
13 | $element.transition( { scale: 1, opacity: 1 }, 100, resolve);
14 | });
15 | });
16 | },
17 |
18 | out: function () {
19 | var $element = this.$();
20 | var self = this;
21 |
22 | return new RSVP.Promise(function (resolve, reject) {
23 | $element.transition({ scale: 0.9, opacity: 0 }, 70, function () {
24 | if (!self.isDestroyed) {
25 | resolve();
26 | } else {
27 | reject();
28 | }
29 | });
30 | });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/popup-menu/flows/around.js:
--------------------------------------------------------------------------------
1 | export default function () {
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 |
--------------------------------------------------------------------------------
/app/popup-menu/flows/dropdown.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge)
3 | .then(this.orientLeft.andSnapTo(this.topEdge, this.bottomEdge))
4 | .then(this.orientRight.andSnapTo(this.topEdge))
5 | .then(this.orientBelow.andSnapTo(this.center));
6 | }
7 |
--------------------------------------------------------------------------------
/app/popup-menu/flows/flip.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return this.orientAbove.andSnapTo(this.center, this.leftEdge, this.rightEdge)
3 | .where(function (boundingRect, _, targetRect) {
4 | var centerY = targetRect.height / 2 + targetRect.y,
5 | halfway = boundingRect.height / 2;
6 | return centerY > halfway;
7 | })
8 | .then(this.orientBelow.andSnapTo(this.center, this.rightEdge, this.leftEdge)
9 | .where(function (boundingRect, _, targetRect) {
10 | var centerY = targetRect.height / 2 + targetRect.y,
11 | halfway = boundingRect.height / 2;
12 | return centerY < halfway;
13 | })
14 | )
15 | .then(this.orientAbove.andSnapTo(this.center));
16 | }
17 |
--------------------------------------------------------------------------------
/app/popup-menu/flows/popup.js:
--------------------------------------------------------------------------------
1 | export default function () {
2 | return this.orientAbove.andSnapTo(this.center, this.rightEdge, this.leftEdge, this.center);
3 | }
4 |
--------------------------------------------------------------------------------
/app/templates/components/popup-menu.hbs:
--------------------------------------------------------------------------------
1 |
2 | {{yield}}
3 |
--------------------------------------------------------------------------------
/blueprints/ember-popup-menu/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | normalizeEntityName: function() {},
3 | afterInstall: function() {
4 | return this.addBowerPackageToProject('dom-ruler#0.1.5');
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-popup-menu",
3 | "dependencies": {
4 | "ember": "1.11.1",
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": "1.0.0-beta.16.1",
8 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.4",
9 | "ember-qunit": "0.3.1",
10 | "ember-qunit-notifications": "0.0.7",
11 | "ember-resolver": "~0.1.15",
12 | "jquery": "^1.11.1",
13 | "loader.js": "ember-cli/loader.js#3.2.0",
14 | "qunit": "~1.17.1",
15 | "dom-ruler": "^0.1.5"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/config/ember-try.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | scenarios: [{
3 | name: '1.10',
4 | dependencies: {
5 | 'ember': '1.10.0'
6 | }
7 | }, {
8 | name: '1.11',
9 | dependencies: {
10 | 'ember': '1.11.3'
11 | }
12 | }, {
13 | name: 'beta',
14 | dependencies: {
15 | 'ember': 'beta'
16 | }
17 | }]
18 | };
19 |
--------------------------------------------------------------------------------
/config/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function(/* environment, appConfig */) {
4 | return { };
5 | };
6 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'ember-popup-menu',
3 | included: function (app) {
4 | this._super.included(app);
5 | app.import('bower_components/dom-ruler/dist/dom-ruler.amd.js', {
6 | exports: {
7 | 'dom-ruler': ['default']
8 | }
9 | });
10 | app.import("vendor/styles/ember-popup-menu.css");
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-popup-menu",
3 | "version": "0.1.19",
4 | "description": "A constraint-based popup-menu 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/paddle8/ember-popup-menu",
15 | "engines": {
16 | "node": ">= 0.10.0"
17 | },
18 | "author": "Tim Evans ",
19 | "license": "MIT",
20 | "devDependencies": {
21 | "broccoli-asset-rev": "^2.0.2",
22 | "ember-cli": "0.2.3",
23 | "ember-cli-app-version": "0.3.3",
24 | "ember-cli-content-security-policy": "0.4.0",
25 | "ember-cli-dependency-checker": "0.0.8",
26 | "ember-cli-htmlbars": "0.7.4",
27 | "ember-cli-ic-ajax": "0.1.1",
28 | "ember-cli-inject-live-reload": "^1.3.0",
29 | "ember-cli-qunit": "0.3.10",
30 | "ember-cli-uglify": "1.0.1",
31 | "ember-data": "1.0.0-beta.16.1",
32 | "ember-disable-prototype-extensions": "^1.0.0",
33 | "ember-export-application-global": "^1.0.2",
34 | "ember-try": "0.0.4"
35 | },
36 | "keywords": [
37 | "ember-addon"
38 | ],
39 | "dependencies": {
40 | "ember-cli-babel": "^5.0.0"
41 | },
42 | "ember-addon": {
43 | "configPath": "tests/dummy/config"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/testem.json:
--------------------------------------------------------------------------------
1 | {
2 | "framework": "qunit",
3 | "test_page": "tests/index.html?hidepassed",
4 | "launch_in_ci": [
5 | "PhantomJS"
6 | ],
7 | "launch_in_dev": [
8 | "PhantomJS",
9 | "Chrome"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/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/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 mouseEnter from '../helpers/mouse-enter';
7 | import mouseLeave from '../helpers/mouse-leave';
8 | import focus from '../helpers/focus';
9 | import blur from '../helpers/blur';
10 |
11 | var App;
12 | var later = Ember.run.later;
13 |
14 | module('Acceptance: Events', {
15 | setup: function() {
16 | App = startApp();
17 | },
18 | teardown: function() {
19 | Ember.run(App, 'destroy');
20 | }
21 | });
22 |
23 | test('on="click"', function() {
24 | expect(6);
25 | visit('/');
26 |
27 | simpleClick("#click");
28 | andThen(function () {
29 | ok(find(".popup-menu:visible").length === 1);
30 | });
31 |
32 | simpleClick("#click");
33 | andThen(function () {
34 | ok(find(".popup-menu:visible").length === 0);
35 | });
36 |
37 | simpleClick("#click span");
38 | andThen(function () {
39 | ok(find(".popup-menu:visible").length === 1);
40 | });
41 |
42 | simpleClick(".other", null, { which: 1 });
43 | andThen(function () {
44 | ok(find(".popup-menu:visible").length === 0);
45 | });
46 |
47 | mouseDown("#click");
48 | andThen(function () {
49 | ok(find(".popup-menu:visible").length === 1);
50 | });
51 |
52 | andThen(function () {
53 | var defer = Ember.RSVP.defer();
54 | later(defer, 'resolve', 400);
55 | return defer.promise;
56 | });
57 |
58 | mouseUp("#click");
59 | andThen(function () {
60 | ok(find(".popup-menu:visible").length === 1);
61 | });
62 | });
63 |
64 | test('on="click hold"', function() {
65 | expect(4);
66 | visit('/');
67 |
68 | mouseDown("#click-hold");
69 | andThen(function () {
70 | ok(find(".popup-menu:visible").length === 1);
71 | });
72 |
73 | andThen(function () {
74 | var defer = Ember.RSVP.defer();
75 | later(defer, 'resolve', 400);
76 | return defer.promise;
77 | });
78 |
79 | mouseUp("#click-hold");
80 | andThen(function () {
81 | ok(find(".popup-menu:visible").length === 0);
82 | });
83 |
84 | simpleClick("#click-hold");
85 | andThen(function () {
86 | ok(find(".popup-menu:visible").length === 1);
87 | });
88 |
89 | simpleClick("#click-hold");
90 | andThen(function () {
91 | ok(find(".popup-menu:visible").length === 0);
92 | });
93 | });
94 |
95 | test('on="hover"', function() {
96 | expect(2);
97 | visit('/');
98 |
99 | mouseEnter("#hover");
100 | andThen(function () {
101 | ok(find(".popup-menu:visible").length === 1);
102 | });
103 |
104 | mouseLeave("#hover");
105 | andThen(function () {
106 | ok(find(".popup-menu:visible").length === 0);
107 | });
108 | });
109 |
110 | test('on="hover hold"', function() {
111 | expect(4);
112 | visit('/');
113 |
114 | mouseEnter("#hover-hold");
115 | andThen(function () {
116 | ok(find(".popup-menu:visible").length === 1);
117 | });
118 |
119 | mouseLeave("#hover-hold");
120 | mouseEnter("#hover-hold-menu");
121 | andThen(function () {
122 | ok(find(".popup-menu:visible").length === 1);
123 | });
124 |
125 | mouseEnter("#hover-hold-menu .inner");
126 | andThen(function () {
127 | ok(find(".popup-menu:visible").length === 1);
128 | });
129 |
130 | mouseLeave("#hover-hold-menu");
131 | andThen(function () {
132 | ok(find(".popup-menu:visible").length === 0);
133 | });
134 | });
135 |
136 | test('on="focus"', function() {
137 | expect(2);
138 | visit('/');
139 |
140 | focus("#focus");
141 | andThen(function () {
142 | ok(find(".popup-menu:visible").length === 1);
143 | });
144 |
145 | blur("#focus");
146 | andThen(function () {
147 | ok(find(".popup-menu:visible").length === 0);
148 | });
149 | });
150 |
151 | test('on="hover focus"', function() {
152 | expect(3);
153 | visit('/');
154 |
155 | focus("#hover-focus");
156 | andThen(function () {
157 | ok(find(".popup-menu:visible").length === 1);
158 | });
159 |
160 | mouseEnter("#hover-focus");
161 | blur("#hover-focus");
162 | andThen(function () {
163 | ok(find(".popup-menu:visible").length === 1);
164 | });
165 |
166 | mouseLeave("#hover-focus");
167 | andThen(function () {
168 | ok(find(".popup-menu:visible").length === 0);
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | Ember.MODEL_FACTORY_INJECTIONS = true;
7 |
8 | var 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 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/components/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/controllers/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/helpers/.gitkeep
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/models/.gitkeep
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/routes/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/styles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/styles/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 20px;
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/templates/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Click
4 | {{#popup-menu for="click" on="click"}}
5 | I have been clicked.
6 | {{/popup-menu}}
7 |
8 | Click + Hold
9 | {{#popup-menu for="click-hold" on="click hold"}}
10 | I have been clicked / held.
11 | {{/popup-menu}}
12 |
13 | Hover
14 | {{#popup-menu for="hover" on="hover"}}
15 | I have been hovered.
16 | {{/popup-menu}}
17 |
18 | Hover + Hold
19 | {{#popup-menu id="hover-hold-menu" for="hover-hold" on="hover hold"}}
20 | I have been hovered / held.
21 | {{/popup-menu}}
22 |
23 | Focus
24 | {{#popup-menu for="focus" on="focus"}}
25 | I have been focused.
26 | {{/popup-menu}}
27 |
28 | Hover + Focus
29 | {{#popup-menu for="hover-focus" on="focus hover"}}
30 | I have been focused / hovered.
31 | {{/popup-menu}}
32 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/templates/components/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/app/views/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/app/views/.gitkeep
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/dummy/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/dummy/public/.gitkeep
--------------------------------------------------------------------------------
/tests/dummy/public/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/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-down.js:
--------------------------------------------------------------------------------
1 | export default function (selector) {
2 | triggerEvent(selector, "mousedown");
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/mouse-out.js:
--------------------------------------------------------------------------------
1 | export default function (selector) {
2 | triggerEvent(selector, "mouseout");
3 | }
4 |
--------------------------------------------------------------------------------
/tests/helpers/mouse-up.js:
--------------------------------------------------------------------------------
1 | export default function (selector) {
2 | triggerEvent(selector, "mouseup");
3 | }
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/helpers/start-app.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import Application from '../../app';
3 | import Router from '../../router';
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 | application.injectTestHelpers();
16 | });
17 |
18 | return application;
19 | }
20 |
--------------------------------------------------------------------------------
/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/test-helper.js:
--------------------------------------------------------------------------------
1 | import resolver from './helpers/resolver';
2 | import {
3 | setResolver
4 | } from 'ember-qunit';
5 |
6 | setResolver(resolver);
7 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/tests/unit/.gitkeep
--------------------------------------------------------------------------------
/tests/unit/components/popup-menu-test.js:
--------------------------------------------------------------------------------
1 | import {
2 | moduleForComponent,
3 | test
4 | } from 'ember-qunit';
5 | import Ember from "ember";
6 |
7 | var get = Ember.get;
8 | var set = Ember.set;
9 | var run = Ember.run;
10 |
11 | moduleForComponent('popup-menu', 'PopupMenuComponent');
12 |
13 | test('"retile" is called when will-change properties change', function() {
14 | expect(4);
15 |
16 | var RETILE_CALLED = false;
17 |
18 | var component;
19 | run(this, function () {
20 | component = this.subject({
21 | on: "click",
22 | retile: function () {
23 | RETILE_CALLED = true;
24 | }
25 | });
26 | this.append();
27 | });
28 |
29 | run(function () {
30 | set(component, 'willChange', "text");
31 | });
32 | ok(RETILE_CALLED);
33 |
34 | RETILE_CALLED = false;
35 | run(function () {
36 | set(component, 'text', "Hello");
37 | });
38 | ok(RETILE_CALLED);
39 |
40 | RETILE_CALLED = false;
41 | run(function () {
42 | set(component, 'willChange', null);
43 | });
44 | ok(RETILE_CALLED);
45 |
46 | RETILE_CALLED = false;
47 | run(function () {
48 | set(component, 'text', "Hello");
49 | });
50 | ok(!RETILE_CALLED);
51 | });
52 |
53 | test('classNames are applied when pointer and orientation are set', function() {
54 | expect(5);
55 |
56 | var component;
57 | run(this, function () {
58 | component = this.subject({
59 | on: "click"
60 | });
61 | this.append();
62 | });
63 |
64 | var $ = component.$();
65 | equal($.prop('class'), "ember-view popup-menu");
66 |
67 | run(function () {
68 | set(component, 'orientation', 'above');
69 | });
70 | equal($.prop('class'), "ember-view popup-menu orient-above");
71 |
72 | run(function () {
73 | set(component, 'orientation', 'below');
74 | set(component, 'pointer', 'center');
75 | });
76 | equal($.prop('class'), "ember-view popup-menu orient-below pointer-center");
77 |
78 | run(function () {
79 | set(component, 'orientation', null);
80 | set(component, 'pointer', 'left');
81 | });
82 | equal($.prop('class'), "ember-view popup-menu pointer-left");
83 |
84 | run(function () {
85 | set(component, 'pointer', null);
86 | });
87 | equal($.prop('class'), "ember-view popup-menu");
88 | });
89 |
--------------------------------------------------------------------------------
/tests/unit/system/constraint-test.js:
--------------------------------------------------------------------------------
1 | import Constraint from "ember-popup-menu/system/constraint";
2 | import Rectangle from "ember-popup-menu/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 popup = 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, popup, pointer);
20 | equal(solution.orientation, 'above');
21 | equal(solution.pointer, 'center');
22 | ok(solution.valid);
23 |
24 | equal(popup.x, 25);
25 | equal(popup.y, 25);
26 |
27 |
28 | constraint = new Constraint({
29 | orientation: 'below',
30 | behavior: 'snap',
31 | guideline: 'center'
32 | });
33 | solution = constraint.solveFor(bounds, target, popup, pointer);
34 | equal(solution.orientation, 'below');
35 | equal(solution.pointer, 'center');
36 | ok(solution.valid);
37 |
38 | equal(popup.x, 25);
39 | equal(popup.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 popup = 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, popup, pointer);
61 | equal(solution.orientation, orientation);
62 | equal(solution.pointer, 'top-edge');
63 | ok(solution.valid);
64 |
65 | equal(popup.top, 0);
66 | equal(popup.left, left);
67 | }
68 |
69 | for (; y < 54; y++) {
70 | target = new Rectangle(45, y, 10, 10);
71 | solution = constraint.solveFor(bounds, target, popup, pointer);
72 | equal(solution.orientation, orientation);
73 | equal(solution.pointer, 'center');
74 | ok(solution.valid);
75 |
76 | equal(popup.top, 0);
77 | equal(popup.left, left);
78 | }
79 |
80 | for (; y <= 90; y++) {
81 | target = new Rectangle(45, y, 10, 10);
82 | solution = constraint.solveFor(bounds, target, popup, pointer);
83 | equal(solution.orientation, orientation);
84 | equal(solution.pointer, 'bottom-edge');
85 | ok(solution.valid);
86 |
87 | if (y < 80) {
88 | equal(popup.top, 0);
89 | } else {
90 | equal(popup.top, y - 80);
91 | }
92 | equal(popup.left, left);
93 | }
94 |
95 | target = new Rectangle(45, y, 10, 10);
96 | solution = constraint.solveFor(bounds, target, popup, 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 popup = 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, popup, pointer);
122 | equal(solution.orientation, orientation);
123 | equal(solution.pointer, 'top-edge');
124 | ok(solution.valid);
125 |
126 | equal(popup.top, 0);
127 | equal(popup.left, left);
128 | }
129 |
130 | for (; y <= 60; y++) {
131 | target = new Rectangle(45, y, 10, 10);
132 | solution = constraint.solveFor(bounds, target, popup, pointer);
133 | equal(solution.orientation, orientation);
134 | equal(solution.pointer, 'center');
135 | ok(solution.valid);
136 |
137 | if (y < 30) {
138 | equal(popup.top, 0);
139 | } else {
140 | equal(popup.top, y - 30);
141 | }
142 | equal(popup.left, left);
143 | }
144 |
145 | target = new Rectangle(45, y, 10, 10);
146 | solution = constraint.solveFor(bounds, target, popup, pointer);
147 | equal(solution.orientation, orientation);
148 | equal(solution.pointer, 'center');
149 | ok(!solution.valid);
150 |
151 | equal(popup.top, 31);
152 | equal(popup.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 popup = 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, popup, pointer);
174 | ok(!solution.valid);
175 |
176 | equal(popup.top, -1);
177 | equal(popup.left, left);
178 |
179 | for (y; y < 40; y++) {
180 | target = new Rectangle(45, y, 10, 10);
181 | solution = constraint.solveFor(bounds, target, popup, pointer);
182 | equal(solution.orientation, orientation);
183 | equal(solution.pointer, 'center');
184 | ok(solution.valid);
185 |
186 | equal(popup.top, 0);
187 | equal(popup.left, left);
188 | }
189 |
190 | for (; y <= 90; y++) {
191 | target = new Rectangle(45, y, 10, 10);
192 | solution = constraint.solveFor(bounds, target, popup, pointer);
193 | equal(solution.orientation, orientation);
194 | equal(solution.pointer, 'bottom-edge');
195 | ok(solution.valid);
196 |
197 | if (y <= 60) {
198 | equal(popup.top, 0);
199 | } else {
200 | equal(popup.top, y - 60);
201 | }
202 | equal(popup.left, left);
203 | }
204 |
205 | target = new Rectangle(45, y, 10, 10);
206 | solution = constraint.solveFor(bounds, target, popup, pointer);
207 | ok(!solution.valid);
208 |
209 | equal(popup.top, 31);
210 | equal(popup.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 popup = 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, popup, pointer);
232 | ok(!solution.valid);
233 |
234 | equal(popup.top, -1);
235 | equal(popup.left, left);
236 |
237 | for (; y < 50; y++) {
238 | target = new Rectangle(45, y, 10, 10);
239 | solution = constraint.solveFor(bounds, target, popup, pointer);
240 | equal(solution.orientation, orientation);
241 | equal(solution.pointer, 'top-edge');
242 | ok(solution.valid);
243 |
244 | if (y < 30) {
245 | equal(popup.top, y);
246 | } else {
247 | equal(popup.top, 30);
248 | }
249 | equal(popup.left, left);
250 | }
251 |
252 | for (; y <= 60; y++) {
253 | target = new Rectangle(45, y, 10, 10);
254 | solution = constraint.solveFor(bounds, target, popup, pointer);
255 | equal(solution.orientation, orientation);
256 | equal(solution.pointer, 'center');
257 | ok(solution.valid);
258 |
259 | equal(popup.top, 30);
260 | equal(popup.left, left);
261 | }
262 |
263 | target = new Rectangle(45, y, 10, 10);
264 | solution = constraint.solveFor(bounds, target, popup, pointer);
265 | ok(!solution.valid);
266 |
267 | equal(popup.top, 31);
268 | equal(popup.left, left);
269 | });
270 | });
271 |
--------------------------------------------------------------------------------
/tests/unit/system/rectangle-test.js:
--------------------------------------------------------------------------------
1 | import Rectangle from "ember-popup-menu/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 |
--------------------------------------------------------------------------------
/tests/unit/system/target-test.js:
--------------------------------------------------------------------------------
1 | import Ember from "ember";
2 | import Target from "ember-popup-menu/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 view', function() {
36 | expect(1);
37 |
38 | var view = Ember.View.create();
39 | run(function () {
40 | view.appendTo("#qunit-fixture");
41 | });
42 | var target = Target.create({
43 | target: view,
44 | on: 'click'
45 | });
46 | target.attach();
47 |
48 | equal(target.element, get(view, 'element'));
49 |
50 | run(function () {
51 | view.destroy();
52 | });
53 | target.detach();
54 | });
55 |
--------------------------------------------------------------------------------
/vendor/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paddle8/ember-popup-menu/7e05039689ea3c87aaabe5fc3d11170c6977b286/vendor/.gitkeep
--------------------------------------------------------------------------------
/vendor/styles/ember-popup-menu.css:
--------------------------------------------------------------------------------
1 | .popup-menu {
2 | position: absolute;
3 | z-index: 100;
4 | background-color: #333;
5 | color: white;
6 | }
7 |
8 | .popup-menu_pointer {
9 | position: absolute;
10 | display: block;
11 | border-style: solid;
12 | border-width: 10px;
13 | height: 0;
14 | width: 0;
15 | z-index: 2;
16 | pointer-events: none;
17 | }
18 |
19 | .orient-above > .popup-menu_pointer {
20 | border-color: #333 transparent transparent transparent;
21 | bottom: -20px;
22 | }
23 |
24 | .orient-below > .popup-menu_pointer {
25 | border-color: transparent transparent #333 transparent;
26 | top: -20px;
27 | }
28 |
29 | .orient-left > .popup-menu_pointer {
30 | border-color: transparent transparent transparent #333;
31 | right: -20px;
32 | }
33 |
34 | .orient-right > .popup-menu_pointer {
35 | border-color: transparent #333 transparent transparent;
36 | left: -20px;
37 | }
38 |
--------------------------------------------------------------------------------