Skip to content

Commit

Permalink
feat: image init
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jun 17, 2023
1 parent fc6f268 commit 9e6cb36
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 64 deletions.
2 changes: 1 addition & 1 deletion src/app/notes/Paper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const Paper: Component = ({ children }) => {
className={clsx(
'relative bg-base-100 md:col-start-1 lg:col-auto',
'-m-4 p-[2rem_1rem] lg:m-0 lg:p-[30px_45px]',
'rounded-[0_6px_6px_0] border border-[#bbb3]',
'rounded-[0_6px_6px_0] border border-[#bbb3] shadow-sm dark:shadow-[#333]',
'note-layout-main',
)}
>
Expand Down
36 changes: 36 additions & 0 deletions src/app/notes/[id]/page.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.with-indent {
:global {
ul .indent,
.paragraph .indent {
border-bottom: 1px solid #00b8bb41;
}

.paragraph:not(:nth-child(1)) > span.indent {
&:nth-child(1) {
margin-left: 2rem;
}
}

main {
> p:first-child {
margin-bottom: 2rem;
}

.paragraph:first-child::first-letter {
float: left;
font-size: 2.4em;
margin: 0 0.2em 0 0;
}
}
}
}

.with-serif {
:global {
main {
@apply font-serif;

font-size: 16px;
}
}
}
21 changes: 19 additions & 2 deletions src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import { useEffect } from 'react'
import { Balancer } from 'react-wrap-balancer'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { useParams } from 'next/navigation'
import type { MarkdownToJSX } from '~/components/ui/markdown'

import { PageDataHolder } from '~/components/common/PageHolder'
import { useSetHeaderMetaInfo } from '~/components/layout/header/internal/hooks'
Expand All @@ -16,6 +18,8 @@ import { ArticleElementProvider } from '~/providers/article/article-element-prov
import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'

import styles from './page.module.css'

const PageImpl = () => {
const { id } = useParams() as { id: string }
const { data } = useNoteByNidQuery(id)
Expand Down Expand Up @@ -52,7 +56,9 @@ const PageImpl = () => {
.format('YYYY 年 M 月 D 日 dddd')

return (
<article className="prose">
<article
className={clsx('prose', styles['with-indent'], styles['with-serif'])}
>
<header>
<h1 className="mt-8 text-left font-bold text-base-content/95">
<Balancer>{note.title}</Balancer>
Expand All @@ -66,7 +72,7 @@ const PageImpl = () => {
</header>

<ArticleElementProvider>
<Markdown value={note.text} className="text-[1.05rem]" />
<Markdown as="main" renderers={Markdownrenderers} value={note.text} />

<NoteLayoutRightSidePortal>
<Toc className="sticky top-[120px] ml-4 mt-[120px]" />
Expand All @@ -77,6 +83,17 @@ const PageImpl = () => {
)
}

const Markdownrenderers: { [name: string]: Partial<MarkdownToJSX.Rule> } = {
text: {
react(node, _, state) {
return (
<span className="indent" key={state?.key}>
{node.content}
</span>
)
},
},
}
export default PageDataHolder(PageImpl, () => {
const { id } = useParams() as { id: string }
return useNoteByNidQuery(id)
Expand Down
23 changes: 23 additions & 0 deletions src/components/common/Lazyload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react'
import { useInView } from 'react-intersection-observer'
import type { FC, PropsWithChildren } from 'react'
import type { IntersectionOptions } from 'react-intersection-observer'

export type LazyLoadProps = {
offset?: number
placeholder?: React.ReactNode
} & IntersectionOptions
export const LazyLoad: FC<PropsWithChildren & LazyLoadProps> = (props) => {
const { placeholder = null, offset = 0, ...rest } = props
const { ref, inView } = useInView({
triggerOnce: true,
rootMargin: `${offset || 0}px`,
...rest,
})
return (
<>
<span data-testid="lazyload-indicator" ref={ref} />
{!inView ? placeholder : props.children}
</>
)
}
271 changes: 271 additions & 0 deletions src/components/ui/image/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import React, {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { clsx } from 'clsx'
import mediumZoom from 'medium-zoom'
import type {
CSSProperties,
DetailedHTMLProps,
FC,
ImgHTMLAttributes,
} from 'react'

import styles from './index.module.css'
import { useCalculateNaturalSize } from './use-calculate-size'

interface ImageProps {
defaultImage?: string
src: string
alt?: string
height?: number | string
width?: number | string
backgroundColor?: string
popup?: boolean
overflowHidden?: boolean
getParentElWidth?: ((parentElementWidth: number) => number) | number
showErrorMessage?: boolean
}

const Image: FC<
{
popup?: boolean
height?: number | string
width?: number | string
loaderFn: () => void
loaded: boolean
} & Pick<
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>,
'src' | 'alt'
>
> = memo(({ src, alt, height, width, popup = false, loaded, loaderFn }) => {
const imageRef = useRef<HTMLImageElement>(null)

useEffect(() => {
if (!popup) {
return
}
const $image = imageRef.current
if ($image) {
const zoom = mediumZoom($image, {
background: 'var(--light-bg)',
})

return () => {
zoom.detach(zoom.getImages())
}
}
}, [popup])

useEffect(() => {
loaderFn()
}, [loaderFn])

return (
<>
<div
className={clsx(
styles['lazyload-image'],
!loaded && styles['image-hide'],
)}
data-status={loaded ? 'loaded' : 'loading'}
onAnimationEnd={onImageAnimationEnd}
>
<img
src={src}
alt={alt}
ref={imageRef}
loading="lazy"
style={{ width, height }}
/>
</div>
</>
)
})

const onImageAnimationEnd: React.AnimationEventHandler<HTMLDivElement> = (
e,
) => {
;(e.target as HTMLElement).dataset.animated = '1'
}

export type ImageLazyRef = { status: 'loading' | 'loaded' }

export const LazyImage = memo(
forwardRef<
ImageLazyRef,
ImageProps &
DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>
>((props, ref) => {
const {
defaultImage,
src,
alt,
height,
width,
backgroundColor = 'rgb(111,111,111)',
popup = false,
style,
overflowHidden = false,
getParentElWidth = (w) => w,
showErrorMessage,
...rest
} = props
useImperativeHandle(ref, () => {
return {
status: loaded ? 'loaded' : ('loading' as any),
}
})
const realImageRef = useRef<HTMLImageElement>(null)
const placeholderRef = useRef<HTMLDivElement>(null)

const wrapRef = useRef<HTMLDivElement>(null)
const [calculatedSize, calculateDimensions] = useCalculateNaturalSize()

const [loaded, setLoad] = useState(false)
const loaderFn = useCallback(() => {
if (!src || loaded) {
return
}

const image = new window.Image()
image.src = src as string
// FIXME
const parentElement = wrapRef.current?.parentElement?.parentElement

if (!height && !width) {
calculateDimensions(
image,
typeof getParentElWidth == 'function'
? getParentElWidth(
parentElement
? parseFloat(getComputedStyle(parentElement).width)
: 0,
)
: getParentElWidth,
)
}

image.onload = () => {
setLoad(true)
try {
if (placeholderRef && placeholderRef.current) {
placeholderRef.current.classList.add('hide')
}

// eslint-disable-next-line no-empty
} catch {}
}
if (showErrorMessage) {
image.onerror = () => {
try {
if (placeholderRef && placeholderRef.current) {
placeholderRef.current.innerHTML = `<p style="color:${
isDarkColorHex(backgroundColor) ? '#eee' : '#333'
};z-index:2"><span>图片加载失败!</span><br/>
<a style="margin: 0 12px;word-break:break-all;white-space:pre-wrap;display:inline-block;" href="${escapeHTMLTag(
image.src,
)}" target="_blank">${escapeHTMLTag(image.src)}</a></p>`
}
// eslint-disable-next-line no-empty
} catch {}
}
}
}, [
src,
loaded,
height,
width,
calculateDimensions,
getParentElWidth,
backgroundColor,
showErrorMessage,
])
const memoPlaceholderImage = useMemo(
() => (
<PlaceholderImage
height={height}
width={width}
backgroundColor={backgroundColor}
ref={placeholderRef}
/>
),
[backgroundColor, height, width],
)

const imageWrapperStyle = useMemo<CSSProperties>(
() => ({
height: loaded ? undefined : height || calculatedSize.height,
width: loaded ? undefined : width || calculatedSize.width,

...(overflowHidden ? { overflow: 'hidden', borderRadius: '3px' } : {}),
}),
[
calculatedSize.height,
calculatedSize.width,
height,
loaded,
overflowHidden,
width,
],
)
return (
<figure style={style} className="inline-block">
{defaultImage ? (
<img src={defaultImage} alt={alt} {...rest} ref={realImageRef} />
) : (
<div
className={clsx(
'relative m-auto inline-block min-h-[1px] max-w-full transition-none',
rest.className,
)}
style={imageWrapperStyle}
ref={wrapRef}
data-info={JSON.stringify({ height, width, calculatedSize })}
data-src={src}
>
<LazyLoad offset={100} placeholder={memoPlaceholderImage}>
<Image
src={src}
alt={alt}
height={height || calculatedSize.height}
width={width || calculatedSize.width}
popup={popup}
loaded={loaded}
loaderFn={loaderFn}
/>
{!loaded && memoPlaceholderImage}
</LazyLoad>
</div>
)}
{alt && <figcaption className={styles['img-alt']}>{alt}</figcaption>}
</figure>
)
}),
)

const PlaceholderImage = memo(
forwardRef<
HTMLDivElement,
{ ref: any; className?: string } & Partial<ImageProps>
>((props, ref) => {
const { backgroundColor, height, width } = props
return (
<div
className={clsx(styles['placeholder-image'], props.className)}
ref={ref}
style={{
height,
width,
color: backgroundColor,
}}
/>
)
}),
)
Loading

0 comments on commit 9e6cb36

Please sign in to comment.