perf: auto focus when opening editing forms

pull/609/head
Ryan Wang 2022-09-09 21:48:07 +08:00
parent daaf5bcf7a
commit ce93d9813b
14 changed files with 216 additions and 125 deletions

View File

@ -0,0 +1,9 @@
export function setFocus(id: string) {
const inputElement = document.getElementById(id);
if (inputElement instanceof HTMLInputElement) {
const timer = setTimeout(() => {
inputElement?.focus();
clearTimeout(timer);
}, 0);
}
}

View File

@ -7,6 +7,7 @@ import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@halo-dev/admin-shared";
import { reset, submitForm } from "@formkit/core";
import { useMagicKeys } from "@vueuse/core";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -95,7 +96,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
@ -119,8 +122,14 @@ watch(
:width="500"
@update:visible="onVisibleChange"
>
<FormKit id="attachment-group-form" type="form" @submit="handleSave">
<FormKit
id="attachment-group-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleSave"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"

View File

@ -133,7 +133,7 @@ onMounted(async () => {
</span>
</div>
<FloatingDropdown v-if="!readonly">
<IconMore />
<IconMore @click.stop />
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">

View File

@ -7,6 +7,7 @@ import { apiClient, useSettingForm } from "@halo-dev/admin-shared";
import { v4 as uuid } from "uuid";
import { reset, submitForm } from "@formkit/core";
import { useMagicKeys } from "@vueuse/core";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -135,11 +136,14 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
setTimeout(() => {
if (visible) {
setFocus("displayNameInput");
} else {
const timer = setTimeout(() => {
policyTemplate.value = undefined;
handleResetForm();
handleResetSettingForm();
clearTimeout(timer);
}, 100);
}
}
@ -192,9 +196,11 @@ const onVisibleChange = (visible: boolean) => {
:actions="false"
:preserve="true"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSave"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"

View File

@ -12,6 +12,7 @@ import type { Category } from "@halo-dev/api-client";
// libs
import cloneDeep from "lodash.clonedeep";
import { reset, submitForm } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useMagicKeys } from "@vueuse/core";
import { v4 as uuid } from "uuid";
@ -105,7 +106,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
@ -129,8 +132,14 @@ watch(
:width="600"
@update:visible="onVisibleChange"
>
<FormKit id="category-form" type="form" @submit="handleSaveCategory">
<FormKit
id="category-form"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSaveCategory"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"

View File

@ -9,7 +9,7 @@ import { useRouter } from "vue-router";
const props = withDefaults(
defineProps<{
tag: Tag;
route: boolean;
route?: boolean;
}>(),
{
route: false,

View File

@ -18,6 +18,7 @@ import type { Tag } from "@halo-dev/api-client";
// libs
import cloneDeep from "lodash.clonedeep";
import { reset, submitForm } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useMagicKeys } from "@vueuse/core";
import { v4 as uuid } from "uuid";
@ -109,7 +110,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
@ -141,8 +144,14 @@ watch(
<IconArrowRight />
</div>
</template>
<FormKit id="tag-form" type="form" @submit="handleSaveTag">
<FormKit
id="tag-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleSaveTag"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"

View File

@ -2,11 +2,12 @@
import { VButton, VModal, VSpace } from "@halo-dev/components";
import type { Menu } from "@halo-dev/api-client";
import { v4 as uuid } from "uuid";
import { computed, ref, watch } from "vue";
import { computed, ref, watch, watchEffect } from "vue";
import { apiClient } from "@halo-dev/admin-shared";
import { reset, submitForm } from "@formkit/core";
import cloneDeep from "lodash.clonedeep";
import { useMagicKeys } from "@vueuse/core";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -73,27 +74,41 @@ const onVisibleChange = (visible: boolean) => {
}
};
watch(props, (newVal) => {
const { Command_Enter } = useMagicKeys();
let keyboardWatcher;
if (newVal.visible) {
keyboardWatcher = watch(Command_Enter, (v) => {
if (v) {
submitForm("menu-form");
}
});
} else {
keyboardWatcher?.unwatch();
}
if (newVal.visible && props.menu) {
formState.value = cloneDeep(props.menu);
return;
}
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
formState.value.metadata.name = uuid();
reset("menu-form");
};
const { Command_Enter } = useMagicKeys();
watchEffect(() => {
if (Command_Enter.value && props.visible) {
submitForm("menu-form");
}
});
watch(
() => props.visible,
(visible) => {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
);
watch(
() => props.menu,
(menu) => {
if (menu) {
formState.value = cloneDeep(menu);
} else {
handleResetForm();
}
}
);
</script>
<template>
<VModal
@ -106,9 +121,11 @@ watch(props, (newVal) => {
id="menu-form"
:classes="{ form: 'w-full' }"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleCreateMenu"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
help="可根据此名称查询菜单项"
label="菜单名称"

View File

@ -9,6 +9,7 @@ import cloneDeep from "lodash.clonedeep";
import { useMagicKeys } from "@vueuse/core";
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -114,7 +115,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
@ -301,7 +304,12 @@ watch(
title="编辑菜单项"
@update:visible="onVisibleChange"
>
<FormKit id="menuitem-form" type="form" @submit="handleSaveMenuItem">
<FormKit
id="menuitem-form"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSaveMenuItem"
>
<FormKit
v-model="selectedMenuItemSource"
:options="menuItemSources"
@ -314,6 +322,7 @@ watch(
<FormKit
v-if="selectedMenuItemSource === 'custom'"
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"

View File

@ -11,6 +11,7 @@ import cloneDeep from "lodash.clonedeep";
import { reset, submitForm } from "@formkit/core";
import { useMagicKeys } from "@vueuse/core";
import { v4 as uuid } from "uuid";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -60,7 +61,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus("displayNameInput");
} else {
handleResetForm();
}
}
@ -124,9 +127,11 @@ const handleResetForm = () => {
id="role-form"
:actions="false"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleCreateOrUpdateRole"
>
<FormKit
id="displayNameInput"
v-model="
formState.metadata.annotations[rbacAnnotations.DISPLAY_NAME]
"
@ -139,6 +144,7 @@ const handleResetForm = () => {
help="角色别名,用于区分角色,不能重复,创建之后不能修改"
label="别名"
type="text"
:disabled="isUpdateMode"
validation="required"
></FormKit>
</FormKit>

View File

@ -10,6 +10,7 @@ export default definePlugin({
{
path: "/settings",
component: SystemSettingsLayout,
redirect: "/settings/basic",
children: [
{
path: ":group",
@ -25,7 +26,7 @@ export default definePlugin({
items: [
{
name: "设置",
path: "/settings/basic",
path: "/settings",
icon: IconSettings,
},
],

View File

@ -102,14 +102,12 @@ onMounted(() => {
<template>
<UserEditingModal
v-model:visible="editingModal"
v-permission="['system:users:manage']"
:user="selectedUser"
@close="onEditingModalClose"
/>
<UserPasswordChangeModal
v-model:visible="passwordChangeModal"
v-permission="['system:users:manage']"
:user="selectedUser"
@close="handleFetchUsers"
/>

View File

@ -15,7 +15,6 @@ import {
} from "@halo-dev/components";
// libs
import { v4 as uuid } from "uuid";
import YAML from "yaml";
import cloneDeep from "lodash.clonedeep";
import { useMagicKeys } from "@vueuse/core";
@ -27,6 +26,7 @@ import { rbacAnnotations } from "@/constants/annotations";
// hooks
import { useFetchRole } from "@/modules/system/roles/composables/use-role";
import type { FormKitOptionsList } from "@formkit/inputs";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -44,41 +44,32 @@ const emit = defineEmits<{
(event: "close"): void;
}>();
interface FormState {
user: User;
saving: boolean;
rawMode: boolean;
raw: string;
}
const initialFormState: FormState = {
user: {
spec: {
displayName: "",
avatar: "",
email: "",
phone: "",
password: "",
bio: "",
disabled: false,
loginHistoryLimit: 0,
},
apiVersion: "v1alpha1",
kind: "User",
metadata: {
name: uuid(),
},
const initialFormState: User = {
spec: {
displayName: "",
avatar: "",
email: "",
phone: "",
password: "",
bio: "",
disabled: false,
loginHistoryLimit: 0,
},
apiVersion: "v1alpha1",
kind: "User",
metadata: {
name: "",
},
saving: false,
rawMode: false,
raw: "",
};
const formState = ref<FormState>(cloneDeep(initialFormState));
const formState = ref<User>(cloneDeep(initialFormState));
const saving = ref(false);
const rawMode = ref(false);
const raw = ref("");
const selectedRole = ref("");
const isUpdateMode = computed(() => {
return !!formState.value.user.metadata.creationTimestamp;
return !!formState.value.metadata.creationTimestamp;
});
const creationModalTitle = computed(() => {
@ -86,7 +77,7 @@ const creationModalTitle = computed(() => {
});
const modalWidth = computed(() => {
return formState.value.rawMode ? 800 : 700;
return rawMode.value ? 800 : 700;
});
const { roles } = useFetchRole();
@ -112,7 +103,9 @@ watchEffect(() => {
watch(
() => props.visible,
(visible) => {
if (!visible) {
if (visible) {
setFocus(isUpdateMode.value ? "displayNameInput" : "userNameInput");
} else {
handleResetForm();
}
}
@ -122,7 +115,7 @@ watch(
() => props.user,
(user) => {
if (user) {
formState.value.user = cloneDeep(user);
formState.value = cloneDeep(user);
} else {
handleResetForm();
}
@ -138,24 +131,23 @@ const onVisibleChange = (visible: boolean) => {
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
formState.value.user.metadata.name = uuid();
reset("user-form");
};
const handleCreateUser = async () => {
try {
formState.value.saving = true;
saving.value = true;
let user: User;
if (isUpdateMode.value) {
const response = await apiClient.extension.user.updatev1alpha1User({
name: formState.value.user.metadata.name,
user: formState.value.user,
name: formState.value.metadata.name,
user: formState.value,
});
user = response.data;
} else {
const response = await apiClient.extension.user.createv1alpha1User({
user: formState.value.user,
user: formState.value,
});
user = response.data;
}
@ -173,17 +165,17 @@ const handleCreateUser = async () => {
} catch (e) {
console.error(e);
} finally {
formState.value.saving = false;
saving.value = false;
}
};
const handleRawModeChange = () => {
formState.value.rawMode = !formState.value.rawMode;
rawMode.value = !rawMode.value;
if (formState.value.rawMode) {
formState.value.raw = YAML.stringify(formState.value.user);
if (rawMode.value) {
raw.value = YAML.stringify(formState.value);
} else {
formState.value.user = YAML.parse(formState.value.raw);
formState.value = YAML.parse(raw.value);
}
};
</script>
@ -196,35 +188,37 @@ const handleRawModeChange = () => {
>
<template #actions>
<div class="modal-header-action" @click="handleRawModeChange">
<IconCodeBoxLine v-if="!formState.rawMode" />
<IconCodeBoxLine v-if="!rawMode" />
<IconEye v-else />
</div>
</template>
<VCodemirror
v-show="formState.rawMode"
v-model="formState.raw"
height="50vh"
language="yaml"
/>
<VCodemirror v-show="rawMode" v-model="raw" height="50vh" language="yaml" />
<div v-show="!formState.rawMode">
<FormKit id="user-form" type="form" @submit="handleCreateUser">
<div v-show="!rawMode">
<FormKit
id="user-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleCreateUser"
>
<FormKit
v-model="formState.user.metadata.name"
id="userNameInput"
v-model="formState.metadata.name"
:disabled="isUpdateMode"
label="用户名"
type="text"
validation="required"
></FormKit>
<FormKit
v-model="formState.user.spec.displayName"
id="displayNameInput"
v-model="formState.spec.displayName"
label="显示名称"
type="text"
validation="required"
></FormKit>
<FormKit
v-model="formState.user.spec.email"
v-model="formState.spec.email"
label="电子邮箱"
type="email"
validation="required"
@ -236,17 +230,17 @@ const handleRawModeChange = () => {
type="select"
></FormKit>
<FormKit
v-model="formState.user.spec.phone"
v-model="formState.spec.phone"
label="手机号"
type="text"
></FormKit>
<FormKit
v-model="formState.user.spec.avatar"
v-model="formState.spec.avatar"
label="头像"
type="text"
></FormKit>
<FormKit
v-model="formState.user.spec.bio"
v-model="formState.spec.bio"
label="描述"
type="textarea"
></FormKit>
@ -255,7 +249,7 @@ const handleRawModeChange = () => {
<template #footer>
<VSpace>
<VButton
:loading="formState.saving"
:loading="saving"
type="secondary"
@click="$formkit.submit('user-form')"
>

View File

@ -1,9 +1,12 @@
<script lang="ts" setup>
import { IconSave, VButton, VModal } from "@halo-dev/components";
import { inject, ref } from "vue";
import { VButton, VModal, VSpace } from "@halo-dev/components";
import { inject, ref, watch, watchEffect } from "vue";
import type { User } from "@halo-dev/api-client";
import { apiClient } from "@halo-dev/admin-shared";
import cloneDeep from "lodash.clonedeep";
import { reset, submitForm } from "@formkit/core";
import { useMagicKeys } from "@vueuse/core";
import { setFocus } from "@/formkit/utils/focus";
const props = withDefaults(
defineProps<{
@ -24,35 +27,54 @@ const emit = defineEmits<{
const currentUser = inject<User>("currentUser");
interface PasswordChangeFormState {
state: {
password: string;
password_confirm?: string;
};
submitting: boolean;
password: string;
password_confirm?: string;
}
const formState = ref<PasswordChangeFormState>({
state: {
password: "",
password_confirm: "",
},
submitting: false,
const initialFormState: PasswordChangeFormState = {
password: "",
password_confirm: "",
};
const formState = ref<PasswordChangeFormState>(cloneDeep(initialFormState));
const saving = ref(false);
const { Command_Enter } = useMagicKeys();
watchEffect(() => {
if (Command_Enter.value && props.visible) {
submitForm("password-form");
}
});
const handleVisibleChange = (visible: boolean) => {
watch(
() => props.visible,
(visible) => {
if (visible) {
setFocus("passwordInput");
} else {
handleResetForm();
}
}
);
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible);
if (!visible) {
formState.value.state.password = "";
formState.value.state.password_confirm = "";
emit("close");
}
};
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
reset("password-form");
};
const handleChangePassword = async () => {
try {
formState.value.submitting = true;
saving.value = true;
const changePasswordRequest = cloneDeep(formState.value.state);
const changePasswordRequest = cloneDeep(formState.value);
delete changePasswordRequest.password_confirm;
if (props.user?.metadata.name === currentUser?.metadata.name) {
@ -67,11 +89,11 @@ const handleChangePassword = async () => {
});
}
handleVisibleChange(false);
onVisibleChange(false);
} catch (e) {
console.error(e);
} finally {
formState.value.submitting = false;
saving.value = false;
}
};
</script>
@ -81,16 +103,18 @@ const handleChangePassword = async () => {
:visible="visible"
:width="500"
title="密码修改"
@update:visible="handleVisibleChange"
@update:visible="onVisibleChange"
>
<FormKit
id="password-form"
v-model="formState.state"
v-model="formState"
:actions="false"
type="form"
:config="{ validationVisibility: 'submit' }"
@submit="handleChangePassword"
>
<FormKit
id="passwordInput"
label="新密码"
name="password"
type="password"
@ -104,16 +128,16 @@ const handleChangePassword = async () => {
></FormKit>
</FormKit>
<template #footer>
<VButton
:loading="formState.submitting"
type="secondary"
@click="$formkit.submit('password-form')"
>
<template #icon>
<IconSave class="h-full w-full" />
</template>
保存
</VButton>
<VSpace>
<VButton
:loading="saving"
type="secondary"
@click="$formkit.submit('password-form')"
>
提交 +
</VButton>
<VButton @click="onVisibleChange(false)"> Esc</VButton>
</VSpace>
</template>
</VModal>
</template>