Initial commit
This commit is contained in:
95
resources/js/components/common/Alert.vue
Normal file
95
resources/js/components/common/Alert.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<transition enter-active-class="linear duration-500 overflow-hidden"
|
||||
enter-class="max-h-0 opacity-0"
|
||||
enter-to-class="max-h-screen opacity-100"
|
||||
leave-active-class="linear duration-500 overflow-hidden"
|
||||
leave-class="max-h-screen opacity-100"
|
||||
leave-to-class="max-h-0 opacity-0"
|
||||
>
|
||||
<div :class="alertClasses" class="border shadow-sm p-2 flex items-center rounded-md">
|
||||
<div class="flex-grow">
|
||||
<p class="mb-0 py-2 px-4" :class="textClasses" v-html="message"/>
|
||||
</div>
|
||||
|
||||
<div class="justify-end">
|
||||
<v-button v-if="type == 'error'" color="red" shade="light" @click="close">
|
||||
Close
|
||||
</v-button>
|
||||
<v-button v-if="type == 'success'" color="green" shade="light" @click="close">
|
||||
OK
|
||||
</v-button>
|
||||
<v-button v-if="type == 'warning'" color="yellow" shade="light" @click="close">
|
||||
OK
|
||||
</v-button>
|
||||
<v-button v-if="type == 'confirmation'" class="mr-1 mb-1" @click="confirm">
|
||||
Yes
|
||||
</v-button>
|
||||
<v-button v-if="type == 'confirmation'" color="gray" shade="light" @click="cancel">
|
||||
No, cancel
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Alert',
|
||||
props: ['type', 'message', 'autoClose', 'confirmationProceed', 'confirmationCancel'],
|
||||
|
||||
data () {
|
||||
return {
|
||||
timeout: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
alertClasses () {
|
||||
if (this.type === 'error') return 'bg-red-100 border-red-500'
|
||||
if (this.type === 'success') return 'bg-green-100 border-green-500'
|
||||
if (this.type === 'warning') return 'bg-yellow-100 border-yellow-500'
|
||||
return 'bg-blue-50 border-nt-blue-light'
|
||||
},
|
||||
textClasses () {
|
||||
if (this.type === 'error') return 'text-red-600'
|
||||
if (this.type === 'success') return 'text-green-600'
|
||||
if (this.type === 'warning') return 'text-yellow-600'
|
||||
return 'text-nt-blue'
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.autoClose) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.close()
|
||||
}, this.autoClose)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
close () {
|
||||
clearTimeout(this.timeout)
|
||||
this.$emit('close')
|
||||
},
|
||||
/**
|
||||
* Confirm and close the modal.
|
||||
*/
|
||||
confirm () {
|
||||
this.confirmationProceed()
|
||||
this.close()
|
||||
},
|
||||
/**
|
||||
* Cancel and close the modal.
|
||||
*/
|
||||
cancel () {
|
||||
if (this.confirmationCancel) {
|
||||
this.confirmationCancel()
|
||||
}
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
41
resources/js/components/common/Breadcrumb.vue
Normal file
41
resources/js/components/common/Breadcrumb.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="breadcrumbs flex">
|
||||
<div v-for="(item,index) in path" :key="item.label" class="flex items-center">
|
||||
<router-link v-if="item.route" class="p-1 hover:bg-blue-50 rounded-md" :to="item.route">
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
<div v-else class="p-1" :class="{'font-semibold': index===path.length-1}">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div v-if="index!==path.length-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Breadcrumb',
|
||||
props: {
|
||||
/**
|
||||
* route: Route object
|
||||
* label: Label
|
||||
*/
|
||||
path: { type: Array }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {},
|
||||
|
||||
mounted () {
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
100
resources/js/components/common/Button.vue
Normal file
100
resources/js/components/common/Button.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<button :type="nativeType" :disabled="loading" :class="`py-${sizes['p-y']} px-${sizes['p-x']}
|
||||
bg-${color}-${colorShades['main']} hover:bg-${color}-${colorShades['hover']} focus:ring-${color}-${colorShades['ring']}
|
||||
focus:ring-offset-${color}-${colorShades['ring-offset']} text-${colorShades['text']}
|
||||
transition ease-in duration-200 text-center text-${sizes['font']} font-semibold shadow-md focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 rounded-lg`"
|
||||
class="btn" @click="$emit('click',$event)"
|
||||
>
|
||||
<template v-if="!loading">
|
||||
<slot />
|
||||
</template>
|
||||
<loader v-else class="h-6 w-6 text-white mx-auto" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VButton',
|
||||
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: 'nt-blue'
|
||||
},
|
||||
|
||||
shade: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
},
|
||||
|
||||
nativeType: {
|
||||
type: String,
|
||||
default: 'submit'
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
colorShades () {
|
||||
if (this.color === 'nt-blue') {
|
||||
return {
|
||||
main: 'default',
|
||||
hover: 'light',
|
||||
ring: 'light',
|
||||
'ring-offset': 'lighter',
|
||||
text: 'white'
|
||||
}
|
||||
}
|
||||
if (this.shade === 'lighter') {
|
||||
return {
|
||||
main: '200',
|
||||
hover: '300',
|
||||
ring: '100',
|
||||
'ring-offset': '50',
|
||||
text: 'gray-900'
|
||||
}
|
||||
}
|
||||
if (this.shade === 'light') {
|
||||
return {
|
||||
main: '400',
|
||||
hover: '500',
|
||||
ring: '300',
|
||||
'ring-offset': '150',
|
||||
text: 'white'
|
||||
}
|
||||
}
|
||||
return {
|
||||
main: '600',
|
||||
hover: '700',
|
||||
ring: '500',
|
||||
'ring-offset': '200',
|
||||
text: 'white'
|
||||
}
|
||||
},
|
||||
sizes () {
|
||||
if (this.size === 'small') {
|
||||
return {
|
||||
font: 'sm',
|
||||
'p-y': '1',
|
||||
'p-x': '2'
|
||||
}
|
||||
}
|
||||
return {
|
||||
font: 'base',
|
||||
'p-y': '2',
|
||||
'p-x': '4'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
27
resources/js/components/common/Card.vue
Normal file
27
resources/js/components/common/Card.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="flex flex-col w-full bg-white rounded-lg shadow"
|
||||
:class="{'px-4 py-8 sm:px-6 md:px-8 lg:px-10':padding}"
|
||||
>
|
||||
<div v-if="title" class="self-center mb-6 text-xl font-light text-gray-900 sm:text-3xl font-bold dark:text-white">
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Card',
|
||||
|
||||
props: {
|
||||
padding: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
40
resources/js/components/common/Collapse.vue
Normal file
40
resources/js/components/common/Collapse.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-full relative">
|
||||
<div class="cursor-pointer" @click="trigger">
|
||||
<slot name="title" />
|
||||
</div>
|
||||
<div class="text-gray-400 hover:text-gray-600 absolute -right-2 -top-1 cursor-pointer p-2" @click="trigger">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transition transform duration-500" :class="{'rotate-180':showContent}" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v3.586L7.707 9.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 10.586V7z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<v-transition>
|
||||
<div v-if="showContent" class="w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</v-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VTransition from './transitions/VTransition'
|
||||
export default {
|
||||
name: 'Collapse',
|
||||
components: { VTransition },
|
||||
props: {
|
||||
defaultValue: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showContent: this.defaultValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
trigger () {
|
||||
this.showContent = !this.showContent
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
53
resources/js/components/common/Dropdown.vue
Normal file
53
resources/js/components/common/Dropdown.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div>
|
||||
<slot name="trigger"
|
||||
:toggle="toggle"
|
||||
:open="open"
|
||||
:close="close"
|
||||
/>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
v-on-clickaway="close"
|
||||
:class="dropdownClass"
|
||||
>
|
||||
<div class="py-1 " role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { directive as onClickaway } from 'vue-clickaway'
|
||||
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
directives: {
|
||||
onClickaway: onClickaway
|
||||
},
|
||||
|
||||
props: {
|
||||
dropdownClass: { type: String, default: 'origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50' }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isOpen: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open () {
|
||||
this.isOpen = true
|
||||
},
|
||||
close () {
|
||||
this.isOpen = false
|
||||
},
|
||||
toggle () {
|
||||
this.isOpen = !this.isOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
57
resources/js/components/common/EditableDiv.vue
Normal file
57
resources/js/components/common/EditableDiv.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div ref="parent"
|
||||
tabindex="0"
|
||||
:class="{
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-800 rounded px-2 cursor-pointer': !editing
|
||||
}"
|
||||
|
||||
class="relative"
|
||||
:style="{height: editing?(divHeight+'px'):'auto'}"
|
||||
@focus="startEditing"
|
||||
>
|
||||
<slot v-if="!editing" :content="content">
|
||||
<label class="cursor-pointer truncate w-full">
|
||||
{{ content }}
|
||||
</label>
|
||||
</slot>
|
||||
<div v-if="editing" class="absolute inset-0 border-2 transition-colors"
|
||||
:class="{'border-transparent':!editing,'border-blue-500':editing}">
|
||||
<input ref="editinput" v-model="content"
|
||||
class="absolute inset-0 focus:outline-none bg-white transition-colors"
|
||||
:class="[{'bg-blue-50':editing},contentClass]" @blur="editing = false" @keyup.enter="editing = false"
|
||||
@input="handleInput"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {required: true},
|
||||
textAlign: {type: String, default: 'left'},
|
||||
contentClass: {type: String | Object, default: ''}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
content: this.value,
|
||||
editing: false,
|
||||
divHeight: 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
startEditing() {
|
||||
this.divHeight = this.$refs.parent.offsetHeight
|
||||
this.editing = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.editinput.focus()
|
||||
})
|
||||
},
|
||||
handleInput(e) {
|
||||
this.$emit('input', this.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
resources/js/components/common/FancyLink.vue
Normal file
103
resources/js/components/common/FancyLink.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<router-link :class="`py-${sizes['p-y']} px-${sizes['p-x']}
|
||||
bg-${color}-${colorShades['main']} hover:bg-${color}-${colorShades['hover']} focus:ring-${color}-${colorShades['ring']}
|
||||
focus:ring-offset-${color}-${colorShades['ring-offset']} text-${colorShades['text']}
|
||||
transition ease-in duration-200 text-center text-${sizes['font']} font-semibold shadow-md focus:outline-none focus:ring-2
|
||||
focus:ring-offset-2 rounded-lg hover:no-underline inline-block`" :to="to" :target="target"
|
||||
>
|
||||
<template v-if="!loading">
|
||||
<slot />
|
||||
</template>
|
||||
<loader v-else class="h-6 w-6 text-white mx-auto" />
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FancyLink',
|
||||
|
||||
props: {
|
||||
to: {
|
||||
type: Object
|
||||
},
|
||||
|
||||
color: {
|
||||
type: String,
|
||||
default: 'nt-blue'
|
||||
},
|
||||
|
||||
target: {
|
||||
type: String,
|
||||
default: '_self'
|
||||
},
|
||||
|
||||
shade: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
},
|
||||
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
},
|
||||
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
colorShades () {
|
||||
if (this.color === 'nt-blue') {
|
||||
return {
|
||||
main: 'default',
|
||||
hover: 'light',
|
||||
ring: 'light',
|
||||
'ring-offset': 'lighter',
|
||||
text: 'white'
|
||||
}
|
||||
}
|
||||
if (this.shade === 'lighter') {
|
||||
return {
|
||||
main: '200',
|
||||
hover: '300',
|
||||
ring: '100',
|
||||
'ring-offset': '50',
|
||||
text: 'gray-900'
|
||||
}
|
||||
}
|
||||
if (this.shade === 'light') {
|
||||
return {
|
||||
main: '400',
|
||||
hover: '500',
|
||||
ring: '300',
|
||||
'ring-offset': '150',
|
||||
text: 'white'
|
||||
}
|
||||
}
|
||||
return {
|
||||
main: '600',
|
||||
hover: '700',
|
||||
ring: '500',
|
||||
'ring-offset': '200',
|
||||
text: 'white'
|
||||
}
|
||||
},
|
||||
sizes () {
|
||||
if (this.size === 'small') {
|
||||
return {
|
||||
font: 'sm',
|
||||
'p-y': '1',
|
||||
'p-x': '2'
|
||||
}
|
||||
}
|
||||
return {
|
||||
font: 'base',
|
||||
'p-y': '2',
|
||||
'p-x': '4'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
13
resources/js/components/common/Loader.vue
Normal file
13
resources/js/components/common/Loader.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Loader',
|
||||
props: {}
|
||||
}
|
||||
</script>
|
||||
79
resources/js/components/common/ProTag.vue
Normal file
79
resources/js/components/common/ProTag.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="inline" v-if="shouldDisplayProTag">
|
||||
<div class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold cursor-pointer"
|
||||
@click.prevent="showPremiumModal=true"
|
||||
>
|
||||
PRO
|
||||
</div>
|
||||
<modal :show="showPremiumModal" @close="showPremiumModal=false">
|
||||
<h2 class="text-nt-blue">
|
||||
OpenForm PRO
|
||||
</h2>
|
||||
<h4 v-if="user.is_subscribed && !user.has_enterprise_subscription" class="text-center mt-5">
|
||||
We're happy to have you as a Pro customer. If you're having any issue with OpenForm, or if you have a
|
||||
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
|
||||
<br><br>
|
||||
If you need to collaborate, or to work with multiple workspaces, or just larger file uploads, you can
|
||||
also upgrade our subscription to get an Enterprise subscription.
|
||||
</h4>
|
||||
<h4 v-if="user.is_subscribed && user.has_enterprise_subscription" class="text-center mt-5">
|
||||
We're happy to have you as an Enterprise customer. If you're having any issue with OpenForm, or if you have a
|
||||
feature request, please <a href="mailto:contact@opnform.com">contact us</a>.
|
||||
</h4>
|
||||
<p v-if="!user.is_subscribed" class="mt-4">
|
||||
All the features with a<span
|
||||
class="bg-nt-blue text-white px-2 text-xs uppercase inline rounded-full font-semibold mx-1"
|
||||
>
|
||||
PRO
|
||||
</span> tag are available in the Pro plan of OpenForm. <b>You can play around and try all Pro features
|
||||
within
|
||||
the form editor, but you can't use them in your real forms</b>. You can subscribe now to gain unlimited access
|
||||
to
|
||||
all our pro features!
|
||||
</p>
|
||||
|
||||
<p class="my-4 text-center">
|
||||
Feel free to <a href="mailto:contact@opnform.com">contact us</a> if you have any feature request.
|
||||
</p>
|
||||
<div class="mb-4 text-center">
|
||||
<v-button color="gray" shade="light" @click="showPremiumModal=false">
|
||||
Close
|
||||
</v-button>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../Modal'
|
||||
import axios from 'axios'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'ProTag',
|
||||
components: { Modal },
|
||||
props: {},
|
||||
|
||||
data () {
|
||||
return {
|
||||
showPremiumModal: false,
|
||||
checkoutLoading: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
currentWorkSpace: 'open/workspaces/getCurrent',
|
||||
}),
|
||||
shouldDisplayProTag() {
|
||||
return false; //!this.user.is_subscribed && !(this.currentWorkSpace.is_pro || this.currentWorkSpace.is_enterprise);
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
},
|
||||
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
184
resources/js/components/common/ScrollShadow.vue
Normal file
184
resources/js/components/common/ScrollShadow.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="scroll-shadow max-w-full" :class="[$style.wrap,{'w-max':!shadow.left && !shadow.right}]">
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
:class="[$style['scroll-container'],{'no-scrollbar':hideScrollbar}]"
|
||||
:style="{ width: width?width:'auto', height }"
|
||||
@scroll.passive="toggleShadow"
|
||||
>
|
||||
<slot />
|
||||
<span :class="[$style['shadow-top'], shadow.top && $style['is-active']]" :style="{
|
||||
top: shadowTopOffset+'px',
|
||||
}"
|
||||
/>
|
||||
<span :class="[$style['shadow-right'], shadow.right && $style['is-active']]" />
|
||||
<span :class="[$style['shadow-bottom'], shadow.bottom && $style['is-active']]" />
|
||||
<span :class="[$style['shadow-left'], shadow.left && $style['is-active']]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function newResizeObserver (callback) {
|
||||
// Skip this feature for browsers which
|
||||
// do not support ResizeObserver.
|
||||
// https://caniuse.com/#search=resizeobserver
|
||||
if (typeof ResizeObserver === 'undefined') return
|
||||
|
||||
return new ResizeObserver(e => e.map(callback))
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ScrollShadow',
|
||||
props: {
|
||||
hideScrollbar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shadowTopOffset: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
shadow: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false
|
||||
},
|
||||
debounceTimeout: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.calcDimensions)
|
||||
|
||||
// Check if shadows are necessary after the element is resized.
|
||||
const scrollContainerObserver = newResizeObserver(this.toggleShadow)
|
||||
if (scrollContainerObserver) {
|
||||
scrollContainerObserver.observe(this.$refs.scrollContainer)
|
||||
// Cleanup when the component is destroyed.
|
||||
this.$once('hook:destroyed', () => scrollContainerObserver.disconnect())
|
||||
}
|
||||
|
||||
// Recalculate the container dimensions when the wrapper is resized.
|
||||
const wrapObserver = newResizeObserver(this.calcDimensions)
|
||||
if (wrapObserver) {
|
||||
wrapObserver.observe(this.$el)
|
||||
// Cleanup when the component is destroyed.
|
||||
this.$once('hook:destroyed', () => wrapObserver.disconnect())
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('resize', this.calcDimensions)
|
||||
},
|
||||
methods: {
|
||||
async calcDimensions () {
|
||||
// Reset dimensions for correctly recalculating parent dimensions.
|
||||
this.width = undefined
|
||||
this.height = undefined
|
||||
await this.$nextTick()
|
||||
|
||||
this.width = `${this.$el.clientWidth}px`
|
||||
this.height = `${this.$el.clientHeight}px`
|
||||
},
|
||||
// Check if shadows are needed.
|
||||
toggleShadow () {
|
||||
const hasHorizontalScrollbar =
|
||||
this.$refs.scrollContainer.clientWidth <
|
||||
this.$refs.scrollContainer.scrollWidth
|
||||
const hasVerticalScrollbar =
|
||||
this.$refs.scrollContainer.clientHeight <
|
||||
this.$refs.scrollContainer.scrollHeight
|
||||
|
||||
const scrolledFromLeft =
|
||||
this.$refs.scrollContainer.offsetWidth +
|
||||
this.$refs.scrollContainer.scrollLeft
|
||||
const scrolledFromTop =
|
||||
this.$refs.scrollContainer.offsetHeight +
|
||||
this.$refs.scrollContainer.scrollTop
|
||||
|
||||
const scrolledToTop = this.$refs.scrollContainer.scrollTop === 0
|
||||
const scrolledToRight =
|
||||
scrolledFromLeft >= this.$refs.scrollContainer.scrollWidth
|
||||
const scrolledToBottom =
|
||||
scrolledFromTop >= this.$refs.scrollContainer.scrollHeight
|
||||
const scrolledToLeft = this.$refs.scrollContainer.scrollLeft === 0
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.shadow.top = hasVerticalScrollbar && !scrolledToTop
|
||||
this.shadow.right = hasHorizontalScrollbar && !scrolledToRight
|
||||
this.shadow.bottom = hasVerticalScrollbar && !scrolledToBottom
|
||||
this.shadow.left = hasHorizontalScrollbar && !scrolledToLeft
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrap {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.shadow-top,
|
||||
.shadow-right,
|
||||
.shadow-bottom,
|
||||
.shadow-left {
|
||||
position: absolute;
|
||||
border-radius: 6em;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shadow-top,
|
||||
.shadow-bottom {
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 1em;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
background-image: linear-gradient(rgba(#555, 0.1) 0%, rgba(#FFF, 0) 100%);
|
||||
}
|
||||
|
||||
.shadow-top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.shadow-bottom {
|
||||
bottom: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.shadow-right,
|
||||
.shadow-left {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1em;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background-image: linear-gradient(90deg, rgba(#555, 0.1) 0%, rgba(#FFF, 0) 100%);
|
||||
}
|
||||
|
||||
.shadow-right {
|
||||
right: 0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.shadow-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
44
resources/js/components/common/Steps.vue
Normal file
44
resources/js/components/common/Steps.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="py-4" :class="{'border-b-2':borderBottom}">
|
||||
<div class="uppercase tracking-wide text-xs font-bold dark:text-gray-400 text-gray-500 mb-1 leading-tight">
|
||||
Step: {{ Math.min(current + 1, steps.length) }} of {{ steps.length }}
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="text-lg font-bold dark:text-gray-300 text-gray-700 leading-tight">
|
||||
{{ steps[current] ? steps[current] : 'Complete!' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center md:w-64">
|
||||
<div class="w-full bg-gray-100 dark:bg-gray-700 rounded-full mr-2">
|
||||
<div class="rounded-full bg-nt-blue text-xs leading-none h-2 text-center text-white transition-all"
|
||||
:style="{'width': parseInt(current / steps.length * 100) +'%', 'min-width': '8px'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs w-10 text-gray-600 dark:text-gray-400" v-text="parseInt(current / steps.length * 100) +'%'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Steps',
|
||||
|
||||
props: {
|
||||
steps: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
borderBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
current: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
15
resources/js/components/common/index.js
vendored
Normal file
15
resources/js/components/common/index.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import Dropdown from './Dropdown'
|
||||
import Card from './Card'
|
||||
import Button from './Button'
|
||||
import FancyLink from './FancyLink';
|
||||
// Components that are registered globaly.
|
||||
[
|
||||
FancyLink,
|
||||
Card,
|
||||
Button,
|
||||
Dropdown
|
||||
].forEach(Component => {
|
||||
Vue.component(Component.name, Component)
|
||||
})
|
||||
19
resources/js/components/common/transitions/VTransition.vue
Normal file
19
resources/js/components/common/transitions/VTransition.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<transition v-if="name=='slideInUp'"
|
||||
enter-active-class="linear duration-300 overflow-hidden"
|
||||
enter-class="max-h-0"
|
||||
enter-to-class="max-h-screen"
|
||||
leave-active-class="linear duration-300 overflow-hidden"
|
||||
leave-class="max-h-screen"
|
||||
leave-to-class="max-h-0"
|
||||
>
|
||||
<slot />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VTransition',
|
||||
props: { name: { default: 'slideInUp' } }
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user