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

元素

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:字體、顏色、對齊方式和檢視樣式均適用)之外:

#文字

Prop Rust 類型 預設值 說明
value .value(..) string 文字,由應用程式控制。
defaultValue .default_value(..) string 初始文字。
placeholder .placeholder(..) string 輸入欄為空時顯示。
multiline .multiline(..) boolean false 多行;除非 submitBehavior 另有指定,否則按 return 會插入換行。
scrollbar .scrollbar(..) boolean | ScrollbarTheme true 多行輸入欄的捲軸:使用平台的指示器,或在應用程式設定後使用其內建捲軸(Scrollbar);物件則是它自己的主題,false 表示不顯示。
maxLength .max_length(..) number 用戶最多可輸入的字元數。
readonly .readonly(..) boolean false 用戶可以選取和複製文字,但不能更改。
masked .masked(..) boolean false 隱藏文字(用於密碼)。
allowFontScaling boolean true 跟隨系統文字大小。
maxFontSizeMultiplier number 0 系統縮放比例的上限(0 表示無上限)。

#鍵盤

Prop Rust 類型 預設值 說明
keyboardType .keyboard_type(KeyboardType::Email) "default" | "number-pad" | "decimal-pad" | "numeric" | "numbers-and-punctuation" | "email-address" | "phone-pad" | "url" "default" 鍵盤佈局(可用值;numbers-and-punctuation 在 iOS 上設有負號)。
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(..) boolean true 系統的修正功能。
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(..) boolean false 出現時取得焦點。

#游標與選取

Prop Rust 類型 說明
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 以同一種顏色為游標和選取範圍着色,有設定時以游標的顏色為準):

樣式鍵 Rust Class 說明
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 | TextContentType iOS 對 autoComplete 的命名("oneTimeCode"、"newPassword" 等)。
inputAccessoryViewID .input_accessory(id) string 輸入欄取得焦點時,顯示於鍵盤上方的 InputAccessoryView 的 nativeID(InputAccessoryView)。
keyboardAppearance .keyboard_appearance(KeyboardAppearance::Dark) "default" | "light" | "dark" 鍵盤的外觀;"default" 跟隨系統外觀。Android 的鍵盤跟隨系統主題。
use nsh_core::element::AutoFill;

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

#事件

事件 Rust Payload 觸發時機
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,因為 iOS 的鍵盤設有附屬工具列。