Skip to content

Commit

Permalink
feat(Icon): cache morpheme icons in local storage
Browse files Browse the repository at this point in the history
  • Loading branch information
gravitano committed Sep 7, 2023
1 parent 5db2e54 commit 6a50f4f
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 37 deletions.
22 changes: 21 additions & 1 deletion packages/icon/src/Icon.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const MorphemeIcons: Story = (args) => ({
<Icon v-bind='args' name="ic:round-local-activity" />
<Icon v-bind='args' name="ph:activity-bold" />
<p class="font-medium mb-2 mt-5">HypenCase</p>
<p class="font-medium mb-2 mt-5">Morpheme Icons</p>
<Icon v-bind='args' name="remix:home-line" />
<Icon v-bind='args' name="untitled:activity-heart" />
<Icon v-bind='args' name="iconsax:outline/add" />
Expand All @@ -104,3 +104,23 @@ export const MorphemeIcons: Story = (args) => ({
<Icon v-bind='args' name="remix:non-exist-icon" />
`,
});

export const MorphemeIconsNoCache: Story = (args) => ({
components: {Icon},
setup() {
return {args};
},
template: `
<p class="font-medium mb-2">
By default morpheme icons are cached and stored in local storage, but you can disable it by setting via <code>no-cache</code> prop.
</p>
<Icon v-bind='args' name="remix:home-line" no-cache />
<Icon v-bind='args' name="untitled:activity-heart" no-cache />
<Icon v-bind='args' name="iconsax:outline/add" no-cache />
<Icon v-bind='args' name="iconsax:bulk/add" no-cache />
<Icon v-bind='args' name="iconsax:broken/add" no-cache />
<Icon v-bind='args' name="iconsax:linear/add" no-cache />
<Icon v-bind='args' name="iconsax:solid/add" no-cache />
<Icon v-bind='args' name="iconsax:twotone/add" no-cache />
`,
});
98 changes: 62 additions & 36 deletions packages/icon/src/Icon.vue
Original file line number Diff line number Diff line change
@@ -1,66 +1,92 @@
<script setup lang="ts">
import { Icon as Iconify } from '@iconify/vue/dist/offline';
import { loadIcon } from '@iconify/vue';
import { computed, defineComponent, h, ref, shallowRef, watch } from 'vue';
import { type DefaultSizes, defaultSizes } from '@morpheme/theme/defaultTheme';
import {Icon as Iconify} from '@iconify/vue/dist/offline';
import {loadIcon} from '@iconify/vue';
import {computed, defineComponent, h, ref, shallowRef, watch} from 'vue';
import {type DefaultSizes, defaultSizes} from '@morpheme/theme/defaultTheme';
export type Props = {
name: string;
size?: DefaultSizes | string | number;
noCache?: boolean;
};
const props = withDefaults(defineProps<Props>(), {
size: 'md',
noCache: false,
});
const isFetching = ref(false);
const icon = shallowRef();
const isMorphemeIcons = computed(() => {
return props.name.startsWith('untitled:') || props.name.startsWith('remix:') || props.name.startsWith('iconsax:') || props.name.startsWith('vuesax:')
})
return (
props.name.startsWith('untitled:') ||
props.name.startsWith('remix:') ||
props.name.startsWith('iconsax:') ||
props.name.startsWith('vuesax:')
);
});
function normalizeName(name: string) {
name = String(name).replace('untitled:', '')
name = String(name)
.replace('untitled:', '')
.replace('remix:', '')
.replace('iconsax:', '')
.replace('vuesax:', '')
.replace('vuesax:', '');
return toKebabCase(name)
return toKebabCase(name);
}
function toKebabCase(string: string) {
return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
}
function createIconComponent(content: string) {
return defineComponent({
setup() {
return () =>
h('span', {
innerHTML: content,
class: classes.value,
style: style.value,
...ariaProps,
});
},
});
}
async function loadIconComponent() {
isFetching.value = true;
if (isMorphemeIcons.value) {
const [collection, name] = props.name.split(':')
const [variant, iconName] = name.split('/')
const variantName = collection === 'iconsax' ? `/${variant}` : ''
const formattedName = normalizeName(iconName || name)
try {
const res = await fetch(`https://cdn.jsdelivr.net/npm/[email protected]/${collection}${variantName}/${formattedName}.svg`)
const content = await res.text()
icon.value = defineComponent({
setup() {
return () => h('span', {
innerHTML: res.status === 200 ? content : props.name,
class: classes.value,
style: style.value,
...ariaProps
})
}
})
} catch(e: any) {
console.warn(`Icon ${props.name} not found`)
} finally {
const [collection, name] = props.name.split(':');
const [variant, iconName] = name.split('/');
const variantName = collection === 'iconsax' ? `/${variant}` : '';
const formattedName = normalizeName(iconName || name);
const storageName = `morphemeicons-${collection}${variantName}-${formattedName}`;
const cachedIcon = localStorage.getItem(storageName);
if (cachedIcon && !props.noCache) {
icon.value = createIconComponent(cachedIcon);
isFetching.value = false;
} else {
try {
const res = await fetch(
`https://cdn.jsdelivr.net/npm/[email protected]/${collection}${variantName}/${formattedName}.svg`,
);
const content = await res.text();
localStorage.setItem(storageName, content);
icon.value = createIconComponent(
res.status === 200 ? content : props.name,
);
} catch (e: any) {
console.warn(`Icon ${props.name} not found`);
icon.value = createIconComponent(props.name);
} finally {
isFetching.value = false;
}
}
}
else {
icon.value = await loadIcon(props.name).catch(() => { });
} else {
icon.value = await loadIcon(props.name).catch(() => {});
}
isFetching.value = false;
}
Expand All @@ -78,14 +104,14 @@ const style = computed(() => {
height: props.size,
};
}
return {}
return {};
});
loadIconComponent();
const ariaProps = {
'aria-hidden': true
}
'aria-hidden': true,
};
</script>

<template>
Expand Down

0 comments on commit 6a50f4f

Please sign in to comment.