Skip to content

Commit

Permalink
feat(NcIcon): add a generic component for icons
Browse files Browse the repository at this point in the history
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
  • Loading branch information
ShGKme committed Jan 4, 2025
1 parent 74bfe86 commit fde18d4
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 0 deletions.
88 changes: 88 additions & 0 deletions src/components/NcIcon/NcIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<docs>
## General

A component to render an icons. It can render:
- A Vue component icon like `vue-material-design-icons/Account.vue`
- An SVG icon like `@mdi/svg/svg/account.svg`
- A path icon like `@mdi/js/account.js`
- An URL icon like `https://path/to/icon.svg` or `data:image/svg+xml;base64,...`
- A class icon like `icon-account` (not recommended)

```vue
<script>
import IconAccountComp from 'vue-material-design-icons/Account.vue'
import IconAccountSvg from '@mdi/svg/svg/account.svg?raw'
import { mdiAccount } from '@mdi/js'

export default {
setup() {
return { IconAccountComp, IconAccountSvg, mdiAccount }
},
}
</script>

<template>
<div>
<NcIcon :icon="IconAccountComp" />
<NcIcon :icon="IconAccountSvg" />
<NcIcon :icon="mdiAccount" />
<NcIcon icon="icon-user-white" />
</div>
</template>
```
</docs>

<script setup>
import { computed } from 'vue'
import { useIcon } from './useIcon.ts'
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'

const props = defineProps({
icon: { type: [Object, String], required: true },
size: { type: [Number, String], default: 20 },
inline: { type: Boolean, default: false },
})

const normalizedIcon = useIcon(() => props.icon)
const normalizedSize = computed(() => typeof props.size === 'number' ? `${props.size}px` : props.size)
</script>

<template>
<component :is="icon" v-if="normalizedIcon.type === 'component'" :size="normalizedSize" />
<NcIconSvgWrapper v-else-if="normalizedIcon.type === 'svg'" :svg="normalizedIcon.icon" :inline="inline" />
<NcIconSvgWrapper v-else-if="normalizedIcon.type === 'path'" :path="normalizedIcon.icon" :inline="inline" />
<span v-else-if="normalizedIcon.type === 'class'"
class="icon"
:class="[normalizedIcon.icon, { inline }]"
:style="'--icon-size: ' + normalizedSize"
aria-hidden="true" />
<span v-else-if="normalizedIcon.type === 'url'"
class="icon icon-url"
:class="{ inline }"
:style="`background-image: url('${normalizedIcon.icon}')`"
aria-hidden="true" />
</template>

<style scoped>
.icon {
display: flex;
justify-content: center;
align-items: center;
min-width: var(--default-clickable-area);
min-height: var(--default-clickable-area);
height: var(--icon-size);
width: var(--icon-size);

&.inline {
display: inline-flex;
min-width: fit-content;
min-height: fit-content;
vertical-align: text-bottom;
}
}
</style>
43 changes: 43 additions & 0 deletions src/components/NcIcon/normalizeIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Component } from 'vue'

export type IconComponent = Component
export type IconPath = `M${number}${string}`
export type IconSvg = `<svg${string}>${string}</svg>`
export type IconUrl = `http://${string}` | `https://${string}` | `data:${string}`
export type IconClass = `icon-${string}`
export type IconGeneral = IconComponent | IconPath | IconClass | IconSvg | IconUrl

export type IconNormalized = { type: 'component', icon: IconComponent }
| { type: 'path', icon: IconPath }
| { type: 'svg', icon: IconSvg }
| { type: 'url', icon: IconUrl }
| { type: 'class', icon: IconClass }
| { type: 'unknown', icon: IconGeneral }

/**
*
* @param icon - Icon in any supported format
*/
export function normalizeIcon(icon: IconGeneral): IconNormalized {
if (typeof icon === 'object' || typeof icon === 'function') {
return { type: 'component', icon: icon as IconComponent }
}
if (icon.startsWith('<svg')) {
return { type: 'svg', icon: icon as IconSvg }
}
if (icon.startsWith('M')) {
return { type: 'path', icon: icon as IconPath }
}
if (icon.startsWith('http://') || icon.startsWith('https://') || icon.startsWith('data:')) {
return { type: 'url', icon: icon as IconUrl }
}
if (icon.startsWith('icon-')) {
return { type: 'class', icon: icon as IconClass }
}
return { type: 'unknown', icon: icon as IconGeneral }
}
19 changes: 19 additions & 0 deletions src/components/NcIcon/useIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ComputedRef } from 'vue'
import type { MaybeRefOrGetter } from '@vueuse/core'
import type { IconGeneral, IconNormalized } from './normalizeIcon.ts'
import { computed } from 'vue'
import { toValue } from '@vueuse/core'
import { normalizeIcon } from './normalizeIcon.ts'

/**
* Reactive normalizeIcon
* @param icon - Icon in any supported format
*/
export function useIcon(icon: MaybeRefOrGetter<IconGeneral>): ComputedRef<IconNormalized> {
return computed(() => normalizeIcon(toValue(icon)))
}

0 comments on commit fde18d4

Please sign in to comment.