-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add <FaStaticSprite> placeholder to transform icons #138
base: master
Are you sure you want to change the base?
Changes from all commits
14ad126
3dd1080
7a4ab30
1feb0c3
e2f9491
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* eslint-env node */ | ||
'use strict'; | ||
|
||
const path = require('path'); | ||
const { getAbstractIcon } = require('./fontawesome-helpers'); | ||
|
||
const prefixToSpriteFile = { | ||
fas: 'solid', | ||
far: 'regular', | ||
fal: 'light', | ||
fad: 'duotone', | ||
fab: 'brands', | ||
}; | ||
|
||
/* | ||
```hbs | ||
<FaStaticIcon @prefix="fas" @icon="coffee" /> | ||
``` | ||
|
||
becomes | ||
|
||
```hbs | ||
<svg> | ||
<use xlink:href="fa-solid.svg#coffee"></use> | ||
</svg> | ||
``` | ||
*/ | ||
module.exports = class FaStaticSpriteTransformPlugin { | ||
constructor(env, options) { | ||
this.syntax = env.syntax; | ||
this.builders = env.syntax.builders; | ||
this.options = options; | ||
this.visitor = this.buildVisitor(); | ||
} | ||
|
||
static instantiate({ options }) { | ||
return { | ||
name: 'fontawesome-static-sprite-transform', | ||
plugin: env => new this(env, options), | ||
parallelBabel: { | ||
requireFile: __filename, | ||
buildUsing: 'instantiate', | ||
params: { options }, | ||
}, | ||
baseDir() { | ||
return `${__dirname}/..`; | ||
} | ||
}; | ||
} | ||
|
||
buildVisitor() { | ||
return { | ||
ElementNode: node => this.transformElementNode(node), | ||
}; | ||
} | ||
|
||
transformElementNode(node) { | ||
if (node.tag === 'FaStaticSprite') { | ||
const controlAttrs = node.attributes.filter(attr => attr.name.startsWith('@')); | ||
const passedAttributes = node.attributes.filter(attr => { | ||
return attr.name !== 'class' && !controlAttrs.includes(attr) | ||
}); | ||
const mappedAttributes = controlAttrs.reduce((obj, attr) => { | ||
obj[attr.name] = attr.value; | ||
return obj; | ||
}, {}); | ||
if (!mappedAttributes.hasOwnProperty('@icon')) { | ||
throw new Error( | ||
'<FaStaticSprite /> requires an @icon parameter'); | ||
} | ||
const iconName = mappedAttributes['@icon'].chars; | ||
const prefix = mappedAttributes.hasOwnProperty('@prefix') ? mappedAttributes['@prefix'].chars : this.options.defaultPrefix; | ||
|
||
//use a set to force uniqueness | ||
const cssClasses = new Set(); | ||
const icon = getAbstractIcon(iconName, prefix); | ||
if (!icon) { | ||
throw new Error(`icon "${iconName}" with prefix "${prefix}" could not be found.`); | ||
} | ||
icon.attributes.class.split(' ').forEach(str => cssClasses.add(str)); | ||
|
||
const passedCssClassAttr = node.attributes.find(attr => attr.name === 'class'); | ||
if (passedCssClassAttr) { | ||
//filter out any null/undefined/empty string falsey values | ||
const values = passedCssClassAttr.value.chars.split(' ').filter(Boolean); | ||
values.forEach(str => cssClasses.add(str)); | ||
} | ||
const hasTitle = mappedAttributes.hasOwnProperty('@title'); | ||
|
||
const defaultAttributes = [ | ||
{ key: 'class', value: [...cssClasses.values()].join(' ') }, | ||
{ key: 'role', value: 'img' }, | ||
{ key: 'focusable', value: String(hasTitle) }, | ||
{ key: 'aria-hidden', value: String(!hasTitle) }, | ||
{ key: 'xmlns', value: icon.attributes.xmlns }, | ||
].map(({key, value}) => { | ||
return this.builders.attr(key, this.builders.text(value)); | ||
}); | ||
const spriteFile = `${prefixToSpriteFile[prefix]}.svg`; | ||
const spritePath = path.join('assets', 'fa-sprites', spriteFile) | ||
|
||
const children = []; | ||
if (hasTitle) { | ||
const title = this.builders.text(mappedAttributes['@title'].chars); | ||
children.push(this.builders.element('title', null, null, [title])); | ||
} | ||
const xlink = this.builders.attr('xlink:href', this.builders.text(`${spritePath}#${iconName}`)); | ||
children.push(this.builders.element('use', [xlink])); | ||
|
||
return this.builders.element('svg', [...defaultAttributes, ...passedAttributes], null, children); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -48,8 +48,10 @@ | |
] | ||
}, | ||
"dependencies": { | ||
"@fortawesome/fontawesome-free": "^5.12.1", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was this intentionally added? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's where the sprites seem to live, are they somewhere else? I figured a zero install experience for free icons was better than documenting a separate install step. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, they live there and that's the correct place to get them from. But this would force people who are just using Pro to download the extra package. I also worry about someone who is trying to use an older version for one reason or another (we have those folks who aren't ready to upgrade) This is going to add a competing version of the package that might have to contend with hoisting. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, I'll replace with installation instructions. |
||
"@fortawesome/fontawesome-svg-core": "^1.2.0", | ||
"broccoli-file-creator": "^2.1.1", | ||
"broccoli-funnel": "^3.0.2", | ||
"broccoli-merge-trees": "^3.0.2", | ||
"broccoli-plugin": "^3.0.0", | ||
"broccoli-rollup": "^4.1.1", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'ember-qunit'; | ||
import { render } from '@ember/test-helpers'; | ||
import hbs from 'htmlbars-inline-precompile'; | ||
|
||
module('Integration | Transform | <FaStaticSprite>', function(hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
test('it renders solid coffee', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="coffee" | ||
@prefix="fas" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'false'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'true'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-coffee'); | ||
assert.dom('svg').hasClass('fa-w-20'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#coffee'); | ||
}); | ||
test('it renders solid coffee as the default prefix', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="coffee" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'false'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'true'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-coffee'); | ||
assert.dom('svg').hasClass('fa-w-20'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#coffee'); | ||
}); | ||
test('it renders solid atom', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="atom" | ||
@prefix="fas" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'false'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'true'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-atom'); | ||
assert.dom('svg').hasClass('fa-w-14'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#atom'); | ||
}); | ||
test('it renders brand fort-awesome', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="fort-awesome" | ||
@prefix="fab" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'false'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'true'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-fort-awesome'); | ||
assert.dom('svg').hasClass('fa-w-16'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/brands.svg#fort-awesome'); | ||
}); | ||
test('it renders with a title', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="coffee" | ||
@prefix="fas" | ||
@title="some title" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'true'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'false'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-coffee'); | ||
assert.dom('svg').hasClass('fa-w-20'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#coffee'); | ||
assert.dom('svg title').exists(); | ||
assert.dom('svg title').hasText('some title'); | ||
}); | ||
test('it renders with custom classes', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="coffee" | ||
@prefix="fas" | ||
class="foo bar foo-bar-baz" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'img'); | ||
assert.dom('svg').hasAttribute('focusable', 'false'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'true'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-coffee'); | ||
assert.dom('svg').hasClass('fa-w-20'); | ||
assert.dom('svg').hasClass('foo'); | ||
assert.dom('svg').hasClass('bar'); | ||
assert.dom('svg').hasClass('foo-bar-baz'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#coffee'); | ||
}); | ||
test('passed attributes override defaults', async function (assert) { | ||
await render(hbs`<FaStaticSprite | ||
@icon="coffee" | ||
@prefix="fas" | ||
role="button" | ||
focusable="true" | ||
aria-hidden="false" | ||
/>`); | ||
|
||
assert.dom('svg').hasAttribute('role', 'button'); | ||
assert.dom('svg').hasAttribute('focusable', 'true'); | ||
assert.dom('svg').hasAttribute('aria-hidden', 'false'); | ||
assert.dom('svg').hasClass('svg-inline--fa'); | ||
assert.dom('svg').hasClass('fa-coffee'); | ||
assert.dom('svg').hasClass('fa-w-20'); | ||
assert.dom('svg use').exists(); | ||
assert.dom('svg use').hasAttribute('xlink:href', 'assets/fa-sprites/solid.svg#coffee'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without the leading / on
${spritePath}
, it is not looking on root url /assets but /user/assets for example