├── demo ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ └── 5.jpg ├── favicon.png ├── drag2.html ├── carousel.html ├── drag.html ├── modal2.html ├── overlay.html ├── modal1.html ├── accordion.html ├── scroll.html ├── touch.html ├── sidebarRight.html ├── forms.html ├── dropdown.html ├── tabs.html ├── toggle.html ├── sidebar.html ├── home.html ├── demo.css ├── swipe.html └── index.html ├── dist ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── css │ ├── mobile-angular-ui-migrate.min.css │ ├── mobile-angular-ui-migrate.css │ ├── mobile-angular-ui-desktop.min.css │ └── mobile-angular-ui-hover.min.css └── js │ └── mobile-angular-ui.migrate.min.js ├── src ├── less │ ├── mobile-angular-ui-migrate.less │ ├── lib │ │ ├── dropdown.less │ │ ├── layout.less │ │ ├── panel.less │ │ ├── justified.less │ │ ├── normalize.less │ │ ├── validations.less │ │ ├── carousel.less │ │ ├── list-groups.less │ │ ├── app.less │ │ ├── modal.less │ │ ├── globals.less │ │ ├── buttons.less │ │ ├── forms.less │ │ ├── scrollable.less │ │ ├── sections.less │ │ ├── switch.less │ │ ├── sidebar.less │ │ └── navbars.less │ ├── sm-grid.less │ ├── migrate │ │ └── overlay.less │ ├── mobile-angular-ui-desktop.less │ ├── mobile-angular-ui-base.less │ └── variables.less └── js │ ├── mobile-angular-ui.migrate.js │ ├── migrate │ ├── disabled.js │ ├── panels.js │ ├── switch.js │ ├── overlay.js │ ├── namespaceAliases.js │ ├── carousel.js │ ├── forms.js │ └── toggle.js │ ├── mobile-angular-ui.js │ ├── core │ ├── fastclick.js │ ├── activeLinks.js │ ├── touchmoveDefaults.js │ ├── outerClick.js │ └── capture.js │ ├── mobile-angular-ui.components.js │ ├── mobile-angular-ui.core.js │ ├── mobile-angular-ui.gestures.js │ ├── components │ ├── navbars.js │ ├── modals.js │ ├── switch.js │ ├── sidebars.js │ └── scrollable.js │ └── gestures │ └── swipe.js ├── test ├── components │ ├── switch-swipe.test.html │ ├── switch.test.html │ └── scrollable.test.html ├── gestures │ ├── drag.test.html │ ├── touch.test.html │ ├── swipe.test.html │ └── transform.test.html ├── firefox.conf.js ├── chrome.conf.js ├── all.conf.js ├── iphone5.conf.js ├── core │ ├── capture.test.html │ ├── sharedstate-interpolation.test.html │ ├── sharedstate-ui-cond.test.html │ └── sharedstate.test.html ├── layout.html ├── server.js ├── manual │ └── fastclick.html ├── htmlTests.js └── onprepare.js ├── README.md ├── LICENSE ├── bower.json ├── package.json ├── .gitignore ├── examples ├── tabs.html ├── lightbulb.html ├── drag.html └── clock.html └── ROADMAP.tasks /demo/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/img/1.jpg -------------------------------------------------------------------------------- /demo/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/img/2.jpg -------------------------------------------------------------------------------- /demo/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/img/3.jpg -------------------------------------------------------------------------------- /demo/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/img/4.jpg -------------------------------------------------------------------------------- /demo/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/img/5.jpg -------------------------------------------------------------------------------- /demo/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/demo/favicon.png -------------------------------------------------------------------------------- /dist/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/dist/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /dist/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/dist/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /dist/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functions/mobile-angular-ui/master/dist/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/less/mobile-angular-ui-migrate.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "bootstrap/less/mixins.less"; 3 | 4 | @import "migrate/overlay.less"; -------------------------------------------------------------------------------- /src/less/lib/dropdown.less: -------------------------------------------------------------------------------- 1 | // always displays modals 2 | // use ng-show/ng-hide or ng-if to 3 | // toggle and ng-animate to fade/animate 4 | 5 | .dropdown-menu { 6 | display: block; 7 | } -------------------------------------------------------------------------------- /src/less/lib/layout.less: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | width: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | body { 8 | height: 100%; 9 | width: 100%; 10 | overflow: hidden; 11 | } -------------------------------------------------------------------------------- /src/less/lib/panel.less: -------------------------------------------------------------------------------- 1 | a.panel-heading { 2 | display: block; 3 | } 4 | 5 | .panel-heading { 6 | >.btn, >.btn-group>.btn { 7 | margin-top: -(@padding-base-vertical + 2); 8 | margin-right: -15px; 9 | } 10 | } -------------------------------------------------------------------------------- /src/less/lib/justified.less: -------------------------------------------------------------------------------- 1 | // Justify Everything 2 | 3 | .justified { 4 | display: table; 5 | width: 100%; 6 | > * { 7 | display: table-cell; 8 | width: 1%; 9 | float: none !important; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/less/lib/normalize.less: -------------------------------------------------------------------------------- 1 | ng-view, 2 | ng-include, 3 | ng-transclude, 4 | ui-state { 5 | display: block; 6 | } 7 | 8 | a { 9 | cursor: pointer; 10 | } 11 | 12 | ng-view.app-content, 13 | .app-content>ng-view { 14 | height: 100%; 15 | } -------------------------------------------------------------------------------- /test/components/switch-swipe.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/less/lib/validations.less: -------------------------------------------------------------------------------- 1 | input.ng-invalid, textarea.ng-invalid { 2 | border-color: @brand-warning; 3 | } 4 | 5 | .ng-dirty input.ng-invalid { 6 | border-color: @brand-warning; 7 | } 8 | 9 | .ng-dirty input.ng-invalid ~ span.ko { 10 | color: @brand-danger; 11 | } 12 | -------------------------------------------------------------------------------- /test/gestures/drag.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/less/lib/carousel.less: -------------------------------------------------------------------------------- 1 | .carousel, .carousel-inner, .carousel-inner>.item { 2 | height: 100%; 3 | max-width: 100%; 4 | text-align: center; 5 | } 6 | 7 | .carousel-inner>.item>a>img, .carousel-inner>.item>img { 8 | max-width: 100%; 9 | max-height: 100%; 10 | margin: auto; 11 | } 12 | -------------------------------------------------------------------------------- /test/gestures/touch.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/firefox.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | directConnect: true, 3 | specs: [ 4 | 'htmlTests.js' 5 | ], 6 | multiCapabilities: [{ 7 | 'browserName': 'firefox' 8 | }], 9 | 10 | onPrepare: require('./onprepare'), 11 | rootElement: 'body', 12 | 13 | jasmineNodeOpts: { includeStackTrace: true } 14 | }; -------------------------------------------------------------------------------- /test/chrome.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | directConnect: true, 3 | chromeDriver: require('chromedriver').path, 4 | 5 | specs: [ 6 | 'htmlTests.js' 7 | ], 8 | multiCapabilities: [{ 9 | 'browserName': 'chrome' 10 | }], 11 | onPrepare: require('./onprepare'), 12 | rootElement: 'body', 13 | jasmineNodeOpts: { includeStackTrace: true } 14 | }; -------------------------------------------------------------------------------- /test/all.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | directConnect: true, 3 | chromeDriver: require('chromedriver').path, 4 | specs: [ 5 | 'htmlTests.js' 6 | ], 7 | 8 | multiCapabilities: [{ 9 | 'browserName': 'chrome' 10 | }, { 11 | 'browserName': 'firefox' 12 | }], 13 | 14 | onPrepare: require('./onprepare'), 15 | rootElement: 'body', 16 | 17 | jasmineNodeOpts: { includeStackTrace: true } 18 | }; 19 | -------------------------------------------------------------------------------- /test/iphone5.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | directConnect: true, 3 | chromeDriver: require('chromedriver').path, 4 | specs: [ 5 | 'htmlTests.js' 6 | ], 7 | multiCapabilities: [{ 8 | browserName: 'chrome', 9 | chromeOptions: { 10 | mobileEmulation: { 11 | deviceName: 'Apple iPhone 5' 12 | } 13 | } 14 | }], 15 | 16 | onPrepare: require('./onprepare'), 17 | rootElement: 'body', 18 | 19 | jasmineNodeOpts: { includeStackTrace: true } 20 | }; 21 | -------------------------------------------------------------------------------- /demo/drag2.html: -------------------------------------------------------------------------------- 1 |
2 | Drag 2 3 |
4 | 5 |
6 |
7 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /demo/carousel.html: -------------------------------------------------------------------------------- 1 |
2 | Carousel with $drag 3 |
4 | 5 | -------------------------------------------------------------------------------- /src/js/mobile-angular-ui.migrate.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('mobile-angular-ui.migrate', [ 5 | 'mobile-angular-ui.migrate.toggle', 6 | 'mobile-angular-ui.migrate.forms', 7 | 'mobile-angular-ui.migrate.panels', 8 | 'mobile-angular-ui.migrate.disabled', 9 | 'mobile-angular-ui.migrate.overlay', 10 | 'mobile-angular-ui.migrate.carousel', 11 | 'mobile-angular-ui.migrate.namespaceAliases', 12 | 'mobile-angular-ui.migrate.switch' 13 | ]); 14 | }()); -------------------------------------------------------------------------------- /src/less/lib/list-groups.less: -------------------------------------------------------------------------------- 1 | // List Groups 2 | .list-group-item, a.list-group-item { 3 | margin-bottom: 0; 4 | border: 0 none; 5 | border-bottom: 1px solid @list-group-border; 6 | 7 | &:first-child { 8 | border-top-right-radius: 0; 9 | border-top-left-radius: 0; 10 | } 11 | 12 | &:last-child { 13 | border-bottom-right-radius: 0; 14 | border-bottom-left-radius: 0; 15 | } 16 | 17 | .fa.pull-right, .icon.pull-right { 18 | line-height: @line-height-base; 19 | } 20 | } 21 | 22 | .list-group-item.media { 23 | margin-bottom: 0; 24 | margin-top: 0; 25 | } -------------------------------------------------------------------------------- /test/core/capture.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 |
12 | Ng Click 13 |
14 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/js/migrate/disabled.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('mobile-angular-ui.migrate.disabled', []).run([ 5 | '$document', function($document) { 6 | return angular.element($document).on("click tap", function(e) { 7 | var target; 8 | target = angular.element(e.target); 9 | if (target.hasClass("disabled")) { 10 | e.preventDefault(); 11 | e.stopPropagation(); 12 | target = null; 13 | return false; 14 | } else { 15 | target = null; 16 | return true; 17 | } 18 | }); 19 | } 20 | ]); 21 | 22 | }()); -------------------------------------------------------------------------------- /demo/drag.html: -------------------------------------------------------------------------------- 1 |
2 | Drag 3 |
4 | 5 |
6 |
7 | Drag right to dismiss notices! 8 |
9 |
10 |
11 |
12 |
15 | {{notice.message}} 16 |
17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/js/mobile-angular-ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mobile-angular-ui 3 | @position 0 4 | @description 5 | 6 | This is the main angular module of `mobile-angular-ui` framework. 7 | 8 | By requiring this module you will have all `mobile-angular-ui.core` 9 | and `mobile-angular-ui.components` features required as well. 10 | 11 | ## Usage 12 | 13 | Declare it as a dependency for your application: 14 | 15 | ``` js 16 | angular.module('myApp', ['mobile-angular-ui']); 17 | ``` 18 | 19 | */ 20 | (function() { 21 | 'use strict'; 22 | 23 | angular.module('mobile-angular-ui', [ 24 | 'mobile-angular-ui.core', 25 | 'mobile-angular-ui.components' 26 | ]); 27 | 28 | }()); -------------------------------------------------------------------------------- /demo/modal2.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/core/fastclick.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | var module = angular.module('mobile-angular-ui.core.fastclick', []); 4 | 5 | module.run(function() { 6 | if ('addEventListener' in document) { 7 | document.addEventListener('DOMContentLoaded', function() { 8 | FastClick.attach(document.body); 9 | }, false); 10 | } 11 | }); 12 | 13 | angular.forEach(['select', 'input', 'textarea'], function(directiveName){ 14 | module.directive(directiveName, function(){ 15 | return { 16 | restrict: 'E', 17 | compile: function(elem) { 18 | elem.addClass('needsclick'); 19 | } 20 | }; 21 | }); 22 | }); 23 | }()); -------------------------------------------------------------------------------- /src/less/lib/app.less: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | position: relative; 4 | z-index: @zindex-app; 5 | width: 100%; 6 | background: @app-bg; 7 | overflow: hidden; 8 | .translate3d(0,0,0); 9 | .transition-transform(~"400ms ease"); 10 | .box-shadow(@app-shadow); 11 | } 12 | 13 | .app-body { 14 | display: block; 15 | height: 100%; 16 | padding: 0; 17 | overflow: hidden; 18 | } 19 | 20 | .app-content { 21 | display: block; 22 | position: relative; 23 | height: 100%; 24 | width: 100%; 25 | padding: 0; 26 | overflow: hidden; 27 | } 28 | 29 | .app-search { 30 | position: relative; 31 | border-width: 0 0 1px; 32 | z-index: @zindex-app-search; 33 | width: 100%; 34 | } -------------------------------------------------------------------------------- /demo/overlay.html: -------------------------------------------------------------------------------- 1 |
2 | Overlays 3 |
4 | 5 |
6 |
7 |
8 | Show Modal 11 | Show Overlay 14 |
15 | 16 |
17 |

18 | {{lorem}} 19 |

20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /demo/modal1.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mobile Angular UI 2 | 3 | ## Angular & Bootstrap 3 for Mobile web and applications 4 | 5 | Mobile Angular UI is an HTML5 mobile UI framework that will let you use Angular Js and Bootstrap 3 for mobile app development. 6 | 7 | GettingStarted, Demo, Docs at http://mobileangularui.com. 8 | 9 | ![](http://mobileangularui.com/assets/img/phone.png) 10 | 11 | ### Support development 12 | 13 | Click here to lend your support to: Mobile Angular UI and make a donation at pledgie.com ! 14 | 15 | Released Under [MIT License](https://github.com/mcasimir/mobile-angular-ui/blob/master/LICENSE). 16 | -------------------------------------------------------------------------------- /src/less/lib/modal.less: -------------------------------------------------------------------------------- 1 | // always displays modals 2 | // use ng-show/ng-hide or ng-if to 3 | // toggle and ng-animate to fade/animate 4 | 5 | .modal { 6 | display: block; 7 | } 8 | 9 | .modal-dialog { 10 | z-index: @zindex-modal; 11 | } 12 | 13 | .modal-overlay { 14 | background: @overlay-background; 15 | .modal-content, .modal-dialog { 16 | background: none; 17 | .box-shadow(none); 18 | border: none; 19 | } 20 | } 21 | 22 | .has-modal, .has-modal-overlay { 23 | .app, .sidebar { 24 | pointer-events: none; 25 | } 26 | } 27 | 28 | .has-modal-overlay { 29 | .app, .sidebar { 30 | -webkit-filter: blur(@overlay-blur); 31 | -ms-filter: blur(@overlay-blur); // hope someday will be used 32 | filter: blur(@overlay-blur); 33 | } 34 | } -------------------------------------------------------------------------------- /dist/css/mobile-angular-ui-migrate.min.css: -------------------------------------------------------------------------------- 1 | [overlay],[data-overlay],.overlay{display:none}.overlay{position:absolute;left:0;top:0;bottom:0;right:0}.overlay .overlay-inner{width:100%;height:100%;position:relative;overflow:hidden}.overlay .overlay-inner>.overlay-background{position:absolute;left:0;top:0;bottom:0;right:0;margin:-5px;background:rgba(255,255,255,.95);z-index:1039}.overlay .overlay-inner>.overlay-dismiss{position:absolute;right:10px;top:10px;z-index:1041;font-size:24px}.overlay .overlay-inner>.overlay-content{width:100%;box-sizing:border-box;display:table;background:0 0;text-align:center;color:#000;height:100%;z-index:1040;position:absolute;left:0;top:0;bottom:0;right:0;padding:20px}.overlay .overlay-inner>.overlay-content>.overlay-body{display:table-cell;vertical-align:middle}.overlay-show{display:block} -------------------------------------------------------------------------------- /src/less/lib/globals.less: -------------------------------------------------------------------------------- 1 | // Globals 2 | 3 | html { 4 | height: 100%; 5 | width: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | body { 10 | height: 100%; 11 | width: 100%; 12 | overflow: hidden; 13 | font-weight: @font-weight; 14 | } 15 | 16 | a { 17 | text-decoration: none; 18 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 19 | color: @link-color; 20 | } 21 | 22 | a:active, .btn:active { 23 | .opacity(@active-link-opacity); 24 | .box-shadow(none); 25 | } 26 | 27 | a:focus { 28 | outline: 0 none; 29 | } 30 | 31 | h1,h2,h3,h4,h5,h6 { 32 | margin-top: 0; 33 | padding-top: 0; 34 | } 35 | 36 | .page-header { 37 | margin-top: @page-header-margin-top; 38 | h1,h2,h3,h4,h5,h6 { 39 | margin-bottom: 0; 40 | padding-bottom: 0; 41 | } 42 | } 43 | 44 | img { 45 | max-width: 100%; 46 | } 47 | -------------------------------------------------------------------------------- /demo/accordion.html: -------------------------------------------------------------------------------- 1 |
2 | Accordion 3 |
4 | 5 |
6 |
7 |
8 |
12 | 13 |
14 |
15 | 16 |

17 | Collapsible Group Item #{{i}} 18 |

19 |
20 | 21 |
22 |
23 | {{lorem}} 24 |
25 |
26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /demo/scroll.html: -------------------------------------------------------------------------------- 1 |
2 | Scroll 3 |
4 | 5 |
6 | 11 | 12 |
13 | 18 |
19 | 20 | 21 | 22 | Show 23 | Hide 24 | Search Bar 25 | 26 |
-------------------------------------------------------------------------------- /src/less/lib/buttons.less: -------------------------------------------------------------------------------- 1 | // Buttons 2 | 3 | .btn { 4 | &.active { 5 | &, &:hover, &:focus, &:active { 6 | .box-shadow(none); 7 | } 8 | } 9 | } 10 | 11 | 12 | 13 | .btn-group { 14 | 15 | &.justified > .btn { 16 | &:not(:last-child) { // Trick for IE8: IE8 should ignore :not thus rendering the 1px border anyway 17 | border-right-width: 0; 18 | } 19 | 20 | &:last-child, &.active { 21 | border-right-width: 1px; 22 | } 23 | 24 | &:not(:last-child).active+.btn { 25 | border-left-width: 0; 26 | } 27 | } 28 | 29 | &.nav-tabs { 30 | border: 0 none; 31 | } 32 | 33 | >.btn-default.active, 34 | >.btn-default.active:hover, 35 | >.btn-default.active:focus { 36 | color: @btn-primary-color; 37 | background-color: @btn-primary-bg; 38 | border-color: @btn-primary-border; 39 | } 40 | } -------------------------------------------------------------------------------- /src/less/lib/forms.less: -------------------------------------------------------------------------------- 1 | // Forms 2 | 3 | textarea, 4 | input[type="email"], 5 | input[type="number"], 6 | input[type="search"], 7 | input[type="tel"], 8 | input[type="text"], 9 | input[type="password"], 10 | input[type="url"] { 11 | -webkit-appearance: none; 12 | } 13 | 14 | .form-control:focus { 15 | outline: 0; 16 | .box-shadow(none); 17 | } 18 | 19 | .form-control { 20 | .box-shadow(none); 21 | border: 1px solid @input-border; 22 | } 23 | 24 | .form-group { 25 | margin-bottom: 0; 26 | padding: @line-height-computed/2 0; 27 | } 28 | 29 | .input-group-addon { 30 | background: @input-bg; 31 | } 32 | 33 | .control-label { 34 | line-height: @input-height-base; 35 | margin: 0; 36 | } 37 | 38 | .app-search { 39 | border-radius: @app-search-border-radius; 40 | } 41 | 42 | .form-actions { 43 | margin-top: @form-actions-margin-top; 44 | } 45 | -------------------------------------------------------------------------------- /src/js/migrate/panels.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var module = angular.module('mobile-angular-ui.migrate.panels', []); 5 | 6 | // 7 | // Old panel helpers 8 | // 9 | module.directive("bsPanel", function() { 10 | return { 11 | restrict: 'EA', 12 | replace: true, 13 | scope: false, 14 | transclude: true, 15 | link: function(scope, elem, attrs) { 16 | elem.removeAttr('title'); 17 | }, 18 | template: function(elems, attrs) { 19 | var heading = ""; 20 | if (attrs.title) { 21 | heading = "
\n

\n " + attrs.title + "\n

\n
"; 22 | } 23 | return "
\n " + heading + "\n
\n
\n
\n
"; 24 | } 25 | }; 26 | }); 27 | 28 | }()); -------------------------------------------------------------------------------- /demo/touch.html: -------------------------------------------------------------------------------- 1 |
2 | Touch 3 |
4 | 5 |
6 |
7 | 18 |
19 |
20 |

Touch Me!

21 |
22 |
23 | -------------------------------------------------------------------------------- /src/less/lib/scrollable.less: -------------------------------------------------------------------------------- 1 | // Scrollable 2 | 3 | .scrollable { 4 | position: absolute; 5 | width: 100%; 6 | height: 100%; 7 | top: 0; 8 | bottom: 0; 9 | left: 0; 10 | right: 0; 11 | } 12 | 13 | .scrollable-header, .scrollable-footer { 14 | position: absolute; 15 | width: 100%; 16 | left: 0; 17 | right: 0; 18 | z-index: @zindex-navbar; 19 | display: block; 20 | border-radius: 0; 21 | border-left: 0; 22 | border-right: 0; 23 | } 24 | 25 | .scrollable-header { 26 | top: 0; 27 | } 28 | 29 | .scrollable-footer { 30 | bottom: 0; 31 | } 32 | 33 | .scrollable-content { 34 | width: 100%; 35 | height: 100%; 36 | overflow: auto; 37 | -webkit-overflow-scrolling: touch; 38 | } 39 | 40 | // deprecated! 41 | 42 | .sidebar-header+.scrollable,.app-name+.scrollable{ 43 | padding-top: @sidebar-header-height; 44 | } 45 | 46 | .app-search + .scrollable { 47 | padding-top: @input-height-base; 48 | } -------------------------------------------------------------------------------- /src/less/sm-grid.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "bootstrap/less/mixins.less"; 3 | 4 | @media (min-width: @screen-sm-min) { 5 | .make-grid(sm); 6 | } 7 | 8 | 9 | @media (min-width: @screen-xs-min) { 10 | .sidebar-left { 11 | width: @sidebar-left-width-xs; 12 | } 13 | .sidebar-right { 14 | width: @sidebar-right-width-xs; 15 | } 16 | .sidebar-left-in .app { 17 | .translate3d(@sidebar-left-width-xs, 0, 0); 18 | } 19 | .sidebar-right-in .app { 20 | .translate3d(-@sidebar-right-width-xs, 0, 0); 21 | } 22 | } 23 | 24 | @media (min-width: @screen-sm-min) { 25 | .sidebar-left { 26 | width: @sidebar-left-width-sm; 27 | } 28 | .sidebar-right { 29 | width: @sidebar-right-width-sm; 30 | } 31 | .sidebar-left-in .app { 32 | .translate3d(@sidebar-left-width-sm, 0, 0); 33 | } 34 | .sidebar-right-in .app { 35 | .translate3d(-@sidebar-right-width-sm, 0, 0); 36 | } 37 | } 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/js/mobile-angular-ui.components.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mobile-angular-ui.components 3 | 4 | @description 5 | 6 | It has directives and services providing mobile friendly 7 | components like navbars and sidebars. 8 | It requires `mobile-angular-ui.base.css` 9 | in order to work properly. 10 | 11 | ## Standalone Usage 12 | 13 | Although `.components` module is required by `mobile-angular-ui` by default 14 | you can use it alone. Some submodules requires `mobile-angular-ui.core` to work, 15 | so be sure its sources are available. 16 | 17 | ``` js 18 | angular.module('myApp', ['mobile-angular-ui.components']); 19 | ``` 20 | 21 | */ 22 | (function() { 23 | 'use strict'; 24 | 25 | angular.module('mobile-angular-ui.components', [ 26 | 'mobile-angular-ui.components.modals', 27 | 'mobile-angular-ui.components.navbars', 28 | 'mobile-angular-ui.components.sidebars', 29 | 'mobile-angular-ui.components.scrollable', 30 | 'mobile-angular-ui.components.switch' 31 | ]); 32 | }()); -------------------------------------------------------------------------------- /src/js/migrate/switch.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | angular.module('mobile-angular-ui.migrate.switch', []) 4 | .directive("switch", function() { 5 | return { 6 | restrict: "EA", 7 | replace: true, 8 | scope: { 9 | model: "=ngModel", 10 | changeExpr: "@ngChange", 11 | disabled: "@" 12 | }, 13 | template: "
", 14 | link: function(scope, elem, attrs) { 15 | 16 | elem.on('click tap', function(){ 17 | if (attrs.disabled === null || attrs.disabled === undefined) { 18 | scope.model = !scope.model; 19 | scope.$apply(); 20 | 21 | if (scope.changeExpr !== null && scope.changeExpr !== undefined) { 22 | scope.$parent.$eval(scope.changeExpr); 23 | } 24 | } 25 | }); 26 | 27 | elem.addClass('switch-transition-enabled'); 28 | } 29 | }; 30 | }); 31 | }()); -------------------------------------------------------------------------------- /demo/sidebarRight.html: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | /* global __dirname: false, module: true */ 2 | 3 | var glob = require('glob'), 4 | path = require('path'), 5 | url = require('url'), 6 | fs = require('fs'), 7 | cheerio = require('cheerio'), 8 | layoutHtml = fs.readFileSync(path.resolve(__dirname, 'layout.html')), 9 | testsDoms = {}, 10 | testFiles = glob.sync(path.resolve(__dirname, '**/*.test.html')); 11 | 12 | for (var i = 0; i < testFiles.length; i++) { 13 | var abs = testFiles[i]; 14 | var key = path.relative(__dirname, abs).replace(/\.test\.html$/, ''); 15 | testsDoms['/' + key] = cheerio.load(fs.readFileSync(abs), {decodeEntities: false}); 16 | } 17 | 18 | module.exports = function(req, res, next) { 19 | var parsedUrl = url.parse(req.url, true); 20 | 21 | var test = testsDoms[parsedUrl.pathname]; 22 | if (!test) { return next(); } 23 | 24 | var layout = cheerio.load(layoutHtml, {decodeEntities: false}); 25 | 26 | layout('head').append(test('head').contents().clone()); 27 | layout('body').append(test('body').contents().clone()); 28 | 29 | res.write(layout.html()); 30 | res.end(); 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 mcasimir (https://github.com/mcasimir) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/less/lib/sections.less: -------------------------------------------------------------------------------- 1 | .section { 2 | .container-fluid; 3 | background: @body-bg; 4 | padding-top: floor(@line-height-computed / 2); 5 | padding-bottom: floor(@line-height-computed / 2); 6 | } 7 | 8 | .section-wide { 9 | padding-left: 0; 10 | padding-right: 0; 11 | } 12 | 13 | .section.section-condensed { 14 | padding-top: 0; padding-bottom: 0; 15 | } 16 | 17 | .section.section-break { 18 | margin-bottom: @line-height-computed; 19 | .box-shadow(~"0 -1px rgba(0, 0, 0, 0.08), 0 1px rgba(0, 0, 0, 0.07), 0 1px 1px rgba(0, 0, 0, 0.05)"); 20 | } 21 | 22 | .section-default { 23 | background-color: @label-default-bg; 24 | color: @label-color; 25 | } 26 | 27 | .section-primary { 28 | background-color: @label-primary-bg; 29 | color: @label-color; 30 | } 31 | 32 | .section-success { 33 | background-color: @label-success-bg; 34 | color: @label-color; 35 | } 36 | 37 | .section-info { 38 | background-color: @label-info-bg; 39 | color: @label-color; 40 | } 41 | 42 | .section-warning { 43 | background-color: @label-warning-bg; 44 | color: @label-color; 45 | } 46 | 47 | .section-danger { 48 | background-color: @label-danger-bg; 49 | color: @label-color; 50 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-angular-ui", 3 | "homepage": "https://github.com/mcasimir/mobile-angular-ui", 4 | "authors": [ 5 | "mcasimir " 6 | ], 7 | "version": "1.2.0", 8 | "description": "Mobile UI for Bootstrap 3 and Angular JS", 9 | "main": [ 10 | "dist/css/mobile-angular-ui-hover.css", 11 | "dist/css/mobile-angular-ui-base.css", 12 | "dist/css/mobile-angular-ui-desktop.css", 13 | "dist/js/mobile-angular-ui.js", 14 | "dist/js/mobile-angular-ui.gestures.js", 15 | "dist/fonts/fontawesome-webfont.eot", 16 | "dist/fonts/fontawesome-webfont.svg", 17 | "dist/fonts/fontawesome-webfont.ttf", 18 | "dist/fonts/fontawesome-webfont.woff" 19 | ], 20 | "keywords": [ 21 | "mobile-angular-ui", 22 | "angularjs", 23 | "boostrap3", 24 | "angular-mobile", 25 | "mobile-ui", 26 | "boostrap3-mobile" 27 | ], 28 | "license": "MIT", 29 | "ignore": [ 30 | "**/.*", 31 | "node_modules", 32 | "bower_components", 33 | "test", 34 | "ROADMAP.tasks" 35 | ], 36 | "devDependencies": { 37 | "font-awesome": "4", 38 | "bootstrap": "3", 39 | "fastclick": "1", 40 | "overthrow": "0.7" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo/forms.html: -------------------------------------------------------------------------------- 1 |
2 | Forms 3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | Login 11 |
12 | 13 | 17 |
18 | 19 |
20 | 21 | 24 |
25 | 26 |
27 | 28 | 30 |
31 |
32 |
33 | 34 | 37 | 38 |
39 | Login 40 |
41 | 42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /src/js/mobile-angular-ui.core.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @module mobile-angular-ui.core 4 | 5 | @description 6 | 7 | It has all the core functionalities of Mobile Angular UI. It aims to act as a common base 8 | for an UI framework providing services and directives to create components and implement 9 | UI interactions with angular. 10 | 11 |
12 | NOTE 13 |
    14 |
  • It has no dependency on Bootstrap.
  • 15 |
  • It is not related to mobile apps only.
  • 16 |
  • It is not requiring CSS support.
  • 17 |
  • You can use it on any Angular Application and with any CSS framework.
  • 18 |
19 |
20 | 21 | ## Standalone Usage 22 | 23 | Although `.core` module is required by `mobile-angular-ui` by default you can use it alone. 24 | 25 | ``` js 26 | angular.module('myApp', ['mobile-angular-ui.core']); 27 | ``` 28 | 29 | */ 30 | (function () { 31 | 'use strict'; 32 | angular.module('mobile-angular-ui.core', [ 33 | 'mobile-angular-ui.core.fastclick', 34 | 'mobile-angular-ui.core.activeLinks', 35 | 'mobile-angular-ui.core.capture', 36 | 'mobile-angular-ui.core.outerClick', 37 | 'mobile-angular-ui.core.sharedState', 38 | 'mobile-angular-ui.core.touchmoveDefaults' 39 | ]); 40 | }()); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-angular-ui", 3 | "version": "1.2.0", 4 | "description": "Mobile Angular UI", 5 | "main": "dist/mobile-angular-ui.js", 6 | "directories": { 7 | "demo": "demo", 8 | "dist": "dist" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "bower install" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/mcasimir/mobile-angular-ui.git" 17 | }, 18 | "author": "", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/mcasimir/mobile-angular-ui/issues" 22 | }, 23 | "devDependencies": { 24 | "cheerio": "^0.18.0", 25 | "chromedriver": "^2.12.0", 26 | "del": "^1.1.1", 27 | "glob": "^4.3.2", 28 | "gulp": "^3.8.10", 29 | "gulp-concat": "^2.4.3", 30 | "gulp-connect": "^2.2.0", 31 | "gulp-csso": "^0.2.9", 32 | "gulp-jshint": "^1.9.0", 33 | "gulp-less": "^2.0.1", 34 | "gulp-mobilizer": "0.0.3", 35 | "gulp-protractor": "0.0.12", 36 | "gulp-release-tasks": "0.0.3", 37 | "gulp-rename": "^1.2.0", 38 | "gulp-sourcemaps": "^1.3.0", 39 | "gulp-uglify": "^1.0.2", 40 | "minimatch": "^2.0.1", 41 | "run-sequence": "^1.0.2", 42 | "slug": "^0.8.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /dist/css/mobile-angular-ui-migrate.css: -------------------------------------------------------------------------------- 1 | [overlay], 2 | [data-overlay] { 3 | display: none; 4 | } 5 | .overlay { 6 | display: none; 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | bottom: 0; 11 | right: 0; 12 | } 13 | .overlay .overlay-inner { 14 | width: 100%; 15 | height: 100%; 16 | position: relative; 17 | overflow: hidden; 18 | } 19 | .overlay .overlay-inner > .overlay-background { 20 | position: absolute; 21 | left: 0; 22 | top: 0; 23 | bottom: 0; 24 | right: 0; 25 | margin: -5px -5px -5px -5px; 26 | background: rgba(255, 255, 255, 0.95); 27 | z-index: 1039; 28 | } 29 | .overlay .overlay-inner > .overlay-dismiss { 30 | position: absolute; 31 | right: 10px; 32 | top: 10px; 33 | z-index: 1041; 34 | font-size: 24px; 35 | } 36 | .overlay .overlay-inner > .overlay-content { 37 | width: 100%; 38 | box-sizing: border-box; 39 | display: table; 40 | background: transparent; 41 | text-align: center; 42 | color: #000000; 43 | height: 100%; 44 | z-index: 1040; 45 | position: absolute; 46 | left: 0; 47 | top: 0; 48 | bottom: 0; 49 | right: 0; 50 | padding: 20px; 51 | } 52 | .overlay .overlay-inner > .overlay-content > .overlay-body { 53 | display: table-cell; 54 | vertical-align: middle; 55 | } 56 | .overlay-show { 57 | display: block; 58 | } 59 | -------------------------------------------------------------------------------- /src/less/lib/switch.less: -------------------------------------------------------------------------------- 1 | ui-switch { 2 | display: block; 3 | } 4 | 5 | .switch { 6 | cursor: pointer; 7 | position: relative; 8 | display: block; 9 | width: @switch-width; 10 | height: @switch-height; 11 | background-color: @input-bg; 12 | border: 1px solid @input-border; 13 | border-radius: @switch-border-radius; 14 | 15 | &[disabled] { 16 | cursor: default; 17 | .opacity(0.4); 18 | } 19 | 20 | // Sliding handle 21 | .switch-handle { 22 | position: absolute; 23 | top: -1px; 24 | left: -1px; 25 | z-index: @zindex-switch-handle; 26 | width: @switch-handle-diameter; 27 | height: @switch-handle-diameter; 28 | background-color: @input-bg; 29 | border: 1px solid @input-border; 30 | border-radius: @switch-handle-border-radius; 31 | } 32 | 33 | 34 | // Active state for switch 35 | &.active { 36 | background-color: @switch-active-color; 37 | border: 1px solid @switch-active-color; 38 | 39 | .switch-handle { 40 | border-color: @switch-active-color; 41 | .translate(@switch-handle-diameter,0); 42 | } 43 | } 44 | } 45 | 46 | .switch-transition-enabled { 47 | .transition(~"background-color 200ms ease, border 200ms ease"); 48 | } 49 | 50 | .switch-transition-enabled>.switch-handle{ 51 | .transition-transform(~"200ms ease, background-color 200ms ease"); 52 | } -------------------------------------------------------------------------------- /demo/dropdown.html: -------------------------------------------------------------------------------- 1 |
2 | Dropdown 3 |
4 | 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 | 31 |
32 |

Note

33 |
34 |
35 | {{lorem}} 36 |
37 |
38 | 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /src/js/mobile-angular-ui.gestures.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mobile-angular-ui.gestures 3 | @memberof! 4 | @position 100 5 | @description 6 | 7 | It has directives and services to support `touch`, `swipe` and `drag` gestures. 8 | 9 | It does not need any `.css` to work. 10 | 11 |
12 |

13 | This module will not work with `ngTouch` cause it is intended, among offering more features, to be a drop-in replacement for it. 14 |

15 |

16 | Be aware that `ngTouch` is still not playing well with `fastclick.js` and its usage with `mobile-angular-ui` is currently discouraged anyway. 17 |

18 |
19 | 20 | ## Usage 21 | 22 | `.gestures` module is not required by `mobile-angular-ui` module. It has no dependency on other modules and is intended to be used alone with any other angular framework. 23 | 24 | You have to include `mobile-angular-ui.gestures.min.js` to your project in order to use it. Ie. 25 | 26 | ``` html 27 | 28 | ``` 29 | 30 | ``` js 31 | angular.module('myApp', ['mobile-angular-ui.gestures']); 32 | ``` 33 | 34 | */ 35 | (function () { 36 | 'use strict'; 37 | 38 | angular.module('mobile-angular-ui.gestures', [ 39 | 'mobile-angular-ui.gestures.drag', 40 | 'mobile-angular-ui.gestures.swipe', 41 | 'mobile-angular-ui.gestures.transform' 42 | ]); 43 | 44 | }()); -------------------------------------------------------------------------------- /src/less/lib/sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | z-index: 0; 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | display: none; 7 | .translate3d(0, 0, 0); 8 | .transition-transform(~"400ms ease"); 9 | pointer-events: none; 10 | } 11 | 12 | .sidebar-left { 13 | background: @sidebar-left-background; 14 | width: @sidebar-left-width; 15 | left: 0; 16 | right: auto; 17 | .transition(~"width 400ms ease"); 18 | } 19 | 20 | .sidebar-right { 21 | background: @sidebar-right-background; 22 | width: @sidebar-right-width; 23 | left: auto; 24 | right: 0; 25 | .transition(~"width 400ms ease"); 26 | } 27 | 28 | .sidebar-left-visible .sidebar-left, .sidebar-right-visible .sidebar-right { 29 | display: block; 30 | } 31 | 32 | .sidebar-left-in .sidebar-left, .sidebar-right-in .sidebar-right { 33 | pointer-events: auto; 34 | } 35 | 36 | .sidebar-left-in .app { 37 | width: 100%; 38 | .translate(@sidebar-left-width, 0); 39 | .transition-transform(~"400ms ease"); 40 | } 41 | 42 | .sidebar-right-in .app { 43 | width: 100%; 44 | .translate(-@sidebar-right-width, 0); 45 | .transition-transform(~"400ms ease"); 46 | } 47 | 48 | .sidebar-header, .app-name { 49 | position: relative; 50 | width: 100%; 51 | line-height: @sidebar-header-height; 52 | padding: @sidebar-header-padding; 53 | font-size: @sidebar-header-font-size; 54 | color: @sidebar-header-color; 55 | margin: 0; 56 | z-index: @zindex-sidebar-header; 57 | } -------------------------------------------------------------------------------- /test/core/sharedstate-interpolation.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 |
11 | 12 |
13 |
Test
14 |
15 |
16 | 17 | 18 | 23 | 24 | 28 | 29 | 33 | 34 | 38 | 39 | 43 | -------------------------------------------------------------------------------- /demo/tabs.html: -------------------------------------------------------------------------------- 1 |
2 | Tabs 3 |
4 | 5 |
6 |
7 |
8 | 9 | 10 | 21 | 22 |
23 | 24 |

{{lorem}}

25 |
26 | 27 |
28 | 29 |

{{lorem}}

30 |
31 | 32 |
33 | 34 |

{{lorem}}

35 |
36 | 37 | 47 |
48 | 49 |
50 |
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by http://gitignore.io 2 | 3 | tmp 4 | 5 | ### OSX ### 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | Icon 10 | 11 | logo.ai 12 | 13 | gh-pages 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear on external disk 19 | .Spotlight-V100 20 | .Trashes 21 | 22 | ### Windows ### 23 | # Windows image file caches 24 | Thumbs.db 25 | ehthumbs.db 26 | 27 | # Folder config file 28 | Desktop.ini 29 | 30 | # Recycle Bin used on file shares 31 | $RECYCLE.BIN/ 32 | 33 | ### Linux ### 34 | .* 35 | !.gitignore 36 | !.git* 37 | *~ 38 | 39 | 40 | ### Eclipse ### 41 | *.pydevproject 42 | .project 43 | .metadata 44 | bin/** 45 | tmp/** 46 | tmp/**/* 47 | *.tmp 48 | *.bak 49 | *.swp 50 | *~.nib 51 | local.properties 52 | .classpath 53 | .settings/ 54 | .loadpath 55 | 56 | # External tool builders 57 | .externalToolBuilders/ 58 | 59 | # Locally stored "Eclipse launch configurations" 60 | *.launch 61 | 62 | # CDT-specific 63 | .cproject 64 | 65 | # PDT-specific 66 | .buildpath 67 | 68 | ### TextMate ### 69 | *.tmproj 70 | *.tmproject 71 | tmtags 72 | 73 | ### SublimeText ### 74 | # SublimeText project files 75 | *.sublime-workspace 76 | 77 | ### Node ### 78 | lib-cov 79 | *.seed 80 | *.log 81 | *.csv 82 | *.dat 83 | *.out 84 | *.pid 85 | *.gz 86 | 87 | pids 88 | logs 89 | results 90 | 91 | npm-debug.log 92 | node_modules 93 | 94 | ### Bower ### 95 | bower_components 96 | 97 | experiments 98 | site/output 99 | site/tmp 100 | 101 | site/crash.log 102 | site/Gemfile.lock 103 | 104 | TODO.md -------------------------------------------------------------------------------- /demo/toggle.html: -------------------------------------------------------------------------------- 1 |
2 | Toggle 3 |
4 | 5 |
6 |
7 |
8 | 9 |

12 | 13 | 14 |

15 | 16 | 36 | 37 |
38 | 39 |
ExclusionGroup example
40 | 41 |
42 | 1 43 | 2 44 | 3 45 |
46 | 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /demo/sidebar.html: -------------------------------------------------------------------------------- 1 |
2 |

Mobile Angular UI 1.2

3 |
4 |
5 | Home 6 | Scroll 7 | Toggle 8 | Tabs 9 | Accordion 10 | Overlay 11 | Forms 12 | Dropdown 13 | Touch 14 | Swipe 15 | Drag 1 16 | Drag 2 17 | Drag 3 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/js/migrate/overlay.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var module = angular.module('mobile-angular-ui.migrate.overlay', []); 4 | module.directive('overlay', ['$compile', function($compile) { 5 | 6 | return { 7 | compile: function(tElem, tAttrs) { 8 | var rawContent = tElem.html(); 9 | tElem.remove(); 10 | 11 | return function postLink(scope, elem, attrs) { 12 | var active = ""; 13 | var body = rawContent; 14 | var id = attrs.overlay; 15 | 16 | if (attrs["default"]) { 17 | active = "default='" + attrs["default"] + "'"; 18 | } 19 | 20 | var html = "
\n
\n
\n \n \n \n
\n
\n " + body + "\n
\n
\n
\n
"; 21 | 22 | var sameId = angular.element(document.getElementById(id)); 23 | 24 | if (sameId.length > 0 && sameId.hasClass('overlay')) { 25 | sameId.remove(); 26 | } 27 | 28 | body = angular.element(document.body); 29 | body.prepend($compile(html)(scope)); 30 | 31 | if (attrs["default"] === "active") { 32 | body.addClass('overlay-in'); 33 | } 34 | }; 35 | } 36 | }; 37 | 38 | }]); 39 | }()); -------------------------------------------------------------------------------- /src/less/migrate/overlay.less: -------------------------------------------------------------------------------- 1 | [overlay],[data-overlay] { 2 | display: none; 3 | } 4 | 5 | .overlay { 6 | display: none; 7 | position: absolute; 8 | left:0; 9 | top:0; 10 | bottom: 0; 11 | right: 0; 12 | 13 | .overlay-inner { 14 | width: 100%; 15 | height: 100%; 16 | position: relative; 17 | overflow: hidden; 18 | >.overlay-background { 19 | position: absolute; 20 | left:0; 21 | top:0; 22 | bottom: 0; 23 | right: 0; 24 | margin: @overlay-margin; 25 | background: @overlay-background; 26 | z-index: @zindex-modal - 1; 27 | } 28 | >.overlay-dismiss { 29 | position: absolute; 30 | right: @overlay-dismiss-margin-right; 31 | top: @overlay-dismiss-margin-top; 32 | z-index: @zindex-modal + 1; 33 | font-size: @overlay-dismiss-font-size; 34 | } 35 | >.overlay-content { 36 | width: 100%; 37 | box-sizing: border-box; 38 | display: table; 39 | background: transparent; 40 | text-align: center; 41 | color: @overlay-content-color; 42 | height: 100%; 43 | z-index: @zindex-modal; 44 | position: absolute; 45 | left:0; 46 | top:0; 47 | bottom: 0; 48 | right: 0; 49 | padding: @overlay-content-padding; 50 | >.overlay-body { 51 | display: table-cell; 52 | vertical-align: middle; 53 | } 54 | } 55 | 56 | } 57 | } 58 | 59 | .overlay-show { 60 | display: block; 61 | } -------------------------------------------------------------------------------- /src/less/lib/navbars.less: -------------------------------------------------------------------------------- 1 | .navbar-app { 2 | min-height: @navbar-app-height; 3 | border-color: @navbar-default-border; 4 | background: @navbar-default-bg; 5 | border-width: 1px; 6 | border-style: solid; 7 | color: @navbar-default-color; 8 | 9 | .btn-icon-only .fa, .btn-icon-only .icon { 10 | font-size: @btn-icon-only-font-size; 11 | vertical-align: middle; 12 | } 13 | 14 | .btn-navbar, .btn { 15 | background: none; 16 | border: none; 17 | line-height: @navbar-app-height; 18 | padding-top: 0; 19 | padding-bottom: 0; 20 | color: @navbar-default-link-color; 21 | text-transform: uppercase; 22 | &:hover { 23 | color: @navbar-default-link-color; 24 | } 25 | } 26 | 27 | } 28 | 29 | .navbar-brand-center { 30 | position: absolute; 31 | text-align: center; 32 | width: 100%; 33 | white-space: nowrap; 34 | overflow: hidden; 35 | -o-text-overflow: ellipsis; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | // Top and bottom navbars with absolute positioning, 40 | // they are necessary to create scroll areas and fake fixed positioning 41 | // without glitches. 42 | 43 | .navbar-absolute-top, .navbar-absolute-bottom { 44 | position: absolute; 45 | z-index: @zindex-navbar; 46 | border-width: 0; 47 | left: 0; 48 | right: 0; 49 | width: 100%; 50 | margin: 0; 51 | } 52 | 53 | .has-navbar-top .app-body { 54 | padding-top: @navbar-app-height + 1; 55 | } 56 | 57 | .has-navbar-bottom .app-body { 58 | padding-bottom: @navbar-app-height + 1; 59 | } 60 | 61 | .navbar-absolute-top { 62 | top:0; 63 | border-bottom-width: 1px; 64 | } 65 | 66 | .navbar-absolute-bottom { 67 | top: auto; 68 | bottom: 0; 69 | border-bottom: 0 none; 70 | border-top-width: 1px; 71 | } 72 | -------------------------------------------------------------------------------- /examples/tabs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tabs component 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 34 | 35 |
36 | 37 |

Content 1

38 |
39 | 40 |
41 | 42 |

Content 2

43 |
44 | 45 |
46 | 47 |

Content 3

48 |
49 |
50 |
51 |
52 | 53 | -------------------------------------------------------------------------------- /examples/lightbulb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom components and SharedState 8 | 9 | 17 | 18 | 19 | 22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 |
31 | 32 |
33 |
34 | 35 | 55 | 56 | 57 |
58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /demo/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Mobile Angular UI 7 | Demo 1.2 8 |

9 |
10 |
11 |
12 | 13 |
14 |
15 |

Play around a little

16 | Open the sidebar and start testing features 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |

Test responsiveness and speed

27 | Scroll, tap, navigate, test that everything is smooth 28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 |
36 |
37 |

Resize your browser

38 | Stretch and squeeze your browser window to see both mobile and desktop versions. 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 |

Submit an issue

48 |

Please submit any issue you'll find, reporting your user agent string:
49 |

50 |
{{userAgent}}
51 |

52 | It would help a lot! 53 |

54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 | -------------------------------------------------------------------------------- /src/js/migrate/namespaceAliases.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var module = angular.module('mobile-angular-ui.migrate.namespaceAliases', []); 5 | 6 | var uncamelize = function(text) { 7 | var separator = "-"; 8 | 9 | text = text.replace(/[A-Z]/g, function (letter) { 10 | return separator + letter.toLowerCase(); 11 | }); 12 | 13 | return text.replace("/^" + separator + "/", ''); 14 | }; 15 | 16 | var aliasDirective = function(aliasName, targetDirective, options) { 17 | options = options || {}; 18 | var beforeLink = options.beforeLink; 19 | var restrict = options.restrict || 'A'; 20 | module.directive(aliasName, ['$compile', function($compile){ 21 | return { 22 | restrict: restrict, 23 | 24 | // this should be higher than any other directives 25 | // declared for the same element 26 | priority: 99999, 27 | compile: function(elem, attrs){ 28 | 29 | var placeholder = angular.element(document.createElement('div')); 30 | elem.after(placeholder); 31 | // Detach element until link phase 32 | // so Angular wont go down to the children. 33 | elem.remove(); 34 | 35 | var dasherizedTarget = uncamelize(targetDirective); 36 | var dasherizedAlias = uncamelize(aliasName); 37 | 38 | if (restrict.match(/A/)) { 39 | // Replace old attr with new attr 40 | elem.attr(dasherizedTarget, elem.attr(dasherizedAlias)); 41 | elem.removeAttr(dasherizedAlias); 42 | } 43 | 44 | if (restrict.match(/E/)) { 45 | elem[0].name = dasherizedTarget; 46 | } 47 | 48 | if (beforeLink) { 49 | beforeLink(elem, attrs); 50 | } 51 | 52 | return function(scope){ 53 | placeholder.replaceWith(elem); 54 | $compile(elem)(scope); 55 | elem = null; 56 | }; 57 | } 58 | }; 59 | }]); 60 | }; 61 | 62 | aliasDirective('switch', 'uiSwitch'); 63 | aliasDirective('contentFor', 'uiContentFor', { 64 | beforeLink: function(elem) { 65 | elem.attr('uiDuplicate', elem.attr('duplicate')); 66 | } 67 | }); 68 | aliasDirective('yieldTo', 'uiYieldTo'); 69 | }()); -------------------------------------------------------------------------------- /test/components/switch.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 30 | 31 | 32 | 41 | 42 | 43 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/gestures/swipe.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 |
17 | 18 |
21 | 22 |
25 |
26 |
27 | 28 | 29 | 52 | 53 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/components/scrollable.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 | 16 | 17 | 43 | 44 | 71 | -------------------------------------------------------------------------------- /src/js/migrate/carousel.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('mobile-angular-ui.migrate.carousel', []) 5 | 6 | .run(["$rootScope", function($rootScope) { 7 | 8 | $rootScope.carouselPrev = function(id) { 9 | $rootScope.$emit("mobile-angular-ui.carousel.prev", id); 10 | }; 11 | 12 | $rootScope.carouselNext = function(id) { 13 | $rootScope.$emit("mobile-angular-ui.carousel.next", id); 14 | }; 15 | 16 | var carouselItems = function(id) { 17 | var elem = angular.element(document.getElementById(id)); 18 | var res = angular.element(elem.children()[0]).children(); 19 | elem = null; 20 | return res; 21 | }; 22 | 23 | var findActiveItemIndex = function(items) { 24 | var idx = -1; 25 | var found = false; 26 | 27 | for (var _i = 0; _i < items.length; _i++) { 28 | var item = items[_i]; 29 | idx += 1; 30 | if (angular.element(item).hasClass('active')) { 31 | found = true; 32 | break; 33 | } 34 | } 35 | 36 | if (found) { 37 | return idx; 38 | } else { 39 | return -1; 40 | } 41 | 42 | }; 43 | 44 | $rootScope.$on("mobile-angular-ui.carousel.prev", function(e, id) { 45 | var items = carouselItems(id); 46 | var idx = findActiveItemIndex(items); 47 | var lastIdx = items.length - 1; 48 | 49 | if (idx !== -1) { 50 | angular.element(items[idx]).removeClass("active"); 51 | } 52 | 53 | if (idx <= 0) { 54 | angular.element(items[lastIdx]).addClass("active"); 55 | } else { 56 | angular.element(items[idx - 1]).addClass("active"); 57 | } 58 | 59 | items = null; 60 | idx = null; 61 | lastIdx = null; 62 | }); 63 | 64 | $rootScope.$on("mobile-angular-ui.carousel.next", function(e, id) { 65 | var items = carouselItems(id); 66 | var idx = findActiveItemIndex(items); 67 | var lastIdx = items.length - 1; 68 | 69 | if (idx !== -1) { 70 | angular.element(items[idx]).removeClass("active"); 71 | } 72 | 73 | if (idx === lastIdx) { 74 | angular.element(items[0]).addClass("active"); 75 | } else { 76 | angular.element(items[idx + 1]).addClass("active"); 77 | } 78 | 79 | items = null; 80 | idx = null; 81 | lastIdx = null; 82 | }); 83 | } 84 | ]); 85 | 86 | }()); 87 | -------------------------------------------------------------------------------- /test/gestures/transform.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 29 | 30 | 31 |
32 |
33 |
34 |
{{et('translate3d').toCss(2)}}
35 |
{{translate3d.toCss(2)}}
36 | 37 |
{{et('rotate3d').toCss(2)}}
38 |
{{rotate3d.toCss(2)}}
39 | 40 |
{{et('scale3d').toCss(2)}}
41 |
{{scale3d.toCss(2)}}
42 | 43 |
{{et('rotate3d_translate3d').toCss(2)}}
44 |
{{rotate3d_translate3d.toCss(2)}}
45 | 46 |
{{et('scale3d_rotate3d_translate3d').toCss(2)}}
47 |
{{scale3d_rotate3d_translate3d.toCss(2)}}
48 | 49 |
{{et('translate3d_rotate3d_scale3d').toCss(2)}}
50 |
{{translate3d_rotate3d_scale3d.toCss(2)}}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | -------------------------------------------------------------------------------- /examples/drag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Limiting $drag movement 8 | 9 | 10 | 11 | 12 | 48 | 51 | 73 | 74 | 75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 | 83 | -------------------------------------------------------------------------------- /src/js/migrate/forms.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var module = angular.module('mobile-angular-ui.migrate.forms', []); 5 | 6 | // 7 | // Old form helpers 8 | // 9 | module.directive("bsFormControl", function() { 10 | var bs_col_classes = {}; 11 | var bs_col_sizes = ['xs', 'sm', 'md', 'lg']; 12 | 13 | for (var i = 0; i < bs_col_sizes.length; i++) { 14 | for (var j = 1; j <= 12; j++) { 15 | bs_col_classes['col-' + bs_col_sizes[i] + "-" + j] = true; 16 | } 17 | } 18 | 19 | function separeBsColClasses(clss) { 20 | var intersection = ""; 21 | var difference = ""; 22 | 23 | for (var i = 0; i < clss.length; i++) { 24 | var v = clss[i]; 25 | if (v in bs_col_classes) { 26 | intersection += (v + " "); 27 | } else { 28 | difference += (v + " "); 29 | } 30 | } 31 | 32 | return {i: intersection.trim(), d: difference.trim()}; 33 | } 34 | 35 | return { 36 | replace: true, 37 | require: "ngModel", 38 | link: function(scope, elem, attrs) { 39 | 40 | if (attrs.labelClass === null || attrs.labelClass === undefined) { 41 | attrs.labelClass = ""; 42 | } 43 | 44 | if (attrs.id === null || attrs.id === undefined) { 45 | attrs.id = attrs.ngModel.replace(".", "_") + "_input"; 46 | } 47 | 48 | if ((elem[0].tagName == "SELECT") || ((elem[0].tagName == "INPUT" || elem[0].tagName == "TEXTAREA") && (attrs.type != "checkbox" && attrs.type != "radio"))) { 49 | elem.addClass("form-control"); 50 | } 51 | 52 | var label = angular.element(""); 53 | var w1 = angular.element("
"); 54 | var w2 = angular.element("
"); 55 | 56 | var labelColClasses = separeBsColClasses(attrs.labelClass.split(/\s+/)); 57 | if (labelColClasses.i === "") { 58 | label.addClass("col-xs-12"); 59 | } 60 | label.addClass(attrs.labelClass); 61 | 62 | var elemColClasses = separeBsColClasses(elem[0].className.split(/\s+/)); 63 | elem.removeClass(elemColClasses.i); 64 | w2.addClass(elemColClasses.i); 65 | if (elemColClasses.i === "") { 66 | w2.addClass("col-xs-12"); 67 | } 68 | elem.wrap(w1).wrap(w2); 69 | elem.parent().parent().prepend(label); 70 | elem.attr('id', attrs.id); 71 | 72 | label = w1 = w2 = labelColClasses = elemColClasses = null; 73 | } 74 | }; 75 | }); 76 | 77 | }()); -------------------------------------------------------------------------------- /src/less/mobile-angular-ui-desktop.less: -------------------------------------------------------------------------------- 1 | @import "variables.less"; 2 | @import "bootstrap/less/mixins.less"; 3 | 4 | @media (min-width: @screen-md-min) { 5 | .make-grid(md); 6 | } 7 | @media (min-width: @screen-lg-min) { 8 | .make-grid(lg); 9 | } 10 | 11 | @import "bootstrap/less/breadcrumbs.less"; 12 | @import "bootstrap/less/pagination.less"; 13 | @import "bootstrap/less/jumbotron.less"; 14 | 15 | @media (min-width: @screen-desktop) { 16 | 17 | .sidebar { 18 | pointer-events: auto; 19 | } 20 | 21 | .sidebar-toggle { 22 | display: none; 23 | } 24 | 25 | .navbar-brand-center { 26 | text-align: left; 27 | width: auto; 28 | float: left; 29 | position: relative; 30 | } 31 | 32 | .sidebar-left { 33 | display: block; 34 | left: 0; 35 | width: @sidebar-left-width-md; 36 | } 37 | 38 | .sidebar-right { 39 | display: block; 40 | right: 0; 41 | left: auto; 42 | width: @sidebar-right-width-md; 43 | } 44 | 45 | .sidebar-right-in .sidebar-left { 46 | -webkit-transform: translate3d(0, 0, 0) !important; 47 | -moz-transform: translate3d(0, 0, 0) !important; 48 | transform: translate3d(0, 0, 0) !important; 49 | } 50 | 51 | .sidebar-left-in .sidebar-right { 52 | -webkit-transform: translate3d(0, 0, 0) !important; 53 | -moz-transform: translate3d(0, 0, 0) !important; 54 | transform: translate3d(0, 0, 0) !important; 55 | } 56 | 57 | .has-sidebar-left .app, .sidebar-left-in .app { 58 | width: auto; 59 | margin-left: @sidebar-left-width-md; 60 | left: 0; 61 | -webkit-transform: translate3d(0, 0, 0) !important; 62 | -moz-transform: translate3d(0, 0, 0) !important; 63 | transform: translate3d(0, 0, 0) !important; 64 | position: relative; 65 | -webkit-transition: -webkit-transform 0 ease; 66 | -moz-transition: -moz-transform 0 ease; 67 | transition: transform 0 ease; 68 | } 69 | 70 | .sidebar-right-in .app { 71 | width: auto; 72 | margin-right: @sidebar-right-width-md; 73 | right: 0; 74 | -webkit-transform: translate3d(0, 0, 0) !important; 75 | -moz-transform: translate3d(0, 0, 0) !important; 76 | transform: translate3d(0, 0, 0) !important; 77 | position: relative; 78 | -webkit-transition: -webkit-transform 0 ease; 79 | -moz-transition: -moz-transform 0 ease; 80 | transition: transform 0 ease; 81 | } 82 | } 83 | 84 | @media (min-width: @screen-lg-min) { 85 | .sidebar-left { 86 | width: @sidebar-left-width-lg; 87 | } 88 | .sidebar-right { 89 | width: @sidebar-right-width-lg; 90 | } 91 | .has-sidebar-left .app, .sidebar-left-in .app { 92 | margin-left: @sidebar-left-width-lg; 93 | } 94 | .sidebar-right-in .app { 95 | margin-right: @sidebar-right-width-lg; 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/js/core/activeLinks.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mobile-angular-ui.core.activeLinks 3 | @description 4 | 5 | `mobile-angular-ui.activeLinks` module sets up `.active` class for `a` elements those `href` attribute matches the current angular `$location` url. It takes care of excluding both search part and hash part from comparison. 6 | 7 | `.active` classes are added/removed each time one of `$locationChangeSuccess` or `$includeContentLoaded` is fired. 8 | 9 | ## Usage 10 | 11 | Just declare it as a dependency to your app unless you have already included one of its super-modules. 12 | 13 | ``` 14 | angular.module('myApp', ['mobile-angular-ui.core.activeLinks']); 15 | ``` 16 | 17 | **NOTE:** if you are using it without Bootstrap you may need to add some css to your stylesheets to reflect the activation state of links. I.e. 18 | 19 | ``` css 20 | a.active { 21 | color: blue; 22 | } 23 | ``` 24 | 25 | */ 26 | (function () { 27 | 'use strict'; 28 | 29 | angular.module("mobile-angular-ui.core.activeLinks", []) 30 | 31 | .run([ 32 | '$rootScope', 33 | '$window', 34 | '$document', 35 | '$location', 36 | function($rootScope, $window, $document, $location){ 37 | 38 | var setupActiveLinks = function() { 39 | // Excludes both search part and hash part from 40 | // comparison. 41 | var url = $location.url(), 42 | firstHash = url.indexOf('#'), 43 | firstSearchMark = url.indexOf('?'), 44 | locationHref = $window.location.href, 45 | plainUrlLength = locationHref.indexOf(url), 46 | newPath; 47 | 48 | if (firstHash === -1 && firstSearchMark === -1) { 49 | newPath = locationHref; 50 | } else if (firstHash !== -1 && firstHash > firstSearchMark) { 51 | newPath = locationHref.slice(0, plainUrlLength + firstHash); 52 | } else if (firstSearchMark !== -1 && firstSearchMark > firstHash) { 53 | newPath = locationHref.slice(0, plainUrlLength + firstSearchMark); 54 | } 55 | 56 | var domLinks = $document[0].links; 57 | for (var i = 0; i < domLinks.length; i++) { 58 | var domLink = domLinks[i]; 59 | var link = angular.element(domLink); 60 | if (link.attr('href') && link.attr('href') !== '' && domLink.href === newPath) { 61 | link.addClass('active'); 62 | } else if (link.attr('href') && link.attr('href') !== '' && domLink.href && domLink.href.length) { 63 | link.removeClass('active'); 64 | } 65 | } 66 | }; 67 | 68 | $rootScope.$on('$locationChangeSuccess', setupActiveLinks); 69 | $rootScope.$on('$includeContentLoaded', setupActiveLinks); 70 | } 71 | ]); 72 | 73 | }()); 74 | 75 | -------------------------------------------------------------------------------- /src/less/mobile-angular-ui-base.less: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // Variables 3 | // ========================================== 4 | 5 | @import "variables.less"; 6 | 7 | // ========================================== 8 | // Font Awesome Stuffs 9 | // ========================================== 10 | 11 | @import "font-awesome/less/font-awesome.less"; 12 | 13 | // ========================================== 14 | // Bootstrap Stuffs 15 | // ========================================== 16 | 17 | // Core variables and mixins 18 | @import "bootstrap/less/mixins.less"; 19 | 20 | // Reset 21 | @import "bootstrap/less/normalize.less"; 22 | 23 | // Core CSS 24 | @import "bootstrap/less/scaffolding.less"; 25 | @import "bootstrap/less/type.less"; 26 | @import "bootstrap/less/code.less"; 27 | @import "bootstrap/less/grid.less"; 28 | @import "bootstrap/less/tables.less"; 29 | @import "bootstrap/less/forms.less"; 30 | @import "bootstrap/less/buttons.less"; 31 | 32 | // Components 33 | @import "bootstrap/less/component-animations.less"; 34 | @import "bootstrap/less/dropdowns.less"; 35 | @import "bootstrap/less/button-groups.less"; 36 | @import "bootstrap/less/input-groups.less"; 37 | @import "bootstrap/less/navs.less"; 38 | @import "bootstrap/less/navbar.less"; 39 | @import "bootstrap/less/pager.less"; 40 | @import "bootstrap/less/labels.less"; 41 | @import "bootstrap/less/badges.less"; 42 | @import "bootstrap/less/thumbnails.less"; 43 | @import "bootstrap/less/alerts.less"; 44 | @import "bootstrap/less/progress-bars.less"; 45 | @import "bootstrap/less/media.less"; 46 | @import "bootstrap/less/list-group.less"; 47 | @import "bootstrap/less/panels.less"; 48 | @import "bootstrap/less/wells.less"; 49 | @import "bootstrap/less/close.less"; 50 | 51 | // Components w/ JavaScript 52 | @import "bootstrap/less/modals.less"; 53 | @import "bootstrap/less/tooltip.less"; 54 | @import "bootstrap/less/popovers.less"; 55 | @import "bootstrap/less/modals.less"; 56 | @import "bootstrap/less/carousel.less"; 57 | 58 | // Utility classes 59 | @import "bootstrap/less/utilities.less"; 60 | @import "bootstrap/less/responsive-utilities.less"; 61 | 62 | // ========================================== 63 | // Mobile Angular Ui Stuffs 64 | // ========================================== 65 | 66 | @import "lib/normalize.less"; 67 | @import "lib/globals.less"; 68 | @import "lib/layout.less"; 69 | @import "lib/forms.less"; 70 | @import "lib/buttons.less"; 71 | @import "lib/list-groups.less"; 72 | @import "lib/app.less"; 73 | @import "lib/navbars.less"; 74 | @import "lib/sidebar.less"; 75 | @import "lib/sections.less"; 76 | @import "lib/justified.less"; 77 | @import "lib/scrollable.less"; 78 | @import "lib/validations.less"; 79 | @import "lib/panel.less"; 80 | @import "lib/carousel.less"; 81 | @import "lib/modal.less"; 82 | @import "lib/switch.less"; 83 | @import "lib/dropdown.less"; 84 | -------------------------------------------------------------------------------- /test/core/sharedstate-ui-cond.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 |
{{Ui.get('testState') ? 'true' : 'false' }}
15 | 16 |
uiIfTrue
17 |
uiIfFalse
18 |
ngShowTrue
19 |
ngShowFalse
20 |
ngHideTrue
21 |
ngHideFalse
22 |
ngClassTrue
23 |
ngClassFalse
24 | 25 | 26 | 27 |
uiScopeContextUi1
28 |
uiScopeContextUi2
29 |
uiScopeContextUi3
30 |
uiScopeContextUi4
31 |
uiScopeContextUi5
32 |
33 | 34 | 35 | 36 | 55 | 56 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | #lightbulb { 2 | font-size: 100px; 3 | display: block; 4 | text-align: center; 5 | } 6 | 7 | h1 { 8 | padding: 0; 9 | margin: 10px 0; 10 | } 11 | 12 | .chat-user-avatar { 13 | font-size: 40px; 14 | background: #ccc; 15 | padding: 5px 10px; 16 | } 17 | 18 | .feature-icon { 19 | font-size: 44px; 20 | padding: 0; 21 | line-height: 68px; 22 | width: 72px; 23 | text-align: center; 24 | opacity: .8; 25 | border: 3px solid; 26 | border-radius: 200px; 27 | height: 72px; 28 | margin-bottom: 20px; 29 | } 30 | 31 | .feature-icon.fa-gamepad { 32 | line-height: 60px; 33 | } 34 | 35 | .feature-icon.fa-tachometer { 36 | line-height: 60px; 37 | } 38 | 39 | .app-content-loading { 40 | text-align: center; 41 | height: 100%; 42 | width: 100%; 43 | background: #fff; 44 | position: relative; 45 | } 46 | 47 | .loading-spinner { 48 | position: absolute; 49 | font-size: 50px; 50 | left: 50%; 51 | top: 50%; 52 | margin-left: -25px; 53 | margin-top: -25px; 54 | } 55 | 56 | .carousel-example-content { 57 | position: absolute; 58 | font-size: 50px; 59 | line-height: 50px; 60 | width: 100%; 61 | text-align: center; 62 | top: 50%; 63 | margin-top: -50px; 64 | } 65 | 66 | 67 | .carousel-item { 68 | display: block; 69 | color: #444; 70 | background: #f4f4f4; 71 | box-shadow: 0px 0px 3px rgba(0,0,0,0.5); 72 | -webkit-transition: opacity 0.3s; 73 | -moz-transition: opacity 0.3s; 74 | transition: opacity 0.3s; 75 | opacity: 1; 76 | -webkit-touch-callout: none; 77 | -webkit-user-select: none; 78 | -khtml-user-select: none; 79 | -moz-user-select: none; 80 | -ms-user-select: none; 81 | user-select: none; 82 | } 83 | 84 | 85 | .carousel { 86 | padding: 20px; 87 | overflow: visible; 88 | } 89 | 90 | .carousel-inner { 91 | position: relative; 92 | overflow: visible; 93 | } 94 | 95 | .carousel>.item, .carousel-item { 96 | top: 0; 97 | left: 0; 98 | position: absolute; 99 | display: block; 100 | width: 100%; 101 | height: 100%; 102 | background-size: cover; 103 | } 104 | 105 | .carousel-item { 106 | -webkit-transition: -webkit-transform 500ms; 107 | -moz-transition: -moz-transform 500ms; 108 | transition: transform 500ms; 109 | } 110 | 111 | [drag-to-dismiss]{ 112 | transform: translate(0,0); /* NOTE: this is required to prevent mobile webkit issues */ 113 | -webkit-user-drag: none; 114 | -webkit-touch-callout: none; 115 | -webkit-user-select: none; 116 | -khtml-user-select: none; 117 | -moz-user-select: none; 118 | -ms-user-select: none; 119 | user-select: none; 120 | -webkit-transition: opacity 300ms, -webkit-transform 300ms; 121 | -moz-transition: opacity 300ms, -moz-transform 300ms; 122 | transition: opacity 300ms, transform 300ms; 123 | opacity: 1; 124 | } 125 | 126 | .notices-container { 127 | overflow-x: hidden; 128 | } 129 | 130 | .dismiss { 131 | -webkit-transition: opacity 300ms, -webkit-transform 300ms; 132 | -moz-transition: opacity 300ms, -moz-transform 300ms; 133 | transition: opacity 300ms, transform 300ms; 134 | opacity: 0.5; 135 | } 136 | 137 | .dismitted { 138 | -webkit-transition: opacity 300ms, -webkit-transform 300ms; 139 | -moz-transition: opacity 300ms, -moz-transform 300ms; 140 | transition: opacity 300ms, transform 300ms; 141 | opacity: 0; 142 | } 143 | 144 | .list-group-item-home { 145 | padding: 20px; 146 | } 147 | 148 | pre { 149 | text-align: left !important; 150 | } 151 | 152 | input.scrollable-header { 153 | border-bottom: 3px solid #ccc; 154 | } 155 | 156 | .toucharea {width: 100%; height: 100%;} -------------------------------------------------------------------------------- /demo/swipe.html: -------------------------------------------------------------------------------- 1 |
2 |
5 | 6 |

Swipe me

7 | 8 |
9 |

10 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 11 |

12 |

13 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 14 |

15 |

16 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 17 |

18 |

19 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 20 |

21 |

22 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 23 |

24 |

25 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 26 |

27 |
28 |
29 |
-------------------------------------------------------------------------------- /ROADMAP.tasks: -------------------------------------------------------------------------------- 1 | /*================================================= 2 | = MOBILE ANGULAR UI ROADMAP = 3 | =================================================*/ 4 | 5 | This file will contain an extremely detailed tasks list 6 | to follow development of new releases. 7 | 8 | Unscheduled: 9 | ❑ Look forward to use typescript/closure compiler or similar to perform maximum compression/optimization and structure sources better 10 | ❑ Support basic material design principles and visual feedbacks 11 | 12 | 13 | Unscheduled 1.2: 14 | ❑ Support ms devices through -ms- touch property 15 | For IE11+, you can use touch-action: manipulation; to disable double-tap-to-zoom on certain elements (like links and buttons). For IE10 use -ms-touch-action: manipulation 16 | ❑ Define multiple states in `ui-state` 17 | ❑ Make shared states exportable to QP 18 | ❑ Fix migration for `ui-switch` 19 | ❑ Fix $transform issues decomposing previous rotations 20 | ❑ Check for: https://github.com/scottjehl/device-bugs/issues/2 !!! 21 | ❑ Fix or invalidate old ui-switch issues: 22 | ❑ https://github.com/mcasimir/mobile-angular-ui/issues/92 23 | ❑ https://github.com/mcasimir/mobile-angular-ui/issues/87 24 | ❑ https://github.com/mcasimir/mobile-angular-ui/issues/86 25 | ❑ https://github.com/mcasimir/mobile-angular-ui/issues/85 26 | 27 | --- ✄ ----------------------- 28 | 29 | 1.2.0.beta.11 (gestures and gulp): 30 | 31 | ✔ switch should work with $drag if present @done (15-01-26 19:47) 32 | 33 | ✔ $swipe @done (15-01-26 13:45) 34 | ✔ Allow for nested $swipe @done (15-01-18 23:03) 35 | ✔ Fix compatibility between $swipe and FastClick @done (15-01-19 00:09) 36 | ✔ move $touch's touch.direction to touch.angle and use touch.direction for 'LEFT', 'RIGHT', 'UP' and 'DOWN' @done (15-01-04 07:34) 37 | 38 | ✔ NOBOUNCE @done (15-01-26 13:03) 39 | ✔ Use nobounce tricks to avoid IOS bouncing after scroll or dragging @done (15-01-19 22:32) 40 | ✔ Move nobounce to directive @done (15-01-26 13:03) 41 | 42 | ✔ $touch @done (15-01-26 13:45) 43 | ✔ multiple $touch bindings should not interfere each other @done (15-01-19 08:43) 44 | ✔ detect multitouch and handle or prevent them @done (15-01-26 13:45) 45 | 46 | ✔ Add tests for $touch and $drag, adapt those for $swipe and $transform @done (15-01-23 22:01) 47 | ✔ Make it simpler to deal with ng-repeat and multiple shared states @done (15-01-23 22:01) 48 | ✔ check for SVG compatibility (remove HTMLElement references and use something more generic) @done (14-12-30 17:04) 49 | ✔ $touch should accept bounduaries either as rect {top, bottom, left, right} or element @done (14-12-30 17:02) 50 | ✔ Add { area: rectOrElement } to options @done (14-12-30 17:02) 51 | ✔ it should treat element and window the same way (or should we use element.ownerDocument instead?) @done (14-12-30 17:02) 52 | ✔ remove option for allowing outer movement (should be the default) @done (14-12-30 17:02) 53 | ✔ $drag.TRANSLATE_INSIDE(elem) should work @done (14-12-30 07:42) 54 | ✔ fix carousel in demo @done (14-12-30 07:42) 55 | ✔ Better detect swipes using direction and velocity @done (14-12-31 00:45) 56 | ✔ Switch to gulp @done (15-01-02 00:39) 57 | ✔ add gestures to bower.json @done (15-01-02 00:39) 58 | ✔ remove tests and any other unnecessary files from bower.json @done (15-01-02 00:44) 59 | ✔ release should take care of npm too @done (15-01-02 00:44) 60 | ✔ simplify testing @done (15-01-04 07:31) 61 | ✔ a test shoudl be an .html file only @done (15-01-04 07:32) 62 | ✔ inspect angular simpler @done (15-01-04 07:33) 63 | ✔ selectively filter tests by id or name @done (15-01-04 07:33) 64 | ✔ automatically ensure console is free from errors @done (15-01-04 09:09) -------------------------------------------------------------------------------- /test/manual/fastclick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | 20 | 21 | 22 | 68 | 69 | 70 |

Layer A responds to click events normally, which on iOS will introduce a 300ms delay.

71 |

Layer B is enhanced with FastClick, and will fire the click handler with no delay.

72 |

The layers will behave normally on platforms that don't support touch events.

73 |

Touch end time:

74 |

Click event time:

75 |

Difference:

76 |
77 |
A
78 |
B
79 |
80 | 81 | -------------------------------------------------------------------------------- /src/less/variables.less: -------------------------------------------------------------------------------- 1 | @import "font-awesome/less/variables"; 2 | @import "bootstrap/less/variables.less"; 3 | 4 | // Bootstrap Overrides 5 | @brand-primary: #007aff; 6 | @brand-success: #4cd964; 7 | @brand-warning: #fc0; 8 | @brand-danger: #ff3b30; 9 | @brand-info: #34aadc; 10 | 11 | @headings-font-family: @font-family-base; 12 | @headings-font-weight: normal; 13 | @btn-font-weight: normal; 14 | @alert-link-font-weight: normal; 15 | @badge-font-weight: normal; 16 | @close-font-weight: normal; 17 | 18 | @navbar-default-color: @brand-primary; 19 | @navbar-default-bg: #f7f7f7; 20 | @navbar-default-link-color: @brand-primary; 21 | @navbar-default-link-hover-color: darken(@brand-primary, 5%); 22 | @navbar-default-link-active-color: lighten(@brand-primary, 5%); 23 | @navbar-default-link-disabled-color: lighten(@brand-primary, 20%); 24 | @navbar-default-toggle-icon-bar-bg: #ccc; 25 | @navbar-default-border: #ccc; 26 | @navbar-border-radius: 0; 27 | 28 | @border-radius-base: 4px; 29 | @border-radius-large: 6px; 30 | @border-radius-small: 3px; 31 | 32 | // Globals 33 | @font-weight: 200; 34 | @active-link-opacity: .6; 35 | @page-header-margin-top: 10px; 36 | 37 | // App 38 | @app-bg: @gray-light; 39 | @zindex-app: 2; 40 | @zindex-app-search: 1029; 41 | @app-shadow: 0 0 4px rgba(0,0,0,0.4); 42 | 43 | // Forms 44 | @form-actions-margin-top: -20px; 45 | @app-search-border-radius: 0; 46 | 47 | // Switch 48 | @switch-handle-diameter: @input-height-base; 49 | @switch-width: @switch-handle-diameter * 2; 50 | @switch-height: @switch-handle-diameter; 51 | @switch-active-color: @brand-primary; 52 | @zindex-switch-handle: 2; 53 | @switch-border-radius: 999px; 54 | @switch-handle-border-radius: @switch-border-radius; 55 | 56 | // Navbars 57 | @btn-icon-only-font-size: 22px; 58 | @navbar-app-height: 50px; 59 | 60 | // Overlays 61 | @overlay-margin: -5px -5px -5px -5px; 62 | @overlay-background: rgba(255, 255, 255, .95); 63 | @overlay-content-padding: 20px; 64 | @overlay-dismiss-font-size: 24px; 65 | @overlay-dismiss-margin: @overlay-content-padding / 2; 66 | @overlay-dismiss-margin-top: @overlay-dismiss-margin; 67 | @overlay-dismiss-margin-right: @overlay-dismiss-margin; 68 | @overlay-content-color: #000; 69 | @overlay-blur: 5px; 70 | 71 | // Panels 72 | 73 | @panel-padding: 10px 15px; 74 | @panel-title-font-size: 20px; 75 | @panel-title-font-weight: 200; 76 | @panel-group-panel-heading-padding: 0 15px; 77 | @panel-title-line-height: @line-height-computed * 2; 78 | @panel-default-panel-heading-padding: 0 0 10px 0; 79 | @panel-default-panel-body-padding: 15px; 80 | 81 | // Sidebars 82 | @sidebar-width: 300px; 83 | @sidebar-width-xs: @screen-xs-min - 40; 84 | @sidebar-width-sm: @screen-sm-min - 80; 85 | @sidebar-width-md: 300px; 86 | @sidebar-width-lg: 320px; 87 | @sidebar-left-width: @sidebar-width; 88 | @sidebar-right-width: @sidebar-width; 89 | 90 | @sidebar-left-width-xs: @sidebar-width-xs; 91 | @sidebar-right-width-xs: @sidebar-width-xs; 92 | 93 | @sidebar-left-width-sm: @sidebar-width-sm; 94 | @sidebar-right-width-sm: @sidebar-width-sm; 95 | 96 | @sidebar-left-width-md: @sidebar-width-md; 97 | @sidebar-right-width-md: @sidebar-width-md; 98 | 99 | @sidebar-left-width-lg: @sidebar-width-lg; 100 | @sidebar-right-width-lg: @sidebar-width-lg; 101 | 102 | 103 | 104 | @sidebar-header-height: 70px; 105 | @sidebar-header-font-size: 23px; 106 | @zindex-sidebar-header: 10; 107 | @sidebar-header-padding: 0 10px; 108 | @sidebar-header-color: #fff; 109 | @sidebar-background: @gray; 110 | @sidebar-left-background: @sidebar-background; 111 | @sidebar-right-background: @sidebar-background; -------------------------------------------------------------------------------- /test/core/sharedstate.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | Turn On and Ng Click 52 | 53 |
54 | Turn On and Ng Click 55 |
56 | 57 | Attach elem 2 58 | Detach elem 1 59 | Detach elem 2 60 | 61 | 62 | 70 | 71 | 78 | 79 | 90 | 91 | 110 | 111 | -------------------------------------------------------------------------------- /test/htmlTests.js: -------------------------------------------------------------------------------- 1 | /* global console: false, __dirname: false, module: true, process: true */ 2 | 3 | var glob = require('glob'), 4 | minimatch = require("minimatch"), 5 | path = require('path'), 6 | fs = require('fs'), 7 | cheerio = require('cheerio'), 8 | slug = require('slug'), 9 | tests = {}, 10 | testFiles = glob.sync(path.resolve(__dirname, '**/*.test.html')); 11 | 12 | function requireFromString(src, filename) { 13 | var Module = module.constructor; 14 | var m = new Module(); 15 | m._compile(src, filename); 16 | return m.exports; 17 | } 18 | 19 | function testToModule(test) { 20 | var src = [ 21 | 'module.exports = function() {', 22 | test.describes.map(function(d) { 23 | return 'describe(' + JSON.stringify(d) + ', function(){'; 24 | }).join(''), 25 | 'it(' + JSON.stringify(test.spec) + ', function(){', 26 | 'browser.get(' + JSON.stringify(test.browserLoad) + ');', 27 | test.func, 28 | test.canThrow ? '' : ';expectNoErrors();', 29 | '});', 30 | test.describes.map(function() { 31 | return '});'; 32 | }).join(''), 33 | '};' 34 | ].join(''); 35 | return requireFromString(src, test.filename); 36 | } 37 | 38 | var tests = {}; 39 | 40 | function addTest(test) { 41 | tests[test.describes.join('/') + (test.id ? '/' + test.id : '')] = testToModule(test); 42 | } 43 | 44 | function traverse($, node, down, up) { 45 | down(node); 46 | var children = node.children(); 47 | children.each(function() { 48 | traverse($, $(this), down, up); 49 | }); 50 | up(node); 51 | } 52 | 53 | function parseTests(file) { 54 | var browserLoad = '/' + path.relative(__dirname, file).replace(/\.test\.html$/, ''); 55 | var html = fs.readFileSync(file).toString(); 56 | var $ = cheerio.load(html, {decodeEntities: false}); 57 | var describes = []; 58 | 59 | var downFn = function(node) { 60 | if (node.attr('describe')) { 61 | describes.push(node.attr('describe')); 62 | } 63 | if (node.attr('type') === 'application/protractor') { 64 | var spec = node.attr('spec'); 65 | 66 | if (!spec || spec.trim() === '') { 67 | throw new Error('Parsing `'+ file +'`: script[type=\'application/protractor\'] requires `spec` attribute to be set.'); 68 | } 69 | 70 | var func = node.text(); 71 | var prev = html.slice(0, html.indexOf(func)); 72 | func = prev.replace(/[^\n]/g, ' ') + func; // very weird trick to retain lineno and col; 73 | 74 | var canThrow = node.attr('can-throw') === '' || node.attr('can-throw') === 'true'; 75 | 76 | addTest({ 77 | describes: describes.slice(0, describes.length), 78 | id: node.attr('id') || slug(spec).toLowerCase(), 79 | spec: spec, 80 | filename: file, 81 | browserLoad: browserLoad, 82 | func: func, 83 | element: node, 84 | canThrow: canThrow 85 | }); 86 | } 87 | }; 88 | 89 | var upFn = function(node) { 90 | if (node.attr('describe')) { 91 | describes.pop(); 92 | } 93 | }; 94 | 95 | traverse($, $.root(), downFn, upFn); 96 | } 97 | 98 | for (var i = 0; i < testFiles.length; i++) { 99 | var abs = testFiles[i]; 100 | parseTests(abs); 101 | } 102 | 103 | // 104 | // Parse args 105 | // 106 | var index = process.argv.indexOf('--tests'); 107 | var pattern = index !== -1 && process.argv[index + 1]; 108 | 109 | // 110 | // run all tests 111 | // 112 | var testsToRun = []; 113 | var testNames = Object.keys(tests); 114 | testNames.forEach(function(name) { 115 | if (!pattern || minimatch(name, pattern)) { 116 | testsToRun.push(tests[name]); 117 | } 118 | }); 119 | 120 | if (testNames.length && !testsToRun.length && pattern) { 121 | console.warn('No tests matched `' + pattern + '`'); 122 | console.log('Available tests:'); 123 | testNames.forEach(function(name) { 124 | console.log(' - ' + name); 125 | }); 126 | } 127 | 128 | testsToRun.forEach(function(fn) { 129 | fn(); 130 | }); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mobile Angular UI Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 34 | 35 | 37 | 38 |
41 | 42 | 43 | 44 | 59 | 60 | 67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 | 78 |
79 | 80 |
81 | 82 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/js/core/touchmoveDefaults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides directives and service to prevent touchmove default behaviour 3 | * for touch devices (ie. bounce on overscroll in IOS). 4 | * 5 | * #### Usage 6 | * 7 | * Use `ui-prevent-touchmove-defaults` directive on root element of your app: 8 | * 9 | * ``` html 10 | * 11 | * 12 | * 13 | * ``` 14 | * 15 | * Doing so `touchmove.preventDefault` logic for inner elements is inverted, 16 | * so any `touchmove` default behaviour is automatically prevented. 17 | * 18 | * If you wish to allow the default behaviour, for example to allow 19 | * inner elements to scroll, you have to explicitly mark an event to allow 20 | * touchmove default. 21 | * 22 | * Mobile Angular UI already handles this for `scrollable` elements, so you don't have 23 | * to do anything in order to support scroll. 24 | * 25 | * If you wish to allow touchmove defaults for certain element under certain conditions 26 | * you can use the `allowTouchmoveDefault` service. 27 | * 28 | * ie. 29 | * 30 | * ``` js 31 | * // always allow touchmove default for an element 32 | * allowTouchmoveDefault(myelem); 33 | * ``` 34 | * 35 | * ``` js 36 | * // allow touchmove default for an element only under certain conditions 37 | * allowTouchmoveDefault(myelem, function(touchmove){ 38 | * return touchmove.pageY > 100; 39 | * }); 40 | * ``` 41 | * 42 | * @module mobile-angular-ui.core.touchmoveDefaults 43 | */ 44 | (function () { 45 | 'use strict'; 46 | var module = angular.module('mobile-angular-ui.core.touchmoveDefaults', []); 47 | 48 | module.directive('uiPreventTouchmoveDefaults', function() { 49 | var preventTouchmoveDefaultsCb = function(e) { 50 | if (e.allowTouchmoveDefault !== true) { 51 | e.preventDefault(); 52 | } 53 | }; 54 | 55 | return { 56 | compile: function(element) { 57 | if ('ontouchmove' in document) { 58 | element.on('touchmove', preventTouchmoveDefaultsCb); 59 | } 60 | } 61 | }; 62 | }); 63 | 64 | /** 65 | * Bind a listener to an element to allow `touchmove` default behaviour 66 | * when `touchmove` happens inside the bound element. 67 | * 68 | * You can also provide a function to decide when to allow and 69 | * when to prevent it. 70 | * 71 | * ``` js 72 | * // always allow touchmove default 73 | * allowTouchmoveDefault(myelem); 74 | * 75 | * // allow touchmove default only under certain conditions 76 | * allowTouchmoveDefault(myelem, function(touchmove){ 77 | * return touchmove.pageY > 100; 78 | * }); 79 | * ``` 80 | * 81 | * @param {Element|$element} element The element to bind. 82 | * @param {function} condition A `function(touchmove)⟶boolean` to decide 83 | * whether to allow default behavior or not. 84 | * 85 | * @service allowTouchmoveDefault 86 | * @as function 87 | * @returns function Function to unbind the listener 88 | */ 89 | 90 | module.factory('allowTouchmoveDefault', function(){ 91 | var fnTrue = function() { return true; }; 92 | 93 | if ('ontouchmove' in document) { 94 | return function($element, condition) { 95 | condition = condition || fnTrue; 96 | 97 | var allowTouchmoveDefaultCallback = function(e) { 98 | if (condition(e)) { e.allowTouchmoveDefault = true; } 99 | }; 100 | 101 | $element = angular.element($element); 102 | $element.on('touchmove', allowTouchmoveDefaultCallback); 103 | 104 | $element.on('$destroy', function() { 105 | $element.off('touchmove', allowTouchmoveDefaultCallback); 106 | $element = null; 107 | }); 108 | 109 | return function() { 110 | if ($element) { 111 | $element.off('touchmove', allowTouchmoveDefaultCallback); 112 | } 113 | }; 114 | }; 115 | } else { 116 | return angular.noop; 117 | } 118 | }); 119 | 120 | }()); -------------------------------------------------------------------------------- /src/js/components/navbars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mobile-angular-ui.components.navbars 3 | * @description 4 | * 5 | * Bootstrap default navbars are awesome for responsive websites, but are not the 6 | * best with a small screen. Also fixed positioning is yet not an option to create 7 | * navbars standing in top or bottom of the screen. 8 | * 9 | * Mobile Angular Ui offers an alternative to bootstrap navbars that is more 10 | * suitable for mobile. 11 | * 12 | * It uses scrollable areas to avoid scroll issues. In the following figure you can 13 | * see the difference between fixed navbars and navbars with absolute positioning. 14 | * 15 | *
16 | * 17 | *
18 | * 19 | * Here is the basic markup to achieve this. 20 | * 21 | * ``` html 22 | *
23 | * 26 | * 27 | * 30 | * 31 | *
32 | * 33 | *
34 | *
35 | * ``` 36 | * 37 | * As you can notice the base class is `.navbar-app` while the positioning is 38 | * obtained adding either `.navbar-absolute-top` or `.navbar-absolute-bottom` 39 | * class. 40 | * 41 | * ### Mobile Navbar Layout 42 | * 43 | * Top navbar in mobile design most of the times follows a clear pattern: a 44 | * centered title surrounded by one or two action buttons, the _back_ or the 45 | * _menu_ buttons are two common examples. 46 | * 47 | * Twitter Bootstrap ships with a different arrangement of components for navbars 48 | * since they are supposed to host an horizontal navigation menu. 49 | * 50 | * `.navbar-app` is specifically designed to support this different type of 51 | * `.interaction and arrangement. 52 | * 53 | * Consider the following example: 54 | * 55 | * ``` html 56 | * 74 | * 75 | * ``` 76 | * 77 | * `.navbar-brand-center` is a specialization of BS3's `.navbar-brand`. It will 78 | * render the title centered and below the two button groups. Note that `.navbar- 79 | * brand-center` will position the title with absolute positioning ensuring that 80 | * it will never cover the buttons, which would cause interaction problems. 81 | * 82 | */ 83 | 84 | (function() { 85 | 'use strict'; 86 | 87 | var module = angular.module('mobile-angular-ui.components.navbars', []); 88 | 89 | /** 90 | * @directive navbarAbsoluteTop 91 | * @restrict C 92 | * @description 93 | * 94 | * Setup absolute positioned top navbar. 95 | * 96 | * ``` html 97 | * 100 | * ``` 101 | */ 102 | 103 | /** 104 | * @directive navbarAbsoluteBottom 105 | * @restrict C 106 | * @description 107 | * 108 | * Setup absolute positioned bottom navbar. 109 | * 110 | * ``` html 111 | * 114 | * ``` 115 | */ 116 | angular.forEach(['top', 'bottom'], function(side) { 117 | var directiveName = 'navbarAbsolute' + side.charAt(0).toUpperCase() + side.slice(1); 118 | module.directive(directiveName, [ 119 | '$rootElement', 120 | function($rootElement) { 121 | return { 122 | restrict: 'C', 123 | link: function(scope) { 124 | $rootElement.addClass('has-navbar-' + side); 125 | scope.$on('$destroy', function(){ 126 | $rootElement.removeClass('has-navbar-' + side); 127 | }); 128 | } 129 | }; 130 | } 131 | ]); 132 | }); 133 | 134 | })(); -------------------------------------------------------------------------------- /src/js/components/modals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module will provide directives to create modals and overlays components. 3 | * 4 | * @module mobile-angular-ui.components.modals 5 | */ 6 | (function () { 7 | 'use strict'; 8 | angular.module('mobile-angular-ui.components.modals', []) 9 | 10 | /** 11 | * A directive to create modals and overlays components. 12 | * 13 | * Modals are basically the same of Bootstrap 3 but you have to use uiState 14 | * with `ngIf/uiIf` or `ngHide/uiHide` to `activate/dismiss` it. 15 | * 16 | * By default both modals and overlay are made always showing up by 17 | * css rule `.modal {display:block}`, so you can use it with 18 | * `ngAnimate` and other angular directives in a simpler way. 19 | * 20 | * Overlay are a style of modal that looks more native in mobile devices providing a blurred 21 | * overlay as background. 22 | * 23 | * You can create an overlay adding `.modal-overlay` class to a modal. 24 | * 25 | * ### Note 26 | * 27 | * For modals and overlays to cover the entire page you have to attach them 28 | * as child of `body` element. To achieve this from a view is a common use for 29 | * `contentFor/yieldTo` directives contained from 30 | * [capture module](/docs/module:mobile-angular-ui/module:core/module:capture): 31 | * 32 | * ``` html 33 | * 34 | * 35 | * 36 | * 37 | *
38 | * 39 | * 40 | * ``` 41 | * 42 | * Then you can wrap your modals and overlays in `contentFor`: 43 | * 44 | * ``` html 45 | *
46 | * * 47 | *
48 | * ``` 49 | * 50 | * **Example.** Create a Modal. 51 | * 52 | * ``` html 53 | *
54 | * 73 | *
74 | * ``` 75 | * 76 | * **Example.** Create an Overlay. 77 | * 78 | * ``` html 79 | *
80 | * 98 | *
99 | * ``` 100 | * 101 | * @directive modal 102 | * @restrict C 103 | */ 104 | .directive('modal', [ 105 | '$rootElement', 106 | function($rootElement) { 107 | return { 108 | restrict: 'C', 109 | link: function(scope, elem) { 110 | $rootElement.addClass('has-modal'); 111 | elem.on('$destroy', function(){ 112 | $rootElement.removeClass('has-modal'); 113 | }); 114 | scope.$on('$destroy', function(){ 115 | $rootElement.removeClass('has-modal'); 116 | }); 117 | 118 | if (elem.hasClass('modal-overlay')) { 119 | $rootElement.addClass('has-modal-overlay'); 120 | elem.on('$destroy', function(){ 121 | $rootElement.removeClass('has-modal-overlay'); 122 | }); 123 | scope.$on('$destroy', function(){ 124 | $rootElement.removeClass('has-modal-overlay'); 125 | }); 126 | } 127 | } 128 | }; 129 | }]); 130 | }()); -------------------------------------------------------------------------------- /src/js/components/switch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module with just a directive to create a switch input component. 3 | * 4 | * @module mobile-angular-ui.components.switch 5 | */ 6 | (function() { 7 | 'use strict'; 8 | angular.module('mobile-angular-ui.components.switch', []) 9 | 10 | /** 11 | * @directive uiSwitch 12 | * @restrict EA 13 | * @requires ngModel 14 | * @description 15 | * 16 | * The `ui-switch` directive (not to be confused with `ng-switch`) lets 17 | * you create a toggle switch control bound to a boolean `ngModel` value. 18 | * 19 | *
20 | * 21 | *
22 | * 23 | * It requires `ngModel`. It supports `ngChange` and `ngDisabled`. 24 | * 25 | * ``` html 26 | * 27 | * ``` 28 | * 29 | * ``` html 30 | * 31 | * ``` 32 | * 33 | * ``` html 34 | * 35 | * ``` 36 | * 37 | * Note that if `$drag` service from `mobile-angular-ui.gestures` is available 38 | * `ui-switch` will support drag too. 39 | * 40 | * @param {expression} ngModel The model bound to this component. 41 | * @param {boolean} [disabled] Whether this component should be disabled. 42 | * @param {expression} [ngChange] An expression to be evaluated when model changes. 43 | */ 44 | .directive('uiSwitch', ['$injector', function($injector) { 45 | var $drag = $injector.has('$drag') && $injector.get('$drag'); 46 | 47 | return { 48 | restrict: 'EA', 49 | scope: { 50 | model: '=ngModel', 51 | changeExpr: '@ngChange' 52 | }, 53 | link: function(scope, elem, attrs) { 54 | elem.addClass('switch'); 55 | 56 | var disabled = attrs.disabled || elem.attr('disabled'); 57 | 58 | var unwatchDisabled = scope.$watch( 59 | function() { 60 | return attrs.disabled || elem.attr('disabled'); 61 | }, 62 | function(value) { 63 | if (!value || value === 'false' || value === '0') { 64 | disabled = false; 65 | } else { 66 | disabled = true; 67 | } 68 | } 69 | ); 70 | 71 | var handle = angular.element('
'); 72 | elem.append(handle); 73 | 74 | if (scope.model) { 75 | elem.addClass('active'); 76 | } 77 | elem.addClass('switch-transition-enabled'); 78 | 79 | var unwatch = scope.$watch('model', function(value) { 80 | if (value) { 81 | elem.addClass('active'); 82 | } else { 83 | elem.removeClass('active'); 84 | } 85 | }); 86 | 87 | var isEnabled = function() { 88 | return !disabled; 89 | }; 90 | 91 | var setModel = function(value) { 92 | if (isEnabled() && value !== scope.model) { 93 | scope.model = value; 94 | scope.$apply(); 95 | if (scope.changeExpr !== null && scope.changeExpr !== undefined) { 96 | scope.$parent.$eval(scope.changeExpr); 97 | } 98 | } 99 | }; 100 | 101 | var clickCb = function() { 102 | setModel(!scope.model); 103 | }; 104 | 105 | elem.on('click tap', clickCb); 106 | 107 | var unbind = angular.noop(); 108 | 109 | if ($drag) { 110 | unbind = $drag.bind(handle, { 111 | transform: $drag.TRANSLATE_INSIDE(elem), 112 | start: function() { 113 | elem.off('click tap', clickCb); 114 | }, 115 | cancel: function() { 116 | handle.removeAttr('style'); 117 | elem.off('click tap', clickCb); 118 | elem.on('click tap', clickCb); 119 | }, 120 | end: function() { 121 | var rh = handle[0].getBoundingClientRect(); 122 | var re = elem[0].getBoundingClientRect(); 123 | if (rh.left - re.left < rh.width / 3) { 124 | setModel(false); 125 | handle.removeAttr('style'); 126 | } else if (re.right - rh.right < rh.width / 3) { 127 | setModel(true); 128 | handle.removeAttr('style'); 129 | } else { 130 | handle.removeAttr('style'); 131 | } 132 | elem.on('click tap', clickCb); 133 | } 134 | }); 135 | } 136 | 137 | elem.on('$destroy', function() { 138 | unbind(); 139 | unwatchDisabled(); 140 | unwatch(); 141 | isEnabled = setModel = unbind = unwatch = unwatchDisabled = clickCb = null; 142 | }); 143 | } 144 | }; 145 | }]); 146 | }()); -------------------------------------------------------------------------------- /src/js/migrate/toggle.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var module = angular.module('mobile-angular-ui.migrate.toggle', ['mobile-angular-ui.core.sharedState']); 5 | 6 | // Note! 7 | // This is an adaptation of 1.1 toggle/toggleable interface for SharedState service, 8 | // although this should fit the most common uses it is not 100% backward compatible. 9 | // 10 | // Differences are: 11 | // - toggleByClass behaviour is not supported 12 | // - toggleByClass/togglerLinked/toggle events are not supported 13 | // 14 | module.directive('toggle', ['SharedState',function(SharedState) { 15 | return { 16 | restrict: 'A', 17 | link: function(scope, elem, attrs) { 18 | var exclusionGroup = attrs.exclusionGroup, 19 | command = attrs.toggle || 'toggle', 20 | bubble = attrs.bubble !== undefined && attrs.bubble !== 'false', 21 | activeClass = attrs.activeClass, 22 | inactiveClass = attrs.inactiveClass, 23 | parentActiveClass = attrs.parentActiveClass, 24 | parentInactiveClass = attrs.parentInactiveClass, 25 | parent = elem.parent(), 26 | id = attrs.target; 27 | 28 | if ((!id) && attrs.href) { 29 | id = attrs.href.slice(1); 30 | } 31 | 32 | if (!id) { 33 | throw new Error('Toggle directive requires "target" attribute to be set. If you are using toggleByClass yet be aware that is not supported by migration version of toggle.\nPlease switch to ui-* directives instead.'); 34 | } 35 | 36 | var setupClasses = function(value) { 37 | if (value) { 38 | if (parentActiveClass) { parent.addClass(parentActiveClass); } 39 | if (activeClass) { elem.addClass(activeClass); } 40 | if (parentInactiveClass) { parent.removeClass(parentInactiveClass); } 41 | if (inactiveClass) { elem.removeClass(inactiveClass); } 42 | } else { 43 | if (parentActiveClass) { parent.removeClass(parentActiveClass); } 44 | if (activeClass) { elem.removeClass(activeClass); } 45 | if (parentInactiveClass) { parent.addClass(parentInactiveClass); } 46 | if (inactiveClass) { elem.addClass(inactiveClass); } 47 | } 48 | }; 49 | 50 | scope.$on('mobile-angular-ui.state.changed.' + id, function(evt, value) { 51 | setupClasses(value); 52 | }); 53 | 54 | setupClasses(SharedState.get('id')); 55 | 56 | elem.on("click tap", function(e) { 57 | if (!scope.$$phase) { 58 | scope.$apply(function() { 59 | if (command === 'on') { 60 | SharedState.turnOn(id); 61 | } else if (command === 'off') { 62 | SharedState.turnOff(id); 63 | } else { 64 | SharedState.toggle(id); 65 | } 66 | }); 67 | } 68 | 69 | if (!bubble) { 70 | e.preventDefault(); 71 | return false; 72 | } else { 73 | return true; 74 | } 75 | }); 76 | } 77 | }; 78 | }]); 79 | 80 | module.directive('toggleable', ['SharedState', '$rootScope', function(SharedState, $rootScope) { 81 | return { 82 | restrict: 'A', 83 | link: function(scope, elem, attrs) { 84 | 85 | var exclusionGroup = attrs.exclusionGroup, 86 | defaultValue = attrs.default === 'active', 87 | activeClass = attrs.activeClass, 88 | inactiveClass = attrs.inactiveClass, 89 | parentActiveClass = attrs.parentActiveClass, 90 | parentInactiveClass = attrs.parentInactiveClass, 91 | parent = elem.parent(), 92 | id = attrs.toggleable || attrs.id; 93 | 94 | scope.$on('mobile-angular-ui.state.changed.' + id, function(evt, value) { 95 | 96 | if (value) { 97 | if (parentActiveClass) { parent.addClass(parentActiveClass); } 98 | if (activeClass) { elem.addClass(activeClass); } 99 | if (parentInactiveClass) { parent.removeClass(parentInactiveClass); } 100 | if (inactiveClass) { elem.removeClass(inactiveClass); } 101 | } else { 102 | if (parentActiveClass) { parent.removeClass(parentActiveClass); } 103 | if (activeClass) { elem.removeClass(activeClass); } 104 | if (parentInactiveClass) { parent.addClass(parentInactiveClass); } 105 | if (inactiveClass) { elem.addClass(inactiveClass); } 106 | } 107 | 108 | $rootScope.$emit('mobile-angular-ui.toggle.toggled', id, value, exclusionGroup); 109 | }); 110 | 111 | 112 | SharedState.initialize(scope, id, {defaultValue: defaultValue, exclusionGroup: exclusionGroup}); 113 | } 114 | }; 115 | }]); 116 | 117 | module.run(['$rootScope', 'SharedState', function($rootScope, SharedState) { 118 | 119 | $rootScope.toggle = function(target, command) { 120 | if (command === 'on') { 121 | SharedState.turnOn(target); 122 | } else if (command === 'off') { 123 | SharedState.turnOff(target); 124 | } else { 125 | SharedState.toggle(target); 126 | } 127 | }; 128 | 129 | // $rootScope.toggleByClass = function(target, command) { 130 | // // Not supported 131 | // }; 132 | 133 | }]); 134 | }()); -------------------------------------------------------------------------------- /src/js/core/outerClick.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | @module mobile-angular-ui.core.outerClick 4 | @description 5 | 6 | Provides a directive to specifiy a behaviour when click/tap events 7 | happen outside an element. This can be easily used 8 | to implement eg. __close on outer click__ feature for a dropdown. 9 | 10 | ## Usage 11 | 12 | Declare it as a dependency to your app unless you have already 13 | included some of its super-modules. 14 | 15 | ``` 16 | angular.module('myApp', ['mobile-angular-ui']); 17 | ``` 18 | 19 | Or 20 | 21 | ``` 22 | angular.module('myApp', ['mobile-angular-ui.core']); 23 | ``` 24 | 25 | Or 26 | 27 | ``` 28 | angular.module('myApp', ['mobile-angular-ui.core.outerClick']); 29 | ``` 30 | 31 | Use `ui-outer-click` to define an expression to evaluate when an _Outer Click_ event happens. 32 | Use `ui-outer-click-if` parameter to define a condition to enable/disable the listener. 33 | 34 | ``` html 35 |
36 | 37 | 38 | 39 | 54 |
55 | ``` 56 | 57 | */ 58 | (function () { 59 | 'use strict'; 60 | 61 | var isAncestorOrSelf = function(element, target) { 62 | var parent = element; 63 | while (parent.length > 0) { 64 | if (parent[0] === target[0]) { 65 | parent = null; 66 | return true; 67 | } 68 | parent = parent.parent(); 69 | } 70 | parent = null; 71 | return false; 72 | }; 73 | 74 | angular.module('mobile-angular-ui.core.outerClick', []) 75 | 76 | /** 77 | * @service bindOuterClick 78 | * @as function 79 | * 80 | * @description 81 | * This is a service function that binds a callback to be conditionally executed 82 | * when a click event happens outside a specified element. 83 | * 84 | * Ie. 85 | * 86 | * ``` js 87 | * app.directive('myDirective', function('bindOuterClick'){ 88 | * return { 89 | * link: function(scope, element) { 90 | * bindOuterClick(element, function(scope, extra){ 91 | * alert('You clicked ouside me!'); 92 | * }, function(e){ 93 | * return element.hasClass('disabled') ? true : false; 94 | * }); 95 | * } 96 | * }; 97 | * }); 98 | * ``` 99 | * @scope {scope} the scope to eval callbacks 100 | * @param {DomElement|$element} element The element to bind to. 101 | * @param {function} callback A `function(scope, options)`, usually the result of `$parse`, that is called when an _outer click_ event happens. 102 | * @param {string|function} condition Angular `$watch` expression to decide whether to run `callback` or not. 103 | */ 104 | .factory('bindOuterClick', [ 105 | '$document', 106 | '$timeout', 107 | function ($document, $timeout) { 108 | 109 | return function (scope, element, outerClickFn, outerClickIf) { 110 | var handleOuterClick = function(event){ 111 | if (!isAncestorOrSelf(angular.element(event.target), element)) { 112 | scope.$apply(function() { 113 | outerClickFn(scope, {$event:event}); 114 | }); 115 | } 116 | }; 117 | 118 | var stopWatching = angular.noop; 119 | var t = null; 120 | 121 | if (outerClickIf) { 122 | stopWatching = scope.$watch(outerClickIf, function(value){ 123 | $timeout.cancel(t); 124 | 125 | if (value) { 126 | // prevents race conditions 127 | // activating with other click events 128 | t = $timeout(function() { 129 | $document.on('click tap', handleOuterClick); 130 | }, 0); 131 | 132 | } else { 133 | $document.unbind('click tap', handleOuterClick); 134 | } 135 | }); 136 | } else { 137 | $timeout.cancel(t); 138 | $document.on('click tap', handleOuterClick); 139 | } 140 | 141 | scope.$on('$destroy', function(){ 142 | stopWatching(); 143 | $document.unbind('click tap', handleOuterClick); 144 | }); 145 | }; 146 | } 147 | ]) 148 | 149 | 150 | /** 151 | * @directive outerClick 152 | * 153 | * @description 154 | * Evaluates an expression when an _Outer Click_ event happens. 155 | * 156 | * @param {expression} uiOuterClick Expression to evaluate when an _Outer Click_ event happens. 157 | * @param {expression} uiOuterClickIf Condition to enable/disable the listener. Defaults to `true`. 158 | */ 159 | .directive('uiOuterClick', [ 160 | 'bindOuterClick', 161 | '$parse', 162 | function(bindOuterClick, $parse){ 163 | return { 164 | restrict: 'A', 165 | compile: function(elem, attrs) { 166 | var outerClickFn = $parse(attrs.uiOuterClick); 167 | var outerClickIf = attrs.uiOuterClickIf; 168 | return function(scope, elem) { 169 | bindOuterClick(scope, elem, outerClickFn, outerClickIf); 170 | }; 171 | } 172 | }; 173 | } 174 | ]); 175 | }()); -------------------------------------------------------------------------------- /src/js/components/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module mobile-angular-ui.components.sidebars 3 | 4 | @description 5 | 6 | Sidebars can be placed either in left side or right side adding respectively `.sidebar-left` and `.sidebar-right` classes. 7 | 8 | ``` html 9 | 22 | 23 | 26 | ``` 27 | 28 | #### Interacting with sidebars 29 | 30 | Under the hood sidebar uses `SharedState` exposing respective statuses: `uiSidebarLeft` and `uiSidebarRight` unless you define different state name through `id` attribute on sidebar elements. 31 | 32 | ``` html 33 | Toggle sidebar left 34 | 35 | Toggle sidebar right 36 | ``` 37 | 38 | You can put `ui-turn-off='uiSidebarLeft'` or `ui-turn-off='uiSidebarLeft'` inside the sidebar to make it close after clicking links inside them. 39 | 40 | By default sidebar are closed by clicking/tapping outside them. 41 | 42 | */ 43 | (function() { 44 | 'use strict'; 45 | 46 | var module = angular.module( 47 | 'mobile-angular-ui.components.sidebars', [ 48 | 'mobile-angular-ui.core.sharedState', 49 | 'mobile-angular-ui.core.outerClick' 50 | ] 51 | ); 52 | 53 | angular.forEach(['left', 'right'], function (side) { 54 | var directiveName = 'sidebar' + side.charAt(0).toUpperCase() + side.slice(1); 55 | var stateName = 'ui' + directiveName.charAt(0).toUpperCase() + directiveName.slice(1); 56 | 57 | module.directive(directiveName, [ 58 | '$rootElement', 59 | 'SharedState', 60 | 'bindOuterClick', 61 | '$location', 62 | function ( 63 | $rootElement, 64 | SharedState, 65 | bindOuterClick, 66 | $location 67 | ) { 68 | return { 69 | restrict: 'C', 70 | link: function (scope, elem, attrs) { 71 | var parentClass = 'has-sidebar-' + side; 72 | var visibleClass = 'sidebar-' + side + '-visible'; 73 | var activeClass = 'sidebar-' + side + '-in'; 74 | 75 | if (attrs.id) { 76 | stateName = attrs.id; 77 | } 78 | 79 | var outerClickCb = function (){ 80 | SharedState.turnOff(stateName); 81 | }; 82 | 83 | var outerClickIf = function() { 84 | return SharedState.isActive(stateName); 85 | }; 86 | 87 | $rootElement.addClass(parentClass); 88 | scope.$on('$destroy', function () { 89 | $rootElement 90 | .removeClass(parentClass); 91 | $rootElement 92 | .removeClass(visibleClass); 93 | $rootElement 94 | .removeClass(activeClass); 95 | }); 96 | 97 | var defaultActive = attrs.active !== undefined && attrs.active !== 'false'; 98 | SharedState.initialize(scope, stateName, {defaultValue: defaultActive}); 99 | 100 | scope.$on('mobile-angular-ui.state.changed.' + stateName, function (e, active) { 101 | if (attrs.uiTrackAsSearchParam === '' || attrs.uiTrackAsSearchParam) { 102 | $location.search(stateName, active || null); 103 | } 104 | 105 | if (active) { 106 | $rootElement 107 | .addClass(visibleClass); 108 | $rootElement 109 | .addClass(activeClass); 110 | } else { 111 | $rootElement 112 | .removeClass(activeClass); 113 | // Note: .removeClass(visibleClass) is called on 'mobile-angular-ui.app.transitionend' 114 | } 115 | }); 116 | 117 | scope.$on('$routeChangeSuccess', function() { 118 | SharedState.turnOff(stateName); 119 | }); 120 | 121 | scope.$on('$routeUpdate', function() { 122 | if (attrs.uiTrackAsSearchParam) { 123 | if (($location.search())[stateName]) { 124 | SharedState.turnOn(stateName); 125 | } else { 126 | SharedState.turnOff(stateName); 127 | } 128 | } 129 | }); 130 | 131 | scope.$on('mobile-angular-ui.app.transitionend', function() { 132 | if (!SharedState.isActive(stateName)) { 133 | $rootElement.removeClass(visibleClass); 134 | } 135 | }); 136 | 137 | if (attrs.closeOnOuterClicks !== 'false') { 138 | bindOuterClick(scope, elem, outerClickCb, outerClickIf); 139 | } 140 | } 141 | }; 142 | } 143 | ]); 144 | }); 145 | 146 | module.directive('app', ['$rootScope', function($rootScope) { 147 | return { 148 | restrict: 'C', 149 | link: function(scope, element) { 150 | 151 | element.on('transitionend webkitTransitionEnd oTransitionEnd otransitionend', function() { 152 | $rootScope.$broadcast('mobile-angular-ui.app.transitionend'); 153 | }); 154 | 155 | } 156 | }; 157 | }]); 158 | }()); -------------------------------------------------------------------------------- /src/js/gestures/swipe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A module providing swipe gesture services and directives. 3 | * 4 | * @module mobile-angular-ui.gestures.swipe 5 | */ 6 | (function() { 7 | 'use strict'; 8 | 9 | var module = angular.module('mobile-angular-ui.gestures.swipe', 10 | ['mobile-angular-ui.gestures.touch']); 11 | 12 | /** 13 | * An adaptation of `ngTouch.$swipe`, it is basically the same despite of: 14 | * 15 | * - It is based on [$touch](../module:touch) 16 | * - Swipes are recognized by touch velocity and direction 17 | * - It does not require `ngTouch` thus is better compatible with fastclick.js 18 | * - Swipe directives are nestable 19 | * - It allows to unbind 20 | * - It has only one difference in interface, and its about how to pass `pointerTypes`: 21 | * 22 | * ``` js 23 | * // ngTouch.$swipe 24 | * $swipe.bind(..., ['mouse', ... }); 25 | * 26 | * // mobile-angular-ui.gestures.swipe.$swipe 27 | * $swipe.bind(..., pointerTypes: { mouse: { start: 'mousedown', ...} }); 28 | * ``` 29 | * This is due to the fact that the second parameter of `$swipe.bind` is destinated to options for 30 | * underlying `$touch` service. 31 | * 32 | * @service $swipe 33 | * @as class 34 | */ 35 | module.factory('$swipe', ['$touch', function($touch) { 36 | var VELOCITY_THRESHOLD = 500; // px/sec 37 | var MOVEMENT_THRESHOLD = 10; // px 38 | var TURNAROUND_MAX = 10; // px 39 | var ANGLE_THRESHOLD = 10; // deg 40 | var abs = Math.abs; 41 | 42 | var defaultOptions = { 43 | movementThreshold: MOVEMENT_THRESHOLD, // start to consider only if movement 44 | // exceeded MOVEMENT_THRESHOLD 45 | valid: function(t) { 46 | var absAngle = abs(t.angle); 47 | absAngle = absAngle >= 90 ? absAngle - 90 : absAngle; 48 | 49 | var validDistance = t.total - t.distance <= TURNAROUND_MAX, 50 | validAngle = absAngle <= ANGLE_THRESHOLD || absAngle >= 90 - ANGLE_THRESHOLD, 51 | validVelocity = t.averageVelocity >= VELOCITY_THRESHOLD; 52 | 53 | return validDistance && validAngle && validVelocity; 54 | } 55 | }; 56 | 57 | return { 58 | /** 59 | * Bind swipe gesture handlers for an element. 60 | * 61 | * ``` js 62 | * var unbind = $swipe.bind(elem, { 63 | * end: function(touch) { 64 | * console.log('Swiped:', touch.direction); 65 | * unbind(); 66 | * } 67 | * }); 68 | * ``` 69 | * 70 | * **Swipes Detection** 71 | * 72 | * Before consider a touch to be a swipe Mobile Angular UI verifies that: 73 | * 74 | * 1. Movement is quick. Average touch velocity should exceed a `VELOCITY_THRESHOLD`. 75 | * 2. Movement is linear. 76 | * 3. Movement has a clear, non-ambiguous direction. So we can assume without error 77 | * that underlying `touch.direction` is exactly the swipe direction. For that 78 | * movement is checked against an `ANGLE_THRESHOLD`. 79 | * 80 | * @param {Element|$element} element The element to observe for swipe gestures. 81 | * @param {object} eventHandlers An object with handlers for specific swipe events. 82 | * @param {function} [eventHandlers.start] The callback for swipe start event. 83 | * @param {function} [eventHandlers.end] The callback for swipe end event. 84 | * @param {function} [eventHandlers.move] The callback for swipe move event. 85 | * @param {function} [eventHandlers.cancel] The callback for swipe cancel event. 86 | * @param {object} [options] Options to be passed to underlying [$touch.bind](../module:touch) function. 87 | * 88 | * @returns {function} The unbind function. 89 | * 90 | * @method bind 91 | * @memberOf mobile-angular-ui.gestures.swipe~$swipe 92 | */ 93 | bind: function(element, eventHandlers, options) { 94 | options = angular.extend({}, defaultOptions, options || {}); 95 | return $touch.bind(element, eventHandlers, options); 96 | } 97 | }; 98 | }]); 99 | 100 | /** 101 | * Specify custom behavior when an element is swiped to the left on a touchscreen device. 102 | * A leftward swipe is a quick, right-to-left slide of the finger. 103 | * 104 | * @directive uiSwipeLeft 105 | * @param {expression} uiSwipeLeft An expression to be evaluated on leftward swipe. 106 | */ 107 | /** 108 | * Specify custom behavior when an element is swiped to the right on a touchscreen device. 109 | * A rightward swipe is a quick, left-to-right slide of the finger. 110 | * 111 | * @directive uiSwipeRight 112 | * @param {expression} uiSwipeRight An expression to be evaluated on rightward swipe. 113 | */ 114 | /** 115 | * Alias for [uiSwipeLeft](#uiswipeleft). 116 | * 117 | * @directive ngSwipeLeft 118 | * @deprecated 119 | */ 120 | /** 121 | * Alias for [uiSwipeRight](#uiswiperight). 122 | * 123 | * @directive ngSwipeRight 124 | * @deprecated 125 | */ 126 | angular.forEach(['ui', 'ng'], function(prefix) { 127 | angular.forEach(['Left', 'Right'], function(direction) { 128 | var directiveName = prefix + 'Swipe' + direction; 129 | module.directive(directiveName, ['$swipe', '$parse', function($swipe, $parse){ 130 | return { 131 | link: function(scope, elem, attrs) { 132 | var onSwipe = $parse(attrs[directiveName]); 133 | $swipe.bind(elem, { 134 | end: function(swipe, event) { 135 | if (swipe.direction === direction.toUpperCase()) { 136 | if (!event.__UiSwipeHandled__) { 137 | event.__UiSwipeHandled__ = true; 138 | scope.$apply(function() { 139 | onSwipe(scope, {$touch: swipe}); 140 | }); 141 | } 142 | } 143 | } 144 | }); 145 | } 146 | }; 147 | }]); 148 | }); 149 | }); 150 | }()); -------------------------------------------------------------------------------- /test/onprepare.js: -------------------------------------------------------------------------------- 1 | /* global module: true, global: true, browser: false, expect: true */ 2 | 3 | module.exports = function() { 4 | 5 | // ie. 6 | // 7 | // expect(ngInvoke(function($rootScope){ 8 | // return $rootScope.myVar; 9 | // })).toEqual(5); 10 | // 11 | var ngInvokeAsIs = function(fn) { 12 | var script = 'return angular.element(document.querySelector(\'[ng-app]\')).injector().invoke(' + fn.toString() + ');'; 13 | return browser.executeScript(script); 14 | }; 15 | 16 | // ie. 17 | // 18 | // expect(ngInvoke(function($rootScope){ 19 | // return $rootScope.myVar; 20 | // })).toEqual(5); 21 | // 22 | // or: 23 | // 24 | // expect(ngInvoke('$rootScope.myVar')).toEqual(5); 25 | // 26 | global.ngInvoke = function(fnOrStr) { 27 | if (typeof fnOrStr === 'string') { 28 | var parts = fnOrStr.split('.'); 29 | var svc = parts[0]; 30 | fnOrStr = 'function('+svc+') { return '+ fnOrStr + '; }'; 31 | } 32 | return ngInvokeAsIs(fnOrStr); 33 | }; 34 | 35 | // ie. 36 | // 37 | // var $rootScope = ngService('$rootscope') 38 | // 39 | // expect($rootScope.prop('myVar')).toEqual(5); 40 | // expect($rootScope.invoke('myFunc', arg1, arg2)).toEqual(5); 41 | // 42 | global.ngService = function(serviceName) { 43 | return { 44 | prop: function(propName) { 45 | return ngInvokeAsIs('function('+serviceName+') { return '+serviceName+'.' +propName+ '; }'); 46 | }, 47 | invoke: function() { 48 | var args = Array.prototype.slice.call(arguments, 0, arguments.length); 49 | var funcName = args.shift(); 50 | args = args.map(function(arg) { 51 | JSON.stringify(arg); 52 | }).join(','); 53 | return ngInvokeAsIs('function('+serviceName+') { return '+ serviceName + '.' +funcName+ '('+args+'); }'); 54 | } 55 | }; 56 | }; 57 | 58 | var oldExpect = global.expect; 59 | 60 | global.expect = function(test) { 61 | if (typeof test === 'function') { 62 | return oldExpect(ngInvokeAsIs(test)); 63 | } else { 64 | return oldExpect(test); 65 | } 66 | }; 67 | 68 | global.ngExpect = function(test) { 69 | return oldExpect(global.ngInvoke(test)); 70 | }; 71 | 72 | global.expectNoErrors = function() { 73 | browser.manage().logs().get('browser').then(function(logs) { 74 | var errors = logs.filter(function(log) { return log.level.value > 900 }) 75 | .map(function(log) { return log.message; }) 76 | .join('\n'); 77 | 78 | if (errors !== '') { // silence if expectation matches 79 | expect(errors).toEqual(''); 80 | } 81 | }); 82 | }; 83 | 84 | 85 | var maxWaitTimeoutMs = 50; // 5secs 86 | 87 | /** 88 | * Custom Jasmine matcher builder that waits for an element to have 89 | * or not have an html class. 90 | * @param {String} expectation The html class name 91 | * @return {Boolean} Returns the expectation result 92 | * 93 | * Uses the following object properties: 94 | * {ElementFinder} this.actual The element to find 95 | * Creates the following object properties: 96 | * {String} this.message The error message to show 97 | * {Error} this.spec.lastStackTrace A better stack trace of user's interest 98 | */ 99 | function toHaveClassFnBuilder(builderTypeBool) { 100 | return function toHaveClass(clsName) { 101 | if (clsName == null) throw new Error( 102 | "Custom matcher toHaveClass needs a class name"); 103 | var customMatcherFnThis = this; 104 | var elmFinder = customMatcherFnThis.actual; 105 | if (!elmFinder.element) throw new Error( 106 | "This custom matcher only works on an actual ElementFinder."); 107 | 108 | var driverWaitIterations = 0; 109 | var lastWebdriverError; 110 | 111 | var thisIsNot = this.isNot; 112 | var testHaveClass = !thisIsNot; 113 | if (!builderTypeBool) { 114 | testHaveClass = !testHaveClass; 115 | } 116 | var haveOrNot = testHaveClass ? 'have' : 'not to have'; 117 | customMatcherFnThis.message = function message() { 118 | var msg = (elmFinder.locator().message || elmFinder.locator().toString()); 119 | return "Expected '" + msg + "' to " + haveOrNot + 120 | " class " + clsName + ". " 121 | "After " + driverWaitIterations + " driverWaitIterations. " + 122 | "Last webdriver error: " + lastWebdriverError; 123 | }; 124 | 125 | // This will be picked up by elgalu/jasminewd#jasmine_retry 126 | customMatcherFnThis.spec.lastStackTrace = new Error('Custom Matcher'); 127 | function haveClassOrNotError(err) { 128 | lastWebdriverError = err.toString(); 129 | return false; 130 | }; 131 | 132 | return browser.driver.wait(function() { 133 | driverWaitIterations++; 134 | return elmFinder.getAttribute('class'). 135 | then(function getAttributeClass(classes) { 136 | var hasClass = classes.split(' ').indexOf(clsName) !== -1; 137 | if (testHaveClass) { 138 | lastWebdriverError = 'class present:' + hasClass; 139 | return hasClass; 140 | } else { 141 | lastWebdriverError = 'class absent:' + !hasClass; 142 | return !hasClass; 143 | } 144 | }, haveClassOrNotError); 145 | }, maxWaitTimeoutMs). 146 | then(function(waitResult) { 147 | if (thisIsNot) { 148 | // Jasmine 1.3.1 expects to fail on negation 149 | return !waitResult; 150 | } else { 151 | return waitResult; 152 | } 153 | }, function(err) { 154 | // Jasmine 1.3.1 expects to fail on negation 155 | return thisIsNot; 156 | }); 157 | }; 158 | }; 159 | 160 | // Add the custom matchers to jasmine 161 | beforeEach(function() { 162 | this.addMatchers({ 163 | toHaveClass: toHaveClassFnBuilder(true), 164 | toNotHaveClass: toHaveClassFnBuilder(false), 165 | }); 166 | }); 167 | }; -------------------------------------------------------------------------------- /examples/clock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Analog clock time picker with $drag 9 | 10 | 11 | 12 | 13 | 74 | 104 | 194 | 195 | 196 | 197 |
198 |
199 | {{ selectedHour || "--" }} : 00 200 |
201 |
202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /dist/css/mobile-angular-ui-desktop.min.css: -------------------------------------------------------------------------------- 1 | @media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11{float:left}.col-md-12{float:left;width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11{float:left}.col-lg-12{float:left;width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#007aff;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#0055b3;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;background-color:#007aff;border-color:#007aff;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;background-color:#eee}.jumbotron,.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}@media (min-width:992px){.sidebar{pointer-events:auto}.sidebar-toggle{display:none}.navbar-brand-center{text-align:left;width:auto;float:left;position:relative}.sidebar-left{display:block;left:0;width:300px}.sidebar-right{display:block;right:0;left:auto;width:300px}.sidebar-right-in .sidebar-left,.sidebar-left-in .sidebar-right{-webkit-transform:translate3d(0,0,0)!important;-moz-transform:translate3d(0,0,0)!important;transform:translate3d(0,0,0)!important}.has-sidebar-left .app,.sidebar-left-in .app{margin-left:300px;left:0}.has-sidebar-left .app,.sidebar-left-in .app,.sidebar-right-in .app{width:auto;-webkit-transform:translate3d(0,0,0)!important;-moz-transform:translate3d(0,0,0)!important;transform:translate3d(0,0,0)!important;position:relative;-webkit-transition:-webkit-transform 0 ease;-moz-transition:-moz-transform 0 ease;transition:transform 0 ease}.sidebar-right-in .app{margin-right:300px;right:0}}@media (min-width:1200px){.sidebar-left,.sidebar-right{width:320px}.has-sidebar-left .app,.sidebar-left-in .app{margin-left:320px}.sidebar-right-in .app{margin-right:320px}} -------------------------------------------------------------------------------- /dist/js/mobile-angular-ui.migrate.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";angular.module("mobile-angular-ui.migrate.carousel",[]).run(["$rootScope",function(e){e.carouselPrev=function(a){e.$emit("mobile-angular-ui.carousel.prev",a)},e.carouselNext=function(a){e.$emit("mobile-angular-ui.carousel.next",a)};var a=function(e){var a=angular.element(document.getElementById(e)),l=angular.element(a.children()[0]).children();return a=null,l},l=function(e){for(var a=-1,l=!1,n=0;n=r?angular.element(t[i]).addClass("active"):angular.element(t[r-1]).addClass("active"),t=null,r=null,i=null}),e.$on("mobile-angular-ui.carousel.next",function(e,n){var t=a(n),r=l(t),i=t.length-1;-1!==r&&angular.element(t[r]).removeClass("active"),r===i?angular.element(t[0]).addClass("active"):angular.element(t[r+1]).addClass("active"),t=null,r=null,i=null})}])}(),function(){"use strict";angular.module("mobile-angular-ui.migrate.disabled",[]).run(["$document",function(e){return angular.element(e).on("click tap",function(e){var a;return a=angular.element(e.target),a.hasClass("disabled")?(e.preventDefault(),e.stopPropagation(),a=null,!1):(a=null,!0)})}])}(),function(){"use strict";var e=angular.module("mobile-angular-ui.migrate.forms",[]);e.directive("bsFormControl",function(){function e(e){for(var l="",n="",t=0;t=t;t++)a["col-"+l[n]+"-"+t]=!0;return{replace:!0,require:"ngModel",link:function(a,l,n){(null===n.labelClass||void 0===n.labelClass)&&(n.labelClass=""),(null===n.id||void 0===n.id)&&(n.id=n.ngModel.replace(".","_")+"_input"),("SELECT"==l[0].tagName||("INPUT"==l[0].tagName||"TEXTAREA"==l[0].tagName)&&"checkbox"!=n.type&&"radio"!=n.type)&&l.addClass("form-control");var t=angular.element('"),r=angular.element('
'),i=angular.element('
'),o=e(n.labelClass.split(/\s+/));""===o.i&&t.addClass("col-xs-12"),t.addClass(n.labelClass);var s=e(l[0].className.split(/\s+/));l.removeClass(s.i),i.addClass(s.i),""===s.i&&i.addClass("col-xs-12"),l.wrap(r).wrap(i),l.parent().parent().prepend(t),l.attr("id",n.id),t=r=i=o=s=null}}})}(),function(){"use strict";var e=angular.module("mobile-angular-ui.migrate.namespaceAliases",[]),a=function(e){var a="-";return e=e.replace(/[A-Z]/g,function(e){return a+e.toLowerCase()}),e.replace("/^"+a+"/","")},l=function(l,n,t){t=t||{};var r=t.beforeLink,i=t.restrict||"A";e.directive(l,["$compile",function(e){return{restrict:i,priority:99999,compile:function(t,o){var s=angular.element(document.createElement("div"));t.after(s),t.remove();var u=a(n),c=a(l);return i.match(/A/)&&(t.attr(u,t.attr(c)),t.removeAttr(c)),i.match(/E/)&&(t[0].name=u),r&&r(t,o),function(a){s.replaceWith(t),e(t)(a),t=null}}}}])};l("switch","uiSwitch"),l("contentFor","uiContentFor",{beforeLink:function(e){e.attr("uiDuplicate",e.attr("duplicate"))}}),l("yieldTo","uiYieldTo")}(),function(){"use strict";var e=angular.module("mobile-angular-ui.migrate.overlay",[]);e.directive("overlay",["$compile",function(e){return{compile:function(a){var l=a.html();return a.remove(),function(a,n,t){var r="",i=l,o=t.overlay;t["default"]&&(r="default='"+t["default"]+"'");var s='
\n
\n
\n \n \n \n
\n
\n '+i+"\n
\n
\n
\n
",u=angular.element(document.getElementById(o));u.length>0&&u.hasClass("overlay")&&u.remove(),i=angular.element(document.body),i.prepend(e(s)(a)),"active"===t["default"]&&i.addClass("overlay-in")}}}}])}(),function(){"use strict";var e=angular.module("mobile-angular-ui.migrate.panels",[]);e.directive("bsPanel",function(){return{restrict:"EA",replace:!0,scope:!1,transclude:!0,link:function(e,a){a.removeAttr("title")},template:function(e,a){var l="";return a.title&&(l='
\n

\n '+a.title+"\n

\n
"),'
\n '+l+'\n
\n
\n
\n
'}}})}(),function(){"use strict";angular.module("mobile-angular-ui.migrate.switch",[]).directive("switch",function(){return{restrict:"EA",replace:!0,scope:{model:"=ngModel",changeExpr:"@ngChange",disabled:"@"},template:"
",link:function(e,a,l){a.on("click tap",function(){(null===l.disabled||void 0===l.disabled)&&(e.model=!e.model,e.$apply(),null!==e.changeExpr&&void 0!==e.changeExpr&&e.$parent.$eval(e.changeExpr))}),a.addClass("switch-transition-enabled")}}})}(),function(){"use strict";var e=angular.module("mobile-angular-ui.migrate.toggle",["mobile-angular-ui.core.sharedState"]);e.directive("toggle",["SharedState",function(e){return{restrict:"A",link:function(a,l,n){var t=(n.exclusionGroup,n.toggle||"toggle"),r=void 0!==n.bubble&&"false"!==n.bubble,i=n.activeClass,o=n.inactiveClass,s=n.parentActiveClass,u=n.parentInactiveClass,c=l.parent(),d=n.target;if(!d&&n.href&&(d=n.href.slice(1)),!d)throw new Error('Toggle directive requires "target" attribute to be set. If you are using toggleByClass yet be aware that is not supported by migration version of toggle.\nPlease switch to ui-* directives instead.');var g=function(e){e?(s&&c.addClass(s),i&&l.addClass(i),u&&c.removeClass(u),o&&l.removeClass(o)):(s&&c.removeClass(s),i&&l.removeClass(i),u&&c.addClass(u),o&&l.addClass(o))};a.$on("mobile-angular-ui.state.changed."+d,function(e,a){g(a)}),g(e.get("id")),l.on("click tap",function(l){return a.$$phase||a.$apply(function(){"on"===t?e.turnOn(d):"off"===t?e.turnOff(d):e.toggle(d)}),r?!0:(l.preventDefault(),!1)})}}}]),e.directive("toggleable",["SharedState","$rootScope",function(e,a){return{restrict:"A",link:function(l,n,t){var r=t.exclusionGroup,i="active"===t.default,o=t.activeClass,s=t.inactiveClass,u=t.parentActiveClass,c=t.parentInactiveClass,d=n.parent(),g=t.toggleable||t.id;l.$on("mobile-angular-ui.state.changed."+g,function(e,l){l?(u&&d.addClass(u),o&&n.addClass(o),c&&d.removeClass(c),s&&n.removeClass(s)):(u&&d.removeClass(u),o&&n.removeClass(o),c&&d.addClass(c),s&&n.addClass(s)),a.$emit("mobile-angular-ui.toggle.toggled",g,l,r)}),e.initialize(l,g,{defaultValue:i,exclusionGroup:r})}}}]),e.run(["$rootScope","SharedState",function(e,a){e.toggle=function(e,l){"on"===l?a.turnOn(e):"off"===l?a.turnOff(e):a.toggle(e)}}])}(),function(){"use strict";angular.module("mobile-angular-ui.migrate",["mobile-angular-ui.migrate.toggle","mobile-angular-ui.migrate.forms","mobile-angular-ui.migrate.panels","mobile-angular-ui.migrate.disabled","mobile-angular-ui.migrate.overlay","mobile-angular-ui.migrate.carousel","mobile-angular-ui.migrate.namespaceAliases","mobile-angular-ui.migrate.switch"])}(); 2 | //# sourceMappingURL=mobile-angular-ui.migrate.min.js.map -------------------------------------------------------------------------------- /src/js/core/capture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mobile-angular-ui.core.capture 3 | * @description 4 | * 5 | * The `capture` module exposes directives to let you extract markup which can be used in other parts of a template using `uiContentFor` and `uiYieldTo` directives. 6 | * 7 | * It provides a way to move or clone a block of markup to other parts of the document. 8 | * 9 | * This method is particularly useful to setup parts of the layout within an angular view. Since blocks of html are transplanted within their original `$scope` is easy to create layout interactions depending on the context. Some tipical task you can accomplish with these directives are: _setup the navbar title depending on the view_ or _place a submit button for a form inside a navbar_. 10 | * 11 | * ## Usage 12 | * 13 | * Declare it as a dependency to your app unless you have already included some of its super-modules. 14 | * 15 | * ``` 16 | * angular.module('myApp', ['mobile-angular-ui']); 17 | * ``` 18 | * 19 | * Or 20 | * 21 | * ``` 22 | * angular.module('myApp', ['mobile-angular-ui']); 23 | * ``` 24 | * 25 | * Or 26 | * 27 | * ``` 28 | * angular.module('myApp', ['mobile-angular-ui.core.capture']); 29 | * ``` 30 | * 31 | * Use `ui-yield-to` as a placeholder. 32 | * 33 | * ``` html 34 | * 35 | * 36 | * 41 | * 42 | *
43 | * 44 | *
45 | * ``` 46 | * 47 | * Use `ui-content-for` inside any view to populate the `ui-yield-to` content. 48 | * 49 | * ``` html 50 | * 51 | * 52 | *
53 | * My View Title 54 | *
55 | * ``` 56 | * 57 | * Since the original scope is preserved you can use directives inside `ui-content-for` blocks to interact with the current scope. In the following example we will add a navbar button to submit a form inside a nested view. 58 | * 59 | * 60 | * ``` html 61 | * 62 | * 63 | * 67 | * 68 | *
69 | * 70 | *
71 | * ``` 72 | * 73 | * ``` html 74 | * 75 | * 76 | *
77 | * 78 | *
79 | * 80 | *
81 | * 82 | *
83 | * 86 | *
87 | * 88 | *
89 | * ``` 90 | * 91 | * ``` javascript 92 | * app.controller('newCustomerController', function($scope, Store){ 93 | * $scope.customer = {}; 94 | * $scope.createCustomer = function(){ 95 | * Store.create($scope.customer); 96 | * // ... 97 | * } 98 | * }); 99 | * ``` 100 | * 101 | * If you wish you can also duplicate markup instead of move it. Just add `duplicate` parameter to `uiContentFor` directive to specify this behaviour. 102 | * 103 | * ``` html 104 | *
105 | * 108 | *
109 | * ``` 110 | */ 111 | (function () { 112 | 'use strict'; 113 | 114 | angular.module('mobile-angular-ui.core.capture', []) 115 | 116 | .run([ 117 | 'Capture', 118 | '$rootScope', 119 | function(Capture, $rootScope) { 120 | $rootScope.$on('$routeChangeStart', function() { 121 | Capture.resetAll(); 122 | }); 123 | } 124 | ]) 125 | 126 | .factory('Capture', [ 127 | '$compile', 128 | function($compile) { 129 | var yielders = {}; 130 | 131 | return { 132 | resetAll: function() { 133 | for (var name in yielders) { 134 | if (yielders.hasOwnProperty(name)) { 135 | this.resetYielder(name); 136 | } 137 | } 138 | }, 139 | 140 | resetYielder: function(name) { 141 | var b = yielders[name]; 142 | this.setContentFor(name, b.defaultContent, b.defaultScope); 143 | }, 144 | 145 | putYielder: function(name, element, defaultScope, defaultContent) { 146 | var yielder = {}; 147 | yielder.name = name; 148 | yielder.element = element; 149 | yielder.defaultContent = defaultContent || ''; 150 | yielder.defaultScope = defaultScope; 151 | yielders[name] = yielder; 152 | }, 153 | 154 | getYielder: function(name) { 155 | return yielders[name]; 156 | }, 157 | 158 | removeYielder: function(name) { 159 | delete yielders[name]; 160 | }, 161 | 162 | setContentFor: function(name, content, scope) { 163 | var b = yielders[name]; 164 | if (!b) { 165 | return; 166 | } 167 | b.element.html(content); 168 | $compile(b.element.contents())(scope); 169 | } 170 | 171 | }; 172 | } 173 | ]) 174 | 175 | /** 176 | * @directive uiContentFor 177 | * @restrict A 178 | * @description 179 | * 180 | * `ui-content-for` makes inner contents to replace the corresponding 181 | * `ui-yield-to` placeholder contents. 182 | * 183 | * `uiContentFor` is intended to be used inside a view in order to populate outer placeholders. 184 | * Any content you send to placeholders via `ui-content-for` is 185 | * reverted to placeholder defaults after view changes (ie. on `$routeChangeStart`). 186 | * 187 | * @param {string} uiContentFor The id of the placeholder to be replaced 188 | * @param {boolean} uiDuplicate If present duplicates the content instead of moving it (default to `false`) 189 | * 190 | */ 191 | .directive('uiContentFor', [ 192 | 'Capture', 193 | function(Capture) { 194 | return { 195 | compile: function(tElem, tAttrs) { 196 | var rawContent = tElem.html(); 197 | if(tAttrs.uiDuplicate === null || tAttrs.uiDuplicate === undefined) { 198 | // no need to compile anything! 199 | tElem.html(''); 200 | tElem.remove(); 201 | } 202 | return function(scope, elem, attrs) { 203 | Capture.setContentFor(attrs.uiContentFor, rawContent, scope); 204 | }; 205 | } 206 | }; 207 | } 208 | ]) 209 | 210 | /** 211 | * @directive uiYieldTo 212 | * @restrict A 213 | * @description 214 | * 215 | * `ui-yield-to` defines a placeholder which contents will be further replaced by `ui-content-for` directive. 216 | * 217 | * Inner html is considered to be a default. Default is restored any time `$routeChangeStart` happens. 218 | * 219 | * @param {string} uiYieldTo The unique id of this placeholder. 220 | * 221 | */ 222 | .directive('uiYieldTo', [ 223 | '$compile', 'Capture', function($compile, Capture) { 224 | return { 225 | link: function(scope, element, attr) { 226 | Capture.putYielder(attr.uiYieldTo, element, scope, element.html()); 227 | 228 | element.on('$destroy', function(){ 229 | Capture.removeYielder(attr.uiYieldTo); 230 | }); 231 | 232 | scope.$on('$destroy', function(){ 233 | Capture.removeYielder(attr.uiYieldTo); 234 | }); 235 | } 236 | }; 237 | } 238 | ]); 239 | 240 | }()); -------------------------------------------------------------------------------- /dist/css/mobile-angular-ui-hover.min.css: -------------------------------------------------------------------------------- 1 | a:hover{outline:0;color:#0055b3;text-decoration:underline}a.text-primary:hover{color:#0062cc}a.text-success:hover{color:#2b542c}a.text-info:hover{color:#245269}a.text-warning:hover{color:#66512c}a.text-danger:hover{color:#843534}a.bg-primary:hover{background-color:#0062cc}a.bg-success:hover{background-color:#c1e2b3}a.bg-info:hover{background-color:#afd9ee}a.bg-warning:hover{background-color:#f7ecb5}a.bg-danger:hover{background-color:#e4b9b9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.btn:hover{color:#333;text-decoration:none}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-primary:hover{color:#fff;background-color:#0062cc;border-color:#0051a8}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover{background-color:#007aff;border-color:#006ee6}.btn-success:hover{color:#fff;background-color:#2ac845;border-color:#24aa3b}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover{background-color:#4cd964;border-color:#37d552}.btn-info:hover{color:#fff;background-color:#218ebd;border-color:#1b779e}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover{background-color:#34aadc;border-color:#249ed2}.btn-warning:hover{color:#fff;background-color:#cca300;border-color:#a88700}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover{background-color:#fc0;border-color:#e6b800}.btn-danger:hover{color:#fff;background-color:#fc0d00;border-color:#d80b00}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover{background-color:#ff3b30;border-color:#ff2317}.btn-link:hover{border-color:transparent;color:#0055b3;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#007aff}.dropdown-menu>.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.input-group-btn>.btn:hover{z-index:2}.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a:hover{background-color:#eee;border-color:#007aff}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}.nav-pills>li.active>a:hover{color:#fff;background-color:#007aff}.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}.navbar-brand:hover{text-decoration:none}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.navbar-default .navbar-brand:hover{color:#0062cc;background-color:transparent}.navbar-default .navbar-nav>li>a:hover{color:#006ee6;background-color:transparent}.navbar-default .navbar-nav>.active>a:hover{color:#1a87ff;background-color:#e6e6e6}.navbar-default .navbar-nav>.disabled>a:hover{color:#66afff;background-color:transparent}.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-nav>.open>a:hover{background-color:#e6e6e6;color:#1a87ff}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#006ee6;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#1a87ff;background-color:#e6e6e6}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#66afff;background-color:transparent}}.navbar-default .navbar-link:hover,.navbar-default .btn-link:hover{color:#006ee6}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover{color:#66afff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-nav>.open>a:hover{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link:hover,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .disabled>a:hover{color:#777;background-color:#fff;cursor:not-allowed}a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-default[href]:hover{background-color:#5e5e5e}.label-primary[href]:hover{background-color:#0062cc}.label-success[href]:hover{background-color:#2ac845}.label-info[href]:hover{background-color:#218ebd}.label-warning[href]:hover{background-color:#cca300}.label-danger[href]:hover{background-color:#fc0d00}a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}a.thumbnail:hover{border-color:#007aff}a.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active:hover{z-index:2;color:#fff;background-color:#007aff;border-color:#007aff}.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>.small{color:inherit}.list-group-item.active:hover .list-group-item-text{color:#cce4ff}a.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}a.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}a.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}a.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.btn.active:hover{-webkit-box-shadow:none;box-shadow:none}.btn-group>.btn-default.active:hover{color:#fff;background-color:#007aff;border-color:#006ee6}.navbar-app .btn-navbar:hover,.navbar-app .btn:hover{color:#007aff} -------------------------------------------------------------------------------- /src/js/components/scrollable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module mobile-angular-ui.components.scrollable 3 | * @description 4 | * 5 | * One thing you'll always have to deal with approaching mobile web app development is scroll and `position:fixed` bugs. 6 | * 7 | * Due to the lack of support in some devices fixed positioned elements may bounce or disappear during scroll. Also mobile interaction often leverages horizontal scroll eg. in carousels or sliders. 8 | * 9 | * We use `overflow:auto` to create scrollable areas and solve any problems related to scroll. 10 | * 11 | * Since `overflow:auto` is not always available in touch devices we use [Overthrow](http://filamentgroup.github.io/Overthrow/) to polyfill that. 12 | * 13 | * Markup for any scrollable areas is as simple as: 14 | * 15 | * ``` html 16 | *
17 | *
...
18 | *
19 | * ``` 20 | * 21 | * This piece of code will trigger a directive that properly setup a new `Overthrow` instance for the `.scrollable` node. 22 | * 23 | * #### Headers and footers 24 | * 25 | * `.scrollable-header/.scrollable-footer` can be used to add fixed header/footer to a scrollable area without having to deal with css height and positioning to avoid breaking scroll. 26 | * 27 | * ``` html 28 | *
29 | *
30 | *
31 | * 32 | *
33 | * ``` 34 | * 35 | * #### scrollTo 36 | * 37 | * `.scrollable-content` controller exposes a `scrollTo` function: `scrollTo(offsetOrElement, margin)` 38 | * 39 | * You have to require it in your directives to use it or obtain through `element().controller`: 40 | * 41 | * ``` js 42 | * var elem = element(document.getElementById('myScrollableContent')); 43 | * var scrollableContentController = elem.controller('scrollableContent'); 44 | * 45 | * // - Scroll to top of containedElement 46 | * scrollableContentController.scrollTo(containedElement); 47 | * 48 | * // - Scroll to top of containedElement with a margin of 10px; 49 | * scrollableContentController.scrollTo(containedElement, 10); 50 | * 51 | * // - Scroll top by 200px; 52 | * scrollableContentController.scrollTo(200); 53 | * ``` 54 | * 55 | * #### `ui-scroll-bottom/ui-scroll-top` 56 | * 57 | * You can use `ui-scroll-bottom/ui-scroll-top` directives handle that events and implement features like _infinite scroll_. 58 | * 59 | * ``` html 60 | *
61 | *
62 | *
    63 | *
  • 64 | * {{item.name}} 65 | *
  • 66 | *
67 | *
68 | *
69 | * ``` 70 | */ 71 | (function() { 72 | 'use strict'; 73 | var module = angular.module('mobile-angular-ui.components.scrollable', 74 | ['mobile-angular-ui.core.touchmoveDefaults']); 75 | 76 | 77 | var getTouchY = function(event) { 78 | var touches = event.touches && event.touches.length ? event.touches : [event]; 79 | var e = (event.changedTouches && event.changedTouches[0]) || 80 | (event.originalEvent && event.originalEvent.changedTouches && 81 | event.originalEvent.changedTouches[0]) || 82 | touches[0].originalEvent || touches[0]; 83 | 84 | return e.clientY; 85 | }; 86 | 87 | module.directive('scrollableContent', function() { 88 | return { 89 | restrict: 'C', 90 | controller: ['$element', 'allowTouchmoveDefault', function($element, allowTouchmoveDefault) { 91 | var scrollableContent = $element[0], 92 | scrollable = $element.parent()[0]; 93 | 94 | // Handle nobounce behaviour 95 | if ('ontouchmove' in document) { 96 | var allowUp, allowDown, prevTop, prevBot, lastY; 97 | var setupTouchstart = function(event) { 98 | allowUp = (scrollableContent.scrollTop > 0); 99 | 100 | allowDown = (scrollableContent.scrollTop < scrollableContent.scrollHeight - scrollableContent.clientHeight); 101 | prevTop = null; 102 | prevBot = null; 103 | lastY = getTouchY(event); 104 | }; 105 | 106 | $element.on('touchstart', setupTouchstart); 107 | $element.on('$destroy', function() { 108 | $element.off('touchstart'); 109 | }); 110 | 111 | allowTouchmoveDefault($element, function(event) { 112 | var currY = getTouchY(event); 113 | var up = (currY > lastY), down = !up; 114 | lastY = currY; 115 | return (up && allowUp) || (down && allowDown); 116 | }); 117 | } 118 | 119 | this.scrollableContent = scrollableContent; 120 | 121 | this.scrollTo = function(elementOrNumber, marginTop) { 122 | marginTop = marginTop || 0; 123 | 124 | if (angular.isNumber(elementOrNumber)) { 125 | scrollableContent.scrollTop = elementOrNumber - marginTop; 126 | } else { 127 | var target = angular.element(elementOrNumber)[0]; 128 | if ((! target.offsetParent) || target.offsetParent === scrollable) { 129 | scrollableContent.scrollTop = target.offsetTop - marginTop; 130 | } else { 131 | // recursively subtract offsetTop from marginTop until it reaches scrollable element. 132 | this.scrollTo(target.offsetParent, marginTop - target.offsetTop); 133 | } 134 | } 135 | }; 136 | }], 137 | link: function(scope, element) { 138 | if (overthrow.support !== 'native') { 139 | element.addClass('overthrow'); 140 | overthrow.forget(); 141 | overthrow.set(); 142 | } 143 | } 144 | }; 145 | }); 146 | 147 | angular.forEach(['input', 'textarea'], function(directiveName) { 148 | module.directive(directiveName, ['$rootScope','$timeout', function($rootScope, $timeout) { 149 | return { 150 | require: '?^^scrollableContent', 151 | link: function(scope, elem, attrs, scrollable) { 152 | // Workaround to avoid soft keyboard hiding inputs 153 | elem.on('focus', function(){ 154 | if (scrollable && scrollable.scrollableContent) { 155 | var h1 = scrollable.scrollableContent.offsetHeight; 156 | $timeout(function() { 157 | var h2 = scrollable.scrollableContent.offsetHeight; 158 | // 159 | // if scrollableContent height is reduced in half second 160 | // since an input got focus we assume soft keyboard is showing. 161 | // 162 | if (h1 > h2) { 163 | scrollable.scrollTo(elem, 10); 164 | } 165 | }, 500); 166 | } 167 | }); 168 | } 169 | }; 170 | }]); 171 | }); 172 | 173 | /** 174 | * @directive uiScrollTop 175 | * @restrict A 176 | * 177 | * @param {expression} uiScrollTop The expression to be evaluated when scroll 178 | * reaches top of element. 179 | */ 180 | 181 | /** 182 | * @directive uiScrollBottom 183 | * @restrict A 184 | * 185 | * @param {expression} uiScrollBottom The expression to be evaluated when scroll 186 | * reaches bottom of element. 187 | */ 188 | angular.forEach( 189 | { 190 | uiScrollTop: function(elem){ 191 | return elem.scrollTop === 0; 192 | }, 193 | uiScrollBottom: function(elem){ 194 | return elem.scrollHeight === elem.scrollTop + elem.clientHeight; 195 | } 196 | }, 197 | function(reached, directiveName){ 198 | module.directive(directiveName, [function() { 199 | return { 200 | restrict: 'A', 201 | link: function(scope, elem, attrs) { 202 | elem.on('scroll', function(){ 203 | /* If reached bottom */ 204 | if ( reached(elem[0]) ) { 205 | /* Do what is specified by onScrollBottom */ 206 | scope.$apply(function(){ 207 | scope.$eval(attrs[directiveName]); 208 | }); 209 | } 210 | }); 211 | } 212 | }; 213 | }]); 214 | }); 215 | 216 | /** 217 | * @directive uiScrollableHeader 218 | * @restrict C 219 | */ 220 | 221 | /** 222 | * @directive uiScrollableFooter 223 | * @restrict C 224 | */ 225 | angular.forEach({Top: 'scrollableHeader', Bottom: 'scrollableFooter'}, 226 | function(directiveName, side) { 227 | module.directive(directiveName, [ 228 | '$window', 229 | function($window) { 230 | return { 231 | restrict: 'C', 232 | link: function(scope, element) { 233 | var el = element[0], 234 | parentStyle = element.parent()[0].style; 235 | 236 | var adjustParentPadding = function() { 237 | var styles = $window.getComputedStyle(el), 238 | margin = parseInt(styles.marginTop, 10) + parseInt(styles.marginBottom, 10); 239 | parentStyle['padding' + side] = el.offsetHeight + margin + 'px'; 240 | }; 241 | 242 | var interval = setInterval(adjustParentPadding, 30); 243 | 244 | element.on('$destroy', function(){ 245 | parentStyle['padding' + side] = null; 246 | clearInterval(interval); 247 | interval = adjustParentPadding = element = null; 248 | }); 249 | } 250 | }; 251 | } 252 | ]); 253 | }); 254 | }()); --------------------------------------------------------------------------------