holder_rails 2.5.2 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc4ac8bc0d47875411ad35a7d876139a5a5e9a61
4
- data.tar.gz: d795d0d4ef3ebbcd6eecf78f489405e155953208
3
+ metadata.gz: a1142badf43c4edf7b7c0f050f4406c6d21e4d43
4
+ data.tar.gz: b323cc9378948ea3045728ac8f6561a614ed3224
5
5
  SHA512:
6
- metadata.gz: 5d09dd56c7047b5407112f5076995b7a567dbd28336fcf165e8ffcbc14c7e6ad77c9b06643eba3f651fe679d2d329e7ec4b34dd84c9a46fda4b635f879059f36
7
- data.tar.gz: ace91629ebd8c6e57b591d896b3cca880af59881e86c5a34d62e1682b535098dbfe0015b2da5b8cec29037abfb2971755b14fb15c3181afe98fbf420e9bf9027
6
+ metadata.gz: a740fa35fce65b0a07eaced4b3aaf12c6f1f98b00271cd7a93ab8ddc07d895b221b1766a4154aadb1aefced45fcd862e7ebb239bff967697f94ed486fc5b8093
7
+ data.tar.gz: cbc93bb919f5604e548fbc24e54f5ae0de98bd2a9c870d001d2207aeb4142ee8b351d794942edb35fe39b8f736554b5098fc7fa917de3785ce88d8edc68daf8f
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-2014 Nihad Abbasov <mail@narkoz.me>
1
+ Copyright (c) 2012-2015 Nihad Abbasov <mail@narkoz.me>
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without
@@ -1,3 +1,3 @@
1
1
  module HolderRails
2
- VERSION = "2.5.2"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
 
3
3
  Holder - client side image placeholders
4
- Version 2.5.2+2mg3u
4
+ Version 2.6.0+51ebp
5
5
  © 2015 Ivan Malopinsky - http://imsky.co
6
6
 
7
7
  Site: http://holderjs.com
@@ -9,2022 +9,1912 @@ Issues: https://github.com/imsky/holder/issues
9
9
  License: http://opensource.org/licenses/MIT
10
10
 
11
11
  */
12
- /*!
13
- * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license
14
- *
15
- * Specially modified to work with Holder.js
16
- */
17
-
18
- ;(function(name, global, callback){
19
- global[name] = callback;
20
- })("onDomReady", this,
21
-
22
- (function(win) {
23
-
24
- 'use strict';
25
-
26
- //Lazy loading fix for Firefox < 3.6
27
- //http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
28
- if (document.readyState == null && document.addEventListener) {
29
- document.addEventListener("DOMContentLoaded", function DOMContentLoaded() {
30
- document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
31
- document.readyState = "complete";
32
- }, false);
33
- document.readyState = "loading";
34
- }
35
-
36
- var doc = win.document,
37
- docElem = doc.documentElement,
38
-
39
- LOAD = "load",
40
- FALSE = false,
41
- ONLOAD = "on"+LOAD,
42
- COMPLETE = "complete",
43
- READYSTATE = "readyState",
44
- ATTACHEVENT = "attachEvent",
45
- DETACHEVENT = "detachEvent",
46
- ADDEVENTLISTENER = "addEventListener",
47
- DOMCONTENTLOADED = "DOMContentLoaded",
48
- ONREADYSTATECHANGE = "onreadystatechange",
49
- REMOVEEVENTLISTENER = "removeEventListener",
50
-
51
- // W3C Event model
52
- w3c = ADDEVENTLISTENER in doc,
53
- top = FALSE,
54
-
55
- // isReady: Is the DOM ready to be used? Set to true once it occurs.
56
- isReady = FALSE,
57
-
58
- // Callbacks pending execution until DOM is ready
59
- callbacks = [];
60
-
61
- // Handle when the DOM is ready
62
- function ready( fn ) {
63
- if ( !isReady ) {
64
-
65
- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
66
- if ( !doc.body ) {
67
- return defer( ready );
68
- }
69
-
70
- // Remember that the DOM is ready
71
- isReady = true;
72
-
73
- // Execute all callbacks
74
- while ( fn = callbacks.shift() ) {
75
- defer( fn );
76
- }
77
- }
78
- }
79
-
80
- // The ready event handler
81
- function completed( event ) {
82
- // readyState === "complete" is good enough for us to call the dom ready in oldIE
83
- if ( w3c || event.type === LOAD || doc[READYSTATE] === COMPLETE ) {
84
- detach();
85
- ready();
86
- }
87
- }
88
-
89
- // Clean-up method for dom ready events
90
- function detach() {
91
- if ( w3c ) {
92
- doc[REMOVEEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
93
- win[REMOVEEVENTLISTENER]( LOAD, completed, FALSE );
94
- } else {
95
- doc[DETACHEVENT]( ONREADYSTATECHANGE, completed );
96
- win[DETACHEVENT]( ONLOAD, completed );
97
- }
98
- }
99
-
100
- // Defers a function, scheduling it to run after the current call stack has cleared.
101
- function defer( fn, wait ) {
102
- // Allow 0 to be passed
103
- setTimeout( fn, +wait >= 0 ? wait : 1 );
104
- }
105
-
106
- // Attach the listeners:
107
-
108
- // Catch cases where onDomReady is called after the browser event has already occurred.
109
- // we once tried to use readyState "interactive" here, but it caused issues like the one
110
- // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
111
- if ( doc[READYSTATE] === COMPLETE ) {
112
- // Handle it asynchronously to allow scripts the opportunity to delay ready
113
- defer( ready );
114
-
115
- // Standards-based browsers support DOMContentLoaded
116
- } else if ( w3c ) {
117
- // Use the handy event callback
118
- doc[ADDEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
119
-
120
- // A fallback to window.onload, that will always work
121
- win[ADDEVENTLISTENER]( LOAD, completed, FALSE );
122
-
123
- // If IE event model is used
124
- } else {
125
- // Ensure firing before onload, maybe late but safe also for iframes
126
- doc[ATTACHEVENT]( ONREADYSTATECHANGE, completed );
127
-
128
- // A fallback to window.onload, that will always work
129
- win[ATTACHEVENT]( ONLOAD, completed );
130
-
131
- // If IE and not a frame
132
- // continually check to see if the document is ready
133
- try {
134
- top = win.frameElement == null && docElem;
135
- } catch(e) {}
136
-
137
- if ( top && top.doScroll ) {
138
- (function doScrollCheck() {
139
- if ( !isReady ) {
140
- try {
141
- // Use the trick by Diego Perini
142
- // http://javascript.nwbox.com/IEContentLoaded/
143
- top.doScroll("left");
144
- } catch(e) {
145
- return defer( doScrollCheck, 50 );
146
- }
147
-
148
- // detach all dom ready events
149
- detach();
150
-
151
- // and execute any waiting functions
152
- ready();
153
- }
154
- })();
155
- }
156
- }
157
-
158
- function onDomReady( fn ) {
159
- // If DOM is ready, execute the function (async), otherwise wait
160
- isReady ? defer( fn ) : callbacks.push( fn );
161
- }
162
-
163
- // Add version
164
- onDomReady.version = "1.4.0";
165
- // Add method to check if DOM is ready
166
- onDomReady.isReady = function(){
167
- return isReady;
168
- };
169
-
170
- return onDomReady;
171
- })(this));
172
-
173
- //https://github.com/inexorabletash/polyfill/blob/master/web.js
174
- if (!document.querySelectorAll) {
175
- document.querySelectorAll = function (selectors) {
176
- var style = document.createElement('style'), elements = [], element;
177
- document.documentElement.firstChild.appendChild(style);
178
- document._qsa = [];
179
-
180
- style.styleSheet.cssText = selectors + '{x-qsa:expression(document._qsa && document._qsa.push(this))}';
181
- window.scrollBy(0, 0);
182
- style.parentNode.removeChild(style);
183
-
184
- while (document._qsa.length) {
185
- element = document._qsa.shift();
186
- element.style.removeAttribute('x-qsa');
187
- elements.push(element);
188
- }
189
- document._qsa = null;
190
- return elements;
191
- };
192
- }
193
-
194
- if (!document.querySelector) {
195
- document.querySelector = function (selectors) {
196
- var elements = document.querySelectorAll(selectors);
197
- return (elements.length) ? elements[0] : null;
198
- };
199
- }
200
-
201
- if (!document.getElementsByClassName) {
202
- document.getElementsByClassName = function (classNames) {
203
- classNames = String(classNames).replace(/^|\s+/g, '.');
204
- return document.querySelectorAll(classNames);
205
- };
206
- }
207
-
208
- //https://github.com/inexorabletash/polyfill
209
- // ES5 15.2.3.14 Object.keys ( O )
210
- // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
211
- if (!Object.keys) {
212
- Object.keys = function (o) {
213
- if (o !== Object(o)) { throw TypeError('Object.keys called on non-object'); }
214
- var ret = [], p;
215
- for (p in o) {
216
- if (Object.prototype.hasOwnProperty.call(o, p)) {
217
- ret.push(p);
218
- }
219
- }
220
- return ret;
221
- };
222
- }
223
-
224
- //https://github.com/inexorabletash/polyfill/blob/master/web.js
225
- (function (global) {
226
- var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
227
- global.atob = global.atob || function (input) {
228
- input = String(input);
229
- var position = 0,
230
- output = [],
231
- buffer = 0, bits = 0, n;
232
-
233
- input = input.replace(/\s/g, '');
234
- if ((input.length % 4) === 0) { input = input.replace(/=+$/, ''); }
235
- if ((input.length % 4) === 1) { throw Error("InvalidCharacterError"); }
236
- if (/[^+/0-9A-Za-z]/.test(input)) { throw Error("InvalidCharacterError"); }
237
-
238
- while (position < input.length) {
239
- n = B64_ALPHABET.indexOf(input.charAt(position));
240
- buffer = (buffer << 6) | n;
241
- bits += 6;
242
-
243
- if (bits === 24) {
244
- output.push(String.fromCharCode((buffer >> 16) & 0xFF));
245
- output.push(String.fromCharCode((buffer >> 8) & 0xFF));
246
- output.push(String.fromCharCode(buffer & 0xFF));
247
- bits = 0;
248
- buffer = 0;
249
- }
250
- position += 1;
251
- }
252
-
253
- if (bits === 12) {
254
- buffer = buffer >> 4;
255
- output.push(String.fromCharCode(buffer & 0xFF));
256
- } else if (bits === 18) {
257
- buffer = buffer >> 2;
258
- output.push(String.fromCharCode((buffer >> 8) & 0xFF));
259
- output.push(String.fromCharCode(buffer & 0xFF));
260
- }
261
-
262
- return output.join('');
263
- };
264
-
265
- global.btoa = global.btoa || function (input) {
266
- input = String(input);
267
- var position = 0,
268
- out = [],
269
- o1, o2, o3,
270
- e1, e2, e3, e4;
271
-
272
- if (/[^\x00-\xFF]/.test(input)) { throw Error("InvalidCharacterError"); }
273
-
274
- while (position < input.length) {
275
- o1 = input.charCodeAt(position++);
276
- o2 = input.charCodeAt(position++);
277
- o3 = input.charCodeAt(position++);
278
-
279
- // 111111 112222 222233 333333
280
- e1 = o1 >> 2;
281
- e2 = ((o1 & 0x3) << 4) | (o2 >> 4);
282
- e3 = ((o2 & 0xf) << 2) | (o3 >> 6);
283
- e4 = o3 & 0x3f;
284
-
285
- if (position === input.length + 2) {
286
- e3 = 64; e4 = 64;
287
- }
288
- else if (position === input.length + 1) {
289
- e4 = 64;
290
- }
291
-
292
- out.push(B64_ALPHABET.charAt(e1),
293
- B64_ALPHABET.charAt(e2),
294
- B64_ALPHABET.charAt(e3),
295
- B64_ALPHABET.charAt(e4));
296
- }
297
-
298
- return out.join('');
299
- };
300
- }(this));
301
-
302
- //https://gist.github.com/jimeh/332357
303
- if (!Object.prototype.hasOwnProperty){
304
- /*jshint -W001, -W103 */
305
- Object.prototype.hasOwnProperty = function(prop) {
306
- var proto = this.__proto__ || this.constructor.prototype;
307
- return (prop in this) && (!(prop in proto) || proto[prop] !== this[prop]);
12
+ (function webpackUniversalModuleDefinition(root, factory) {
13
+ if(typeof exports === 'object' && typeof module === 'object')
14
+ module.exports = factory();
15
+ else if(typeof define === 'function' && define.amd)
16
+ define(factory);
17
+ else if(typeof exports === 'object')
18
+ exports["Holder"] = factory();
19
+ else
20
+ root["Holder"] = factory();
21
+ })(this, function() {
22
+ return /******/ (function(modules) { // webpackBootstrap
23
+ /******/ // The module cache
24
+ /******/ var installedModules = {};
25
+
26
+ /******/ // The require function
27
+ /******/ function __webpack_require__(moduleId) {
28
+
29
+ /******/ // Check if module is in cache
30
+ /******/ if(installedModules[moduleId])
31
+ /******/ return installedModules[moduleId].exports;
32
+
33
+ /******/ // Create a new module (and put it into the cache)
34
+ /******/ var module = installedModules[moduleId] = {
35
+ /******/ exports: {},
36
+ /******/ id: moduleId,
37
+ /******/ loaded: false
38
+ /******/ };
39
+
40
+ /******/ // Execute the module function
41
+ /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
42
+
43
+ /******/ // Flag the module as loaded
44
+ /******/ module.loaded = true;
45
+
46
+ /******/ // Return the exports of the module
47
+ /******/ return module.exports;
48
+ /******/ }
49
+
50
+
51
+ /******/ // expose the modules object (__webpack_modules__)
52
+ /******/ __webpack_require__.m = modules;
53
+
54
+ /******/ // expose the module cache
55
+ /******/ __webpack_require__.c = installedModules;
56
+
57
+ /******/ // __webpack_public_path__
58
+ /******/ __webpack_require__.p = "";
59
+
60
+ /******/ // Load entry module and return exports
61
+ /******/ return __webpack_require__(0);
62
+ /******/ })
63
+ /************************************************************************/
64
+ /******/ ([
65
+ /* 0 */
66
+ /***/ function(module, exports, __webpack_require__) {
67
+
68
+ /* WEBPACK VAR INJECTION */(function(global) {/*
69
+ Holder.js - client side image placeholders
70
+ © 2012-2015 Ivan Malopinsky - http://imsky.co
71
+ */
72
+
73
+ //Libraries and functions
74
+ var onDomReady = __webpack_require__(1);
75
+ var SceneGraph = __webpack_require__(2);
76
+ var utils = __webpack_require__(3);
77
+
78
+ var extend = utils.extend;
79
+ var cssProps = utils.cssProps;
80
+ var encodeHtmlEntity = utils.encodeHtmlEntity;
81
+ var decodeHtmlEntity = utils.decodeHtmlEntity;
82
+ var imageExists = utils.imageExists;
83
+ var getNodeArray = utils.getNodeArray;
84
+ var dimensionCheck = utils.dimensionCheck;
85
+
86
+ //Constants and definitions
87
+ var SVG_NS = 'http://www.w3.org/2000/svg';
88
+ var NODE_TYPE_COMMENT = 8;
89
+ var version = '2.6.0';
90
+ var generatorComment = '\n' +
91
+ 'Created with Holder.js ' + version + '.\n' +
92
+ 'Learn more at http://holderjs.com\n' +
93
+ '(c) 2012-2015 Ivan Malopinsky - http://imsky.co\n';
94
+
95
+ var Holder = {
96
+ version: version,
97
+
98
+ /**
99
+ * Adds a theme to default settings
100
+ *
101
+ * @param {string} name Theme name
102
+ * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties.
103
+ */
104
+ addTheme: function(name, theme) {
105
+ name != null && theme != null && (App.settings.themes[name] = theme);
106
+ delete App.vars.cache.themeKeys;
107
+ return this;
108
+ },
109
+
110
+ /**
111
+ * Appends a placeholder to an element
112
+ *
113
+ * @param {string} src Placeholder URL string
114
+ * @param {string} el Selector of target element(s)
115
+ */
116
+ addImage: function(src, el) {
117
+ var node = document.querySelectorAll(el);
118
+ if (node.length) {
119
+ for (var i = 0, l = node.length; i < l; i++) {
120
+ var img = newEl('img');
121
+ var domProps = {};
122
+ domProps[App.vars.dataAttr] = src;
123
+ setAttr(img, domProps);
124
+ node[i].appendChild(img);
125
+ }
126
+ }
127
+ return this;
128
+ },
129
+
130
+ /**
131
+ * Sets whether or not an image is updated on resize.
132
+ * If an image is set to be updated, it is immediately rendered.
133
+ *
134
+ * @param {Object} el Image DOM element
135
+ * @param {Boolean} value Resizable update flag value
136
+ */
137
+ setResizeUpdate: function(el, value) {
138
+ if (el.holderData) {
139
+ el.holderData.resizeUpdate = !!value;
140
+ if (el.holderData.resizeUpdate) {
141
+ updateResizableElements(el);
142
+ }
143
+ }
144
+ },
145
+
146
+ /**
147
+ * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes.
148
+ *
149
+ * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties
150
+ */
151
+ run: function(userOptions) {
152
+ userOptions = userOptions || {};
153
+ var engineSettings = {};
154
+ var options = extend(App.settings, userOptions);
155
+
156
+ App.vars.preempted = true;
157
+ App.vars.dataAttr = options.dataAttr || App.vars.dataAttr;
158
+
159
+ engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer;
160
+ if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) {
161
+ engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html');
162
+ }
163
+
164
+ var images = getNodeArray(options.images);
165
+ var bgnodes = getNodeArray(options.bgnodes);
166
+ var stylenodes = getNodeArray(options.stylenodes);
167
+ var objects = getNodeArray(options.objects);
168
+
169
+ engineSettings.stylesheets = [];
170
+ engineSettings.svgXMLStylesheet = true;
171
+ engineSettings.noFontFallback = options.noFontFallback ? options.noFontFallback : false;
172
+
173
+ for (var i = 0; i < stylenodes.length; i++) {
174
+ var styleNode = stylenodes[i];
175
+ if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') {
176
+ var href = styleNode.attributes.href.value;
177
+ //todo: write isomorphic relative-to-absolute URL function
178
+ var proxyLink = newEl('a');
179
+ proxyLink.href = href;
180
+ var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search;
181
+ engineSettings.stylesheets.push(stylesheetURL);
182
+ }
183
+ }
184
+
185
+ for (i = 0; i < bgnodes.length; i++) {
186
+ //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background
187
+ if (!global.getComputedStyle) continue;
188
+ var backgroundImage = global.getComputedStyle(bgnodes[i], null).getPropertyValue('background-image');
189
+ var dataBackgroundImage = bgnodes[i].getAttribute('data-background-src');
190
+ var rawURL = null;
191
+
192
+ if (dataBackgroundImage == null) {
193
+ rawURL = backgroundImage;
194
+ } else {
195
+ rawURL = dataBackgroundImage;
196
+ }
197
+
198
+ var holderURL = null;
199
+ var holderString = '?' + options.domain + '/';
200
+
201
+ if (rawURL.indexOf(holderString) === 0) {
202
+ holderURL = rawURL.slice(1);
203
+ } else if (rawURL.indexOf(holderString) != -1) {
204
+ var fragment = rawURL.substr(rawURL.indexOf(holderString)).slice(1);
205
+ var fragmentMatch = fragment.match(/([^\"]*)"?\)/);
206
+
207
+ if (fragmentMatch != null) {
208
+ holderURL = fragmentMatch[1];
209
+ }
210
+ }
211
+
212
+ if (holderURL != null) {
213
+ var holderFlags = parseURL(holderURL, options);
214
+ if (holderFlags) {
215
+ prepareDOMElement({
216
+ mode: 'background',
217
+ el: bgnodes[i],
218
+ flags: holderFlags,
219
+ engineSettings: engineSettings
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ for (i = 0; i < objects.length; i++) {
226
+ var object = objects[i];
227
+ var objectAttr = {};
228
+
229
+ try {
230
+ objectAttr.data = object.getAttribute('data');
231
+ objectAttr.dataSrc = object.getAttribute(App.vars.dataAttr);
232
+ } catch (e) {}
233
+
234
+ var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0;
235
+ var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0;
236
+
237
+ if (objectHasSrcURL) {
238
+ prepareImageElement(options, engineSettings, objectAttr.data, object);
239
+ } else if (objectHasDataSrcURL) {
240
+ prepareImageElement(options, engineSettings, objectAttr.dataSrc, object);
241
+ }
242
+ }
243
+
244
+ for (i = 0; i < images.length; i++) {
245
+ var image = images[i];
246
+ var imageAttr = {};
247
+
248
+ try {
249
+ imageAttr.src = image.getAttribute('src');
250
+ imageAttr.dataSrc = image.getAttribute(App.vars.dataAttr);
251
+ imageAttr.rendered = image.getAttribute('data-holder-rendered');
252
+ } catch (e) {}
253
+
254
+ var imageHasSrc = imageAttr.src != null;
255
+ var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0;
256
+ var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true';
257
+
258
+ if (imageHasSrc) {
259
+ if (imageAttr.src.indexOf(options.domain) === 0) {
260
+ prepareImageElement(options, engineSettings, imageAttr.src, image);
261
+ } else if (imageHasDataSrcURL) {
262
+ //Image has a valid data-src and an invalid src
263
+ if (imageRendered) {
264
+ //If the placeholder has already been render, re-render it
265
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
266
+ } else {
267
+ //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't
268
+ (function(src, options, engineSettings, dataSrc, image) {
269
+ imageExists(src, function(exists) {
270
+ if (!exists) {
271
+ prepareImageElement(options, engineSettings, dataSrc, image);
272
+ }
273
+ });
274
+ })(imageAttr.src, options, engineSettings, imageAttr.dataSrc, image);
275
+ }
276
+ }
277
+ } else if (imageHasDataSrcURL) {
278
+ prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
279
+ }
280
+ }
281
+
282
+ return this;
283
+ }
284
+ };
285
+
286
+ var App = {
287
+ settings: {
288
+ domain: 'holder.js',
289
+ images: 'img',
290
+ objects: 'object',
291
+ bgnodes: 'body .holderjs',
292
+ stylenodes: 'head link.holderjs',
293
+ stylesheets: [],
294
+ themes: {
295
+ 'gray': {
296
+ background: '#EEEEEE',
297
+ foreground: '#AAAAAA'
298
+ },
299
+ 'social': {
300
+ background: '#3a5a97',
301
+ foreground: '#FFFFFF'
302
+ },
303
+ 'industrial': {
304
+ background: '#434A52',
305
+ foreground: '#C2F200'
306
+ },
307
+ 'sky': {
308
+ background: '#0D8FDB',
309
+ foreground: '#FFFFFF'
310
+ },
311
+ 'vine': {
312
+ background: '#39DBAC',
313
+ foreground: '#1E292C'
314
+ },
315
+ 'lava': {
316
+ background: '#F8591A',
317
+ foreground: '#1C2846'
318
+ }
319
+ }
320
+ },
321
+ defaults: {
322
+ size: 10,
323
+ units: 'pt',
324
+ scale: 1 / 16
325
+ },
326
+ flags: {
327
+ dimensions: {
328
+ regex: /^(\d+)x(\d+)$/,
329
+ output: function(val) {
330
+ var exec = this.regex.exec(val);
331
+ return {
332
+ width: +exec[1],
333
+ height: +exec[2]
334
+ };
335
+ }
336
+ },
337
+ fluid: {
338
+ regex: /^([0-9]+%?)x([0-9]+%?)$/,
339
+ output: function(val) {
340
+ var exec = this.regex.exec(val);
341
+ return {
342
+ width: exec[1],
343
+ height: exec[2]
344
+ };
345
+ }
346
+ },
347
+ colors: {
348
+ regex: /(?:#|\^)([0-9a-f]{3,})\:(?:#|\^)([0-9a-f]{3,})/i,
349
+ output: function(val) {
350
+ var exec = this.regex.exec(val);
351
+ return {
352
+ foreground: '#' + exec[2],
353
+ background: '#' + exec[1]
354
+ };
355
+ }
356
+ },
357
+ text: {
358
+ regex: /text\:(.*)/,
359
+ output: function(val) {
360
+ return this.regex.exec(val)[1].replace('\\/', '/');
361
+ }
362
+ },
363
+ font: {
364
+ regex: /font\:(.*)/,
365
+ output: function(val) {
366
+ return this.regex.exec(val)[1];
367
+ }
368
+ },
369
+ auto: {
370
+ regex: /^auto$/
371
+ },
372
+ textmode: {
373
+ regex: /textmode\:(.*)/,
374
+ output: function(val) {
375
+ return this.regex.exec(val)[1];
376
+ }
377
+ },
378
+ random: {
379
+ regex: /^random$/
380
+ },
381
+ size: {
382
+ regex: /size\:(\d+)/,
383
+ output: function(val) {
384
+ return this.regex.exec(val)[1];
385
+ }
386
+ }
387
+ }
388
+ };
389
+
390
+ /**
391
+ * Processes provided source attribute and sets up the appropriate rendering workflow
392
+ *
393
+ * @private
394
+ * @param options Instance options from Holder.run
395
+ * @param renderSettings Instance configuration
396
+ * @param src Image URL
397
+ * @param el Image DOM element
398
+ */
399
+ function prepareImageElement(options, engineSettings, src, el) {
400
+ var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options);
401
+ if (holderFlags) {
402
+ prepareDOMElement({
403
+ mode: null,
404
+ el: el,
405
+ flags: holderFlags,
406
+ engineSettings: engineSettings
407
+ });
408
+ }
308
409
  }
309
- /*jshint +W001, +W103 */
310
- }
311
-
312
- //requestAnimationFrame polyfill for older Firefox/Chrome versions
313
- if (!window.requestAnimationFrame) {
314
- if (window.webkitRequestAnimationFrame) {
315
- //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-webkit.js
316
- (function (global) {
317
- // window.requestAnimationFrame
318
- global.requestAnimationFrame = function (callback) {
319
- return webkitRequestAnimationFrame(function () {
320
- callback(global.performance.now());
321
- });
322
- };
323
-
324
- // window.cancelAnimationFrame
325
- global.cancelAnimationFrame = webkitCancelAnimationFrame;
326
- }(this));
327
- } else if (window.mozRequestAnimationFrame) {
328
- //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-moz.js
329
- (function (global) {
330
- // window.requestAnimationFrame
331
- global.requestAnimationFrame = function (callback) {
332
- return mozRequestAnimationFrame(function () {
333
- callback(global.performance.now());
334
- });
335
- };
336
-
337
- // window.cancelAnimationFrame
338
- global.cancelAnimationFrame = mozCancelAnimationFrame;
339
- }(this));
340
- } else {
341
- (function (global) {
342
- global.requestAnimationFrame = function (callback) {
343
- return global.setTimeout(callback, 1000 / 60);
344
- }
345
-
346
- global.cancelAnimationFrame = global.clearTimeout;
347
- })(this);
348
- }
349
- }
350
- (function (global, factory) {
351
- global.augment = factory();
352
- }(this, function () {
353
- var Factory = function () {};
354
- var slice = Array.prototype.slice;
355
-
356
- var augment = function (base, body) {
357
- var uber = Factory.prototype = typeof base === "function" ? base.prototype : base;
358
- var prototype = new Factory(), properties = body.apply(prototype, slice.call(arguments, 2).concat(uber));
359
- if (typeof properties === "object") for (var key in properties) prototype[key] = properties[key];
360
- if (!prototype.hasOwnProperty("constructor")) return prototype;
361
- var constructor = prototype.constructor;
362
- constructor.prototype = prototype;
363
- return constructor;
364
- };
365
-
366
- augment.defclass = function (prototype) {
367
- var constructor = prototype.constructor;
368
- constructor.prototype = prototype;
369
- return constructor;
370
- };
371
-
372
- augment.extend = function (base, body) {
373
- return augment(base, function (uber) {
374
- this.uber = uber;
375
- return body;
376
- });
377
- };
378
-
379
- return augment;
380
- }));
381
-
382
- /*
383
- Holder.js - client side image placeholders
384
- © 2012-2015 Ivan Malopinsky - http://imsky.co
385
- */
386
- (function(register, global, undefined) {
387
- //Constants and definitions
388
- var SVG_NS = 'http://www.w3.org/2000/svg';
389
- var NODE_TYPE_COMMENT = 8;
390
- var document = global.document;
391
- var version = '2.5.2';
392
- var generatorComment = '\n' +
393
- 'Created with Holder.js ' + version + '.\n' +
394
- 'Learn more at http://holderjs.com\n' +
395
- '(c) 2012-2015 Ivan Malopinsky - http://imsky.co\n';
396
-
397
- var Holder = {
398
- version: version,
399
-
400
- /**
401
- * Adds a theme to default settings
402
- *
403
- * @param {string} name Theme name
404
- * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties.
405
- */
406
- addTheme: function(name, theme) {
407
- name != null && theme != null && (App.settings.themes[name] = theme);
408
- delete App.vars.cache.themeKeys;
409
- return this;
410
- },
411
-
412
- /**
413
- * Appends a placeholder to an element
414
- *
415
- * @param {string} src Placeholder URL string
416
- * @param {string} el Selector of target element(s)
417
- */
418
- addImage: function(src, el) {
419
- var node = document.querySelectorAll(el);
420
- if (node.length) {
421
- for (var i = 0, l = node.length; i < l; i++) {
422
- var img = newEl('img');
423
- setAttr(img, {
424
- 'data-src': src
425
- });
426
- node[i].appendChild(img);
427
- }
428
- }
429
- return this;
430
- },
431
-
432
- /**
433
- * Sets whether or not an image is updated on resize.
434
- * If an image is set to be updated, it is immediately rendered.
435
- *
436
- * @param {Object} el Image DOM element
437
- * @param {Boolean} value Resizable update flag value
438
- */
439
- setResizeUpdate: function(el, value) {
440
- if (el.holderData) {
441
- el.holderData.resizeUpdate = !!value;
442
- if (el.holderData.resizeUpdate) {
443
- updateResizableElements(el);
444
- }
445
- }
446
- },
447
-
448
- /**
449
- * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes.
450
- *
451
- * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties
452
- */
453
- run: function(userOptions) {
454
- userOptions = userOptions || {};
455
- var engineSettings = {};
456
-
457
- App.vars.preempted = true;
458
-
459
- var options = extend(App.settings, userOptions);
460
-
461
- engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer;
462
- if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) {
463
- engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html');
464
- }
465
-
466
- var images = getNodeArray(options.images);
467
- var bgnodes = getNodeArray(options.bgnodes);
468
- var stylenodes = getNodeArray(options.stylenodes);
469
- var objects = getNodeArray(options.objects);
470
-
471
- engineSettings.stylesheets = [];
472
- engineSettings.svgXMLStylesheet = true;
473
- engineSettings.noFontFallback = options.noFontFallback ? options.noFontFallback : false;
474
-
475
- for (var i = 0; i < stylenodes.length; i++) {
476
- var styleNode = stylenodes[i];
477
- if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') {
478
- var href = styleNode.attributes.href.value;
479
- //todo: write isomorphic relative-to-absolute URL function
480
- var proxyLink = newEl('a');
481
- proxyLink.href = href;
482
- var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search;
483
- engineSettings.stylesheets.push(stylesheetURL);
484
- }
485
- }
486
-
487
- for (i = 0; i < bgnodes.length; i++) {
488
- //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background
489
- if (!global.getComputedStyle) continue;
490
- var backgroundImage = global.getComputedStyle(bgnodes[i], null).getPropertyValue('background-image');
491
- var dataBackgroundImage = bgnodes[i].getAttribute('data-background-src');
492
- var rawURL = null;
493
-
494
- if (dataBackgroundImage == null) {
495
- rawURL = backgroundImage;
496
- } else {
497
- rawURL = dataBackgroundImage;
498
- }
499
-
500
- var holderURL = null;
501
- var holderString = '?' + options.domain + '/';
502
-
503
- if (rawURL.indexOf(holderString) === 0) {
504
- holderURL = rawURL.slice(1);
505
- } else if (rawURL.indexOf(holderString) != -1) {
506
- var fragment = rawURL.substr(rawURL.indexOf(holderString)).slice(1);
507
- var fragmentMatch = fragment.match(/([^\"]*)"?\)/);
508
-
509
- if (fragmentMatch != null) {
510
- holderURL = fragmentMatch[1];
511
- }
512
- }
513
-
514
- if (holderURL != null) {
515
- var holderFlags = parseURL(holderURL, options);
516
- if (holderFlags) {
517
- prepareDOMElement({
518
- mode: 'background',
519
- el: bgnodes[i],
520
- flags: holderFlags,
521
- engineSettings: engineSettings
522
- });
523
- }
524
- }
525
- }
526
-
527
- for (i = 0; i < objects.length; i++) {
528
- var object = objects[i];
529
- var objectAttr = {};
530
-
531
- try {
532
- objectAttr.data = object.getAttribute('data');
533
- objectAttr.dataSrc = object.getAttribute('data-src');
534
- } catch (e) {}
535
-
536
- var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0;
537
- var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0;
538
-
539
- if (objectHasSrcURL) {
540
- prepareImageElement(options, engineSettings, objectAttr.data, object);
541
- } else if (objectHasDataSrcURL) {
542
- prepareImageElement(options, engineSettings, objectAttr.dataSrc, object);
543
- }
544
- }
545
-
546
- for (i = 0; i < images.length; i++) {
547
- var image = images[i];
548
- var imageAttr = {};
549
-
550
- try {
551
- imageAttr.src = image.getAttribute('src');
552
- imageAttr.dataSrc = image.getAttribute('data-src');
553
- imageAttr.rendered = image.getAttribute('data-holder-rendered');
554
- } catch (e) {}
555
-
556
- var imageHasSrc = imageAttr.src != null;
557
- var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0;
558
- var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true';
559
-
560
- if (imageHasSrc) {
561
- if (imageAttr.src.indexOf(options.domain) === 0) {
562
- prepareImageElement(options, engineSettings, imageAttr.src, image);
563
- } else if (imageHasDataSrcURL) {
564
- //Image has a valid data-src and an invalid src
565
- if (imageRendered) {
566
- //If the placeholder has already been render, re-render it
567
- prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
568
- } else {
569
- //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't
570
- (function(src, options, engineSettings, dataSrc, image) {
571
- imageExists(src, function(exists) {
572
- if (!exists) {
573
- prepareImageElement(options, engineSettings, dataSrc, image);
574
- }
575
- });
576
- })(imageAttr.src, options, engineSettings, imageAttr.dataSrc, image);
577
- }
578
- }
579
- } else if (imageHasDataSrcURL) {
580
- prepareImageElement(options, engineSettings, imageAttr.dataSrc, image);
581
- }
582
- }
583
-
584
- return this;
585
- }
586
- };
587
-
588
- var App = {
589
- settings: {
590
- domain: 'holder.js',
591
- images: 'img',
592
- objects: 'object',
593
- bgnodes: 'body .holderjs',
594
- stylenodes: 'head link.holderjs',
595
- stylesheets: [],
596
- themes: {
597
- 'gray': {
598
- background: '#EEEEEE',
599
- foreground: '#AAAAAA'
600
- },
601
- 'social': {
602
- background: '#3a5a97',
603
- foreground: '#FFFFFF'
604
- },
605
- 'industrial': {
606
- background: '#434A52',
607
- foreground: '#C2F200'
608
- },
609
- 'sky': {
610
- background: '#0D8FDB',
611
- foreground: '#FFFFFF'
612
- },
613
- 'vine': {
614
- background: '#39DBAC',
615
- foreground: '#1E292C'
616
- },
617
- 'lava': {
618
- background: '#F8591A',
619
- foreground: '#1C2846'
620
- }
621
- }
622
- },
623
- defaults: {
624
- size: 10,
625
- units: 'pt',
626
- scale: 1 / 16
627
- },
628
- flags: {
629
- dimensions: {
630
- regex: /^(\d+)x(\d+)$/,
631
- output: function(val) {
632
- var exec = this.regex.exec(val);
633
- return {
634
- width: +exec[1],
635
- height: +exec[2]
636
- };
637
- }
638
- },
639
- fluid: {
640
- regex: /^([0-9]+%?)x([0-9]+%?)$/,
641
- output: function(val) {
642
- var exec = this.regex.exec(val);
643
- return {
644
- width: exec[1],
645
- height: exec[2]
646
- };
647
- }
648
- },
649
- colors: {
650
- regex: /(?:#|\^)([0-9a-f]{3,})\:(?:#|\^)([0-9a-f]{3,})/i,
651
- output: function(val) {
652
- var exec = this.regex.exec(val);
653
- return {
654
- foreground: '#' + exec[2],
655
- background: '#' + exec[1]
656
- };
657
- }
658
- },
659
- text: {
660
- regex: /text\:(.*)/,
661
- output: function(val) {
662
- return this.regex.exec(val)[1].replace('\\/', '/');
663
- }
664
- },
665
- font: {
666
- regex: /font\:(.*)/,
667
- output: function(val) {
668
- return this.regex.exec(val)[1];
669
- }
670
- },
671
- auto: {
672
- regex: /^auto$/
673
- },
674
- textmode: {
675
- regex: /textmode\:(.*)/,
676
- output: function(val) {
677
- return this.regex.exec(val)[1];
678
- }
679
- },
680
- random: {
681
- regex: /^random$/
682
- },
683
- size: {
684
- regex: /size\:(\d+)/,
685
- output: function(val){
686
- return this.regex.exec(val)[1];
687
- }
688
- }
689
- }
690
- };
691
-
692
- /**
693
- * Processes provided source attribute and sets up the appropriate rendering workflow
694
- *
695
- * @private
696
- * @param options Instance options from Holder.run
697
- * @param renderSettings Instance configuration
698
- * @param src Image URL
699
- * @param el Image DOM element
700
- */
701
- function prepareImageElement(options, engineSettings, src, el) {
702
- var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options);
703
- if (holderFlags) {
704
- prepareDOMElement({
705
- mode: null,
706
- el: el,
707
- flags: holderFlags,
708
- engineSettings: engineSettings
709
- });
710
- }
711
- }
712
-
713
- /**
714
- * Processes a Holder URL and extracts flags
715
- *
716
- * @private
717
- * @param url URL
718
- * @param options Instance options from Holder.run
719
- */
720
- function parseURL(url, options) {
721
- var ret = {
722
- theme: extend(App.settings.themes.gray, null),
723
- stylesheets: options.stylesheets,
724
- holderURL: []
725
- };
726
- var render = false;
727
- var vtab = String.fromCharCode(11);
728
- var flags = url.replace(/([^\\])\//g, '$1' + vtab).split(vtab);
729
- var uriRegex = /%[0-9a-f]{2}/gi;
730
- for (var fl = flags.length, j = 0; j < fl; j++) {
731
- var flag = flags[j];
732
- if (flag.match(uriRegex)) {
733
- try {
734
- flag = decodeURIComponent(flag);
735
- } catch (e) {
736
- flag = flags[j];
737
- }
738
- }
739
-
740
- var push = false;
741
-
742
- if (App.flags.dimensions.match(flag)) {
743
- render = true;
744
- ret.dimensions = App.flags.dimensions.output(flag);
745
- push = true;
746
- } else if (App.flags.fluid.match(flag)) {
747
- render = true;
748
- ret.dimensions = App.flags.fluid.output(flag);
749
- ret.fluid = true;
750
- push = true;
751
- } else if (App.flags.textmode.match(flag)) {
752
- ret.textmode = App.flags.textmode.output(flag);
753
- push = true;
754
- } else if (App.flags.colors.match(flag)) {
755
- var colors = App.flags.colors.output(flag);
756
- ret.theme = extend(ret.theme, colors);
757
- //todo: convert implicit theme use to a theme: flag
758
- push = true;
759
- } else if (options.themes[flag]) {
760
- //If a theme is specified, it will override custom colors
761
- if (options.themes.hasOwnProperty(flag)) {
762
- ret.theme = extend(options.themes[flag], null);
763
- }
764
- push = true;
765
- } else if (App.flags.font.match(flag)) {
766
- ret.font = App.flags.font.output(flag);
767
- push = true;
768
- } else if (App.flags.auto.match(flag)) {
769
- ret.auto = true;
770
- push = true;
771
- } else if (App.flags.text.match(flag)) {
772
- ret.text = App.flags.text.output(flag);
773
- push = true;
774
- } else if (App.flags.size.match(flag)) {
775
- ret.size = App.flags.size.output(flag);
776
- push = true;
777
- } else if (App.flags.random.match(flag)) {
778
- if (App.vars.cache.themeKeys == null) {
779
- App.vars.cache.themeKeys = Object.keys(options.themes);
780
- }
781
- var theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length];
782
- ret.theme = extend(options.themes[theme], null);
783
- push = true;
784
- }
785
-
786
- if (push) {
787
- ret.holderURL.push(flag);
788
- }
789
- }
790
- ret.holderURL.unshift(options.domain);
791
- ret.holderURL = ret.holderURL.join('/');
792
- return render ? ret : false;
793
- }
794
-
795
- /**
796
- * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders)
797
- *
798
- * @private
799
- * @param settings DOM prep settings
800
- */
801
- function prepareDOMElement(prepSettings) {
802
- var mode = prepSettings.mode;
803
- var el = prepSettings.el;
804
- var flags = prepSettings.flags;
805
- var _engineSettings = prepSettings.engineSettings;
806
- var dimensions = flags.dimensions,
807
- theme = flags.theme;
808
- var dimensionsCaption = dimensions.width + 'x' + dimensions.height;
809
- mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode;
810
-
811
- if (flags.text != null) {
812
- theme.text = flags.text;
813
-
814
- //<object> SVG embedding doesn't parse Unicode properly
815
- if (el.nodeName.toLowerCase() === 'object') {
816
- var textLines = theme.text.split('\\n');
817
- for (var k = 0; k < textLines.length; k++) {
818
- textLines[k] = encodeHtmlEntity(textLines[k]);
819
- }
820
- theme.text = textLines.join('\\n');
821
- }
822
- }
823
-
824
- var holderURL = flags.holderURL;
825
- var engineSettings = extend(_engineSettings, null);
826
-
827
- if (flags.font) {
828
- theme.font = flags.font;
829
- //Only run the <canvas> webfont fallback if noFontFallback is false, if the node is not an image, and if canvas is supported
830
- if (!engineSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && engineSettings.renderer === 'svg') {
831
- engineSettings = extend(engineSettings, {
832
- renderer: 'canvas'
833
- });
834
- }
835
- }
836
-
837
- //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas
838
- if (flags.font && engineSettings.renderer == 'canvas') {
839
- engineSettings.reRender = true;
840
- }
841
-
842
- if (mode == 'background') {
843
- if (el.getAttribute('data-background-src') == null) {
844
- setAttr(el, {
845
- 'data-background-src': holderURL
846
- });
847
- }
848
- } else {
849
- setAttr(el, {
850
- 'data-src': holderURL
851
- });
852
- }
853
-
854
- flags.theme = theme;
855
-
856
- //todo consider using all renderSettings in holderData
857
- el.holderData = {
858
- flags: flags,
859
- engineSettings: engineSettings
860
- };
861
-
862
- if (mode == 'image' || mode == 'fluid') {
863
- setAttr(el, {
864
- 'alt': (theme.text ? theme.text + ' [' + dimensionsCaption + ']' : dimensionsCaption)
865
- });
866
- }
867
-
868
- var renderSettings = {
869
- mode: mode,
870
- el: el,
871
- holderSettings: {
872
- dimensions: dimensions,
873
- theme: theme,
874
- flags: flags
875
- },
876
- engineSettings: engineSettings
877
- };
878
-
879
- if (mode == 'image') {
880
- if (engineSettings.renderer == 'html' || !flags.auto) {
881
- el.style.width = dimensions.width + 'px';
882
- el.style.height = dimensions.height + 'px';
883
- }
884
- if (engineSettings.renderer == 'html') {
885
- el.style.backgroundColor = theme.background;
886
- } else {
887
- render(renderSettings);
888
-
889
- if (flags.textmode == 'exact') {
890
- el.holderData.resizeUpdate = true;
891
- App.vars.resizableImages.push(el);
892
- updateResizableElements(el);
893
- }
894
- }
895
- } else if (mode == 'background' && engineSettings.renderer != 'html') {
896
- render(renderSettings);
897
- } else if (mode == 'fluid') {
898
- el.holderData.resizeUpdate = true;
899
-
900
- if (dimensions.height.slice(-1) == '%') {
901
- el.style.height = dimensions.height;
902
- } else if (flags.auto == null || !flags.auto) {
903
- el.style.height = dimensions.height + 'px';
904
- }
905
- if (dimensions.width.slice(-1) == '%') {
906
- el.style.width = dimensions.width;
907
- } else if (flags.auto == null || !flags.auto) {
908
- el.style.width = dimensions.width + 'px';
909
- }
910
- if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') {
911
- el.style.display = 'block';
912
- }
913
-
914
- setInitialDimensions(el);
915
-
916
- if (engineSettings.renderer == 'html') {
917
- el.style.backgroundColor = theme.background;
918
- } else {
919
- App.vars.resizableImages.push(el);
920
- updateResizableElements(el);
921
- }
922
- }
923
- }
924
-
925
- /**
926
- * Core function that takes output from renderers and sets it as the source or background-image of the target element
927
- *
928
- * @private
929
- * @param renderSettings Renderer settings
930
- */
931
- function render(renderSettings) {
932
- var image = null;
933
- var mode = renderSettings.mode;
934
- var holderSettings = renderSettings.holderSettings;
935
- var el = renderSettings.el;
936
- var engineSettings = renderSettings.engineSettings;
937
-
938
- switch (engineSettings.renderer) {
939
- case 'svg':
940
- if (!App.setup.supportsSVG) return;
941
- break;
942
- case 'canvas':
943
- if (!App.setup.supportsCanvas) return;
944
- break;
945
- default:
946
- return;
947
- }
948
-
949
- //todo: move generation of scene up to flag generation to reduce extra object creation
950
- var scene = {
951
- width: holderSettings.dimensions.width,
952
- height: holderSettings.dimensions.height,
953
- theme: holderSettings.theme,
954
- flags: holderSettings.flags
955
- };
956
-
957
- var sceneGraph = buildSceneGraph(scene);
958
-
959
- function getRenderedImage() {
960
- var image = null;
961
- switch (engineSettings.renderer) {
962
- case 'canvas':
963
- image = sgCanvasRenderer(sceneGraph, renderSettings);
964
- break;
965
- case 'svg':
966
- image = sgSVGRenderer(sceneGraph, renderSettings);
967
- break;
968
- default:
969
- throw 'Holder: invalid renderer: ' + engineSettings.renderer;
970
- }
971
- return image;
972
- }
973
-
974
- image = getRenderedImage();
975
-
976
- if (image == null) {
977
- throw 'Holder: couldn\'t render placeholder';
978
- }
979
-
980
- //todo: add <object> canvas rendering
981
- if (mode == 'background') {
982
- el.style.backgroundImage = 'url(' + image + ')';
983
- el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px';
984
- } else {
985
- if (el.nodeName.toLowerCase() === 'img') {
986
- setAttr(el, {
987
- 'src': image
988
- });
989
- } else if (el.nodeName.toLowerCase() === 'object') {
990
- setAttr(el, {
991
- 'data': image
992
- });
993
- setAttr(el, {
994
- 'type': 'image/svg+xml'
995
- });
996
- }
997
- if (engineSettings.reRender) {
998
- global.setTimeout(function() {
999
- var image = getRenderedImage();
1000
- if (image == null) {
1001
- throw 'Holder: couldn\'t render placeholder';
1002
- }
1003
- if (el.nodeName.toLowerCase() === 'img') {
1004
- setAttr(el, {
1005
- 'src': image
1006
- });
1007
- } else if (el.nodeName.toLowerCase() === 'object') {
1008
- setAttr(el, {
1009
- 'data': image
1010
- });
1011
- setAttr(el, {
1012
- 'type': 'image/svg+xml'
1013
- });
1014
- }
1015
- }, 100);
1016
- }
1017
- }
1018
- setAttr(el, {
1019
- 'data-holder-rendered': true
1020
- });
1021
- }
1022
-
1023
- /**
1024
- * Core function that takes a Holder scene description and builds a scene graph
1025
- *
1026
- * @private
1027
- * @param scene Holder scene object
1028
- */
1029
- function buildSceneGraph(scene) {
1030
- var fontSize = App.defaults.size;
1031
- if (parseFloat(scene.theme.size)) {
1032
- fontSize = scene.theme.size;
1033
- } else if (parseFloat(scene.flags.size)) {
1034
- fontSize = scene.flags.size;
1035
- }
1036
-
1037
- scene.font = {
1038
- family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif',
1039
- size: textSize(scene.width, scene.height, fontSize),
1040
- units: scene.theme.units ? scene.theme.units : App.defaults.units,
1041
- weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold'
1042
- };
1043
- scene.text = scene.theme.text ? scene.theme.text : Math.floor(scene.width) + 'x' + Math.floor(scene.height);
1044
-
1045
- switch (scene.flags.textmode) {
1046
- case 'literal':
1047
- scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height;
1048
- break;
1049
- case 'exact':
1050
- if (!scene.flags.exactDimensions) break;
1051
- scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height);
1052
- break;
1053
- }
1054
-
1055
- var sceneGraph = new SceneGraph({
1056
- width: scene.width,
1057
- height: scene.height
1058
- });
1059
-
1060
- var Shape = sceneGraph.Shape;
1061
-
1062
- var holderBg = new Shape.Rect('holderBg', {
1063
- fill: scene.theme.background
1064
- });
1065
-
1066
- holderBg.resize(scene.width, scene.height);
1067
- sceneGraph.root.add(holderBg);
1068
-
1069
- var holderTextGroup = new Shape.Group('holderTextGroup', {
1070
- text: scene.text,
1071
- align: 'center',
1072
- font: scene.font,
1073
- fill: scene.theme.foreground
1074
- });
1075
-
1076
- holderTextGroup.moveTo(null, null, 1);
1077
- sceneGraph.root.add(holderTextGroup);
1078
-
1079
- var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph);
1080
- if (!tpdata) {
1081
- throw 'Holder: staging fallback not supported yet.';
1082
- }
1083
- holderTextGroup.properties.leading = tpdata.boundingBox.height;
1084
-
1085
- //todo: alignment: TL, TC, TR, CL, CR, BL, BC, BR
1086
- var textNode = null;
1087
- var line = null;
1088
-
1089
- function finalizeLine(parent, line, width, height) {
1090
- line.width = width;
1091
- line.height = height;
1092
- parent.width = Math.max(parent.width, line.width);
1093
- parent.height += line.height;
1094
- parent.add(line);
1095
- }
1096
-
1097
- if (tpdata.lineCount > 1) {
1098
- var offsetX = 0;
1099
- var offsetY = 0;
1100
- var maxLineWidth = scene.width * App.setup.lineWrapRatio;
1101
- var lineIndex = 0;
1102
- line = new Shape.Group('line' + lineIndex);
1103
-
1104
- for (var i = 0; i < tpdata.words.length; i++) {
1105
- var word = tpdata.words[i];
1106
- textNode = new Shape.Text(word.text);
1107
- var newline = word.text == '\\n';
1108
- if (offsetX + word.width >= maxLineWidth || newline === true) {
1109
- finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
1110
- offsetX = 0;
1111
- offsetY += holderTextGroup.properties.leading;
1112
- lineIndex += 1;
1113
- line = new Shape.Group('line' + lineIndex);
1114
- line.y = offsetY;
1115
- }
1116
- if (newline === true) {
1117
- continue;
1118
- }
1119
- textNode.moveTo(offsetX, 0);
1120
- offsetX += tpdata.spaceWidth + word.width;
1121
- line.add(textNode);
1122
- }
1123
-
1124
- finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
1125
-
1126
- for (var lineKey in holderTextGroup.children) {
1127
- line = holderTextGroup.children[lineKey];
1128
- line.moveTo(
1129
- (holderTextGroup.width - line.width) / 2,
1130
- null,
1131
- null);
1132
- }
1133
-
1134
- holderTextGroup.moveTo(
1135
- (scene.width - holderTextGroup.width) / 2,
1136
- (scene.height - holderTextGroup.height) / 2,
1137
- null);
1138
-
1139
- //If the text exceeds vertical space, move it down so the first line is visible
1140
- if ((scene.height - holderTextGroup.height) / 2 < 0) {
1141
- holderTextGroup.moveTo(null, 0, null);
1142
- }
1143
- } else {
1144
- textNode = new Shape.Text(scene.text);
1145
- line = new Shape.Group('line0');
1146
- line.add(textNode);
1147
- holderTextGroup.add(line);
1148
-
1149
- holderTextGroup.moveTo(
1150
- (scene.width - tpdata.boundingBox.width) / 2,
1151
- (scene.height - tpdata.boundingBox.height) / 2,
1152
- null);
1153
- }
1154
-
1155
- //todo: renderlist
1156
-
1157
- return sceneGraph;
1158
- }
1159
-
1160
- /**
1161
- * Adaptive text sizing function
1162
- *
1163
- * @private
1164
- * @param width Parent width
1165
- * @param height Parent height
1166
- * @param fontSize Requested text size
1167
- */
1168
- function textSize(width, height, fontSize) {
1169
- var stageWidth = parseInt(width, 10);
1170
- var stageHeight = parseInt(height, 10);
1171
-
1172
- var bigSide = Math.max(stageWidth, stageHeight);
1173
- var smallSide = Math.min(stageWidth, stageHeight);
1174
-
1175
- var newHeight = 0.8 * Math.min(smallSide, bigSide * App.defaults.scale);
1176
- return Math.round(Math.max(fontSize, newHeight));
1177
- }
1178
-
1179
- /**
1180
- * Iterates over resizable (fluid or auto) placeholders and renders them
1181
- *
1182
- * @private
1183
- * @param element Optional element selector, specified only if a specific element needs to be re-rendered
1184
- */
1185
- function updateResizableElements(element) {
1186
- var images;
1187
- if (element == null || element.nodeType == null) {
1188
- images = App.vars.resizableImages;
1189
- } else {
1190
- images = [element];
1191
- }
1192
- for (var i = 0, l = images.length; i < l; i++) {
1193
- var el = images[i];
1194
- if (el.holderData) {
1195
- var flags = el.holderData.flags;
1196
- var dimensions = dimensionCheck(el);
1197
- if (dimensions) {
1198
- if (!el.holderData.resizeUpdate) {
1199
- continue;
1200
- }
1201
-
1202
- if (flags.fluid && flags.auto) {
1203
- var fluidConfig = el.holderData.fluidConfig;
1204
- switch (fluidConfig.mode) {
1205
- case 'width':
1206
- dimensions.height = dimensions.width / fluidConfig.ratio;
1207
- break;
1208
- case 'height':
1209
- dimensions.width = dimensions.height * fluidConfig.ratio;
1210
- break;
1211
- }
1212
- }
1213
-
1214
- var settings = {
1215
- mode: 'image',
1216
- holderSettings: {
1217
- dimensions: dimensions,
1218
- theme: flags.theme,
1219
- flags: flags
1220
- },
1221
- el: el,
1222
- engineSettings: el.holderData.engineSettings
1223
- };
1224
-
1225
- if (flags.textmode == 'exact') {
1226
- flags.exactDimensions = dimensions;
1227
- settings.holderSettings.dimensions = flags.dimensions;
1228
- }
1229
-
1230
- render(settings);
1231
- } else {
1232
- setInvisible(el);
1233
- }
1234
- }
1235
- }
1236
- }
1237
-
1238
- /**
1239
- * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing
1240
- *
1241
- * @private
1242
- * @param el Image DOM element
1243
- */
1244
- function setInitialDimensions(el) {
1245
- if (el.holderData) {
1246
- var dimensions = dimensionCheck(el);
1247
- if (dimensions) {
1248
- var flags = el.holderData.flags;
1249
-
1250
- var fluidConfig = {
1251
- fluidHeight: flags.dimensions.height.slice(-1) == '%',
1252
- fluidWidth: flags.dimensions.width.slice(-1) == '%',
1253
- mode: null,
1254
- initialDimensions: dimensions
1255
- };
1256
-
1257
- if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) {
1258
- fluidConfig.mode = 'width';
1259
- fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height);
1260
- } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) {
1261
- fluidConfig.mode = 'height';
1262
- fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height;
1263
- }
1264
-
1265
- el.holderData.fluidConfig = fluidConfig;
1266
- } else {
1267
- setInvisible(el);
1268
- }
1269
- }
1270
- }
1271
-
1272
- /**
1273
- * Returns an element's dimensions if it's visible, `false` otherwise.
1274
- *
1275
- * @private
1276
- * @param el DOM element
1277
- */
1278
- function dimensionCheck(el) {
1279
- var dimensions = {
1280
- height: el.clientHeight,
1281
- width: el.clientWidth
1282
- };
1283
-
1284
- if (dimensions.height && dimensions.width) {
1285
- return dimensions;
1286
- }
1287
- else{
1288
- return false;
1289
- }
1290
- }
1291
-
1292
- /**
1293
- * Iterates through all current invisible images, and if they're visible, renders them and removes them from further checks. Runs every animation frame.
1294
- *
1295
- * @private
1296
- */
1297
- function visibilityCheck() {
1298
- var renderableImages = [];
1299
- var keys = Object.keys(App.vars.invisibleImages);
1300
- var el;
1301
- for (var i = 0, l = keys.length; i < l; i++) {
1302
- el = App.vars.invisibleImages[keys[i]];
1303
- if (dimensionCheck(el) && el.nodeName.toLowerCase() == 'img') {
1304
- renderableImages.push(el);
1305
- delete App.vars.invisibleImages[keys[i]];
1306
- }
1307
- }
1308
-
1309
- if (renderableImages.length) {
1310
- Holder.run({
1311
- images: renderableImages
1312
- });
1313
- }
1314
-
1315
- global.requestAnimationFrame(visibilityCheck);
1316
- }
1317
-
1318
- /**
1319
- * Starts checking for invisible placeholders if not doing so yet. Does nothing otherwise.
1320
- *
1321
- * @private
1322
- */
1323
- function startVisibilityCheck() {
1324
- if (!App.vars.visibilityCheckStarted) {
1325
- global.requestAnimationFrame(visibilityCheck);
1326
- App.vars.visibilityCheckStarted = true;
1327
- }
1328
- }
1329
-
1330
- /**
1331
- * Sets a unique ID for an image detected to be invisible and adds it to the map of invisible images checked by visibilityCheck
1332
- *
1333
- * @private
1334
- * @param el Invisible DOM element
1335
- */
1336
- function setInvisible(el) {
1337
- if (!el.holderData.invisibleId) {
1338
- App.vars.invisibleId += 1;
1339
- App.vars.invisibleImages['i' + App.vars.invisibleId] = el;
1340
- el.holderData.invisibleId = App.vars.invisibleId;
1341
- }
1342
- }
1343
-
1344
- //todo: see if possible to convert stagingRenderer to use HTML only
1345
- var stagingRenderer = (function() {
1346
- var svg = null,
1347
- stagingText = null,
1348
- stagingTextNode = null;
1349
- return function(graph) {
1350
- var rootNode = graph.root;
1351
- if (App.setup.supportsSVG) {
1352
- var firstTimeSetup = false;
1353
- var tnode = function(text) {
1354
- return document.createTextNode(text);
1355
- };
1356
- if (svg == null || svg.parentNode !== document.body) {
1357
- firstTimeSetup = true;
1358
- }
1359
-
1360
- svg = initSVG(svg, rootNode.properties.width, rootNode.properties.height);
1361
- //Show staging element before staging
1362
- svg.style.display = 'block';
1363
-
1364
- if (firstTimeSetup) {
1365
- stagingText = newEl('text', SVG_NS);
1366
- stagingTextNode = tnode(null);
1367
- setAttr(stagingText, {
1368
- x: 0
1369
- });
1370
- stagingText.appendChild(stagingTextNode);
1371
- svg.appendChild(stagingText);
1372
- document.body.appendChild(svg);
1373
- svg.style.visibility = 'hidden';
1374
- svg.style.position = 'absolute';
1375
- svg.style.top = '-100%';
1376
- svg.style.left = '-100%';
1377
- //todo: workaround for zero-dimension <svg> tag in Opera 12
1378
- //svg.setAttribute('width', 0);
1379
- //svg.setAttribute('height', 0);
1380
- }
1381
-
1382
- var holderTextGroup = rootNode.children.holderTextGroup;
1383
- var htgProps = holderTextGroup.properties;
1384
- setAttr(stagingText, {
1385
- 'y': htgProps.font.size,
1386
- 'style': cssProps({
1387
- 'font-weight': htgProps.font.weight,
1388
- 'font-size': htgProps.font.size + htgProps.font.units,
1389
- 'font-family': htgProps.font.family
1390
- })
1391
- });
1392
-
1393
- //Get bounding box for the whole string (total width and height)
1394
- stagingTextNode.nodeValue = htgProps.text;
1395
- var stagingTextBBox = stagingText.getBBox();
1396
-
1397
- //Get line count and split the string into words
1398
- var lineCount = Math.ceil(stagingTextBBox.width / (rootNode.properties.width * App.setup.lineWrapRatio));
1399
- var words = htgProps.text.split(' ');
1400
- var newlines = htgProps.text.match(/\\n/g);
1401
- lineCount += newlines == null ? 0 : newlines.length;
1402
-
1403
- //Get bounding box for the string with spaces removed
1404
- stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, '');
1405
- var computedNoSpaceLength = stagingText.getComputedTextLength();
1406
-
1407
- //Compute average space width
1408
- var diffLength = stagingTextBBox.width - computedNoSpaceLength;
1409
- var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1));
1410
-
1411
- //Get widths for every word with space only if there is more than one line
1412
- var wordWidths = [];
1413
- if (lineCount > 1) {
1414
- stagingTextNode.nodeValue = '';
1415
- for (var i = 0; i < words.length; i++) {
1416
- if (words[i].length === 0) continue;
1417
- stagingTextNode.nodeValue = decodeHtmlEntity(words[i]);
1418
- var bbox = stagingText.getBBox();
1419
- wordWidths.push({
1420
- text: words[i],
1421
- width: bbox.width
1422
- });
1423
- }
1424
- }
1425
-
1426
- //Hide staging element after staging
1427
- svg.style.display = 'none';
1428
-
1429
- return {
1430
- spaceWidth: spaceWidth,
1431
- lineCount: lineCount,
1432
- boundingBox: stagingTextBBox,
1433
- words: wordWidths
1434
- };
1435
- } else {
1436
- //todo: canvas fallback for measuring text on android 2.3
1437
- return false;
1438
- }
1439
- };
1440
- })();
1441
-
1442
- var sgCanvasRenderer = (function() {
1443
- var canvas = newEl('canvas');
1444
- var ctx = null;
1445
-
1446
- return function(sceneGraph) {
1447
- if (ctx == null) {
1448
- ctx = canvas.getContext('2d');
1449
- }
1450
- var root = sceneGraph.root;
1451
- canvas.width = App.dpr(root.properties.width);
1452
- canvas.height = App.dpr(root.properties.height);
1453
- ctx.textBaseline = 'middle';
1454
-
1455
- ctx.fillStyle = root.children.holderBg.properties.fill;
1456
- ctx.fillRect(0, 0, App.dpr(root.children.holderBg.width), App.dpr(root.children.holderBg.height));
1457
-
1458
- var textGroup = root.children.holderTextGroup;
1459
- var tgProps = textGroup.properties;
1460
- ctx.font = textGroup.properties.font.weight + ' ' + App.dpr(textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace';
1461
- ctx.fillStyle = textGroup.properties.fill;
1462
-
1463
- for (var lineKey in textGroup.children) {
1464
- var line = textGroup.children[lineKey];
1465
- for (var wordKey in line.children) {
1466
- var word = line.children[wordKey];
1467
- var x = App.dpr(textGroup.x + line.x + word.x);
1468
- var y = App.dpr(textGroup.y + line.y + word.y + (textGroup.properties.leading / 2));
1469
-
1470
- ctx.fillText(word.properties.text, x, y);
1471
- }
1472
- }
1473
-
1474
- return canvas.toDataURL('image/png');
1475
- };
1476
- })();
1477
-
1478
- var sgSVGRenderer = (function() {
1479
- //Prevent IE <9 from initializing SVG renderer
1480
- if (!global.XMLSerializer) return;
1481
- var svg = initSVG(null, 0, 0);
1482
- var bgEl = newEl('rect', SVG_NS);
1483
- svg.appendChild(bgEl);
1484
-
1485
- //todo: create a reusable pool for textNodes, resize if more words present
1486
-
1487
- return function(sceneGraph, renderSettings) {
1488
- var root = sceneGraph.root;
1489
-
1490
- var holderURL = renderSettings.holderSettings.flags.holderURL;
1491
- var commentNode = document.createComment('\n' + 'Source URL: ' + holderURL + generatorComment);
1492
-
1493
- initSVG(svg, root.properties.width, root.properties.height);
1494
- svg.insertBefore(commentNode, svg.firstChild);
1495
-
1496
- var groups = svg.querySelectorAll('g');
1497
-
1498
- for (var i = 0; i < groups.length; i++) {
1499
- groups[i].parentNode.removeChild(groups[i]);
1500
- }
1501
-
1502
- setAttr(bgEl, {
1503
- 'width': root.children.holderBg.width,
1504
- 'height': root.children.holderBg.height,
1505
- 'fill': root.children.holderBg.properties.fill
1506
- });
1507
-
1508
- var textGroup = root.children.holderTextGroup;
1509
- var tgProps = textGroup.properties;
1510
- var textGroupEl = newEl('g', SVG_NS);
1511
- var tpdata = textGroup.textPositionData;
1512
- svg.appendChild(textGroupEl);
1513
-
1514
- textGroup.y += tpdata.boundingBox.height * 0.8;
1515
-
1516
- for (var lineKey in textGroup.children) {
1517
- var line = textGroup.children[lineKey];
1518
- for (var wordKey in line.children) {
1519
- var word = line.children[wordKey];
1520
- var x = textGroup.x + line.x + word.x;
1521
- var y = textGroup.y + line.y + word.y;
1522
-
1523
- var textEl = newEl('text', SVG_NS);
1524
- var textNode = document.createTextNode(null);
1525
-
1526
- setAttr(textEl, {
1527
- 'x': x,
1528
- 'y': y,
1529
- 'style': cssProps({
1530
- 'fill': tgProps.fill,
1531
- 'font-weight': tgProps.font.weight,
1532
- 'font-family': tgProps.font.family + ', monospace',
1533
- 'font-size': tgProps.font.size + tgProps.font.units
1534
- })
1535
- });
1536
-
1537
- textNode.nodeValue = word.properties.text;
1538
- textEl.appendChild(textNode);
1539
- textGroupEl.appendChild(textEl);
1540
- }
1541
- }
1542
-
1543
- var svgString = 'data:image/svg+xml;base64,' +
1544
- btoa(unescape(encodeURIComponent(serializeSVG(svg, renderSettings.engineSettings))));
1545
- return svgString;
1546
- };
1547
- })();
1548
-
1549
- //Helpers
1550
-
1551
- /**
1552
- * Generic new DOM element function
1553
- *
1554
- * @private
1555
- * @param tag Tag to create
1556
- * @param namespace Optional namespace value
1557
- */
1558
- function newEl(tag, namespace) {
1559
- if (namespace == null) {
1560
- return document.createElement(tag);
1561
- } else {
1562
- return document.createElementNS(namespace, tag);
1563
- }
1564
- }
1565
-
1566
- /**
1567
- * Generic setAttribute function
1568
- *
1569
- * @private
1570
- * @param el Reference to DOM element
1571
- * @param attrs Object with attribute keys and values
1572
- */
1573
- function setAttr(el, attrs) {
1574
- for (var a in attrs) {
1575
- el.setAttribute(a, attrs[a]);
1576
- }
1577
- }
1578
-
1579
- /**
1580
- * Generic SVG element creation function
1581
- *
1582
- * @private
1583
- * @param svg SVG context, set to null if new
1584
- * @param width Document width
1585
- * @param height Document height
1586
- */
1587
- function initSVG(svg, width, height) {
1588
- if (svg == null) {
1589
- svg = newEl('svg', SVG_NS);
1590
- var defs = newEl('defs', SVG_NS);
1591
- svg.appendChild(defs);
1592
- }
1593
- //IE throws an exception if this is set and Chrome requires it to be set
1594
- if (svg.webkitMatchesSelector) {
1595
- svg.setAttribute('xmlns', SVG_NS);
1596
- }
1597
- //Remove comment nodes
1598
- for (var i = 0; i < svg.childNodes.length; i++) {
1599
- if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) {
1600
- svg.removeChild(svg.childNodes[i]);
1601
- }
1602
- }
1603
-
1604
- setAttr(svg, {
1605
- 'width': width,
1606
- 'height': height,
1607
- 'viewBox': '0 0 ' + width + ' ' + height,
1608
- 'preserveAspectRatio': 'none'
1609
- });
1610
- return svg;
1611
- }
1612
-
1613
- /**
1614
- * Generic SVG serialization function
1615
- *
1616
- * @private
1617
- * @param svg SVG context
1618
- * @param stylesheets CSS stylesheets to include
1619
- */
1620
- function serializeSVG(svg, engineSettings) {
1621
- if (!global.XMLSerializer) return;
1622
- var serializer = new XMLSerializer();
1623
- var svgCSS = '';
1624
- var stylesheets = engineSettings.stylesheets;
1625
- var defs = svg.querySelector('defs');
1626
-
1627
- //External stylesheets: Processing Instruction method
1628
- if (engineSettings.svgXMLStylesheet) {
1629
- var xml = new DOMParser().parseFromString('<xml />', 'application/xml');
1630
- //Add <?xml-stylesheet ?> directives
1631
- for (var i = stylesheets.length - 1; i >= 0; i--) {
1632
- var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"');
1633
- xml.insertBefore(csspi, xml.firstChild);
1634
- }
1635
-
1636
- //Add <?xml ... ?> UTF-8 directive
1637
- var xmlpi = xml.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8" standalone="yes"');
1638
- xml.insertBefore(xmlpi, xml.firstChild);
1639
- xml.removeChild(xml.documentElement);
1640
- svgCSS = serializer.serializeToString(xml);
1641
- }
1642
-
1643
- /*
1644
-
1645
- //External stylesheets: <link> method
1646
- if (renderSettings.svgLinkStylesheet) {
1647
-
1648
- defs.removeChild(defs.firstChild);
1649
- for (i = 0; i < stylesheets.length; i++) {
1650
- var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'link');
1651
- link.setAttribute('href', stylesheets[i]);
1652
- link.setAttribute('rel', 'stylesheet');
1653
- link.setAttribute('type', 'text/css');
1654
- defs.appendChild(link);
1655
- }
1656
- }
1657
-
1658
- //External stylesheets: <style> and @import method
1659
- if (renderSettings.svgImportStylesheet) {
1660
- var style = document.createElementNS(SVG_NS, 'style');
1661
- var styleText = [];
1662
-
1663
- for (i = 0; i < stylesheets.length; i++) {
1664
- styleText.push('@import url(' + stylesheets[i] + ');');
1665
- }
1666
-
1667
- var styleTextNode = document.createTextNode(styleText.join('\n'));
1668
- style.appendChild(styleTextNode);
1669
- defs.appendChild(style);
1670
- }
1671
-
1672
- */
1673
-
1674
- var svgText = serializer.serializeToString(svg);
1675
- svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1');
1676
- return svgCSS + svgText;
1677
- }
1678
-
1679
- /**
1680
- * Shallow object clone and merge
1681
- *
1682
- * @param a Object A
1683
- * @param b Object B
1684
- * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties
1685
- */
1686
- function extend(a, b) {
1687
- var c = {};
1688
- for (var x in a) {
1689
- if (a.hasOwnProperty(x)) {
1690
- c[x] = a[x];
1691
- }
1692
- }
1693
- if (b != null) {
1694
- for (var y in b) {
1695
- if (b.hasOwnProperty(y)) {
1696
- c[y] = b[y];
1697
- }
1698
- }
1699
- }
1700
- return c;
1701
- }
1702
-
1703
- /**
1704
- * Takes a k/v list of CSS properties and returns a rule
1705
- *
1706
- * @param props CSS properties object
1707
- */
1708
- function cssProps(props) {
1709
- var ret = [];
1710
- for (var p in props) {
1711
- if (props.hasOwnProperty(p)) {
1712
- ret.push(p + ':' + props[p]);
1713
- }
1714
- }
1715
- return ret.join(';');
1716
- }
1717
-
1718
- /**
1719
- * Prevents a function from being called too often, waits until a timer elapses to call it again
1720
- *
1721
- * @param fn Function to call
1722
- */
1723
- function debounce(fn) {
1724
- if (!App.vars.debounceTimer) fn.call(this);
1725
- if (App.vars.debounceTimer) global.clearTimeout(App.vars.debounceTimer);
1726
- App.vars.debounceTimer = global.setTimeout(function() {
1727
- App.vars.debounceTimer = null;
1728
- fn.call(this);
1729
- }, App.setup.debounce);
1730
- }
1731
-
1732
- /**
1733
- * Holder-specific resize/orientation change callback, debounced to prevent excessive execution
1734
- */
1735
- function resizeEvent() {
1736
- debounce(function() {
1737
- updateResizableElements(null);
1738
- });
1739
- }
1740
-
1741
- /**
1742
- * Converts a value into an array of DOM nodes
1743
- *
1744
- * @param val A string, a NodeList, a Node, or an HTMLCollection
1745
- */
1746
- function getNodeArray(val) {
1747
- var retval = null;
1748
- if (typeof(val) == 'string') {
1749
- retval = document.querySelectorAll(val);
1750
- } else if (global.NodeList && val instanceof global.NodeList) {
1751
- retval = val;
1752
- } else if (global.Node && val instanceof global.Node) {
1753
- retval = [val];
1754
- } else if (global.HTMLCollection && val instanceof global.HTMLCollection) {
1755
- retval = val;
1756
- } else if (val instanceof Array) {
1757
- retval = val;
1758
- } else if (val === null) {
1759
- retval = [];
1760
- }
1761
- return retval;
1762
- }
1763
-
1764
- /**
1765
- * Checks if an image exists
1766
- *
1767
- * @param src URL of image
1768
- * @param callback Callback to call once image status has been found
1769
- */
1770
- function imageExists(src, callback) {
1771
- var image = new Image();
1772
- image.onerror = function() {
1773
- callback.call(this, false);
1774
- };
1775
- image.onload = function() {
1776
- callback.call(this, true);
1777
- };
1778
- image.src = src;
1779
- }
1780
-
1781
- /**
1782
- * Encodes HTML entities in a string
1783
- *
1784
- * @param str Input string
1785
- */
1786
- function encodeHtmlEntity(str) {
1787
- var buf = [];
1788
- var charCode = 0;
1789
- for (var i = str.length - 1; i >= 0; i--) {
1790
- charCode = str.charCodeAt(i);
1791
- if (charCode > 128) {
1792
- buf.unshift(['&#', charCode, ';'].join(''));
1793
- } else {
1794
- buf.unshift(str[i]);
1795
- }
1796
- }
1797
- return buf.join('');
1798
- }
1799
-
1800
- /**
1801
- * Decodes HTML entities in a stirng
1802
- *
1803
- * @param str Input string
1804
- */
1805
- function decodeHtmlEntity(str) {
1806
- return str.replace(/&#(\d+);/g, function(match, dec) {
1807
- return String.fromCharCode(dec);
1808
- });
1809
- }
1810
-
1811
- // Scene graph
1812
-
1813
- var SceneGraph = function(sceneProperties) {
1814
- var nodeCount = 1;
1815
-
1816
- //todo: move merge to helpers section
1817
- function merge(parent, child) {
1818
- for (var prop in child) {
1819
- parent[prop] = child[prop];
1820
- }
1821
- return parent;
1822
- }
1823
-
1824
- var SceneNode = augment.defclass({
1825
- constructor: function(name) {
1826
- nodeCount++;
1827
- this.parent = null;
1828
- this.children = {};
1829
- this.id = nodeCount;
1830
- this.name = 'n' + nodeCount;
1831
- if (name != null) {
1832
- this.name = name;
1833
- }
1834
- this.x = 0;
1835
- this.y = 0;
1836
- this.z = 0;
1837
- this.width = 0;
1838
- this.height = 0;
1839
- },
1840
- resize: function(width, height) {
1841
- if (width != null) {
1842
- this.width = width;
1843
- }
1844
- if (height != null) {
1845
- this.height = height;
1846
- }
1847
- },
1848
- moveTo: function(x, y, z) {
1849
- this.x = x != null ? x : this.x;
1850
- this.y = y != null ? y : this.y;
1851
- this.z = z != null ? z : this.z;
1852
- },
1853
- add: function(child) {
1854
- var name = child.name;
1855
- if (this.children[name] == null) {
1856
- this.children[name] = child;
1857
- child.parent = this;
1858
- } else {
1859
- throw 'SceneGraph: child with that name already exists: ' + name;
1860
- }
1861
- }
1862
- /*, // probably unnecessary in Holder
1863
- remove: function(name){
1864
- if(this.children[name] == null){
1865
- throw 'SceneGraph: child with that name doesn\'t exist: '+name;
1866
- }
1867
- else{
1868
- child.parent = null;
1869
- delete this.children[name];
1870
- }
1871
- },
1872
- removeAll: function(){
1873
- for(var child in this.children){
1874
- this.remove(child);
1875
- }
1876
- }*/
1877
- });
1878
-
1879
- var RootNode = augment(SceneNode, function(uber) {
1880
- this.constructor = function() {
1881
- uber.constructor.call(this, 'root');
1882
- this.properties = sceneProperties;
1883
- };
1884
- });
1885
-
1886
- var Shape = augment(SceneNode, function(uber) {
1887
- function constructor(name, props) {
1888
- uber.constructor.call(this, name);
1889
- this.properties = {
1890
- fill: '#000'
1891
- };
1892
- if (props != null) {
1893
- merge(this.properties, props);
1894
- } else if (name != null && typeof name !== 'string') {
1895
- throw 'SceneGraph: invalid node name';
1896
- }
1897
- }
1898
-
1899
- this.Group = augment.extend(this, {
1900
- constructor: constructor,
1901
- type: 'group'
1902
- });
1903
-
1904
- this.Rect = augment.extend(this, {
1905
- constructor: constructor,
1906
- type: 'rect'
1907
- });
1908
-
1909
- this.Text = augment.extend(this, {
1910
- constructor: function(text) {
1911
- constructor.call(this);
1912
- this.properties.text = text;
1913
- },
1914
- type: 'text'
1915
- });
1916
- });
1917
-
1918
- var root = new RootNode();
1919
-
1920
- this.Shape = Shape;
1921
- this.root = root;
1922
-
1923
- return this;
1924
- };
1925
-
1926
- //Set up flags
1927
-
1928
- for (var flag in App.flags) {
1929
- if (!App.flags.hasOwnProperty(flag)) continue;
1930
- App.flags[flag].match = function(val) {
1931
- return val.match(this.regex);
1932
- };
1933
- }
1934
-
1935
- //Properties set once on setup
1936
-
1937
- App.setup = {
1938
- renderer: 'html',
1939
- debounce: 100,
1940
- ratio: 1,
1941
- supportsCanvas: false,
1942
- supportsSVG: false,
1943
- lineWrapRatio: 0.9,
1944
- renderers: ['html', 'canvas', 'svg']
1945
- };
1946
-
1947
- App.dpr = function(val) {
1948
- return val * App.setup.ratio;
1949
- };
1950
-
1951
- //Properties modified during runtime
1952
-
1953
- App.vars = {
1954
- preempted: false,
1955
- resizableImages: [],
1956
- invisibleImages: {},
1957
- invisibleId: 0,
1958
- visibilityCheckStarted: false,
1959
- debounceTimer: null,
1960
- cache: {}
1961
- };
1962
-
1963
- //Pre-flight
1964
-
1965
- (function() {
1966
- var devicePixelRatio = 1,
1967
- backingStoreRatio = 1;
1968
-
1969
- var canvas = newEl('canvas');
1970
- var ctx = null;
1971
-
1972
- if (canvas.getContext) {
1973
- if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) {
1974
- App.setup.renderer = 'canvas';
1975
- ctx = canvas.getContext('2d');
1976
- App.setup.supportsCanvas = true;
1977
- }
1978
- }
1979
-
1980
- if (App.setup.supportsCanvas) {
1981
- devicePixelRatio = global.devicePixelRatio || 1;
1982
- backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
1983
- }
1984
-
1985
- App.setup.ratio = devicePixelRatio / backingStoreRatio;
1986
-
1987
- if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
1988
- App.setup.renderer = 'svg';
1989
- App.setup.supportsSVG = true;
1990
- }
1991
- })();
1992
-
1993
- //Starts checking for invisible placeholders
1994
- startVisibilityCheck();
1995
-
1996
- //Exposing to environment and setting up listeners
1997
- register(Holder, 'Holder', global);
1998
-
1999
- if (global.onDomReady) {
2000
- global.onDomReady(function() {
2001
- if (!App.vars.preempted) {
2002
- Holder.run();
2003
- }
2004
- if (global.addEventListener) {
2005
- global.addEventListener('resize', resizeEvent, false);
2006
- global.addEventListener('orientationchange', resizeEvent, false);
2007
- } else {
2008
- global.attachEvent('onresize', resizeEvent);
2009
- }
2010
-
2011
- if (typeof global.Turbolinks == 'object') {
2012
- global.document.addEventListener('page:change', function() {
2013
- Holder.run();
2014
- });
2015
- }
2016
- });
2017
- }
2018
-
2019
- })(function(fn, name, global) {
2020
- var isAMD = (typeof define === 'function' && define.amd);
2021
- var isNode = (typeof exports === 'object');
2022
- var isWeb = !isNode;
2023
-
2024
- if (isAMD) {
2025
- define(fn);
2026
- } else {
2027
- //todo: npm/browserify registration
2028
- global[name] = fn;
2029
- }
2030
- }, this);
410
+
411
+ /**
412
+ * Processes a Holder URL and extracts flags
413
+ *
414
+ * @private
415
+ * @param url URL
416
+ * @param options Instance options from Holder.run
417
+ */
418
+ function parseURL(url, options) {
419
+ var ret = {
420
+ theme: extend(App.settings.themes.gray, null),
421
+ stylesheets: options.stylesheets,
422
+ holderURL: []
423
+ };
424
+ var render = false;
425
+ var vtab = String.fromCharCode(11);
426
+ var flags = url.replace(/([^\\])\//g, '$1' + vtab).split(vtab);
427
+ var uriRegex = /%[0-9a-f]{2}/gi;
428
+ for (var fl = flags.length, j = 0; j < fl; j++) {
429
+ var flag = flags[j];
430
+ if (flag.match(uriRegex)) {
431
+ try {
432
+ flag = decodeURIComponent(flag);
433
+ } catch (e) {
434
+ flag = flags[j];
435
+ }
436
+ }
437
+
438
+ var push = false;
439
+
440
+ if (App.flags.dimensions.match(flag)) {
441
+ render = true;
442
+ ret.dimensions = App.flags.dimensions.output(flag);
443
+ push = true;
444
+ } else if (App.flags.fluid.match(flag)) {
445
+ render = true;
446
+ ret.dimensions = App.flags.fluid.output(flag);
447
+ ret.fluid = true;
448
+ push = true;
449
+ } else if (App.flags.textmode.match(flag)) {
450
+ ret.textmode = App.flags.textmode.output(flag);
451
+ push = true;
452
+ } else if (App.flags.colors.match(flag)) {
453
+ var colors = App.flags.colors.output(flag);
454
+ ret.theme = extend(ret.theme, colors);
455
+ //todo: convert implicit theme use to a theme: flag
456
+ push = true;
457
+ } else if (options.themes[flag]) {
458
+ //If a theme is specified, it will override custom colors
459
+ if (options.themes.hasOwnProperty(flag)) {
460
+ ret.theme = extend(options.themes[flag], null);
461
+ }
462
+ push = true;
463
+ } else if (App.flags.font.match(flag)) {
464
+ ret.font = App.flags.font.output(flag);
465
+ push = true;
466
+ } else if (App.flags.auto.match(flag)) {
467
+ ret.auto = true;
468
+ push = true;
469
+ } else if (App.flags.text.match(flag)) {
470
+ ret.text = App.flags.text.output(flag);
471
+ push = true;
472
+ } else if (App.flags.size.match(flag)) {
473
+ ret.size = App.flags.size.output(flag);
474
+ push = true;
475
+ } else if (App.flags.random.match(flag)) {
476
+ if (App.vars.cache.themeKeys == null) {
477
+ App.vars.cache.themeKeys = Object.keys(options.themes);
478
+ }
479
+ var theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length];
480
+ ret.theme = extend(options.themes[theme], null);
481
+ push = true;
482
+ }
483
+
484
+ if (push) {
485
+ ret.holderURL.push(flag);
486
+ }
487
+ }
488
+ ret.holderURL.unshift(options.domain);
489
+ ret.holderURL = ret.holderURL.join('/');
490
+ return render ? ret : false;
491
+ }
492
+
493
+ /**
494
+ * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders)
495
+ *
496
+ * @private
497
+ * @param settings DOM prep settings
498
+ */
499
+ function prepareDOMElement(prepSettings) {
500
+ var mode = prepSettings.mode;
501
+ var el = prepSettings.el;
502
+ var flags = prepSettings.flags;
503
+ var _engineSettings = prepSettings.engineSettings;
504
+ var dimensions = flags.dimensions,
505
+ theme = flags.theme;
506
+ var dimensionsCaption = dimensions.width + 'x' + dimensions.height;
507
+ mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode;
508
+
509
+ if (flags.text != null) {
510
+ theme.text = flags.text;
511
+
512
+ //<object> SVG embedding doesn't parse Unicode properly
513
+ if (el.nodeName.toLowerCase() === 'object') {
514
+ var textLines = theme.text.split('\\n');
515
+ for (var k = 0; k < textLines.length; k++) {
516
+ textLines[k] = encodeHtmlEntity(textLines[k]);
517
+ }
518
+ theme.text = textLines.join('\\n');
519
+ }
520
+ }
521
+
522
+ var holderURL = flags.holderURL;
523
+ var engineSettings = extend(_engineSettings, null);
524
+
525
+ if (flags.font) {
526
+ theme.font = flags.font;
527
+ //Only run the <canvas> webfont fallback if noFontFallback is false, if the node is not an image, and if canvas is supported
528
+ if (!engineSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && engineSettings.renderer === 'svg') {
529
+ engineSettings = extend(engineSettings, {
530
+ renderer: 'canvas'
531
+ });
532
+ }
533
+ }
534
+
535
+ //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas
536
+ if (flags.font && engineSettings.renderer == 'canvas') {
537
+ engineSettings.reRender = true;
538
+ }
539
+
540
+ if (mode == 'background') {
541
+ if (el.getAttribute('data-background-src') == null) {
542
+ setAttr(el, {
543
+ 'data-background-src': holderURL
544
+ });
545
+ }
546
+ } else {
547
+ var domProps = {};
548
+ domProps[App.vars.dataAttr] = holderURL;
549
+ setAttr(el, domProps);
550
+ }
551
+
552
+ flags.theme = theme;
553
+
554
+ //todo consider using all renderSettings in holderData
555
+ el.holderData = {
556
+ flags: flags,
557
+ engineSettings: engineSettings
558
+ };
559
+
560
+ if (mode == 'image' || mode == 'fluid') {
561
+ setAttr(el, {
562
+ 'alt': (theme.text ? theme.text + ' [' + dimensionsCaption + ']' : dimensionsCaption)
563
+ });
564
+ }
565
+
566
+ var renderSettings = {
567
+ mode: mode,
568
+ el: el,
569
+ holderSettings: {
570
+ dimensions: dimensions,
571
+ theme: theme,
572
+ flags: flags
573
+ },
574
+ engineSettings: engineSettings
575
+ };
576
+
577
+ if (mode == 'image') {
578
+ if (engineSettings.renderer == 'html' || !flags.auto) {
579
+ el.style.width = dimensions.width + 'px';
580
+ el.style.height = dimensions.height + 'px';
581
+ }
582
+ if (engineSettings.renderer == 'html') {
583
+ el.style.backgroundColor = theme.background;
584
+ } else {
585
+ render(renderSettings);
586
+
587
+ if (flags.textmode == 'exact') {
588
+ el.holderData.resizeUpdate = true;
589
+ App.vars.resizableImages.push(el);
590
+ updateResizableElements(el);
591
+ }
592
+ }
593
+ } else if (mode == 'background' && engineSettings.renderer != 'html') {
594
+ render(renderSettings);
595
+ } else if (mode == 'fluid') {
596
+ el.holderData.resizeUpdate = true;
597
+
598
+ if (dimensions.height.slice(-1) == '%') {
599
+ el.style.height = dimensions.height;
600
+ } else if (flags.auto == null || !flags.auto) {
601
+ el.style.height = dimensions.height + 'px';
602
+ }
603
+ if (dimensions.width.slice(-1) == '%') {
604
+ el.style.width = dimensions.width;
605
+ } else if (flags.auto == null || !flags.auto) {
606
+ el.style.width = dimensions.width + 'px';
607
+ }
608
+ if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') {
609
+ el.style.display = 'block';
610
+ }
611
+
612
+ setInitialDimensions(el);
613
+
614
+ if (engineSettings.renderer == 'html') {
615
+ el.style.backgroundColor = theme.background;
616
+ } else {
617
+ App.vars.resizableImages.push(el);
618
+ updateResizableElements(el);
619
+ }
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Core function that takes output from renderers and sets it as the source or background-image of the target element
625
+ *
626
+ * @private
627
+ * @param renderSettings Renderer settings
628
+ */
629
+ function render(renderSettings) {
630
+ var image = null;
631
+ var mode = renderSettings.mode;
632
+ var holderSettings = renderSettings.holderSettings;
633
+ var el = renderSettings.el;
634
+ var engineSettings = renderSettings.engineSettings;
635
+
636
+ switch (engineSettings.renderer) {
637
+ case 'svg':
638
+ if (!App.setup.supportsSVG) return;
639
+ break;
640
+ case 'canvas':
641
+ if (!App.setup.supportsCanvas) return;
642
+ break;
643
+ default:
644
+ return;
645
+ }
646
+
647
+ //todo: move generation of scene up to flag generation to reduce extra object creation
648
+ var scene = {
649
+ width: holderSettings.dimensions.width,
650
+ height: holderSettings.dimensions.height,
651
+ theme: holderSettings.theme,
652
+ flags: holderSettings.flags
653
+ };
654
+
655
+ var sceneGraph = buildSceneGraph(scene);
656
+
657
+ function getRenderedImage() {
658
+ var image = null;
659
+ switch (engineSettings.renderer) {
660
+ case 'canvas':
661
+ image = sgCanvasRenderer(sceneGraph, renderSettings);
662
+ break;
663
+ case 'svg':
664
+ image = sgSVGRenderer(sceneGraph, renderSettings);
665
+ break;
666
+ default:
667
+ throw 'Holder: invalid renderer: ' + engineSettings.renderer;
668
+ }
669
+ return image;
670
+ }
671
+
672
+ image = getRenderedImage();
673
+
674
+ if (image == null) {
675
+ throw 'Holder: couldn\'t render placeholder';
676
+ }
677
+
678
+ //todo: add <object> canvas rendering
679
+ if (mode == 'background') {
680
+ el.style.backgroundImage = 'url(' + image + ')';
681
+ el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px';
682
+ } else {
683
+ if (el.nodeName.toLowerCase() === 'img') {
684
+ setAttr(el, {
685
+ 'src': image
686
+ });
687
+ } else if (el.nodeName.toLowerCase() === 'object') {
688
+ setAttr(el, {
689
+ 'data': image
690
+ });
691
+ setAttr(el, {
692
+ 'type': 'image/svg+xml'
693
+ });
694
+ }
695
+ if (engineSettings.reRender) {
696
+ global.setTimeout(function() {
697
+ var image = getRenderedImage();
698
+ if (image == null) {
699
+ throw 'Holder: couldn\'t render placeholder';
700
+ }
701
+ //todo: refactor this code into a function
702
+ if (el.nodeName.toLowerCase() === 'img') {
703
+ setAttr(el, {
704
+ 'src': image
705
+ });
706
+ } else if (el.nodeName.toLowerCase() === 'object') {
707
+ setAttr(el, {
708
+ 'data': image
709
+ });
710
+ setAttr(el, {
711
+ 'type': 'image/svg+xml'
712
+ });
713
+ }
714
+ }, 100);
715
+ }
716
+ }
717
+ //todo: account for re-rendering
718
+ setAttr(el, {
719
+ 'data-holder-rendered': true
720
+ });
721
+ }
722
+
723
+ /**
724
+ * Core function that takes a Holder scene description and builds a scene graph
725
+ *
726
+ * @private
727
+ * @param scene Holder scene object
728
+ */
729
+ function buildSceneGraph(scene) {
730
+ var fontSize = App.defaults.size;
731
+ if (parseFloat(scene.theme.size)) {
732
+ fontSize = scene.theme.size;
733
+ } else if (parseFloat(scene.flags.size)) {
734
+ fontSize = scene.flags.size;
735
+ }
736
+
737
+ scene.font = {
738
+ family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif',
739
+ size: textSize(scene.width, scene.height, fontSize),
740
+ units: scene.theme.units ? scene.theme.units : App.defaults.units,
741
+ weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold'
742
+ };
743
+ scene.text = scene.theme.text ? scene.theme.text : Math.floor(scene.width) + 'x' + Math.floor(scene.height);
744
+
745
+ switch (scene.flags.textmode) {
746
+ case 'literal':
747
+ scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height;
748
+ break;
749
+ case 'exact':
750
+ if (!scene.flags.exactDimensions) break;
751
+ scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height);
752
+ break;
753
+ }
754
+
755
+ var sceneGraph = new SceneGraph({
756
+ width: scene.width,
757
+ height: scene.height
758
+ });
759
+
760
+ var Shape = sceneGraph.Shape;
761
+
762
+ var holderBg = new Shape.Rect('holderBg', {
763
+ fill: scene.theme.background
764
+ });
765
+
766
+ holderBg.resize(scene.width, scene.height);
767
+ sceneGraph.root.add(holderBg);
768
+
769
+ var holderTextGroup = new Shape.Group('holderTextGroup', {
770
+ text: scene.text,
771
+ align: 'center',
772
+ font: scene.font,
773
+ fill: scene.theme.foreground
774
+ });
775
+
776
+ holderTextGroup.moveTo(null, null, 1);
777
+ sceneGraph.root.add(holderTextGroup);
778
+
779
+ var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph);
780
+ if (!tpdata) {
781
+ throw 'Holder: staging fallback not supported yet.';
782
+ }
783
+ holderTextGroup.properties.leading = tpdata.boundingBox.height;
784
+
785
+ //todo: alignment: TL, TC, TR, CL, CR, BL, BC, BR
786
+ var textNode = null;
787
+ var line = null;
788
+
789
+ function finalizeLine(parent, line, width, height) {
790
+ line.width = width;
791
+ line.height = height;
792
+ parent.width = Math.max(parent.width, line.width);
793
+ parent.height += line.height;
794
+ parent.add(line);
795
+ }
796
+
797
+ if (tpdata.lineCount > 1) {
798
+ var offsetX = 0;
799
+ var offsetY = 0;
800
+ var maxLineWidth = scene.width * App.setup.lineWrapRatio;
801
+ var lineIndex = 0;
802
+ line = new Shape.Group('line' + lineIndex);
803
+
804
+ for (var i = 0; i < tpdata.words.length; i++) {
805
+ var word = tpdata.words[i];
806
+ textNode = new Shape.Text(word.text);
807
+ var newline = word.text == '\\n';
808
+ if (offsetX + word.width >= maxLineWidth || newline === true) {
809
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
810
+ offsetX = 0;
811
+ offsetY += holderTextGroup.properties.leading;
812
+ lineIndex += 1;
813
+ line = new Shape.Group('line' + lineIndex);
814
+ line.y = offsetY;
815
+ }
816
+ if (newline === true) {
817
+ continue;
818
+ }
819
+ textNode.moveTo(offsetX, 0);
820
+ offsetX += tpdata.spaceWidth + word.width;
821
+ line.add(textNode);
822
+ }
823
+
824
+ finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading);
825
+
826
+ for (var lineKey in holderTextGroup.children) {
827
+ line = holderTextGroup.children[lineKey];
828
+ line.moveTo(
829
+ (holderTextGroup.width - line.width) / 2,
830
+ null,
831
+ null);
832
+ }
833
+
834
+ holderTextGroup.moveTo(
835
+ (scene.width - holderTextGroup.width) / 2, (scene.height - holderTextGroup.height) / 2,
836
+ null);
837
+
838
+ //If the text exceeds vertical space, move it down so the first line is visible
839
+ if ((scene.height - holderTextGroup.height) / 2 < 0) {
840
+ holderTextGroup.moveTo(null, 0, null);
841
+ }
842
+ } else {
843
+ textNode = new Shape.Text(scene.text);
844
+ line = new Shape.Group('line0');
845
+ line.add(textNode);
846
+ holderTextGroup.add(line);
847
+
848
+ holderTextGroup.moveTo(
849
+ (scene.width - tpdata.boundingBox.width) / 2, (scene.height - tpdata.boundingBox.height) / 2,
850
+ null);
851
+ }
852
+
853
+ //todo: renderlist
854
+
855
+ return sceneGraph;
856
+ }
857
+
858
+ /**
859
+ * Adaptive text sizing function
860
+ *
861
+ * @private
862
+ * @param width Parent width
863
+ * @param height Parent height
864
+ * @param fontSize Requested text size
865
+ */
866
+ function textSize(width, height, fontSize) {
867
+ var stageWidth = parseInt(width, 10);
868
+ var stageHeight = parseInt(height, 10);
869
+
870
+ var bigSide = Math.max(stageWidth, stageHeight);
871
+ var smallSide = Math.min(stageWidth, stageHeight);
872
+
873
+ var newHeight = 0.8 * Math.min(smallSide, bigSide * App.defaults.scale);
874
+ return Math.round(Math.max(fontSize, newHeight));
875
+ }
876
+
877
+ /**
878
+ * Iterates over resizable (fluid or auto) placeholders and renders them
879
+ *
880
+ * @private
881
+ * @param element Optional element selector, specified only if a specific element needs to be re-rendered
882
+ */
883
+ function updateResizableElements(element) {
884
+ var images;
885
+ if (element == null || element.nodeType == null) {
886
+ images = App.vars.resizableImages;
887
+ } else {
888
+ images = [element];
889
+ }
890
+ for (var i = 0, l = images.length; i < l; i++) {
891
+ var el = images[i];
892
+ if (el.holderData) {
893
+ var flags = el.holderData.flags;
894
+ var dimensions = dimensionCheck(el);
895
+ if (dimensions) {
896
+ if (!el.holderData.resizeUpdate) {
897
+ continue;
898
+ }
899
+
900
+ if (flags.fluid && flags.auto) {
901
+ var fluidConfig = el.holderData.fluidConfig;
902
+ switch (fluidConfig.mode) {
903
+ case 'width':
904
+ dimensions.height = dimensions.width / fluidConfig.ratio;
905
+ break;
906
+ case 'height':
907
+ dimensions.width = dimensions.height * fluidConfig.ratio;
908
+ break;
909
+ }
910
+ }
911
+
912
+ var settings = {
913
+ mode: 'image',
914
+ holderSettings: {
915
+ dimensions: dimensions,
916
+ theme: flags.theme,
917
+ flags: flags
918
+ },
919
+ el: el,
920
+ engineSettings: el.holderData.engineSettings
921
+ };
922
+
923
+ if (flags.textmode == 'exact') {
924
+ flags.exactDimensions = dimensions;
925
+ settings.holderSettings.dimensions = flags.dimensions;
926
+ }
927
+
928
+ render(settings);
929
+ } else {
930
+ setInvisible(el);
931
+ }
932
+ }
933
+ }
934
+ }
935
+
936
+ /**
937
+ * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing
938
+ *
939
+ * @private
940
+ * @param el Image DOM element
941
+ */
942
+ function setInitialDimensions(el) {
943
+ if (el.holderData) {
944
+ var dimensions = dimensionCheck(el);
945
+ if (dimensions) {
946
+ var flags = el.holderData.flags;
947
+
948
+ var fluidConfig = {
949
+ fluidHeight: flags.dimensions.height.slice(-1) == '%',
950
+ fluidWidth: flags.dimensions.width.slice(-1) == '%',
951
+ mode: null,
952
+ initialDimensions: dimensions
953
+ };
954
+
955
+ if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) {
956
+ fluidConfig.mode = 'width';
957
+ fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height);
958
+ } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) {
959
+ fluidConfig.mode = 'height';
960
+ fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height;
961
+ }
962
+
963
+ el.holderData.fluidConfig = fluidConfig;
964
+ } else {
965
+ setInvisible(el);
966
+ }
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Iterates through all current invisible images, and if they're visible, renders them and removes them from further checks. Runs every animation frame.
972
+ *
973
+ * @private
974
+ */
975
+ function visibilityCheck() {
976
+ var renderableImages = [];
977
+ var keys = Object.keys(App.vars.invisibleImages);
978
+ var el;
979
+ for (var i = 0, l = keys.length; i < l; i++) {
980
+ el = App.vars.invisibleImages[keys[i]];
981
+ if (dimensionCheck(el) && el.nodeName.toLowerCase() == 'img') {
982
+ renderableImages.push(el);
983
+ delete App.vars.invisibleImages[keys[i]];
984
+ }
985
+ }
986
+
987
+ if (renderableImages.length) {
988
+ Holder.run({
989
+ images: renderableImages
990
+ });
991
+ }
992
+
993
+ global.requestAnimationFrame(visibilityCheck);
994
+ }
995
+
996
+ /**
997
+ * Starts checking for invisible placeholders if not doing so yet. Does nothing otherwise.
998
+ *
999
+ * @private
1000
+ */
1001
+ function startVisibilityCheck() {
1002
+ if (!App.vars.visibilityCheckStarted) {
1003
+ global.requestAnimationFrame(visibilityCheck);
1004
+ App.vars.visibilityCheckStarted = true;
1005
+ }
1006
+ }
1007
+
1008
+ /**
1009
+ * Sets a unique ID for an image detected to be invisible and adds it to the map of invisible images checked by visibilityCheck
1010
+ *
1011
+ * @private
1012
+ * @param el Invisible DOM element
1013
+ */
1014
+ function setInvisible(el) {
1015
+ if (!el.holderData.invisibleId) {
1016
+ App.vars.invisibleId += 1;
1017
+ App.vars.invisibleImages['i' + App.vars.invisibleId] = el;
1018
+ el.holderData.invisibleId = App.vars.invisibleId;
1019
+ }
1020
+ }
1021
+
1022
+ //todo: see if possible to convert stagingRenderer to use HTML only
1023
+ var stagingRenderer = (function() {
1024
+ var svg = null,
1025
+ stagingText = null,
1026
+ stagingTextNode = null;
1027
+ return function(graph) {
1028
+ var rootNode = graph.root;
1029
+ if (App.setup.supportsSVG) {
1030
+ var firstTimeSetup = false;
1031
+ var tnode = function(text) {
1032
+ return document.createTextNode(text);
1033
+ };
1034
+ if (svg == null || svg.parentNode !== document.body) {
1035
+ firstTimeSetup = true;
1036
+ }
1037
+
1038
+ svg = initSVG(svg, rootNode.properties.width, rootNode.properties.height);
1039
+ //Show staging element before staging
1040
+ svg.style.display = 'block';
1041
+
1042
+ if (firstTimeSetup) {
1043
+ stagingText = newEl('text', SVG_NS);
1044
+ stagingTextNode = tnode(null);
1045
+ setAttr(stagingText, {
1046
+ x: 0
1047
+ });
1048
+ stagingText.appendChild(stagingTextNode);
1049
+ svg.appendChild(stagingText);
1050
+ document.body.appendChild(svg);
1051
+ svg.style.visibility = 'hidden';
1052
+ svg.style.position = 'absolute';
1053
+ svg.style.top = '-100%';
1054
+ svg.style.left = '-100%';
1055
+ //todo: workaround for zero-dimension <svg> tag in Opera 12
1056
+ //svg.setAttribute('width', 0);
1057
+ //svg.setAttribute('height', 0);
1058
+ }
1059
+
1060
+ var holderTextGroup = rootNode.children.holderTextGroup;
1061
+ var htgProps = holderTextGroup.properties;
1062
+ setAttr(stagingText, {
1063
+ 'y': htgProps.font.size,
1064
+ 'style': cssProps({
1065
+ 'font-weight': htgProps.font.weight,
1066
+ 'font-size': htgProps.font.size + htgProps.font.units,
1067
+ 'font-family': htgProps.font.family
1068
+ })
1069
+ });
1070
+
1071
+ //Get bounding box for the whole string (total width and height)
1072
+ stagingTextNode.nodeValue = htgProps.text;
1073
+ var stagingTextBBox = stagingText.getBBox();
1074
+
1075
+ //Get line count and split the string into words
1076
+ var lineCount = Math.ceil(stagingTextBBox.width / (rootNode.properties.width * App.setup.lineWrapRatio));
1077
+ var words = htgProps.text.split(' ');
1078
+ var newlines = htgProps.text.match(/\\n/g);
1079
+ lineCount += newlines == null ? 0 : newlines.length;
1080
+
1081
+ //Get bounding box for the string with spaces removed
1082
+ stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, '');
1083
+ var computedNoSpaceLength = stagingText.getComputedTextLength();
1084
+
1085
+ //Compute average space width
1086
+ var diffLength = stagingTextBBox.width - computedNoSpaceLength;
1087
+ var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1));
1088
+
1089
+ //Get widths for every word with space only if there is more than one line
1090
+ var wordWidths = [];
1091
+ if (lineCount > 1) {
1092
+ stagingTextNode.nodeValue = '';
1093
+ for (var i = 0; i < words.length; i++) {
1094
+ if (words[i].length === 0) continue;
1095
+ stagingTextNode.nodeValue = decodeHtmlEntity(words[i]);
1096
+ var bbox = stagingText.getBBox();
1097
+ wordWidths.push({
1098
+ text: words[i],
1099
+ width: bbox.width
1100
+ });
1101
+ }
1102
+ }
1103
+
1104
+ //Hide staging element after staging
1105
+ svg.style.display = 'none';
1106
+
1107
+ return {
1108
+ spaceWidth: spaceWidth,
1109
+ lineCount: lineCount,
1110
+ boundingBox: stagingTextBBox,
1111
+ words: wordWidths
1112
+ };
1113
+ } else {
1114
+ //todo: canvas fallback for measuring text on android 2.3
1115
+ return false;
1116
+ }
1117
+ };
1118
+ })();
1119
+
1120
+ var sgCanvasRenderer = (function() {
1121
+ var canvas = newEl('canvas');
1122
+ var ctx = null;
1123
+
1124
+ return function(sceneGraph) {
1125
+ if (ctx == null) {
1126
+ ctx = canvas.getContext('2d');
1127
+ }
1128
+ var root = sceneGraph.root;
1129
+ canvas.width = App.dpr(root.properties.width);
1130
+ canvas.height = App.dpr(root.properties.height);
1131
+ ctx.textBaseline = 'middle';
1132
+
1133
+ ctx.fillStyle = root.children.holderBg.properties.fill;
1134
+ ctx.fillRect(0, 0, App.dpr(root.children.holderBg.width), App.dpr(root.children.holderBg.height));
1135
+
1136
+ var textGroup = root.children.holderTextGroup;
1137
+ var tgProps = textGroup.properties;
1138
+ ctx.font = textGroup.properties.font.weight + ' ' + App.dpr(textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace';
1139
+ ctx.fillStyle = textGroup.properties.fill;
1140
+
1141
+ for (var lineKey in textGroup.children) {
1142
+ var line = textGroup.children[lineKey];
1143
+ for (var wordKey in line.children) {
1144
+ var word = line.children[wordKey];
1145
+ var x = App.dpr(textGroup.x + line.x + word.x);
1146
+ var y = App.dpr(textGroup.y + line.y + word.y + (textGroup.properties.leading / 2));
1147
+
1148
+ ctx.fillText(word.properties.text, x, y);
1149
+ }
1150
+ }
1151
+
1152
+ return canvas.toDataURL('image/png');
1153
+ };
1154
+ })();
1155
+
1156
+ var sgSVGRenderer = (function() {
1157
+ //Prevent IE <9 from initializing SVG renderer
1158
+ if (!global.XMLSerializer) return;
1159
+ var xml = createXML();
1160
+ var svg = initSVG(null, 0, 0);
1161
+ var bgEl = newEl('rect', SVG_NS);
1162
+ svg.appendChild(bgEl);
1163
+
1164
+ //todo: create a reusable pool for textNodes, resize if more words present
1165
+
1166
+ return function(sceneGraph, renderSettings) {
1167
+ var root = sceneGraph.root;
1168
+
1169
+ initSVG(svg, root.properties.width, root.properties.height);
1170
+
1171
+ var groups = svg.querySelectorAll('g');
1172
+
1173
+ for (var i = 0; i < groups.length; i++) {
1174
+ groups[i].parentNode.removeChild(groups[i]);
1175
+ }
1176
+
1177
+ var holderURL = renderSettings.holderSettings.flags.holderURL;
1178
+ var holderId = 'holder_' + (Number(new Date()) + 32768 + (0 | Math.random() * 32768)).toString(16);
1179
+ var sceneGroupEl = newEl('g', SVG_NS);
1180
+ var textGroup = root.children.holderTextGroup;
1181
+ var tgProps = textGroup.properties;
1182
+ var textGroupEl = newEl('g', SVG_NS);
1183
+ var tpdata = textGroup.textPositionData;
1184
+ var textCSSRule = '#' + holderId + ' text { ' +
1185
+ cssProps({
1186
+ 'fill': tgProps.fill,
1187
+ 'font-weight': tgProps.font.weight,
1188
+ 'font-family': tgProps.font.family + ', monospace',
1189
+ 'font-size': tgProps.font.size + tgProps.font.units
1190
+ }) + ' } ';
1191
+ var commentNode = xml.createComment('\n' + 'Source URL: ' + holderURL + generatorComment);
1192
+ var holderCSS = xml.createCDATASection(textCSSRule);
1193
+ var styleEl = svg.querySelector('style');
1194
+
1195
+ setAttr(sceneGroupEl, {
1196
+ id: holderId
1197
+ });
1198
+
1199
+ svg.insertBefore(commentNode, svg.firstChild);
1200
+ styleEl.appendChild(holderCSS);
1201
+
1202
+ sceneGroupEl.appendChild(bgEl);
1203
+ sceneGroupEl.appendChild(textGroupEl);
1204
+ svg.appendChild(sceneGroupEl);
1205
+
1206
+ setAttr(bgEl, {
1207
+ 'width': root.children.holderBg.width,
1208
+ 'height': root.children.holderBg.height,
1209
+ 'fill': root.children.holderBg.properties.fill
1210
+ });
1211
+
1212
+ textGroup.y += tpdata.boundingBox.height * 0.8;
1213
+
1214
+ for (var lineKey in textGroup.children) {
1215
+ var line = textGroup.children[lineKey];
1216
+ for (var wordKey in line.children) {
1217
+ var word = line.children[wordKey];
1218
+ var x = textGroup.x + line.x + word.x;
1219
+ var y = textGroup.y + line.y + word.y;
1220
+
1221
+ var textEl = newEl('text', SVG_NS);
1222
+ var textNode = document.createTextNode(null);
1223
+
1224
+ setAttr(textEl, {
1225
+ 'x': x,
1226
+ 'y': y
1227
+ });
1228
+
1229
+ textNode.nodeValue = word.properties.text;
1230
+ textEl.appendChild(textNode);
1231
+ textGroupEl.appendChild(textEl);
1232
+ }
1233
+ }
1234
+
1235
+ var svgString = 'data:image/svg+xml;base64,' +
1236
+ btoa(unescape(encodeURIComponent(serializeSVG(svg, renderSettings.engineSettings))));
1237
+ return svgString;
1238
+ };
1239
+ })();
1240
+
1241
+ //Helpers
1242
+
1243
+ /**
1244
+ * Generic new DOM element function
1245
+ *
1246
+ * @private
1247
+ * @param tag Tag to create
1248
+ * @param namespace Optional namespace value
1249
+ */
1250
+ function newEl(tag, namespace) {
1251
+ if (namespace == null) {
1252
+ return document.createElement(tag);
1253
+ } else {
1254
+ return document.createElementNS(namespace, tag);
1255
+ }
1256
+ }
1257
+
1258
+ /**
1259
+ * Generic setAttribute function
1260
+ *
1261
+ * @private
1262
+ * @param el Reference to DOM element
1263
+ * @param attrs Object with attribute keys and values
1264
+ */
1265
+ function setAttr(el, attrs) {
1266
+ for (var a in attrs) {
1267
+ el.setAttribute(a, attrs[a]);
1268
+ }
1269
+ }
1270
+
1271
+ /**
1272
+ * Generic SVG element creation function
1273
+ *
1274
+ * @private
1275
+ * @param svg SVG context, set to null if new
1276
+ * @param width Document width
1277
+ * @param height Document height
1278
+ */
1279
+ function initSVG(svg, width, height) {
1280
+ var defs, style;
1281
+
1282
+ if (svg == null) {
1283
+ svg = newEl('svg', SVG_NS);
1284
+ defs = newEl('defs', SVG_NS);
1285
+ style = newEl('style', SVG_NS);
1286
+ setAttr(style, {
1287
+ 'type': 'text/css'
1288
+ });
1289
+ defs.appendChild(style);
1290
+ svg.appendChild(defs);
1291
+ } else {
1292
+ style = svg.querySelector('style');
1293
+ }
1294
+
1295
+ //IE throws an exception if this is set and Chrome requires it to be set
1296
+ if (svg.webkitMatchesSelector) {
1297
+ svg.setAttribute('xmlns', SVG_NS);
1298
+ }
1299
+ //Remove comment nodes
1300
+ for (var i = 0; i < svg.childNodes.length; i++) {
1301
+ if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) {
1302
+ svg.removeChild(svg.childNodes[i]);
1303
+ }
1304
+ }
1305
+
1306
+ //Remove CSS
1307
+ while (style.childNodes.length) {
1308
+ style.removeChild(style.childNodes[0]);
1309
+ }
1310
+
1311
+ setAttr(svg, {
1312
+ 'width': width,
1313
+ 'height': height,
1314
+ 'viewBox': '0 0 ' + width + ' ' + height,
1315
+ 'preserveAspectRatio': 'none'
1316
+ });
1317
+
1318
+ return svg;
1319
+ }
1320
+
1321
+ /**
1322
+ * Returns XML processing instructions
1323
+ *
1324
+ * @private
1325
+ * @param svg SVG context
1326
+ * @param stylesheets CSS stylesheets to include
1327
+ */
1328
+ function serializeSVG(svg, engineSettings) {
1329
+ if (!global.XMLSerializer) return;
1330
+ var serializer = new XMLSerializer();
1331
+ var svgCSS = '';
1332
+ var stylesheets = engineSettings.stylesheets;
1333
+
1334
+ //External stylesheets: Processing Instruction method
1335
+ if (engineSettings.svgXMLStylesheet) {
1336
+ var xml = createXML();
1337
+ //Add <?xml-stylesheet ?> directives
1338
+ for (var i = stylesheets.length - 1; i >= 0; i--) {
1339
+ var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"');
1340
+ xml.insertBefore(csspi, xml.firstChild);
1341
+ }
1342
+
1343
+ //Add <?xml ... ?> UTF-8 directive
1344
+ var xmlpi = xml.createProcessingInstruction('xml', 'version="1.0" encoding="UTF-8" standalone="yes"');
1345
+ xml.insertBefore(xmlpi, xml.firstChild);
1346
+ xml.removeChild(xml.documentElement);
1347
+ svgCSS = serializer.serializeToString(xml);
1348
+ }
1349
+
1350
+ var svgText = serializer.serializeToString(svg);
1351
+ svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1');
1352
+ return svgCSS + svgText;
1353
+ }
1354
+
1355
+ /**
1356
+ * Creates a XML document
1357
+ * @private
1358
+ */
1359
+ function createXML() {
1360
+ if (!global.DOMParser) return;
1361
+ return new DOMParser().parseFromString('<xml />', 'application/xml');
1362
+ }
1363
+
1364
+ /**
1365
+ * Prevents a function from being called too often, waits until a timer elapses to call it again
1366
+ *
1367
+ * @param fn Function to call
1368
+ */
1369
+ function debounce(fn) {
1370
+ if (!App.vars.debounceTimer) fn.call(this);
1371
+ if (App.vars.debounceTimer) global.clearTimeout(App.vars.debounceTimer);
1372
+ App.vars.debounceTimer = global.setTimeout(function() {
1373
+ App.vars.debounceTimer = null;
1374
+ fn.call(this);
1375
+ }, App.setup.debounce);
1376
+ }
1377
+
1378
+ /**
1379
+ * Holder-specific resize/orientation change callback, debounced to prevent excessive execution
1380
+ */
1381
+ function resizeEvent() {
1382
+ debounce(function() {
1383
+ updateResizableElements(null);
1384
+ });
1385
+ }
1386
+
1387
+ //Set up flags
1388
+
1389
+ for (var flag in App.flags) {
1390
+ if (!App.flags.hasOwnProperty(flag)) continue;
1391
+ App.flags[flag].match = function(val) {
1392
+ return val.match(this.regex);
1393
+ };
1394
+ }
1395
+
1396
+ //Properties set once on setup
1397
+
1398
+ App.setup = {
1399
+ renderer: 'html',
1400
+ debounce: 100,
1401
+ ratio: 1,
1402
+ supportsCanvas: false,
1403
+ supportsSVG: false,
1404
+ lineWrapRatio: 0.9,
1405
+ renderers: ['html', 'canvas', 'svg']
1406
+ };
1407
+
1408
+ App.dpr = function(val) {
1409
+ return val * App.setup.ratio;
1410
+ };
1411
+
1412
+ //Properties modified during runtime
1413
+
1414
+ App.vars = {
1415
+ preempted: false,
1416
+ resizableImages: [],
1417
+ invisibleImages: {},
1418
+ invisibleId: 0,
1419
+ visibilityCheckStarted: false,
1420
+ debounceTimer: null,
1421
+ cache: {},
1422
+ dataAttr: 'data-src'
1423
+ };
1424
+
1425
+ //Pre-flight
1426
+
1427
+ (function() {
1428
+ var devicePixelRatio = 1,
1429
+ backingStoreRatio = 1;
1430
+
1431
+ var canvas = newEl('canvas');
1432
+ var ctx = null;
1433
+
1434
+ if (canvas.getContext) {
1435
+ if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) {
1436
+ App.setup.renderer = 'canvas';
1437
+ ctx = canvas.getContext('2d');
1438
+ App.setup.supportsCanvas = true;
1439
+ }
1440
+ }
1441
+
1442
+ if (App.setup.supportsCanvas) {
1443
+ devicePixelRatio = global.devicePixelRatio || 1;
1444
+ backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
1445
+ }
1446
+
1447
+ App.setup.ratio = devicePixelRatio / backingStoreRatio;
1448
+
1449
+ if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) {
1450
+ App.setup.renderer = 'svg';
1451
+ App.setup.supportsSVG = true;
1452
+ }
1453
+ })();
1454
+
1455
+ //Starts checking for invisible placeholders
1456
+ startVisibilityCheck();
1457
+
1458
+ if (onDomReady) {
1459
+ onDomReady(function() {
1460
+ if (!App.vars.preempted) {
1461
+ Holder.run();
1462
+ }
1463
+ if (global.addEventListener) {
1464
+ global.addEventListener('resize', resizeEvent, false);
1465
+ global.addEventListener('orientationchange', resizeEvent, false);
1466
+ } else {
1467
+ global.attachEvent('onresize', resizeEvent);
1468
+ }
1469
+
1470
+ if (typeof global.Turbolinks == 'object') {
1471
+ global.document.addEventListener('page:change', function() {
1472
+ Holder.run();
1473
+ });
1474
+ }
1475
+ });
1476
+ }
1477
+
1478
+ module.exports = Holder;
1479
+
1480
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
1481
+
1482
+ /***/ },
1483
+ /* 1 */
1484
+ /***/ function(module, exports, __webpack_require__) {
1485
+
1486
+ /*!
1487
+ * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license
1488
+ *
1489
+ * Specially modified to work with Holder.js
1490
+ */
1491
+
1492
+ function _onDomReady(win) {
1493
+ //Lazy loading fix for Firefox < 3.6
1494
+ //http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html
1495
+ if (document.readyState == null && document.addEventListener) {
1496
+ document.addEventListener("DOMContentLoaded", function DOMContentLoaded() {
1497
+ document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false);
1498
+ document.readyState = "complete";
1499
+ }, false);
1500
+ document.readyState = "loading";
1501
+ }
1502
+
1503
+ var doc = win.document,
1504
+ docElem = doc.documentElement,
1505
+
1506
+ LOAD = "load",
1507
+ FALSE = false,
1508
+ ONLOAD = "on"+LOAD,
1509
+ COMPLETE = "complete",
1510
+ READYSTATE = "readyState",
1511
+ ATTACHEVENT = "attachEvent",
1512
+ DETACHEVENT = "detachEvent",
1513
+ ADDEVENTLISTENER = "addEventListener",
1514
+ DOMCONTENTLOADED = "DOMContentLoaded",
1515
+ ONREADYSTATECHANGE = "onreadystatechange",
1516
+ REMOVEEVENTLISTENER = "removeEventListener",
1517
+
1518
+ // W3C Event model
1519
+ w3c = ADDEVENTLISTENER in doc,
1520
+ _top = FALSE,
1521
+
1522
+ // isReady: Is the DOM ready to be used? Set to true once it occurs.
1523
+ isReady = FALSE,
1524
+
1525
+ // Callbacks pending execution until DOM is ready
1526
+ callbacks = [];
1527
+
1528
+ // Handle when the DOM is ready
1529
+ function ready( fn ) {
1530
+ if ( !isReady ) {
1531
+
1532
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
1533
+ if ( !doc.body ) {
1534
+ return defer( ready );
1535
+ }
1536
+
1537
+ // Remember that the DOM is ready
1538
+ isReady = true;
1539
+
1540
+ // Execute all callbacks
1541
+ while ( fn = callbacks.shift() ) {
1542
+ defer( fn );
1543
+ }
1544
+ }
1545
+ }
1546
+
1547
+ // The ready event handler
1548
+ function completed( event ) {
1549
+ // readyState === "complete" is good enough for us to call the dom ready in oldIE
1550
+ if ( w3c || event.type === LOAD || doc[READYSTATE] === COMPLETE ) {
1551
+ detach();
1552
+ ready();
1553
+ }
1554
+ }
1555
+
1556
+ // Clean-up method for dom ready events
1557
+ function detach() {
1558
+ if ( w3c ) {
1559
+ doc[REMOVEEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
1560
+ win[REMOVEEVENTLISTENER]( LOAD, completed, FALSE );
1561
+ } else {
1562
+ doc[DETACHEVENT]( ONREADYSTATECHANGE, completed );
1563
+ win[DETACHEVENT]( ONLOAD, completed );
1564
+ }
1565
+ }
1566
+
1567
+ // Defers a function, scheduling it to run after the current call stack has cleared.
1568
+ function defer( fn, wait ) {
1569
+ // Allow 0 to be passed
1570
+ setTimeout( fn, +wait >= 0 ? wait : 1 );
1571
+ }
1572
+
1573
+ // Attach the listeners:
1574
+
1575
+ // Catch cases where onDomReady is called after the browser event has already occurred.
1576
+ // we once tried to use readyState "interactive" here, but it caused issues like the one
1577
+ // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
1578
+ if ( doc[READYSTATE] === COMPLETE ) {
1579
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
1580
+ defer( ready );
1581
+
1582
+ // Standards-based browsers support DOMContentLoaded
1583
+ } else if ( w3c ) {
1584
+ // Use the handy event callback
1585
+ doc[ADDEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE );
1586
+
1587
+ // A fallback to window.onload, that will always work
1588
+ win[ADDEVENTLISTENER]( LOAD, completed, FALSE );
1589
+
1590
+ // If IE event model is used
1591
+ } else {
1592
+ // Ensure firing before onload, maybe late but safe also for iframes
1593
+ doc[ATTACHEVENT]( ONREADYSTATECHANGE, completed );
1594
+
1595
+ // A fallback to window.onload, that will always work
1596
+ win[ATTACHEVENT]( ONLOAD, completed );
1597
+
1598
+ // If IE and not a frame
1599
+ // continually check to see if the document is ready
1600
+ try {
1601
+ _top = win.frameElement == null && docElem;
1602
+ } catch(e) {}
1603
+
1604
+ if ( _top && _top.doScroll ) {
1605
+ (function doScrollCheck() {
1606
+ if ( !isReady ) {
1607
+ try {
1608
+ // Use the trick by Diego Perini
1609
+ // http://javascript.nwbox.com/IEContentLoaded/
1610
+ _top.doScroll("left");
1611
+ } catch(e) {
1612
+ return defer( doScrollCheck, 50 );
1613
+ }
1614
+
1615
+ // detach all dom ready events
1616
+ detach();
1617
+
1618
+ // and execute any waiting functions
1619
+ ready();
1620
+ }
1621
+ })();
1622
+ }
1623
+ }
1624
+
1625
+ function onDomReady( fn ) {
1626
+ // If DOM is ready, execute the function (async), otherwise wait
1627
+ isReady ? defer( fn ) : callbacks.push( fn );
1628
+ }
1629
+
1630
+ // Add version
1631
+ onDomReady.version = "1.4.0";
1632
+ // Add method to check if DOM is ready
1633
+ onDomReady.isReady = function(){
1634
+ return isReady;
1635
+ };
1636
+
1637
+ return onDomReady;
1638
+ }
1639
+
1640
+ module.exports = typeof window !== "undefined" && _onDomReady(window);
1641
+
1642
+ /***/ },
1643
+ /* 2 */
1644
+ /***/ function(module, exports, __webpack_require__) {
1645
+
1646
+ var augment = __webpack_require__(4);
1647
+
1648
+ var SceneGraph = function(sceneProperties) {
1649
+ var nodeCount = 1;
1650
+
1651
+ //todo: move merge to helpers section
1652
+ function merge(parent, child) {
1653
+ for (var prop in child) {
1654
+ parent[prop] = child[prop];
1655
+ }
1656
+ return parent;
1657
+ }
1658
+
1659
+ var SceneNode = augment.defclass({
1660
+ constructor: function(name) {
1661
+ nodeCount++;
1662
+ this.parent = null;
1663
+ this.children = {};
1664
+ this.id = nodeCount;
1665
+ this.name = 'n' + nodeCount;
1666
+ if (name != null) {
1667
+ this.name = name;
1668
+ }
1669
+ this.x = 0;
1670
+ this.y = 0;
1671
+ this.z = 0;
1672
+ this.width = 0;
1673
+ this.height = 0;
1674
+ },
1675
+ resize: function(width, height) {
1676
+ if (width != null) {
1677
+ this.width = width;
1678
+ }
1679
+ if (height != null) {
1680
+ this.height = height;
1681
+ }
1682
+ },
1683
+ moveTo: function(x, y, z) {
1684
+ this.x = x != null ? x : this.x;
1685
+ this.y = y != null ? y : this.y;
1686
+ this.z = z != null ? z : this.z;
1687
+ },
1688
+ add: function(child) {
1689
+ var name = child.name;
1690
+ if (this.children[name] == null) {
1691
+ this.children[name] = child;
1692
+ child.parent = this;
1693
+ } else {
1694
+ throw 'SceneGraph: child with that name already exists: ' + name;
1695
+ }
1696
+ }
1697
+ });
1698
+
1699
+ var RootNode = augment(SceneNode, function(uber) {
1700
+ this.constructor = function() {
1701
+ uber.constructor.call(this, 'root');
1702
+ this.properties = sceneProperties;
1703
+ };
1704
+ });
1705
+
1706
+ var Shape = augment(SceneNode, function(uber) {
1707
+ function constructor(name, props) {
1708
+ uber.constructor.call(this, name);
1709
+ this.properties = {
1710
+ fill: '#000'
1711
+ };
1712
+ if (props != null) {
1713
+ merge(this.properties, props);
1714
+ } else if (name != null && typeof name !== 'string') {
1715
+ throw 'SceneGraph: invalid node name';
1716
+ }
1717
+ }
1718
+
1719
+ this.Group = augment.extend(this, {
1720
+ constructor: constructor,
1721
+ type: 'group'
1722
+ });
1723
+
1724
+ this.Rect = augment.extend(this, {
1725
+ constructor: constructor,
1726
+ type: 'rect'
1727
+ });
1728
+
1729
+ this.Text = augment.extend(this, {
1730
+ constructor: function(text) {
1731
+ constructor.call(this);
1732
+ this.properties.text = text;
1733
+ },
1734
+ type: 'text'
1735
+ });
1736
+ });
1737
+
1738
+ var root = new RootNode();
1739
+
1740
+ this.Shape = Shape;
1741
+ this.root = root;
1742
+
1743
+ return this;
1744
+ };
1745
+
1746
+ module.exports = SceneGraph;
1747
+
1748
+
1749
+ /***/ },
1750
+ /* 3 */
1751
+ /***/ function(module, exports, __webpack_require__) {
1752
+
1753
+ /* WEBPACK VAR INJECTION */(function(global) {/**
1754
+ * Shallow object clone and merge
1755
+ *
1756
+ * @param a Object A
1757
+ * @param b Object B
1758
+ * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties
1759
+ */
1760
+ exports.extend = function(a, b) {
1761
+ var c = {};
1762
+ for (var x in a) {
1763
+ if (a.hasOwnProperty(x)) {
1764
+ c[x] = a[x];
1765
+ }
1766
+ }
1767
+ if (b != null) {
1768
+ for (var y in b) {
1769
+ if (b.hasOwnProperty(y)) {
1770
+ c[y] = b[y];
1771
+ }
1772
+ }
1773
+ }
1774
+ return c;
1775
+ };
1776
+
1777
+ /**
1778
+ * Takes a k/v list of CSS properties and returns a rule
1779
+ *
1780
+ * @param props CSS properties object
1781
+ */
1782
+ exports.cssProps = function(props) {
1783
+ var ret = [];
1784
+ for (var p in props) {
1785
+ if (props.hasOwnProperty(p)) {
1786
+ ret.push(p + ':' + props[p]);
1787
+ }
1788
+ }
1789
+ return ret.join(';');
1790
+ };
1791
+
1792
+ /**
1793
+ * Encodes HTML entities in a string
1794
+ *
1795
+ * @param str Input string
1796
+ */
1797
+ exports.encodeHtmlEntity = function(str) {
1798
+ var buf = [];
1799
+ var charCode = 0;
1800
+ for (var i = str.length - 1; i >= 0; i--) {
1801
+ charCode = str.charCodeAt(i);
1802
+ if (charCode > 128) {
1803
+ buf.unshift(['&#', charCode, ';'].join(''));
1804
+ } else {
1805
+ buf.unshift(str[i]);
1806
+ }
1807
+ }
1808
+ return buf.join('');
1809
+ };
1810
+
1811
+
1812
+ /**
1813
+ * Converts a value into an array of DOM nodes
1814
+ *
1815
+ * @param val A string, a NodeList, a Node, or an HTMLCollection
1816
+ */
1817
+ exports.getNodeArray = function(val) {
1818
+ var retval = null;
1819
+ if (typeof(val) == 'string') {
1820
+ retval = document.querySelectorAll(val);
1821
+ } else if (global.NodeList && val instanceof global.NodeList) {
1822
+ retval = val;
1823
+ } else if (global.Node && val instanceof global.Node) {
1824
+ retval = [val];
1825
+ } else if (global.HTMLCollection && val instanceof global.HTMLCollection) {
1826
+ retval = val;
1827
+ } else if (val instanceof Array) {
1828
+ retval = val;
1829
+ } else if (val === null) {
1830
+ retval = [];
1831
+ }
1832
+ return retval;
1833
+ };
1834
+
1835
+ /**
1836
+ * Checks if an image exists
1837
+ *
1838
+ * @param src URL of image
1839
+ * @param callback Callback to call once image status has been found
1840
+ */
1841
+ exports.imageExists = function(src, callback) {
1842
+ var image = new Image();
1843
+ image.onerror = function() {
1844
+ callback.call(this, false);
1845
+ };
1846
+ image.onload = function() {
1847
+ callback.call(this, true);
1848
+ };
1849
+ image.src = src;
1850
+ };
1851
+
1852
+ /**
1853
+ * Decodes HTML entities in a string
1854
+ *
1855
+ * @param str Input string
1856
+ */
1857
+ exports.decodeHtmlEntity = function(str) {
1858
+ return str.replace(/&#(\d+);/g, function(match, dec) {
1859
+ return String.fromCharCode(dec);
1860
+ });
1861
+ };
1862
+
1863
+
1864
+ /**
1865
+ * Returns an element's dimensions if it's visible, `false` otherwise.
1866
+ *
1867
+ * @private
1868
+ * @param el DOM element
1869
+ */
1870
+ exports.dimensionCheck = function(el) {
1871
+ var dimensions = {
1872
+ height: el.clientHeight,
1873
+ width: el.clientWidth
1874
+ };
1875
+
1876
+ if (dimensions.height && dimensions.width) {
1877
+ return dimensions;
1878
+ } else {
1879
+ return false;
1880
+ }
1881
+ };
1882
+
1883
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
1884
+
1885
+ /***/ },
1886
+ /* 4 */
1887
+ /***/ function(module, exports, __webpack_require__) {
1888
+
1889
+ var Factory = function () {};
1890
+ var slice = Array.prototype.slice;
1891
+
1892
+ var augment = function (base, body) {
1893
+ var uber = Factory.prototype = typeof base === "function" ? base.prototype : base;
1894
+ var prototype = new Factory(), properties = body.apply(prototype, slice.call(arguments, 2).concat(uber));
1895
+ if (typeof properties === "object") for (var key in properties) prototype[key] = properties[key];
1896
+ if (!prototype.hasOwnProperty("constructor")) return prototype;
1897
+ var constructor = prototype.constructor;
1898
+ constructor.prototype = prototype;
1899
+ return constructor;
1900
+ };
1901
+
1902
+ augment.defclass = function (prototype) {
1903
+ var constructor = prototype.constructor;
1904
+ constructor.prototype = prototype;
1905
+ return constructor;
1906
+ };
1907
+
1908
+ augment.extend = function (base, body) {
1909
+ return augment(base, function (uber) {
1910
+ this.uber = uber;
1911
+ return body;
1912
+ });
1913
+ };
1914
+
1915
+ module.exports = augment;
1916
+
1917
+ /***/ }
1918
+ /******/ ])
1919
+ });
1920
+ ;