Skip to content

Commit 4bed8cc

Browse files
authored
feat: add swipeable functionality (#1618)
* feat: add swipeable mixin * feat(MdDrawer): swipeable drawer * docs(MdDrawer): add swipeable prop * feat: allow dynamic swipeElement * feat: reset swiped value after touchend * feat: tweaks of default values * fix: wrong axis for restraints * feat(MdTabs): swipeable tabs * feat(MdTabs): allow swipeable only on tabs content * feat: change naming to mdSwipeElement * feat(MdDrawer): use parent of drawer as swipe element * docs: add api docs for drawer and tabs
1 parent e6a967b commit 4bed8cc

File tree

6 files changed

+195
-3
lines changed

6 files changed

+195
-3
lines changed

docs/app/pages/Components/Drawer/Drawer.vue

+37
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
<api-table :headings="drawer.props.headings" :props="drawer.props.props" slot="props" />
5858
<api-table :headings="drawer.events.headings" :props="drawer.events.props" slot="events" />
5959
</api-item>
60+
61+
<api-item title="API - Swipeable">
62+
<p>The following options can be applied to any <code>md-drawer</code> component that is using <code>md-swipeable</code> prop.</p>
63+
64+
<api-table :headings="swipeable.props.headings" :props="swipeable.props.props" slot="props" />
65+
</api-item>
6066
</page-container>
6167
</template>
6268

@@ -67,6 +73,31 @@
6773
name: 'DocDrawer',
6874
mixins: [examples],
6975
data: () => ({
76+
swipeable: {
77+
props: {
78+
headings: ['Name', 'Description', 'Default'],
79+
props: [
80+
{
81+
name: 'md-swipe-threshold',
82+
type: 'Number',
83+
description: 'The minimal distance traveled to be considered swipe.',
84+
defaults: '50'
85+
},
86+
{
87+
name: 'md-swipe-restraint',
88+
type: 'Number',
89+
description: 'The maximum distance allowed at the same time in perpendicular direction.',
90+
defaults: '100'
91+
},
92+
{
93+
name: 'md-swipe-time',
94+
type: 'Number',
95+
description: 'The maximum time allowed to detect swipe.',
96+
defaults: '400'
97+
},
98+
]
99+
}
100+
},
70101
drawer: {
71102
props: {
72103
headings: ['Name', 'Description', 'Default'],
@@ -77,6 +108,12 @@
77108
description: 'Option used to trigger the drawer visibility. Should be used with the <code>.sync</code> modifier.',
78109
defaults: 'false'
79110
},
111+
{
112+
name: 'md-swipeable',
113+
type: 'Boolean',
114+
description: 'Option used to enable touch support to be opened/closed by swipe. For more option see API - Swipeable',
115+
defaults: 'false'
116+
},
80117
{
81118
name: 'md-fixed',
82119
type: 'Boolean',

docs/app/pages/Components/Drawer/examples/Temporary.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</div>
1212
</md-toolbar>
1313

14-
<md-drawer :md-active.sync="showNavigation">
14+
<md-drawer :md-active.sync="showNavigation" md-swipeable>
1515
<md-toolbar class="md-transparent" md-elevation="0">
1616
<span class="md-title">My App name</span>
1717
</md-toolbar>

docs/app/pages/Components/Tabs/Tabs.vue

+37
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959

6060
<api-table :headings="tab.headings" :props="tab.props" slot="props" />
6161
</api-item>
62+
63+
<api-item title="API - Swipeable">
64+
<p>The following options can be applied to any <code>md-tabs</code> component that is using <code>md-swipeable</code> prop.</p>
65+
66+
<api-table :headings="swipeable.props.headings" :props="swipeable.props.props" slot="props" />
67+
</api-item>
6268
</div>
6369
</page-container>
6470
</template>
@@ -70,6 +76,31 @@
7076
name: 'DocTabs',
7177
mixins: [examples],
7278
data: () => ({
79+
swipeable: {
80+
props: {
81+
headings: ['Name', 'Description', 'Default'],
82+
props: [
83+
{
84+
name: 'md-swipe-threshold',
85+
type: 'Number',
86+
description: 'The minimal distance traveled to be considered swipe.',
87+
defaults: '50'
88+
},
89+
{
90+
name: 'md-swipe-restraint',
91+
type: 'Number',
92+
description: 'The maximum distance allowed at the same time in perpendicular direction.',
93+
defaults: '100'
94+
},
95+
{
96+
name: 'md-swipe-time',
97+
type: 'Number',
98+
description: 'The maximum time allowed to detect swipe.',
99+
defaults: '400'
100+
},
101+
]
102+
}
103+
},
73104
tabs: {
74105
props: {
75106
headings: ['Name', 'Description', 'Default'],
@@ -80,6 +111,12 @@
80111
description: 'Set the current selected tab. Works by providing the id of the desired <code>md-tab</code>.',
81112
defaults: 'null'
82113
},
114+
{
115+
name: 'md-swipeable',
116+
type: 'Boolean',
117+
description: 'Option used to enable touch support to move between tabs by swipe. For more option see API - Swipeable',
118+
defaults: 'false'
119+
},
83120
{
84121
name: 'md-sync-route',
85122
type: 'Boolean',

src/components/MdDrawer/MdDrawer.vue

+11
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
import MdOverlay from 'components/MdOverlay/MdOverlay'
1212
import MdPropValidator from 'core/utils/MdPropValidator'
1313
14+
import MdSwipeable from 'core/mixins/MdSwipeable/MdSwipeable'
15+
1416
export default new MdComponent({
1517
name: 'MdDrawer',
18+
mixins: [MdSwipeable],
1619
components: {
1720
MdOverlay
1821
},
@@ -43,6 +46,11 @@
4346
} else {
4447
this.$emit('md-closed')
4548
}
49+
},
50+
swiped (value) {
51+
if (value === 'right' || value === 'left') {
52+
this.$emit('update:mdActive', value === 'right')
53+
}
4654
}
4755
},
4856
computed: {
@@ -89,6 +97,9 @@
8997
if (this.mdPermanent) {
9098
return this.mdPermanent
9199
}
100+
},
101+
mdSwipeElement () {
102+
return this.$el.parentNode
92103
}
93104
},
94105
methods: {

src/components/MdTabs/MdTabs.vue

+22-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<span class="md-tabs-indicator" :style="indicatorStyles" :class="indicatorClass" ref="indicator"></span>
2727
</div>
2828

29-
<md-content class="md-tabs-content" :style="contentStyles" v-show="hasContent">
29+
<md-content ref="tabsContent" class="md-tabs-content" :style="contentStyles" v-show="hasContent">
3030
<div class="md-tabs-container" :style="containerStyles">
3131
<slot />
3232
</div>
@@ -41,10 +41,11 @@
4141
import MdPropValidator from 'core/utils/MdPropValidator'
4242
import MdObserveElement from 'core/utils/MdObserveElement'
4343
import MdContent from 'components/MdContent/MdContent'
44+
import MdSwipeable from 'core/mixins/MdSwipeable/MdSwipeable'
4445
4546
export default new MdComponent({
4647
name: 'MdTabs',
47-
mixins: [MdAssetIcon],
48+
mixins: [MdAssetIcon, MdSwipeable],
4849
components: {
4950
MdContent
5051
},
@@ -93,6 +94,9 @@
9394
},
9495
navigationClasses () {
9596
return 'md-elevation-' + this.mdElevation
97+
},
98+
mdSwipeElement () {
99+
return this.$refs.tabsContent.$el
96100
}
97101
},
98102
watch: {
@@ -112,6 +116,15 @@
112116
mdActiveTab (tab) {
113117
this.activeTab = tab
114118
this.$emit('md-changed', tab)
119+
},
120+
swiped (value) {
121+
const { keys } = this.getItemsAndKeys()
122+
const max = keys.length || 0
123+
if (this.activeTabIndex < max && value === 'right') {
124+
this.setSwipeActiveTabByIndex(this.activeTabIndex + 1)
125+
} else if (this.activeTabIndex > 0 && value === 'left') {
126+
this.setSwipeActiveTabByIndex(this.activeTabIndex - 1)
127+
}
115128
}
116129
},
117130
methods: {
@@ -137,6 +150,13 @@
137150
this.activeTabIndex = [].indexOf.call(activeButton.parentNode.childNodes, activeButton)
138151
}
139152
},
153+
setSwipeActiveTabByIndex (index) {
154+
const { keys } = this.getItemsAndKeys()
155+
156+
if (keys) {
157+
this.activeTab = keys[index]
158+
}
159+
},
140160
setActiveTabByIndex (index) {
141161
const { keys } = this.getItemsAndKeys()
142162
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
export default {
2+
props: {
3+
mdSwipeable: Boolean,
4+
mdSwipeThreshold: {
5+
type: Number,
6+
default: 150
7+
},
8+
mdSwipeRestraint: {
9+
type: Number,
10+
default: 100
11+
},
12+
mdSwipeTime: {
13+
type: Number,
14+
default: 300
15+
}
16+
},
17+
data: () => ({
18+
swipeStart: false,
19+
swipeStartTime: null,
20+
swiped: null,
21+
touchPosition: {
22+
startX: 0,
23+
startY: 0
24+
}
25+
}),
26+
computed: {
27+
getSwipeElement () {
28+
return this.mdSwipeElement || window
29+
}
30+
},
31+
methods: {
32+
handleTouchStart (event) {
33+
this.touchPosition.startX = event.touches[0].screenX
34+
this.touchPosition.startY = event.touches[0].screenY
35+
this.swipeStartTime = new Date()
36+
37+
this.swipeStart = true
38+
},
39+
handleTouchMove (event) {
40+
if (!this.swipeStart) {
41+
return
42+
}
43+
44+
const touchmoveX = event.touches[0].screenX
45+
const touchmoveY = event.touches[0].screenY
46+
47+
const actualX = touchmoveX - this.touchPosition.startX
48+
const actualY = touchmoveY - this.touchPosition.startY
49+
50+
const elapsedTime = new Date() - this.swipeStartTime
51+
52+
if (elapsedTime <= this.mdSwipeTime) {
53+
if (Math.abs(actualX) >= this.mdSwipeThreshold && Math.abs(actualY) <= this.mdSwipeRestraint) {
54+
this.swiped = actualX < 0
55+
? 'left'
56+
: 'right'
57+
} else if (Math.abs(actualY) >= this.mdSwipeThreshold && Math.abs(actualX) <= this.mdSwipeRestraint) {
58+
this.swiped = actualY < 0
59+
? 'up'
60+
: 'down'
61+
}
62+
}
63+
},
64+
handleTouchEnd () {
65+
this.touchPosition = {
66+
startX: 0,
67+
startY: 0
68+
}
69+
this.swiped = null
70+
this.swipeStart = false
71+
},
72+
},
73+
mounted () {
74+
if (this.mdSwipeable) {
75+
this.getSwipeElement.addEventListener('touchstart', this.handleTouchStart, false)
76+
this.getSwipeElement.addEventListener('touchend', this.handleTouchEnd, false)
77+
this.getSwipeElement.addEventListener('touchmove', this.handleTouchMove, false)
78+
}
79+
},
80+
beforeDestroy () {
81+
if (this.mdSwipeable) {
82+
this.getSwipeElement.removeEventListener('touchstart', this.handleTouchStart, false)
83+
this.getSwipeElement.removeEventListener('touchend', this.handleTouchEnd, false)
84+
this.getSwipeElement.removeEventListener('touchmove', this.handleTouchMove, false)
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)