├── .gitattributes ├── .yo-rc.json ├── .bowerrc ├── app ├── robots.txt ├── favicon.ico ├── images │ ├── agektmr.jpg │ ├── agektmr-320.jpg │ ├── agektmr-900.jpg │ └── responsive │ │ ├── abstract1.jpg │ │ ├── abstract2.jpg │ │ ├── abstract3.jpg │ │ ├── abstract1-320.jpg │ │ ├── abstract1-900.jpg │ │ ├── abstract2-320.jpg │ │ ├── abstract2-900.jpg │ │ ├── abstract3-320.jpg │ │ ├── abstract3-900.jpg │ │ ├── illustration-home-inverted.png │ │ ├── illustration-home-inverted-320.png │ │ └── illustration-home-inverted-900.png ├── responsive-manifest.json ├── scripts │ ├── main.js │ └── worker.js ├── styles │ └── main.scss └── index.html ├── test ├── .bowerrc ├── bower.json ├── spec │ └── test.js └── index.html ├── .gitignore ├── bower.json ├── .jshintrc ├── .editorconfig ├── package.json ├── README.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-mocha": {} 3 | } -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /test/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/images/agektmr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/agektmr.jpg -------------------------------------------------------------------------------- /app/images/agektmr-320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/agektmr-320.jpg -------------------------------------------------------------------------------- /app/images/agektmr-900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/agektmr-900.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract1.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract2.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract3.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract1-320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract1-320.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract1-900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract1-900.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract2-320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract2-320.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract2-900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract2-900.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract3-320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract3-320.jpg -------------------------------------------------------------------------------- /app/images/responsive/abstract3-900.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/abstract3-900.jpg -------------------------------------------------------------------------------- /app/images/responsive/illustration-home-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/illustration-home-inverted.png -------------------------------------------------------------------------------- /app/images/responsive/illustration-home-inverted-320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/illustration-home-inverted-320.png -------------------------------------------------------------------------------- /app/images/responsive/illustration-home-inverted-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agektmr/Responsive-Resource-Loader/HEAD/app/images/responsive/illustration-home-inverted-900.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resposive-resource-loader", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap-sass-official": "~3.2.0", 6 | "modernizr": "~2.8.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "dependencies": { 5 | "chai": "~1.8.0", 6 | "mocha": "~1.14.0" 7 | }, 8 | "devDependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /test/spec/test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | describe('Give it some context', function () { 7 | describe('maybe a bit more context here', function () { 8 | it('should run here few assertions', function () { 9 | 10 | }); 11 | }); 12 | }); 13 | })(); 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "jquery": true 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/responsive-manifest.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "media-query": { 4 | "max-width": 320, 5 | "min-device-pixel-ratio": 1 6 | }, 7 | "extensions": [ 8 | "png", "jpg" 9 | ], 10 | "suffix": "-320" 11 | }, { 12 | "media-query": { 13 | "max-width": 640, 14 | "min-device-pixel-ratio": 1 15 | }, 16 | "extensions": [ 17 | "png", "jpg" 18 | ], 19 | "suffix": "" 20 | }, { 21 | "media-query": { 22 | "max-width": 900, 23 | "min-device-pixel-ratio": 1 24 | }, 25 | "extensions": [ 26 | "png", "jpg" 27 | ], 28 | "suffix": "-900" 29 | }, { 30 | "media-query": { 31 | "min-width": 900, 32 | "min-device-pixel-ratio": 2 33 | }, 34 | "extensions": [ 35 | "png", "jpg" 36 | ], 37 | "suffix": "-900" 38 | } 39 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsive-resource-loader", 3 | "devDependencies": { 4 | "apache-server-configs": "^2.7.1", 5 | "grunt": "^0.4.5", 6 | "grunt-autoprefixer": "^1.0.0", 7 | "grunt-concurrent": "^0.5.0", 8 | "grunt-contrib-clean": "^0.6.0", 9 | "grunt-contrib-concat": "^0.5.0", 10 | "grunt-contrib-connect": "^0.8.0", 11 | "grunt-contrib-copy": "^0.5.0", 12 | "grunt-contrib-cssmin": "^0.10.0", 13 | "grunt-contrib-htmlmin": "^0.3.0", 14 | "grunt-contrib-imagemin": "^0.8.0", 15 | "grunt-contrib-jshint": "^0.10.0", 16 | "grunt-contrib-sass": "^0.7.3", 17 | "grunt-contrib-uglify": "^0.5.1", 18 | "grunt-contrib-watch": "^0.6.1", 19 | "grunt-mocha": "^0.4.10", 20 | "grunt-modernizr": "^0.5.2", 21 | "grunt-newer": "^0.7.0", 22 | "grunt-rev": "^0.1.0", 23 | "grunt-svgmin": "^0.4.0", 24 | "grunt-usemin": "^2.3.0", 25 | "grunt-wiredep": "^1.7.0", 26 | "jshint-stylish": "^0.4.0", 27 | "load-grunt-tasks": "^0.4.0", 28 | "time-grunt": "^0.4.0" 29 | }, 30 | "engines": { 31 | "node": ">=0.10.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/scripts/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var sw = null, 3 | viewInfo = null; 4 | var unregister = function() { 5 | sw.unregsiter(); 6 | }; 7 | 8 | var postViewInfo = function() { 9 | viewInfo = { 10 | 'width': window.innerWidth, 11 | 'height': window.innerHeight, 12 | 'device-pixel-ratio': window.devicePixelRatio 13 | }; 14 | console.log('postViewInfo', viewInfo); 15 | navigator.serviceWorker.controller.postMessage({ 16 | command: 'viewInfo', 17 | viewInfo: viewInfo 18 | }); 19 | } 20 | 21 | window.addEventListener('resize', postViewInfo); 22 | window.addEventListener('message', function(e) { 23 | switch (e.data.command) { 24 | case 'viewInfo': 25 | postViewInfo(); 26 | break; 27 | 28 | default: 29 | break; 30 | } 31 | }); 32 | 33 | navigator.serviceWorker.register('scripts/worker.js', { 34 | scope: location.pathname 35 | }).then(function(_reg) { 36 | sw = _reg; 37 | postViewInfo(); 38 | console.log(sw); 39 | }).catch(function(err) { 40 | console.error(err); 41 | }); 42 | 43 | })(); 44 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "../bower_components/bootstrap-sass-official/assets/fonts/bootstrap/"; 2 | // bower:scss 3 | @import "bootstrap-sass-official/assets/stylesheets/_bootstrap.scss"; 4 | // endbower 5 | 6 | .browsehappy { 7 | margin: 0.2em 0; 8 | background: #ccc; 9 | color: #000; 10 | padding: 0.2em 0; 11 | } 12 | 13 | /* Space out content a bit */ 14 | body { 15 | padding-top: 20px; 16 | padding-bottom: 20px; 17 | } 18 | 19 | /* Everything but the jumbotron gets side spacing for mobile first views */ 20 | .header, 21 | .marketing, 22 | .footer { 23 | padding-left: 15px; 24 | padding-right: 15px; 25 | } 26 | 27 | /* Custom page header */ 28 | .header { 29 | border-bottom: 1px solid #e5e5e5; 30 | 31 | /* Make the masthead heading the same height as the navigation */ 32 | h3 { 33 | margin-top: 0; 34 | margin-bottom: 0; 35 | line-height: 40px; 36 | padding-bottom: 19px; 37 | } 38 | } 39 | 40 | /* Custom page footer */ 41 | .footer { 42 | padding-top: 19px; 43 | color: #777; 44 | border-top: 1px solid #e5e5e5; 45 | } 46 | 47 | .container-narrow > hr { 48 | margin: 30px 0; 49 | } 50 | 51 | /* Main marketing message and sign up button */ 52 | .jumbotron { 53 | text-align: center; 54 | border-bottom: 1px solid #e5e5e5; 55 | .btn { 56 | font-size: 21px; 57 | padding: 14px 24px; 58 | } 59 | } 60 | 61 | /* Supporting marketing content */ 62 | .marketing { 63 | margin: 40px 0; 64 | p + h4 { 65 | margin-top: 28px; 66 | } 67 | } 68 | 69 | /* Responsive: Portrait tablets and up */ 70 | @media screen and (min-width: 768px) { 71 | .container { 72 | max-width: 730px; 73 | } 74 | 75 | /* Remove the padding we set earlier */ 76 | .header, 77 | .marketing, 78 | .footer { 79 | padding-left: 0; 80 | padding-right: 0; 81 | } 82 | 83 | /* Space out the masthead */ 84 | .header { 85 | margin-bottom: 30px; 86 | } 87 | 88 | /* Remove the bottom border on the jumbotron for visual effect */ 89 | .jumbotron { 90 | border-bottom: 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Responsive Resource Loader 2 | This is a proof of concept implementation of RRL which gives you responsive images for free (almost) using ServiceWorker interception. No more `img[srcset]` and `` element! 3 | 4 | * Using huge images is redundant for mobile devices. 5 | * Assigning `srcset` or `picture` for every single `img` tag is a P.I.T.A. 6 | * Loading images with relevant size for the device is a pain. 7 | 8 | ## Example 9 | Write your `img` tag as you used to. 10 | 11 | 12 | 13 | You might expect to make it responsive using `srcset`. For example: 14 | 15 | 16 | 17 | But you don't want to write this for every single images. You can define a formula such as: 18 | 19 | - Append `-900` for all images with `640w 2x` 20 | ex) `agektmr-900.jpg`, `abstract1-900.jpg`, `abstract2-900.jpg` 21 | - Append `-640` for all images with `640w` 22 | ex) `agektmr-640.jpg`, `abstract1-640.jpg`, `abstract2-640.jpg` 23 | 24 | This will become a manifest file that looks like: 25 | 26 | [ 27 | { 28 | "media-query": { 29 | "max-width": 320, 30 | "min-device-pixel-ratio": 1 31 | }, 32 | "extensions": [ 33 | "png", "jpg" 34 | ], 35 | "suffix": "-320" 36 | }, { 37 | "media-query": { 38 | "max-width": 640, 39 | "min-device-pixel-ratio": 1 40 | }, 41 | "extensions": [ 42 | "png", "jpg" 43 | ], 44 | "suffix": "-640" 45 | }, { 46 | "media-query": { 47 | "max-width": 900, 48 | "min-device-pixel-ratio": 1 49 | }, 50 | "extensions": [ 51 | "png", "jpg" 52 | ], 53 | "suffix": "-900" 54 | }, { 55 | "media-query": { 56 | "min-width": 900, 57 | "min-device-pixel-ratio": 2 58 | }, 59 | "extensions": [ 60 | "png", "jpg" 61 | ], 62 | "suffix": "-900" 63 | } 64 | ] 65 | 66 | # How to try this 67 | 1. clone this repository 68 | ``` 69 | git clone git@github.com:agektmr/Responsive-Resource-Loader.git 70 | ``` 71 | 2. install dependencies 72 | ``` 73 | cd Responsive-Resource-Loader 74 | npm install 75 | bower install 76 | ``` 77 | 3. run server 78 | ``` 79 | grunt server 80 | ``` 81 | 4. access on a browser at [http://localhost:9000/](http://localhost:9000/) 82 | 83 | The demo is not publicly available as ServiceWorker requires SSL connection. 84 | 85 | # Inspect in DevTools 86 | 1. access chrome://serviceworker-internals 87 | 2. click "Inspect" of registered ServiceWorker 88 | 3. using the device mode should help you try this easier. click on phone 89 | icon next to magnifier icon at top left of devtools. learn more: [DevBytes: Chrome DevTools Device Mode](https://www.youtube.com/watch?v=FrAZWiMWRa4) 90 | 91 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PortableCache 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 |
20 |
21 |

Responsive Resource Loader

22 |

No more hassle with <picture> and img[srcset].

23 |
    24 |
  • Using huge images is redundant for mobile devices.
  • 25 |
  • Assigning srcset or picture for every single img tag is a P.I.T.A.
  • 26 |
  • Loading images with relevant size for the device is a pain.
  • 27 |
28 |

Get responsive images for free by just writing single manifest file that defines naming formula.

29 | View on Github 30 |
31 | 32 |

Examples

33 | 34 |

Yeoman.

35 | 36 |

Photo by Jim Nix / Nomadic

37 | 38 |

Photo by zen

39 | 40 |

Photo by cobalt123

41 |

Write a manifest

42 |

Example Formula Manifest

43 |
[
44 |   {
45 |     "media-query": {
46 |       "max-width": 320,
47 |       "min-device-pixel-ratio": 1
48 |     },
49 |     "extensions": [
50 |       "png", "jpg"
51 |     ],
52 |     "suffix": "-320"
53 |   }, {
54 |     "media-query": {
55 |       "max-width": 640,
56 |       "min-device-pixel-ratio": 1
57 |     },
58 |     "extensions": [
59 |       "png", "jpg"
60 |     ],
61 |     "suffix": "-640"
62 |   }, {
63 |     "media-query": {
64 |       "max-width": 900,
65 |       "min-device-pixel-ratio": 1
66 |     },
67 |     "extensions": [
68 |       "png", "jpg"
69 |     ],
70 |     "suffix": "-900"
71 |   }, {
72 |     "media-query": {
73 |       "min-width": 900,
74 |       "min-device-pixel-ratio": 2
75 |     },
76 |     "extensions": [
77 |       "png", "jpg"
78 |     ],
79 |     "suffix": "-900"
80 |   }
81 | ]
82 |
83 | 94 |
95 | 98 | 99 | -------------------------------------------------------------------------------- /app/scripts/worker.js: -------------------------------------------------------------------------------- 1 | var ResponsiveResourceLoader = (function() { 2 | var DEFAULT_MANIFEST = [ 3 | { 4 | "media-query": { 5 | "max-width": 320, 6 | "min-device-pixel-ratio": 1 7 | }, 8 | "extensions": [ 9 | "png", "jpg" 10 | ], 11 | "suffix": "-320" 12 | }, { 13 | "media-query": { 14 | "max-width": 640, 15 | "min-device-pixel-ratio": 1 16 | }, 17 | "extensions": [ 18 | "png", "jpg" 19 | ], 20 | "suffix": "" 21 | }, { 22 | "media-query": { 23 | "max-width": 900, 24 | "min-device-pixel-ratio": 1 25 | }, 26 | "extensions": [ 27 | "png", "jpg" 28 | ], 29 | "suffix": "-900" 30 | }, { 31 | "media-query": { 32 | "min-width": 900, 33 | "min-device-pixel-ratio": 2 34 | }, 35 | "extensions": [ 36 | "png", "jpg" 37 | ], 38 | "suffix": "-900" 39 | } 40 | ]; 41 | 42 | var rrl = function() { 43 | var that = this; 44 | this.manifest = null; 45 | this.viewInfo = null; 46 | this.suffix = ''; 47 | 48 | self.addEventListener('install', function(event) { 49 | console.log('install'); 50 | fetch('/responsive-manifest.json').then(function(response) { 51 | return response.text(); 52 | }, function(err) { 53 | console.error('Manifest couldn\'t be loaded:', err.message); 54 | that.manifest = DEFAULT_MANIFEST; 55 | }).then(function(txt) { 56 | console.log('response: ', txt); 57 | that.manifest = JSON.parse(txt); 58 | }).catch(function(err) { 59 | console.error('Invalid manifest:', err.message); 60 | console.info('Using default manifest instead.'); 61 | that.manifest = DEFAULT_MANIFEST; 62 | }); 63 | }); 64 | 65 | self.addEventListener('activate', function(event) { 66 | console.log('activation'); 67 | }); 68 | 69 | var viewInfoLoadedPromise = new Promise(function(resolve, reject) { 70 | self.addEventListener('message', function(event) { 71 | switch (event.data.command) { 72 | case 'viewInfo': 73 | that.viewInfo = event.data.viewInfo; 74 | console.log('viewInfo: ', that.viewInfo); 75 | resolve(); 76 | break; 77 | 78 | default: 79 | break; 80 | } 81 | }); 82 | }); 83 | 84 | self.addEventListener('fetch', function(event) { 85 | viewInfoLoadedPromise.then(function() { 86 | event.respondWith( 87 | that.convert(event.request.url) 88 | ); 89 | }); 90 | }); 91 | }; 92 | rrl.prototype.convert = function(url) { 93 | // TODO: change architecture of manifest and apply suffix depending on extension 94 | var suffix = this.getSuffix(url) 95 | // suffix is `null` if no match found 96 | if (suffix) { 97 | var url = url.replace(/\.(.{3,5}?)$/, suffix+'.$1') 98 | console.log('fetching: ', url); 99 | } 100 | return fetch(url); 101 | }; 102 | rrl.prototype.getSuffix = function(url) { 103 | if (!this.manifest) { 104 | console.info('manifest not loaded yet.'); 105 | return ''; 106 | } 107 | console.log('window width: %s, height: %s, dpr: %s', 108 | this.viewInfo.width, this.viewInfo.height, this.viewInfo['device-pixel-ratio']); 109 | var last_match = null; 110 | for (var i = 0; i < this.manifest.length; i++) { 111 | var v = this.manifest[i]; 112 | if (v.extensions !== '') { 113 | var regex = new RegExp('\.('+v.extensions.join('|')+')$'); 114 | 115 | // if extension doesn't match, continue 116 | if (!regex.test(url)) continue; 117 | } 118 | var mq = v['media-query']; 119 | if (mq['min-width']) { 120 | if (this.viewInfo.width < mq['min-width']) continue; 121 | last_match = v; 122 | console.log('min-width: %s matched', mq['min-width']); 123 | } 124 | if (mq['max-width']) { 125 | if (this.viewInfo.width > mq['max-width']) continue; 126 | last_match = v; 127 | console.log('max-width: %s matched', mq['max-width']); 128 | } 129 | if (mq['min-height']) { 130 | if (this.viewInfo.height < mq['min-height']) continue; 131 | last_match = v; 132 | console.log('min-height: %s matched', mq['min-height']); 133 | } 134 | if (mq['max-height']) { 135 | if (this.viewInfo.height > mq['min-height']) continue; 136 | last_match = v; 137 | console.log('max-height: %s matched', mq['max-height']); 138 | } 139 | if (mq['min-device-pixel-ratio']) { 140 | if (this.viewInfo['device-pixel-ratio'] < mq['min-device-pixel-ratio']) continue; 141 | last_match = v; 142 | console.log('device-pixel-ratio: %s matched', mq['min-device-pixel-ratio']); 143 | } 144 | 145 | console.log('match found', v); 146 | return last_match['suffix'] || ''; 147 | } 148 | return last_match && last_match['suffix'] || ''; 149 | }; 150 | return new rrl(); 151 | })(); 152 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-10-21 using 2 | // generator-webapp 0.5.1 3 | 'use strict'; 4 | 5 | // # Globbing 6 | // for performance reasons we're only matching one level down: 7 | // 'test/spec/{,*/}*.js' 8 | // If you want to recursively match all subfolders, use: 9 | // 'test/spec/**/*.js' 10 | 11 | module.exports = function (grunt) { 12 | 13 | // Time how long tasks take. Can help when optimizing build times 14 | require('time-grunt')(grunt); 15 | 16 | // Load grunt tasks automatically 17 | require('load-grunt-tasks')(grunt); 18 | 19 | // Configurable paths 20 | var config = { 21 | app: 'app', 22 | dist: 'dist' 23 | }; 24 | 25 | // Define the configuration for all the tasks 26 | grunt.initConfig({ 27 | 28 | // Project settings 29 | config: config, 30 | 31 | // Watches files for changes and runs tasks based on the changed files 32 | watch: { 33 | bower: { 34 | files: ['bower.json'], 35 | tasks: ['wiredep'] 36 | }, 37 | js: { 38 | files: ['<%= config.app %>/scripts/{,*/}*.js'], 39 | tasks: ['jshint'], 40 | options: { 41 | livereload: true 42 | } 43 | }, 44 | jstest: { 45 | files: ['test/spec/{,*/}*.js'], 46 | tasks: ['test:watch'] 47 | }, 48 | gruntfile: { 49 | files: ['Gruntfile.js'] 50 | }, 51 | sass: { 52 | files: ['<%= config.app %>/styles/{,*/}*.{scss,sass}'], 53 | tasks: ['sass:server', 'autoprefixer'] 54 | }, 55 | styles: { 56 | files: ['<%= config.app %>/styles/{,*/}*.css'], 57 | tasks: ['newer:copy:styles', 'autoprefixer'] 58 | }, 59 | livereload: { 60 | options: { 61 | livereload: '<%= connect.options.livereload %>' 62 | }, 63 | files: [ 64 | '<%= config.app %>/{,*/}*.html', 65 | '.tmp/styles/{,*/}*.css', 66 | '<%= config.app %>/images/{,*/}*' 67 | ] 68 | } 69 | }, 70 | 71 | // The actual grunt server settings 72 | connect: { 73 | options: { 74 | port: 9000, 75 | open: true, 76 | livereload: 35729, 77 | // Change this to '0.0.0.0' to access the server from outside 78 | hostname: 'localhost' 79 | }, 80 | livereload: { 81 | options: { 82 | middleware: function(connect) { 83 | return [ 84 | connect.static('.tmp'), 85 | connect().use('/bower_components', connect.static('./bower_components')), 86 | connect.static(config.app) 87 | ]; 88 | } 89 | } 90 | }, 91 | test: { 92 | options: { 93 | open: false, 94 | port: 9001, 95 | middleware: function(connect) { 96 | return [ 97 | connect.static('.tmp'), 98 | connect.static('test'), 99 | connect().use('/bower_components', connect.static('./bower_components')), 100 | connect.static(config.app) 101 | ]; 102 | } 103 | } 104 | }, 105 | dist: { 106 | options: { 107 | base: '<%= config.dist %>', 108 | livereload: false 109 | } 110 | } 111 | }, 112 | 113 | // Empties folders to start fresh 114 | clean: { 115 | dist: { 116 | files: [{ 117 | dot: true, 118 | src: [ 119 | '.tmp', 120 | '<%= config.dist %>/*', 121 | '!<%= config.dist %>/.git*' 122 | ] 123 | }] 124 | }, 125 | server: '.tmp' 126 | }, 127 | 128 | // Make sure code styles are up to par and there are no obvious mistakes 129 | jshint: { 130 | options: { 131 | jshintrc: '.jshintrc', 132 | reporter: require('jshint-stylish') 133 | }, 134 | all: [ 135 | 'Gruntfile.js', 136 | '<%= config.app %>/scripts/{,*/}*.js', 137 | '!<%= config.app %>/scripts/vendor/*', 138 | 'test/spec/{,*/}*.js' 139 | ] 140 | }, 141 | 142 | // Mocha testing framework configuration options 143 | mocha: { 144 | all: { 145 | options: { 146 | run: true, 147 | urls: ['http://<%= connect.test.options.hostname %>:<%= connect.test.options.port %>/index.html'] 148 | } 149 | } 150 | }, 151 | 152 | // Compiles Sass to CSS and generates necessary files if requested 153 | sass: { 154 | options: { 155 | loadPath: 'bower_components' 156 | }, 157 | dist: { 158 | files: [{ 159 | expand: true, 160 | cwd: '<%= config.app %>/styles', 161 | src: ['*.{scss,sass}'], 162 | dest: '.tmp/styles', 163 | ext: '.css' 164 | }] 165 | }, 166 | server: { 167 | files: [{ 168 | expand: true, 169 | cwd: '<%= config.app %>/styles', 170 | src: ['*.{scss,sass}'], 171 | dest: '.tmp/styles', 172 | ext: '.css' 173 | }] 174 | } 175 | }, 176 | 177 | // Add vendor prefixed styles 178 | autoprefixer: { 179 | options: { 180 | browsers: ['> 1%', 'last 2 versions', 'Firefox ESR', 'Opera 12.1'] 181 | }, 182 | dist: { 183 | files: [{ 184 | expand: true, 185 | cwd: '.tmp/styles/', 186 | src: '{,*/}*.css', 187 | dest: '.tmp/styles/' 188 | }] 189 | } 190 | }, 191 | 192 | // Automatically inject Bower components into the HTML file 193 | wiredep: { 194 | app: { 195 | ignorePath: /^\/|\.\.\//, 196 | src: ['<%= config.app %>/index.html'], 197 | exclude: ['bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js'] 198 | }, 199 | sass: { 200 | src: ['<%= config.app %>/styles/{,*/}*.{scss,sass}'], 201 | ignorePath: /(\.\.\/){1,2}bower_components\// 202 | } 203 | }, 204 | 205 | // Renames files for browser caching purposes 206 | rev: { 207 | dist: { 208 | files: { 209 | src: [ 210 | '<%= config.dist %>/scripts/{,*/}*.js', 211 | '<%= config.dist %>/styles/{,*/}*.css', 212 | '<%= config.dist %>/images/{,*/}*.*', 213 | '<%= config.dist %>/styles/fonts/{,*/}*.*', 214 | '<%= config.dist %>/*.{ico,png}' 215 | ] 216 | } 217 | } 218 | }, 219 | 220 | // Reads HTML for usemin blocks to enable smart builds that automatically 221 | // concat, minify and revision files. Creates configurations in memory so 222 | // additional tasks can operate on them 223 | useminPrepare: { 224 | options: { 225 | dest: '<%= config.dist %>' 226 | }, 227 | html: '<%= config.app %>/index.html' 228 | }, 229 | 230 | // Performs rewrites based on rev and the useminPrepare configuration 231 | usemin: { 232 | options: { 233 | assetsDirs: [ 234 | '<%= config.dist %>', 235 | '<%= config.dist %>/images', 236 | '<%= config.dist %>/styles' 237 | ] 238 | }, 239 | html: ['<%= config.dist %>/{,*/}*.html'], 240 | css: ['<%= config.dist %>/styles/{,*/}*.css'] 241 | }, 242 | 243 | // The following *-min tasks produce minified files in the dist folder 244 | imagemin: { 245 | dist: { 246 | files: [{ 247 | expand: true, 248 | cwd: '<%= config.app %>/images', 249 | src: '{,*/}*.{gif,jpeg,jpg,png}', 250 | dest: '<%= config.dist %>/images' 251 | }] 252 | } 253 | }, 254 | 255 | svgmin: { 256 | dist: { 257 | files: [{ 258 | expand: true, 259 | cwd: '<%= config.app %>/images', 260 | src: '{,*/}*.svg', 261 | dest: '<%= config.dist %>/images' 262 | }] 263 | } 264 | }, 265 | 266 | htmlmin: { 267 | dist: { 268 | options: { 269 | collapseBooleanAttributes: true, 270 | collapseWhitespace: true, 271 | conservativeCollapse: true, 272 | removeAttributeQuotes: true, 273 | removeCommentsFromCDATA: true, 274 | removeEmptyAttributes: true, 275 | removeOptionalTags: true, 276 | removeRedundantAttributes: true, 277 | useShortDoctype: true 278 | }, 279 | files: [{ 280 | expand: true, 281 | cwd: '<%= config.dist %>', 282 | src: '{,*/}*.html', 283 | dest: '<%= config.dist %>' 284 | }] 285 | } 286 | }, 287 | 288 | // By default, your `index.html`'s will take care 289 | // of minification. These next options are pre-configured if you do not 290 | // wish to use the Usemin blocks. 291 | // cssmin: { 292 | // dist: { 293 | // files: { 294 | // '<%= config.dist %>/styles/main.css': [ 295 | // '.tmp/styles/{,*/}*.css', 296 | // '<%= config.app %>/styles/{,*/}*.css' 297 | // ] 298 | // } 299 | // } 300 | // }, 301 | // uglify: { 302 | // dist: { 303 | // files: { 304 | // '<%= config.dist %>/scripts/scripts.js': [ 305 | // '<%= config.dist %>/scripts/scripts.js' 306 | // ] 307 | // } 308 | // } 309 | // }, 310 | // concat: { 311 | // dist: {} 312 | // }, 313 | 314 | // Copies remaining files to places other tasks can use 315 | copy: { 316 | dist: { 317 | files: [{ 318 | expand: true, 319 | dot: true, 320 | cwd: '<%= config.app %>', 321 | dest: '<%= config.dist %>', 322 | src: [ 323 | '*.{ico,png,txt}', 324 | 'images/{,*/}*.webp', 325 | '{,*/}*.html', 326 | 'styles/fonts/{,*/}*.*' 327 | ] 328 | }, { 329 | src: 'node_modules/apache-server-configs/dist/.htaccess', 330 | dest: '<%= config.dist %>/.htaccess' 331 | }, { 332 | expand: true, 333 | dot: true, 334 | cwd: '.', 335 | src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', 336 | dest: '<%= config.dist %>' 337 | }] 338 | }, 339 | styles: { 340 | expand: true, 341 | dot: true, 342 | cwd: '<%= config.app %>/styles', 343 | dest: '.tmp/styles/', 344 | src: '{,*/}*.css' 345 | } 346 | }, 347 | 348 | // Generates a custom Modernizr build that includes only the tests you 349 | // reference in your app 350 | modernizr: { 351 | dist: { 352 | devFile: 'bower_components/modernizr/modernizr.js', 353 | outputFile: '<%= config.dist %>/scripts/vendor/modernizr.js', 354 | files: { 355 | src: [ 356 | '<%= config.dist %>/scripts/{,*/}*.js', 357 | '<%= config.dist %>/styles/{,*/}*.css', 358 | '!<%= config.dist %>/scripts/vendor/*' 359 | ] 360 | }, 361 | uglify: true 362 | } 363 | }, 364 | 365 | // Run some tasks in parallel to speed up build process 366 | concurrent: { 367 | server: [ 368 | 'sass:server', 369 | 'copy:styles' 370 | ], 371 | test: [ 372 | 'copy:styles' 373 | ], 374 | dist: [ 375 | 'sass', 376 | 'copy:styles', 377 | 'imagemin', 378 | 'svgmin' 379 | ] 380 | } 381 | }); 382 | 383 | 384 | grunt.registerTask('serve', 'start the server and preview your app, --allow-remote for remote access', function (target) { 385 | if (grunt.option('allow-remote')) { 386 | grunt.config.set('connect.options.hostname', '0.0.0.0'); 387 | } 388 | if (target === 'dist') { 389 | return grunt.task.run(['build', 'connect:dist:keepalive']); 390 | } 391 | 392 | grunt.task.run([ 393 | 'clean:server', 394 | 'wiredep', 395 | 'concurrent:server', 396 | 'autoprefixer', 397 | 'connect:livereload', 398 | 'watch' 399 | ]); 400 | }); 401 | 402 | grunt.registerTask('server', function (target) { 403 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 404 | grunt.task.run([target ? ('serve:' + target) : 'serve']); 405 | }); 406 | 407 | grunt.registerTask('test', function (target) { 408 | if (target !== 'watch') { 409 | grunt.task.run([ 410 | 'clean:server', 411 | 'concurrent:test', 412 | 'autoprefixer' 413 | ]); 414 | } 415 | 416 | grunt.task.run([ 417 | 'connect:test', 418 | 'mocha' 419 | ]); 420 | }); 421 | 422 | grunt.registerTask('build', [ 423 | 'clean:dist', 424 | 'wiredep', 425 | 'useminPrepare', 426 | 'concurrent:dist', 427 | 'autoprefixer', 428 | 'concat', 429 | 'cssmin', 430 | 'uglify', 431 | 'copy:dist', 432 | 'modernizr', 433 | 'rev', 434 | 'usemin', 435 | 'htmlmin' 436 | ]); 437 | 438 | grunt.registerTask('default', [ 439 | 'newer:jshint', 440 | 'test', 441 | 'build' 442 | ]); 443 | }; 444 | --------------------------------------------------------------------------------