feat: implement image lightbox

This commit is contained in:
Hossein Mehrabi
2023-06-19 10:29:51 +03:30
parent f8d7700792
commit 344d9f9c59
10 changed files with 289 additions and 3 deletions
@@ -82,6 +82,7 @@ export default function logosPreset(
})
plugins.push('docusaurus-plugin-sass')
themes.push('@docusaurus/theme-mermaid')
if (options.theme?.name !== ThemeNames.DocusaurusDefault)
plugins.push(
@@ -24,6 +24,8 @@ import StatusSvg from '../../static/icons/status.svg'
import TelegramSvg from '../../static/icons/telegram.svg'
import TwitterSvg from '../../static/icons/twitter.svg'
import EditSvg from '../../static/icons/edit.svg'
import FullscreenSvg from '../../static/icons/fullscreen.svg'
import FullscreenExitSvg from '../../static/icons/fullscreen-exit.svg'
type TIconProps = {
size?: 's' | 'm' | 'l'
@@ -178,3 +180,15 @@ export const IconEdit = (props: TIconProps): JSX.Element => (
<EditSvg />
</Icon>
)
export const IconFullscreen = (props: TIconProps): JSX.Element => (
<Icon {...props}>
<FullscreenSvg />
</Icon>
)
export const IconFullscreenExit = (props: TIconProps): JSX.Element => (
<Icon {...props}>
<FullscreenExitSvg />
</Icon>
)
@@ -0,0 +1,161 @@
import { IconButton } from '@acid-info/lsd-react'
import NavbarLogo from '@theme/Navbar/Logo'
import clsx from 'clsx'
import React, {
CSSProperties,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useWindowScroll } from 'react-use'
import { IconFullscreen, IconFullscreenExit } from '../../components/Icon/Icon'
import { Portal } from '../../components/Portal/Portal'
import { useHydrated } from '../../lib/useHydrated'
import styles from './styles.module.scss'
export const LightBoxProvider: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
const hydrated = useHydrated()
const scroll = useWindowScroll()
const [active, setActive] = useState<HTMLElement | null>(null)
const [activeStyle, setActiveStyle] = useState<CSSProperties>({
opacity: '0.5',
})
const style: React.CSSProperties = useMemo(() => {
return {
opacity: 1,
transform: 'scale(1) translate(0px, 0px)',
transition: '0.3s',
}
}, [active])
const display = (element: HTMLElement) => {
setActive(element)
const vw = document.body.clientWidth
const vh = document.body.clientHeight
const maxWidth = window.innerWidth > 768 ? vw * 0.9375 : vw - 32
const maxHeight = vh - 128
const rect = element.getBoundingClientRect()
const scale = Math.min(maxHeight / rect.height, maxWidth / rect.width)
const center = [rect.left + rect.width / 2, rect.top + rect.height / 2]
const windowCenter = [vw / 2, vh / 2]
const translate = windowCenter.map((w, i) => (w - center[i]!) / scale)
setActiveStyle({
zIndex: 202,
transform: `scale(${scale}) translate(${translate[0]}px, ${translate[1]}px)`,
position: 'relative',
})
}
const close = () => {
setActive(null)
}
const toggle = (element: HTMLElement) => {
const current = active
close()
current !== element && display(element)
}
useEffect(() => {
if (active && window.innerWidth > 768) close()
}, [scroll])
return (
<LightBoxContext.Provider
value={{ active, style, activeStyle, display, close, toggle }}
>
{children}
{hydrated && (
<Portal containerId="lsd-presentation">
<div className={clsx(styles.nav, active && styles.visible)}>
<nav>
<NavbarLogo />
<IconButton size="medium" onClick={close}>
<IconFullscreenExit />
</IconButton>
</nav>
</div>
</Portal>
)}
</LightBoxContext.Provider>
)
}
export type LightBoxContextType = {
active: HTMLElement | null
display: (element: HTMLElement) => void
close: () => void
toggle: (element: HTMLElement) => void
style: React.CSSProperties
activeStyle: React.CSSProperties
}
export const LightBoxContext = React.createContext<LightBoxContextType>({
style: {},
activeStyle: {},
active: null,
close: null as any,
toggle: null as any,
display: null as any,
})
export const useLightBox = () => {
const ctx = useContext(LightBoxContext)
const getStyle = (element: HTMLElement) => ({
...ctx.style,
...(element === ctx.active ? ctx.activeStyle : {}),
})
return {
getStyle,
style: ctx.style,
activeStyle: ctx.activeStyle,
active: ctx.active,
isActive: !!ctx.active,
close: ctx.close,
toggle: ctx.toggle,
display: ctx.display,
isActiveElement: (el: HTMLElement) => ctx.active === el,
}
}
export const LightBoxWrapper: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
const ref = useRef<HTMLDivElement>(null)
const { getStyle, display, isActiveElement } = useLightBox()
return (
<div
className={clsx(
styles.wrapper,
isActiveElement(ref.current!) && styles.active,
)}
ref={ref}
style={ref.current ? getStyle(ref.current) : {}}
>
{children}
<IconButton
className={styles.fullscreenButton}
size="medium"
onClick={() => ref.current && display(ref.current)}
>
<IconFullscreen />
</IconButton>
</div>
)
}
@@ -0,0 +1 @@
export * from './LightBox'
@@ -0,0 +1,69 @@
@use '../../css/utils';
.nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgb(var(--lsd-surface-primary));
z-index: 201;
opacity: 0;
visibility: hidden;
}
.visible {
opacity: 1;
visibility: visible;
}
.nav > nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: var(--ifm-navbar-height);
max-width: var(--container-max-width);
margin: 0 auto;
padding: var(--ifm-navbar-padding-vertical)
var(--ifm-navbar-padding-horizontal);
}
.wrapper {
position: relative;
.fullscreenButton {
display: none;
}
&:not(.active) {
.fullscreenButton {
display: block;
transition: 0.3s;
position: absolute;
bottom: 8px;
right: 8px;
background: rgb(var(--lsd-surface-primary));
}
@include utils.responsive('lg', 'up') {
.fullscreenButton {
display: block;
opacity: 0;
visibility: hidden;
transition: 0.3s;
position: absolute;
bottom: 8px;
right: 8px;
background: rgb(var(--lsd-surface-primary));
}
&:hover {
.fullscreenButton {
visibility: visible;
opacity: 1;
}
}
}
}
}
@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.66675 11.0832V9.33317H2.91675V8.1665H5.83341V11.0832H4.66675ZM8.16675 11.0832V8.1665H11.0834V9.33317H9.33342V11.0832H8.16675ZM2.91675 5.83317V4.6665H4.66675V2.9165H5.83341V5.83317H2.91675ZM8.16675 5.83317V2.9165H9.33342V4.6665H11.0834V5.83317H8.16675Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.75 2.91667V5.25H2.91667V2.91667H5.25V1.75H2.91667C2.275 1.75 1.75 2.275 1.75 2.91667ZM2.91667 8.75H1.75V11.0833C1.75 11.725 2.275 12.25 2.91667 12.25H5.25V11.0833H2.91667V8.75ZM11.0833 11.0833H8.75V12.25H11.0833C11.725 12.25 12.25 11.725 12.25 11.0833V8.75H11.0833V11.0833ZM11.0833 1.75H8.75V2.91667H11.0833V5.25H12.25V2.91667C12.25 2.275 11.725 1.75 11.0833 1.75Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,15 @@
import Img from '@docusaurus/theme-classic/lib/theme/MDXComponents/Img'
import type { WrapperProps } from '@docusaurus/types'
import type ImgType from '@theme/MDXComponents/Img'
import React from 'react'
import { LightBoxWrapper } from '../../../containers/LightBox/LightBox'
type Props = WrapperProps<typeof ImgType>
export default function ImgWrapper(props: Props): JSX.Element {
return (
<LightBoxWrapper>
<Img {...props} />
</LightBoxWrapper>
)
}
@@ -0,0 +1,16 @@
// @ts-ignore
import Mermaid from '@docusaurus/theme-mermaid/lib/theme/Mermaid'
import type { WrapperProps } from '@docusaurus/types'
import type MermaidType from '@theme/Mermaid'
import React from 'react'
import { LightBoxWrapper } from '../../containers/LightBox/LightBox'
type Props = WrapperProps<typeof MermaidType>
export default function MermaidWrapper(props: Props): JSX.Element {
return (
<LightBoxWrapper>
<Mermaid {...props} />
</LightBoxWrapper>
)
}
@@ -1,5 +1,6 @@
import { ColorModeProvider } from '@docusaurus/theme-common/internal'
import React from 'react'
import { LightBoxProvider } from '../containers/LightBox/LightBox'
import { ThemeProvider } from '../containers/ThemeProvider'
import { useDocThemeOptions } from '../lib/useThemeOptions'
import styles from './style.module.css'
@@ -11,9 +12,11 @@ export default function Root({ children }) {
return (
<ColorModeProvider>
<ThemeProvider>
<div className={styles.root} data-hidden-doc-sidebar={hideDocSidebar}>
{children}
</div>
<LightBoxProvider>
<div className={styles.root} data-hidden-doc-sidebar={hideDocSidebar}>
{children}
</div>
</LightBoxProvider>
</ThemeProvider>
</ColorModeProvider>
)