UNPKG

@pmndrs/uikit

Version:

Build performant 3D user interfaces with Three.js and yoga.

117 lines (116 loc) 5.77 kB
import { boolean, enum as enumSchema, string } from 'zod'; import { createInPropertiesSchema, defineSchema, functionSchema, numberValueSchema } from '../properties/schema.js'; import { computed, signal } from '@preact/signals-core'; import { getSelectionTransformations } from '../text/index.js'; import { abortableEffect } from '../utils.js'; import { Text, textDefaults, textOutPropertiesSchema } from './text.js'; import { setupCaret } from '../text/selection/caret.js'; import { createSelection } from '../text/selection/ranges.js'; import { setupSelectionHandlers } from '../text/selection/pointer.js'; import { updateHtmlSelectionRange } from '../text/selection/state.js'; import { createHtmlInputElement, setupHtmlInputElement, setupUpdateHasFocus } from '../text/input/hidden-input.js'; export const inputOutPropertiesSchema = /* @__PURE__ */ defineSchema(() => textOutPropertiesSchema.omit({ text: true }).extend({ placeholder: string().optional(), defaultValue: string().optional(), value: string().optional(), disabled: boolean().optional(), tabIndex: numberValueSchema.optional(), autocomplete: string().optional(), type: enumSchema(['text', 'password', 'number']).optional(), onValueChange: functionSchema.optional(), onFocusChange: functionSchema.optional(), whiteSpace: enumSchema(['normal', 'collapse', 'pre', 'pre-line']).optional(), })); export const InputPropertiesSchema = /* @__PURE__ */ defineSchema(() => createInPropertiesSchema(inputOutPropertiesSchema)); export const inputDefaults = { ...textDefaults, type: 'text', disabled: false, tabIndex: 0, autocomplete: '', whiteSpace: 'pre', }; export class Input extends Text { inputConfig; element; selectionRange; hasFocus; updateSelectionRange = () => { }; uncontrolledSignal = signal(undefined); currentSignal = computed(() => this.properties.value.value ?? this.uncontrolledSignal.value ?? this.properties.value.defaultValue ?? ''); constructor(inputProperties, initialClasses, inputConfig) { const caretColor = signal(undefined); const selectionHandlers = signal(undefined); let element; const htmlSelectionRange = signal(undefined); const updateSelectionRange = () => updateHtmlSelectionRange(htmlSelectionRange, element); const hasFocus = signal(false); const selectionRange = computed(() => { if (!hasFocus.value) { return undefined; } return htmlSelectionRange.value; }); super(inputProperties, initialClasses, { defaults: inputDefaults, dynamicHandlers: selectionHandlers, hasFocus, isPlaceholder: computed(() => this.currentSignal.value.length === 0), ...inputConfig, defaultOverrides: { cursor: 'text', ...{ text: computed(() => this.currentSignal.value.length === 0 ? this.properties.value.placeholder : this.properties.value.type === 'password' ? '*'.repeat(this.currentSignal.value.length ?? 0) : this.currentSignal.value), }, caretColor, ...inputConfig?.defaultOverrides, }, }); this.inputConfig = inputConfig; this.selectionRange = selectionRange; this.hasFocus = hasFocus; this.updateSelectionRange = updateSelectionRange; abortableEffect(() => { caretColor.value = this.properties.value.color; }, this.abortSignal); setupSelectionHandlers(selectionHandlers, this.properties, this.currentSignal, this, this.textLayout, this.focus.bind(this), this.abortSignal); const textSelection = computed(() => getSelectionTransformations(this.textLayout.value, selectionRange.value)); const caretTransformation = computed(() => textSelection.value.caret); const selectionTransformations = computed(() => textSelection.value.selections); const parentClippingRect = computed(() => this.parentContainer.value?.clippingRect.value); this.element = createHtmlInputElement((newValue) => { if (this.properties.peek().value == null) { this.uncontrolledSignal.value = newValue; } this.properties.peek().onValueChange?.(newValue); }, inputConfig?.multiline ?? false, updateSelectionRange); element = this.element; setupCaret(this.properties, this.globalTextMatrix, caretTransformation, this.isVisible, this.backgroundOrderInfo, this.backgroundGroupDeps, parentClippingRect, this.root, this.abortSignal); createSelection(this.properties, this.root, this.globalTextMatrix, selectionTransformations, this.isVisible, this.backgroundOrderInfo, this.backgroundGroupDeps, parentClippingRect, this.abortSignal); setupHtmlInputElement(this.properties, this.element, this.currentSignal, this.abortSignal); setupUpdateHasFocus(this.element, this.hasFocus, (hasFocus) => { this.properties.peek().onFocusChange?.(hasFocus); }, this.abortSignal); } focus(start, end, direction) { if (!this.hasFocus.peek()) { this.element.focus(); } if (start != null && end != null) { this.element.setSelectionRange(start, end, direction); } this.updateSelectionRange(); } clone(recursive) { const cloned = new Input(this.inputProperties, this.initialClasses, this.inputConfig); this.copyInto(cloned, recursive); return cloned; } blur() { this.element.blur(); } }