mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-13 12:39:14 +00:00
Compare commits
23 Commits
include-er
...
icon-fetch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b001f144c8 | ||
|
|
8a7a7dc3b0 | ||
|
|
eefa57f61a | ||
|
|
c69cabf413 | ||
|
|
7de111341e | ||
|
|
5558a0f2cd | ||
|
|
de4221365a | ||
|
|
b85e6bcdf3 | ||
|
|
ad539a00f2 | ||
|
|
1559b08a63 | ||
|
|
400db9a608 | ||
|
|
1f280b3b9f | ||
|
|
e9f2104f15 | ||
|
|
ad53a0aa56 | ||
|
|
d79d753f4c | ||
|
|
6306955c74 | ||
|
|
cff9d56b3f | ||
|
|
9fde4a820e | ||
|
|
34c0b562fe | ||
|
|
f499a932f9 | ||
|
|
85cca22d71 | ||
|
|
c261f25c65 | ||
|
|
ac1d412a8f |
9
docs/_data/systemIcons.js
Normal file
9
docs/_data/systemIcons.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { inlined } from '../../dist/components/icon/library.wa.js';
|
||||
|
||||
let { classic } = inlined;
|
||||
let { solid, regular } = classic;
|
||||
|
||||
export default [
|
||||
...Object.entries(solid).map(([name, svg]) => ({ name, family: 'solid', variant: 'solid', svg })),
|
||||
...Object.entries(regular).map(([name, svg]) => ({ name, family: 'regular', variant: 'regular', svg })),
|
||||
];
|
||||
@@ -141,7 +141,7 @@
|
||||
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
|
||||
<div slot="label">
|
||||
Gray colorfulness
|
||||
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="system:circle-xmark" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">Neutral</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
@change="tweaking.hue = tweaking.{{ hue }} = false">
|
||||
<div slot="label">
|
||||
Tweak {{ hue }} hue
|
||||
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="system:circle-xmark" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More {{hueBefore}}</div>
|
||||
@@ -204,7 +204,7 @@ style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
|
||||
@change="tweaking.chroma = false">
|
||||
<div slot="label">
|
||||
Overall colorfulness
|
||||
<wa-icon-button @click="chromaScale = 1" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
<wa-icon-button @click="chromaScale = 1" class="clear-button" name="system:circle-xmark" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More muted</div>
|
||||
|
||||
@@ -186,7 +186,8 @@ Custom icons can be loaded individually with the `src` attribute. Only SVGs on a
|
||||
|
||||
You can register additional icons to use with the `<wa-icon>` component through icon libraries. Icon files can exist locally or on a CORS-enabled endpoint (e.g. a CDN). There is no limit to how many icon libraries you can register and there is no cost associated with registering them, as individual icons are only requested when they're used.
|
||||
|
||||
Web Awesome ships with two built-in icon libraries, `default` and `system`. The [default icon library](#customizing-the-default-library) is provided courtesy of [Font Awesome](https://fontawesome.com/). The [system icon library](#customizing-the-system-library) contains only a small subset of icons that are used internally by Web Awesome components.
|
||||
Web Awesome ships with one built-in icon library: `default`.
|
||||
The [default icon library](#customizing-the-default-library) is provided courtesy of [Font Awesome](https://fontawesome.com/).
|
||||
|
||||
To register an additional icon library, use the `registerIconLibrary()` function that's exported from `dist/webawesome.js`. At a minimum, you must provide a name and a resolver function. The resolver function translates an icon name to a URL where the corresponding SVG file exists. Refer to the examples below to better understand how it works.
|
||||
|
||||
@@ -198,8 +199,9 @@ Here's an example that registers an icon library located in the `/assets/icons`
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('my-icons', {
|
||||
resolver: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
registerIconLibrary({
|
||||
name: 'my-icons',
|
||||
getUrl: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor')
|
||||
});
|
||||
</script>
|
||||
@@ -214,6 +216,289 @@ To display an icon, set the `library` and `name` attributes of an `<wa-icon>` el
|
||||
|
||||
If an icon is used before registration occurs, it will be empty initially but shown when registered.
|
||||
|
||||
### Customizing the Default Icon Library
|
||||
|
||||
The default icon library contains over 2,000 icons courtesy of [Font Awesome](https://fontawesome.com/).
|
||||
These are the icons that display when you use `<wa-icon>` without the `library` attribute.
|
||||
If you prefer to have these icons resolve to a different icon library, simply register it using the `default` name:
|
||||
|
||||
For example, this will change the default icon library to use [Bootstrap Icons](https://icons.getbootstrap.com/) loaded from the jsDelivr CDN.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('default', {
|
||||
name: 'default',
|
||||
getUrl: (name, family) => {
|
||||
const suffix = family === 'filled' ? '-fill' : '';
|
||||
return `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/${name}${suffix}.svg`
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
You can also register an existing library as the default:
|
||||
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
let myIcons = registerIconLibrary({
|
||||
name: 'my-icons',
|
||||
getUrl: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor')
|
||||
});
|
||||
|
||||
// Alias of my-icons to default
|
||||
registerIconLibrary('default', myIcons);
|
||||
</script>
|
||||
```
|
||||
|
||||
This allows you to have multiple libraries that co-exist, only one of which is the default.
|
||||
|
||||
### Customizing system icons
|
||||
|
||||
Web Awesome components use a number of icons internally.
|
||||
For example, the checkmark icon in `<wa-checkbox>` or the chevron used in `<wa-details>`.
|
||||
These icons have a `system:` prefix in their name, e.g. `system:check` or `system:chevron-down`.
|
||||
|
||||
To specify how these map to your icon library, you can define a `system()` function that maps Web Awesome’s system icons to your library’s icons.
|
||||
The `system()` function receives the icon name, family and variant as arguments and returns an object with the new `name`, `family`, and `variant`.
|
||||
For example, here is how to define system icon mappings for the Bootstrap Icons library:
|
||||
|
||||
```js
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
let system = {
|
||||
filled: ['circle', 'pause', 'play', 'star', 'user'],
|
||||
nameMap: {
|
||||
'check': 'check-lg',
|
||||
'circle': 'circle',
|
||||
'circle-xmark': 'x-circle',
|
||||
'eye-dropper': 'eyedropper',
|
||||
'eye': 'eye',
|
||||
'eye-slash': 'eye-slash',
|
||||
'grip-vertical': 'grip-vertical',
|
||||
'indeterminate': 'dash-lg',
|
||||
'minus': 'dash-lg',
|
||||
'pause': 'pause',
|
||||
'play': 'play',
|
||||
'user': 'person',
|
||||
'xmark': 'x-lg',
|
||||
}
|
||||
};
|
||||
|
||||
registerIconLibrary({
|
||||
name: 'bootstrap-icons',
|
||||
system(name, family) {
|
||||
return {
|
||||
// Transform names where different
|
||||
name: system.nameMap[name] || name,
|
||||
|
||||
// Default to non-filled as many of system's "solid" icons are not filled in Bootstrap Icons
|
||||
family: system.filled.includes(name) ? 'filled' : ''
|
||||
};
|
||||
},
|
||||
getUrl: (name, family) => {
|
||||
const suffix = family === 'filled' ? '-fill' : '';
|
||||
return `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/${name}${suffix}.svg`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can also specify a `library` key to resolve a system icon through a different library, e.g. WA’s default icon library.
|
||||
For example, the code above will try to resolve any system icons through your library and only fall back to WA's default library if they fail to load.
|
||||
This maximizes the odds that icons from your library are used, but can also be unpredictable.
|
||||
To resolve any system icon you have not vetted via the Web Awesome default library, you can use the `library` key:
|
||||
|
||||
```js
|
||||
let system = {
|
||||
filled: ['circle', 'pause', 'play', 'star', 'user'],
|
||||
nameMap: {
|
||||
'chevron-down': 'chevron-down',
|
||||
'chevron-left': 'chevron-left',
|
||||
'chevron-right': 'chevron-right',
|
||||
'check': 'check-lg',
|
||||
'circle': 'circle',
|
||||
'eye-dropper': 'eyedropper',
|
||||
'grip-vertical': 'grip-vertical',
|
||||
'indeterminate': 'dash-lg',
|
||||
'pause': 'pause',
|
||||
'play': 'play',
|
||||
'circle-xmark': 'x-circle',
|
||||
'grip-vertical': 'grip-vertical',
|
||||
'eye-slash': 'eye-slash',
|
||||
'eye': 'eye',
|
||||
'user': 'person',
|
||||
'xmark': 'x-lg',
|
||||
}
|
||||
};
|
||||
|
||||
registerIconLibrary({
|
||||
name: 'bootstrap-icons',
|
||||
system(name, family) {
|
||||
if (!system.nameMap[name]) {
|
||||
// If the icon is not known, use the default library
|
||||
return { library: 'wa', name, family, variant };
|
||||
}
|
||||
|
||||
return {
|
||||
name: system.nameMap[name],
|
||||
family: system.filled.includes(name) ? 'filled' : ''
|
||||
};
|
||||
},
|
||||
getUrl: (name, family) => {
|
||||
const suffix = family === 'filled' ? '-fill' : '';
|
||||
return `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/${name}${suffix}.svg`
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Index of All System Icons { #system-icons-index }
|
||||
|
||||
These are all system icons used currently by Web Awesome components (`system:` prefix omitted for readability):
|
||||
|
||||
| Name | Variant | Icon |
|
||||
| --- | --- | --- |
|
||||
{%- for icon in systemIcons %}
|
||||
| `{{ icon.name }}` | `{{ icon.variant }}` | <wa-icon library="wa" name="system:{{ icon.name }}" family="classic" variant="{{ icon.variant }}"></wa-icon> |
|
||||
{%- endfor %}
|
||||
|
||||
### Inlined icons { #inlined }
|
||||
|
||||
Inlined icons are SVG icons that are loaded directly from code rather than from HTTP requests, making them load faster in your application.
|
||||
Inlining critical icons improves performance by eliminating HTTP requests, reducing load times, and ensuring they are immediately available.
|
||||
As an example, all of [Web Awesome’s default system icons](#system-icons-index) are inlined, and we strongly advise you inline the corresponding icons in your custom icon libraries as well.
|
||||
|
||||
#### How to Use Inlined Icons
|
||||
|
||||
The `inlined` property allows you to provide icon SVG markup directly in your code.
|
||||
This creates a mapping of `(name, family, variant)` to SVG markup.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
getUrl: name => `/path/to/custom/icons/${name}.svg`,
|
||||
inlined: {
|
||||
check: '<svg xmlns="http://www.w3.org/2000/svg">...</svg>',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
The structure of the `inlined` property is as follows:
|
||||
- For simple libraries (no families or variants): `{ name: svg }`
|
||||
- For libraries with families (no variants): `{ family: { name: svg } }`
|
||||
- For complex libraries (with both family and variant): `{ family: { variant: { name: svg } } }`
|
||||
|
||||
#### Adding More Inlined Icons
|
||||
|
||||
To add additional icons to an existing library, use the `inline()` method:
|
||||
|
||||
```js
|
||||
import { getIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
let defaultLibrary = getIconLibrary('default');
|
||||
defaultLibrary.inline({
|
||||
classic: { // family
|
||||
regular: { // variant
|
||||
'circle-info': '<svg xmlns="http://www.w3.org/2000/svg">...</svg>',
|
||||
'triangle-exclamation': '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
|
||||
// ...
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### Best Practice
|
||||
|
||||
When using custom icon libraries with Web Awesome components, always inline system icons to ensure optimal performance.
|
||||
|
||||
### Customize icon markup, for SVG sprites and more
|
||||
|
||||
By default, icon markup is produced by fetching the URL returned by the `getUrl()` function.
|
||||
You can provide a `getMarkup()` function to customize this.
|
||||
|
||||
A common use case for that is SVG sprites, often used to improve performance by avoiding multiple trips for each SVG.
|
||||
The browser will load the sprite sheet once and then you reference the particular SVG within the sprite sheet using its id:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary({
|
||||
name: 'sprite',
|
||||
getUrl: name => `/assets/images/sprite.svg#${name}`,
|
||||
getMarkup: url => `<svg fill="currentColor"><use href="${url}"></use></svg>`
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
As always, make sure to benchmark these changes. When using HTTP/2, it may in fact be more bandwidth-friendly to use multiple small requests instead of 1 large sprite sheet.
|
||||
|
||||
:::warning
|
||||
When using sprite sheets, the `wa-load` and `wa-error` events will not fire.
|
||||
|
||||
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<wa-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
|
||||
:::
|
||||
|
||||
### Fallbacks
|
||||
|
||||
By default, if an icon fails to load, WA will retry using the WA default icon library, and if that also fails, a blank icon will be displayed.
|
||||
You can provide a `fallback()` function to customize this behavior.
|
||||
Some examples for common use cases follow.
|
||||
|
||||
If you never want icons outside your library to display:
|
||||
|
||||
```js
|
||||
registerIconLibrary({
|
||||
name: 'my-icons',
|
||||
getUrl: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
fallback: (name, family, variant) => {
|
||||
// Don't show anything if an icon is not found
|
||||
return null;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you want to display a certain designated "missing icon" icon:
|
||||
|
||||
```js
|
||||
registerIconLibrary({
|
||||
name: 'my-icons',
|
||||
getUrl: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
inlined: {
|
||||
// Make sure the missing icon never fails by inlining it:
|
||||
'missing-icon': '<svg xmlns="http://www.w3.org/2000/svg">...</svg>'
|
||||
}
|
||||
fallback: (name, family, variant) => {
|
||||
return {name: 'missing-icon'};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
If you want to retry with the default family and variant:
|
||||
|
||||
```js
|
||||
registerIconLibrary({
|
||||
name: 'my-icons',
|
||||
getUrl: (name, family, variant) => `/assets/icons/${name}.svg`,
|
||||
fallback: (name, family, variant) => {
|
||||
if (family !== 'classic' || variant !== 'solid') {
|
||||
return { name, family: 'classic', variant: 'solid' };
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Icon Library Examples { #icon-libraries }
|
||||
|
||||
The following examples demonstrate how to register a number of popular, open source icon libraries via CDN. Feel free to adapt the code as you see fit to use your own origin or naming conventions.
|
||||
|
||||
### Bootstrap Icons
|
||||
@@ -226,8 +511,42 @@ Icons in this library are licensed under the [MIT License](https://github.com/tw
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('default', {
|
||||
resolver: (name, family) => {
|
||||
let system = {
|
||||
filled: ['circle', 'pause', 'play', 'star', 'user'],
|
||||
nameMap: {
|
||||
'chevron-down': 'chevron-down',
|
||||
'chevron-left': 'chevron-left',
|
||||
'chevron-right': 'chevron-right',
|
||||
'check': 'check-lg',
|
||||
'circle': 'circle',
|
||||
'eye-dropper': 'eyedropper',
|
||||
'grip-vertical': 'grip-vertical',
|
||||
'indeterminate': 'dash-lg',
|
||||
'pause': 'pause',
|
||||
'play': 'play',
|
||||
'circle-xmark': 'x-circle',
|
||||
'grip-vertical': 'grip-vertical',
|
||||
'eye-slash': 'eye-slash',
|
||||
'eye': 'eye',
|
||||
'user': 'person',
|
||||
'xmark': 'x-lg',
|
||||
}
|
||||
};
|
||||
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
system(name, family) {
|
||||
if (!system.nameMap[name]) {
|
||||
// If the icon is not known, use the default library
|
||||
return { library: 'wa', name: name, family, variant };
|
||||
}
|
||||
|
||||
return {
|
||||
name: system.nameMap[name],
|
||||
family: system.filled.includes(name) ? 'filled' : ''
|
||||
};
|
||||
},
|
||||
getUrl: (name, family) => {
|
||||
const suffix = family === 'filled' ? '-fill' : '';
|
||||
return `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/${name}${suffix}.svg`
|
||||
}
|
||||
@@ -245,8 +564,9 @@ Icons in this library are licensed under the [Creative Commons 4.0 License](http
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('boxicons', {
|
||||
resolver: name => {
|
||||
registerIconLibrary({
|
||||
name: 'boxicons',
|
||||
getUrl: name => {
|
||||
let folder = 'regular';
|
||||
if (name.substring(0, 4) === 'bxs-') folder = 'solid';
|
||||
if (name.substring(0, 4) === 'bxl-') folder = 'logos';
|
||||
@@ -299,8 +619,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/lu
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('lucide', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/lucide-static@0.16.29/icons/${name}.svg`
|
||||
registerIconLibrary({
|
||||
name: 'lucide',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/npm/lucide-static@0.16.29/icons/${name}.svg`
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -315,8 +636,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/ta
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('heroicons', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/heroicons@2.0.1/24/outline/${name}.svg`
|
||||
registerIconLibrary({
|
||||
name: 'heroicons',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/npm/heroicons@2.0.1/24/outline/${name}.svg`
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -340,8 +662,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/lu
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('iconoir', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/gh/lucaburgio/iconoir@latest/icons/${name}.svg`
|
||||
registerIconLibrary({
|
||||
name: 'iconoir',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/gh/lucaburgio/iconoir@latest/icons/${name}.svg`
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -365,8 +688,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/io
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('ionicons', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/ionicons@5.1.2/dist/ionicons/svg/${name}.svg`,
|
||||
registerIconLibrary({
|
||||
name: 'ionicons',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/npm/ionicons@5.1.2/dist/ionicons/svg/${name}.svg`,
|
||||
mutator: svg => {
|
||||
svg.setAttribute('fill', 'currentColor');
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
@@ -410,8 +734,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/mi
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('jam', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/jam-icons@2.0.0/svg/${name}.svg`,
|
||||
registerIconLibrary({
|
||||
name: 'jam',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/npm/jam-icons@2.0.0/svg/${name}.svg`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor')
|
||||
});
|
||||
</script>
|
||||
@@ -443,8 +768,9 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('material', {
|
||||
resolver: name => {
|
||||
registerIconLibrary({
|
||||
name: 'material',
|
||||
getUrl: name => {
|
||||
const match = name.match(/^(.*?)(_(round|sharp))?$/);
|
||||
return `https://cdn.jsdelivr.net/npm/@material-icons/svg@1.0.5/svg/${match[1]}/${match[3] || 'outline'}.svg`;
|
||||
},
|
||||
@@ -486,8 +812,9 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('remixicon', {
|
||||
resolver: name => {
|
||||
registerIconLibrary({
|
||||
name: 'remixicon',
|
||||
getUrl: name => {
|
||||
const match = name.match(/^(.*?)\/(.*?)?$/);
|
||||
match[1] = match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
||||
return `https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/${match[1]}/${match[2]}.svg`;
|
||||
@@ -523,8 +850,9 @@ Icons in this library are licensed under the [MIT License](https://github.com/ta
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('tabler', {
|
||||
resolver: name => `https://cdn.jsdelivr.net/npm/@tabler/icons@1.68.0/icons/${name}.svg`,
|
||||
registerIconLibrary({
|
||||
name: 'tabler',
|
||||
getUrl: name => `https://cdn.jsdelivr.net/npm/@tabler/icons@1.68.0/icons/${name}.svg`,
|
||||
mutator: svg => {
|
||||
svg.style.fill = 'none';
|
||||
svg.setAttribute('stroke', 'currentColor');
|
||||
@@ -559,8 +887,9 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('unicons', {
|
||||
resolver: name => {
|
||||
registerIconLibrary({
|
||||
name: 'unicons',
|
||||
getUrl: name => {
|
||||
const match = name.match(/^(.*?)(-s)?$/);
|
||||
return `https://cdn.jsdelivr.net/npm/@iconscout/unicons@3.0.3/svg/${match[2] === '-s' ? 'solid' : 'line'}/${
|
||||
match[1]
|
||||
@@ -587,61 +916,6 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
|
||||
</div>
|
||||
```
|
||||
|
||||
### Customizing the Default Library
|
||||
|
||||
The default icon library contains over 2,000 icons courtesy of [Font Awesome](https://fontawesome.com/). These are the icons that display when you use `<wa-icon>` without the `library` attribute. If you prefer to have these icons resolve elsewhere or to a different icon library, register an icon library using the `default` name and a custom resolver.
|
||||
|
||||
For example, this will change the default icon library to use [Bootstrap Icons](https://icons.getbootstrap.com/) loaded from the jsDelivr CDN.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('default', {
|
||||
resolver: (name, family) => {
|
||||
const suffix = family === 'filled' ? '-fill' : '';
|
||||
return `https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/icons/${name}${suffix}.svg`
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Customize the default library to use SVG sprites
|
||||
|
||||
To improve performance you can use a SVG sprites to avoid multiple trips for each SVG. The browser will load the sprite sheet once and then you reference the particular SVG within the sprite sheet using hash selector.
|
||||
|
||||
As always, make sure to benchmark these changes. When using HTTP/2, it may in fact be more bandwidth-friendly to use multiple small requests instead of 1 large sprite sheet.
|
||||
|
||||
:::warning
|
||||
When using sprite sheets, the `wa-load` and `wa-error` events will not fire.
|
||||
|
||||
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<wa-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
|
||||
:::
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('sprite', {
|
||||
resolver: name => `/assets/images/sprite.svg#${name}`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor'),
|
||||
spriteSheet: true
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Customizing the System Library
|
||||
|
||||
The system library contains only the icons used internally by Web Awesome components. Unlike the default icon library, the system library does not rely on physical assets. Instead, its icons are hard-coded as data URIs into the resolver to ensure their availability.
|
||||
|
||||
If you want to change the icons Web Awesome uses internally, you can register an icon library using the `system` name and a custom resolver. If you choose to do this, it's your responsibility to provide all of the icons that are required by components. You can reference `src/components/library.system.ts` for a complete list of system icons used by Web Awesome.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
registerIconLibrary('system', {
|
||||
resolver: name => `/path/to/custom/icons/${name}.svg`
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -1412,7 +1412,8 @@ hasOutline: false
|
||||
import { registerIconLibrary } from '/dist/webawesome.js';
|
||||
|
||||
// Ensure regular icons are always available for the knobs
|
||||
registerIconLibrary('fa-classic-regular', {
|
||||
registerIconLibrary({
|
||||
name: 'fa-classic-regular',
|
||||
resolver: name => `https://ka-f.fontawesome.com/releases/v6.5.1/svgs/regular/${name}.svg`
|
||||
});
|
||||
|
||||
@@ -1453,36 +1454,47 @@ hasOutline: false
|
||||
break;
|
||||
case 'premium':
|
||||
iconFamily.value = 'custom';
|
||||
registerIconLibrary('default', {
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
resolver: name => `/assets/icons/chunk/${name}.svg`,
|
||||
mutator: svg => {[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));}
|
||||
});
|
||||
registerIconLibrary('system', {
|
||||
registerIconLibrary({
|
||||
name: 'system',
|
||||
resolver: name => `/assets/icons/chunk/${name}.svg`,
|
||||
mutator: svg => {[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));}
|
||||
});
|
||||
break;
|
||||
case 'playful':
|
||||
iconFamily.value = 'custom';
|
||||
registerIconLibrary('default', {
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
resolver: name => `/assets/icons/jelly/${name}.svg`,
|
||||
mutator: svg => {[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));}
|
||||
});
|
||||
registerIconLibrary('system', {
|
||||
registerIconLibrary({
|
||||
name: 'system',
|
||||
name: 'system',
|
||||
name: 'system',
|
||||
resolver: name => `/assets/icons/jelly/${name}.svg`,
|
||||
mutator: svg => {[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));}
|
||||
});
|
||||
break;
|
||||
case 'brutalist':
|
||||
caregisterIconLibrary({
|
||||
registerIconLibrary({
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
iconFamily.value = 'custom';
|
||||
registerIconLibrary('default', {
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
resolver: name => `/assets/icons/utility/${name}.svg`,
|
||||
mutator: svg => {
|
||||
[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));
|
||||
[...svg.querySelectorAll('[stroke="black"]')].map(el => el.setAttribute('stroke', 'currentColor'));
|
||||
}
|
||||
});
|
||||
registerIconLibrary('system', {
|
||||
registerIconLibrary({
|
||||
name: 'system',
|
||||
resolver: name => `/assets/icons/utility/${name}.svg`,
|
||||
mutator: svg => {
|
||||
[...svg.querySelectorAll('[fill="black"]')].map(el => el.setAttribute('fill', 'currentColor'));
|
||||
@@ -1497,10 +1509,12 @@ hasOutline: false
|
||||
break;
|
||||
case 'classic':
|
||||
iconFamily.value = 'custom';
|
||||
registerIconLibrary('default', {
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
resolver: name => `/assets/icons/bootstrap/${name}.svg`,
|
||||
});
|
||||
registerIconLibrary('system', {
|
||||
registerIconLibrary({
|
||||
name: 'system',
|
||||
resolver: name => `/assets/icons/bootstrap/${name}.svg`,
|
||||
});
|
||||
break;
|
||||
@@ -1531,7 +1545,8 @@ hasOutline: false
|
||||
iconLibrary = 'sharp-solid';
|
||||
}
|
||||
// Ensures sharp-solid variations are available for ratings, etc.
|
||||
registerIconLibrary('always-solid', {
|
||||
registerIconLibrary({
|
||||
name: 'always-solid',
|
||||
resolver: name => `https://ka-f.fontawesome.com/releases/v6.5.1/svgs/sharp-solid/${name}.svg`
|
||||
});
|
||||
solidifyRatingStars();
|
||||
@@ -1557,15 +1572,18 @@ hasOutline: false
|
||||
iconLibrary = 'solid';
|
||||
}
|
||||
// Ensures solid variations are available for radios, ratings, etc.
|
||||
registerIconLibrary('always-solid', {
|
||||
registerIconLibrary({
|
||||
name: 'always-solid',
|
||||
resolver: name => `https://ka-f.fontawesome.com/releases/v6.5.1/svgs/solid/${name}.svg`
|
||||
});
|
||||
solidifyRatingStars();
|
||||
}
|
||||
registerIconLibrary('default', {
|
||||
registerIconLibrary({
|
||||
name: 'default',
|
||||
resolver: name => `https://ka-f.fontawesome.com/releases/v6.5.1/svgs/${iconLibrary}/${name}.svg`
|
||||
});
|
||||
registerIconLibrary('system', {
|
||||
registerIconLibrary({
|
||||
name: 'system',
|
||||
resolver: name => `https://ka-f.fontawesome.com/releases/v6.5.1/svgs/${iconLibrary}/${name}.svg`
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,6 +14,14 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
|
||||
## Next
|
||||
|
||||
- All icon libraries can now declare **pre-fetched** icons and skip the HTTP request.
|
||||
When these icons are used, the pre-fetched version is automatically used, with no additional opt-in.
|
||||
- 🚨 BREAKING: No more `system` library, just use `default`.
|
||||
The improvement above allowed us to fold Web Awesome’s own `system` icon library into the `default` library
|
||||
so the performance benefits can be automatic and shared across all uses of the `default` library,
|
||||
rather then requiring a conscious decision to use a different library.
|
||||
This also makes WA components play better with different icon libraries and different default families and variants.
|
||||
- Fixed a bug that caused an undesired margin below radio groups
|
||||
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparer>` and improved compatibility for non-image content
|
||||
- Added support for Duotone Thin, Light, and Regular styles and the Sharp Duotone family of styles to `<wa-icon>`
|
||||
- Fixed a bug that caused `<wa-radio-group>` to have an undesired margin below it
|
||||
|
||||
@@ -352,13 +352,11 @@ Form controls should support submission and validation through the following con
|
||||
|
||||
### System Icons
|
||||
|
||||
Avoid inlining SVG icons inside of templates. If a component requires an icon, make sure `<wa-icon>` is a dependency of the component and use the [system library](/components/icon#customizing-the-system-library):
|
||||
Avoid inlining SVG icons inside of templates.
|
||||
If a component requires an icon, make sure `<wa-icon>` is a dependency of the component.
|
||||
|
||||
```html
|
||||
<wa-icon library="system" name="..." variant="..."></wa-icon>
|
||||
```
|
||||
|
||||
This will render the icons instantly whereas the default library will fetch them from a remote source. If an icon isn't available in the system library, you will need to add it to `library.system.ts`. Using the system library ensures that all icons load instantly and are customizable by users who wish to provide a custom resolver for the system library.
|
||||
If it is not one of the [pre-fetched icons](/components/icon#fetched) in the `default` library,
|
||||
you should add it. This will render the icons instantly rather than fetching them from a remote source.
|
||||
|
||||
### Writing tests
|
||||
|
||||
|
||||
@@ -108,15 +108,14 @@ export default class WaAnimatedImage extends WebAwesomeElement {
|
||||
<div part="control-box" class="control-box">
|
||||
<slot name="play-icon">
|
||||
<wa-icon
|
||||
name="play"
|
||||
library="system"
|
||||
name="system:play"
|
||||
variant="solid"
|
||||
class="default"
|
||||
style="margin-inline-start: 3px;"
|
||||
></wa-icon>
|
||||
</slot>
|
||||
<slot name="pause-icon">
|
||||
<wa-icon name="pause" library="system" variant="solid" class="default"></wa-icon>
|
||||
<wa-icon name="system:pause" variant="solid" class="default"></wa-icon>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class WaAvatar extends WebAwesomeElement {
|
||||
} else {
|
||||
avatarWithoutImage = html`
|
||||
<slot name="icon" part="icon" class="icon" role="img" aria-label=${this.label}>
|
||||
<wa-icon name="user" library="system" variant="solid"></wa-icon>
|
||||
<wa-icon name="system:user" variant="solid"></wa-icon>
|
||||
</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -92,8 +92,7 @@ export default class WaBreadcrumb extends WebAwesomeElement {
|
||||
<span hidden aria-hidden="true">
|
||||
<slot name="separator">
|
||||
<wa-icon
|
||||
name=${this.localize.dir() === 'rtl' ? 'chevron-left' : 'chevron-right'}
|
||||
library="system"
|
||||
name="system:${this.localize.dir() === 'rtl' ? 'chevron-left' : 'chevron-right'}"
|
||||
variant="solid"
|
||||
></wa-icon>
|
||||
</slot>
|
||||
|
||||
@@ -265,9 +265,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
${
|
||||
this.caret
|
||||
? html`
|
||||
<wa-icon part="caret" class="caret" library="system" name="chevron-down" variant="solid"></wa-icon>
|
||||
`
|
||||
? html` <wa-icon part="caret" class="caret" name="system:chevron-down" variant="solid"></wa-icon> `
|
||||
: ''
|
||||
}
|
||||
${this.loading ? html`<wa-spinner part="spinner"></wa-spinner>` : ''}
|
||||
|
||||
@@ -608,7 +608,7 @@ export default class WaCarousel extends WebAwesomeElement {
|
||||
@click=${prevEnabled ? () => this.previous() : null}
|
||||
>
|
||||
<slot name="previous-icon">
|
||||
<wa-icon library="system" name="${isRTL ? 'chevron-right' : 'chevron-left'}"></wa-icon>
|
||||
<wa-icon name="system:${isRTL ? 'chevron-right' : 'chevron-left'}"></wa-icon>
|
||||
</slot>
|
||||
</button>
|
||||
|
||||
@@ -625,7 +625,7 @@ export default class WaCarousel extends WebAwesomeElement {
|
||||
@click=${nextEnabled ? () => this.next() : null}
|
||||
>
|
||||
<slot name="next-icon">
|
||||
<wa-icon library="system" name="${isRTL ? 'chevron-left' : 'chevron-right'}"></wa-icon>
|
||||
<wa-icon name="system:${isRTL ? 'chevron-left' : 'chevron-right'}"></wa-icon>
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -234,7 +234,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
@click=${this.handleClick}
|
||||
/>
|
||||
|
||||
<wa-icon part="${iconState}-icon icon" library="system" name=${iconName}></wa-icon>
|
||||
<wa-icon part="${iconState}-icon icon" name="system:${iconName}"></wa-icon>
|
||||
</span>
|
||||
|
||||
<slot part="label"></slot>
|
||||
|
||||
@@ -1041,8 +1041,7 @@ export default class WaColorPicker extends WebAwesomeFormAssociatedElement {
|
||||
@focus=${this.stopNestedEventPropagation}
|
||||
>
|
||||
<wa-icon
|
||||
library="system"
|
||||
name="eye-dropper"
|
||||
name="system:eye-dropper"
|
||||
variant="solid"
|
||||
label=${this.localize.term('selectAColorFromTheScreen')}
|
||||
></wa-icon>
|
||||
|
||||
@@ -133,7 +133,7 @@ export default class WaComparer extends WebAwesomeElement {
|
||||
tabindex="0"
|
||||
>
|
||||
<slot name="handle">
|
||||
<wa-icon library="system" name="grip-vertical" variant="solid"></wa-icon>
|
||||
<wa-icon name="system:grip-vertical" variant="solid"></wa-icon>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -196,13 +196,13 @@ export default class WaCopyButton extends WebAwesomeElement {
|
||||
<!-- Render a visually hidden label to appease the accessibility checking gods -->
|
||||
<span class="wa-visually-hidden">${this.currentLabel}</span>
|
||||
<slot part="copy-icon" name="copy-icon">
|
||||
<wa-icon library="system" name="copy" variant="regular" fixed-width></wa-icon>
|
||||
<wa-icon name="system:copy" variant="regular" fixed-width></wa-icon>
|
||||
</slot>
|
||||
<slot part="success-icon" name="success-icon" variant="solid" hidden>
|
||||
<wa-icon library="system" name="check" fixed-width></wa-icon>
|
||||
<wa-icon name="system:check" fixed-width></wa-icon>
|
||||
</slot>
|
||||
<slot part="error-icon" name="error-icon" variant="solid" hidden>
|
||||
<wa-icon library="system" name="xmark" fixed-width></wa-icon>
|
||||
<wa-icon name="system:xmark" fixed-width></wa-icon>
|
||||
</slot>
|
||||
<wa-tooltip
|
||||
class=${classMap({
|
||||
|
||||
@@ -235,20 +235,10 @@ export default class WaDetails extends WebAwesomeElement {
|
||||
|
||||
<span part="icon">
|
||||
<slot name="expand-icon">
|
||||
<wa-icon
|
||||
library="system"
|
||||
variant="solid"
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
fixed-width
|
||||
></wa-icon>
|
||||
<wa-icon name="system:${isRtl ? 'chevron-left' : 'chevron-right'}" variant="solid" fixed-width></wa-icon>
|
||||
</slot>
|
||||
<slot name="collapse-icon">
|
||||
<wa-icon
|
||||
library="system"
|
||||
variant="solid"
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
fixed-width
|
||||
></wa-icon>
|
||||
<wa-icon name="system:${isRtl ? 'chevron-left' : 'chevron-right'}" variant="solid" fixed-width></wa-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
@@ -237,9 +237,8 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="close"
|
||||
name="xmark"
|
||||
name="system:xmark"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
variant="solid"
|
||||
@click="${(event: PointerEvent) => this.requestClose(event.target as Element)}"
|
||||
></wa-icon-button>
|
||||
|
||||
@@ -253,9 +253,8 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="close"
|
||||
name="xmark"
|
||||
name="system:xmark"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
variant="solid"
|
||||
@click="${(event: PointerEvent) => this.requestClose(event.target as Element)}"
|
||||
></wa-icon-button>
|
||||
|
||||
@@ -34,11 +34,7 @@ describe('<wa-icon-button>', () => {
|
||||
describe('when styling the host element', () => {
|
||||
it('renders the correct color and font size', async () => {
|
||||
const el = await fixture<WaIconButton>(html`
|
||||
<wa-icon-button
|
||||
library="system"
|
||||
name="check"
|
||||
style="color: rgb(0, 136, 221); font-size: 2rem;"
|
||||
></wa-icon-button>
|
||||
<wa-icon-button name="system:check" style="color: rgb(0, 136, 221); font-size: 2rem;"></wa-icon-button>
|
||||
`);
|
||||
const icon = el.shadowRoot!.querySelector('wa-icon')!;
|
||||
const styles = getComputedStyle(icon);
|
||||
@@ -51,7 +47,7 @@ describe('<wa-icon-button>', () => {
|
||||
describe('when icon attributes are present', () => {
|
||||
it('renders an wa-icon from a library', async () => {
|
||||
const el = await fixture<WaIconButton>(html`
|
||||
<wa-icon-button library="system" name="check"></wa-icon-button>
|
||||
<wa-icon-buttonname="system:check"></wa-icon-button>
|
||||
`);
|
||||
expect(el.shadowRoot?.querySelector('wa-icon')).to.exist;
|
||||
});
|
||||
|
||||
@@ -24,8 +24,9 @@ const testLibraryIcons = {
|
||||
|
||||
describe('<wa-icon>', () => {
|
||||
before(() => {
|
||||
registerIconLibrary('test-library', {
|
||||
resolver: (name: keyof typeof testLibraryIcons) => {
|
||||
registerIconLibrary({
|
||||
name: 'test-library',
|
||||
getUrl: (name: keyof typeof testLibraryIcons) => {
|
||||
// only for testing a bad request
|
||||
if (name === ('bad-request' as keyof typeof testLibraryIcons)) {
|
||||
return `data:image/svg+xml`;
|
||||
@@ -53,10 +54,10 @@ describe('<wa-icon>', () => {
|
||||
});
|
||||
|
||||
it('renders pre-loaded system icons and emits wa-load event', async () => {
|
||||
const el = await fixture<WaIcon>(html` <wa-icon library="system"></wa-icon> `);
|
||||
const el = await fixture<WaIcon>(html` <wa-icon></wa-icon> `);
|
||||
const listener = oneEvent(el, 'wa-load') as Promise<WaLoadEvent>;
|
||||
|
||||
el.name = 'check';
|
||||
el.name = 'system:check';
|
||||
const ev = await listener;
|
||||
await elementUpdated(el);
|
||||
|
||||
@@ -65,12 +66,12 @@ describe('<wa-icon>', () => {
|
||||
});
|
||||
|
||||
it('the icon is accessible', async () => {
|
||||
const el = await fixture<WaIcon>(html` <wa-icon library="system" name="check"></wa-icon> `);
|
||||
const el = await fixture<WaIcon>(html` <wa-icon name="system:check"></wa-icon> `);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('the icon has the correct default aria attributes', async () => {
|
||||
const el = await fixture<WaIcon>(html` <wa-icon library="system" name="check"></wa-icon> `);
|
||||
const el = await fixture<WaIcon>(html` <wa-icon name="system:check"></wa-icon> `);
|
||||
|
||||
expect(el.getAttribute('role')).to.be.null;
|
||||
expect(el.getAttribute('aria-label')).to.be.null;
|
||||
@@ -81,9 +82,7 @@ describe('<wa-icon>', () => {
|
||||
describe('when a label is provided', () => {
|
||||
it('the icon has the correct default aria attributes', async () => {
|
||||
const fakeLabel = 'a label';
|
||||
const el = await fixture<WaIcon>(html`
|
||||
<wa-icon label="${fakeLabel}" library="system" name="check"></wa-icon>
|
||||
`);
|
||||
const el = await fixture<WaIcon>(html` <wa-icon label="${fakeLabel}" name="system:check"></wa-icon> `);
|
||||
|
||||
expect(el.getAttribute('role')).to.equal('img');
|
||||
expect(el.getAttribute('aria-label')).to.equal(fakeLabel);
|
||||
@@ -172,10 +171,11 @@ describe('<wa-icon>', () => {
|
||||
describe('svg sprite sheets', () => {
|
||||
// TODO: this test is skipped because Bootstrap sprite.svg doesn't seem to be available in CI. Will fix in a future PR. [Konnor]
|
||||
it.skip('Should properly grab an SVG and render it from bootstrap icons', async () => {
|
||||
registerIconLibrary('sprite', {
|
||||
resolver: name => `/docs/assets/images/sprite.svg#${name}`,
|
||||
registerIconLibrary({
|
||||
name: 'sprite',
|
||||
getUrl: name => `/docs/assets/images/sprite.svg#${name}`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor'),
|
||||
spriteSheet: true,
|
||||
getMarkup: url => `<svg fill="currentColor"><use part="use" href="${url}"></use></svg>`,
|
||||
});
|
||||
|
||||
const el = await fixture<WaIcon>(html`<wa-icon name="arrow-left" library="sprite"></wa-icon>`);
|
||||
@@ -199,10 +199,11 @@ describe('<wa-icon>', () => {
|
||||
});
|
||||
|
||||
it('Should render nothing if the sprite hash is wrong', async () => {
|
||||
registerIconLibrary('sprite', {
|
||||
resolver: name => `/docs/assets/images/sprite.svg#${name}`,
|
||||
registerIconLibrary({
|
||||
name: 'sprite',
|
||||
getUrl: name => `/docs/assets/images/sprite.svg#${name}`,
|
||||
mutator: svg => svg.setAttribute('fill', 'currentColor'),
|
||||
spriteSheet: true,
|
||||
getMarkup: url => `<svg fill="currentColor"><use part="use" href="${url}"></use></svg>`,
|
||||
});
|
||||
|
||||
const el = await fixture<WaIcon>(html`<wa-icon name="non-existent" library="sprite"></wa-icon>`);
|
||||
@@ -226,14 +227,13 @@ describe('<wa-icon>', () => {
|
||||
// TODO: <use> svg icons don't emit a "load" or "error" event...if we can figure out how to get the event to emit errors.
|
||||
// Once we figure out how to emit errors / loading perhaps we can actually test this?
|
||||
it.skip("Should produce an error if the icon doesn't exist.", async () => {
|
||||
registerIconLibrary('sprite', {
|
||||
resolver(name) {
|
||||
return `/docs/assets/images/sprite.svg#${name}`;
|
||||
},
|
||||
registerIconLibrary({
|
||||
name: 'sprite',
|
||||
getUrl: name => `/docs/assets/images/sprite.svg#${name}`,
|
||||
mutator(svg) {
|
||||
return svg.setAttribute('fill', 'currentColor');
|
||||
},
|
||||
spriteSheet: true,
|
||||
getMarkup: url => `<svg fill="currentColor"><use part="use" href="${url}"></use></svg>`,
|
||||
});
|
||||
|
||||
const el = await fixture<WaIcon>(html`<wa-icon name="bad-icon" library="sprite"></wa-icon>`);
|
||||
|
||||
@@ -1,38 +1,31 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { isTemplateResult } from 'lit/directive-helpers.js';
|
||||
import { WaErrorEvent } from '../../events/error.js';
|
||||
import { WaLoadEvent } from '../../events/load.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import styles from './icon.css';
|
||||
import { getIconLibrary, unwatchIcon, watchIcon, type IconLibrary } from './library.js';
|
||||
import {
|
||||
CACHEABLE_ERROR,
|
||||
getIconLibrary,
|
||||
RETRYABLE_ERROR,
|
||||
unwatchIcon,
|
||||
watchIcon,
|
||||
type IconLibrary,
|
||||
} from './registry.js';
|
||||
|
||||
import type { HTMLTemplateResult, PropertyValues } from 'lit';
|
||||
|
||||
const CACHEABLE_ERROR = Symbol();
|
||||
const RETRYABLE_ERROR = Symbol();
|
||||
type SVGResult = HTMLTemplateResult | SVGSVGElement | typeof RETRYABLE_ERROR | typeof CACHEABLE_ERROR;
|
||||
|
||||
let parser: DOMParser;
|
||||
const iconCache = new Map<string, Promise<SVGResult>>();
|
||||
|
||||
interface IconSource {
|
||||
url?: string;
|
||||
fromLibrary: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Icons are symbols that can be used to represent various options within an application.
|
||||
* @documentation https://backers.webawesome.com/docs/components/icon
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @event wa-load - Emitted when the icon has loaded. When using `spriteSheet: true` this will not emit.
|
||||
* @event wa-error - Emitted when the icon fails to load due to an error. When using `spriteSheet: true` this will not emit.
|
||||
* @event wa-load - Emitted when the icon has loaded.
|
||||
* @event wa-error - Emitted when the icon fails to load due to an error.
|
||||
*
|
||||
* @csspart svg - The internal SVG element.
|
||||
* @csspart use - The `<use>` element generated when using `spriteSheet: true`
|
||||
*
|
||||
* @cssproperty [--primary-color=currentColor] - Sets a duotone icon's primary color.
|
||||
* @cssproperty [--primary-opacity=1] - Sets a duotone icon's primary opacity.
|
||||
@@ -82,6 +75,9 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
/** The name of a registered custom icon library. */
|
||||
@property({ cssProperty: '--wa-icon-library', default: 'default' }) library = 'default';
|
||||
|
||||
/** The icon library object being used. */
|
||||
private iconLibrary?: IconLibrary;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -99,68 +95,14 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
unwatchIcon(this);
|
||||
}
|
||||
|
||||
private getIconSource(): IconSource {
|
||||
const library = getIconLibrary(this.library);
|
||||
if (this.name && library) {
|
||||
return {
|
||||
url: library.resolver(this.name, this.family, this.variant),
|
||||
fromLibrary: true,
|
||||
};
|
||||
private getIconSource(): string | undefined {
|
||||
let ref = this.src ?? this.name;
|
||||
|
||||
if (ref) {
|
||||
return this.iconLibrary?.getUrl(ref, this.family, this.variant);
|
||||
}
|
||||
|
||||
return {
|
||||
url: this.src,
|
||||
fromLibrary: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Given a URL, this function returns the resulting SVG element or an appropriate error symbol. */
|
||||
private async resolveIcon(url: string, library?: IconLibrary): Promise<SVGResult> {
|
||||
let fileData: Response;
|
||||
|
||||
if (library?.spriteSheet) {
|
||||
this.svg = html`<svg part="svg">
|
||||
<use part="use" href="${url}"></use>
|
||||
</svg>`;
|
||||
|
||||
// Using a templateResult requires the SVG to be written to the DOM first before we can grab the SVGElement
|
||||
// to be passed to the library's mutator function.
|
||||
await this.updateComplete;
|
||||
|
||||
const svg = this.shadowRoot!.querySelector("[part='svg']")!;
|
||||
|
||||
if (typeof library.mutator === 'function') {
|
||||
library.mutator(svg as SVGElement);
|
||||
}
|
||||
|
||||
return this.svg;
|
||||
}
|
||||
|
||||
try {
|
||||
fileData = await fetch(url, { mode: 'cors' });
|
||||
if (!fileData.ok) return fileData.status === 410 ? CACHEABLE_ERROR : RETRYABLE_ERROR;
|
||||
} catch {
|
||||
return RETRYABLE_ERROR;
|
||||
}
|
||||
|
||||
try {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = await fileData.text();
|
||||
|
||||
const svg = div.firstElementChild;
|
||||
if (svg?.tagName?.toLowerCase() !== 'svg') return CACHEABLE_ERROR;
|
||||
|
||||
if (!parser) parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svg.outerHTML, 'text/html');
|
||||
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
if (!svgEl) return CACHEABLE_ERROR;
|
||||
|
||||
svgEl.part.add('svg');
|
||||
return document.adoptNode(svgEl);
|
||||
} catch {
|
||||
return CACHEABLE_ERROR;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@watch('label')
|
||||
@@ -178,21 +120,27 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch(['library', 'src'])
|
||||
setIconLibrary() {
|
||||
// Clear it out so that next time `setIcon()` is called it sets it.
|
||||
this.iconLibrary = undefined;
|
||||
}
|
||||
|
||||
@watch(['family', 'name', 'library', 'variant', 'src'])
|
||||
async setIcon() {
|
||||
const { url, fromLibrary } = this.getIconSource();
|
||||
const library = fromLibrary ? getIconLibrary(this.library) : undefined;
|
||||
this.iconLibrary ??= getIconLibrary(this.src ? 'custom' : this.library);
|
||||
|
||||
if (!url) {
|
||||
let { src, name, family, variant } = this;
|
||||
let ref = src ?? name;
|
||||
|
||||
const url = ref ? this.iconLibrary?.getUrl(ref, family, variant) : undefined;
|
||||
|
||||
if (!ref || !url || !this.iconLibrary) {
|
||||
this.svg = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let iconResolver = iconCache.get(url);
|
||||
if (!iconResolver) {
|
||||
iconResolver = this.resolveIcon(url, library);
|
||||
iconCache.set(url, iconResolver);
|
||||
}
|
||||
let iconResolver = this.iconLibrary.getElement(ref, family, variant);
|
||||
|
||||
// If we haven't rendered yet, exit early. This avoids unnecessary work due to watching multiple props.
|
||||
if (!this.initialRender) {
|
||||
@@ -201,20 +149,11 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
|
||||
const svg = await iconResolver;
|
||||
|
||||
if (svg === RETRYABLE_ERROR) {
|
||||
iconCache.delete(url);
|
||||
}
|
||||
|
||||
if (url !== this.getIconSource().url) {
|
||||
if (url !== this.getIconSource()) {
|
||||
// If the url has changed while fetching the icon, ignore this request
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTemplateResult(svg)) {
|
||||
this.svg = svg;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (svg) {
|
||||
case RETRYABLE_ERROR:
|
||||
case CACHEABLE_ERROR:
|
||||
@@ -223,23 +162,10 @@ export default class WaIcon extends WebAwesomeElement {
|
||||
break;
|
||||
default:
|
||||
this.svg = svg.cloneNode(true) as SVGElement;
|
||||
library?.mutator?.(this.svg);
|
||||
this.dispatchEvent(new WaLoadEvent());
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// Sometimes (like with SSR -> hydration) mutators dont get applied due to race conditions. This ensures mutators get re-applied.
|
||||
const library = getIconLibrary(this.library);
|
||||
|
||||
const svg = this.shadowRoot?.querySelector('svg');
|
||||
if (svg) {
|
||||
library?.mutator?.(svg);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.hasUpdated) {
|
||||
return this.svg;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { getKitCode } from '../../utilities/base-path.js';
|
||||
import type { IconLibrary } from './library.js';
|
||||
|
||||
function getIconUrl(name: string, family: string, variant: string) {
|
||||
const kitCode = getKitCode();
|
||||
const isPro = kitCode.length > 0;
|
||||
let folder = 'solid';
|
||||
|
||||
// Classic
|
||||
if (family === 'classic') {
|
||||
if (variant === 'thin') folder = 'thin';
|
||||
if (variant === 'light') folder = 'light';
|
||||
if (variant === 'regular') folder = 'regular';
|
||||
if (variant === 'solid') folder = 'solid';
|
||||
}
|
||||
|
||||
// Sharp
|
||||
if (family === 'sharp') {
|
||||
if (variant === 'thin') folder = 'sharp-thin';
|
||||
if (variant === 'light') folder = 'sharp-light';
|
||||
if (variant === 'regular') folder = 'sharp-regular';
|
||||
if (variant === 'solid') folder = 'sharp-solid';
|
||||
}
|
||||
|
||||
// Duotone
|
||||
if (family === 'duotone') {
|
||||
if (variant === 'thin') folder = 'duotone-thin';
|
||||
if (variant === 'light') folder = 'duotone-light';
|
||||
if (variant === 'regular') folder = 'duotone-regular';
|
||||
if (variant === 'solid') folder = 'duotone';
|
||||
}
|
||||
|
||||
// Sharp Duotone
|
||||
if (family === 'sharp-duotone') {
|
||||
if (variant === 'thin') folder = 'sharp-duotone-thin';
|
||||
if (variant === 'light') folder = 'sharp-duotone-light';
|
||||
if (variant === 'regular') folder = 'sharp-duotone-regular';
|
||||
if (variant === 'solid') folder = 'sharp-duotone-solid';
|
||||
}
|
||||
|
||||
// Brands
|
||||
if (family === 'brands') {
|
||||
folder = 'brands';
|
||||
}
|
||||
|
||||
// Use the default CDN
|
||||
return isPro
|
||||
? `https://ka-p.fontawesome.com/releases/v6.7.2/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`
|
||||
: `https://ka-f.fontawesome.com/releases/v6.7.2/svgs/${folder}/${name}.svg`;
|
||||
}
|
||||
|
||||
const library: IconLibrary = {
|
||||
name: 'default',
|
||||
resolver: (name: string, family = 'classic', variant = 'solid') => {
|
||||
return getIconUrl(name, family, variant);
|
||||
},
|
||||
};
|
||||
|
||||
export default library;
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { IconLibrary } from './library.js';
|
||||
|
||||
function dataUri(svg: string) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
export const iconsByVariant: { [key: string]: { [key: string]: string } } = {
|
||||
solid: {
|
||||
check: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>`,
|
||||
'chevron-down': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>`,
|
||||
'chevron-left': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>`,
|
||||
'chevron-right': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>`,
|
||||
circle: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512z"/></svg>`,
|
||||
'eye-dropper': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M341.6 29.2L240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4L482.8 170.4c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6v42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480h42.4c21.2 0 41.6-8.4 56.6-23.4L309.4 335.9l-45.3-45.3L143.4 411.3c-3 3-7.1 4.7-11.3 4.7H96V379.9c0-4.2 1.7-8.3 4.7-11.3L221.4 247.9l-45.3-45.3L55.4 323.3z"/></svg>`,
|
||||
'grip-vertical': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M40 352l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 320c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 192l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 160c-22.1 0-40-17.9-40-40L0 72C0 49.9 17.9 32 40 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40z"/></svg>`,
|
||||
indeterminate: `<svg part="indeterminate-icon" class="icon" viewBox="0 0 16 16"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"><g stroke="currentColor" stroke-width="2"><g transform="translate(2.285714, 6.857143)"><path d="M10.2857143,1.14285714 L1.14285714,1.14285714"></path></g></g></g></svg>`,
|
||||
minus: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z"/></svg>`,
|
||||
pause: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"/></svg>`,
|
||||
play: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>`,
|
||||
star: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>`,
|
||||
user: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>`,
|
||||
xmark: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>`,
|
||||
},
|
||||
regular: {
|
||||
'circle-xmark': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"/></svg>`,
|
||||
copy: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M384 336H192c-8.8 0-16-7.2-16-16V64c0-8.8 7.2-16 16-16l140.1 0L400 115.9V320c0 8.8-7.2 16-16 16zM192 384H384c35.3 0 64-28.7 64-64V115.9c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1H192c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H256c35.3 0 64-28.7 64-64V416H272v32c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H96V128H64z"/></svg>`,
|
||||
eye: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"/></svg>`,
|
||||
'eye-slash': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm51.3 163.3l-41.9-33C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5zm-88-69.3L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8z"/></svg>`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Union of all icons, across variants
|
||||
*/
|
||||
export const icons: { [key: string]: string } = Object.assign({}, ...Object.values(iconsByVariant));
|
||||
|
||||
//
|
||||
// System icons are a separate library to ensure they're always available, regardless of how the default icon library is
|
||||
// configured or if its icons resolve properly. All Web Awesome components must use the system library instead of the
|
||||
// default library.
|
||||
//
|
||||
const systemLibrary: IconLibrary = {
|
||||
name: 'system',
|
||||
resolver: (name: string, family = 'classic', variant = 'solid') => {
|
||||
if (family === 'classic') {
|
||||
// Try given variant first, fall back to any variant
|
||||
let svg = iconsByVariant[variant]?.[name];
|
||||
|
||||
if (svg) {
|
||||
return dataUri(svg);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
export default systemLibrary;
|
||||
@@ -1,53 +1,282 @@
|
||||
import type WaIcon from '../icon/icon.js';
|
||||
import defaultLibrary from './library.default.js';
|
||||
import systemLibrary from './library.system.js';
|
||||
import { flatten } from '../../utilities/deep.js';
|
||||
export const CACHEABLE_ERROR = Symbol('CACHEABLE_ERROR');
|
||||
export const RETRYABLE_ERROR = Symbol('RETRYABLE_ERROR');
|
||||
|
||||
export type IconLibraryResolver = (name: string, family: string, variant: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
export interface IconLibrary {
|
||||
name: string;
|
||||
resolver: IconLibraryResolver;
|
||||
mutator?: IconLibraryMutator;
|
||||
spriteSheet?: boolean;
|
||||
}
|
||||
// 410: Gone
|
||||
// NOTE: Resist the temptation to add 403 and 404 to this list.
|
||||
// We may get them before a token is added, and we need to be able to retry.
|
||||
export const CACHEABLE_HTTP_ERRORS = [410];
|
||||
|
||||
let registry: IconLibrary[] = [defaultLibrary, systemLibrary];
|
||||
let watchedIcons: WaIcon[] = [];
|
||||
let parser: DOMParser;
|
||||
|
||||
/** Adds an icon to the list of watched icons. */
|
||||
export function watchIcon(icon: WaIcon) {
|
||||
watchedIcons.push(icon);
|
||||
}
|
||||
|
||||
/** Removes an icon from the list of watched icons. */
|
||||
export function unwatchIcon(icon: WaIcon) {
|
||||
watchedIcons = watchedIcons.filter(el => el !== icon);
|
||||
}
|
||||
|
||||
/** Returns a library from the registry. */
|
||||
export function getIconLibrary(name?: string) {
|
||||
return registry.find(lib => lib.name === name);
|
||||
}
|
||||
|
||||
/** Adds an icon library to the registry, or overrides an existing one. */
|
||||
export function registerIconLibrary(name: string, options: Omit<IconLibrary, 'name'>) {
|
||||
unregisterIconLibrary(name);
|
||||
registry.push({
|
||||
const defaultFallback = function (name: string, family?: string, variant?: string): IconLocator | undefined {
|
||||
return {
|
||||
name,
|
||||
resolver: options.resolver,
|
||||
mutator: options.mutator,
|
||||
spriteSheet: options.spriteSheet,
|
||||
});
|
||||
family,
|
||||
variant,
|
||||
library: 'wa',
|
||||
};
|
||||
};
|
||||
|
||||
// Redraw watched icons
|
||||
watchedIcons.forEach(icon => {
|
||||
if (icon.library === name) {
|
||||
icon.setIcon();
|
||||
export default class IconLibrary {
|
||||
/** The original object used to create this library */
|
||||
readonly spec: UnregisteredIconLibrary;
|
||||
|
||||
readonly name: string;
|
||||
readonly mutator?: IconLibraryMutator;
|
||||
readonly system?: IconMapping;
|
||||
readonly fallback?: IconMapping;
|
||||
|
||||
/** Inlined markup, keyed by URL */
|
||||
inlined: IconLibraryCacheFlat = {};
|
||||
|
||||
/** DOM nodes, keyed by URL */
|
||||
cache: Record<string, SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> = {};
|
||||
|
||||
constructor(library: UnregisteredIconLibrary) {
|
||||
// Store library definition
|
||||
this.spec = library;
|
||||
|
||||
// Copy certain properties
|
||||
this.name = library.name;
|
||||
this.mutator = library.mutator;
|
||||
// Resolve system icons through the default icon library if no system() function is provided
|
||||
this.system = library.system ?? defaultFallback;
|
||||
// Resolve failed to load icons through the default icon library if no fallback() function is provided
|
||||
this.fallback = library.fallback ?? defaultFallback;
|
||||
|
||||
if (library.inlined) {
|
||||
this.inline(library.inlined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an icon name, family, and variant into a URL
|
||||
*/
|
||||
getUrl(name: string, family?: string, variant?: string): string;
|
||||
getUrl(url: string): string;
|
||||
getUrl(nameOrUrl: string, family?: string, variant?: string): string {
|
||||
if (nameOrUrl.startsWith('system:')) {
|
||||
nameOrUrl = nameOrUrl.slice(7);
|
||||
|
||||
if (this.system) {
|
||||
let resolved = this.system(nameOrUrl, family, variant);
|
||||
|
||||
if (resolved) {
|
||||
nameOrUrl = resolved.name ?? nameOrUrl;
|
||||
family = resolved.family ?? family;
|
||||
variant = resolved.variant ?? variant;
|
||||
|
||||
if (resolved.library && resolved.library !== this.name) {
|
||||
let library = IconLibrary.registry.get(resolved.library);
|
||||
|
||||
if (library) {
|
||||
return library.getUrl(nameOrUrl, family, variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.spec.getUrl) {
|
||||
return this.spec.getUrl(nameOrUrl, family, variant);
|
||||
}
|
||||
|
||||
// If no getUrl function was provided, this library does not use names,
|
||||
// just pass the URL through
|
||||
return nameOrUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a URL into a cache key
|
||||
*/
|
||||
getCacheKey(url: string) {
|
||||
return this.spec.getCacheKey?.(url) ?? url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the markup for an icon as a string
|
||||
*/
|
||||
getMarkup(url: string): IconFetchedResult | Promise<IconFetchedResult> {
|
||||
if (this.spec.getMarkup) {
|
||||
return this.spec.getMarkup(url);
|
||||
}
|
||||
|
||||
let cacheKey = this.getCacheKey(url);
|
||||
let markup = this.inlined[cacheKey];
|
||||
|
||||
if (!markup) {
|
||||
return fetchIcon(url).then(markup => {
|
||||
if (typeof markup === 'string') {
|
||||
// TBD: Should we add to inlined? DOM nodes are cached anyway, perhaps that’s enough?
|
||||
// Or perhaps we should go the other way and cache CACHEABLE_ERROR too?
|
||||
this.inlined[cacheKey] = markup;
|
||||
}
|
||||
|
||||
return markup;
|
||||
});
|
||||
}
|
||||
|
||||
return markup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a name, family, and variant, this function returns the resulting SVG element or an appropriate error symbol.
|
||||
* If the icon library defines fallbacks, they will be tried in order.
|
||||
*/
|
||||
async getElement(
|
||||
name: string,
|
||||
family?: string,
|
||||
variant?: string,
|
||||
): Promise<SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR> {
|
||||
let url = this.getUrl(name, family, variant);
|
||||
let cacheKey = this.getCacheKey(url);
|
||||
|
||||
if (this.cache[cacheKey]) {
|
||||
return this.cache[cacheKey];
|
||||
}
|
||||
|
||||
let markup = await this.getMarkup(url);
|
||||
let result;
|
||||
|
||||
if (markup === CACHEABLE_ERROR || markup === RETRYABLE_ERROR) {
|
||||
result = markup;
|
||||
} else {
|
||||
result = await this.getElementFromMarkup(markup);
|
||||
}
|
||||
|
||||
if (result === CACHEABLE_ERROR || result === RETRYABLE_ERROR) {
|
||||
if (this.spec.fallback) {
|
||||
// Try again with fallback
|
||||
let fallback = this.spec.fallback(name, family, variant);
|
||||
if (fallback) {
|
||||
let library: IconLibrary | undefined = this;
|
||||
|
||||
if (fallback.library && fallback.library !== this.name) {
|
||||
library = IconLibrary.registry.get(fallback.library);
|
||||
}
|
||||
|
||||
if (library) {
|
||||
return library.getElement(fallback.name, fallback.family, fallback.variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result === CACHEABLE_ERROR) {
|
||||
this.cache[cacheKey] = result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL, this function synchronously returns the resulting SVG element or an appropriate error symbol.
|
||||
*/
|
||||
getElementFromMarkup(markup: string): SVGElement | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR {
|
||||
let svgEl;
|
||||
try {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.innerHTML = markup;
|
||||
|
||||
const svg = div.firstElementChild;
|
||||
|
||||
if (svg?.tagName?.toLowerCase() === 'svg') {
|
||||
parser ??= new DOMParser();
|
||||
const doc = parser.parseFromString(svg.outerHTML, 'text/html');
|
||||
|
||||
svgEl = doc.body.querySelector('svg');
|
||||
|
||||
if (svgEl) {
|
||||
svgEl.part.add('svg');
|
||||
svgEl = document.adoptNode(svgEl);
|
||||
|
||||
// Cache mutations
|
||||
if (this.mutator) {
|
||||
this.mutator(svgEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return svgEl ?? CACHEABLE_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the deep family → variant → icon name → markup cache that is more convenient to write out manually
|
||||
* to the flat URL → markup cache that icon libraries use internally
|
||||
**/
|
||||
inline(cache: IconLibraryCacheDeep) {
|
||||
// If no getUrl function was provided, this library does not use names,
|
||||
// so this should already be a flat URL → markup mapping
|
||||
let flatCache = cache;
|
||||
|
||||
if (this.spec.getUrl) {
|
||||
// Convert deep family → variant → icon name → markup cache that is easier to write
|
||||
// to the flat URL → markup cache that we use internally
|
||||
flatCache = flatten(cache, {
|
||||
getKey: (path: (keyof any)[]) => {
|
||||
// name is always the last value no matter the depth
|
||||
let name = path.pop()!;
|
||||
let [family, variant] = path;
|
||||
let url = this.getUrl(name as string, family as string, variant as string);
|
||||
return this.getCacheKey(url!);
|
||||
},
|
||||
}) as IconLibraryCacheFlat;
|
||||
}
|
||||
|
||||
Object.assign(this.inlined, flatCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of this library, optionally overriding some of its properties.
|
||||
*/
|
||||
extend(library: Partial<UnregisteredIconLibrary> = {}) {
|
||||
return new IconLibrary({ ...this.spec, ...library });
|
||||
}
|
||||
|
||||
static registry = new Map<string, IconLibrary>();
|
||||
}
|
||||
|
||||
/** Removes an icon library from the registry. */
|
||||
export function unregisterIconLibrary(name: string) {
|
||||
registry = registry.filter(lib => lib.name !== name);
|
||||
export type IconLibraryResolver = (name: string, family?: string, variant?: string) => string;
|
||||
export type IconLocator = { name: string; family?: string; variant?: string; library?: string };
|
||||
export type IconMapping = (name: string, family?: string, variant?: string) => IconLocator | undefined;
|
||||
export type IconLibraryGetKey = (name: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
export type IconFetchedResult = string | typeof CACHEABLE_ERROR | typeof RETRYABLE_ERROR;
|
||||
export type IconLibraryGetMarkup = (url: string) => string | Promise<string>;
|
||||
|
||||
export type IconLibraryCacheFlat = Record<string, string>;
|
||||
export type IconLibraryCacheDeep =
|
||||
| IconLibraryCacheFlat
|
||||
| Record<string, IconLibraryCacheFlat>
|
||||
| Record<string, Record<string, IconLibraryCacheFlat>>;
|
||||
|
||||
export interface UnregisteredIconLibrary {
|
||||
name: string;
|
||||
getUrl?: IconLibraryResolver;
|
||||
system?: IconMapping;
|
||||
fallback?: IconMapping;
|
||||
mutator?: IconLibraryMutator;
|
||||
getCacheKey?: IconLibraryGetKey;
|
||||
getMarkup?: IconLibraryGetMarkup;
|
||||
|
||||
// Max depth: family → variant → icon name → markup
|
||||
// but may be shallower for libraries that don't use variants or families
|
||||
inlined?: IconLibraryCacheDeep;
|
||||
}
|
||||
|
||||
export async function fetchIcon(url: string) {
|
||||
try {
|
||||
let fileData = await fetch(url, { mode: 'cors' });
|
||||
|
||||
if (!fileData.ok) {
|
||||
return CACHEABLE_HTTP_ERRORS.includes(fileData.status) ? CACHEABLE_ERROR : RETRYABLE_ERROR;
|
||||
}
|
||||
|
||||
return fileData.text();
|
||||
} catch (e) {
|
||||
return RETRYABLE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
118
src/components/icon/library.wa.ts
Normal file
118
src/components/icon/library.wa.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { IconLibraryCacheDeep, UnregisteredIconLibrary } from './library.js';
|
||||
|
||||
let kitCode = '';
|
||||
|
||||
/** Sets the library's Web Awesome kit code. */
|
||||
export function setKitCode(code: string) {
|
||||
kitCode = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the library's Web Awesome kit code.
|
||||
*
|
||||
* The kit code is used to fetch premium assets, so it needs to be set for certain components to work correctly. This
|
||||
* isn't something we can infer, so the user will need to provide it using the `data-fa-kit-code` attribute. This can
|
||||
* be on any element, but ideally it should exist on the script that imports Web Awesome.
|
||||
*
|
||||
* <script src="bundle.js" data-fa-kit-code="abc123"></script>
|
||||
*
|
||||
* Alternatively, you can set the kit code manually using the exported `setKitCode()` function.
|
||||
*
|
||||
*/
|
||||
export function getKitCode() {
|
||||
if (!kitCode) {
|
||||
const el = document.querySelector('[data-fa-kit-code]');
|
||||
|
||||
if (el) {
|
||||
setKitCode(el.getAttribute('data-fa-kit-code') || '');
|
||||
}
|
||||
}
|
||||
|
||||
return kitCode;
|
||||
}
|
||||
|
||||
function getIconUrl(name: string, family: string, variant: string) {
|
||||
const kitCode = getKitCode();
|
||||
const isPro = kitCode.length > 0;
|
||||
let folder = 'solid';
|
||||
|
||||
// Classic
|
||||
if (family === 'classic') {
|
||||
if (variant === 'thin') folder = 'thin';
|
||||
if (variant === 'light') folder = 'light';
|
||||
if (variant === 'regular') folder = 'regular';
|
||||
if (variant === 'solid') folder = 'solid';
|
||||
}
|
||||
|
||||
// Sharp
|
||||
if (family === 'sharp') {
|
||||
if (variant === 'thin') folder = 'sharp-thin';
|
||||
if (variant === 'light') folder = 'sharp-light';
|
||||
if (variant === 'regular') folder = 'sharp-regular';
|
||||
if (variant === 'solid') folder = 'sharp-solid';
|
||||
}
|
||||
|
||||
// Duotone
|
||||
if (family === 'duotone') {
|
||||
if (variant === 'thin') folder = 'duotone-thin';
|
||||
if (variant === 'light') folder = 'duotone-light';
|
||||
if (variant === 'regular') folder = 'duotone-regular';
|
||||
if (variant === 'solid') folder = 'duotone';
|
||||
}
|
||||
|
||||
// Sharp Duotone
|
||||
if (family === 'sharp-duotone') {
|
||||
if (variant === 'thin') folder = 'sharp-duotone-thin';
|
||||
if (variant === 'light') folder = 'sharp-duotone-light';
|
||||
if (variant === 'regular') folder = 'sharp-duotone-regular';
|
||||
if (variant === 'solid') folder = 'sharp-duotone-solid';
|
||||
}
|
||||
|
||||
// Brands
|
||||
if (family === 'brands') {
|
||||
folder = 'brands';
|
||||
}
|
||||
|
||||
// Use the default CDN
|
||||
return isPro
|
||||
? `https://ka-p.fontawesome.com/releases/v6.7.2/svgs/${folder}/${name}.svg?token=${encodeURIComponent(kitCode)}`
|
||||
: `https://ka-f.fontawesome.com/releases/v6.7.2/svgs/${folder}/${name}.svg`;
|
||||
}
|
||||
|
||||
// Icons used for internal components, prefetched for speed
|
||||
export const inlined: IconLibraryCacheDeep = {
|
||||
classic: {
|
||||
solid: {
|
||||
check: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>`,
|
||||
'chevron-down': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>`,
|
||||
'chevron-left': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>`,
|
||||
'chevron-right': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>`,
|
||||
circle: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512z"/></svg>`,
|
||||
'eye-dropper': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M341.6 29.2L240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4L482.8 170.4c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6v42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480h42.4c21.2 0 41.6-8.4 56.6-23.4L309.4 335.9l-45.3-45.3L143.4 411.3c-3 3-7.1 4.7-11.3 4.7H96V379.9c0-4.2 1.7-8.3 4.7-11.3L221.4 247.9l-45.3-45.3L55.4 323.3z"/></svg>`,
|
||||
'grip-vertical': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M40 352l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 320c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 192l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 160c-22.1 0-40-17.9-40-40L0 72C0 49.9 17.9 32 40 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40z"/></svg>`,
|
||||
indeterminate: `<svg part="indeterminate-icon" class="icon" viewBox="0 0 16 16"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"><g stroke="currentColor" stroke-width="2"><g transform="translate(2.285714, 6.857143)"><path d="M10.2857143,1.14285714 L1.14285714,1.14285714"></path></g></g></g></svg>`,
|
||||
minus: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z"/></svg>`,
|
||||
pause: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"/></svg>`,
|
||||
play: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>`,
|
||||
star: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>`,
|
||||
user: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>`,
|
||||
xmark: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>`,
|
||||
},
|
||||
regular: {
|
||||
'circle-xmark': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"/></svg>`,
|
||||
copy: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M384 336H192c-8.8 0-16-7.2-16-16V64c0-8.8 7.2-16 16-16l140.1 0L400 115.9V320c0 8.8-7.2 16-16 16zM192 384H384c35.3 0 64-28.7 64-64V115.9c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1H192c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H256c35.3 0 64-28.7 64-64V416H272v32c0 8.8-7.2 16-16 16H64c-8.8 0-16-7.2-16-16V192c0-8.8 7.2-16 16-16H96V128H64z"/></svg>`,
|
||||
eye: `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"/></svg>`,
|
||||
'eye-slash': `<svg xmlns="http://www.w3.org/2000/svg" height="16" width="20" viewBox="0 0 640 512"><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm51.3 163.3l-41.9-33C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5zm-88-69.3L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8z"/></svg>`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const library: UnregisteredIconLibrary = {
|
||||
name: 'wa',
|
||||
getUrl: getIconUrl,
|
||||
// Cache icons using the free URL
|
||||
getCacheKey: url => url.replace(/\?token=[^&]+/, '').replace(/ka-p\./, 'ka-f.'),
|
||||
inlined,
|
||||
};
|
||||
|
||||
export default library;
|
||||
80
src/components/icon/registry.ts
Normal file
80
src/components/icon/registry.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type WaIcon from './icon.js';
|
||||
import IconLibrary, {
|
||||
CACHEABLE_ERROR,
|
||||
RETRYABLE_ERROR,
|
||||
fetchIcon,
|
||||
type IconFetchedResult,
|
||||
type IconLibraryCacheDeep,
|
||||
type IconLibraryCacheFlat,
|
||||
type UnregisteredIconLibrary,
|
||||
} from './library.js';
|
||||
import waDefaultLibrary from './library.wa.js';
|
||||
|
||||
export { CACHEABLE_ERROR, RETRYABLE_ERROR, fetchIcon };
|
||||
export type { IconFetchedResult, IconLibrary, IconLibraryCacheDeep, IconLibraryCacheFlat, UnregisteredIconLibrary };
|
||||
|
||||
let registry = IconLibrary.registry;
|
||||
let watchedIcons: WaIcon[] = [];
|
||||
|
||||
registerIconLibrary(waDefaultLibrary);
|
||||
registerIconLibrary('default', waDefaultLibrary);
|
||||
registerIconLibrary({ name: 'custom' });
|
||||
|
||||
/** Adds an icon to the list of watched icons. */
|
||||
export function watchIcon(icon: WaIcon) {
|
||||
watchedIcons.push(icon);
|
||||
}
|
||||
|
||||
/** Removes an icon from the list of watched icons. */
|
||||
export function unwatchIcon(icon: WaIcon) {
|
||||
watchedIcons = watchedIcons.filter(el => el !== icon);
|
||||
}
|
||||
|
||||
/** Returns a library from the registry. */
|
||||
export function getIconLibrary(name?: string) {
|
||||
return name ? registry.get(name) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an icon library to the registry, or overrides an existing one.
|
||||
* Optionally accepts a name argument, which will override the library's built-in name, allowing you to register aliases.
|
||||
*/
|
||||
export function registerIconLibrary(name: string, library: UnregisteredIconLibrary | IconLibrary): IconLibrary;
|
||||
export function registerIconLibrary(library: UnregisteredIconLibrary | IconLibrary): IconLibrary;
|
||||
export function registerIconLibrary(
|
||||
nameOrLibrary: string | UnregisteredIconLibrary | IconLibrary,
|
||||
library?: UnregisteredIconLibrary | IconLibrary,
|
||||
) {
|
||||
let name;
|
||||
if (typeof nameOrLibrary === 'string') {
|
||||
name = nameOrLibrary;
|
||||
} else {
|
||||
library = nameOrLibrary;
|
||||
}
|
||||
|
||||
if (!library) {
|
||||
throw new Error('No library provided');
|
||||
}
|
||||
|
||||
let instance = library instanceof IconLibrary ? library : new IconLibrary(library);
|
||||
|
||||
if (name) {
|
||||
instance = instance.extend({ name });
|
||||
}
|
||||
|
||||
registry.set(instance.name, instance);
|
||||
|
||||
// Redraw watched icons
|
||||
watchedIcons.forEach(icon => {
|
||||
if (icon.library === library.name) {
|
||||
icon.setIcon();
|
||||
}
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Removes an icon library from the registry. */
|
||||
export function unregisterIconLibrary(name: string) {
|
||||
registry.delete(name);
|
||||
}
|
||||
@@ -445,7 +445,7 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot name="clear-icon">
|
||||
<wa-icon name="circle-xmark" library="system" variant="regular"></wa-icon>
|
||||
<wa-icon name="system:circle-xmark" variant="regular"></wa-icon>
|
||||
</slot>
|
||||
</button>
|
||||
`
|
||||
@@ -463,12 +463,12 @@ export default class WaInput extends WebAwesomeFormAssociatedElement {
|
||||
${!this.passwordVisible
|
||||
? html`
|
||||
<slot name="show-password-icon">
|
||||
<wa-icon name="eye" library="system" variant="regular"></wa-icon>
|
||||
<wa-icon name="system:eye" variant="regular"></wa-icon>
|
||||
</slot>
|
||||
`
|
||||
: html`
|
||||
<slot name="hide-password-icon">
|
||||
<wa-icon name="eye-slash" library="system" variant="regular"></wa-icon>
|
||||
<wa-icon name="system:eye-slash" variant="regular"></wa-icon>
|
||||
</slot>
|
||||
`}
|
||||
</button>
|
||||
|
||||
@@ -211,7 +211,7 @@ export default class WaMenuItem extends WebAwesomeElement {
|
||||
?aria-expanded="${isSubmenuExpanded ? true : false}"
|
||||
>
|
||||
<span part="checked-icon" class="check">
|
||||
<wa-icon name="check" library="system" variant="solid" aria-hidden="true"></wa-icon>
|
||||
<wa-icon name="system:check" variant="solid" aria-hidden="true"></wa-icon>
|
||||
</span>
|
||||
|
||||
<slot name="prefix" part="prefix" class="prefix"></slot>
|
||||
@@ -222,8 +222,7 @@ export default class WaMenuItem extends WebAwesomeElement {
|
||||
|
||||
<span part="submenu-icon" class="chevron">
|
||||
<wa-icon
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
library="system"
|
||||
name="system:${isRtl ? 'chevron-left' : 'chevron-right'}"
|
||||
variant="solid"
|
||||
aria-hidden="true"
|
||||
></wa-icon>
|
||||
|
||||
@@ -184,14 +184,7 @@ export default class WaOption extends WebAwesomeElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<wa-icon
|
||||
part="checked-icon"
|
||||
class="check"
|
||||
name="check"
|
||||
library="system"
|
||||
variant="solid"
|
||||
aria-hidden="true"
|
||||
></wa-icon>
|
||||
<wa-icon part="checked-icon" class="check" name="system:check" variant="solid" aria-hidden="true"></wa-icon>
|
||||
<slot part="prefix" name="prefix" class="prefix"></slot>
|
||||
<slot part="label" class="label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
<slot part="suffix" name="suffix" class="suffix"></slot>
|
||||
|
||||
@@ -68,8 +68,7 @@ export default class WaRating extends WebAwesomeElement {
|
||||
* The function should return a string containing trusted HTML of the symbol to render at the specified value. Works
|
||||
* well with `<wa-icon>` elements.
|
||||
*/
|
||||
@property() getSymbol: (value: number) => string = () =>
|
||||
'<wa-icon name="star" library="system" variant="solid"></wa-icon>';
|
||||
@property() getSymbol: (value: number) => string = () => '<wa-icon name="system:star" variant="solid"></wa-icon>';
|
||||
|
||||
/** The component's size. */
|
||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||
|
||||
@@ -957,7 +957,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot name="clear-icon">
|
||||
<wa-icon name="circle-xmark" library="system" variant="regular"></wa-icon>
|
||||
<wa-icon name="system:circle-xmark" variant="regular"></wa-icon>
|
||||
</slot>
|
||||
</button>
|
||||
`
|
||||
@@ -966,7 +966,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
<slot name="suffix" part="suffix" class="suffix"></slot>
|
||||
|
||||
<slot name="expand-icon" part="expand-icon" class="expand-icon">
|
||||
<wa-icon library="system" name="chevron-down" variant="solid"></wa-icon>
|
||||
<wa-icon name="system:chevron-down" variant="solid"></wa-icon>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -404,8 +404,7 @@ export default class WaTabGroup extends WebAwesomeElement {
|
||||
part="scroll-button scroll-button-start"
|
||||
exportparts="base:scroll-button__base"
|
||||
class="scroll-button scroll-button-start"
|
||||
name=${isRtl ? 'chevron-right' : 'chevron-left'}
|
||||
library="system"
|
||||
name="system:${isRtl ? 'chevron-right' : 'chevron-left'}"
|
||||
variant="solid"
|
||||
label=${this.localize.term('scrollToStart')}
|
||||
@click=${this.handleScrollToStart}
|
||||
@@ -426,8 +425,7 @@ export default class WaTabGroup extends WebAwesomeElement {
|
||||
part="scroll-button scroll-button-end"
|
||||
class="scroll-button scroll-button-end"
|
||||
exportparts="base:scroll-button__base"
|
||||
name=${isRtl ? 'chevron-left' : 'chevron-right'}
|
||||
library="system"
|
||||
name="system:${isRtl ? 'chevron-left' : 'chevron-right'}"
|
||||
variant="solid"
|
||||
label=${this.localize.term('scrollToEnd')}
|
||||
@click=${this.handleScrollToEnd}
|
||||
|
||||
@@ -67,8 +67,7 @@ export default class WaTag extends WebAwesomeElement {
|
||||
<wa-icon-button
|
||||
part="remove-button"
|
||||
exportparts="base:remove-button__base"
|
||||
name="xmark"
|
||||
library="system"
|
||||
name="system:xmark"
|
||||
variant="solid"
|
||||
label=${this.localize.term('remove')}
|
||||
class="remove"
|
||||
|
||||
@@ -276,10 +276,10 @@ export default class WaTreeItem extends WebAwesomeElement {
|
||||
this.loading,
|
||||
() => html` <wa-spinner part="spinner" exportparts="base:spinner__base"></wa-spinner> `,
|
||||
)}
|
||||
<wa-icon name=${isRtl ? 'chevron-left' : 'chevron-right'} library="system" variant="solid"></wa-icon>
|
||||
<wa-icon name="system:${isRtl ? 'chevron-left' : 'chevron-right'}" variant="solid"></wa-icon>
|
||||
</slot>
|
||||
<slot class="expand-icon-slot" name="collapse-icon">
|
||||
<wa-icon name=${isRtl ? 'chevron-left' : 'chevron-right'} library="system" variant="solid"></wa-icon>
|
||||
<wa-icon name="system:${isRtl ? 'chevron-left' : 'chevron-right'}" variant="solid"></wa-icon>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
let basePath = '';
|
||||
let kitCode = '';
|
||||
|
||||
/** Sets the library's base path to the specified directory or URL. */
|
||||
export function setBasePath(path: string) {
|
||||
@@ -49,32 +48,3 @@ export function getBasePath(subpath = '') {
|
||||
// Return the base path without a trailing slash. If one exists, append the subpath separated by a slash.
|
||||
return basePath.replace(/\/$/, '') + (subpath ? `/${subpath.replace(/^\//, '')}` : ``);
|
||||
}
|
||||
|
||||
/** Sets the library's Web Awesome kit code. */
|
||||
export function setKitCode(code: string) {
|
||||
kitCode = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the library's Web Awesome kit code.
|
||||
*
|
||||
* The kit code is used to fetch premium assets, so it needs to be set for certain components to work correctly. This
|
||||
* isn't something we can infer, so the user will need to provide it using the `data-fa-kit-code` attribute. This can
|
||||
* be on any element, but ideally it should exist on the script that imports Web Awesome.
|
||||
*
|
||||
* <script src="bundle.js" data-fa-kit-code="abc123"></script>
|
||||
*
|
||||
* Alternatively, you can set the kit code manually using the exported `setKitCode()` function.
|
||||
*
|
||||
*/
|
||||
export function getKitCode() {
|
||||
if (!kitCode) {
|
||||
const el = document.querySelector('[data-fa-kit-code]');
|
||||
|
||||
if (el) {
|
||||
setKitCode(el.getAttribute('data-fa-kit-code') || '');
|
||||
}
|
||||
}
|
||||
|
||||
return kitCode;
|
||||
}
|
||||
|
||||
160
src/utilities/deep.ts
Normal file
160
src/utilities/deep.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
export function isPlainObject(obj: any) {
|
||||
return isObject(obj, 'Object');
|
||||
}
|
||||
|
||||
export function isObject(obj: any, type: string) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let proto = Object.getPrototypeOf(obj);
|
||||
return proto.constructor?.name === type;
|
||||
}
|
||||
|
||||
type Property = keyof any;
|
||||
export type EachCallback<T = any> = (value: any, path: Property[], parent?: object) => T;
|
||||
export type EachOptions = {
|
||||
filter?: EachCallback<boolean>;
|
||||
descend?: EachCallback<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterate over a deep object or array recursively
|
||||
*
|
||||
* @param obj The object to iterate over. Can be a plain object,or array, or even a primitive value.
|
||||
* @param callback. The callback to execute for each value. Will not be called on circular references.
|
||||
* Returning `false` will stop the iteration and a non-undefined value will overwrite the value.
|
||||
*/
|
||||
export function deepEach(obj: any, callback: EachCallback, options: EachOptions = {}): any {
|
||||
// Used to track circular references
|
||||
// WeakSet is used to avoid memory leaks
|
||||
let visited = new WeakSet();
|
||||
|
||||
return _deepEach(
|
||||
obj,
|
||||
(value, path, parent) => {
|
||||
if (value && typeof value === 'object') {
|
||||
if (visited.has(value)) {
|
||||
// Abort mission
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.add(value);
|
||||
}
|
||||
|
||||
return callback(value, path, parent);
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private recursive function to support `deepEach()`.
|
||||
* @param value Same as {@link deepEach}
|
||||
* @param callback Same as {@link deepEach}
|
||||
* @param path The path to `value` from the root object as an array of keys.
|
||||
* @param parent The parent object of the current value. `value === parent[path.at(-1)]`
|
||||
*/
|
||||
function _deepEach(value: any, callback: EachCallback, options: EachOptions, path: Property[] = [], parent?: object) {
|
||||
if (path.length > 0) {
|
||||
let included = options.filter?.(value, path, parent) ?? true;
|
||||
|
||||
if (included) {
|
||||
let ret = callback(value, path, parent!);
|
||||
|
||||
if (ret !== undefined) {
|
||||
// Overwrite value
|
||||
let key = path.at(-1);
|
||||
// @ts-expect-error TS doesn't know that if path.length > 0, parent is an object
|
||||
value = parent[key] = ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isArray = Array.isArray(value);
|
||||
let isObject = !isArray && isPlainObject(value);
|
||||
let isContainer = isArray || isObject;
|
||||
|
||||
if (isContainer) {
|
||||
let descend = options.descend?.(value, path, parent) ?? true;
|
||||
|
||||
if (!descend) {
|
||||
// Do not descend further
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
_deepEach(value[i], callback, options, [...path, i], value);
|
||||
}
|
||||
} else if (isObject) {
|
||||
for (let key in value) {
|
||||
_deepEach(value[key], callback, options, [...path, key], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like Object.entries, but for deeply nested objects.
|
||||
* For shallow objects the output is the same as Object.entries.
|
||||
* Circular references will not be included.
|
||||
* @param obj The object to iterate over.
|
||||
* @param options.filter - If this returns false, the entry is not added to the result.
|
||||
* @param options.transformPath - Transform the path, e.g. to serialize it to a single key or reduce levels.
|
||||
* @returns Array of arrays. In each item, the last value is the value, and all values before that are the keys.
|
||||
* So for an object with N levels, the result will be an array of arrays with N+1 items each.
|
||||
*/
|
||||
export function deepEntries(obj: any, options?: EachOptions): any[][] {
|
||||
let entries: any[][] = [];
|
||||
deepEach(
|
||||
obj,
|
||||
(value, path) => {
|
||||
if (Array.isArray(value) || isPlainObject(value)) {
|
||||
// We only want leaf values
|
||||
return;
|
||||
}
|
||||
|
||||
entries.push([...path, value]);
|
||||
},
|
||||
options,
|
||||
);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export type FlattenOptions = {
|
||||
getKey?: (path: Property[], value?: any) => Property;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a potentially deeply nested object to a flat object.
|
||||
* Circular references will not be included.
|
||||
* @param obj
|
||||
* @param options.getKey - A function to transform the path to a key. The default is to join the path with '.'.
|
||||
*/
|
||||
export function flatten<T = unknown>(
|
||||
obj: Record<keyof any, unknown>,
|
||||
options: FlattenOptions = {},
|
||||
): Record<keyof any, T> {
|
||||
let { getKey = path => path.join('.') } = options;
|
||||
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return {} as Record<keyof any, T>;
|
||||
}
|
||||
|
||||
let entries = deepEntries(obj)
|
||||
.map(pathAndValue => {
|
||||
if (pathAndValue.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let value = pathAndValue.pop() as T;
|
||||
let path = pathAndValue as Property[];
|
||||
let key = getKey(path, value);
|
||||
return [key, value];
|
||||
})
|
||||
.filter(Boolean) as [string, T][];
|
||||
|
||||
return Object.fromEntries(entries) as Record<keyof any, T>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { registerIconLibrary, unregisterIconLibrary } from './components/icon/library.js';
|
||||
export { getKitCode, setKitCode } from './components/icon/library.wa.js';
|
||||
export { registerIconLibrary, unregisterIconLibrary } from './components/icon/registry.js';
|
||||
export { discover, preventTurboFouce, startLoader, stopLoader } from './utilities/autoloader.js';
|
||||
export { getBasePath, getKitCode, setBasePath, setKitCode } from './utilities/base-path.js';
|
||||
export { getBasePath, setBasePath } from './utilities/base-path.js';
|
||||
export { allDefined } from './utilities/defined.js';
|
||||
export { registerTranslation } from './utilities/localize.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user