├── .npmignore ├── .gitignore ├── .babelrc ├── package.json ├── license.txt ├── src ├── icon.d.ts └── icon.js └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | dist/* 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": [ 4 | "transform-react-jsx" 5 | ] 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iconify/react", 3 | "description": "Iconify icon component for React.", 4 | "author": "Vjacheslav Trushkin", 5 | "version": "1.1.4", 6 | "license": "MIT", 7 | "bugs": "https://github.com/iconify/iconify-react/issues", 8 | "homepage": "https://github.com/iconify/iconify-react", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/iconify/iconify-react.git" 12 | }, 13 | "scripts": { 14 | "build": "node build" 15 | }, 16 | "main": "dist/icon.js", 17 | "types": "dist/icon.d.ts", 18 | "devDependencies": { 19 | "@babel/core": "^7.12.13", 20 | "@babel/preset-env": "^7.12.13", 21 | "babel-plugin-transform-react-jsx": "^6.24.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vjacheslav Trushkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/icon.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the @iconify/react package. 3 | * 4 | * (c) Vjacheslav Trushkin 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { ComponentType } from 'react'; 11 | 12 | export interface IconifyIcon { 13 | // mandatory icon object 14 | icon: object; 15 | 16 | // should not have any children 17 | children?: never; 18 | 19 | // optional properties passed to node 20 | id?: string; 21 | className?: string; 22 | style?: object; 23 | 24 | // optional properties for icon 25 | color?: string; 26 | inline?: boolean | string; 27 | box?: boolean | string; 28 | 29 | // dimensions and alignment 30 | width?: number | string; 31 | height?: number | string; 32 | align?: string; 33 | 34 | // transformations 35 | hFlip?: boolean; 36 | vFlip?: boolean; 37 | flip?: string; 38 | rotate?: number | string; 39 | } 40 | 41 | declare const Icon: ComponentType; 42 | declare const InlineIcon: ComponentType; 43 | 44 | export default Icon; 45 | export { Icon, InlineIcon }; 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This repository contains old version 1 of Iconify for React. 2 | 3 | The latest version has been moved to Iconify monorepo: https://github.com/iconify/iconify/tree/master/packages/react 4 | 5 | # Iconify for React 6 | 7 | Iconify is a modern open source SVG alternative to glyph fonts. It is a unified framework, aimed to offer all popular icon sets with one easy to use syntax: Font Awesome, Material Design Icons, Jam Icons and several emoji sets: Noto Emoji, Twemoji, EmojiOne, Firefox OS Emoji. 8 | 9 | You can use over 40,000 icons without installing multiple dependencies. It can also be used with custom and premium icon sets. No fonts, no massive packages. 10 | 11 | Iconify for React generates separate files for each icon, so when compiling your application only icons you use in project will be bundled. That means you can use FontAwesome, MDI, Vaadin, EmojiOne and other icons on same page without loading massive amounts of data. 12 | 13 | ## Installation 14 | 15 | If you are using NPM: 16 | 17 | ```bash 18 | npm install @iconify/react 19 | ``` 20 | 21 | If you are using Yarn: 22 | 23 | ```bash 24 | yarn add @iconify/react 25 | ``` 26 | 27 | This package does not include icons. Icons are split into separate packages that available at NPM. See below. 28 | 29 | ## Usage 30 | 31 | Install `@iconify/react` and packages for selected icon sets. Import Icon and/or InlineIcon from `@iconify/react` and icon data for icon you want to use: 32 | 33 | ```typescript 34 | import { Icon, InlineIcon } from "@iconify/react"; 35 | import home from "@iconify/icons-mdi-light/home"; 36 | import faceWithMonocle from "@iconify/icons-twemoji/face-with-monocle"; 37 | ``` 38 | 39 | Then use Icon or InlineIcon component with icon data as "icon" parameter: 40 | 41 | ```html 42 | 43 |

This is some text with

44 | ``` 45 | 46 | ## Icon and InlineIcon 47 | 48 | Both components are the same, the only difference is InlineIcon has negative vertical alignment, so it behaves like a glyph. 49 | 50 | Use Icon for decorations, InlineIcon if you are migrating from glyph font. 51 | 52 | ## Properties 53 | 54 | You can pass any custom properties to Icon and InlineIcon. 55 | 56 | Custom properties: 57 | 58 | * icon - required property, value is icon data. 59 | * width, height - width and/or height of icon. If one attribute is set, other attribute will be calculated using width/height ratio of icon. Default value is "1em". 60 | * hFlip, vFlip - boolean attributes. You can use them to flip icon vertically or horizontally 61 | * flip - string attribute, same as hFlip and vFlip. Value is "horizontal", "vertical" or "horizontal,vertical" 62 | * rotate - rotate icon. Value is number 0-3 (1 = 90deg, 2 = 180deg, 3 = 270deg) or string "90deg", "180deg", "270deg" 63 | * color - icon color, usable only for colorless icons. By default colorless icons use currentColor, so you can set color using stylesheet by setting text color. This property can override it. 64 | * align - icon alignment. It matters only when width and height are both set and width/height ratio doesn't match icon ratio. Value is a string that includes any of these values separated by comma: horizontal alignment: "left", "center", "right", vertical alignment: "top", "middle", "bottom", slice: "meet", "slice". Example: align="left,middle,slice". Default value is "center,middle,meet" 65 | 66 | ## TypeScript 67 | 68 | Iconify for React is compatible with TypeScript. 69 | 70 | If you are using TypeScript, only attributes "id", "className" and "style" are passed to node. If you want to pass other custom attributes, edit dist/icon.d.ts file (or suggest a change by opening issue on @iconify/react repository). 71 | 72 | ## Icon Packages 73 | 74 | As of version 1.1.0 this package no longer includes icons. There are over 40k icons, each in its own file. That takes a lot of disc space. Because of that icons were moved to multiple separate packages, one package per icon set. 75 | 76 | You can find all available icons at https://iconify.design/icon-sets/ 77 | 78 | Browse or search icons, click any icon and you will see a "React" tab that will give you exact code for the React component. 79 | 80 | Import format for each icon is "@iconify/icon-{prefix}/{icon}" where {prefix} is collection prefix, and {icon} is the icon name. 81 | 82 | Usage examples for a few popular icon packages: 83 | 84 | ### Material Design Icons 85 | 86 | Package: https://www.npmjs.com/package/@iconify/icons-mdi 87 | 88 | Icons list: https://iconify.design/icon-sets/mdi/ 89 | 90 | Installation: 91 | 92 | ```bash 93 | npm install @iconify/icons-mdi 94 | ``` 95 | 96 | Usage: 97 | 98 | ```typescript 99 | import { Icon, InlineIcon } from "@iconify/react"; 100 | import home from "@iconify/icons-mdi/home"; 101 | import accountCheck from "@iconify/icons-mdi/account-check"; 102 | ``` 103 | 104 | ```html 105 | 106 |

This is some text with

107 | ``` 108 | 109 | ### Simple Icons (big collection of logos) 110 | 111 | Package: https://www.npmjs.com/package/@iconify/icons-simple-icons 112 | 113 | Icons list: https://iconify.design/icon-sets/simple-icons/ 114 | 115 | Installation: 116 | 117 | ```bash 118 | npm install @iconify/icons-simple-icons 119 | ``` 120 | 121 | Usage: 122 | 123 | ```typescript 124 | import { Icon, InlineIcon } from "@iconify/react"; 125 | import behanceIcon from "@iconify/icons-simple-icons/behance"; 126 | import mozillafirefoxIcon from "@iconify/icons-simple-icons/mozillafirefox"; 127 | ``` 128 | 129 | ```html 130 | 131 |

Mozilla Firefox is the best browser!

132 | ``` 133 | 134 | ### Font Awesome 5 Solid 135 | 136 | Package: https://www.npmjs.com/package/@iconify/icons-fa-solid 137 | 138 | Icons list: https://iconify.design/icon-sets/fa-solid/ 139 | 140 | Installation: 141 | 142 | ```bash 143 | npm install @iconify/icons-fa-solid 144 | ``` 145 | 146 | Usage: 147 | 148 | ```typescript 149 | import { Icon, InlineIcon } from "@iconify/react"; 150 | import toggleOn from "@iconify/icons-fa-solid/toggle-on"; 151 | import chartBar from "@iconify/icons-fa-solid/chart-bar"; 152 | ``` 153 | 154 | ```html 155 | 156 |

Click to toggle

157 | ``` 158 | 159 | ### Noto Emoji 160 | 161 | Package: https://www.npmjs.com/package/@iconify/icons-noto 162 | 163 | Icons list: https://iconify.design/icon-sets/noto/ 164 | 165 | Installation: 166 | 167 | ```bash 168 | npm install @iconify/icons-noto 169 | ``` 170 | 171 | Usage: 172 | 173 | ```typescript 174 | import { Icon, InlineIcon } from "@iconify/react"; 175 | import greenApple from "@iconify/icons-noto/green-apple"; 176 | import huggingFace from "@iconify/icons-noto/hugging-face"; 177 | ``` 178 | 179 | ```html 180 | 181 |

Its time for hugs !

182 | ``` 183 | 184 | ### Other icon sets 185 | 186 | There are over 50 icon sets. This readme shows only few examples. See [Iconify icon sets](http://iconify.design/icon-sets/) for a full list of available icon sets. Click any icon to see code. 187 | 188 | ## License 189 | 190 | React component is released with MIT license. 191 | 192 | © 2019 Vjacheslav Trushkin 193 | 194 | See [Iconify icon sets page](https://iconify.design/icon-sets/) for list of collections and their licenses. 195 | -------------------------------------------------------------------------------- /src/icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the @iconify/react package. 3 | * 4 | * (c) Vjacheslav Trushkin 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import React from 'react'; 11 | 12 | /** 13 | * Unique id counter 14 | * 15 | * @type {number} 16 | */ 17 | let idCounter = 0; 18 | 19 | /** 20 | * Regex used to split dimensions 21 | * 22 | * @type {RegExp} 23 | * @private 24 | */ 25 | const unitsSplit = /(-?[0-9.]*[0-9]+[0-9.]*)/g; 26 | const unitsTest = /^-?[0-9.]*[0-9]+[0-9.]*$/g; 27 | 28 | /** 29 | * Attributes used for icon 30 | * 31 | * @type {string[]} 32 | */ 33 | const iconAttributes = ['width', 'height', 'inline', 'hFlip', 'vFlip', 'flip', 'rotate', 'align', 'color', 'box']; 34 | 35 | /** 36 | * Default attribute values 37 | * 38 | * @type {object} 39 | */ 40 | const defaultAttributes = { 41 | left: 0, 42 | top: 0, 43 | width: 16, 44 | height: 16, 45 | rotate: 0, 46 | hFlip: false, 47 | vFlip: false 48 | }; 49 | 50 | /** 51 | * Add missing properties to icon 52 | * 53 | * Important: in PHP version of this library this function is part of Collection class: Collection::addMissingAttributes() 54 | * 55 | * JavaScript version uses separate file so this function could be used in React and other components without loading 56 | * entire Collection class. 57 | * 58 | * @param {object} data 59 | * @return {object} 60 | */ 61 | function normalize(data) { 62 | // Object.create, compatible with IE11 63 | let item = Object.create(null); 64 | let key; 65 | for (key in defaultAttributes) { 66 | item[key] = defaultAttributes[key]; 67 | } 68 | for (key in data) { 69 | item[key] = data[key]; 70 | } 71 | 72 | // Attributes derived from other attributes 73 | if (item.inlineTop === void 0) { 74 | item.inlineTop = item.top; 75 | } 76 | if (item.inlineHeight === void 0) { 77 | item.inlineHeight = item.height; 78 | } 79 | if (item.verticalAlign === void 0) { 80 | // -0.143 if icon is designed for 14px height, 81 | // otherwise assume icon is designed for 16px height 82 | item.verticalAlign = item.height % 7 === 0 && item.height % 8 !== 0 ? -0.143 : -0.125; 83 | } 84 | return item; 85 | } 86 | 87 | /** 88 | * Get preserveAspectRatio attribute value 89 | * 90 | * @param {object} align 91 | * @return {string} 92 | * @private 93 | */ 94 | function getAlignment(align) { 95 | let result; 96 | switch (align.horizontal) { 97 | case 'left': 98 | result = 'xMin'; 99 | break; 100 | 101 | case 'right': 102 | result = 'xMax'; 103 | break; 104 | 105 | default: 106 | result = 'xMid'; 107 | } 108 | switch (align.vertical) { 109 | case 'top': 110 | result += 'YMin'; 111 | break; 112 | 113 | case 'bottom': 114 | result += 'YMax'; 115 | break; 116 | 117 | default: 118 | result += 'YMid'; 119 | } 120 | result += align.slice ? ' slice' : ' meet'; 121 | return result; 122 | } 123 | 124 | /** 125 | * SVG class 126 | * 127 | * @see @iconify/json-tools/src/svg.js 128 | */ 129 | class SVG { 130 | /** 131 | * Constructor 132 | * 133 | * @param icon Icon data 134 | * Use Collection.getIconData() to retrieve icon data 135 | */ 136 | constructor(icon) { 137 | this._item = icon; 138 | } 139 | 140 | /** 141 | * Get SVG attributes 142 | * 143 | * @param {object} props Custom properties (same as query string in Iconify API) 144 | * @returns {string} 145 | */ 146 | getAttributes(props) { 147 | let item = this._item; 148 | if (typeof props !== 'object') { 149 | props = Object.create(null); 150 | } 151 | 152 | // Set data 153 | let align = { 154 | horizontal: 'center', 155 | vertical: 'middle', 156 | slice: false 157 | }; 158 | let transform = { 159 | rotate: item.rotate, 160 | hFlip: item.hFlip, 161 | vFlip: item.vFlip 162 | }; 163 | let style = Object.create(null); 164 | 165 | let attributes = Object.create(null); 166 | 167 | // Get width/height 168 | let inline = props.inline === true || props.inline === 'true' || props.inline === '1'; 169 | 170 | let box = { 171 | left: item.left, 172 | top: inline ? item.inlineTop : item.top, 173 | width: item.width, 174 | height: inline ? item.inlineHeight : item.height 175 | }; 176 | 177 | // Transformations 178 | ['hFlip', 'vFlip'].forEach(key => { 179 | if (props[key] !== void 0 && (props[key] === true || props[key] === 'true' || props[key] === '1')) { 180 | transform[key] = !transform[key]; 181 | } 182 | }); 183 | if (props.flip !== void 0) { 184 | props.flip.toLowerCase().split(/[\s,]+/).forEach(value => { 185 | switch (value) { 186 | case 'horizontal': 187 | transform.hFlip = !transform.hFlip; 188 | break; 189 | 190 | case 'vertical': 191 | transform.vFlip = !transform.vFlip; 192 | } 193 | }); 194 | } 195 | if (props.rotate !== void 0) { 196 | let value = props.rotate; 197 | if (typeof value === 'number') { 198 | transform.rotate += value; 199 | } else if (typeof value === 'string') { 200 | let units = value.replace(/^-?[0-9.]*/, ''); 201 | if (units === '') { 202 | value = parseInt(value); 203 | if (!isNaN(value)) { 204 | transform.rotate += value; 205 | } 206 | } else if (units !== value) { 207 | let split = false; 208 | switch (units) { 209 | case '%': 210 | // 25% -> 1, 50% -> 2, ... 211 | split = 25; 212 | break; 213 | 214 | case 'deg': 215 | // 90deg -> 1, 180deg -> 2, ... 216 | split = 90; 217 | } 218 | if (split) { 219 | value = parseInt(value.slice(0, value.length - units.length)); 220 | if (!isNaN(value)) { 221 | transform.rotate += Math.round(value / split); 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | // Apply transformations to box 229 | let transformations = [], 230 | tempValue; 231 | if (transform.hFlip) { 232 | if (transform.vFlip) { 233 | transform.rotate += 2; 234 | } else { 235 | // Horizontal flip 236 | transformations.push('translate(' + (box.width + box.left) + ' ' + (0 - box.top) + ')'); 237 | transformations.push('scale(-1 1)'); 238 | box.top = box.left = 0; 239 | } 240 | } else if (transform.vFlip) { 241 | // Vertical flip 242 | transformations.push('translate(' + (0 - box.left) + ' ' + (box.height + box.top) + ')'); 243 | transformations.push('scale(1 -1)'); 244 | box.top = box.left = 0; 245 | } 246 | switch (transform.rotate % 4) { 247 | case 1: 248 | // 90deg 249 | tempValue = box.height / 2 + box.top; 250 | transformations.unshift('rotate(90 ' + tempValue + ' ' + tempValue + ')'); 251 | // swap width/height and x/y 252 | if (box.left !== 0 || box.top !== 0) { 253 | tempValue = box.left; 254 | box.left = box.top; 255 | box.top = tempValue; 256 | } 257 | if (box.width !== box.height) { 258 | tempValue = box.width; 259 | box.width = box.height; 260 | box.height = tempValue; 261 | } 262 | break; 263 | 264 | case 2: 265 | // 180deg 266 | transformations.unshift('rotate(180 ' + (box.width / 2 + box.left) + ' ' + (box.height / 2 + box.top) + ')'); 267 | break; 268 | 269 | case 3: 270 | // 270deg 271 | tempValue = box.width / 2 + box.left; 272 | transformations.unshift('rotate(-90 ' + tempValue + ' ' + tempValue + ')'); 273 | // swap width/height and x/y 274 | if (box.left !== 0 || box.top !== 0) { 275 | tempValue = box.left; 276 | box.left = box.top; 277 | box.top = tempValue; 278 | } 279 | if (box.width !== box.height) { 280 | tempValue = box.width; 281 | box.width = box.height; 282 | box.height = tempValue; 283 | } 284 | break; 285 | } 286 | 287 | // Calculate dimensions 288 | // Values for width/height: null = default, 'auto' = from svg, false = do not set 289 | // Default: if both values aren't set, height defaults to '1em', width is calculated from height 290 | let customWidth = props.width ? props.width : null; 291 | let customHeight = props.height ? props.height : null; 292 | 293 | let width, height; 294 | if (customWidth === null && customHeight === null) { 295 | customHeight = '1em'; 296 | } 297 | if (customWidth !== null && customHeight !== null) { 298 | width = customWidth; 299 | height = customHeight; 300 | } else if (customWidth !== null) { 301 | width = customWidth; 302 | height = SVG.calculateDimension(width, box.height / box.width); 303 | } else { 304 | height = customHeight; 305 | width = SVG.calculateDimension(height, box.width / box.height); 306 | } 307 | 308 | if (width !== false) { 309 | attributes.width = width === 'auto' ? box.width : width; 310 | } 311 | if (height !== false) { 312 | attributes.height = height === 'auto' ? box.height : height; 313 | } 314 | 315 | // Add vertical-align for inline icon 316 | if (inline && item.verticalAlign !== 0) { 317 | style['vertical-align'] = item.verticalAlign + 'em'; 318 | } 319 | 320 | // Check custom alignment 321 | if (props.align !== void 0) { 322 | props.align.toLowerCase().split(/[\s,]+/).forEach(value => { 323 | switch (value) { 324 | case 'left': 325 | case 'right': 326 | case 'center': 327 | align.horizontal = value; 328 | break; 329 | 330 | case 'top': 331 | case 'bottom': 332 | case 'middle': 333 | align.vertical = value; 334 | break; 335 | 336 | case 'crop': 337 | align.slice = true; 338 | break; 339 | 340 | case 'meet': 341 | align.slice = false; 342 | } 343 | }); 344 | } 345 | 346 | // Generate viewBox and preserveAspectRatio attributes 347 | attributes.preserveAspectRatio = getAlignment(align); 348 | attributes.viewBox = box.left + ' ' + box.top + ' ' + box.width + ' ' + box.height; 349 | 350 | // Generate body 351 | let body = SVG.replaceIDs(item.body); 352 | 353 | if (props.color !== void 0) { 354 | body = body.replace(/currentColor/g, props.color); 355 | } 356 | if (transformations.length) { 357 | body = '' + body + ''; 358 | } 359 | if (props.box === true || props.box === 'true' || props.box === '1') { 360 | // Add transparent bounding box 361 | body += ''; 362 | } 363 | 364 | return { 365 | attributes: attributes, 366 | body: body, 367 | style: style 368 | }; 369 | } 370 | 371 | /** 372 | * Generate SVG 373 | * 374 | * @param {object} props Custom properties (same as query string in Iconify API) 375 | * @param {boolean} [addExtra] True if extra attributes should be added to SVG. 376 | * Due to lack of functions in JavaScript for escaping attributes, it is your job to make sure key and value are both properly escaped. Default value is false. 377 | * @returns {string} 378 | */ 379 | getSVG(props, addExtra) { 380 | let attributes = SVG.splitAttributes(props), 381 | data = this.getAttributes(attributes.icon); 382 | 383 | let svg = ' { 388 | svg += ' ' + attr + '="' + attributes.node[attr] + '"'; 389 | }); 390 | } 391 | 392 | // Add SVG attributes 393 | Object.keys(data.attributes).forEach(attr => { 394 | svg += ' ' + attr + '="' + data.attributes[attr] + '"'; 395 | }); 396 | 397 | // Add style with 360deg transformation to style to prevent subpixel rendering bug 398 | svg += ' style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);'; 399 | Object.keys(data.style).forEach(attr => { 400 | svg += ' ' + attr + ': ' + data.style[attr] + ';'; 401 | }); 402 | if (props && props.style !== void 0) { 403 | svg += props.style; 404 | } 405 | svg += '">'; 406 | 407 | svg += data.body + ''; 408 | 409 | return svg; 410 | } 411 | 412 | /** 413 | * Split attributes 414 | * 415 | * @param props 416 | * @return {{icon: {}, node: {}}} 417 | */ 418 | static splitAttributes(props) { 419 | let result = { 420 | icon: Object.create(null), 421 | node: Object.create(null) 422 | }; 423 | 424 | Object.keys(props).forEach(name => { 425 | result[iconAttributes.indexOf(name) === -1 ? 'node' : 'icon'][name] = props[name]; 426 | }); 427 | 428 | return result; 429 | } 430 | 431 | /** 432 | * Calculate second dimension when only 1 dimension is set 433 | * 434 | * @param {string|number} size One dimension (such as width) 435 | * @param {number} ratio Width/height ratio. 436 | * If size == width, ratio = height/width 437 | * If size == height, ratio = width/height 438 | * @param {number} [precision] Floating number precision in result to minimize output. Default = 100 439 | * @return {string|number|null} Another dimension, null on error 440 | */ 441 | static calculateDimension(size, ratio, precision) { 442 | if (ratio === 1) { 443 | return size; 444 | } 445 | 446 | precision = precision === void 0 ? 100 : precision; 447 | if (typeof size === 'number') { 448 | return Math.ceil(size * ratio * precision) / precision; 449 | } 450 | 451 | // split code into sets of strings and numbers 452 | let split = size.split(unitsSplit); 453 | if (split === null || !split.length) { 454 | return null; 455 | } 456 | let results = [], 457 | code = split.shift(), 458 | isNumber = unitsTest.test(code), 459 | num; 460 | 461 | while (true) { 462 | if (isNumber) { 463 | num = parseFloat(code); 464 | if (isNaN(num)) { 465 | results.push(code); 466 | } else { 467 | results.push(Math.ceil(num * ratio * precision) / precision); 468 | } 469 | } else { 470 | results.push(code); 471 | } 472 | 473 | // next 474 | code = split.shift(); 475 | if (code === void 0) { 476 | return results.join(''); 477 | } 478 | isNumber = !isNumber; 479 | } 480 | } 481 | 482 | /** 483 | * Replace IDs in SVG output with unique IDs 484 | * Fast replacement without parsing XML, assuming commonly used patterns. 485 | * 486 | * @param {string} body 487 | * @return {string} 488 | */ 489 | static replaceIDs(body) { 490 | let regex = /\sid="(\S+)"/g, 491 | ids = [], 492 | match, prefix; 493 | 494 | function strReplace(search, replace, subject) { 495 | let pos = 0; 496 | 497 | while ((pos = subject.indexOf(search, pos)) !== -1) { 498 | subject = subject.slice(0, pos) + replace + subject.slice(pos + search.length); 499 | pos += replace.length; 500 | } 501 | 502 | return subject; 503 | } 504 | 505 | // Find all IDs 506 | while (match = regex.exec(body)) { 507 | ids.push(match[1]); 508 | } 509 | if (!ids.length) { 510 | return body; 511 | } 512 | 513 | prefix = 'IconifyId-' + Date.now().toString(16) + '-' + (Math.random() * 0x1000000 | 0).toString(16) + '-'; 514 | 515 | // Replace with unique ids 516 | ids.forEach(function (id) { 517 | let newID = prefix + idCounter; 518 | idCounter++; 519 | body = strReplace('="' + id + '"', '="' + newID + '"', body); 520 | body = strReplace('="#' + id + '"', '="#' + newID + '"', body); 521 | body = strReplace('(#' + id + ')', '(#' + newID + ')', body); 522 | }); 523 | 524 | return body; 525 | } 526 | } 527 | 528 | /** 529 | * Create React component with SVG data 530 | * 531 | * @param {object} props 532 | * @param {boolean} inline 533 | * @return {null|React.Component} 534 | */ 535 | function component(props, inline) { 536 | if (typeof props.icon !== 'object') { 537 | return null; 538 | } 539 | 540 | // Split properties into SVG properties and icon properties 541 | let split = SVG.splitAttributes(props), 542 | iconProps = split.icon, 543 | customAttributes = split.node; 544 | 545 | delete customAttributes.icon; 546 | 547 | // Set default inline value 548 | if (iconProps.inline === void 0) { 549 | iconProps.inline = inline; 550 | } 551 | 552 | // Get SVG data 553 | let svg = new SVG(normalize(props.icon)); 554 | let iconData = svg.getAttributes(iconProps); 555 | 556 | // Set style 557 | let style = { 558 | transform: 'rotate(360deg)' 559 | }; 560 | 561 | if (iconData.style['vertical-align'] !== void 0) { 562 | style.verticalAlign = iconData.style['vertical-align']; 563 | } 564 | if (props.style !== void 0) { 565 | for (let key in props.style) { 566 | style[key] = props.style[key]; 567 | } 568 | } 569 | 570 | // Generate element attributes 571 | let attributes = { 572 | xmlns: 'http://www.w3.org/2000/svg', 573 | focusable: false, 574 | style: style 575 | }; 576 | 577 | let key; 578 | for (key in customAttributes) { 579 | attributes[key] = customAttributes[key]; 580 | } 581 | for (key in iconData.attributes) { 582 | attributes[key] = iconData.attributes[key]; 583 | } 584 | 585 | attributes.dangerouslySetInnerHTML = { __html: iconData.body }; 586 | 587 | // Generate SVG 588 | return React.createElement('svg', attributes, null); 589 | } 590 | 591 | /** 592 | * Icon without vertical alignment 593 | * 594 | * @param {object} props 595 | * @return {React.Component} 596 | * @constructor 597 | */ 598 | export const Icon = props => component(props, false); 599 | 600 | /** 601 | * Icon with vertical alignment 602 | * 603 | * @param {object} props 604 | * @return {React.Component} 605 | * @constructor 606 | */ 607 | export const InlineIcon = props => component(props, true); 608 | 609 | /** 610 | * Default export 611 | */ 612 | export default Icon; 613 | --------------------------------------------------------------------------------