From 9ca93eaa40426d8a62d1477c6c5372598b14769f Mon Sep 17 00:00:00 2001 From: SunLn Date: Thu, 27 Aug 2020 21:22:32 +0800 Subject: [PATCH] feat: add image component (#124) --- packages/avatar/__tests__/avatar.spec.ts | 1 + packages/element-plus/index.ts | 3 + packages/element-plus/package.json | 1 + packages/image/__tests__/image.spec.ts | 123 ++++++++ packages/image/doc/basic.vue | 40 +++ packages/image/doc/bigImage.vue | 26 ++ packages/image/doc/error.vue | 54 ++++ packages/image/doc/index.stories.ts | 10 + packages/image/doc/lazyload.vue | 44 +++ packages/image/doc/placeholder.vue | 62 ++++ packages/image/index.ts | 5 + packages/image/package.json | 12 + packages/image/src/image-viewer.vue | 357 +++++++++++++++++++++++ packages/image/src/index.vue | 277 ++++++++++++++++++ packages/utils/dom.ts | 13 +- 15 files changed, 1021 insertions(+), 7 deletions(-) create mode 100644 packages/image/__tests__/image.spec.ts create mode 100644 packages/image/doc/basic.vue create mode 100644 packages/image/doc/bigImage.vue create mode 100644 packages/image/doc/error.vue create mode 100644 packages/image/doc/index.stories.ts create mode 100644 packages/image/doc/lazyload.vue create mode 100644 packages/image/doc/placeholder.vue create mode 100644 packages/image/index.ts create mode 100644 packages/image/package.json create mode 100644 packages/image/src/image-viewer.vue create mode 100644 packages/image/src/index.vue diff --git a/packages/avatar/__tests__/avatar.spec.ts b/packages/avatar/__tests__/avatar.spec.ts index 341395485d..c014f007bc 100644 --- a/packages/avatar/__tests__/avatar.spec.ts +++ b/packages/avatar/__tests__/avatar.spec.ts @@ -78,3 +78,4 @@ describe('Avatar.vue', () => { } }) }) + diff --git a/packages/element-plus/index.ts b/packages/element-plus/index.ts index 0e4eb17e99..cff7533fbe 100644 --- a/packages/element-plus/index.ts +++ b/packages/element-plus/index.ts @@ -13,6 +13,7 @@ import ElTimeline from '@element-plus/timeline' import ElProgress from '@element-plus/progress' import ElBreadcrumb from '@element-plus/breadcrumb' import ElIcon from '@element-plus/icon' +import ElImage from '@element-plus/image' import ElLink from '@element-plus/link' import ElRate from '@element-plus/rate' import ElSwitch from '@element-plus/switch' @@ -42,6 +43,7 @@ export { ElProgress, ElBreadcrumb, ElIcon, + ElImage, ElLink, ElRate, ElSwitch, @@ -71,6 +73,7 @@ export default function install(app: App): void { ElProgress(app) ElBreadcrumb(app) ElIcon(app) + ElImage(app) ElLink(app) ElRate(app) ElSwitch(app) diff --git a/packages/element-plus/package.json b/packages/element-plus/package.json index 216a24d1e9..9844113910 100644 --- a/packages/element-plus/package.json +++ b/packages/element-plus/package.json @@ -28,6 +28,7 @@ "@element-plus/link": "^0.0.0", "@element-plus/progress": "^0.0.0", "@element-plus/rate": "^0.0.0", + "@element-plus/image": "^0.0.0", "@element-plus/switch": "^0.0.0", "@element-plus/radio": "^0.0.0", "@element-plus/page-header": "^0.0.0", diff --git a/packages/image/__tests__/image.spec.ts b/packages/image/__tests__/image.spec.ts new file mode 100644 index 0000000000..f9e86952ed --- /dev/null +++ b/packages/image/__tests__/image.spec.ts @@ -0,0 +1,123 @@ +import { mount } from '@vue/test-utils' +import Image from '../src/index.vue' +import { nextTick } from 'vue' + +import { IMAGE_SUCCESS, IMAGE_FAIL } from '../../test-utils/mock' + + +describe('Image.vue', () => { + + beforeAll(() => { + Object.defineProperty(global.Image.prototype, 'src', { + set(src) { + const type = !src || src === IMAGE_FAIL ? 'error' : 'load' + const event = new Event(type) + this.dispatchEvent(event) + }, + }) + }) + + test('render test', () => { + const wrapper = mount(Image) + expect(wrapper.find('.el-image').exists()).toBe(true) + }) + + test('image load success test', async () => { + const alt = 'this ia alt' + const wrapper = mount(Image, { + props: { + src: IMAGE_SUCCESS, + alt, + }, + }) + expect(wrapper.find('.el-image__placeholder').exists()).toBe(true) + await nextTick() + expect(wrapper.find('.el-image__inner').exists()).toBe(true) + expect(wrapper.find('img').exists()).toBe(true) + expect(wrapper.find('.el-image__placeholder').exists()).toBe(false) + expect(wrapper.find('.el-image__error').exists()).toBe(false) + }) + + test('image load error test', async () => { + const wrapper = mount(Image, { + props: { + src: IMAGE_FAIL, + }, + }) + expect(wrapper.emitted('error')).toBeDefined() + await nextTick() + expect(wrapper.find('.el-image__inner').exists()).toBe(false) + expect(wrapper.find('img').exists()).toBe(false) + expect(wrapper.find('.el-image__error').exists()).toBe(true) + }) + + test('imageStyle fit test', async () => { + const fits = ['fill', 'contain', 'cover', 'none', 'scale-down'] + for (const fit of fits) { + const wrapper = mount(Image, { + props: { fit, src: IMAGE_SUCCESS }, + }) + await nextTick() + expect(wrapper.find('img').attributes('style')).toContain(`object-fit: ${fit};`) + } + }) + + test('preview classname test', async () => { + const wrapper = mount(Image, { + props: { + fit: 'cover', + src: IMAGE_SUCCESS, + previewSrcList: new Array(3).fill(IMAGE_SUCCESS), + }, + }) + await nextTick() + expect(wrapper.find('img').classes()).toContain('el-image__preview') + }) + + + test('$attrs', async () => { + const alt = 'this ia alt' + const wrapper = mount(Image, { + props: { + src: IMAGE_SUCCESS, + alt, + referrerpolicy: 'origin', + }, + }) + await nextTick() + expect(wrapper.find('img').attributes('alt')).toBe(alt) + expect(wrapper.find('img').attributes('referrerpolicy')).toBe('origin') + }) + + test('pass event listeners', async() => { + let result = false + const wrapper = mount(Image, { + props: { + src: IMAGE_SUCCESS, + onClick: () => result = true, + }, + }) + await nextTick() + const inner = wrapper.find('.el-image__inner').element as HTMLElement + inner.click() + expect(result).toBeTruthy() + }) + + //@todo lazy image test + + test('big image preview', async() => { + const wrapper = mount(Image, { + props: { + src: IMAGE_SUCCESS, + previewSrcList: [IMAGE_SUCCESS], + }, + }) + await nextTick() + expect(wrapper.find('.el-image__inner').exists()).toBe(true) + await wrapper.find('.el-image__inner').trigger('click') + const viewer = wrapper.find('.el-image-viewer__wrapper') + expect(viewer.exists()).toBe(true) + await wrapper.find('.el-image-viewer__close').trigger('click') + expect(wrapper.find('.el-image-viewer__wrapper').exists()).toBe(false) + }) +}) diff --git a/packages/image/doc/basic.vue b/packages/image/doc/basic.vue new file mode 100644 index 0000000000..2957c1988a --- /dev/null +++ b/packages/image/doc/basic.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/packages/image/doc/bigImage.vue b/packages/image/doc/bigImage.vue new file mode 100644 index 0000000000..4b4076aa8d --- /dev/null +++ b/packages/image/doc/bigImage.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/image/doc/error.vue b/packages/image/doc/error.vue new file mode 100644 index 0000000000..7de8075a49 --- /dev/null +++ b/packages/image/doc/error.vue @@ -0,0 +1,54 @@ + + + + diff --git a/packages/image/doc/index.stories.ts b/packages/image/doc/index.stories.ts new file mode 100644 index 0000000000..31c8439485 --- /dev/null +++ b/packages/image/doc/index.stories.ts @@ -0,0 +1,10 @@ +export { default as BasicUsage } from './basic.vue' +export { default as LoadError } from './error.vue' +export { default as LoadPlaceholder } from './placeholder.vue' +export { default as LazyLoad } from './lazyload.vue' +export { default as BigImage } from './bigImage.vue' + +export default { + title: 'Image', +} + diff --git a/packages/image/doc/lazyload.vue b/packages/image/doc/lazyload.vue new file mode 100644 index 0000000000..87080831dc --- /dev/null +++ b/packages/image/doc/lazyload.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/image/doc/placeholder.vue b/packages/image/doc/placeholder.vue new file mode 100644 index 0000000000..3cd2cd638b --- /dev/null +++ b/packages/image/doc/placeholder.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/image/index.ts b/packages/image/index.ts new file mode 100644 index 0000000000..0ac362fee6 --- /dev/null +++ b/packages/image/index.ts @@ -0,0 +1,5 @@ +import { App } from 'vue' +import Image from './src/index.vue' +export default (app: App): void => { + app.component(Image.name, Image) +} diff --git a/packages/image/package.json b/packages/image/package.json new file mode 100644 index 0000000000..d732a39a03 --- /dev/null +++ b/packages/image/package.json @@ -0,0 +1,12 @@ +{ + "name": "@element-plus/image", + "version": "0.0.0", + "main": "dist/index.js", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0-rc.1" + }, + "devDependencies": { + "@vue/test-utils": "^2.0.0-beta.0" + } +} diff --git a/packages/image/src/image-viewer.vue b/packages/image/src/image-viewer.vue new file mode 100644 index 0000000000..c61acff22a --- /dev/null +++ b/packages/image/src/image-viewer.vue @@ -0,0 +1,357 @@ + + + diff --git a/packages/image/src/index.vue b/packages/image/src/index.vue new file mode 100644 index 0000000000..a531c92bd8 --- /dev/null +++ b/packages/image/src/index.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/packages/utils/dom.ts b/packages/utils/dom.ts index 27046fa0c8..61e9ac5324 100644 --- a/packages/utils/dom.ts +++ b/packages/utils/dom.ts @@ -113,8 +113,10 @@ export const getStyle = function( styleName = 'cssFloat' } try { + const style = element.style[styleName] + if (style) return style const computed = document.defaultView.getComputedStyle(element, '') - return element.style[styleName] || computed ? computed[styleName] : null + return computed ? computed[styleName] : '' } catch (e) { return element.style[styleName] } @@ -146,13 +148,12 @@ export const isScroll = ( isVertical?: Nullable, ): RegExpMatchArray => { if (isServer) return - - const determinedDirection = isVertical !== null || isVertical !== undefined + const determinedDirection = isVertical === null || isVertical === undefined const overflow = determinedDirection - ? isVertical + ? getStyle(el, 'overflow') + : isVertical ? getStyle(el, 'overflow-y') : getStyle(el, 'overflow-x') - : getStyle(el, 'overflow') return overflow.match(/(scroll|auto)/) } @@ -173,7 +174,6 @@ export const getScrollContainer = ( } parent = parent.parentNode as HTMLElement } - return parent } @@ -200,7 +200,6 @@ export const isInContainer = ( } else { containerRect = container.getBoundingClientRect() } - return ( elRect.top < containerRect.bottom && elRect.bottom > containerRect.top &&