Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/src/formkit/formkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { singlePageSelect } from "./inputs/singlePage-select";
import { switchInput } from "./inputs/switch";
import { tagCheckbox } from "./inputs/tag-checkbox";
import { tagSelect } from "./inputs/tag-select";
import { toggle } from "./inputs/toggle";
import { userSelect } from "./inputs/user-select";
import { verificationForm } from "./inputs/verify-form";
import autoScrollToErrors from "./plugins/auto-scroll-to-errors";
Expand Down Expand Up @@ -83,6 +84,7 @@ const config: DefaultConfigOptions = {
iconify,
attachment,
switch: switchInput,
toggle,
},
locales: { zh, en },
locale: "zh",
Expand Down
137 changes: 137 additions & 0 deletions ui/src/formkit/inputs/toggle/ToggleInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { FormKitFrameworkContext } from "@formkit/core";
import { computed } from "vue";
import type { ToggleOption, ToggleValue } from ".";

const { context } = defineProps<{
context: FormKitFrameworkContext;
}>();

const options = computed<ToggleOption[]>(
() => context.node.props.options ?? []
);

const multiple = computed<boolean>(() => context.node.props.multiple ?? false);
const renderType = computed<"text" | "image" | "color">(
() => context.node.props.renderType ?? "text"
);

function isSelected(value: ToggleValue) {
const currentValue = context.node._value;

if (multiple.value) {
return (currentValue as ToggleValue[]).includes(value);
}

return currentValue === value;
}

function handleSelect(value: ToggleValue) {
console.log(context);
if (multiple.value) {
const currentValue = context.node._value as ToggleValue[];
if (currentValue.includes(value)) {
context.node.input(currentValue.filter((v) => v !== value));
} else {
context.node.input([...currentValue, value]);
}
return;
}

context.node.input(value);
}

const defaultSize = () => {
switch (renderType.value) {
case "image":
return "100px";
case "text":
return "100px";
case "color":
return "40px";
default:
return "100px";
}
};

const size = computed(() => {
const size = context.node.props.size;
if (!size) {
return defaultSize();
}

const sizeValue = Number(size);
if (Number.isNaN(sizeValue)) {
return defaultSize();
}
return `${sizeValue}px`;
});

const gap = computed(() => {
const gap = context.node.props.gap;
if (!gap) {
return "12px";
}

const gapValue = Number(gap);
if (Number.isNaN(gapValue)) {
return "12px";
}
return `${gap}px`;
});
</script>
<template>
<div class="flex flex-wrap" :style="{ gap: gap }">
<div
v-for="option in options"
:key="String(option.value)"
class="group flex cursor-pointer flex-col items-center justify-center gap-2"
@click="handleSelect(option.value)"
>
<div
class="border-2 p-0.5 transition-all duration-200"
:class="[
isSelected(option.value)
? 'border-primary shadow-md'
: 'border-transparent group-hover:border-gray-300 group-hover:shadow-sm',
renderType === 'color' ? 'rounded-full' : 'rounded-lg',
]"
>
<img
v-if="renderType === 'image'"
:src="option.render"
:alt="option.label"
:style="{ height: size }"
class="rounded-md object-cover"
/>
<div
v-if="renderType === 'color'"
:style="{ backgroundColor: option.render, height: size, width: size }"
class="rounded-full"
></div>
<span
v-if="renderType === 'text'"
class="flex items-center justify-center rounded-md p-1 text-base font-medium"
:class="
isSelected(option.value)
? 'text-primary'
: 'text-gray-600 group-hover:text-gray-900'
"
>
{{ option.render || option.label || option.value }}
</span>
</div>
<label
v-if="option.label && renderType !== 'text'"
class="cursor-pointer select-none text-sm transition-colors duration-200"
:class="
isSelected(option.value)
? 'font-semibold text-gray-900'
: 'text-gray-500 group-hover:text-gray-700'
"
>
{{ option.label }}
</label>
</div>
</div>
</template>
19 changes: 19 additions & 0 deletions ui/src/formkit/inputs/toggle/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { FormKitNode } from "@formkit/core";
import { undefine } from "@formkit/utils";

export default function toggleFeature(node: FormKitNode): void {
node.on("created", () => {
node.props.multiple = undefine(node.props.multiple) ?? false;
node.props.renderType = node.props.renderType ?? "text";

if (node.props.multiple) {
if (node.value === undefined) {
node.input([], false);
}
}

if (node.context) {
node.context.initialValue = node.value || "";
}
});
}
77 changes: 77 additions & 0 deletions ui/src/formkit/inputs/toggle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { FormKitTypeDefinition } from "@formkit/core";
import {
createSection,
help,
label,
message,
messages,
outer,
wrapper,
type FormKitInputs,
} from "@formkit/inputs";
import { defineAsyncComponent } from "vue";
import toggleFeature from "./feature";

const cmpName = "ToggleInput";

export const toggleSection = createSection("toggleSection", () => ({
$cmp: cmpName,
props: {
context: "$node.context",
},
}));

/**
* Input definition for a toggle input.
* @public
*/
export const toggle: FormKitTypeDefinition = {
/**
* The actual schema of the input, or a function that returns the schema.
*/
schema: outer(
wrapper(label("$label"), help("$help"), toggleSection()),
messages(message("$message.value"))
),

/**
* The type of node, can be a list, group, or input.
*/
type: "input",
/**
* An array of extra props to accept for this input.
*/
props: ["options", "multiple", "renderType", "size", "gap"],

features: [toggleFeature],

/**
* Additional features that make this input work.
*/
library: {
ToggleInput: defineAsyncComponent(() => import("./ToggleInput.vue")),
},
schemaMemoKey: "custom-toggle",
};

export type ToggleValue = string | number | boolean;

export type ToggleOption = {
render?: string;
label?: string;
value: string | number | boolean;
};

declare module "@formkit/inputs" {
export interface FormKitInputProps<Props extends FormKitInputs<Props>> {
toggle: {
type: "toggle";
options: ToggleOption[];
value?: ToggleValue | ToggleValue[];
multiple?: boolean;
renderType?: "text" | "image" | "color";
size?: number;
gap?: number;
};
}
}
6 changes: 6 additions & 0 deletions ui/src/formkit/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ const theme: Record<string, Record<string, string>> = {
wrapper: "flex flex-col gap-0",
help: "!mt-1",
},
toggle: {
label: textClassification.label,
wrapper: textClassification.wrapper,
help: "mb-1 !mt-0",
inner: "inline-flex items-center gap-1",
},
attachment: {
label: textClassification.label,
inner: "inline-flex w-full",
Expand Down
Loading