Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a75fd95

Browse files
committedNov 16, 2018
initial commit
0 parents  commit a75fd95

File tree

5 files changed

+557
-0
lines changed

5 files changed

+557
-0
lines changed
 

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

‎README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# NativeScript-Vue Multi Drawer
2+
3+
A plugin which provides a drawer component that supports multiple drawers.
4+
5+
All drawers are optional, and can be configured individually.
6+
7+
Features:
8+
* drawers on left, right, top and bottom
9+
* swipe to open
10+
* swipe to close
11+
* tap outside to close
12+
* progressively dim main content as the drawer is opened
13+
14+
## Quick Start
15+
16+
```bash
17+
$ npm i --save nativescript-vue-multi-drawer
18+
```
19+
20+
21+
```diff
22+
// main.js
23+
import Vue from 'nativescript-vue'
24+
...
25+
+ import MultiDrawer from 'nativescript-vue-multi-drawer'
26+
+ Vue.use(MultiDrawer)
27+
```
28+
29+
Optionally set default options by passing `options` when installing the plugin:
30+
```js
31+
Vue.use(MultiDrawer, {
32+
// override any option here
33+
// for example enable debug mode
34+
debug: true
35+
})
36+
```
37+
38+
```xml
39+
<MultiDrawer>
40+
<StackLayout slot="left">
41+
<Label text="Im in the left drawer" />
42+
</StackLayout>
43+
<StackLayout slot="right">
44+
<Label text="Im in the right drawer" />
45+
</StackLayout>
46+
<StackLayout slot="top">
47+
<Label text="Im in the top drawer" />
48+
</StackLayout>
49+
<StackLayout slot="bottom">
50+
<Label text="Im in the bottom drawer" />
51+
</StackLayout>
52+
53+
<Frame /> <!-- main content goes into the default slot -->
54+
</MultiDrawer>
55+
```
56+
57+
The component will only enable drawers that have contents in them, so if you only need a left and a right side drawer, you can just remove the top and bottom slots and it will work as expected.
58+

‎components/MultiDrawer.vue

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
<template>
2+
<GridLayout>
3+
<!-- Main Content (default slot) -->
4+
<slot/>
5+
6+
<Label v-show="backdropVisible"
7+
ref="backDrop"
8+
opacity="0"
9+
:backgroundColor="optionsInternal.backdropColor"
10+
@pan="onBackDropPan"
11+
@tap="close()"/>
12+
13+
<template v-for="side in computedSidesEnabled">
14+
<!-- Drawer Content -->
15+
<GridLayout @layoutChanged="onDrawerLayoutChange(side)"
16+
@tap="noop"
17+
@pan="onDrawerPan(side, $event)"
18+
:ref="`${side}Drawer`"
19+
:style="computedDrawerStyle(side)">
20+
<slot :name="side"/>
21+
</GridLayout>
22+
<!-- Open Trigger -->
23+
<Label v-show="computedShowSwipeOpenTrigger(side)"
24+
v-bind="computedSwipeOpenTriggerProperties(side)"
25+
@pan="onOpenTriggerPan(side, $event)"/>
26+
</template>
27+
</GridLayout>
28+
</template>
29+
30+
<script>
31+
import * as utils from 'tns-core-modules/utils/utils'
32+
import mergeOptions from 'merge-options'
33+
import {defaultOptions} from "../index";
34+
35+
export default {
36+
model: {
37+
prop: 'state',
38+
event: 'stateChange',
39+
},
40+
props: {
41+
enabled: {
42+
type: Boolean,
43+
default: true,
44+
},
45+
options: {
46+
type: Object,
47+
required: false,
48+
},
49+
state: {
50+
type: [String, Boolean],
51+
default: false,
52+
},
53+
},
54+
watch: {
55+
async state(side) {
56+
if (this.computedOpenSide !== side) {
57+
await this.close()
58+
}
59+
if (side) {
60+
this.open(side)
61+
}
62+
},
63+
options: {
64+
handler(options) {
65+
this.optionsInternal = mergeOptions(defaultOptions, options)
66+
},
67+
immediate: true,
68+
deep: true,
69+
},
70+
},
71+
data() {
72+
return {
73+
// handled by the watcher
74+
optionsInternal: {},
75+
sides: {
76+
left: {
77+
open: false,
78+
translationOffset: 0,
79+
},
80+
right: {
81+
open: false,
82+
translationOffset: 0,
83+
},
84+
top: {
85+
open: false,
86+
translationOffset: 0,
87+
},
88+
bottom: {
89+
open: false,
90+
translationOffset: 0,
91+
},
92+
},
93+
backdropVisible: false,
94+
isAnimating: false,
95+
isPanning: false,
96+
}
97+
},
98+
computed: {
99+
computedSidesEnabled() {
100+
const validSides = Object.keys(this.sides)
101+
return Object.keys(this.$slots).filter(slotName =>
102+
validSides.includes(slotName)
103+
)
104+
},
105+
computedDrawerStyle() {
106+
return side => ({
107+
transform: `translate${this.optionsInternal[side].axis}(${
108+
this.sides[side].open ? 0 : this.sides[side].translationOffset
109+
})`,
110+
...(this.optionsInternal[side].width
111+
? {width: this.optionsInternal[side].width}
112+
: {}),
113+
...(this.optionsInternal[side].height
114+
? {height: this.optionsInternal[side].height}
115+
: {}),
116+
backgroundColor: this.optionsInternal[side].backgroundColor,
117+
[this.optionsInternal[side].axis === 'X'
118+
? 'horizontalAlignment'
119+
: 'verticalAlignment']: side,
120+
})
121+
},
122+
computedSwipeOpenTriggerProperties() {
123+
return side => ({
124+
...(this.optionsInternal[side].swipeOpenTriggerWidth
125+
? {width: this.optionsInternal[side].swipeOpenTriggerWidth}
126+
: {}),
127+
...(this.optionsInternal[side].swipeOpenTriggerHeight
128+
? {height: this.optionsInternal[side].swipeOpenTriggerHeight}
129+
: {}),
130+
[this.optionsInternal[side].axis === 'X'
131+
? 'horizontalAlignment'
132+
: 'verticalAlignment']: side,
133+
...(this.optionsInternal.debug
134+
? {backgroundColor: 'rgba(0, 255, 0, 0.3)'}
135+
: {}),
136+
...this.optionsInternal[side].swipeOpenTriggerProperties,
137+
})
138+
},
139+
computedShowSwipeOpenTrigger() {
140+
return side => {
141+
if (!this.optionsInternal[side].canSwipeOpen) {
142+
return false
143+
}
144+
return !(this.computedOpenSide || this.isPanning || this.isAnimating)
145+
}
146+
},
147+
computedOpenSide() {
148+
return (
149+
this.computedSidesEnabled.find(side => this.sides[side].open) || false
150+
)
151+
},
152+
},
153+
methods: {
154+
noop() {
155+
// helper for catching events that we don't want to pass through.
156+
},
157+
async open(side = null) {
158+
if (!side) {
159+
if (!this.computedSidesEnabled.length) {
160+
throw new Error(
161+
'No sides are enabled, at least one side must be enabled to open the drawer'
162+
)
163+
}
164+
side = this.computedSidesEnabled[0]
165+
}
166+
167+
if (!this.computedSidesEnabled.includes(side)) {
168+
return
169+
}
170+
171+
if (this.isPanning || this.isAnimating) {
172+
return
173+
}
174+
175+
this.isPanning = false
176+
this.isAnimating = true
177+
this.backdropVisible = true
178+
179+
const duration = this.optionsInternal[side].animation.openDuration
180+
181+
this.$refs.backDrop.nativeView.animate({
182+
opacity: 1,
183+
duration,
184+
})
185+
await this.$refs[`${side}Drawer`][0].nativeView.animate({
186+
translate: {
187+
x: 0,
188+
y: 0,
189+
},
190+
duration,
191+
})
192+
193+
this.sides[side].open = true
194+
this.isAnimating = false
195+
this.$emit('stateChange', side)
196+
},
197+
async close(side = null) {
198+
if (this.isAnimating) {
199+
return
200+
}
201+
if (!side) {
202+
side = this.computedOpenSide
203+
}
204+
if (!side) {
205+
return
206+
}
207+
208+
this.isPanning = false
209+
this.isAnimating = true
210+
211+
const duration = this.optionsInternal[side].animation.closeDuration
212+
213+
this.$refs[`${side}Drawer`][0].nativeView.animate({
214+
translate: {
215+
...(this.optionsInternal[side].axis === 'X'
216+
? {x: this.sides[side].translationOffset}
217+
: {x: 0}),
218+
...(this.optionsInternal[side].axis === 'Y'
219+
? {y: this.sides[side].translationOffset}
220+
: {y: 0}),
221+
},
222+
duration,
223+
})
224+
await this.$refs.backDrop.nativeView.animate({
225+
opacity: 0,
226+
duration,
227+
})
228+
229+
this.sides[side].open = false
230+
this.backdropVisible = false
231+
this.isAnimating = false
232+
this.$emit('stateChange', false)
233+
},
234+
onDrawerLayoutChange(side) {
235+
const view = this.$refs[`${side}Drawer`][0].nativeView
236+
this.sides[side].translationOffset =
237+
this.optionsInternal[side].translationOffsetMultiplier *
238+
utils.layout.toDeviceIndependentPixels(
239+
this.optionsInternal[side].axis === 'X'
240+
? view.getMeasuredWidth()
241+
: view.getMeasuredHeight()
242+
)
243+
},
244+
onBackDropPan(args) {
245+
this.onDrawerPan(this.computedOpenSide, args)
246+
},
247+
onOpenTriggerPan(side, args) {
248+
this.onDrawerPan(side, args)
249+
},
250+
onDrawerPan(side, args) {
251+
if ((this.isPanning && this.isPanning !== side) || this.isAnimating) {
252+
return
253+
}
254+
if (!side) {
255+
return
256+
}
257+
const view = this.$refs[`${side}Drawer`][0].nativeView
258+
let panProgress = 0
259+
260+
if (args.state === 1) {
261+
// down
262+
this.isPanning = side
263+
264+
if (!this.sides[side].open) {
265+
this.$refs.backDrop.nativeView.opacity = 0
266+
this.backdropVisible = true
267+
}
268+
269+
this.prevDeltaX = 0
270+
this.prevDeltaY = 0
271+
} else if (args.state === 2) {
272+
// panning
273+
274+
if (this.optionsInternal[side].axis === 'X') {
275+
this.constrainX(
276+
view,
277+
side,
278+
view.translateX + (args.deltaX - this.prevDeltaX)
279+
)
280+
panProgress =
281+
Math.abs(view.translateX) /
282+
Math.abs(this.sides[side].translationOffset)
283+
} else {
284+
this.constrainY(
285+
view,
286+
side,
287+
view.translateY + (args.deltaY - this.prevDeltaY)
288+
)
289+
panProgress =
290+
Math.abs(view.translateY) /
291+
Math.abs(this.sides[side].translationOffset)
292+
}
293+
294+
this.prevDeltaX = args.deltaX
295+
this.prevDeltaY = args.deltaY
296+
297+
this.$refs.backDrop.nativeView.opacity = 1 - panProgress
298+
} else if (args.state === 3) {
299+
// up
300+
this.isPanning = false
301+
302+
if (this.computedOpenSide === side) {
303+
// already open
304+
let distanceFromFullyOpen = 0
305+
if (this.optionsInternal[side].axis === 'X') {
306+
distanceFromFullyOpen = Math.abs(view.translateX)
307+
} else {
308+
distanceFromFullyOpen = Math.abs(view.translateY)
309+
}
310+
if (
311+
distanceFromFullyOpen >
312+
this.optionsInternal[side].swipeCloseTriggerMinDrag
313+
) {
314+
this.close(side)
315+
} else {
316+
this.open(side)
317+
}
318+
} else {
319+
const offsetAbs = Math.abs(this.sides[side].translationOffset)
320+
const multiplier = this.optionsInternal[side]
321+
.translationOffsetMultiplier
322+
let distanceFromEdge = 0
323+
if (this.optionsInternal[side].axis === 'X') {
324+
distanceFromEdge = offsetAbs - multiplier * view.translateX
325+
} else {
326+
distanceFromEdge = offsetAbs - multiplier * view.translateY
327+
}
328+
329+
if (
330+
distanceFromEdge <
331+
this.optionsInternal[side].swipeOpenTriggerMinDrag
332+
) {
333+
this.close(side)
334+
} else {
335+
this.open(side)
336+
}
337+
}
338+
339+
this.prevDeltaX = 0
340+
this.prevDeltaY = 0
341+
}
342+
},
343+
constrainX(view, side, x) {
344+
const offset = this.sides[side].translationOffset
345+
if (offset < 0) {
346+
if (x > 0) {
347+
view.translateX = 0
348+
} else if (this.sides[side].open && x < offset) {
349+
view.translateX = offset
350+
} else {
351+
view.translateX = x
352+
}
353+
} else {
354+
if (x < 0) {
355+
view.translateX = 0
356+
} else if (this.sides[side].open && x > offset) {
357+
view.translateX = offset
358+
} else {
359+
view.translateX = x
360+
}
361+
}
362+
},
363+
constrainY(view, side, y) {
364+
const offset = this.sides[side].translationOffset
365+
if (offset < 0) {
366+
if (y > 0) {
367+
view.translateY = 0
368+
} else if (this.sides[side].open && y < offset) {
369+
view.translateY = offset
370+
} else {
371+
view.translateY = y
372+
}
373+
} else {
374+
if (y < 0) {
375+
view.translateY = 0
376+
} else if (this.sides[side].open && y > offset) {
377+
view.translateY = offset
378+
} else {
379+
view.translateY = y
380+
}
381+
}
382+
},
383+
},
384+
}
385+
</script>

‎index.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import mergeOptions from 'merge-options'
2+
3+
import MultiDrawer from './components/MultiDrawer'
4+
5+
export let defaultOptions = {
6+
debug: false,
7+
backdropColor: 'rgba(0, 0, 0, 0.7)',
8+
left: {
9+
width: '70%',
10+
height: null,
11+
backgroundColor: '#ffffff',
12+
canSwipeOpen: true,
13+
swipeOpenTriggerWidth: 30,
14+
swipeOpenTriggerHeight: null,
15+
swipeOpenTriggerMinDrag: 50,
16+
swipeCloseTriggerMinDrag: 50,
17+
swipeOpenTriggerProperties: {},
18+
animation: {
19+
openDuration: 300,
20+
closeDuration: 300,
21+
},
22+
translationOffsetMultiplier: -1,
23+
axis: 'X',
24+
},
25+
right: {
26+
width: '70%',
27+
height: null,
28+
backgroundColor: '#ffffff',
29+
canSwipeOpen: true,
30+
swipeOpenTriggerWidth: 30,
31+
swipeOpenTriggerHeight: null,
32+
swipeOpenTriggerMinDrag: 50,
33+
swipeCloseTriggerMinDrag: 50,
34+
swipeOpenTriggerProperties: {},
35+
animation: {
36+
openDuration: 300,
37+
closeDuration: 300,
38+
},
39+
translationOffsetMultiplier: 1,
40+
axis: 'X',
41+
},
42+
top: {
43+
width: null,
44+
height: '40%',
45+
backgroundColor: '#ffffff',
46+
canSwipeOpen: true,
47+
swipeOpenTriggerWidth: null,
48+
swipeOpenTriggerHeight: 50,
49+
swipeOpenTriggerMinDrag: 50,
50+
swipeCloseTriggerMinDrag: 50,
51+
swipeOpenTriggerProperties: {},
52+
animation: {
53+
openDuration: 300,
54+
closeDuration: 300,
55+
},
56+
translationOffsetMultiplier: -1,
57+
axis: 'Y',
58+
},
59+
bottom: {
60+
width: null,
61+
height: '40%',
62+
backgroundColor: '#ffffff',
63+
canSwipeOpen: true,
64+
swipeOpenTriggerWidth: null,
65+
swipeOpenTriggerHeight: 30,
66+
swipeOpenTriggerMinDrag: 50,
67+
swipeCloseTriggerMinDrag: 50,
68+
swipeOpenTriggerProperties: {},
69+
animation: {
70+
openDuration: 300,
71+
closeDuration: 300,
72+
},
73+
translationOffsetMultiplier: 1,
74+
axis: 'Y',
75+
},
76+
}
77+
78+
export default function install(Vue, options) {
79+
if(options) {
80+
defaultOptions = mergeOptions(defaultOptions, options)
81+
}
82+
83+
Vue.component('MultiDrawer', MultiDrawer)
84+
}

‎package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "nativescript-vue-multi-drawer",
3+
"version": "0.0.1",
4+
"description": "A NativeScript-Vue component for creating multiple side drawers",
5+
"main": "index.js",
6+
"files": [
7+
"index.js",
8+
"components"
9+
],
10+
"scripts": {
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"keywords": [
14+
"nativescript",
15+
"nativescript-vue",
16+
"drawer",
17+
"sidedrawer",
18+
"multi",
19+
"multiple"
20+
],
21+
"author": "Igor Randjelovic",
22+
"license": "MIT",
23+
"dependencies": {
24+
"merge-options": "^1.0.1"
25+
},
26+
"peerDependencies": {
27+
"tns-core-modules": "*"
28+
}
29+
}

0 commit comments

Comments
 (0)
Please sign in to comment.