vendor/assets/javascripts/holder.js in holder_rails-2.7.1 vs vendor/assets/javascripts/holder.js in holder_rails-2.8.0

- old
+ new

@@ -1,14 +1,14 @@ /*! Holder - client side image placeholders -Version 2.7.1+6hydf +Version 2.8.0+7srgw © 2015 Ivan Malopinsky - http://imsky.co Site: http://holderjs.com Issues: https://github.com/imsky/holder/issues -License: http://opensource.org/licenses/MIT +License: MIT */ (function (window) { if (!window.document) return; var document = window.document; @@ -283,29 +283,44 @@ /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { + /* + Holder.js - client side image placeholders + (c) 2012-2015 Ivan Malopinsky - http://imsky.co + */ + + module.exports = __webpack_require__(1); + + +/***/ }, +/* 1 */ +/***/ function(module, exports, __webpack_require__) { + /* WEBPACK VAR INJECTION */(function(global) {/* Holder.js - client side image placeholders - © 2012-2015 Ivan Malopinsky - http://imsky.co + (c) 2012-2015 Ivan Malopinsky - http://imsky.co */ //Libraries and functions - var onDomReady = __webpack_require__(1); - var SceneGraph = __webpack_require__(2); - var utils = __webpack_require__(3); - var querystring = __webpack_require__(4); + var onDomReady = __webpack_require__(3); + var querystring = __webpack_require__(2); + var SceneGraph = __webpack_require__(4); + var utils = __webpack_require__(5); + var SVG = __webpack_require__(6); + var DOM = __webpack_require__(7); + var Color = __webpack_require__(8); + var extend = utils.extend; - var getNodeArray = utils.getNodeArray; var dimensionCheck = utils.dimensionCheck; //Constants and definitions var SVG_NS = 'http://www.w3.org/2000/svg'; var NODE_TYPE_COMMENT = 8; - var version = '2.7.1'; + var version = '2.8.0'; var generatorComment = '\n' + 'Created with Holder.js ' + version + '.\n' + 'Learn more at http://holderjs.com\n' + '(c) 2012-2015 Ivan Malopinsky - http://imsky.co\n'; @@ -326,20 +341,21 @@ /** * Appends a placeholder to an element * * @param {string} src Placeholder URL string - * @param {string} el Selector of target element(s) + * @param el A selector or a reference to a DOM node */ addImage: function(src, el) { - var node = document.querySelectorAll(el); + //todo: use jquery fallback if available for all QSA references + var node = DOM.getNodeArray(el); if (node.length) { for (var i = 0, l = node.length; i < l; i++) { - var img = newEl('img'); + var img = DOM.newEl('img'); var domProps = {}; - domProps[App.vars.dataAttr] = src; - setAttr(img, domProps); + domProps[App.setup.dataAttr] = src; + DOM.setAttr(img, domProps); node[i].appendChild(img); } } return this; }, @@ -364,37 +380,39 @@ * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes. * * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties */ run: function(userOptions) { + //todo: split processing into separate queues userOptions = userOptions || {}; var engineSettings = {}; var options = extend(App.settings, userOptions); App.vars.preempted = true; - App.vars.dataAttr = options.dataAttr || App.vars.dataAttr; + App.vars.dataAttr = options.dataAttr || App.setup.dataAttr; + App.vars.lineWrapRatio = options.lineWrapRatio || App.setup.lineWrapRatio; engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer; if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) { engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html'); } - var images = getNodeArray(options.images); - var bgnodes = getNodeArray(options.bgnodes); - var stylenodes = getNodeArray(options.stylenodes); - var objects = getNodeArray(options.objects); + var images = DOM.getNodeArray(options.images); + var bgnodes = DOM.getNodeArray(options.bgnodes); + var stylenodes = DOM.getNodeArray(options.stylenodes); + var objects = DOM.getNodeArray(options.objects); engineSettings.stylesheets = []; engineSettings.svgXMLStylesheet = true; engineSettings.noFontFallback = options.noFontFallback ? options.noFontFallback : false; for (var i = 0; i < stylenodes.length; i++) { var styleNode = stylenodes[i]; if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') { var href = styleNode.attributes.href.value; //todo: write isomorphic relative-to-absolute URL function - var proxyLink = newEl('a'); + var proxyLink = DOM.newEl('a'); proxyLink.href = href; var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search; engineSettings.stylesheets.push(stylesheetURL); } } @@ -402,18 +420,12 @@ for (i = 0; i < bgnodes.length; i++) { //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background if (!global.getComputedStyle) continue; var backgroundImage = global.getComputedStyle(bgnodes[i], null).getPropertyValue('background-image'); var dataBackgroundImage = bgnodes[i].getAttribute('data-background-src'); - var rawURL = null; + var rawURL = dataBackgroundImage || backgroundImage; - if (dataBackgroundImage == null) { - rawURL = backgroundImage; - } else { - rawURL = dataBackgroundImage; - } - var holderURL = null; var holderString = '?' + options.domain + '/'; if (rawURL.indexOf(holderString) === 0) { holderURL = rawURL.slice(1); @@ -505,11 +517,10 @@ domain: 'holder.js', images: 'img', objects: 'object', bgnodes: 'body .holderjs', stylenodes: 'head link.holderjs', - stylesheets: [], themes: { 'gray': { background: '#EEEEEE', foreground: '#AAAAAA' }, @@ -537,73 +548,10 @@ }, defaults: { size: 10, units: 'pt', scale: 1 / 16 - }, - //todo: remove in 2.8 - flags: { - dimensions: { - regex: /^(\d+)x(\d+)$/, - output: function(val) { - var exec = this.regex.exec(val); - return { - width: +exec[1], - height: +exec[2] - }; - } - }, - fluid: { - regex: /^([0-9]+%?)x([0-9]+%?)$/, - output: function(val) { - var exec = this.regex.exec(val); - return { - width: exec[1], - height: exec[2] - }; - } - }, - colors: { - regex: /(?:#|\^)([0-9a-f]{3,})\:(?:#|\^)([0-9a-f]{3,})/i, - output: function(val) { - var exec = this.regex.exec(val); - return { - foreground: '#' + exec[2], - background: '#' + exec[1] - }; - } - }, - text: { - regex: /text\:(.*)/, - output: function(val) { - return this.regex.exec(val)[1].replace('\\/', '/'); - } - }, - font: { - regex: /font\:(.*)/, - output: function(val) { - return this.regex.exec(val)[1]; - } - }, - auto: { - regex: /^auto$/ - }, - textmode: { - regex: /textmode\:(.*)/, - output: function(val) { - return this.regex.exec(val)[1]; - } - }, - random: { - regex: /^random$/ - }, - size: { - regex: /size\:(\d+)/, - output: function(val) { - return this.regex.exec(val)[1]; - } - } } }; /** * Processes provided source attribute and sets up the appropriate rendering workflow @@ -638,15 +586,11 @@ theme: extend(App.settings.themes.gray, null), stylesheets: options.stylesheets, instanceOptions: options }; - if (url.match(/([\d]+p?)x([\d]+p?)(?:\?|$)/)) { - return parseQueryString(url, holder); - } else { - return parseFlags(url, holder); - } + return parseQueryString(url, holder); } /** * Processes a Holder URL and extracts configuration from query string * @@ -683,10 +627,15 @@ if (options.fg) { holder.theme.foreground = (options.fg.indexOf('#') === -1 ? '#' : '') + options.fg; } + //todo: add automatic foreground to themes without foreground + if (options.bg && !options.fg) { + holder.autoFg = true; + } + if (options.theme && holder.instanceOptions.themes.hasOwnProperty(options.theme)) { holder.theme = extend(holder.instanceOptions.themes[options.theme], null); } // Text @@ -715,103 +664,23 @@ // Miscellaneous holder.auto = utils.truthy(options.auto); + holder.outline = utils.truthy(options.outline); + if (utils.truthy(options.random)) { App.vars.cache.themeKeys = App.vars.cache.themeKeys || Object.keys(holder.instanceOptions.themes); var _theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length]; holder.theme = extend(holder.instanceOptions.themes[_theme], null); } } return holder; } - //todo: remove in 2.8 /** - * Processes a Holder URL and extracts flags - * - * @private - * @deprecated - * @param url URL - * @param holder Staging Holder object - */ - function parseFlags(url, holder) { - var render = false; - var vtab = String.fromCharCode(11); - var flags = url.replace(/([^\\])\//g, '$1' + vtab).split(vtab); - var uriRegex = /%[0-9a-f]{2}/gi; - var options = holder.instanceOptions; - - holder.holderURL = []; - - for (var fl = flags.length, j = 0; j < fl; j++) { - var flag = flags[j]; - if (flag.match(uriRegex)) { - try { - flag = decodeURIComponent(flag); - } catch (e) { - flag = flags[j]; - } - } - - var push = false; - - if (App.flags.dimensions.match(flag)) { - render = true; - holder.dimensions = App.flags.dimensions.output(flag); - push = true; - } else if (App.flags.fluid.match(flag)) { - render = true; - holder.dimensions = App.flags.fluid.output(flag); - holder.fluid = true; - push = true; - } else if (App.flags.textmode.match(flag)) { - holder.textmode = App.flags.textmode.output(flag); - push = true; - } else if (App.flags.colors.match(flag)) { - var colors = App.flags.colors.output(flag); - holder.theme = extend(holder.theme, colors); - push = true; - } else if (options.themes[flag]) { - //If a theme is specified, it will override custom colors - if (options.themes.hasOwnProperty(flag)) { - holder.theme = extend(options.themes[flag], null); - } - push = true; - } else if (App.flags.font.match(flag)) { - holder.font = App.flags.font.output(flag); - push = true; - } else if (App.flags.auto.match(flag)) { - holder.auto = true; - push = true; - } else if (App.flags.text.match(flag)) { - holder.text = App.flags.text.output(flag); - push = true; - } else if (App.flags.size.match(flag)) { - holder.size = App.flags.size.output(flag); - push = true; - } else if (App.flags.random.match(flag)) { - if (App.vars.cache.themeKeys == null) { - App.vars.cache.themeKeys = Object.keys(options.themes); - } - var theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length]; - holder.theme = extend(options.themes[theme], null); - push = true; - } - - if (push) { - holder.holderURL.push(flag); - } - } - holder.holderURL.unshift(options.domain); - holder.holderURL = holder.holderURL.join('/'); - return render ? holder : false; - } - - /** * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders) * * @private * @param settings DOM prep settings */ @@ -856,18 +725,18 @@ engineSettings.reRender = true; } if (mode == 'background') { if (el.getAttribute('data-background-src') == null) { - setAttr(el, { + DOM.setAttr(el, { 'data-background-src': holderURL }); } } else { var domProps = {}; domProps[App.vars.dataAttr] = holderURL; - setAttr(el, domProps); + DOM.setAttr(el, domProps); } flags.theme = theme; //todo consider using all renderSettings in holderData @@ -875,11 +744,11 @@ flags: flags, engineSettings: engineSettings }; if (mode == 'image' || mode == 'fluid') { - setAttr(el, { + DOM.setAttr(el, { 'alt': (theme.text ? theme.text + ' [' + dimensionsCaption + ']' : dimensionsCaption) }); } var renderSettings = { @@ -946,12 +815,12 @@ * @param renderSettings Renderer settings */ function render(renderSettings) { var image = null; var mode = renderSettings.mode; - var holderSettings = renderSettings.holderSettings; var el = renderSettings.el; + var holderSettings = renderSettings.holderSettings; var engineSettings = renderSettings.engineSettings; switch (engineSettings.renderer) { case 'svg': if (!App.setup.supportsSVG) return; @@ -999,18 +868,18 @@ if (mode == 'background') { el.style.backgroundImage = 'url(' + image + ')'; el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px'; } else { if (el.nodeName.toLowerCase() === 'img') { - setAttr(el, { + DOM.setAttr(el, { 'src': image }); } else if (el.nodeName.toLowerCase() === 'object') { - setAttr(el, { + DOM.setAttr(el, { 'data': image }); - setAttr(el, { + DOM.setAttr(el, { 'type': 'image/svg+xml' }); } if (engineSettings.reRender) { global.setTimeout(function() { @@ -1018,26 +887,26 @@ if (image == null) { throw 'Holder: couldn\'t render placeholder'; } //todo: refactor this code into a function if (el.nodeName.toLowerCase() === 'img') { - setAttr(el, { + DOM.setAttr(el, { 'src': image }); } else if (el.nodeName.toLowerCase() === 'object') { - setAttr(el, { + DOM.setAttr(el, { 'data': image }); - setAttr(el, { + DOM.setAttr(el, { 'type': 'image/svg+xml' }); } - }, 100); + }, 150); } } //todo: account for re-rendering - setAttr(el, { + DOM.setAttr(el, { 'data-holder-rendered': true }); } /** @@ -1091,15 +960,37 @@ }); holderBg.resize(scene.width, scene.height); sceneGraph.root.add(holderBg); + if (scene.flags.outline) { + //todo: generalize darken/lighten to more than RRGGBB hex values + var outlineColor = new Color(holderBg.properties.fill); + + outlineColor = outlineColor.lighten(outlineColor.lighterThan('7f7f7f') ? -0.1 : 0.1); + + holderBg.properties.outline = { + fill: outlineColor.toHex(true), + width: 2 + }; + } + + var holderTextColor = scene.theme.foreground; + + if (scene.flags.autoFg) { + var holderBgColor = new Color(holderBg.properties.fill); + var lightColor = new Color('fff'); + var darkColor = new Color('000', { 'alpha': 0.285714 }); + + holderTextColor = holderBgColor.blendAlpha(holderBgColor.lighterThan('7f7f7f') ? darkColor : lightColor).toHex(true); + } + var holderTextGroup = new Shape.Group('holderTextGroup', { text: scene.text, align: scene.align, font: scene.font, - fill: scene.theme.foreground + fill: holderTextColor }); holderTextGroup.moveTo(null, null, 1); sceneGraph.root.add(holderTextGroup); @@ -1117,11 +1008,11 @@ line.height = height; parent.width = Math.max(parent.width, line.width); parent.height += line.height; } - var sceneMargin = scene.width * App.setup.lineWrapRatio; + var sceneMargin = scene.width * App.vars.lineWrapRatio; var maxLineWidth = sceneMargin; if (tpdata.lineCount > 1) { var offsetX = 0; var offsetY = 0; @@ -1129,11 +1020,11 @@ var lineKey; line = new Shape.Group('line' + lineIndex); //Double margin so that left/right-aligned next is not flush with edge of image if (scene.align === 'left' || scene.align === 'right') { - maxLineWidth = scene.width * (1 - (1 - (App.setup.lineWrapRatio)) * 2); + maxLineWidth = scene.width * (1 - (1 - (App.vars.lineWrapRatio)) * 2); } for (var i = 0; i < tpdata.words.length; i++) { var word = tpdata.words[i]; textNode = new Shape.Text(word.text); @@ -1198,11 +1089,10 @@ holderTextGroup.moveTo(null, (scene.height - tpdata.boundingBox.height) / 2, null); } //todo: renderlist - return sceneGraph; } /** * Adaptive text sizing function @@ -1382,18 +1272,18 @@ }; if (svg == null || svg.parentNode !== document.body) { firstTimeSetup = true; } - svg = initSVG(svg, rootNode.properties.width, rootNode.properties.height); + svg = SVG.initSVG(svg, rootNode.properties.width, rootNode.properties.height); //Show staging element before staging svg.style.display = 'block'; if (firstTimeSetup) { - stagingText = newEl('text', SVG_NS); + stagingText = DOM.newEl('text', SVG_NS); stagingTextNode = tnode(null); - setAttr(stagingText, { + DOM.setAttr(stagingText, { x: 0 }); stagingText.appendChild(stagingTextNode); svg.appendChild(stagingText); document.body.appendChild(svg); @@ -1406,11 +1296,11 @@ //svg.setAttribute('height', 0); } var holderTextGroup = rootNode.children.holderTextGroup; var htgProps = holderTextGroup.properties; - setAttr(stagingText, { + DOM.setAttr(stagingText, { 'y': htgProps.font.size, 'style': utils.cssProps({ 'font-weight': htgProps.font.weight, 'font-size': htgProps.font.size + htgProps.font.units, 'font-family': htgProps.font.family @@ -1420,11 +1310,11 @@ //Get bounding box for the whole string (total width and height) stagingTextNode.nodeValue = htgProps.text; var stagingTextBBox = stagingText.getBBox(); //Get line count and split the string into words - var lineCount = Math.ceil(stagingTextBBox.width / (rootNode.properties.width * App.setup.lineWrapRatio)); + var lineCount = Math.ceil(stagingTextBBox.width / (rootNode.properties.width * App.vars.lineWrapRatio)); var words = htgProps.text.split(' '); var newlines = htgProps.text.match(/\\n/g); lineCount += newlines == null ? 0 : newlines.length; //Get bounding box for the string with spaces removed @@ -1465,11 +1355,11 @@ } }; })(); var sgCanvasRenderer = (function() { - var canvas = newEl('canvas'); + var canvas = DOM.newEl('canvas'); var ctx = null; return function(sceneGraph) { if (ctx == null) { ctx = canvas.getContext('2d'); @@ -1477,13 +1367,38 @@ var root = sceneGraph.root; canvas.width = App.dpr(root.properties.width); canvas.height = App.dpr(root.properties.height); ctx.textBaseline = 'middle'; - ctx.fillStyle = root.children.holderBg.properties.fill; - ctx.fillRect(0, 0, App.dpr(root.children.holderBg.width), App.dpr(root.children.holderBg.height)); + var bg = root.children.holderBg; + var bgWidth = App.dpr(bg.width); + var bgHeight = App.dpr(bg.height); + //todo: parametrize outline width (e.g. in scene object) + var outlineWidth = 2; + var outlineOffsetWidth = outlineWidth / 2; + ctx.fillStyle = bg.properties.fill; + ctx.fillRect(0, 0, bgWidth, bgHeight); + + if (bg.properties.outline) { + //todo: abstract this into a method + ctx.strokeStyle = bg.properties.outline.fill; + ctx.lineWidth = bg.properties.outline.width; + ctx.moveTo(outlineOffsetWidth, outlineOffsetWidth); + // TL, TR, BR, BL + ctx.lineTo(bgWidth - outlineOffsetWidth, outlineOffsetWidth); + ctx.lineTo(bgWidth - outlineOffsetWidth, bgHeight - outlineOffsetWidth); + ctx.lineTo(outlineOffsetWidth, bgHeight - outlineOffsetWidth); + ctx.lineTo(outlineOffsetWidth, outlineOffsetWidth); + // Diagonals + ctx.moveTo(0, outlineOffsetWidth); + ctx.lineTo(bgWidth, bgHeight - outlineOffsetWidth); + ctx.moveTo(0, bgHeight - outlineOffsetWidth); + ctx.lineTo(bgWidth, outlineOffsetWidth); + ctx.stroke(); + } + var textGroup = root.children.holderTextGroup; var tgProps = textGroup.properties; ctx.font = textGroup.properties.font.weight + ' ' + App.dpr(textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace'; ctx.fillStyle = textGroup.properties.fill; @@ -1503,34 +1418,34 @@ })(); var sgSVGRenderer = (function() { //Prevent IE <9 from initializing SVG renderer if (!global.XMLSerializer) return; - var xml = createXML(); - var svg = initSVG(null, 0, 0); - var bgEl = newEl('rect', SVG_NS); + var xml = DOM.createXML(); + var svg = SVG.initSVG(null, 0, 0); + var bgEl = DOM.newEl('rect', SVG_NS); svg.appendChild(bgEl); //todo: create a reusable pool for textNodes, resize if more words present return function(sceneGraph, renderSettings) { var root = sceneGraph.root; - initSVG(svg, root.properties.width, root.properties.height); + SVG.initSVG(svg, root.properties.width, root.properties.height); var groups = svg.querySelectorAll('g'); for (var i = 0; i < groups.length; i++) { groups[i].parentNode.removeChild(groups[i]); } var holderURL = renderSettings.holderSettings.flags.holderURL; var holderId = 'holder_' + (Number(new Date()) + 32768 + (0 | Math.random() * 32768)).toString(16); - var sceneGroupEl = newEl('g', SVG_NS); + var sceneGroupEl = DOM.newEl('g', SVG_NS); var textGroup = root.children.holderTextGroup; var tgProps = textGroup.properties; - var textGroupEl = newEl('g', SVG_NS); + var textGroupEl = DOM.newEl('g', SVG_NS); var tpdata = textGroup.textPositionData; var textCSSRule = '#' + holderId + ' text { ' + utils.cssProps({ 'fill': tgProps.fill, 'font-weight': tgProps.font.weight, @@ -1538,26 +1453,52 @@ 'font-size': tgProps.font.size + tgProps.font.units }) + ' } '; var commentNode = xml.createComment('\n' + 'Source URL: ' + holderURL + generatorComment); var holderCSS = xml.createCDATASection(textCSSRule); var styleEl = svg.querySelector('style'); + var bg = root.children.holderBg; - setAttr(sceneGroupEl, { + DOM.setAttr(sceneGroupEl, { id: holderId }); svg.insertBefore(commentNode, svg.firstChild); styleEl.appendChild(holderCSS); sceneGroupEl.appendChild(bgEl); + + //todo: abstract this into a cross-browser SVG outline method + if (bg.properties.outline) { + var outlineEl = DOM.newEl('path', SVG_NS); + var outlineWidth = bg.properties.outline.width; + var outlineOffsetWidth = outlineWidth / 2; + DOM.setAttr(outlineEl, { + 'd': [ + 'M', outlineOffsetWidth, outlineOffsetWidth, + 'H', bg.width - outlineOffsetWidth, + 'V', bg.height - outlineOffsetWidth, + 'H', outlineOffsetWidth, + 'V', 0, + 'M', 0, outlineOffsetWidth, + 'L', bg.width, bg.height - outlineOffsetWidth, + 'M', 0, bg.height - outlineOffsetWidth, + 'L', bg.width, outlineOffsetWidth + ].join(' '), + 'stroke-width': bg.properties.outline.width, + 'stroke': bg.properties.outline.fill, + 'fill': 'none' + }); + sceneGroupEl.appendChild(outlineEl); + } + sceneGroupEl.appendChild(textGroupEl); svg.appendChild(sceneGroupEl); - setAttr(bgEl, { - 'width': root.children.holderBg.width, - 'height': root.children.holderBg.height, - 'fill': root.children.holderBg.properties.fill + DOM.setAttr(bgEl, { + 'width': bg.width, + 'height': bg.height, + 'fill': bg.properties.fill }); textGroup.y += tpdata.boundingBox.height * 0.8; for (var lineKey in textGroup.children) { @@ -1565,14 +1506,14 @@ for (var wordKey in line.children) { var word = line.children[wordKey]; var x = textGroup.x + line.x + word.x; var y = textGroup.y + line.y + word.y; - var textEl = newEl('text', SVG_NS); + var textEl = DOM.newEl('text', SVG_NS); var textNode = document.createTextNode(null); - setAttr(textEl, { + DOM.setAttr(textEl, { 'x': x, 'y': y }); textNode.nodeValue = word.properties.text; @@ -1580,157 +1521,18 @@ textGroupEl.appendChild(textEl); } } //todo: factor the background check up the chain, perhaps only return reference - var svgString = svgStringToDataURI(serializeSVG(svg, renderSettings.engineSettings), renderSettings.mode === 'background'); + var svgString = SVG.svgStringToDataURI(SVG.serializeSVG(svg, renderSettings.engineSettings), renderSettings.mode === 'background'); return svgString; }; })(); //Helpers - //todo: move svg-related helpers to a dedicated file - /** - * Converts serialized SVG to a string suitable for data URI use - * @param svgString Serialized SVG string - * @param [base64] Use base64 encoding for data URI - */ - var svgStringToDataURI = function() { - var rawPrefix = 'data:image/svg+xml;charset=UTF-8,'; - var base64Prefix = 'data:image/svg+xml;charset=UTF-8;base64,'; - - return function(svgString, base64) { - if (base64) { - return base64Prefix + btoa(unescape(encodeURIComponent(svgString))); - } else { - return rawPrefix + encodeURIComponent(svgString); - } - }; - }(); - - /** - * Generic new DOM element function - * - * @private - * @param tag Tag to create - * @param namespace Optional namespace value - */ - function newEl(tag, namespace) { - if (namespace == null) { - return document.createElement(tag); - } else { - return document.createElementNS(namespace, tag); - } - } - - /** - * Generic setAttribute function - * - * @private - * @param el Reference to DOM element - * @param attrs Object with attribute keys and values - */ - function setAttr(el, attrs) { - for (var a in attrs) { - el.setAttribute(a, attrs[a]); - } - } - - /** - * Generic SVG element creation function - * - * @private - * @param svg SVG context, set to null if new - * @param width Document width - * @param height Document height - */ - function initSVG(svg, width, height) { - var defs, style; - - if (svg == null) { - svg = newEl('svg', SVG_NS); - defs = newEl('defs', SVG_NS); - style = newEl('style', SVG_NS); - setAttr(style, { - 'type': 'text/css' - }); - defs.appendChild(style); - svg.appendChild(defs); - } else { - style = svg.querySelector('style'); - } - - //IE throws an exception if this is set and Chrome requires it to be set - if (svg.webkitMatchesSelector) { - svg.setAttribute('xmlns', SVG_NS); - } - - //Remove comment nodes - for (var i = 0; i < svg.childNodes.length; i++) { - if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) { - svg.removeChild(svg.childNodes[i]); - } - } - - //Remove CSS - while (style.childNodes.length) { - style.removeChild(style.childNodes[0]); - } - - setAttr(svg, { - 'width': width, - 'height': height, - 'viewBox': '0 0 ' + width + ' ' + height, - 'preserveAspectRatio': 'none' - }); - - return svg; - } - - /** - * Returns serialized SVG with XML processing instructions - * - * @private - * @param svg SVG context - * @param stylesheets CSS stylesheets to include - */ - function serializeSVG(svg, engineSettings) { - if (!global.XMLSerializer) return; - var serializer = new XMLSerializer(); - var svgCSS = ''; - var stylesheets = engineSettings.stylesheets; - - //External stylesheets: Processing Instruction method - if (engineSettings.svgXMLStylesheet) { - var xml = createXML(); - //Add <?xml-stylesheet ?> directives - for (var i = stylesheets.length - 1; i >= 0; i--) { - var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"'); - xml.insertBefore(csspi, xml.firstChild); - } - - xml.removeChild(xml.documentElement); - svgCSS = serializer.serializeToString(xml); - } - - var svgText = serializer.serializeToString(svg); - svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1'); - return svgCSS + svgText; - } - - /** - * Creates a XML document - * @private - */ - function createXML() { - if (!global.DOMParser) return; - return new DOMParser().parseFromString('<xml />', 'application/xml'); - } - - /** * Prevents a function from being called too often, waits until a timer elapses to call it again * * @param fn Function to call */ function debounce(fn) { @@ -1767,10 +1569,11 @@ debounce: 100, ratio: 1, supportsCanvas: false, supportsSVG: false, lineWrapRatio: 0.9, + dataAttr: 'data-src', renderers: ['html', 'canvas', 'svg'] }; App.dpr = function(val) { return val * App.setup.ratio; @@ -1783,21 +1586,20 @@ resizableImages: [], invisibleImages: {}, invisibleId: 0, visibilityCheckStarted: false, debounceTimer: null, - cache: {}, - dataAttr: 'data-src' + cache: {} }; //Pre-flight (function() { var devicePixelRatio = 1, backingStoreRatio = 1; - var canvas = newEl('canvas'); + var canvas = DOM.newEl('canvas'); var ctx = null; if (canvas.getContext) { if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) { App.setup.renderer = 'canvas'; @@ -1845,13 +1647,121 @@ module.exports = Holder; /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, -/* 1 */ +/* 2 */ /***/ function(module, exports, __webpack_require__) { + //Modified version of component/querystring + //Changes: updated dependencies, dot notation parsing, JSHint fixes + //Fork at https://github.com/imsky/querystring + + /** + * Module dependencies. + */ + + var encode = encodeURIComponent; + var decode = decodeURIComponent; + var trim = __webpack_require__(10); + var type = __webpack_require__(9); + + var arrayRegex = /(\w+)\[(\d+)\]/; + var objectRegex = /\w+\.\w+/; + + /** + * Parse the given query `str`. + * + * @param {String} str + * @return {Object} + * @api public + */ + + exports.parse = function(str){ + if ('string' !== typeof str) return {}; + + str = trim(str); + if ('' === str) return {}; + if ('?' === str.charAt(0)) str = str.slice(1); + + var obj = {}; + var pairs = str.split('&'); + for (var i = 0; i < pairs.length; i++) { + var parts = pairs[i].split('='); + var key = decode(parts[0]); + var m, ctx, prop; + + if (m = arrayRegex.exec(key)) { + obj[m[1]] = obj[m[1]] || []; + obj[m[1]][m[2]] = decode(parts[1]); + continue; + } + + if (m = objectRegex.test(key)) { + m = key.split('.'); + ctx = obj; + + while (m.length) { + prop = m.shift(); + + if (!prop.length) continue; + + if (!ctx[prop]) { + ctx[prop] = {}; + } else if (ctx[prop] && typeof ctx[prop] !== 'object') { + break; + } + + if (!m.length) { + ctx[prop] = decode(parts[1]); + } + + ctx = ctx[prop]; + } + + continue; + } + + obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]); + } + + return obj; + }; + + /** + * Stringify the given `obj`. + * + * @param {Object} obj + * @return {String} + * @api public + */ + + exports.stringify = function(obj){ + if (!obj) return ''; + var pairs = []; + + for (var key in obj) { + var value = obj[key]; + + if ('array' == type(value)) { + for (var i = 0; i < value.length; ++i) { + pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i])); + } + continue; + } + + pairs.push(encode(key) + '=' + encode(obj[key])); + } + + return pairs.join('&'); + }; + + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + /*! * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license * * Specially modified to work with Holder.js */ @@ -2005,15 +1915,13 @@ } module.exports = typeof window !== "undefined" && _onDomReady(window); /***/ }, -/* 2 */ +/* 4 */ /***/ function(module, exports, __webpack_require__) { - var augment = __webpack_require__(5); - var SceneGraph = function(sceneProperties) { var nodeCount = 1; //todo: move merge to helpers section function merge(parent, child) { @@ -2021,105 +1929,111 @@ parent[prop] = child[prop]; } return parent; } - var SceneNode = augment.defclass({ - constructor: function(name) { - nodeCount++; - this.parent = null; - this.children = {}; - this.id = nodeCount; - this.name = 'n' + nodeCount; - if (name != null) { - this.name = name; - } - this.x = 0; - this.y = 0; - this.z = 0; - this.width = 0; - this.height = 0; - }, - resize: function(width, height) { - if (width != null) { - this.width = width; - } - if (height != null) { - this.height = height; - } - }, - moveTo: function(x, y, z) { - this.x = x != null ? x : this.x; - this.y = y != null ? y : this.y; - this.z = z != null ? z : this.z; - }, - add: function(child) { - var name = child.name; - if (this.children[name] == null) { - this.children[name] = child; - child.parent = this; - } else { - throw 'SceneGraph: child with that name already exists: ' + name; - } + var SceneNode = function(name) { + nodeCount++; + this.parent = null; + this.children = {}; + this.id = nodeCount; + this.name = 'n' + nodeCount; + if (typeof name !== 'undefined') { + this.name = name; } - }); + this.x = this.y = this.z = 0; + this.width = this.height = 0; + }; - var RootNode = augment(SceneNode, function(uber) { - this.constructor = function() { - uber.constructor.call(this, 'root'); - this.properties = sceneProperties; - }; - }); + SceneNode.prototype.resize = function(width, height) { + if (width != null) { + this.width = width; + } + if (height != null) { + this.height = height; + } + }; - var Shape = augment(SceneNode, function(uber) { - function constructor(name, props) { - uber.constructor.call(this, name); - this.properties = { - fill: '#000' - }; - if (props != null) { - merge(this.properties, props); - } else if (name != null && typeof name !== 'string') { - throw 'SceneGraph: invalid node name'; - } + SceneNode.prototype.moveTo = function(x, y, z) { + this.x = x != null ? x : this.x; + this.y = y != null ? y : this.y; + this.z = z != null ? z : this.z; + }; + + SceneNode.prototype.add = function(child) { + var name = child.name; + if (typeof this.children[name] === 'undefined') { + this.children[name] = child; + child.parent = this; + } else { + throw 'SceneGraph: child already exists: ' + name; } + }; - this.Group = augment.extend(this, { - constructor: constructor, - type: 'group' - }); + var RootNode = function() { + SceneNode.call(this, 'root'); + this.properties = sceneProperties; + }; - this.Rect = augment.extend(this, { - constructor: constructor, - type: 'rect' - }); + RootNode.prototype = new SceneNode(); - this.Text = augment.extend(this, { - constructor: function(text) { - constructor.call(this); - this.properties.text = text; - }, - type: 'text' - }); - }); + var Shape = function(name, props) { + SceneNode.call(this, name); + this.properties = { + 'fill': '#000000' + }; + if (typeof props !== 'undefined') { + merge(this.properties, props); + } else if (typeof name !== 'undefined' && typeof name !== 'string') { + throw 'SceneGraph: invalid node name'; + } + }; + Shape.prototype = new SceneNode(); + + var Group = function() { + Shape.apply(this, arguments); + this.type = 'group'; + }; + + Group.prototype = new Shape(); + + var Rect = function() { + Shape.apply(this, arguments); + this.type = 'rect'; + }; + + Rect.prototype = new Shape(); + + var Text = function(text) { + Shape.call(this); + this.type = 'text'; + this.properties.text = text; + }; + + Text.prototype = new Shape(); + var root = new RootNode(); - this.Shape = Shape; - this.root = root; + this.Shape = { + 'Rect': Rect, + 'Text': Text, + 'Group': Group + }; + this.root = root; return this; }; module.exports = SceneGraph; /***/ }, -/* 3 */ +/* 5 */ /***/ function(module, exports, __webpack_require__) { - /* WEBPACK VAR INJECTION */(function(global) {/** + /** * Shallow object clone and merge * * @param a Object A * @param b Object B * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties @@ -2173,35 +2087,11 @@ } } return buf.join(''); }; - /** - * Converts a value into an array of DOM nodes - * - * @param val A string, a NodeList, a Node, or an HTMLCollection - */ - exports.getNodeArray = function(val) { - var retval = null; - if (typeof(val) == 'string') { - retval = document.querySelectorAll(val); - } else if (global.NodeList && val instanceof global.NodeList) { - retval = val; - } else if (global.Node && val instanceof global.Node) { - retval = [val]; - } else if (global.HTMLCollection && val instanceof global.HTMLCollection) { - retval = val; - } else if (val instanceof Array) { - retval = val; - } else if (val === null) { - retval = []; - } - return retval; - }; - - /** * Checks if an image exists * * @param src URL of image * @param callback Callback to call once image status has been found */ @@ -2256,174 +2146,343 @@ return val === 'true' || val === 'yes' || val === '1' || val === 'on' || val === '✓'; } return !!val; }; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }, -/* 4 */ +/* 6 */ /***/ function(module, exports, __webpack_require__) { - //Modified version of component/querystring - //Changes: updated dependencies, dot notation parsing, JSHint fixes - //Fork at https://github.com/imsky/querystring + /* WEBPACK VAR INJECTION */(function(global) {var DOM = __webpack_require__(7); + var SVG_NS = 'http://www.w3.org/2000/svg'; + var NODE_TYPE_COMMENT = 8; + /** - * Module dependencies. + * Generic SVG element creation function + * + * @private + * @param svg SVG context, set to null if new + * @param width Document width + * @param height Document height */ + exports.initSVG = function(svg, width, height) { + var defs, style, initialize = false; - var encode = encodeURIComponent; - var decode = decodeURIComponent; - var trim = __webpack_require__(6); - var type = __webpack_require__(7); + if (svg && svg.querySelector) { + style = svg.querySelector('style'); + if (style === null) { + initialize = true; + } + } else { + svg = DOM.newEl('svg', SVG_NS); + initialize = true; + } - var arrayRegex = /(\w+)\[(\d+)\]/; - var objectRegex = /\w+\.\w+/; + if (initialize) { + defs = DOM.newEl('defs', SVG_NS); + style = DOM.newEl('style', SVG_NS); + DOM.setAttr(style, { + 'type': 'text/css' + }); + defs.appendChild(style); + svg.appendChild(defs); + } + //IE throws an exception if this is set and Chrome requires it to be set + if (svg.webkitMatchesSelector) { + svg.setAttribute('xmlns', SVG_NS); + } + + //Remove comment nodes + for (var i = 0; i < svg.childNodes.length; i++) { + if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) { + svg.removeChild(svg.childNodes[i]); + } + } + + //Remove CSS + while (style.childNodes.length) { + style.removeChild(style.childNodes[0]); + } + + DOM.setAttr(svg, { + 'width': width, + 'height': height, + 'viewBox': '0 0 ' + width + ' ' + height, + 'preserveAspectRatio': 'none' + }); + + return svg; + }; + /** - * Parse the given query `str`. - * - * @param {String} str - * @return {Object} - * @api public + * Converts serialized SVG to a string suitable for data URI use + * @param svgString Serialized SVG string + * @param [base64] Use base64 encoding for data URI */ + exports.svgStringToDataURI = function() { + var rawPrefix = 'data:image/svg+xml;charset=UTF-8,'; + var base64Prefix = 'data:image/svg+xml;charset=UTF-8;base64,'; - exports.parse = function(str){ - if ('string' !== typeof str) return {}; + return function(svgString, base64) { + if (base64) { + return base64Prefix + btoa(unescape(encodeURIComponent(svgString))); + } else { + return rawPrefix + encodeURIComponent(svgString); + } + }; + }(); - str = trim(str); - if ('' === str) return {}; - if ('?' === str.charAt(0)) str = str.slice(1); + /** + * Returns serialized SVG with XML processing instructions + * + * @private + * @param svg SVG context + * @param stylesheets CSS stylesheets to include + */ + exports.serializeSVG = function(svg, engineSettings) { + if (!global.XMLSerializer) return; + var serializer = new XMLSerializer(); + var svgCSS = ''; + var stylesheets = engineSettings.stylesheets; - var obj = {}; - var pairs = str.split('&'); - for (var i = 0; i < pairs.length; i++) { - var parts = pairs[i].split('='); - var key = decode(parts[0]); - var m, ctx, prop; + //External stylesheets: Processing Instruction method + if (engineSettings.svgXMLStylesheet) { + var xml = DOM.createXML(); + //Add <?xml-stylesheet ?> directives + for (var i = stylesheets.length - 1; i >= 0; i--) { + var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"'); + xml.insertBefore(csspi, xml.firstChild); + } - if (m = arrayRegex.exec(key)) { - obj[m[1]] = obj[m[1]] || []; - obj[m[1]][m[2]] = decode(parts[1]); - continue; + xml.removeChild(xml.documentElement); + svgCSS = serializer.serializeToString(xml); } - if (m = objectRegex.test(key)) { - m = key.split('.'); - ctx = obj; - - while (m.length) { - prop = m.shift(); + var svgText = serializer.serializeToString(svg); + svgText = svgText.replace(/\&amp;(\#[0-9]{2,}\;)/g, '&$1'); + return svgCSS + svgText; + }; - if (!prop.length) continue; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - if (!ctx[prop]) { - ctx[prop] = {}; - } else if (ctx[prop] && typeof ctx[prop] !== 'object') { - break; - } +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - if (!m.length) { - ctx[prop] = decode(parts[1]); - } + /* WEBPACK VAR INJECTION */(function(global) {/** + * Generic new DOM element function + * + * @private + * @param tag Tag to create + * @param namespace Optional namespace value + */ + exports.newEl = function(tag, namespace) { + if (!global.document) return; - ctx = ctx[prop]; - } + if (namespace == null) { + return document.createElement(tag); + } else { + return document.createElementNS(namespace, tag); + } + }; - continue; + /** + * Generic setAttribute function + * + * @private + * @param el Reference to DOM element + * @param attrs Object with attribute keys and values + */ + exports.setAttr = function(el, attrs) { + for (var a in attrs) { + el.setAttribute(a, attrs[a]); } + }; - obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]); - } - - return obj; + /** + * Creates a XML document + * @private + */ + exports.createXML = function() { + if (!global.DOMParser) return; + return new DOMParser().parseFromString('<xml />', 'application/xml'); }; /** - * Stringify the given `obj`. + * Converts a value into an array of DOM nodes * - * @param {Object} obj - * @return {String} - * @api public + * @param val A string, a NodeList, a Node, or an HTMLCollection */ + exports.getNodeArray = function(val) { + var retval = null; + if (typeof(val) == 'string') { + retval = document.querySelectorAll(val); + } else if (global.NodeList && val instanceof global.NodeList) { + retval = val; + } else if (global.Node && val instanceof global.Node) { + retval = [val]; + } else if (global.HTMLCollection && val instanceof global.HTMLCollection) { + retval = val; + } else if (val instanceof Array) { + retval = val; + } else if (val === null) { + retval = []; + } + return retval; + }; - exports.stringify = function(obj){ - if (!obj) return ''; - var pairs = []; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - for (var key in obj) { - var value = obj[key]; +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { - if ('array' == type(value)) { - for (var i = 0; i < value.length; ++i) { - pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i])); - } - continue; + var Color = function (color, options) { + //todo: support array->color conversion + //todo: support rgba, hsla, and rrggbbaa notation + if (typeof color !== 'string') return; + + if (color.charAt(0) === '#') { + color = color.slice(1); } - pairs.push(encode(key) + '=' + encode(obj[key])); - } + if (/[^a-f0-9]+/i.test(color)) return; - return pairs.join('&'); + if (color.length === 3) { + color = color.replace(/./g, '$&$&'); + } + + if (color.length !== 6) return; + + this.alpha = 1; + + if (options) { + this.alpha = options.alpha || this.alpha; + } + + colorSet.call(this, parseInt(color, 16)); }; + Color.rgbToHex = function (r, g, b) { + return (((r | 0) << 16) + ((g | 0) << 8) + (b | 0)).toString(16); + }; -/***/ }, -/* 5 */ -/***/ function(module, exports, __webpack_require__) { + /** + * Sets the color from a raw RGB888 integer + * @param raw RGB888 representation of color + */ + //todo: refactor into a more generic method + function colorSet (raw) { + this.rgb = {}; + this.yuv = {}; + this.raw = raw; - var Factory = function () {}; - var slice = Array.prototype.slice; + this.rgb.r = (raw & 0xFF0000) >> 16; + this.rgb.g = (raw & 0x00FF00) >> 8; + this.rgb.b = (raw & 0x0000FF); - var augment = function (base, body) { - var uber = Factory.prototype = typeof base === "function" ? base.prototype : base; - var prototype = new Factory(), properties = body.apply(prototype, slice.call(arguments, 2).concat(uber)); - if (typeof properties === "object") for (var key in properties) prototype[key] = properties[key]; - if (!prototype.hasOwnProperty("constructor")) return prototype; - var constructor = prototype.constructor; - constructor.prototype = prototype; - return constructor; + // BT.709 + this.yuv.y = 0.2126 * this.rgb.r + 0.7152 * this.rgb.g + 0.0722 * this.rgb.b; + this.yuv.u = -0.09991 * this.rgb.r - 0.33609 * this.rgb.g + 0.436 * this.rgb.b; + this.yuv.v = 0.615 * this.rgb.r - 0.55861 * this.rgb.g - 0.05639 * this.rgb.b; + + return this; + } + + /** + * Lighten or darken a color + * @param multiplier Amount to lighten or darken (-1 to 1) + */ + Color.prototype.lighten = function (multiplier) { + var r = this.rgb.r; + var g = this.rgb.g; + var b = this.rgb.b; + + var m = (255 * multiplier) | 0; + + return new Color(Color.rgbToHex(r + m, g + m, b + m)); }; - augment.defclass = function (prototype) { - var constructor = prototype.constructor; - constructor.prototype = prototype; - return constructor; + /** + * Output color in hex format + * @param addHash Add a hash character to the beginning of the output + */ + Color.prototype.toHex = function (addHash) { + return (addHash ? '#' : '') + this.raw.toString(16); }; - augment.extend = function (base, body) { - return augment(base, function (uber) { - this.uber = uber; - return body; - }); + /** + * Returns whether or not current color is lighter than another color + * @param color Color to compare against + */ + Color.prototype.lighterThan = function (color) { + if (!(color instanceof Color)) { + color = new Color(color); + } + + return this.yuv.y > color.yuv.y; }; - module.exports = augment; + /** + * Returns the result of mixing current color with another color + * @param color Color to mix with + * @param multiplier How much to mix with the other color + */ + /* + Color.prototype.mix = function (color, multiplier) { + if (!(color instanceof Color)) { + color = new Color(color); + } -/***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { + var r = this.rgb.r; + var g = this.rgb.g; + var b = this.rgb.b; + var a = this.alpha; - - exports = module.exports = trim; + var m = typeof multiplier !== 'undefined' ? multiplier : 0.5; - function trim(str){ - return str.replace(/^\s*|\s*$/g, ''); - } + //todo: write a lerp function + r = r + m * (color.rgb.r - r); + g = g + m * (color.rgb.g - g); + b = b + m * (color.rgb.b - b); + a = a + m * (color.alpha - a); - exports.left = function(str){ - return str.replace(/^\s*/, ''); + return new Color(Color.rgbToHex(r, g, b), { + 'alpha': a + }); }; + */ - exports.right = function(str){ - return str.replace(/\s*$/, ''); + /** + * Returns the result of blending another color on top of current color with alpha + * @param color Color to blend on top of current color, i.e. "Ca" + */ + //todo: see if .blendAlpha can be merged into .mix + Color.prototype.blendAlpha = function (color) { + if (!(color instanceof Color)) { + color = new Color(color); + } + + var Ca = color; + var Cb = this; + + //todo: write alpha blending function + var r = Ca.alpha * Ca.rgb.r + (1 - Ca.alpha) * Cb.rgb.r; + var g = Ca.alpha * Ca.rgb.g + (1 - Ca.alpha) * Cb.rgb.g; + var b = Ca.alpha * Ca.rgb.b + (1 - Ca.alpha) * Cb.rgb.b; + + return new Color(Color.rgbToHex(r, g, b)); }; + module.exports = Color; + /***/ }, -/* 7 */ +/* 9 */ /***/ function(module, exports, __webpack_require__) { /** * toString ref. */ @@ -2455,9 +2514,29 @@ val = val.valueOf ? val.valueOf() : Object.prototype.valueOf.apply(val) return typeof val; + }; + + +/***/ }, +/* 10 */ +/***/ function(module, exports, __webpack_require__) { + + + exports = module.exports = trim; + + function trim(str){ + return str.replace(/^\s*|\s*$/g, ''); + } + + exports.left = function(str){ + return str.replace(/^\s*/, ''); + }; + + exports.right = function(str){ + return str.replace(/\s*$/, ''); }; /***/ } /******/ ])