2
0
mirror of https://github.com/tenrok/vue-form-wizard.git synced 2026-05-21 17:24:04 +03:00
Files
vue-form-wizard/src/components/FormWizard.vue
T
cristijora 17cbb17b5d #15 Trigger back only on back button
Add media query for mobile < 350px
2017-06-04 11:54:58 +03:00

398 lines
11 KiB
Vue

<template>
<div class="vue-form-wizard">
<div class="wizard-header">
<slot name="title">
<h4 class="wizard-title">{{title}}</h4>
<p class="category">{{subtitle}}</p>
</slot>
</div>
<div class="wizard-navigation">
<div class="wizard-progress-with-circle">
<div class="wizard-progress-bar"
:style="progressBarStyle"></div>
</div>
<ul class="wizard-nav wizard-nav-pills">
<li v-for="(tab, index) in tabs" :class="{active:tab.active}">
<a href="" @click.prevent="navigateToTab(index)">
<div class="wizard-icon-circle"
:class="{checked:isChecked(index),square_shape:isStepSquare, tab_shape:isTabShape}"
:style="[isChecked(index)? stepCheckedStyle : {}, tab.validationError ? errorStyle : {}]">
<transition :name="transition" mode="out-in">
<div v-if="tab.active" class="wizard-icon-container"
:class="{square_shape:isStepSquare, tab_shape:isTabShape}"
:style="[tab.active ? iconActiveStyle: {}, tab.validationError ? errorStyle : {}]">
<i v-if="tab.icon" :class="tab.icon" class="wizard-icon"></i>
<i v-else class="wizard-icon">{{index + 1}}</i>
</div>
<i v-if="!tab.active && tab.icon" :class="tab.icon" class="wizard-icon"></i>
<i v-if="!tab.active && !tab.icon" class="wizard-icon">{{index + 1}}</i>
</transition>
</div>
<span class="stepTitle"
:class="{active:tab.active, has_error:tab.validationError}"
:style="tab.active ? stepTitleStyle : {}">
{{tab.title}}
</span>
</a>
</li>
</ul>
<div class="wizard-tab-content">
<slot>
</slot>
</div>
</div>
<div class="wizard-card-footer clearfix" v-if="!hideButtons">
<template>
<span @click="prevTab" v-if="displayPrevButton" class="wizard-footer-left">
<slot name="prev">
<button type="button" class="wizard-btn btn-default wizard-btn-wd" :style="fillButtonStyle"
:disabled="loading">
{{backButtonText}}
</button>
</slot>
</span>
</template>
<template>
<span @click="finish" class="wizard-footer-right" v-if="isLastStep">
<slot name="finish">
<button type="button" class="wizard-btn btn-fill wizard-btn-wd btn-next" :style="fillButtonStyle">
{{finishButtonText}}
</button>
</slot>
</span>
</template>
<template>
<span @click="nextTab" class="wizard-footer-right" v-if="!isLastStep">
<slot name="next">
<button type="button" class="wizard-btn btn-fill wizard-btn-wd btn-next" :style="fillButtonStyle"
:disabled="loading">
{{nextButtonText}}
</button>
</slot>
</span>
</template>
</div>
</div>
</template>
<script>
export default{
props: {
title: {
type: String,
default: 'Awesome Wizard'
},
subtitle: {
type: String,
default: 'Split a complicated flow in multiple steps'
},
nextButtonText: {
type: String,
default: 'Next'
},
backButtonText: {
type: String,
default: 'Back'
},
finishButtonText: {
type: String,
default: 'Finish'
},
hideButtons: {
type: Boolean,
default: false
},
validateOnBack: Boolean,
/***
* Applies to text, border and circle
*/
color: {
type: String,
default: '#e74c3c'
},
errorColor: {
type: String,
default: '#8b0000'
},
shape: {
type: String,
default: 'circle'
},
/**
* Name of the transition when transition between steps
* */
transition: {
type: String,
default: ''
},
/***
*
* Index of the initial tab to display
*/
startIndex: {
type: Number,
default: 0,
validator: (value) => {
return value >= 0
}
}
},
data () {
return {
activeTabIndex: 0,
isLastStep: false,
currentPercentage: 0,
maxStep: 0,
loading: false,
tabs: []
}
},
computed: {
tabCount () {
return this.tabs.length
},
displayPrevButton () {
return this.activeTabIndex !== 0
},
stepPercentage () {
return 1 / (this.tabCount * 2) * 100
},
progressBarStyle () {
return {
backgroundColor: this.color,
width: `${this.progress}%`,
color: this.color
}
},
iconActiveStyle () {
return {
backgroundColor: this.color
}
},
stepCheckedStyle () {
return {
borderColor: this.color
}
},
errorStyle () {
return {
borderColor: this.errorColor,
backgroundColor: this.errorColor
}
},
stepTitleStyle () {
var isError = this.tabs[this.activeTabIndex].validationError
return {
color: isError ? this.errorColor : this.color
}
},
isStepSquare () {
return this.shape === 'square'
},
isTabShape () {
return this.shape === 'tab'
},
fillButtonStyle () {
return {
backgroundColor: this.color,
borderColor: this.color,
color: 'white'
}
},
progress () {
let percentage = 0
if (this.activeTabIndex > 0) {
let stepsToAdd = 1
let stepMultiplier = 2
percentage = this.stepPercentage * ((this.activeTabIndex * stepMultiplier) + stepsToAdd)
} else {
percentage = this.stepPercentage
}
return percentage
}
},
methods: {
isChecked (index) {
return index <= this.maxStep
},
navigateToTab (index, validate = true) {
if (index <= this.maxStep) {
let cb = () => {
this.changeTab(this.activeTabIndex, index)
}
if (validate) {
this.beforeTabChange(this.activeTabIndex, cb)
} else {
cb()
}
}
},
setLoading (value) {
this.loading = value
this.$emit('on-loading', value)
},
setValidationError (error) {
this.tabs[this.activeTabIndex].validationError = error
this.$emit('on-error', error)
},
validateBeforeChange (promiseFn, callback) {
this.setValidationError(null)
// we have a promise
if (promiseFn.then && typeof promiseFn.then === 'function') {
this.setLoading(true)
promiseFn.then((res) => {
this.setLoading(false)
let validationResult = res === true
this.executeBeforeChange(validationResult, callback)
}).catch((error) => {
this.setLoading(false)
this.setValidationError(error)
})
// we have a simple function
} else {
let validationResult = promiseFn === true
this.executeBeforeChange(validationResult, callback)
}
},
executeBeforeChange (validationResult, callback) {
this.$emit('on-validate', validationResult, this.activeTabIndex)
if (validationResult) {
callback()
} else {
this.tabs[this.activeTabIndex].validationError = 'error'
}
},
beforeTabChange (index, callback) {
if (this.loading) {
return
}
let oldTab = this.tabs[index]
if (oldTab && oldTab.beforeChange !== undefined) {
let tabChangeRes = oldTab.beforeChange()
this.validateBeforeChange(tabChangeRes, callback)
} else {
callback()
}
},
changeTab (oldIndex, newIndex) {
let oldTab = this.tabs[oldIndex]
let newTab = this.tabs[newIndex]
if (oldTab) {
oldTab.active = false
}
if (newTab) {
newTab.active = true
}
this.activeTabIndex = newIndex
this.checkStep()
this.tryChangeRoute(newTab)
this.increaseMaxStep()
return true
},
tryChangeRoute (tab) {
if (this.$router && tab.route) {
this.$router.push(tab.route)
}
},
checkStep () {
if (this.activeTabIndex === this.tabCount - 1) {
this.isLastStep = true
} else {
this.isLastStep = false
}
},
increaseMaxStep () {
if (this.activeTabIndex > this.maxStep) {
this.maxStep = this.activeTabIndex
}
},
nextTab () {
let cb = () => {
if (this.activeTabIndex < this.tabCount - 1) {
this.changeTab(this.activeTabIndex, this.activeTabIndex + 1)
} else {
this.isLastStep = true
this.$emit('finished')
}
}
this.beforeTabChange(this.activeTabIndex, cb)
},
prevTab () {
let cb = () => {
if (this.activeTabIndex > 0) {
this.setValidationError(null)
this.changeTab(this.activeTabIndex, this.activeTabIndex - 1)
this.isLastStep = false
}
}
if (this.validateOnBack) {
this.beforeTabChange(this.activeTabIndex, cb)
} else {
cb()
}
},
finish () {
let cb = () => {
this.$emit('on-complete')
}
this.beforeTabChange(this.activeTabIndex, cb)
},
checkRouteChange (route) {
let matchingTabIndex = -1
let matchingTab = this.tabs.find((tab, index) => {
let match = tab.route === route
if (match) {
matchingTabIndex = index
}
return match
})
if (matchingTab && !matchingTab.active) {
const shouldValidate = matchingTabIndex > this.activeTabIndex
this.navigateToTab(matchingTabIndex, shouldValidate)
}
}
},
mounted () {
this.tabs = this.$children.filter((comp) => comp.$options.name === 'tab-content')
if (this.tabs.length > 0 && this.startIndex === 0) {
let firstTab = this.tabs[this.activeTabIndex]
firstTab.active = true
this.tryChangeRoute(firstTab)
}
if (this.startIndex < this.tabs.length) {
let tabToActivate = this.tabs[this.startIndex]
this.activeTabIndex = this.startIndex
tabToActivate.active = true
this.maxStep = this.startIndex
this.tryChangeRoute(this.tabs[this.startIndex])
} else {
console.warn(`Prop startIndex set to ${this.startIndex} is greater than the number of tabs - ${this.tabs.length}. Make sure that the starting index is less than the number of tabs registered`)
}
},
watch: {
'$route.path': function (newRoute) {
this.checkRouteChange(newRoute)
}
}
}
</script>
<style lang="scss">
@import "./../assets/wizard";
.fade-enter-active, .fade-leave-active {
transition: opacity .15s
}
.fade-enter, .fade-leave-to /* .fade-leave-active in <2.1.8 */
{
opacity: 0
}
</style>