question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Native support for SVG-use at icons

See original GitHub issue

Hi guys! I am starting to use SVG icons. An optimal way is to save these icons in a single file and display them with “svg use”. In my case, the SVG-collection is generated by an external process not related to quasar.

<q-icon>
  <svg><use xlink:href='/assets/icons.svg#folder'/></svg>
</q-icon>

is working perfect. Unfortunately there are still many places where this approach does not work. This procedure cannot be used e.g. for “dropdown-icon” and other specifications like these. Would it be possible to use an abbreviation that works similar to the “img” keyword, for example “svguse”? It could be used like this:

<q-btn-dropdown
   icon="svguse:/assets/icons.svg#folder"
   dropdown-icon="svguse:/assets/icons.svg#chevron-down'"
/>

Maybe this could become a general functionality, so it could be used everywhere like e.g. with q-img, too. IMHO advantages of the svg-use solution is that one can more easily create own icons with a low footprint and work towards getting rid of the symbol fonts.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:5
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

6reactions
webnoobcommented, Jul 20, 2020

Will be available when Quasar-v1.12.13 is released later today.

1reaction
Zireaelcommented, Jul 25, 2020

In similar fashion to svguse:, would it be possible to add just svg: switch that loads a string supplied as an inline svg?

Please see my qicon override attached that loads a string as a svg node. It also merges in attributes passed from the parent component (i.e. q-tab).

Extras:

  1. I have also a sanitizer function in there that strips out <script> tags from supplied svg string, but it probably won’t be required for general implementation.
  2. the file also contains an html: injector that loads the supplied string as pure HTML string/node as quasar icon. This needs to be a pre-compiled HTML - I couldn’t figure out how to load a .vue template here and get it compiled on the fly by the vue template compiler.

usage:

svg loaded as an icon in q-tab: <q-tab name="something" :icon="'svg:'+mail" label="something" extra_attribute="hello"/>

importing svg string into my component: import { mail } from "@/js/quasar/icons/myicons.js";

making the imported svg string available to vue template:

  data() {
    return {
      tab: "something",
      mail: mail,
    };
  },

qicon.txt

Current svg loader implementations in quasar have some drawbacks:

  • Svg-icon-format <-- weird syntax, let me just load my < svg>…< /svg> file without any gimmics
  • inlined svg <-- doesn’t work in elements that use q-icon under the hood, like q-tab
  • dynamic svg loaded as img: <-- doesn’t support css variables. It’s a ‘dumb’ svg loaded as image uri

@webnoob What do you think?

import Vue from "vue";

import SizeMixin from "./size.js";
import slot from "quasar/src/utils/slot.js";

export default Vue.extend({
  name: "QIcon",

  mixins: [SizeMixin],

  props: {
    name: String,
    color: String,
    left: Boolean,
    right: Boolean
  },

  computed: {
    type() {
      let cls;
      let icon = this.name;

      if (!icon) {
        return {
          cls: void 0,
          content: void 0
        };
      }

      const commonCls =
        "q-icon" +
        (this.left === true ? " on-left" : "") +
        (this.right === true ? " on-right" : "");

      if (icon.startsWith("img:") === true) {
        return {
          img: true,
          cls: commonCls,
          src: icon.substring(4)
        };
      }

      // add option to use inline svg as icon image
      if (icon.startsWith("svg:") === true) {
        // test svg string:
        // const safesvg = svg:<svg version='1.1' id='svg2' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 43.4 43.4' style='enable-background:new 0 0 43.4 43.4' xml:space='preserve'> <g id='g4' transform='matrix(0.01712,0,0,0.01712,-116.963,48.724999)'> <path id='path8' style='fill:%23263238' d='M8306.9-1579.1'/>	<path id='path10' style='fill:%2342A5F5' d='M9019.8-2110.5'/> <g id='g8856-6' transform='matrix(1.0887031,0,0,1.0887031,-870.07715,252.64209)'>	<circle id='circle8858-1' style='fill:%23FFFFFF' cx='8238.4' cy='-1682.4' r='1163.9'/>	<path id='path8860-5' style='fill:%23263238' d='M8428.9-1682.4c0,105.2-85.2,190.4-190.4,190.4h0c-105.2,0-190.4-85.3-190.4-190.4	c0,0,0,0,0,0c0-105.2,85.3-190.4,190.4-190.4C8343.6-1872.8,8428.9-1787.6,8428.9-1682.4z'/>	<path id='path8862-5' style='fill:%231976D2' d='M9083.7-2170.5c-41.4-71.2-91.7-136.9-149.6-195.5L8714-2238.9	c-68.6-58.6-146.6-103.5-229.8-133.2c-75.2,76.2-134.9,161.3-177.9,253.8c245-16.7,498.1,72,733,255.6l138.5-80	C9156.2-2022.3,9124.6-2098.8,9083.7-2170.5L9083.7-2170.5z'/>	<path id='path8864-4' style='fill:%2342A5F5' d='M9083.7-1194.4c41-71.5,72.8-147.9,94.6-227.3l-220.1-127.1	c16.5-88.7,16.3-178.7,0.4-265.6c-103.5-27-207.1-36.2-308.8-27.1c137,203.9,186.7,467.4,145.2,762.6l138.5,80	C8991.7-1057.6,9042.2-1123.2,9083.7-1194.4z'/> <path id='path8866-7' style='fill:%231976D2' d='M8238.5-706.4c82.4-0.2,164.4-10.9,244.1-31.8v-254.2	c85.1-30.1,162.9-75.2,230.3-132.4c-28.4-103.2-72.2-197.4-130.9-281c-108.1,220.5-311.4,395.4-587.8,507.1v160	C8073.9-717.6,8156-706.8,8238.5-706.4z'/>	<path id='path8868-6' style='fill:%2342A5F5' d='M7393.2-1194.3c41.4,71.2,91.7,136.9,149.6,195.5l220.1-127.1	c68.6,58.6,146.6,103.5,229.8,133.2c75.2-76.2,134.9-161.3,177.9-253.8c-245,16.7-498.1-72-733-255.6l-138.5,80	C7320.7-1342.5,7352.3-1266,7393.2-1194.3z'/>	<path id='path8870-5' style='fill:%231976D2' d='M7393.1-2170.4c-41,71.5-72.8,147.9-94.6,227.3l220.1,127.1	c-16.5,88.7-16.3,178.7-0.4,265.6c103.5,27,207.1,36.2,308.8,27.1c-137-203.9-186.7-467.4-145.2-762.6l-138.5-80	C7485.2-2307.3,7434.7-2241.6,7393.1-2170.4z'/>	<path id='path8872-6' style='fill:%2342A5F5' d='M8238.4-2658.4c-82.4,0.2-164.4,10.9-244.1,31.8v254.2	c-85.1,30.1-162.9,75.2-230.3,132.4c28.4,103.2,72.2,197.4,130.9,281c108.1-220.5,311.4-395.4,587.8-507.1v-160	C8403-2647.2,8320.9-2658,8238.4-2658.4z'/></g></g></svg>
        //
        // test svg string containing JavaScript tags:
        // const unsafesvg = `svg:<svg version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle cx="250" cy="250" r="50" fill="red"/> < script type="text/javascript"><![CDATA[ var KEY={w:87, a:65, s:83, d:68}; var moveSpeed=5; var circle=document.getElementsByTagName("circle")[0]; var x=circle.getAttribute('cx')*1, y=circle.getAttribute('cy')*1; document.documentElement.addEventListener('keydown',function(evt){switch (evt.keyCode){case KEY.w: circle.setAttribute('cy',y-=moveSpeed); // Alternatively: // circle.cy.baseVal.value=(y-=moveSpeed); break; case KEY.s: circle.setAttribute('cy',y+=moveSpeed); break; case KEY.a: circle.setAttribute('cx',x-=moveSpeed); break; case KEY.d: circle.setAttribute('cx',x+=moveSpeed); break;}},false);]]> < / script ><circle cx="250" cy="250" r="50" fill="red"/><circle cx="250" cy="250" r="50" fill="red"/><circle cx="250" cy="250" r="50" fill="red"/><script type="text/javascript"><![CDATA[ var KEY={w:87, a:65, s:83, d:68}; var moveSpeed=5; var circle=document.getElementsByTagName("circle")[0]; var x=circle.getAttribute('cx')*1, y=circle.getAttribute('cy')*1; document.documentElement.addEventListener('keydown',function(evt){switch (evt.keyCode){case KEY.w: circle.setAttribute('cy',y-=moveSpeed); // Alternatively: // circle.cy.baseVal.value=(y-=moveSpeed); break; case KEY.s: circle.setAttribute('cy',y+=moveSpeed); break; case KEY.a: circle.setAttribute('cx',x-=moveSpeed); break; case KEY.d: circle.setAttribute('cx',x+=moveSpeed); break;}},false);]]></script><circle cx="250" cy="250" r="50" fill="red"/><circle cx="250" cy="250" r="50" fill="red"/></svg>`
        //
        // split svg into elements regex:
        // /(<svg)(.*?)(>)(.*?)(<\/svg>)$/gims
        //
        // split svg attributes into groups regex:
        // /([^=]\w*\S\w*)=(("|').*?("|')|\d*)/gims
        //
        // svg may contain colour references in HEX which have # uri encoded as %23
        // use either decodeURIComponent() or below regex idea to decode %23 to #
        //  /((: *)(%23)[A-F 0-9]{3,6})+/gim
        // var replaced = Regex.Replace(text, pattern, m => m.Groups[1].Value + 'xyz' + m.Groups[3].Value)

        const svgImgRaw = decodeURIComponent(icon) + "";

        // sanitize string for script tags
        // regex:
        // /(<|%3C)\s*?script[\s\S]*?(>|%3E)[\s\S]*?(<|%3C)\s*?(\/|%2F)\s*?script[\s\S]*?(>|%3E)/gmis

        const sanitizeSvg = /(<|%3C)\s*?script[\s\S]*?(>|%3E)[\s\S]*?(<|%3C)\s*?(\/|%2F)\s*?script[\s\S]*?(>|%3E)/gims;
        // const svgImg = Regex.Replace(text, pattern, m => m.Groups[1].Value + 'xyz' + m.Groups[3].Value)
        // const svgImg = svgImgRaw.replace(/(<|%3C)\s*?script[\s\S]*?(>|%3E)[\s\S]*?(<|%3C)\s*?(\/|%2F)\s*?script[\s\S]*?(>|%3E)/gmis, '<!-- script tags inside svg are not safe and have been removed -->')

        var svgImg = svgImgRaw + "";

        let bugs = sanitizeSvg.exec(svgImgRaw);
        // console.log(nasty);

        while (bugs != null) {
          // console.log(bugs[0]);
          svgImg = svgImg.replace(
            bugs[0],
            "<!-- Script tags inside svg are not safe and have been removed. To load complex object use html: switch instead. -->"
          );
          bugs = sanitizeSvg.exec(svgImgRaw);
        }

        // console.log(nasty);
        // console.log(nice);
        //
        // split svg into elements and return them in svgObject
        const svgParse = /(<svg)(.*?)(>)(.*?)(<\/svg>)$/gim;
        let svgGroups = svgParse.exec(svgImg);
        const svgObject = {
          svg: "",
          attributes: "",
          paths: ""
        };
        while (svgGroups != null) {
          // console.log(svgGroups[0]) // entire svg string
          // console.log(svgGroups[1]) // '<svg'
          // console.log(svgGroups[2]) // '>'
          // console.log(svgGroups[3]) // attributes
          // console.log(svgGroups[4]) // paths
          // console.log(svgGroups[5]) // '</svg>'
          svgObject.svg = svgGroups[0];
          svgObject.attributes = svgGroups[2];
          svgObject.paths = svgGroups[4];
          svgGroups = svgParse.exec(svgImg);
        }

        // split svg attributes and return them in svgAttributes
        const attributeParse = /([^=]\w*\S\w*)=(("|').*?("|')|\d*)/gims;
        let attributeGroups = attributeParse.exec(svgObject.attributes);

        const svgAttributes = {};
        let attribute = {};

        while (attributeGroups != null) {
          // console.log(attributeGroups[0]) // entire attribute
          // console.log(attributeGroups[1]) // key
          // console.log(attributeGroups[2]) // value
          attribute[
            attributeGroups[1].toString().trim()
          ] = attributeGroups[2].replace(/['"]+/g, ""); // stringify, remove whitespace, remove double quotes and prepare attrubute key:value pair for merging
          Object.assign(svgAttributes, attribute); // merge new attribute into svgAttributes
          attributeGroups = attributeParse.exec(svgObject.attributes);
        }

        return {
          svg: true,
          cls:
            commonCls + (svgAttributes.class ? " " + svgAttributes.class : ""), // merge q-icon classes with svg root classes
          svgElements: svgObject,
          svgAttributes: svgAttributes
        };
      }

      // add option to use inline html
      if (icon.startsWith("html:") === true) {
        // test html string:
        // const htmlRaw = `html:<div id="main-page" class="article-page" dense dark><div class="banner"><div :class="container"> <h1>{{ article.title }}</h1><ArticleMeta :article="article" :actions="true"></ArticleMeta></div></div></div>`
        //
        // split html into elements regex:
        // /(<.*?>){1}(.*)(<.*?>)$/ims
        //
        // split html attributes into groups regex:
        // /([^=]\w*\S\w*)=(("|').*?("|')|\d*)/gims
        //
        // html may contain colour references in HEX which have # uri encoded as %23
        // use either decodeURIComponent() or below regex idea to decode %23 to #
        //  /((: *)(%23)[A-F 0-9]{3,6})+/gim
        // var replaced = Regex.Replace(text, pattern, m => m.Groups[1].Value + 'xyz' + m.Groups[3].Value)

        const htmlRaw = icon;
        // console.log(htmlRaw);

        // split html into elements and return them in svgObject
        const htmlTag = /<\W*([a-z0-9*]*).*?>/ims;
        const htmlParse = /(<.*?>){1}(.*)(<.*?>)$/ims;
        let htmlGroups = htmlParse.exec(htmlRaw);
        const htmlObject = {
          html: "",
          htmlTag: "",
          attributes: "",
          paths: ""
        };
        // while (htmlGroups != null) {
        htmlGroups = htmlParse.exec(htmlRaw);
        // console.log(htmlGroups[0]); // entire html string
        // console.log(htmlGroups[1]); // '<xxx class="abc">'
        // console.log(htmlGroups[2]); // innter HTML
        // console.log(htmlGroups[3]); // '</xxx>'
        htmlObject.html = htmlGroups[0];
        htmlObject.attributes = htmlGroups[1];
        htmlObject.paths = htmlGroups[2];
        // }
        // console.log(htmlObject);

        // get HTML tag of the string
        htmlObject.htmlTag = htmlTag.exec(htmlObject.attributes)[1];

        // split html attributes and return them in htmlAttributes
        const attributeParse = /([^=]\w*\S\w*)=(("|').*?("|')|\d*)/gims;
        let attributeGroups = attributeParse.exec(htmlObject.attributes);

        const htmlAttributes = {};
        let attribute = {};

        while (attributeGroups != null) {
          // console.log(attributeGroups[0]) // entire attribute
          // console.log(attributeGroups[1]) // key
          // console.log(attributeGroups[2]) // value
          // console.log(attributeGroups[3]) // opening apostrophe
          // console.log(attributeGroups[4]) // closing apostrophe
          attribute[
            attributeGroups[1].toString().trim()
          ] = attributeGroups[2].replace(/['"]+/g, ""); // stringify, remove whitespace, remove double quotes and prepare attrubute key:value pair for merging
          Object.assign(htmlAttributes, attribute); // merge new attribute into htmlAttributes
          attributeGroups = attributeParse.exec(htmlObject.attributes);
        }
        // console.log(htmlObject);
        // console.log(htmlAttributes);

        return {
          html: true,
          cls:
            commonCls +
            (htmlAttributes.class ? " " + htmlAttributes.class : ""), // merge q-icon classes with tag root classes
          Elements: htmlObject,
          Attributes: htmlAttributes
        };
      }

      let content = " ";

      if (
        /^fa[s|r|l|b|d]{0,1} /.test(icon) ||
        icon.startsWith("icon-") === true
      ) {
        cls = icon;
      } else if (icon.startsWith("bt-") === true) {
        cls = `bt ${icon}`;
      } else if (icon.startsWith("eva-") === true) {
        cls = `eva ${icon}`;
      } else if (/^ion-(md|ios|logo)/.test(icon) === true) {
        cls = `ionicons ${icon}`;
      } else if (icon.startsWith("ion-") === true) {
        cls = `ionicons ion-${
          this.$q.platform.is.ios === true ? "ios" : "md"
        }${icon.substr(3)}`;
      } else if (icon.startsWith("mdi-") === true) {
        cls = `mdi ${icon}`;
      } else if (icon.startsWith("iconfont ") === true) {
        cls = `${icon}`;
      } else if (icon.startsWith("ti-") === true) {
        cls = `themify-icon ${icon}`;
      } else {
        cls = "material-icons";

        if (icon.startsWith("o_") === true) {
          icon = icon.substring(2);
          cls += "-outlined";
        } else if (icon.startsWith("r_") === true) {
          icon = icon.substring(2);
          cls += "-round";
        } else if (icon.startsWith("s_") === true) {
          icon = icon.substring(2);
          cls += "-sharp";
        }
        content = icon;
      }

      return {
        cls:
          cls +
          " " +
          commonCls +
          (this.color !== void 0 ? ` text-${this.color}` : ""),
        content
      };
    }
  },

  render(h) {
    if (this.type.img === true) {
      return h("img", {
        staticClass: this.type.cls,
        style: this.sizeStyle,
        on: this.$listeners,
        attrs: { src: this.type.src }
      });
    } else if (this.type.svg === true) {
      return h("svg", {
        staticClass: this.type.cls,
        style: this.sizeStyle,
        on: this.$listeners,
        attrs: this.type.svgAttributes,
        // domProps:innerHTML is equivalent of v-html.
        domProps: { innerHTML: this.type.svgElements.paths }
      });
    } else if (this.type.html === true) {
      return h(this.type.Elements.htmlTag, {
        staticClass: this.type.cls,
        style: this.sizeStyle,
        on: this.$listeners,
        attrs: this.type.htmlAttributes,
        // domProps:innerHTML is equivalent of v-html.
        domProps: { innerHTML: this.type.Elements.paths }
      });
    } else {
      return h(
        "i",
        {
          staticClass: this.type.cls,
          style: this.sizeStyle,
          on: this.$listeners,
          attrs: { "aria-hidden": true }
        },
        [this.type.content, slot(this, "default")]
      );
    }
  }
});

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to: Use Angular Material's Mat-Icon with local SVG Images?
I wanted to use the Mat-Icon component out of the box. I still wanted to be able to change the colours of the...
Read more >
Angular Material SVG loading full icon library #4913 - GitHub
The only thing I am concerned about is how native Angular Material components use material icons, and how can them be adapted for...
Read more >
How To Use Custom SVG Icons in Angular Material
Let's learn how to use our own SVG icons in Angular Material components. In this tutorial, you will use the <mat-icon> component to...
Read more >
Let's End the Debate on Icon Fonts vs SVG Icons in 2021
Icon Fonts vs SVG Icons! This is a debate that shows no signs of abating and is splitting the dev community into two....
Read more >
decrease mat icon svg font weight - Stack Overflow
set SVG { path { fill: currentColor } } (globally) not stroke it was my mistake I set currentColor for stroke.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found