2
头图

类似antd5 的水印,可防止删除、修改水印元素

type TWatermark = {
  /** 渲染水印的同时并创建水印被修改的观察器 */
  init: () => void
  /** 卸载观察器 */
  uninstallObserver: () => void
}

type WatermarkConfig = {
  /** 水印盒子元素 */
  wrapperElement: HTMLElement,
  /** 字体大小 */
  fontSize?: number,
  /** 水印之间的间隙 */
  gap?: number,
  /** 水印文字 */
  text?: string
}

class Watermark implements TWatermark {
  protected observer: MutationObserver;
  protected watermarkElement: HTMLElement | null;
  protected config: Required<WatermarkConfig>
  constructor(config: WatermarkConfig) {
    this.config = {
      wrapperElement: config.wrapperElement,
      fontSize: config.fontSize || 20,
      gap: config.gap || 10,
      text: config.text || 'watermark'
    };
    this.watermarkElement = null;
    this.observer = new MutationObserver(this.handleMutation.bind(this));
  }

  // 渲染水印的同时并创建水印被修改的观察器
  public init() {
    if (!this.config.wrapperElement) {
      return
    }
    // 将传入的水印盒子设置定位,后边会将水印添加到这个元素中
    this.config.wrapperElement.style.position = 'relative';
    // 渲染水印
    this.resetWatermark()
    // 观察传入的元素也就是水印的父元素
    this.observer.observe(this.config.wrapperElement, {
      childList: true,// 观察子节点
      subtree: true,// 观察所有的后代节点
      attributes: true,// 观察所有属性变化
    });
  }

  // 卸载观察器
  public uninstallObserver() {
    this.watermarkElement = null
    this.observer.disconnect();
  }

  // 修改后执行的方法
  private handleMutation(entries: MutationRecord[]) {
    for (const entry of entries) {
      // 删除
      for (const node of entry.removedNodes) {
        // 如果删除的元素是水印元素
        if (node === this.watermarkElement) {
          this.resetWatermark();
        }
      }
      // 修改,如果修改的元素是水印元素
      if (entry.target === this.watermarkElement) {
        this.resetWatermark();
      }
    }
  }

  // canvas 生成 水印背景
  private watermarkBg() {
    const canvas = document.createElement('canvas');
    const devicePixelRatio = window.devicePixelRatio || 1;
    const fontSize = this.config.fontSize * devicePixelRatio;
    const font = fontSize + 'px "Microsoft YaHei", sans-serif';
    const ctx = canvas.getContext('2d');
    if (!ctx) {
      return null
    }
    ctx.font = font;
    const { width } = ctx.measureText(this.config.text);
    const canvasSize = Math.max(100, width) + this.config.gap * devicePixelRatio
    canvas.width = canvasSize;
    canvas.height = canvasSize;
    ctx.translate(canvasSize / 2, canvasSize / 2);
    ctx.rotate((Math.PI / 180) * 45);
    ctx.fillStyle = 'rgba(0,0,0,0.3)';
    ctx.font = font;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(this.config.text, 0, 0);
    return {
      base64: canvas.toDataURL('image/png'),
      size: canvasSize / devicePixelRatio
    }
  }

  private resetWatermark() {
    if (!this.config.wrapperElement) {
      console.log('未获取到父元素');
      return
    }
    // 由于监听元素变化后会重新创建,此处做判断如果有水印元素则要删除重新创建,防止水印元素重复创建
    if (this.watermarkElement) {
      this.watermarkElement.remove();
    }
    const bg = this.watermarkBg()
    if (!bg) {
      return
    }
    const { base64, size } = bg
    this.watermarkElement = document.createElement('div');
    this.watermarkElement.style.position = 'absolute';
    this.watermarkElement.style.backgroundImage = `url(${base64})`;
    this.watermarkElement.style.backgroundSize = `${size}px ${size}px`;
    this.watermarkElement.style.backgroundRepeat = 'repeat';
    this.watermarkElement.style.zIndex = '9999'
    this.watermarkElement.style.pointerEvents = 'none';
    this.watermarkElement.style.inset = '0'
    this.config.wrapperElement.appendChild(this.watermarkElement);
  }
}

export default Watermark


白水
1.8k 声望389 粉丝