preface
Manually package a Split component similar to the one in Iview to Split a region into two parts that can be dragged to adjust the width or height. The result is as follows:
start
Basic layout
Create SplitPane components in the VUE project to introduce page usage.
<template>
<div class="page">
<SplitPane />
</div>
</template>
<script>
import SplitPane from './components/split-pane'
export default {
components: {
SplitPane
},
data() {
return{}}}</script>
<style scoped lang="scss">
.page {
height: 100%;
padding: 10px;
background: # 000;
}
</style>
Copy the code
// split-pane.vue
<template>
<div class="split-pane">
split
</div>
</template>
<script>
export default {
data() {
return{}}}</script>
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
}
</style>
Copy the code
The SplitPane component consists of three parts: zone 1, zone 2, and a slider. Add these three elements and each class name, noting that pane is shared by zone 1 and zone 2.
<template>
<div class="split-pane">
<div class="pane pane-one"></div>
<div class="pane-trigger"></div>
<div class="pane pane-two"></div>
</div>
</template>
Copy the code
Set the container to the Flex layout and the Flex property of zone 2 to 1, and zone 2 ADAPTS to the width of zone 1.
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
.pane-one {
width: 50%;
background: palevioletred;
}
.pane-trigger {
width: 10px;
height: 100%;
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise;
}
}
</style>
Copy the code
You can see that setting the width change for region 1 is the core point to implement this component.
Support vertical layout in addition to landscape, so add a direction attribute to the component that is passed in from the outside with a value of row or column bound to the parent element’s Flex-direction attribute.
<template>
<div class="split-pane" :style="{ flexDirection: direction }">
<div class="pane pane-one"></div>
<div class="pane-trigger"></div>
<div class="pane pane-two"></div>
</div>
</template>
<script>
export default {
props: {
direction: {
type: String.default: 'row'}},data() {
return{}}}</script>
Copy the code
In the landscape layout, set width to 50% for region 1 and width to 10px for the slider, while in the portrait layout, both widths should change to height. So remove these two width Settings from the style and add a lengthType calculation attribute that sets the width and height of each element in the inline style depending on the direction.
<template>
<div class="split-pane" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':50%'"></div>
<div class="pane-trigger" :style="lengthType + ':10px'"></div>
<div class="pane pane-two"></div>
</div>
</template>
Copy the code
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'}}Copy the code
Also, in landscape layout, zone 1, Zone 2, and slider height should be 100%, and in vertical layout should be changed to Width: 100%. So remove the original height setting, bind direction to a class of the container, and use that class to set the attributes of the three child elements to 100% in both cases.
<template>
<div class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':50%'"></div>
<div class="pane-trigger" :style="lengthType + ':10px'"></div>
<div class="pane pane-two"></div>
</div>
</template>
<script>
export default {
props: {
direction: {
type: String.default: 'row'}},data() {
return{}},computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'}}}</script>
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
height: 100%;
}
.pane-trigger {
height: 100%; }} &.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%; }}.pane-one {
background: palevioletred;
}
.pane-trigger {
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise; }}</style>
Copy the code
If you pass the component direction=”column” in the page, you can see that it has become vertical
<template>
<div class="page">
<SplitPane direction="column" />
</div>
</template>
Copy the code
Data binding
The width (height) of the current region 1 and the width (height) of the slider are both written in the style and need to be bound in JS to operate, first putting the number that can be used for calculation in data
data() {
return {
paneLengthPercent: 50.// Area 1 width (%)
triggerLength: 10 // Slider width (px)}}Copy the code
Then, using computed to return the strings needed in both styles, and to keep the slider between regions 1 and 2, the width of region 1 should be subtracted by half of the width of the slider.
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
paneLengthValue() {
return `calc(The ${this.paneLengthPercent}% - The ${this.triggerLength / 2 + 'px'}) `
},
triggerLengthValue() {
return this.triggerLength + 'px'}}Copy the code
Finally, it is bound to a template
<template>
<div class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':' + paneLengthValue"></div>
<div class="pane-trigger" :style="lengthType + ':' + triggerLengthValue"></div>
<div class="pane pane-two"></div>
</div>
</template>
Copy the code
event
Imagine dragging and dropping a slider. The first step is to hold down the mouse over the slider and add a Mousedown event to the slider
<div class="pane-trigger" :style="lengthType + ':' + triggerLengthValue" @mousedown="handleMouseDown"></div>
Copy the code
You should listen for mousemove events after you hold down the mouse, but not on the slider, but on the entire document, because the mouse can slide anywhere on the page. When the user releases the mouse, the entire document mousemove should be unlistened to, so at the moment the mouse is pressed, two events should be added to the document: mousemove and mouse release
methods: {
// Press the slider
handleMouseDown(e) {
document.addEventListener('mousemove'.this.handleMouseMove)
document.addEventListener('mouseup'.this.handleMouseUp)
},
// Press the slider to move the mouse
handleMouseMove(e) {
console.log('Dragging')},// Release the slider
handleMouseUp() {
document.removeEventListener('mousemove'.this.handleMouseMove)
}
}
Copy the code
What we really want to control is the width of area 1, so that the width of area 1 is equal to the current mouse distance to the left of the container, i.e. the width of area 1 is equal to the middle length if the mouse moves over the circle below:
The length can be calculated by subtracting the container’s distance from the leftmost part of the page from the current mouse distance, i.e. the green length is equal to red minus blue:
Add a ref to the container to get dom information for the container
. <div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }">...Copy the code
If you print the ref’s getBoundingClientRect(), you see the following:
console.log(this.$refs.splitPane.getBoundingClientRect())
Copy the code
Left represents the distance between the container and the left side of the page, and width represents the width of the container.
The distance between the current mouse pointer and the left side of the page can be obtained by using the pageX of the mouse event object, and the desired distance between the mouse pointer and the left side of the container can be calculated. Finally, divide the distance by the width of the container times 100 to get the percentage of the distance, which is assigned to paneLengthPercent.
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
const offset = e.pageX - clientRect.left
const paneLengthPercent = (offset / clientRect.width) * 100
this.paneLengthPercent = paneLengthPercent
},
Copy the code
Compatible with vertical layout.
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top
paneLengthPercent = (offset / clientRect.height) * 100
}
this.paneLengthPercent = paneLengthPercent
},
Copy the code
To optimize the
At this point it looks like the requirements are complete, but there are a few areas to optimize as a generic component.
Optimize a jitter problem
A jitter problem can be found when the slider width is set to larger:
Pressing down on both sides of the slider and gently moving it will result in a large offset, because current computational logic always assumes that the mouse is right in the middle of the slider, without taking into account the slider width.
In Data, define a left (top) offset of the current mouse pitch slider
data() {
return {
paneLengthPercent: 50.// Area 1 width (%)
triggerLength: 100.// Slider width (px)
triggerLeftOffset: 0 // The left (top) offset of the mouse pitch slider}}Copy the code
This value is equal to the mouse from page minus the slider on the left side of the distance from the page on the left side of the distance (through e.s rcElement. GetBoundingClientRect ()), in the time slider is pressed for assignment, also want to distinguish between horizontal/vertical layout: red and blue = green
// Press the slider
handleMouseDown(e) {
document.addEventListener('mousemove'.this.handleMouseMove)
document.addEventListener('mouseup'.this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
Copy the code
With this triggerLeftOffset, set the width of zone 1 to: mouse leftoffset left side of container minus the left side of mouse leftoffset plus half of the leftoffset width of the slider. This puts the mouse back in the middle of the slider.
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.height) * 100
}
this.paneLengthPercent = paneLengthPercent
},
Copy the code
There is no more jitter
Optimized two mouse styles
The mouse over the slider should change the style to tell the user that they can drag. Add the mouse style change to the horizontal and vertical slider CSS, respectively.
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
height: 100%;
}
.pane-trigger {
height: 100%;
cursor: col-resize; / / here
}
}
&.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%;
cursor: row-resize; / / here
}
}
.pane-one {
background: palevioletred;
}
.pane-trigger {
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise;
}
}
</style>
Copy the code
Optimize the three slide limit
As a general component, it should provide external functions to set the limit of sliding minimum and maximum distance, and to receive both min and Max props.
props: {
direction: {
type: String.default: 'row'
},
min: {
type: Number.default: 10
},
max: {
type: Number.default: 90}},Copy the code
Add judgment to handleMouseMove:
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.height) * 100
}
if (paneLengthPercent < this.min) {
paneLengthPercent = this.min
}
if (paneLengthPercent > this.max) {
paneLengthPercent = this.max
}
this.paneLengthPercent = paneLengthPercent
}
Copy the code
Optimize the default width and slider width of the four panels
As a general component, the panel initialization ratio and slider width should also be determined by external users. Move paneLengthPercent and triggerLength from Data to props to receive externally.
props: {
direction: {
type: String.default: 'row'
},
min: {
type: Number.default: 10
},
max: {
type: Number.default: 90
},
paneLengthPercent: {
type: Number.default: 50
},
triggerLength: {
type: Number.default: 10}},data() {
return {
triggerLeftOffset: 0 // The left (top) offset of the mouse pitch slider}},Copy the code
On the page, you pass in paneLengthPercent. Note that paneLengthPercent must be a data defined in data, with the.sync modifier, because the value is dynamically modified.
// page.vue
<template>
<div class="page">
<SplitPane direction="row" :paneLengthPercent.sync="paneLengthPercent" />
</div>
</template>
...
data() {
return {
paneLengthPercent: 30}}...Copy the code
Then modify the paneLengthPercent value in the handleMouseMove component by means of this.$emit events.
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.height) * 100
}
if (paneLengthPercent < this.min) {
paneLengthPercent = this.min
}
if (paneLengthPercent > this.max) {
paneLengthPercent = this.max
}
this.$emit('update:paneLengthPercent', paneLengthPercent) / / here
},
Copy the code
At this point, component element information can be controlled by external props.
Optimized five slots
Not being able to add content as a container component is not a waste of time, adding two named slots to each region.
<template>
<div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':' + paneLengthValue">
<slot name="one"></slot>
</div>
<div
class="pane-trigger"
:style="lengthType + ':' + triggerLengthValue"
@mousedown="handleMouseDown">
</div>
<div class="pane pane-two">
<slot name="two"></slot>
</div>
</div>
</template>
Copy the code
Optimization 6 disable
In the drag process, if there is text content in the area may be selected text situation, add disable select effect to the slider.
. .pane-trigger { user-select: none; background: palegoldenrod; }...Copy the code
The end of the
Component complete code
All background colors are reserved only for the purpose of display, and deleted in actual use
<template>
<div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':' + paneLengthValue">
<slot name="one"></slot>
</div>
<div
class="pane-trigger"
:style="lengthType + ':' + triggerLengthValue"
@mousedown="handleMouseDown"
></div>
<div class="pane pane-two">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
direction: {
type: String.default: 'row'
},
min: {
type: Number.default: 10
},
max: {
type: Number.default: 90
},
paneLengthPercent: {
type: Number.default: 50
},
triggerLength: {
type: Number.default: 10}},data() {
return {
triggerLeftOffset: 0 // The left (top) offset of the mouse pitch slider}},computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
paneLengthValue() {
return `calc(The ${this.paneLengthPercent}% - The ${this.triggerLength / 2 + 'px'}) `
},
triggerLengthValue() {
return this.triggerLength + 'px'}},methods: {
// Press the slider
handleMouseDown(e) {
document.addEventListener('mousemove'.this.handleMouseMove)
document.addEventListener('mouseup'.this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// Press the slider to move the mouse
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.height) * 100
}
if (paneLengthPercent < this.min) {
paneLengthPercent = this.min
}
if (paneLengthPercent > this.max) {
paneLengthPercent = this.max
}
this.$emit('update:paneLengthPercent', paneLengthPercent)
},
// Release the slider
handleMouseUp() {
document.removeEventListener('mousemove'.this.handleMouseMove)
}
}
}
</script>
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
height: 100%;
}
.pane-trigger {
height: 100%;
cursor: col-resize; }} &.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%;
cursor: row-resize; }}.pane-one {
background: palevioletred;
}
.pane-trigger {
user-select: none;
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise; }}</style>
Copy the code
Component Usage Examples
All background colors are reserved only for the purpose of display, and deleted in actual use
<template>
<div class="page">
<SplitPane
direction="column"
:min="20"
:max="80"
:triggerLength="20"
:paneLengthPercent.sync="paneLengthPercent"
>
<template v-slot:one>
<div>Area a</div>
</template>
<template v-slot:two>
<div>Area 2</div>
</template>
</SplitPane>
</div>
</template>
<script>
import SplitPane from './components/split-pane'
export default {
components: {
SplitPane
},
data() {
return {
paneLengthPercent: 30}}}</script>
<style scoped lang="scss">
.page {
height: 100%;
padding: 10px;
background: # 000;
}
</style>
Copy the code