Initial commit
This commit is contained in:
29
resources/js/components/forms/CheckboxInput.vue
Normal file
29
resources/js/components/forms/CheckboxInput.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<v-checkbox :id="id?id:name" v-model="compVal" :disabled="disabled" :name="name" @input="$emit('input',$event)">
|
||||
{{ label }}
|
||||
</v-checkbox>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
import VCheckbox from './components/VCheckbox'
|
||||
export default {
|
||||
name: 'CheckboxInput',
|
||||
|
||||
components: { VCheckbox },
|
||||
mixins: [inputMixin],
|
||||
props: {},
|
||||
|
||||
mounted () {
|
||||
this.compVal = !!this.compVal
|
||||
this.$emit('input', !!this.compVal)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
53
resources/js/components/forms/CodeInput.vue
Normal file
53
resources/js/components/forms/CodeInput.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.CodeInput.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
|
||||
<prism-editor :id="id?id:name" v-model="compVal" :disabled="disabled"
|
||||
class="code-editor"
|
||||
:class="[theme.CodeInput.input,{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name), 'cursor-not-allowed bg-gray-200':disabled }]"
|
||||
:style="inputStyle" :name="name"
|
||||
:placeholder="placeholder"
|
||||
:highlight="highlighter" @change="onChange"
|
||||
/>
|
||||
|
||||
<small v-if="help" :class="theme.CodeInput.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import Prism Editor
|
||||
import { PrismEditor } from 'vue-prism-editor'
|
||||
import 'vue-prism-editor/dist/prismeditor.min.css' // import the styles somewhere
|
||||
// import highlighting library (you can use any library you want just return html string)
|
||||
|
||||
import { highlight, languages } from 'prismjs/components/prism-core'
|
||||
import 'prismjs/components/prism-clike'
|
||||
import 'prismjs/components/prism-markup'
|
||||
import 'prismjs/themes/prism-tomorrow.css' // import syntax highlighting styles
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'CodeInput',
|
||||
|
||||
components: { PrismEditor },
|
||||
mixins: [inputMixin],
|
||||
|
||||
methods: {
|
||||
onChange (event) {
|
||||
const file = event.target.files[0]
|
||||
this.$set(this.form, this.name, file)
|
||||
},
|
||||
highlighter (code) {
|
||||
return highlight(code, languages.markup) // languages.<insert language> to return html with markup
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
25
resources/js/components/forms/ColorInput.vue
Normal file
25
resources/js/components/forms/ColorInput.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<input :id="id?id:name" v-model="compVal" :disabled="disabled"
|
||||
type="color"
|
||||
:name="name"
|
||||
>
|
||||
<label v-if="label" :for="id?id:name" class="text-gray-700 dark:text-gray-300">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'ColorInput',
|
||||
mixins: [inputMixin]
|
||||
}
|
||||
</script>
|
||||
78
resources/js/components/forms/DateInput.vue
Normal file
78
resources/js/components/forms/DateInput.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<t-datepicker :id="id?id:name" ref="datepicker" v-model="compVal" class="datepicker" :disabled="disabled"
|
||||
:class="{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name), 'cursor-not-allowed bg-gray-200':disabled }"
|
||||
:style="inputStyle" :name="name" :fixed-classes="fixedClasses" :range="dateRange"
|
||||
:placeholder="placeholder" :timepicker="useTime"
|
||||
:date-format="useTime?'Z':'Y-m-d'"
|
||||
:user-format="useTime?'F j, Y - H:i':'F j, Y'"
|
||||
/>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fixedClasses } from '../../plugins/config/vue-tailwind/datePicker'
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'DateInput',
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
withTime: { type: Boolean, default: false },
|
||||
dateRange: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
fixedClasses: fixedClasses
|
||||
}),
|
||||
|
||||
computed: {
|
||||
useTime () {
|
||||
return this.withTime && !this.dateRange
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
color: {
|
||||
handler () {
|
||||
this.setInputColor()
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
fixedClasses.input = this.theme.default.input
|
||||
this.setInputColor()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Pressing enter won't submit form
|
||||
* @param event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onEnterPress (event) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
},
|
||||
setInputColor () {
|
||||
if (this.$refs.datepicker) {
|
||||
const dateInput = this.$refs.datepicker.$el.getElementsByTagName('input')[0]
|
||||
dateInput.style.setProperty('--tw-ring-color', this.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
244
resources/js/components/forms/FileInput.vue
Normal file
244
resources/js/components/forms/FileInput.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" role="button"
|
||||
class="flex cursor-pointer relative w-full" :class="[theme.default.input,{'ring-red-500 ring-2': hasValidation && form.errors.has(name)}]"
|
||||
:style="inputStyle" @click.self="showUploadModal=true"
|
||||
>
|
||||
<div v-if="currentUrl==null" class="h-6 text-gray-600 dark:text-gray-400 flex-grow" @click.prevent="showUploadModal=true">
|
||||
Upload {{ multiple?'file(s)':'a file' }} <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="flex-grow h-6 text-gray-600 dark:text-gray-400" @click.prevent="showUploadModal=true">
|
||||
<div class="truncate">
|
||||
<p v-if="files.length==1"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline mr-2 -mt-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>{{ files[0].file.name }}</p>
|
||||
<p v-else><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline mr-2 -mt-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>{{ files.length }} files</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" v-if="files.length>0" class="hover:text-nt-blue" @click.prevent="clearAll" role="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg></a>
|
||||
</template>
|
||||
</button>
|
||||
</span>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
|
||||
<!-- Modal -->
|
||||
<modal :portal-order="2" :show="showUploadModal" @close="showUploadModal=false">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Upload {{ multiple?'file(s)':'a file' }}
|
||||
</h2>
|
||||
|
||||
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||
<div class="sm:mt-5 sm:grid sm:grid-cols-1 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<div class="mt-2 sm:mt-0 sm:col-span-2 mb-5">
|
||||
<div
|
||||
v-cloak
|
||||
class="w-full flex justify-center items-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md h-128"
|
||||
@dragover.prevent="onUploadDragoverEvent($event)"
|
||||
@drop.prevent="onUploadDropEvent($event)"
|
||||
>
|
||||
<div v-if="loading" class="text-gray-600 dark:text-gray-400">
|
||||
<loader class="h-6 w-6 mx-auto m-10" />
|
||||
<p class="text-center mt-6">
|
||||
Uploading your file...
|
||||
</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
class="absolute rounded-full bg-gray-100 h-20 w-20 z-10 transition-opacity duration-500 ease-in-out"
|
||||
:class="{
|
||||
'opacity-100': uploadDragoverTracking,
|
||||
'opacity-0': !uploadDragoverTracking
|
||||
}"
|
||||
/>
|
||||
<div class="relative z-20 text-center">
|
||||
<input ref="actual-input" class="hidden" :multiple="multiple" type="file" :name="name"
|
||||
@change="manualFileUpload"
|
||||
:accept="acceptExtensions"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-24 w-24 text-gray-200" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
class="font-semibold text-nt-blue hover:text-nt-blue-dark focus:outline-none focus:underline transition duration-150 ease-in-out"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
Upload {{ multiple?'file(s)':'a file' }}
|
||||
</button>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Up to {{ mbLimit }}mb
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="files.length" class="mt-4">
|
||||
<div class="border rounded-md">
|
||||
<div v-for="file,index in files" class="flex p-2" :class="{'border-t':index!==0}">
|
||||
<p class="flex-grow truncate text-gray-500">
|
||||
{{ file.file.name }}
|
||||
</p>
|
||||
<div>
|
||||
<a href="#" class="text-gray-400 dark:text-gray-600 hover:text-nt-blue flex" @click.prevent="clearFile(index)" role="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../Modal'
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'FileInput',
|
||||
|
||||
components: { Modal },
|
||||
mixins: [inputMixin],
|
||||
props: {
|
||||
multiple: { type: Boolean, default: true },
|
||||
mbLimit: { type: Number, default: 5 },
|
||||
accept: { type: String, default: "" }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
showUploadModal: false,
|
||||
|
||||
files: [],
|
||||
uploadDragoverTracking: false,
|
||||
uploadDragoverEvent: false,
|
||||
loading: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
currentUrl () {
|
||||
return this.form[this.name]
|
||||
},
|
||||
acceptExtensions(){
|
||||
if(this.accept){
|
||||
return this.accept.split(",").map((i) => {
|
||||
return "."+i.trim()
|
||||
}).join(",")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
files: {
|
||||
deep: true,
|
||||
handler (files) {
|
||||
this.compVal = files.map(file => file.url)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
},
|
||||
|
||||
methods: {
|
||||
clearAll () {
|
||||
this.files = []
|
||||
},
|
||||
clearFile (index) {
|
||||
this.files.splice(index, 1)
|
||||
},
|
||||
onUploadDragoverEvent (e) {
|
||||
this.uploadDragoverEvent = true
|
||||
this.uploadDragoverTracking = true
|
||||
},
|
||||
onUploadDropEvent (e) {
|
||||
this.uploadDragoverEvent = false
|
||||
this.uploadDragoverTracking = false
|
||||
this.droppedFiles(e)
|
||||
},
|
||||
droppedFiles (e) {
|
||||
const droppedFiles = e.dataTransfer.files
|
||||
|
||||
if (!droppedFiles) return
|
||||
|
||||
droppedFiles.forEach(file => {
|
||||
this.uploadFileToServer(file)
|
||||
})
|
||||
},
|
||||
openFileUpload () {
|
||||
this.$refs['actual-input'].click()
|
||||
},
|
||||
manualFileUpload (e) {
|
||||
e.target.files.forEach(file => {
|
||||
this.uploadFileToServer(file)
|
||||
})
|
||||
},
|
||||
uploadFileToServer (file) {
|
||||
this.loading = true
|
||||
this.storeFile(file).then(response => {
|
||||
if (!this.multiple) {
|
||||
this.files = []
|
||||
}
|
||||
this.files.push({
|
||||
file: file,
|
||||
url: file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension
|
||||
})
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
}).catch((error) => {
|
||||
this.clearAll()
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
86
resources/js/components/forms/FlatSelectInput.vue
Normal file
86
resources/js/components/forms/FlatSelectInput.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
|
||||
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
<div v-else v-for="option in options" :key="option[optionKey]" class="flex border mb-4 p-3 cursor-pointer rounded-2xl" @click="onSelect(option[optionKey])">
|
||||
<p class="flex-grow">
|
||||
{{ option[displayKey] }}
|
||||
</p>
|
||||
<span v-if="isSelected(option[optionKey])" class="float-right">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<small v-if="help" :class="theme.SelectInput.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
/**
|
||||
* Options: {name,value} objects
|
||||
*/
|
||||
export default {
|
||||
name: 'FlatSelectInput',
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: 'value' },
|
||||
emitKey: { type: String, default: 'value' },
|
||||
displayKey: { type: String, default: 'name' },
|
||||
loading: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
onSelect (value) {
|
||||
if (this.multiple) {
|
||||
const emitValue = Array.isArray(this.compVal) ? [...this.compVal] : []
|
||||
|
||||
// Already in value, remove it
|
||||
if (this.isSelected(value)) {
|
||||
this.compVal = emitValue.filter((item) => {
|
||||
return item !== value
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise add value
|
||||
emitValue.push(value)
|
||||
this.compVal = emitValue
|
||||
} else {
|
||||
this.compVal = (this.compVal === value) ? null : value
|
||||
}
|
||||
},
|
||||
isSelected (value) {
|
||||
if(!this.compVal) return false
|
||||
|
||||
if (this.multiple) {
|
||||
return this.compVal.includes(value)
|
||||
}
|
||||
return this.compVal === value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
188
resources/js/components/forms/ImageInput.vue
Normal file
188
resources/js/components/forms/ImageInput.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
||||
class="cursor-pointer relative w-full" :class="[theme.default.input,{'ring-red-500 ring-2': hasValidation && form.errors.has(name)}]"
|
||||
:style="inputStyle" @click.prevent="showUploadModal=true"
|
||||
>
|
||||
<div v-if="currentUrl==null" class="h-6 text-gray-600 dark:text-gray-400">
|
||||
Upload image <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div v-else class="h-6 text-gray-600 dark:text-gray-400 flex">
|
||||
<div class="flex-grow">
|
||||
<img :src="currentUrl" class="h-6 rounded shadow-md">
|
||||
</div>
|
||||
<a href="#" class="hover:text-nt-blue flex" @click.prevent="clearUrl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg></a>
|
||||
</div>
|
||||
</button>
|
||||
</span>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
|
||||
<!-- Modal -->
|
||||
<modal :show="showUploadModal" @close="showUploadModal=false">
|
||||
<h2 class="text-lg font-semibold">
|
||||
Upload an image
|
||||
</h2>
|
||||
|
||||
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||
<div class="sm:mt-5 sm:grid sm:grid-cols-1 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<div class="mt-2 sm:mt-0 sm:col-span-2 mb-5">
|
||||
<div
|
||||
v-cloak
|
||||
class="w-full flex justify-center items-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md h-128"
|
||||
@dragover.prevent="onUploadDragoverEvent($event)"
|
||||
@drop.prevent="onUploadDropEvent($event)"
|
||||
>
|
||||
<div v-if="loading" class="text-gray-600 dark:text-gray-400">
|
||||
<loader class="h-6 w-6 mx-auto m-10" />
|
||||
<p class="text-center mt-6">
|
||||
Uploading your file...
|
||||
</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
class="absolute rounded-full bg-gray-100 h-20 w-20 z-10 transition-opacity duration-500 ease-in-out"
|
||||
:class="{
|
||||
'opacity-100': uploadDragoverTracking,
|
||||
'opacity-0': !uploadDragoverTracking
|
||||
}"
|
||||
/>
|
||||
<div class="relative z-20 text-center">
|
||||
<input ref="actual-input" class="hidden" type="file" :name="name"
|
||||
accept="image/png, image/gif, image/jpeg, image/bmp" @change="manualFileUpload"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-24 w-24 text-gray-200" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
|
||||
/>
|
||||
</svg>
|
||||
<p class="mt-5 text-sm text-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
class="font-semibold text-nt-blue hover:text-nt-blue-dark focus:outline-none focus:underline transition duration-150 ease-in-out"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
Upload your image
|
||||
</button>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
.jpg, .jpeg, .png, .bmp, .gif up to 5mb
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from '../Modal'
|
||||
import axios from 'axios'
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'ImageInput',
|
||||
|
||||
components: { Modal },
|
||||
mixins: [inputMixin],
|
||||
props: {},
|
||||
|
||||
data: () => ({
|
||||
showUploadModal: false,
|
||||
|
||||
file: [],
|
||||
uploadDragoverTracking: false,
|
||||
uploadDragoverEvent: false,
|
||||
loading: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
currentUrl () {
|
||||
return this.compVal
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
clearUrl () {
|
||||
this.$set(this.form, this.name, null)
|
||||
},
|
||||
onUploadDragoverEvent (e) {
|
||||
this.uploadDragoverEvent = true
|
||||
this.uploadDragoverTracking = true
|
||||
},
|
||||
onUploadDropEvent (e) {
|
||||
this.uploadDragoverEvent = false
|
||||
this.uploadDragoverTracking = false
|
||||
this.droppedFiles(e)
|
||||
},
|
||||
droppedFiles (e) {
|
||||
const droppedFiles = e.dataTransfer.files
|
||||
|
||||
if (!droppedFiles) return
|
||||
|
||||
this.file = droppedFiles[0]
|
||||
this.uploadFileToServer()
|
||||
},
|
||||
openFileUpload () {
|
||||
this.$refs['actual-input'].click()
|
||||
},
|
||||
manualFileUpload (e) {
|
||||
this.file = e.target.files[0]
|
||||
this.uploadFileToServer()
|
||||
},
|
||||
uploadFileToServer () {
|
||||
this.loading = true
|
||||
// Store file in s3
|
||||
this.storeFile(this.file).then(response => {
|
||||
// Move file to permanent storage for form assets
|
||||
axios.post('/api/open/forms/assets/upload', {
|
||||
url: this.file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension
|
||||
}).then(moveFileResponse => {
|
||||
if (!this.multiple) {
|
||||
this.files = []
|
||||
}
|
||||
this.compVal = moveFileResponse.data.url
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
}).catch((error) => {
|
||||
this.compVal = null
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
}).catch((error) => {
|
||||
this.compVal = null
|
||||
this.showUploadModal = false
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
67
resources/js/components/forms/RatingInput.vue
Normal file
67
resources/js/components/forms/RatingInput.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div :class="wrapperClass" :style="inputStyle">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
|
||||
<div class="stars-outer">
|
||||
<div v-for="i in numberOfStars" :key="i"
|
||||
class="cursor-pointer inline-block"
|
||||
:class="{'text-yellow-400':i<=compVal, 'text-yellow-100 dark:text-yellow-900':i>compVal && i<=hoverRating ,'text-gray-200 dark:text-gray-800':i>compVal && i>hoverRating}"
|
||||
role="button" @click="setRating(i)"
|
||||
@mouseover="hoverRating = i"
|
||||
@mouseleave="hoverRating = null"
|
||||
>
|
||||
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'RatingInput',
|
||||
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
numberOfStars: { type: Number, default: 5 }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
hoverRating: null
|
||||
}
|
||||
},
|
||||
|
||||
updated () {
|
||||
if (this.compVal === null) {
|
||||
this.compVal = 0
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setRating (val) {
|
||||
if (this.compVal === val) {
|
||||
this.compVal = 0
|
||||
} else {
|
||||
this.compVal = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
70
resources/js/components/forms/RichTextAreaInput.vue
Normal file
70
resources/js/components/forms/RichTextAreaInput.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.RichTextAreaInput.label, {'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<vue-editor :id="id?id:name" ref="editor" v-model="compVal" :disabled="disabled"
|
||||
:placeholder="placeholder" :class="[{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name) }, theme.RichTextAreaInput.input]"
|
||||
:editor-toolbar="editorToolbar" class="rich-editor resize-y"
|
||||
:style="inputStyle"
|
||||
/>
|
||||
|
||||
<small v-if="help" :class="theme.RichTextAreaInput.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { VueEditor, Quill } from 'vue2-editor'
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
Quill.imports['formats/link'].PROTOCOL_WHITELIST.push('notion')
|
||||
|
||||
export default {
|
||||
name: 'RichTextAreaInput',
|
||||
components: { VueEditor },
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
editorToolbar: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [
|
||||
[{ header: 1 }, { header: 2 }],
|
||||
['bold', 'italic', 'underline', 'link'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.rich-editor {
|
||||
.ql-container {
|
||||
border-bottom: 0px !important;
|
||||
border-right: 0px !important;
|
||||
border-left: 0px !important;
|
||||
|
||||
.ql-editor {
|
||||
min-height: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ql-toolbar {
|
||||
border-top: 0px !important;
|
||||
border-right: 0px !important;
|
||||
border-left: 0px !important;
|
||||
}
|
||||
|
||||
.ql-snow .ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar button.ql-active, .ql-snow .ql-toolbar button:focus, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar button.ql-active, .ql-snow.ql-toolbar button:focus, .ql-snow.ql-toolbar button:hover {
|
||||
@apply text-nt-blue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
resources/js/components/forms/SelectInput.vue
Normal file
109
resources/js/components/forms/SelectInput.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<v-select v-model="compVal"
|
||||
:dusk="name"
|
||||
:data="finalOptions"
|
||||
:label="label"
|
||||
:option-key="optionKey"
|
||||
:emit-key="emitKey"
|
||||
:required="required"
|
||||
:multiple="multiple"
|
||||
:searchable="searchable"
|
||||
:loading="loading"
|
||||
:color="color"
|
||||
:placeholder="placeholder"
|
||||
:uppercase-labels="uppercaseLabels"
|
||||
:theme="theme"
|
||||
:has-error="hasValidation && form.errors.has(name)"
|
||||
:allowCreation="allowCreation"
|
||||
|
||||
@update-options="updateOptions"
|
||||
>
|
||||
<template #selected="{option}">
|
||||
<template v-if="multiple">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<span v-for="(item,index) in option" :key="item" class="truncate">
|
||||
<span v-if="index!==0">, </span>
|
||||
{{ getOptionName(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="selected" :option="option" :optionName="getOptionName(option)">
|
||||
<div class="flex items-center truncate mr-6">
|
||||
<div>{{ getOptionName(option) }}</div>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</template>
|
||||
<template #option="{option, selected}">
|
||||
<slot name="option" :option="option" :selected="selected">
|
||||
<span class="flex group-hover:text-white">
|
||||
<p class="flex-grow group-hover:text-white">
|
||||
{{ option.name }}
|
||||
</p>
|
||||
<span v-if="selected" class="absolute inset-y-0 right-0 flex items-center pr-4 dark:text-white">
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</v-select>
|
||||
<small v-if="help" :class="theme.SelectInput.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
/**
|
||||
* Options: {name,value} objects
|
||||
*/
|
||||
export default {
|
||||
name: 'SelectInput',
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
options: { type: Array, required: true },
|
||||
optionKey: { type: String, default: 'value' },
|
||||
emitKey: { type: String, default: 'value' },
|
||||
displayKey: { type: String, default: 'name' },
|
||||
loading: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
searchable: { type: Boolean, default: false },
|
||||
allowCreation: { type: Boolean, default: false }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
additionalOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
finalOptions(){
|
||||
return this.options.concat(this.additionalOptions)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getOptionName (val) {
|
||||
const option = this.finalOptions.find((optionCandidate) => {
|
||||
return optionCandidate[this.optionKey] === val
|
||||
})
|
||||
if (option) return option[this.displayKey]
|
||||
return null
|
||||
},
|
||||
updateOptions(newItem) {
|
||||
if(newItem){
|
||||
this.additionalOptions.push(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
29
resources/js/components/forms/TextAreaInput.vue
Normal file
29
resources/js/components/forms/TextAreaInput.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.default.label, {'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<textarea :id="id?id:name" v-model="compVal" :disabled="disabled"
|
||||
:class="[theme.default.input,{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name) }]"
|
||||
class="resize-y"
|
||||
:name="name" :style="inputStyle"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<small v-if="help" :class="theme.default.help">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
|
||||
export default {
|
||||
name: 'TextAreaInput',
|
||||
mixins: [inputMixin]
|
||||
}
|
||||
</script>
|
||||
95
resources/js/components/forms/TextInput.vue
Normal file
95
resources/js/components/forms/TextInput.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div :class="wrapperClass" :style="inputStyle">
|
||||
<label v-if="label" :for="id?id:name"
|
||||
:class="[theme.default.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
<input :id="id?id:name" v-model="compVal" :disabled="disabled"
|
||||
:type="nativeType"
|
||||
:style="inputStyle"
|
||||
:class="[theme.default.input,{ 'ring-red-500 ring-2': hasValidation && form.errors.has(name), 'cursor-not-allowed bg-gray-200':disabled }]"
|
||||
:name="name" :accept="accept"
|
||||
:placeholder="placeholder" :min="min" :max="max" :maxlength="maxCharLimit"
|
||||
@change="onChange" @keydown.enter.prevent="onEnterPress"
|
||||
>
|
||||
<div v-if="help || showCharLimit" class="flex">
|
||||
<small v-if="help" :class="theme.default.help" class="flex-grow">
|
||||
<slot name="help">{{ help }}</slot>
|
||||
</small>
|
||||
<small v-else class="flex-grow"></small>
|
||||
<small v-if="showCharLimit && maxCharLimit" :class="theme.default.help">
|
||||
{{ charCount }}/{{ maxCharLimit }}
|
||||
</small>
|
||||
</div>
|
||||
<has-error v-if="hasValidation" :form="form" :field="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import inputMixin from '~/mixins/forms/input'
|
||||
export default {
|
||||
name: 'TextInput',
|
||||
|
||||
mixins: [inputMixin],
|
||||
|
||||
props: {
|
||||
nativeType: { type: String, default: 'text' },
|
||||
accept: { type: String, default: null },
|
||||
min: { type: Number, required: false, default: null },
|
||||
max: { type: Number, required: false, default: null },
|
||||
maxCharLimit: { type: Number, required: false, default: null },
|
||||
showCharLimit: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
|
||||
data: () => ({}),
|
||||
|
||||
computed: {
|
||||
compVal: {
|
||||
set (val) {
|
||||
if (this.form) {
|
||||
this.$set(this.form, this.nativeType !== 'file' ? this.name : 'file-' + this.name, val)
|
||||
} else {
|
||||
this.content = val
|
||||
}
|
||||
if (this.hasValidation) {
|
||||
this.form.errors.clear(this.name)
|
||||
}
|
||||
this.$emit('input', val)
|
||||
},
|
||||
get () {
|
||||
if (this.form) {
|
||||
return this.form[this.nativeType !== 'file' ? this.name : 'file-' + this.name]
|
||||
}
|
||||
return this.content
|
||||
}
|
||||
},
|
||||
charCount() {
|
||||
return (this.compVal) ? this.compVal.length : 0
|
||||
}
|
||||
},
|
||||
|
||||
watch: {},
|
||||
|
||||
created () {},
|
||||
|
||||
methods: {
|
||||
onChange (event) {
|
||||
if (this.nativeType !== 'file') return
|
||||
|
||||
const file = event.target.files[0]
|
||||
this.$set(this.form, this.name, file)
|
||||
},
|
||||
/**
|
||||
* Pressing enter won't submit form
|
||||
* @param event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onEnterPress (event) {
|
||||
event.preventDefault()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
88
resources/js/components/forms/components/VCheckbox.vue
Normal file
88
resources/js/components/forms/components/VCheckbox.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
:id="id || name"
|
||||
:name="name"
|
||||
:checked="internalValue"
|
||||
type="checkbox"
|
||||
:class="sizeClasses"
|
||||
class="rounded border-gray-500 cursor-pointer"
|
||||
:disabled="disabled"
|
||||
@click="handleClick"
|
||||
>
|
||||
<label :for="id || name" class="text-gray-700 dark:text-gray-300 ml-2" :class="{'cursor-not-allowed':disabled}">
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'VCheckbox',
|
||||
|
||||
props: {
|
||||
id: { type: String, default: null },
|
||||
name: { type: String, default: 'checkbox' },
|
||||
value: { type: [Boolean, String], default: false },
|
||||
checked: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
size: { type: String, default: 'normal' }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
internalValue: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
sizeClasses () {
|
||||
if (this.size === 'small') {
|
||||
return 'w-3 h-3'
|
||||
}
|
||||
return 'w-5 h-5'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (val) {
|
||||
this.internalValue = val
|
||||
},
|
||||
|
||||
checked (val) {
|
||||
this.internalValue = val
|
||||
},
|
||||
|
||||
internalValue (val, oldVal) {
|
||||
// Support form data string checkbox (string 1 or 0)
|
||||
if (val === 0 || val === '0') val = false
|
||||
if (val === 1 || val === '1') val = true
|
||||
|
||||
if (val !== oldVal) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
this.internalValue = this.value
|
||||
|
||||
if ('checked' in this.$options.propsData) {
|
||||
this.internalValue = this.checked
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.$emit('input', this.internalValue)
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClick (e) {
|
||||
this.$emit('click', e)
|
||||
|
||||
if (!e.isPropagationStopped) {
|
||||
this.internalValue = e.target.checked
|
||||
this.$emit('input', this.internalValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
223
resources/js/components/forms/components/VSelect.vue
Normal file
223
resources/js/components/forms/components/VSelect.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="v-select">
|
||||
<label v-if="label"
|
||||
:class="[theme.SelectInput.label,{'uppercase text-xs':uppercaseLabels, 'text-sm':!uppercaseLabels}]"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 required-dot">*</span>
|
||||
</label>
|
||||
|
||||
<div v-on-clickaway="closeDropdown"
|
||||
class="relative"
|
||||
>
|
||||
<span class="inline-block w-full rounded-md">
|
||||
<button type="button" :dusk="dusk" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label"
|
||||
class="cursor-pointer"
|
||||
:style="inputStyle" :class="[theme.SelectInput.input,{'py-2':!multiple || loading,'py-1': multiple, 'ring-red-500 ring-2': hasError}]"
|
||||
@click="openDropdown"
|
||||
>
|
||||
<div :class="{'h-6':!multiple, 'min-h-8':multiple && !loading}">
|
||||
<transition name="fade" mode="out-in">
|
||||
<loader v-if="loading" key="loader" class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
<div v-else-if="value" key="value" class="flex" :class="{'min-h-8':multiple}">
|
||||
<slot name="selected" :option="value" />
|
||||
</div>
|
||||
<div v-else key="placeholder">
|
||||
<slot name="placeholder">
|
||||
<div class="text-gray-400 dark:text-gray-500 w-full text-left" :class="{'py-1':multiple && !loading}">
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button></span>
|
||||
<!-- Select popover, show/hide based on select state. -->
|
||||
<div v-show="isOpen" :dusk="dusk+'_dropdown'"
|
||||
class="absolute mt-1 w-full rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10"
|
||||
>
|
||||
<ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3"
|
||||
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
|
||||
:class="{'max-h-42 py-1': !isSearchable,'max-h-48 pb-1': isSearchable}"
|
||||
>
|
||||
<div v-if="isSearchable" class="px-2 pt-2 sticky top-0 bg-white dark:bg-notion-dark-light z-10">
|
||||
<text-input name="search" :color="color" v-model="searchTerm" :theme="theme"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loading" class="w-full py-2 flex justify-center">
|
||||
<loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
<template v-if="filteredOptions.length>0">
|
||||
<li v-for="item in filteredOptions" :key="item[optionKey]" role="option"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9 cursor-pointer group hover:text-white hover:bg-nt-blue focus:outline-none focus:text-white focus:bg-nt-blue"
|
||||
:dusk="dusk+'_option'" @click="select(item)"
|
||||
>
|
||||
<slot name="option" :option="item" :selected="isSelected(item)" />
|
||||
</li>
|
||||
</template>
|
||||
<p v-else-if="!loading" class="w-full text-gray-500 text-center py-2">
|
||||
No option available.
|
||||
</p>
|
||||
<li v-if="allowCreation && searchTerm" role="option"
|
||||
class="text-gray-900 cursor-default select-none relative py-2 pl-3 pr-9 cursor-pointer group hover:text-white hover:bg-nt-blue focus:outline-none focus:text-white focus:bg-nt-blue"
|
||||
@click="createOption(searchTerm)"
|
||||
>
|
||||
Create <b class="px-1 bg-gray-300 rounded group-hover:text-black">{{searchTerm}}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { directive as onClickaway } from 'vue-clickaway'
|
||||
import TextInput from '../TextInput'
|
||||
import Fuse from 'fuse.js'
|
||||
import { themes } from '~/config/form-themes'
|
||||
import debounce from 'debounce'
|
||||
|
||||
export default {
|
||||
name: 'VSelect',
|
||||
components: { TextInput },
|
||||
directives: {
|
||||
onClickaway: onClickaway
|
||||
},
|
||||
props: {
|
||||
data: Array,
|
||||
value: { default: null },
|
||||
label: { type: String, default: null },
|
||||
dusk: { type: String, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
required: { type: Boolean, default: false },
|
||||
multiple: { type: Boolean, default: false },
|
||||
searchable: { type: Boolean, default: false },
|
||||
hasError: { type: Boolean, default: false },
|
||||
remote: { type: Function, default: null },
|
||||
searchKeys: { type: Array, default: () => ['name'] },
|
||||
optionKey: { type: String, default: 'id' },
|
||||
emitKey: { type: String, default: null }, // Key used for emitted value, emit object if null,
|
||||
color: { type: String, default: '#3B82F6' },
|
||||
placeholder: { type: String, default: null },
|
||||
uppercaseLabels: { type: Boolean, default: true },
|
||||
theme: { type: Object, default: () => themes.default },
|
||||
allowCreation: { type: Boolean, default: false },
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isOpen: false,
|
||||
searchTerm: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputStyle () {
|
||||
return {
|
||||
'--tw-ring-color': this.color
|
||||
}
|
||||
},
|
||||
debouncedRemote () {
|
||||
if (this.remote) {
|
||||
return debounce(this.remote, 300)
|
||||
}
|
||||
return null
|
||||
},
|
||||
filteredOptions () {
|
||||
if (!this.data) return []
|
||||
if (!this.searchable || this.remote || this.searchTerm === '') {
|
||||
return this.data
|
||||
}
|
||||
|
||||
// Fuze search
|
||||
const fuzeOptions = {
|
||||
keys: this.searchKeys
|
||||
}
|
||||
const fuse = new Fuse(this.data, fuzeOptions)
|
||||
return fuse.search(this.searchTerm).map((res) => {
|
||||
return res.item
|
||||
})
|
||||
},
|
||||
isSearchable () {
|
||||
return this.searchable || this.remote !== null || this.allowCreation
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'searchTerm': function (val) {
|
||||
if (!this.debouncedRemote) return
|
||||
if ((this.remote && val) || (val === '' && !this.value) || (val === '' && this.isOpen)) {
|
||||
return this.debouncedRemote(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isSelected (value) {
|
||||
if (!this.value) return false
|
||||
|
||||
if (this.emitKey && value[this.emitKey]) {
|
||||
value = value[this.emitKey]
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
return this.value.includes(value)
|
||||
}
|
||||
return this.value === value
|
||||
},
|
||||
closeDropdown () {
|
||||
this.isOpen = false
|
||||
this.searchTerm = ''
|
||||
},
|
||||
openDropdown () {
|
||||
this.isOpen = !this.isOpen
|
||||
},
|
||||
select (value) {
|
||||
if (!this.multiple) {
|
||||
this.closeDropdown()
|
||||
}
|
||||
|
||||
if (this.emitKey) {
|
||||
value = value[this.emitKey]
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
const emitValue = Array.isArray(this.value) ? [...this.value] : []
|
||||
|
||||
// Already in value, remove it
|
||||
if (this.isSelected(value)) {
|
||||
this.$emit('input', emitValue.filter((item) => {
|
||||
if (this.emitKey) {
|
||||
return item !== value
|
||||
}
|
||||
return item[this.optionKey] !== value && item[this.optionKey] !== value[this.optionKey]
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise add value
|
||||
emitValue.push(value)
|
||||
this.$emit('input', emitValue)
|
||||
} else {
|
||||
if (this.value === value) {
|
||||
this.$emit('input', null)
|
||||
} else {
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
createOption(newOption) {
|
||||
if(newOption){
|
||||
let newItem = {
|
||||
'name': newOption,
|
||||
'value': newOption,
|
||||
}
|
||||
this.$emit("update-options", newItem)
|
||||
this.select(newItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
79
resources/js/components/forms/components/VSwitch.vue
Normal file
79
resources/js/components/forms/components/VSwitch.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div>
|
||||
<Motion
|
||||
v-model="value"
|
||||
:options="{
|
||||
duration: 150,
|
||||
}"
|
||||
:trigger="[
|
||||
'bg-gray-200 border-gray-300 duration-100 dark:bg-gray-700 dark:border-gray-600',
|
||||
'bg-gray-200 dark:bg-gray-700',
|
||||
'bg-nt-blue border-nt-blue',
|
||||
'bg-nt-blue duration-100',
|
||||
]"
|
||||
class="inline-flex items-center h-6 w-12 p-1 border rounded-full cursor-pointer focus:outline-none"
|
||||
@click="$emit('input',!internalValue)"
|
||||
>
|
||||
<Motion
|
||||
v-model="internalValue"
|
||||
tag="span"
|
||||
:options="{
|
||||
duration: 150,
|
||||
}"
|
||||
:trigger="[
|
||||
'translate-x-0 duration-150',
|
||||
'rounded-2xl scale-75 duration-100',
|
||||
'translate-x-6 duration-100',
|
||||
'scale-100 duration-150',
|
||||
]"
|
||||
class="inline-block h-4 w-4 rounded-full bg-white dark:bg-gray-500 shadow"
|
||||
/>
|
||||
</Motion>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Motion from 'tinymotion'
|
||||
export default {
|
||||
name: 'VSwitch',
|
||||
components: { Motion },
|
||||
|
||||
props: {
|
||||
value: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
internalValue: this.value
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
sizeClasses () {
|
||||
if (this.size === 'small') {
|
||||
return 'w-3 h-3'
|
||||
}
|
||||
return 'w-5 h-5'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value (val) {
|
||||
this.internalValue = val
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.internalValue = this.value
|
||||
},
|
||||
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.translate-x-6 {
|
||||
--tw-translate-x: 1.4rem !important;
|
||||
}
|
||||
</style>
|
||||
40
resources/js/components/forms/index.js
vendored
Normal file
40
resources/js/components/forms/index.js
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
import HasError from './validation/HasError.vue'
|
||||
import AlertError from './validation/AlertError'
|
||||
import AlertSuccess from './validation/AlertSuccess'
|
||||
import VCheckbox from './components/VCheckbox'
|
||||
import TextInput from './TextInput'
|
||||
import TextAreaInput from './TextAreaInput'
|
||||
import VSelect from './components/VSelect'
|
||||
import CheckboxInput from './CheckboxInput'
|
||||
import SelectInput from './SelectInput'
|
||||
import ColorInput from './ColorInput'
|
||||
import RichTextAreaInput from './RichTextAreaInput'
|
||||
import FileInput from './FileInput'
|
||||
import ImageInput from './ImageInput'
|
||||
import DateInput from './DateInput';
|
||||
import RatingInput from './RatingInput';
|
||||
import FlatSelectInput from './FlatSelectInput';
|
||||
|
||||
// Components that are registered globaly.
|
||||
[
|
||||
HasError,
|
||||
AlertError,
|
||||
AlertSuccess,
|
||||
VCheckbox,
|
||||
VSelect,
|
||||
CheckboxInput,
|
||||
ColorInput,
|
||||
TextInput,
|
||||
SelectInput,
|
||||
TextAreaInput,
|
||||
FileInput,
|
||||
ImageInput,
|
||||
RichTextAreaInput,
|
||||
DateInput,
|
||||
RatingInput,
|
||||
FlatSelectInput
|
||||
].forEach(Component => {
|
||||
Vue.component(Component.name, Component)
|
||||
})
|
||||
21
resources/js/components/forms/validation/Alert.js
vendored
Normal file
21
resources/js/components/forms/validation/Alert.js
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
dismissible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
dismiss () {
|
||||
if (this.dismissible) {
|
||||
this.form.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
resources/js/components/forms/validation/AlertError.vue
Normal file
29
resources/js/components/forms/validation/AlertError.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div v-if="form.errors.any()" class="alert alert-danger alert-dismissible" role="alert">
|
||||
<button v-if="dismissible" type="button" class="close" aria-label="Close" @click="dismiss">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
|
||||
<slot>
|
||||
<div v-if="form.errors.has('error')" v-html="form.errors.get('error')"/>
|
||||
<div v-else v-html="message"/>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Alert from './Alert'
|
||||
|
||||
export default {
|
||||
name: 'AlertError',
|
||||
|
||||
extends: Alert,
|
||||
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: 'There were some problems with your input.'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
37
resources/js/components/forms/validation/AlertSuccess.vue
Normal file
37
resources/js/components/forms/validation/AlertSuccess.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="form.successful" class="bg-green-200 border-green-600 text-green-600 border-l-4 p-4 relative rounded-lg"
|
||||
role="alert">
|
||||
<button v-if="dismissible"
|
||||
type="button"
|
||||
@click.prevent="dismiss()"
|
||||
class="absolute right-2 top-0 -mr-1 flex-shrink-0 flex p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 sm:-mr-2">
|
||||
<span class="sr-only">
|
||||
Dismiss
|
||||
</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="h-6 w-6 text-green-500"
|
||||
viewBox="0 0 1792 1792">
|
||||
<path
|
||||
d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="font-bold">
|
||||
Success
|
||||
</p>
|
||||
<div v-html="message"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Alert from './Alert'
|
||||
|
||||
export default {
|
||||
name: 'AlertSuccess',
|
||||
extends: Alert,
|
||||
props: {
|
||||
message: { type: String, default: '' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
resources/js/components/forms/validation/HasError.vue
Normal file
43
resources/js/components/forms/validation/HasError.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="errorMessage" class="has-error text-sm text-red-500 -bottom-3"
|
||||
v-html="errorMessage"
|
||||
/>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HasError',
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
errorMessage () {
|
||||
if (!this.form.errors || !this.form.errors.any()) return null
|
||||
const subErrorsKeys = Object.keys(this.form.errors.all()).filter((key) => {
|
||||
return key.startsWith(this.field) && key !== this.field
|
||||
})
|
||||
const baseError = this.form.errors.get(this.field) ?? (subErrorsKeys.length ? 'This field has some errors:' : null)
|
||||
// If no error and no sub errors, return
|
||||
if (!baseError) return null
|
||||
|
||||
return `<p class="text-red-500">${baseError}</p><ul class="list-disc list-inside">${subErrorsKeys.map((key) => {
|
||||
return '<li>' + this.getSubError(key) + '</li>'
|
||||
})}</ul>`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSubError (subErrorKey) {
|
||||
return this.form.errors.get(subErrorKey).replace(subErrorKey, 'item')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user