UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

269 lines • 11.1 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { serializable } from "../../engine/engine_serialization_decorator.js"; import { FrameEvent } from "../../engine/engine_setup.js"; import { DeviceUtilities, getParam } from "../../engine/engine_utils.js"; import { Behaviour, GameObject } from "../Component.js"; import { EventList } from "../EventList.js"; import { Text } from "./Text.js"; import { tryGetUIComponent } from "./Utils.js"; const debug = getParam("debuginputfield"); /** * @category User Interface * @group Components */ export class InputField extends Behaviour { get text() { return this.textComponent?.text ?? ""; } set text(value) { if (this.textComponent) { this.textComponent.text = value; if (this.placeholder) { if (value.length > 0) this.placeholder.gameObject.visible = false; else this.placeholder.gameObject.visible = true; } } } get isFocused() { return InputField.active === this; } textComponent; placeholder; onValueChanged; onEndEdit; static active = null; static activeTime = -1; static htmlField = null; static htmlFieldFocused = false; inputEventFn; _iosEventFn; start() { if (debug) console.log(this.name, this); } onEnable() { if (!InputField.htmlField) { InputField.htmlField = document.createElement("input"); // we can not use visibility or display because it won't receive input then InputField.htmlField.style.width = "0px"; InputField.htmlField.style.height = "0px"; InputField.htmlField.style.padding = "0px"; InputField.htmlField.style.border = "none"; InputField.htmlField.style.overflow = "hidden"; InputField.htmlField.style.caretColor = "transparent"; InputField.htmlField.style.outline = "none"; InputField.htmlField.classList.add("ar"); InputField.htmlField.onfocus = () => InputField.htmlFieldFocused = true; InputField.htmlField.onblur = () => InputField.htmlFieldFocused = false; // TODO: instead of this we should add it to the shadowdom? document.body.append(InputField.htmlField); } if (!this.inputEventFn) { this.inputEventFn = this.onInput.bind(this); } // InputField.htmlField.addEventListener("input", this.mobileInputEventListener); InputField.htmlField.addEventListener("keyup", this.inputEventFn); // InputField.htmlField.addEventListener("change", this.inputEventFn); if (this.placeholder && this.textComponent?.text.length) { GameObject.setActive(this.placeholder.gameObject, false); } if (DeviceUtilities.isiOS()) { this._iosEventFn = this.processInputOniOS.bind(this); window.addEventListener("click", this._iosEventFn); } } onDisable() { // InputField.htmlField?.removeEventListener("input", this.mobileInputEventListener); InputField.htmlField?.removeEventListener("keyup", this.inputEventFn); // InputField.htmlField?.removeEventListener("change", this.inputEventFn); this.onDeselected(); if (this._iosEventFn) { window.removeEventListener("click", this._iosEventFn); } } /** Clear the input field if it's currently active */ clear() { if (InputField.active === this && InputField.htmlField) { InputField.htmlField.value = ""; this.setTextFromInputField(); } else { if (this.textComponent) this.textComponent.text = ""; if (this.placeholder) GameObject.setActive(this.placeholder.gameObject, true); } } /** Select the input field, set it active to receive keyboard input */ select() { this.onSelected(); } /** Deselect the input field, stop receiving keyboard input */ deselect() { this.onDeselected(); } onPointerEnter(_args) { const canSetCursor = _args.event.pointerType === "mouse" && _args.button === 0; if (canSetCursor) this.context.input.setCursor("text"); } onPointerExit(_args) { this.context.input.unsetCursor("text"); } onPointerClick(_args) { if (debug) console.log("CLICK", _args, InputField.active); InputField.activeTime = this.context.time.time; if (InputField.active !== this) { this.startCoroutine(this.activeLoop(), FrameEvent.LateUpdate); } this.selectInputField(); } *activeLoop() { this.onSelected(); while (InputField.active === this) { if (this.context.input.getPointerClicked(0)) { if (this.context.time.time - InputField.activeTime > 0.2) { break; } } this.setTextFromInputField(); yield; } this.onDeselected(); } onSelected() { if (InputField.active === this) return; if (debug) console.log("Select", this.name, this, InputField.htmlField, this.context.isInXR, this.context.arOverlayElement, this.textComponent?.text, InputField.htmlField?.value); InputField.active?.onDeselected(); InputField.active = this; if (this.placeholder) GameObject.setActive(this.placeholder.gameObject, false); if (InputField.htmlField) { InputField.htmlField.value = this.textComponent?.text || ""; if (debug) console.log("set input field value", InputField.htmlField.value); if (this.context.isInXR) { const overlay = this.context.arOverlayElement; if (overlay) { overlay.append(InputField.htmlField); } } this.selectInputField(); } } onDeselected() { if (InputField.active !== this) return; InputField.active = null; if (debug) console.log("Deselect", this.name, this); if (InputField.htmlField) { InputField.htmlField.blur(); document.body.append(InputField.htmlField); } if (this.placeholder && (!this.textComponent || this.textComponent.text.length <= 0)) GameObject.setActive(this.placeholder.gameObject, true); if (InputField.htmlField) this.onEndEdit?.invoke(InputField.htmlField.value); } // @Marwie, I can provide this fix. But the issue seems to comes from Raycaster+EventSystem // As we rollout InputField, and no others elements is behind raycast, // ThreeMeshUI.update is not called. update() { if (InputField.active === this) { this.textComponent?.markDirty(); } } onInput(evt) { if (InputField.active !== this) return; if (debug) console.log(evt.code, evt, InputField.htmlField?.value, this.textComponent?.text); if (evt.code === "Escape" || evt.code === "Enter") { this.onDeselected(); return; } if (InputField.htmlField) { if (this.textComponent) { this.setTextFromInputField(); if (this.placeholder) { GameObject.setActive(this.placeholder.gameObject, this.textComponent.text.length <= 0); } } this.selectInputField(); } // switch (evt.inputType) { // case "insertCompositionText": // this.appendLetter(evt.data?.charAt(evt.data.length - 1) || null); // break; // case "insertText": // console.log(evt.data); // this.appendLetter(evt.data); // break; // case "deleteContentBackward": // this.deleteLetter(); // break; // } } setTextFromInputField() { if (this.textComponent && InputField.htmlField) { const oldValue = this.textComponent.text; const newValue = InputField.htmlField.value; const changed = this.textComponent.text !== InputField.htmlField.value; this.textComponent.text = InputField.htmlField.value; if (changed) { if (debug) console.log("[InputField] value changed:", newValue, oldValue); this.onValueChanged?.invoke(newValue, oldValue); } } } selectInputField() { if (InputField.htmlField) { if (debug) console.log("Focus Inputfield", InputField.htmlFieldFocused); InputField.htmlField.setSelectionRange(InputField.htmlField.value.length, InputField.htmlField.value.length); if (DeviceUtilities.isiOS()) InputField.htmlField.focus({ preventScroll: true }); else { // on Andoid if we don't focus in a timeout the keyboard will close the second time we click the InputField setTimeout(() => InputField.htmlField?.focus(), 1); } } } processInputOniOS() { // focus() on safari ios doesnt open the keyboard when not processed from dom event // so we try in a touch end event if this is hit const hits = this.context.physics.raycast(); if (!hits.length) return; const hit = hits[0]; const obj = hit.object; const component = tryGetUIComponent(obj); if (component?.gameObject === this.gameObject || component?.gameObject.parent === this.gameObject) this.selectInputField(); } } __decorate([ serializable(Text) ], InputField.prototype, "textComponent", void 0); __decorate([ serializable(Text) ], InputField.prototype, "placeholder", void 0); __decorate([ serializable(EventList) ], InputField.prototype, "onValueChanged", void 0); __decorate([ serializable(EventList) ], InputField.prototype, "onEndEdit", void 0); //# sourceMappingURL=InputField.js.map