mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 04:29:37 +01:00
feat: rewrite to use app config and rework docs (#143)
Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: Sébastien Chopin <seb@nuxt.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
:id="name"
|
||||
@@ -9,102 +9,90 @@
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
:class="inputClass"
|
||||
:class="[ui.base, ui.custom]"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ml-3 text-sm">
|
||||
<label :for="name" :class="labelClass">
|
||||
<label :for="name" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<p v-if="help" :class="helpClass">
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [Boolean, Array],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.checkbox.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UCheckbox' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [Boolean, Array],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.checkbox>>,
|
||||
default: () => appConfig.ui.checkbox
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.checkbox>>(() => defu({}, props.ui, appConfig.ui.checkbox))
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div v-if="label || $slots.label" :class="labelWrapperClass">
|
||||
<label :for="name" :class="labelClass">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
</label>
|
||||
<span v-if="$slots.hint || hint" :class="hintClass">
|
||||
<slot name="hint">{{ hint }}</slot>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="description" :class="descriptionClass">
|
||||
{{ description }}
|
||||
</p>
|
||||
<div :class="!!label && containerClass">
|
||||
<slot />
|
||||
<p v-if="help" :class="helpClass">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import $ui from '#build/ui'
|
||||
|
||||
defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.wrapper
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.container
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.label
|
||||
},
|
||||
labelWrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.labelWrapper
|
||||
},
|
||||
descriptionClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.description
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.required
|
||||
},
|
||||
hintClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.hint
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.formGroup.help
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UFormGroup' }
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<input
|
||||
:id="name"
|
||||
ref="input"
|
||||
@@ -8,7 +8,7 @@
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || loading"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autocomplete"
|
||||
:spellcheck="spellcheck"
|
||||
@@ -18,176 +18,216 @@
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
<slot />
|
||||
<div v-if="isLeading" :class="iconLeadingWrapperClass">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
<div v-if="isLeading && leadingIconName" :class="leadingIconClass">
|
||||
<Icon :name="leadingIconName" :class="iconClass" />
|
||||
</div>
|
||||
<div v-if="isTrailing" :class="iconTrailingWrapperClass">
|
||||
<Icon v-if="iconName" :name="iconName" :class="iconClass" />
|
||||
<div v-if="isTrailing && trailingIconName" :class="trailingIconClass">
|
||||
<Icon :name="trailingIconName" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
<script lang="ts">
|
||||
import { ref, computed, onMounted, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.loading
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.input.size).includes(value)
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
spellcheck: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
loadingIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.loadingIcon
|
||||
},
|
||||
leadingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailingIcon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
trailing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
leading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.input.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.input.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.input.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.input>>,
|
||||
default: () => appConfig.ui.input
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.input.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.input.appearance).includes(value)
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defu({}, props.ui, appConfig.ui.input))
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
input.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
isLeading.value && ui.value.leading.spacing[props.size],
|
||||
isTrailing.value && ui.value.trailing.spacing[props.size],
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing) || props.leadingIcon
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing) || props.trailingIcon
|
||||
})
|
||||
|
||||
const leadingIconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.leadingIcon || props.icon
|
||||
})
|
||||
|
||||
const trailingIconName = computed(() => {
|
||||
if (props.loading && !isLeading.value) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.trailingIcon || props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isLeading,
|
||||
isTrailing,
|
||||
inputClass,
|
||||
iconClass,
|
||||
leadingIconName,
|
||||
leadingIconClass,
|
||||
trailingIconName,
|
||||
trailingIconClass,
|
||||
onInput
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
input.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const isLeading = computed(() => {
|
||||
return (props.icon && props.leading) || (props.icon && !props.trailing) || (props.loading && !props.trailing)
|
||||
})
|
||||
|
||||
const isTrailing = computed(() => {
|
||||
return (props.icon && props.trailing) || (props.loading && props.trailing)
|
||||
})
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.input.size[props.size],
|
||||
$ui.input.spacing[props.size],
|
||||
$ui.input.appearance[props.appearance],
|
||||
isLeading.value && $ui.input.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconName = computed(() => {
|
||||
if (props.loading) {
|
||||
return props.loadingIcon
|
||||
}
|
||||
|
||||
return props.icon
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.input.icon.size[props.size],
|
||||
isLeading.value && $ui.input.icon.leading.spacing[props.size],
|
||||
isTrailing.value && $ui.input.icon.trailing.spacing[props.size],
|
||||
props.loading && 'animate-spin'
|
||||
)
|
||||
})
|
||||
|
||||
const iconLeadingWrapperClass = $ui.input.icon.leading.wrapper
|
||||
const iconTrailingWrapperClass = $ui.input.icon.trailing.wrapper
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UInput' }
|
||||
</script>
|
||||
|
||||
78
src/runtime/components/forms/InputGroup.vue
Normal file
78
src/runtime/components/forms/InputGroup.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div :class="ui.wrapper">
|
||||
<div v-if="label || $slots.label" :class="ui.labelWrapper">
|
||||
<label :for="name" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<span v-if="$slots.hint || hint" :class="ui.hint">
|
||||
<slot name="hint">{{ hint }}</slot>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="description" :class="ui.description">
|
||||
{{ description }}
|
||||
</p>
|
||||
<div :class="!!label && ui.container">
|
||||
<slot />
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
hint: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.inputGroup>>,
|
||||
default: () => appConfig.ui.inputGroup
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.inputGroup>>(() => defu({}, props.ui, appConfig.ui.inputGroup))
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<div class="flex items-center h-5">
|
||||
<input
|
||||
:id="`${name}-${value}`"
|
||||
@@ -9,102 +9,90 @@
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
type="radio"
|
||||
:class="radioClass"
|
||||
:class="[ui.base, ui.custom]"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)"
|
||||
>
|
||||
</div>
|
||||
<div v-if="label || $slots.label" class="ml-3 text-sm">
|
||||
<label :for="`${name}-${value}`" :class="labelClass">
|
||||
<label :for="`${name}-${value}`" :class="ui.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
<span v-if="required" :class="requiredClass">*</span>
|
||||
<span v-if="required" :class="ui.required">*</span>
|
||||
</label>
|
||||
<p v-if="help" :class="helpClass">
|
||||
<p v-if="help" :class="ui.help">
|
||||
{{ help }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.base
|
||||
},
|
||||
labelClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.label
|
||||
},
|
||||
requiredClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.required
|
||||
},
|
||||
helpClass: {
|
||||
type: String,
|
||||
default: () => $ui.radio.help
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const radioClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'URadio' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.radio>>,
|
||||
default: () => appConfig.ui.radio
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.radio>>(() => defu({}, props.ui, appConfig.ui.radio))
|
||||
|
||||
const isChecked = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
isChecked
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<select
|
||||
:id="name"
|
||||
:name="name"
|
||||
@@ -17,7 +17,7 @@
|
||||
:label="option[textAttribute]"
|
||||
>
|
||||
<option
|
||||
v-for="(childOption, index2) in option.children"
|
||||
v-for="(childOption, index2) in (option.children as any[])"
|
||||
:key="`${childOption[valueAttribute]}-${index}-${index2}`"
|
||||
:value="childOption[valueAttribute]"
|
||||
:selected="childOption[valueAttribute] === normalizedValue"
|
||||
@@ -36,169 +36,197 @@
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div v-if="icon" :class="iconWrapperClass">
|
||||
<div v-if="icon" :class="leadingIconClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</div>
|
||||
|
||||
<span :class="trailingIconClass">
|
||||
<Icon name="i-heroicons-chevron-down-20-solid" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { get } from 'lodash-es'
|
||||
import { classNames } from '../../utils'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.base
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.select.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
|
||||
const guessOptionText = (option: any) => {
|
||||
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
||||
}
|
||||
|
||||
const normalizeOption = (option: any) => {
|
||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||
return {
|
||||
[props.valueAttribute]: option,
|
||||
[props.textAttribute]: option
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
[props.valueAttribute]: guessOptionValue(option),
|
||||
[props.textAttribute]: guessOptionText(option)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedOptions = computed(() => {
|
||||
return props.options.map(option => normalizeOption(option))
|
||||
})
|
||||
|
||||
const normalizedOptionsWithPlaceholder = computed(() => {
|
||||
if (!props.placeholder) {
|
||||
return normalizedOptions.value
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
[props.valueAttribute]: '',
|
||||
[props.textAttribute]: props.placeholder,
|
||||
disabled: true
|
||||
},
|
||||
...normalizedOptions.value
|
||||
]
|
||||
})
|
||||
|
||||
const normalizedValue = computed(() => {
|
||||
const normalizeModelValue = normalizeOption(props.modelValue)
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option[props.valueAttribute] === normalizeModelValue[props.valueAttribute])
|
||||
if (!foundOption) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return foundOption[props.valueAttribute]
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.select.size[props.size],
|
||||
$ui.select.spacing[props.size],
|
||||
$ui.select.appearance[props.appearance],
|
||||
!!props.icon && $ui.select.leading.spacing[props.size],
|
||||
$ui.select.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.select.icon.size[props.size],
|
||||
!!props.icon && $ui.select.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const iconWrapperClass = $ui.select.icon.leading.wrapper
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USelect' }
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { get } from 'lodash-es'
|
||||
import { defu } from 'defu'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Icon
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
valueAttribute: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
|
||||
default: () => appConfig.ui.select
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.ui, appConfig.ui.select))
|
||||
|
||||
const onInput = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
const guessOptionValue = (option: any) => {
|
||||
return get(option, props.valueAttribute, get(option, props.textAttribute))
|
||||
}
|
||||
|
||||
const guessOptionText = (option: any) => {
|
||||
return get(option, props.textAttribute, get(option, props.valueAttribute))
|
||||
}
|
||||
|
||||
const normalizeOption = (option: any) => {
|
||||
if (['string', 'number', 'boolean'].includes(typeof option)) {
|
||||
return {
|
||||
[props.valueAttribute]: option,
|
||||
[props.textAttribute]: option
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...option,
|
||||
[props.valueAttribute]: guessOptionValue(option),
|
||||
[props.textAttribute]: guessOptionText(option)
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedOptions = computed(() => {
|
||||
return props.options.map(option => normalizeOption(option))
|
||||
})
|
||||
|
||||
const normalizedOptionsWithPlaceholder = computed(() => {
|
||||
if (!props.placeholder) {
|
||||
return normalizedOptions.value
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
[props.valueAttribute]: '',
|
||||
[props.textAttribute]: props.placeholder,
|
||||
disabled: true
|
||||
},
|
||||
...normalizedOptions.value
|
||||
]
|
||||
})
|
||||
|
||||
const normalizedValue = computed(() => {
|
||||
const normalizeModelValue = normalizeOption(props.modelValue)
|
||||
const foundOption = normalizedOptionsWithPlaceholder.value.find(option => option[props.valueAttribute] === normalizeModelValue[props.valueAttribute])
|
||||
if (!foundOption) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return foundOption[props.valueAttribute]
|
||||
})
|
||||
|
||||
const selectClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
!!props.icon && ui.value.leading.spacing[props.size],
|
||||
ui.value.trailing.spacing[props.size],
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.base,
|
||||
ui.value.icon.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.leading.wrapper,
|
||||
ui.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.icon.trailing.wrapper,
|
||||
ui.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
normalizedOptionsWithPlaceholder,
|
||||
normalizedValue,
|
||||
selectClass,
|
||||
iconClass,
|
||||
leadingIconClass,
|
||||
trailingIconClass,
|
||||
onInput
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
<template>
|
||||
<Combobox
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:nullable="nullable"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="wrapperClass"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1" aria-hidden="true">
|
||||
|
||||
<ComboboxButton ref="trigger" v-slot="{ disabled: buttonDisabled }" as="div" role="button" class="inline-flex w-full">
|
||||
<slot :open="open" :disabled="buttonDisabled">
|
||||
<button :class="selectCustomClass" :disabled="disabled" type="button">
|
||||
<slot name="label">
|
||||
<span v-if="modelValue" class="block truncate">{{ (modelValue as any)[textAttribute] }}</span>
|
||||
<span v-else class="block truncate u-text-gray-400">{{ placeholder }}</span>
|
||||
</slot>
|
||||
<slot name="icon">
|
||||
<span :class="iconWrapperClass">
|
||||
<Icon v-if="icon" :name="icon" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</slot>
|
||||
</button>
|
||||
</slot>
|
||||
</ComboboxButton>
|
||||
|
||||
<div v-if="open" ref="container" :class="[listContainerClass, listWidthClass]">
|
||||
<transition appear v-bind="listTransitionClass">
|
||||
<ComboboxOptions static :class="listBaseClass">
|
||||
<ComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="listInputClass"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<ComboboxOption
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="resolveOptionClass({ active, selected, disabled: optionDisabled })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<span class="block truncate">{{ option[textAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="resolveOptionIconClass({ active })">
|
||||
<Icon v-if="listOptionIcon" :name="listOptionIcon" :class="listOptionIconSizeClass" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
|
||||
<ComboboxOption v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="resolveOptionClass({ active, selected })">
|
||||
<div :class="listOptionContainerClass">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[textAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="listOptionEmptyClass">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results found for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
</ComboboxOptions>
|
||||
</transition>
|
||||
</div>
|
||||
</Combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
ComboboxInput
|
||||
} from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nullable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Select an option'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.base
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.name
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.icon.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.selectCustom.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
listBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.base
|
||||
},
|
||||
listContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.container
|
||||
},
|
||||
listWidthClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.width
|
||||
},
|
||||
listInputClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.input
|
||||
},
|
||||
listTransitionClass: {
|
||||
type: Object,
|
||||
default: () => $ui.selectCustom.list.transition
|
||||
},
|
||||
listOptionBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.base
|
||||
},
|
||||
listOptionContainerClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.container
|
||||
},
|
||||
listOptionActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.active
|
||||
},
|
||||
listOptionInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.inactive
|
||||
},
|
||||
listOptionSelectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.selected
|
||||
},
|
||||
listOptionUnselectedClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.unselected
|
||||
},
|
||||
listOptionDisabledClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.disabled
|
||||
},
|
||||
listOptionEmptyClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.empty
|
||||
},
|
||||
listOptionIcon: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.name
|
||||
},
|
||||
listOptionIconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.base
|
||||
},
|
||||
listOptionIconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.active
|
||||
},
|
||||
listOptionIconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.inactive
|
||||
},
|
||||
listOptionIconSizeClass: {
|
||||
type: String,
|
||||
default: () => $ui.selectCustom.list.option.icon.size
|
||||
},
|
||||
textAttribute: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
popperOptions: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'open', 'close'])
|
||||
|
||||
const popperOptions = computed<PopperOptions>(() => defu({}, props.popperOptions, $ui.selectCustom.popperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popperOptions.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectCustomClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.selectCustom.size[props.size],
|
||||
$ui.selectCustom.spacing[props.size],
|
||||
$ui.selectCustom.appearance[props.appearance],
|
||||
$ui.selectCustom.trailing.spacing[props.size],
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
props.iconBaseClass,
|
||||
$ui.selectCustom.icon.size[props.size],
|
||||
'mr-2'
|
||||
)
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
query.value === ''
|
||||
? props.options
|
||||
: (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.textAttribute]).some((searchAttribute: any) => {
|
||||
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.textAttribute]: query.value }
|
||||
})
|
||||
|
||||
const iconWrapperClass = classNames(
|
||||
$ui.selectCustom.icon.trailing.wrapper
|
||||
)
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
function resolveOptionClass ({ active, selected, disabled }: { active: boolean, selected: boolean, disabled?: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionBaseClass,
|
||||
active ? props.listOptionActiveClass : props.listOptionInactiveClass,
|
||||
selected ? props.listOptionSelectedClass : props.listOptionUnselectedClass,
|
||||
disabled && props.listOptionDisabledClass
|
||||
)
|
||||
}
|
||||
|
||||
function resolveOptionIconClass ({ active }: { active: boolean }) {
|
||||
return classNames(
|
||||
props.listOptionIconBaseClass,
|
||||
active ? props.listOptionIconActiveClass : props.listOptionIconInactiveClass
|
||||
)
|
||||
}
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'USelectCustom' }
|
||||
</script>
|
||||
329
src/runtime/components/forms/SelectMenu.vue
Normal file
329
src/runtime/components/forms/SelectMenu.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<component
|
||||
:is="searchable ? 'Combobox' : 'Listbox'"
|
||||
v-slot="{ open }"
|
||||
:by="by"
|
||||
:name="name"
|
||||
:model-value="modelValue"
|
||||
:multiple="multiple"
|
||||
:disabled="disabled"
|
||||
as="div"
|
||||
:class="ui.wrapper"
|
||||
@update:model-value="onUpdate"
|
||||
>
|
||||
<!-- TODO: check that `name` fixes required -->
|
||||
<!-- <input :value="modelValue" :required="required" class="absolute inset-0 w-px opacity-0 cursor-default" tabindex="-1" aria-hidden="true"> -->
|
||||
|
||||
<component
|
||||
:is="searchable ? 'ComboboxButton' : 'ListboxButton'"
|
||||
ref="trigger"
|
||||
as="div"
|
||||
role="button"
|
||||
class="inline-flex w-full"
|
||||
>
|
||||
<slot :open="open" :disabled="disabled">
|
||||
<button :class="selectMenuClass" :disabled="disabled" type="button">
|
||||
<span v-if="icon" :class="leadingIconClass">
|
||||
<Icon :name="icon" :class="iconClass" />
|
||||
</span>
|
||||
|
||||
<slot name="label">
|
||||
<span v-if="modelValue" class="block truncate">{{ typeof modelValue === 'string' ? modelValue : (modelValue as any)[optionAttribute] }}</span>
|
||||
<span v-else class="block truncate text-gray-400 dark:text-gray-500">{{ placeholder || ' ' }}</span>
|
||||
</slot>
|
||||
|
||||
<span :class="trailingIconClass">
|
||||
<Icon name="i-heroicons-chevron-down-20-solid" :class="iconClass" aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</slot>
|
||||
</component>
|
||||
|
||||
<div v-if="open" ref="container" :class="[ui.container, ui.width]">
|
||||
<transition v-bind="ui.transition">
|
||||
<component :is="searchable ? 'ComboboxOptions' : 'ListboxOptions'" static :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.spacing, ui.height]">
|
||||
<ComboboxInput
|
||||
v-if="searchable"
|
||||
ref="searchInput"
|
||||
:display-value="() => query"
|
||||
name="q"
|
||||
placeholder="Search..."
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
:class="ui.input"
|
||||
@change="query = $event.target.value"
|
||||
/>
|
||||
<component
|
||||
:is="searchable ? 'ComboboxOption' : 'ListboxOption'"
|
||||
v-for="(option, index) in filteredOptions"
|
||||
v-slot="{ active, selected, disabled: optionDisabled }"
|
||||
:key="index"
|
||||
as="template"
|
||||
:value="option"
|
||||
:disabled="option.disabled"
|
||||
>
|
||||
<li :class="resolveOptionClass({ active, disabled: optionDisabled })">
|
||||
<div :class="ui.option.container">
|
||||
<slot name="option" :option="option" :active="active" :selected="selected">
|
||||
<Icon v-if="option.icon" :name="option.icon" :class="[ui.option.icon.base, active ? ui.option.icon.active : ui.option.icon.inactive, option.iconClass]" aria-hidden="true" />
|
||||
<Avatar
|
||||
v-else-if="option.avatar"
|
||||
v-bind="{ size: ui.option.avatar.size, ...option.avatar }"
|
||||
:class="ui.option.avatar.base"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else-if="option.chip" :class="ui.option.chip.base" :style="{ background: `#${option.chip}` }" />
|
||||
|
||||
<span class="truncate">{{ typeof option === 'string' ? option : option[optionAttribute] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<span v-if="selected" :class="ui.option.selected.wrapper">
|
||||
<Icon :name="selectedIcon" :class="ui.option.selected.icon" aria-hidden="true" />
|
||||
</span>
|
||||
</li>
|
||||
</component>
|
||||
|
||||
<component :is="searchable ? 'ComboboxOption' : 'ListboxOption'" v-if="creatable && queryOption && !filteredOptions.length" v-slot="{ active, selected }" :value="queryOption" as="template">
|
||||
<li :class="resolveOptionClass({ active })">
|
||||
<div :class="ui.option.container">
|
||||
<slot name="option-create" :option="queryOption" :active="active" :selected="selected">
|
||||
<span class="block truncate">Create "{{ queryOption[optionAttribute] }}"</span>
|
||||
</slot>
|
||||
</div>
|
||||
</li>
|
||||
</component>
|
||||
<p v-else-if="searchable && query && !filteredOptions.length" :class="ui.option.empty">
|
||||
<slot name="option-empty" :query="query">
|
||||
No results found for "{{ query }}".
|
||||
</slot>
|
||||
</p>
|
||||
</component>
|
||||
</transition>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed, watch, defineComponent } from 'vue'
|
||||
import type { PropType, ComponentPublicInstance } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Combobox, ComboboxButton, ComboboxOptions, ComboboxOption, ComboboxInput, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import Avatar from '../elements/Avatar.vue'
|
||||
import { classNames } from '../../utils'
|
||||
import { usePopper } from '../../composables/usePopper'
|
||||
import type { PopperOptions } from '../../types'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
ComboboxOptions,
|
||||
ComboboxOption,
|
||||
ComboboxInput,
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOptions,
|
||||
ListboxOption,
|
||||
Icon,
|
||||
Avatar
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
by: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
options: {
|
||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||
default: () => []
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
selectedIcon: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.selectMenu.default.selectedIcon
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.select.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.select.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
optionAttribute: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
},
|
||||
searchAttributes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
popper: {
|
||||
type: Object as PropType<PopperOptions>,
|
||||
default: () => ({})
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.selectMenu>>,
|
||||
default: () => appConfig.ui.selectMenu
|
||||
},
|
||||
uiSelect: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.select>>,
|
||||
default: () => appConfig.ui.select
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'open', 'close'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.selectMenu>>(() => defu({}, props.ui, appConfig.ui.selectMenu))
|
||||
const uiSelect = computed<Partial<typeof appConfig.ui.select>>(() => defu({}, props.uiSelect, appConfig.ui.select))
|
||||
|
||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, ui.value.popper as PopperOptions))
|
||||
|
||||
const [trigger, container] = usePopper(popper.value)
|
||||
|
||||
const query = ref('')
|
||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||
|
||||
const selectMenuClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.base,
|
||||
'text-left cursor-default',
|
||||
uiSelect.value.size[props.size],
|
||||
uiSelect.value.gap[props.size],
|
||||
uiSelect.value.spacing[props.size],
|
||||
uiSelect.value.appearance[props.appearance],
|
||||
!!props.icon && uiSelect.value.leading.spacing[props.size],
|
||||
uiSelect.value.trailing.spacing[props.size],
|
||||
uiSelect.value.custom,
|
||||
'inline-flex items-center'
|
||||
)
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.base,
|
||||
uiSelect.value.icon.size[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const leadingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.leading.wrapper,
|
||||
uiSelect.value.icon.leading.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const trailingIconClass = computed(() => {
|
||||
return classNames(
|
||||
uiSelect.value.icon.trailing.wrapper,
|
||||
uiSelect.value.icon.trailing.spacing[props.size]
|
||||
)
|
||||
})
|
||||
|
||||
const filteredOptions = computed(() =>
|
||||
query.value === ''
|
||||
? props.options
|
||||
: (props.options as any[]).filter((option: any) => {
|
||||
return (props.searchAttributes?.length ? props.searchAttributes : [props.optionAttribute]).some((searchAttribute: any) => {
|
||||
return typeof option === 'string' ? option.search(new RegExp(query.value, 'i')) !== -1 : (option[searchAttribute] && option[searchAttribute].search(new RegExp(query.value, 'i')) !== -1)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const queryOption = computed(() => {
|
||||
return query.value === '' ? null : { [props.optionAttribute]: query.value }
|
||||
})
|
||||
|
||||
watch(container, (value) => {
|
||||
if (value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
}
|
||||
})
|
||||
|
||||
function resolveOptionClass ({ active, disabled }: { active: boolean, disabled?: boolean }) {
|
||||
return classNames(
|
||||
ui.value.option.base,
|
||||
active ? ui.value.option.active : ui.value.option.inactive,
|
||||
disabled && ui.value.option.disabled
|
||||
)
|
||||
}
|
||||
|
||||
function onUpdate (event: any) {
|
||||
if (query.value && searchInput.value?.$el) {
|
||||
query.value = ''
|
||||
// explicitly set input text because `ComboboxInput` `displayValue` is not reactive
|
||||
searchInput.value.$el.value = ''
|
||||
}
|
||||
emit('update:modelValue', event)
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
trigger,
|
||||
container,
|
||||
selectMenuClass,
|
||||
iconClass,
|
||||
leadingIconClass,
|
||||
trailingIconClass,
|
||||
filteredOptions,
|
||||
queryOption,
|
||||
query,
|
||||
resolveOptionClass,
|
||||
onUpdate
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="wrapperClass">
|
||||
<div :class="ui.wrapper">
|
||||
<textarea
|
||||
:id="name"
|
||||
ref="textarea"
|
||||
@@ -18,141 +18,150 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { classNames } from '../../utils'
|
||||
import $ui from '#build/ui'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoresize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.textarea.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator (value: string) {
|
||||
return Object.keys($ui.textarea.size).includes(value)
|
||||
}
|
||||
},
|
||||
wrapperClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.wrapper
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.textarea.base
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
if (props.autoresize) {
|
||||
if (!textarea.value) {
|
||||
return
|
||||
}
|
||||
|
||||
textarea.value.rows = props.rows
|
||||
|
||||
const styles = window.getComputedStyle(textarea.value)
|
||||
const paddingTop = parseInt(styles.paddingTop)
|
||||
const paddingBottom = parseInt(styles.paddingBottom)
|
||||
const padding = paddingTop + paddingBottom
|
||||
const lineHeight = parseInt(styles.lineHeight)
|
||||
const { scrollHeight } = textarea.value
|
||||
const newRows = (scrollHeight - padding) / lineHeight
|
||||
|
||||
if (newRows > props.rows) {
|
||||
textarea.value.rows = newRows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
autoResize()
|
||||
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
autoResize()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const textareaClass = computed(() => {
|
||||
return classNames(
|
||||
props.baseClass,
|
||||
$ui.textarea.size[props.size],
|
||||
$ui.textarea.spacing[props.size],
|
||||
$ui.textarea.appearance[props.appearance],
|
||||
!props.resize && 'resize-none',
|
||||
props.customClass
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UTextarea' }
|
||||
import { ref, computed, watch, onMounted, nextTick, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { classNames } from '../../utils'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoresize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.textarea.default.size,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.textarea.size).includes(value)
|
||||
}
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
default: () => appConfig.ui.textarea.default.appearance,
|
||||
validator (value: string) {
|
||||
return Object.keys(appConfig.ui.textarea.appearance).includes(value)
|
||||
}
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.textarea>>,
|
||||
default: () => appConfig.ui.textarea
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'focus', 'blur'],
|
||||
setup (props, { emit }) {
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defu({}, props.ui, appConfig.ui.textarea))
|
||||
|
||||
const autoFocus = () => {
|
||||
if (props.autofocus) {
|
||||
textarea.value?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const autoResize = () => {
|
||||
if (props.autoresize) {
|
||||
if (!textarea.value) {
|
||||
return
|
||||
}
|
||||
|
||||
textarea.value.rows = props.rows
|
||||
|
||||
const styles = window.getComputedStyle(textarea.value)
|
||||
const paddingTop = parseInt(styles.paddingTop)
|
||||
const paddingBottom = parseInt(styles.paddingBottom)
|
||||
const padding = paddingTop + paddingBottom
|
||||
const lineHeight = parseInt(styles.lineHeight)
|
||||
const { scrollHeight } = textarea.value
|
||||
const newRows = (scrollHeight - padding) / lineHeight
|
||||
|
||||
if (newRows > props.rows) {
|
||||
textarea.value.rows = newRows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = (value: string) => {
|
||||
autoResize()
|
||||
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
nextTick(autoResize)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
autoFocus()
|
||||
autoResize()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
const textareaClass = computed(() => {
|
||||
return classNames(
|
||||
ui.value.base,
|
||||
ui.value.size[props.size],
|
||||
ui.value.spacing[props.size],
|
||||
ui.value.appearance[props.appearance],
|
||||
!props.resize && 'resize-none',
|
||||
ui.value.custom
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
textareaClass,
|
||||
onInput
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,96 +1,77 @@
|
||||
<template>
|
||||
<Switch
|
||||
v-model="active"
|
||||
:class="[active ? activeClass : inactiveClass, baseClass]"
|
||||
:class="[active ? ui.active : ui.inactive, ui.base]"
|
||||
>
|
||||
<span :class="[active ? containerActiveClass : containerInactiveClass, containerBaseClass]">
|
||||
<span v-if="iconOn" :class="[active ? iconActiveClass : iconInactiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOn" :class="iconOnClass" />
|
||||
<span :class="[active ? ui.container.active : ui.container.inactive, ui.container.base]">
|
||||
<span v-if="iconOn" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||
<Icon :name="iconOn" :class="ui.icon.on" />
|
||||
</span>
|
||||
<span v-if="iconOff" :class="[active ? iconInactiveClass : iconActiveClass, iconBaseClass]" aria-hidden="true">
|
||||
<Icon :name="iconOff" :class="iconOffClass" />
|
||||
<span v-if="iconOff" :class="[active ? ui.icon.active : ui.icon.inactive, ui.icon.base]" aria-hidden="true">
|
||||
<Icon :name="iconOff" :class="ui.icon.off" />
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { Switch } from '@headlessui/vue'
|
||||
import Icon from '../elements/Icon.vue'
|
||||
import $ui from '#build/ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
// TODO: Remove
|
||||
// @ts-expect-error
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconOff: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
baseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.base
|
||||
},
|
||||
activeClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.active
|
||||
},
|
||||
inactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.inactive
|
||||
},
|
||||
containerBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.base
|
||||
},
|
||||
containerActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.active
|
||||
},
|
||||
containerInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.container.inactive
|
||||
},
|
||||
iconBaseClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.base
|
||||
},
|
||||
iconActiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.active
|
||||
},
|
||||
iconInactiveClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.inactive
|
||||
},
|
||||
iconOnClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.on
|
||||
},
|
||||
iconOffClass: {
|
||||
type: String,
|
||||
default: () => $ui.toggle.icon.off
|
||||
}
|
||||
})
|
||||
// const appConfig = useAppConfig()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
export default defineComponent({
|
||||
components: {
|
||||
// eslint-disable-next-line vue/no-reserved-component-names
|
||||
Switch,
|
||||
Icon
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconOff: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
ui: {
|
||||
type: Object as PropType<Partial<typeof appConfig.ui.toggle>>,
|
||||
default: () => appConfig.ui.toggle
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup (props, { emit }) {
|
||||
// TODO: Remove
|
||||
const appConfig = useAppConfig()
|
||||
|
||||
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defu({}, props.ui, appConfig.ui.toggle))
|
||||
|
||||
const active = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
ui,
|
||||
active
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { name: 'UToggle' }
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user