Skip to content

Commit

Permalink
feat: improve viewport scrolling (#24)
Browse files Browse the repository at this point in the history
* feat: improve scrolling

* refactor: gather all scroll logic into renderer comoponent

* chore: fix typo

* refactor: remove useless code

* test: add renderer spec
  • Loading branch information
ktsn committed Apr 18, 2018
1 parent b73c909 commit b99f861
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 14 deletions.
117 changes: 103 additions & 14 deletions src/view/components/Renderer.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<template>
<div class="renderer" @click="$emit('select')">
<Viewport :width="width" :height="height" @resize="$emit('resize', arguments[0])">
<VueComponent
:uri="document.uri"
:template="document.template"
:props="document.props"
:data="document.data"
:child-components="document.childComponents"
:styles="document.styleCode"
@select="$emit('select', arguments[0])"
@dragover="$emit('dragover', arguments[0])"
@add="$emit('add')"
/>
</Viewport>
<div class="renderer-scroll-content" :style="scrollContentStyle">
<Viewport :width="width" :height="height" @resize="$emit('resize', arguments[0])">
<VueComponent
:uri="document.uri"
:template="document.template"
:props="document.props"
:data="document.data"
:child-components="document.childComponents"
:styles="document.styleCode"
@select="$emit('select', arguments[0])"
@dragover="$emit('dragover', arguments[0])"
@add="$emit('add')"
/>
</Viewport>
</div>
</div>
</template>

Expand All @@ -22,6 +24,8 @@ import Viewport from './Viewport.vue'
import VueComponent from './VueComponent.vue'
import { ScopedDocument } from '../store/modules/project'
const scrollContentPadding = 100
export default Vue.extend({
name: 'Renderer',
Expand All @@ -43,6 +47,88 @@ export default Vue.extend({
type: Number,
required: true
}
},
data() {
return {
rendererSize: {
width: 0,
height: 0
}
}
},
computed: {
scrollContentSize(): { width: number; height: number } {
const renderer = this.rendererSize
const thresholdWidth = Math.max(0, renderer.width - scrollContentPadding)
const thresholdHeight = Math.max(
0,
renderer.height - scrollContentPadding
)
// If the viewport size is enough smaller than renderer size,
// the scroll content size is the same as the renderer size so that the viewport will not be scrollable.
// Otherwise, the scroll content size will be much lager than renderer size to allow scrolling.
// This is similar behavior with Photoshop.
return {
width:
thresholdWidth > this.width
? renderer.width
: this.width + thresholdWidth * 2,
height:
thresholdHeight > this.height
? renderer.height
: this.height + thresholdHeight * 2
}
},
scrollContentStyle(): Record<string, string> {
const { scrollContentSize: size } = this
return {
width: size.width + 'px',
height: size.height + 'px'
}
},
scrollContentCenter(): { x: number; y: number } {
const { scrollContentSize: size } = this
return {
x: size.width / 2,
y: size.height / 2
}
}
},
watch: {
scrollContentCenter(
{ x, y }: { x: number; y: number },
{ x: prevX, y: prevY }: { x: number; y: number }
): void {
const el = this.$el
// Adjust scroll offset after DOM is rerendered
// to avoid flickering viewport
requestAnimationFrame(() => {
el.scrollLeft = el.scrollLeft + (x - prevX)
el.scrollTop = el.scrollTop + (y - prevY)
})
}
},
mounted() {
const listener = () => {
const el = this.$el
const { width, height } = el.getBoundingClientRect()
this.rendererSize.width = width
this.rendererSize.height = height
}
window.addEventListener('resize', listener)
listener()
this.$once('hook:beforeDestroy', () => {
window.removeEventListener('resize', listener)
})
}
})
</script>
Expand All @@ -52,8 +138,11 @@ export default Vue.extend({
all: initial;
overflow: auto;
display: block;
position: relative;
height: 100%;
width: 100%;
}
.renderer-scroll-content {
position: relative;
}
</style>
93 changes: 93 additions & 0 deletions test/view/Renderer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { shallow } from '@vue/test-utils'
import Renderer from '@/view/components/Renderer.vue'

describe('Renderer', () => {
let mockWidth = 1000
let mockHeight = 1000

function mockGetBoundingClientRect() {
return {
x: 0,
y: 0,
bottom: 0,
left: 0,
top: 0,
right: 0,
width: mockWidth,
height: mockHeight
}
}

beforeAll(() => {
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect
})

beforeEach(() => {
mockWidth = 1000
mockHeight = 1000
})

afterAll(() => {
delete Element.prototype.getBoundingClientRect
})

it('scroll content has the same size with renderer when the viewport is not over the renderer size', () => {
const wrapper = shallow<any>(Renderer, {
propsData: {
document: {},
width: 800,
height: 600
}
})

const size = wrapper.vm.scrollContentSize
expect(size.width).toBe(1000)
expect(size.height).toBe(1000)
})

it('scroll content has the much larser size than renderer when the viewport is over the renderer size', () => {
const wrapper = shallow<any>(Renderer, {
propsData: {
document: {},
width: 800,
height: 1200
}
})

const size = wrapper.vm.scrollContentSize
expect(size.width).toBe(1000) // width is not changed since is smaller than renderer width
expect(size.height).toBe(3000)
})

it('retain current position when the scroll content size is changed', async () => {
const wrapper = shallow<any>(Renderer, {
propsData: {
document: {},
width: 800,
height: 600
}
})

// Waiting for mount
await nextFrame()

const el = wrapper.element
el.scrollTop = 0
el.scrollLeft = 0

// This let the scroll content size be from 1000x1000 to 3000x3000
wrapper.setProps({
width: 1200,
height: 1200
})

await nextFrame()

expect(el.scrollTop).toBe(1000)
expect(el.scrollLeft).toBe(1000)
})
})

function nextFrame() {
return new Promise(requestAnimationFrame)
}

0 comments on commit b99f861

Please sign in to comment.