@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
JavaScript
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