`.
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 |
20 |
21 |
26 |
32 |
33 |
HTML source
34 |
100 |
101 |
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 |
20 |
21 |
Width 1200px
22 |
23 |
24 |
25 |
Device width 600px
26 |
27 |
28 |
32 |
33 |
Smartphone
34 |
35 |
36 |
37 |
Custom profile
38 |
39 |
40 |
41 |
Re-rendering
42 |
43 |
44 |
45 |
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