init crater

This commit is contained in:
Mohit Panjwani
2019-11-11 12:16:00 +05:30
commit bdf2ba51d6
668 changed files with 158503 additions and 0 deletions

View File

@ -0,0 +1,40 @@
/**
* First we will load all of this project's JavaScript dependencies which
* include Vue and Vue Resource. This gives a great starting point for
* building robust, powerful web applications using Vue and Laravel.
*/
import router from './router.js'
import Layout from './helpers/layout'
import Plugin from './helpers/plugin'
import store from './store/index'
import utils from './helpers/utilities'
import { mapActions, mapGetters } from 'vuex'
import i18n from './plugins/i18n'
import swal from 'sweetalert'
require('./bootstrap')
Vue.prototype.$utils = utils
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
window.hub = new Vue()
window.i18n = i18n
window.Plugin = Plugin
const app = new Vue({
router,
store,
i18n,
swal,
computed: {
...mapGetters([
'isAdmin'
])
},
methods: {
onOverlayClick () {
this.$utils.toggleSidebar()
}
}
}).$mount('#app')

122
resources/assets/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,122 @@
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import Ls from './services/ls'
import store from './store/index.js'
import Vue from 'vue'
import Vuelidate from 'vuelidate'
import VDropdown from './components/dropdown/VDropdown.vue'
import VDropdownItem from './components/dropdown/VDropdownItem.vue'
import VDropdownDivider from './components/dropdown/VDropdownDivider.vue'
import DotIcon from './components/icon/DotIcon.vue'
import CustomerModal from './components/base/modal/CustomerModal.vue'
import TaxTypeModal from './components/base/modal/TaxTypeModal.vue'
import CategoryModal from './components/base/modal/CategoryModal.vue'
import money from 'v-money'
/**
* Global css plugins
*/
import 'vue-tabs-component/docs/resources/tabs-component.css'
Vue.use(Vuelidate)
window._ = require('lodash')
/**
* Vue is a modern JavaScript library for building interactive web interfaces
* using reactive data binding and reusable components. Vue's API is clean
* and simple, leaving you to focus on building your next great project.
*/
window.Vue = require('vue')
/**
* Font Awesome
*/
require('../plugins/vue-font-awesome/index')
/**
* Custom Directives
*/
require('./helpers/directives')
/**
* Base Components
*/
require('./components/base')
/**
* We'll register a HTTP interceptor to attach the "CSRF" header to each of
* the outgoing requests issued by this application. The CSRF middleware
* included with Laravel will automatically verify the header's value.
*/
window.axios = require('axios')
window.Ls = Ls
global.$ = global.jQuery = require('jquery')
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest'
}
/**
* Interceptors
*/
window.axios.interceptors.request.use(function (config) {
// Do something before request is sent
const AUTH_TOKEN = Ls.get('auth.token')
const companyId = Ls.get('selectedCompany')
if (AUTH_TOKEN) {
config.headers.common['Authorization'] = `Bearer ${AUTH_TOKEN}`
}
if (companyId) {
config.headers.common['company'] = companyId
}
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})
/**
* Global Axios Response Interceptor
*/
global.axios.interceptors.response.use(undefined, function (err) {
// Do something with request error
return new Promise((resolve, reject) => {
console.log(err.response)
if (err.response.data.error === 'invalid_credentials') {
window.toastr['error']('Invalid Credentials')
}
if (err.response.data && err.response.statusText === 'Unauthorized') {
store.dispatch('auth/logout', true)
} else {
throw err
}
})
})
/**
* Global plugins
*/
window.toastr = require('toastr')
Vue.use(VueRouter)
Vue.use(Vuex)
// register directive v-money and component <money>
Vue.use(money, {precision: 2})
Vue.component('v-dropdown', VDropdown)
Vue.component('v-dropdown-item', VDropdownItem)
Vue.component('v-dropdown-divider', VDropdownDivider)
Vue.component('dot-icon', DotIcon)
Vue.component('customer-modal', CustomerModal)
Vue.component('tax-type-modal', TaxTypeModal)
Vue.component('category-modal', CategoryModal)

View File

@ -0,0 +1,175 @@
<template>
<button :type="type" :class="btnClass" :disabled="disabled" @click="handleClick">
<font-awesome-icon v-if="icon && !loading && !rightIcon" :class="iconClass" :icon="icon" class="vue-icon icon-left" />
<font-awesome-icon v-if="loading" :class="iconClass" icon="spinner" class="fa-spin"/>
<slot />
<font-awesome-icon v-if="icon && !loading && rightIcon" :class="iconClass" :icon="icon" class="vue-icon icon-right" />
</button>
</template>
<script>
export default {
props: {
icon: {
type: String,
required: false,
default: ''
},
color: {
type: String,
required: false,
default: ''
},
round: {
type: Boolean,
required: false,
default: false
},
outline: {
type: Boolean,
required: false,
default: false
},
size: {
type: String,
required: false,
default: 'default'
},
loading: {
type: Boolean,
required: false,
default: false
},
block: {
type: Boolean,
required: false,
default: false
},
iconButton: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
},
rightIcon: {
type: Boolean,
required: false,
default: false
},
type: {
type: String,
required: false,
default: 'button'
}
},
computed: {
btnClass () {
if (this.isCustomStyle) {
return ''
}
let btnClass = 'base-button '
switch (this.color) {
case 'success':
if (this.outline) {
btnClass += `btn btn-outline-success `
} else {
btnClass += `btn btn-success `
}
break
case 'danger':
if (this.outline) {
btnClass += `btn btn-outline-danger `
} else {
btnClass += `btn btn-danger `
}
break
case 'warning':
if (this.outline) {
btnClass += `btn btn-outline-warning `
} else {
btnClass += `btn btn-warning `
}
break
case 'info':
if (this.outline) {
btnClass += `btn btn-outline-info `
} else {
btnClass += `btn btn-info `
}
break
case 'theme':
if (this.outline) {
btnClass += `btn btn-outline-primary `
} else {
btnClass += `btn btn-primary `
}
break
case 'theme-light':
if (this.outline) {
btnClass += `btn btn-outline-light `
} else {
btnClass += `btn btn-light `
}
break
default:
if (this.outline) {
btnClass += `btn btn-outline-dark `
} else {
btnClass += `btn btn-dark `
}
break
}
switch (this.size) {
case 'large':
btnClass += 'btn-lg '
break
case 'small':
btnClass += 'btn-sm '
break
default:
btnClass += 'default-size '
}
if (this.block) {
btnClass += 'btn-block '
}
if (this.disabled) {
btnClass += ' btn-cursor-not-allowed'
}
return btnClass
},
iconClass () {
if (this.loading || !this.iconButton) {
if (this.rightIcon) {
return 'ml-2'
}
return 'mr-2'
}
return 'icon-button'
}
},
methods: {
handleClick (e) {
this.$emit('click')
}
}
}
</script>

View File

@ -0,0 +1,60 @@
<template>
<div class="item-selector">
<base-select
ref="baseSelect"
v-model="customerSelect"
:options="customers"
:show-labels="false"
:preserve-search="false"
:placeholder="$t('customers.type_or_click')"
label="name"
class="multi-select-item"
@close="checkCustomers"
@value="onTextChange"
@select="(val) => $emit('select', val)"
@remove="deselectCustomer"
/>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
customerSelect: null,
loading: false
}
},
computed: {
...mapGetters('customer', [
'customers'
])
},
methods: {
...mapActions('customer', [
'fetchCustomers'
]),
async searchCustomers (search) {
this.loading = true
await this.fetchCustomers({search})
this.loading = false
},
onTextChange (val) {
this.searchCustomers(val)
},
checkCustomers (val) {
if (!this.customers.length) {
this.fetchCustomers()
}
},
deselectCustomer () {
this.customerSelect = null
this.$emit('deselect')
}
}
}
</script>

View File

@ -0,0 +1,133 @@
<template>
<div class="base-input">
<font-awesome-icon v-if="icon && isAlignLeftIcon" :icon="icon" class="left-icon"/>
<input
ref="baseInput"
v-model="inputValue"
:type="type"
:disabled="disabled"
:readonly="readOnly"
:name="name"
:tabindex="tabIndex"
:class="[{'input-field-left-icon': icon && isAlignLeftIcon ,'input-field-right-icon': icon && !isAlignLeftIcon ,'invalid': isFieldValid, 'disabled': disabled, 'small-input': small}, inputClass]"
:placeholder="placeholder"
:autocomplete="autocomplete"
class="input-field"
@input="handleInput"
@change="handleChange"
@keyup="handleKeyupEnter"
@keydown="handleKeyDownEnter"
@blur="handleFocusOut"
>
<font-awesome-icon v-if="icon && !isAlignLeftIcon" :icon="icon" class="right-icon" />
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
default: ''
},
type: {
type: String,
default: 'text'
},
tabIndex: {
type: String,
default: ''
},
value: {
type: [String, Number, File],
default: ''
},
placeholder: {
type: String,
default: ''
},
invalid: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
readOnly: {
type: Boolean,
default: false
},
icon: {
type: String,
default: ''
},
inputClass: {
type: String,
default: ''
},
small: {
type: Boolean,
default: false
},
alignIcon: {
type: String,
default: 'left'
},
autocomplete: {
type: String,
default: 'on'
}
},
data () {
return {
inputValue: this.value,
focus: false
}
},
computed: {
isFieldValid () {
return this.invalid
},
isAlignLeftIcon () {
if (this.alignIcon === 'left') {
return true
}
return false
}
},
watch: {
'value' () {
this.inputValue = this.value
},
focus () {
this.focusInput()
}
},
mounted () {
this.focusInput()
},
methods: {
focusInput () {
if (this.focus) {
this.$refs.baseInput.focus()
}
},
handleInput (e) {
this.$emit('input', this.inputValue)
},
handleChange (e) {
this.$emit('change', this.inputValue)
},
handleKeyupEnter (e) {
this.$emit('keyup', this.inputValue)
},
handleKeyDownEnter (e) {
this.$emit('keydown', e, this.inputValue)
},
handleFocusOut (e) {
this.$emit('blur', this.inputValue)
}
}
}
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="base-loader">
<div class="spinner"/>
<div class="overlay">
<div class="loader-inner ball-scale-ripple-multiple">
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.overlay {
height: 100%;
width: 100%;
background: rgba(255,255,255,0.4);
position: absolute;
top: 7%;
left: 13%;
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="base-switch">
<input
:id="uniqueId"
v-model="checkValue"
type="checkbox"
@input="handleInput"
@change="handleChange"
@keyup="handleKeyupEnter"
@blur="handleFocusOut"
>
<label class="switch-label" :for="uniqueId"/>
</div>
</template>
<script>
export default {
props: {
value: {
type: Boolean,
required: false,
default: false
},
disabled: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
id: null,
checkValue: this.value
}
},
computed: {
uniqueId () {
return '_' + Math.random().toString(36).substr(2, 9)
}
},
watch: {
'value' () {
this.checkValue = this.value
}
},
methods: {
handleInput (e) {
this.$emit('input', e.target.checked)
},
handleChange (e) {
this.$emit('change', this.checkValue)
},
handleKeyupEnter (e) {
this.$emit('keyup', this.checkValue)
},
handleFocusOut (e) {
this.$emit('blur', this.checkValue)
}
}
}
</script>
<style scoped>
/* .switch-label {
margin-bottom: 3px !important
} */
</style>

View File

@ -0,0 +1,79 @@
<template>
<textarea
v-model="inputValue"
:rows="rows"
:cols="cols"
:disabled="disabled"
:class="['base-text-area',{'invalid': isFieldValid, 'disabled': disabled}, inputClass]"
:placeholder="placeholder"
class="text-area-field"
@input="handleInput"
@change="handleChange"
@keyup="handleKeyupEnter"
/>
</template>
<script>
export default {
props: {
rows: {
type: String,
default: '4'
},
cols: {
type: String,
default: '10'
},
value: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
invalid: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
icon: {
type: String,
default: ''
},
inputClass: {
type: String,
default: ''
}
},
data () {
return {
inputValue: this.value
}
},
computed: {
isFieldValid () {
return this.invalid
}
},
watch: {
'value' () {
this.inputValue = this.value
}
},
methods: {
handleInput (e) {
this.$emit('input', this.inputValue)
},
handleChange (e) {
this.$emit('change', this.inputValue)
},
handleKeyupEnter (e) {
this.$emit('keyup', this.inputValue)
}
}
}
</script>

View File

@ -0,0 +1,649 @@
<template>
<div :class="[wrapperClass, isRtl ? 'rtl' : '']" class="base-date-input">
<date-input
:selected-date="selectedDate"
:reset-typed-date="resetTypedDate"
:format="customFormatter"
:translation="translation"
:inline="inline"
:id="id"
:name="name"
:ref-name="refName"
:open-date="openDate"
:placeholder="placeholder"
:input-class="inputClass"
:typeable="typeable"
:clear-button="clearButton"
:clear-button-icon="clearButtonIcon"
:calendar-button="calendarButton"
:calendar-button-icon="calendarButtonIcon"
:calendar-button-icon-content="calendarButtonIconContent"
:disabled="disabled"
:required="required"
:class="{'required-date': invalid}"
:bootstrap-styling="bootstrapStyling"
:use-utc="useUtc"
@showCalendar="showCalendar"
@closeCalendar="close"
@typedDate="setTypedDate"
@clearDate="clearDate">
<slot slot="afterDateInput" name="afterDateInput"/>
</date-input>
<!-- Day View -->
<picker-day
v-if="allowedToShowView('day')"
:page-date="pageDate"
:selected-date="selectedDate"
:show-day-view="showDayView"
:full-month-name="fullMonthName"
:allowed-to-show-view="allowedToShowView"
:disabled-dates="disabledDates"
:highlighted="highlighted"
:calendar-class="calendarClass"
:calendar-style="calendarStyle"
:translation="translation"
:page-timestamp="pageTimestamp"
:is-rtl="isRtl"
:monday-first="mondayFirst"
:day-cell-content="dayCellContent"
:use-utc="useUtc"
@changedMonth="handleChangedMonthFromDayPicker"
@selectDate="selectDate"
@showMonthCalendar="showMonthCalendar"
@selectedDisabled="selectDisabledDate">
<slot slot="beforeCalendarHeader" name="beforeCalendarHeader"/>
</picker-day>
<!-- Month View -->
<picker-month
v-if="allowedToShowView('month')"
:page-date="pageDate"
:selected-date="selectedDate"
:show-month-view="showMonthView"
:allowed-to-show-view="allowedToShowView"
:disabled-dates="disabledDates"
:calendar-class="calendarClass"
:calendar-style="calendarStyle"
:translation="translation"
:is-rtl="isRtl"
:use-utc="useUtc"
@selectMonth="selectMonth"
@showYearCalendar="showYearCalendar"
@changedYear="setPageDate">
<slot slot="beforeCalendarHeader" name="beforeCalendarHeader"/>
</picker-month>
<!-- Year View -->
<picker-year
v-if="allowedToShowView('year')"
:page-date="pageDate"
:selected-date="selectedDate"
:show-year-view="showYearView"
:allowed-to-show-view="allowedToShowView"
:disabled-dates="disabledDates"
:calendar-class="calendarClass"
:calendar-style="calendarStyle"
:translation="translation"
:is-rtl="isRtl"
:use-utc="useUtc"
@selectYear="selectYear"
@changedDecade="setPageDate">
<slot slot="beforeCalendarHeader" name="beforeCalendarHeader"/>
</picker-year>
</div>
</template>
<script>
import en from './src/locale/translations/en'
import DateInput from './DateInput'
import PickerDay from './PickerDay.vue'
import PickerMonth from './PickerMonth.vue'
import PickerYear from './PickerYear.vue'
import utils, { makeDateUtils } from './src/DateUtils'
import { mapGetters } from 'vuex'
import moment from 'moment'
export default {
components: {
DateInput,
PickerDay,
PickerMonth,
PickerYear
},
props: {
value: {
validator: val => utils.validateDateInput(val)
},
name: String,
refName: String,
id: String,
// format: {
// type: [String, Function],
// default: 'dd MMM yyyy'
// },
language: {
type: Object,
default: () => en
},
openDate: {
validator: val => utils.validateDateInput(val)
},
dayCellContent: Function,
fullMonthName: Boolean,
disabledDates: Object,
highlighted: Object,
placeholder: String,
inline: Boolean,
calendarClass: [String, Object, Array],
inputClass: [String, Object, Array],
wrapperClass: [String, Object, Array],
mondayFirst: Boolean,
clearButton: Boolean,
clearButtonIcon: String,
calendarButton: Boolean,
calendarButtonIcon: String,
calendarButtonIconContent: String,
bootstrapStyling: Boolean,
initialView: String,
disabled: Boolean,
required: Boolean,
invalid: Boolean,
typeable: Boolean,
useUtc: Boolean,
minimumView: {
type: String,
default: 'day'
},
maximumView: {
type: String,
default: 'year'
}
},
data () {
const startDate = this.openDate ? new Date(this.openDate) : new Date()
const constructedDateUtils = makeDateUtils(this.useUtc)
const pageTimestamp = constructedDateUtils.setDate(startDate, 1)
return {
/*
* Vue cannot observe changes to a Date Object so date must be stored as a timestamp
* This represents the first day of the current viewing month
* {Number}
*/
pageTimestamp,
/*
* Selected Date
* {Date}
*/
selectedDate: null,
/*
* Flags to show calendar views
* {Boolean}
*/
showDayView: false,
showMonthView: false,
showYearView: false,
/*
* Positioning
*/
calendarHeight: 0,
resetTypedDate: new Date(),
utils: constructedDateUtils
}
},
watch: {
value (value) {
this.setValue(value)
},
openDate () {
this.setPageDate()
},
initialView () {
this.setInitialView()
}
},
computed: {
...mapGetters('preferences', {
'format': 'getMomentDateFormat'
}),
customFormatter () {
let newDate = new Date(this.value)
return moment(newDate).format(this.format)
},
computedInitialView () {
if (!this.initialView) {
return this.minimumView
}
return this.initialView
},
pageDate () {
return new Date(this.pageTimestamp)
},
translation () {
return this.language
},
calendarStyle () {
return {
position: this.isInline ? 'static' : undefined
}
},
isOpen () {
return this.showDayView || this.showMonthView || this.showYearView
},
isInline () {
return !!this.inline
},
isRtl () {
return this.translation.rtl === true
}
},
mounted () {
this.init()
},
methods: {
/**
* Called in the event that the user navigates to date pages and
* closes the picker without selecting a date.
*/
resetDefaultPageDate () {
if (this.selectedDate === null) {
this.setPageDate()
return
}
this.setPageDate(this.selectedDate)
},
/**
* Effectively a toggle to show/hide the calendar
* @return {mixed}
*/
showCalendar () {
if (this.disabled || this.isInline) {
return false
}
if (this.isOpen) {
return this.close(true)
}
this.setInitialView()
},
/**
* Sets the initial picker page view: day, month or year
*/
setInitialView () {
const initialView = this.computedInitialView
if (!this.allowedToShowView(initialView)) {
throw new Error(`initialView '${this.initialView}' cannot be rendered based on minimum '${this.minimumView}' and maximum '${this.maximumView}'`)
}
switch (initialView) {
case 'year':
this.showYearCalendar()
break
case 'month':
this.showMonthCalendar()
break
default:
this.showDayCalendar()
break
}
},
/**
* Are we allowed to show a specific picker view?
* @param {String} view
* @return {Boolean}
*/
allowedToShowView (view) {
const views = ['day', 'month', 'year']
const minimumViewIndex = views.indexOf(this.minimumView)
const maximumViewIndex = views.indexOf(this.maximumView)
const viewIndex = views.indexOf(view)
return viewIndex >= minimumViewIndex && viewIndex <= maximumViewIndex
},
/**
* Show the day picker
* @return {Boolean}
*/
showDayCalendar () {
if (!this.allowedToShowView('day')) {
return false
}
this.close()
this.showDayView = true
return true
},
/**
* Show the month picker
* @return {Boolean}
*/
showMonthCalendar () {
if (!this.allowedToShowView('month')) {
return false
}
this.close()
this.showMonthView = true
return true
},
/**
* Show the year picker
* @return {Boolean}
*/
showYearCalendar () {
if (!this.allowedToShowView('year')) {
return false
}
this.close()
this.showYearView = true
return true
},
/**
* Set the selected date
* @param {Number} timestamp
*/
setDate (timestamp) {
const date = new Date(timestamp)
this.selectedDate = date
this.setPageDate(date)
this.$emit('selected', date)
this.$emit('input', date)
},
/**
* Clear the selected date
*/
clearDate () {
this.selectedDate = null
this.setPageDate()
this.$emit('selected', null)
this.$emit('input', null)
this.$emit('cleared')
},
/**
* @param {Object} date
*/
selectDate (date) {
this.setDate(date.timestamp)
if (!this.isInline) {
this.close(true)
}
this.resetTypedDate = new Date()
},
/**
* @param {Object} date
*/
selectDisabledDate (date) {
this.$emit('selectedDisabled', date)
},
/**
* @param {Object} month
*/
selectMonth (month) {
const date = new Date(month.timestamp)
if (this.allowedToShowView('day')) {
this.setPageDate(date)
this.$emit('changedMonth', month)
this.showDayCalendar()
} else {
this.selectDate(month)
}
},
/**
* @param {Object} year
*/
selectYear (year) {
const date = new Date(year.timestamp)
if (this.allowedToShowView('month')) {
this.setPageDate(date)
this.$emit('changedYear', year)
this.showMonthCalendar()
} else {
this.selectDate(year)
}
},
/**
* Set the datepicker value
* @param {Date|String|Number|null} date
*/
setValue (date) {
if (typeof date === 'string' || typeof date === 'number') {
let parsed = new Date(date)
date = isNaN(parsed.valueOf()) ? null : parsed
}
if (!date) {
this.setPageDate()
this.selectedDate = null
return
}
this.selectedDate = date
this.setPageDate(date)
},
/**
* Sets the date that the calendar should open on
*/
setPageDate (date) {
if (!date) {
if (this.openDate) {
date = new Date(this.openDate)
} else {
date = new Date()
}
}
this.pageTimestamp = this.utils.setDate(new Date(date), 1)
},
/**
* Handles a month change from the day picker
*/
handleChangedMonthFromDayPicker (date) {
this.setPageDate(date)
this.$emit('changedMonth', date)
},
/**
* Set the date from a typedDate event
*/
setTypedDate (date) {
this.setDate(date.getTime())
},
/**
* Close all calendar layers
* @param {Boolean} emitEvent - emit close event
*/
close (emitEvent) {
this.showDayView = this.showMonthView = this.showYearView = false
if (!this.isInline) {
if (emitEvent) {
this.$emit('closed')
}
document.removeEventListener('click', this.clickOutside, false)
}
},
/**
* Initiate the component
*/
init () {
if (this.value) {
this.setValue(this.value)
}
if (this.isInline) {
this.setInitialView()
}
}
},
}
// eslint-disable-next-line
;
</script>
<style lang="css">
.rtl {
direction: rtl;
}
.required-date {
border: 1px solid #FB7178;
border-radius: 5px;
}
.vdp-datepicker {
position: relative;
text-align: left;
}
.vdp-datepicker * {
box-sizing: border-box;
}
.vdp-datepicker__calendar {
position: absolute;
z-index: 100;
background: #fff;
width: 300px;
border: 1px solid #ccc;
}
.vdp-datepicker__calendar header {
display: block;
line-height: 40px;
}
.vdp-datepicker__calendar header span {
display: inline-block;
text-align: center;
width: 71.42857142857143%;
float: left;
}
.vdp-datepicker__calendar header .prev,
.vdp-datepicker__calendar header .next {
width: 14.285714285714286%;
float: left;
text-indent: -10000px;
position: relative;
}
.vdp-datepicker__calendar header .prev:after,
.vdp-datepicker__calendar header .next:after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
border: 6px solid transparent;
}
.vdp-datepicker__calendar header .prev:after {
border-right: 10px solid #000;
margin-left: -5px;
}
.vdp-datepicker__calendar header .prev.disabled:after {
border-right: 10px solid #ddd;
}
.vdp-datepicker__calendar header .next:after {
border-left: 10px solid #000;
margin-left: 5px;
}
.vdp-datepicker__calendar header .next.disabled:after {
border-left: 10px solid #ddd;
}
.vdp-datepicker__calendar header .prev:not(.disabled),
.vdp-datepicker__calendar header .next:not(.disabled),
.vdp-datepicker__calendar header .up:not(.disabled) {
cursor: pointer;
}
.vdp-datepicker__calendar header .prev:not(.disabled):hover,
.vdp-datepicker__calendar header .next:not(.disabled):hover,
.vdp-datepicker__calendar header .up:not(.disabled):hover {
background: #eee;
}
.vdp-datepicker__calendar .disabled {
color: #ddd;
cursor: default;
}
.vdp-datepicker__calendar .flex-rtl {
display: flex;
width: inherit;
flex-wrap: wrap;
}
.vdp-datepicker__calendar .cell {
display: inline-block;
padding: 0 5px;
width: 14.285714285714286%;
height: 40px;
line-height: 40px;
text-align: center;
vertical-align: middle;
border: 1px solid transparent;
}
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).day,
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).month,
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).year {
cursor: pointer;
}
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).day:hover,
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).month:hover,
.vdp-datepicker__calendar .cell:not(.blank):not(.disabled).year:hover {
border: 1px solid #4bd;
}
.vdp-datepicker__calendar .cell.selected {
background: #4bd;
}
.vdp-datepicker__calendar .cell.selected:hover {
background: #4bd;
}
.vdp-datepicker__calendar .cell.selected.highlighted {
background: #4bd;
}
.vdp-datepicker__calendar .cell.highlighted {
background: #cae5ed;
}
.vdp-datepicker__calendar .cell.highlighted.disabled {
color: #a3a3a3;
}
.vdp-datepicker__calendar .cell.grey {
color: #888;
}
.vdp-datepicker__calendar .cell.grey:hover {
background: inherit;
}
.vdp-datepicker__calendar .cell.day-header {
font-size: 75%;
white-space: nowrap;
cursor: inherit;
}
.vdp-datepicker__calendar .cell.day-header:hover {
background: inherit;
}
.vdp-datepicker__calendar .month,
.vdp-datepicker__calendar .year {
width: 33.333%;
}
.vdp-datepicker__clear-button,
.vdp-datepicker__calendar-button {
cursor: pointer;
font-style: normal;
}
.vdp-datepicker__clear-button.disabled,
.vdp-datepicker__calendar-button.disabled {
color: #999;
cursor: default;
}
</style>

View File

@ -0,0 +1,158 @@
<template>
<div :class="{'input-group' : bootstrapStyling}">
<!-- Calendar Button -->
<span v-if="calendarButton" class="vdp-datepicker__calendar-button" :class="{'input-group-prepend' : bootstrapStyling}" @click="showCalendar" v-bind:style="{'cursor:not-allowed;' : disabled}">
<span :class="{'input-group-text' : bootstrapStyling}">
<font-awesome-icon :icon="calendarButtonIcon"/>
</span>
</span>
<!-- Input -->
<input
:type="inline ? 'hidden' : 'text'"
:class="[computedInputClass, {'invalid': isFieldValid}]"
:name="name"
:ref="refName"
:id="id"
:value="formattedValue"
:open-date="openDate"
:placeholder="placeholder"
:clear-button="clearButton"
:disabled="disabled"
:required="required"
:readonly="!typeable"
class="date-field"
@click="showCalendar"
@keyup="parseTypedDate"
@blur="inputBlurred"
autocomplete="off">
<!-- Clear Button -->
<span v-if="clearButton && selectedDate" class="vdp-datepicker__clear-button" :class="{'input-group-append' : bootstrapStyling}" @click="clearDate()">
<span :class="{'input-group-text' : bootstrapStyling}">
<!-- <i :class="clearButtonIcon">
<span v-if="!clearButtonIcon">&times;</span>
</i> -->
<font-awesome-icon :icon="clearButtonIcon"/>
</span>
</span>
<slot name="afterDateInput"></slot>
</div>
</template>
<script>
import { makeDateUtils } from './src/DateUtils'
export default {
props: {
selectedDate: Date,
resetTypedDate: [Date],
format: [String, Function],
translation: Object,
inline: Boolean,
id: String,
name: String,
refName: String,
openDate: Date,
placeholder: String,
inputClass: [String, Object, Array],
clearButton: Boolean,
clearButtonIcon: String,
calendarButton: Boolean,
calendarButtonIcon: String,
calendarButtonIconContent: String,
disabled: Boolean,
required: Boolean,
typeable: Boolean,
bootstrapStyling: Boolean,
useUtc: Boolean,
invalid: Boolean
},
data () {
const constructedDateUtils = makeDateUtils(this.useUtc)
return {
input: null,
typedDate: false,
utils: constructedDateUtils
}
},
computed: {
formattedValue () {
if (!this.selectedDate) {
return null
}
if (this.typedDate) {
return this.typedDate
}
return typeof this.format === 'function'
? this.format(this.selectedDate)
: this.utils.formatDate(new Date(this.selectedDate), this.format, this.translation)
},
computedInputClass () {
if (this.bootstrapStyling) {
if (typeof this.inputClass === 'string') {
return [this.inputClass, 'form-control'].join(' ')
}
return {'form-control': true, ...this.inputClass}
}
return this.inputClass
},
isFieldValid () {
return this.invalid
}
},
watch: {
resetTypedDate () {
this.typedDate = false
}
},
methods: {
showCalendar () {
this.$emit('showCalendar')
},
/**
* Attempt to parse a typed date
* @param {Event} event
*/
parseTypedDate (event) {
// close calendar if escape or enter are pressed
if ([
27, // escape
13 // enter
].includes(event.keyCode)) {
this.input.blur()
}
if (this.typeable) {
const typedDate = Date.parse(this.input.value)
if (!isNaN(typedDate)) {
this.typedDate = this.input.value
this.$emit('typedDate', new Date(this.typedDate))
}
}
},
/**
* nullify the typed date to defer to regular formatting
* called once the input is blurred
*/
inputBlurred () {
if (this.typeable && isNaN(Date.parse(this.input.value))) {
this.clearDate()
this.input.value = null
this.typedDate = null
}
this.$emit('closeCalendar')
},
/**
* emit a clearDate event
*/
clearDate () {
this.$emit('clearDate')
}
},
mounted () {
this.input = this.$el.querySelector('input')
}
}
// eslint-disable-next-line
;
</script>

View File

@ -0,0 +1,375 @@
<template>
<div :class="[calendarClass, 'vdp-datepicker__calendar']" v-show="showDayView" :style="calendarStyle" @mousedown.prevent>
<slot name="beforeCalendarHeader"></slot>
<header>
<span
@click="isRtl ? nextMonth() : previousMonth()"
class="prev"
:class="{'disabled': isLeftNavDisabled}">&lt;</span>
<span class="day__month_btn" @click="showMonthCalendar" :class="allowedToShowView('month') ? 'up' : ''">{{ isYmd ? currYearName : currMonthName }} {{ isYmd ? currMonthName : currYearName }}</span>
<span
@click="isRtl ? previousMonth() : nextMonth()"
class="next"
:class="{'disabled': isRightNavDisabled}">&gt;</span>
</header>
<div :class="isRtl ? 'flex-rtl' : ''">
<span class="cell day-header" v-for="d in daysOfWeek" :key="d.timestamp">{{ d }}</span>
<template v-if="blankDays > 0">
<span class="cell day blank" v-for="d in blankDays" :key="d.timestamp"></span>
</template><!--
--><span class="cell day"
v-for="day in days"
:key="day.timestamp"
:class="dayClasses(day)"
v-html="dayCellContent(day)"
@click="selectDate(day)"></span>
</div>
</div>
</template>
<script>
import { makeDateUtils } from './src/DateUtils'
export default {
props: {
showDayView: Boolean,
selectedDate: Date,
pageDate: Date,
pageTimestamp: Number,
fullMonthName: Boolean,
allowedToShowView: Function,
dayCellContent: {
type: Function,
default: day => day.date
},
disabledDates: Object,
highlighted: Object,
calendarClass: [String, Object, Array],
calendarStyle: Object,
translation: Object,
isRtl: Boolean,
mondayFirst: Boolean,
useUtc: Boolean
},
data () {
const constructedDateUtils = makeDateUtils(this.useUtc)
return {
utils: constructedDateUtils
}
},
computed: {
/**
* Returns an array of day names
* @return {String[]}
*/
daysOfWeek () {
if (this.mondayFirst) {
const tempDays = this.translation.days.slice()
tempDays.push(tempDays.shift())
return tempDays
}
return this.translation.days
},
/**
* Returns the day number of the week less one for the first of the current month
* Used to show amount of empty cells before the first in the day calendar layout
* @return {Number}
*/
blankDays () {
const d = this.pageDate
let dObj = this.useUtc
? new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1))
: new Date(d.getFullYear(), d.getMonth(), 1, d.getHours(), d.getMinutes())
if (this.mondayFirst) {
return this.utils.getDay(dObj) > 0 ? this.utils.getDay(dObj) - 1 : 6
}
return this.utils.getDay(dObj)
},
/**
* @return {Object[]}
*/
days () {
const d = this.pageDate
let days = []
// set up a new date object to the beginning of the current 'page'
let dObj = this.useUtc
? new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1))
: new Date(d.getFullYear(), d.getMonth(), 1, d.getHours(), d.getMinutes())
let daysInMonth = this.utils.daysInMonth(this.utils.getFullYear(dObj), this.utils.getMonth(dObj))
for (let i = 0; i < daysInMonth; i++) {
days.push({
date: this.utils.getDate(dObj),
timestamp: dObj.getTime(),
isSelected: this.isSelectedDate(dObj),
isDisabled: this.isDisabledDate(dObj),
isHighlighted: this.isHighlightedDate(dObj),
isHighlightStart: this.isHighlightStart(dObj),
isHighlightEnd: this.isHighlightEnd(dObj),
isToday: this.utils.compareDates(dObj, new Date()),
isWeekend: this.utils.getDay(dObj) === 0 || this.utils.getDay(dObj) === 6,
isSaturday: this.utils.getDay(dObj) === 6,
isSunday: this.utils.getDay(dObj) === 0
})
this.utils.setDate(dObj, this.utils.getDate(dObj) + 1)
}
return days
},
/**
* Gets the name of the month the current page is on
* @return {String}
*/
currMonthName () {
const monthName = this.fullMonthName ? this.translation.months : this.translation.monthsAbbr
return this.utils.getMonthNameAbbr(this.utils.getMonth(this.pageDate), monthName)
},
/**
* Gets the name of the year that current page is on
* @return {Number}
*/
currYearName () {
const yearSuffix = this.translation.yearSuffix
return `${this.utils.getFullYear(this.pageDate)}${yearSuffix}`
},
/**
* Is this translation using year/month/day format?
* @return {Boolean}
*/
isYmd () {
return this.translation.ymd && this.translation.ymd === true
},
/**
* Is the left hand navigation button disabled?
* @return {Boolean}
*/
isLeftNavDisabled () {
return this.isRtl
? this.isNextMonthDisabled(this.pageTimestamp)
: this.isPreviousMonthDisabled(this.pageTimestamp)
},
/**
* Is the right hand navigation button disabled?
* @return {Boolean}
*/
isRightNavDisabled () {
return this.isRtl
? this.isPreviousMonthDisabled(this.pageTimestamp)
: this.isNextMonthDisabled(this.pageTimestamp)
}
},
methods: {
selectDate (date) {
if (date.isDisabled) {
this.$emit('selectedDisabled', date)
return false
}
this.$emit('selectDate', date)
},
/**
* @return {Number}
*/
getPageMonth () {
return this.utils.getMonth(this.pageDate)
},
/**
* Emit an event to show the month picker
*/
showMonthCalendar () {
this.$emit('showMonthCalendar')
},
/**
* Change the page month
* @param {Number} incrementBy
*/
changeMonth (incrementBy) {
let date = this.pageDate
this.utils.setMonth(date, this.utils.getMonth(date) + incrementBy)
this.$emit('changedMonth', date)
},
/**
* Decrement the page month
*/
previousMonth () {
if (!this.isPreviousMonthDisabled()) {
this.changeMonth(-1)
}
},
/**
* Is the previous month disabled?
* @return {Boolean}
*/
isPreviousMonthDisabled () {
if (!this.disabledDates || !this.disabledDates.to) {
return false
}
let d = this.pageDate
return this.utils.getMonth(this.disabledDates.to) >= this.utils.getMonth(d) &&
this.utils.getFullYear(this.disabledDates.to) >= this.utils.getFullYear(d)
},
/**
* Increment the current page month
*/
nextMonth () {
if (!this.isNextMonthDisabled()) {
this.changeMonth(+1)
}
},
/**
* Is the next month disabled?
* @return {Boolean}
*/
isNextMonthDisabled () {
if (!this.disabledDates || !this.disabledDates.from) {
return false
}
let d = this.pageDate
return this.utils.getMonth(this.disabledDates.from) <= this.utils.getMonth(d) &&
this.utils.getFullYear(this.disabledDates.from) <= this.utils.getFullYear(d)
},
/**
* Whether a day is selected
* @param {Date}
* @return {Boolean}
*/
isSelectedDate (dObj) {
return this.selectedDate && this.utils.compareDates(this.selectedDate, dObj)
},
/**
* Whether a day is disabled
* @param {Date}
* @return {Boolean}
*/
isDisabledDate (date) {
let disabledDates = false
if (typeof this.disabledDates === 'undefined') {
return false
}
if (typeof this.disabledDates.dates !== 'undefined') {
this.disabledDates.dates.forEach((d) => {
if (this.utils.compareDates(date, d)) {
disabledDates = true
return true
}
})
}
if (typeof this.disabledDates.to !== 'undefined' && this.disabledDates.to && date < this.disabledDates.to) {
disabledDates = true
}
if (typeof this.disabledDates.from !== 'undefined' && this.disabledDates.from && date > this.disabledDates.from) {
disabledDates = true
}
if (typeof this.disabledDates.ranges !== 'undefined') {
this.disabledDates.ranges.forEach((range) => {
if (typeof range.from !== 'undefined' && range.from && typeof range.to !== 'undefined' && range.to) {
if (date < range.to && date > range.from) {
disabledDates = true
return true
}
}
})
}
if (typeof this.disabledDates.days !== 'undefined' && this.disabledDates.days.indexOf(this.utils.getDay(date)) !== -1) {
disabledDates = true
}
if (typeof this.disabledDates.daysOfMonth !== 'undefined' && this.disabledDates.daysOfMonth.indexOf(this.utils.getDate(date)) !== -1) {
disabledDates = true
}
if (typeof this.disabledDates.customPredictor === 'function' && this.disabledDates.customPredictor(date)) {
disabledDates = true
}
return disabledDates
},
/**
* Whether a day is highlighted (only if it is not disabled already except when highlighted.includeDisabled is true)
* @param {Date}
* @return {Boolean}
*/
isHighlightedDate (date) {
if (!(this.highlighted && this.highlighted.includeDisabled) && this.isDisabledDate(date)) {
return false
}
let highlighted = false
if (typeof this.highlighted === 'undefined') {
return false
}
if (typeof this.highlighted.dates !== 'undefined') {
this.highlighted.dates.forEach((d) => {
if (this.utils.compareDates(date, d)) {
highlighted = true
return true
}
})
}
if (this.isDefined(this.highlighted.from) && this.isDefined(this.highlighted.to)) {
highlighted = date >= this.highlighted.from && date <= this.highlighted.to
}
if (typeof this.highlighted.days !== 'undefined' && this.highlighted.days.indexOf(this.utils.getDay(date)) !== -1) {
highlighted = true
}
if (typeof this.highlighted.daysOfMonth !== 'undefined' && this.highlighted.daysOfMonth.indexOf(this.utils.getDate(date)) !== -1) {
highlighted = true
}
if (typeof this.highlighted.customPredictor === 'function' && this.highlighted.customPredictor(date)) {
highlighted = true
}
return highlighted
},
dayClasses (day) {
return {
'selected': day.isSelected,
'disabled': day.isDisabled,
'highlighted': day.isHighlighted,
'today': day.isToday,
'weekend': day.isWeekend,
'sat': day.isSaturday,
'sun': day.isSunday,
'highlight-start': day.isHighlightStart,
'highlight-end': day.isHighlightEnd
}
},
/**
* Whether a day is highlighted and it is the first date
* in the highlighted range of dates
* @param {Date}
* @return {Boolean}
*/
isHighlightStart (date) {
return this.isHighlightedDate(date) &&
(this.highlighted.from instanceof Date) &&
(this.utils.getFullYear(this.highlighted.from) === this.utils.getFullYear(date)) &&
(this.utils.getMonth(this.highlighted.from) === this.utils.getMonth(date)) &&
(this.utils.getDate(this.highlighted.from) === this.utils.getDate(date))
},
/**
* Whether a day is highlighted and it is the first date
* in the highlighted range of dates
* @param {Date}
* @return {Boolean}
*/
isHighlightEnd (date) {
return this.isHighlightedDate(date) &&
(this.highlighted.to instanceof Date) &&
(this.utils.getFullYear(this.highlighted.to) === this.utils.getFullYear(date)) &&
(this.utils.getMonth(this.highlighted.to) === this.utils.getMonth(date)) &&
(this.utils.getDate(this.highlighted.to) === this.utils.getDate(date))
},
/**
* Helper
* @param {mixed} prop
* @return {Boolean}
*/
isDefined (prop) {
return typeof prop !== 'undefined' && prop
}
}
}
// eslint-disable-next-line
;
</script>

View File

@ -0,0 +1,200 @@
<template>
<div :class="[calendarClass, 'vdp-datepicker__calendar']" v-show="showMonthView" :style="calendarStyle" @mousedown.prevent>
<slot name="beforeCalendarHeader"></slot>
<header>
<span
@click="isRtl ? nextYear() : previousYear()"
class="prev"
:class="{'disabled': isLeftNavDisabled}">&lt;</span>
<span class="month__year_btn" @click="showYearCalendar" :class="allowedToShowView('year') ? 'up' : ''">{{ pageYearName }}</span>
<span
@click="isRtl ? previousYear() : nextYear()"
class="next"
:class="{'disabled': isRightNavDisabled}">&gt;</span>
</header>
<span class="cell month"
v-for="month in months"
:key="month.timestamp"
:class="{'selected': month.isSelected, 'disabled': month.isDisabled}"
@click.stop="selectMonth(month)">{{ month.month }}</span>
</div>
</template>
<script>
import { makeDateUtils } from './src/DateUtils'
export default {
props: {
showMonthView: Boolean,
selectedDate: Date,
pageDate: Date,
pageTimestamp: Number,
disabledDates: Object,
calendarClass: [String, Object, Array],
calendarStyle: Object,
translation: Object,
isRtl: Boolean,
allowedToShowView: Function,
useUtc: Boolean
},
data () {
const constructedDateUtils = makeDateUtils(this.useUtc)
return {
utils: constructedDateUtils
}
},
computed: {
months () {
const d = this.pageDate
let months = []
// set up a new date object to the beginning of the current 'page'
let dObj = this.useUtc
? new Date(Date.UTC(d.getUTCFullYear(), 0, d.getUTCDate()))
: new Date(d.getFullYear(), 0, d.getDate(), d.getHours(), d.getMinutes())
for (let i = 0; i < 12; i++) {
months.push({
month: this.utils.getMonthName(i, this.translation.months),
timestamp: dObj.getTime(),
isSelected: this.isSelectedMonth(dObj),
isDisabled: this.isDisabledMonth(dObj)
})
this.utils.setMonth(dObj, this.utils.getMonth(dObj) + 1)
}
return months
},
/**
* Get year name on current page.
* @return {String}
*/
pageYearName () {
const yearSuffix = this.translation.yearSuffix
return `${this.utils.getFullYear(this.pageDate)}${yearSuffix}`
},
/**
* Is the left hand navigation disabled
* @return {Boolean}
*/
isLeftNavDisabled () {
return this.isRtl
? this.isNextYearDisabled(this.pageTimestamp)
: this.isPreviousYearDisabled(this.pageTimestamp)
},
/**
* Is the right hand navigation disabled
* @return {Boolean}
*/
isRightNavDisabled () {
return this.isRtl
? this.isPreviousYearDisabled(this.pageTimestamp)
: this.isNextYearDisabled(this.pageTimestamp)
}
},
methods: {
/**
* Emits a selectMonth event
* @param {Object} month
*/
selectMonth (month) {
if (month.isDisabled) {
return false
}
this.$emit('selectMonth', month)
},
/**
* Changes the year up or down
* @param {Number} incrementBy
*/
changeYear (incrementBy) {
let date = this.pageDate
this.utils.setFullYear(date, this.utils.getFullYear(date) + incrementBy)
this.$emit('changedYear', date)
},
/**
* Decrements the year
*/
previousYear () {
if (!this.isPreviousYearDisabled()) {
this.changeYear(-1)
}
},
/**
* Checks if the previous year is disabled or not
* @return {Boolean}
*/
isPreviousYearDisabled () {
if (!this.disabledDates || !this.disabledDates.to) {
return false
}
return this.utils.getFullYear(this.disabledDates.to) >= this.utils.getFullYear(this.pageDate)
},
/**
* Increments the year
*/
nextYear () {
if (!this.isNextYearDisabled()) {
this.changeYear(1)
}
},
/**
* Checks if the next year is disabled or not
* @return {Boolean}
*/
isNextYearDisabled () {
if (!this.disabledDates || !this.disabledDates.from) {
return false
}
return this.utils.getFullYear(this.disabledDates.from) <= this.utils.getFullYear(this.pageDate)
},
/**
* Emits an event that shows the year calendar
*/
showYearCalendar () {
this.$emit('showYearCalendar')
},
/**
* Whether the selected date is in this month
* @param {Date}
* @return {Boolean}
*/
isSelectedMonth (date) {
return (this.selectedDate &&
this.utils.getFullYear(this.selectedDate) === this.utils.getFullYear(date) &&
this.utils.getMonth(this.selectedDate) === this.utils.getMonth(date))
},
/**
* Whether a month is disabled
* @param {Date}
* @return {Boolean}
*/
isDisabledMonth (date) {
let disabledDates = false
if (typeof this.disabledDates === 'undefined') {
return false
}
if (typeof this.disabledDates.to !== 'undefined' && this.disabledDates.to) {
if (
(this.utils.getMonth(date) < this.utils.getMonth(this.disabledDates.to) && this.utils.getFullYear(date) <= this.utils.getFullYear(this.disabledDates.to)) ||
this.utils.getFullYear(date) < this.utils.getFullYear(this.disabledDates.to)
) {
disabledDates = true
}
}
if (typeof this.disabledDates.from !== 'undefined' && this.disabledDates.from) {
if (
(this.utils.getMonth(date) > this.utils.getMonth(this.disabledDates.from) && this.utils.getFullYear(date) >= this.utils.getFullYear(this.disabledDates.from)) ||
this.utils.getFullYear(date) > this.utils.getFullYear(this.disabledDates.from)
) {
disabledDates = true
}
}
if (typeof this.disabledDates.customPredictor === 'function' && this.disabledDates.customPredictor(date)) {
disabledDates = true
}
return disabledDates
}
}
}
// eslint-disable-next-line
;
</script>

View File

@ -0,0 +1,174 @@
<template>
<div :class="[calendarClass, 'vdp-datepicker__calendar']" v-show="showYearView" :style="calendarStyle" @mousedown.prevent>
<slot name="beforeCalendarHeader"></slot>
<header>
<span
@click="isRtl ? nextDecade() : previousDecade()"
class="prev"
:class="{'disabled': isLeftNavDisabled}">&lt;</span>
<span>{{ getPageDecade }}</span>
<span
@click="isRtl ? previousDecade() : nextDecade()"
class="next"
:class="{'disabled': isRightNavDisabled}">&gt;</span>
</header>
<span
class="cell year"
v-for="year in years"
:key="year.timestamp"
:class="{ 'selected': year.isSelected, 'disabled': year.isDisabled }"
@click.stop="selectYear(year)">{{ year.year }}</span>
</div>
</template>
<script>
import { makeDateUtils } from './src/DateUtils'
export default {
props: {
showYearView: Boolean,
selectedDate: Date,
pageDate: Date,
pageTimestamp: Number,
disabledDates: Object,
highlighted: Object,
calendarClass: [String, Object, Array],
calendarStyle: Object,
translation: Object,
isRtl: Boolean,
allowedToShowView: Function,
useUtc: Boolean
},
computed: {
years () {
const d = this.pageDate
let years = []
// set up a new date object to the beginning of the current 'page'7
let dObj = this.useUtc
? new Date(Date.UTC(Math.floor(d.getUTCFullYear() / 10) * 10, d.getUTCMonth(), d.getUTCDate()))
: new Date(Math.floor(d.getFullYear() / 10) * 10, d.getMonth(), d.getDate(), d.getHours(), d.getMinutes())
for (let i = 0; i < 10; i++) {
years.push({
year: this.utils.getFullYear(dObj),
timestamp: dObj.getTime(),
isSelected: this.isSelectedYear(dObj),
isDisabled: this.isDisabledYear(dObj)
})
this.utils.setFullYear(dObj, this.utils.getFullYear(dObj) + 1)
}
return years
},
/**
* @return {String}
*/
getPageDecade () {
const decadeStart = Math.floor(this.utils.getFullYear(this.pageDate) / 10) * 10
const decadeEnd = decadeStart + 9
const yearSuffix = this.translation.yearSuffix
return `${decadeStart} - ${decadeEnd}${yearSuffix}`
},
/**
* Is the left hand navigation button disabled?
* @return {Boolean}
*/
isLeftNavDisabled () {
return this.isRtl
? this.isNextDecadeDisabled(this.pageTimestamp)
: this.isPreviousDecadeDisabled(this.pageTimestamp)
},
/**
* Is the right hand navigation button disabled?
* @return {Boolean}
*/
isRightNavDisabled () {
return this.isRtl
? this.isPreviousDecadeDisabled(this.pageTimestamp)
: this.isNextDecadeDisabled(this.pageTimestamp)
}
},
data () {
const constructedDateUtils = makeDateUtils(this.useUtc)
return {
utils: constructedDateUtils
}
},
methods: {
selectYear (year) {
if (year.isDisabled) {
return false
}
this.$emit('selectYear', year)
},
changeYear (incrementBy) {
let date = this.pageDate
this.utils.setFullYear(date, this.utils.getFullYear(date) + incrementBy)
this.$emit('changedDecade', date)
},
previousDecade () {
if (this.isPreviousDecadeDisabled()) {
return false
}
this.changeYear(-10)
},
isPreviousDecadeDisabled () {
if (!this.disabledDates || !this.disabledDates.to) {
return false
}
const disabledYear = this.utils.getFullYear(this.disabledDates.to)
const lastYearInPreviousPage = Math.floor(this.utils.getFullYear(this.pageDate) / 10) * 10 - 1
return disabledYear > lastYearInPreviousPage
},
nextDecade () {
if (this.isNextDecadeDisabled()) {
return false
}
this.changeYear(10)
},
isNextDecadeDisabled () {
if (!this.disabledDates || !this.disabledDates.from) {
return false
}
const disabledYear = this.utils.getFullYear(this.disabledDates.from)
const firstYearInNextPage = Math.ceil(this.utils.getFullYear(this.pageDate) / 10) * 10
return disabledYear < firstYearInNextPage
},
/**
* Whether the selected date is in this year
* @param {Date}
* @return {Boolean}
*/
isSelectedYear (date) {
return this.selectedDate && this.utils.getFullYear(this.selectedDate) === this.utils.getFullYear(date)
},
/**
* Whether a year is disabled
* @param {Date}
* @return {Boolean}
*/
isDisabledYear (date) {
let disabledDates = false
if (typeof this.disabledDates === 'undefined' || !this.disabledDates) {
return false
}
if (typeof this.disabledDates.to !== 'undefined' && this.disabledDates.to) {
if (this.utils.getFullYear(date) < this.utils.getFullYear(this.disabledDates.to)) {
disabledDates = true
}
}
if (typeof this.disabledDates.from !== 'undefined' && this.disabledDates.from) {
if (this.utils.getFullYear(date) > this.utils.getFullYear(this.disabledDates.from)) {
disabledDates = true
}
}
if (typeof this.disabledDates.customPredictor === 'function' && this.disabledDates.customPredictor(date)) {
disabledDates = true
}
return disabledDates
}
}
}
// eslint-disable-next-line
;
</script>

View File

@ -0,0 +1,252 @@
import en from './locale/translations/en'
const utils = {
/**
* @type {Boolean}
*/
useUtc: false,
/**
* Returns the full year, using UTC or not
* @param {Date} date
*/
getFullYear (date) {
return this.useUtc ? date.getUTCFullYear() : date.getFullYear()
},
/**
* Returns the month, using UTC or not
* @param {Date} date
*/
getMonth (date) {
return this.useUtc ? date.getUTCMonth() : date.getMonth()
},
/**
* Returns the date, using UTC or not
* @param {Date} date
*/
getDate (date) {
return this.useUtc ? date.getUTCDate() : date.getDate()
},
/**
* Returns the day, using UTC or not
* @param {Date} date
*/
getDay (date) {
return this.useUtc ? date.getUTCDay() : date.getDay()
},
/**
* Returns the hours, using UTC or not
* @param {Date} date
*/
getHours (date) {
return this.useUtc ? date.getUTCHours() : date.getHours()
},
/**
* Returns the minutes, using UTC or not
* @param {Date} date
*/
getMinutes (date) {
return this.useUtc ? date.getUTCMinutes() : date.getMinutes()
},
/**
* Sets the full year, using UTC or not
* @param {Date} date
*/
setFullYear (date, value, useUtc) {
return this.useUtc ? date.setUTCFullYear(value) : date.setFullYear(value)
},
/**
* Sets the month, using UTC or not
* @param {Date} date
*/
setMonth (date, value, useUtc) {
return this.useUtc ? date.setUTCMonth(value) : date.setMonth(value)
},
/**
* Sets the date, using UTC or not
* @param {Date} date
* @param {Number} value
*/
setDate (date, value, useUtc) {
return this.useUtc ? date.setUTCDate(value) : date.setDate(value)
},
/**
* Check if date1 is equivalent to date2, without comparing the time
* @see https://stackoverflow.com/a/6202196/4455925
* @param {Date} date1
* @param {Date} date2
*/
compareDates (date1, date2) {
const d1 = new Date(date1.getTime())
const d2 = new Date(date2.getTime())
if (this.useUtc) {
d1.setUTCHours(0, 0, 0, 0)
d2.setUTCHours(0, 0, 0, 0)
} else {
d1.setHours(0, 0, 0, 0)
d2.setHours(0, 0, 0, 0)
}
return d1.getTime() === d2.getTime()
},
/**
* Validates a date object
* @param {Date} date - an object instantiated with the new Date constructor
* @return {Boolean}
*/
isValidDate (date) {
if (Object.prototype.toString.call(date) !== '[object Date]') {
return false
}
return !isNaN(date.getTime())
},
/**
* Return abbreviated week day name
* @param {Date}
* @param {Array}
* @return {String}
*/
getDayNameAbbr (date, days) {
if (typeof date !== 'object') {
throw TypeError('Invalid Type')
}
return days[this.getDay(date)]
},
/**
* Return name of the month
* @param {Number|Date}
* @param {Array}
* @return {String}
*/
getMonthName (month, months) {
if (!months) {
throw Error('missing 2nd parameter Months array')
}
if (typeof month === 'object') {
return months[this.getMonth(month)]
}
if (typeof month === 'number') {
return months[month]
}
throw TypeError('Invalid type')
},
/**
* Return an abbreviated version of the month
* @param {Number|Date}
* @return {String}
*/
getMonthNameAbbr (month, monthsAbbr) {
if (!monthsAbbr) {
throw Error('missing 2nd paramter Months array')
}
if (typeof month === 'object') {
return monthsAbbr[this.getMonth(month)]
}
if (typeof month === 'number') {
return monthsAbbr[month]
}
throw TypeError('Invalid type')
},
/**
* Alternative get total number of days in month
* @param {Number} year
* @param {Number} m
* @return {Number}
*/
daysInMonth (year, month) {
return /8|3|5|10/.test(month) ? 30 : month === 1 ? (!(year % 4) && year % 100) || !(year % 400) ? 29 : 28 : 31
},
/**
* Get nth suffix for date
* @param {Number} day
* @return {String}
*/
getNthSuffix (day) {
switch (day) {
case 1:
case 21:
case 31:
return 'st'
case 2:
case 22:
return 'nd'
case 3:
case 23:
return 'rd'
default:
return 'th'
}
},
/**
* Formats date object
* @param {Date}
* @param {String}
* @param {Object}
* @return {String}
*/
formatDate (date, format, translation) {
translation = (!translation) ? en : translation
let year = this.getFullYear(date)
let month = this.getMonth(date) + 1
let day = this.getDate(date)
let str = format
.replace(/dd/, ('0' + day).slice(-2))
.replace(/d/, day)
.replace(/yyyy/, year)
.replace(/yy/, String(year).slice(2))
.replace(/MMMM/, this.getMonthName(this.getMonth(date), translation.months))
.replace(/MMM/, this.getMonthNameAbbr(this.getMonth(date), translation.monthsAbbr))
.replace(/MM/, ('0' + month).slice(-2))
.replace(/M(?!a|ä|e)/, month)
.replace(/su/, this.getNthSuffix(this.getDate(date)))
.replace(/D(?!e|é|i)/, this.getDayNameAbbr(date, translation.days))
return str
},
/**
* Creates an array of dates for each day in between two dates.
* @param {Date} start
* @param {Date} end
* @return {Array}
*/
createDateArray (start, end) {
let dates = []
while (start <= end) {
dates.push(new Date(start))
start = this.setDate(new Date(start), this.getDate(new Date(start)) + 1)
}
return dates
},
/**
* method used as a prop validator for input values
* @param {*} val
* @return {Boolean}
*/
validateDateInput (val) {
return val === null || val instanceof Date || typeof val === 'string' || typeof val === 'number'
}
}
export const makeDateUtils = useUtc => ({...utils, useUtc})
export default {
...utils
}
// eslint-disable-next-line
;

View File

@ -0,0 +1,57 @@
export default class Language {
constructor (language, months, monthsAbbr, days) {
this.language = language
this.months = months
this.monthsAbbr = monthsAbbr
this.days = days
this.rtl = false
this.ymd = false
this.yearSuffix = ''
}
get language () {
return this._language
}
set language (language) {
if (typeof language !== 'string') {
throw new TypeError('Language must be a string')
}
this._language = language
}
get months () {
return this._months
}
set months (months) {
if (months.length !== 12) {
throw new RangeError(`There must be 12 months for ${this.language} language`)
}
this._months = months
}
get monthsAbbr () {
return this._monthsAbbr
}
set monthsAbbr (monthsAbbr) {
if (monthsAbbr.length !== 12) {
throw new RangeError(`There must be 12 abbreviated months for ${this.language} language`)
}
this._monthsAbbr = monthsAbbr
}
get days () {
return this._days
}
set days (days) {
if (days.length !== 7) {
throw new RangeError(`There must be 7 days for ${this.language} language`)
}
this._days = days
}
}
// eslint-disable-next-line
;

View File

@ -0,0 +1,105 @@
import af from './translations/af'
import ar from './translations/ar'
import bg from './translations/bg'
import bs from './translations/bs'
import ca from './translations/ca'
import cs from './translations/cs'
import da from './translations/da'
import de from './translations/de'
import ee from './translations/ee'
import el from './translations/el'
import en from './translations/en'
import es from './translations/es'
import fa from './translations/fa'
import fi from './translations/fi'
import fo from './translations/fo'
import fr from './translations/fr'
import ge from './translations/ge'
import gl from './translations/gl'
import he from './translations/he'
import hr from './translations/hr'
import hu from './translations/hu'
import id from './translations/id'
import is from './translations/is'
import it from './translations/it'
import ja from './translations/ja'
import kk from './translations/kk'
import ko from './translations/ko'
import lb from './translations/lb'
import lt from './translations/lt'
import lv from './translations/lv'
import mk from './translations/mk'
import mn from './translations/mn'
import nbNO from './translations/nb-NO'
import nl from './translations/nl'
import pl from './translations/pl'
import ptBR from './translations/pt-BR'
import ro from './translations/ro'
import ru from './translations/ru'
import sk from './translations/sk'
import slSI from './translations/sl-SI'
import srCYRL from './translations/sr-CYRL'
import sr from './translations/sr'
import sv from './translations/sv'
import th from './translations/th'
import tr from './translations/tr'
import uk from './translations/uk'
import ur from './translations/ur'
import vi from './translations/vi'
import zh from './translations/zh'
import zhHK from './translations/zh-HK'
export {
af,
ar,
bg,
bs,
ca,
cs,
da,
de,
ee,
el,
en,
es,
fa,
fi,
fo,
fr,
ge,
gl,
he,
hr,
hu,
id,
is,
it,
ja,
kk,
ko,
lb,
lt,
lv,
mk,
mn,
nbNO,
nl,
pl,
ptBR,
ro,
ru,
sk,
slSI,
srCYRL,
sr,
sv,
th,
tr,
uk,
ur,
vi,
zh,
zhHK
}
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Afrikaans',
['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember'],
['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
['So.', 'Ma.', 'Di.', 'Wo.', 'Do.', 'Vr.', 'Sa.']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Arabic',
['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوڤمبر', 'ديسمبر'],
['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوڤمبر', 'ديسمبر'],
['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت']
)
language.rtl = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Bulgarian',
['Януари', 'Февруари', 'Март', 'Април', 'Май', 'Юни', 'Юли', 'Август', 'Септември', 'Октомври', 'Ноември', 'Декември'],
['Ян', 'Фев', 'Мар', 'Апр', 'Май', 'Юни', 'Юли', 'Авг', 'Сеп', 'Окт', 'Ное', 'Дек'],
['Нд', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Bosnian',
['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Juni', 'Juli', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'],
['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
['Ned', 'Pon', 'Uto', 'Sri', 'Čet', 'Pet', 'Sub']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Catalan',
['Gener', 'Febrer', 'Març', 'Abril', 'Maig', 'Juny', 'Juliol', 'Agost', 'Setembre', 'Octubre', 'Novembre', 'Desembre'],
['Gen', 'Feb', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Oct', 'Nov', 'Des'],
['Diu', 'Dil', 'Dmr', 'Dmc', 'Dij', 'Div', 'Dis']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Czech',
['leden', 'únor', 'březen', 'duben', 'květen', 'červen', 'červenec', 'srpen', 'září', 'říjen', 'listopad', 'prosinec'],
['led', 'úno', 'bře', 'dub', 'kvě', 'čer', 'čec', 'srp', 'zář', 'říj', 'lis', 'pro'],
['ne', 'po', 'út', 'st', 'čt', 'pá', 'so']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Danish',
['Januar', 'Februar', 'Marts', 'April', 'Maj', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'December'],
['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'German',
['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Estonian',
['Jaanuar', 'Veebruar', 'Märts', 'Aprill', 'Mai', 'Juuni', 'Juuli', 'August', 'September', 'Oktoober', 'November', 'Detsember'],
['Jaan', 'Veebr', 'Märts', 'Apr', 'Mai', 'Juuni', 'Juuli', 'Aug', 'Sept', 'Okt', 'Nov', 'Dets'],
['P', 'E', 'T', 'K', 'N', 'R', 'L']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Greek',
['Ιανουάριος', 'Φεβρουάριος', 'Μάρτιος', 'Απρίλιος', 'Μάϊος', 'Ιούνιος', 'Ιούλιος', 'Αύγουστος', 'Σεπτέμβριος', 'Οκτώβριος', 'Νοέμβριος', 'Δεκέμβριος'],
['Ιαν', 'Φεβ', 'Μαρ', 'Απρ', 'Μαι', 'Ιουν', 'Ιουλ', 'Αυγ', 'Σεπ', 'Οκτ', 'Νοε', 'Δεκ'],
['Κυρ', 'Δευ', 'Τρι', 'Τετ', 'Πεμ', 'Παρ', 'Σαβ']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'English',
['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Spanish',
['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Persian',
['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'],
['فرو', 'ارد', 'خرد', 'تیر', 'مرد', 'شهر', 'مهر', 'آبا', 'آذر', 'دی', 'بهم', 'اسف'],
['یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه', 'شنبه']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Finnish',
['tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', 'toukokuu', 'kesäkuu', 'heinäkuu', 'elokuu', 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'],
['tammi', 'helmi', 'maalis', 'huhti', 'touko', 'kesä', 'heinä', 'elo', 'syys', 'loka', 'marras', 'joulu'],
['su', 'ma', 'ti', 'ke', 'to', 'pe', 'la']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Faroese',
['Januar', 'Februar', 'Mars', 'Apríl', 'Mai', 'Juni', 'Juli', 'August', 'Septembur', 'Oktobur', 'Novembur', 'Desembur'],
['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
['Sun', 'Mán', 'Týs', 'Mik', 'Hós', 'Frí', 'Ley']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'French',
['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sep', 'Oct', 'Nov', 'Déc'],
['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Georgia',
['იანვარი', 'თებერვალი', 'მარტი', 'აპრილი', 'მაისი', 'ივნისი', 'ივლისი', 'აგვისტო', 'სექტემბერი', 'ოქტომბერი', 'ნოემბერი', 'დეკემბერი'],
['იან', 'თებ', 'მარ', 'აპრ', 'მაი', 'ივნ', 'ივლ', 'აგვ', 'სექ', 'ოქტ', 'ნოე', 'დეკ'],
['კვი', 'ორშ', 'სამ', 'ოთხ', 'ხუთ', 'პარ', 'შაბ']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Galician',
['Xaneiro', 'Febreiro', 'Marzo', 'Abril', 'Maio', 'Xuño', 'Xullo', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Decembro'],
['Xan', 'Feb', 'Mar', 'Abr', 'Mai', 'Xuñ', 'Xul', 'Ago', 'Set', 'Out', 'Nov', 'Dec'],
['Dom', 'Lun', 'Mar', 'Mér', 'Xov', 'Ven', 'Sáb']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Hebrew',
['ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר'],
['ינו', 'פבר', 'מרץ', 'אפר', 'מאי', 'יונ', 'יול', 'אוג', 'ספט', 'אוק', 'נוב', 'דצמ'],
['א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ש']
)
language.rtl = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Croatian',
['Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj', 'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac'],
['Sij', 'Velj', 'Ožu', 'Tra', 'Svi', 'Lip', 'Srp', 'Kol', 'Ruj', 'Lis', 'Stu', 'Pro'],
['Ned', 'Pon', 'Uto', 'Sri', 'Čet', 'Pet', 'Sub']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Hungarian',
['Január', 'Február', 'Március', 'Április', 'Május', 'Június', 'Július', 'Augusztus', 'Szeptember', 'Október', 'November', 'December'],
['Jan', 'Febr', 'Márc', 'Ápr', 'Máj', 'Jún', 'Júl', 'Aug', 'Szept', 'Okt', 'Nov', 'Dec'],
['Vas', 'Hét', 'Ke', 'Sze', 'Csü', 'Pén', 'Szo']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Indonesian',
['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'],
['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Icelandic',
['Janúar', 'Febrúar', 'Mars', 'Apríl', 'Maí', 'Júní', 'Júlí', 'Ágúst', 'September', 'Október', 'Nóvember', 'Desember'],
['Jan', 'Feb', 'Mars', 'Apr', 'Maí', 'Jún', 'Júl', 'Ágú', 'Sep', 'Okt', 'Nóv', 'Des'],
['Sun', 'Mán', 'Þri', 'Mið', 'Fim', 'Fös', 'Lau']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Italian',
['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,15 @@
import Language from '../Language'
const language = new Language(
'Japanese',
['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
['日', '月', '火', '水', '木', '金', '土']
)
language.yearSuffix = '年'
language.ymd = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Kazakh',
['Қаңтар', 'Ақпан', 'Наурыз', 'Сәуір', 'Мамыр', 'Маусым', 'Шілде', 'Тамыз', 'Қыркүйек', 'Қазан', 'Қараша', 'Желтоқсан'],
['Қаң', 'Ақп', 'Нау', 'Сәу', 'Мам', 'Мау', 'Шіл', 'Там', 'Қыр', 'Қаз', 'Қар', 'Жел'],
['Жк', 'Дй', 'Сй', 'Ср', 'Бй', 'Жм', 'Сн']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Korean',
['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
['일', '월', '화', '수', '목', '금', '토']
)
language.yearSuffix = '년'
language.ymd = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Luxembourgish',
['Januar', 'Februar', 'Mäerz', 'Abrëll', 'Mee', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
['Jan', 'Feb', 'Mäe', 'Abr', 'Mee', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
['So.', 'Mé.', 'Dë.', 'Më.', 'Do.', 'Fr.', 'Sa.']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Lithuanian',
['Sausis', 'Vasaris', 'Kovas', 'Balandis', 'Gegužė', 'Birželis', 'Liepa', 'Rugpjūtis', 'Rugsėjis', 'Spalis', 'Lapkritis', 'Gruodis'],
['Sau', 'Vas', 'Kov', 'Bal', 'Geg', 'Bir', 'Lie', 'Rugp', 'Rugs', 'Spa', 'Lap', 'Gru'],
['Sek', 'Pir', 'Ant', 'Tre', 'Ket', 'Pen', 'Šeš']
)
language.ymd = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Latvian',
['Janvāris', 'Februāris', 'Marts', 'Aprīlis', 'Maijs', 'Jūnijs', 'Jūlijs', 'Augusts', 'Septembris', 'Oktobris', 'Novembris', 'Decembris'],
['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jūn', 'Jūl', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
['Sv', 'Pr', 'Ot', 'Tr', 'Ce', 'Pk', 'Se']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Macedonian',
['Јануари', 'Февруари', 'Март', 'Април', 'Мај', 'Јуни', 'Јули', 'Август', 'Септември', 'Октомври', 'Ноември', 'Декември'],
['Јан', 'Фев', 'Мар', 'Апр', 'Мај', 'Јун', 'Јул', 'Авг', 'Сеп', 'Окт', 'Ное', 'Дек'],
['Нед', 'Пон', 'Вто', 'Сре', 'Чет', 'Пет', 'Саб']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Mongolia',
['1 дүгээр сар', '2 дугаар сар', '3 дугаар сар', '4 дүгээр сар', '5 дугаар сар', '6 дугаар сар', '7 дугаар сар', '8 дугаар сар', '9 дүгээр сар', '10 дугаар сар', '11 дүгээр сар', '12 дугаар сар'],
['1-р сар', '2-р сар', '3-р сар', '4-р сар', '5-р сар', '6-р сар', '7-р сар', '8-р сар', '9-р сар', '10-р сар', '11-р сар', '12-р сар'],
['Ня', 'Да', 'Мя', 'Лх', 'Пү', 'Ба', 'Бя']
)
language.ymd = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Norwegian Bokmål',
['Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Desember'],
['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
['Sø', 'Ma', 'Ti', 'On', 'To', 'Fr', 'Lø']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Dutch',
['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
['jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Polish',
['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'],
['Nd', 'Pn', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Brazilian',
['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Romanian',
['Ianuarie', 'Februarie', 'Martie', 'Aprilie', 'Mai', 'Iunie', 'Iulie', 'August', 'Septembrie', 'Octombrie', 'Noiembrie', 'Decembrie'],
['Ian', 'Feb', 'Mar', 'Apr', 'Mai', 'Iun', 'Iul', 'Aug', 'Sep', 'Oct', 'Noi', 'Dec'],
['D', 'L', 'Ma', 'Mi', 'J', 'V', 'S']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Russian',
['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
['Янв', 'Февр', 'Март', 'Апр', 'Май', 'Июнь', 'Июль', 'Авг', 'Сент', 'Окт', 'Нояб', 'Дек'],
['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Slovakian',
['január', 'február', 'marec', 'apríl', 'máj', 'jún', 'júl', 'august', 'september', 'október', 'november', 'december'],
['jan', 'feb', 'mar', 'apr', 'máj', 'jún', 'júl', 'aug', 'sep', 'okt', 'nov', 'dec'],
['ne', 'po', 'ut', 'st', 'št', 'pi', 'so']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Sloveian',
['Januar', 'Februar', 'Marec', 'April', 'Maj', 'Junij', 'Julij', 'Avgust', 'September', 'Oktober', 'November', 'December'],
['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
['Ned', 'Pon', 'Tor', 'Sre', 'Čet', 'Pet', 'Sob']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Serbian in Cyrillic script',
['Јануар', 'Фебруар', 'Март', 'Април', 'Мај', 'Јун', 'Јул', 'Август', 'Септембар', 'Октобар', 'Новембар', 'Децембар'],
['Јан', 'Феб', 'Мар', 'Апр', 'Мај', 'Јун', 'Јул', 'Авг', 'Сеп', 'Окт', 'Нов', 'Дец'],
['Нед', 'Пон', 'Уто', 'Сре', 'Чет', 'Пет', 'Суб']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Serbian',
['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Jun', 'Jul', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'],
['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
['Ned', 'Pon', 'Uto', 'Sre', 'Čet', 'Pet', 'Sub']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Swedish',
['Januari', 'Februari', 'Mars', 'April', 'Maj', 'Juni', 'Juli', 'Augusti', 'September', 'Oktober', 'November', 'December'],
['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'],
['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Thai',
['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน', 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'],
['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'],
['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Turkish',
['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'],
['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Eki', 'Kas', 'Ara'],
['Paz', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Ukraine',
['Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень'],
['Січ', 'Лют', 'Бер', 'Квіт', 'Трав', 'Чер', 'Лип', 'Серп', 'Вер', 'Жовт', 'Лист', 'Груд'],
['Нд', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,14 @@
import Language from '../Language'
const language = new Language(
'Urdu',
['جنوری', 'فروری', 'مارچ', 'اپریل', 'مئی', 'جون', 'جولائی', 'اگست', 'سپتمبر', 'اکتوبر', 'نومبر', 'دسمبر'],
['جنوری', 'فروری', 'مارچ', 'اپریل', 'مئی', 'جون', 'جولائی', 'اگست', 'سپتمبر', 'اکتوبر', 'نومبر', 'دسمبر'],
['اتوار', 'پیر', 'منگل', 'بدھ', 'جمعرات', 'جمعہ', 'ہفتہ']
)
language.rtl = true
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,10 @@
import Language from '../Language'
export default new Language(
'Vietnamese',
['Tháng 1', 'Tháng 2', 'Tháng 3', 'Tháng 4', 'Tháng 5', 'Tháng 6', 'Tháng 7', 'Tháng 8', 'Tháng 9', 'Tháng 10', 'Tháng 11', 'Tháng 12'],
['T 01', 'T 02', 'T 03', 'T 04', 'T 05', 'T 06', 'T 07', 'T 08', 'T 09', 'T 10', 'T 11', 'T 12'],
['CN', 'Thứ 2', 'Thứ 3', 'Thứ 4', 'Thứ 5', 'Thứ 6', 'Thứ 7']
)
// eslint-disable-next-line
;

View File

@ -0,0 +1,11 @@
import Language from '../Language'
const language = new Language(
'Chinese_HK',
['壹月', '贰月', '叁月', '肆月', '伍月', '陆月', '柒月', '捌月', '玖月', '拾月', '拾壹月', '拾贰月'],
['壹月', '贰月', '叁月', '肆月', '伍月', '陆月', '柒月', '捌月', '玖月', '拾月', '拾壹月', '拾贰月'],
['日', '壹', '贰', '叁', '肆', '伍', '陆']
)
language.yearSuffix = '年'
export default language

View File

@ -0,0 +1,13 @@
import Language from '../Language'
const language = new Language(
'Chinese',
['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
['日', '一', '二', '三', '四', '五', '六']
)
language.yearSuffix = '年'
export default language
// eslint-disable-next-line
;

View File

@ -0,0 +1,368 @@
<template>
<div
:tabindex="searchable ? -1 : tabindex"
:class="{'multiselect--active': isOpen, 'multiselect--disabled': disabled, 'multiselect--above': isAbove }"
:aria-owns="'listbox-'+id"
class="base-select multiselect"
role="combobox"
@focus="activate()"
@blur="searchable ? false : deactivate()"
@keydown.self.down.prevent="pointerForward()"
@keydown.self.up.prevent="pointerBackward()"
@keypress.enter.tab.stop.self="addPointerElement($event)"
@keyup.esc="deactivate()"
>
<slot :toggle="toggle" name="caret">
<div class="multiselect__select" @mousedown.prevent.stop="toggle()" />
</slot>
<!-- <slot name="clear" :search="search"></slot> -->
<div ref="tags" :class="{'in-valid': invalid}" class="multiselect__tags">
<slot
:search="search"
:remove="removeElement"
:values="visibleValues"
:is-open="isOpen"
name="selection"
>
<div v-show="visibleValues.length > 0" class="multiselect__tags-wrap">
<template v-for="(option, index) of visibleValues" @mousedown.prevent>
<slot :option="option" :search="search" :remove="removeElement" name="tag">
<span :key="index" class="multiselect__tag">
<span v-text="getOptionLabel(option)"/>
<i class="multiselect__tag-icon" tabindex="1" @keypress.enter.prevent="removeElement(option)" @mousedown.prevent="removeElement(option)"/>
</span>
</slot>
</template>
</div>
<template v-if="internalValue && internalValue.length > limit">
<slot name="limit">
<strong class="multiselect__strong" v-text="limitText(internalValue.length - limit)"/>
</slot>
</template>
</slot>
<transition name="multiselect__loading">
<slot name="loading">
<div v-show="loading" class="multiselect__spinner"/>
</slot>
</transition>
<input
ref="search"
:name="name"
:id="id"
:placeholder="placeholder"
:style="inputStyle"
:value="search"
:disabled="disabled"
:tabindex="tabindex"
:aria-controls="'listbox-'+id"
:class="['multiselect__input']"
type="text"
autocomplete="off"
spellcheck="false"
@input="updateSearch($event.target.value)"
@focus.prevent="activate()"
@blur.prevent="deactivate()"
@keyup.esc="deactivate()"
@keydown.down.prevent="pointerForward()"
@keydown.up.prevent="pointerBackward()"
@keypress.enter.prevent.stop.self="addPointerElement($event)"
@keydown.delete.stop="removeLastElement()"
>
<span
v-if="isSingleLabelVisible"
class="multiselect__single"
@mousedown.prevent="toggle"
>
<slot :option="singleValue" name="singleLabel">
<template>{{ currentOptionLabel }}</template>
</slot>
</span>
</div>
<transition name="multiselect">
<div
v-show="isOpen"
ref="list"
:style="{ maxHeight: optimizedHeight + 'px' }"
class="multiselect__content-wrapper"
tabindex="-1"
@focus="activate"
@mousedown.prevent
>
<ul :style="contentStyle" :id="'listbox-'+id" class="multiselect__content" role="listbox">
<slot name="beforeList"/>
<li v-if="multiple && max === internalValue.length">
<span class="multiselect__option">
<slot name="maxElements"> {{ $t('validation.maximum_options_error', { max: max }) }} </slot>
</span>
</li>
<template v-if="!max || internalValue.length < max">
<li
v-for="(option, index) of filteredOptions"
:key="index"
:id="id + '-' + index"
:role="!(option && (option.$isLabel || option.$isDisabled)) ? 'option' : null"
class="multiselect__element"
>
<span
v-if="!(option && (option.$isLabel || option.$isDisabled))"
:class="optionHighlight(index, option)"
:data-select="option && option.isTag ? tagPlaceholder : selectLabelText"
:data-selected="selectedLabelText"
:data-deselect="deselectLabelText"
class="multiselect__option"
@click.stop="select(option)"
@mouseenter.self="pointerSet(index)"
>
<slot :option="option" :search="search" name="option">
<span>{{ getOptionLabel(option) }}</span>
</slot>
</span>
<span
v-if="option && (option.$isLabel || option.$isDisabled)"
:data-select="groupSelect && selectGroupLabelText"
:data-deselect="groupSelect && deselectGroupLabelText"
:class="groupHighlight(index, option)"
class="multiselect__option"
@mouseenter.self="groupSelect && pointerSet(index)"
@mousedown.prevent="selectGroup(option)"
>
<slot :option="option" :search="search" name="option">
<span>{{ getOptionLabel(option) }}</span>
</slot>
</span>
</li>
</template>
<li v-if="showNoOptions && (options.length === 0 && !search && !loading)">
<span class="multiselect__option">
<slot name="noOptions">{{ $t('general.list_is_empty') }}</slot>
</span>
</li>
</ul>
<slot name="afterList"/>
</div>
</transition>
</div>
</template>
<script>
import multiselectMixin from './multiselectMixin'
import pointerMixin from './pointerMixin'
export default {
name: 'vue-multiselect',
mixins: [multiselectMixin, pointerMixin],
props: {
/**
* name attribute to match optional label element
* @default ''
* @type {String}
*/
name: {
type: String,
default: ''
},
/**
* String to show when pointing to an option
* @default 'Press enter to select'
* @type {String}
*/
selectLabel: {
type: String,
default: ''
},
/**
* String to show when pointing to an option
* @default 'Press enter to select'
* @type {String}
*/
selectGroupLabel: {
type: String,
default: ''
},
/**
* String to show next to selected option
* @default 'Selected'
* @type {String}
*/
selectedLabel: {
type: String,
default: 'Selected'
},
/**
* String to show when pointing to an already selected option
* @default 'Press enter to remove'
* @type {String}
*/
deselectLabel: {
type: String,
default: 'Press enter to remove'
},
/**
* String to show when pointing to an already selected option
* @default 'Press enter to remove'
* @type {String}
*/
deselectGroupLabel: {
type: String,
default: 'Press enter to deselect group'
},
/**
* Decide whether to show pointer labels
* @default true
* @type {Boolean}
*/
showLabels: {
type: Boolean,
default: true
},
/**
* Limit the display of selected options. The rest will be hidden within the limitText string.
* @default 99999
* @type {Integer}
*/
limit: {
type: Number,
default: 99999
},
/**
* Sets maxHeight style value of the dropdown
* @default 300
* @type {Integer}
*/
maxHeight: {
type: Number,
default: 300
},
/**
* Function that process the message shown when selected
* elements pass the defined limit.
* @default 'and * more'
* @param {Int} count Number of elements more than limit
* @type {Function}
*/
limitText: {
type: Function,
default: count => `and ${count} more`
},
/**
* Set true to trigger the loading spinner.
* @default False
* @type {Boolean}
*/
loading: {
type: Boolean,
default: false
},
/**
* Disables the multiselect if true.
* @default false
* @type {Boolean}
*/
disabled: {
type: Boolean,
default: false
},
/**
* Fixed opening direction
* @default ''
* @type {String}
*/
openDirection: {
type: String,
default: ''
},
/**
* Shows slot with message about empty options
* @default true
* @type {Boolean}
*/
showNoOptions: {
type: Boolean,
default: true
},
showNoResults: {
type: Boolean,
default: true
},
tabindex: {
type: Number,
default: 0
},
invalid: {
type: Boolean,
default: false
}
},
computed: {
isSingleLabelVisible () {
return (
(this.singleValue || this.singleValue === 0) &&
(!this.isOpen || !this.searchable) &&
!this.visibleValues.length
)
},
isPlaceholderVisible () {
return !this.internalValue.length && (!this.searchable || !this.isOpen)
},
visibleValues () {
return this.multiple ? this.internalValue.slice(0, this.limit) : []
},
singleValue () {
return this.internalValue[0]
},
deselectLabelText () {
return this.showLabels ? this.deselectLabel : ''
},
deselectGroupLabelText () {
return this.showLabels ? this.deselectGroupLabel : ''
},
selectLabelText () {
return this.showLabels ? this.selectLabel : ''
},
selectGroupLabelText () {
return this.showLabels ? this.selectGroupLabel : ''
},
selectedLabelText () {
return this.showLabels ? this.selectedLabel : ''
},
inputStyle () {
if (
this.searchable ||
(this.multiple && this.value && this.value.length)
) {
// Hide input by setting the width to 0 allowing it to receive focus
return this.isOpen
? { width: '100%' }
: ((this.value) ? { width: '0', position: 'absolute', padding: '0' } : '')
}
},
contentStyle () {
return this.options.length
? { display: 'inline-block' }
: { display: 'block' }
},
isAbove () {
if (this.openDirection === 'above' || this.openDirection === 'top') {
return true
} else if (
this.openDirection === 'below' ||
this.openDirection === 'bottom'
) {
return false
} else {
return this.preferredOpenDirection === 'above'
}
},
showSearchInput () {
return (
this.searchable &&
(this.hasSingleSelectedSlot &&
(this.visibleSingleValue || this.visibleSingleValue === 0)
? this.isOpen
: true)
)
}
}
}
</script>

View File

@ -0,0 +1,7 @@
import Multiselect from './Multiselect'
import multiselectMixin from './multiselectMixin'
import pointerMixin from './pointerMixin'
export default Multiselect
export { Multiselect, multiselectMixin, pointerMixin }

View File

@ -0,0 +1,722 @@
function isEmpty (opt) {
if (opt === 0) return false
if (Array.isArray(opt) && opt.length === 0) return true
return !opt
}
function not (fun) {
return (...params) => !fun(...params)
}
function includes (str, query) {
/* istanbul ignore else */
if (str === undefined) str = 'undefined'
if (str === null) str = 'null'
if (str === false) str = 'false'
const text = str.toString().toLowerCase()
return text.indexOf(query.trim()) !== -1
}
function filterOptions (options, search, label, customLabel) {
return options.filter(option => includes(customLabel(option, label), search))
}
function stripGroups (options) {
return options.filter(option => !option.$isLabel)
}
function flattenOptions (values, label) {
return (options) =>
options.reduce((prev, curr) => {
/* istanbul ignore else */
if (curr[values] && curr[values].length) {
prev.push({
$groupLabel: curr[label],
$isLabel: true
})
return prev.concat(curr[values])
}
return prev
}, [])
}
function filterGroups (search, label, values, groupLabel, customLabel) {
return (groups) =>
groups.map(group => {
/* istanbul ignore else */
if (!group[values]) {
console.warn(`Options passed to vue-multiselect do not contain groups, despite the config.`)
return []
}
const groupOptions = filterOptions(group[values], search, label, customLabel)
return groupOptions.length
? {
[groupLabel]: group[groupLabel],
[values]: groupOptions
}
: []
})
}
const flow = (...fns) => x => fns.reduce((v, f) => f(v), x)
export default {
data () {
return {
search: '',
isOpen: false,
preferredOpenDirection: 'below',
optimizedHeight: this.maxHeight
}
},
props: {
initialSearch: {
type: String,
default: ''
},
/**
* Decide whether to filter the results based on search query.
* Useful for async filtering, where we search through more complex data.
* @type {Boolean}
*/
internalSearch: {
type: Boolean,
default: true
},
/**
* Array of available options: Objects, Strings or Integers.
* If array of objects, visible label will default to option.label.
* If `labal` prop is passed, label will equal option['label']
* @type {Array}
*/
options: {
type: Array,
required: true
},
/**
* Equivalent to the `multiple` attribute on a `<select>` input.
* @default false
* @type {Boolean}
*/
multiple: {
type: Boolean,
default: false
},
/**
* Presets the selected options value.
* @type {Object||Array||String||Integer}
*/
value: {
type: null,
default () {
return []
}
},
/**
* Key to compare objects
* @default 'id'
* @type {String}
*/
trackBy: {
type: String
},
/**
* Label to look for in option Object
* @default 'label'
* @type {String}
*/
label: {
type: String
},
/**
* Enable/disable search in options
* @default true
* @type {Boolean}
*/
searchable: {
type: Boolean,
default: true
},
/**
* Clear the search input after `)
* @default true
* @type {Boolean}
*/
clearOnSelect: {
type: Boolean,
default: true
},
/**
* Hide already selected options
* @default false
* @type {Boolean}
*/
hideSelected: {
type: Boolean,
default: false
},
/**
* Equivalent to the `placeholder` attribute on a `<select>` input.
* @default 'Select option'
* @type {String}
*/
placeholder: {
type: String,
default: 'Select option'
},
/**
* Allow to remove all selected values
* @default true
* @type {Boolean}
*/
allowEmpty: {
type: Boolean,
default: true
},
/**
* Reset this.internalValue, this.search after this.internalValue changes.
* Useful if want to create a stateless dropdown.
* @default false
* @type {Boolean}
*/
resetAfter: {
type: Boolean,
default: false
},
/**
* Enable/disable closing after selecting an option
* @default true
* @type {Boolean}
*/
closeOnSelect: {
type: Boolean,
default: true
},
/**
* Function to interpolate the custom label
* @default false
* @type {Function}
*/
customLabel: {
type: Function,
default (option, label) {
if (isEmpty(option)) return ''
return label ? option[label] : option
}
},
/**
* Disable / Enable tagging
* @default false
* @type {Boolean}
*/
taggable: {
type: Boolean,
default: false
},
/**
* String to show when highlighting a potential tag
* @default 'Press enter to create a tag'
* @type {String}
*/
tagPlaceholder: {
type: String,
default: 'Press enter to create a tag'
},
/**
* By default new tags will appear above the search results.
* Changing to 'bottom' will revert this behaviour
* and will proritize the search results
* @default 'top'
* @type {String}
*/
tagPosition: {
type: String,
default: 'top'
},
/**
* Number of allowed selected options. No limit if 0.
* @default 0
* @type {Number}
*/
max: {
type: [Number, Boolean],
default: false
},
/**
* Will be passed with all events as second param.
* Useful for identifying events origin.
* @default null
* @type {String|Integer}
*/
id: {
default: null
},
/**
* Limits the options displayed in the dropdown
* to the first X options.
* @default 1000
* @type {Integer}
*/
optionsLimit: {
type: Number,
default: 1000
},
/**
* Name of the property containing
* the group values
* @default 1000
* @type {String}
*/
groupValues: {
type: String
},
/**
* Name of the property containing
* the group label
* @default 1000
* @type {String}
*/
groupLabel: {
type: String
},
/**
* Allow to select all group values
* by selecting the group label
* @default false
* @type {Boolean}
*/
groupSelect: {
type: Boolean,
default: false
},
/**
* Array of keyboard keys to block
* when selecting
* @default 1000
* @type {String}
*/
blockKeys: {
type: Array,
default () {
return []
}
},
/**
* Prevent from wiping up the search value
* @default false
* @type {Boolean}
*/
preserveSearch: {
type: Boolean,
default: false
},
/**
* Select 1st options if value is empty
* @default false
* @type {Boolean}
*/
preselectFirst: {
type: Boolean,
default: false
}
},
mounted () {
/* istanbul ignore else */
if (!this.multiple && this.max) {
console.warn('[Vue-Multiselect warn]: Max prop should not be used when prop Multiple equals false.')
}
if (
this.preselectFirst &&
!this.internalValue.length &&
this.options.length
) {
this.select(this.filteredOptions[0])
}
if (this.initialSearch) {
this.search = this.initialSearch
}
},
computed: {
internalValue () {
return this.value || this.value === 0
? Array.isArray(this.value) ? this.value : [this.value]
: []
},
filteredOptions () {
const search = this.search || ''
const normalizedSearch = search.toLowerCase().trim()
let options = this.options.concat()
/* istanbul ignore else */
if (this.internalSearch) {
options = this.groupValues
? this.filterAndFlat(options, normalizedSearch, this.label)
: filterOptions(options, normalizedSearch, this.label, this.customLabel)
} else {
options = this.groupValues ? flattenOptions(this.groupValues, this.groupLabel)(options) : options
}
options = this.hideSelected
? options.filter(not(this.isSelected))
: options
/* istanbul ignore else */
if (this.taggable && normalizedSearch.length && !this.isExistingOption(normalizedSearch)) {
if (this.tagPosition === 'bottom') {
options.push({ isTag: true, label: search })
} else {
options.unshift({ isTag: true, label: search })
}
}
return options.slice(0, this.optionsLimit)
},
valueKeys () {
if (this.trackBy) {
return this.internalValue.map(element => element[this.trackBy])
} else {
return this.internalValue
}
},
optionKeys () {
const options = this.groupValues ? this.flatAndStrip(this.options) : this.options
return options.map(element => this.customLabel(element, this.label).toString().toLowerCase())
},
currentOptionLabel () {
return this.multiple
? this.searchable ? '' : this.placeholder
: this.internalValue.length
? this.getOptionLabel(this.internalValue[0])
: this.searchable ? '' : this.placeholder
}
},
watch: {
internalValue () {
/* istanbul ignore else */
if (this.resetAfter && this.internalValue.length) {
this.search = ''
this.$emit('input', this.multiple ? [] : null)
}
},
search () {
this.$emit('search-change', this.search, this.id)
}
},
methods: {
/**
* Returns the internalValue in a way it can be emited to the parent
* @returns {Object||Array||String||Integer}
*/
getValue () {
return this.multiple
? this.internalValue
: this.internalValue.length === 0
? null
: this.internalValue[0]
},
/**
* Filters and then flattens the options list
* @param {Array}
* @returns {Array} returns a filtered and flat options list
*/
filterAndFlat (options, search, label) {
return flow(
filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
flattenOptions(this.groupValues, this.groupLabel)
)(options)
},
/**
* Flattens and then strips the group labels from the options list
* @param {Array}
* @returns {Array} returns a flat options list without group labels
*/
flatAndStrip (options) {
return flow(
flattenOptions(this.groupValues, this.groupLabel),
stripGroups
)(options)
},
/**
* Updates the search value
* @param {String}
*/
updateSearch (query) {
this.search = query
this.$emit('value', this.search)
},
/**
* Finds out if the given query is already present
* in the available options
* @param {String}
* @returns {Boolean} returns true if element is available
*/
isExistingOption (query) {
return !this.options
? false
: this.optionKeys.indexOf(query) > -1
},
/**
* Finds out if the given element is already present
* in the result value
* @param {Object||String||Integer} option passed element to check
* @returns {Boolean} returns true if element is selected
*/
isSelected (option) {
const opt = this.trackBy
? option[this.trackBy]
: option
return this.valueKeys.indexOf(opt) > -1
},
/**
* Finds out if the given option is disabled
* @param {Object||String||Integer} option passed element to check
* @returns {Boolean} returns true if element is disabled
*/
isOptionDisabled (option) {
return !!option.$isDisabled
},
/**
* Returns empty string when options is null/undefined
* Returns tag query if option is tag.
* Returns the customLabel() results and casts it to string.
*
* @param {Object||String||Integer} Passed option
* @returns {Object||String}
*/
getOptionLabel (option) {
if (isEmpty(option)) return ''
/* istanbul ignore else */
if (option.isTag) return option.label
/* istanbul ignore else */
if (option.$isLabel) return option.$groupLabel
let label = this.customLabel(option, this.label)
/* istanbul ignore else */
if (isEmpty(label)) return ''
return label
},
/**
* Add the given option to the list of selected options
* or sets the option as the selected option.
* If option is already selected -> remove it from the results.
*
* @param {Object||String||Integer} option to select/deselect
* @param {Boolean} block removing
*/
select (option, key) {
/* istanbul ignore else */
if (option.$isLabel && this.groupSelect) {
this.selectGroup(option)
return
}
if (this.blockKeys.indexOf(key) !== -1 ||
this.disabled ||
option.$isDisabled ||
option.$isLabel
) return
/* istanbul ignore else */
if (this.max && this.multiple && this.internalValue.length === this.max) return
/* istanbul ignore else */
if (key === 'Tab' && !this.pointerDirty) return
if (option.isTag) {
this.$emit('tag', option.label, this.id)
this.search = ''
if (this.closeOnSelect && !this.multiple) this.deactivate()
} else {
const isSelected = this.isSelected(option)
if (isSelected) {
if (key !== 'Tab') this.removeElement(option)
return
}
this.$emit('select', option, this.id)
if (this.multiple) {
this.$emit('input', this.internalValue.concat([option]), this.id)
} else {
this.$emit('input', option, this.id)
}
/* istanbul ignore else */
if (this.clearOnSelect) this.search = ''
}
/* istanbul ignore else */
if (this.closeOnSelect) this.deactivate()
},
/**
* Add the given group options to the list of selected options
* If all group optiona are already selected -> remove it from the results.
*
* @param {Object||String||Integer} group to select/deselect
*/
selectGroup (selectedGroup) {
const group = this.options.find(option => {
return option[this.groupLabel] === selectedGroup.$groupLabel
})
if (!group) return
if (this.wholeGroupSelected(group)) {
this.$emit('remove', group[this.groupValues], this.id)
const newValue = this.internalValue.filter(
option => group[this.groupValues].indexOf(option) === -1
)
this.$emit('input', newValue, this.id)
} else {
const optionsToAdd = group[this.groupValues].filter(
option => !(this.isOptionDisabled(option) || this.isSelected(option))
)
this.$emit('select', optionsToAdd, this.id)
this.$emit(
'input',
this.internalValue.concat(optionsToAdd),
this.id
)
}
},
/**
* Helper to identify if all values in a group are selected
*
* @param {Object} group to validated selected values against
*/
wholeGroupSelected (group) {
return group[this.groupValues].every(option => this.isSelected(option) || this.isOptionDisabled(option)
)
},
/**
* Helper to identify if all values in a group are disabled
*
* @param {Object} group to check for disabled values
*/
wholeGroupDisabled (group) {
return group[this.groupValues].every(this.isOptionDisabled)
},
/**
* Removes the given option from the selected options.
* Additionally checks this.allowEmpty prop if option can be removed when
* it is the last selected option.
*
* @param {type} option description
* @returns {type} description
*/
removeElement (option, shouldClose = true) {
/* istanbul ignore else */
if (this.disabled) return
/* istanbul ignore else */
if (option.$isDisabled) return
/* istanbul ignore else */
if (!this.allowEmpty && this.internalValue.length <= 1) {
this.deactivate()
return
}
const index = typeof option === 'object'
? this.valueKeys.indexOf(option[this.trackBy])
: this.valueKeys.indexOf(option)
this.$emit('remove', option, this.id)
if (this.multiple) {
const newValue = this.internalValue.slice(0, index).concat(this.internalValue.slice(index + 1))
this.$emit('input', newValue, this.id)
} else {
this.$emit('input', null, this.id)
}
/* istanbul ignore else */
if (this.closeOnSelect && shouldClose) this.deactivate()
},
/**
* Calls this.removeElement() with the last element
* from this.internalValue (selected element Array)
*
* @fires this#removeElement
*/
removeLastElement () {
/* istanbul ignore else */
if (this.blockKeys.indexOf('Delete') !== -1) return
/* istanbul ignore else */
if (this.search.length === 0 && Array.isArray(this.internalValue) && this.internalValue.length) {
this.removeElement(this.internalValue[this.internalValue.length - 1], false)
}
},
/**
* Opens the multiselects dropdown.
* Sets this.isOpen to TRUE
*/
activate () {
/* istanbul ignore else */
if (this.isOpen || this.disabled) return
this.adjustPosition()
/* istanbul ignore else */
if (this.groupValues && this.pointer === 0 && this.filteredOptions.length) {
this.pointer = 1
}
this.isOpen = true
/* istanbul ignore else */
if (this.searchable) {
if (!this.preserveSearch) this.search = ''
this.$nextTick(() => this.$refs.search && this.$refs.search.focus())
} else {
this.$el.focus()
}
this.$emit('open', this.id)
},
/**
* Closes the multiselects dropdown.
* Sets this.isOpen to FALSE
*/
deactivate () {
/* istanbul ignore else */
if (!this.isOpen) return
this.isOpen = false
/* istanbul ignore else */
if (this.searchable) {
this.$refs.search && this.$refs.search.blur()
} else {
this.$el.blur()
}
if (!this.preserveSearch) this.search = ''
this.$emit('close', this.getValue(), this.id)
},
/**
* Call this.activate() or this.deactivate()
* depending on this.isOpen value.
*
* @fires this#activate || this#deactivate
* @property {Boolean} isOpen indicates if dropdown is open
*/
toggle () {
this.isOpen
? this.deactivate()
: this.activate()
},
/**
* Updates the hasEnoughSpace variable used for
* detecting where to expand the dropdown
*/
adjustPosition () {
if (typeof window === 'undefined') return
const spaceAbove = this.$el.getBoundingClientRect().top
const spaceBelow = window.innerHeight - this.$el.getBoundingClientRect().bottom
const hasEnoughSpaceBelow = spaceBelow > this.maxHeight
if (hasEnoughSpaceBelow || spaceBelow > spaceAbove || this.openDirection === 'below' || this.openDirection === 'bottom') {
this.preferredOpenDirection = 'below'
this.optimizedHeight = Math.min(spaceBelow - 40, this.maxHeight)
} else {
this.preferredOpenDirection = 'above'
this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight)
}
}
}
}

View File

@ -0,0 +1,140 @@
export default {
data () {
return {
pointer: 0,
pointerDirty: false
}
},
props: {
/**
* Enable/disable highlighting of the pointed value.
* @type {Boolean}
* @default true
*/
showPointer: {
type: Boolean,
default: true
},
optionHeight: {
type: Number,
default: 40
}
},
computed: {
pointerPosition () {
return this.pointer * this.optionHeight
},
visibleElements () {
return this.optimizedHeight / this.optionHeight
}
},
watch: {
filteredOptions () {
this.pointerAdjust()
},
isOpen () {
this.pointerDirty = false
},
pointer () {
this.$refs.search.setAttribute('aria-activedescendant', this.id + '-' + this.pointer.toString())
}
},
methods: {
optionHighlight (index, option) {
return {
'multiselect__option--highlight': index === this.pointer && this.showPointer,
'multiselect__option--selected': this.isSelected(option)
}
},
groupHighlight (index, selectedGroup) {
if (!this.groupSelect) {
return ['multiselect__option--group', 'multiselect__option--disabled']
}
const group = this.options.find(option => {
return option[this.groupLabel] === selectedGroup.$groupLabel
})
return group && !this.wholeGroupDisabled(group) ? [
'multiselect__option--group',
{ 'multiselect__option--highlight': index === this.pointer && this.showPointer },
{ 'multiselect__option--group-selected': this.wholeGroupSelected(group) }
] : 'multiselect__option--disabled'
},
addPointerElement ({ key } = 'Enter') {
/* istanbul ignore else */
if (this.filteredOptions.length > 0) {
this.select(this.filteredOptions[this.pointer], key)
}
this.pointerReset()
},
pointerForward () {
/* istanbul ignore else */
if (this.pointer < this.filteredOptions.length - 1) {
this.pointer++
/* istanbul ignore next */
if (this.$refs.list.scrollTop <= this.pointerPosition - (this.visibleElements - 1) * this.optionHeight) {
this.$refs.list.scrollTop = this.pointerPosition - (this.visibleElements - 1) * this.optionHeight
}
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
) this.pointerForward()
}
this.pointerDirty = true
},
pointerBackward () {
if (this.pointer > 0) {
this.pointer--
/* istanbul ignore else */
if (this.$refs.list.scrollTop >= this.pointerPosition) {
this.$refs.list.scrollTop = this.pointerPosition
}
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
) this.pointerBackward()
} else {
/* istanbul ignore else */
if (
this.filteredOptions[this.pointer] &&
this.filteredOptions[0].$isLabel &&
!this.groupSelect
) this.pointerForward()
}
this.pointerDirty = true
},
pointerReset () {
/* istanbul ignore else */
if (!this.closeOnSelect) return
this.pointer = 0
/* istanbul ignore else */
if (this.$refs.list) {
this.$refs.list.scrollTop = 0
}
},
pointerAdjust () {
/* istanbul ignore else */
if (this.pointer >= this.filteredOptions.length - 1) {
this.pointer = this.filteredOptions.length
? this.filteredOptions.length - 1
: 0
}
if (this.filteredOptions.length > 0 &&
this.filteredOptions[this.pointer].$isLabel &&
!this.groupSelect
) {
this.pointerForward()
}
},
pointerSet (index) {
this.pointer = index
this.pointerDirty = true
}
}
}

View File

@ -0,0 +1,64 @@
import { pick } from '../helpers'
export default class Column {
constructor (columnComponent) {
const properties = pick(columnComponent, [
'show', 'label', 'dataType', 'sortable', 'sortBy', 'filterable',
'filterOn', 'hidden', 'formatter', 'cellClass', 'headerClass', 'sortAs'
])
for (const property in properties) {
this[property] = columnComponent[property]
}
this.template = columnComponent.$scopedSlots.default
}
isFilterable () {
return this.filterable
}
getFilterFieldName () {
return this.filterOn || this.show
}
isSortable () {
return this.sortable
}
getSortPredicate (sortOrder, allColumns) {
const sortFieldName = this.getSortFieldName()
const sortColumn = allColumns.find(column => (column.sortAs === sortFieldName || column.show === sortFieldName))
const dataType = sortColumn.dataType
if (dataType.startsWith('date') || dataType === 'numeric') {
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName)
const value2 = row2.getSortableValue(sortFieldName)
if (sortOrder === 'desc') {
return value2 < value1 ? -1 : 1
}
return value1 < value2 ? -1 : 1
}
}
return (row1, row2) => {
const value1 = row1.getSortableValue(sortFieldName)
const value2 = row2.getSortableValue(sortFieldName)
if (sortOrder === 'desc') {
return value2.localeCompare(value1)
}
return value1.localeCompare(value2)
}
}
getSortFieldName () {
return this.sortBy || this.sortAs || this.show
}
}

View File

@ -0,0 +1,61 @@
import moment from 'moment'
import { get } from '../helpers'
export default class Row {
constructor (data, columns) {
this.data = data
this.columns = columns
}
getValue (columnName) {
return get(this.data, columnName)
}
getColumn (columnName) {
return this.columns.find(column => (column.show === columnName || column.sortAs === columnName))
}
getFilterableValue (columnName) {
const value = this.getValue(columnName)
if (!value) {
return ''
}
return value.toString().toLowerCase()
}
getSortableValue (columnName) {
const dataType = this.getColumn(columnName).dataType
let value = this.getValue(columnName)
if (value === undefined || value === null) {
return ''
}
if (value instanceof String) {
value = value.toLowerCase()
}
if (dataType.startsWith('date')) {
const format = dataType.replace('date:', '')
return moment(value, format).format('YYYYMMDDHHmmss')
}
if (dataType === 'numeric') {
return value
}
return value.toString()
}
passesFilter (filter) {
return this.columns
.filter(column => column.isFilterable())
.map(column => this.getFilterableValue(column.getFilterFieldName()))
.filter(filterableValue => filterableValue.indexOf(filter.toLowerCase()) >= 0)
.length
}
}

View File

@ -0,0 +1,120 @@
<template>
<nav v-if="shouldShowPagination">
<ul class="pagination justify-content-center">
<li :class="{ disabled: pagination.currentPage === 1 }">
<a
:class="{ disabled: pagination.currentPage === 1 }"
@click="pageClicked( pagination.currentPage - 1 )"
>
<i class="left chevron icon">«</i>
</a>
</li>
<li v-if="hasFirst" :class="{ active: isActive(1) }" class="page-item">
<a class="page-link" @click="pageClicked(1)">1</a>
</li>
<li v-if="hasFirstEllipsis"><span class="pagination-ellipsis">&hellip;</span></li>
<li v-for="page in pages" :key="page" :class="{ active: isActive(page), disabled: page === '...' }" class="page-item">
<a class="page-link" @click="pageClicked(page)">{{ page }}</a>
</li>
<li v-if="hasLastEllipsis"><span class="pagination-ellipsis">&hellip;</span></li>
<li
v-if="hasLast"
:class="{ active: isActive(this.pagination.totalPages) }"
class="page-item"
>
<a class="page-link" @click="pageClicked(pagination.totalPages)">
{{ pagination.totalPages }}
</a>
</li>
<li>
<a
:class="{ disabled: pagination.currentPage === pagination.totalPages }"
@click="pageClicked( pagination.currentPage + 1 )"
>
<i class="right chevron icon">»</i>
</a>
</li>
</ul>
</nav>
</template>
<script>
export default {
props: {
pagination: {
type: Object,
default: () => ({})
}
},
computed: {
pages () {
return this.pagination.totalPages === undefined
? []
: this.pageLinks()
},
hasFirst () {
return this.pagination.currentPage >= 4 || this.pagination.totalPages < 10
},
hasLast () {
return this.pagination.currentPage <= this.pagination.totalPages - 3 || this.pagination.totalPages < 10
},
hasFirstEllipsis () {
return this.pagination.currentPage >= 4 && this.pagination.totalPages >= 10
},
hasLastEllipsis () {
return this.pagination.currentPage <= this.pagination.totalPages - 3 && this.pagination.totalPages >= 10
},
shouldShowPagination () {
if (this.pagination.totalPages === undefined) {
return false
}
if (this.pagination.count === 0) {
return false
}
return this.pagination.totalPages > 1
}
},
methods: {
isActive (page) {
const currentPage = this.pagination.currentPage || 1
return currentPage === page
},
pageClicked (page) {
if (page === '...' ||
page === this.pagination.currentPage ||
page > this.pagination.totalPages ||
page < 1) {
return
}
this.$emit('pageChange', page)
},
pageLinks () {
const pages = []
let left = 2
let right = this.pagination.totalPages - 1
if (this.pagination.totalPages >= 10) {
left = Math.max(1, this.pagination.currentPage - 2)
right = Math.min(this.pagination.currentPage + 2, this.pagination.totalPages)
}
for (let i = left; i <= right; i++) {
pages.push(i)
}
return pages
}
}
}
</script>

View File

@ -0,0 +1,24 @@
export default {
functional: true,
props: ['column', 'row', 'responsiveLabel'],
render (createElement, { props }) {
const data = {}
if (props.column.cellClass) {
data.class = props.column.cellClass
}
if (props.column.template) {
return createElement('td', data, props.column.template(props.row.data))
}
data.domProps = {}
data.domProps.innerHTML = props.column.formatter(props.row.getValue(props.column.show), props.row.data)
return createElement('td', [
createElement('span', props.responsiveLabel), data.domProps.innerHTML
])
}
}

View File

@ -0,0 +1,32 @@
<template>
<!-- Never render the contents -->
<!-- The scoped slot won't have the required data -->
<div v-if="false">
<slot></slot>
</div>
</template>
<script>
import settings from '../settings'
export default {
props: {
show: { required: false, type: String },
label: { default: null, type: String },
dataType: { default: 'string', type: String },
sortable: { default: true, type: Boolean },
sortBy: { default: null },
filterable: { default: true, type: Boolean },
sortAs: { default: null },
filterOn: { default: null },
formatter: { default: v => v, type: Function },
hidden: { default: false, type: Boolean },
cellClass: { default: settings.cellClass },
headerClass: { default: settings.headerClass },
}
}
</script>

View File

@ -0,0 +1,72 @@
<template>
<th
v-if="this.isVisible"
slot-scope="col"
:aria-sort="ariaSort"
:aria-disabled="ariaDisabled"
:class="headerClass"
role="columnheader"
@click="clicked"
>
{{ label }}
</th>
</template>
<script>
import { classList } from '../helpers'
export default {
props: ['column', 'sort'],
computed: {
ariaDisabled () {
if (!this.column.isSortable()) {
return 'true'
}
return false
},
ariaSort () {
if (!this.column.isSortable ()) {
return false
}
if ((this.column.sortAs || this.column.show) !== this.sort.fieldName) {
return 'none'
}
return this.sort.order === 'asc' ? 'ascending' : 'descending';
},
headerClass () {
if (!this.column.isSortable()) {
return classList('table-component__th', this.column.headerClass);
}
if ((this.column.sortAs || this.column.show) !== this.sort.fieldName) {
return classList('table-component__th table-component__th--sort', this.column.headerClass);
}
return classList(`table-component__th table-component__th--sort-${this.sort.order}`, this.column.headerClass);
},
isVisible () {
return !this.column.hidden
},
label () {
if (this.column.label === null) {
return this.column.show
}
return this.column.label
}
},
methods: {
clicked () {
if (this.column.isSortable()) {
this.$emit('click', this.column)
}
}
}
}
</script>

View File

@ -0,0 +1,330 @@
<template>
<div class="table-component">
<div v-if="showFilter && filterableColumnExists" class="table-component__filter">
<input
:class="fullFilterInputClass"
v-model="filter"
:placeholder="filterPlaceholder"
type="text"
>
<a v-if="filter" class="table-component__filter__clear" @click="filter = ''">×</a>
</div>
<div class="table-component__table-wrapper">
<base-loader v-if="loading" class="table-loader" />
<table :class="fullTableClass">
<caption
v-if="showCaption"
class="table-component__table__caption"
role="alert"
aria-live="polite"
>{{ ariaCaption }}</caption>
<thead :class="fullTableHeadClass">
<tr>
<table-column-header
v-for="column in columns"
:key="column.show || column.show"
:sort="sort"
:column="column"
@click="changeSorting"
/>
</tr>
</thead>
<tbody :class="fullTableBodyClass">
<table-row
v-for="row in displayedRows"
:key="row.vueTableComponentInternalRowId"
:row="row"
:columns="columns"
@rowClick="emitRowClick"
/>
</tbody>
<tfoot>
<slot :rows="rows" name="tfoot" />
</tfoot>
</table>
</div>
<div v-if="displayedRows.length === 0 && !loading" class="table-component__message">{{ filterNoResults }}</div>
<div style="display:none;">
<slot />
</div>
<pagination v-if="pagination && !loading" :pagination="pagination" @pageChange="pageChange" />
</div>
</template>
<script>
import Column from '../classes/Column'
import expiringStorage from '../expiring-storage'
import Row from '../classes/Row'
import TableColumnHeader from './TableColumnHeader'
import TableRow from './TableRow'
import settings from '../settings'
import Pagination from './Pagination'
import { classList, pick } from '../helpers'
export default {
components: {
TableColumnHeader,
TableRow,
Pagination
},
props: {
data: { default: () => [], type: [Array, Function] },
showFilter: { type: Boolean, default: true },
showCaption: { type: Boolean, default: true },
sortBy: { default: '', type: String },
sortOrder: { default: '', type: String },
cacheKey: { default: null },
cacheLifetime: { default: 5 },
tableClass: { default: () => settings.tableClass },
theadClass: { default: () => settings.theadClass },
tbodyClass: { default: () => settings.tbodyClass },
filterInputClass: { default: () => settings.filterInputClass },
filterPlaceholder: { default: () => settings.filterPlaceholder },
filterNoResults: { default: () => settings.filterNoResults }
},
data: () => ({
columns: [],
rows: [],
filter: '',
sort: {
fieldName: '',
order: ''
},
pagination: null,
loading: false,
localSettings: {}
}),
computed: {
fullTableClass () {
return classList('table-component__table', this.tableClass)
},
fullTableHeadClass () {
return classList('table-component__table__head', this.theadClass)
},
fullTableBodyClass () {
return classList('table-component__table__body', this.tbodyClass)
},
fullFilterInputClass () {
return classList('table-component__filter__field', this.filterInputClass)
},
ariaCaption () {
if (this.sort.fieldName === '') {
return 'Table not sorted'
}
return (
`Table sorted by ${this.sort.fieldName} ` +
(this.sort.order === 'asc' ? '(ascending)' : '(descending)')
)
},
usesLocalData () {
return Array.isArray(this.data)
},
displayedRows () {
if (!this.usesLocalData) {
return this.sortedRows
}
if (!this.showFilter) {
return this.sortedRows
}
if (!this.columns.filter(column => column.isFilterable()).length) {
return this.sortedRows
}
return this.sortedRows.filter(row => row.passesFilter(this.filter))
},
sortedRows () {
if (!this.usesLocalData) {
return this.rows
}
if (this.sort.fieldName === '') {
return this.rows
}
if (this.columns.length === 0) {
return this.rows
}
const sortColumn = this.getColumn(this.sort.fieldName)
if (!sortColumn) {
return this.rows
}
return this.rows.sort(
sortColumn.getSortPredicate(this.sort.order, this.columns)
)
},
filterableColumnExists () {
return this.columns.filter(c => c.isFilterable()).length > 0
},
storageKey () {
return this.cacheKey
? `vue-table-component.${this.cacheKey}`
: `vue-table-component.${window.location.host}${window.location.pathname}${this.cacheKey}`
}
},
watch: {
filter () {
if (!this.usesLocalData) {
this.mapDataToRows()
}
this.saveState()
},
data () {
if (this.usesLocalData) {
this.mapDataToRows()
}
}
},
created () {
this.sort.order = this.sortOrder
this.restoreState()
},
async mounted () {
this.sort.fieldName = this.sortBy
const columnComponents = this.$slots.default
.filter(column => column.componentInstance)
.map(column => column.componentInstance)
this.columns = columnComponents.map(column => new Column(column))
columnComponents.forEach(columnCom => {
Object.keys(columnCom.$options.props).forEach(prop =>
columnCom.$watch(prop, () => {
this.columns = columnComponents.map(column => new Column(column))
})
)
})
await this.mapDataToRows()
},
methods: {
async pageChange (page) {
this.pagination.currentPage = page
await this.mapDataToRows()
},
async mapDataToRows () {
const data = this.usesLocalData
? this.prepareLocalData()
: await this.fetchServerData()
let rowId = 0
this.rows = data
.map(rowData => {
rowData.vueTableComponentInternalRowId = rowId++
return rowData
})
.map(rowData => new Row(rowData, this.columns))
},
prepareLocalData () {
this.pagination = null
return this.data
},
async fetchServerData () {
const page = (this.pagination && this.pagination.currentPage) || 1
this.loading = true
const response = await this.data({
filter: this.filter,
sort: this.sort,
page: page
})
this.pagination = response.pagination
this.loading = false
return response.data
},
async refresh () {
if (this.pagination) {
this.pagination.currentPage = 1
}
await this.mapDataToRows()
},
changeSorting (column) {
if (this.sort.fieldName !== (column.sortAs || column.show)) {
this.sort.fieldName = (column.sortAs || column.show)
this.sort.order = 'asc'
} else {
this.sort.order = this.sort.order === 'asc' ? 'desc' : 'asc'
}
if (!this.usesLocalData) {
this.mapDataToRows()
}
this.saveState()
},
getColumn (columnName) {
return this.columns.find(column => column.show === columnName)
},
saveState () {
expiringStorage.set(
this.storageKey,
pick(this.$data, ['filter', 'sort']),
this.cacheLifetime
)
},
restoreState () {
const previousState = expiringStorage.get(this.storageKey)
if (previousState === null) {
return
}
this.sort = previousState.sort
this.filter = previousState.filter
this.saveState()
},
emitRowClick (row) {
this.$emit('rowClick', row)
this.$emit('row-click', row)
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<tr @click="onClick">
<table-cell
v-for="column in visibleColumns"
:row="row"
:column="column"
:key="column.id"
:responsive-label="column.label"
></table-cell>
</tr>
</template>
<script>
import TableCell from './TableCell';
export default {
props: ['columns', 'row'],
components: {
TableCell,
},
computed: {
visibleColumns() {
return this.columns.filter(column => ! column.hidden);
}
},
methods: {
onClick(e) {
this.$emit('rowClick', {
e,
row: this.row
})
}
}
};
</script>

View File

@ -0,0 +1,34 @@
class ExpiringStorage {
get (key) {
const cached = JSON.parse(
localStorage.getItem(key)
)
if (!cached) {
return null
}
const expires = new Date(cached.expires)
if (expires < new Date()) {
localStorage.removeItem(key)
return null
}
return cached.value
}
has (key) {
return this.get(key) !== null
}
set (key, value, lifeTimeInMinutes) {
const currentTime = new Date().getTime()
const expires = new Date(currentTime + lifeTimeInMinutes * 60000)
localStorage.setItem(key, JSON.stringify({ value, expires }))
}
}
export default new ExpiringStorage()

View File

@ -0,0 +1,30 @@
export function classList (...classes) {
return classes
.map(c => Array.isArray(c) ? c : [c])
.reduce((classes, c) => classes.concat(c), [])
}
export function get (object, path) {
if (!path) {
return object
}
if (object === null || typeof object !== 'object') {
return object
}
const [pathHead, pathTail] = path.split(/\.(.+)/)
return get(object[pathHead], pathTail)
}
export function pick (object, properties) {
return properties.reduce((pickedObject, property) => {
pickedObject[property] = object[property]
return pickedObject
}, {})
}
export function range (from, to) {
return [...Array(to - from)].map((_, i) => i + from)
}

View File

@ -0,0 +1,20 @@
import TableComponent from './components/TableComponent'
import TableColumn from './components/TableColumn'
import Pagination from './components/Pagination'
import { mergeSettings } from './settings'
export default {
install (Vue, options = {}) {
mergeSettings(options)
Vue.component('table-component', TableComponent)
Vue.component('table-column', TableColumn)
Vue.component('pagination', Pagination)
},
settings (settings) {
mergeSettings(settings)
}
}
export { TableComponent, TableColumn }

View File

@ -0,0 +1,18 @@
const settings = {
tableClass: '',
theadClass: '',
tbodyClass: '',
headerClass: '',
cellClass: '',
filterInputClass: '',
filterPlaceholder: 'Filter table…',
filterNoResults: 'There are no matching rows'
}
export function mergeSettings (newSettings) {
for (const setting in newSettings) {
settings[setting] = newSettings[setting]
}
}
export default settings

View File

@ -0,0 +1,35 @@
import BaseButton from './BaseButton.vue'
import ItemModal from './modal/ItemModal.vue'
import BaseModal from './modal/BaseModal.vue'
import BaseDatePicker from './base-date-picker/BaseDatePicker.vue'
import BaseInput from './BaseInput.vue'
import BaseSwitch from './BaseSwitch.vue'
import BaseTextArea from './BaseTextArea.vue'
import BaseSelect from './base-select/BaseSelect.vue'
import BaseLoader from './BaseLoader.vue'
import BaseCustomerSelect from './BaseCustomerSelect.vue'
import BasePopup from './popup/BasePopup.vue'
import CustomerSelectPopup from './popup/CustomerSelectPopup.vue'
import TaxSelectPopup from './popup/TaxSelectPopup.vue'
import {TableColumn, TableComponent} from './base-table/index'
Vue.component('base-button', BaseButton)
Vue.component('item-modal', ItemModal)
Vue.component('base-modal', BaseModal)
Vue.component('base-date-picker', BaseDatePicker)
Vue.component('base-input', BaseInput)
Vue.component('base-switch', BaseSwitch)
Vue.component('base-text-area', BaseTextArea)
Vue.component('base-loader', BaseLoader)
Vue.component('table-component', TableComponent)
Vue.component('table-column', TableColumn)
Vue.component('base-select', BaseSelect)
Vue.component('base-customer-select', BaseCustomerSelect)
Vue.component('base-popup', BasePopup)
Vue.component('customer-select-popup', CustomerSelectPopup)
Vue.component('tax-select-popup', TaxSelectPopup)

View File

@ -0,0 +1,73 @@
<template>
<transition name="fade">
<div v-if="modalActive" class="base-modal" :class="'size-' + modalSize">
<div class="modal-body">
<div class="close-icon">
<font-awesome-icon class="mr-2" icon="times" @click="closeModal"/>
</div>
<div class="modal-header p-3">
<h5 class="modal-heading">{{ modalTitle }}</h5>
</div>
<component :is="component" />
</div>
</div>
</transition>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import TaxTypeModal from './TaxTypeModal'
import ItemModal from './ItemModal'
import EstimateTemplate from './EstimateTemplate'
import InvoiceTemplate from './InvoiceTemplate'
import CustomerModal from './CustomerModal'
import CategoryModal from './CategoryModal'
export default {
components: {
TaxTypeModal,
ItemModal,
EstimateTemplate,
InvoiceTemplate,
CustomerModal,
CategoryModal
},
data () {
return {
component: '',
hasFocus: false
}
},
computed: {
...mapGetters('modal', [
'modalActive',
'modalTitle',
'componentName',
'modalSize',
'modalData'
])
},
watch: {
componentName (component) {
if (!component) {
return
}
this.component = component
}
},
methods: {
...mapActions('modal', [
'openModal',
'closeModal'
])
}
}
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div class="category-modal">
<form action="" @submit.prevent="submitCategoryData">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('expenses.category') }}<span class="required text-danger">*</span></label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.name.minLength" class="text-danger"> {{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }} </span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('expenses.description') }}</label>
<div class="col-sm-7">
<base-text-area
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
<div v-if="$v.formData.description.$error">
<span v-if="!$v.formData.name.maxLength" class="text-danger"> {{ $tc('validation.description_maxlength') }} </span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
@click="closeCategoryModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength, maxLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
description: null
}
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
validations: {
formData: {
name: {
required,
minLength: minLength(3)
},
description: {
maxLength: maxLength(255)
}
}
},
watch: {
'modalDataID' (val) {
if (val) {
this.isEdit = true
this.setData()
} else {
this.isEdit = false
}
},
'modalActive' (val) {
if (!this.modalActive) {
this.resetFormData()
}
}
},
mounted () {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
}
},
destroyed () {
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
...mapActions('category', [
'addCategory',
'updateCategory'
]),
resetFormData () {
this.formData = {
id: null,
name: null,
description: null
}
this.$v.formData.$reset()
},
async submitCategoryData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (!this.isEdit) {
response = await this.addCategory(this.formData)
} else {
response = await this.updateCategory(this.formData)
}
if (response.data) {
window.toastr['success'](this.$t('settings.expense_category.created_message'))
window.hub.$emit('newCategory', response.data.category)
this.closeCategoryModal()
this.isLoading = false
return true
}
window.toastr['error'](response.data.error)
},
async setData () {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
description: this.modalData.description
}
},
closeCategoryModal () {
this.resetFormData()
this.closeModal()
}
}
}
</script>

View File

@ -0,0 +1,681 @@
<template>
<div class="customer-modal">
<form action="" @submit.prevent="submitCustomerData">
<div class="card-body">
<!-- tab-1 -->
<tabs :options="{defaultTabHash: 'basic-home' }" class="tabs-simple">
<tab id="basic-home" name="Basic Info">
<div class="basic-info">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.display_name') }} <span class="required">*</span></label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model.trim="formData.name"
type="text"
name="name"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.name.minLength" class="text-danger"> {{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }} </span>
<span v-if="!$v.formData.name.alpha" class="text-danger">{{ $tc('validation.characters_only') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.primary_display_name') }}</label>
<div class="col-sm-7">
<base-input
v-model="formData.contact_name"
type="text"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('login.email') }}</label>
<div class="col-sm-7">
<base-input
:invalid="$v.formData.email.$error"
v-model.trim="formData.email"
type="text"
name="email"
@input="$v.formData.email.$touch()"
/>
<div v-if="$v.formData.email.$error">
<span v-if="!$v.formData.email.email" class="text-danger"> {{ $t('validation.email_incorrect') }} </span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $tc('settings.currencies.currency') }}</label>
<div class="col-sm-7">
<base-select
v-model="currency"
:options="currencies"
:searchable="true"
:show-labels="false"
label="name"
track-by="id"
placeholder="select currency"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.phone') }}</label>
<div class="col-sm-7">
<base-input
:invalid="$v.formData.phone.$error"
v-model.trim="formData.phone"
type="text"
name="phone"
@input="$v.formData.phone.$touch()"
/>
<div v-if="$v.formData.phone.$error">
<span v-if="!$v.formData.phone.numeric" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.website') }}</label>
<div class="col-sm-7">
<base-input
v-model="formData.website"
:invalid="$v.formData.website.$error"
type="url"
@input="$v.formData.website.$touch()"
/>
<div v-if="$v.formData.website.$error">
<span v-if="!$v.formData.website.url" class="text-danger">{{ $tc('validation.invalid_url') }}</span>
</div>
</div>
</div>
</div>
</tab>
<!-- tab-2 -->
<tab id="basic-profile" name="Billing Address">
<div class="basic-info">
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.name') }}</label>
<div class="col-sm-7">
<base-input
v-model="billing.name"
type="text"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.phone') }}</label>
<div class="col-sm-7">
<base-input
:invalid="$v.billing.phone.$error"
v-model.trim="billing.phone"
type="text"
name="phone"
@input="$v.billing.phone.$touch()"
/>
<div v-if="$v.billing.phone.$error">
<span v-if="!$v.billing.phone.numberic" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.address') }}</label>
<div class="col-sm-7">
<base-text-area
v-model="billing.address_street_1"
rows="2"
cols="50"
placeholder="Street 1"
class="mb-1"
@input="$v.billing.address_street_1.$touch()"
/>
<div v-if="$v.billing.address_street_1.$error">
<span v-if="!$v.billing.address_street_1.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
</div>
<base-text-area
v-model="billing.address_street_2"
rows="2"
cols="50"
placeholder="Street 2"
@input="$v.billing.address_street_2.$touch()"
/>
<div v-if="$v.billing.address_street_2.$error">
<span v-if="!$v.billing.address_street_2.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.country') }}</label>
<div class="col-sm-7">
<base-select
v-model="billingCountry"
:options="countryList"
:searchable="true"
:show-labels="false"
:allow-empty="false"
track-by="id"
label="name"
placeholder="select country"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.state') }}</label>
<div class="col-sm-7">
<base-select
v-model="billingState"
:options="billingStates"
:searchable="true"
:show-labels="false"
:disabled="isDisabledBillingState"
track-by="id"
label="name"
placeholder="select state"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.city') }}</label>
<div class="col-sm-7">
<base-select
v-model="billingCity"
:options="billingCities"
:searchable="true"
:show-labels="false"
:disabled="isDisabledBillingCity"
track-by="id"
label="name"
placeholder="select city"
/>
</div>
</div>
<!-- <div class="form-group row">
<label class="col-sm-4 col-form-label">Zip Code</label>
<div class="col-sm-7">
<base-input
v-model="billing.zip"
type="text"
/>
</div>
</div> -->
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.zip_code') }}</label>
<div class="col-sm-7">
<base-input
v-model="billing.zip"
type="text"
/>
</div>
</div>
</div>
</tab>
<!-- tab-3 -->
<tab id="basic-message" name="Shipping Address">
<div class="basic-info">
<div class="form-group row ">
<div class="col-sm-12 copy-address-button">
<base-button ref="sameAddress" icon="copy" class="mr-2 btn-sm" color="theme" @click="copyAddress(true)">
{{ $t('customers.copy_billing_address') }}
</base-button>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.name') }}</label>
<div class="col-sm-7">
<base-input
v-model="shipping.name"
type="text"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.phone') }}</label>
<div class="col-sm-7">
<base-input
:invalid="$v.shipping.phone.$error"
v-model.trim="shipping.phone"
type="text"
name="phone"
@input="$v.shipping.phone.$touch()"
/>
<div v-if="$v.shipping.phone.$error">
<span v-if="!$v.shipping.phone.numberic" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.address') }}</label>
<div class="col-sm-7">
<base-text-area
v-model="shipping.address_street_1"
rows="2"
cols="50"
placeholder="Street 1"
class="mb-1"
@input="$v.shipping.address_street_1.$touch()"
/>
<div v-if="$v.shipping.address_street_1.$error">
<span v-if="!$v.shipping.address_street_1.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
</div>
<base-text-area
v-model="shipping.address_street_2"
rows="2"
cols="50"
placeholder="Street 2"
@input="$v.shipping.address_street_2.$touch()"
/>
<div v-if="$v.shipping.address_street_2.$error">
<span v-if="!$v.shipping.address_street_2.maxLength" class="text-danger">{{ $t('validation.address_maxlength') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.country') }}</label>
<div class="col-sm-7">
<base-select
v-model="shippingCountry"
:options="countryList"
:searchable="true"
:show-labels="false"
:allow-empty="false"
track-by="id"
label="name"
placeholder="select country"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.state') }}</label>
<div class="col-sm-7">
<base-select
v-model="shippingState"
:options="shippingStates"
:searchable="true"
:show-labels="false"
:disabled="isDisabledShippingState"
track-by="id"
label="name"
placeholder="select state"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.city') }}</label>
<div class="col-sm-7">
<base-select
v-model="shippingCity"
:options="shippingCities"
:searchable="true"
:show-labels="false"
:disabled="isDisabledShippingCity"
track-by="id"
label="name"
placeholder="select city"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('customers.zip_code') }}</label>
<div class="col-sm-7">
<base-input
v-model="shipping.zip"
type="text"
/>
</div>
</div>
</div>
</tab>
</tabs>
</div>
<div class="card-footer">
<base-button :outline="true" class="mr-3" color="theme" @click="cancelCustomer">
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { Tabs, Tab } from 'vue-tabs-component'
import MultiSelect from 'vue-multiselect'
import { validationMixin } from 'vuelidate'
import { mapActions, mapGetters } from 'vuex'
import AddressStub from '../../../stub/address'
const { required, minLength, email, numeric, alpha, url, maxLength } = require('vuelidate/lib/validators')
export default {
components: {
'tabs': Tabs,
'tab': Tab,
MultiSelect
},
mixins: [validationMixin],
data () {
return {
isLoading: false,
countryList: [],
billingStates: [],
billingCities: [],
billingCountry: null,
billingState: null,
billingCity: null,
shippingStates: [],
shippingCities: [],
shippingCountry: null,
shippingState: null,
shippingCity: null,
isCopyFromBilling: false,
currencyList: [],
currency: '',
isDisabledBillingState: true,
isDisabledBillingCity: true,
isDisabledShippingState: true,
isDisabledShippingCity: true,
formData: {
id: null,
name: null,
currency_id: null,
phone: null,
website: null,
contact_name: null,
addresses: []
},
billing: {...AddressStub},
shipping: {...AddressStub}
}
},
validations: {
formData: {
name: {
required,
minLength: minLength(3),
alpha
},
email: {
email
},
phone: {
numeric
},
website: {
url
}
},
billing: {
phone: {
numeric
},
address_street_1: {
maxLength: maxLength(255)
},
address_street_2: {
maxLength: maxLength(255)
}
},
shipping: {
phone: {
numeric
},
address_street_1: {
maxLength: maxLength(255)
},
address_street_2: {
maxLength: maxLength(255)
}
}
},
computed: {
...mapGetters('currency', [
'defaultCurrency',
'currencies'
])
},
watch: {
billingCountry () {
if (this.billingCountry) {
this.billing.country_id = this.billingCountry.id
this.isDisabledBillingState = false
this.fetchBillingStates(this.billingCountry.id)
this.billingState = null
this.billingCity = null
return true
}
},
billingState () {
if (this.billingState) {
this.billing.state_id = this.billingState.id
this.isDisabledBillingCity = false
this.fetchBillingCities(this.billingState.id)
this.billingCity = null
return true
}
this.billingCity = null
this.isDisabledBillingCity = true
},
billingCity () {
if (this.billingCity) {
this.billing.city_id = this.billingCity.id
}
},
shippingCountry () {
if (this.shippingCountry) {
this.shipping.country_id = this.shippingCountry.id
this.isDisabledShippingState = false
this.fetchShippingStates(this.shippingCountry.id)
if (this.isCopyFromBilling) {
return true
}
this.shippingState = null
this.shippingCity = null
return true
}
},
shippingState () {
if (this.shippingState) {
this.shipping.state_id = this.shippingState.id
this.isDisabledShippingCity = false
this.fetchShippingCities(this.shippingState.id)
if (this.isCopyFromBilling) {
this.isCopyFromBilling = false
return true
}
this.shippingCity = null
return true
}
this.shippingCity = null
this.isDisabledShippingCity = true
},
shippingCity () {
if (this.shippingCity) {
this.shipping.city_id = this.shippingCity.id
}
}
},
mounted () {
this.$refs.name.focus = true
this.currency = this.defaultCurrency
this.fetchCountry()
},
methods: {
...mapActions('invoice', {
setInvoiceCustomer: 'selectCustomer'
}),
...mapActions('estimate', {
setEstimateCustomer: 'selectCustomer'
}),
...mapActions('customer', [
'fetchCustomer',
'addCustomer',
'updateCustomer'
]),
...mapActions('modal', [
'closeModal'
]),
resetData () {
this.formData = {
name: null,
currency_id: null,
phone: null,
website: null,
contact_name: null,
addresses: []
}
this.billingStates = []
this.billingCities = []
this.billingCountry = null
this.billingState = null
this.billingCity = null
this.shippingStates = []
this.shippingCities = []
this.shippingCountry = null
this.shippingState = null
this.shippingCity = null
this.billing = {...AddressStub}
this.shipping = {...AddressStub}
this.$v.formData.$reset()
},
cancelCustomer () {
this.resetData()
this.closeModal()
},
copyAddress (val) {
if (val === true) {
this.isCopyFromBilling = true
this.shipping = {...this.billing, type: 'shipping'}
this.shippingCountry = this.billingCountry
this.shippingState = this.billingState
this.shippingCity = this.billingCity
} else {
this.shipping = {...AddressStub, type: 'shipping'}
this.shippingCountry = null
this.shippingState = null
this.shippingCity = null
}
},
async loadData () {
let response = await this.fetchCustomer()
this.currencyList = this.currencies
this.formData.currency_id = response.data.currency.id
return true
},
checkAddress () {
const isBillingEmpty = Object.values(this.billing).every(val => (val === null || val === ''))
const isShippingEmpty = Object.values(this.shipping).every(val => (val === null || val === ''))
if (isBillingEmpty === true && isBillingEmpty === true) {
this.formData.addresses = []
return true
}
if (isBillingEmpty === false && isShippingEmpty === false) {
this.formData.addresses = [{...this.billing, type: 'billing'}, {...this.shipping, type: 'shipping'}]
return true
}
if (isBillingEmpty === false) {
this.formData.addresses.push({...this.billing, type: 'billing'})
return true
}
this.formData.addresses = [{...this.shipping, type: 'shipping'}]
return true
},
async submitCustomerData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
// this.checkAddress()
this.formData.addresses = [{...this.shipping, type: 'shipping'}, {...this.billing, type: 'billing'}]
this.isLoading = true
if (this.currency) {
this.formData.currency_id = this.currency.id
} else {
this.formData.currency_id = this.defaultCurrency.id
}
let response = await this.addCustomer(this.formData)
if (response.data) {
window.toastr['success'](this.$tc('customers.created_message'))
this.isLoading = false
if (this.$route.name === 'invoices.create') {
this.setInvoiceCustomer(response.data.customer.id)
}
if (this.$route.name === 'estimates.create') {
this.setEstimateCustomer(response.data.customer.id)
}
this.resetData()
this.closeModal()
return true
}
window.toastr['error'](response.data.error)
},
async fetchCountry () {
let res = await window.axios.get('/api/countries')
if (res) {
this.countryList = res.data.countries
}
},
async fetchBillingStates (id) {
let res = await window.axios.get(`/api/states/${id}`)
if (res) {
this.billingStates = res.data.states
}
},
async fetchBillingCities (id) {
let res = await window.axios.get(`/api/cities/${id}`)
if (res) {
this.billingCities = res.data.cities
}
},
async fetchShippingStates (id) {
let res = await window.axios.get(`/api/states/${id}`)
if (res) {
this.shippingStates = res.data.states
}
},
async fetchShippingCities (id) {
let res = await window.axios.get(`/api/cities/${id}`)
if (res) {
this.shippingCities = res.data.cities
}
}
}
}
</script>

View File

@ -0,0 +1,83 @@
<template>
<div class="template-modal">
<div class="card-body">
<div class="template-container">
<div
v-for="(template,index) in modalData"
:key="index"
:class="{'selected-template': selectedTemplate === template.id}"
class="template-img"
>
<img
:src="template.path"
alt="template-image"
height="200" width="140"
@click="selectedTemplate = template.id"
>
<img
v-if="selectedTemplate === template.id"
class="check-icon"
src="/assets/img/tick.png"
>
</div>
</div>
</div>
<div class="card-footer">
<base-button outline class="mr-3" color="theme" @click="closeEstimateModal">
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
@click="chooseTemplate()"
>
{{ $t('general.choose_template') }}
</base-button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
selectedTemplate: 1,
isLoading: false
}
},
computed: {
...mapGetters('modal', [
'modalData'
]),
...mapGetters('estimate', [
'getTemplateId'
])
},
mounted () {
this.selectedTemplate = this.getTemplateId
},
methods: {
...mapActions('estimate', [
'setTemplate'
]),
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
async chooseTemplate () {
this.isLoading = true
let resp = await this.setTemplate(this.selectedTemplate)
if (resp) {
this.isLoading = false
this.resetModalData()
this.closeModal()
}
},
closeEstimateModal () {
this.selectedTemplate = this.getTemplateId
this.closeModal()
this.resetModalData()
}
}
}
</script>

View File

@ -0,0 +1,83 @@
<template>
<div class="template-modal">
<div class="card-body">
<div class="template-container">
<div
v-for="(template,index) in modalData"
:key="index"
:class="{'selected-template': selectedTemplate === template.id}"
class="template-img"
>
<img
:src="template.path"
alt="template-image"
height="200" width="140"
@click="selectedTemplate = template.id"
>
<img
v-if="selectedTemplate === template.id"
class="check-icon"
src="/assets/img/tick.png"
>
</div>
</div>
</div>
<div class="card-footer">
<base-button outline class="mr-3" color="theme" @click="closeInvoiceModal">
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
@click="chooseTemplate()"
>
{{ $t('general.choose_template') }}
</base-button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
data () {
return {
selectedTemplate: 1,
isLoading: false
}
},
computed: {
...mapGetters('modal', [
'modalData'
]),
...mapGetters('invoice', [
'getTemplateId'
])
},
mounted () {
this.selectedTemplate = this.getTemplateId
},
methods: {
...mapActions('invoice', [
'setTemplate'
]),
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
async chooseTemplate () {
this.isLoading = true;
let resp = await this.setTemplate(this.selectedTemplate)
if (resp) {
this.isLoading = false
this.resetModalData()
this.closeModal()
}
},
closeInvoiceModal () {
this.selectedTemplate = this.getTemplateId
this.closeModal()
this.resetModalData()
}
}
}
</script>

View File

@ -0,0 +1,251 @@
<template>
<div class="item-modal">
<form action="" @submit.prevent="submitItemData">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label">
{{ $t('items.name') }}<span class="required">*</span>
</label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.name.minLength" class="text-danger"> {{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }} </span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('items.price') }}<span class="required">*</span></label>
<div class="col-sm-7">
<div class="base-input">
<money
:class="{'invalid' : $v.formData.price.$error}"
v-model="price"
v-bind="defaultCurrencyForInput"
class="input-field"
/>
</div>
<div v-if="$v.formData.price.$error">
<span v-if="!$v.formData.price.required" class="text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.price.numeric" class="text-danger">{{ $tc('validation.numbers_only') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('items.unit') }}</label>
<div class="col-sm-7">
<base-select
v-model="formData.unit"
:options="units"
:searchable="true"
:show-labels="false"
label="name"
/>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label">{{ $t('items.description') }}</label>
<div class="col-sm-7">
<base-text-area
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
<div v-if="$v.formData.description.$error">
<span v-if="!$v.formData.description.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
</div>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
type="button"
@click="closeItemModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
v-if="isEdit"
:loading="isLoading"
color="theme"
@click="submitItemData"
>
{{ $t('general.update') }}
</base-button>
<base-button
v-else
:loading="isLoading"
icon="save"
color="theme"
type="submit"
>
{{ $t('general.save') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength, numeric, maxLength, minValue } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
tempData: null,
units: [
{ name: 'box', value: 'box' },
{ name: 'cm', value: 'cm' },
{ name: 'dz', value: 'dz' },
{ name: 'ft', value: 'ft' },
{ name: 'g', value: 'g' },
{ name: 'in', value: 'in' },
{ name: 'kg', value: 'kg' },
{ name: 'km', value: 'km' },
{ name: 'lb', value: 'lb' },
{ name: 'mg', value: 'mg' }
],
formData: {
name: null,
price: null,
description: null,
unit: null
}
}
},
validations: {
formData: {
name: {
required,
minLength: minLength(3)
},
price: {
required,
numeric,
minValue: minValue(0.1)
},
description: {
maxLength: maxLength(255)
}
}
},
computed: {
...mapGetters('currency', [
'defaultCurrencyForInput'
]),
price: {
get: function () {
return this.formData.price / 100
},
set: function (newValue) {
this.formData.price = newValue * 100
}
},
...mapGetters('modal', [
'modalDataID'
]),
...mapGetters('item', [
'getItemById'
])
},
watch: {
modalDataID () {
this.isEdit = true
this.fetchEditData()
}
},
created () {
if (this.modalDataID) {
this.isEdit = true
this.fetchEditData()
}
},
mounted () {
this.$refs.name.focus = true
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
...mapActions('item', [
'addItem',
'updateItem'
]),
...mapActions('invoice', [
'setItem'
]),
resetFormData () {
this.formData = {
name: null,
price: null,
description: null,
unit: null,
id: null
}
this.$v.$reset()
},
fetchEditData () {
this.tempData = this.getItemById(this.modalDataID)
if (this.tempData) {
this.formData.name = this.tempData.name
this.formData.price = this.tempData.price
this.formData.description = this.tempData.description
this.formData.unit = this.tempData.unit
this.formData.id = this.tempData.id
}
},
async submitItemData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
if (this.formData.unit) {
this.formData.unit = this.formData.unit.name
}
this.isLoading = true
let response
if (this.isEdit) {
response = await this.updateItem(this.formData)
} else {
response = await this.addItem(this.formData)
}
if (response.data) {
window.toastr['success'](this.$tc('items.created_message'))
this.setItem(response.data.item)
window.hub.$emit('newItem', response.data.item)
this.isLoading = false
this.resetModalData()
this.resetFormData()
this.closeModal()
return true
}
window.toastr['error'](response.data.error)
},
closeItemModal () {
this.resetFormData()
this.closeModal()
this.resetModalData()
}
}
}
</script>

View File

@ -0,0 +1,216 @@
<template>
<div class="tax-type-modal">
<form action="" @submit.prevent="submitTaxTypeData">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('tax_types.name') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<base-input
ref="name"
:invalid="$v.formData.name.$error"
v-model="formData.name"
type="text"
@input="$v.formData.name.$touch()"
/>
<div v-if="$v.formData.name.$error">
<span v-if="!$v.formData.name.required" class="form-group__message text-danger">{{ $tc('validation.required') }}</span>
<span v-if="!$v.formData.name.minLength" class="form-group__message text-danger"> {{ $tc('validation.name_min_length', $v.formData.name.$params.minLength.min, { count: $v.formData.name.$params.minLength.min }) }} </span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('tax_types.percent') }} <span class="required"> *</span></label>
<div class="col-sm-7">
<div class="base-input">
<money
:class="{'invalid' : $v.formData.percent.$error}"
v-model="formData.percent"
v-bind="defaultInput"
class="input-field"
/>
</div>
<div v-if="$v.formData.percent.$error">
<span v-if="!$v.formData.percent.required" class="text-danger">{{ $t('validation.required') }}</span>
<span v-if="!$v.formData.percent.between" class="form-group__message text-danger">{{ $t('validation.enter_valid_tax_rate') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('tax_types.description') }}</label>
<div class="col-sm-7">
<base-text-area
v-model="formData.description"
rows="4"
cols="50"
@input="$v.formData.description.$touch()"
/>
<div v-if="$v.formData.description.$error">
<span v-if="!$v.formData.description.maxLength" class="text-danger">{{ $t('validation.description_maxlength') }}</span>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-4 col-form-label input-label">{{ $t('tax_types.compound_tax') }}</label>
<div class="col-sm-7 mr-4">
<base-switch
v-model="formData.compound_tax"
class="btn-switch compound-tax-toggle"
/>
</div>
</div>
</div>
<div class="card-footer">
<base-button
:outline="true"
class="mr-3"
color="theme"
type="button"
@click="closeTaxModal"
>
{{ $t('general.cancel') }}
</base-button>
<base-button
:loading="isLoading"
color="theme"
icon="save"
type="submit"
>
{{ !isEdit ? $t('general.save') : $t('general.update') }}
</base-button>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { validationMixin } from 'vuelidate'
const { required, minLength, between, maxLength } = require('vuelidate/lib/validators')
export default {
mixins: [validationMixin],
data () {
return {
isEdit: false,
isLoading: false,
formData: {
id: null,
name: null,
percent: '',
description: null,
compound_tax: false,
collective_tax: 0
},
defaultInput: {
decimal: '.',
thousands: ',',
prefix: '% ',
precision: 2,
masked: false
}
}
},
computed: {
...mapGetters('modal', [
'modalDataID',
'modalData',
'modalActive'
])
},
validations: {
formData: {
name: {
required,
minLength: minLength(3)
},
percent: {
required,
between: between(0.10, 100)
},
description: {
maxLength: maxLength(255)
}
}
},
// watch: {
// 'modalDataID' (val) {
// if (val) {
// this.isEdit = true
// this.setData()
// } else {
// this.isEdit = false
// }
// },
// 'modalActive' (val) {
// if (!this.modalActive) {
// this.resetFormData()
// }
// }
// },
async mounted () {
this.$refs.name.focus = true
if (this.modalDataID) {
this.isEdit = true
this.setData()
// this.resetFormData()
}
},
methods: {
...mapActions('modal', [
'closeModal',
'resetModalData'
]),
...mapActions('taxType', [
'addTaxType',
'updateTaxType',
'fetchTaxType'
]),
resetFormData () {
this.formData = {
id: null,
name: null,
percent: null,
description: null,
collective_tax: 0
}
this.$v.formData.$reset()
},
async submitTaxTypeData () {
this.$v.formData.$touch()
if (this.$v.$invalid) {
return true
}
this.isLoading = true
let response
if (!this.isEdit) {
response = await this.addTaxType(this.formData)
} else {
response = await this.updateTaxType(this.formData)
}
if (response.data) {
window.toastr['success'](this.$t('settings.sales_taxes.created_message'))
window.hub.$emit('newTax', response.data.taxType)
this.closeTaxModal()
this.isLoading = false
return true
}
window.toastr['error'](response.data.error)
},
async setData () {
this.formData = {
id: this.modalData.id,
name: this.modalData.name,
percent: this.modalData.percent,
description: this.modalData.description,
compound_tax: this.modalData.compound_tax ? true : false
}
},
closeTaxModal () {
this.resetModalData()
this.resetFormData()
this.closeModal()
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<div v-click-outside="clickOutsideMenu" class="search-select" >
<div
class="activator"
@click="toggleSearchMenu">
<slot name="activator" />
</div>
<transition name="fade">
<div
v-if="showMenu"
:class="{'selector-menu-above': isAbove}"
class="selector-menu"
>
<slot />
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
toggle: {
type: Boolean,
default: true
},
openDirection: {
type: String,
default: ''
},
maxHeight: {
type: Number,
default: 180
}
},
data () {
return {
showMenu: false,
preferredOpenDirection: 'below',
optimizedHeight: null
}
},
computed: {
isAbove () {
if (this.openDirection === 'above' || this.openDirection === 'top') {
return true
} else if (this.openDirection === 'below' || this.openDirection === 'bottom') {
return false
} else {
return this.preferredOpenDirection === 'above'
}
}
},
methods: {
toggleSearchMenu () {
this.adjustPosition()
if (this.toggle) {
this.showMenu = !this.showMenu
} else {
this.showMenu = true
}
},
clickOutsideMenu () {
this.showMenu = false
},
open () {
this.showMenu = true
},
close () {
this.showMenu = false
},
adjustPosition () {
if (typeof window === 'undefined') return
const spaceAbove = this.$el.getBoundingClientRect().top
const spaceBelow = window.innerHeight - this.$el.getBoundingClientRect().bottom
const hasEnoughSpaceBelow = spaceBelow > this.maxHeight
if (hasEnoughSpaceBelow || spaceBelow > spaceAbove || this.openDirection === 'below' || this.openDirection === 'bottom') {
this.preferredOpenDirection = 'below'
this.optimizedHeight = Math.min(spaceBelow - 20, this.maxHeight)
} else {
this.preferredOpenDirection = 'above'
this.optimizedHeight = Math.min(spaceAbove - 20, this.maxHeight)
}
}
}
}
</script>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div class="customer-select">
<div class="main">
<div class="search-bar">
<base-input
v-model="search"
:placeholder="$t('general.search')"
focus
type="text"
icon="search"
@input="searchCustomer"
/>
</div>
<div v-if="(customers.length > 0) && !loading" class="list">
<div
v-for="(customer, index) in customers"
:key="index"
class="list-item"
@click="selectNewCustomer(customer.id)"
>
<span class="avatar" >{{ initGenerator(customer.name) }}</span>
<div class="name">
<label class="title">{{ customer.name }}</label>
<label class="sub-title">{{ customer.contact_name }}</label>
</div>
</div>
</div>
<div v-if="loading" class="list flex justify-content-center align-items-center">
<font-awesome-icon icon="spinner" class="fa-spin"/>
</div>
<div v-if="customers.length === 0" class="no-data-label">
<label> {{ $t('customers.no_customers_found') }} </label>
</div>
</div>
<button class="list-add-button" @click="openCustomerModal">
<font-awesome-icon class="icon" icon="user-plus" />
<label>{{ $t('customers.add_new_customer') }}</label>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
type: {
type: String,
required: true
}
},
data () {
return {
search: null,
loading: false
}
},
computed: {
...mapGetters('customer', [
'customers'
])
},
created () {
this.fetchInitialCustomers()
},
methods: {
...mapActions('modal', [
'openModal'
]),
...mapActions('customer', [
'fetchCustomers'
]),
...mapActions('invoice', {
setInvoiceCustomer: 'selectCustomer'
}),
...mapActions('estimate', {
setEstimateCustomer: 'selectCustomer'
}),
async fetchInitialCustomers () {
await this.fetchCustomers({
filter: {},
orderByField: '',
orderBy: ''
})
},
async searchCustomer () {
let data = {
display_name: this.search,
email: '',
phone: '',
orderByField: '',
orderBy: '',
page: 1
}
this.loading = true
await this.fetchCustomers(data)
this.loading = false
},
openCustomerModal () {
this.openModal({
title: 'Add Customer',
componentName: 'CustomerModal',
size: 'lg'
})
},
initGenerator (name) {
if (name) {
let nameSplit = name.split(' ')
let initials = nameSplit[0].charAt(0).toUpperCase()
return initials
}
},
selectNewCustomer (id) {
if (this.type === 'estimate') {
this.setEstimateCustomer(id)
} else {
this.setInvoiceCustomer(id)
}
}
}
}
</script>

View File

@ -0,0 +1,85 @@
<template>
<div class="tax-select">
<div class="main-section">
<div class="search-bar">
<base-input
v-model="textSearch"
:placeholder="$t('general.search')"
focus
icon="search"
class="search-input"
type="text"
/>
</div>
<div v-if="filteredTaxType.length > 0" class="list" >
<div
v-for="(taxType, index) in filteredTaxType"
:key="index"
:class="{'item-disabled': taxes.find(val => {return val.tax_type_id === taxType.id})}"
class="list-item"
@click="selectTaxType(index)"
>
<label>{{ taxType.name }}</label>
<label>{{ taxType.percent }} %</label>
</div>
</div>
<div v-else class="no-data-label">
<label>{{ $t('general.no_tax_found') }}</label>
</div>
</div>
<button type="button" class="list-add-button" @click="openTaxModal">
<font-awesome-icon class="icon" icon="check-circle" />
<label>{{ $t('invoices.add_new_tax') }}</label>
</button>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default {
props: {
taxes: {
type: Array,
required: false,
default: null
}
},
data () {
return {
textSearch: null
}
},
computed: {
...mapGetters('taxType', [
'taxTypes'
]),
filteredTaxType () {
if (this.textSearch) {
var textSearch = this.textSearch
return this.taxTypes.filter(function (el) {
return el.name.toLowerCase().indexOf(textSearch.toLowerCase()) !== -1
})
} else {
return this.taxTypes
}
}
},
methods: {
...mapActions('modal', [
'openModal'
]),
selectTaxType (index) {
this.$emit('select', {...this.taxTypes[index]})
},
openTaxModal () {
this.openModal({
'title': 'Add Tax',
'componentName': 'TaxTypeModal'
})
}
}
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph"
/>
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false,
legend: {
display: false
}
}
let data = {
labels: this.labels,
datasets: [
{
label: 'My First dataset',
backgroundColor: 'rgba(79, 196, 127,0.2)',
borderColor: 'rgba(79, 196, 127,1)',
borderWidth: 1,
hoverBackgroundColor: 'rgba(79, 196, 127,0.4)',
hoverBorderColor: 'rgba(79, 196, 127,1)',
data: this.values
}
]
}
this.myBarChart = new Chart(context, {
type: 'bar',
data: data,
options: options
})
},
beforeDestroy () {
this.myBarChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph"/>
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
},
bgColors: {
type: Array,
require: true,
default: Array
},
hoverBgColors: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false
}
let data = {
labels: this.labels,
datasets: [
{
data: this.values,
backgroundColor: this.bgColors,
hoverBackgroundColor: this.hoverBgColors
}
]
}
this.myDoughnutChart = new Chart(context, {
type: 'doughnut',
data: data,
options: options
})
},
beforeDestroy () {
this.myDoughnutChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph" />
</div>
</template>
<script>
import Chart from 'chart.js'
import Utils from '../../helpers/utilities'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
},
invoices: {
type: Array,
require: true,
default: Array
},
expenses: {
type: Array,
require: true,
default: Array
},
receipts: {
type: Array,
require: true,
default: Array
},
income: {
type: Array,
require: true,
default: Array
},
formatMoney: {
type: Function,
require: false,
default: Function
}
},
watch: {
labels (val) {
this.update()
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false,
tooltips: {
enabled: true,
callbacks: {
label: function (tooltipItem, data) {
return Utils.formatGraphMoney(tooltipItem.value)
}
}
},
legend: {
display: false
}
}
let data = {
labels: this.labels,
datasets: [
{
label: 'Sales',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: '#040405',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: '#040405',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: '#040405',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.invoices
},
{
label: 'Receipts',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(230, 254, 249)',
borderColor: 'rgb(2, 201, 156)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgb(2, 201, 156)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgb(2, 201, 156)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.receipts
},
{
label: 'Expenses',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(245, 235, 242)',
borderColor: 'rgb(255,0,0)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgb(255,0,0)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgb(255,0,0)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.expenses
},
{
label: 'Net Income',
fill: false,
lineTension: 0.3,
backgroundColor: 'rgba(236, 235, 249)',
borderColor: 'rgba(88, 81, 216, 1)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgba(88, 81, 216, 1)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: 'rgba(88, 81, 216, 1)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 4,
pointHitRadius: 10,
data: this.income
}
]
}
this.myLineChart = new Chart(context, {
type: 'line',
data: data,
options: options
})
},
methods: {
update () {
this.myLineChart.data.labels = this.labels
this.myLineChart.data.datasets[0].data = this.invoices
this.myLineChart.data.datasets[1].data = this.receipts
this.myLineChart.data.datasets[2].data = this.expenses
this.myLineChart.data.datasets[3].data = this.income
this.myLineChart.update({
lazy: true
})
},
beforeDestroy () {
this.myLineChart.destroy()
}
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="graph-container">
<canvas
id="graph"
ref="graph" />
</div>
</template>
<script>
import Chart from 'chart.js'
export default {
props: {
labels: {
type: Array,
require: true,
default: Array
},
values: {
type: Array,
require: true,
default: Array
},
bgColors: {
type: Array,
require: true,
default: Array
},
hoverBgColors: {
type: Array,
require: true,
default: Array
}
},
mounted () {
let context = this.$refs.graph.getContext('2d')
let options = {
responsive: true,
maintainAspectRatio: false
}
let data = {
labels: this.labels,
datasets: [
{
data: this.values,
backgroundColor: this.bgColors,
hoverBackgroundColor: this.hoverBgColors
}
]
}
this.pieChart = new Chart(context, {
type: 'pie',
data: data,
options: options
})
},
beforeDestroy () {
this.pieChart.destroy()
}
}
</script>
<style scoped>
.graph-container {
height: 300px;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div class="graph-container easy-pie-chart">
<svg width="100%" height="100%" viewBox="0 0 34 34" class="donut">
<circle :stroke-width="strokeWidth" class="donut-segment" cx="17" cy="17" r="15.91549430918954" fill="transparent" :stroke="strokeColor" stroke-dasharray="100 0" />
<circle :stroke-width="strokeWidth" :stroke="color" :stroke-dasharray="successProgress" class="donut-segment" cx="17" cy="17" r="15.91549430918954" fill="transparent" />
<!-- <g class="chart-text">
<text :style="'fill:' + color" x="48%" y="50%" class="chart-number" >
{{ progress }}
</text>
<text :style="'fill:' + color" x="73%" y="50%" class="chart-label" >
%
</text>
</g> -->
</svg>
</div>
</template>
<script>
export default {
props: {
values: {
type: Number,
require: true,
default: 100
},
strokeWidth: {
type: Number,
require: false,
default: 1.2
},
strokeColor: {
type: String,
require: true,
default: '#eeeeee'
},
color: {
type: String,
require: true,
default: '#007dcc'
}
},
data () {
return {
progress: 0
}
},
watch: {
values (newvalue, oldvalue) {
if (newvalue !== oldvalue) {
this.setProgress()
}
}
},
computed: {
successProgress () {
return this.progress + ' ' + (100 - this.progress)
},
remainProgress () {
return 100 - this.progress + ' ' + this.progress
},
},
mounted () {
this.setProgress()
},
methods: {
setProgress () {
let self = this
for (let i = 0; i < this.values; i++) {
setTimeout(function () {
++self.progress
}, 15 * i)
}
}
}
}
</script>
<style scoped>
.chart-text {
font: 6px "Montserrat", Arial, sans-serif;
fill: #000;
-moz-transform: translateY(0.25em);
-ms-transform: translateY(0.25em);
-webkit-transform: translateY(0.25em);
transform: translateY(0.5em);
}
.chart-number {
font-size: 8px;
line-height: 1;
text-anchor: middle;
}
.chart-label {
font-size: 5px;
text-transform: uppercase;
text-anchor: middle;
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="collapse-group">
<slot/>
</div>
</template>
<script>
export default {
props: {
accordion: {
type: Boolean,
require: true,
default: false
}
}
}
</script>

View File

@ -0,0 +1,120 @@
<template>
<div :class="['collapse-group-item', { active: isCollapseOpen } ]">
<div v-if="!itemTrigger" class="collapse-item-title" @click="toggleCollapse">
<slot name="item-title"/>
</div>
<div v-else class="collapse-item-title">
<slot name="item-title" :trigger="toggleCollapse"/>
</div>
<transition
:duration="{ enter: 0 }"
name="slide"
@after-enter="afterEnter"
@after-leave="afterLeave"
>
<div
v-show="isCollapseOpen"
v-if="hasChild"
ref="collapseItems"
:style="'max-height:' + height + 'px'"
class="collapse-group-items"
>
<slot/>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
activeUrl: {
type: String,
require: true,
default: ''
},
isActive: {
type: Boolean,
require: true,
default: false
},
itemTrigger: {
type: Boolean,
require: true,
default: false
}
},
data () {
return {
height: '',
originalHeight: '',
isCollapseOpen: true,
hasChild: true,
accordion: this.$parent.accordion
}
},
mounted () {
this.$nextTick(() => {
if (this.accordion === true) {
this.hasActive()
} else {
this.isCollapseOpen = false
}
this.height = this.originalHeight = this.$refs.collapseItems.clientHeight
if (this.$refs.collapseItems.children.length === 0) {
this.hasChild = false
}
})
},
methods: {
hasActiveUrl () {
return this.$route.path.indexOf(this.activeUrl) > -1
},
hasActive () {
if (this.isActive) {
this.isCollapseOpen = this.isActive
} else {
if (this.activeUrl) {
this.isCollapseOpen = this.hasActiveUrl()
} else {
this.isCollapseOpen = false
}
}
},
toggleCollapse () {
let self = this
if (this.accordion) {
if (this.isCollapseOpen === false) {
this.$parent.$children.filter((value) => {
if (value !== self) {
if (value.isCollapseOpen === true) {
value.isCollapseOpen = false
}
}
})
}
}
this.isCollapseOpen = !this.isCollapseOpen
},
afterEnter () {
this.height = this.originalHeight
},
afterLeave () {
this.height = 0
}
}
}
</script>
<style scoped>
.collapse-group-items {
overflow: hidden;
transition: max-height .3s ease-in-out;
}
.slide-enter-active, .slide-leave-active {
overflow: hidden;
}
.slide-leave-to {
max-height: 0px !important;
}
</style>

Some files were not shown because too many files have changed in this diff Show More