Последняя версия с сервера прошлого разработчика
This commit is contained in:
71
resources/js/Shared/Form/Dropdown.vue
Executable file
71
resources/js/Shared/Form/Dropdown.vue
Executable file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<button type="button" @click="show = true">
|
||||
<slot />
|
||||
<teleport to="body">
|
||||
<div v-if="show">
|
||||
<div class="bg-indigo-200 bg-opacity-25" style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998;" @click="show = false" />
|
||||
<div ref="dropdown" style="position: absolute; z-index: 99999;" @click.stop="show = autoClose ? false : true">
|
||||
<slot name="dropdown" />
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popper from 'popper.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-end',
|
||||
},
|
||||
offset: {
|
||||
type: String,
|
||||
default: '0, 10',
|
||||
},
|
||||
boundary: {
|
||||
type: String,
|
||||
default: 'scrollParent',
|
||||
},
|
||||
autoClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(show) {
|
||||
if (show) {
|
||||
this.$nextTick(() => {
|
||||
this.popper = new Popper(this.$el, this.$refs.dropdown, {
|
||||
placement: this.placement,
|
||||
modifiers: {
|
||||
preventOverflow: { boundariesElement: this.boundary, padding: 0},
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: this.offset
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
})
|
||||
} else if (this.popper) {
|
||||
setTimeout(() => this.popper.destroy(), 100)
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.show = false
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
26
resources/js/Shared/Form/DropdownMenu.vue
Executable file
26
resources/js/Shared/Form/DropdownMenu.vue
Executable file
@@ -0,0 +1,26 @@
|
||||
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||
<template>
|
||||
<Menu as="div" class="relative inline-block text-left z-50">
|
||||
<div>
|
||||
<MenuButton class="transition inline-flex items-center justify-center shadow-classic2 rounded-full bg-orange text-white focus:outline-none w-12 h-12">
|
||||
<svg class="h-4 w-4 md:h-5 md:w-5 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
</MenuButton>
|
||||
</div>
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<slot />
|
||||
</transition>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuItems,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
27
resources/js/Shared/Form/DropdownMenuPoint.vue
Executable file
27
resources/js/Shared/Form/DropdownMenuPoint.vue
Executable file
@@ -0,0 +1,27 @@
|
||||
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||
<template>
|
||||
<Menu as="div" v-slot="{ open }" class="relative inline-block text-left z-50">
|
||||
<div>
|
||||
<MenuButton class="transition inline-flex items-center justify-center focus:outline-none ">
|
||||
<svg :class="[open ? 'rotate-90' : 'rotate-180' ,'transform-gpu transition-transform transform w-6 h-6']">
|
||||
<use xlink:href="#more-vertical"></use>
|
||||
</svg>
|
||||
</MenuButton>
|
||||
</div>
|
||||
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<slot />
|
||||
</transition>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Menu, MenuButton } from '@headlessui/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Menu,
|
||||
MenuButton,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
69
resources/js/Shared/Form/FileInput.vue
Executable file
69
resources/js/Shared/Form/FileInput.vue
Executable file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div>
|
||||
<label v-if="label" class="cursor-pointer text-gray-light text-lg mb-2"
|
||||
@click="browse"
|
||||
>{{ label }}:</label>
|
||||
<div :class="{ error: error }">
|
||||
<input ref="file" type="file"
|
||||
:accept="accept" class="hidden"
|
||||
@change="change"
|
||||
>
|
||||
<div v-if="!modelValue" class="py-2">
|
||||
<button type="button" class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
|
||||
@click="browse"
|
||||
>
|
||||
Выбрать файл
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex flex-col justify-center max-w-2xl mt-3 p-2 border rounded-md">
|
||||
<div class="flex flex-col md:flex-row md:items-start md:justify-between p-1">
|
||||
<div class="md:w-5/6 flex-1 flex flex-col pr-1 text-gray">
|
||||
<span class="truncate">{{ modelValue.name }}</span>
|
||||
<span class="text-xs text-gray-light">({{ filesize(modelValue.size) }})</span>
|
||||
</div>
|
||||
<button type="button" class="md:w-1/6 px-1 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
|
||||
@click="remove"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="text-red text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
modelValue: File,
|
||||
label: String,
|
||||
accept: String,
|
||||
error: String,
|
||||
},
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (!value) {
|
||||
this.$refs.file.value = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filesize(size) {
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return (size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||
},
|
||||
browse() {
|
||||
this.$refs.file.click()
|
||||
},
|
||||
change(e) {
|
||||
this.$emit('update:modelValue', e.target.files[0])
|
||||
},
|
||||
remove() {
|
||||
this.$emit('update:modelValue', null)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
104
resources/js/Shared/Form/FileInputMultiple.vue
Executable file
104
resources/js/Shared/Form/FileInputMultiple.vue
Executable file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div>
|
||||
<label v-if="label" class="cursor-pointer text-gray-light text-lg mb-2"
|
||||
@click="browse"
|
||||
>{{ label }}:</label>
|
||||
<div :class="{ error: error }">
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="change"
|
||||
/>
|
||||
<div v-if="!modelValue" class="py-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
|
||||
@click="browse"
|
||||
>
|
||||
Выбрать файлы
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex flex-col justify-center max-w-2xl mt-3 p-2 border rounded-md">
|
||||
<div
|
||||
v-for="(file, index) in modelValue"
|
||||
:key="index"
|
||||
class="flex flex-col md:flex-row md:items-start md:justify-between p-1"
|
||||
>
|
||||
<div class="md:w-5/6 text-sm md:text-base flex-1 pr-2 text-gray flex flex-col">
|
||||
<span class="truncate">{{ file.name }}</span>
|
||||
<span class="text-xs text-gray-light">({{ filesize(file.size) }})</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="md:w-1/6 px-1 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
|
||||
@click="remove(index)"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="text-red text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: FileList,
|
||||
label: String,
|
||||
accept: String,
|
||||
error: String,
|
||||
},
|
||||
emits:['update:modelValue'],
|
||||
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (!value) {
|
||||
this.$refs.file.value = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
filesize(size) {
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024))
|
||||
return (
|
||||
(size / Math.pow(1024, i)).toFixed(2) * 1 +
|
||||
' ' +
|
||||
['B', 'kB', 'MB', 'GB', 'TB'][i]
|
||||
)
|
||||
},
|
||||
browse() {
|
||||
this.$refs.file.click()
|
||||
},
|
||||
change(e) {
|
||||
this.$emit('update:modelValue', e.target.files)
|
||||
},
|
||||
|
||||
remove(file_index) {
|
||||
const dt = new DataTransfer()
|
||||
let files = Array.from(this.$refs.file.files)
|
||||
|
||||
files.map(function (file, index) {
|
||||
if (index !== file_index) {
|
||||
dt.items.add(file)
|
||||
}
|
||||
})
|
||||
if (dt.files.length) {
|
||||
this.$refs.file.files = dt.files
|
||||
this.$emit('update:modelValue', dt.files)
|
||||
} else {
|
||||
this.$refs.file.files = null
|
||||
this.$emit('update:modelValue', null)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
120
resources/js/Shared/Form/FileInputMultipleDecode.vue
Executable file
120
resources/js/Shared/Form/FileInputMultipleDecode.vue
Executable file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<label v-if="label" @click="browse" class="cursor-pointer text-gray-light text-lg mb-2">{{ label }}:</label>
|
||||
<div :class="{ error: error }">
|
||||
<input
|
||||
ref="file"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="change"
|
||||
/>
|
||||
<div v-if="!modelValue" class="py-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-6 py-2 bg-indigo-300 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:outline-none focus:border-transparent rounded-sm text-sm text-white"
|
||||
@click="browse"
|
||||
>
|
||||
Выбрать файлы
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="flex flex-col justify-center max-w-lg mt-3 p-2 border rounded-md">
|
||||
<div
|
||||
v-for="(file, index) in modelValue"
|
||||
:key="index"
|
||||
class="flex items-center justify-between p-1"
|
||||
>
|
||||
<div class="flex-1 pr-1 text-gray">
|
||||
{{ file.name }}
|
||||
<span class="text-xs text-gray-light"
|
||||
>({{ filesize(file.size) }})</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-1 bg-indigo-300 hover:bg-indigo-100 rounded-sm text-xs font-medium text-white"
|
||||
@click="remove(index)"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="error" class="text-red text-sm">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import helper from "@/includes/helper";
|
||||
export default {
|
||||
props: {
|
||||
modelValue: FileList,
|
||||
label: String,
|
||||
accept: String,
|
||||
error: String,
|
||||
},
|
||||
emits: ["fileTime", "loadFileStart", "update:modelValue"],
|
||||
watch: {
|
||||
modelValue(value) {
|
||||
if (!value) {
|
||||
this.$refs.file.value = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
filesize(size) {
|
||||
var i = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (
|
||||
(size / Math.pow(1024, i)).toFixed(2) * 1 +
|
||||
" " +
|
||||
["B", "kB", "MB", "GB", "TB"][i]
|
||||
);
|
||||
},
|
||||
browse() {
|
||||
this.$refs.file.click();
|
||||
},
|
||||
change(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
const that = this;
|
||||
files.map(function (file) {
|
||||
that.$emit("loadFileStart");
|
||||
var audioCtx = new (AudioContext || webkitAudioContext)();
|
||||
var readerAudio = new FileReader();
|
||||
readerAudio.readAsArrayBuffer(file);
|
||||
readerAudio.onload = function (ev) {
|
||||
audioCtx.decodeAudioData(ev.target.result).then(function (buffer) {
|
||||
that.$emit(
|
||||
"fileTime",
|
||||
file.name + "," + that.formatTimeSong(buffer.duration)
|
||||
);
|
||||
});
|
||||
};
|
||||
});
|
||||
this.$emit("update:modelValue", e.target.files);
|
||||
},
|
||||
|
||||
formatTimeSong(value) {
|
||||
return helper.formatTime(value);
|
||||
},
|
||||
|
||||
remove(file_index) {
|
||||
const dt = new DataTransfer();
|
||||
let files = Array.from(this.$refs.file.files);
|
||||
|
||||
files.map(function (file, index) {
|
||||
if (index !== file_index) {
|
||||
dt.items.add(file);
|
||||
}
|
||||
});
|
||||
if (dt.files.length) {
|
||||
this.$refs.file.files = dt.files;
|
||||
this.$emit("update:modelValue", dt.files);
|
||||
} else {
|
||||
this.$refs.file.files = null;
|
||||
this.$emit("update:modelValue", null);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
14
resources/js/Shared/Form/LoadingButton.vue
Executable file
14
resources/js/Shared/Form/LoadingButton.vue
Executable file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<button :disabled="loading">
|
||||
<div v-if="loading" class="btn-spinner mr-2" />
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loading: Boolean,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
57
resources/js/Shared/Form/SearchFilter.vue
Executable file
57
resources/js/Shared/Form/SearchFilter.vue
Executable file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="flex w-full rounded">
|
||||
<dropdown :auto-close="false" class="px-4 md:px-6 border border-indigo-300 rounded-l bg-indigo-200 hover:bg-indigo-100 focus:z-10 focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent" placement="bottom-start">
|
||||
<div class="flex text-gray items-baseline">
|
||||
<span class="text-base hidden md:inline">Фильтр</span>
|
||||
<svg class="w-2 h-2 fill-current md:ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 961.243 599.998">
|
||||
<path d="M239.998 239.999L0 0h961.243L721.246 240c-131.999 132-240.28 240-240.624 239.999-.345-.001-108.625-108.001-240.624-240z" />
|
||||
</svg>
|
||||
</div>
|
||||
<template v-slot:dropdown>
|
||||
<div class="mt-2 px-4 py-6 w-screen shadow-xl bg-indigo-300 rounded-md" :style="{ maxWidth: `${maxWidth}px` }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
</dropdown>
|
||||
|
||||
<div class="flex-1 relative">
|
||||
<div class="absolute inset-y-0 left-3 flex items-center z-[1]">
|
||||
<svg class="flex-shrink-0 h-5 w-5 text-gray-light" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24" fill="currentColor">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11 4a7 7 0 100 14 7 7 0 000-14zm-9 7a9 9 0 1118 0 9 9 0 01-18 0z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M15.943 15.943a1 1 0 011.414 0l4.35 4.35a1 1 0 01-1.414 1.414l-4.35-4.35a1 1 0 010-1.414z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
autocomplete="off"
|
||||
name="search"
|
||||
class="relative w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-r placeholder-gray-light !pl-10 h-14" placeholder="Поиск"
|
||||
type="search">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ml-3 text-sm text-gray focus:text-orange" type="button" @click="$emit('reset')">Сбросить</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropdown from "@/Shared/Form/Dropdown.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Dropdown,
|
||||
},
|
||||
emits: ["update:modelValue", "reset"],
|
||||
props: {
|
||||
modelValue: String,
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
190
resources/js/Shared/Form/TagInput.vue
Executable file
190
resources/js/Shared/Form/TagInput.vue
Executable file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div class="relative" :class="{ 'with-count': showCount }">
|
||||
<input
|
||||
v-model="newTag"
|
||||
type="text"
|
||||
:list="id"
|
||||
placeholder="Теги"
|
||||
class="w-full focus:ring-4 focus:ring-offset-1 focus:ring-orange focus:ring-opacity-20 focus:ring-offset-orange focus:border-transparent text-gray border border-indigo-300 bg-indigo-200 rounded-md"
|
||||
autocomplete="off"
|
||||
:style="{ 'padding-left': `${paddingLeft}px` }"
|
||||
@keydown.prevent.enter="addTag(newTag)"
|
||||
@blur.prevent="addTag(newTag)"
|
||||
/>
|
||||
|
||||
<datalist v-if="options" :id="id">
|
||||
<option v-for="option in availableOptions" :key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</datalist>
|
||||
|
||||
<ul ref="tagsUl" class="tags">
|
||||
<li
|
||||
v-for="(tag, index) in tags"
|
||||
:key="tag"
|
||||
class=""
|
||||
:class="{ 'duplicate-shake': tag === duplicate }"
|
||||
>
|
||||
<span class="inline-flex rounded-full items-center py-0.5 pl-2.5 pr-1 text-sm font-medium bg-orange text-indigo-300">
|
||||
{{ tag }}
|
||||
<button type="button" class="flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-indigo-300 hover:bg-indigo-200 hover:text-white focus:outline-none focus:bg-indigo-200 focus:text-white"
|
||||
@click="removeTag(index)"
|
||||
>
|
||||
<span class="sr-only">Удалить</span>
|
||||
<svg class="h-2 w-2" stroke="currentColor"
|
||||
fill="none" viewBox="0 0 8 8"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-width="1.5"
|
||||
d="M1 1l6 6m0-6L1 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="showCount" class="count">
|
||||
<span>{{ tags.length }}</span> tags
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { toRef, ref, watch, nextTick, onMounted, computed } from 'vue'
|
||||
export default {
|
||||
props: {
|
||||
name: { type: String, default: '' },
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
options: { type: [Array, Boolean], default: false },
|
||||
allowCustom: { type: Boolean, default: true },
|
||||
showCount: { type: Boolean, default: false },
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
// Tags
|
||||
const tags = toRef(props, 'modelValue')
|
||||
|
||||
const newTag = ref('')
|
||||
const id = Math.random().toString(36).substring(7)
|
||||
|
||||
const addTag = (tag) => {
|
||||
if (!tag) return // prevent empty tag
|
||||
// only allow predefined tags when allowCustom is false
|
||||
if (!props.allowCustom && !props.options.includes(tag)) return
|
||||
// return early if duplicate
|
||||
if (tags.value.includes(tag)) {
|
||||
handleDuplicate(tag)
|
||||
return
|
||||
}
|
||||
tags.value.push(tag)
|
||||
newTag.value = '' // reset newTag
|
||||
}
|
||||
const removeTag = (index) => {
|
||||
tags.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// handling duplicates
|
||||
const duplicate = ref(null)
|
||||
const handleDuplicate = (tag) => {
|
||||
duplicate.value = tag
|
||||
setTimeout(() => (duplicate.value = null), 1000)
|
||||
newTag.value = ''
|
||||
}
|
||||
|
||||
// positioning and handling tag change
|
||||
const paddingLeft = ref(10)
|
||||
const tagsUl = ref(null)
|
||||
const onTagsChange = () => {
|
||||
// position cursor
|
||||
const extraCushion = 15
|
||||
paddingLeft.value = tagsUl.value.clientWidth + extraCushion
|
||||
// scroll to end of tags
|
||||
tagsUl.value.scrollTo(tagsUl.value.scrollWidth, 0)
|
||||
// emit value on tags change
|
||||
emit('update:modelValue', tags.value)
|
||||
}
|
||||
watch(tags, () => nextTick(onTagsChange), { deep: true })
|
||||
onMounted(onTagsChange)
|
||||
|
||||
// options
|
||||
const availableOptions = computed(() => {
|
||||
if (!props.options) return false
|
||||
return props.options.filter((option) => !tags.value.includes(option))
|
||||
})
|
||||
|
||||
return {
|
||||
tags,
|
||||
newTag,
|
||||
addTag,
|
||||
removeTag,
|
||||
paddingLeft,
|
||||
tagsUl,
|
||||
availableOptions,
|
||||
id,
|
||||
duplicate,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10px;
|
||||
max-width: 75%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: scale(0.9) translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: scale(0.9) translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: scale(0.9) translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: scale(0.9) translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
.duplicate-shake {
|
||||
animation: shake 1s;
|
||||
}
|
||||
.count {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 10px;
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.count span {
|
||||
background: #eee;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.with-count input {
|
||||
padding-right: 60px;
|
||||
}
|
||||
.with-count ul {
|
||||
max-width: 60%;
|
||||
}
|
||||
</style>
|
||||
45
resources/js/Shared/Form/TextInput.vue
Executable file
45
resources/js/Shared/Form/TextInput.vue
Executable file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<label v-if="label" class="text-gray-light text-lg mb-2"
|
||||
:for="id"
|
||||
>{{ label }}:</label>
|
||||
<input :id="id" ref="input"
|
||||
v-bind="$attrs" :class="{ error: error }"
|
||||
:type="type" :value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<div v-if="error" class="text-red text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default() {
|
||||
return `select-input-${Math.random() * 1000}`
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
modelValue: [String, Number],
|
||||
label: String,
|
||||
error: String,
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
select() {
|
||||
this.$refs.input.select()
|
||||
},
|
||||
setSelectionRange(start, end) {
|
||||
this.$refs.input.setSelectionRange(start, end)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
30
resources/js/Shared/Form/TextareaInput.vue
Executable file
30
resources/js/Shared/Form/TextareaInput.vue
Executable file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<label class="text-gray-light text-lg mb-2" v-if="label" :for="id">{{ label }}:</label>
|
||||
<textarea :id="id" ref="input" v-bind="$attrs" :class="{ error: error }" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
|
||||
<div v-if="error" class="text-red text-sm">{{ error }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default() {
|
||||
return `select-input-${Math.random() * 1000}`;
|
||||
},
|
||||
},
|
||||
modelValue: String,
|
||||
label: String,
|
||||
error: String,
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.input.focus()
|
||||
},
|
||||
select() {
|
||||
this.$refs.input.select()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
50
resources/js/Shared/Form/Toggle.vue
Executable file
50
resources/js/Shared/Form/Toggle.vue
Executable file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<SwitchGroup as="div" :class="{'pointer-events-none': disabled}"
|
||||
class="flex items-center"
|
||||
@click="clicked"
|
||||
>
|
||||
<Switch v-model="enabled" :class="[enabled ? 'bg-orange' : 'bg-indigo-200', 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500']">
|
||||
<!-- <span class="sr-only">Use setting</span> -->
|
||||
<span aria-hidden="true" :class="[enabled ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200']" />
|
||||
</Switch>
|
||||
<SwitchLabel as="span" class="ml-3">
|
||||
<span v-show="!enabled" class="text-sm text-white">{{ textin }}</span>
|
||||
<span v-show="enabled" class="text-sm text-white">{{ textout }}</span>
|
||||
</SwitchLabel>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Switch,
|
||||
SwitchGroup,
|
||||
SwitchLabel,
|
||||
},
|
||||
props: {
|
||||
textin: String,
|
||||
textout: String,
|
||||
user_id: Number,
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
emits: ['clicked', 'prohibited'],
|
||||
methods: {
|
||||
clicked(){
|
||||
if(this.disabled === false){
|
||||
this.$emit('clicked', this.user_id)
|
||||
}else{
|
||||
this.$emit('prohibited')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user