@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
117 lines (116 loc) • 5.77 kB
JavaScript
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();
}
}