initial
This commit is contained in:
127
layers/ui/components/switch/index.vue
Normal file
127
layers/ui/components/switch/index.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
cn.b(),
|
||||
cn.is('loading', loading),
|
||||
cn.is('checked', checked),
|
||||
cn.is('focused', focused),
|
||||
cn.is('disabled', disabled),
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="cn.e('control')"
|
||||
@click="switchValue"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:class="cn.e('input')"
|
||||
:disabled="disabled"
|
||||
:checked="checked"
|
||||
:value="trueValue"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
@change="handleChange"
|
||||
>
|
||||
<div :class="cn.e('action')">
|
||||
<UiSpinner
|
||||
v-if="loading && !disabled"
|
||||
:class="cn.e('spinner')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
import { isPromise } from '@vue/shared'
|
||||
import { useField } from 'vee-validate'
|
||||
import useClassname from '../../composables/use-classname'
|
||||
|
||||
import UiSpinner from '../spinner/index.vue'
|
||||
|
||||
export interface Props {
|
||||
id: string
|
||||
disabled?: boolean
|
||||
modelValue?: boolean | string | number
|
||||
trueValue?: boolean | string | number
|
||||
falseValue?: boolean | string | number
|
||||
dummy?: boolean
|
||||
loading?: boolean
|
||||
beforeChange?: () => Promise<boolean> | boolean
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'UiSwitch',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
trueValue: true,
|
||||
falseValue: false,
|
||||
dummy: false,
|
||||
modelValue: undefined,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [state: boolean | string | number]
|
||||
change: [state: boolean | string | number]
|
||||
focus: []
|
||||
blur: []
|
||||
}>()
|
||||
|
||||
const cn = useClassname('ui-switch')
|
||||
|
||||
const { checked, handleChange: fieldHandleChange } = useField(
|
||||
toRef(props, 'id'),
|
||||
null,
|
||||
{
|
||||
type: 'checkbox',
|
||||
initialValue: props.modelValue ?? props.falseValue,
|
||||
checkedValue: props.trueValue,
|
||||
uncheckedValue: props.falseValue,
|
||||
syncVModel: true,
|
||||
},
|
||||
)
|
||||
|
||||
const focused = ref(false)
|
||||
|
||||
const handleChange = computed<(e: Event) => void>(() => {
|
||||
return props.dummy ? handleChangeDummy : fieldHandleChange
|
||||
})
|
||||
|
||||
function handleChangeDummy() {
|
||||
emit('change', checked.value ? props.falseValue : props.trueValue)
|
||||
}
|
||||
|
||||
async function switchValue(e: Event) {
|
||||
if (props.disabled || props.loading)
|
||||
return
|
||||
|
||||
const { beforeChange } = props
|
||||
|
||||
if (!beforeChange) {
|
||||
handleChange.value(e)
|
||||
return
|
||||
}
|
||||
|
||||
const shouldChange = beforeChange()
|
||||
|
||||
if (isPromise(shouldChange)) {
|
||||
try {
|
||||
const result = await shouldChange
|
||||
|
||||
result && handleChange.value(e)
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
else if (shouldChange) {
|
||||
handleChange.value(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'styles';
|
||||
</style>
|
||||
93
layers/ui/components/switch/styles.scss
Normal file
93
layers/ui/components/switch/styles.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
@use '../../styles/mixins' as *;
|
||||
@use '../../styles/variables' as *;
|
||||
|
||||
.ui-switch {
|
||||
$self: &;
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
&__control {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
background: var(--switch-background);
|
||||
width: 28px;
|
||||
height: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
outline: none;
|
||||
|
||||
#{$self}.is-loading & {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
#{$self}.is-disabled & {
|
||||
cursor: not-allowed;
|
||||
background-color: var(--switch-disabled-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__action {
|
||||
--action-size: var(--switch-action-size, 16px);
|
||||
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: 0;
|
||||
width: var(--action-size);
|
||||
height: var(--action-size);
|
||||
background: var(--switch-off-action-background);
|
||||
border-radius: 50%;
|
||||
transition: .2s ease;
|
||||
transition-property: background-color, left;
|
||||
|
||||
#{$self}.is-focused &,
|
||||
#{$self}__control:hover & {
|
||||
background: var(--switch-off-action-hover-background);
|
||||
}
|
||||
|
||||
#{$self}.is-checked & {
|
||||
background: var(--switch-on-action-background);
|
||||
left: calc(100% - var(--action-size));
|
||||
}
|
||||
|
||||
#{$self}:not(.is-disabled).is-checked.is-focused &,
|
||||
#{$self}:not(.is-disabled).is-checked #{$self}__control:hover & {
|
||||
background-color: var(--switch-on-action-hover-background);
|
||||
}
|
||||
|
||||
#{$self}.is-disabled & {
|
||||
background-color: var(--switch-off-action-disabled-background);
|
||||
}
|
||||
|
||||
#{$self}.is-disabled.is-checked & {
|
||||
background-color: var(--switch-on-action-disabled-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__spinner {
|
||||
--spinner-size: 12px;
|
||||
--spinner-color: #{$clr-white};
|
||||
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
@include element-variant('switch', '', (
|
||||
'background': $clr-grey-300,
|
||||
'disabled-background': $clr-grey-200,
|
||||
'off-action-background': $clr-grey-400,
|
||||
'off-action-hover-background': $clr-grey-500,
|
||||
'off-action-disabled-background': $clr-grey-300,
|
||||
'on-action-background': $clr-cyan-500,
|
||||
'on-action-hover-background': $clr-cyan-400,
|
||||
'on-action-disabled-background': $clr-cyan-300
|
||||
));
|
||||
}
|
||||
Reference in New Issue
Block a user