index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. "use client";
  2. import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
  3. import React, { useEffect } from 'react';
  4. import { useMutateObserver } from '@rc-component/mutate-observer';
  5. import classNames from 'classnames';
  6. import useEvent from "rc-util/es/hooks/useEvent";
  7. import toList from '../_util/toList';
  8. import { useToken } from '../theme/internal';
  9. import WatermarkContext from './context';
  10. import useClips, { FontGap } from './useClips';
  11. import useRafDebounce from './useRafDebounce';
  12. import useSingletonCache from './useSingletonCache';
  13. import useWatermark from './useWatermark';
  14. import { getPixelRatio, reRendering } from './utils';
  15. /**
  16. * Only return `next` when size changed.
  17. * This is only used for elements compare, not a shallow equal!
  18. */
  19. function getSizeDiff(prev, next) {
  20. return prev.size === next.size ? prev : next;
  21. }
  22. const DEFAULT_GAP_X = 100;
  23. const DEFAULT_GAP_Y = 100;
  24. const fixedStyle = {
  25. position: 'relative',
  26. overflow: 'hidden'
  27. };
  28. const Watermark = props => {
  29. var _a, _b;
  30. const {
  31. /**
  32. * The antd content layer zIndex is basically below 10
  33. * https://github.com/ant-design/ant-design/blob/6192403b2ce517c017f9e58a32d58774921c10cd/components/style/themes/default.less#L335
  34. */
  35. zIndex = 9,
  36. rotate = -22,
  37. width,
  38. height,
  39. image,
  40. content,
  41. font = {},
  42. style,
  43. className,
  44. rootClassName,
  45. gap = [DEFAULT_GAP_X, DEFAULT_GAP_Y],
  46. offset,
  47. children,
  48. inherit = true
  49. } = props;
  50. const mergedStyle = Object.assign(Object.assign({}, fixedStyle), style);
  51. const [, token] = useToken();
  52. const {
  53. color = token.colorFill,
  54. fontSize = token.fontSizeLG,
  55. fontWeight = 'normal',
  56. fontStyle = 'normal',
  57. fontFamily = 'sans-serif',
  58. textAlign = 'center'
  59. } = font;
  60. const [gapX = DEFAULT_GAP_X, gapY = DEFAULT_GAP_Y] = gap;
  61. const gapXCenter = gapX / 2;
  62. const gapYCenter = gapY / 2;
  63. const offsetLeft = (_a = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _a !== void 0 ? _a : gapXCenter;
  64. const offsetTop = (_b = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _b !== void 0 ? _b : gapYCenter;
  65. const markStyle = React.useMemo(() => {
  66. const mergedMarkStyle = {
  67. zIndex,
  68. position: 'absolute',
  69. left: 0,
  70. top: 0,
  71. width: '100%',
  72. height: '100%',
  73. pointerEvents: 'none',
  74. backgroundRepeat: 'repeat'
  75. };
  76. /** Calculate the style of the offset */
  77. let positionLeft = offsetLeft - gapXCenter;
  78. let positionTop = offsetTop - gapYCenter;
  79. if (positionLeft > 0) {
  80. mergedMarkStyle.left = `${positionLeft}px`;
  81. mergedMarkStyle.width = `calc(100% - ${positionLeft}px)`;
  82. positionLeft = 0;
  83. }
  84. if (positionTop > 0) {
  85. mergedMarkStyle.top = `${positionTop}px`;
  86. mergedMarkStyle.height = `calc(100% - ${positionTop}px)`;
  87. positionTop = 0;
  88. }
  89. mergedMarkStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`;
  90. return mergedMarkStyle;
  91. }, [zIndex, offsetLeft, gapXCenter, offsetTop, gapYCenter]);
  92. const [container, setContainer] = React.useState();
  93. // Used for nest case like Modal, Drawer
  94. const [subElements, setSubElements] = React.useState(() => new Set());
  95. // Nest elements should also support watermark
  96. const targetElements = React.useMemo(() => {
  97. const list = container ? [container] : [];
  98. return [].concat(list, _toConsumableArray(Array.from(subElements)));
  99. }, [container, subElements]);
  100. // ============================ Content =============================
  101. /**
  102. * Get the width and height of the watermark. The default values are as follows
  103. * Image: [120, 64]; Content: It's calculated by content;
  104. */
  105. const getMarkSize = ctx => {
  106. let defaultWidth = 120;
  107. let defaultHeight = 64;
  108. if (!image && ctx.measureText) {
  109. ctx.font = `${Number(fontSize)}px ${fontFamily}`;
  110. const contents = toList(content);
  111. const sizes = contents.map(item => {
  112. const metrics = ctx.measureText(item);
  113. return [metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent];
  114. });
  115. defaultWidth = Math.ceil(Math.max.apply(Math, _toConsumableArray(sizes.map(size => size[0]))));
  116. defaultHeight = Math.ceil(Math.max.apply(Math, _toConsumableArray(sizes.map(size => size[1])))) * contents.length + (contents.length - 1) * FontGap;
  117. }
  118. return [width !== null && width !== void 0 ? width : defaultWidth, height !== null && height !== void 0 ? height : defaultHeight];
  119. };
  120. const getClips = useClips();
  121. const getClipsCache = useSingletonCache();
  122. const [watermarkInfo, setWatermarkInfo] = React.useState(null);
  123. // Generate new Watermark content
  124. const renderWatermark = () => {
  125. const canvas = document.createElement('canvas');
  126. const ctx = canvas.getContext('2d');
  127. if (ctx) {
  128. const ratio = getPixelRatio();
  129. const [markWidth, markHeight] = getMarkSize(ctx);
  130. const drawCanvas = drawContent => {
  131. const params = [drawContent || '', rotate, ratio, markWidth, markHeight, {
  132. color,
  133. fontSize,
  134. fontStyle,
  135. fontWeight,
  136. fontFamily,
  137. textAlign
  138. }, gapX, gapY];
  139. const [nextClips, clipWidth] = getClipsCache(params, () => getClips.apply(void 0, params));
  140. setWatermarkInfo([nextClips, clipWidth]);
  141. };
  142. if (image) {
  143. const img = new Image();
  144. img.onload = () => {
  145. drawCanvas(img);
  146. };
  147. img.onerror = () => {
  148. drawCanvas(content);
  149. };
  150. img.crossOrigin = 'anonymous';
  151. img.referrerPolicy = 'no-referrer';
  152. img.src = image;
  153. } else {
  154. drawCanvas(content);
  155. }
  156. }
  157. };
  158. const syncWatermark = useRafDebounce(renderWatermark);
  159. // ============================= Effect =============================
  160. // Append watermark to the container
  161. const [appendWatermark, removeWatermark, isWatermarkEle] = useWatermark(markStyle);
  162. useEffect(() => {
  163. if (watermarkInfo) {
  164. targetElements.forEach(holder => {
  165. appendWatermark(watermarkInfo[0], watermarkInfo[1], holder);
  166. });
  167. }
  168. }, [watermarkInfo, targetElements]);
  169. // ============================ Observe =============================
  170. const onMutate = useEvent(mutations => {
  171. mutations.forEach(mutation => {
  172. if (reRendering(mutation, isWatermarkEle)) {
  173. syncWatermark();
  174. } else if (mutation.target === container && mutation.attributeName === 'style') {
  175. // We've only force container not modify.
  176. // Not consider nest case.
  177. const keyStyles = Object.keys(fixedStyle);
  178. for (let i = 0; i < keyStyles.length; i += 1) {
  179. const key = keyStyles[i];
  180. const oriValue = mergedStyle[key];
  181. const currentValue = container.style[key];
  182. if (oriValue && oriValue !== currentValue) {
  183. container.style[key] = oriValue;
  184. }
  185. }
  186. }
  187. });
  188. });
  189. useMutateObserver(targetElements, onMutate);
  190. useEffect(syncWatermark, [rotate, zIndex, width, height, image, content, color, fontSize, fontWeight, fontStyle, fontFamily, textAlign, gapX, gapY, offsetLeft, offsetTop]);
  191. // ============================ Context =============================
  192. const watermarkContext = React.useMemo(() => ({
  193. add: ele => {
  194. setSubElements(prev => {
  195. const clone = new Set(prev);
  196. clone.add(ele);
  197. return getSizeDiff(prev, clone);
  198. });
  199. },
  200. remove: ele => {
  201. removeWatermark(ele);
  202. setSubElements(prev => {
  203. const clone = new Set(prev);
  204. clone.delete(ele);
  205. return getSizeDiff(prev, clone);
  206. });
  207. }
  208. }), []);
  209. // ============================= Render =============================
  210. const childNode = inherit ? (/*#__PURE__*/React.createElement(WatermarkContext.Provider, {
  211. value: watermarkContext
  212. }, children)) : children;
  213. return /*#__PURE__*/React.createElement("div", {
  214. ref: setContainer,
  215. className: classNames(className, rootClassName),
  216. style: mergedStyle
  217. }, childNode);
  218. };
  219. if (process.env.NODE_ENV !== 'production') {
  220. Watermark.displayName = 'Watermark';
  221. }
  222. export default Watermark;