Skip to content

Commit

Permalink
textInput pass 3 - adding some styling for background, fix cursor \n …
Browse files Browse the repository at this point in the history
…issue, and some more cleanup (#578)

Signed-off-by: hanbollar <[email protected]>
  • Loading branch information
hanbollar authored Apr 13, 2024
1 parent e46e708 commit f4e896d
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 228 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
![The MRjs logo, an indigo and purple bowtie.](https://docs.mrjs.io/static/mrjs-logo.svg)

An extensible library of Web Components for the spatial web.

[![npm run build](https://github.com/Volumetrics-io/mrjs/actions/workflows/build.yml/badge.svg)](https://github.com/Volumetrics-io/mrjs/actions/workflows/build.yml) [![npm run test](https://github.com/Volumetrics-io/mrjs/actions/workflows/test.yml/badge.svg)](https://github.com/Volumetrics-io/mrjs/actions/workflows/test.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Volumetrics-io/mrjs/blob/main/LICENSE)
Expand Down
14 changes: 7 additions & 7 deletions dist/mr.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion samples/examples/models.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body >
<mr-app debug="true"> <!--stats="true">-->
<mr-app> <!-- debug="true"> !--stats="true">-->
<mr-panel id="panel" class="layout" data-comp-anchor="type: fixed;">
<mr-div id="navbar">
<mr-a href="https://mrjs.io" class="mrjs">
Expand Down
8 changes: 6 additions & 2 deletions samples/examples/text-style.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@font-face {
font-family: 'Roboto';
src: url('/assets/fonts/Roboto-Regular.ttf') format('truetype');
src: url('/examples-assets/fonts/Roboto-Regular.ttf') format('truetype');
font-weight: 100 900;
}

@font-face {
font-family: 'Bricolage';
src: url('/assets/fonts/BricolageGrotesque.ttf') format('truetype');
src: url('/examples-assets/fonts/BricolageGrotesque.ttf') format('truetype');
font-weight: 100 900;
}

Expand Down Expand Up @@ -194,6 +194,10 @@ mr-text {
color: rgba(24, 24, 24, 0.75);
}

.text-section {
margin-bottom: 90px;
}

.label-2 {
font-family: var(--font-title);
font-size: 1.5vw;
Expand Down
44 changes: 25 additions & 19 deletions samples/examples/text.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<html>
<head>
<meta charset="utf-8">
<title>mr.js - models</title>
<title>mr.js - text</title>
<meta name="description" content="mr.js example - text">
<script src="/mr.js"></script>
<link rel="stylesheet" type="text/css" href="models-style.css" />
<link rel="stylesheet" type="text/css" href="text-style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body >
<mr-app debug="true">
<mr-app> <!--debug="true" -->
<mr-panel id="panel" class="layout" data-comp-anchor="type: fixed;">
<mr-div id="navbar">
<mr-a href="https://mrjs.io" class="mrjs">
Expand All @@ -29,14 +29,14 @@
</mr-div>

<mr-text class="col-2 title" >
Text - TODOTODO
Text and TextInputs
</mr-text>

<mr-text class="col-2">
Text - todo description
Text runs the internet, hence it being very important when it comes to web-library creation. It includes features such as readable text and input fields for user interaction.
</mr-text>

<mr-text class="col-2 label">
<mr-text class="col-2 subtitle">
Examples
</mr-text>

Expand Down Expand Up @@ -64,34 +64,40 @@
<mr-text class="col-2 label">
mr-textarea
</mr-text>
<mr-text class="col-2">Try typing in the below...</mr-text>
<mr-textarea class="col-2" style="z-index:3;" placeholder="Hi hello I'm a bunch of text. I'm on one line with no breaks \n\nIm on another line now.">
</mr-textarea>

<mr-text class="col-2 label" >
textfield
mr-textfield
</mr-text>
<!-- <mr-textfield class="col-2">
Hi hello I'm a bunch of text. I'm on one line with no breaks
<mr-text class="col-2">Try typing in the below...</mr-text>
<mr-textfield class="col-2">
</mr-textfield>

Im on another line now.
</mr-textfield> -->
<mr-text class="col-2 subtitle" >
Text
</mr-text>

<!-- <mr-text class="col-2 subtitle" >
model
<mr-text class="col-2">
Written pure text is achieved using the text component, `mr-text` with the text written within the tags (<...>words go here<.../>).
</mr-text>

<mr-text class="col-2 subtitle" >
TextArea
</mr-text>

<mr-text class="col-2">
Model loading is achieved using the model component, `mr-model`, it has one attribute:
TextArea is a multi-line input version of `mr-text`, extending from the MRTextInput class. It is achieved using the `mr-textarea` component and follows the usual textarea attributes.
</mr-text>

<mr-text class="label col-2">
src
<mr-text class="col-2 subtitle" >
TextField
</mr-text>

<mr-text class="col-2">
You fill this out as you would any `src` attribute on an <img/> or <video/> html tag.
That is, with the path to the file to be loaded (for example `/assets/foo.glb`)
</mr-text> -->
TextField is a one-line input version of `mr-text`, extending from the MRTextInput class. It is achieved using the `mr-textfield` component and mimics the general 'input' html element.
</mr-text>

</mr-panel>

Expand Down
47 changes: 34 additions & 13 deletions src/core/componentSystems/TextSystem.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getSelectionRects, preloadFont } from 'troika-three-text';

import { MRSystem } from 'mrjs/core/MRSystem';
import { MRButtonEntity } from 'mrjs/core/entities/MRButtonEntity';
import { MREntity } from 'mrjs/core/MREntity';
import { MRSystem } from 'mrjs/core/MRSystem';
import { MRTextEntity } from 'mrjs/core/entities/MRTextEntity';
import { MRTextInputEntity } from 'mrjs/core/entities/MRTextInputEntity';
import { MRTextFieldEntity } from 'mrjs/core/entities/MRTextFieldEntity';
Expand Down Expand Up @@ -126,6 +126,12 @@ export class TextSystem extends MRSystem {
entity.textObj.maxHeight = entity.height;
entity.textObj.position.setX(-entity.width / 2);
entity.textObj.position.setY(entity.height / 2);
// background positioning and dimensions
// Always want background to be slightly bigger for input field 'niceness', but
// we dont want this to be specifically based on the margin since the user might
// have other purposes for the margin css attribute.
entity.background.scale.x = entity.textObj.scale.x + mrjsUtils.css.pxToThree(30);
entity.background.scale.y = entity.textObj.scale.y + mrjsUtils.css.pxToThree(30);
// cursor positioning and dimensions
entity.cursorStartingPosition.x = entity.textObj.position.x;
entity.cursorStartingPosition.y = entity.textObj.position.y - entity.cursorHeight / 2;
Expand Down Expand Up @@ -155,17 +161,6 @@ export class TextSystem extends MRSystem {
}
};

/**
* @function
* @description The per-frame system update call for all text items including updates for style and cleaning of content for special characters.
* @param {number} deltaTime - given timestep to be used for any feature changes
* @param {object} frame - given frame information to be used for any feature changes
*/
update(deltaTime, frame) {
// For this system, since we have the 'per entity' and 'per scene event' update calls,
// we dont need a main update call here.
}

/**
* @function
* @description Updates the style for the text's information based on compStyle and inputted css elements.
Expand All @@ -186,7 +181,11 @@ export class TextSystem extends MRSystem {
textObj.lineHeight = this.getLineHeight(entity.compStyle.lineHeight, entity);

// Color and opacity
mrjsUtils.color.setTEXTObject3DColor(textObj, entity.compStyle.color, entity.compStyle.opacity ?? 1);
// TODO - swap this to use the mrjsUtils.color.setObject3DColor function in future.
// For now since that creates a weird affect for styling (white edges), leaving as the
// current implementation. This probably just means there's a default style css thing
// we need to change before we swap.
this.setTextObject3DColor(textObj, entity.compStyle.color);

// Whitespace and Wrapping
textObj.whiteSpace = entity.compStyle.whiteSpace ?? textObj.whiteSpace;
Expand Down Expand Up @@ -324,4 +323,26 @@ export class TextSystem extends MRSystem {

return obj;
}

/**
* @function
* @description Sets the text object3D color.
* @param {object} object3D - the threejs object representation of the troika textt to be colored
* @param {string} color - the string representation of the color in rgba, hex, or name ('red') form
* @param {string} default_color - fallback color used if the system does not understand the color parameter. Defaults to black.
*/
setTextObject3DColor = function (object3D, color, default_color = '#000') {
if (color.includes('rgba')) {
const rgba = color
.substring(5, color.length - 1)
.split(',')
.map((part) => parseFloat(part.trim()));
object3D.material.color.setStyle(`rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`);

object3D.material.opacity = rgba[3];
} else {
object3D.material.color.setStyle(color ?? '#000');
}
object3D.material.needsUpdate = true;
};
}
79 changes: 33 additions & 46 deletions src/core/entities/MRTextAreaEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,64 @@ import { MRTextInputEntity } from 'mrjs/core/entities/MRTextInputEntity';
export class MRTextAreaEntity extends MRTextInputEntity {
/**
* @class
* @description Constructor for the MRTextInputEntity entity component.
*/
constructor() {
super();
// Define additional properties for handling multiline text and scrolling
this.lineHeight = 1.2; // Default line height, can be adjusted as needed
this.scrollOffset = 0; // The vertical scroll position
this.maxVisibleLines = 10; // Maximum number of lines visible without scrolling
this.object3D.name = 'textArea';
}

/**
*
* @function
* @description Called by connected to make sure the hiddenInput dom element is created as expected.
*/
createHiddenInputElement() {
// setup
const inputElement = document.createElement('textarea');

// style
inputElement.style.position = 'absolute';
inputElement.style.left = '-9999px'; // Position off-screen
inputElement.style.height = '1px';
inputElement.style.width = '1px';
inputElement.style.overflow = 'hidden';
document.body.appendChild(inputElement); // Ensure it's part of the DOM for event capturing

// Ensure it's part of the DOM for event capturing
this.shadowRoot.appendChild(inputElement);
this.hiddenInput = inputElement;
}

/**
*
* @function
* @description Called by connected after createHiddenInputElement to fill
* it in with the user's given attribute information.
*/
fillInHiddenInputElementWithUserData() {
// name: The name associated with the <textarea> for form submission and backend processing.
this.hiddenInput.name = this.getAttribute('name') ?? undefined;
this.hiddenInput.setAttribute('name', this.getAttribute('name') ?? undefined);
// rows and cols: These attributes control the size of the <textarea> in terms of the number of text rows and columns visible.
this.hiddenInput.rows = this.getAttribute('rows') ?? undefined;
this.hiddenInput.setAttribute('rows', this.getAttribute('rows') ?? undefined);
this.hiddenInput.setAttribute('cols', this.getAttribute('cols') ?? undefined);
// placeholder: Provides a hint to the user about what they should type into the <textarea>.
this.hiddenInput.placeholder = this.getAttribute('placeholder') ?? undefined;
this.hiddenInput.setAttribute('placeholder', this.getAttribute('placeholder') ?? '');
// readonly: Makes the <textarea> uneditable, allowing the text to be only read, not modified.
this.hiddenInput.readonly = this.getAttribute('readonly') ?? undefined;
this.hiddenInput.setAttribute('readonly', this.getAttribute('readonly') ?? false);
// disabled: Disables the text area so it cannot be interacted with or submitted.
this.hiddenInput.disabled = this.getAttribute('disabled') ?? undefined;
this.hiddenInput.setAttribute('disabled', this.getAttribute('disabled') ?? false);
// maxlength: Specifies the maximum number of characters that the user can enter.
this.hiddenInput.maxlength = this.getAttribute('maxlength') ?? undefined;
this.hiddenInput.setAttribute('maxlength', this.getAttribute('maxlength') ?? undefined);
// wrap: Controls how text is wrapped in the textarea, with values like soft and hard affecting form submission.
this.hiddenInput.wrap = this.getAttribute('wrap') ?? undefined;
this.hiddenInput.setAttribute('wrap', this.getAttribute('wrap') ?? undefined);
// overflowwrap : Controls how wrap breaks, at whitespace characters or in the middle of words.
this.hiddenInput.overflowWrap = this.getAttribute('overflowWrap') ?? undefined;
this.hiddenInput.setAttribute('overflowWrap', this.getAttribute('overflowWrap') ?? undefined);
// whitespace : Controls if text wraps with the overflowWrap feature or not.
this.hiddenInput.whiteSpace = this.getAttribute('whitespace') ?? undefined;
this.hiddenInput.setAttribute('whitespace', this.getAttribute('whitespace') ?? undefined);
}

/**
*
* @function
* @description Used on event trigger to update the textObj visual based on
* the hiddenInput DOM element.
*/
updateTextDisplay() {
// XXX - add scrolling logic in here for areas where text is greater than
Expand All @@ -71,9 +79,8 @@ export class MRTextAreaEntity extends MRTextInputEntity {
}

/**
* Handles keydown events for scrolling and cursor navigation. Note
* that this is different than an input event which for our purposes,
* handles the non-navigation key-presses.
* @function
* @description Called by the keydown event trigger. Handles the arrow key movements.
* @param {event} event - the keydown event
*/
handleKeydown(event) {
Expand All @@ -82,30 +89,12 @@ export class MRTextAreaEntity extends MRTextInputEntity {
const isUpArrow = keyCode === 38;
const isRightArrow = keyCode === 39;
const isDownArrow = keyCode === 40;
const isBackspace = keyCode === 8;
const isDelete = keyCode === 46;
const isEnter = keyCode === 13;

if (!(isLeftArrow || isUpArrow || isRightArrow || isDownArrow || isBackspace || isDelete || isEnter)) {
// not special event, then handle as normal input
this.updateTextDisplay();
}

let fromCursorMove = !(isBackspace || isDelete || isEnter);

// Handle Special Keys, then Up/Down, then Left/Right
// as some may trigger the others being required
// based on assumed implementations for them for the
// textarea dom element.

if (isBackspace || isDelete) {
this.updateTextDisplay();
this.updateCursorPosition(fromCursorMove);
return;
} else if (isEnter) {
this.updateCursorPosition(fromCursorMove, true);
return;
}
// We need to handle the up/down arrows in a special way here; otherwise,
// they'll default to the left/right implementation.
//
// And in all cases, we need to update the selction points and the cursor
// position here.

// Some shared variables
const cursorIndex = this.hiddenInput.selectionStart;
Expand Down Expand Up @@ -156,9 +145,7 @@ export class MRTextAreaEntity extends MRTextInputEntity {
this.hiddenInput.selectionEnd = this.hiddenInput.selectionStart;

// Ensure the cursor position is updated to reflect the current caret position
setTimeout(() => {
this.updateCursorPosition(fromCursorMove);
}, 0);
this.updateCursorPosition(true);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/core/entities/MRTextEntity.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ export class MRTextEntity extends MRDivEntity {
}

/**
*
* @param {object} textObj - the textobj
* @function
* @description Helper method for debugging the textObj visual information
* @param {object} textObj - the textobj threejs renderable visual. Handled by troika.
* If no object is passed, defers to the textObj within this entity.
*/
printCurrentTextDebugInfo(textObj) {
if (!textObj) {
Expand Down
Loading

0 comments on commit f4e896d

Please sign in to comment.