diff options
Diffstat (limited to 'packages/client/src/components/form')
| -rw-r--r-- | packages/client/src/components/form/checkbox.vue | 143 | ||||
| -rw-r--r-- | packages/client/src/components/form/folder.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/components/form/group.vue | 36 | ||||
| -rw-r--r-- | packages/client/src/components/form/input.vue | 264 | ||||
| -rw-r--r-- | packages/client/src/components/form/radio.vue | 20 | ||||
| -rw-r--r-- | packages/client/src/components/form/radios.vue | 32 | ||||
| -rw-r--r-- | packages/client/src/components/form/range.vue | 256 | ||||
| -rw-r--r-- | packages/client/src/components/form/select.vue | 272 | ||||
| -rw-r--r-- | packages/client/src/components/form/split.vue | 4 | ||||
| -rw-r--r-- | packages/client/src/components/form/switch.vue | 48 |
10 files changed, 536 insertions, 543 deletions
diff --git a/packages/client/src/components/form/checkbox.vue b/packages/client/src/components/form/checkbox.vue new file mode 100644 index 0000000000..fadb770aee --- /dev/null +++ b/packages/client/src/components/form/checkbox.vue @@ -0,0 +1,143 @@ +<template> +<div + class="ziffeoms" + :class="{ disabled, checked }" +> + <input + ref="input" + type="checkbox" + :disabled="disabled" + @keydown.enter="toggle" + > + <span ref="button" v-adaptive-border v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle"> + <i class="check fas fa-check"></i> + </span> + <span class="label"> + <!-- TODO: 無名slotの方は廃止 --> + <span @click="toggle"><slot name="label"></slot><slot></slot></span> + <p class="caption"><slot name="caption"></slot></p> + </span> +</div> +</template> + +<script lang="ts" setup> +import { toRefs, Ref } from 'vue'; +import * as os from '@/os'; +import Ripple from '@/components/ripple.vue'; + +const props = defineProps<{ + modelValue: boolean | Ref<boolean>; + disabled?: boolean; +}>(); + +const emit = defineEmits<{ + (ev: 'update:modelValue', v: boolean): void; +}>(); + +let button = $ref<HTMLElement>(); +const checked = toRefs(props).modelValue; +const toggle = () => { + if (props.disabled) return; + emit('update:modelValue', !checked.value); + + if (!checked.value) { + const rect = button.getBoundingClientRect(); + const x = rect.left + (button.offsetWidth / 2); + const y = rect.top + (button.offsetHeight / 2); + os.popup(Ripple, { x, y, particle: false }, {}, 'end'); + } +}; +</script> + +<style lang="scss" scoped> +.ziffeoms { + position: relative; + display: flex; + transition: all 0.2s ease; + + > * { + user-select: none; + } + + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + margin: 0; + } + + > .button { + position: relative; + display: inline-flex; + flex-shrink: 0; + margin: 0; + box-sizing: border-box; + width: 23px; + height: 23px; + outline: none; + background: var(--panel); + border: solid 1px var(--panel); + border-radius: 4px; + cursor: pointer; + transition: inherit; + + > .check { + margin: auto; + opacity: 0; + color: var(--fgOnAccent); + font-size: 13px; + transform: scale(0.5); + transition: all 0.2s ease; + } + } + + &:hover { + > .button { + border-color: var(--inputBorderHover) !important; + } + } + + > .label { + margin-left: 12px; + margin-top: 2px; + display: block; + transition: inherit; + color: var(--fg); + + > span { + display: block; + line-height: 20px; + cursor: pointer; + transition: inherit; + } + + > .caption { + margin: 8px 0 0 0; + color: var(--fgTransparentWeak); + font-size: 0.85em; + + &:empty { + display: none; + } + } + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.checked { + > .button { + background-color: var(--accent) !important; + border-color: var(--accent) !important; + + > .check { + opacity: 1; + transform: scale(1); + } + } + } +} +</style> diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue index 1b960657d7..a9d8bd97b8 100644 --- a/packages/client/src/components/form/folder.vue +++ b/packages/client/src/components/form/folder.vue @@ -9,13 +9,13 @@ <i v-else class="fas fa-angle-down icon"></i> </span> </div> - <keep-alive> + <KeepAlive> <div v-if="openedAtLeastOnce" v-show="opened" class="body"> <MkSpacer :margin-min="14" :margin-max="22"> <slot></slot> </MkSpacer> </div> - </keep-alive> + </KeepAlive> </div> </template> diff --git a/packages/client/src/components/form/group.vue b/packages/client/src/components/form/group.vue deleted file mode 100644 index 1e8376ca44..0000000000 --- a/packages/client/src/components/form/group.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> -<div v-sticky-container class="adfeebaf _formBlock"> - <div class="label"><slot name="label"></slot></div> - <div class="main _formRoot"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts"> -import { defineComponent } from 'vue'; - -export default defineComponent({ -}); -</script> - -<style lang="scss" scoped> -.adfeebaf { - padding: 24px 24px; - border: solid 1px var(--divider); - border-radius: var(--radius); - - > .label { - font-weight: bold; - padding: 0 0 16px 0; - - &:empty { - display: none; - } - } - - > .main { - - } -} -</style> diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index 7165671af3..ec1ad20de3 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -3,7 +3,8 @@ <div class="label" @click="focus"><slot name="label"></slot></div> <div class="input" :class="{ inline, disabled, focused }"> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> - <input ref="inputEl" + <input + ref="inputEl" v-model="v" v-adaptive-border :type="type" @@ -32,176 +33,118 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; -import MkButton from '@/components/ui/button.vue'; +<script lang="ts" setup> +import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; +import MkButton from '@/components/ui/button.vue'; +import { useInterval } from '@/scripts/use-interval'; -export default defineComponent({ - components: { - MkButton, - }, - - props: { - modelValue: { - required: true - }, - type: { - type: String, - required: false - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - pattern: { - type: String, - required: false - }, - placeholder: { - type: String, - required: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - autocomplete: { - required: false - }, - spellcheck: { - required: false - }, - step: { - required: false - }, - datalist: { - type: Array, - required: false, - }, - inline: { - type: Boolean, - required: false, - default: false - }, - debounce: { - type: Boolean, - required: false, - default: false - }, - manualSave: { - type: Boolean, - required: false, - default: false - }, - }, - - emits: ['change', 'keydown', 'enter', 'update:modelValue'], - - setup(props, context) { - const { modelValue, type, autofocus } = toRefs(props); - const v = ref(modelValue.value); - const id = Math.random().toString(); // TODO: uuid? - const focused = ref(false); - const changed = ref(false); - const invalid = ref(false); - const filled = computed(() => v.value !== '' && v.value != null); - const inputEl = ref<HTMLElement>(); - const prefixEl = ref<HTMLElement>(); - const suffixEl = ref<HTMLElement>(); +const props = defineProps<{ + modelValue: string | number; + type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time'; + required?: boolean; + readonly?: boolean; + disabled?: boolean; + pattern?: string; + placeholder?: string; + autofocus?: boolean; + autocomplete?: boolean; + spellcheck?: boolean; + step?: any; + datalist?: string[]; + inline?: boolean; + debounce?: boolean; + manualSave?: boolean; + small?: boolean; + large?: boolean; +}>(); - const focus = () => inputEl.value.focus(); - const onInput = (ev) => { - changed.value = true; - context.emit('change', ev); - }; - const onKeydown = (ev: KeyboardEvent) => { - context.emit('keydown', ev); +const emit = defineEmits<{ + (ev: 'change', _ev: KeyboardEvent): void; + (ev: 'keydown', _ev: KeyboardEvent): void; + (ev: 'enter'): void; + (ev: 'update:modelValue', value: string | number): void; +}>(); - if (ev.code === 'Enter') { - context.emit('enter'); - } - }; +const { modelValue, type, autofocus } = toRefs(props); +const v = ref(modelValue.value); +const id = Math.random().toString(); // TODO: uuid? +const focused = ref(false); +const changed = ref(false); +const invalid = ref(false); +const filled = computed(() => v.value !== '' && v.value != null); +const inputEl = ref<HTMLElement>(); +const prefixEl = ref<HTMLElement>(); +const suffixEl = ref<HTMLElement>(); +const height = + props.small ? 38 : + props.large ? 42 : + 40; - const updated = () => { - changed.value = false; - if (type?.value === 'number') { - context.emit('update:modelValue', parseFloat(v.value)); - } else { - context.emit('update:modelValue', v.value); - } - }; +const focus = () => inputEl.value.focus(); +const onInput = (ev: KeyboardEvent) => { + changed.value = true; + emit('change', ev); +}; +const onKeydown = (ev: KeyboardEvent) => { + emit('keydown', ev); - const debouncedUpdated = debounce(1000, updated); + if (ev.code === 'Enter') { + emit('enter'); + } +}; - watch(modelValue, newValue => { - v.value = newValue; - }); +const updated = () => { + changed.value = false; + if (type.value === 'number') { + emit('update:modelValue', parseFloat(v.value)); + } else { + emit('update:modelValue', v.value); + } +}; - watch(v, newValue => { - if (!props.manualSave) { - if (props.debounce) { - debouncedUpdated(); - } else { - updated(); - } - } +const debouncedUpdated = debounce(1000, updated); - invalid.value = inputEl.value.validity.badInput; - }); +watch(modelValue, newValue => { + v.value = newValue; +}); - onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } +watch(v, newValue => { + if (!props.manualSave) { + if (props.debounce) { + debouncedUpdated(); + } else { + updated(); + } + } - // このコンポーネントが作成された時、非表示状態である場合がある - // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する - const clock = window.setInterval(() => { - if (prefixEl.value) { - if (prefixEl.value.offsetWidth) { - inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; - } - } - if (suffixEl.value) { - if (suffixEl.value.offsetWidth) { - inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px'; - } - } - }, 100); + invalid.value = inputEl.value.validity.badInput; +}); - onUnmounted(() => { - window.clearInterval(clock); - }); - }); - }); +// このコンポーネントが作成された時、非表示状態である場合がある +// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する +useInterval(() => { + if (prefixEl.value) { + if (prefixEl.value.offsetWidth) { + inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; + } + } + if (suffixEl.value) { + if (suffixEl.value.offsetWidth) { + inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px'; + } + } +}, 100, { + immediate: true, + afterMounted: true, +}); - return { - id, - v, - focused, - invalid, - changed, - filled, - inputEl, - prefixEl, - suffixEl, - focus, - onInput, - onKeydown, - updated, - }; - }, +onMounted(() => { + nextTick(() => { + if (autofocus.value) { + focus(); + } + }); }); </script> @@ -228,14 +171,13 @@ export default defineComponent({ } > .input { - $height: 42px; position: relative; > input { appearance: none; -webkit-appearance: none; display: block; - height: $height; + height: v-bind("height + 'px'"); width: 100%; margin: 0; padding: 0 12px; @@ -265,7 +207,7 @@ export default defineComponent({ top: 0; padding: 0 12px; font-size: 1em; - height: $height; + height: v-bind("height + 'px'"); pointer-events: none; &:empty { diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue index 2becbec6f3..b4d39507e3 100644 --- a/packages/client/src/components/form/radio.vue +++ b/packages/client/src/components/form/radio.vue @@ -7,7 +7,8 @@ :aria-disabled="disabled" @click="toggle" > - <input type="radio" + <input + type="radio" :disabled="disabled" > <span class="button"> @@ -23,27 +24,27 @@ import { defineComponent } from 'vue'; export default defineComponent({ props: { modelValue: { - required: false + required: false, }, value: { - required: false + required: false, }, disabled: { type: Boolean, - default: false - } + default: false, + }, }, computed: { checked(): boolean { return this.modelValue === this.value; - } + }, }, methods: { toggle() { if (this.disabled) return; this.$emit('update:modelValue', this.value); - } - } + }, + }, }); </script> @@ -53,7 +54,8 @@ export default defineComponent({ display: inline-block; text-align: left; cursor: pointer; - padding: 10px 12px; + padding: 9px 12px; + min-width: 60px; background-color: var(--panel); background-clip: padding-box !important; border: solid 1px var(--panel); diff --git a/packages/client/src/components/form/radios.vue b/packages/client/src/components/form/radios.vue index a52acae9e1..bde4a8fb00 100644 --- a/packages/client/src/components/form/radios.vue +++ b/packages/client/src/components/form/radios.vue @@ -4,11 +4,11 @@ import MkRadio from './radio.vue'; export default defineComponent({ components: { - MkRadio + MkRadio, }, props: { modelValue: { - required: false + required: false, }, }, data() { @@ -19,7 +19,7 @@ export default defineComponent({ watch: { value() { this.$emit('update:modelValue', this.value); - } + }, }, render() { let options = this.$slots.default(); @@ -30,25 +30,25 @@ export default defineComponent({ if (options.length === 1 && options[0].props == null) options = options[0].children; return h('div', { - class: 'novjtcto' + class: 'novjtcto', }, [ ...(label ? [h('div', { - class: 'label' + class: 'label', }, [label])] : []), h('div', { - class: 'body' + class: 'body', }, options.map(option => h(MkRadio, { - key: option.key, - value: option.props.value, - modelValue: this.value, - 'onUpdate:modelValue': value => this.value = value, - }, option.children)), + key: option.key, + value: option.props.value, + modelValue: this.value, + 'onUpdate:modelValue': value => this.value = value, + }, option.children)), ), ...(caption ? [h('div', { - class: 'caption' + class: 'caption', }, [caption])] : []), ]); - } + }, }); </script> @@ -65,9 +65,9 @@ export default defineComponent({ } > .body { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - grid-gap: 12px; + display: flex; + gap: 12px; + flex-wrap: wrap; } > .caption { diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index 07f2c23124..ebec482d84 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -1,164 +1,142 @@ <template> <div class="timctyfi" :class="{ disabled }"> <div class="label"><slot name="label"></slot></div> - <div v-panel class="body"> + <div v-adaptive-border class="body"> <div ref="containerEl" class="container"> <div class="track"> - <div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div> + <div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> </div> - <div v-if="steps" class="ticks"> + <div v-if="steps && showTicks" class="ticks"> <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> </div> <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> </div> </div> + <div class="caption"><slot name="caption"></slot></div> </div> </template> -<script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'; +<script lang="ts" setup> +import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue'; import * as os from '@/os'; -export default defineComponent({ - props: { - modelValue: { - type: Number, - required: false, - default: 0 - }, - disabled: { - type: Boolean, - required: false, - default: false - }, - min: { - type: Number, - required: false, - default: 0 - }, - max: { - type: Number, - required: false, - default: 100 - }, - step: { - type: Number, - required: false, - default: 1 - }, - autofocus: { - type: Boolean, - required: false - }, - textConverter: { - type: Function, - required: false, - default: (v) => v.toString(), - }, - }, +const props = withDefaults(defineProps<{ + modelValue: number; + disabled?: boolean; + min: number; + max: number; + step?: number; + textConverter?: (value: number) => string, + showTicks?: boolean; +}>(), { + step: 1, + textConverter: (v) => v.toString(), +}); - setup(props, context) { - const containerEl = ref<HTMLElement>(); - const thumbEl = ref<HTMLElement>(); +const emit = defineEmits<{ + (ev: 'update:modelValue', value: number): void; +}>(); - const rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); - const steppedValue = computed(() => { - if (props.step) { - const step = props.step / (props.max - props.min); - return (step * Math.round(rawValue.value / step)); - } else { - return rawValue.value; - } - }); - const finalValue = computed(() => { - return (steppedValue.value * (props.max - props.min)) + props.min; - }); - watch(finalValue, () => { - context.emit('update:modelValue', finalValue.value); - }); +const containerEl = ref<HTMLElement>(); +const thumbEl = ref<HTMLElement>(); - const thumbWidth = computed(() => { - if (thumbEl.value == null) return 0; - return thumbEl.value!.offsetWidth; - }); - const thumbPosition = ref(0); - const calcThumbPosition = () => { - if (containerEl.value == null) { - thumbPosition.value = 0; - } else { - thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedValue.value; - } - }; - watch([steppedValue, containerEl], calcThumbPosition); - onMounted(() => { - const ro = new ResizeObserver((entries, observer) => { - calcThumbPosition(); - }); - ro.observe(containerEl.value); - onUnmounted(() => { - ro.disconnect(); - }); - }); +const rawValue = ref((props.modelValue - props.min) / (props.max - props.min)); +const steppedRawValue = computed(() => { + if (props.step) { + const step = props.step / (props.max - props.min); + return (step * Math.round(rawValue.value / step)); + } else { + return rawValue.value; + } +}); +const finalValue = computed(() => { + if (Number.isInteger(props.step)) { + return Math.round((steppedRawValue.value * (props.max - props.min)) + props.min); + } else { + return (steppedRawValue.value * (props.max - props.min)) + props.min; + } +}); - const steps = computed(() => { - if (props.step) { - return (props.max - props.min) / props.step; - } else { - return 0; - } - }); +const thumbWidth = computed(() => { + if (thumbEl.value == null) return 0; + return thumbEl.value!.offsetWidth; +}); +const thumbPosition = ref(0); +const calcThumbPosition = () => { + if (containerEl.value == null) { + thumbPosition.value = 0; + } else { + thumbPosition.value = (containerEl.value.offsetWidth - thumbWidth.value) * steppedRawValue.value; + } +}; +watch([steppedRawValue, containerEl], calcThumbPosition); - const onMousedown = (ev: MouseEvent | TouchEvent) => { - ev.preventDefault(); +let ro: ResizeObserver | undefined; - const tooltipShowing = ref(true); - os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { - showing: tooltipShowing, - text: computed(() => { - return props.textConverter(finalValue.value); - }), - targetElement: thumbEl, - }, {}, 'closed'); +onMounted(() => { + ro = new ResizeObserver((entries, observer) => { + calcThumbPosition(); + }); + ro.observe(containerEl.value); +}); - const style = document.createElement('style'); - style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); - document.head.appendChild(style); +onUnmounted(() => { + if (ro) ro.disconnect(); +}); - const onDrag = (ev: MouseEvent | TouchEvent) => { - ev.preventDefault(); - const containerRect = containerEl.value!.getBoundingClientRect(); - const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX; - const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2)); - rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value))); - }; +const steps = computed(() => { + if (props.step) { + return (props.max - props.min) / props.step; + } else { + return 0; + } +}); - const onMouseup = () => { - document.head.removeChild(style); - tooltipShowing.value = false; - window.removeEventListener('mousemove', onDrag); - window.removeEventListener('touchmove', onDrag); - window.removeEventListener('mouseup', onMouseup); - window.removeEventListener('touchend', onMouseup); - }; +const onMousedown = (ev: MouseEvent | TouchEvent) => { + ev.preventDefault(); - window.addEventListener('mousemove', onDrag); - window.addEventListener('touchmove', onDrag); - window.addEventListener('mouseup', onMouseup, { once: true }); - window.addEventListener('touchend', onMouseup, { once: true }); - }; + const tooltipShowing = ref(true); + os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), { + showing: tooltipShowing, + text: computed(() => { + return props.textConverter(finalValue.value); + }), + targetElement: thumbEl, + }, {}, 'closed'); - return { - rawValue, - finalValue, - steppedValue, - onMousedown, - containerEl, - thumbEl, - thumbPosition, - steps, - }; - }, -}); + const style = document.createElement('style'); + style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); + document.head.appendChild(style); + + const onDrag = (ev: MouseEvent | TouchEvent) => { + ev.preventDefault(); + const containerRect = containerEl.value!.getBoundingClientRect(); + const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX; + const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth.value / 2)); + rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth.value))); + }; + + let beforeValue = finalValue.value; + + const onMouseup = () => { + document.head.removeChild(style); + tooltipShowing.value = false; + window.removeEventListener('mousemove', onDrag); + window.removeEventListener('touchmove', onDrag); + window.removeEventListener('mouseup', onMouseup); + window.removeEventListener('touchend', onMouseup); + + // 値が変わってたら通知 + if (beforeValue !== finalValue.value) { + emit('update:modelValue', finalValue.value); + } + }; + + window.addEventListener('mousemove', onDrag); + window.addEventListener('touchmove', onDrag); + window.addEventListener('mouseup', onMouseup, { once: true }); + window.addEventListener('touchend', onMouseup, { once: true }); +}; </script> <style lang="scss" scoped> @@ -191,7 +169,9 @@ export default defineComponent({ $thumbWidth: 20px; > .body { - padding: 12px; + padding: 10px 12px; + background: var(--panel); + border: solid 1px var(--panel); border-radius: 6px; > .container { @@ -209,7 +189,7 @@ export default defineComponent({ height: 3px; background: rgba(0, 0, 0, 0.1); border-radius: 999px; - overflow: clip; + overflow: hidden; overflow: clip; > .highlight { position: absolute; @@ -218,7 +198,7 @@ export default defineComponent({ height: 100%; background: var(--accent); opacity: 0.5; - transition: width 0.2s cubic-bezier(0,0,0,1); + //transition: width 0.2s cubic-bezier(0,0,0,1); } } @@ -251,7 +231,7 @@ export default defineComponent({ cursor: grab; background: var(--accent); border-radius: 999px; - transition: left 0.2s cubic-bezier(0,0,0,1); + //transition: left 0.2s cubic-bezier(0,0,0,1); &:hover { background: var(--accentLighten); diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue index 87196027a8..fe8c08cd6c 100644 --- a/packages/client/src/components/form/select.vue +++ b/packages/client/src/components/form/select.vue @@ -3,7 +3,8 @@ <div class="label" @click="focus"><slot name="label"></slot></div> <div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick"> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> - <select ref="inputEl" + <select + ref="inputEl" v-model="v" v-adaptive-border class="select" @@ -25,178 +26,139 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode } from 'vue'; +<script lang="ts" setup> +import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; -export default defineComponent({ - components: { - MkButton, - }, +const props = defineProps<{ + modelValue: string; + required?: boolean; + readonly?: boolean; + disabled?: boolean; + placeholder?: string; + autofocus?: boolean; + inline?: boolean; + manualSave?: boolean; + small?: boolean; + large?: boolean; +}>(); - props: { - modelValue: { - required: true - }, - required: { - type: Boolean, - required: false - }, - readonly: { - type: Boolean, - required: false - }, - disabled: { - type: Boolean, - required: false - }, - placeholder: { - type: String, - required: false - }, - autofocus: { - type: Boolean, - required: false, - default: false - }, - inline: { - type: Boolean, - required: false, - default: false - }, - manualSave: { - type: Boolean, - required: false, - default: false - }, - }, +const emit = defineEmits<{ + (ev: 'change', _ev: KeyboardEvent): void; + (ev: 'update:modelValue', value: string): void; +}>(); - emits: ['change', 'update:modelValue'], +const slots = useSlots(); - setup(props, context) { - const { modelValue, autofocus } = toRefs(props); - const v = ref(modelValue.value); - const focused = ref(false); - const changed = ref(false); - const invalid = ref(false); - const filled = computed(() => v.value !== '' && v.value != null); - const inputEl = ref(null); - const prefixEl = ref(null); - const suffixEl = ref(null); - const container = ref(null); +const { modelValue, autofocus } = toRefs(props); +const v = ref(modelValue.value); +const focused = ref(false); +const changed = ref(false); +const invalid = ref(false); +const filled = computed(() => v.value !== '' && v.value != null); +const inputEl = ref(null); +const prefixEl = ref(null); +const suffixEl = ref(null); +const container = ref(null); +const height = + props.small ? 38 : + props.large ? 42 : + 40; - const focus = () => inputEl.value.focus(); - const onInput = (ev) => { - changed.value = true; - context.emit('change', ev); - }; +const focus = () => inputEl.value.focus(); +const onInput = (ev) => { + changed.value = true; + emit('change', ev); +}; - const updated = () => { - changed.value = false; - context.emit('update:modelValue', v.value); - }; +const updated = () => { + changed.value = false; + emit('update:modelValue', v.value); +}; - watch(modelValue, newValue => { - v.value = newValue; - }); +watch(modelValue, newValue => { + v.value = newValue; +}); - watch(v, newValue => { - if (!props.manualSave) { - updated(); - } +watch(v, newValue => { + if (!props.manualSave) { + updated(); + } - invalid.value = inputEl.value.validity.badInput; - }); + invalid.value = inputEl.value.validity.badInput; +}); - onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } +// このコンポーネントが作成された時、非表示状態である場合がある +// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する +useInterval(() => { + if (prefixEl.value) { + if (prefixEl.value.offsetWidth) { + inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; + } + } + if (suffixEl.value) { + if (suffixEl.value.offsetWidth) { + inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px'; + } + } +}, 100, { + immediate: true, + afterMounted: true, +}); - // このコンポーネントが作成された時、非表示状態である場合がある - // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する - const clock = window.setInterval(() => { - if (prefixEl.value) { - if (prefixEl.value.offsetWidth) { - inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; - } - } - if (suffixEl.value) { - if (suffixEl.value.offsetWidth) { - inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px'; - } - } - }, 100); +onMounted(() => { + nextTick(() => { + if (autofocus.value) { + focus(); + } + }); +}); - onUnmounted(() => { - window.clearInterval(clock); - }); - }); - }); +const onClick = (ev: MouseEvent) => { + focused.value = true; - const onClick = (ev: MouseEvent) => { - focused.value = true; + const menu = []; + let options = slots.default!(); - const menu = []; - let options = context.slots.default(); + const pushOption = (option: VNode) => { + menu.push({ + text: option.children, + active: v.value === option.props.value, + action: () => { + v.value = option.props.value; + }, + }); + }; - const pushOption = (option: VNode) => { + const scanOptions = (options: VNode[]) => { + for (const vnode of options) { + if (vnode.type === 'optgroup') { + const optgroup = vnode; menu.push({ - text: option.children, - active: v.value === option.props.value, - action: () => { - v.value = option.props.value; - }, + type: 'label', + text: optgroup.props.label, }); - }; - - const scanOptions = (options: VNode[]) => { - for (const vnode of options) { - if (vnode.type === 'optgroup') { - const optgroup = vnode; - menu.push({ - type: 'label', - text: optgroup.props.label, - }); - scanOptions(optgroup.children); - } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある - const fragment = vnode; - scanOptions(fragment.children); - } else { - const option = vnode; - pushOption(option); - } - } - }; - - scanOptions(options); + scanOptions(optgroup.children); + } else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある + const fragment = vnode; + scanOptions(fragment.children); + } else { + const option = vnode; + pushOption(option); + } + } + }; - os.popupMenu(menu, container.value, { - width: container.value.offsetWidth, - }).then(() => { - focused.value = false; - }); - }; + scanOptions(options); - return { - v, - focused, - invalid, - changed, - filled, - inputEl, - prefixEl, - suffixEl, - container, - focus, - onInput, - onClick, - updated, - }; - }, -}); + os.popupMenu(menu, container.value, { + width: container.value.offsetWidth, + }).then(() => { + focused.value = false; + }); +}; </script> <style lang="scss" scoped> @@ -222,7 +184,6 @@ export default defineComponent({ } > .input { - $height: 42px; position: relative; cursor: pointer; @@ -236,7 +197,7 @@ export default defineComponent({ appearance: none; -webkit-appearance: none; display: block; - height: $height; + height: v-bind("height + 'px'"); width: 100%; margin: 0; padding: 0 12px; @@ -253,6 +214,7 @@ export default defineComponent({ cursor: pointer; transition: border-color 0.1s ease-out; pointer-events: none; + user-select: none; } > .prefix, @@ -264,7 +226,7 @@ export default defineComponent({ top: 0; padding: 0 12px; font-size: 1em; - height: $height; + height: v-bind("height + 'px'"); pointer-events: none; &:empty { diff --git a/packages/client/src/components/form/split.vue b/packages/client/src/components/form/split.vue index 676b293967..301a8a84e5 100644 --- a/packages/client/src/components/form/split.vue +++ b/packages/client/src/components/form/split.vue @@ -6,9 +6,9 @@ <script lang="ts" setup> const props = withDefaults(defineProps<{ - minWidth: number; + minWidth?: number; }>(), { - minWidth: 210, + minWidth: 210, }); const minWidth = props.minWidth + 'px'; diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue index fadb770aee..fead163552 100644 --- a/packages/client/src/components/form/switch.vue +++ b/packages/client/src/components/form/switch.vue @@ -1,6 +1,6 @@ <template> <div - class="ziffeoms" + class="ziffeomt" :class="{ disabled, checked }" > <input @@ -9,8 +9,8 @@ :disabled="disabled" @keydown.enter="toggle" > - <span ref="button" v-adaptive-border v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle"> - <i class="check fas fa-check"></i> + <span ref="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle"> + <div class="knob"></div> </span> <span class="label"> <!-- TODO: 無名slotの方は廃止 --> @@ -23,7 +23,6 @@ <script lang="ts" setup> import { toRefs, Ref } from 'vue'; import * as os from '@/os'; -import Ripple from '@/components/ripple.vue'; const props = defineProps<{ modelValue: boolean | Ref<boolean>; @@ -41,16 +40,13 @@ const toggle = () => { emit('update:modelValue', !checked.value); if (!checked.value) { - const rect = button.getBoundingClientRect(); - const x = rect.left + (button.offsetWidth / 2); - const y = rect.top + (button.offsetHeight / 2); - os.popup(Ripple, { x, y, particle: false }, {}, 'end'); + } }; </script> <style lang="scss" scoped> -.ziffeoms { +.ziffeomt { position: relative; display: flex; transition: all 0.2s ease; @@ -73,21 +69,25 @@ const toggle = () => { flex-shrink: 0; margin: 0; box-sizing: border-box; - width: 23px; + width: 32px; height: 23px; outline: none; - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 4px; + background: var(--swutchOffBg); + background-clip: content-box; + border: solid 1px var(--swutchOffBg); + border-radius: 999px; cursor: pointer; transition: inherit; + user-select: none; - > .check { - margin: auto; - opacity: 0; - color: var(--fgOnAccent); - font-size: 13px; - transform: scale(0.5); + > .knob { + position: absolute; + top: 3px; + left: 3px; + width: 15px; + height: 15px; + background: var(--swutchOffFg); + border-radius: 999px; transition: all 0.2s ease; } } @@ -130,12 +130,12 @@ const toggle = () => { &.checked { > .button { - background-color: var(--accent) !important; - border-color: var(--accent) !important; + background-color: var(--swutchOnBg) !important; + border-color: var(--swutchOnBg) !important; - > .check { - opacity: 1; - transform: scale(1); + > .knob { + left: 12px; + background: var(--swutchOnFg); } } } |