mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-15 17:19:11 +08:00
282 lines
7.6 KiB
TypeScript
282 lines
7.6 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { useMutateObserver } from '@rc-component/mutate-observer';
|
|
import classNames from 'classnames';
|
|
|
|
import { useToken } from '../theme/internal';
|
|
import WatermarkContext from './context';
|
|
import type { WatermarkContextProps } from './context';
|
|
import useClips, { FontGap } from './useClips';
|
|
import useRafDebounce from './useRafDebounce';
|
|
import useWatermark from './useWatermark';
|
|
import { getPixelRatio, reRendering } from './utils';
|
|
|
|
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] = 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 =============================
|
|
/**
|
|
* Get the width and height of the watermark. The default values are as follows
|
|
* Image: [120, 64]; Content: It's calculated by content;
|
|
*/
|
|
const getMarkSize = (ctx: CanvasRenderingContext2D) => {
|
|
let defaultWidth = 120;
|
|
let defaultHeight = 64;
|
|
if (!image && ctx.measureText) {
|
|
ctx.font = `${Number(fontSize)}px ${fontFamily}`;
|
|
const contents = Array.isArray(content) ? content : [content];
|
|
const sizes = contents.map((item) => {
|
|
const metrics = ctx.measureText(item!);
|
|
|
|
return [metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent];
|
|
});
|
|
defaultWidth = Math.ceil(Math.max(...sizes.map((size) => size[0])));
|
|
defaultHeight =
|
|
Math.ceil(Math.max(...sizes.map((size) => size[1]))) * contents.length +
|
|
(contents.length - 1) * FontGap;
|
|
}
|
|
return [width ?? defaultWidth, height ?? defaultHeight] as const;
|
|
};
|
|
|
|
const getClips = useClips();
|
|
|
|
const [watermarkInfo, setWatermarkInfo] = React.useState<[base64: string, contentWidth: number]>(
|
|
null!,
|
|
);
|
|
|
|
// Generate new Watermark content
|
|
const renderWatermark = () => {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (ctx) {
|
|
const ratio = getPixelRatio();
|
|
const [markWidth, markHeight] = getMarkSize(ctx);
|
|
|
|
const drawCanvas = (
|
|
drawContent?: NonNullable<WatermarkProps['content']> | HTMLImageElement,
|
|
) => {
|
|
const [nextClips, clipWidth] = getClips(
|
|
drawContent || '',
|
|
rotate,
|
|
ratio,
|
|
markWidth,
|
|
markHeight,
|
|
{
|
|
color,
|
|
fontSize,
|
|
fontStyle,
|
|
fontWeight,
|
|
fontFamily,
|
|
},
|
|
gapX,
|
|
gapY,
|
|
);
|
|
|
|
setWatermarkInfo([nextClips, clipWidth]);
|
|
};
|
|
|
|
if (image) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
drawCanvas(img);
|
|
};
|
|
img.onerror = () => {
|
|
drawCanvas(content);
|
|
};
|
|
img.crossOrigin = 'anonymous';
|
|
img.referrerPolicy = 'no-referrer';
|
|
img.src = image;
|
|
} else {
|
|
drawCanvas(content);
|
|
}
|
|
}
|
|
};
|
|
|
|
const syncWatermark = useRafDebounce(renderWatermark);
|
|
|
|
// ============================= Effect =============================
|
|
// Append watermark to the container
|
|
const [appendWatermark, removeWatermark, isWatermarkEle] = useWatermark(markStyle);
|
|
|
|
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;
|