diff --git a/README.md b/README.md index 1fbe3a6..bd20249 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,6 @@ This plugin provides an ES6 Promise implementation. If the browser does not prov See [docs/Promise.md](./docs/Promise.md) for documentation. ## svg -This plugins provide an API for loading and bundling svg graphics. +This plugin loads an svg graphic and defines it in the DOM, so you can reference it in a `` tag. See [docs/svg.md](./docs/svg.md) for documentation. diff --git a/bower.json b/bower.json index fdd51f0..f831c0a 100644 --- a/bower.json +++ b/bower.json @@ -5,7 +5,8 @@ "dependencies": { "lie": ">=2.8", "requirejs": "2.1.x", - "requirejs-text": "2.0.x" + "requirejs-text": "2.0.x", + "requirejs-domready": "2.0.x" }, "devDependencies": { "jquery": ">=2.1" diff --git a/docs/svg.md b/docs/svg.md index 790530d..bcaacc1 100644 --- a/docs/svg.md +++ b/docs/svg.md @@ -5,20 +5,20 @@ title: requirejs-dplugins/svg # requirejs-dplugins/svg! -This plugin load svg graphics and declares them in one sprite. This sprite is automatically added to your DOM, so that you can reference included graphics in a `` tag. +This plugin loads an svg graphic and defines it in the DOM, so you can reference it in a `` tag. ## Example ```js define([ - "requirejs-dplugins/svg!./icon1.svg", - "requirejs-dplugins/svg!./icon2.svg" + "requirejs-dplugins/svg!./icon1.svg", // ... @@ -27,7 +27,7 @@ This will fetch `icon1.svg` and `icon2.svg` and add two symbols to the sprite. ``` -You can then use the icons anytime only with +You can then use the icons anytime with ``` @@ -35,19 +35,24 @@ You can then use the icons anytime only with ``` -If the first `` tag of your graphic holds an `id` attribute, this id will be used as the reference. Otherwise, the name of the file is used. +Note that the first `` tag of your graphic should have an `id` attribute which will be used as the reference. +It should also have a `viewBox` attribute. -## Build -The build step will merge all graphics in one sprite beforehand and save the result in a `.svg`. -When running the built version, this sprite is fetched as soon as one of the graphic inside is required. - - -## Creating graphics that work well with this plugin - -To work properly, your graphic should include a `viewBox` attribute. The `id` is optional. -As an example, here is the minimal markup your graphic should include: +As an example, here is the minimal markup your graphic should follow: ```svg ... ``` +## Build +`jsdom` is used during the build step to merge all graphics in one sprite beforehand and save the result in a `.svg`. +When running the built version, this sprite is fetched as soon as one of the graphics inside is required. + +Note that `jsdom` should be added to your application `devDependencies` property in `package.json` so it is +automatically installed with `npm install`. +The following command will do that automatically: + +```bash +$ npm install --save-dev jsdom +``` + diff --git a/samples/svg.html b/samples/svg.html index 6f3fa00..9a7c7d6 100644 --- a/samples/svg.html +++ b/samples/svg.html @@ -7,14 +7,11 @@ diff --git a/svg.js b/svg.js index 967c181..3a08927 100644 --- a/svg.js +++ b/svg.js @@ -1,13 +1,13 @@ /** * Svg loading plugin. * - * This plugins loads SVG files and merges them into one SVG sprite + * This plugin loads an svg graphic and defines it in the DOM, so you can reference it in a `` tag. * * @example: * To load the svg file `myicons/myicon.svg`: * ``` - * require(["requirejs-dplugins/svg!myicons/myicon.svg"], function (){ - * // myicon was added to the sprite + * require(["requirejs-dplugins/svg!myicons/myicon.svg"], function (myiconId){ + * // myicon was added to the DOM * }); * ``` * @module requirejs-dplugins/svg @@ -16,11 +16,14 @@ define([ "./has", "./Promise!", - "module" -], function (has, Promise, module) { + "module", + "require", + "requirejs-text/text", + "requirejs-domready/domReady" +], function (has, Promise, module, localRequire, textPlugin) { "use strict"; - var cache = {}, // paths of loaded svgs + var loaded = {}, // paths of loaded svgs SPRITE_ID = 'requirejs-dplugins-svg', sprite = null; @@ -31,47 +34,43 @@ define([ /** * Loads an svg file. * @param {string} path - The svg file to load. - * @param {Function} require - A local require function to use to load other modules. + * @param {Function} resourceRequire - A require function local to the module calling the svg plugin. * @param {Function} onload - A function to call when the specified svg file have been loaded. * @method */ - load: function (path, require, onload) { + load: function (path, resourceRequire, onload) { if (has("builder")) { // when building - cache[path] = true; + loaded[path] = true; onload(); } else { // when running - // special case: when running a built version // Replace graphic by corresponding sprite. + var idInLayer; var layersMap = module.config().layersMap; - if (layersMap) { - path = layersMap[path] || path; + if (layersMap && layersMap[path]) { + idInLayer = layersMap[path].id; + path = layersMap[path].redirectTo; } - var filename = getFilename(path); - if (path in cache) { - cache[path].then(function (graphic) { - onload(graphic.id); - }); - } else { - if (!sprite) { - sprite = createSprite(document, SPRITE_ID); - document.body.appendChild(sprite); - } - cache[path] = new Promise(function (resolve) { - require(['requirejs-text/text!' + path], function (svgText) { - var graphic = extractGraphic(document, svgText, filename), - symbol = createSymbol(document, graphic.id, graphic.element, graphic.viewBox); - sprite.appendChild(symbol); - cache[path] = graphic.id; - resolve(graphic); + if (!(path in loaded)) { + loaded[path] = new Promise(function (resolve) { + localRequire(["requirejs-domready/domReady!"], function () { + textPlugin.load(path, resourceRequire, function (svgText) { + if (!sprite) { + sprite = createSprite(document, SPRITE_ID); + document.body.appendChild(sprite); + } + var symbol = extractGraphicAsSymbol(document, svgText); + sprite.appendChild(symbol); + resolve(symbol.getAttribute("id")); + }); }); }); - - cache[path].then(function (graphic) { - onload(graphic.id); - }); } + + loaded[path].then(function (symbolId) { + onload(idInLayer || symbolId); + }); } } }; @@ -89,8 +88,8 @@ define([ * config: { * "requirejs-dplugins/svg": { * layersMap: { - * "file1.svg": "path/to/layer.svg", - * "file2.svg": "path/to/layer.svg" + * "file1.svg": {redirectTo: "path/to/layer.svg", id: "id-inside-file-1"}, + * "file2.svg": {redirectTo: "path/to/layer.svg", id: "id-inside-file-2"} * } * } * } @@ -101,9 +100,9 @@ define([ * and writes it to the modules layer. * @param {string} mid - Current module id. * @param {string} dest - Current svg sprite path. - * @param {Array} loadList - List of svg files contained in current sprite. + * @param {Array} loaded - Maps the paths of the svg files contained in current sprite to their ids. */ - writeConfig: function (write, mid, destMid, loadList) { + writeConfig: function (write, mid, destMid, loaded) { var svgConf = { config: {}, paths: {} @@ -111,9 +110,9 @@ define([ svgConf.config[mid] = { layersMap: {} }; - loadList.forEach(function (path) { - svgConf.config[mid].layersMap[path] = destMid; - }); + for (var path in loaded) { + svgConf.config[mid].layersMap[path] = {redirectTo: destMid, id: loaded[path]}; + } write("require.config(" + JSON.stringify(svgConf) + ");"); }, @@ -124,49 +123,75 @@ define([ * @param {Function} writePluginFiles - The write function provided by the builder to `writeFile`. * and writes it to the modules layer. * @param {string} dest - Current svg sprite path. - * @param {Array} loadList - List of svg files contained in current sprite. + * @param {Array} loaded - Maps the paths of the svg files contained in current sprite to their ids. */ - writeLayer: function (writePluginFiles, dest, loadList) { - var fs = require.nodeRequire("fs"), - jsdom = require.nodeRequire("jsdom").jsdom; - - var document = jsdom("").parentWindow.document; - var sprite = createSprite(document); + writeLayer: function (writePluginFiles, dest, loaded) { + function tryRequire(paths) { + var module; + var path = paths.shift(); + if (path) { + try { + // This is a node-require so it is synchronous. + module = require.nodeRequire(path); + } catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + return tryRequire(paths); + } else { + throw e; + } + } + } + return module; + } - loadList.forEach(function (path) { - var filename = getFilename(path), - svgText = fs.readFileSync(require.toUrl(path), "utf8"), - graphic = extractGraphic(document, svgText, filename), - symbol = createSymbol(document, graphic.id, graphic.element, graphic.viewBox); - sprite.appendChild(symbol); - }); + var fs = require.nodeRequire("fs"), + jsDomPath = require.getNodePath(require.toUrl(module.id).replace(/[^\/]*$/, "node_modules/jsdom")), + jsDomModule = tryRequire([jsDomPath, "jsdom"]); + + var path, url, svgText; + if (!jsDomModule) { + console.log(">> WARNING: Node module jsdom not found. Skipping SVG bundling. If you" + + " want SVG bundling run 'npm install jsdom' in your console."); + for (path in loaded) { + url = require.toUrl(path); + svgText = fs.readFileSync(url, "utf8"); + writePluginFiles(url, svgText); + } + return false; + } else { + var jsdom = jsDomModule.jsdom, + document = jsdom(""), + sprite = createSprite(document); + + for (path in loaded) { + url = require.toUrl(path); + svgText = fs.readFileSync(url, "utf8"); + var symbol = extractGraphicAsSymbol(document, svgText); + sprite.appendChild(symbol); + loaded[path] = symbol.getAttribute("id"); + } - writePluginFiles(dest, sprite.outerHTML); + writePluginFiles(dest, sprite.outerHTML); + return true; + } } }; - loadSVG.writeFile = function (pluginName, resource, require, write) { writePluginFiles = write; }; - loadSVG.addModules = function (pluginName, resource, addModules) { - addModules(["requirejs-text/text"]); - }; - loadSVG.onLayerEnd = function (write, layer) { if (layer.name && layer.path) { var dest = layer.path.replace(/^(.*\/)?(.*).js$/, "$1/$2.svg"), destMid = layer.name + ".svg"; - var loadList = Object.keys(cache); - // Write layer file and config - buildFunctions.writeLayer(writePluginFiles, dest, loadList); - buildFunctions.writeConfig(write, module.id, destMid, loadList); + var success = buildFunctions.writeLayer(writePluginFiles, dest, loaded); + success && buildFunctions.writeConfig(write, module.id, destMid, loaded); // Reset cache - cache = {}; + loaded = {}; } }; @@ -174,34 +199,25 @@ define([ return loadSVG; - - // takes a path and returns the filename - function getFilename(filepath) { - return filepath.replace(/.*\/(.*)\.svg$/, "$1"); - } - // makes a symbol out of an svg graphic - function extractGraphic(document, svgText, filename) { + function extractGraphicAsSymbol(document, svgText) { var div = document.createElement("div"); div.innerHTML = svgText; var element = div.querySelector("svg"), - id = element.getAttribute("id") || filename, - viewBox = element.getAttribute("viewbox") || element.getAttribute("viewBox") || ""; - return { - id: id, - viewBox: viewBox, - element: element - }; + id = element.getAttribute("id"), + viewBox = element.getAttribute("viewbox") || element.getAttribute("viewBox"), + symbol = createSymbol(document, id, element, viewBox); + return symbol; } // makes symbol from svg element function createSymbol(document, id, element, viewBox) { var symbol = document.createElementNS("http://www.w3.org/2000/svg", "symbol"); - symbol.setAttribute("id", id); while (element.firstChild) { symbol.appendChild(element.firstChild); } - viewBox && symbol.setAttribute("viewBox", viewBox); + typeof id === "string" && symbol.setAttribute("id", id); + typeof viewBox === "string" && symbol.setAttribute("viewBox", viewBox); return symbol; } diff --git a/tests/unit/resources/svg/icon-with-empty-id.svg b/tests/unit/resources/svg/icon-with-empty-id.svg new file mode 100644 index 0000000..ae820a9 --- /dev/null +++ b/tests/unit/resources/svg/icon-with-empty-id.svg @@ -0,0 +1,5 @@ + + + + diff --git a/tests/unit/resources/svg/icon-with-empty-viewBox.svg b/tests/unit/resources/svg/icon-with-empty-viewBox.svg new file mode 100644 index 0000000..0cc0a0b --- /dev/null +++ b/tests/unit/resources/svg/icon-with-empty-viewBox.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/tests/unit/resources/svg/icon-with-lowercase-viewbox.svg b/tests/unit/resources/svg/icon-with-lowercase-viewbox.svg new file mode 100644 index 0000000..36a81b0 --- /dev/null +++ b/tests/unit/resources/svg/icon-with-lowercase-viewbox.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/tests/unit/resources/svg/icon-without-id.svg b/tests/unit/resources/svg/icon-without-id.svg new file mode 100644 index 0000000..2593b67 --- /dev/null +++ b/tests/unit/resources/svg/icon-without-id.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/tests/unit/resources/svg/icon-without-viewBox.svg b/tests/unit/resources/svg/icon-without-viewBox.svg new file mode 100644 index 0000000..5fa19c4 --- /dev/null +++ b/tests/unit/resources/svg/icon-without-viewBox.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/tests/unit/resources/svg/never-loaded.svg b/tests/unit/resources/svg/never-loaded.svg index e45b594..f4e7747 100644 --- a/tests/unit/resources/svg/never-loaded.svg +++ b/tests/unit/resources/svg/never-loaded.svg @@ -1,4 +1,3 @@ - diff --git a/tests/unit/svg.js b/tests/unit/svg.js index 1b378c7..ffc8f51 100644 --- a/tests/unit/svg.js +++ b/tests/unit/svg.js @@ -7,11 +7,14 @@ define([ return require.config({ context: "svg" + context++, baseUrl: "../../../requirejs-dplugins", - paths: {lie: "../lie", "requirejs-text": "../requirejs-text/"}, + paths: {lie: "../lie", "requirejs-text": "../requirejs-text/", "requirejs-domready": "../requirejs-domready/"}, config: { "svg": { layersMap: { - "tests/unit/resources/svg/never-loaded.svg": "tests/unit/resources/svg/sprite.svg" + "tests/unit/resources/svg/never-loaded.svg": { + redirectTo: "tests/unit/resources/svg/sprite.svg", + id: "never-loaded" + } } } } @@ -34,7 +37,7 @@ define([ contextRequire([ "svg!tests/unit/resources/svg/icon1.svg", "svg!tests/unit/resources/svg/icon2.svg" - ], dfd.callback(function () { + ], dfd.callback(function (id1, id2) { var spriteContainer = document.getElementById(CONTAINER_ID); assert.isNotNull(spriteContainer, "Sprite was correctly created"); var icon1 = spriteContainer.querySelector("symbol#icon1"), @@ -45,6 +48,8 @@ define([ assert.isNotNull(icon2, "icon2 was correctly added"); assert.isNull(inexistentIcon, "inexistent-icon was not found as expected"); assert.strictEqual(symbols.length, 2, "total number of symbols found is correct"); + assert.strictEqual(id1, "icon1", "id1 is correct"); + assert.strictEqual(id2, "icon2", "id1 is correct"); })); }, "Checking svgs are correctly added to sprite": function () { @@ -70,10 +75,12 @@ define([ contextRequire([ "svg!tests/unit/resources/svg/icon1.svg", "svg!tests/unit/resources/svg/icon1.svg" - ], dfd.callback(function () { + ], dfd.callback(function (id1, id2) { var spriteContainer = document.getElementById(CONTAINER_ID); var svgs = spriteContainer.querySelectorAll("symbol#icon1"); assert.strictEqual(svgs.length, 1, "Icon was not added twice"); + assert.strictEqual(id1, "icon1", "Id1 is correct"); + assert.strictEqual(id2, "icon1", "Id2 is correct"); })); }, "Checking svg defined in sprite can't be reloaded": function () { @@ -81,12 +88,80 @@ define([ var contextRequire = getContextRequire(); contextRequire([ "svg!tests/unit/resources/svg/never-loaded.svg" - ], dfd.callback(function () { + ], dfd.callback(function (id) { var spriteContainer = document.getElementById(CONTAINER_ID); var svgs = spriteContainer.querySelectorAll("symbol#never-loaded"), sprite = spriteContainer.querySelectorAll("symbol#sprite"); assert.strictEqual(svgs.length, 0, "Icon was not loaded"); assert.strictEqual(sprite.length, 1, "Sprite was loaded instead"); + assert.strictEqual(id, "never-loaded", "Id is correct"); + })); + }, + "Loading an icon without an id": function () { + var dfd = this.async(); + var contextRequire = getContextRequire(); + contextRequire([ + "svg!tests/unit/resources/svg/icon-without-id.svg" + ], dfd.callback(function (id) { + var spriteContainer = document.getElementById(CONTAINER_ID); + var svgs = spriteContainer.querySelectorAll("symbol"); + assert.strictEqual(svgs.length, 1, "Icon without id was loaded"); + var idAttr = svgs[0].getAttribute("id"); + assert.strictEqual(idAttr, null, "Icon without id gets no id attribute in the sprite"); + assert.strictEqual(id, null, "svg! returns null when fetching a icon without id"); + })); + }, + "Loading an icon with an empty id": function () { + var dfd = this.async(); + var contextRequire = getContextRequire(); + contextRequire([ + "svg!tests/unit/resources/svg/icon-with-empty-id.svg" + ], dfd.callback(function (id) { + var spriteContainer = document.getElementById(CONTAINER_ID); + var svgs = spriteContainer.querySelectorAll("symbol"); + assert.strictEqual(svgs.length, 1, "Icon with empty id was loaded"); + var idAttr = svgs[0].getAttribute("id"); + assert.strictEqual(idAttr, "", "Icon without id gets no id attribute in the sprite"); + assert.strictEqual(id, "", "svg! returns null when fetching a icon without id"); + })); + }, + "Loading an icon without a viewBox": function () { + var dfd = this.async(); + var contextRequire = getContextRequire(); + contextRequire([ + "svg!tests/unit/resources/svg/icon-without-viewBox.svg" + ], dfd.callback(function () { + var spriteContainer = document.getElementById(CONTAINER_ID); + var svgs = spriteContainer.querySelectorAll("symbol"); + assert.strictEqual(svgs.length, 1, "Icon without viewBox was loaded"); + var viewBoxAttr = svgs[0].getAttribute("viewBox"); + assert.strictEqual(viewBoxAttr, null, "Icon without viewBox gets no viewBox attribute in the sprite"); + })); + }, + "Loading an icon with an empty viewBox": function () { + var dfd = this.async(); + var contextRequire = getContextRequire(); + contextRequire([ + "svg!tests/unit/resources/svg/icon-with-empty-viewBox.svg" + ], dfd.callback(function () { + var spriteContainer = document.getElementById(CONTAINER_ID); + var svgs = spriteContainer.querySelectorAll("symbol"); + assert.strictEqual(svgs.length, 1, "Icon with empty viewBox was loaded"); + var viewBoxAttr = svgs[0].getAttribute("viewBox"); + assert.strictEqual(viewBoxAttr, "", "Icon with empty viewBox gets an empty viewBox attribute in the sprite"); + })); + }, + "Loading an icon with a lowercase viewbox": function () { + var dfd = this.async(); + var contextRequire = getContextRequire(); + contextRequire([ + "svg!tests/unit/resources/svg/icon-with-lowercase-viewbox.svg" + ], dfd.callback(function (id) { + var spriteContainer = document.getElementById(CONTAINER_ID); + var svgs = spriteContainer.querySelectorAll("symbol"); + assert.strictEqual(svgs.length, 1, "Icon with lowercase viewbox was loaded"); + var viewBoxAttr = svgs[0].getAttribute("viewBox"); + assert.strictEqual(viewBoxAttr, "0 0 32 32", "lowercase viewbox attribute was converted to camel case viewBox"); })); } });