├── demo ├── pc.png ├── demo.png ├── kitten.jpg ├── tablet.png ├── smartphone.png ├── demo.css ├── demo.js ├── index.html └── demo.html ├── .gitignore ├── .editorconfig ├── .travis.yml ├── bower.json ├── CHANGELOG.md ├── LICENSE ├── package.json ├── karma.conf.js ├── dyframe.min.js ├── gulpfile.js ├── README.md ├── src └── dyframe.js ├── dyframe.js └── test └── spec └── dyframe-test.js /demo/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htanjo/dyframe/HEAD/demo/pc.png -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htanjo/dyframe/HEAD/demo/demo.png -------------------------------------------------------------------------------- /demo/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htanjo/dyframe/HEAD/demo/kitten.jpg -------------------------------------------------------------------------------- /demo/tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htanjo/dyframe/HEAD/demo/tablet.png -------------------------------------------------------------------------------- /demo/smartphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htanjo/dyframe/HEAD/demo/smartphone.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | bower_components/ 4 | coverage/ 5 | .publish/ 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "stable" 5 | env: 6 | global: 7 | - secure: U4CyMK+05vj8bJNu9SnF4+HPRRAcvmsjq/sdvxJFJdtt15KMyDdcxFZW5VaBn5Ll87TtaMJb/SyI3Z+VcxOdIqwsDJNKETROkvr+/onhQuKtAEXFFqUjseoPrIvijXs1zUQzJ0bZqnEKWjOpCxht1C3liWasPhA+NfdQFwMhkrk= 8 | - secure: PthUvoUKmz6Oh1fyWNwLNQyvPyMOC4bvn9AwaUNQX71OOatxy6ZDpukKt6ujR5aj6Mbm/3IZuoWTgfPGDTbXCUWj93cUG/RrPiNjAw8F41m5krQa/qU9SjBTswNFgZMKk6JTo2L4BA1YMPim8i/ALteFPc1LpdDqJ/3uKm33OLs= 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dyframe", 3 | "version": "0.5.1", 4 | "description": "Dynamically render responsive HTML into iframe.", 5 | "authors": [ 6 | "Hiroyuki Tanjo" 7 | ], 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/htanjo/dyframe.git" 12 | }, 13 | "homepage": "https://github.com/htanjo/dyframe", 14 | "keywords": [ 15 | "javascript", 16 | "html", 17 | "iframe", 18 | "viewport" 19 | ], 20 | "main": "dyframe.js", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "package.json", 26 | "gulpfile.js", 27 | "test", 28 | "src", 29 | "demo" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.1 (2015-09-24) 4 | - Implement [UMD](https://github.com/umdjs/umd) (Universal Module Definition) correctly. 5 | 6 | ## 0.5.0 (2015-04-24) 7 | - Add `interval` option to prevent frequent re-rendering. 8 | 9 | ## 0.4.0 (2015-04-23) 10 | - Add `.destroy()` method to clean up. 11 | - Support `initial-scale` value of ``. 12 | - Improve tests using [Sauce Labs](https://saucelabs.com/u/dyframe) and [Coveralls](https://coveralls.io/r/htanjo/dyframe). 13 | 14 | ## 0.3.0 (2015-04-20) 15 | - Add `deviceWidth` option. 16 | - Support custom profile. 17 | - Add class name to element for styling with css easily. 18 | - Change default `profile` value to `null`. 19 | 20 | ## 0.2.0 (2015-04-16) 21 | - Support AMD. 22 | - Minor bug fix. 23 | 24 | ## 0.1.0 (2015-04-15) 25 | - Initial release. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Hiroyuki Tanjo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dyframe", 3 | "version": "0.5.1", 4 | "description": "Dynamically render responsive HTML into iframe.", 5 | "author": "Hiroyuki Tanjo", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/htanjo/dyframe.git" 10 | }, 11 | "homepage": "https://github.com/htanjo/dyframe", 12 | "bugs": "https://github.com/htanjo/dyframe/issues", 13 | "keywords": [ 14 | "javascript", 15 | "html", 16 | "iframe", 17 | "viewport" 18 | ], 19 | "engines": { 20 | "node": ">=0.10.0" 21 | }, 22 | "scripts": { 23 | "test": "gulp test" 24 | }, 25 | "main": "dyframe.js", 26 | "files": [ 27 | "dyframe.js", 28 | "dyframe.min.js" 29 | ], 30 | "devDependencies": { 31 | "browser-sync": "^2.6.1", 32 | "chai": "^3.3.0", 33 | "del": "^2.0.2", 34 | "eslint-config-xo-space": "^0.14.0", 35 | "gulp": "^3.8.11", 36 | "gulp-bump": "^2.2.0", 37 | "gulp-eslint": "^3.0.1", 38 | "gulp-gh-pages": "^0.5.0", 39 | "gulp-git": "^1.1.1", 40 | "gulp-header": "^1.2.2", 41 | "gulp-if": "^2.0.1", 42 | "gulp-load-plugins": "^1.2.4", 43 | "gulp-rename": "^1.2.2", 44 | "gulp-replace": "^0.5.3", 45 | "gulp-uglify": "^2.0.0", 46 | "karma": "^1.1.1", 47 | "karma-chai": "^0.1.0", 48 | "karma-chrome-launcher": "^2.0.0", 49 | "karma-coverage": "^1.1.0", 50 | "karma-coveralls": "^1.1.2", 51 | "karma-mocha": "^1.1.1", 52 | "karma-mocha-reporter": "^2.0.4", 53 | "karma-phantomjs-launcher": "^1.0.1", 54 | "karma-sauce-launcher": "^1.0.0", 55 | "minimist": "^1.1.1", 56 | "mocha": "^3.0.0", 57 | "opn": "^4.0.2", 58 | "run-sequence": "^1.0.2" 59 | }, 60 | "eslintConfig": { 61 | "extends": "xo-space/browser", 62 | "env": { 63 | "amd": true, 64 | "commonjs": true 65 | }, 66 | "rules": { 67 | "wrap-iife": [ 68 | "error", 69 | "outside" 70 | ] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint-disable quote-props */ 3 | 'use strict'; 4 | 5 | module.exports = function (config) { 6 | config.set({ 7 | basePath: '', 8 | frameworks: ['mocha', 'chai'], 9 | files: [ 10 | 'src/*.js', 11 | 'test/spec/*.js' 12 | ], 13 | exclude: [], 14 | preprocessors: { 15 | 'src/*.js': ['coverage'] 16 | }, 17 | reporters: ['mocha', 'coverage'], 18 | port: 9876, 19 | colors: true, 20 | logLevel: config.LOG_INFO, 21 | autoWatch: true, 22 | browsers: ['Chrome', 'PhantomJS'], 23 | singleRun: true, 24 | client: { 25 | mocha: { 26 | reporter: 'html' 27 | } 28 | }, 29 | coverageReporter: { 30 | type: 'lcov', 31 | dir: 'coverage/' 32 | }, 33 | 34 | // Browser test on Sauce Labs 35 | sauceLabs: { 36 | testName: 'Dyframe Tests' 37 | }, 38 | customLaunchers: { 39 | 'SL_Chrome': { 40 | base: 'SauceLabs', 41 | browserName: 'chrome', 42 | platform: 'Windows 8.1' 43 | }, 44 | 'SL_Firefox': { 45 | base: 'SauceLabs', 46 | browserName: 'firefox', 47 | platform: 'Windows 8.1' 48 | }, 49 | 'SL_Opera': { 50 | base: 'SauceLabs', 51 | browserName: 'opera', 52 | platform: 'Windows 7' 53 | }, 54 | 'SL_IE_11': { 55 | base: 'SauceLabs', 56 | browserName: 'internet explorer', 57 | platform: 'Windows 8.1', 58 | version: '11' 59 | }, 60 | 'SL_IE_10': { 61 | base: 'SauceLabs', 62 | browserName: 'internet explorer', 63 | platform: 'Windows 7', 64 | version: '10' 65 | }, 66 | 'SL_IE_9': { 67 | base: 'SauceLabs', 68 | browserName: 'internet explorer', 69 | platform: 'Windows 7', 70 | version: '9' 71 | }, 72 | 'SL_Safari': { 73 | base: 'SauceLabs', 74 | browserName: 'safari', 75 | platform: 'OS X 10.10' 76 | }, 77 | 'SL_iOS': { 78 | base: 'SauceLabs', 79 | browserName: 'iphone', 80 | version: '8.2' 81 | }, 82 | 'SL_Android': { 83 | base: 'SauceLabs', 84 | browserName: 'android', 85 | version: '4.4' 86 | } 87 | } 88 | }); 89 | 90 | // Override concfig for CI environment 91 | if (process.env.CI) { 92 | if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) { 93 | config.reporters.push('saucelabs'); 94 | config.browsers = Object.keys(config.customLaunchers); 95 | config.captureTimeout = 0; 96 | } else { 97 | config.browsers = ['PhantomJS']; 98 | } 99 | config.reporters.push('coveralls'); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:600,400"); 2 | 3 | body { 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .home { 8 | width: 970px; 9 | } 10 | 11 | .hero { 12 | margin-top: 60px; 13 | margin-bottom: 60px; 14 | text-align: center; 15 | } 16 | 17 | .hero h1 { 18 | font-size: 54px; 19 | } 20 | 21 | .hero p { 22 | font-size: 20px; 23 | } 24 | 25 | .buttons { 26 | margin-top: 40px; 27 | text-align: center; 28 | } 29 | 30 | .btn { 31 | border-radius: 3px; 32 | } 33 | 34 | .btn-lg { 35 | padding-top: 8px; 36 | padding-bottom: 8px; 37 | } 38 | 39 | .buttons .btn { 40 | margin-left: 2px; 41 | margin-right: 2px; 42 | } 43 | 44 | .section { 45 | margin-top: 80px; 46 | margin-bottom: 80px; 47 | text-align: center; 48 | } 49 | 50 | h2, 51 | h3 { 52 | margin-top: 0; 53 | margin-bottom: 20px; 54 | text-align: center; 55 | } 56 | 57 | .example { 58 | margin-bottom: 40px; 59 | } 60 | 61 | .devices { 62 | position: relative; 63 | width: 940px; 64 | height: 545px; 65 | margin-top: 20px; 66 | margin-bottom: 20px; 67 | } 68 | 69 | .device { 70 | position: absolute; 71 | overflow: hidden; 72 | } 73 | 74 | .device-pc { 75 | top: 0; 76 | left: 0; 77 | width: 940px; 78 | height: 533px; 79 | padding: 35px 168px 120px; 80 | background-image: url(pc.png); 81 | background-repeat: no-repeat; 82 | } 83 | .device-tablet { 84 | right: 40px; 85 | bottom: 0; 86 | width: 340px; 87 | height: 482px; 88 | padding: 45px 23px; 89 | background-image: url(tablet.png); 90 | background-repeat: no-repeat; 91 | } 92 | .device-smartphone { 93 | left: 90px; 94 | bottom: 0; 95 | width: 150px; 96 | height: 300px; 97 | padding: 35px 10px 36px; 98 | background-image: url(smartphone.png); 99 | background-repeat: no-repeat; 100 | } 101 | 102 | .dyframe { 103 | height: 360px; 104 | border: 1px solid #ddd; 105 | } 106 | 107 | .form { 108 | max-width: 750px; 109 | margin: 20px auto 40px; 110 | } 111 | 112 | textarea { 113 | overflow-x: auto; 114 | white-space: pre; 115 | word-wrap: normal; 116 | resize: vertical; 117 | } 118 | 119 | .form-control { 120 | border-radius: 0; 121 | } 122 | .form-control:focus { 123 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 2px rgba(102, 175, 233, 0.6); 124 | } 125 | 126 | .separator { 127 | font-size: 30px; 128 | text-align: center; 129 | } 130 | 131 | .set-icon { 132 | display: inline-block; 133 | } 134 | .set-icon .glyphicon { 135 | display: block; 136 | } 137 | .set-icon .glyphicon:not(:first-child) { 138 | margin-top: -0.65em; 139 | } 140 | 141 | .footer { 142 | margin-top: 80px; 143 | margin-bottom: 40px; 144 | text-align: center; 145 | } 146 | 147 | .ribbon { 148 | display: block; 149 | position: fixed; 150 | top: 3.2em; 151 | right: -3.7em; 152 | z-index: 1; 153 | padding: 0.4em 3.5em; 154 | -webkit-transform: rotate(45deg); 155 | -ms-transform: rotate(45deg); 156 | transform: rotate(45deg); 157 | font-size: 13px; 158 | text-align: center; 159 | text-decoration: none !important; 160 | color: #fff !important; 161 | background-color: #789; 162 | -webkit-user-select: none; 163 | -moz-user-select: none; 164 | -ms-user-select: none; 165 | user-select: none; 166 | } 167 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var Dyframe = window.Dyframe; 5 | 6 | // Add custom profile 7 | Dyframe.addProfile('pc', { 8 | width: 1366, 9 | deviceWidth: null 10 | }); 11 | Dyframe.addProfile('custom', { 12 | width: 980, 13 | deviceWidth: 280 14 | }); 15 | 16 | var input = document.getElementById('input'); 17 | var html = input.value; 18 | var dyframes = []; 19 | 20 | // Base 21 | var baseElement = document.getElementById('dyframe-base'); 22 | if (baseElement) { 23 | dyframes.push(new Dyframe(baseElement, { 24 | html: html, 25 | interval: 500 26 | })); 27 | } 28 | 29 | // Width 1200px 30 | var widthElement = document.getElementById('dyframe-width'); 31 | if (widthElement) { 32 | dyframes.push(new Dyframe(widthElement, { 33 | html: html, 34 | width: 1200, 35 | interval: 500 36 | })); 37 | } 38 | 39 | // Device width 600px 40 | var deviceWidthElement = document.getElementById('dyframe-device-width'); 41 | if (deviceWidthElement) { 42 | dyframes.push(new Dyframe(deviceWidthElement, { 43 | html: html, 44 | deviceWidth: 600, 45 | interval: 500 46 | })); 47 | } 48 | 49 | // PC 50 | var pcElement = document.getElementById('dyframe-pc'); 51 | if (pcElement) { 52 | dyframes.push(new Dyframe(pcElement, { 53 | html: html, 54 | profile: 'pc', 55 | interval: 500 56 | })); 57 | } 58 | 59 | // Tablet 60 | var tabletElement = document.getElementById('dyframe-tablet'); 61 | if (tabletElement) { 62 | dyframes.push(new Dyframe(tabletElement, { 63 | html: html, 64 | profile: 'tablet', 65 | interval: 500 66 | })); 67 | } 68 | 69 | // Smartphone 70 | var smartphoneElement = document.getElementById('dyframe-smartphone'); 71 | if (smartphoneElement) { 72 | dyframes.push(new Dyframe(smartphoneElement, { 73 | html: html, 74 | profile: 'smartphone', 75 | interval: 500 76 | })); 77 | } 78 | 79 | // Custom profile 80 | var customProfileElement = document.getElementById('dyframe-custom-profile'); 81 | if (customProfileElement) { 82 | dyframes.push(new Dyframe(customProfileElement, { 83 | html: html, 84 | profile: 'custom', 85 | interval: 500 86 | })); 87 | } 88 | 89 | // Re-rendering 90 | var renderElement = document.getElementById('dyframe-render'); 91 | if (renderElement) { 92 | var renderDyframe = new Dyframe(renderElement, { 93 | html: html, 94 | interval: 500 95 | }); 96 | dyframes.push(renderDyframe); 97 | var renderProfile = null; 98 | setInterval(function () { 99 | switch (renderProfile) { 100 | case 'tablet': 101 | renderProfile = 'smartphone'; 102 | break; 103 | case 'smartphone': 104 | renderProfile = null; 105 | break; 106 | case null: 107 | default: 108 | renderProfile = 'tablet'; 109 | } 110 | renderDyframe.render({ 111 | profile: renderProfile 112 | }); 113 | }, 3000); 114 | } 115 | 116 | // Sync HTML content with textarea value 117 | var updateHtml = function () { 118 | html = input.value; 119 | dyframes.forEach(function (dyframe) { 120 | dyframe.render({ 121 | html: html 122 | }); 123 | }); 124 | }; 125 | input.addEventListener('change', function () { 126 | updateHtml(); 127 | }); 128 | input.addEventListener('keydown', function () { 129 | setTimeout(function () { 130 | if (input.value === html) { 131 | return; 132 | } 133 | updateHtml(); 134 | }, 0); 135 | }); 136 | 137 | // Hotfix for image rendering 138 | setTimeout(function () { 139 | updateHtml(); 140 | }, 1000); 141 | }()); 142 | -------------------------------------------------------------------------------- /dyframe.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Dyframe 3 | * @version 0.5.1 4 | * @link https://github.com/htanjo/dyframe 5 | * @author Hiroyuki Tanjo 6 | * @license MIT 7 | */ 8 | !function(t,e){"use strict";"function"==typeof define&&define.amd?define(e):"object"==typeof module&&module.exports?module.exports=e():t.Dyframe=e()}(this,function(){"use strict";var t={smartphone:{width:980,deviceWidth:375},tablet:{width:980,deviceWidth:768}},e="df-",i={html:"",width:980,deviceWidth:null,profile:null,interval:0},o=function(t,i){this.element=t,this.wrapper=document.createElement("div"),this.viewport=document.createElement("iframe"),this.width=0,this.height=0,this.queued=!1,this.waiting=!1,s(this.element,e+"element"),r(this.wrapper,{position:"relative",display:"block",height:0,padding:0,overflow:"hidden"}),r(this.viewport,{position:"absolute",top:0,left:0,bottom:0,height:"100%",width:"100%",border:0,webkitTransformOrigin:"0 0",msTransformOrigin:"0 0",transformOrigin:"0 0"}),this.wrapper.appendChild(this.viewport),this.element.appendChild(this.wrapper),this.render(i||{}),this.initialized=!0};o.prototype.render=function(t){return"object"==typeof t&&this.updateOptions(t),this.waiting?void(this.queued=!0):void this.renderDom()},o.prototype.renderDom=function(){var t=this;this.updateClass();var e=d(this.element);this.width=e.width,this.height=e.height,this.wrapper.style.paddingBottom=this.height+"px",this.scale(),this.viewport.contentWindow.document.open(),this.viewport.contentWindow.document.write(this.options.html),this.viewport.contentWindow.document.close(),this.queued=!1,this.options.interval>0&&(this.waiting=!0,setTimeout(function(){t.waiting=!1,t.queued&&t.renderDom()},this.options.interval))},o.prototype.updateOptions=function(t){return this.options?void n(this.options,t):void(this.options=n({},i,t))},o.prototype.hasActiveProfile=function(){return!(!this.options.profile||!t[this.options.profile])},o.prototype.updateClass=function(){a(this.element,e+"profile-"),this.hasActiveProfile()&&s(this.element,e+"profile-"+this.options.profile)},o.prototype.scale=function(){var t=this.width/this.getViewportWidth();r(this.viewport,{width:100/t+"%",height:100/t+"%",webkitTransform:"scale("+t+")",msTransform:"scale("+t+")",transform:"scale("+t+")"})},o.prototype.getViewportWidth=function(){var e=this.hasActiveProfile()?t[this.options.profile]:{width:this.options.width,deviceWidth:this.options.deviceWidth};if(!e.deviceWidth)return e.width;var i=this.getViewportData(),o=i.width,n=i["initial-scale"];return o?"device-width"===o?e.deviceWidth:parseInt(o,10):n&&n>0?Math.floor(e.deviceWidth/parseFloat(n)):e.width},o.prototype.getViewportData=function(){var t,e,i=document.createElement("div"),o={};return i.innerHTML=this.options.html,(t=i.querySelector('meta[name="viewport"]'))&&(e=t.getAttribute("content"))?(e.split(",").forEach(function(t){var e=t.trim().split("=");e[0]&&e[1]&&(o[e[0].trim()]=e[1].trim())}),o):o},o.prototype.destroy=function(){this.initialized&&(h(this.element,"df-element"),a(this.element,"df-profile-"),this.element.removeChild(this.wrapper),this.initialized=!1)},o.addProfile=function(e,o){var r={width:i.width,deviceWidth:i.deviceWidth},s=n({},r,o);t[e]=s};var n=function(){var t,e,i=arguments[0];for(t=1;t', 29 | ' * @link <%= pkg.homepage %>', 30 | ' * @author <%= pkg.author %>', 31 | ' * @license <%= pkg.license %>', 32 | ' */', 33 | ''].join('\n'); 34 | var scripts = [ 35 | 'gulpfile.js', 36 | 'karma.conf.js', 37 | 'src/*.js', 38 | 'demo/*.js', 39 | 'test/spec/*.js' 40 | ]; 41 | 42 | gulp.task('clean', del.bind(null, ['coverage'])); 43 | 44 | gulp.task('eslint', function () { 45 | return gulp.src(scripts) 46 | .pipe($.eslint()) 47 | .pipe($.eslint.format()) 48 | .pipe($.eslint.failAfterError()); 49 | }); 50 | 51 | gulp.task('karma', ['clean'], function (callback) { 52 | new Karma({ 53 | configFile: path.resolve('./karma.conf.js') 54 | }, callback).start(); 55 | }); 56 | 57 | gulp.task('scripts', function () { 58 | var pkg = getJson('package.json'); 59 | return gulp.src('src/*.js') 60 | .pipe($.header(banner, {pkg: pkg})) 61 | .pipe(gulp.dest('.')) 62 | .pipe($.uglify({preserveComments: 'some'})) 63 | .pipe($.rename({suffix: '.min'})) 64 | .pipe(gulp.dest('.')); 65 | }); 66 | 67 | gulp.task('serve', ['clean'], function (callback) { 68 | bs.init({ 69 | server: { 70 | baseDir: ['src', 'demo'] 71 | }, 72 | notify: false, 73 | open: false 74 | }, function () { 75 | opn('http://localhost:3000/demo.html'); 76 | callback(); 77 | }); 78 | new Karma({ 79 | configFile: path.resolve('./karma.conf.js'), 80 | singleRun: false 81 | }).start(); 82 | gulp.watch([ 83 | 'demo/*', 84 | 'src/*.js' 85 | ]).on('change', bs.reload); 86 | gulp.watch(scripts, ['eslint']); 87 | }); 88 | 89 | gulp.task('link', function () { 90 | var downloadBase = 'https://github.com/htanjo/dyframe/raw/'; 91 | var pattern = new RegExp(downloadBase + '.*?[/]', 'g'); 92 | var version = getJson('package.json').version; 93 | var downloadDir = downloadBase + 'v' + version + '/'; 94 | return gulp.src(['README.md', 'demo/index.html'], {base: '.'}) 95 | .pipe($.replace(pattern, downloadDir)) 96 | .pipe(gulp.dest('.')); 97 | }); 98 | 99 | gulp.task('bump', function () { 100 | return gulp.src(['package.json', 'bower.json']) 101 | .pipe($.bump({type: options.bump})) 102 | .pipe(gulp.dest('.')); 103 | }); 104 | 105 | gulp.task('commit', ['build', 'link'], function () { 106 | var version = getJson('package.json').version; 107 | return gulp.src(['package.json', 'bower.json', '*.js', 'README.md', 'demo/index.html']) 108 | .pipe($.git.commit('Release v' + version)); 109 | }); 110 | 111 | gulp.task('tag', function (callback) { 112 | var version = getJson('package.json').version; 113 | $.git.tag('v' + version, 'Release v' + version, callback); 114 | }); 115 | 116 | gulp.task('deploy', ['test'], function () { 117 | return gulp.src(['src/*.js', 'demo/**/*', '!demo/demo.html']) 118 | .pipe($.ghPages()); 119 | }); 120 | 121 | gulp.task('test', ['eslint', 'karma']); 122 | 123 | gulp.task('build', ['scripts']); 124 | 125 | gulp.task('release', function (callback) { 126 | runSequence('test', 'bump', 'commit', 'tag', callback); 127 | }); 128 | 129 | gulp.task('default', function (callback) { 130 | runSequence('test', 'build', callback); 131 | }); 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dyframe 2 | > Dynamically render responsive HTML into iframe. 3 | 4 | [![Bower Version][bower-image]][bower-url] 5 | [![npm Version][npm-image]][npm-url] 6 | [![Build Status][travis-image]][travis-url] 7 | [![Coverage Status][coveralls-image]][coveralls-url] 8 | 9 | [![Dyframe](demo/demo.png)](http://htanjo.github.io/dyframe/) 10 | 11 | [**See demo**](http://htanjo.github.io/dyframe/) 12 | 13 | ## Getting started 14 | ### Install 15 | 16 | Download and include `dyframe.js` to your HTML. 17 | Available on [Bower](http://bower.io/) and [npm](https://www.npmjs.com/). 18 | 19 | * Download: [**dyframe.js**][uncompressed-url] / [minified][minified-url] 20 | * Bower: `$ bower install dyframe --save` 21 | * npm: `$ npm install dyframe --save` 22 | 23 | ### Example 24 | ```html 25 | 26 | 27 | 28 | 29 | Dyframe 30 | 37 | 38 | 39 |
40 | 41 | 47 | 48 | 49 | ``` 50 | 51 | ## Constructor 52 | ```js 53 | new Dyframe(element, [options]); 54 | ``` 55 | 56 | - `element` : Target DOM element. 57 | - `options` : Options for rendering HTML content. 58 | 59 | ### Options 60 | 61 | #### html 62 | Type: `String` 63 | Default: `''` 64 | 65 | HTML to render. 66 | Set whole HTML code including `doctype`, ``, `` and `` tag. 67 | 68 | #### width 69 | Type `Number` 70 | Default: `980` (px) 71 | 72 | Width for HTML rendering. 73 | But if you have `profile` option, `width` value will be ignored. (See below) 74 | 75 | #### deviceWidth 76 | Type `Number` | `null` 77 | Default: `null` (px) 78 | 79 | Device width for HTML rendering. 80 | The HTML scaling can be emulated based on `` when you set number. 81 | But if you have `profile` option, `deviceWidth` value will be ignored. (See below) 82 | 83 | #### profile 84 | Type: `String` | `null` 85 | Default: `null` 86 | 87 | Profile name for device emulation. 88 | When you set proper profile, the scaling will be emulated using profile setting instead of `width` and `deviceWidth` option. 89 | 90 | You can use the following profiles, or create custom profile using `Dyframe.addProfile()`. 91 | 92 | - **smartphone:** 93 | width: 980, deviceWidth: 375. Same as iPhone 6 portrait. 94 | - **tablet:** 95 | width: 980, deviceWidth: 768. Same as iPad Air 2 portrait. 96 | 97 | **Tip:** Profiled element has additional class `df-profile-`. 98 | It can be helpful for styling with CSS. 99 | 100 | #### interval 101 | Type: `Number` 102 | Default: `0` (ms) 103 | 104 | Interval to skip rendering. 105 | Frequent re-rendering, such as [live HTML preview](http://htanjo.github.io/dyframe/), could put heavy load on CPU. 106 | To prevent that, you can limit the frequency using this option. 107 | 108 | When you set `500` to this option, the actual DOM rendering takes place only once in 500 ms even if `.render()` method called many times. 109 | 110 | ## Methods 111 | Create "dyframe" object before using methods. 112 | 113 | ```js 114 | var dyframe = new Dyframe(element, options); 115 | ``` 116 | 117 | ### .render([options]) 118 | Re-render the preview content. 119 | If you call this method with argument, the options will be overriden and re-render. 120 | 121 | ```js 122 | var element = document.getElementById('dyframe'); 123 | var dyframe = new Dyframe(element, { 124 | html: 'Hello, world!' 125 | }); 126 | 127 | setTiemout(funciton () { 128 | 129 | // Update HTML content 130 | dyframe.render({ 131 | html: 'Updated!' 132 | }); 133 | 134 | }, 1000); 135 | ``` 136 | 137 | ### .destroy() 138 | Clean up the target element. 139 | 140 | ## Customizing 141 | 142 | ### Dyframe.addProfile(name, profile) 143 | Add custom device profile to the Dyframe global config. 144 | 145 | #### name 146 | Type: `String` 147 | 148 | Custom profile name. 149 | 150 | #### profile 151 | Type: `Object` 152 | 153 | Custom profile data. 154 | Need to define `width` and `deviceWidth` property. 155 | 156 | ```js 157 | // Add custom profile 158 | Dyframe.addProfile('nexus-6', { 159 | width: 980, 160 | deviceWidth: 412 161 | }); 162 | 163 | // Render using "nexus-6" profile 164 | new Dyframe(element, { 165 | html: 'Hello, world!', 166 | profile: 'nexus-6' 167 | }); 168 | 169 | ``` 170 | 171 | ## Compatibility 172 | 173 | ### Browser support 174 | Dyframe works on most modern browsers including smart devices. 175 | [Tested](https://saucelabs.com/u/dyframe) on the following browsers: 176 | 177 | - Internet Explorer (9+) 178 | - Chrome 179 | - Firefox 180 | - Safari 181 | - Opera 182 | - iOS Safari 183 | - Android Browser 184 | 185 | ### Module interface 186 | - CommonJS 187 | - AMD 188 | 189 | ## License 190 | Copyright (c) 2015 Hiroyuki Tanjo. Licensed under the [MIT License](LICENSE). 191 | 192 | [bower-image]: https://img.shields.io/bower/v/dyframe.svg 193 | [bower-url]: http://bower.io/ 194 | [npm-image]: https://img.shields.io/npm/v/dyframe.svg 195 | [npm-url]: https://www.npmjs.com/package/dyframe 196 | [travis-image]: https://img.shields.io/travis/htanjo/dyframe/master.svg 197 | [travis-url]: https://travis-ci.org/htanjo/dyframe 198 | [coveralls-image]: https://img.shields.io/coveralls/htanjo/dyframe/master.svg 199 | [coveralls-url]: https://coveralls.io/r/htanjo/dyframe 200 | [uncompressed-url]: https://github.com/htanjo/dyframe/raw/v0.5.1/dyframe.js 201 | [minified-url]: https://github.com/htanjo/dyframe/raw/v0.5.1/dyframe.min.js 202 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dyframe 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 |
14 |

Dyframe

15 |

Dynamically render responsive HTML into iframe.

16 |
17 | dyframe.js 18 | dyframe.min.js 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 |

HTML source

34 | 100 |
101 |
102 | 103 |
104 | 107 |
108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dyframe Demo 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |

Dyframe Demo

14 |
15 |
16 |
17 |

Base

18 |
19 |
20 |
21 |

Width 1200px

22 |
23 |
24 |
25 |

Device width 600px

26 |
27 |
28 |
29 |

Tablet

30 |
31 |
32 |
33 |

Smartphone

34 |
35 |
36 |
37 |

Custom profile

38 |
39 |
40 |
41 |

Re-rendering

42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 |
50 |
51 |
52 |

HTML source

53 | 119 |
120 |
121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/dyframe.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | 'use strict'; 3 | if (typeof define === 'function' && define.amd) { 4 | define(factory); 5 | } else if (typeof module === 'object' && module.exports) { 6 | module.exports = factory(); 7 | } else { 8 | global.Dyframe = factory(); 9 | } 10 | }(this, function () { 11 | 'use strict'; 12 | 13 | // Device profiles 14 | var profiles = { 15 | // iPhone 6 portrait 16 | smartphone: { 17 | width: 980, 18 | deviceWidth: 375 19 | }, 20 | // iPad Air 2 portrait 21 | tablet: { 22 | width: 980, 23 | deviceWidth: 768 24 | } 25 | }; 26 | 27 | // Prefix for class names 28 | var prefix = 'df-'; 29 | 30 | // Default options 31 | var defaults = { 32 | html: '', 33 | width: 980, 34 | deviceWidth: null, 35 | profile: null, 36 | interval: 0 37 | }; 38 | 39 | // Utility for merging objects 40 | var mergeObjects = function () { 41 | var merged = arguments[0]; 42 | var i; 43 | var prop; 44 | for (i = 1; i < arguments.length; i++) { 45 | for (prop in arguments[i]) { 46 | if ({}.hasOwnProperty.call(arguments[i], prop)) { 47 | merged[prop] = arguments[i][prop]; 48 | } 49 | } 50 | } 51 | return merged; 52 | }; 53 | 54 | // Utility for setting styles 55 | var setStyles = function (element, styles) { 56 | var prop; 57 | for (prop in styles) { 58 | if ({}.hasOwnProperty.call(styles, prop)) { 59 | element.style[prop] = styles[prop]; 60 | } 61 | } 62 | }; 63 | 64 | // Utility for adding class 65 | var addClass = function (element, className) { 66 | if (element.classList) { 67 | element.classList.add(className); 68 | } else { 69 | element.className += ' ' + className; 70 | } 71 | }; 72 | 73 | // Utility for removing class 74 | var removeClass = function (element, className) { 75 | if (element.classList) { 76 | element.classList.remove(className); 77 | } else { 78 | var pattern = new RegExp('(^|\\s)' + className + '(?!\\S)', 'g'); 79 | element.className = element.className.replace(pattern, ''); 80 | } 81 | }; 82 | 83 | // Utility for removing prefixed classes (e.g. "df-profile-*") 84 | var removePrefixedClass = function (element, classPrefix) { 85 | var pattern = new RegExp('(^|\\s)' + classPrefix + '\\S+', 'g'); 86 | element.className = element.className.replace(pattern, ''); 87 | }; 88 | 89 | // Get inner width/height of element 90 | var getInnerSize = function (element) { 91 | var style = window.getComputedStyle(element); 92 | var paddingTop = parseInt(style.getPropertyValue('padding-top'), 0); 93 | var paddingLeft = parseInt(style.getPropertyValue('padding-left'), 0); 94 | var paddingRight = parseInt(style.getPropertyValue('padding-right'), 0); 95 | var paddingBottom = parseInt(style.getPropertyValue('padding-bottom'), 0); 96 | var innerSize = { 97 | width: element.clientWidth - (paddingLeft + paddingRight), 98 | height: element.clientHeight - (paddingTop + paddingBottom) 99 | }; 100 | return innerSize; 101 | }; 102 | 103 | // Constructor 104 | var Dyframe = function (element, options) { 105 | this.element = element; 106 | this.wrapper = document.createElement('div'); 107 | this.viewport = document.createElement('iframe'); 108 | this.width = 0; 109 | this.height = 0; 110 | this.queued = false; 111 | this.waiting = false; 112 | addClass(this.element, prefix + 'element'); 113 | setStyles(this.wrapper, { 114 | position: 'relative', 115 | display: 'block', 116 | height: 0, 117 | padding: 0, 118 | overflow: 'hidden' 119 | }); 120 | setStyles(this.viewport, { 121 | position: 'absolute', 122 | top: 0, 123 | left: 0, 124 | bottom: 0, 125 | height: '100%', 126 | width: '100%', 127 | border: 0, 128 | webkitTransformOrigin: '0 0', 129 | msTransformOrigin: '0 0', 130 | transformOrigin: '0 0' 131 | }); 132 | this.wrapper.appendChild(this.viewport); 133 | this.element.appendChild(this.wrapper); 134 | this.render(options || {}); 135 | this.initialized = true; 136 | }; 137 | 138 | // Render viewport 139 | Dyframe.prototype.render = function (options) { 140 | if (typeof options === 'object') { 141 | this.updateOptions(options); 142 | } 143 | if (this.waiting) { 144 | this.queued = true; 145 | return; 146 | } 147 | this.renderDom(); 148 | }; 149 | 150 | // Actually update DOM 151 | Dyframe.prototype.renderDom = function () { 152 | var self = this; 153 | this.updateClass(); 154 | var innerSize = getInnerSize(this.element); 155 | this.width = innerSize.width; 156 | this.height = innerSize.height; 157 | this.wrapper.style.paddingBottom = this.height + 'px'; 158 | this.scale(); 159 | this.viewport.contentWindow.document.open(); 160 | this.viewport.contentWindow.document.write(this.options.html); 161 | this.viewport.contentWindow.document.close(); 162 | this.queued = false; 163 | if (this.options.interval > 0) { 164 | this.waiting = true; 165 | setTimeout(function () { 166 | self.waiting = false; 167 | if (self.queued) { 168 | self.renderDom(); 169 | } 170 | }, this.options.interval); 171 | } 172 | }; 173 | 174 | // Init or override options 175 | Dyframe.prototype.updateOptions = function (options) { 176 | if (!this.options) { 177 | this.options = mergeObjects({}, defaults, options); 178 | return; 179 | } 180 | mergeObjects(this.options, options); 181 | }; 182 | 183 | // Check if active profile is given 184 | Dyframe.prototype.hasActiveProfile = function () { 185 | return Boolean(this.options.profile && profiles[this.options.profile]); 186 | }; 187 | 188 | // Update class name of dyframe.element 189 | Dyframe.prototype.updateClass = function () { 190 | removePrefixedClass(this.element, prefix + 'profile-'); 191 | if (this.hasActiveProfile()) { 192 | addClass(this.element, prefix + 'profile-' + this.options.profile); 193 | } 194 | }; 195 | 196 | // Scale preview accroding to options 197 | Dyframe.prototype.scale = function () { 198 | var scale = this.width / this.getViewportWidth(); 199 | setStyles(this.viewport, { 200 | width: (100 / scale) + '%', 201 | height: (100 / scale) + '%', 202 | webkitTransform: 'scale(' + scale + ')', 203 | msTransform: 'scale(' + scale + ')', 204 | transform: 'scale(' + scale + ')' 205 | }); 206 | }; 207 | 208 | // Get width of rendering HTML 209 | Dyframe.prototype.getViewportWidth = function () { 210 | var config = this.hasActiveProfile() ? profiles[this.options.profile] : { 211 | width: this.options.width, 212 | deviceWidth: this.options.deviceWidth 213 | }; 214 | if (!config.deviceWidth) { 215 | return config.width; 216 | } 217 | var viewportData = this.getViewportData(); 218 | var width = viewportData.width; 219 | var initialScale = viewportData['initial-scale']; 220 | if (width) { 221 | if (width === 'device-width') { 222 | return config.deviceWidth; 223 | } 224 | return parseInt(width, 10); 225 | } 226 | if (initialScale && initialScale > 0) { 227 | return Math.floor(config.deviceWidth / parseFloat(initialScale)); 228 | } 229 | return config.width; 230 | }; 231 | 232 | // Get viewport content as object 233 | Dyframe.prototype.getViewportData = function () { 234 | var el = document.createElement('div'); 235 | var viewportElement; 236 | var viewportContent; 237 | var viewportData = {}; 238 | el.innerHTML = this.options.html; 239 | viewportElement = el.querySelector('meta[name="viewport"]'); 240 | if (!viewportElement) { 241 | return viewportData; 242 | } 243 | viewportContent = viewportElement.getAttribute('content'); 244 | if (!viewportContent) { 245 | return viewportData; 246 | } 247 | viewportContent.split(',').forEach(function (configSet) { 248 | var config = configSet.trim().split('='); 249 | if (!config[0] || !config[1]) { 250 | return; 251 | } 252 | viewportData[config[0].trim()] = config[1].trim(); 253 | }); 254 | return viewportData; 255 | }; 256 | 257 | // Clean up element and remove classes 258 | Dyframe.prototype.destroy = function () { 259 | if (!this.initialized) { 260 | return; 261 | } 262 | removeClass(this.element, 'df-element'); 263 | removePrefixedClass(this.element, 'df-profile-'); 264 | this.element.removeChild(this.wrapper); 265 | this.initialized = false; 266 | }; 267 | 268 | // Add custom profile 269 | Dyframe.addProfile = function (name, profileData) { 270 | var profileDefaults = { 271 | width: defaults.width, 272 | deviceWidth: defaults.deviceWidth 273 | }; 274 | var profile = mergeObjects({}, profileDefaults, profileData); 275 | profiles[name] = profile; 276 | }; 277 | 278 | return Dyframe; 279 | })); 280 | -------------------------------------------------------------------------------- /dyframe.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Dyframe 3 | * @version 0.5.1 4 | * @link https://github.com/htanjo/dyframe 5 | * @author Hiroyuki Tanjo 6 | * @license MIT 7 | */ 8 | (function (global, factory) { 9 | 'use strict'; 10 | 11 | if (typeof define === 'function' && define.amd) { 12 | define(factory); 13 | } 14 | else if (typeof module === 'object' && module.exports) { 15 | module.exports = factory(); 16 | } 17 | else { 18 | global.Dyframe = factory(); 19 | } 20 | 21 | }(this, function () { 22 | 'use strict'; 23 | 24 | // Device profiles 25 | var profiles = { 26 | // iPhone 6 portrait 27 | smartphone: { 28 | width: 980, 29 | deviceWidth: 375 30 | }, 31 | // iPad Air 2 portrait 32 | tablet: { 33 | width: 980, 34 | deviceWidth: 768 35 | } 36 | }; 37 | 38 | // Prefix for class names 39 | var prefix = 'df-'; 40 | 41 | // Default options 42 | var defaults = { 43 | html: '', 44 | width: 980, 45 | deviceWidth: null, 46 | profile: null, 47 | interval: 0 48 | }; 49 | 50 | // Constructor 51 | var Dyframe = function (element, options) { 52 | this.element = element; 53 | this.wrapper = document.createElement('div'); 54 | this.viewport = document.createElement('iframe'); 55 | this.width = 0; 56 | this.height = 0; 57 | this.queued = false; 58 | this.waiting = false; 59 | addClass(this.element, prefix + 'element'); 60 | setStyles(this.wrapper, { 61 | position: 'relative', 62 | display: 'block', 63 | height: 0, 64 | padding: 0, 65 | overflow: 'hidden' 66 | }); 67 | setStyles(this.viewport, { 68 | position: 'absolute', 69 | top: 0, 70 | left: 0, 71 | bottom: 0, 72 | height: '100%', 73 | width: '100%', 74 | border: 0, 75 | webkitTransformOrigin: '0 0', 76 | msTransformOrigin: '0 0', 77 | transformOrigin: '0 0' 78 | }); 79 | this.wrapper.appendChild(this.viewport); 80 | this.element.appendChild(this.wrapper); 81 | this.render(options || {}); 82 | this.initialized = true; 83 | }; 84 | 85 | // Render viewport 86 | Dyframe.prototype.render = function (options) { 87 | if (typeof options === 'object') { 88 | this.updateOptions(options); 89 | } 90 | if (this.waiting) { 91 | this.queued = true; 92 | return; 93 | } 94 | this.renderDom(); 95 | }; 96 | 97 | // Actually update DOM 98 | Dyframe.prototype.renderDom = function () { 99 | var self = this; 100 | this.updateClass(); 101 | var innerSize = getInnerSize(this.element); 102 | this.width = innerSize.width; 103 | this.height = innerSize.height; 104 | this.wrapper.style.paddingBottom = this.height + 'px'; 105 | this.scale(); 106 | this.viewport.contentWindow.document.open(); 107 | this.viewport.contentWindow.document.write(this.options.html); 108 | this.viewport.contentWindow.document.close(); 109 | this.queued = false; 110 | if (this.options.interval > 0) { 111 | this.waiting = true; 112 | setTimeout(function () { 113 | self.waiting = false; 114 | if (self.queued) { 115 | self.renderDom(); 116 | } 117 | }, this.options.interval); 118 | } 119 | }; 120 | 121 | // Init or override options 122 | Dyframe.prototype.updateOptions = function (options) { 123 | if (!this.options) { 124 | this.options = mergeObjects({}, defaults, options); 125 | return; 126 | } 127 | mergeObjects(this.options, options); 128 | }; 129 | 130 | // Check if active profile is given 131 | Dyframe.prototype.hasActiveProfile = function () { 132 | return !!(this.options.profile && profiles[this.options.profile]); 133 | }; 134 | 135 | // Update class name of dyframe.element 136 | Dyframe.prototype.updateClass = function () { 137 | removePrefixedClass(this.element, prefix + 'profile-'); 138 | if (this.hasActiveProfile()) { 139 | addClass(this.element, prefix + 'profile-' + this.options.profile); 140 | } 141 | }; 142 | 143 | // Scale preview accroding to options 144 | Dyframe.prototype.scale = function () { 145 | var scale = this.width / this.getViewportWidth(); 146 | setStyles(this.viewport, { 147 | width: (100 / scale) + '%', 148 | height: (100 / scale) + '%', 149 | webkitTransform: 'scale(' + scale + ')', 150 | msTransform: 'scale(' + scale + ')', 151 | transform: 'scale(' + scale + ')' 152 | }); 153 | }; 154 | 155 | // Get width of rendering HTML 156 | Dyframe.prototype.getViewportWidth = function () { 157 | var config = this.hasActiveProfile() ? profiles[this.options.profile] : { 158 | width: this.options.width, 159 | deviceWidth: this.options.deviceWidth 160 | }; 161 | if (!config.deviceWidth) { 162 | return config.width; 163 | } 164 | var viewportData = this.getViewportData(); 165 | var width = viewportData.width; 166 | var initialScale = viewportData['initial-scale']; 167 | if (width) { 168 | if (width === 'device-width') { 169 | return config.deviceWidth; 170 | } 171 | return parseInt(width, 10); 172 | } 173 | if (initialScale && initialScale > 0) { 174 | return Math.floor(config.deviceWidth / parseFloat(initialScale)); 175 | } 176 | return config.width; 177 | }; 178 | 179 | // Get viewport content as object 180 | Dyframe.prototype.getViewportData = function () { 181 | var el = document.createElement('div'); 182 | var viewportElement; 183 | var viewportContent; 184 | var viewportData = {}; 185 | el.innerHTML = this.options.html; 186 | viewportElement = el.querySelector('meta[name="viewport"]'); 187 | if (!viewportElement) { 188 | return viewportData; 189 | } 190 | viewportContent = viewportElement.getAttribute('content'); 191 | if (!viewportContent) { 192 | return viewportData; 193 | } 194 | viewportContent.split(',').forEach(function (configSet) { 195 | var config = configSet.trim().split('='); 196 | if (!config[0] || !config[1]) { 197 | return; 198 | } 199 | viewportData[config[0].trim()] = config[1].trim(); 200 | }); 201 | return viewportData; 202 | }; 203 | 204 | // Clean up element and remove classes 205 | Dyframe.prototype.destroy = function () { 206 | if (!this.initialized) { 207 | return; 208 | } 209 | removeClass(this.element, 'df-element'); 210 | removePrefixedClass(this.element, 'df-profile-'); 211 | this.element.removeChild(this.wrapper); 212 | this.initialized = false; 213 | }; 214 | 215 | // Add custom profile 216 | Dyframe.addProfile = function (name, profileData) { 217 | var profileDefaults = { 218 | width: defaults.width, 219 | deviceWidth: defaults.deviceWidth 220 | }; 221 | var profile = mergeObjects({}, profileDefaults, profileData); 222 | profiles[name] = profile; 223 | }; 224 | 225 | // Utility for merging objects 226 | var mergeObjects = function () { 227 | var merged = arguments[0]; 228 | var i; 229 | var prop; 230 | for (i = 1; i < arguments.length; i++) { 231 | for (prop in arguments[i]) { 232 | if (arguments[i].hasOwnProperty(prop)) { 233 | merged[prop] = arguments[i][prop]; 234 | } 235 | } 236 | } 237 | return merged; 238 | }; 239 | 240 | // Utility for setting styles 241 | var setStyles = function (element, styles) { 242 | var prop; 243 | for (prop in styles) { 244 | element.style[prop] = styles[prop]; 245 | } 246 | }; 247 | 248 | // Utility for adding class 249 | var addClass = function (element, className) { 250 | if (element.classList) { 251 | element.classList.add(className); 252 | } 253 | else { 254 | element.className += ' ' + className; 255 | } 256 | }; 257 | 258 | // Utility for removing class 259 | var removeClass = function (element, className) { 260 | if (element.classList) { 261 | element.classList.remove(className); 262 | } 263 | else { 264 | var pattern = new RegExp('(^|\\s)' + className + '(?!\\S)', 'g'); 265 | element.className = element.className.replace(pattern, ''); 266 | } 267 | }; 268 | 269 | // Utility for removing prefixed classes (e.g. "df-profile-*") 270 | var removePrefixedClass = function (element, classPrefix) { 271 | var pattern = new RegExp('(^|\\s)' + classPrefix + '\\S+', 'g'); 272 | element.className = element.className.replace(pattern, ''); 273 | }; 274 | 275 | // Get inner width/height of element 276 | var getInnerSize = function (element) { 277 | var style = window.getComputedStyle(element); 278 | var paddingTop = parseInt(style.getPropertyValue('padding-top'), 0); 279 | var paddingLeft = parseInt(style.getPropertyValue('padding-left'), 0); 280 | var paddingRight = parseInt(style.getPropertyValue('padding-right'), 0); 281 | var paddingBottom = parseInt(style.getPropertyValue('padding-bottom'), 0); 282 | var innerSize = { 283 | width: element.clientWidth - (paddingLeft + paddingRight), 284 | height: element.clientHeight - (paddingTop + paddingBottom) 285 | }; 286 | return innerSize; 287 | }; 288 | 289 | return Dyframe; 290 | 291 | })); 292 | -------------------------------------------------------------------------------- /test/spec/dyframe-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint-disable no-unused-expressions */ 3 | /* global expect */ 4 | (function () { 5 | 'use strict'; 6 | 7 | var Dyframe = window.Dyframe; 8 | var element = document.createElement('div'); 9 | var dyframe; 10 | 11 | // Helpers 12 | var hasClass = function (element, className) { 13 | if (element.classList) { 14 | return element.classList.contains(className); 15 | } 16 | return new RegExp('(^|\\s)' + className + '(?!\\S)', 'g').test(element.className); 17 | }; 18 | var addClass = function (element, className) { 19 | if (element.classList) { 20 | element.classList.add(className); 21 | } else { 22 | element.className += ' ' + className; 23 | } 24 | }; 25 | 26 | // Set up fixtures 27 | element.style.width = '100px'; 28 | element.style.height = '200px'; 29 | document.body.appendChild(element); 30 | 31 | // Specs 32 | describe('Dyframe', function () { 33 | describe('.addProfile()', function () { 34 | afterEach(function () { 35 | dyframe.destroy(); 36 | }); 37 | 38 | it('adds active profile for dyframe objects', function () { 39 | Dyframe.addProfile('custom', {}); 40 | dyframe = new Dyframe(element, { 41 | profile: 'custom' 42 | }); 43 | expect(dyframe.hasActiveProfile()).to.be.true; 44 | }); 45 | }); 46 | 47 | describe('Constructor', function () { 48 | afterEach(function () { 49 | dyframe.destroy(); 50 | }); 51 | 52 | it('creates
and