import { DefaultProps } from '@motajs/render-vue'; import { computed, defineComponent, onUnmounted, ref, watch } from 'vue'; import { TextContent, TextContentProps } from './textbox'; import { RectRCircleParams } from '@motajs/render-elements'; import { Container, ElementLocator, MotaRenderer, RenderItem, Transform } from '@motajs/render-core'; import { Font } from '@motajs/render-style'; import { transitionedColor } from '../use'; import { linear } from 'mutate-animate'; import { Background, Selection } from './misc'; import { GameUI, IUIMountable, SetupComponentOptions } from '@motajs/system-ui'; export interface InputProps extends DefaultProps, Partial { /** 输入框的提示内容 */ placeholder?: string; /** 输入框的值 */ value?: string; /** 是否是多行输入,多行输入时,允许换行 */ multiline?: boolean; /** 边框颜色 */ border?: string; /** 边框圆角 */ circle?: RectRCircleParams; /** 边框宽度 */ borderWidth?: number; /** 内边距 */ pad?: number; } export type InputEmits = { /** * 当输入框的值被确认时触发,例如失焦时 * @param value 输入框的值 */ change: (value: string) => void; /** * 当输入框的值发生改变时触发,例如输入了一个字符,或者删除了一个字母 * @param value 输入框的值 */ input: (value: string) => void; 'update:value': (value: string) => void; }; const inputProps = { props: ['placeholder', 'value', 'multiline'], emits: ['change', 'input', 'update:value'] } satisfies SetupComponentOptions; /** * 输入组件,点击后允许编辑。参数参考 {@link InputProps},事件参考 {@link InputEmits}。 * 完全继承 TextContent 组件的参数,用于控制输入内容的显示方式。用法示例: * ```tsx * const inputValue = ref(''); * * ``` */ export const Input = defineComponent( (props, { attrs, emit }) => { let ele: HTMLInputElement | HTMLTextAreaElement | null = null; const value = ref(props.value ?? ''); const root = ref(); const width = computed(() => props.loc?.[2] ?? 200); const height = computed(() => props.loc?.[3] ?? 200); const showText = computed(() => value.value || props.placeholder || ''); const padding = computed(() => props.pad ?? 4); const textLoc = computed(() => [ padding.value, padding.value, width.value - padding.value * 2, height.value - padding.value * 2 ]); const borderColor = transitionedColor( props.border ?? '#ddd', 200, linear() )!; const listenInput = () => { if (!ele) return; ele.addEventListener('input', () => { if (ele) { updateInput(ele.value); } }); ele.addEventListener('change', () => { if (ele) { updateValue(ele.value); } }); ele.addEventListener('blur', () => { ele?.remove(); }); }; const createInput = (mul: boolean) => { if (mul) { ele = document.createElement('textarea'); } else { ele = document.createElement('input'); } // See file://./../../../../../src/styles.less ele.classList.add('motajs-input-element'); listenInput(); }; const updateValue = (newValue: string) => { value.value = newValue; emit('update:value', newValue); emit('change', newValue); }; const updateInput = (newValue: string) => { value.value = newValue; emit('update:value', newValue); emit('input', newValue); }; const click = () => { if (!ele) createInput(props.multiline ?? false); if (!ele) return; // 计算当前绝对位置 const renderer = MotaRenderer.get('render-main'); const canvas = renderer?.getCanvas(); if (!canvas) return; const chain: RenderItem[] = []; let now: RenderItem | undefined = root.value; if (!now) return; while (now) { chain.unshift(now); now = now.parent; } const { clientLeft, clientTop } = canvas; const trans = new Transform(); trans.translate(clientLeft, clientTop); trans.scale(core.domStyle.scale); for (const item of chain) { const { anchorX, anchorY, width, height } = item; trans.translate(-anchorX * width, -anchorY * height); trans.multiply(item.transform); } trans.translate(padding.value, padding.value); const [a, b, , c, d, , e, f] = trans.mat; const str = `matrix(${a},${b},${c},${d},${e},${f})`; const w = width.value * core.domStyle.scale; const h = height.value * core.domStyle.scale; const font = props.font ?? Font.defaults(); ele.style.transform = str; ele.style.width = `${w - padding.value * 2}px`; ele.style.height = `${h - padding.value * 2}px`; ele.style.font = font.string(); ele.style.color = String(props.fillStyle ?? 'white'); document.body.appendChild(ele); ele.focus(); }; const enter = () => { borderColor.set('#0ff'); }; const leave = () => { borderColor.set(props.border ?? '#ddd'); }; watch( () => props.value, newValue => { value.value = newValue ?? ''; } ); watch( () => props.multiline, value => { createInput(value ?? false); }, { immediate: true } ); onUnmounted(() => { ele?.remove(); }); return () => ( ); }, inputProps ); export interface InputBoxProps extends TextContentProps { loc: ElementLocator; input?: InputProps; winskin?: ImageIds; color?: CanvasStyle; border?: CanvasStyle; pad?: number; inputHeight?: number; text?: string; yesText?: string; noText?: string; selFont?: Font; selFill?: CanvasStyle; } export type InputBoxEmits = { /** * 当确认输入框的内容时触发 * @param value 输入框的内容 */ confirm: (value: string) => void; /** * 当取消时触发 * @param value 输入框的内容 */ cancel: (value: string) => void; } & InputEmits; const inputBoxProps = { props: [ 'loc', 'input', 'winskin', 'color', 'border', 'pad', 'inputHeight', 'text', 'yesText', 'noText', 'selFont', 'selFill', 'width' ], emits: ['confirm', 'cancel', 'change', 'input', 'update:value'] } satisfies SetupComponentOptions< InputBoxProps, InputBoxEmits, keyof InputBoxEmits >; /** * 输入框组件,与 2.x 的 myconfirm 类似,单次调用参考 {@link getInput}。用法与 `ConfirmBox` 类似。 * 参数参考 {@link InputBoxProps},事件参考 {@link InputBoxEmits},用例如下: * ```tsx * const onConfirm = (value: string) => console.log(value); * * * ``` */ export const InputBox = defineComponent< InputBoxProps, InputBoxEmits, keyof InputBoxEmits >((props, { attrs, emit }) => { const contentHeight = ref(0); const value = ref(''); const selected = ref(false); const yesSize = ref<[number, number]>([0, 0]); const noSize = ref<[number, number]>([0, 0]); const height = ref(200); const yesText = computed(() => props.yesText ?? '确认'); const noText = computed(() => props.noText ?? '取消'); const text = computed(() => props.text ?? '请输入内容:'); const padding = computed(() => props.pad ?? 8); const inputHeight = computed(() => props.inputHeight ?? 16); const inputLoc = computed(() => [ padding.value, padding.value * 2 + contentHeight.value, props.width - padding.value * 2, inputHeight.value - padding.value * 2 ]); const yesLoc = computed(() => { const y = height.value - padding.value; return [props.width / 3, y, void 0, void 0, 0.5, 1]; }); const noLoc = computed(() => { const y = height.value - padding.value; return [(props.width / 3) * 2, y, void 0, void 0, 0.5, 1]; }); const selectionLoc = computed(() => { if (selected.value) { const [x = 0, y = 0] = yesLoc.value; const [width, height] = yesSize.value; return [x, y + 4, width + 8, height + 8, 0.5, 1]; } else { const [x = 0, y = 0] = noLoc.value; const [width, height] = noSize.value; return [x, y + 4, width + 8, height + 8, 0.5, 1]; } }); const updateHeight = (h: number) => { contentHeight.value = h; height.value = h + inputHeight.value + padding.value * 4; }; const change = (value: string) => { emit('change', value); }; const input = (value: string) => { emit('update:value', value); emit('input', value); }; const setYes = (_: string, width: number, height: number) => { yesSize.value = [width, height]; }; const setNo = (_: string, width: number, height: number) => { noSize.value = [width, height]; }; const confirm = () => { emit('confirm', value.value); }; const cancel = () => { emit('cancel', value.value); }; return () => ( (selected.value = true)} onSetText={setYes} /> (selected.value = false)} onSetText={setNo} /> ); }, inputBoxProps); /** * 弹出一个输入框,然后将结果返回: * ```ts * const value = await getInput( * // 在哪个 UI 控制器上打开,对于一般 UI 组件来说,直接填写 props.controller 即可 * props.controller, * // 提示内容 * '请输入文本:', * // 输入框的位置,宽度由下一个参数指定,高度参数由组件内部计算得出,指定无效 * [240, 240, void 0, void 0, 0.5, 0.5], * // 宽度设为 240 * 240, * // 可以给选择框传入其他的 props,例如指定字体,此项可选 * { font: new Font('Verdana', 20) } * ); * // 之后,就可以根据 value 来执行不同的操作了 * console.log(value); // 输出用户输入的内容 * ``` * @param controller UI 控制器 * @param text 确认文本内容 * @param loc 确认框的位置 * @param width 确认框的宽度 * @param props 额外的 props,参考 {@link ConfirmBoxProps} */ export function getInput( controller: IUIMountable, text: string, loc: ElementLocator, width: number, props?: InputBoxProps ) { return new Promise(res => { const instance = controller.open( InputBoxUI, { ...(props ?? {}), text, loc, width, onConfirm: value => { controller.close(instance); res(value); }, onCancel: () => { controller.close(instance); res(''); } }, true ); }); } /** * 与 `getInput` 类似,不过会将结果转为数字。用法参考 {@link getInput} */ export async function getInputNumber( controller: IUIMountable, text: string, loc: ElementLocator, width: number, props?: InputBoxProps ) { const value = await getInput(controller, text, loc, width, props); return parseFloat(value); } export const InputBoxUI = new GameUI('input-box', InputBox);