From 61e4ef3a0589b6872f6d77272005e23e3a29844f Mon Sep 17 00:00:00 2001 From: MrWook Date: Fri, 9 Oct 2020 10:02:24 +0200 Subject: [PATCH 1/4] feat(datepicker): add popup to append calendar to body --- docs/guide/Props/README.md | 1 + docs/guide/README.md | 2 +- example/Demo.vue | 53 +++++- src/components/Datepicker.vue | 163 +++++++----------- src/components/Popup.vue | 95 ++++++++++ src/utils/calendarSlots.js | 10 ++ src/utils/dom.js | 124 +++++++++++++ test/unit/specs/Datepicker/Datepicker.spec.js | 34 +++- .../Datepicker/setPickerPosition.spec.js | 96 +++++++---- 9 files changed, 437 insertions(+), 141 deletions(-) create mode 100644 src/components/Popup.vue create mode 100644 src/utils/calendarSlots.js create mode 100644 src/utils/dom.js diff --git a/docs/guide/Props/README.md b/docs/guide/Props/README.md index 4c7031c5..ab4f6067 100755 --- a/docs/guide/Props/README.md +++ b/docs/guide/Props/README.md @@ -3,6 +3,7 @@ | Prop | Type | Default | Description | | ----------------------------- | -----------------| ----------- | ----------------------------------------------- | +| append-to-body | Boolean | false | Append datepicker calendar to body | | autofocus | String | | Sets html `autofocus` attribute on input | | bootstrap-styling | Boolean | false | Use bootstrap v4 styling classes. | | calendar-button | Boolean | false | Show an icon that that can be clicked | diff --git a/docs/guide/README.md b/docs/guide/README.md index be7e9f2e..1f4ea15f 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -55,6 +55,6 @@ If you use [SASS](https://sass-lang.com/) you can directly import the src file. ```vue ``` diff --git a/example/Demo.vue b/example/Demo.vue index 9d5d4cbd..bdb83a78 100755 --- a/example/Demo.vue +++ b/example/Demo.vue @@ -3,7 +3,10 @@

Datepicker Examples

Default datepicker...

- + <datepicker placeholder="Select Date"></datepicker> @@ -37,6 +40,7 @@ <datepicker placeholder="Select Date" v-model="vmodelexample"></datepicker> @@ -45,6 +49,18 @@

{{ vModelExample }}

+
+

Append datepicker to body

+ +

Don't append datepicker to body

+ + + <datepicker :append-to-body="true"></datepicker> + +
+

Format datepicker

@@ -294,6 +310,29 @@ :initialView="'year'"></datepicker>
+ +
+

Fixed positions

+ + + <datepicker :fixed-position="fixedPosition"></datepicker> + +
+
Settings
+ +
+
@@ -369,6 +408,15 @@ export default { vModelExample: null, languages: lang, language: 'en', + fixedPositions: [ + 'bottom', + 'bottom-left', + 'bottom-right', + 'top', + 'top-left', + 'top-right', + ], + fixedPosition: 'bottom', } }, computed: { @@ -499,4 +547,7 @@ h5 { font-size: 80%; display: block; } +.overflow-scroll { + overflow:scroll +} diff --git a/src/components/Datepicker.vue b/src/components/Datepicker.vue index deade7cc..bc575d38 100644 --- a/src/components/Datepicker.vue +++ b/src/components/Datepicker.vue @@ -49,78 +49,64 @@ /> - - + + diff --git a/src/components/Popup.vue b/src/components/Popup.vue new file mode 100644 index 00000000..b9682b31 --- /dev/null +++ b/src/components/Popup.vue @@ -0,0 +1,95 @@ + diff --git a/src/utils/calendarSlots.js b/src/utils/calendarSlots.js new file mode 100644 index 00000000..0383cf83 --- /dev/null +++ b/src/utils/calendarSlots.js @@ -0,0 +1,10 @@ +export default [ + 'beforeCalendarHeaderDay', + 'calendarFooterDay', + 'beforeCalendarHeaderMonth', + 'calendarFooterMonth', + 'beforeCalendarHeaderYear', + 'calendarFooterYear', + 'nextIntervalBtn', + 'prevIntervalBtn', +] diff --git a/src/utils/dom.js b/src/utils/dom.js new file mode 100644 index 00000000..630ccae2 --- /dev/null +++ b/src/utils/dom.js @@ -0,0 +1,124 @@ +/* eslint no-param-reassign: 0 */ +/** + * get the hidden element width, height + * @param {HTMLElement} element dom + */ +export function getPopupElementSize(element) { + const originalDisplay = element.style.display + const originalVisibility = element.style.visibility + element.style.display = 'block' + element.style.visibility = 'hidden' + const styles = window.getComputedStyle(element) + const width = element.offsetWidth + parseInt(styles.marginLeft, 10) + parseInt( + styles.marginRight, + 10, + ) + const height = element.offsetHeight + parseInt( + styles.marginTop, + 10, + ) + parseInt(styles.marginBottom, 10) + element.style.display = originalDisplay + element.style.visibility = originalVisibility + + return { + width, + height, + } +} + +/** + * get the popup position + * @param {Element} el element + * @param {Element} elRelative relative element + * @param {Number} targetWidth target element's width + * @param {Number} targetHeight target element's height + * @param {Boolean} fixed + * @param {String} fixedPosition + * @param {Boolean} rtl + */ +export function getRelativePosition({ + el, + elRelative, + targetWidth, + targetHeight, + fixed, + fixedPosition, + rtl, +}) { + let left = 0 + let top = 0 + let offsetX = 0 + let offsetY = 0 + const relativeRect = elRelative.getBoundingClientRect() + const documentWidth = document.documentElement.clientWidth + const documentHeight = document.documentElement.clientHeight + if (fixed) { + offsetX = window.pageXOffset + relativeRect.left + offsetY = window.pageYOffset + relativeRect.top + } + + const calendarBounding = el.getBoundingClientRect() + const outOfBoundsRight = calendarBounding.right > window.innerWidth + const outOfBoundsBottom = calendarBounding.bottom > window.innerHeight + + const fixedPositionRight = fixedPosition && fixedPosition.indexOf('right') !== -1 + const fixedPositionTop = fixedPosition && fixedPosition.indexOf('top') !== -1 + + const setLeft = () => { + left = offsetX + } + const setRight = () => { + left = offsetX + relativeRect.width - targetWidth + } + const setBottom = () => { + top = offsetY + relativeRect.height + } + const setTop = () => { + top = offsetY - targetHeight + } + + if (fixedPosition === '') { + if (outOfBoundsRight || rtl) { + setRight() + } else { + setLeft() + } + + if (outOfBoundsBottom) { + setTop() + } else { + setBottom() + } + + const hasRelativWidth = documentWidth - relativeRect.left < targetWidth + && relativeRect.right < targetWidth + + const hasRelativHeight = relativeRect.top <= targetHeight + && documentHeight - relativeRect.bottom <= targetHeight + + if (hasRelativWidth) { + left = offsetX - relativeRect.left + 1 + } + + if (hasRelativHeight) { + top = offsetY + documentHeight - relativeRect.top - targetHeight + } + } else { + if (fixedPositionRight) { + setRight() + } else { + setLeft() + } + + if (fixedPositionTop) { + setTop() + } else { + setBottom() + } + } + + return { + left: `${left}px`, + top: `${top}px`, + } +} diff --git a/test/unit/specs/Datepicker/Datepicker.spec.js b/test/unit/specs/Datepicker/Datepicker.spec.js index 91550c82..cd8f8e04 100755 --- a/test/unit/specs/Datepicker/Datepicker.spec.js +++ b/test/unit/specs/Datepicker/Datepicker.spec.js @@ -317,13 +317,17 @@ describe('Datepicker.vue using UTC', () => { describe('Datepicker.vue inline', () => { let wrapper beforeEach(() => { - wrapper = shallowMount(Datepicker, { + wrapper = mount(Datepicker, { propsData: { inline: true, }, }) }) + afterEach(() => { + wrapper.vm.$destroy() + }) + it('should not showCalendar as already open', () => { expect(wrapper.vm.showCalendar()).toEqual(false) expect(wrapper.vm.isInline).toEqual(true) @@ -369,3 +373,31 @@ describe('Datepicker with initial-view', () => { expect(wrapper.vm.currentPicker).toEqual('PickerYear') }) }) + +describe('Datepicker on body', () => { + let wrapper + it('should append popup to body', async () => { + wrapper = mount(Datepicker, { + propsData: { + appendToBody: true, + }, + }) + wrapper.vm.showCalendar() + await wrapper.vm.$nextTick() + expect(wrapper.vm.$el.querySelector('.vdp-datepicker__calendar')).toBeNull() + expect(document.querySelector('.vdp-datepicker__calendar')).toBeDefined() + wrapper.vm.$destroy() + }) + + it('should remove popup on body on component removal', async () => { + wrapper = mount(Datepicker, { + propsData: { + appendToBody: true, + }, + }) + wrapper.vm.showCalendar() + await wrapper.vm.$nextTick() + wrapper.vm.$destroy() + expect(document.querySelector('.vdp-datepicker__calendar')).toBeNull() + }) +}) diff --git a/test/unit/specs/Datepicker/setPickerPosition.spec.js b/test/unit/specs/Datepicker/setPickerPosition.spec.js index 8a6139f5..0e1c3ee3 100755 --- a/test/unit/specs/Datepicker/setPickerPosition.spec.js +++ b/test/unit/specs/Datepicker/setPickerPosition.spec.js @@ -24,13 +24,15 @@ describe('Datepicker mounted', () => { bottom: 10, height: 10, })) + wrapper.vm.$refs.popup.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.showCalendar() await wrapper.vm.$nextTick() - wrapper.vm.$refs.datepicker.getBoundingClientRect = getBoundingClientRect - wrapper.vm.$refs.datepicker.parentElement.getBoundingClientRect = getBoundingClientRect - const calendar = wrapper.vm.$refs.datepicker - expect(calendar.style.bottom).toBe('') - expect(calendar.style.right).toBe('') + + const calendar = wrapper.vm.$refs.popup + expect(calendar.$el.style.left).toBe('0px') + expect(calendar.$el.style.top).toBe('10px') }) it('everything out of bound', async () => { @@ -38,14 +40,24 @@ describe('Datepicker mounted', () => { right: 2000, bottom: 1000, height: 10, + width: 0, + })) + + global.getComputedStyle = jest.fn(() => ({ + marginLeft: '0', + marginRight: '0', + marginTop: '-10', + marginBottom: '0', })) + wrapper.vm.$refs.popup.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.showCalendar() - wrapper.vm.$refs.datepicker.getBoundingClientRect = getBoundingClientRect - wrapper.vm.$refs.datepicker.parentElement.getBoundingClientRect = getBoundingClientRect await wrapper.vm.$nextTick() - const calendar = wrapper.vm.$refs.datepicker - expect(calendar.style.right).toBe('0px') - expect(calendar.style.bottom).toBe('10px') + + const calendar = wrapper.vm.$refs.popup + expect(calendar.$el.style.left).toBe('0px') + expect(calendar.$el.style.top).toBe('10px') }) it('fixed position top right', async () => { @@ -53,46 +65,64 @@ describe('Datepicker mounted', () => { right: 10, bottom: 10, height: 10, + width: 10, })) + global.getComputedStyle = jest.fn(() => ({ + marginLeft: '0', + marginRight: '0', + marginTop: '0', + marginBottom: '0', + })) + wrapper.vm.$refs.popup.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.$el.getBoundingClientRect = getBoundingClientRect wrapper.setProps({ fixedPosition: 'top-right', }) + wrapper.vm.showCalendar() - wrapper.vm.$refs.datepicker.parentElement.getBoundingClientRect = getBoundingClientRect await wrapper.vm.$nextTick() - const calendar = wrapper.vm.$refs.datepicker - expect(calendar.style.right).toBe('0px') - expect(calendar.style.bottom).toBe('10px') + + const calendar = wrapper.vm.$refs.popup + expect(calendar.$el.style.left).toBe('10px') + expect(calendar.$el.style.top).toBe('0px') }) it('fixed position bottom left', async () => { wrapper.setProps({ fixedPosition: 'bottom-left', }) + wrapper.vm.showCalendar() await wrapper.vm.$nextTick() - const calendar = wrapper.vm.$refs.datepicker - expect(calendar.style.right).toBe('') - expect(calendar.style.bottom).toBe('') - }) - it('without picker', async () => { - wrapper.setData({ - currentPicker: '', - }) - wrapper.vm.setPickerPosition() - await wrapper.vm.$nextTick() + const calendar = wrapper.vm.$refs.popup + expect(calendar.$el.style.left).toBe('0px') + expect(calendar.$el.style.top).toBe('0px') + }) - wrapper.vm.$refs = { - datepicker: { - style: {}, - }, - } + it('should have relative position', async () => { + const getBoundingClientRect = jest.fn(() => ({ + left: 10, + right: 10, + bottom: 10, + height: 10, + width: 10, + top: 10, + })) + global.getComputedStyle = jest.fn(() => ({ + marginLeft: '0', + marginRight: '50', + marginTop: '50', + marginBottom: '0', + })) + wrapper.vm.$refs.popup.$el.getBoundingClientRect = getBoundingClientRect + wrapper.vm.$el.getBoundingClientRect = getBoundingClientRect - wrapper.vm.setPickerPosition() + wrapper.vm.showCalendar() await wrapper.vm.$nextTick() - const calendar = wrapper.vm.$refs.datepicker - expect(calendar.style.right).toBe('unset') - expect(calendar.style.bottom).toBe('unset') + + const calendar = wrapper.vm.$refs.popup + expect(calendar.$el.style.left).toBe('-9px') + expect(calendar.$el.style.top).toBe('-60px') }) }) From 5562400ad431de19fea04e4bac447500f356d8c4 Mon Sep 17 00:00:00 2001 From: MrWook Date: Fri, 9 Oct 2020 10:03:05 +0200 Subject: [PATCH 2/4] test(project): resolve test config --- jest.conf.js | 23 ----------------------- jest.config.js | 23 +++++++++++++++++++++++ package.json | 2 +- test/unit/jest.conf.js | 23 ----------------------- 4 files changed, 24 insertions(+), 47 deletions(-) delete mode 100755 jest.conf.js create mode 100755 jest.config.js delete mode 100755 test/unit/jest.conf.js diff --git a/jest.conf.js b/jest.conf.js deleted file mode 100755 index 56123255..00000000 --- a/jest.conf.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - rootDir: 'src/', - moduleFileExtensions: [ - 'js', - 'json', - 'vue', - ], - moduleNameMapper: { - '^~(.*)$': '/$1', - }, - transform: { - '^.+.js$': 'babel-jest', - '.*.(vue)$': 'vue-jest', - }, - setupFiles: ['/tests/unit/setup'], - coverageDirectory: '/tests/unit/coverage', - collectCoverageFrom: [ - 'src/**/*.{js,vue}', - '!src/locale/translations/**/*.js', - ], - verbose: false, - testURL: 'http://localhost', -} diff --git a/jest.config.js b/jest.config.js new file mode 100755 index 00000000..7f516a88 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + rootDir: './', + moduleFileExtensions: [ + 'js', + 'json', + 'vue', + ], + moduleNameMapper: { + '^~(.*)$': '/src/$1', + }, + transform: { + '^.+.js$': 'babel-jest', + '^.+.vue': 'vue-jest', + }, + setupFiles: ['/test/unit/setup.js'], + coverageDirectory: '/test/unit/coverage', + collectCoverageFrom: [ + './src/**/*.{js,vue}', + '!./src/locale/translations/**/*.js', + ], + verbose: false, + testURL: 'http://localhost', +} diff --git a/package.json b/package.json index c6bb8e3a..cf74cc65 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "audit": "npm audit --registry=https://registry.npmjs.org", "serve": "rollup -c scripts/serve.js --watch", "lint": "eslint --ext .js,.vue src test/unit/specs", - "test": "jest --config test/unit/jest.conf.js --coverage", + "test": "jest --coverage", "release": "standard-version", "prerelease": "standard-version --dry-run", "docs:dev": "vuepress dev docs", diff --git a/test/unit/jest.conf.js b/test/unit/jest.conf.js deleted file mode 100755 index 044466c4..00000000 --- a/test/unit/jest.conf.js +++ /dev/null @@ -1,23 +0,0 @@ -const path = require('path') - -module.exports = { - rootDir: path.resolve(__dirname, '../../'), - moduleFileExtensions: [ - 'js', - 'json', - 'vue', - ], - moduleNameMapper: { - '^~/(.*)$': '/src/$1', - }, - transform: { - '^.+\\.js$': '/node_modules/babel-jest', - '.*\\.(vue)$': '/node_modules/vue-jest', - }, - setupFiles: ['/test/unit/setup'], - coverageDirectory: '/test/unit/coverage', - collectCoverageFrom: [ - 'src/**/*.{js,vue}', - '!src/locale/translations/**/*.js', - ], -} From d147b238dcbfebfcb2bdfcdedee58fa124a153a4 Mon Sep 17 00:00:00 2001 From: MrWook Date: Fri, 9 Oct 2020 10:08:53 +0200 Subject: [PATCH 3/4] chore(styles): adjust coding style --- docs/.vuepress/components/style.css | 4 ++++ example/Demo.vue | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/.vuepress/components/style.css b/docs/.vuepress/components/style.css index df8d89d8..bff1bae9 100644 --- a/docs/.vuepress/components/style.css +++ b/docs/.vuepress/components/style.css @@ -38,3 +38,7 @@ .error { color: red; } + +.overflow-scroll { + overflow: scroll +} diff --git a/example/Demo.vue b/example/Demo.vue index bdb83a78..30676175 100755 --- a/example/Demo.vue +++ b/example/Demo.vue @@ -547,7 +547,8 @@ h5 { font-size: 80%; display: block; } + .overflow-scroll { - overflow:scroll + overflow: scroll } From 2b7feaca69853d716a8fcf593f5460e00d519c24 Mon Sep 17 00:00:00 2001 From: MrWook Date: Fri, 16 Oct 2020 14:13:28 +0200 Subject: [PATCH 4/4] refactor(project): order props alphabetic --- src/components/Datepicker.vue | 4 ++-- src/components/Popup.vue | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/Datepicker.vue b/src/components/Datepicker.vue index 66117c8a..f32c3f0c 100644 --- a/src/components/Datepicker.vue +++ b/src/components/Datepicker.vue @@ -52,10 +52,10 @@