├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Brocfile.js ├── README.md ├── app ├── adapters │ └── application.js ├── app.js ├── components │ ├── .gitkeep │ ├── animated-card.js │ ├── animated-deck.js │ ├── content-overlay.js │ ├── slide-menu.js │ └── toggle-menu.js ├── controllers │ ├── .gitkeep │ ├── application.js │ ├── config.js │ └── news │ │ └── show.js ├── helpers │ └── .gitkeep ├── index.html ├── initializers │ ├── network-monitor-service.js │ └── offline-support.js ├── mixins │ └── gesture-listener.js ├── models │ ├── .gitkeep │ └── article.js ├── router.js ├── routes │ ├── .gitkeep │ ├── application.js │ ├── news.js │ └── news │ │ ├── index.js │ │ └── show.js ├── services │ ├── browser-detector.js │ ├── config.js │ └── network-monitor.js ├── styles │ ├── animated-deck.scss │ ├── app-index.scss │ ├── app-menu.scss │ ├── app.scss │ ├── constants.scss │ ├── defaults.scss │ ├── layout.scss │ ├── news.scss │ ├── reset.scss │ ├── utility-classes.scss │ └── utility-mixins.scss ├── templates │ ├── application.hbs │ ├── components │ │ ├── .gitkeep │ │ ├── animated-card.hbs │ │ ├── animated-deck.hbs │ │ ├── content-overlay.hbs │ │ ├── slide-menu.hbs │ │ └── toggle-menu.hbs │ ├── config.hbs │ ├── index.hbs │ ├── loading.hbs │ ├── news.hbs │ └── news │ │ ├── index.hbs │ │ └── show.hbs ├── transitions.js └── utils │ ├── bezier-easing.js │ ├── gesture.js │ └── swipe-gesture.js ├── bower.json ├── config └── environment.js ├── divshot.json ├── package.json ├── public ├── .gitkeep ├── crossdomain.xml ├── images │ ├── black-moustache.jpg │ ├── brown-moustache.jpg │ ├── cloth-pattern.png │ ├── concentric-maze.jpg │ ├── gorbipuff-coding.jpg │ ├── gorbipuff-hole.jpg │ ├── motocross.jpg │ ├── no-signal-white.svg │ ├── no-signal.svg │ ├── three-headed-monkey.jpg │ ├── thug-life-hamster.jpg │ └── trapped-in-time.jpg └── robots.txt ├── server ├── .jshintrc ├── index.js └── mocks │ └── articles.js ├── testem.json ├── tests ├── .jshintrc ├── helpers │ ├── resolver.js │ └── start-app.js ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ ├── adapters │ └── application-test.js │ ├── components │ ├── animated-card-test.js │ ├── animated-deck-test.js │ ├── content-overlay-test.js │ └── toggle-menu-test.js │ ├── controllers │ ├── application-test.js │ └── news │ │ └── show-test.js │ ├── initializers │ └── offline-support-test.js │ ├── mixins │ └── gesture-listener-test.js │ ├── models │ └── article-test.js │ ├── routes │ ├── application-test.js │ └── news-test.js │ ├── services │ ├── browser-detector-test.js │ ├── config-test.js │ └── network-monitor-test.js │ ├── utils │ ├── gesture-test.js │ └── swipe-gesture-test.js │ └── views │ └── application-test.js ├── vendor └── .gitkeep └── workers └── offline-support.js /.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 | .divshot-cache 19 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise", 6 | "KeyframeEffect", 7 | "GroupEffect", 8 | "requestAnimationFrame" 9 | ], 10 | "browser": true, 11 | "boss": true, 12 | "curly": true, 13 | "debug": false, 14 | "devel": true, 15 | "eqeqeq": true, 16 | "evil": true, 17 | "forin": false, 18 | "immed": false, 19 | "laxbreak": false, 20 | "newcap": true, 21 | "noarg": true, 22 | "noempty": false, 23 | "nonew": false, 24 | "nomen": false, 25 | "onevar": false, 26 | "plusplus": false, 27 | "regexp": false, 28 | "undef": true, 29 | "sub": true, 30 | "strict": false, 31 | "white": false, 32 | "eqnull": true, 33 | "esnext": true, 34 | "unused": true 35 | } 36 | -------------------------------------------------------------------------------- /.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 EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | var funnel = require('broccoli-funnel'); 5 | 6 | var app = new EmberApp(); 7 | 8 | app.import('bower_components/web-animations-js/web-animations-next.min.js'); 9 | app.import('bower_components/bezier-easing/bezier-easing.js'); 10 | app.import('bower_components/eventEmitter/EventEmitter.min.js'); 11 | 12 | var workers = funnel('workers'); 13 | 14 | // Use `app.import` to add additional libraries to the generated 15 | // output files. 16 | // 17 | // If you need to use different assets in different 18 | // environments, specify an object as the first parameter. That 19 | // object's keys should be the environment name and the values 20 | // should be the asset to use in that environment. 21 | // 22 | // If the library that you are including contains AMD or ES6 23 | // modules that you would like to import into your application 24 | // please specify an object with the list of modules as keys 25 | // along with the exports of each module as its value. 26 | 27 | module.exports = app.toTree(workers); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile-patterns 2 | 3 | Showcase of mobile UI patterns implemented in Ember.js 4 | 5 | This is made for fun and to serve other developers as source of inspiration for creation 6 | touch-aware components and apps. 7 | 8 | If you have any idea, request or suggestion, don't hesitate in open a issue/PR. 9 | 10 | ### Live demo 11 | 12 | You can find this app in [http://development.mobile-patterns.divshot.io/](http://development.mobile-patterns.divshot.io/) 13 | -------------------------------------------------------------------------------- /app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import ENV from 'mobile-patterns/config/environment'; 3 | 4 | export default DS.RESTAdapter.extend({ 5 | namespace: ENV.apiNamespace, 6 | ajaxSuccess: function(jqXHR, jsonPayload) { 7 | var monitor = this.get('networkMonitorService'); 8 | if (jsonPayload.meta && jsonPayload.meta.offlineSupportWorker) { 9 | monitor.registerOfflineSuccess(); 10 | } else { 11 | monitor.registerOnlineSuccess(); 12 | } 13 | return this._super(jqXHR, jsonPayload); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/components/.gitkeep -------------------------------------------------------------------------------- /app/components/animated-card.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import SwipeGesture from 'mobile-patterns/utils/swipe-gesture'; 3 | import GestureListenerMixin from 'mobile-patterns/mixins/gesture-listener'; 4 | 5 | export default Ember.Component.extend(GestureListenerMixin, { 6 | classNames: ['animated-card'], 7 | classNameBindings: ['content::placeholder'], 8 | gesture: new SwipeGesture(), 9 | gestureWarn: 'prepareAnimation', 10 | gestureProgress: 'updateAnimation', 11 | gestureEnd: 'finalizeAnimation', 12 | 13 | touchStart: function(e) { 14 | if (this.get('content.isLoaded')) { 15 | this.cardPrepared = true; 16 | this.gesture.push(e.originalEvent); 17 | } 18 | }, 19 | 20 | touchMove: function(e) { 21 | if (this.cardPrepared) { 22 | this.gesture.push(e.originalEvent); 23 | } 24 | }, 25 | 26 | touchEnd: function(e) { 27 | if (this.cardPrepared) { 28 | this.gesture.push(e.originalEvent); 29 | this.gesture.clear(); 30 | this.cardPrepared = false; 31 | } 32 | }, 33 | 34 | prepareAnimation: function() { 35 | this.sendAction('gestureWarn', this.gesture); 36 | }, 37 | 38 | updateAnimation: function() { 39 | this.sendAction('gestureProgress', this.gesture); 40 | }, 41 | 42 | finalizeAnimation: function() { 43 | this.sendAction('gestureEnd', this.gesture); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /app/components/animated-deck.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var computed = Ember.computed; 4 | 5 | export default Ember.Component.extend({ 6 | classNames: ['animated-deck'], 7 | classNameBindings: ['effectClass'], 8 | duration: 500, 9 | 10 | // CPs 11 | previous: computed.alias('current.previousArticle.content'), 12 | next: computed.alias('current.nextArticle.content'), 13 | effectClass: computed('effect', function() { 14 | this.effect = this.get('effect'); 15 | return `effect-${this.effect}`; 16 | }), 17 | 18 | // Observers 19 | resetAnimation: function() { 20 | if(this.player){ 21 | this.player.currentTime = 0; 22 | this.player.cancel(); 23 | this.set('animatingToPrevious', false); 24 | this.set('animatingToNext', false); 25 | this.element.querySelector('#current-card').scrollTop = 0; 26 | } 27 | }.observes('current'), 28 | 29 | // Initializers 30 | cacheWidth: function() { 31 | this.width = this.element.offsetWidth; 32 | }.on('didInsertElement'), 33 | 34 | cleanup: function() { 35 | if (this.player) { 36 | this.player.cancel(); 37 | } 38 | }.on('willDestroyElement'), 39 | 40 | // Functions 41 | actions: { 42 | prepareAnimation: function(gesture) { 43 | let opts = { duration: this.duration, fill: 'both' }; 44 | let currentCardkeyframes, otherCardkeyframes; 45 | if (gesture.deltaX < 0) { 46 | // Animate to next 47 | this.set('animatingToPrevious', false); 48 | this.set('animatingToNext', true); 49 | currentCardkeyframes = this.getKeyframes({card: 'current', direction: 'next'}); 50 | otherCardkeyframes = this.getKeyframes({card: 'next', direction: 'next'}); 51 | } else { 52 | // Animate to previous 53 | this.set('animatingToPrevious', true); 54 | this.set('animatingToNext', false); 55 | currentCardkeyframes = this.getKeyframes({card: 'current', direction: 'previous'}); 56 | otherCardkeyframes = this.getKeyframes({card: 'previous', direction: 'previous'}); 57 | } 58 | Ember.run.schedule('afterRender', this, function() { 59 | var group; 60 | if (this.animatingToPrevious) { 61 | group = new GroupEffect([ 62 | new KeyframeEffect(this.element.querySelector('#current-card'), currentCardkeyframes, opts), 63 | new KeyframeEffect(this.element.querySelector('#previous-card'), otherCardkeyframes, opts), 64 | ]); 65 | } else { 66 | group = new GroupEffect([ 67 | new KeyframeEffect(this.element.querySelector('#current-card'), currentCardkeyframes, opts), 68 | new KeyframeEffect(this.element.querySelector('#next-card'), otherCardkeyframes, opts), 69 | ]); 70 | } 71 | this.player = document.timeline.play(group); 72 | this.player.pause(); 73 | }); 74 | }, 75 | 76 | updateAnimation: function(gesture) { 77 | let progress = Math.abs(-gesture.deltaX + gesture.startOffset) / this.width; 78 | this.player.currentTime = progress * this.duration; 79 | }, 80 | 81 | finalizeAnimation: function(gesture) { 82 | let progress = Math.abs(-gesture.deltaX + gesture.startOffset) / this.width; 83 | let speed = Math.abs(gesture.speedX) / this.width; 84 | let target = this.get(this.animatingToPrevious ? 'previous' : 'next'); 85 | if (target && (progress > 0.5 || speed > 1)) { 86 | this.player.playbackRate = Math.max(speed, 1); 87 | this.player.onfinish = () => { 88 | this.sendAction('onChange', target); 89 | this.player.pause(); 90 | }; 91 | } else { 92 | this.player.playbackRate = -1; 93 | this.player.onfinish = () => this.player.pause(); 94 | } 95 | this.player.play(); 96 | }, 97 | }, 98 | 99 | getKeyframes: function({ card, direction }) { 100 | if (this.effect === 'slide') { 101 | if (direction === 'next') { 102 | return [{ transform: `translate(0, 0)` }, { transform: `translate(-${this.width}px, 0)` }]; 103 | } else { 104 | return [{ transform: `translate(0,0)` }, { transform: `translate(${this.width}px, 0)` }]; 105 | } 106 | } else if (this.effect === 'expose') { 107 | if (direction === 'next') { 108 | return [ 109 | { transform: `scale(1) translate(0,0)` }, 110 | { transform: `scale(0.9) translate(0,0)`, offset: 1.5/10 }, 111 | { transform: `scale(0.9) translate(-${this.width}px, 0)`, offset: 8.5/10 }, 112 | { transform: `scale(1) translate(-${this.width}px, 0)` } 113 | ]; 114 | } else { 115 | return [ 116 | { transform: `scale(1) translate(0,0)` }, 117 | { transform: `scale(0.9) translate(0,0)`, offset: 1.5/10 }, 118 | { transform: `scale(0.9) translate(${this.width}px, 0)`, offset: 8.5/10 }, 119 | { transform: `scale(1) translate(${this.width}px, 0)` } 120 | ]; 121 | } 122 | } else if (this.effect === 'stack') { 123 | if (direction === 'next') { 124 | if (card === 'current') { 125 | return [{ transform: 'scale(1)', opacity: 1 }, { transform: 'scale(0.8)', opacity: 0 }]; 126 | } else { 127 | return [{ transform: `translate(0,0)` }, { transform: `translate(-${this.width}px, 0)` }]; 128 | } 129 | } else if (direction === 'previous') { 130 | if (card === 'current') { 131 | return [{ transform: `translate(0,0)` }, { transform: `translate(${this.width}px, 0)` }]; 132 | } else { 133 | return [{ transform: 'scale(0.8)', opacity: 0 }, { transform: 'scale(1)', opacity: 1 }]; 134 | } 135 | } 136 | } 137 | } 138 | }); 139 | -------------------------------------------------------------------------------- /app/components/content-overlay.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | setupAnimation: function() { 5 | this.duration = this.get('duration'); 6 | var animation = new KeyframeEffect( 7 | this.element, 8 | [{ opacity: 0, visibility: 'hidden' }, { opacity: 1, visibility: 'visible' }], 9 | { duration: this.duration, fill: 'both' } 10 | ); 11 | this.sendAction('action', animation); 12 | }.on('didInsertElement') 13 | }); 14 | -------------------------------------------------------------------------------- /app/components/slide-menu.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Gesture from 'mobile-patterns/utils/gesture'; 3 | import BezierEasing from 'mobile-patterns/utils/bezier-easing'; 4 | 5 | export default Ember.Component.extend({ 6 | inverseEasing: new BezierEasing(0, 0.42, 1, 0.58), 7 | browserDetector: Ember.inject.service(), 8 | 9 | // Events 10 | setupAnimation: function() { 11 | this.duration = this.get('duration'); 12 | this.width = this.element.offsetWidth; 13 | var animation = new KeyframeEffect( 14 | this.element, 15 | [{ transform: 'translateX(0)' }, { transform: `translateX(${this.width}px)` }], 16 | { duration: this.duration, fill: 'both', easing: 'cubic-bezier(0.42, 0, 0.58, 1)' } 17 | ); 18 | this.sendAction('action', animation); 19 | }.on('didInsertElement'), 20 | 21 | setupEventListeners: function(){ 22 | this.width = this.element.offsetWidth; 23 | this.player = this.get('player'); 24 | if (this.get('browserDetector').isSafari) { 25 | return; 26 | } 27 | let rootNode = document.querySelector('#' + this.get('observed-element')); 28 | 29 | let handleTouchMove = evt => { 30 | this.gesture.push(evt); 31 | let newProgress = Math.min((this.gesture.x + this.offset) / this.width, 1); 32 | this.player.currentTime = this.inverseEasing(newProgress) * this.duration; 33 | }; 34 | 35 | let handleTouchEnd = () => { 36 | rootNode.removeEventListener('touchmove', handleTouchMove); 37 | rootNode.removeEventListener('touchend', handleTouchEnd); 38 | this.completeExpansion(); 39 | }; 40 | 41 | let handleTouchStart = evt => { 42 | var progress = this.player.currentTime / this.duration; 43 | this.gesture = new Gesture().push(evt); 44 | if (progress === 1 || this.gesture.initX <= 20) { 45 | this.gesture.adquire(); 46 | this.offset = Math.max(0, progress * this.width - this.gesture.initX); 47 | rootNode.addEventListener('touchmove', handleTouchMove); 48 | rootNode.addEventListener('touchend', handleTouchEnd); 49 | } 50 | }; 51 | 52 | rootNode.addEventListener('touchstart', handleTouchStart, true); 53 | }.on('didInsertElement'), 54 | 55 | completeExpansion: function(){ 56 | let progress = this.player.currentTime / this.duration; 57 | if (progress === 0 || progress === 1) { 58 | return; 59 | } 60 | 61 | let speed = this.gesture.speedX * this.duration / this.width / 1000; 62 | if (speed < -1 || speed <= 1 && progress < 0.5) { 63 | this.player.playbackRate = Math.min(speed, -1); 64 | } else { 65 | this.player.playbackRate = Math.max(speed, 1); 66 | } 67 | this.player.play(); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /app/components/toggle-menu.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | setupAnimation: function() { 5 | var opts = { duration: this.get('duration'), fill: 'both' }; 6 | var a1 = new KeyframeEffect( 7 | this.element.querySelector('#hamburger-stroke-top'), 8 | [{ transform: 'rotate(0deg) translate(0,0)' }, { transform: 'rotate(45deg) translate(7px, 5.5px)' }], 9 | opts 10 | ); 11 | var a2 = new KeyframeEffect( 12 | this.element.querySelector('#hamburger-stroke-middle'), 13 | [{ opacity: 1 }, { opacity: 0 }], 14 | opts 15 | ); 16 | var a3 = new KeyframeEffect( 17 | this.element.querySelector('#hamburger-stroke-bottom'), 18 | [{ transform: 'rotate(0deg) translate(0,0)' }, { transform: 'rotate(-45deg) translate(7px, -5.5px)' }], 19 | opts 20 | ); 21 | this.sendAction('action', new GroupEffect([a1, a2, a3])); 22 | }.on('didInsertElement'), 23 | 24 | click: function(){ 25 | var player = this.get('player'); 26 | console.log(player); 27 | if (player.playState !== 'paused'){ 28 | return; 29 | } 30 | if (player.currentTime === 0) { 31 | player.playbackRate = 1; 32 | } else { 33 | player.playbackRate = -1; 34 | } 35 | player.play(); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/controllers/.gitkeep -------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | menuAnimations: [], 5 | menuAnimationDuration: 333, 6 | 7 | actions: { 8 | setupAnimation: function(animation) { 9 | this.menuAnimations.push(animation); 10 | Ember.run.scheduleOnce('afterRender', this, this.createAnimationGroup); 11 | }, 12 | 13 | collapseMenu: function() { 14 | var player = this.get('menu-player'); 15 | if (player.currentTime !== 0 && player.playState === 'paused') { 16 | player.playbackRate = -1; 17 | player.play(); 18 | } 19 | } 20 | }, 21 | 22 | createAnimationGroup: function(){ 23 | var group = new GroupEffect(this.menuAnimations); 24 | var player = document.timeline.play(group); 25 | player.onfinish = () => player.pause(); 26 | player.pause(); 27 | this.set('menu-player', player); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /app/controllers/config.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | config: Ember.inject.service() 5 | }); 6 | -------------------------------------------------------------------------------- /app/controllers/news/show.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | sample: 'value', 5 | config: Ember.inject.service() 6 | }); 7 | -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/helpers/.gitkeep -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MobilePatterns 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | 12 | 13 | 14 | {{content-for 'head-footer'}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{content-for 'body'}} 25 | 26 | 27 | 28 | 29 | {{content-for 'body-footer'}} 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/initializers/network-monitor-service.js: -------------------------------------------------------------------------------- 1 | export function initialize(container, application) { 2 | application.inject('adapter', 'networkMonitorService', 'service:network-monitor'); 3 | application.inject('controller:application', 'networkMonitorService', 'service:network-monitor'); 4 | } 5 | 6 | export default { 7 | name: 'network-monitor-service', 8 | initialize: initialize 9 | }; 10 | -------------------------------------------------------------------------------- /app/initializers/offline-support.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'offline-support', 3 | initialize() { 4 | if ('serviceWorker' in window.navigator) { 5 | window.navigator.serviceWorker.register('/offline-support.js'); 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /app/mixins/gesture-listener.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | _addGestureListeners: function() { 5 | if (this.gestureWarn) { 6 | this._onGestureWarn = () => this[this.gestureWarn](); 7 | this.gesture.on('warn', this._onGestureWarn); 8 | } 9 | if (this.gestureStart) { 10 | this._onGestureStart = () => this[this.gestureStart](); 11 | this.gesture.on('start', this._onGestureStart); 12 | } 13 | if (this.gestureProgress) { 14 | this._onGestureProgress = () => this[this.gestureProgress](); 15 | this.gesture.on('progress', this._onGestureProgress); 16 | } 17 | if (this.gestureEnd) { 18 | this._onGestureEnd = () => this[this.gestureEnd](); 19 | this.gesture.on('end', this._onGestureEnd); 20 | } 21 | }.on('didInsertElement'), 22 | 23 | _removeGestureListeners: function() { 24 | if (this._onGestureWarn) { 25 | this.gesture.off('warn', this._onGestureWarn); 26 | } 27 | if (this._onGestureStart) { 28 | this.gesture.off('start', this._onGestureStart); 29 | } 30 | if (this.onGestureProgress) { 31 | this.gesture.off('progress', this._onGestureProgress); 32 | } 33 | if (this.onGestureEnd) { 34 | this.gesture.off('end', this._onGestureEnd); 35 | } 36 | }.on('willDestroyElement') 37 | }); 38 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/models/.gitkeep -------------------------------------------------------------------------------- /app/models/article.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | header: DS.attr('string'), 5 | body: DS.attr('string'), 6 | thumbnailUrl: DS.attr('string'), 7 | previousArticle: DS.belongsTo('article', { async: true, inverse: 'nextArticle' }), 8 | nextArticle: DS.belongsTo('article', { async: true, inverse: 'previousArticle' }) 9 | }); 10 | -------------------------------------------------------------------------------- /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 | this.resource("news", function(){ 10 | this.route("show", { path: "/:article_id" }); 11 | }); 12 | this.route("config"); 13 | }); 14 | 15 | export default Router; 16 | -------------------------------------------------------------------------------- /app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/routes/.gitkeep -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | willTransition: function() { 6 | this.controller.send('collapseMenu'); 7 | } 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /app/routes/news.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/routes/news/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model: function() { 5 | return this.store.find('article').then(articles => articles.sortBy('id')); 6 | }, 7 | 8 | actions: { 9 | show: function(article) { 10 | this.transitionTo("news.show", article); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/routes/news/show.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model: function(params) { 5 | return this.store.find('article', params.article_id); 6 | }, 7 | 8 | actions: { 9 | transitionToSibling: function(article) { 10 | if (article) { 11 | this.transitionTo('news.show', article); 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/services/browser-detector.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | get isIOS() { 5 | if (this._isIOS === undefined) { 6 | var userAgent = window.navigator.userAgent.toLowerCase(); 7 | this._isIOS = /iphone|ipod|ipad/.test(userAgent); 8 | } 9 | return this._isIOS; 10 | }, 11 | 12 | get isStandalone() { 13 | if (this._isStandalone === undefined) { 14 | this._isStandalone = window.navigator.standalone; 15 | } 16 | return this._isStandalone; 17 | }, 18 | 19 | get isSafari(){ 20 | if (this._isSafari === undefined) { 21 | var userAgent = window.navigator.userAgent.toLowerCase(); 22 | this._isSafari = this.isIOS && !this.isStandalone && /safari/.test(userAgent); 23 | } 24 | return this._isSafari; 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /app/services/config.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | deckEffects: ['slide', 'expose', 'stack'], 5 | deckEffect: Ember.computed({ 6 | get: function() { 7 | return (window.localStorage && window.localStorage.deckEffect) || this.deckEffects[0]; 8 | }, 9 | set: function(key, value) { 10 | if (window.localStorage) { 11 | window.localStorage.deckEffect = value; 12 | } 13 | return value; 14 | } 15 | }), 16 | }); 17 | -------------------------------------------------------------------------------- /app/services/network-monitor.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | offline: false, 5 | 6 | registerOfflineSuccess() { 7 | this.set('offline', true); 8 | }, 9 | 10 | registerOnlineSuccess() { 11 | this.set('offline', false); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/styles/animated-deck.scss: -------------------------------------------------------------------------------- 1 | @import 'utility-mixins'; 2 | @import 'constants'; 3 | 4 | .animated-deck { 5 | background: #555 url(/images/cloth-pattern.png); 6 | height: calc(100vh - #{$app-header-height}); 7 | position: relative; 8 | overflow-x: hidden; 9 | } 10 | 11 | .animated-card { 12 | background-color: #eee; 13 | padding: 10px; 14 | height: calc(100vh - #{$app-header-height}); 15 | width: 100%; 16 | overflow-y: scroll; 17 | &.placeholder { display: none; } 18 | .effect-expose &, .effect-slide & { 19 | position: absolute; 20 | will-change: transform; 21 | &#next-card { left: 100%; } 22 | &#previous-card { left: -100%; } 23 | } 24 | .effect-stack & { 25 | will-change: transform; 26 | position: absolute; 27 | &#next-card { left: 100%; } 28 | } 29 | .card-loading-icon { 30 | margin: 50% auto; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/styles/app-index.scss: -------------------------------------------------------------------------------- 1 | $default-padding: 10px; 2 | 3 | #app-index { 4 | padding: $default-padding; 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/app-menu.scss: -------------------------------------------------------------------------------- 1 | $menu-width: 320px; 2 | #app-menu { 3 | background-color: #555; 4 | color: white; 5 | position: fixed; 6 | width: $menu-width; 7 | height: 100%; 8 | left: -$menu-width; 9 | will-change: transform; 10 | z-index: 100; 11 | a.active, a:active { 12 | background-color: #666; 13 | } 14 | } 15 | .menu-item { 16 | padding: 0.3333rem; 17 | display: block; 18 | line-height: 250%; 19 | } 20 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | @import 'defaults'; 3 | @import 'utility-classes'; 4 | @import 'layout'; 5 | @import 'app-menu'; 6 | @import 'app-index'; 7 | @import 'animated-deck'; 8 | @import 'news'; 9 | -------------------------------------------------------------------------------- /app/styles/constants.scss: -------------------------------------------------------------------------------- 1 | // Constants 2 | $app-header-height: 3rem; 3 | -------------------------------------------------------------------------------- /app/styles/defaults.scss: -------------------------------------------------------------------------------- 1 | // Base (some styles copied from Foundation) 2 | 3 | html, body { 4 | height: 100%; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | box-sizing: border-box; 13 | } 14 | 15 | html, 16 | body { 17 | font-size: 100%; 18 | } 19 | 20 | body { 21 | background: #fff; 22 | color: #222; 23 | padding: 0; 24 | margin: 0; 25 | font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; 26 | font-weight: normal; 27 | font-style: normal; 28 | line-height: 1.5; 29 | position: relative; 30 | cursor: auto; } 31 | 32 | a:hover { 33 | cursor: pointer; } 34 | 35 | img { 36 | max-width: 100%; 37 | height: auto; 38 | -ms-interpolation-mode: bicubic; 39 | } 40 | 41 | // Headers 42 | 43 | h1, h2, h3, h4, h5, h6 { 44 | text-rendering: optimizeLegibility; 45 | margin-top: 0.2rem; 46 | margin-bottom: 0.5rem; 47 | line-height: 1.4; 48 | small { 49 | font-size: 60%; 50 | color: #6f6f6f; 51 | line-height: 0; 52 | } 53 | } 54 | 55 | h1 { font-size: 2.125rem; } 56 | h2 { font-size: 1.6875rem; } 57 | h3 { font-size: 1.375rem; } 58 | h4 { font-size: 1.125rem; } 59 | h5 { font-size: 1.125rem; } 60 | h6 { font-size: 1rem; } 61 | 62 | // Anchors 63 | a { 64 | color: inherit; 65 | text-decoration: none; 66 | } 67 | 68 | // Paragraphs 69 | 70 | p { 71 | font-family: inherit; 72 | font-weight: normal; 73 | font-size: 1rem; 74 | line-height: 1.6; 75 | margin-bottom: 1.11111rem; 76 | text-rendering: optimizeLegibility; 77 | } 78 | 79 | // Buttons 80 | button { 81 | border: none; 82 | background-color: transparent; 83 | } 84 | 85 | -------------------------------------------------------------------------------- /app/styles/layout.scss: -------------------------------------------------------------------------------- 1 | @import 'constants'; 2 | 3 | // App container 4 | #mobile-patterns { 5 | // No styles yet 6 | } 7 | 8 | // App header 9 | #app-header { 10 | background-color: red; 11 | color: white; 12 | height: $app-header-height; 13 | display: flex; 14 | > * { flex: 1; } 15 | > .signal-icon { 16 | max-width: 2rem; 17 | margin-right: 0.5rem; 18 | } 19 | } 20 | 21 | #app-title { 22 | line-height: 2.65rem; 23 | padding: 0 0.3rem; 24 | } 25 | 26 | #toggle-main-menu { 27 | max-width: $app-header-height; 28 | padding: 8px; 29 | cursor: pointer; 30 | 31 | .stroke { 32 | width: 100%; 33 | height: 5px; 34 | background-color: white; 35 | margin: 4px 0; 36 | will-change: transform; 37 | } 38 | } 39 | 40 | // App content 41 | #app-content { 42 | min-height: calc(100vh - #{$app-header-height}); 43 | } 44 | 45 | // Content overlay 46 | #content-overlay { 47 | background-color: rgba(black, 0.4); 48 | position: fixed; 49 | left: 0; 50 | top: $app-header-height; 51 | width: 100%; 52 | height: 100%; 53 | z-index: 99; 54 | opacity: 0; 55 | visibility: hidden; 56 | will-change: opacity; 57 | } 58 | 59 | // Loading icon 60 | .loading-icon { 61 | position: fixed; 62 | left: calc(50% - 25px); 63 | top: calc(50% - 25px); 64 | fill: #F99; 65 | } 66 | -------------------------------------------------------------------------------- /app/styles/news.scss: -------------------------------------------------------------------------------- 1 | @import 'constants'; 2 | 3 | // News index 4 | #news-index { 5 | background-color: #eee; 6 | padding: 10px; 7 | height: calc(100vh - #{$app-header-height}); 8 | overflow: scroll; 9 | } 10 | 11 | .article-summary { 12 | background-color: white; 13 | margin-bottom: 10px; 14 | border: 1px solid #ccc; 15 | border-radius: 5px; 16 | display: block; 17 | .header { 18 | background-color: white; 19 | color: black; 20 | font-family: "Impact"; 21 | font-style: bold; 22 | padding: 0 18px; 23 | } 24 | .thumbnail { 25 | display: block; 26 | margin: 0 auto; 27 | max-width:100%; 28 | max-height:100%; 29 | } 30 | } 31 | 32 | .article-detail { 33 | background-color: #eee; 34 | padding: 10px; 35 | #news-index &.clone { 36 | position: fixed; 37 | top: $app-header-height; 38 | height: calc(100vh - #{$app-header-height}); 39 | left: 100%; 40 | width: 100% 41 | } 42 | } 43 | 44 | // News show 45 | .article-body { 46 | text-align: justify; 47 | img { 48 | display: block; 49 | width: 100%; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/styles/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | font-size: 100%; 6 | font: inherit; 7 | vertical-align: baseline; 8 | } 9 | 10 | article, aside, details, figcaption, figure, footer, header, menu, nav, section { 11 | display: block; 12 | } 13 | 14 | body { 15 | line-height: 1; 16 | } 17 | 18 | ol, ul { 19 | list-style: none; 20 | } 21 | 22 | blockquote, q { 23 | quotes: none; 24 | &:before, &:after { 25 | content: ''; 26 | content: none; 27 | } 28 | } 29 | 30 | table { 31 | border-collapse: collapse; 32 | border-spacing: 0; 33 | } 34 | -------------------------------------------------------------------------------- /app/styles/utility-classes.scss: -------------------------------------------------------------------------------- 1 | // Some very common utility classes 2 | .text-center { 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /app/styles/utility-mixins.scss: -------------------------------------------------------------------------------- 1 | // Some very common utility mixins 2 | @mixin clearfix { 3 | *zoom: 1; 4 | 5 | &:after { clear: both; } 6 | &:before, &:after { 7 | content: " "; 8 | display: table; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#toggle-menu id="toggle-main-menu" action="setupAnimation" player=menu-player duration=menuAnimationDuration}}{{/toggle-menu}} 4 | {{#link-to 'application'}}

Mobile Patterns

{{/link-to}} 5 | no-signal 6 |
7 | {{#slide-menu id="app-menu" observed-element="mobile-patterns" action="setupAnimation" player=menu-player duration=menuAnimationDuration}} 8 | {{link-to "Newspaper" "news" class="menu-item"}} 9 | {{link-to "Config" "config" class="menu-item"}} 10 | {{/slide-menu}} 11 |
12 | {{outlet}} 13 |
14 | {{content-overlay id="content-overlay" duration=menuAnimationDuration action="setupAnimation"}} 15 |
16 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /app/templates/components/animated-card.hbs: -------------------------------------------------------------------------------- 1 | {{#if content.isLoaded}} 2 | {{yield this}} 3 | {{else}} 4 | 5 | 6 | 7 | {{/if}} 8 | -------------------------------------------------------------------------------- /app/templates/components/animated-deck.hbs: -------------------------------------------------------------------------------- 1 | {{#if animatingToPrevious}} 2 | {{#animated-card id="previous-card" content=previous as |card|}}{{yield card}}{{/animated-card}} 3 | {{/if}} 4 | {{#animated-card id="current-card" content=current as |card|}}{{yield card}}{{/animated-card}} 5 | {{#if animatingToNext}} 6 | {{#animated-card id="next-card" content=next as |card|}}{{yield card}}{{/animated-card}} 7 | {{/if}} 8 | -------------------------------------------------------------------------------- /app/templates/components/content-overlay.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/app/templates/components/content-overlay.hbs -------------------------------------------------------------------------------- /app/templates/components/slide-menu.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} 2 | -------------------------------------------------------------------------------- /app/templates/components/toggle-menu.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /app/templates/config.hbs: -------------------------------------------------------------------------------- 1 |

Configuration

2 |

Newspaper transition: {{view "select" content=config.deckEffects value=config.deckEffect}}

3 | -------------------------------------------------------------------------------- /app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Mobile Patterns in Ember.js

3 |

4 | This is a sample showcase of common mobile UI patterns in mobile development done in Ember.js 5 |

6 |

7 | It's ultimate goal is to serve to other Ember.js devs as source of inspiration for implementing 8 | their solutions and components. 9 |

10 |
11 | -------------------------------------------------------------------------------- /app/templates/loading.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/templates/news.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{liquid-outlet}} 3 |
4 | -------------------------------------------------------------------------------- /app/templates/news/index.hbs: -------------------------------------------------------------------------------- 1 | {{!-- 2 |
3 |

{{list.selection.header}}

4 |
{{{list.selection.body}}}
5 |
6 |
7 | --}} 8 | 9 |
10 | {{#each model as |article|}} 11 | {{#link-to "news.show" article class="article-summary"}} 12 | 13 |

{{article.header}}

14 | {{/link-to}} 15 | {{/each}} 16 |
17 | -------------------------------------------------------------------------------- /app/templates/news/show.hbs: -------------------------------------------------------------------------------- 1 | {{#animated-deck current=model item-class="article-detail" effect=config.deckEffect onChange="transitionToSibling" as |card|}} 2 |

{{card.content.header}}

3 |
{{{card.content.body}}}
4 | {{/animated-deck}} 5 | -------------------------------------------------------------------------------- /app/transitions.js: -------------------------------------------------------------------------------- 1 | export default function(){ 2 | this.transition( 3 | this.fromRoute('news.index'), 4 | this.toRoute('news.show'), 5 | this.use('toLeft'), 6 | this.reverse('toRight') 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /app/utils/bezier-easing.js: -------------------------------------------------------------------------------- 1 | /* global BezierEasing */ 2 | export default BezierEasing; 3 | -------------------------------------------------------------------------------- /app/utils/gesture.js: -------------------------------------------------------------------------------- 1 | /* global EventEmitter */ 2 | class Gesture extends EventEmitter { 3 | constructor(opts = {}) { 4 | // opts.defaultPrevented = opts.defaultPrevented || false; 5 | opts.propagationStopped = opts.defaultPrevented || false; 6 | super(opts); 7 | this._originalOpts = opts; 8 | for (let key in opts) { 9 | this[key] = opts[key]; 10 | } 11 | this.events = []; 12 | } 13 | 14 | // Properties 15 | get first(){ 16 | return this.events[0]; 17 | } 18 | 19 | get x(){ 20 | return this.last.x; 21 | } 22 | 23 | get y(){ 24 | return this.last.y; 25 | } 26 | 27 | get initX() { 28 | return this.first.x; 29 | } 30 | 31 | get initY() { 32 | return this.first.y; 33 | } 34 | 35 | get deltaX() { 36 | return this.x - this.initX; 37 | } 38 | 39 | get deltaY() { 40 | return this.y - this.initY; 41 | } 42 | 43 | get delta() { 44 | return Math.sqrt(Math.pow(this.deltaX, 2) + Math.pow(this.deltaY, 2)); 45 | } 46 | 47 | get direction() { 48 | let x = this.deltaX; 49 | let y = -this.deltaY; 50 | let radians = Math.acos(Math.abs(y) / Math.sqrt(x*x + y*y)); 51 | let degrees = radians * 180 / Math.PI; 52 | if (x >= 0 && y >= 0) { 53 | return degrees; // 1st cuadrant 54 | } else if (x >= 0 && y < 0) { 55 | return 180 - degrees; // 2nd cuadrant 56 | } else if (x < 0 && y < 0) { 57 | return 180 + degrees; // 3rd cuadrant 58 | } else { 59 | return 360 - degrees; // 4th cuadrant 60 | } 61 | } 62 | 63 | get duration() { 64 | return this.last.timeStamp - this.first.timeStamp; 65 | } 66 | 67 | get speedX() { 68 | let lastEvents = this._getLastEvents(); 69 | let initX = lastEvents[0].x; 70 | let initTime = lastEvents[0].timeStamp; 71 | let lastX = lastEvents[lastEvents.length - 1].x; 72 | let lastTime = lastEvents[lastEvents.length - 1].timeStamp; 73 | 74 | return (lastX - initX) / (lastTime - initTime) * 1000 || 0; 75 | } 76 | 77 | get speedY() { 78 | let lastEvents = this._getLastEvents(); 79 | let initY = lastEvents[0].y; 80 | let initTime = lastEvents[0].timeStamp; 81 | let lastY = lastEvents[lastEvents.length - 1].y; 82 | let lastTime = lastEvents[lastEvents.length - 1].timeStamp; 83 | 84 | return (lastY - initY) / (lastTime - initTime) * 1000 || 0; 85 | } 86 | 87 | // Methods 88 | push(event) { 89 | let touch = event.touches[0]; 90 | let summary = { timeStamp: event.timeStamp, x: touch.pageX, y: touch.pageY }; 91 | this.events.push(summary); 92 | this.last = summary; 93 | if (this.defaultPrevented) { 94 | event.preventDefault(); 95 | } 96 | if (this.propagationStopped) { 97 | event.stopPropagation(); 98 | } 99 | return this; 100 | } 101 | 102 | isHorizontal(margin = 15) { 103 | let mod = this.direction % 180; 104 | return (mod < 90 + margin) && (mod > 90 - margin); 105 | } 106 | 107 | clear() { 108 | for (let key in this._originalOpts) { 109 | this[key] = this._originalOpts[key]; 110 | } 111 | this.events = []; 112 | this.last = null; 113 | } 114 | 115 | preventDefault(thisArg, condFn) { 116 | var func = arguments.length === 2 ? condFn.bind(thisArg) : thisArg; 117 | var result = func ? func(this) : true; 118 | this.defaultPrevented = result; 119 | return result; 120 | } 121 | 122 | stopPropagation(thisArg, condFn) { 123 | var func = arguments.length === 2 ? condFn.bind(thisArg) : thisArg; 124 | var result = func ? func(this) : true; 125 | this.propagationStopped = result; 126 | return result; 127 | } 128 | 129 | adquire(thisArg, condFn) { 130 | var func = arguments.length === 2 ? condFn.bind(thisArg) : thisArg; 131 | var result = func ? func(this) : true; 132 | this.defaultPrevented = result; 133 | this.propagationStopped = result; 134 | return result; 135 | } 136 | 137 | // Private methods 138 | _getLastEvents() { 139 | return this.events.slice(Math.max(this.events.length - 5, 0), this.events.length); 140 | } 141 | } 142 | 143 | export default Gesture; 144 | -------------------------------------------------------------------------------- /app/utils/swipe-gesture.js: -------------------------------------------------------------------------------- 1 | // TODO: Refactor gesture to handle more logic about gesture detection and tracking 2 | // 3 | // Desired API: 4 | // 5 | // var swipe = new Gesture('swipe'); 6 | // // Equivalent to `new Gesture('swipe', { minLength: 20, warnLength: 10, errorMargin: 20, exclusive: true, trackOffset: true })` 7 | // 8 | // swipe.on('warn', function(gesture) { 9 | // // Do my stuff 10 | // }); 11 | // 12 | // swipe.on('warn', function(gesture) { 13 | // // Do my stuff 14 | // }); 15 | // 16 | // swipe.on('progress', function(gesture) { 17 | // // Do my stuff 18 | // }); 19 | // 20 | // swipe.on('end', function(gesture) { 21 | // // Do my stuff 22 | // }); 23 | // 24 | // About the options: 25 | // 26 | // - minLength: Given that user gesture aren't perfect, a swipe gesture needs to have a minimum 27 | // length to be detected. The event will not emit a `start` event until the lenght 28 | // surpasses that length. 29 | // 30 | // - warnLength: In order to respond as fast as possible, sometime we want to preemtively do some 31 | // work ahead of time when we foreseen that the gesture will be a swipe. The gesture 32 | // will emit a `warn` event when the gesture looks like a swipe and it's length is at 33 | // least this one. 34 | // 35 | // - errorMargin: Swipe gesture should be horizontal, but there must be a certain error marging. A 36 | // gesture will be considered horizontal if it's closer than 20deg of being completely 37 | // horitontal. 38 | // 39 | // - exclusive: Wheter this gesture, once possitively detected as a swipe, should be defaultPrevented 40 | // or propagationStopped. Defaults to true. 41 | // 42 | // Other non configurable behavior: 43 | // 44 | // A swipe gesture is a one-try gesture. If by the time the gesture surpasses the `minLength` the 45 | // gesture is not possitively detected as a swipe, the gesture will not be tracked ever again. 46 | // IDEA: Maybe a `fail` event should be triggered?? 47 | // 48 | 49 | import Gesture from 'mobile-patterns/utils/gesture'; 50 | 51 | export default class SwipeGesture extends Gesture { 52 | constructor(opts = {}) { 53 | opts.minLength = opts.minLength === undefined && 20 || opts.minLength; 54 | opts.warnLength = opts.warnLength === undefined && 8 || opts.warnLength; 55 | opts.errorMargin = opts.errorMargin === undefined && 25 || opts.errorMargin; 56 | opts.exclusive = opts.exclusive === undefined && true || false; 57 | super(opts); 58 | } 59 | 60 | push(e) { 61 | if (this._ignoring) { 62 | return this; 63 | } 64 | if (e.type === 'touchend') { 65 | // 66 | // TODO: Test this bit. 67 | // 68 | if (this._started) { 69 | this.emit('end', this); 70 | } 71 | return this; 72 | } 73 | 74 | super.push(e); 75 | 76 | if (this._mustIgnore()) { 77 | this._ignoring = true; 78 | return this; 79 | } 80 | 81 | if (!this._warned && this._mustWarn()) { 82 | this._warned = true; 83 | this.emit('warn', this); 84 | } 85 | if (!this._started && this._mustStart()) { 86 | this._started = true; 87 | this.startOffset = this.deltaX; 88 | if (this.exclusive) { 89 | this.adquire(); 90 | } 91 | this.emit('start', this); 92 | return this; 93 | } 94 | if (this._started) { 95 | // TODO: Verify that when a swipe starts in once direcction, it can't change that direcction. 96 | this.emit('progress', this); 97 | } 98 | return this; 99 | } 100 | 101 | clear() { 102 | // 103 | // TODO: Add tests 104 | // 105 | // 106 | super.clear(); 107 | this._ignoring = false; 108 | this._warned = false; 109 | this._started = false; 110 | this.startOffset = null; 111 | } 112 | 113 | // Private 114 | _mustIgnore() { 115 | return !this._started && Math.abs(this.deltaX) >= this.minLength && !this.isHorizontal(); 116 | } 117 | 118 | _mustStart() { 119 | return Math.abs(this.deltaX) >= this.minLength && this.isHorizontal(); 120 | } 121 | 122 | _mustWarn() { 123 | return Math.abs(this.deltaX) >= this.warnLength && this.isHorizontal(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-patterns", 3 | "dependencies": { 4 | "bezier-easing": "1.0.0", 5 | "ember": "1.13.2", 6 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", 7 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", 8 | "ember-data": "1.0.0-beta.19.1", 9 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.4", 10 | "ember-qunit": "0.3.3", 11 | "ember-qunit-notifications": "0.0.7", 12 | "ember-resolver": "~0.1.15", 13 | "eventEmitter": "^4.2.11", 14 | "jquery": "^1.11.1", 15 | "loader.js": "ember-cli/loader.js#3.2.0", 16 | "qunit": "~1.17.1", 17 | "roboto-fontface": "~0.4.2", 18 | "web-animations-js": "2.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'mobile-patterns', 6 | environment: environment, 7 | baseURL: '/', 8 | apiNamespace: environment === 'development' ? 'api' : '__/proxy/api', 9 | locationType: 'auto', 10 | contentSecurityPolicy: { 11 | 'style-src': "'self' 'unsafe-inline'", 12 | 'img-src': "'self' *", 13 | }, 14 | EmberENV: { 15 | FEATURES: { 16 | // Here you can enable experimental features on an ember canary build 17 | // e.g. 'with-controller': true 18 | 'ember-htmlbars-component-generation': true, 19 | 'ember-htmlbars-attribute-syntax': true, 20 | 'ember-metal-injected-properties': true, 21 | 'ember-htmlbars-inline-if-helper': true, 22 | 'new-computed-syntax': true 23 | } 24 | }, 25 | 26 | APP: { 27 | // Here you can pass flags/options to your application instance 28 | // when it is created 29 | } 30 | }; 31 | 32 | if (environment === 'development') { 33 | // ENV.APP.LOG_RESOLVER = true; 34 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 35 | // ENV.APP.LOG_TRANSITIONS = true; 36 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 37 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 38 | } 39 | 40 | if (environment === 'test') { 41 | // Testem prefers this... 42 | ENV.baseURL = '/'; 43 | ENV.locationType = 'none'; 44 | 45 | // keep test console output quieter 46 | ENV.APP.LOG_ACTIVE_GENERATION = false; 47 | ENV.APP.LOG_VIEW_LOOKUPS = false; 48 | 49 | ENV.APP.rootElement = '#ember-testing'; 50 | } 51 | 52 | if (environment === 'production') { 53 | 54 | } 55 | 56 | return ENV; 57 | }; 58 | -------------------------------------------------------------------------------- /divshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-patterns", 3 | "root": "./dist", 4 | "routes": { 5 | "/tests": "tests/index.html", 6 | "/tests/**": "tests/index.html", 7 | "/**": "index.html" 8 | }, 9 | "proxy": { 10 | "api": { 11 | "origin":"https://immense-ocean-7154.herokuapp.com/api/", 12 | "headers": { 13 | "Accept": "application/json" 14 | }, 15 | "cookies": false, 16 | "timeout": 30 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-patterns", 3 | "version": "0.0.0", 4 | "description": "Small description for reader goes here", 5 | "private": true, 6 | "directories": { 7 | "doc": "doc", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "start": "ember server", 12 | "build": "ember build", 13 | "test": "ember test" 14 | }, 15 | "repository": "", 16 | "engines": { 17 | "node": ">= 0.10.0" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "broccoli-asset-rev": "^2.0.2", 23 | "broccoli-funnel": "0.2.3", 24 | "broccoli-sass": "^0.6.6", 25 | "ember-cli": "0.2.7", 26 | "ember-cli-app-version": "0.3.3", 27 | "ember-cli-babel": "^5.0.0", 28 | "ember-cli-dependency-checker": "^1.0.0", 29 | "ember-cli-htmlbars": "0.7.9", 30 | "ember-cli-ic-ajax": "0.1.1", 31 | "ember-cli-inject-live-reload": "^1.3.0", 32 | "ember-cli-qunit": "0.3.13", 33 | "ember-cli-uglify": "^1.0.1", 34 | "ember-data": "1.13", 35 | "ember-disable-proxy-controllers": "^1.0.0", 36 | "ember-export-application-global": "^1.0.2", 37 | "express": "^4.12.4", 38 | "glob": "^4.5.3", 39 | "liquid-fire": "0.20.4", 40 | "morgan": "^1.5.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/.gitkeep -------------------------------------------------------------------------------- /public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/images/black-moustache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/black-moustache.jpg -------------------------------------------------------------------------------- /public/images/brown-moustache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/brown-moustache.jpg -------------------------------------------------------------------------------- /public/images/cloth-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/cloth-pattern.png -------------------------------------------------------------------------------- /public/images/concentric-maze.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/concentric-maze.jpg -------------------------------------------------------------------------------- /public/images/gorbipuff-coding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/gorbipuff-coding.jpg -------------------------------------------------------------------------------- /public/images/gorbipuff-hole.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/gorbipuff-hole.jpg -------------------------------------------------------------------------------- /public/images/motocross.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/motocross.jpg -------------------------------------------------------------------------------- /public/images/no-signal-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /public/images/no-signal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /public/images/three-headed-monkey.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/three-headed-monkey.jpg -------------------------------------------------------------------------------- /public/images/thug-life-hamster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/thug-life-hamster.jpg -------------------------------------------------------------------------------- /public/images/trapped-in-time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/public/images/trapped-in-time.jpg -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true 3 | } 4 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // To use it create some files under `routes/` 2 | // e.g. `server/routes/ember-hamsters.js` 3 | // 4 | // module.exports = function(app) { 5 | // app.get('/ember-hamsters', function(req, res) { 6 | // res.send('hello'); 7 | // }); 8 | // }; 9 | 10 | module.exports = function(app) { 11 | var globSync = require('glob').sync; 12 | var mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); 13 | var proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); 14 | 15 | // Log proxy requests 16 | var morgan = require('morgan'); 17 | app.use(morgan('dev')); 18 | 19 | mocks.forEach(function(route) { route(app); }); 20 | proxies.forEach(function(route) { route(app); }); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /server/mocks/articles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var express = require('express'); 3 | var articlesRouter = express.Router(); 4 | var articlesFixtures = [ 5 | { 6 | id: 1, 7 | header: 'Three headed monkey panic in LA', 8 | body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.

Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dignissim lacinia nunc.

', 9 | thumbnailUrl: '/images/three-headed-monkey.jpg', 10 | previousArticle: null, 11 | nextArticle: 2 12 | }, 13 | { 14 | id: 2, 15 | header: 'Tomster and Kim Kardashian, more than friends?', 16 | body: 'Sed dignissim lacinia nunc. Curabitur tortor. Pellentesque nibh.

Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor.

Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa. Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque volutpat condimentum velit.

', 17 | thumbnailUrl: '/images/thug-life-hamster.jpg', 18 | previousArticle: 1, 19 | nextArticle: 3 20 | }, 21 | { 22 | id: 3, 23 | header: 'Angular 2.0 will be renamed to RoundedScript!', 24 | body: 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui.

Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet.

Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim. Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. Integer lacinia sollicitudin massa. Cras metus. Sed aliquet risus a tortor.

', 25 | thumbnailUrl: '/images/concentric-maze.jpg', 26 | previousArticle: 2, 27 | nextArticle: 4 28 | }, 29 | { 30 | id: 4, 31 | header: 'Breaking: HTMLBars are the parents', 32 | body: 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui.

Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet.

Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim.

Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. Integer lacinia sollicitudin massa. Cras metus. Sed aliquet risus a tortor.

', 33 | thumbnailUrl: '/images/black-moustache.jpg', 34 | previousArticle: 3, 35 | nextArticle: 5 36 | }, 37 | { 38 | id: 5, 39 | header: 'Gorbypuff: "I am forking Node.js"', 40 | body: '

Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Morbi lacinia molestie dui.

Praesent blandit dolor. Sed non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit amet pede facilisis laoreet.

Donec lacus nunc, viverra nec, blandit vel, egestas et, augue. Vestibulum tincidunt malesuada tellus. Ut ultrices ultrices enim.

Curabitur sit amet mauris. Morbi in dui quis est pulvinar ullamcorper. Nulla facilisi. Integer lacinia sollicitudin massa. Cras metus. Sed aliquet risus a tortor.

', 41 | thumbnailUrl: '/images/gorbipuff-coding.jpg', 42 | previousArticle: 4, 43 | nextArticle: null 44 | }, 45 | ] 46 | 47 | 48 | articlesRouter.get('/', function(req, res) { 49 | setTimeout(function() { 50 | res.send({ 'articles': articlesFixtures }); 51 | }, 500); 52 | }); 53 | 54 | articlesRouter.post('/', function(req, res) { 55 | res.status(201).end(); 56 | }); 57 | 58 | articlesRouter.get('/:id', function(req, res) { 59 | var article = articlesFixtures.filter(function(art) { return art.id == req.params.id })[0]; 60 | if (article) { 61 | setTimeout(function() { 62 | res.send({ 'articles': article }); 63 | }, 500); 64 | } else { 65 | res.status(404).end(); 66 | } 67 | }); 68 | 69 | articlesRouter.put('/:id', function(req, res) { 70 | res.send({ 71 | 'articles': { 72 | id: req.params.id 73 | } 74 | }); 75 | }); 76 | 77 | articlesRouter.delete('/:id', function(req, res) { 78 | res.status(204).end(); 79 | }); 80 | 81 | app.use('/api/articles', articlesRouter); 82 | }; 83 | -------------------------------------------------------------------------------- /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 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": false, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esnext": true 51 | } 52 | -------------------------------------------------------------------------------- /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/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 | MobilePatterns 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/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/adapters/application-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('adapter:application', 'ApplicationAdapter', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['serializer:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var adapter = this.subject(); 14 | assert.assert.ok(adapter); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/components/animated-card-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForComponent, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleForComponent('animated-card', 'AnimatedCardComponent', { 7 | // specify the other units that are required for this test 8 | // needs: ['component:foo', 'helper:bar'] 9 | }); 10 | 11 | test('it renders', function(assert) { 12 | assert.expect(2); 13 | 14 | // creates the component instance 15 | var component = this.subject(); 16 | assert.equal(component._state, 'preRender'); 17 | 18 | // appends the component to the page 19 | this.render(); 20 | assert.equal(component._state, 'inDOM'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/components/animated-deck-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForComponent, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleForComponent('animated-deck', 'AnimatedDeckComponent', { 7 | // specify the other units that are required for this test 8 | // needs: ['component:foo', 'helper:bar'] 9 | needs: ['component:animated-card'] 10 | }); 11 | 12 | test('it renders', function(assert) { 13 | assert.expect(2); 14 | 15 | // creates the component instance 16 | var component = this.subject(); 17 | assert.equal(component._state, 'preRender'); 18 | 19 | // appends the component to the page 20 | this.render(); 21 | assert.equal(component._state, 'inDOM'); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/components/content-overlay-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForComponent, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleForComponent('content-overlay', 'ContentOverlayComponent', { 7 | // specify the other units that are required for this test 8 | // needs: ['component:foo', 'helper:bar'] 9 | }); 10 | 11 | test('it renders', function(assert) { 12 | assert.expect(2); 13 | 14 | // creates the component instance 15 | var component = this.subject(); 16 | assert.equal(component._state, 'preRender'); 17 | 18 | // appends the component to the page 19 | this.render(); 20 | assert.equal(component._state, 'inDOM'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/components/toggle-menu-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForComponent, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleForComponent('toggle-menu', 'ToggleMenuComponent', { 7 | // specify the other units that are required for this test 8 | // needs: ['component:foo', 'helper:bar'] 9 | }); 10 | 11 | test('it renders', function(assert) { 12 | assert.expect(2); 13 | 14 | // creates the component instance 15 | var component = this.subject(); 16 | assert.equal(component._state, 'preRender'); 17 | 18 | // appends the component to the page 19 | this.render(); 20 | assert.equal(component._state, 'inDOM'); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/unit/controllers/application-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('controller:application', 'ApplicationController', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['controller:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var controller = this.subject(); 14 | assert.ok(controller); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/controllers/news/show-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('controller:news/show', 'NewsShowController', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['controller:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var controller = this.subject(); 14 | assert.ok(controller); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/initializers/offline-support-test.js: -------------------------------------------------------------------------------- 1 | import { test, module } from 'ember-qunit'; 2 | import Ember from 'ember'; 3 | import { initialize } from 'mobile-patterns/initializers/offline-support'; 4 | 5 | var container, application; 6 | 7 | module('OfflineSupportInitializer', { 8 | setup: function() { 9 | Ember.run(function() { 10 | application = Ember.Application.create(); 11 | container = application.__container__; 12 | application.deferReadiness(); 13 | }); 14 | } 15 | }); 16 | 17 | // Replace this with your real tests. 18 | test('it works', function(assert) { 19 | initialize(container, application); 20 | 21 | // you would normally confirm the results of the initializer here 22 | assert.ok(true); 23 | }); 24 | 25 | -------------------------------------------------------------------------------- /tests/unit/mixins/gesture-listener-test.js: -------------------------------------------------------------------------------- 1 | import { test, module } from 'ember-qunit'; 2 | import Ember from 'ember'; 3 | import GestureListenerMixin from 'mobile-patterns/mixins/gesture-listener'; 4 | 5 | module('GestureListenerMixin'); 6 | 7 | // Replace this with your real tests. 8 | test('it works', function(assert) { 9 | var GestureListenerObject = Ember.Object.extend(GestureListenerMixin); 10 | var subject = GestureListenerObject.create(); 11 | assert.ok(subject); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/unit/models/article-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleForModel, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleForModel('article', 'Article', { 7 | // Specify the other units that are required for this test. 8 | needs: [] 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /tests/unit/routes/application-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('route:application', 'ApplicationRoute', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['controller:foo'] 9 | }); 10 | 11 | test('it exists', function(assert) { 12 | var route = this.subject(); 13 | assert.ok(route); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/routes/news-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('route:news', 'NewsRoute', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['controller:foo'] 9 | }); 10 | 11 | test('it exists', function(assert) { 12 | var route = this.subject(); 13 | assert.ok(route); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/unit/services/browser-detector-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('service:browser-detector', 'BrowserDetectorService', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['service:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var service = this.subject(); 14 | assert.ok(service); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/services/config-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('service:config', 'ConfigService', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['service:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var service = this.subject(); 14 | assert.ok(service); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/services/network-monitor-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('service:network-monitor', 'NetworkMonitorService', { 7 | // Specify the other units that are required for this test. 8 | // needs: ['service:foo'] 9 | }); 10 | 11 | // Replace this with your real tests. 12 | test('it exists', function(assert) { 13 | var service = this.subject(); 14 | assert.ok(service); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/utils/gesture-test.js: -------------------------------------------------------------------------------- 1 | import { test, module } from 'ember-qunit'; 2 | import Gesture from 'mobile-patterns/utils/gesture'; 3 | 4 | var gesture; 5 | 6 | module('Gesture - constructor'); 7 | 8 | test('sets the proper default values', function(assert) { 9 | gesture = new Gesture({abc: 123}); 10 | assert.deepEqual(gesture.events, [], 'it has no events'); 11 | assert.ok(!gesture.defaultPrevented, 'defaultPrevented is false'); 12 | assert.ok(!gesture.propagationStopped, 'propagationStopped is false'); 13 | assert.equal(gesture.abc, 123, 'Any property given in the initializer is setted'); 14 | }); 15 | 16 | module('Gesture - push'); 17 | 18 | test('appends the given event to the gesture', function(assert) { 19 | gesture = new Gesture(); 20 | 21 | assert.equal(gesture.events.length, 0); 22 | assert.equal(gesture.last, null); 23 | gesture.push({timeStamp: 123, touches: [{pageX: 1, pageY: 2}]}); 24 | assert.equal(gesture.events.length, 1); 25 | assert.deepEqual(gesture.last, { timeStamp: 123, x: 1, y: 2 }); 26 | }); 27 | 28 | test('calls preventDefault on the given element when defaultPrevented is set to true', function(assert) { 29 | gesture = new Gesture(); 30 | var evtNotPrevented = { 31 | timeStamp: 123, 32 | touches: [{ pageX: 1, pageY: 2 }], 33 | preventDefault: function() { 34 | assert.ok(false, 'This event must not be prevented'); 35 | } 36 | }; 37 | gesture.push(evtNotPrevented); 38 | 39 | gesture.defaultPrevented = true; 40 | var evtPrevented = { 41 | timeStamp: 123, 42 | touches: [{ pageX: 1, pageY: 2 }], 43 | preventDefault: function() { 44 | assert.ok(true, 'This event must be prevented'); 45 | } 46 | }; 47 | gesture.push(evtPrevented); 48 | }); 49 | 50 | test('calls stopPropagation on the given element when propagationStopped is set to true', function(assert) { 51 | gesture = new Gesture(); 52 | var evtNotStopped = { 53 | timeStamp: 123, 54 | touches: [{ pageX: 1, pageY: 2 }], 55 | stopPropagation: function() { 56 | assert.ok(false, 'The propagation of this event must not be stopped'); 57 | } 58 | }; 59 | gesture.push(evtNotStopped); 60 | 61 | gesture.propagationStopped = true; 62 | var evtStopped = { 63 | timeStamp: 123, 64 | touches: [{ pageX: 1, pageY: 2 }], 65 | stopPropagation: function() { 66 | assert.ok(true, 'The propagation of this event must be stopped'); 67 | } 68 | }; 69 | gesture.push(evtStopped); 70 | }); 71 | 72 | test('returns the gesture so we can chain calls', function(assert) { 73 | gesture = new Gesture(); 74 | var returnValue = gesture.push({timeStamp: 123, touches: [{pageX: 1, pageY: 2}]}); 75 | assert.equal(returnValue, gesture, 'push returns the gesture itself'); 76 | }); 77 | 78 | module('Gesture - first and last', { 79 | setup: function() { 80 | gesture = new Gesture(); 81 | gesture.push({ timeStamp: 123, touches: [{ pageX: 1, pageY: 2 }] }); 82 | gesture.push({ timeStamp: 234, touches: [{ pageX: 5, pageY: 6 }] }); 83 | } 84 | }); 85 | 86 | test('first contains the first captured event', function(assert) { 87 | assert.deepEqual(gesture.first, { timeStamp: 123, x: 1, y: 2 }); 88 | }); 89 | 90 | test('last contains the last captured event', function(assert) { 91 | assert.deepEqual(gesture.last, { timeStamp: 234, x: 5, y: 6 }); 92 | }); 93 | 94 | module('Gesture - coordinates', { 95 | setup: function() { 96 | gesture = new Gesture(); 97 | gesture.push({ timeStamp: 123, touches: [{ pageX: 1, pageY: 2 }] }); 98 | gesture.push({ timeStamp: 234, touches: [{ pageX: 5, pageY: 6 }] }); 99 | } 100 | }); 101 | 102 | test('x contains the pageX of the last event', function(assert) { 103 | assert.equal(gesture.x, 5); 104 | }); 105 | 106 | test('y contains the pageY of the last event', function(assert) { 107 | assert.equal(gesture.y, 6); 108 | }); 109 | 110 | test('initX contains the pageX of the first event', function(assert) { 111 | assert.equal(gesture.initX, 1); 112 | }); 113 | 114 | test('initY contains the pageX of the first event', function(assert) { 115 | assert.equal(gesture.initY, 2); 116 | }); 117 | 118 | module('Gesture - deltas', { 119 | setup: function() { 120 | gesture = new Gesture(); 121 | gesture.push({ touches: [{pageX: 100, pageY: 250}], timeStamp: 1419263004600 }); 122 | gesture.push({ touches: [{pageX: 110, pageY: 270}], timeStamp: 1419263004610 }); 123 | gesture.push({ touches: [{pageX: 120, pageY: 290}], timeStamp: 1419263004620 }); 124 | } 125 | }); 126 | 127 | test('deltaX contains the X delta between the beginning and the current point of the gesture', function(assert) { 128 | assert.equal(gesture.deltaX, 20); 129 | }); 130 | 131 | test('deltaY contains the X delta between the beginning and the current point of the gesture', function(assert) { 132 | assert.equal(gesture.deltaY, 40); 133 | }); 134 | 135 | test('delta contains the distance in px between the beginning and the current point of the gesture', function(assert) { 136 | assert.ok(gesture.delta - 44.731359 < 0.0001, 'Calculates the delta using Pythagoras theorem'); 137 | }); 138 | 139 | module('Gesture - direction'); 140 | 141 | test('returns the direction of the gesture in degrees (0-360)', function(assert) { 142 | // Gesture to the 1st cuandrant 143 | var gesture1 = new Gesture(); 144 | gesture1.push({ touches: [{pageX: 100, pageY: 270}] }); 145 | gesture1.push({ touches: [{pageX: 110, pageY: 250}] }); 146 | assert.equal(gesture1.direction, 26.56505117707799); 147 | 148 | // Gesture to the 2st cuandrant 149 | var gesture2 = new Gesture(); 150 | gesture2.push({ touches: [{pageX: 100, pageY: 250}] }); 151 | gesture2.push({ touches: [{pageX: 110, pageY: 270}] }); 152 | assert.equal(gesture2.direction, 153.43494882292202); 153 | 154 | // Gesture to the 3rd cuandrant 155 | var gesture3 = new Gesture(); 156 | gesture3.push({ touches: [{pageX: 110, pageY: 250}] }); 157 | gesture3.push({ touches: [{pageX: 100, pageY: 270}] }); 158 | assert.equal(gesture3.direction, 206.56505117707799); 159 | 160 | // Gesture to the 4th cuandrant 161 | var gesture4 = new Gesture(); 162 | gesture4.push({ touches: [{pageX: 110, pageY: 270}] }); 163 | gesture4.push({ touches: [{pageX: 100, pageY: 250}] }); 164 | assert.equal(gesture4.direction, 333.43494882292202); 165 | }); 166 | 167 | module('Gesture - isHorizontal'); 168 | 169 | test('returns true if the gesture is horizontal with a error margin smaller than the given one', function(assert) { 170 | var gesture = new Gesture(); 171 | gesture.push({ touches: [{pageX: 100, pageY: 250}] }); 172 | gesture.push({ touches: [{pageX: 140, pageY: 255}] }); 173 | gesture.push({ touches: [{pageX: 180, pageY: 260}] }); 174 | assert.ok(gesture.isHorizontal(), 'The gesture is horizontal with an error margin of 15deg'); // The error margin defalts to ± 15° 175 | assert.ok(!gesture.isHorizontal(2), 'The gesture is not horizontal with an error margin of 2deg'); // The error margin is set to ± 2° 176 | 177 | var gesture2 = new Gesture(); 178 | gesture2.push({ touches: [{pageX: 250, pageY: 100}] }); 179 | gesture2.push({ touches: [{pageX: 255, pageY: 140}] }); 180 | gesture2.push({ touches: [{pageX: 260, pageY: 180}] }); 181 | assert.ok(!gesture2.isHorizontal(), 'The new gesture is not horizontal'); 182 | }); 183 | 184 | module('Gesture - speed', { 185 | setup: function() { 186 | gesture = new Gesture(); 187 | gesture.push({ touches: [{pageX: 100, pageY: 250}], timeStamp: 1419263004600 }); 188 | gesture.push({ touches: [{pageX: 110, pageY: 270}], timeStamp: 1419263004610 }); 189 | gesture.push({ touches: [{pageX: 120, pageY: 290}], timeStamp: 1419263004620 }); 190 | } 191 | }); 192 | 193 | test('speedX contains the speed of the gesture in the X axis', function(assert) { 194 | assert.equal(gesture.speedX, 1000); 195 | }); 196 | 197 | test('speedY contains the speed of the gesture in the X axis', function(assert) { 198 | assert.equal(gesture.speedY, 2000); 199 | }); 200 | 201 | module('Gesture - duration', { 202 | setup: function() { 203 | gesture = new Gesture(); 204 | gesture.push({ touches: [{pageX: 100, pageY: 250}], timeStamp: 1419263004600 }); 205 | gesture.push({ touches: [{pageX: 110, pageY: 270}], timeStamp: 1419263004610 }); 206 | gesture.push({ touches: [{pageX: 120, pageY: 290}], timeStamp: 1419263004620 }); 207 | } 208 | }); 209 | 210 | test('contains the duration of the gesture in milliseconds', function(assert) { 211 | assert.equal(gesture.duration, 20); 212 | }); 213 | 214 | module('Gesture - clear', { 215 | setup: function() { 216 | gesture = new Gesture({abc: 123}); 217 | gesture.push({ touches: [{pageX: 100, pageY: 250}], timeStamp: 1419263004600 }); 218 | gesture.push({ touches: [{pageX: 110, pageY: 270}], timeStamp: 1419263004610 }); 219 | gesture.push({ touches: [{pageX: 120, pageY: 290}], timeStamp: 1419263004620 }); 220 | gesture.abc = 234; 221 | } 222 | }); 223 | 224 | test('returns the gesture to the state it had when it was initialized', function(assert) { 225 | gesture.clear(); 226 | assert.deepEqual(gesture.events, [], 'There is not events'); 227 | assert.ok(!gesture.defaultPrevented); 228 | assert.ok(!gesture.propagationStopped); 229 | assert.ok(!gesture.first, null, 'There is no first event'); 230 | assert.ok(!gesture.last, null, 'There is no last event'); 231 | assert.equal(gesture.abc, 123, 'Other properties are restored l'); 232 | }); 233 | 234 | // test('`Gesture#preventDefault` sets the defaultPrevented flag to true if the given condition is met', function(assert) { 235 | // var gesture = new Gesture(); 236 | // assert.ok(!gesture.defaultPrevented, 'Newly created gestures are not default prevented'); 237 | // gesture.preventDefault(); 238 | // assert.ok(gesture.defaultPrevented, 'When not condition is passed, preventDefault() sets the the flag to true'); 239 | 240 | // var gesture2 = new Gesture(); 241 | // var conditionFn = function(g) { 242 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 243 | // return true; 244 | // }; 245 | // var falseyConditionFn = function(g) { 246 | // return false; 247 | // }; 248 | // assert.ok(!gesture2.preventDefault(falseyConditionFn), 'preventDefault() returns false if the gesture was not prevented'); 249 | // assert.ok(!gesture2.defaultPrevented, 'When the condition function returns false, the gesture is not prevented'); 250 | // assert.ok(gesture2.preventDefault(conditionFn), 'preventDefault() returns true if the gesture was prevented'); 251 | // assert.ok(gesture2.defaultPrevented, 'When the condition function returns true, the gesture is prevented'); 252 | 253 | // var gesture3 = new Gesture(); 254 | // gesture3.preventDefault('given-context', function(g) { 255 | // assert.equal(this, 'given-context', 'Inside the function, the context has been bound correctly'); 256 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 257 | // }); 258 | // }); 259 | 260 | // test('`Gesture#stopPropagation` sets the propagationStopped flag to true if the given condition is met', function(assert) { 261 | // var gesture = new Gesture(); 262 | // assert.ok(!gesture.propagationStopped, 'Newly created gestures have not its propagation stopped'); 263 | // gesture.stopPropagation(); 264 | // assert.ok(gesture.propagationStopped, 'When not condition is passed, stopPropagation() sets the flag to true'); 265 | 266 | // var gesture2 = new Gesture(); 267 | // var conditionFn = function(g) { 268 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 269 | // return true; 270 | // }; 271 | // var falseyConditionFn = function(g) { 272 | // return false; 273 | // }; 274 | // assert.ok(!gesture2.stopPropagation(falseyConditionFn), 'stopPropagation() returns false if the gesture propagation was not stopped'); 275 | // assert.ok(!gesture2.propagationStopped, 'When the condition function returns false, the gesture ipropagation s not stopped'); 276 | // assert.ok(gesture2.stopPropagation(conditionFn), 'stopPropagation() returns true if the gesture propagation was stopped'); 277 | // assert.ok(gesture2.propagationStopped, 'When the condition function returns true, the gesture propagation is stopped'); 278 | 279 | // var gesture3 = new Gesture(); 280 | // gesture3.stopPropagation('given-context', function(g) { 281 | // assert.equal(this, 'given-context', 'Inside the function, the context has been bound correctly'); 282 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 283 | // }); 284 | // }); 285 | 286 | // test('`Gesture#adquire` sets both `defaultPrevented` and `propagationStopped` to true if the given condition is met', function(assert) { 287 | // var gesture = new Gesture(); 288 | // gesture.adquire(); 289 | // assert.ok(gesture.defaultPrevented, 'When not condition is passed, adquire() sets the flags to true'); 290 | // assert.ok(gesture.propagationStopped, 'When not condition is passed, adquire() sets the flags to true'); 291 | 292 | // var gesture2 = new Gesture(); 293 | // var counter = 0; 294 | // var conditionFn = function(g) { 295 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 296 | // assert.ok(++counter < 2, 'This method is only invoked once'); 297 | // return true; 298 | // }; 299 | // var falseyConditionFn = function(g) { 300 | // return false; 301 | // }; 302 | // assert.ok(!gesture2.adquire(falseyConditionFn), 'adquire() returns false if the gesture propagation was not stopped'); 303 | // assert.ok(!gesture2.defaultPrevented, 'When the condition function returns false, the gesture is default not prevented'); 304 | // assert.ok(!gesture2.propagationStopped, 'When the condition function returns false, the gesture propagation is not stopped'); 305 | // assert.ok(gesture2.adquire(conditionFn), 'adquire() returns true if the gesture propagation was stopped'); 306 | // assert.ok(gesture2.defaultPrevented, 'When the condition function returns true, the gesture is default prevented'); 307 | // assert.ok(gesture2.propagationStopped, 'When the condition function returns true, the gesture propagation is stopped'); 308 | 309 | // var gesture3 = new Gesture(); 310 | // gesture3.adquire('given-context', function(g) { 311 | // assert.equal(this, 'given-context', 'Inside the function, the context has been bound correctly'); 312 | // assert.equal(g.constructor, Gesture, 'The condition function receives the gesture'); 313 | // }); 314 | // }); 315 | -------------------------------------------------------------------------------- /tests/unit/utils/swipe-gesture-test.js: -------------------------------------------------------------------------------- 1 | import { test, module } from 'ember-qunit'; 2 | import SwipeGesture from 'mobile-patterns/utils/swipe-gesture'; 3 | 4 | var swipe; 5 | 6 | module('SwipeGesture - constructor'); 7 | 8 | test('initializes the default values', function(assert) { 9 | swipe = new SwipeGesture(); 10 | assert.equal(swipe.minLength, 20, 'minLength is 20 by default'); 11 | assert.equal(swipe.warnLength, 10, 'warnLength is 10 by default'); 12 | assert.equal(swipe.errorMargin, 20, 'errorMargin is 10 by default'); 13 | assert.ok(swipe.exclusive, 'exclusive is true by default'); 14 | assert.ok(!swipe.defaultPrevented, 'defaultPrevented is false by default'); 15 | assert.ok(!swipe.propagationStopped, 'propagationStopped is false by default'); 16 | }); 17 | 18 | module('SwipeGesture - push'); 19 | 20 | test('returns the swipe so we can chain calls', function(assert) { 21 | swipe = new SwipeGesture(); 22 | var returnValue = swipe.push({timeStamp: 123, touches: [{pageX: 1, pageY: 2}]}); 23 | assert.equal(returnValue, swipe, 'push returns the swipe gesture itself'); 24 | }); 25 | 26 | module('SwipeGesture - warn event'); 27 | 28 | test('is fired once for horizontal gestures when the warn length is surpassed', function(assert) { 29 | assert.expect(2); 30 | var eventsCount = 0; 31 | swipe = new SwipeGesture(); 32 | 33 | swipe.on('warn', function(gesture) { 34 | assert.equal(++eventsCount, 1, 'The warn event is fired once'); 35 | assert.equal(gesture.x, 10, 'The gesture contains the last event'); 36 | setTimeout(assert.async, 1); 37 | }); 38 | 39 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 40 | swipe.push({ timeStamp: 234, touches: [{ pageX: 5, pageY: 10 }] }); 41 | swipe.push({ timeStamp: 345, touches: [{ pageX: 9, pageY: 10 }] }); 42 | swipe.push({ timeStamp: 456, touches: [{ pageX: 10, pageY: 10 }] }); 43 | swipe.push({ timeStamp: 567, touches: [{ pageX: 15, pageY: 10 }] }); 44 | }); 45 | 46 | test('is never fired if the gesture is not horizontal', function(assert) { 47 | assert.expect(0); 48 | swipe = new SwipeGesture(); 49 | 50 | swipe.on('warn', function(gesture) { 51 | assert.ok(false, 'no warn event is emitted'); 52 | }); 53 | 54 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 55 | swipe.push({ timeStamp: 234, touches: [{ pageX: 5, pageY: 20 }] }); 56 | swipe.push({ timeStamp: 345, touches: [{ pageX: 9, pageY: 30 }] }); 57 | swipe.push({ timeStamp: 456, touches: [{ pageX: 10, pageY: 40 }] }); 58 | swipe.push({ timeStamp: 567, touches: [{ pageX: 15, pageY: 50 }] }); 59 | setTimeout(assert.async, 1); 60 | }); 61 | 62 | test('if the gesture is not horizontal by the time the warnLength is surpassed but it gets corrected before surpassing the minLength the event is fired', function(assert) { 63 | assert.expect(2); 64 | swipe = new SwipeGesture(); 65 | 66 | swipe.on('warn', function(gesture) { 67 | assert.ok(true, 'the event is emitted'); 68 | assert.equal(gesture.x, 15, 'The gesture contains the last event'); 69 | }); 70 | 71 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 72 | swipe.push({ timeStamp: 234, touches: [{ pageX: 5, pageY: 20 }] }); 73 | swipe.push({ timeStamp: 345, touches: [{ pageX: 9, pageY: 30 }] }); 74 | swipe.push({ timeStamp: 456, touches: [{ pageX: 10, pageY: 40 }] }); 75 | swipe.push({ timeStamp: 567, touches: [{ pageX: 15, pageY: 10 }] }); 76 | setTimeout(assert.async, 1); 77 | }); 78 | 79 | module('SwipeGesture - start event'); 80 | 81 | test('is fired once for horizontal gestures when the minimum length is surpassed', function(assert) { 82 | assert.expect(2); 83 | var eventsCount = 0; 84 | swipe = new SwipeGesture(); 85 | 86 | swipe.on('start', function(gesture) { 87 | assert.equal(++eventsCount, 1, 'The start event is fired once'); 88 | assert.equal(gesture.x, 30, 'The gesture contains the last event'); 89 | setTimeout(assert.async, 1); 90 | }); 91 | 92 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 93 | swipe.push({ timeStamp: 234, touches: [{ pageX: 10, pageY: 10 }] }); 94 | swipe.push({ timeStamp: 345, touches: [{ pageX: 19, pageY: 10 }] }); 95 | swipe.push({ timeStamp: 456, touches: [{ pageX: 30, pageY: 10 }] }); 96 | swipe.push({ timeStamp: 567, touches: [{ pageX: 35, pageY: 10 }] }); 97 | }); 98 | 99 | test('is never fired if the gesture is not horizontal', function(assert) { 100 | assert.expect(0); 101 | swipe = new SwipeGesture(); 102 | 103 | swipe.on('start', function(gesture) { 104 | assert.ok(false, 'no start event is emitted'); 105 | }); 106 | 107 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 108 | swipe.push({ timeStamp: 234, touches: [{ pageX: 10, pageY: 20 }] }); 109 | swipe.push({ timeStamp: 345, touches: [{ pageX: 19, pageY: 30 }] }); 110 | swipe.push({ timeStamp: 456, touches: [{ pageX: 30, pageY: 40 }] }); 111 | swipe.push({ timeStamp: 567, touches: [{ pageX: 35, pageY: 50 }] }); 112 | setTimeout(assert.async, 1); 113 | }); 114 | 115 | test('if the gesture is not horizontal by the time the minLength is surpassed, the event is not emitted even if the gesture becomes horizontal', function(assert) { 116 | assert.expect(0); 117 | swipe = new SwipeGesture(); 118 | 119 | swipe.on('start', function(gesture) { 120 | assert.ok(false, 'no start event is emitted'); 121 | }); 122 | 123 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 124 | swipe.push({ timeStamp: 234, touches: [{ pageX: 10, pageY: 20 }] }); 125 | swipe.push({ timeStamp: 345, touches: [{ pageX: 19, pageY: 30 }] }); 126 | swipe.push({ timeStamp: 456, touches: [{ pageX: 30, pageY: 40 }] }); 127 | swipe.push({ timeStamp: 567, touches: [{ pageX: 35, pageY: 10 }] }); 128 | setTimeout(assert.async, 1); 129 | }); 130 | 131 | module('SwipeGesture - progress event'); 132 | 133 | test('is fired when an event is added to a tracked gesture', function(assert) { 134 | assert.expect(2); 135 | var eventsCount = 0; 136 | swipe = new SwipeGesture(); 137 | 138 | swipe.on('progress', function(gesture) { 139 | assert.equal(++eventsCount, 1, 'The event that triggers the start event does not trigger a progress event'); 140 | assert.equal(gesture.x, 35, 'The gesture contains the last event'); 141 | setTimeout(assert.async, 1); 142 | }); 143 | 144 | swipe.push({ timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 145 | swipe.push({ timeStamp: 234, touches: [{ pageX: 10, pageY: 10 }] }); 146 | swipe.push({ timeStamp: 345, touches: [{ pageX: 19, pageY: 10 }] }); 147 | swipe.push({ timeStamp: 456, touches: [{ pageX: 30, pageY: 10 }] }); 148 | swipe.push({ timeStamp: 567, touches: [{ pageX: 35, pageY: 10 }] }); 149 | }); 150 | 151 | module('SwipeGesture - end event'); 152 | 153 | test('is fired when a tracked event finalized', function(assert) { 154 | assert.expect(2); 155 | var eventsCount = 0; 156 | swipe = new SwipeGesture(); 157 | 158 | swipe.on('end', function(gesture) { 159 | assert.equal(++eventsCount, 1, 'The event that triggers the start event does not trigger a progress event'); 160 | assert.equal(gesture.x, 31, 'The last event is the previous one'); 161 | setTimeout(assert.async, 1); 162 | }); 163 | 164 | swipe.push({ type: 'touchstart', timeStamp: 123, touches: [{ pageX: 0, pageY: 10 }] }); 165 | swipe.push({ type: 'touchmove', timeStamp: 234, touches: [{ pageX: 30, pageY: 10 }] }); 166 | swipe.push({ type: 'touchmove', timeStamp: 345, touches: [{ pageX: 31, pageY: 11 }] }); 167 | swipe.push({ type: 'touchend', timeStamp: 456, touches: [{ pageX: 35, pageY: 10}] }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/unit/views/application-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | moduleFor, 3 | test 4 | } from 'ember-qunit'; 5 | 6 | moduleFor('view:application', 'ApplicationView'); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | var view = this.subject(); 11 | assert.ok(view); 12 | }); 13 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cibernox/mobile-patterns/1041e661a802ed31bb714974aa79f77d1ef49f3b/vendor/.gitkeep -------------------------------------------------------------------------------- /workers/offline-support.js: -------------------------------------------------------------------------------- 1 | var CACHE_NAME = 'mobile-patterns-v1'; 2 | 3 | self.addEventListener('fetch', function(event) { 4 | var fetchRequest = event.request.clone(); 5 | var cacheRequest = event.request.clone(); 6 | 7 | // Respond with content from fetch or cache 8 | event.respondWith( 9 | 10 | // Try fetch 11 | fetch(fetchRequest) 12 | .then(function(response) { 13 | // when fetch is successful, we update the cache 14 | 15 | // A response is a stream and can be consumed only once. 16 | // Because we want the browser to consume the response, 17 | // as well as cache to consume the response, we need to 18 | // clone it so we have 2 streams 19 | var responseToCache = response.clone(); 20 | 21 | // and update the cache 22 | caches.open(self.CACHE_NAME).then(function(cache) { 23 | // Clone the request again to use it 24 | // as the key for our cache 25 | var cacheSaveRequest = event.request.clone(); 26 | cache.put(cacheSaveRequest, responseToCache); 27 | }); 28 | 29 | // Return the response stream to be consumed by browser 30 | return response; 31 | 32 | }).catch(function(err) { 33 | // when fetch times out or fails 34 | 35 | var cachedResponse = caches.match(cacheRequest); 36 | 37 | if (/\/api\//.test(cacheRequest.url)) { 38 | return cachedResponse.then(function(response) { 39 | return response.json().then(function(json) { 40 | json.meta = json.meta || {}; 41 | json.meta.offlineSupportWorker = true; 42 | var blob = new Blob([JSON.stringify(json)], { type: 'application/json' }) 43 | return new Response(blob, { headers: response.headers }); 44 | }) 45 | }); 46 | } else { 47 | return cachedResponse; 48 | } 49 | 50 | }) 51 | ); 52 | }); 53 | 54 | // Now we need to clean up resources in the previous versions 55 | // of Service Worker scripts 56 | self.addEventListener('activate', function(event) { 57 | // Destroy the cache 58 | event.waitUntil(caches.delete(self.CACHE_NAME)); 59 | }); 60 | --------------------------------------------------------------------------------