diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/components/form | |
| parent | wip: retention for dashboard (diff) | |
| download | misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 misskey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/components/form')
| -rw-r--r-- | packages/client/src/components/form/checkbox.vue | 144 | ||||
| -rw-r--r-- | packages/client/src/components/form/folder.vue | 107 | ||||
| -rw-r--r-- | packages/client/src/components/form/input.vue | 263 | ||||
| -rw-r--r-- | packages/client/src/components/form/link.vue | 95 | ||||
| -rw-r--r-- | packages/client/src/components/form/radio.vue | 132 | ||||
| -rw-r--r-- | packages/client/src/components/form/radios.vue | 83 | ||||
| -rw-r--r-- | packages/client/src/components/form/range.vue | 259 | ||||
| -rw-r--r-- | packages/client/src/components/form/section.vue | 43 | ||||
| -rw-r--r-- | packages/client/src/components/form/select.vue | 279 | ||||
| -rw-r--r-- | packages/client/src/components/form/slot.vue | 41 | ||||
| -rw-r--r-- | packages/client/src/components/form/split.vue | 27 | ||||
| -rw-r--r-- | packages/client/src/components/form/suspense.vue | 98 | ||||
| -rw-r--r-- | packages/client/src/components/form/switch.vue | 144 | ||||
| -rw-r--r-- | packages/client/src/components/form/textarea.vue | 260 |
14 files changed, 0 insertions, 1975 deletions
diff --git a/packages/client/src/components/form/checkbox.vue b/packages/client/src/components/form/checkbox.vue deleted file mode 100644 index ba3b2dc146..0000000000 --- a/packages/client/src/components/form/checkbox.vue +++ /dev/null @@ -1,144 +0,0 @@ -<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 ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> - <i class="check ti ti-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/MkRipple.vue'; -import { i18n } from '@/i18n'; - -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 deleted file mode 100644 index 1256dfcbb4..0000000000 --- a/packages/client/src/components/form/folder.vue +++ /dev/null @@ -1,107 +0,0 @@ -<template> -<div class="dwzlatin" :class="{ opened }"> - <div class="header _button" @click="toggle"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot name="label"></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i v-if="opened" class="ti ti-chevron-up icon"></i> - <i v-else class="ti ti-chevron-down icon"></i> - </span> - </div> - <KeepAlive> - <div v-if="openedAtLeastOnce" v-show="opened" class="body"> - <MkSpacer :margin-min="14" :margin-max="22"> - <slot></slot> - </MkSpacer> - </div> - </KeepAlive> -</div> -</template> - -<script lang="ts" setup> -const props = withDefaults(defineProps<{ - defaultOpen: boolean; -}>(), { - defaultOpen: false, -}); - -let opened = $ref(props.defaultOpen); -let openedAtLeastOnce = $ref(props.defaultOpen); - -const toggle = () => { - opened = !opened; - if (opened) { - openedAtLeastOnce = true; - } -}; -</script> - -<style lang="scss" scoped> -.dwzlatin { - display: block; - - > .header { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px 10px 14px; - background: var(--buttonBg); - border-radius: 6px; - - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } - - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } - - > .icon { - margin-right: 0.75em; - flex-shrink: 0; - text-align: center; - opacity: 0.8; - - &:empty { - display: none; - - & + .text { - padding-left: 4px; - } - } - } - - > .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - padding-right: 12px; - } - - > .right { - margin-left: auto; - opacity: 0.7; - white-space: nowrap; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } - - > .body { - background: var(--panel); - border-radius: 0 0 6px 6px; - } - - &.opened { - > .header { - border-radius: 6px 6px 0 0; - } - } -} -</style> diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue deleted file mode 100644 index 939e9691a6..0000000000 --- a/packages/client/src/components/form/input.vue +++ /dev/null @@ -1,263 +0,0 @@ -<template> -<div class="matxzzsk"> - <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" - v-model="v" - v-adaptive-border - :type="type" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - :step="step" - :list="id" - @focus="focused = true" - @blur="focused = false" - @keydown="onKeydown($event)" - @input="onInput" - > - <datalist v-if="datalist" :id="id"> - <option v-for="data in datalist" :value="data"/> - </datalist> - <div ref="suffixEl" class="suffix"><slot name="suffix"></slot></div> - </div> - <div class="caption"><slot name="caption"></slot></div> - - <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; -import { debounce } from 'throttle-debounce'; -import MkButton from '@/components/MkButton.vue'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; - -const props = defineProps<{ - modelValue: string | number; - type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search'; - 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 emit = defineEmits<{ - (ev: 'change', _ev: KeyboardEvent): void; - (ev: 'keydown', _ev: KeyboardEvent): void; - (ev: 'enter'): void; - (ev: 'update:modelValue', value: string | number): void; -}>(); - -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 ? 36 : - props.large ? 40 : - 38; - -const focus = () => inputEl.value.focus(); -const onInput = (ev: KeyboardEvent) => { - changed.value = true; - emit('change', ev); -}; -const onKeydown = (ev: KeyboardEvent) => { - emit('keydown', ev); - - if (ev.code === 'Enter') { - emit('enter'); - } -}; - -const updated = () => { - changed.value = false; - if (type.value === 'number') { - emit('update:modelValue', parseFloat(v.value)); - } else { - emit('update:modelValue', v.value); - } -}; - -const debouncedUpdated = debounce(1000, updated); - -watch(modelValue, newValue => { - v.value = newValue; -}); - -watch(v, newValue => { - if (!props.manualSave) { - if (props.debounce) { - debouncedUpdated(); - } else { - updated(); - } - } - - invalid.value = inputEl.value.validity.badInput; -}); - -// このコンポーネントが作成された時、非表示状態である場合がある -// 非表示状態だと要素の幅などは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, -}); - -onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } - }); -}); -</script> - -<style lang="scss" scoped> -.matxzzsk { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } - - > .input { - position: relative; - - > input { - appearance: none; - -webkit-appearance: none; - display: block; - height: v-bind("height + 'px'"); - width: 100%; - margin: 0; - padding: 0 12px; - font: inherit; - font-weight: normal; - font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - outline: none; - box-shadow: none; - box-sizing: border-box; - transition: border-color 0.1s ease-out; - - &:hover { - border-color: var(--inputBorderHover) !important; - } - } - - > .prefix, - > .suffix { - display: flex; - align-items: center; - position: absolute; - z-index: 1; - top: 0; - padding: 0 12px; - font-size: 1em; - height: v-bind("height + 'px'"); - pointer-events: none; - - &:empty { - display: none; - } - - > * { - display: inline-block; - min-width: 16px; - max-width: 150px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - > .prefix { - left: 0; - padding-right: 6px; - } - - > .suffix { - right: 0; - padding-left: 6px; - } - - &.inline { - display: inline-block; - margin: 0; - } - - &.focused { - > input { - border-color: var(--accent) !important; - //box-shadow: 0 0 0 4px var(--focus); - } - } - - &.disabled { - opacity: 0.7; - - &, * { - cursor: not-allowed !important; - } - } - } - - > .save { - margin: 8px 0 0 0; - } -} -</style> diff --git a/packages/client/src/components/form/link.vue b/packages/client/src/components/form/link.vue deleted file mode 100644 index a1775c0bdb..0000000000 --- a/packages/client/src/components/form/link.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> -<div class="ffcbddfc" :class="{ inline }"> - <a v-if="external" class="main _button" :href="to" target="_blank"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="ti ti-external-link icon"></i> - </span> - </a> - <MkA v-else class="main _button" :class="{ active }" :to="to" :behavior="behavior"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i class="ti ti-chevron-right icon"></i> - </span> - </MkA> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; - -const props = defineProps<{ - to: string; - active?: boolean; - external?: boolean; - behavior?: null | 'window' | 'browser' | 'modalWindow'; - inline?: boolean; -}>(); -</script> - -<style lang="scss" scoped> -.ffcbddfc { - display: block; - - &.inline { - display: inline-block; - } - - > .main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px; - background: var(--buttonBg); - border-radius: 6px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } - - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } - - > .icon { - margin-right: 0.75em; - flex-shrink: 0; - text-align: center; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - - & + .text { - padding-left: 4px; - } - } - } - - > .text { - flex-shrink: 1; - white-space: normal; - padding-right: 12px; - text-align: center; - } - - > .right { - margin-left: auto; - opacity: 0.7; - white-space: nowrap; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } -} -</style> diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue deleted file mode 100644 index fcf454c77a..0000000000 --- a/packages/client/src/components/form/radio.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> -<div - v-adaptive-border - class="novjtctn" - :class="{ disabled, checked }" - :aria-checked="checked" - :aria-disabled="disabled" - @click="toggle" -> - <input - type="radio" - :disabled="disabled" - > - <span class="button"> - <span></span> - </span> - <span class="label"><slot></slot></span> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; - -const props = defineProps<{ - modelValue: any; - value: any; - disabled: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'update:modelValue', value: any): void; -}>(); - -let checked = $computed(() => props.modelValue === props.value); - -function toggle(): void { - if (props.disabled) return; - emit('update:modelValue', props.value); -} -</script> - -<style lang="scss" scoped> -.novjtctn { - position: relative; - display: inline-block; - text-align: left; - cursor: pointer; - padding: 7px 10px; - min-width: 60px; - background-color: var(--panel); - background-clip: padding-box !important; - border: solid 1px var(--panel); - border-radius: 6px; - font-size: 90%; - transition: all 0.2s; - - > * { - user-select: none; - } - - &.disabled { - opacity: 0.6; - - &, * { - cursor: not-allowed !important; - } - } - - &:hover { - border-color: var(--inputBorderHover) !important; - } - - &.checked { - background-color: var(--accentedBg) !important; - border-color: var(--accentedBg) !important; - color: var(--accent); - - &, * { - cursor: default !important; - } - - > .button { - border-color: var(--accent); - - &:after { - background-color: var(--accent); - transform: scale(1); - opacity: 1; - } - } - } - - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - margin: 0; - } - - > .button { - position: absolute; - width: 14px; - height: 14px; - background: none; - border: solid 2px var(--inputBorder); - border-radius: 100%; - transition: inherit; - - &:after { - content: ''; - display: block; - position: absolute; - top: 3px; - right: 3px; - bottom: 3px; - left: 3px; - border-radius: 100%; - opacity: 0; - transform: scale(0); - transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); - } - } - - > .label { - margin-left: 28px; - display: block; - line-height: 20px; - cursor: pointer; - } -} -</style> diff --git a/packages/client/src/components/form/radios.vue b/packages/client/src/components/form/radios.vue deleted file mode 100644 index bde4a8fb00..0000000000 --- a/packages/client/src/components/form/radios.vue +++ /dev/null @@ -1,83 +0,0 @@ -<script lang="ts"> -import { defineComponent, h } from 'vue'; -import MkRadio from './radio.vue'; - -export default defineComponent({ - components: { - MkRadio, - }, - props: { - modelValue: { - required: false, - }, - }, - data() { - return { - value: this.modelValue, - }; - }, - watch: { - value() { - this.$emit('update:modelValue', this.value); - }, - }, - render() { - let options = this.$slots.default(); - const label = this.$slots.label && this.$slots.label(); - const caption = this.$slots.caption && this.$slots.caption(); - - // なぜかFragmentになることがあるため - if (options.length === 1 && options[0].props == null) options = options[0].children; - - return h('div', { - class: 'novjtcto', - }, [ - ...(label ? [h('div', { - class: 'label', - }, [label])] : []), - h('div', { - 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)), - ), - ...(caption ? [h('div', { - class: 'caption', - }, [caption])] : []), - ]); - }, -}); -</script> - -<style lang="scss"> -.novjtcto { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .body { - display: flex; - gap: 12px; - flex-wrap: wrap; - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } -} -</style> diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue deleted file mode 100644 index db21c35717..0000000000 --- a/packages/client/src/components/form/range.vue +++ /dev/null @@ -1,259 +0,0 @@ -<template> -<div class="timctyfi" :class="{ disabled, easing }"> - <div class="label"><slot name="label"></slot></div> - <div v-adaptive-border class="body"> - <div ref="containerEl" class="container"> - <div class="track"> - <div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> - </div> - <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" setup> -import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue'; -import * as os from '@/os'; - -const props = withDefaults(defineProps<{ - modelValue: number; - disabled?: boolean; - min: number; - max: number; - step?: number; - textConverter?: (value: number) => string, - showTicks?: boolean; - easing?: boolean; -}>(), { - step: 1, - textConverter: (v) => v.toString(), - easing: false, -}); - -const emit = defineEmits<{ - (ev: 'update:modelValue', value: number): void; -}>(); - -const containerEl = ref<HTMLElement>(); -const thumbEl = ref<HTMLElement>(); - -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 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); - -let ro: ResizeObserver | undefined; - -onMounted(() => { - ro = new ResizeObserver((entries, observer) => { - calcThumbPosition(); - }); - ro.observe(containerEl.value); -}); - -onUnmounted(() => { - if (ro) ro.disconnect(); -}); - -const steps = computed(() => { - if (props.step) { - return (props.max - props.min) / props.step; - } else { - return 0; - } -}); - -const onMousedown = (ev: MouseEvent | TouchEvent) => { - ev.preventDefault(); - - const tooltipShowing = ref(true); - os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { - showing: tooltipShowing, - text: computed(() => { - return props.textConverter(finalValue.value); - }), - targetElement: thumbEl, - }, {}, 'closed'); - - 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> -@use "sass:math"; - -.timctyfi { - position: relative; - - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } - - $thumbHeight: 20px; - $thumbWidth: 20px; - - > .body { - padding: 10px 12px; - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - - > .container { - position: relative; - height: $thumbHeight; - - > .track { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - width: calc(100% - #{$thumbWidth}); - height: 3px; - background: rgba(0, 0, 0, 0.1); - border-radius: 999px; - overflow: clip; - - > .highlight { - position: absolute; - top: 0; - left: 0; - height: 100%; - background: var(--accent); - opacity: 0.5; - } - } - - > .ticks { - $tickWidth: 3px; - - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - width: calc(100% - #{$thumbWidth}); - - > .tick { - position: absolute; - bottom: 0; - width: $tickWidth; - height: 3px; - margin-left: - math.div($tickWidth, 2); - background: var(--divider); - border-radius: 999px; - } - } - - > .thumb { - position: absolute; - width: $thumbWidth; - height: $thumbHeight; - cursor: grab; - background: var(--accent); - border-radius: 999px; - - &:hover { - background: var(--accentLighten); - } - } - } - } - - &.easing { - > .body { - > .container { - > .track { - > .highlight { - transition: width 0.2s cubic-bezier(0,0,0,1); - } - } - - > .thumb { - transition: left 0.2s cubic-bezier(0,0,0,1); - } - } - } - } -} -</style> diff --git a/packages/client/src/components/form/section.vue b/packages/client/src/components/form/section.vue deleted file mode 100644 index c6e34ef1cc..0000000000 --- a/packages/client/src/components/form/section.vue +++ /dev/null @@ -1,43 +0,0 @@ -<template> -<div class="vrtktovh _formBlock"> - <div class="label"><slot name="label"></slot></div> - <div class="main _formRoot"> - <slot></slot> - </div> -</div> -</template> - -<script lang="ts" setup> -</script> - -<style lang="scss" scoped> -.vrtktovh { - border-top: solid 0.5px var(--divider); - border-bottom: solid 0.5px var(--divider); - - & + .vrtktovh { - border-top: none; - } - - &:first-child { - border-top: none; - } - - &:last-child { - border-bottom: none; - } - - > .label { - font-weight: bold; - margin: 1.5em 0 16px 0; - - &:empty { - display: none; - } - } - - > .main { - margin: 1.5em 0; - } -} -</style> diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue deleted file mode 100644 index eaf4b131cd..0000000000 --- a/packages/client/src/components/form/select.vue +++ /dev/null @@ -1,279 +0,0 @@ -<template> -<div class="vblkjoeq"> - <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" - v-model="v" - v-adaptive-border - class="select" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - @focus="focused = true" - @blur="focused = false" - @input="onInput" - > - <slot></slot> - </select> - <div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down"></i></div> - </div> - <div class="caption"><slot name="caption"></slot></div> - - <MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> -</div> -</template> - -<script lang="ts" setup> -import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue'; -import MkButton from '@/components/MkButton.vue'; -import * as os from '@/os'; -import { useInterval } from '@/scripts/use-interval'; -import { i18n } from '@/i18n'; - -const props = defineProps<{ - modelValue: string; - required?: boolean; - readonly?: boolean; - disabled?: boolean; - placeholder?: string; - autofocus?: boolean; - inline?: boolean; - manualSave?: boolean; - small?: boolean; - large?: boolean; -}>(); - -const emit = defineEmits<{ - (ev: 'change', _ev: KeyboardEvent): void; - (ev: 'update:modelValue', value: string): void; -}>(); - -const slots = useSlots(); - -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 ? 36 : - props.large ? 40 : - 38; - -const focus = () => inputEl.value.focus(); -const onInput = (ev) => { - changed.value = true; - emit('change', ev); -}; - -const updated = () => { - changed.value = false; - emit('update:modelValue', v.value); -}; - -watch(modelValue, newValue => { - v.value = newValue; -}); - -watch(v, newValue => { - if (!props.manualSave) { - updated(); - } - - invalid.value = inputEl.value.validity.badInput; -}); - -// このコンポーネントが作成された時、非表示状態である場合がある -// 非表示状態だと要素の幅などは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, -}); - -onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } - }); -}); - -const onClick = (ev: MouseEvent) => { - focused.value = true; - - const menu = []; - let options = slots.default!(); - - const pushOption = (option: VNode) => { - menu.push({ - text: option.children, - active: v.value === option.props.value, - action: () => { - v.value = option.props.value; - }, - }); - }; - - 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 if (vnode.props == null) { // v-if で条件が false のときにこうなる - // nop? - } else { - const option = vnode; - pushOption(option); - } - } - }; - - scanOptions(options); - - os.popupMenu(menu, container.value, { - width: container.value.offsetWidth, - }).then(() => { - focused.value = false; - }); -}; -</script> - -<style lang="scss" scoped> -.vblkjoeq { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } - - > .input { - position: relative; - cursor: pointer; - - &:hover { - > .select { - border-color: var(--inputBorderHover) !important; - } - } - - > .select { - appearance: none; - -webkit-appearance: none; - display: block; - height: v-bind("height + 'px'"); - width: 100%; - margin: 0; - padding: 0 12px; - font: inherit; - font-weight: normal; - font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - outline: none; - box-shadow: none; - box-sizing: border-box; - cursor: pointer; - transition: border-color 0.1s ease-out; - pointer-events: none; - user-select: none; - } - - > .prefix, - > .suffix { - display: flex; - align-items: center; - position: absolute; - z-index: 1; - top: 0; - padding: 0 12px; - font-size: 1em; - height: v-bind("height + 'px'"); - pointer-events: none; - - &:empty { - display: none; - } - - > * { - display: inline-block; - min-width: 16px; - max-width: 150px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - > .prefix { - left: 0; - padding-right: 6px; - } - - > .suffix { - right: 0; - padding-left: 6px; - } - - &.inline { - display: inline-block; - margin: 0; - } - - &.focused { - > select { - border-color: var(--accent) !important; - } - } - - &.disabled { - opacity: 0.7; - - &, * { - cursor: not-allowed !important; - } - } - } -} -</style> diff --git a/packages/client/src/components/form/slot.vue b/packages/client/src/components/form/slot.vue deleted file mode 100644 index 79ce8fe51f..0000000000 --- a/packages/client/src/components/form/slot.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> -<div class="adhpbeou"> - <div class="label" @click="focus"><slot name="label"></slot></div> - <div class="content"> - <slot></slot> - </div> - <div class="caption"><slot name="caption"></slot></div> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; - -function focus() { - // TODO -} -</script> - -<style lang="scss" scoped> -.adhpbeou { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } -} -</style> diff --git a/packages/client/src/components/form/split.vue b/packages/client/src/components/form/split.vue deleted file mode 100644 index 301a8a84e5..0000000000 --- a/packages/client/src/components/form/split.vue +++ /dev/null @@ -1,27 +0,0 @@ -<template> -<div class="terlnhxf _formBlock"> - <slot></slot> -</div> -</template> - -<script lang="ts" setup> -const props = withDefaults(defineProps<{ - minWidth?: number; -}>(), { - minWidth: 210, -}); - -const minWidth = props.minWidth + 'px'; -</script> - -<style lang="scss" scoped> -.terlnhxf { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(v-bind('minWidth'), 1fr)); - grid-gap: 12px; - - > ::v-deep(*) { - margin: 0 !important; - } -} -</style> diff --git a/packages/client/src/components/form/suspense.vue b/packages/client/src/components/form/suspense.vue deleted file mode 100644 index 7efa501f27..0000000000 --- a/packages/client/src/components/form/suspense.vue +++ /dev/null @@ -1,98 +0,0 @@ -<template> -<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> - <div v-if="pending"> - <MkLoading/> - </div> - <div v-else-if="resolved"> - <slot :result="result"></slot> - </div> - <div v-else> - <div class="wszdbhzo"> - <div><i class="ti ti-alert-triangle"></i> {{ $ts.somethingHappened }}</div> - <MkButton inline class="retry" @click="retry"><i class="ti ti-reload"></i> {{ $ts.retry }}</MkButton> - </div> - </div> -</transition> -</template> - -<script lang="ts"> -import { defineComponent, PropType, ref, watch } from 'vue'; -import MkButton from '@/components/MkButton.vue'; - -export default defineComponent({ - components: { - MkButton, - }, - - props: { - p: { - type: Function as PropType<() => Promise<any>>, - required: true, - }, - }, - - setup(props, context) { - const pending = ref(true); - const resolved = ref(false); - const rejected = ref(false); - const result = ref(null); - - const process = () => { - if (props.p == null) { - return; - } - const promise = props.p(); - pending.value = true; - resolved.value = false; - rejected.value = false; - promise.then((_result) => { - pending.value = false; - resolved.value = true; - result.value = _result; - }); - promise.catch(() => { - pending.value = false; - rejected.value = true; - }); - }; - - watch(() => props.p, () => { - process(); - }, { - immediate: true, - }); - - const retry = () => { - process(); - }; - - return { - pending, - resolved, - rejected, - result, - retry, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.fade-enter-active, -.fade-leave-active { - transition: opacity 0.125s ease; -} -.fade-enter-from, -.fade-leave-to { - opacity: 0; -} - -.wszdbhzo { - padding: 16px; - text-align: center; - - > .retry { - margin-top: 16px; - } -} -</style> diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue deleted file mode 100644 index 1ed00ae655..0000000000 --- a/packages/client/src/components/form/switch.vue +++ /dev/null @@ -1,144 +0,0 @@ -<template> -<div - class="ziffeomt" - :class="{ disabled, checked }" -> - <input - ref="input" - type="checkbox" - :disabled="disabled" - @keydown.enter="toggle" - > - <span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle"> - <div class="knob"></div> - </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 { i18n } from '@/i18n'; - -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) { - - } -}; -</script> - -<style lang="scss" scoped> -.ziffeomt { - 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: 32px; - height: 23px; - outline: none; - background: var(--swutchOffBg); - background-clip: content-box; - border: solid 1px var(--swutchOffBg); - border-radius: 999px; - cursor: pointer; - transition: inherit; - user-select: none; - - > .knob { - position: absolute; - top: 3px; - left: 3px; - width: 15px; - height: 15px; - background: var(--swutchOffFg); - border-radius: 999px; - 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(--swutchOnBg) !important; - border-color: var(--swutchOnBg) !important; - - > .knob { - left: 12px; - background: var(--swutchOnFg); - } - } - } -} -</style> diff --git a/packages/client/src/components/form/textarea.vue b/packages/client/src/components/form/textarea.vue deleted file mode 100644 index d34d7b1775..0000000000 --- a/packages/client/src/components/form/textarea.vue +++ /dev/null @@ -1,260 +0,0 @@ -<template> -<div class="adhpbeos"> - <div class="label" @click="focus"><slot name="label"></slot></div> - <div class="input" :class="{ disabled, focused, tall, pre }"> - <textarea - ref="inputEl" - v-model="v" - v-adaptive-border - :class="{ code, _monospace: code }" - :disabled="disabled" - :required="required" - :readonly="readonly" - :placeholder="placeholder" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false" - @keydown="onKeydown($event)" - @input="onInput" - ></textarea> - </div> - <div class="caption"><slot name="caption"></slot></div> - - <MkButton v-if="manualSave && changed" primary class="save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> -</div> -</template> - -<script lang="ts"> -import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue'; -import { debounce } from 'throttle-debounce'; -import MkButton from '@/components/MkButton.vue'; -import { i18n } from '@/i18n'; - -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, - }, - code: { - type: Boolean, - required: false, - }, - tall: { - type: Boolean, - required: false, - default: false, - }, - pre: { - 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, 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 focus = () => inputEl.value.focus(); - const onInput = (ev) => { - changed.value = true; - context.emit('change', ev); - }; - const onKeydown = (ev: KeyboardEvent) => { - context.emit('keydown', ev); - - if (ev.code === 'Enter') { - context.emit('enter'); - } - }; - - const updated = () => { - changed.value = false; - context.emit('update:modelValue', v.value); - }; - - const debouncedUpdated = debounce(1000, updated); - - watch(modelValue, newValue => { - v.value = newValue; - }); - - watch(v, newValue => { - if (!props.manualSave) { - if (props.debounce) { - debouncedUpdated(); - } else { - updated(); - } - } - - invalid.value = inputEl.value.validity.badInput; - }); - - onMounted(() => { - nextTick(() => { - if (autofocus.value) { - focus(); - } - }); - }); - - return { - v, - focused, - invalid, - changed, - filled, - inputEl, - focus, - onInput, - onKeydown, - updated, - i18n, - }; - }, -}); -</script> - -<style lang="scss" scoped> -.adhpbeos { - > .label { - font-size: 0.85em; - padding: 0 0 8px 0; - user-select: none; - - &:empty { - display: none; - } - } - - > .caption { - font-size: 0.85em; - padding: 8px 0 0 0; - color: var(--fgTransparentWeak); - - &:empty { - display: none; - } - } - - > .input { - position: relative; - - > textarea { - appearance: none; - -webkit-appearance: none; - display: block; - width: 100%; - min-width: 100%; - max-width: 100%; - min-height: 130px; - margin: 0; - padding: 12px; - font: inherit; - font-weight: normal; - font-size: 1em; - color: var(--fg); - background: var(--panel); - border: solid 1px var(--panel); - border-radius: 6px; - outline: none; - box-shadow: none; - box-sizing: border-box; - transition: border-color 0.1s ease-out; - - &:hover { - border-color: var(--inputBorderHover) !important; - } - } - - &.focused { - > textarea { - border-color: var(--accent) !important; - } - } - - &.disabled { - opacity: 0.7; - - &, * { - cursor: not-allowed !important; - } - } - - &.tall { - > textarea { - min-height: 200px; - } - } - - &.pre { - > textarea { - white-space: pre; - } - } - } - - > .save { - margin: 8px 0 0 0; - } -} -</style> |