Commit 8979c734695c2e4e9063e622f8a0bb3accc97a5b
1 parent
273cc057
add split components
Showing
9 changed files
with
392 additions
and
0 deletions
Show diff stats
examples/app.vue
| ... | ... | @@ -16,6 +16,7 @@ nav { |
| 16 | 16 | <div class="container"> |
| 17 | 17 | <nav> |
| 18 | 18 | <ul> |
| 19 | + <li><router-link to="/split">Split</router-link></li> | |
| 19 | 20 | <li><router-link to="/layout">Layout</router-link></li> |
| 20 | 21 | <li><router-link to="/affix">Affix</router-link></li> |
| 21 | 22 | <li><router-link to="/grid">Grid</router-link></li> | ... | ... |
examples/main.js
| ... | ... | @@ -20,6 +20,10 @@ const router = new VueRouter({ |
| 20 | 20 | esModule: false, |
| 21 | 21 | routes: [ |
| 22 | 22 | { |
| 23 | + path: '/split', | |
| 24 | + component: (resolve) => require(['./routers/split.vue'], resolve) | |
| 25 | + }, | |
| 26 | + { | |
| 23 | 27 | path: '/layout', |
| 24 | 28 | component: (resolve) => require(['./routers/layout.vue'], resolve) |
| 25 | 29 | }, | ... | ... |
| 1 | +<template> | |
| 2 | + <div class="split-pane-page-wrapper"> | |
| 3 | + <Split v-model="offset" @on-moving="handleMoving"> | |
| 4 | + <div slot="left" class="pane left-pane"> | |
| 5 | + <Split v-model="offsetVertical" mode="vertical" @on-moving="handleMoving"> | |
| 6 | + <div slot="top" class="pane top-pane"></div> | |
| 7 | + <div slot="bottom" class="pane bottom-pane"></div> | |
| 8 | + <div slot="trigger" class="custom-trigger"> | |
| 9 | + <Icon class="trigger-icon" :size="22" type="android-more-vertical" color="#000000"/> | |
| 10 | + </div> | |
| 11 | + </Split> | |
| 12 | + </div> | |
| 13 | + <div slot="right" class="pane right-pane"></div> | |
| 14 | + </Split> | |
| 15 | + </div> | |
| 16 | +</template> | |
| 17 | + | |
| 18 | +<script> | |
| 19 | +export default { | |
| 20 | + name: 'split_pane_page', | |
| 21 | + data () { | |
| 22 | + return { | |
| 23 | + offset: 0.6, | |
| 24 | + offsetVertical: '250px' | |
| 25 | + } | |
| 26 | + }, | |
| 27 | + methods: { | |
| 28 | + handleMoving (e) { | |
| 29 | + console.log(e.atMin, e.atMax) | |
| 30 | + } | |
| 31 | + } | |
| 32 | +} | |
| 33 | +</script> | |
| 34 | + | |
| 35 | +<style lang="less"> | |
| 36 | +.center-middle{ | |
| 37 | + position: absolute; | |
| 38 | + left: 50%; | |
| 39 | + top: 50%; | |
| 40 | + transform: translate(-50%, -50%); | |
| 41 | +} | |
| 42 | +.split-pane-page-wrapper{ | |
| 43 | + height: 600px; | |
| 44 | + .pane{ | |
| 45 | + width: 100%; | |
| 46 | + height: 100%; | |
| 47 | + &.left-pane{ | |
| 48 | + background: sandybrown; | |
| 49 | + } | |
| 50 | + &.right-pane{ | |
| 51 | + background: palevioletred; | |
| 52 | + } | |
| 53 | + &.top-pane{ | |
| 54 | + background: sandybrown; | |
| 55 | + } | |
| 56 | + &.bottom-pane{ | |
| 57 | + background: palevioletred; | |
| 58 | + } | |
| 59 | + } | |
| 60 | + .custom-trigger{ | |
| 61 | + width: 20px; | |
| 62 | + height: 20px; | |
| 63 | + border-radius: 50%; | |
| 64 | + background: #fff; | |
| 65 | + position: absolute; | |
| 66 | + .center-middle; | |
| 67 | + box-shadow: 0 0 6px 0 rgba(28, 36, 56, 0.4); | |
| 68 | + cursor: row-resize; | |
| 69 | + i.trigger-icon{ | |
| 70 | + .center-middle; | |
| 71 | + } | |
| 72 | + } | |
| 73 | +} | |
| 74 | +</style> | ... | ... |
| 1 | +<template> | |
| 2 | + <div ref="outerWrapper" :class="wrapperClasses"> | |
| 3 | + <div v-if="isHorizontal" :class="`${prefix}-horizontal`"> | |
| 4 | + <div :style="{right: `${anotherOffset}%`}" :class="[`${prefix}-pane`, 'left-pane']"><slot name="left"/></div> | |
| 5 | + <div :class="`${prefix}-trigger-con`" :style="{left: `${offset}%`}" @mousedown="handleMousedown"> | |
| 6 | + <slot name="trigger"> | |
| 7 | + <trigger mode="vertical"/> | |
| 8 | + </slot> | |
| 9 | + </div> | |
| 10 | + <div :style="{left: `${offset}%`}" :class="[`${prefix}-pane`, 'right-pane']"><slot name="right"/></div> | |
| 11 | + </div> | |
| 12 | + <div v-else :class="`${prefix}-vertical`"> | |
| 13 | + <div :style="{bottom: `${anotherOffset}%`}" :class="[`${prefix}-pane`, 'top-pane']"><slot name="top"/></div> | |
| 14 | + <div :class="`${prefix}-trigger-con`" :style="{top: `${offset}%`}" @mousedown="handleMousedown"> | |
| 15 | + <slot name="trigger"> | |
| 16 | + <trigger mode="horizontal"/> | |
| 17 | + </slot> | |
| 18 | + </div> | |
| 19 | + <div :style="{top: `${offset}%`}" :class="[`${prefix}-pane`, 'bottom-pane']"><slot name="bottom"/></div> | |
| 20 | + </div> | |
| 21 | + </div> | |
| 22 | +</template> | |
| 23 | + | |
| 24 | +<script> | |
| 25 | +import { oneOf } from '../../utils/assist'; | |
| 26 | +import { on, off } from '../../utils/dom'; | |
| 27 | +import Trigger from './trigger.vue' | |
| 28 | +export default { | |
| 29 | + name: 'SplitPane', | |
| 30 | + components: { | |
| 31 | + Trigger | |
| 32 | + }, | |
| 33 | + props: { | |
| 34 | + value: { | |
| 35 | + type: [Number, String], | |
| 36 | + default: 0.5 | |
| 37 | + }, | |
| 38 | + mode: { | |
| 39 | + validator (value) { | |
| 40 | + return oneOf(value, ['horizontal', 'vertical']) | |
| 41 | + }, | |
| 42 | + default: 'horizontal' | |
| 43 | + }, | |
| 44 | + min: { | |
| 45 | + type: [Number, String], | |
| 46 | + default: '40px' | |
| 47 | + }, | |
| 48 | + max: { | |
| 49 | + type: [Number, String], | |
| 50 | + default: '40px' | |
| 51 | + } | |
| 52 | + }, | |
| 53 | + /** | |
| 54 | + * Events | |
| 55 | + * @on-move-start | |
| 56 | + * @on-moving 返回值:事件对象,但是在事件对象中加入了两个参数:atMin(当前是否在最小值处), atMax(当前是否在最大值处) | |
| 57 | + * @on-move-end | |
| 58 | + */ | |
| 59 | + data () { | |
| 60 | + return { | |
| 61 | + prefix: 'ivu-split', | |
| 62 | + offset: 0, | |
| 63 | + oldOffset: 0, | |
| 64 | + isMoving: false | |
| 65 | + } | |
| 66 | + }, | |
| 67 | + computed: { | |
| 68 | + wrapperClasses () { | |
| 69 | + return [ | |
| 70 | + `${this.prefix}-wrapper`, | |
| 71 | + this.isMoving ? 'no-select' : '' | |
| 72 | + ] | |
| 73 | + }, | |
| 74 | + isHorizontal () { | |
| 75 | + return this.mode === 'horizontal' | |
| 76 | + }, | |
| 77 | + anotherOffset () { | |
| 78 | + return 100 - this.offset | |
| 79 | + }, | |
| 80 | + valueIsPx () { | |
| 81 | + return typeof this.value === 'string' | |
| 82 | + }, | |
| 83 | + offsetSize () { | |
| 84 | + return this.isHorizontal ? 'offsetWidth' : 'offsetHeight' | |
| 85 | + }, | |
| 86 | + computedMin () { | |
| 87 | + return this.getComputedThresholdValue('min') | |
| 88 | + }, | |
| 89 | + computedMax () { | |
| 90 | + return this.getComputedThresholdValue('max') | |
| 91 | + } | |
| 92 | + }, | |
| 93 | + methods: { | |
| 94 | + px2percent (numerator, denominator) { | |
| 95 | + return parseFloat(numerator) / parseFloat(denominator) | |
| 96 | + }, | |
| 97 | + getComputedThresholdValue (type) { | |
| 98 | + let size = this.$refs.outerWrapper[this.offsetSize] | |
| 99 | + if (this.valueIsPx) return typeof this[type] === 'string' ? this[type] : size * this[type] | |
| 100 | + else return typeof this[type] === 'string' ? this.px2percent(this[type], size) : this[type] | |
| 101 | + }, | |
| 102 | + getMin (value1, value2) { | |
| 103 | + if (this.valueIsPx) return `${Math.min(parseFloat(value1), parseFloat(value2))}px` | |
| 104 | + else return Math.min(value1, value2) | |
| 105 | + }, | |
| 106 | + getMax (value1, value2) { | |
| 107 | + if (this.valueIsPx) return `${Math.max(parseFloat(value1), parseFloat(value2))}px` | |
| 108 | + else return Math.max(value1, value2) | |
| 109 | + }, | |
| 110 | + getAnotherOffset (value) { | |
| 111 | + let res = 0 | |
| 112 | + if (this.valueIsPx) res = `${this.$refs.outerWrapper[this.offsetSize] - parseFloat(value)}px` | |
| 113 | + else res = 1 - value | |
| 114 | + return res | |
| 115 | + }, | |
| 116 | + handleMove (e) { | |
| 117 | + let pageOffset = this.isHorizontal ? e.pageX : e.pageY | |
| 118 | + let offset = pageOffset - this.initOffset | |
| 119 | + let outerWidth = this.$refs.outerWrapper[this.offsetSize] | |
| 120 | + let value = this.valueIsPx ? `${parseFloat(this.oldOffset) + offset}px` : (this.px2percent(outerWidth * this.oldOffset + offset, outerWidth)) | |
| 121 | + let anotherValue = this.getAnotherOffset(value) | |
| 122 | + if (parseFloat(value) <= parseFloat(this.computedMin)) value = this.getMax(value, this.computedMin) | |
| 123 | + if (parseFloat(anotherValue) <= parseFloat(this.computedMax)) value = this.getAnotherOffset(this.getMax(anotherValue, this.computedMax)) | |
| 124 | + e.atMin = this.value === this.computedMin | |
| 125 | + e.atMax = this.valueIsPx ? this.getAnotherOffset(this.value) === this.computedMax : this.getAnotherOffset(this.value).toFixed(5) === this.computedMax.toFixed(5) | |
| 126 | + this.$emit('input', value) | |
| 127 | + this.$emit('on-moving', e) | |
| 128 | + }, | |
| 129 | + handleUp () { | |
| 130 | + this.isMoving = false | |
| 131 | + off(document, 'mousemove', this.handleMove) | |
| 132 | + off(document, 'mouseup', this.handleUp) | |
| 133 | + this.$emit('on-move-end') | |
| 134 | + }, | |
| 135 | + handleMousedown (e) { | |
| 136 | + this.initOffset = this.isHorizontal ? e.pageX : e.pageY | |
| 137 | + this.oldOffset = this.value | |
| 138 | + this.isMoving = true | |
| 139 | + on(document, 'mousemove', this.handleMove) | |
| 140 | + on(document, 'mouseup', this.handleUp) | |
| 141 | + this.$emit('on-move-start') | |
| 142 | + } | |
| 143 | + }, | |
| 144 | + watch: { | |
| 145 | + value () { | |
| 146 | + this.offset = (this.valueIsPx ? this.px2percent(this.value, this.$refs.outerWrapper[this.offsetSize]) : this.value) * 10000 / 100 | |
| 147 | + } | |
| 148 | + }, | |
| 149 | + mounted () { | |
| 150 | + this.$nextTick(() => { | |
| 151 | + this.offset = (this.valueIsPx ? this.px2percent(this.value, this.$refs.outerWrapper[this.offsetSize]) : this.value) * 10000 / 100 | |
| 152 | + }) | |
| 153 | + } | |
| 154 | +} | |
| 155 | +</script> | ... | ... |
| 1 | +<template> | |
| 2 | + <div :class="classes"> | |
| 3 | + <div :class="barConClasses"> | |
| 4 | + <i :class="`${prefix}-bar`" v-once v-for="i in 8" :key="`trigger-${i}`"></i> | |
| 5 | + </div> | |
| 6 | + </div> | |
| 7 | +</template> | |
| 8 | + | |
| 9 | +<script> | |
| 10 | +export default { | |
| 11 | + name: 'Trigger', | |
| 12 | + props: { | |
| 13 | + mode: String | |
| 14 | + }, | |
| 15 | + data () { | |
| 16 | + return { | |
| 17 | + prefix: 'ivu-split-trigger', | |
| 18 | + initOffset: 0 | |
| 19 | + } | |
| 20 | + }, | |
| 21 | + computed: { | |
| 22 | + isVertical () { | |
| 23 | + return this.mode === 'vertical' | |
| 24 | + }, | |
| 25 | + classes () { | |
| 26 | + return [ | |
| 27 | + this.prefix, | |
| 28 | + this.isVertical ? `${this.prefix}-vertical` : `${this.prefix}-horizontal` | |
| 29 | + ] | |
| 30 | + }, | |
| 31 | + barConClasses () { | |
| 32 | + return [ | |
| 33 | + `${this.prefix}-bar-con`, | |
| 34 | + this.isVertical ? 'vertical' : 'horizontal' | |
| 35 | + ] | |
| 36 | + } | |
| 37 | + } | |
| 38 | +} | |
| 39 | +</script> | ... | ... |
src/index.js
| ... | ... | @@ -23,6 +23,7 @@ import Icon from './components/icon'; |
| 23 | 23 | import Input from './components/input'; |
| 24 | 24 | import InputNumber from './components/input-number'; |
| 25 | 25 | import Scroll from './components/scroll'; |
| 26 | +import Split from './components/split'; | |
| 26 | 27 | import Layout from './components/layout'; |
| 27 | 28 | import LoadingBar from './components/loading-bar'; |
| 28 | 29 | import Menu from './components/menu'; |
| ... | ... | @@ -86,6 +87,7 @@ const components = { |
| 86 | 87 | InputNumber, |
| 87 | 88 | Scroll, |
| 88 | 89 | Sider: Sider, |
| 90 | + Split, | |
| 89 | 91 | Submenu: Menu.Sub, |
| 90 | 92 | Layout: Layout, |
| 91 | 93 | LoadingBar, | ... | ... |
src/styles/components/index.less
| 1 | +@split-prefix-cls: ~"@{css-prefix}split"; | |
| 2 | +@box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.4); | |
| 3 | +@trigger-bar-background: rgba(23, 35, 61, 0.25); | |
| 4 | +@trigger-background: #F8F8F9; | |
| 5 | +@trigger-width: 6px; | |
| 6 | +@trigger-bar-width: 4px; | |
| 7 | +@trigger-bar-offset: (@trigger-width - @trigger-bar-width) / 2; | |
| 8 | +@trigger-bar-interval: 3px; | |
| 9 | +@trigger-bar-weight: 1px; | |
| 10 | +@trigger-bar-con-height: (@trigger-bar-weight + @trigger-bar-interval) * 8; | |
| 11 | + | |
| 12 | +.@{split-prefix-cls}{ | |
| 13 | + &-wrapper{ | |
| 14 | + position: relative; | |
| 15 | + width: 100%; | |
| 16 | + height: 100%; | |
| 17 | + } | |
| 18 | + &-pane{ | |
| 19 | + position: absolute; | |
| 20 | + &.left-pane, &.right-pane{ | |
| 21 | + top: 0px; | |
| 22 | + bottom: 0px; | |
| 23 | + } | |
| 24 | + &.left-pane{ | |
| 25 | + left: 0px; | |
| 26 | + } | |
| 27 | + &.right-pane{ | |
| 28 | + right: 0px; | |
| 29 | + } | |
| 30 | + &.top-pane, &.bottom-pane{ | |
| 31 | + left: 0px; | |
| 32 | + right: 0px; | |
| 33 | + } | |
| 34 | + &.top-pane{ | |
| 35 | + top: 0px; | |
| 36 | + } | |
| 37 | + &.bottom-pane{ | |
| 38 | + bottom: 0px; | |
| 39 | + } | |
| 40 | + } | |
| 41 | + &-trigger{ | |
| 42 | + &-con{ | |
| 43 | + position: absolute; | |
| 44 | + transform: translate(-50%, -50%); | |
| 45 | + z-index: 10; | |
| 46 | + } | |
| 47 | + &-bar-con{ | |
| 48 | + position: absolute; | |
| 49 | + overflow: hidden; | |
| 50 | + &.vertical{ | |
| 51 | + left: @trigger-bar-offset; | |
| 52 | + top: 50%; | |
| 53 | + height: @trigger-bar-con-height; | |
| 54 | + transform: translate(0, -50%); | |
| 55 | + } | |
| 56 | + &.horizontal{ | |
| 57 | + left: 50%; | |
| 58 | + top: @trigger-bar-offset; | |
| 59 | + width: @trigger-bar-con-height; | |
| 60 | + transform: translate(-50%, 0); | |
| 61 | + } | |
| 62 | + } | |
| 63 | + &-vertical{ | |
| 64 | + width: @trigger-width; | |
| 65 | + height: 100%; | |
| 66 | + background: @trigger-background; | |
| 67 | + box-shadow: @box-shadow; | |
| 68 | + cursor: col-resize; | |
| 69 | + .@{split-prefix-cls}-trigger-bar{ | |
| 70 | + width: @trigger-bar-width; | |
| 71 | + height: 1px; | |
| 72 | + background: @trigger-bar-background; | |
| 73 | + float: left; | |
| 74 | + margin-top: @trigger-bar-interval; | |
| 75 | + } | |
| 76 | + } | |
| 77 | + &-horizontal{ | |
| 78 | + height: @trigger-width; | |
| 79 | + width: 100%; | |
| 80 | + background: @trigger-background; | |
| 81 | + box-shadow: @box-shadow; | |
| 82 | + cursor: row-resize; | |
| 83 | + .@{split-prefix-cls}-trigger-bar{ | |
| 84 | + height: @trigger-bar-width; | |
| 85 | + width: 1px; | |
| 86 | + background: @trigger-bar-background; | |
| 87 | + float: left; | |
| 88 | + margin-right: @trigger-bar-interval; | |
| 89 | + } | |
| 90 | + } | |
| 91 | + } | |
| 92 | + &-horizontal{ | |
| 93 | + .@{split-prefix-cls}-trigger-con{ | |
| 94 | + top: 50%; | |
| 95 | + height: 100%; | |
| 96 | + width: 0; | |
| 97 | + } | |
| 98 | + } | |
| 99 | + &-vertical{ | |
| 100 | + .@{split-prefix-cls}-trigger-con{ | |
| 101 | + left: 50%; | |
| 102 | + height: 0; | |
| 103 | + width: 100%; | |
| 104 | + } | |
| 105 | + } | |
| 106 | + .no-select{ | |
| 107 | + -webkit-touch-callout: none; | |
| 108 | + -webkit-user-select: none; | |
| 109 | + -khtml-user-select: none; | |
| 110 | + -moz-user-select: none; | |
| 111 | + -ms-user-select: none; | |
| 112 | + user-select: none; | |
| 113 | + } | |
| 114 | +} | ... | ... |