mirror of
https://github.com/VueFileManager/vuefilemanager.git
synced 2026-04-18 08:12:15 +00:00
vue components refactoring
This commit is contained in:
64
resources/js/components/Inputs/AvatarInput.vue
Normal file
64
resources/js/components/Inputs/AvatarInput.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="relative flex items-center justify-center cursor-pointer h-14 w-14 overflow-hidden bg-light-background rounded-xl cursor-pointer z-10">
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
@change="showImagePreview($event)"
|
||||
class="absolute top-0 bottom-0 left-0 right-0 z-10 w-full cursor-pointer opacity-0"
|
||||
/>
|
||||
<camera-icon v-if="!imagePreview" size="22" class="vue-feather text-gray-300" />
|
||||
<img
|
||||
v-if="imagePreview"
|
||||
ref="image"
|
||||
:src="imagePreview"
|
||||
class="relative w-full h-full z-0 object-cover shadow-lg md:h-16 md:w-16"
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CameraIcon} from 'vue-feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'AvatarInput',
|
||||
props: ['avatar'],
|
||||
components: {
|
||||
CameraIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imagePreview: undefined,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imagePreview(val) {
|
||||
this.$store.commit('UPDATE_AVATAR', val)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showImagePreview(event) {
|
||||
let imgPath = event.target.files[0].name,
|
||||
extension = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (['png', 'jpg', 'jpeg'].includes(extension)) {
|
||||
let file = event.target.files[0],
|
||||
reader = new FileReader()
|
||||
|
||||
reader.onload = () => (this.imagePreview = reader.result)
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// Update user avatar
|
||||
this.$updateImage('/user/settings', 'avatar', event.target.files[0])
|
||||
} else {
|
||||
alert(this.$t('wrong_image_error'))
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// If there is default image then load
|
||||
if (this.avatar) this.imagePreview = this.avatar
|
||||
},
|
||||
}
|
||||
</script>
|
||||
40
resources/js/components/Inputs/CheckBox.vue
Normal file
40
resources/js/components/Inputs/CheckBox.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="flex h-5 w-5 items-center justify-center rounded-md"
|
||||
:class="{
|
||||
'bg-theme': isClicked,
|
||||
'bg-light-background dark:bg-4x-dark-foreground': !isClicked,
|
||||
}"
|
||||
@click="changeState"
|
||||
>
|
||||
<CheckIcon v-if="isClicked" class="vue-feather text-white" size="17" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CheckIcon } from 'vue-feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'CheckBox',
|
||||
props: ['isClicked'],
|
||||
components: {
|
||||
CheckIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSwitched: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeState() {
|
||||
this.isSwitched = !this.isSwitched
|
||||
this.$emit('input', this.isSwitched)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isSwitched = this.isClicked
|
||||
},
|
||||
}
|
||||
</script>
|
||||
52
resources/js/components/Inputs/CopyInput.vue
Normal file
52
resources/js/components/Inputs/CopyInput.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div @click="copyUrl" class="relative flex items-center">
|
||||
<input ref="sel" :value="str" :id="id" type="text" class="focus-border-theme input-dark !pr-10" readonly />
|
||||
|
||||
<!--Copy icon-->
|
||||
<div class="absolute right-0 px-4">
|
||||
<copy-icon v-if="!isCopiedLink" size="16" class="hover-text-theme vue-feather cursor-pointer" />
|
||||
<check-icon v-if="isCopiedLink" size="16" class="text-theme vue-feather cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CopyIcon, CheckIcon, SendIcon } from 'vue-feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'CopyInput',
|
||||
props: ['size', 'str'],
|
||||
components: {
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
SendIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCopiedLink: false,
|
||||
id: 'link-input-' + Math.floor(Math.random() * 10000000),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyUrl() {
|
||||
// Get input value
|
||||
let copyText = document.getElementById(this.id)
|
||||
|
||||
// select link
|
||||
copyText.select()
|
||||
copyText.setSelectionRange(0, 99999)
|
||||
|
||||
// Copy
|
||||
document.execCommand('copy')
|
||||
|
||||
// Mark button as copied
|
||||
this.isCopiedLink = true
|
||||
|
||||
// Reset copy button
|
||||
setTimeout(() => {
|
||||
this.isCopiedLink = false
|
||||
}, 1000)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
199
resources/js/components/Inputs/CopyShareLink.vue
Normal file
199
resources/js/components/Inputs/CopyShareLink.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
ref="sel"
|
||||
:value="item.data.relationships.shared.data.attributes.link"
|
||||
:id="id"
|
||||
type="text"
|
||||
class="focus-border-theme w-full appearance-none rounded-lg border border-transparent bg-light-background py-2 pr-16 pl-3 text-sm font-bold dark:bg-2x-dark-foreground"
|
||||
readonly
|
||||
/>
|
||||
|
||||
<!--Copy icon-->
|
||||
<div class="flex items-center">
|
||||
<div @click="copyUrl" class="absolute right-9 p-1">
|
||||
<copy-icon v-if="!isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
|
||||
<check-icon v-if="isCopiedLink" size="14" class="hover-text-theme vue-feather cursor-pointer" />
|
||||
</div>
|
||||
<div @click.stop.prevent="moreOptions" class="absolute right-2.5 p-1">
|
||||
<more-horizontal-icon size="14" class="hover-text-theme vue-feather cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Hidden options-->
|
||||
<ul
|
||||
v-if="isOpenedMoreOptions"
|
||||
class="absolute top-12 left-0 right-0 z-10 select-none overflow-y-auto overflow-x-hidden rounded-lg shadow-xl"
|
||||
>
|
||||
<li
|
||||
v-if="item.data.type !== 'folder' && !item.data.relationships.shared.data.attributes.protected"
|
||||
@click="copyDirectLink"
|
||||
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
|
||||
>
|
||||
<div class="w-8">
|
||||
<download-icon size="14" />
|
||||
</div>
|
||||
<span class="text-sm font-bold">
|
||||
{{ $t('copy_direct_download_link') }}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@click="getQrCode"
|
||||
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
|
||||
>
|
||||
<div class="w-8">
|
||||
<camera-icon size="14" />
|
||||
</div>
|
||||
<span class="text-sm font-bold">
|
||||
{{ $t('get_qr_code') }}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@click="sendViaEmail"
|
||||
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
|
||||
>
|
||||
<div class="w-8">
|
||||
<send-icon size="14" />
|
||||
</div>
|
||||
<span class="text-sm font-bold">
|
||||
{{ $t('sharelink.share_via_email') }}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@click="copyIframe"
|
||||
class="block flex cursor-pointer items-center bg-white py-2.5 px-5 hover:bg-light-background dark:bg-2x-dark-foreground dark:hover:bg-4x-dark-foreground"
|
||||
>
|
||||
<div class="w-8">
|
||||
<code-icon size="14" />
|
||||
</div>
|
||||
<span class="text-sm font-bold">
|
||||
{{ $t('sharelink.copy_embed') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<textarea
|
||||
v-model="directLink"
|
||||
ref="directLinkTextarea"
|
||||
class="pointer-events-none absolute right-full opacity-0"
|
||||
></textarea>
|
||||
|
||||
<textarea
|
||||
v-model="iframeCode"
|
||||
ref="iframe"
|
||||
class="pointer-events-none absolute right-full opacity-0"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DownloadIcon, CameraIcon, CopyIcon, CheckIcon, SendIcon, MoreHorizontalIcon, CodeIcon } from 'vue-feather-icons'
|
||||
import { events } from '../../bus'
|
||||
|
||||
export default {
|
||||
name: 'CopyShareLink',
|
||||
props: ['item'],
|
||||
components: {
|
||||
MoreHorizontalIcon,
|
||||
CameraIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CodeIcon,
|
||||
SendIcon,
|
||||
DownloadIcon,
|
||||
},
|
||||
watch: {
|
||||
'item': function () {
|
||||
this.setClipboard()
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: 'link-input-' + Math.floor(Math.random() * 10000000),
|
||||
directLink: undefined,
|
||||
iframeCode: undefined,
|
||||
isCopiedLink: false,
|
||||
isOpenedMoreOptions: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
moreOptions() {
|
||||
this.isOpenedMoreOptions = !this.isOpenedMoreOptions
|
||||
},
|
||||
getQrCode() {
|
||||
events.$emit('popup:open', {
|
||||
name: 'share-edit',
|
||||
item: this.item,
|
||||
section: 'qr-code',
|
||||
})
|
||||
|
||||
this.isOpenedMoreOptions = false
|
||||
},
|
||||
sendViaEmail() {
|
||||
events.$emit('popup:open', {
|
||||
name: 'share-edit',
|
||||
item: this.item,
|
||||
section: 'email-sharing',
|
||||
})
|
||||
|
||||
this.isOpenedMoreOptions = false
|
||||
},
|
||||
copyDirectLink() {
|
||||
let copyText = this.$refs.directLinkTextarea
|
||||
|
||||
copyText.select()
|
||||
copyText.setSelectionRange(0, 99999)
|
||||
|
||||
document.execCommand('copy')
|
||||
|
||||
events.$emit('toaster', {
|
||||
type: 'success',
|
||||
message: this.$t('direct_link_copied'),
|
||||
})
|
||||
|
||||
this.isOpenedMoreOptions = false
|
||||
},
|
||||
copyIframe() {
|
||||
let copyText = this.$refs.iframe
|
||||
|
||||
copyText.select()
|
||||
copyText.setSelectionRange(0, 99999)
|
||||
|
||||
document.execCommand('copy')
|
||||
|
||||
events.$emit('toaster', {
|
||||
type: 'success',
|
||||
message: this.$t('web_code_copied'),
|
||||
})
|
||||
|
||||
this.isOpenedMoreOptions = false
|
||||
},
|
||||
copyUrl() {
|
||||
// Get input value
|
||||
let copyText = document.getElementById(this.id)
|
||||
|
||||
// select link
|
||||
copyText.select()
|
||||
copyText.setSelectionRange(0, 99999)
|
||||
|
||||
// Copy
|
||||
document.execCommand('copy')
|
||||
|
||||
// Mark button as copied
|
||||
this.isCopiedLink = true
|
||||
|
||||
// Reset copy button
|
||||
setTimeout(() => {
|
||||
this.isCopiedLink = false
|
||||
}, 1000)
|
||||
},
|
||||
setClipboard() {
|
||||
this.directLink = this.item.data.relationships.shared.data.attributes.link + '/direct'
|
||||
this.iframeCode = `<iframe src="${this.item.data.relationships.shared.data.attributes.link}" width="790" height="400" allowfullscreen frameborder="0"></iframe>`
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.setClipboard()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
97
resources/js/components/Inputs/ImageInput.vue
Normal file
97
resources/js/components/Inputs/ImageInput.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-[175px] items-center justify-center rounded-lg bg-light-background dark:bg-2x-dark-foreground"
|
||||
:class="{ 'is-error': error }"
|
||||
>
|
||||
<!--Reset Image-->
|
||||
<div
|
||||
v-if="imagePreview"
|
||||
@click="resetImage"
|
||||
class="absolute right-0 top-0 z-[9] flex h-7 w-7 -translate-y-3 translate-x-3 cursor-pointer items-center justify-center rounded-md rounded-full dark:bg-4x-dark-foreground bg-white shadow-lg"
|
||||
>
|
||||
<x-icon size="14" class="vue-feather dark:text-gray-500" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
@change="showImagePreview($event)"
|
||||
ref="file"
|
||||
type="file"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 z-10 w-full cursor-pointer opacity-0"
|
||||
/>
|
||||
|
||||
<!--Default image preview-->
|
||||
<img
|
||||
v-if="imagePreview"
|
||||
:src="imagePreview"
|
||||
ref="image"
|
||||
class="absolute h-full w-full object-contain py-4 px-12"
|
||||
/>
|
||||
|
||||
<!--Drop image zone-->
|
||||
<div v-if="!isData" class="text-center">
|
||||
<image-icon size="34" class="vue-feather text-theme mb-4 inline-block" />
|
||||
|
||||
<b class="block text-base font-bold leading-3">
|
||||
{{ $te('input_image.title') ? $t('input_image.title') : 'Upload Image' }}
|
||||
</b>
|
||||
<small class="text-xs text-gray-500">
|
||||
{{
|
||||
$te('input_image.supported')
|
||||
? $t('input_image.supported')
|
||||
: 'Supported formats are .png, .jpg, .jpeg.'
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { XIcon, ImageIcon } from 'vue-feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'ImageInput',
|
||||
props: ['image', 'error'],
|
||||
components: {
|
||||
ImageIcon,
|
||||
XIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imagePreview: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isData() {
|
||||
return !(typeof this.imagePreview === 'undefined' || this.imagePreview === '')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetImage() {
|
||||
this.imagePreview = undefined
|
||||
this.$emit('input', undefined)
|
||||
},
|
||||
showImagePreview(event) {
|
||||
const imgPath = event.target.files[0].name,
|
||||
extn = imgPath.substring(imgPath.lastIndexOf('.') + 1).toLowerCase()
|
||||
|
||||
if (['png', 'jpg', 'jpeg', 'svg'].includes(extn)) {
|
||||
const file = event.target.files[0],
|
||||
reader = new FileReader()
|
||||
|
||||
reader.onload = () => (this.imagePreview = reader.result)
|
||||
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// Update user avatar
|
||||
this.$emit('input', event.target.files[0])
|
||||
} else {
|
||||
alert(this.$t('wrong_image_error'))
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// If has default image then load
|
||||
if (this.image) this.imagePreview = this.image
|
||||
},
|
||||
}
|
||||
</script>
|
||||
99
resources/js/components/Inputs/MultiEmailInput.vue
Normal file
99
resources/js/components/Inputs/MultiEmailInput.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div
|
||||
class="focus-border-theme input-dark focus-within-border-theme"
|
||||
:class="{'!border-rose-600':isError, '!py-3.5': !emails.length, '!px-2 !pt-[10px] !pb-0.5': emails.length}"
|
||||
>
|
||||
<div @click="$refs.input.focus()" class="flex flex-wrap cursor-text items-center" style="grid-template-columns: auto minmax(0,1fr);">
|
||||
<div
|
||||
class="whitespace-nowrap flex items-center rounded-lg bg-theme-100 mr-2 mb-2 py-1 px-2 cursor-pointer w-fit"
|
||||
@click="removeEmail(email)"
|
||||
v-for="(email, i) in emails"
|
||||
:key="i"
|
||||
>
|
||||
<small class="text-sm text-theme mr-1">
|
||||
{{ email }}
|
||||
</small>
|
||||
<x-icon class="vue-feather text-theme" size="14" />
|
||||
</div>
|
||||
<input
|
||||
@keydown.delete="removeLastEmail($event)"
|
||||
@keyup="handleEmail()"
|
||||
v-model="email"
|
||||
:size="inputSize"
|
||||
class="w-auto font-bold text-sm bg-transparent"
|
||||
:class="{'mb-2': emails.length}"
|
||||
:placeholder="placeHolder"
|
||||
autocomplete="new-password"
|
||||
ref="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { XIcon } from 'vue-feather-icons'
|
||||
import { events } from '../../bus'
|
||||
|
||||
export default {
|
||||
name: 'MultiEmailInput',
|
||||
components: { XIcon },
|
||||
props: ['isError', 'label'],
|
||||
computed: {
|
||||
placeHolder() {
|
||||
return !this.emails.length ? this.$t('shared_form.email_placeholder') : ''
|
||||
},
|
||||
inputSize() {
|
||||
return this.email && this.email.length > 14 ? this.email.length : 14
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
emails: [],
|
||||
email: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeEmail(email) {
|
||||
this.emails = this.emails.filter((item) => item !== email)
|
||||
|
||||
// After remove email send new emails list to parent
|
||||
events.$emit('emailsInputValues', this.emails)
|
||||
},
|
||||
removeLastEmail(event) {
|
||||
// If is input empty and presse backspace remove last email from array
|
||||
if (event.code === 'Backspace' && this.email === '') this.emails.pop()
|
||||
},
|
||||
handleEmail() {
|
||||
if (! this.email.length)
|
||||
return;
|
||||
|
||||
// Get index of @ and last dot
|
||||
let lastDot = this.email.lastIndexOf('.')
|
||||
let at = this.email.indexOf('@')
|
||||
|
||||
// Check if is after @ some dot, if email have @ anf if dont have more like one
|
||||
if (lastDot < at || at === -1 || this.email.match(/@/g).length > 1) return
|
||||
|
||||
// First email dont need to be separated by comma or space to be sended
|
||||
if (this.emails.length === 0) events.$emit('emailsInputValues', [this.email])
|
||||
|
||||
// After come or backspace push the single email to array or emails
|
||||
if (this.email.includes(',') || this.email.includes(' ')) {
|
||||
let email = this.email.replace(/[","," "]/, '')
|
||||
|
||||
this.email = ''
|
||||
|
||||
// Push single email to aray of emails
|
||||
this.emails.push(email)
|
||||
|
||||
events.$emit('emailsInputValues', this.emails)
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
113
resources/js/components/Inputs/SearchInput.vue
Normal file
113
resources/js/components/Inputs/SearchInput.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<div v-if="!query" class="icon">
|
||||
<search-icon size="19" />
|
||||
</div>
|
||||
<div @click="clearInput" v-if="query" class="icon">
|
||||
<x-icon class="pointer" size="19"></x-icon>
|
||||
</div>
|
||||
<input
|
||||
v-model="query"
|
||||
@input="$emit('input', query)"
|
||||
class="query focus-border-theme"
|
||||
type="text"
|
||||
name="searchInput"
|
||||
:placeholder="$t('search_translations')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SearchIcon, XIcon } from 'vue-feather-icons'
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
components: {
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearInput() {
|
||||
this.query = undefined
|
||||
this.$emit('reset-query')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../sass/vuefilemanager/variables';
|
||||
@import '../../../sass/vuefilemanager/mixins';
|
||||
@import '../../../sass/vuefilemanager/forms';
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
|
||||
input {
|
||||
background: $light_background;
|
||||
border-radius: 8px;
|
||||
outline: 0;
|
||||
padding: 9px 20px 9px 43px;
|
||||
font-weight: 700;
|
||||
@include font-size(16);
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
min-width: 175px;
|
||||
transition: 0.15s all ease;
|
||||
border: 1px solid transparent;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: $light_text;
|
||||
@include font-size(14);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&:focus + .icon {
|
||||
path {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 11px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
circle,
|
||||
line {
|
||||
color: $light_text;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.search-bar {
|
||||
input {
|
||||
background: $dark_mode_foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
85
resources/js/components/Inputs/SelectBoxInput.vue
Normal file
85
resources/js/components/Inputs/SelectBoxInput.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="select-box">
|
||||
<div
|
||||
class="box-item active-bg-theme-100 active-border-theme"
|
||||
:class="{ active: item.value === input }"
|
||||
@click="getSelectedValue(item)"
|
||||
v-for="(item, i) in data"
|
||||
:key="i"
|
||||
>
|
||||
<span class="box-value active-text-theme">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectBoxInput',
|
||||
props: ['data', 'value'],
|
||||
data() {
|
||||
return {
|
||||
input: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getSelectedValue(item) {
|
||||
if (!this.input || this.input !== item.value) this.input = item.value
|
||||
else this.input = undefined
|
||||
|
||||
this.$emit('input', this.input)
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.value) this.input = this.value
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../sass/vuefilemanager/variables';
|
||||
@import '../../../sass/vuefilemanager/mixins';
|
||||
@import '../../../sass/vuefilemanager/inapp-forms';
|
||||
@import '../../../sass/vuefilemanager/forms';
|
||||
|
||||
.select-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.box-item {
|
||||
margin-bottom: 10px;
|
||||
padding: 12px 4px;
|
||||
text-align: center;
|
||||
background: $light_background;
|
||||
border-radius: 8px;
|
||||
font-weight: 700;
|
||||
border: 2px solid $light_background;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
flex-basis: 55px;
|
||||
|
||||
.box-value {
|
||||
@include font-size(15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
.select-box {
|
||||
.box-item {
|
||||
flex-basis: calc(34% - 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.select-box {
|
||||
.box-item {
|
||||
border-color: $dark_mode_border_color;
|
||||
background: lighten($dark_mode_foreground, 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
276
resources/js/components/Inputs/SelectInput.vue
Normal file
276
resources/js/components/Inputs/SelectInput.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="select">
|
||||
<!--Area-->
|
||||
<div
|
||||
class="input-area rounded-lg bg-light-background dark:bg-2x-dark-foreground"
|
||||
:class="{ 'is-active': isOpen, '!border-rose-600': isError }"
|
||||
@click="openMenu"
|
||||
>
|
||||
<!--If is selected-->
|
||||
<div class="selected flex w-full items-center" v-if="selected">
|
||||
<div class="option-icon" v-if="selected.icon">
|
||||
<user-icon v-if="selected.icon === 'user'" size="14" class="vue-feather text-theme" />
|
||||
<edit2-icon v-if="selected.icon === 'user-edit'" size="14" class="vue-feather text-theme" />
|
||||
</div>
|
||||
<span class="option-value inline-block w-full overflow-hidden text-ellipsis whitespace-nowrap pl-2">
|
||||
{{ selected.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!--If is empty-->
|
||||
<div class="not-selected" v-if="!selected">
|
||||
<span class="option-value placehoder">{{ placeholder }}</span>
|
||||
</div>
|
||||
|
||||
<chevron-down-icon size="19" class="chevron" />
|
||||
</div>
|
||||
|
||||
<!--Options-->
|
||||
<transition name="slide-in">
|
||||
<div class="input-options rounded-lg" v-if="isOpen">
|
||||
<div v-if="options.length > 5" class="select-search">
|
||||
<input
|
||||
v-model="query"
|
||||
ref="search"
|
||||
type="text"
|
||||
:placeholder="$te('search_in_list') ? $t('search_in_list') : 'Search in list...'"
|
||||
class="search-input focus-border-theme rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<ul class="option-list">
|
||||
<li class="option-item" @click="selectOption(option)" v-for="(option, i) in optionList" :key="i">
|
||||
<div class="option-icon" v-if="option.icon">
|
||||
<user-icon v-if="option.icon === 'user'" size="14" />
|
||||
<edit2-icon v-if="option.icon === 'user-edit'" size="14" />
|
||||
</div>
|
||||
<span class="option-value">
|
||||
{{ $t(option.label) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ChevronDownIcon, Edit2Icon, UserIcon } from 'vue-feather-icons'
|
||||
import { debounce, omitBy } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'SelectInput',
|
||||
props: ['placeholder', 'options', 'isError', 'default'],
|
||||
components: {
|
||||
Edit2Icon,
|
||||
UserIcon,
|
||||
ChevronDownIcon,
|
||||
},
|
||||
watch: {
|
||||
query: debounce(function (val) {
|
||||
this.searchedResults = omitBy(this.options, (string) => {
|
||||
return !string.label.toLowerCase().includes(val.toLowerCase())
|
||||
})
|
||||
}, 200),
|
||||
},
|
||||
computed: {
|
||||
isSearching() {
|
||||
return this.searchedResults && this.query !== ''
|
||||
},
|
||||
optionList() {
|
||||
return this.isSearching ? this.searchedResults : this.options
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchedResults: undefined,
|
||||
selected: undefined,
|
||||
isOpen: false,
|
||||
query: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectOption(option) {
|
||||
// Emit selected
|
||||
this.$emit('input', option.value)
|
||||
this.$emit('change', option.value)
|
||||
|
||||
// Get selected
|
||||
this.selected = option
|
||||
|
||||
// Close menu
|
||||
this.isOpen = false
|
||||
},
|
||||
openMenu() {
|
||||
this.isOpen = !this.isOpen
|
||||
|
||||
if (this.$refs.search && this.isOpen) {
|
||||
this.$nextTick(() => this.$refs.search.focus())
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.default) this.selected = this.options.find((option) => option.value === this.default)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../sass/vuefilemanager/variables';
|
||||
@import '../../../sass/vuefilemanager/mixins';
|
||||
|
||||
/* TODO: refactor to the tailwind */
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-search {
|
||||
background: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 13px;
|
||||
|
||||
.search-input {
|
||||
border: 1px solid transparent;
|
||||
background: $light_background;
|
||||
@include transition(150ms);
|
||||
@include font-size(14);
|
||||
padding: 13px 20px;
|
||||
appearance: none;
|
||||
font-weight: 700;
|
||||
outline: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.input-options {
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.12);
|
||||
background: white;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
top: 65px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
max-height: 295px;
|
||||
overflow-y: auto;
|
||||
|
||||
.option-item {
|
||||
padding: 13px 20px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $theme;
|
||||
background: $light_background;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-area {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: transparent;
|
||||
justify-content: space-between;
|
||||
@include transition(150ms);
|
||||
align-items: center;
|
||||
padding: 13px 20px;
|
||||
display: flex;
|
||||
outline: 0;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
.chevron {
|
||||
@include transition(150ms);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
//box-shadow: 0 0 7px rgba($theme, 0.3);
|
||||
|
||||
.chevron {
|
||||
@include transform(rotate(180deg));
|
||||
}
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
border-color: $danger;
|
||||
box-shadow: 0 0 7px rgba($danger, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
@include font-size(10);
|
||||
}
|
||||
|
||||
.option-value {
|
||||
@include font-size(14);
|
||||
font-weight: 700;
|
||||
vertical-align: middle;
|
||||
|
||||
&.placehoder {
|
||||
color: rgba($text, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-in-enter-active {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.slide-in-enter /* .list-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.dark {
|
||||
.select-search {
|
||||
background: $dark_mode_foreground;
|
||||
|
||||
.search-input {
|
||||
background: $dark_mode_background;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-wrapper {
|
||||
.input-area {
|
||||
background: lighten($dark_mode_foreground, 3%);
|
||||
}
|
||||
}
|
||||
|
||||
.input-options {
|
||||
background: $dark_mode_foreground;
|
||||
|
||||
.option-item {
|
||||
border-bottom: none;
|
||||
|
||||
&:hover {
|
||||
background: lighten($dark_mode_foreground, 5%);
|
||||
|
||||
.option-icon {
|
||||
path,
|
||||
circle {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-value {
|
||||
&.placehoder {
|
||||
color: $dark_mode_text_secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
resources/js/components/Inputs/SwitchInput.vue
Normal file
103
resources/js/components/Inputs/SwitchInput.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="switch-content">
|
||||
<label class="input-label" v-if="label"> {{ label }}: </label>
|
||||
<small class="input-info" v-if="info">
|
||||
{{ info }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="switch-content text-right">
|
||||
<div class="switch" :class="{ active: state }" @click="changeState">
|
||||
<div class="switch-button"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SwitchInput',
|
||||
props: ['label', 'name', 'state', 'info', 'input', 'isDisabled'],
|
||||
data() {
|
||||
return {
|
||||
isSwitched: undefined,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeState() {
|
||||
if (this.isDisabled) return
|
||||
|
||||
this.isSwitched = !this.isSwitched
|
||||
this.$emit('input', this.isSwitched)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.isSwitched = this.state
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../sass/vuefilemanager/variables';
|
||||
@import '../../../sass/vuefilemanager/mixins';
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.input-label {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.switch-content {
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
border-radius: 50px;
|
||||
display: block;
|
||||
background: #f1f1f5;
|
||||
position: relative;
|
||||
@include transition;
|
||||
|
||||
.switch-button {
|
||||
@include transition;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50px;
|
||||
display: block;
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
box-shadow: 0 2px 4px rgba(37, 38, 94, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.switch-button {
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.switch {
|
||||
background: $dark_mode_foreground;
|
||||
}
|
||||
|
||||
.popup-wrapper {
|
||||
.switch {
|
||||
background: lighten($dark_mode_foreground, 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user