From bbe6d7f5f5ba75d37f23a25fdb8a3be049ee4a57 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Mon, 18 Sep 2023 16:10:15 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20=E5=A4=B4=E5=83=8F=E8=A3=81?= =?UTF-8?q?=E5=89=AA=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 29 ++ src/config.json | 10 + .../__tests__/avatarcropper.spec.ts | 67 +++ src/packages/__VUE/avatarcropper/demo.vue | 22 + src/packages/__VUE/avatarcropper/doc.en-US.md | 64 +++ src/packages/__VUE/avatarcropper/doc.md | 64 +++ src/packages/__VUE/avatarcropper/doc.taro.md | 5 + src/packages/__VUE/avatarcropper/index.scss | 81 ++++ src/packages/__VUE/avatarcropper/index.vue | 424 ++++++++++++++++++ 10 files changed, 767 insertions(+) create mode 100644 src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts create mode 100644 src/packages/__VUE/avatarcropper/demo.vue create mode 100644 src/packages/__VUE/avatarcropper/doc.en-US.md create mode 100644 src/packages/__VUE/avatarcropper/doc.md create mode 100644 src/packages/__VUE/avatarcropper/doc.taro.md create mode 100644 src/packages/__VUE/avatarcropper/index.scss create mode 100644 src/packages/__VUE/avatarcropper/index.vue diff --git a/package.json b/package.json index 1529dcc4d7..de9a91359e 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "unplugin-vue-markdown": "^0.24.3", "vite": "^4.4.9", "vitest": "^0.34.3", + "vitest-canvas-mock": "^0.3.3", "vue": "^3.3.4", "vue-tsc": "^1.8.8" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b14d53df4e..f9885aa47d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: vitest: specifier: ^0.34.3 version: 0.34.3(@vitest/ui@0.34.3)(sass@1.66.1) + vitest-canvas-mock: + specifier: ^0.3.3 + version: 0.3.3(vitest@0.34.3) vue: specifier: ^3.3.4 version: 3.3.4 @@ -6634,6 +6637,10 @@ packages: engines: {node: '>=4'} hasBin: true + /cssfontparser@1.2.1: + resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==} + dev: true + /cssnano-preset-default@5.2.14(postcss@8.4.29): resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} engines: {node: ^10 || ^12 || >=14.0} @@ -9865,6 +9872,13 @@ packages: resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} dev: true + /jest-canvas-mock@2.5.2: + resolution: {integrity: sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==} + dependencies: + cssfontparser: 1.2.1 + moo-color: 1.0.3 + dev: true + /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -11136,6 +11150,12 @@ packages: yargs-unparser: 2.0.0 dev: true + /moo-color@1.0.3: + resolution: {integrity: sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==} + dependencies: + color-name: 1.1.4 + dev: true + /move-concurrently@1.0.1: resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} dependencies: @@ -15226,6 +15246,15 @@ packages: fsevents: 2.3.3 dev: true + /vitest-canvas-mock@0.3.3(vitest@0.34.3): + resolution: {integrity: sha512-3P968tYBpqYyzzOaVtqnmYjqbe13576/fkjbDEJSfQAkHtC5/UjuRHOhFEN/ZV5HVZIkaROBUWgazDKJ+Ibw+Q==} + peerDependencies: + vitest: '*' + dependencies: + jest-canvas-mock: 2.5.2 + vitest: 0.34.3(@vitest/ui@0.34.3)(sass@1.66.1) + dev: true + /vitest@0.34.3(@vitest/ui@0.34.3)(sass@1.66.1): resolution: {integrity: sha512-7+VA5Iw4S3USYk+qwPxHl8plCMhA5rtfwMjgoQXMT7rO5ldWcdsdo3U1QD289JgglGK4WeOzgoLTsGFu6VISyQ==} engines: {node: '>=v14.18.0'} diff --git a/src/config.json b/src/config.json index 66787dbd91..34bf61b0a7 100644 --- a/src/config.json +++ b/src/config.json @@ -1300,6 +1300,16 @@ "type": "component", "author": "ailululu", "taro": true + }, + { + "version": "1.0.0", + "name": "AvatarCropper", + "type": "component", + "show": true, + "cName": "头像裁剪", + "desc": "头像裁剪", + "exportEmpty": true, + "author": "Marvin" } ] } diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts new file mode 100644 index 0000000000..a99cd612f8 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -0,0 +1,67 @@ +import 'vitest-canvas-mock'; +import { mount } from '@vue/test-utils'; +import AvatarCropper from '../index.vue'; +import { sleep } from '@/packages/utils/unit'; + +const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { + type: 'test' +}); + +test('layout default slot', () => { + const wrapper = mount(AvatarCropper, { + slots: { + default: 'Main Content' + } + }); + + expect(wrapper.find('.nut-avatar-cropper').html()).toContain('Main Content'); +}); + +test('should render base cutAvatar and type', async () => { + const wrapper = mount(AvatarCropper); + const up_load = wrapper.find('.nut-avatar-cropper'); + expect(up_load.exists()).toBe(true); + const up_load1 = wrapper.find('.nut-avatar-cropper__input'); + expect(up_load1.attributes().type).toBe('file'); +}); + +test('AvatarCropper: Select the image to open the crop window', async () => { + const wrapper = mount(AvatarCropper); + const input: any = wrapper.find('.nut-avatar-cropper__input'); + expect(input.exists()).toBe(true); + const smallFile = new File([new ArrayBuffer(100)], 'small.jpg'); + Object.defineProperty(input.element, 'files', { + get: vi.fn().mockReturnValue([mockFile, smallFile]) + }); + expect(wrapper.find('.nut-cropper-popup').exists()).toBe(false); + await input.trigger('change'); + expect(wrapper.emitted('change')).toBeTruthy(); + await sleep(); + expect(wrapper.find('.nut-cropper-popup').exists()).toBe(true); + const canvas = wrapper.find('.nut-cropper-popup__canvas'); + expect(canvas.exists()).toBe(true); + + const btns = wrapper.findAll('.nut-cropper-popup__btns-item'); + expect(btns.length).toBe(4); + + const cancel = btns[0]; + cancel.trigger('click'); + expect(wrapper.emitted('cancel')).toBeTruthy(); + expect(input.element.value).toBe(''); + await sleep(); + expect(wrapper.find('.nut-cropper-popup').exists()).toBe(false); + + const resetAngle = btns[1]; + resetAngle.trigger('click'); + expect(wrapper.vm.angle).toBe(0); + + const setAngle = btns[2]; + setAngle.trigger('click'); + expect(wrapper.vm.angle).toBe(90); + setAngle.trigger('click'); + expect(wrapper.vm.angle).toBe(180); + + const confirm = btns[3]; + confirm.trigger('click'); + expect(wrapper.emitted('confirm')).toBeTruthy(); +}); diff --git a/src/packages/__VUE/avatarcropper/demo.vue b/src/packages/__VUE/avatarcropper/demo.vue new file mode 100644 index 0000000000..f177d2712c --- /dev/null +++ b/src/packages/__VUE/avatarcropper/demo.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/packages/__VUE/avatarcropper/doc.en-US.md b/src/packages/__VUE/avatarcropper/doc.en-US.md new file mode 100644 index 0000000000..d788def22a --- /dev/null +++ b/src/packages/__VUE/avatarcropper/doc.en-US.md @@ -0,0 +1,64 @@ +# AvatarCropper Head cropping + +### introduce + +Used to cut the profile picture to create a new image. + +### install + +```js +import { createApp } from 'vue'; +import { AvatarCropper } from '@nutui/nutui'; + +const app = createApp(); +app.use(AvatarCropper); +``` + +### 基础用法 + +Use the avatar component directly in the middle, and the image content will be replaced with the new one after cropping. + +:::demo + +```vue + + +``` + +::: + +## API + +### AvatarCropper Props + +| Attribute | Description | Type | Default | +| --------- | --------------------------------------------------- | ------ | ------- | +| maxZoom | Maximum zoom | number | 3 | +| space | The gap reserved on both sides of the clipping area | number | 20 | + +### AvatarCropper Slots + +| Name | Description | +| ------- | --------------------------------------------------------------------- | +| default | The default slot for placing elements such as images, ICONS, and text | + +### AvatarCropper Events + +| Name | Description | Callback Arguments | +| ------- | --------------------------------------- | ---------------------- | +| confirm | Click Confirm to trigger after cropping | url:The trimmed base64 | +| cancel | Click cancel trigger | - | diff --git a/src/packages/__VUE/avatarcropper/doc.md b/src/packages/__VUE/avatarcropper/doc.md new file mode 100644 index 0000000000..51f11b0062 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/doc.md @@ -0,0 +1,64 @@ +# AvatarCropper 头像剪切 + +### 介绍 + +用来对头像进行剪切生成一张新的图片。 + +### 安装 + +```js +import { createApp } from 'vue'; +import { AvatarCropper } from '@nutui/nutui'; + +const app = createApp(); +app.use(AvatarCropper); +``` + +### 基础用法 + +中间直接使用avatar组件,裁剪后图片内容会被替换为新的。 + +:::demo + +```vue + + +``` + +::: + +## API + +### AvatarCropper Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------- | ---------------------- | ------ | ------ | +| maxZoom | 最大缩放倍数 | number | 3 | +| space | 裁剪区域两边预留的间隙 | number | 20 | + +### AvatarCropper Slots + +| 名称 | 描述 | +| ------- | -------------------------------------- | +| default | 默认插槽,可放置图片、图标、文本等元素 | + +### AvatarCropper Events + +| 名称 | 描述 | 回调参数 | +| ------- | ------------------ | ------------------ | +| confirm | 裁剪后点击确认触发 | url:裁剪后的base64 | +| cancel | 点击取消触发 | - | diff --git a/src/packages/__VUE/avatarcropper/doc.taro.md b/src/packages/__VUE/avatarcropper/doc.taro.md new file mode 100644 index 0000000000..49ca132ad5 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/doc.taro.md @@ -0,0 +1,5 @@ +# AvatarCropper 头像剪切 + +### 介绍 + +后续再进行开发 diff --git a/src/packages/__VUE/avatarcropper/index.scss b/src/packages/__VUE/avatarcropper/index.scss new file mode 100644 index 0000000000..f93d07c2d9 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/index.scss @@ -0,0 +1,81 @@ +.nut-avatar-cropper { + position: relative; + &::after { + content: '编辑'; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba($color: #000000, $alpha: 0.3); + z-index: 1; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + } + &__input { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; // 隐藏原生上传按钮 + cursor: pointer; + z-index: 2; + } +} + +.nut-cropper-popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--nut-overlay-bg-color, rgba(0, 0, 0, 0.7)); + z-index: 1000; + &__canvas { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + } + &__btns { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + display: flex; + justify-content: space-between; + z-index: 2; + &-item { + color: #fff; + padding: 15px; + cursor: pointer; + display: flex; + align-items: center; + } + } + &__highlight { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + background-color: transparent; + .highlight { + position: absolute; + width: 365px; + height: 365px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 1; + background-color: transparent; + box-shadow: 0 0 1000px 1000px rgba(0, 0, 0, 0.6); + } + } +} diff --git a/src/packages/__VUE/avatarcropper/index.vue b/src/packages/__VUE/avatarcropper/index.vue new file mode 100644 index 0000000000..63cdde6d20 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/index.vue @@ -0,0 +1,424 @@ + + + + + From 2d09298be7a0d8d0a5085ff853cba7fae510d4f9 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Mon, 18 Sep 2023 16:33:20 +0800 Subject: [PATCH 02/21] docs: update config.json --- src/config.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/config.json b/src/config.json index 34bf61b0a7..c651332f03 100644 --- a/src/config.json +++ b/src/config.json @@ -1305,10 +1305,13 @@ "version": "1.0.0", "name": "AvatarCropper", "type": "component", + "tarodoc": true, "show": true, "cName": "头像裁剪", - "desc": "头像裁剪", - "exportEmpty": true, + "desc": "仿微信头像裁剪功能", + "taro": false, + "exportEmpty": false, + "exportEmptyTaro": false, "author": "Marvin" } ] From 843bac65796ba5693ec04f9ec2bc1153a81bc938 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Mon, 18 Sep 2023 18:05:08 +0800 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0bottom=E6=8F=92?= =?UTF-8?q?=E6=A7=BD=EF=BC=8C=E6=8E=A7=E5=88=B6=E5=B7=A5=E5=85=B7=E6=A0=8F?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=EF=BC=8C=E6=8A=9B=E5=87=BA=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=A0=8F=E7=9B=B8=E5=85=B3=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/__VUE/avatarcropper/demo.vue | 24 +++++++ src/packages/__VUE/avatarcropper/doc.en-US.md | 71 +++++++++++++++++-- src/packages/__VUE/avatarcropper/doc.md | 71 +++++++++++++++++-- src/packages/__VUE/avatarcropper/index.scss | 10 ++- src/packages/__VUE/avatarcropper/index.vue | 61 +++++++++++----- src/packages/__VUE/avatarcropper/types.ts | 1 + 6 files changed, 203 insertions(+), 35 deletions(-) create mode 100644 src/packages/__VUE/avatarcropper/types.ts diff --git a/src/packages/__VUE/avatarcropper/demo.vue b/src/packages/__VUE/avatarcropper/demo.vue index f177d2712c..5b2ffbb89a 100644 --- a/src/packages/__VUE/avatarcropper/demo.vue +++ b/src/packages/__VUE/avatarcropper/demo.vue @@ -8,6 +8,22 @@ +

裁剪区域bottom插槽

+ + + + + + + + @@ -16,7 +32,15 @@ import { ref } from 'vue'; const imageUrl = ref( 'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png' ); +const avatarCropperRef = ref(); const cutImage = (url: string) => { imageUrl.value = url; }; + + diff --git a/src/packages/__VUE/avatarcropper/doc.en-US.md b/src/packages/__VUE/avatarcropper/doc.en-US.md index d788def22a..fccd2b4b45 100644 --- a/src/packages/__VUE/avatarcropper/doc.en-US.md +++ b/src/packages/__VUE/avatarcropper/doc.en-US.md @@ -41,20 +41,68 @@ const cutImage = (url: string) => { ::: +### Clipping region bottom slots + +Customize the clipping area toolbar, and BNs-position controls the toolbar position + +:::demo + +```vue + + + + + +``` + +::: + ## API ### AvatarCropper Props -| Attribute | Description | Type | Default | -| --------- | --------------------------------------------------- | ------ | ------- | -| maxZoom | Maximum zoom | number | 3 | -| space | The gap reserved on both sides of the clipping area | number | 20 | +| Attribute | Description | Type | Default | +| ------------- | ----------------------------------------------------------------------------------- | ------ | ------- | +| maxZoom | Maximum zoom | number | 3 | +| space | The gap reserved on both sides of the clipping area | number | 20 | +| btnsPosition | Location of the toolbar in the clipping area. The optional value is:`top` `bottom` | string | bottom | +| cancelText | Cancel button text | string | 取消 | +| cancelConfirm | Confirm button text | string | 确认 | ### AvatarCropper Slots -| Name | Description | -| ------- | --------------------------------------------------------------------- | -| default | The default slot for placing elements such as images, ICONS, and text | +| Name | Description | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| default | The default slot for placing elements such as images, ICONS, and text | +| bottom | After selecting the file, crop the bottom element of the pop-up window can be customized, and invoke the method of the component through ref | ### AvatarCropper Events @@ -62,3 +110,12 @@ const cutImage = (url: string) => { | ------- | --------------------------------------- | ---------------------- | | confirm | Click Confirm to trigger after cropping | url:The trimmed base64 | | cancel | Click cancel trigger | - | + +### AvatarCropper Ref + +| Event | Explain | +| ---------- | ------------------ | +| cancel | uncrop | +| resetAngle | Reset to 0 degrees | +| setAngle | Rotate 90 degrees | +| confirm | Definite cut | diff --git a/src/packages/__VUE/avatarcropper/doc.md b/src/packages/__VUE/avatarcropper/doc.md index 51f11b0062..f6c2a87836 100644 --- a/src/packages/__VUE/avatarcropper/doc.md +++ b/src/packages/__VUE/avatarcropper/doc.md @@ -41,20 +41,68 @@ const cutImage = (url: string) => { ::: +### 裁剪区域bottom插槽 + +自定义裁剪区域工具栏,btns-position控制工具栏位置 + +:::demo + +```vue + + + + + +``` + +::: + ## API ### AvatarCropper Props -| 参数 | 说明 | 类型 | 默认值 | -| ------- | ---------------------- | ------ | ------ | -| maxZoom | 最大缩放倍数 | number | 3 | -| space | 裁剪区域两边预留的间隙 | number | 20 | +| 参数 | 说明 | 类型 | 默认值 | +| ------------- | ------------------------------------------- | ------ | ------ | +| maxZoom | 最大缩放倍数 | number | 3 | +| space | 裁剪区域两边预留的间隙 | number | 10 | +| btnsPosition | 裁剪区域工具栏位置,可选值为:`top` `bottom` | string | bottom | +| cancelText | 取消按钮的文字 | string | 取消 | +| cancelConfirm | 确认按钮的文字 | string | 确认 | ### AvatarCropper Slots -| 名称 | 描述 | -| ------- | -------------------------------------- | -| default | 默认插槽,可放置图片、图标、文本等元素 | +| 名称 | 描述 | +| ------- | ----------------------------------------------------------- | +| default | 默认插槽,可放置图片、图标、文本等元素 | +| bottom | 选择文件后裁剪弹窗底部元素可以自定义,通过ref调用组件的方法 | ### AvatarCropper Events @@ -62,3 +110,12 @@ const cutImage = (url: string) => { | ------- | ------------------ | ------------------ | | confirm | 裁剪后点击确认触发 | url:裁剪后的base64 | | cancel | 点击取消触发 | - | + +### AvatarCropper Ref + +| 事件名 | 说明 | +| ---------- | --------- | +| cancel | 取消裁剪 | +| resetAngle | 重置为0度 | +| setAngle | 旋转90度 | +| confirm | 确定裁剪 | diff --git a/src/packages/__VUE/avatarcropper/index.scss b/src/packages/__VUE/avatarcropper/index.scss index f93d07c2d9..825a8dc8be 100644 --- a/src/packages/__VUE/avatarcropper/index.scss +++ b/src/packages/__VUE/avatarcropper/index.scss @@ -47,9 +47,15 @@ bottom: 0; left: 0; width: 100%; - display: flex; - justify-content: space-between; z-index: 2; + &.top { + top: 0; + bottom: inherit; + } + .flex-sb { + display: flex; + justify-content: space-between; + } &-item { color: #fff; padding: 15px; diff --git a/src/packages/__VUE/avatarcropper/index.vue b/src/packages/__VUE/avatarcropper/index.vue index 63cdde6d20..4c89e59396 100644 --- a/src/packages/__VUE/avatarcropper/index.vue +++ b/src/packages/__VUE/avatarcropper/index.vue @@ -20,27 +20,31 @@ > - - - 取消 - - - - - - - - - 确定 + + + + + {{ cancelText }} + + + + + + + + + {{ confirmText }} + diff --git a/src/config.json b/src/config.json index c651332f03..b44e73dd95 100644 --- a/src/config.json +++ b/src/config.json @@ -1309,9 +1309,7 @@ "show": true, "cName": "头像裁剪", "desc": "仿微信头像裁剪功能", - "taro": false, - "exportEmpty": false, - "exportEmptyTaro": false, + "taro": true, "author": "Marvin" } ] diff --git a/src/packages/__VUE/avatarcropper/canvas-util.ts b/src/packages/__VUE/avatarcropper/canvas-util.ts new file mode 100644 index 0000000000..be0fa47e00 --- /dev/null +++ b/src/packages/__VUE/avatarcropper/canvas-util.ts @@ -0,0 +1,107 @@ +import Taro from '@tarojs/taro'; +import CanvasContext = Taro.CanvasContext; + +const compareVersion = (v1Old: string, v2Old: string) => { + let v1 = v1Old.split('.'); + let v2 = v2Old.split('.'); + const len = Math.max(v1.length, v2.length); + + while (v1.length < len) { + v1.push('0'); + } + while (v2.length < len) { + v2.push('0'); + } + + for (let i = 0; i < len; i++) { + const num1 = parseInt(v1[i]); + const num2 = parseInt(v2[i]); + + if (num1 > num2) { + return 1; + } else if (num1 < num2) { + return -1; + } + } + + return 0; +}; + +const isWeapp = () => { + return process.env.TARO_ENV === 'weapp'; +}; + +////////////////////////////////////////////////////////////////////////////////// +//////// 微信小程序自1.9.90起废除若干个CanvasContext的函数,改为属性,以下为兼容代码 +////////////////////////////////////////////////////////////////////////////////// + +function _easyCanvasContextBase( + systemInfo: any, + lowCallback: () => void, + highCallback: () => void, + targetVersion: string = '1.9.90' +) { + if (isWeapp() && compareVersion(systemInfo.SDKVersion, targetVersion) >= 0) { + highCallback(); + } else { + lowCallback(); + } +} +/** + * + * 基础库 1.9.90 开始支持,低版本需做兼容处理。填充颜色。用法同 CanvasContext.setFillStyle()。 + * @param systemInfo + * @param canvasContext + * @param color + */ +function easySetStrokeStyle( + systemInfo: Taro.getSystemInfoSync.Result, + canvasContext: CanvasContext, + color: string | CanvasGradient +) { + _easyCanvasContextBase( + systemInfo, + () => { + canvasContext.setStrokeStyle(color); + console.log('???'); + }, + () => { + if (typeof color === 'string') { + canvasContext.strokeStyle = color; + } + console.log('2333'); + } + ); +} + +function easySetLineWidth(systemInfo: Taro.getSystemInfoSync.Result, canvasContext: CanvasContext, lineWidth: number) { + _easyCanvasContextBase( + systemInfo, + () => { + canvasContext.setLineWidth(lineWidth); + }, + () => { + canvasContext.lineWidth = lineWidth; + } + ); +} + +function easySetFillStyle( + systemInfo: Taro.getSystemInfoSync.Result, + canvasContext: CanvasContext, + color: string | CanvasGradient +) { + _easyCanvasContextBase( + systemInfo, + () => { + canvasContext.setFillStyle(color); + }, + () => { + if (typeof color === 'string') { + canvasContext.fillStyle = color; + } + } + ); +} + +export { easySetStrokeStyle, easySetLineWidth, easySetFillStyle }; diff --git a/src/packages/__VUE/avatarcropper/doc.taro.md b/src/packages/__VUE/avatarcropper/doc.taro.md index 49ca132ad5..d5e2efb89b 100644 --- a/src/packages/__VUE/avatarcropper/doc.taro.md +++ b/src/packages/__VUE/avatarcropper/doc.taro.md @@ -2,4 +2,123 @@ ### 介绍 -后续再进行开发 +用来对头像进行剪切生成一张新的图片。 + +### 安装 + +```js +import { createApp } from 'vue'; +import { AvatarCropper } from '@nutui/nutui'; + +const app = createApp(); +app.use(AvatarCropper); +``` + +### 基础用法 + +中间直接使用avatar组件,裁剪后图片内容会被替换为新的。 + +:::demo + +```vue + + +``` + +::: + +### 裁剪区域toolbar插槽 + +自定义裁剪区域工具栏,toolbar-position控制工具栏位置 + +:::demo + +```vue + + + + + +``` + +::: + +## API + +### AvatarCropper Props + +| 参数 | 说明 | 类型 | 默认值 | +| ---------------- | -------------------------------------------------- | ------ | -------------------------- | +| max-zoom | 最大缩放倍数 | number | 3 | +| space | 裁剪区域两边预留的间隙 | number | 10 | +| toolbar-position | 裁剪区域工具栏位置,可选值为:`top` `bottom` | string | bottom | +| edit-text | 中间的文字内容 | string | 编辑 | +| cancel-text | 取消按钮的文字 | string | 取消 | +| cancel-confirm | 确认按钮的文字 | string | 确认 | +| size-type | 所选的图片的尺寸: 可选值:`original` `compressed` | Array | ['original', 'compressed'] | +| source-type | 选择图片的来源: 可选值:`album` `camera` | Array | ['album', 'camera'] | + +### AvatarCropper Slots + +| 名称 | 描述 | +| ------- | ----------------------------------------------------------- | +| default | 默认插槽,可放置图片、图标、文本等元素 | +| toolbar | 选择文件后裁剪弹窗底部元素可以自定义,通过ref调用组件的方法 | + +### AvatarCropper Events + +| 名称 | 描述 | 回调参数 | +| ------- | ------------------ | ------------------ | +| confirm | 裁剪后点击确认触发 | url:裁剪后的base64 | +| cancel | 点击取消触发 | - | + +### AvatarCropper Ref + +| 事件名 | 说明 | +| ------- | --------- | +| cancel | 取消裁剪 | +| reset | 重置为0度 | +| rotate | 旋转90度 | +| confirm | 确定裁剪 | diff --git a/src/packages/__VUE/avatarcropper/index.scss b/src/packages/__VUE/avatarcropper/index.scss index 2564505ce8..016984a48e 100644 --- a/src/packages/__VUE/avatarcropper/index.scss +++ b/src/packages/__VUE/avatarcropper/index.scss @@ -1,6 +1,7 @@ .nut-avatar-cropper { position: relative; - &::after { + &::after, + &__edit-text { content: attr(data-edit-text); position: absolute; top: 0; @@ -14,6 +15,11 @@ justify-content: center; align-items: center; } + &.taro { + &::after { + content: none; + } + } &__input { position: absolute; top: 0; @@ -34,7 +40,8 @@ height: 100%; background: var(--nut-overlay-bg-color, rgba(0, 0, 0, 0.7)); z-index: 1000; - &__canvas { + &__canvas, + &__cut-canvas { position: absolute; bottom: 0; left: 0; @@ -42,6 +49,9 @@ height: 100%; z-index: 1; } + &__cut-canvas { + z-index: 0; + } &__toolbar { position: absolute; bottom: 0; diff --git a/src/packages/__VUE/avatarcropper/index.taro.vue b/src/packages/__VUE/avatarcropper/index.taro.vue new file mode 100644 index 0000000000..f948d9f33c --- /dev/null +++ b/src/packages/__VUE/avatarcropper/index.taro.vue @@ -0,0 +1,619 @@ + + + diff --git a/src/packages/__VUE/avatarcropper/types.ts b/src/packages/__VUE/avatarcropper/types.ts index 47efbe1e5b..155fcd07e8 100644 --- a/src/packages/__VUE/avatarcropper/types.ts +++ b/src/packages/__VUE/avatarcropper/types.ts @@ -1 +1,3 @@ export type AvatarCropperToolbarPosition = 'top' | 'bottom'; +export type AvatarCropperSizeType = 'original' | 'compressed'; +export type AvatarCropperSourceType = 'album' | 'camera'; From b9e775f89b168929492d30b587336a6319f5c33e Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sat, 23 Sep 2023 13:18:20 +0800 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/nutui-taro-demo/project.private.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nutui-taro-demo/project.private.config.json b/packages/nutui-taro-demo/project.private.config.json index 79196e4582..39f7acfbef 100644 --- a/packages/nutui-taro-demo/project.private.config.json +++ b/packages/nutui-taro-demo/project.private.config.json @@ -65,4 +65,4 @@ }, "projectname": "vue4.x", "libVersion": "2.27.1" -} +} \ No newline at end of file From b2a70404d690ab59626652227ea6149e8099f16c Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sat, 23 Sep 2023 13:50:39 +0800 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/__VUE/avatarcropper/doc.taro.md | 2 +- src/packages/__VUE/avatarcropper/index.taro.vue | 5 ++--- .../{__VUE/avatarcropper/canvas-util.ts => utils/canvas.ts} | 0 3 files changed, 3 insertions(+), 4 deletions(-) rename src/packages/{__VUE/avatarcropper/canvas-util.ts => utils/canvas.ts} (100%) diff --git a/src/packages/__VUE/avatarcropper/doc.taro.md b/src/packages/__VUE/avatarcropper/doc.taro.md index d5e2efb89b..59611620e9 100644 --- a/src/packages/__VUE/avatarcropper/doc.taro.md +++ b/src/packages/__VUE/avatarcropper/doc.taro.md @@ -8,7 +8,7 @@ ```js import { createApp } from 'vue'; -import { AvatarCropper } from '@nutui/nutui'; +import { AvatarCropper } from '@nutui/nutui-taro'; const app = createApp(); app.use(AvatarCropper); diff --git a/src/packages/__VUE/avatarcropper/index.taro.vue b/src/packages/__VUE/avatarcropper/index.taro.vue index f948d9f33c..7a89e7efbf 100644 --- a/src/packages/__VUE/avatarcropper/index.taro.vue +++ b/src/packages/__VUE/avatarcropper/index.taro.vue @@ -51,14 +51,13 @@ import { watch, ref, reactive, toRefs, computed, PropType, onMounted } from 'vue'; import Button from '../button/index.vue'; import { createComponent } from '@/packages/utils/create'; -import type { AvatarCropperToolbarPosition } from './types'; +import type { AvatarCropperToolbarPosition, AvatarCropperSizeType, AvatarCropperSourceType } from './types'; const { create } = createComponent('avatar-cropper'); import { IconFont } from '@nutui/icons-vue-taro'; import { useTouch } from '@/packages/utils/useTouch'; import { preventDefault, clamp } from '@/packages/utils/util'; -import { AvatarCropperSizeType, AvatarCropperSourceType } from './types'; import Taro from '@tarojs/taro'; -import { easySetFillStyle } from './canvas-util'; +import { easySetFillStyle } from '@/packages/utils/canvas'; export default create({ components: { diff --git a/src/packages/__VUE/avatarcropper/canvas-util.ts b/src/packages/utils/canvas.ts similarity index 100% rename from src/packages/__VUE/avatarcropper/canvas-util.ts rename to src/packages/utils/canvas.ts From 5a9934f5d2bc2bd8c6e05f66743380dd8f4df8a8 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sat, 23 Sep 2023 15:51:49 +0800 Subject: [PATCH 10/21] =?UTF-8?q?feat:=20=E9=92=88=E5=AF=B9web=E7=BB=98?= =?UTF-8?q?=E5=88=B6=E4=BD=BF=E7=94=A8=E8=AE=BE=E5=A4=87=E5=83=8F=E7=B4=A0?= =?UTF-8?q?=E6=AF=94=E6=8F=90=E5=8D=87=E5=9B=BE=E5=83=8F=E8=B4=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__VUE/avatarcropper/index.taro.vue | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/index.taro.vue b/src/packages/__VUE/avatarcropper/index.taro.vue index 7a89e7efbf..2295c30e14 100644 --- a/src/packages/__VUE/avatarcropper/index.taro.vue +++ b/src/packages/__VUE/avatarcropper/index.taro.vue @@ -4,18 +4,11 @@ {{ editText }} - + { @@ -162,8 +156,8 @@ export default create({ // 高亮框样式 const highlightStyle = computed(() => { - const { displayWidth } = state; - const width = displayWidth - props.space * 2 + 'px'; + const { cropperWidth } = state; + const width = cropperWidth / pixelRatio + 'px'; const height = width; return { width, @@ -174,18 +168,18 @@ export default create({ const canvasStyle = computed(() => { const { displayWidth, displayHeight } = state; return { - width: `${displayWidth}px`, - height: `${displayHeight}px` + width: `${displayWidth / pixelRatio}px`, + height: `${displayHeight / pixelRatio}px` }; }); const cutCanvasStyle = computed(() => { const { displayWidth, displayHeight, cropperWidth } = state; return { - top: `${(displayHeight - cropperWidth) / 2}px`, - left: `${(displayWidth - cropperWidth) / 2}px`, - width: `${cropperWidth}px`, - height: `${cropperWidth}px` + top: `${(displayHeight / pixelRatio - cropperWidth / pixelRatio) / 2}px`, + left: `${(displayWidth / pixelRatio - cropperWidth / pixelRatio) / 2}px`, + width: `${cropperWidth / pixelRatio}px`, + height: `${cropperWidth / pixelRatio}px` }; }); @@ -218,6 +212,7 @@ export default create({ }); }; + // web绘制 const webDraw = () => { const { src, width, height, x, y } = drawImage.value; const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; @@ -235,7 +230,7 @@ export default create({ ctx.fillStyle = '#666'; ctx.fillRect(0, 0, displayWidth, displayHeight); ctx.fillStyle = '#000'; - ctx.fillRect(props.space, (displayHeight - cropperWidth) / 2, cropperWidth, cropperWidth); + ctx.fillRect(props.space * pixelRatio, (displayHeight - cropperWidth) / 2, cropperWidth, cropperWidth); // 绘制偏移量 ctx.translate(displayWidth / 2 + moveX, displayHeight / 2 + moveY); @@ -486,7 +481,7 @@ export default create({ canvas && croppedCtx.drawImage( canvas, - props.space, + props.space * pixelRatio, (displayHeight - cropperWidth) / 2, width, height, @@ -508,7 +503,6 @@ export default create({ confirmWEB(); return; } - const { pixelRatio } = systemInfo; const { cropperWidth, displayWidth, displayHeight } = state; const { cropperCutCanvasContext, canvasId, cutCanvasId } = canvasAll; // 将编辑后的canvas内容转成图片 @@ -544,8 +538,8 @@ export default create({ y: 0, width: cropperWidth, height: cropperWidth, - destWidth: cropperWidth * pixelRatio, - destHeight: cropperWidth * pixelRatio, + destWidth: cropperWidth * systemInfo.pixelRatio, + destHeight: cropperWidth * systemInfo.pixelRatio, success: (res: Taro.canvasToTempFilePath.SuccessCallbackResult) => { let filePath = res.tempFilePath; From c7a751a983e9e67e2a2ba2fabb158b369da934e4 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Mon, 25 Sep 2023 14:03:12 +0800 Subject: [PATCH 11/21] =?UTF-8?q?feat:=20=E9=80=82=E9=85=8D=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AE=9D=E5=B0=8F=E7=A8=8B=E5=BA=8Fcanvas=202d?= =?UTF-8?q?=EF=BC=8C=E8=A6=81=E6=B1=82=E5=9F=BA=E7=A1=80=E5=BA=93=E7=89=88?= =?UTF-8?q?=E6=9C=AC2.7.0=E6=88=96=E6=9B=B4=E9=AB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__VUE/avatarcropper/index.taro.vue | 177 +++++++++++------- src/packages/utils/canvas.ts | 2 +- 2 files changed, 112 insertions(+), 67 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/index.taro.vue b/src/packages/__VUE/avatarcropper/index.taro.vue index 2295c30e14..5eee51091a 100644 --- a/src/packages/__VUE/avatarcropper/index.taro.vue +++ b/src/packages/__VUE/avatarcropper/index.taro.vue @@ -4,12 +4,12 @@ {{ editText }} - ({ canvasId: `canvas-${Date.now()}`, - cutCanvasId: `cut-canvas-${Date.now()}`, - cropperCanvasContext: null, - cropperCutCanvasContext: null + cropperCanvas: null, + cropperCanvasContext: null }); // 绘制图片 const drawImage = ref({ @@ -138,17 +136,36 @@ export default create({ const touch = useTouch(); // 获取系统信息 const systemInfo: Taro.getSystemInfoSync.Result = Taro.getSystemInfoSync(); - const pixelRatio = Taro.getEnv() === 'WEB' ? systemInfo.pixelRatio : 1; + // 支付宝基础库2.7.0以上支持,需要开启支付宝小程序canvas2d + const showAlipayCanvas2D = computed(() => { + return Taro.getEnv() === 'ALIPAY' && parseInt((Taro as any).SDKVersion.replace(/\./g, '')) >= 270; + }); + const showPixelRatio = Taro.getEnv() === 'WEB' || showAlipayCanvas2D.value; + const pixelRatio = showPixelRatio ? systemInfo.pixelRatio : 1; state.displayWidth = systemInfo.windowWidth * pixelRatio; state.displayHeight = systemInfo.windowHeight * pixelRatio; state.cropperWidth = state.cropperHeight = state.displayWidth - props.space * pixelRatio * 2; + useReady(() => { + if (showAlipayCanvas2D.value) { + const { canvasId } = canvasAll; + Taro.createSelectorQuery() + .select(`#${canvasId}`) + .node(({ node: canvas }) => { + canvas.width = state.displayWidth; + canvas.height = state.displayHeight; + canvasAll.cropperCanvas = canvas; + }) + .exec(); + } + }); + // 初始化canvas onMounted(() => { - const { canvasId, cutCanvasId } = canvasAll; + const { canvasId } = canvasAll; canvasAll.cropperCanvasContext = Taro.createCanvasContext(canvasId); - canvasAll.cropperCutCanvasContext = Taro.createCanvasContext(cutCanvasId); }); + // 是否是横向 const isAngle = computed(() => { return state.angle === 90 || state.angle === 270; @@ -212,20 +229,19 @@ export default create({ }); }; - // web绘制 - const webDraw = () => { - const { src, width, height, x, y } = drawImage.value; - const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; - const canvasDom: HTMLElement | null = document.getElementById(canvasAll.canvasId); - let canvas: HTMLCanvasElement = canvasDom as HTMLCanvasElement; - if (canvasDom?.tagName !== 'CANVAS') { - canvas = canvasDom?.getElementsByTagName('canvas')[0] as HTMLCanvasElement; - canvas.width = displayWidth; - canvas.height = displayHeight; - } + // base64转图片(canvasImage) + const dataURLToCanvasImage = (canvas: any, dataURL: string): Promise => { + return new Promise((resolve) => { + const img = new canvas.createImage(); + img.onload = () => resolve(img); + img.src = dataURL; + }); + }; - const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + const canvas2dDraw = (ctx: CanvasRenderingContext2D) => { if (!ctx) return; + const { src, width, height, x, y } = drawImage.value; + const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; ctx.clearRect(0, 0, displayWidth, displayHeight); ctx.fillStyle = '#666'; ctx.fillRect(0, 0, displayWidth, displayHeight); @@ -242,12 +258,38 @@ export default create({ ctx.drawImage(src as HTMLImageElement, x, y, width, height); }; + // web绘制 + const webDraw = () => { + const { displayWidth, displayHeight } = state; + const canvasDom: HTMLElement | null = document.getElementById(canvasAll.canvasId); + let canvas: HTMLCanvasElement = canvasDom as HTMLCanvasElement; + if (canvasDom?.tagName !== 'CANVAS') { + canvas = canvasDom?.getElementsByTagName('canvas')[0] as HTMLCanvasElement; + canvas.width = displayWidth; + canvas.height = displayHeight; + } + + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + canvas2dDraw(ctx); + }; + + const alipayDraw = () => { + const { cropperCanvas } = canvasAll; + let ctx = cropperCanvas.getContext('2d') as CanvasRenderingContext2D; + ctx && ctx.resetTransform(); + canvas2dDraw(ctx); + }; + // 绘制显示的canvas内容 const draw = () => { if (Taro.getEnv() === 'WEB') { webDraw(); return; } + if (showAlipayCanvas2D.value) { + alipayDraw(); + return; + } const { src, width, height, x, y } = drawImage.value; const { moveX, moveY, scale, angle, displayWidth, displayHeight, cropperWidth } = state; const { cropperCanvasContext } = canvasAll; @@ -272,7 +314,7 @@ export default create({ ctx.scale(scale, scale); // 绘制图片 ctx.drawImage(src as string, x, y, width, height); - ctx.draw(false); + ctx.draw(); }; // 设置绘制图片 @@ -285,6 +327,9 @@ export default create({ if (Taro.getEnv() === 'WEB') { drawImg.src = await dataURLToImage(image.path); } + if (showAlipayCanvas2D.value) { + drawImg.src = await dataURLToCanvasImage(canvasAll.cropperCanvas, image.path); + } const isPortrait = imgHeight > imgWidth; const rate = isPortrait ? imgWidth / imgHeight : imgHeight / imgWidth; @@ -461,6 +506,7 @@ export default create({ isEmit && emit('cancel'); }; + // web裁剪图片 const confirmWEB = () => { const { cropperWidth, displayHeight } = state; const canvasDom: HTMLElement | null = document.getElementById(canvasAll.canvasId); @@ -497,56 +543,54 @@ export default create({ cancel(false); }; + // 支付宝基础库2.7.0以上支持,需要开启支付宝小程序canvas2d + const confirmALIPAY = () => { + const { cropperWidth, displayHeight } = state; + const { cropperCanvas } = canvasAll; + Taro.canvasToTempFilePath({ + canvas: cropperCanvas, + x: props.space, + y: (displayHeight - cropperWidth) / 2, + width: cropperWidth, + height: cropperWidth, + destWidth: cropperWidth, + destHeight: cropperWidth, + success: async (res: Taro.canvasToTempFilePath.SuccessCallbackResult) => { + let filePath = res.tempFilePath; + emit('confirm', filePath); + cancel(false); + return; + } + }); + }; + // 裁剪图片 const confirm = () => { if (Taro.getEnv() === 'WEB') { confirmWEB(); return; } - const { cropperWidth, displayWidth, displayHeight } = state; - const { cropperCutCanvasContext, canvasId, cutCanvasId } = canvasAll; + if (showAlipayCanvas2D.value) { + confirmALIPAY(); + return; + } + const { cropperWidth, displayHeight } = state; + const { canvasId } = canvasAll; // 将编辑后的canvas内容转成图片 Taro.canvasToTempFilePath({ canvasId, - x: 0, - y: 0, - width: displayWidth, - height: displayHeight, - destWidth: displayWidth, - destHeight: displayHeight, + x: props.space, + y: (displayHeight - cropperWidth) / 2, + width: cropperWidth, + height: cropperWidth, + destWidth: cropperWidth * systemInfo.pixelRatio, + destHeight: cropperWidth * systemInfo.pixelRatio, success: async (res: Taro.canvasToTempFilePath.SuccessCallbackResult) => { let filePath = res.tempFilePath; - // 绘制裁剪的canvas内容 - const ctx = cropperCutCanvasContext; - if (!ctx) return; - ctx.drawImage( - filePath, - props.space, - (displayHeight - cropperWidth) / 2, - cropperWidth, - cropperWidth, - 0, - 0, - cropperWidth, - cropperWidth - ); - ctx.draw(); - // 将裁剪的canvas内容转成图片 - Taro.canvasToTempFilePath({ - canvasId: cutCanvasId, - x: 0, - y: 0, - width: cropperWidth, - height: cropperWidth, - destWidth: cropperWidth * systemInfo.pixelRatio, - destHeight: cropperWidth * systemInfo.pixelRatio, - success: (res: Taro.canvasToTempFilePath.SuccessCallbackResult) => { - let filePath = res.tempFilePath; - - emit('confirm', filePath); - cancel(false); - } - }); + + emit('confirm', filePath); + cancel(false); + return; } }); }; @@ -595,6 +639,7 @@ export default create({ return { ...toRefs(state), ...toRefs(canvasAll), + showAlipayCanvas2D, highlightStyle, canvasStyle, cutCanvasStyle, diff --git a/src/packages/utils/canvas.ts b/src/packages/utils/canvas.ts index be0fa47e00..fdaf27c918 100644 --- a/src/packages/utils/canvas.ts +++ b/src/packages/utils/canvas.ts @@ -104,4 +104,4 @@ function easySetFillStyle( ); } -export { easySetStrokeStyle, easySetLineWidth, easySetFillStyle }; +export { compareVersion, easySetStrokeStyle, easySetLineWidth, easySetFillStyle }; From 5223b7464e4fd32d75db91c52dffdf6872a73607 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:14:42 +0800 Subject: [PATCH 12/21] =?UTF-8?q?feat:=20vue=E7=89=88=E6=9C=AC=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=8E=B7=E5=8F=96=E5=85=83=E7=B4=A0=E5=AE=BD=E9=AB=98?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/__VUE/avatarcropper/index.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/index.vue b/src/packages/__VUE/avatarcropper/index.vue index 3bbdd7da19..bd2031f181 100644 --- a/src/packages/__VUE/avatarcropper/index.vue +++ b/src/packages/__VUE/avatarcropper/index.vue @@ -9,7 +9,7 @@ @change="inputImageChange" /> -
+
; // canvas const canvasRef = ref() as Ref; // input @@ -195,8 +197,8 @@ export default create({ // 设置绘制图片 const setDrawImg = (image: HTMLImageElement) => { - const nutCutPopup = document.querySelector('.nut-cropper-popup') as HTMLElement; - const { clientWidth, clientHeight } = nutCutPopup; + const rect = useRect(cropperPopupRef.value); + const { width: clientWidth, height: clientHeight } = rect; const canvasWidth = (state.displayWidth = clientWidth * devicePixelRatio); const canvasHeight = (state.displayHeight = clientHeight * devicePixelRatio); @@ -232,10 +234,8 @@ export default create({ const base64 = await fileToDataURL(files[0]); const image = await dataURLToImage(base64); - setTimeout(() => { - setDrawImg(image); - draw(); - }, 200); + setDrawImg(image); + draw(); }; // 重设缩放 @@ -432,6 +432,7 @@ export default create({ return { ...toRefs(state), + cropperPopupRef, canvasRef, inputImageRef, highlightStyle, From b80590f31efd95ff8539751f239f229c6e579438 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:32:13 +0800 Subject: [PATCH 13/21] test: update test --- .../__snapshots__/avatarcropper.spec.ts.snap | 38 +++++++++++++++++++ .../__tests__/avatarcropper.spec.ts | 17 ++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap diff --git a/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap b/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap new file mode 100644 index 0000000000..b62e9fc16d --- /dev/null +++ b/src/packages/__VUE/avatarcropper/__tests__/__snapshots__/avatarcropper.spec.ts.snap @@ -0,0 +1,38 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout default slot 1`] = ` +"
+
+
+
+
+
+
+
+ + + + + 取消 + + +
+
+ +
+
+ +
+
+ + + + + 确定 + + +
+
+
+
" +`; diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index 3de1a7a795..e376ac3dff 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -2,6 +2,7 @@ import 'vitest-canvas-mock'; import { mount } from '@vue/test-utils'; import AvatarCropper from '../index.vue'; import { sleep } from '@/packages/utils/unit'; +import { h } from 'vue'; const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { type: 'test' @@ -10,11 +11,16 @@ const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { test('layout default slot', () => { const wrapper = mount(AvatarCropper, { slots: { - default: 'Main Content' + default: h('img', { + src: 'https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png' + }) } }); - expect(wrapper.find('.nut-avatar-cropper').html()).toContain('Main Content'); + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find('.nut-avatar-cropper').html()).toContain( + '' + ); }); test('should render base cutAvatar and type', async () => { @@ -33,11 +39,10 @@ test('AvatarCropper: Select the image to open the crop window', async () => { Object.defineProperty(input.element, 'files', { get: vi.fn().mockReturnValue([mockFile, smallFile]) }); - expect(wrapper.find('.nut-cropper-popup').exists()).toBe(false); + expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;'); await input.trigger('change'); - expect(wrapper.emitted('change')).toBeTruthy(); await sleep(); - expect(wrapper.find('.nut-cropper-popup').exists()).toBe(true); + expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', ''); const canvas = wrapper.find('.nut-cropper-popup__canvas'); expect(canvas.exists()).toBe(true); @@ -49,7 +54,7 @@ test('AvatarCropper: Select the image to open the crop window', async () => { expect(wrapper.emitted('cancel')).toBeTruthy(); expect(input.element.value).toBe(''); await sleep(); - expect(wrapper.find('.nut-cropper-popup').exists()).toBe(false); + expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;'); const reset = toolbar[1]; reset.trigger('click'); From 81f084356b49846486f5674ae6c6d19e31cda4df Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:41:20 +0800 Subject: [PATCH 14/21] test: update test avatarcropper toouch --- .../__tests__/avatarcropper.spec.ts | 7 ++ src/packages/utils/test/event.ts | 65 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/packages/utils/test/event.ts diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index e376ac3dff..780e4397ca 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -2,6 +2,7 @@ import 'vitest-canvas-mock'; import { mount } from '@vue/test-utils'; import AvatarCropper from '../index.vue'; import { sleep } from '@/packages/utils/unit'; +import { trigger, triggerDrag } from '@/packages/utils/test/event'; import { h } from 'vue'; const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { @@ -46,6 +47,12 @@ test('AvatarCropper: Select the image to open the crop window', async () => { const canvas = wrapper.find('.nut-cropper-popup__canvas'); expect(canvas.exists()).toBe(true); + const track = wrapper.find('.nut-cropper-popup__highlight'); + + trigger(track, 'touchstart', 0, 0); + trigger(track, 'touchmove', 0, 20); + trigger(track, 'touchend', 0, 100); + triggerDrag(track, 0, 100); const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item'); expect(toolbar.length).toBe(4); diff --git a/src/packages/utils/test/event.ts b/src/packages/utils/test/event.ts new file mode 100644 index 0000000000..10dbfeb1ca --- /dev/null +++ b/src/packages/utils/test/event.ts @@ -0,0 +1,65 @@ +function getTouch(el: HTMLElement | Window, x: number, y: number) { + return { + identifier: Date.now(), + target: el, + pageX: x, + pageY: y, + clientX: x, + clientY: y, + radiusX: 2.5, + radiusY: 2.5, + rotationAngle: 10, + force: 0.5 + }; +} + +// Trigger pointer/touch event +export function trigger(wrapper: any, eventName: string, x = 0, y = 0, options: any = {}) { + const el = 'element' in wrapper ? wrapper.element : wrapper; + const touchList = options.touchList || [getTouch(el, x, y)]; + + if (options.x || options.y) { + touchList.push(getTouch(el, options.x, options.y)); + } + + const event = document.createEvent('CustomEvent'); + event.initCustomEvent(eventName, true, true, {}); + + Object.assign(event, { + clientX: x, + clientY: y, + touches: touchList, + targetTouches: touchList, + changedTouches: touchList + }); + + el.dispatchEvent(event); +} + +export function sleep(delay = 0): Promise { + return new Promise((resolve) => { + setTimeout(resolve, delay); + }); +} + +// simulate drag gesture +export function triggerDrag(el: any, relativeX = 0, relativeY = 0): void { + let x = relativeX; + let y = relativeY; + let startX = 0; + let startY = 0; + if (relativeX < 0) { + startX = Math.abs(relativeX); + x = 0; + } + if (relativeY < 0) { + startY = Math.abs(relativeY); + y = 0; + } + trigger(el, 'touchstart', startX, startY); + trigger(el, 'touchmove', x / 4, y / 4); + trigger(el, 'touchmove', x / 3, y / 3); + trigger(el, 'touchmove', x / 2, y / 2); + trigger(el, 'touchmove', x, y); + trigger(el, 'touchend', x, y); +} From 858911e37301961c3ad1333c887d439db6cf79d2 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:51:32 +0800 Subject: [PATCH 15/21] Revert "test: update test avatarcropper toouch" This reverts commit 81f084356b49846486f5674ae6c6d19e31cda4df. --- .../__tests__/avatarcropper.spec.ts | 7 -- src/packages/utils/test/event.ts | 65 ------------------- 2 files changed, 72 deletions(-) delete mode 100644 src/packages/utils/test/event.ts diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index 780e4397ca..e376ac3dff 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -2,7 +2,6 @@ import 'vitest-canvas-mock'; import { mount } from '@vue/test-utils'; import AvatarCropper from '../index.vue'; import { sleep } from '@/packages/utils/unit'; -import { trigger, triggerDrag } from '@/packages/utils/test/event'; import { h } from 'vue'; const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { @@ -47,12 +46,6 @@ test('AvatarCropper: Select the image to open the crop window', async () => { const canvas = wrapper.find('.nut-cropper-popup__canvas'); expect(canvas.exists()).toBe(true); - const track = wrapper.find('.nut-cropper-popup__highlight'); - - trigger(track, 'touchstart', 0, 0); - trigger(track, 'touchmove', 0, 20); - trigger(track, 'touchend', 0, 100); - triggerDrag(track, 0, 100); const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item'); expect(toolbar.length).toBe(4); diff --git a/src/packages/utils/test/event.ts b/src/packages/utils/test/event.ts deleted file mode 100644 index 10dbfeb1ca..0000000000 --- a/src/packages/utils/test/event.ts +++ /dev/null @@ -1,65 +0,0 @@ -function getTouch(el: HTMLElement | Window, x: number, y: number) { - return { - identifier: Date.now(), - target: el, - pageX: x, - pageY: y, - clientX: x, - clientY: y, - radiusX: 2.5, - radiusY: 2.5, - rotationAngle: 10, - force: 0.5 - }; -} - -// Trigger pointer/touch event -export function trigger(wrapper: any, eventName: string, x = 0, y = 0, options: any = {}) { - const el = 'element' in wrapper ? wrapper.element : wrapper; - const touchList = options.touchList || [getTouch(el, x, y)]; - - if (options.x || options.y) { - touchList.push(getTouch(el, options.x, options.y)); - } - - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(eventName, true, true, {}); - - Object.assign(event, { - clientX: x, - clientY: y, - touches: touchList, - targetTouches: touchList, - changedTouches: touchList - }); - - el.dispatchEvent(event); -} - -export function sleep(delay = 0): Promise { - return new Promise((resolve) => { - setTimeout(resolve, delay); - }); -} - -// simulate drag gesture -export function triggerDrag(el: any, relativeX = 0, relativeY = 0): void { - let x = relativeX; - let y = relativeY; - let startX = 0; - let startY = 0; - if (relativeX < 0) { - startX = Math.abs(relativeX); - x = 0; - } - if (relativeY < 0) { - startY = Math.abs(relativeY); - y = 0; - } - trigger(el, 'touchstart', startX, startY); - trigger(el, 'touchmove', x / 4, y / 4); - trigger(el, 'touchmove', x / 3, y / 3); - trigger(el, 'touchmove', x / 2, y / 2); - trigger(el, 'touchmove', x, y); - trigger(el, 'touchend', x, y); -} From 86afda9ab6d1b48e7ed50ceceb4b01c6079cc7b9 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:53:24 +0800 Subject: [PATCH 16/21] test: update test avatarcropper toouch --- .../__VUE/avatarcropper/__tests__/avatarcropper.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index e376ac3dff..ebef0ab03c 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -1,7 +1,7 @@ import 'vitest-canvas-mock'; import { mount } from '@vue/test-utils'; import AvatarCropper from '../index.vue'; -import { sleep } from '@/packages/utils/unit'; +import { sleep, trigger, triggerDrag } from '@/packages/utils/unit'; import { h } from 'vue'; const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { @@ -46,6 +46,13 @@ test('AvatarCropper: Select the image to open the crop window', async () => { const canvas = wrapper.find('.nut-cropper-popup__canvas'); expect(canvas.exists()).toBe(true); + const track = wrapper.find('.nut-cropper-popup__highlight'); + + trigger(track, 'touchstart', 0, 0); + trigger(track, 'touchmove', 0, 20); + trigger(track, 'touchend', 0, 100); + triggerDrag(track, 0, 100); + const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item'); expect(toolbar.length).toBe(4); From 2c6e7122ee459ab298f8b7926de19845a5f7d477 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 00:54:31 +0800 Subject: [PATCH 17/21] test: touch x --- .../__VUE/avatarcropper/__tests__/avatarcropper.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index ebef0ab03c..46836ac2c6 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -50,8 +50,9 @@ test('AvatarCropper: Select the image to open the crop window', async () => { trigger(track, 'touchstart', 0, 0); trigger(track, 'touchmove', 0, 20); - trigger(track, 'touchend', 0, 100); - triggerDrag(track, 0, 100); + trigger(track, 'touchmove', 20, 20); + trigger(track, 'touchend', 20, 100); + triggerDrag(track, 20, 100); const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item'); expect(toolbar.length).toBe(4); From 3301a287b7239d58acfee7ace211e828d394fc78 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 01:06:44 +0800 Subject: [PATCH 18/21] test: update touch --- .../avatarcropper/__tests__/avatarcropper.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index 46836ac2c6..9a0de550f3 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -48,12 +48,11 @@ test('AvatarCropper: Select the image to open the crop window', async () => { const track = wrapper.find('.nut-cropper-popup__highlight'); - trigger(track, 'touchstart', 0, 0); - trigger(track, 'touchmove', 0, 20); - trigger(track, 'touchmove', 20, 20); - trigger(track, 'touchend', 20, 100); - triggerDrag(track, 20, 100); + trigger(track, 'touchstart', 0, 0, { x: 0, y: 0 }); + trigger(track, 'touchmove', 20, 20, { x: 40, y: 60 }); + trigger(track, 'touchend', 20, 100, { x: 40, y: 60 }); + triggerDrag(track, 50, 60); const toolbar = wrapper.findAll('.nut-cropper-popup__toolbar-item'); expect(toolbar.length).toBe(4); @@ -71,6 +70,7 @@ test('AvatarCropper: Select the image to open the crop window', async () => { const rotate = toolbar[2]; rotate.trigger('click'); expect(wrapper.vm.angle).toBe(90); + triggerDrag(track, 1000, 2000); rotate.trigger('click'); expect(wrapper.vm.angle).toBe(180); rotate.trigger('click'); From ef51d8ea2ae02deb2edb79a090fdf8e45fdd4027 Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 01:14:09 +0800 Subject: [PATCH 19/21] test: update test image --- .../__VUE/avatarcropper/__tests__/avatarcropper.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index 9a0de550f3..3c5e7155e0 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -4,9 +4,7 @@ import AvatarCropper from '../index.vue'; import { sleep, trigger, triggerDrag } from '@/packages/utils/unit'; import { h } from 'vue'; -const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { - type: 'test' -}); +const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { type: 'image/ipg' }); test('layout default slot', () => { const wrapper = mount(AvatarCropper, { @@ -40,7 +38,7 @@ test('AvatarCropper: Select the image to open the crop window', async () => { get: vi.fn().mockReturnValue([mockFile, smallFile]) }); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;'); - await input.trigger('change'); + await input.trigger('change', { target: { files: [mockFile] } }); await sleep(); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', ''); const canvas = wrapper.find('.nut-cropper-popup__canvas'); From b70434924901395944796e67c37ce866ff88466a Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 01:25:24 +0800 Subject: [PATCH 20/21] test: update input set value --- .../__VUE/avatarcropper/__tests__/avatarcropper.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index 3c5e7155e0..d756cf77c3 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -38,7 +38,8 @@ test('AvatarCropper: Select the image to open the crop window', async () => { get: vi.fn().mockReturnValue([mockFile, smallFile]) }); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;'); - await input.trigger('change', { target: { files: [mockFile] } }); + input.elemnt.value = vi.fn().mockReturnValue([mockFile, smallFile]); + await input.trigger('change'); await sleep(); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', ''); const canvas = wrapper.find('.nut-cropper-popup__canvas'); From 991a6ead7f2abdaf50b9ed1c1adc146a772e983f Mon Sep 17 00:00:00 2001 From: Marvin <454846659@qq.com> Date: Sun, 1 Oct 2023 01:32:02 +0800 Subject: [PATCH 21/21] Revert "test: update test image" This reverts commit ef51d8ea2ae02deb2edb79a090fdf8e45fdd4027. --- .../__VUE/avatarcropper/__tests__/avatarcropper.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts index d756cf77c3..e61bfdc078 100644 --- a/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts +++ b/src/packages/__VUE/avatarcropper/__tests__/avatarcropper.spec.ts @@ -4,7 +4,9 @@ import AvatarCropper from '../index.vue'; import { sleep, trigger, triggerDrag } from '@/packages/utils/unit'; import { h } from 'vue'; -const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { type: 'image/ipg' }); +const mockFile = new File([new ArrayBuffer(10000)], 'test.jpg', { + type: 'image/jpg' +}); test('layout default slot', () => { const wrapper = mount(AvatarCropper, { @@ -38,7 +40,6 @@ test('AvatarCropper: Select the image to open the crop window', async () => { get: vi.fn().mockReturnValue([mockFile, smallFile]) }); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', 'display: none;'); - input.elemnt.value = vi.fn().mockReturnValue([mockFile, smallFile]); await input.trigger('change'); await sleep(); expect(wrapper.find('.nut-cropper-popup').attributes()).toHaveProperty('style', '');