ae5d052274
to keep things manageable i merged a lot of one off values into just a handful of common sizes, so some parts of the ui will look different than upstream even with the "Misskey" rounding mode
276 lines
5.7 KiB
Vue
276 lines
5.7 KiB
Vue
<!--
|
|
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
-->
|
|
|
|
<template>
|
|
<div>
|
|
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
|
|
<div :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]">
|
|
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
|
|
<input
|
|
ref="inputEl"
|
|
v-model="v"
|
|
v-adaptive-border
|
|
:class="$style.inputCore"
|
|
:type="type"
|
|
:disabled="disabled"
|
|
:required="required"
|
|
:readonly="readonly"
|
|
:placeholder="placeholder"
|
|
:pattern="pattern"
|
|
:autocomplete="autocomplete"
|
|
:autocapitalize="autocapitalize"
|
|
:spellcheck="spellcheck"
|
|
:step="step"
|
|
:list="id"
|
|
:min="min"
|
|
:max="max"
|
|
@focus="focused = true"
|
|
@blur="focused = false"
|
|
@keydown="onKeydown($event)"
|
|
@input="onInput"
|
|
>
|
|
<datalist v-if="datalist" :id="id">
|
|
<option v-for="data in datalist" :key="data" :value="data"/>
|
|
</datalist>
|
|
<div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div>
|
|
</div>
|
|
<div :class="$style.caption"><slot name="caption"></slot></div>
|
|
|
|
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
|
import { debounce } from 'throttle-debounce';
|
|
import MkButton from '@/components/MkButton.vue';
|
|
import { useInterval } from '@/scripts/use-interval.js';
|
|
import { i18n } from '@/i18n.js';
|
|
|
|
const props = defineProps<{
|
|
modelValue: string | number | null;
|
|
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
|
required?: boolean;
|
|
readonly?: boolean;
|
|
disabled?: boolean;
|
|
pattern?: string;
|
|
placeholder?: string;
|
|
autofocus?: boolean;
|
|
autocomplete?: string;
|
|
autocapitalize?: string;
|
|
spellcheck?: boolean;
|
|
step?: any;
|
|
datalist?: string[];
|
|
min?: number;
|
|
max?: number | 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 = shallowRef<HTMLElement>();
|
|
const prefixEl = shallowRef<HTMLElement>();
|
|
const suffixEl = shallowRef<HTMLElement>();
|
|
const height =
|
|
props.small ? 33 :
|
|
props.large ? 39 :
|
|
36;
|
|
|
|
const focus = () => inputEl.value.focus();
|
|
const onInput = (ev: KeyboardEvent) => {
|
|
changed.value = true;
|
|
emit('change', ev);
|
|
};
|
|
const onKeydown = (ev: KeyboardEvent) => {
|
|
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
|
|
|
|
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();
|
|
}
|
|
});
|
|
});
|
|
|
|
defineExpose({
|
|
focus,
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.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;
|
|
|
|
&.inline {
|
|
display: inline-block;
|
|
margin: 0;
|
|
}
|
|
|
|
&.focused {
|
|
> .inputCore {
|
|
border-color: var(--accent) !important;
|
|
//box-shadow: 0 0 0 4px var(--focus);
|
|
}
|
|
}
|
|
|
|
&.disabled {
|
|
opacity: 0.7;
|
|
|
|
&,
|
|
> .inputCore {
|
|
cursor: not-allowed !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
.inputCore {
|
|
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: var(--radius-sm);
|
|
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'");
|
|
min-width: 16px;
|
|
max-width: 150px;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
box-sizing: border-box;
|
|
pointer-events: none;
|
|
|
|
&:empty {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.prefix {
|
|
left: 0;
|
|
padding-right: 6px;
|
|
}
|
|
|
|
.suffix {
|
|
right: 0;
|
|
padding-left: 6px;
|
|
}
|
|
.save {
|
|
margin: 8px 0 0 0;
|
|
}
|
|
</style>
|