Skip to content

Commit

Permalink
Merge pull request #35 from nervina-labs/feat/image
Browse files Browse the repository at this point in the history
feat: Add image
  • Loading branch information
yuche authored Oct 14, 2021
2 parents 160813f + f833e29 commit 585eadf
Show file tree
Hide file tree
Showing 15 changed files with 519 additions and 214 deletions.
1 change: 0 additions & 1 deletion .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ module.exports = {
stories: [],
addons: [
'@storybook/addon-essentials',
// "storybook-addon-performance/register",
"@storybook/addon-a11y",
"@storybook/addon-toolbars",
],
Expand Down
3 changes: 3 additions & 0 deletions apps/mibao-ui-docs/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ module.exports = {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest'
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$': 'jest-transform-stub'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/mibao-ui-docs'
}
53 changes: 53 additions & 0 deletions apps/mibao-ui-docs/src/app/pages/image.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Story, Meta } from '@storybook/react'
import {
Image as MibaoImage,
MibaoProvider
} from 'mibao-ui'
import { Spinner, Stack } from '@chakra-ui/react'

export default {
component: MibaoImage,
title: 'Components/Image',
argTypes: {
aspectRatio: {
defaultValue: true
},
objectFit: {
options: ['fill', 'contain', 'cover', 'none', 'scale-down'],
control: { type: 'select' }
},
loading: {
options: ['', 'eager', 'lazy'],
control: { type: 'select' }
},
crossOrigin: {
options: ['', 'anonymous', 'use-credentials'],
control: { type: 'select' }
}
},
parameters: {
fallback: 'ReactNode'
}
} as Meta

const Template: Story = (args) =>
<MibaoProvider>
<Stack spacing={3} direction="row">
<MibaoImage {...args} />
<MibaoImage {...args} loader={<Spinner color="primary.600" emptyColor="gray.200" m="auto" />} />
<MibaoImage {...args} fallback={<div>Failed</div>} />
</Stack>
</MibaoProvider>

export const Image = Template.bind({})
Image.args = {
src: 'https://oss.jinse.cc/production/bd7c7508-026e-4fcd-abf8-1fa1061f4d6c.png',
alt: '',
aspectRatio: true,
width: '200px',
height: '',
objectFit: 'cover',
crossOrigin: '',
disableContextMenu: false,
resizeScale: 200
}
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ const { getJestProjects } = require('@nrwl/jest');

module.exports = {
projects: getJestProjects(),
moduleNameMapper: {
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$": "jest-transform-stub"
}
};
3 changes: 2 additions & 1 deletion libs/mibao-ui/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"semi": ["error", "never"],
"import/extensions": 0,
"@typescript-eslint/consistent-type-assertions": 0,
"@typescript-eslint/explicit-function-return-type": 0
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/strict-boolean-expressions": 0
}
},
{
Expand Down
13 changes: 13 additions & 0 deletions libs/mibao-ui/assets/images/fallback.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions libs/mibao-ui/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module.exports = {
transform: {
'^.+\\.[tj]sx?$': 'babel-jest'
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2|svg)$': 'jest-transform-stub'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/mibao-ui'
}
9 changes: 9 additions & 0 deletions libs/mibao-ui/src/constants/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const OSS_IMG_PROCESS_QUERY_KEY = process.env.OSS_IMG_PROCESS_QUERY_KEY ?? 'x-oss-process'
export const OSS_IMG_PROCESS_QUERY_KEY_SCALE = process.env.OSS_IMG_PROCESS_QUERY_KEY_SCALE ?? 'image/resize,s_'
export const OSS_IMG_PROCESS_QUERY_KEY_FORMAT_WEBP = process.env.OSS_IMG_PROCESS_QUERY_KEY_FORMAT_WEBP ?? '/format,webp'
export const OSS_IMG_HOSTS = process.env.OSS_IMG_HOSTS
? process.env.OSS_IMG_HOSTS.split(',')
: [
'https://oss.jinse.cc',
'https://goldenlegend.oss-accelerate.aliyuncs.com'
]
2 changes: 1 addition & 1 deletion libs/mibao-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './index.scss'

export * from './lib/image/image'
export * from './lib/drawer/drawer'

export * from './lib/tooltip/tooltip'
export * from './lib/tab/tab'
export * from './theme'
Expand Down
26 changes: 26 additions & 0 deletions libs/mibao-ui/src/lib/image/image.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.image {
}

.image[hide="true"] {
opacity: 0;
pointer-events: none;
}

.image[data-aspect-ratio="true"] {
aspect-ratio: 1/1;
}

.loading {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
}

.container {
position: relative;
width: auto;
height: auto;
}
10 changes: 10 additions & 0 deletions libs/mibao-ui/src/lib/image/image.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react'

import { Image } from './image'

describe('Image', () => {
it('should render successfully', () => {
const { baseElement } = render(<Image />)
expect(baseElement).toBeTruthy()
})
})
88 changes: 88 additions & 0 deletions libs/mibao-ui/src/lib/image/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Box, Image as ChakraImage, ImageProps as ChakraImageProps, Skeleton } from '@chakra-ui/react'
import styles from './image.module.scss'
import { useState, useMemo, useEffect, useCallback, ReactNode } from 'react'
import FALLBACK_SRC from '../../../assets/images/fallback.svg'
import { addParamsToUrl, disableImageContext, getImagePreviewUrl, omit } from '../../utils'

export interface ImageProps extends ChakraImageProps {
aspectRatio?: boolean
loader?: ReactNode
srcQueryParams?: Record<string, string | number>
disableContextMenu?: boolean
resizeScale?: number // Specifies the shortest edge of the target zoom graph.
}

export const Image = (props: ImageProps) => {
const { fallbackSrc = FALLBACK_SRC } = props
const [isLoading, setIsLoading] = useState<boolean>(true)
const [isError, setIsError] = useState(false)
// loading element
const loaderEl = useMemo(() => {
if (props.loader) return props.loader
return <Skeleton width="100%" height="100%" rounded={props.rounded} />
}, [props.loader, props.rounded])
// src
const imageSrc = useMemo(() => {
if (!props.src) return props.src
const srcQueryParams = props.srcQueryParams ?? {}
if (props.resizeScale) {
return addParamsToUrl(getImagePreviewUrl(props.src, props.resizeScale), srcQueryParams)
}
return addParamsToUrl(props.src, srcQueryParams)
}, [props])
// omit props
const imageProps = useMemo(() => omit(props, [
'aspectRatio',
'loader',
'srcQueryParams',
'resizeScale'
]), [props])

useEffect(() => {
if (props.src) {
setIsLoading(true)
setIsError(false)
}
}, [props.src])

const onLoaded = useCallback((event) => {
setIsLoading(false)
if (props.onLoad) props.onLoad(event)
}, [props])

const onError = useCallback((event) => {
setIsLoading(false)
setIsError(true)
if (props.onError) props.onError(event)
}, [props])

return (
<Box className={styles.container} style={{
width: props.width as string,
height: props.height as string
}}>
{
isLoading && (
<Box className={styles.loading}>
{loaderEl}
</Box>
)
}
<ChakraImage
{...imageProps}
hide={!props.src}
data-aspect-ratio={props.aspectRatio}
src={imageSrc}
className={`${styles.image} ${props.className ? props.className : ''}`}
onLoad={onLoaded}
onError={onError}
objectFit={props.objectFit ?? 'cover'}
htmlWidth={props.width as string}
htmlHeight={props.height as string}
fallbackSrc={isError ? fallbackSrc : undefined}
fallback={isError ? props.fallback : undefined}
onContextMenu={props.disableContextMenu ? disableImageContext : undefined}
/>
</Box>
)
}
53 changes: 53 additions & 0 deletions libs/mibao-ui/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
OSS_IMG_HOSTS,
OSS_IMG_PROCESS_QUERY_KEY,
OSS_IMG_PROCESS_QUERY_KEY_SCALE
} from '../constants/env'

export function addParamsToUrl (
url: string,
params: Record<string, string | number>,
options?: {
ignoreDuplicates?: boolean
}
): string {
if (!url) {
return url
}
const urlObj = new URL(url)
const urlSearchParams = urlObj.searchParams
Object.keys(params).forEach((key) => {
if (!urlSearchParams.has(key) || options?.ignoreDuplicates) {
urlSearchParams.set(key, String(params[key]))
}
})
return decodeURI(urlObj.toString())
}

export function getImagePreviewUrl<U extends string | undefined> (
url: U,
size = 300
): U {
if (!url) {
return url
}
const urlObj = new URL(url)
const isOssHost = OSS_IMG_HOSTS.some((host) => url?.startsWith(host))
const isSvgOrWebp = /\.(svg|webp)$/i.test(urlObj.pathname)
if (!isOssHost || isSvgOrWebp) {
return url
}
const params: Record<string, string | number> = {}
params[OSS_IMG_PROCESS_QUERY_KEY] = `${OSS_IMG_PROCESS_QUERY_KEY_SCALE}${size}`
return addParamsToUrl(url, params) as U
}

export function omit<T extends {[key: string]: any}, K extends keyof T> (obj: T, keys: K[]): Omit<T, K> {
return keys.reduce((acc, key) => ({ ...acc, [key]: undefined }), obj)
}

export const disableImageContext = (e: any): boolean => {
e?.preventDefault?.()
e?.stopPropagation?.()
return false
}
Loading

1 comment on commit 585eadf

@vercel
Copy link

@vercel vercel bot commented on 585eadf Oct 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.