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,6 +16,7 @@ nav { | ||
16 | <div class="container"> | 16 | <div class="container"> |
17 | <nav> | 17 | <nav> |
18 | <ul> | 18 | <ul> |
19 | + <li><router-link to="/split">Split</router-link></li> | ||
19 | <li><router-link to="/layout">Layout</router-link></li> | 20 | <li><router-link to="/layout">Layout</router-link></li> |
20 | <li><router-link to="/affix">Affix</router-link></li> | 21 | <li><router-link to="/affix">Affix</router-link></li> |
21 | <li><router-link to="/grid">Grid</router-link></li> | 22 | <li><router-link to="/grid">Grid</router-link></li> |
examples/main.js
@@ -20,6 +20,10 @@ const router = new VueRouter({ | @@ -20,6 +20,10 @@ const router = new VueRouter({ | ||
20 | esModule: false, | 20 | esModule: false, |
21 | routes: [ | 21 | routes: [ |
22 | { | 22 | { |
23 | + path: '/split', | ||
24 | + component: (resolve) => require(['./routers/split.vue'], resolve) | ||
25 | + }, | ||
26 | + { | ||
23 | path: '/layout', | 27 | path: '/layout', |
24 | component: (resolve) => require(['./routers/layout.vue'], resolve) | 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,6 +23,7 @@ import Icon from './components/icon'; | ||
23 | import Input from './components/input'; | 23 | import Input from './components/input'; |
24 | import InputNumber from './components/input-number'; | 24 | import InputNumber from './components/input-number'; |
25 | import Scroll from './components/scroll'; | 25 | import Scroll from './components/scroll'; |
26 | +import Split from './components/split'; | ||
26 | import Layout from './components/layout'; | 27 | import Layout from './components/layout'; |
27 | import LoadingBar from './components/loading-bar'; | 28 | import LoadingBar from './components/loading-bar'; |
28 | import Menu from './components/menu'; | 29 | import Menu from './components/menu'; |
@@ -86,6 +87,7 @@ const components = { | @@ -86,6 +87,7 @@ const components = { | ||
86 | InputNumber, | 87 | InputNumber, |
87 | Scroll, | 88 | Scroll, |
88 | Sider: Sider, | 89 | Sider: Sider, |
90 | + Split, | ||
89 | Submenu: Menu.Sub, | 91 | Submenu: Menu.Sub, |
90 | Layout: Layout, | 92 | Layout: Layout, |
91 | LoadingBar, | 93 | LoadingBar, |
src/styles/components/index.less
@@ -24,6 +24,7 @@ | @@ -24,6 +24,7 @@ | ||
24 | @import "modal"; | 24 | @import "modal"; |
25 | @import "select"; | 25 | @import "select"; |
26 | @import "select-dropdown"; | 26 | @import "select-dropdown"; |
27 | +@import "split"; | ||
27 | @import "tooltip"; | 28 | @import "tooltip"; |
28 | @import "poptip"; | 29 | @import "poptip"; |
29 | @import "input"; | 30 | @import "input"; |
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 | +} |