本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

Input

Input 是原生文字輸入欄位:在 iOS 與 Mac Catalyst 上是 UITextField(單行)或 UITextView(多行),在 Android 上則是 EditText。輸入、游標、選取、自動修正與自動填入皆由平台本身處理。Rust 以 nsh_base::input() 建立,TSX 則使用來自 nsh/base 的 <Input>。

el-input.tsx
el-input.tsxLive
import { type InputRef, type JSX, mount, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { button, buttonText, field, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const name = signal("");
  const password = signal("");
  const notes = signal("");
  const submitted = signal("");
  let passwordInput!: InputRef;
  const signIn = () => submitted.set(`Signed in as ${name() || "nobody"}`);
  const nameField = () => (
    <Input
      {...field()}
      value={name()}
      onChange={({ value }) => name.set(value)}
      placeholder="Ada Lovelace"
      autoCapitalize="words"
      maxLength={20}
      returnKeyType="next"
      onSubmit={() => passwordInput.focus()}
    />
  );
  const count = () => <Text style={{ fontSize: 12, color: ui().muted }}>{name().length} / 20</Text>;
  const passwordField = () => (
    <Input
      ref={passwordInput}
      {...field()}
      value={password()}
      onChange={({ value }) => password.set(value)}
      masked
      autoComplete="current-password"
      returnKeyType="done"
      onSubmit={signIn}
    />
  );
  const notesField = () => (
    <Input
      {...field({ style: { height: wide() ? 80 : 100 } })}
      multiline
      value={notes()}
      onChange={({ value }) => notes.set(value)}
      placeholder="Anything else?"
    />
  );
  const signInButton = () => (
    <View onPress={signIn} disabled={password().length === 0} {...button("primary", { style: { marginTop: 4 } })}>
      <Text style={buttonText("primary")}>Sign in</Text>
    </View>
  );
  /** A label above its field, and what describes it below. */
  const group = (label: string, input: JSX.Element, after?: JSX.Element) => (
    <View style={{ gap: 6 }}>
      <Text style={{ fontSize: 13, color: ui().muted }}>{label}</Text>
      {input}
      {after}
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>
            <Text style={{ color: ui().muted }}>Tab moves between the fields; Return in the name goes to the password</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={340} gap={16}>
                {group("Name (controlled, maxLength 20)", nameField(), count())}
                {group("Password", passwordField())}
                {group("Notes (multiline)", notesField())}
                {signInButton()}
              </Stage>
              <Inspector>
                <Panel title="value">
                  <Text style={{ fontSize: 13, color: ui().text }}>name {name() ? `“${name()}”` : "—"}</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>password {password().length} characters</Text>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>notes {notes().length} characters</Text>
                </Panel>
                <Panel title="onSubmit" grow>
                  <Text style={{ fontSize: 13, color: ui().up }}>{submitted()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 480, alignSelf: "center", padding: 20, paddingTop: 24, gap: 10 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>input</Text>

            <Text style={{ color: ui().muted }}>Name (controlled, maxLength 20)</Text>
            {nameField()}
            {count()}

            <Text style={{ color: ui().muted }}>Password</Text>
            {passwordField()}

            <Text style={{ color: ui().muted }}>Notes (multiline)</Text>
            {notesField()}

            {signInButton()}
            <Text style={{ color: ui().up }}>{submitted()}</Text>
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_base::input;
use nsh_core::prelude::*;

fn name_field() -> impl IntoElement {
    let name = signal(String::new());
    input()
        .value(name)
        .on_change(move |text: &str| name.set(text.to_string()))
        .placeholder("Your name")
        .auto_capitalize(AutoCapitalize::Words)
        .return_key_type(ReturnKeyType::Done)
        .text_base()
        .p_3()
        .border_1()
        .border_color(rgb(0xd0d7de))
        .rounded(px(8.0))
}

#受控與非受控

  • 受控:傳入 value,並在 onChange 中更新它。應用程式可以改寫使用者輸入的內容(例如去除空白、格式化):文字會立即顯示該次編輯,並在新的 value 抵達時被修正。每次原生編輯都帶有一個計數,來自應用程式的 value 只有在以最新一次編輯為基礎時才會被套用,因此較新的輸入永遠優先於較慢的更新。
  • 非受控:傳入 defaultValue(.default_value(..),或不傳),並從 onChange 或 onSubmit 讀取文字。

使用者輸入期間必須成立的規則是 props,由原生端處理:maxLength 限制長度,submitBehavior 決定 return 鍵的行為。請參閱表單與鍵盤。

#Props

除了通用 props(其中 style 的型別為 TextStyle:字型、顏色、對齊與檢視樣式皆適用)之外:

#文字

PropRust型別預設值說明
value.value(..)string文字,由應用程式控制。
defaultValue.default_value(..)string初始文字。
placeholder.placeholder(..)string輸入欄位為空時顯示。
multiline.multiline(..)booleanfalse多行;除非 submitBehavior 另有指定,否則 return 會插入換行。
scrollbar.scrollbar(..)boolean | ScrollbarThemetrue多行輸入欄位的捲軸:使用平台的指示器,或在應用程式設定後使用其內建捲軸(Scrollbar);傳入物件則為其專屬的主題,false 表示不顯示。
maxLength.max_length(..)number使用者最多可輸入的字元數。
readonly.readonly(..)booleanfalse使用者可以選取並複製文字,但無法修改。
masked.masked(..)booleanfalse文字會被隱藏(用於密碼)。
allowFontScalingbooleantrue跟隨系統文字大小。
maxFontSizeMultipliernumber0系統縮放比例的上限(0 表示無上限)。

#鍵盤

PropRust型別預設值說明
keyboardType.keyboard_type(KeyboardType::Email)"default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url""default"鍵盤配置(可用值;在 iOS 上 numbers-and-punctuation 含有減號)。
returnKeyType.return_key_type(ReturnKeyType::Next)"default" | "done" | "go" | "next" | "search" | "send""default"return 鍵的標籤。
autoCapitalize.auto_capitalize(AutoCapitalize::None)"none" | "sentences" | "words" | "characters""sentences"自動大寫。
autoCorrect.auto_correct(..)booleantrue系統的修正功能。
autoComplete.auto_fill(AutoFill::OneTimeCode)AutoComplete | TextContentType輸入欄位所存放的內容,供 iOS、Android 與網頁的自動填入使用:"username"、"current-password"、"new-password"、"one-time-code"、"email"、"tel"、"postal-code"、"cc-number"、"off"、…
pasteBehavior.paste_behavior(PasteBehavior::Report)"insert" | "report""insert"貼上時的行為:"insert" 會插入貼上的文字,"report" 則不插入任何內容,交由應用程式處理 onPaste 所回報的內容。圖片與檔案永遠不會被插入。
submitBehavior.submit_behavior(SubmitBehavior::Submit)"submit" | "blurAndSubmit" | "newline""blurAndSubmit"(單行)、"newline"(多行)return 鍵的行為:"submit" 會回報 onSubmit 並保持焦點,"blurAndSubmit" 還會取消焦點,"newline" 會插入換行(多行;單行則為提交)。在實體鍵盤上,提交型的多行輸入欄位仍可透過 Shift+Return 或 Option / Alt+Return 輸入換行。"submit" 會讓鍵盤保持顯示(例如連續傳送多則訊息)。
autoFocus.auto_focus(..)booleanfalse出現時取得焦點。

#游標與選取

PropRust型別說明
caret.caret(..)boolean是否繪製游標(預設 true;false 會隱藏游標)。
selectedRange.selected_range((start, end)){ start: number; end?: number }受控的選取範圍,以 UTF-16 單位計算;end 預設為 start(即游標)。

游標會在其視窗顯示且處於作用中時閃爍。在 Linux 的 GPUI 視窗中,若十秒內未輸入或移動游標,游標會保持顯示,與 GTK 應用程式相同,因此被擱置的焦點欄位不會消耗任何影格。

#顏色

預留位置、游標與選取高亮的顏色是樣式鍵,各有對應的 Tailwind class,在 Rust 中則為方法(UIKit 以同一種顏色為游標與選取著色,若已設定則採用游標的顏色):

樣式鍵RustClass說明
placeholderColor.placeholder_color(..)placeholder:text-*預留位置的顏色。
caretColor.caret_color(..)caret-*游標的顏色。
selectionColor.selection_color(..)selection:text-*選取高亮的顏色。
input()
    .placeholder("Search")
    .text_color(rgb(0x18181b))
    .placeholder_color(rgb(0x9ca3af))
    .caret_color(rgb(0x2563eb))
    .selection_color(rgb(0x93c5fd))

#iOS

ios 鍵Rust型別說明
textContentType.auto_fill(..)AutoComplete | TextContentTypeiOS 對 autoComplete 的命名("oneTimeCode"、"newPassword"、…)。
inputAccessoryViewID.input_accessory(id)string在輸入欄位取得焦點期間,顯示於鍵盤上方的 InputAccessoryView(InputAccessoryView)的 nativeID。
keyboardAppearance.keyboard_appearance(KeyboardAppearance::Dark)"default" | "light" | "dark"鍵盤的外觀;"default" 跟隨系統的外觀。Android 的鍵盤跟隨系統的主題。
use nsh_core::element::AutoFill;

input().auto_fill(AutoFill::OneTimeCode).keyboard_type(KeyboardType::Number)

#事件

事件RustPayload時機
onChange.on_change(|text: &str| ..){ value }使用者更改了文字:value 為目前的文字。
onSubmit.on_submit(|text: &str| ..){ text }在會提交的輸入欄位中按下 return 鍵(單行,或 submitBehavior "submit" / "blurAndSubmit")。
onFocus / onBlur.on_focus / .on_blur無輸入欄位取得/失去焦點。
onSelectionChange.on_selection_change(|s: &Selection| ..){ start, end }游標或選取範圍移動(UTF-16 單位)。
onPaste.on_paste(|e: &PasteEvent| ..){ text, html, files, uris }使用者貼上內容:包含文字、其 HTML 格式(Linux、網頁)、以 File 表示的圖片(例如螢幕截圖),以及本機檔案的 file: URL。發生於其所插入文字的 onChange 之前(表單與鍵盤)。
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }游標移動,無論是隨選取範圍移動,或因文字捲動或換行而移動:bounds 為其在輸入欄位中的位置,pageBounds 為其在視窗中的位置,以及 Overlay 的 anchor(表單與鍵盤)。
onKeyPress.on_key_press(|key: &str| ..){ key }文字變更之前:key 為輸入的文字,"Backspace" 或 "Enter"。
onContentSizeChange.on_content_size_change(|size: &Size<Points>| ..){ size }文字的尺寸在輸入時發生變化(多行時以輸入欄位的寬度計算);空的輸入欄位會回報一個空行,不論其預留位置為何。
onCompositionChange.on_composition_change(|e: &Composition| ..){ composing, text }輸入法的組字開始、變更或結束:text 為正在組字的內容(選字前的拼音、轉換前的假名),結束後為 ""。已確認的文字會在結束之前傳送到 onChange。

會隨文字增長的多行輸入欄位:

let height = signal(px(40.0));

input()
    .multiline(true)
    .on_content_size_change(move |size: &Size<Points>| {
        height.set(px((size.height.0 + 16.0).clamp(40.0, 120.0)));
    })
    .h(move || height.get())
    .px_3()
    .py_2()

#Ref 方法

方法Rust說明
focus()focus()讓輸入欄位取得焦點並顯示鍵盤。
blur()blur()取消其焦點。
setSelection(start, end?)set_selection(start, end)選取 start..end(UTF-16 單位);兩者相等時為游標。

按下 return 時將焦點移到下一個欄位:

use nsh_core::element::AutoFill;

let password = element_ref();

view()
    .child(input().return_key_type(ReturnKeyType::Next).on_submit(move |_: &str| password.focus()))
    .child(input().element_ref(&password).masked(true).auto_fill(AutoFill::Password))

來自 nsh 的 Keyboard.dismiss()(Rust 中為 dismiss_keyboard())會隱藏鍵盤,不論是哪個輸入欄位持有它。

#平台注意事項

  • 在 macOS 上,單行文字、預留位置與游標共用原生欄位的字型度量,並一起在內容框內置中。lineHeight 會參與版面配置;多行編輯器也會用它來決定段落間距。
  • 位於捲動檢視內的輸入欄位會由原生端自動避開鍵盤。在其他情況下,請使用等同 env(keyboard-inset-height) 的底部 padding 或 margin。
  • disabled 會停用輸入欄位(無法取得焦點),同時套用停用樣式;readonly 的輸入欄位則保有其焦點與選取範圍。
  • inputAccessoryViewID 適用於 iOS,因為該平台的鍵盤具有附屬列。