From 49750cd91e5bb3776ed1f0c4a6d0fabc435150b0 Mon Sep 17 00:00:00 2001 From: aibayanyu Date: Tue, 21 Feb 2023 07:10:05 +0800 Subject: [PATCH 1/4] feat: add watermark --- components/watermark/demo/basic.vue | 7 ++++ components/watermark/index.en-US.md | 51 ++++++++++++++++++++++++++++ components/watermark/index.tsx | 38 +++++++++++++++++++++ components/watermark/index.zh-CN.md | 52 +++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 components/watermark/demo/basic.vue create mode 100644 components/watermark/index.en-US.md create mode 100644 components/watermark/index.tsx create mode 100644 components/watermark/index.zh-CN.md diff --git a/components/watermark/demo/basic.vue b/components/watermark/demo/basic.vue new file mode 100644 index 0000000000..bd7cc11eb0 --- /dev/null +++ b/components/watermark/demo/basic.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md new file mode 100644 index 0000000000..bde88e861b --- /dev/null +++ b/components/watermark/index.en-US.md @@ -0,0 +1,51 @@ +--- +category: Components +group: Other +title: Watermark +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*duAQQbjHlHQAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 1 +--- + +Add specific text or patterns to the page. + +## When To Use + +- Use when the page needs to be watermarked to identify the copyright. +- Suitable for preventing information theft. + +## Examples + + +Basic +Multi-line watermark +Image watermark +Custom configuration + +## API + +### Watermark + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| width | The width of the watermark, the default value of `content` is its own width | number | 120 | | +| height | The height of the watermark, the default value of `content` is its own height | number | 64 | | +| rotate | When the watermark is drawn, the rotation Angle, unit `°` | number | -22 | | +| zIndex | The z-index of the appended watermark element | number | 9 | | +| image | Image source, it is recommended to export 2x or 3x image, high priority | string | - | | +| content | Watermark text content | string \| string[] | - | | +| font | Text style | [Font](#font) | [Font](#font) | | +| gap | The spacing between watermarks | \[number, number\] | \[100, 100\] | | +| offset | The offset of the watermark from the upper left corner of the container. The default is `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | | + +### Font + + +| Property | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | +| color | font color | string | rgba(0,0,0,.15) | | +| fontSize | font size | number | 16 | | +| fontWeight | font weight | `normal` \| `light` \| `weight` \| number | normal | | +| fontFamily | font family | string | sans-serif | | +| fontStyle | font style | `none` \| `normal` \| `italic` \| `oblique` | normal | | diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx new file mode 100644 index 0000000000..09835cb2b2 --- /dev/null +++ b/components/watermark/index.tsx @@ -0,0 +1,38 @@ +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; + +export interface WatermarkFontType { + color?: string; + fontSize?: number | string; + fontWeight?: 'normal' | 'light' | 'weight' | number; + fontStyle?: 'none' | 'normal' | 'italic' | 'oblique'; + fontFamily?: string; +} +export const watermarkProps = () => ({ + zIndex: Number, + rotate: Number, + width: Number, + height: Number, + image: String, + content: [String, Array], + font: { + type: Object as PropType, + default: () => ({}), + }, + rootClassName: String, + gap: { + type: Array as PropType>, + default: undefined, + }, + offset: { + type: Array as PropType>, + default: undefined, + }, +}); +export default defineComponent({ + name: 'AWatermark', + props: watermarkProps(), + setup(props) { + return () =>
{/* */}
; + }, +}); diff --git a/components/watermark/index.zh-CN.md b/components/watermark/index.zh-CN.md new file mode 100644 index 0000000000..bf3ebdbdc7 --- /dev/null +++ b/components/watermark/index.zh-CN.md @@ -0,0 +1,52 @@ +--- +category: Components +subtitle: 水印 +group: 其他 +title: Watermark +cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original +coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*duAQQbjHlHQAAAAAAAAAAAAADrJ8AQ/original +demo: + cols: 1 +--- + +给页面的某个区域加上水印。 + +## 何时使用 + +- 页面需要添加水印标识版权时使用。 +- 适用于防止信息盗用。 + +## 代码演示 + + +基本 +多行水印 +图片水印 +自定义配置 + +## API + +### Watermark + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| width | 水印的宽度,`content` 的默认值为自身的宽度 | number | 120 | | +| height | 水印的高度,`content` 的默认值为自身的高度 | number | 64 | | +| rotate | 水印绘制时,旋转的角度,单位 `°` | number | -22 | | +| zIndex | 追加的水印元素的 z-index | number | 9 | | +| image | 图片源,建议导出 2 倍或 3 倍图,优先级高 | string | - | | +| content | 水印文字内容 | string \| string[] | - | | +| font | 文字样式 | [Font](#font) | [Font](#font) | | +| gap | 水印之间的间距 | \[number, number\] | \[100, 100\] | | +| offset | 水印距离容器左上角的偏移量,默认为 `gap/2` | \[number, number\] | \[gap\[0\]/2, gap\[1\]/2\] | | + +### Font + + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | +| color | 字体颜色 | string | rgba(0,0,0,.15) | | +| fontSize | 字体大小 | number | 16 | | +| fontWeight | 字体粗细 | `normal` \| `light` \| `weight` \| number | normal | | +| fontFamily | 字体类型 | string | sans-serif | | +| fontStyle | 字体样式 | `none` \| `normal` \| `italic` \| `oblique` | normal | | From fc11b9f73c3a910813de693d58f3c68beaa2f6da Mon Sep 17 00:00:00 2001 From: aibayanyu20 Date: Tue, 21 Feb 2023 11:41:13 +0800 Subject: [PATCH 2/4] feat: add watermark demo --- components/_util/mutateObserver.tsx | 27 +++ components/components.ts | 3 + components/watermark/demo/basic.vue | 24 ++- components/watermark/demo/custom.vue | 82 ++++++++ components/watermark/demo/image.vue | 27 +++ components/watermark/demo/index.vue | 28 +++ components/watermark/demo/multi-line.vue | 23 +++ components/watermark/index.en-US.md | 8 - components/watermark/index.tsx | 229 ++++++++++++++++++++++- components/watermark/index.zh-CN.md | 10 - components/watermark/utils.ts | 42 +++++ site/src/demo.js | 7 + 12 files changed, 482 insertions(+), 28 deletions(-) create mode 100644 components/_util/mutateObserver.tsx create mode 100644 components/watermark/demo/custom.vue create mode 100644 components/watermark/demo/image.vue create mode 100644 components/watermark/demo/index.vue create mode 100644 components/watermark/demo/multi-line.vue create mode 100644 components/watermark/utils.ts diff --git a/components/_util/mutateObserver.tsx b/components/_util/mutateObserver.tsx new file mode 100644 index 0000000000..df5c11b42b --- /dev/null +++ b/components/_util/mutateObserver.tsx @@ -0,0 +1,27 @@ +import type { PropType } from 'vue'; +import { defineComponent } from 'vue'; +export const mutateObserverProps = { + onMutate: Function as PropType<(mutations: MutationRecord[], observer: MutationObserver) => void>, + options: Object as PropType, +}; +export const DomWrapper = defineComponent({ + name: 'DomWrapper', + inheritAttrs: false, + setup(_, { slots }) { + return () => { + return slots.default?.(); + }; + }, +}); +export default defineComponent({ + name: 'MutateObserver', + inheritAttrs: false, + props: { + ...mutateObserverProps, + }, + setup(_props, { slots }) { + return () => { + return {slots.default?.()}; + }; + }, +}); diff --git a/components/components.ts b/components/components.ts index 020c5add2a..c5d5fbc2f1 100644 --- a/components/components.ts +++ b/components/components.ts @@ -244,3 +244,6 @@ export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from export { default as Upload, UploadDragger } from './upload'; export { default as LocaleProvider } from './locale-provider'; + +export { default as Watermark } from './watermark'; +export type { WatermarkProps } from './watermark'; diff --git a/components/watermark/demo/basic.vue b/components/watermark/demo/basic.vue index bd7cc11eb0..b48df546bb 100644 --- a/components/watermark/demo/basic.vue +++ b/components/watermark/demo/basic.vue @@ -1,7 +1,23 @@ - + +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. + + - - diff --git a/components/watermark/demo/custom.vue b/components/watermark/demo/custom.vue new file mode 100644 index 0000000000..62cb77edf0 --- /dev/null +++ b/components/watermark/demo/custom.vue @@ -0,0 +1,82 @@ + +--- +order: 0 +title: + zh-CN: 自定义配置 + en-US: Custom +--- + +## zh-CN + +通过自定义参数配置预览水印效果。 + +## en-US + +Preview the watermark effect by configuring custom parameters. + + + + + + diff --git a/components/watermark/demo/image.vue b/components/watermark/demo/image.vue new file mode 100644 index 0000000000..2f36dac1e2 --- /dev/null +++ b/components/watermark/demo/image.vue @@ -0,0 +1,27 @@ + +--- +order: 0 +title: + zh-CN: 图片水印 + en-US: Multi-line watermark +--- + +## zh-CN + +通过 `image` 指定图片地址。为保证图片高清且不被拉伸,请设置 width 和 height, 并上传至少两倍的宽高的 logo 图片地址。 + +## en-US + +Specify the image address via 'image'. To ensure that the image is high definition and not stretched, set the width and height, and upload at least twice the width and height of the logo image address. + + + + diff --git a/components/watermark/demo/index.vue b/components/watermark/demo/index.vue new file mode 100644 index 0000000000..38012df8e9 --- /dev/null +++ b/components/watermark/demo/index.vue @@ -0,0 +1,28 @@ + + + diff --git a/components/watermark/demo/multi-line.vue b/components/watermark/demo/multi-line.vue new file mode 100644 index 0000000000..af8226524d --- /dev/null +++ b/components/watermark/demo/multi-line.vue @@ -0,0 +1,23 @@ + +--- +order: 0 +title: + zh-CN: 多行水印 + en-US: Multi-line watermark +--- + +## zh-CN + +通过 `content` 设置 字符串数组 指定多行文字水印内容。 + +## en-US + +Use 'content' to set a string array to specify multi-line text watermark content. + + + + diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md index bde88e861b..d71a3089f4 100644 --- a/components/watermark/index.en-US.md +++ b/components/watermark/index.en-US.md @@ -15,14 +15,6 @@ Add specific text or patterns to the page. - Use when the page needs to be watermarked to identify the copyright. - Suitable for preventing information theft. -## Examples - - -Basic -Multi-line watermark -Image watermark -Custom configuration - ## API ### Watermark diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx index 09835cb2b2..9fb5dc50c2 100644 --- a/components/watermark/index.tsx +++ b/components/watermark/index.tsx @@ -1,5 +1,15 @@ -import type { PropType } from 'vue'; -import { defineComponent } from 'vue'; +import type { PropType, ExtractPropTypes, CSSProperties } from 'vue'; +import { computed, defineComponent, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'; +import { getStyleStr, getPixelRatio, rotateWatermark } from './utils'; +import { withInstall } from '../_util/type'; +import MutateObserver from '../_util/mutateObserver'; + +/** + * Base size of the canvas, 1 for parallel layout and 2 for alternate layout + * Only alternate layout is currently supported + */ +const BaseSize = 2; +const FontGap = 3; export interface WatermarkFontType { color?: string; @@ -17,7 +27,13 @@ export const watermarkProps = () => ({ content: [String, Array], font: { type: Object as PropType, - default: () => ({}), + default: () => ({ + fontSize: 16, + color: 'rgba(0, 0, 0, 0.15)', + fontWeight: 'normal', + fontStyle: 'normal', + fontFamily: 'sans-serif', + }), }, rootClassName: String, gap: { @@ -29,10 +45,211 @@ export const watermarkProps = () => ({ default: undefined, }, }); -export default defineComponent({ +export type WatermarkProps = Partial>>; +const Watermark = defineComponent({ name: 'AWatermark', + inheritAttrs: false, props: watermarkProps(), - setup(props) { - return () =>
{/* */}
; + setup(props, { slots, attrs }) { + const containerRef = shallowRef(); + const watermarkRef = shallowRef(); + const stopObservation = shallowRef(false); + const gapX = computed(() => props.gap?.[0] ?? 100); + const gapY = computed(() => props.gap?.[1] ?? 100); + const gapXCenter = computed(() => gapX.value / 2); + const gapYCenter = computed(() => gapY.value / 2); + const offsetLeft = computed(() => props.offset?.[0] ?? gapXCenter.value); + const offsetTop = computed(() => props.offset?.[1] ?? gapYCenter.value); + const fontSize = computed(() => props.font?.fontSize ?? 16); + const fontWeight = computed(() => props.font?.fontWeight ?? 'normal'); + const fontStyle = computed(() => props.font?.fontStyle ?? 'normal'); + const fontFamily = computed(() => props.font?.fontFamily ?? 'sans-serif'); + const color = computed(() => props.font?.color ?? 'rgba(0, 0, 0, 0.15)'); + const getMarkStyle = () => { + const markStyle: CSSProperties = { + zIndex: props.zIndex ?? 9, + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + pointerEvents: 'none', + backgroundRepeat: 'repeat', + }; + + /** Calculate the style of the offset */ + let positionLeft = offsetLeft.value - gapXCenter.value; + let positionTop = offsetTop.value - gapYCenter.value; + if (positionLeft > 0) { + markStyle.left = `${positionLeft}px`; + markStyle.width = `calc(100% - ${positionLeft}px)`; + positionLeft = 0; + } + if (positionTop > 0) { + markStyle.top = `${positionTop}px`; + markStyle.height = `calc(100% - ${positionTop}px)`; + positionTop = 0; + } + markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`; + + return markStyle; + }; + const destroyWatermark = () => { + if (watermarkRef.value) { + watermarkRef.value.remove(); + watermarkRef.value = undefined; + } + }; + + const appendWatermark = (base64Url: string, markWidth: number) => { + if (containerRef.value && watermarkRef.value) { + stopObservation.value = true; + watermarkRef.value.setAttribute( + 'style', + getStyleStr({ + ...getMarkStyle(), + backgroundImage: `url('${base64Url}')`, + backgroundSize: `${(gapX.value + markWidth) * BaseSize}px`, + }), + ); + containerRef.value?.append(watermarkRef.value); + // Delayed execution + setTimeout(() => { + stopObservation.value = false; + }); + } + }; + /** + * 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; + const content = props.content; + const image = props.image; + const width = props.width; + const height = props.height; + if (!image && ctx.measureText) { + ctx.font = `${Number(fontSize.value)}px ${fontFamily.value}`; + const contents = Array.isArray(content) ? content : [content]; + const widths = contents.map(item => ctx.measureText(item!).width); + defaultWidth = Math.ceil(Math.max(...widths)); + defaultHeight = Number(fontSize.value) * contents.length + (contents.length - 1) * FontGap; + } + return [width ?? defaultWidth, height ?? defaultHeight] as const; + }; + const fillTexts = ( + ctx: CanvasRenderingContext2D, + drawX: number, + drawY: number, + drawWidth: number, + drawHeight: number, + ) => { + const ratio = getPixelRatio(); + const content = props.content; + const mergedFontSize = Number(fontSize.value) * ratio; + ctx.font = `${fontStyle.value} normal ${fontWeight.value} ${mergedFontSize}px/${drawHeight}px ${fontFamily.value}`; + ctx.fillStyle = color.value; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.translate(drawWidth / 2, 0); + const contents = Array.isArray(content) ? content : [content]; + contents?.forEach((item, index) => { + ctx.fillText(item ?? '', drawX, drawY + index * (mergedFontSize + FontGap * ratio)); + }); + }; + const renderWatermark = () => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const image = props.image; + const rotate = props.rotate ?? -22; + + if (ctx) { + if (!watermarkRef.value) { + watermarkRef.value = document.createElement('div'); + } + + const ratio = getPixelRatio(); + const [markWidth, markHeight] = getMarkSize(ctx); + const canvasWidth = (gapX.value + markWidth) * ratio; + const canvasHeight = (gapY.value + markHeight) * ratio; + canvas.setAttribute('width', `${canvasWidth * BaseSize}px`); + canvas.setAttribute('height', `${canvasHeight * BaseSize}px`); + + const drawX = (gapX.value * ratio) / 2; + const drawY = (gapY.value * ratio) / 2; + const drawWidth = markWidth * ratio; + const drawHeight = markHeight * ratio; + const rotateX = (drawWidth + gapX.value * ratio) / 2; + const rotateY = (drawHeight + gapY.value * ratio) / 2; + /** Alternate drawing parameters */ + const alternateDrawX = drawX + canvasWidth; + const alternateDrawY = drawY + canvasHeight; + const alternateRotateX = rotateX + canvasWidth; + const alternateRotateY = rotateY + canvasHeight; + + ctx.save(); + rotateWatermark(ctx, rotateX, rotateY, rotate); + + if (image) { + const img = new Image(); + img.onload = () => { + ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight); + /** Draw interleaved pictures after rotation */ + ctx.restore(); + rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate); + ctx.drawImage(img, alternateDrawX, alternateDrawY, drawWidth, drawHeight); + appendWatermark(canvas.toDataURL(), markWidth); + }; + img.crossOrigin = 'anonymous'; + img.referrerPolicy = 'no-referrer'; + img.src = image; + } else { + fillTexts(ctx, drawX, drawY, drawWidth, drawHeight); + /** Fill the interleaved text after rotation */ + ctx.restore(); + rotateWatermark(ctx, alternateRotateX, alternateRotateY, rotate); + fillTexts(ctx, alternateDrawX, alternateDrawY, drawWidth, drawHeight); + appendWatermark(canvas.toDataURL(), markWidth); + } + } + }; + onMounted(() => { + renderWatermark(); + }); + watch( + () => props, + () => { + renderWatermark(); + }, + { + deep: true, + flush: 'post', + }, + ); + onBeforeUnmount(() => { + destroyWatermark(); + }); + const onMutate = () => { + if (stopObservation.value) { + return; + } + }; + return () => { + return ( + +
+ {slots.default?.()} +
+
+ ); + }; }, }); + +export default withInstall(Watermark); diff --git a/components/watermark/index.zh-CN.md b/components/watermark/index.zh-CN.md index bf3ebdbdc7..8a0aba8448 100644 --- a/components/watermark/index.zh-CN.md +++ b/components/watermark/index.zh-CN.md @@ -5,8 +5,6 @@ group: 其他 title: Watermark cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*duAQQbjHlHQAAAAAAAAAAAAADrJ8AQ/original -demo: - cols: 1 --- 给页面的某个区域加上水印。 @@ -16,14 +14,6 @@ demo: - 页面需要添加水印标识版权时使用。 - 适用于防止信息盗用。 -## 代码演示 - - -基本 -多行水印 -图片水印 -自定义配置 - ## API ### Watermark diff --git a/components/watermark/utils.ts b/components/watermark/utils.ts new file mode 100644 index 0000000000..60d23f88ea --- /dev/null +++ b/components/watermark/utils.ts @@ -0,0 +1,42 @@ +import type { CSSProperties } from 'vue'; +/** converting camel-cased strings to be lowercase and link it with Separato */ +export function toLowercaseSeparator(key: string) { + return key.replace(/([A-Z])/g, '-$1').toLowerCase(); +} + +export function getStyleStr(style: CSSProperties): string { + return Object.keys(style) + .map((key: keyof CSSProperties) => `${toLowercaseSeparator(key)}: ${style[key]};`) + .join(' '); +} + +/** Returns the ratio of the device's physical pixel resolution to the css pixel resolution */ +export function getPixelRatio() { + return window.devicePixelRatio || 1; +} + +/** Rotate with the watermark as the center point */ +export function rotateWatermark( + ctx: CanvasRenderingContext2D, + rotateX: number, + rotateY: number, + rotate: number, +) { + ctx.translate(rotateX, rotateY); + ctx.rotate((Math.PI / 180) * Number(rotate)); + ctx.translate(-rotateX, -rotateY); +} + +/** Whether to re-render the watermark */ +export const reRendering = (mutation: MutationRecord, watermarkElement?: HTMLElement) => { + let flag = false; + // Whether to delete the watermark node + if (mutation.removedNodes.length) { + flag = Array.from(mutation.removedNodes).some(node => node === watermarkElement); + } + // Whether the watermark dom property value has been modified + if (mutation.type === 'attributes' && mutation.target === watermarkElement) { + flag = true; + } + return flag; +}; diff --git a/site/src/demo.js b/site/src/demo.js index ff3209fa1f..79ec233677 100644 --- a/site/src/demo.js +++ b/site/src/demo.js @@ -224,6 +224,13 @@ export default { type: 'Other', title: 'BackTop', }, + watermark: { + category: 'Components', + subtitle: '水印', + type: 'Other', + title: 'Watermark', + cols: 1, + }, modal: { category: 'Components', subtitle: '对话框', From 6cc6e61f81be93fd19a73bd79a2967fd29b4b5ef Mon Sep 17 00:00:00 2001 From: aibayanyu20 Date: Tue, 21 Feb 2023 14:19:27 +0800 Subject: [PATCH 3/4] feat: add mutationObserver --- .../hooks/_vueuse/useMutationObserver.ts | 62 +++++++++++++++++++ components/_util/mutateObserver.tsx | 27 -------- components/watermark/__tests__/demo.test.js | 3 + components/watermark/__tests__/index.test.js | 29 +++++++++ components/watermark/index.tsx | 29 +++++---- 5 files changed, 111 insertions(+), 39 deletions(-) create mode 100644 components/_util/hooks/_vueuse/useMutationObserver.ts delete mode 100644 components/_util/mutateObserver.tsx create mode 100644 components/watermark/__tests__/demo.test.js create mode 100644 components/watermark/__tests__/index.test.js diff --git a/components/_util/hooks/_vueuse/useMutationObserver.ts b/components/_util/hooks/_vueuse/useMutationObserver.ts new file mode 100644 index 0000000000..3a191d396f --- /dev/null +++ b/components/_util/hooks/_vueuse/useMutationObserver.ts @@ -0,0 +1,62 @@ +import { tryOnScopeDispose } from './tryOnScopeDispose'; +import { watch } from 'vue'; +import type { MaybeElementRef } from './unrefElement'; +import { unrefElement } from './unrefElement'; +import { useSupported } from './useSupported'; +import type { ConfigurableWindow } from './_configurable'; +import { defaultWindow } from './_configurable'; + +export interface UseMutationObserverOptions extends MutationObserverInit, ConfigurableWindow {} + +/** + * Watch for changes being made to the DOM tree. + * + * @see https://vueuse.org/useMutationObserver + * @see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver MutationObserver MDN + * @param target + * @param callback + * @param options + */ +export function useMutationObserver( + target: MaybeElementRef, + callback: MutationCallback, + options: UseMutationObserverOptions = {}, +) { + const { window = defaultWindow, ...mutationOptions } = options; + let observer: MutationObserver | undefined; + const isSupported = useSupported(() => window && 'MutationObserver' in window); + + const cleanup = () => { + if (observer) { + observer.disconnect(); + observer = undefined; + } + }; + + const stopWatch = watch( + () => unrefElement(target), + el => { + cleanup(); + + if (isSupported.value && window && el) { + observer = new MutationObserver(callback); + observer!.observe(el, mutationOptions); + } + }, + { immediate: true }, + ); + + const stop = () => { + cleanup(); + stopWatch(); + }; + + tryOnScopeDispose(stop); + + return { + isSupported, + stop, + }; +} + +export type UseMutationObserverReturn = ReturnType; diff --git a/components/_util/mutateObserver.tsx b/components/_util/mutateObserver.tsx deleted file mode 100644 index df5c11b42b..0000000000 --- a/components/_util/mutateObserver.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { PropType } from 'vue'; -import { defineComponent } from 'vue'; -export const mutateObserverProps = { - onMutate: Function as PropType<(mutations: MutationRecord[], observer: MutationObserver) => void>, - options: Object as PropType, -}; -export const DomWrapper = defineComponent({ - name: 'DomWrapper', - inheritAttrs: false, - setup(_, { slots }) { - return () => { - return slots.default?.(); - }; - }, -}); -export default defineComponent({ - name: 'MutateObserver', - inheritAttrs: false, - props: { - ...mutateObserverProps, - }, - setup(_props, { slots }) { - return () => { - return {slots.default?.()}; - }; - }, -}); diff --git a/components/watermark/__tests__/demo.test.js b/components/watermark/__tests__/demo.test.js new file mode 100644 index 0000000000..60b75d5802 --- /dev/null +++ b/components/watermark/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('watermark'); diff --git a/components/watermark/__tests__/index.test.js b/components/watermark/__tests__/index.test.js new file mode 100644 index 0000000000..badcbd7d39 --- /dev/null +++ b/components/watermark/__tests__/index.test.js @@ -0,0 +1,29 @@ +import Watermark from '..'; +import mountTest from '../../../tests/shared/mountTest'; +import { mount } from '@vue/test-utils'; + +describe('Watermark', () => { + mountTest(Watermark); + const mockSrcSet = jest.spyOn(Image.prototype, 'src', 'set'); + beforeAll(() => { + mockSrcSet.mockImplementation(function fn() { + this.onload?.(); + }); + }); + + afterAll(() => { + mockSrcSet.mockRestore(); + }); + + it('The watermark should render successfully ', function () { + const wrapper = mount({ + setup() { + return () => { + return ; + }; + }, + }); + expect(wrapper.find('.watermark').exists()).toBe(true); + expect(wrapper.html()).toMatchSnapshot(); + }); +}); diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx index 9fb5dc50c2..afd50a8183 100644 --- a/components/watermark/index.tsx +++ b/components/watermark/index.tsx @@ -1,8 +1,8 @@ import type { PropType, ExtractPropTypes, CSSProperties } from 'vue'; import { computed, defineComponent, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'; -import { getStyleStr, getPixelRatio, rotateWatermark } from './utils'; +import { getStyleStr, getPixelRatio, rotateWatermark, reRendering } from './utils'; import { withInstall } from '../_util/type'; -import MutateObserver from '../_util/mutateObserver'; +import { useMutationObserver } from '../_util/hooks/_vueuse/useMutationObserver'; /** * Base size of the canvas, 1 for parallel layout and 2 for alternate layout @@ -231,22 +231,27 @@ const Watermark = defineComponent({ onBeforeUnmount(() => { destroyWatermark(); }); - const onMutate = () => { + const onMutate = (mutations: MutationRecord[]) => { if (stopObservation.value) { return; } + mutations.forEach(mutation => { + if (reRendering(mutation, watermarkRef.value)) { + destroyWatermark(); + renderWatermark(); + } + }); }; + useMutationObserver(containerRef, onMutate); return () => { return ( - -
- {slots.default?.()} -
-
+
+ {slots.default?.()} +
); }; }, From 2fded3f4847f06679cc4219a7fa45a483efa1307 Mon Sep 17 00:00:00 2001 From: aibayanyu20 Date: Tue, 21 Feb 2023 15:17:30 +0800 Subject: [PATCH 4/4] feat: add watermark demo --- components/watermark/demo/custom.vue | 36 ++++++++++++++++++++++++++++ components/watermark/demo/image.vue | 2 +- components/watermark/index.en-US.md | 4 +--- components/watermark/index.tsx | 8 ++++--- components/watermark/index.zh-CN.md | 2 +- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/components/watermark/demo/custom.vue b/components/watermark/demo/custom.vue index 62cb77edf0..7cdeb0f7e0 100644 --- a/components/watermark/demo/custom.vue +++ b/components/watermark/demo/custom.vue @@ -63,6 +63,35 @@ Preview the watermark effect by configuring custom parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -73,6 +102,13 @@ export default defineComponent({ setup() { const model = reactive({ content: 'Ant Design Vue', + font: { + fontSize: 16, + }, + zIndex: 11, + rotate: -22, + gap: [100, 100] as [number, number], + offset: [], }); return { model, diff --git a/components/watermark/demo/image.vue b/components/watermark/demo/image.vue index 2f36dac1e2..59dd33e7fe 100644 --- a/components/watermark/demo/image.vue +++ b/components/watermark/demo/image.vue @@ -3,7 +3,7 @@ order: 0 title: zh-CN: 图片水印 - en-US: Multi-line watermark + en-US: Image watermark --- ## zh-CN diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md index d71a3089f4..46560d8aa8 100644 --- a/components/watermark/index.en-US.md +++ b/components/watermark/index.en-US.md @@ -1,11 +1,9 @@ --- category: Components -group: Other +type: Other title: Watermark cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*wr1ISY50SyYAAAAAAAAAAAAADrJ8AQ/original coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*duAQQbjHlHQAAAAAAAAAAAAADrJ8AQ/original -demo: - cols: 1 --- Add specific text or patterns to the page. diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx index afd50a8183..5b07db5902 100644 --- a/components/watermark/index.tsx +++ b/components/watermark/index.tsx @@ -37,11 +37,11 @@ export const watermarkProps = () => ({ }, rootClassName: String, gap: { - type: Array as PropType>, + type: [Array, Object] as PropType<[number, number]>, default: undefined, }, offset: { - type: Array as PropType>, + type: [Array, Object] as PropType<[number, number]>, default: undefined, }, }); @@ -242,7 +242,9 @@ const Watermark = defineComponent({ } }); }; - useMutationObserver(containerRef, onMutate); + useMutationObserver(containerRef, onMutate, { + attributes: true, + }); return () => { return (