تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

Input

Input هو حقل نص أصلي: UITextField (سطر واحد) أو UITextView (متعدد الأسطر) على iOS وMac Catalyst، وEditText على Android. الكتابة والمؤشر والتحديد والتصحيح التلقائي والملء التلقائي كلها من المنصة نفسها. في Rust يُنشأ بواسطة nsh_base::input()، وفي TSX بواسطة <Input> من nsh/base.

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.

القواعد التي يجب أن تسري أثناء كتابة المستخدم هي خصائص تُعالَج أصليًا: maxLength يحدّ الطول، وsubmitBehavior يحدد ما يفعله زر الإدخال (return). انظر النماذج ولوحة المفاتيح.

#الخصائص

إضافةً إلى الخصائص المشتركة (مع style من النوع TextStyle: تنطبق الخطوط والألوان والمحاذاة وأنماط العرض):

#النص

الخاصيةRustالنوعالافتراضيالوصف
value.value(..)stringالنص، يتحكم به التطبيق.
defaultValue.default_value(..)stringالنص الابتدائي.
placeholder.placeholder(..)stringيظهر ما دام الحقل فارغًا.
multiline.multiline(..)booleanfalseعدة أسطر؛ يُدرج زر الإدخال فاصل سطر ما لم يحدد submitBehavior خلاف ذلك.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueشريط التمرير في الحقل المتعدد الأسطر: مؤشر المنصة، أو الشريط المضمّن في التطبيق متى ضبط التطبيق واحدًا (Scrollbar)؛ والكائن سمة مستقلة بذاته، وfalse لا شيء.
maxLength.max_length(..)numberأقصى عدد من الأحرف يستطيع المستخدم إدخاله.
readonly.readonly(..)booleanfalseيستطيع المستخدم تحديد النص ونسخه لكن لا يستطيع تغييره.
masked.masked(..)booleanfalseالنص مخفي (كلمات المرور).
allowFontScalingbooleantrueاتباع حجم نص النظام.
maxFontSizeMultipliernumber0الحد الأعلى لمقياس النظام (القيمة 0 تعني بلا حد).

#لوحة المفاتيح

الخاصية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"تسمية زر الإدخال.
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" (متعدد الأسطر)ما يفعله زر الإدخال: "submit" يبلّغ عن onSubmit ويُبقي التركيز، و"blurAndSubmit" يُزيل التركيز أيضًا، و"newline" يُدرج فاصل سطر (متعدد الأسطر؛ أما السطر الواحد فيُرسل). يظل الحقل المتعدد الأسطر الذي يُرسل يقبل فواصل الأسطر من Shift+Return أو Option / Alt+Return على لوحات المفاتيح الفعلية. "submit" يُبقي لوحة المفاتيح ظاهرة (مثلًا لإرسال عدة رسائل).
autoFocus.auto_focus(..)booleanfalseيأخذ التركيز عند ظهوره.

#المؤشر والتحديد

الخاصيةRustالنوعالوصف
caret.caret(..)booleanيُرسم المؤشر (الافتراضي true؛ وfalse يخفيه).
selectedRange.selected_range((start, end)){ start: number; end?: number }التحديد المتحكَّم به، بوحدات UTF-16؛ القيمة الافتراضية لـ end هي start (مؤشر).

يومض المؤشر ما دامت نافذته ظاهرة ونشطة. في نوافذ GPUI على Linux يبقى ظاهرًا بعد عشر ثوانٍ من دون كتابة أو تحريك، كما في تطبيقات GTK، فلا يكلّف الحقل المركَّز المتروك أي إطارات.

#الألوان

ألوان النص البديل والمؤشر وتمييز التحديد هي مفاتيح أنماط، مع أصناف Tailwind الخاصة بها، وطرق في Rust (يلوّن UIKit المؤشر والتحديد بلون واحد، هو لون المؤشر إن ضُبط):

مفتاح النمطRustالصنفالوصف
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

مفتاح iosRustالنوعالوصف
textContentType.auto_fill(..)AutoComplete | TextContentTypeأسماء iOS لـ autoComplete ("oneTimeCode"، "newPassword"، …).
inputAccessoryViewID.input_accessory(id)stringnativeID لـ InputAccessoryView (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البياناتمتى
onChange.on_change(|text: &str| ..){ value }غيّر المستخدم النص: value هو النص الآن.
onSubmit.on_submit(|text: &str| ..){ text }ضُغط زر الإدخال في حقل يُرسل (سطر واحد، أو 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، وعناوين URL من النوع file: للملفات المحلية. يقع قبل onChange للنص الذي يُدرجه (النماذج ولوحة المفاتيح).
onCaretMove.on_caret_move(|e: &CaretEvent| ..){ bounds, pageBounds }تحرك المؤشر، مع التحديد أو مع تمرير النص أو التفافه: bounds هو موضعه في الحقل، وpageBounds موضعه في النافذة، وهو anchor لـ Overlay (النماذج ولوحة المفاتيح).
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)؛ ويكون مؤشرًا عندما يتساويان.

نقل التركيز إلى الحقل التالي عند الضغط على زر الإدخال:

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))

Keyboard.dismiss() من nsh (dismiss_keyboard() في Rust) يُخفي لوحة المفاتيح أيًّا كان الحقل الذي يحملها.

#ملاحظات المنصات

  • على macOS، يتشارك النص ذو السطر الواحد والنصوص البديلة والمؤشرات مقاييس خط الحقل الأصلي، وتُوسَّط معًا داخل صندوق المحتوى. lineHeight يشارك في التخطيط؛ وتستخدمه المحررات المتعددة الأسطر أيضًا للتباعد بين الفقرات.
  • تُبعَد الحقول داخل scroll view عن لوحة المفاتيح أصليًا. في غير ذلك استخدم حشوة سفلية أو هامشًا سفليًا بقيمة env(keyboard-inset-height).
  • disabled يعطّل الحقل (بلا تركيز) إضافةً إلى تطبيق أنماط التعطيل؛ أما الحقل readonly فيحتفظ بتركيزه وتحديده.
  • inputAccessoryViewID ينطبق على iOS، حيث تحتوي لوحة المفاتيح على شريط ملحقات.