ant-design/components/watermark/index.tsx
二货爱吃白萝卜 8a3870fc31
feat: Watermark support nest Modal & Drawer (#44104)
* docs: add demo

* refactor: init

* refactor: of it

* refactor: simple content

* chore: unique func

* chore: refactor

* chore: support modal watermark

* feat: support nest watermark

* test: add test case

* chore: fix lint

* chore: bump rc-image

* test: add test case

* refactor: use same func
2023-08-08 16:48:26 +08:00

219 lines
5.7 KiB
TypeScript

import { useMutateObserver } from '@rc-component/mutate-observer';
import classNames from 'classnames';
import React, { useEffect } from 'react';
import { reRendering } from './utils';
import theme from '../theme';
import useWatermark from './useWatermark';
import useRafDebounce from './useRafDebounce';
import useContent from './useContent';
import WatermarkContext from './context';
import type { WatermarkContextProps } from './context';
export interface WatermarkProps {
zIndex?: number;
rotate?: number;
width?: number;
height?: number;
image?: string;
content?: string | string[];
font?: {
color?: string;
fontSize?: number | string;
fontWeight?: 'normal' | 'light' | 'weight' | number;
fontStyle?: 'none' | 'normal' | 'italic' | 'oblique';
fontFamily?: string;
};
style?: React.CSSProperties;
className?: string;
rootClassName?: string;
gap?: [number, number];
offset?: [number, number];
children?: React.ReactNode;
}
/**
* Only return `next` when size changed.
* This is only used for elements compare, not a shallow equal!
*/
function getSizeDiff<T>(prev: Set<T>, next: Set<T>) {
return prev.size === next.size ? prev : next;
}
const Watermark: React.FC<WatermarkProps> = (props) => {
const {
/**
* The antd content layer zIndex is basically below 10
* https://github.com/ant-design/ant-design/blob/6192403b2ce517c017f9e58a32d58774921c10cd/components/style/themes/default.less#L335
*/
zIndex = 9,
rotate = -22,
width,
height,
image,
content,
font = {},
style,
className,
rootClassName,
gap = [100, 100],
offset,
children,
} = props;
const { token } = theme.useToken();
const {
color = token.colorFill,
fontSize = token.fontSizeLG,
fontWeight = 'normal',
fontStyle = 'normal',
fontFamily = 'sans-serif',
} = font;
const [gapX, gapY] = gap;
const gapXCenter = gapX / 2;
const gapYCenter = gapY / 2;
const offsetLeft = offset?.[0] ?? gapXCenter;
const offsetTop = offset?.[1] ?? gapYCenter;
const markStyle = React.useMemo(() => {
const mergedStyle: React.CSSProperties = {
zIndex,
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
backgroundRepeat: 'repeat',
};
/** Calculate the style of the offset */
let positionLeft = offsetLeft - gapXCenter;
let positionTop = offsetTop - gapYCenter;
if (positionLeft > 0) {
mergedStyle.left = `${positionLeft}px`;
mergedStyle.width = `calc(100% - ${positionLeft}px)`;
positionLeft = 0;
}
if (positionTop > 0) {
mergedStyle.top = `${positionTop}px`;
mergedStyle.height = `calc(100% - ${positionTop}px)`;
positionTop = 0;
}
mergedStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`;
return mergedStyle;
}, [zIndex, offsetLeft, gapXCenter, offsetTop, gapYCenter]);
const [container, setContainer] = React.useState<HTMLDivElement | null>();
// Used for nest case like Modal, Drawer
const [subElements, setSubElements] = React.useState(new Set<HTMLElement>());
// Nest elements should also support watermark
const targetElements = React.useMemo(() => {
const list = container ? [container] : [];
return [...list, ...Array.from(subElements)];
}, [container, subElements]);
// ============================ Content =============================
const [watermarkInfo, setWatermarkInfo] = React.useState<[base64: string, contentWidth: number]>(
null!,
);
// Generate new Watermark content
const renderWatermark = useContent(
{
...props,
rotate,
gap,
},
(base64, contentWidth) => {
setWatermarkInfo([base64, contentWidth]);
},
);
const syncWatermark = useRafDebounce(renderWatermark);
// ============================= Effect =============================
// Append watermark to the container
const [appendWatermark, removeWatermark, isWatermarkEle] = useWatermark(markStyle, gapX);
useEffect(() => {
if (watermarkInfo) {
targetElements.forEach((holder) => {
appendWatermark(watermarkInfo[0], watermarkInfo[1], holder);
});
}
}, [watermarkInfo, targetElements]);
// ============================ Observe =============================
const onMutate = (mutations: MutationRecord[]) => {
mutations.forEach((mutation) => {
if (reRendering(mutation, isWatermarkEle)) {
syncWatermark();
}
});
};
useMutateObserver(targetElements, onMutate);
useEffect(syncWatermark, [
rotate,
zIndex,
width,
height,
image,
content,
color,
fontSize,
fontWeight,
fontStyle,
fontFamily,
gapX,
gapY,
offsetLeft,
offsetTop,
]);
// ============================ Context =============================
const watermarkContext = React.useMemo<WatermarkContextProps>(
() => ({
add: (ele) => {
setSubElements((prev) => {
const clone = new Set(prev);
clone.add(ele);
return getSizeDiff(prev, clone);
});
},
remove: (ele) => {
removeWatermark(ele);
setSubElements((prev) => {
const clone = new Set(prev);
clone.delete(ele);
return getSizeDiff(prev, clone);
});
},
}),
[],
);
// ============================= Render =============================
return (
<div
ref={setContainer}
className={classNames(className, rootClassName)}
style={{ position: 'relative', ...style }}
>
<WatermarkContext.Provider value={watermarkContext}>{children}</WatermarkContext.Provider>
</div>
);
};
if (process.env.NODE_ENV !== 'production') {
Watermark.displayName = 'Watermark';
}
export default Watermark;