This commit is contained in:
kakkokari-gtyih 2025-11-01 17:45:20 +09:00
parent 6f76b598a1
commit 1efa7e037a
8 changed files with 976 additions and 52 deletions

34
locales/index.d.ts vendored
View File

@ -5605,6 +5605,10 @@ export interface Locale extends ILocale {
*
*/
"deviceInfoDescription": string;
/**
*
*/
"calendarInvalidDateError": string;
"_compression": {
"_quality": {
/**
@ -9545,6 +9549,36 @@ export interface Locale extends ILocale {
*/
"saturday": string;
};
"_weekdayShort": {
/**
*
*/
"sunday": string;
/**
*
*/
"monday": string;
/**
*
*/
"tuesday": string;
/**
*
*/
"wednesday": string;
/**
*
*/
"thursday": string;
/**
*
*/
"friday": string;
/**
*
*/
"saturday": string;
};
"_widgets": {
/**
*

View File

@ -1396,6 +1396,7 @@ scheduled: "予約"
widgets: "ウィジェット"
deviceInfo: "デバイス情報"
deviceInfoDescription: "技術的なお問い合わせの際に、以下の情報を併記すると問題の解決に役立つことがあります。"
calendarInvalidDateError: "入力が不正なためカレンダーを表示できません"
_compression:
_quality:
@ -2509,6 +2510,15 @@ _weekday:
friday: "金曜日"
saturday: "土曜日"
_weekdayShort:
sunday: "日"
monday: "月"
tuesday: "火"
wednesday: "水"
thursday: "木"
friday: "金"
saturday: "土"
_widgets:
profile: "プロフィール"
instanceInfo: "サーバー情報"

View File

@ -9,6 +9,7 @@ import { lang } from '@@/js/config.js';
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
let _dateTimeFormat: Intl.DateTimeFormat;
try {
_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
year: 'numeric',
@ -32,6 +33,7 @@ try {
second: 'numeric',
});
}
export const dateTimeFormat = _dateTimeFormat;
export const timeZone = dateTimeFormat.resolvedOptions().timeZone;

View File

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { versatileLang } from '@@/js/intl-const.js';
export function createDateTimeFormatter(options: Intl.DateTimeFormatOptions) {
try {
return new Intl.DateTimeFormat(versatileLang, options);
} catch {
// Fallback to en-US
return new Intl.DateTimeFormat('en-US', options);
}
}
export function createNumberFormatter(options?: Intl.NumberFormatOptions) {
try {
return new Intl.NumberFormat(versatileLang, options);
} catch {
// Fallback to en-US
return new Intl.NumberFormat('en-US', options);
}
}

View File

@ -0,0 +1,520 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal
ref="modal"
v-slot="{ type: modalType, maxHeight }"
:zPriority="'high'"
:anchorElement="anchorElement"
:transparentBg="true"
:returnFocusTo="returnFocusTo"
@click="close"
@close="onModalClose"
@closed="onModalClosed"
>
<div
class="_popup _shadow"
:class="[$style.root, {
[$style.asDrawer]: modalType === 'drawer',
[$style.widthSpecified]: width != null,
}]"
:style="{
'--width': width != null ? `${width}px` : 'auto',
maxHeight: maxHeight ? `${maxHeight}px` : 'auto',
}">
<div :class="[$style.formRoot, { [$style.datetime]: type === 'datetime' }]">
<div v-if="type === 'date' || type === 'datetime'" :class="$style.dateRoot">
<div :class="$style.calendarRoot">
<div :class="$style.calendarControls">
<MkButton iconOnly @click="prevMonth"><i class="ti ti-chevron-left"></i></MkButton>
<div>
<input type="number" v-model.number="year" style="width: 4em; text-align: center;" />
/
<input type="number" v-model.number="month" style="width: 2em; text-align: center;" />
</div>
<MkButton iconOnly @click="nextMonth"><i class="ti ti-chevron-right"></i></MkButton>
</div>
<template v-if="isCalInvalid">
<div :class="$style.calendarInvalid">{{ i18n.ts.calendarInvalidDateError }}</div>
</template>
<template v-else>
<div :class="$style.calendarHeader">
<div>{{ i18n.ts._weekdayShort.sunday }}</div>
<div>{{ i18n.ts._weekdayShort.monday }}</div>
<div>{{ i18n.ts._weekdayShort.tuesday }}</div>
<div>{{ i18n.ts._weekdayShort.wednesday }}</div>
<div>{{ i18n.ts._weekdayShort.thursday }}</div>
<div>{{ i18n.ts._weekdayShort.friday }}</div>
<div>{{ i18n.ts._weekdayShort.saturday }}</div>
</div>
<div v-for="date in calDateArray" :key="date.radioValue">
<input
type="radio"
:class="$style.calendarDayRadio"
:name="id"
:id="`${id}-day-${date.radioValue}`"
:disabled="date.disabled"
:value="date.radioValue"
v-model="dateValue"
/>
<label
class="_button"
:class="[$style.calendarDayLabel, {
[$style.today]: date.isToday,
[$style.saturday]: date.day === 6,
[$style.sunday]: date.day === 0,
[$style.notCurrentMonth]: !date.isCurrentMonth,
[$style.initiallySelected]: date.isInitiallySelected,
[$style.disabled]: date.disabled,
}]"
:for="`${id}-day-${date.radioValue}`"
>{{ date.date.date }}</label>
</div>
</template>
</div>
</div>
<div v-if="type === 'time' || type === 'datetime'" :class="$style.timeRoot">
</div>
</div>
</div>
</MkModal>
</template>
<script lang="ts" setup generic="F extends FormType">
import { computed, ref, shallowRef, useTemplateRef, watch } from 'vue';
import { genId } from '@/utility/id.js';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import type { FormType, MkDateTimeInputDateObject, MkDateTimeInputTimeObject, MkDateTimeInputValue } from './MkDateTimeInput.vue';
import { i18n } from '@/i18n';
const props = defineProps<{
type: F;
initialValue: MkDateTimeInputValue<F> | null;
min?: MkDateTimeInputValue<F>;
max?: MkDateTimeInputValue<F>;
width?: number;
anchorElement?: HTMLElement | null;
returnFocusTo?: HTMLElement | null;
}>();
const emit = defineEmits<{
(ev: 'chosen', value: MkDateTimeInputValue<F> | null): void;
(ev: 'closing'): void;
(ev: 'closed'): void;
}>();
const fixedInitialValue = props.initialValue;
const id = genId();
const modal = useTemplateRef('modal');
function close() {
modal.value?.close();
}
function onModalClose() {
emit('closing');
}
function onModalClosed() {
emit('closed');
}
const now = new Date();
const minDate = shallowRef<Date | null>(null);
const maxDate = shallowRef<Date | null>(null);
watch(() => props.min, (to) => {
minDate.value = props.type !== 'time' && to != null ? new Date((to as MkDateTimeInputDateObject).year, (to as MkDateTimeInputDateObject).month - 1, (to as MkDateTimeInputDateObject).date) : null;
}, { immediate: true });
watch(() => props.max, (to) => {
maxDate.value = props.type !== 'time' && to != null ? new Date((to as MkDateTimeInputDateObject).year, (to as MkDateTimeInputDateObject).month - 1, (to as MkDateTimeInputDateObject).date) : null;
}, { immediate: true });
//#region Calendar Controls
const year = ref((props.initialValue as MkDateTimeInputDateObject)?.year ?? now.getFullYear());
const month = ref((props.initialValue as MkDateTimeInputDateObject)?.month ?? now.getMonth() + 1);
const isCalInvalid = computed(() => {
return month.value < 1 || month.value > 12;
});
function prevMonth() {
if (month.value <= 1) {
month.value = 12;
year.value -= 1;
} else {
month.value--;
}
}
function nextMonth() {
if (month.value >= 12) {
month.value = 1;
year.value += 1;
} else {
month.value++;
}
}
//#endregion
//#region Calendar Values
type CalendarValue = {
radioValue: string;
date: MkDateTimeInputDateObject;
day: number;
isToday: boolean;
isCurrentMonth: boolean;
isInitiallySelected: boolean;
disabled: boolean;
};
function isSameDay(date1: Date, date2: Date): boolean {
return date1.getFullYear() === date2.getFullYear()
&& date1.getMonth() === date2.getMonth()
&& date1.getDate() === date2.getDate();
}
function isSameDateObject(date1: MkDateTimeInputDateObject, date2: MkDateTimeInputDateObject): boolean {
return date1.year === date2.year
&& date1.month === date2.month
&& date1.date === date2.date;
}
function isDisabled(date: MkDateTimeInputDateObject): boolean {
if (props.min == null && props.max == null) {
return false;
}
const dateInstance = new Date(date.year, date.month - 1, date.date);
if (minDate.value != null) {
if (dateInstance < minDate.value) {
return true;
}
}
if (maxDate.value != null) {
if (dateInstance > maxDate.value) {
return true;
}
}
return false;
}
const calDateArray = computed<CalendarValue[]>(() => {
const dateArray: CalendarValue[] = [];
const firstDay = new Date(year.value, month.value - 1, 1).getDay();
const lastDate = new Date(year.value, month.value, 0).getDate();
for (let i = firstDay; i > 0; i--) {
//
const dateInstance = new Date(year.value, month.value - 1, 0);
dateInstance.setDate(dateInstance.getDate() - i + 1);
const dayYear = dateInstance.getFullYear();
const dayMonth = dateInstance.getMonth() + 1;
const dayDate = dateInstance.getDate();
const dateString = `${dayYear.toString().padStart(4, '0')}-${dayMonth.toString().padStart(2, '0')}-${dayDate.toString().padStart(2, '0')}`;
dateArray.push({
radioValue: dateString,
date: { year: dayYear, month: dayMonth, date: dayDate },
day: (firstDay - i) % 7,
isCurrentMonth: false,
isToday: isSameDay(dateInstance, now),
isInitiallySelected: fixedInitialValue != null && isSameDateObject(fixedInitialValue as MkDateTimeInputDateObject, { year: dayYear, month: dayMonth, date: dayDate }),
disabled: isDisabled({ year: dayYear, month: dayMonth, date: dayDate }),
});
}
for (let i = 1; i <= lastDate; i++) {
const dateInstance = new Date(year.value, month.value - 1, i);
const dateString = `${year.value.toString().padStart(4, '0')}-${month.value.toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`;
dateArray.push({
radioValue: dateString,
date: { year: year.value, month: month.value, date: i },
day: (firstDay + i - 1) % 7,
isCurrentMonth: true,
isToday: isSameDay(dateInstance, now),
isInitiallySelected: fixedInitialValue != null && isSameDateObject(fixedInitialValue as MkDateTimeInputDateObject, { year: year.value, month: month.value, date: i }),
disabled: isDisabled({ year: year.value, month: month.value, date: i }),
});
}
const remainingDays = 7 - (dateArray.length % 7);
for (let i = 1; i <= remainingDays; i++) {
//
const dateInstance = new Date(year.value, month.value, i);
const dayYear = dateInstance.getFullYear();
const dayMonth = dateInstance.getMonth() + 1;
const dayDate = dateInstance.getDate();
const dateString = `${dayYear.toString().padStart(4, '0')}-${dayMonth.toString().padStart(2, '0')}-${dayDate.toString().padStart(2, '0')}`;
dateArray.push({
radioValue: dateString,
date: { year: dayYear, month: dayMonth, date: dayDate },
day: (firstDay + lastDate + i - 1) % 7,
isCurrentMonth: false,
isToday: isSameDay(dateInstance, now),
isInitiallySelected: fixedInitialValue != null && isSameDateObject(fixedInitialValue as MkDateTimeInputDateObject, { year: dayYear, month: dayMonth, date: dayDate }),
disabled: isDisabled({ year: dayYear, month: dayMonth, date: dayDate }),
});
}
return dateArray;
});
//#endregion
//#region Date Value
const dateValue = ref<string>((() => {
if (props.initialValue == null) {
return '';
}
if (props.type === 'date' || props.type === 'datetime') {
const val = props.initialValue as MkDateTimeInputDateObject;
return `${val.year.toString().padStart(4, '0')}-${val.month.toString().padStart(2, '0')}-${val.date.toString().padStart(2, '0')}`;
}
return '';
})());
const hourValue = ref<number>((() => {
if (props.initialValue == null) {
return 0;
}
if (props.type === 'time' || props.type === 'datetime') {
const val = props.initialValue as MkDateTimeInputTimeObject;
return val.hour;
}
return 0;
})());
const minuteValue = ref<number>((() => {
if (props.initialValue == null) {
return 0;
}
if (props.type === 'time' || props.type === 'datetime') {
const val = props.initialValue as MkDateTimeInputTimeObject;
return val.minute;
}
return 0;
})());
const valueObject = computed<MkDateTimeInputValue<F> | null>(() => {
if (props.type === 'date') {
if (dateValue.value) {
const [yearStr, monthStr, dateStr] = dateValue.value.split('-');
return {
year: parseInt(yearStr, 10),
month: parseInt(monthStr, 10),
date: parseInt(dateStr, 10),
} as MkDateTimeInputDateObject as MkDateTimeInputValue<F>;
}
} else if (props.type === 'time') {
return {
hour: hourValue.value,
minute: minuteValue.value,
} as MkDateTimeInputTimeObject as MkDateTimeInputValue<F>;
} else if (props.type === 'datetime') {
if (dateValue.value) {
const [yearStr, monthStr, dateStr] = dateValue.value.split('-');
return {
year: parseInt(yearStr, 10),
month: parseInt(monthStr, 10),
date: parseInt(dateStr, 10),
hour: hourValue.value,
minute: minuteValue.value,
} as MkDateTimeInputDateObject & MkDateTimeInputTimeObject as MkDateTimeInputValue<F>;
}
}
return null;
});
//#endregion
watch(valueObject, (to) => {
emit('chosen', to);
});
</script>
<style module lang="scss">
.root {
padding: 24px;
box-sizing: border-box;
max-width: 100vw;
min-width: 200px;
overflow: auto;
overscroll-behavior: contain;
container-type: inline-size;
position: relative;
&:focus-visible {
outline: none;
}
&:not(.asDrawer).widthSpecified {
width: var(--width);
}
&:not(.asDrawer):not(.widthSpecified) {
max-width: 400px;
}
&.asDrawer {
max-width: 600px;
margin: auto;
padding: 24px 24px max(env(safe-area-inset-bottom, 0px), 24px);
width: 100%;
border-radius: 24px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
.formRoot {
display: grid;
grid-template-columns: 1fr;
gap: var(--MI-margin);
}
@container (min-width: 400px) {
.formRoot.datetime {
grid-template-columns: 2fr 1fr;
}
}
.calendarRoot {
display: grid;
grid-template-columns: repeat(7, 32px);
grid-template-rows: auto auto repeat(6, 32px);
text-align: center;
width: max-content;
margin: 0 auto;
}
.calendarInvalid {
grid-column: 1 / -1;
grid-row: 2 / -1;
color: var(--MI_THEME-error);
text-align: center;
font-size: 0.9em;
font-weight: bold;
}
.calendarControls {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--MI-margin);
font-weight: bold;
font-size: 1.1em;
}
.calendarHeader {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(7, 32px);
text-align: center;
font-weight: bold;
font-size: 0.8em;
margin-bottom: 4px;
}
.calendarDayRadio {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.calendarDayLabel {
position: relative;
display: block;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
cursor: pointer;
&.today {
font-weight: bold;
}
&.saturday {
color: #2563eb;
}
&.sunday {
color: #dc2626;
}
&.notCurrentMonth {
color: color(from var(--MI_THEME-fg) srgb r g b / 0.5);
}
&.disabled {
opacity: 0.3;
cursor: not-allowed;
&:after {
content: '';
position: absolute;
width: 2px;
height: 32px;
background: var(--MI_THEME-fg);
top: 0;
left: 50%;
transform: translateX(-50%) rotate(45deg);
}
}
&:before {
content: '';
display: block;
width: 32px;
height: 32px;
box-sizing: border-box;
border-width: 2px;
border-style: solid;
border-color: transparent;
border-radius: 50%;
background-color: transparent;
position: absolute;
z-index: -1;
top: 0;
left: 0;
}
&.initiallySelected::before {
border-color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
border-style: dashed;
}
&.today::before {
border-color: color(from var(--MI_THEME-fg) srgb r g b / 0.5);
}
&:hover::before {
background: var(--MI_THEME-panelHighlight);
}
}
.calendarDayRadio:checked + .calendarDayLabel {
color: var(--MI_THEME-fgOnAccent);
&:before {
background: var(--MI_THEME-accent);
border-color: var(--MI_THEME-accent);
border-style: solid;
}
}
</style>

View File

@ -0,0 +1,361 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<div :class="$style.label" @click="focus"><slot name="label"></slot></div>
<div
ref="container"
tabindex="0"
:class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]"
@focus="focused = true"
@blur="focused = false"
@mousedown.prevent="show"
@keydown.space.enter="show"
>
<div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div>
<div
ref="inputEl"
v-adaptive-border
tabindex="-1"
:class="$style.inputCore"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
@mousedown.prevent="() => {}"
@keydown.prevent="() => {}"
>
<div style="pointer-events: none;">{{ currentValueText ?? '' }}</div>
<div style="display: none;">
<slot></slot>
</div>
</div>
<div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="update"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
export interface MkDateTimeInputDateObject {
year: number;
month: number;
date: number;
}
export interface MkDateTimeInputTimeObject {
hour: number;
minute: number;
}
export interface DateTimeObject extends MkDateTimeInputDateObject, MkDateTimeInputTimeObject {}
export type FormType = 'date' | 'time' | 'datetime';
export type MkDateTimeInputValue<F extends FormType> =
F extends 'date'
? MkDateTimeInputDateObject
: F extends 'time'
? MkDateTimeInputTimeObject
: F extends 'datetime'
? DateTimeObject
: never;
</script>
<script lang="ts" setup generic="FORM extends FormType">
import { onMounted, nextTick, ref, computed, toRefs, useTemplateRef, watch, shallowRef, defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js';
import { dateTimeFormat } from '@@/js/intl-const.js';
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
const props = defineProps<{
type: FORM;
min?: MkDateTimeInputValue<FORM>;
max?: MkDateTimeInputValue<FORM>;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
placeholder?: string;
autofocus?: boolean;
inline?: boolean;
small?: boolean;
large?: boolean;
manualSave?: boolean;
}>();
const model = defineModel<MkDateTimeInputValue<FORM> | null>({ required: true });
const value = ref(model.value);
const changed = computed(() => {
if (model.value == null && value.value == null) {
return false;
} else if (model.value == null && value.value != null) {
return true;
} else if (model.value != null && value.value == null) {
return true;
} else {
if (props.type === 'date') {
const mv = model.value as MkDateTimeInputDateObject;
const vv = value.value as MkDateTimeInputDateObject;
return mv.year !== vv.year || mv.month !== vv.month || mv.date !== vv.date;
} else if (props.type === 'time') {
const mv = model.value as MkDateTimeInputTimeObject;
const vv = value.value as MkDateTimeInputTimeObject;
return mv.hour !== vv.hour || mv.minute !== vv.minute;
} else if (props.type === 'datetime') {
const mv = model.value as DateTimeObject;
const vv = value.value as DateTimeObject;
return mv.year !== vv.year || mv.month !== vv.month || mv.date !== vv.date || mv.hour !== vv.hour || mv.minute !== vv.minute;
}
return false;
}
});
watch(model, () => {
if (props.manualSave) {
value.value = model.value;
}
});
watch(value, () => {
if (!props.manualSave) {
model.value = value.value;
}
});
function update() {
model.value = value.value;
}
function assertDateObject<F extends FormType | FORM>(obj: unknown, type: F): obj is MkDateTimeInputValue<F> {
return props.type === type;
}
const { autofocus } = toRefs(props);
const focused = ref(false);
const opening = ref(false);
const currentValueText = computed<string | null>(() => {
if (value.value == null) {
return null;
}
if (assertDateObject(value.value, 'date')) {
return `${value.value.year.toString().padStart(4, '0')}/${value.value.month.toString().padStart(2, '0')}/${value.value.date.toString().padStart(2, '0')}`;
} else if (assertDateObject(value.value, 'time')) {
return `${value.value.hour.toString().padStart(2, '0')}:${value.value.minute.toString().padStart(2, '0')}`;
} else if (assertDateObject(value.value, 'datetime')) {
const v = value.value as DateTimeObject;
const date = new Date(
v.year,
v.month - 1,
v.date,
v.hour,
v.minute,
);
return dateTimeFormat.format(date);
}
return null;
});
const inputEl = useTemplateRef('inputEl');
const prefixEl = useTemplateRef('prefixEl');
const suffixEl = useTemplateRef('suffixEl');
const container = useTemplateRef('container');
const height =
props.small ? 33 :
props.large ? 39 :
36;
const focus = () => container.value?.focus();
//
// 0
useInterval(() => {
if (inputEl.value == null) return;
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,
});
function show() {
if (opening.value || props.disabled || props.readonly) return;
focus();
opening.value = true;
const { dispose } = os.popup(defineAsyncComponent(() => import('./MkDateTimeInput.dialog.vue')), {
initialValue: value.value,
type: props.type,
min: props.min,
max: props.max,
width: container.value?.offsetWidth,
anchorElement: container.value || null,
returnFocusTo: getHTMLElementOrNull(container.value) ?? getHTMLElementOrNull(window.document.activeElement),
}, {
chosen: (newValue: MkDateTimeInputValue<FORM> | null) => {
value.value = newValue;
},
closing: () => {
opening.value = false;
},
closed: () => {
dispose();
},
});
}
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
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: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
&:empty {
display: none;
}
}
.input {
position: relative;
cursor: pointer;
&.inline {
display: inline-block;
margin: 0;
}
&.focused {
> .inputCore {
border-color: var(--MI_THEME-accent) !important;
//box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
&.disabled {
opacity: 0.7;
&,
> .inputCore {
cursor: not-allowed !important;
}
}
&:focus {
outline: none;
}
&:hover {
> .inputCore {
border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
}
.inputCore {
appearance: none;
-webkit-appearance: none;
display: flex;
align-items: center;
height: v-bind("height + 'px'");
width: 100%;
margin: 0;
padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
color: var(--MI_THEME-fg);
background: var(--MI_THEME-panel);
border: solid 1px var(--MI_THEME-panel);
border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
transition: border-color 0.1s ease-out;
cursor: pointer;
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'");
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;
}
.chevron {
transition: transform 0.1s ease-out;
}
.chevronOpening {
transform: rotateX(180deg);
}
</style>

View File

@ -46,10 +46,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</SearchMarker>
<SearchMarker :keywords="['birthday', 'birthdate', 'age']">
<MkInput v-model="profile.birthday" type="date" manualSave>
<MkDateTimeInput v-model="birthday" type="date" manualSave>
<template #label><SearchLabel>{{ i18n.ts.birthday }}</SearchLabel></template>
<template #prefix><i class="ti ti-cake"></i></template>
</MkInput>
</MkDateTimeInput>
</SearchMarker>
<SearchMarker :keywords="['language', 'locale']">
@ -185,6 +185,8 @@ import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { genId } from '@/utility/id.js';
import MkDateTimeInput from '@/components/MkDateTimeInput.vue';
import type { MkDateTimeInputDateObject } from '@/components/MkDateTimeInput.vue';
const $i = ensureSignin();
@ -207,6 +209,26 @@ const profile = reactive({
isCat: $i.isCat ?? false,
});
const birthday = computed<MkDateTimeInputDateObject | null>({
get: () => {
if (!profile.birthday) return null;
const [year, month, date] = (profile.birthday ?? '').split('-').map(v => parseInt(v, 10));
if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(date)) {
return null;
}
return { year, month, date };
},
set: (value) => {
if (value === null) {
profile.birthday = null;
} else {
const monthStr = value.month.toString().padStart(2, '0');
const dayStr = value.date.toString().padStart(2, '0');
profile.birthday = `${value.year}-${monthStr}-${dayStr}`;
}
},
});
watch(() => profile, () => {
save();
}, {

View File

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { lang } from '@@/js/config.js';
export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
let _dateTimeFormat: Intl.DateTimeFormat;
try {
_dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
} catch (err) {
console.warn(err);
if (_DEV_) console.log('[Intl] Fallback to en-US');
// Fallback to en-US
_dateTimeFormat = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
});
}
export const dateTimeFormat = _dateTimeFormat;
export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
let _numberFormat: Intl.NumberFormat;
try {
_numberFormat = new Intl.NumberFormat(versatileLang);
} catch (err) {
console.warn(err);
if (_DEV_) console.log('[Intl] Fallback to en-US');
// Fallback to en-US
_numberFormat = new Intl.NumberFormat('en-US');
}
export const numberFormat = _numberFormat;