summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkSelect.vue
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-01-07 15:09:46 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-01-07 15:09:46 +0900
commit58bfb4dca4a0b0f0d8468667f5a6b0f0bdc374f5 (patch)
treece743174bfa62d80b77d29e0c473c577af13bead /packages/frontend/src/components/MkSelect.vue
parentfix typo (diff)
downloadmisskey-58bfb4dca4a0b0f0d8468667f5a6b0f0bdc374f5.tar.gz
misskey-58bfb4dca4a0b0f0d8468667f5a6b0f0bdc374f5.tar.bz2
misskey-58bfb4dca4a0b0f0d8468667f5a6b0f0bdc374f5.zip
refactor
Diffstat (limited to 'packages/frontend/src/components/MkSelect.vue')
-rw-r--r--packages/frontend/src/components/MkSelect.vue294
1 files changed, 294 insertions, 0 deletions
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
new file mode 100644
index 0000000000..4b5a14f5be
--- /dev/null
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -0,0 +1,294 @@
+<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" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></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 opening = 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 ? 34 :
+ props.large ? 40 :
+ 37;
+
+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;
+ opening.value = true;
+
+ const menu = [];
+ let options = slots.default!();
+
+ const pushOption = (option: VNode) => {
+ menu.push({
+ text: option.children,
+ active: computed(() => 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,
+ onClosing: () => {
+ opening.value = false;
+ },
+ }).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>
+
+<style lang="scss" module>
+.chevron {
+ transition: transform 0.5s ease;
+}
+
+.chevronOpening {
+ transform: rotateX(180deg);
+}
+</style>