From f4e896ddb7b670929d35f6a03cc695587050282c Mon Sep 17 00:00:00 2001 From: Hannah Bollar Date: Fri, 12 Apr 2024 17:42:17 -0700 Subject: [PATCH] textInput pass 3 - adding some styling for background, fix cursor \n issue, and some more cleanup (#578) Signed-off-by: hanbollar --- README.md | 2 +- dist/mr.js | 14 +- samples/examples/models.html | 2 +- samples/examples/text-style.css | 8 +- samples/examples/text.html | 44 ++-- src/core/componentSystems/TextSystem.js | 47 +++-- src/core/entities/MRTextAreaEntity.js | 79 +++---- src/core/entities/MRTextEntity.js | 6 +- src/core/entities/MRTextFieldEntity.js | 95 ++++++--- src/core/entities/MRTextInputEntity.js | 265 ++++++++++++++++-------- src/defaultStyle.css | 17 +- src/utils/Color.js | 15 -- 12 files changed, 366 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 45591be7..a0dec386 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/dist/mr.js b/dist/mr.js index fef9f2f3..49bcb2b0 100644 --- a/dist/mr.js +++ b/dist/mr.js @@ -27,7 +27,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `mr-app {\n display: block;\n height: 100vh;\n width: 100%;\n}\n\n.inXR {\n width: 100vw;\n}\n\nmr-app * {\n box-sizing: border-box;\n opacity: 0%;\n}\n\nmr-app > canvas {\n position:fixed;\n visibility:visible;\n opacity: 100%;\n z-index: 999;\n}\n\nmr-div, mr-button, mr-img, mr-video, mr-a, mr-text, mr-textarea, mr-textfield, mr-stats {\n display: inline-block;\n position: relative;\n z-index: inherit;\n opacity: 100%;\n}\n\nmr-text, mr-textarea, mr-textfield, mr-stats {\n line-height: 100%;\n font-size: 16px;\n}\n\nmr-panel {\n background-color: #fff;\n border-radius: 2%;\n position: fixed;\n overflow: auto;\n height: 100vh;\n width: 100vw;\n}\n\nmr-img, mr-video {\n object-fit: contain;\n}\n\nmr-button {\n padding: 5px;\n text-align: center;\n vertical-align: middle;\n background-color: #8a8a8a;\n border-radius: 0.5%;\n /* animation: back 0.25s ease-out forwards; */\n width: fit-content;\n}\n\nmr-button.hover {\n background-color: #333;\n z-index: 5; /* end */\n /* animation: forward 0.25s ease-in forwards; */\n}\n\nmr-a {\n color: darkblue;\n}\n\nmr-a.hover {\n color: blue;\n z-index: 5;\n}\n\nmr-a.active {\n color: purple;\n\n}\n\n/* these work differently from in XR than expected, laurent should look into it */\n/* button animation! */\n\n@keyframes forward {\n to {\n z-index: 5; /* end */\n }\n}\n\n@keyframes back {\n to {\n z-index: 1; /* end */\n }\n}\n\n\nbody {\n margin: 0; /* unclear why we need this */\n}\n`, \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://mrjs/./src/defaultStyle.css?./node_modules/css-loader/dist/cjs.js"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, `mr-app {\n display: block;\n height: 100vh;\n width: 100%;\n}\n\n.inXR {\n width: 100vw;\n}\n\nmr-app * {\n box-sizing: border-box;\n opacity: 0%;\n}\n\nmr-app > canvas {\n position:fixed;\n visibility:visible;\n opacity: 100%;\n z-index: 999;\n}\n\nmr-div, mr-button, mr-img, mr-video, mr-a, mr-text, mr-textarea, mr-textfield, mr-stats {\n display: inline-block;\n position: relative;\n z-index: inherit;\n opacity: 100%;\n}\n\nmr-text, mr-stats {\n line-height: 100%;\n font-size: 16px;\n}\n\nmr-textarea, mr-textfield {\n background-color: rgba(255, 255, 255, 0.75);\n font-size: 16px;\n border-radius: 1%;\n}\n\nmr-textfield {\n min-height: 2em;\n font-size: 100%;\n}\n\nmr-textarea {\n min-height: 7em;\n}\n\nmr-panel {\n background-color: #fff;\n border-radius: 2%;\n position: fixed;\n overflow: auto;\n height: 100vh;\n width: 100vw;\n}\n\nmr-img, mr-video {\n object-fit: contain;\n}\n\nmr-button {\n padding: 5px;\n text-align: center;\n vertical-align: middle;\n background-color: #8a8a8a;\n border-radius: 0.5%;\n /* animation: back 0.25s ease-out forwards; */\n width: fit-content;\n}\n\nmr-button.hover {\n background-color: #333;\n z-index: 5; /* end */\n /* animation: forward 0.25s ease-in forwards; */\n}\n\nmr-a {\n color: darkblue;\n}\n\nmr-a.hover {\n color: blue;\n z-index: 5;\n}\n\nmr-a.active {\n color: purple;\n\n}\n\n/* these work differently from in XR than expected, laurent should look into it */\n/* button animation! */\n\n@keyframes forward {\n to {\n z-index: 5; /* end */\n }\n}\n\n@keyframes back {\n to {\n z-index: 1; /* end */\n }\n}\n\n\nbody {\n margin: 0; /* unclear why we need this */\n}\n`, \"\"]);\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://mrjs/./src/defaultStyle.css?./node_modules/css-loader/dist/cjs.js"); /***/ }), @@ -652,7 +652,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ TextSystem: () => (/* binding */ TextSystem)\n/* harmony export */ });\n/* harmony import */ var troika_three_text__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! troika-three-text */ \"./node_modules/troika-three-text/dist/troika-three-text.esm.js\");\n/* harmony import */ var mrjs_core_MRSystem__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! mrjs/core/MRSystem */ \"./src/core/MRSystem.js\");\n/* harmony import */ var mrjs_core_entities_MRButtonEntity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! mrjs/core/entities/MRButtonEntity */ \"./src/core/entities/MRButtonEntity.js\");\n/* harmony import */ var mrjs_core_MREntity__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! mrjs/core/MREntity */ \"./src/core/MREntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextEntity__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! mrjs/core/entities/MRTextEntity */ \"./src/core/entities/MRTextEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! mrjs/core/entities/MRTextInputEntity */ \"./src/core/entities/MRTextInputEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextFieldEntity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! mrjs/core/entities/MRTextFieldEntity */ \"./src/core/entities/MRTextFieldEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextAreaEntity__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! mrjs/core/entities/MRTextAreaEntity */ \"./src/core/entities/MRTextAreaEntity.js\");\n/* harmony import */ var mrjs__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! mrjs */ \"./src/index.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * @class TextSystem\n * @classdesc Handles text creation and font rendering for `mr-text`, `mr-textfield`, and `mr-textarea` with a starting framerate of 1/30.\n * @augments MRSystem\n */\nclass TextSystem extends mrjs_core_MRSystem__WEBPACK_IMPORTED_MODULE_0__.MRSystem {\n /**\n * @class\n * @description TextSystem's default constructor\n */\n constructor() {\n super(false);\n\n // Setup all the preloaded fonts\n this.preloadedFonts = {};\n const styleSheets = Array.from(document.styleSheets);\n styleSheets.forEach((styleSheet) => {\n const cssRules = Array.from(styleSheet.cssRules);\n // all the font-faces rules\n const rulesFontFace = cssRules.filter((rule) => rule.cssText.startsWith('@font-face'));\n\n rulesFontFace.forEach((fontFace) => {\n const fontData = this.parseFontFace(fontFace.cssText);\n\n (0,troika_three_text__WEBPACK_IMPORTED_MODULE_8__.preloadFont)(\n {\n font: fontData.src,\n },\n () => {\n this.preloadedFonts[fontFace.style.getPropertyValue('font-family')] = fontData.src;\n document.dispatchEvent(new CustomEvent('font-loaded'));\n }\n );\n });\n });\n\n // Handle text style needs update\n this.app.addEventListener('trigger-text-style-update', (e) => {\n // The event has the entity stored as its detail.\n if (e.detail !== undefined) {\n this._updateSpecificEntity(e.detail);\n }\n });\n }\n\n /**\n * @function\n * @description When a new entity is created, adds it to the physics registry and initializes the physics aspects of the entity.\n * @param {MREntity} entity - the entity being set up\n */\n onNewEntity(entity) {\n if (!(entity instanceof mrjs_core_entities_MRTextEntity__WEBPACK_IMPORTED_MODULE_3__.MRTextEntity)) {\n return;\n }\n this.registry.add(entity);\n this._updateSpecificEntity(entity);\n }\n\n /**\n * @function\n * @param {object} entity - the entity that needs to be updated.\n * @description The per entity triggered update call. Handles updating all text items including updates for style and cleaning of content for special characters.\n */\n _updateSpecificEntity(entity) {\n this.checkIfTextContentChanged(entity);\n this.handleTextContentUpdate(entity);\n }\n\n /**\n *\n * @param {object} entity - checks if the content changed and if so, updates it to match.\n * @returns {boolean} true if the content needed to be updated, false otherwise.\n */\n checkIfTextContentChanged(entity) {\n // Add a check in case a user manually updates the text value\n let text =\n entity instanceof mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__.MRTextInputEntity\n ? entity.hiddenInput?.value ?? false\n : // troika honors newlines/white space\n // we want to mimic h1, p, etc which do not honor these values\n // so we have to clean these from the text\n // ref: https://github.com/protectwise/troika/issues/289#issuecomment-1841916850\n entity.textContent\n .replace(/(\\n)\\s+/g, '$1')\n .replace(/(\\r\\n|\\n|\\r)/gm, ' ')\n .trim();\n\n if (entity.textObj.text != text) {\n entity.textObj.text = text;\n return true;\n }\n return false;\n }\n\n /**\n *\n * @param {object} entity - the entity whose content updated.\n */\n handleTextContentUpdate(entity) {\n this.updateStyle(entity);\n\n // The sync step ensures troika's text render info and geometry is up to date\n // with any text content changes.\n entity.textObj.sync(() => {\n if (entity instanceof mrjs_core_entities_MRButtonEntity__WEBPACK_IMPORTED_MODULE_1__.MRButtonEntity) {\n // MRButtonEntity\n\n entity.textObj.anchorX = 'center';\n } else if (entity instanceof mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__.MRTextInputEntity) {\n // MRTextAreaEntity, MRTextFieldEntity, etc\n\n // textObj positioning and dimensions\n entity.textObj.maxWidth = entity.width;\n entity.textObj.maxHeight = entity.height;\n entity.textObj.position.setX(-entity.width / 2);\n entity.textObj.position.setY(entity.height / 2);\n // cursor positioning and dimensions\n entity.cursorStartingPosition.x = entity.textObj.position.x;\n entity.cursorStartingPosition.y = entity.textObj.position.y - entity.cursorHeight / 2;\n // handle activity\n if (entity == document.activeElement) {\n entity.updateCursorPosition();\n } else {\n entity.blur();\n }\n } else {\n // MRTextEntity\n\n entity.textObj.position.setX(-entity.width / 2);\n entity.textObj.position.setY(entity.height / 2);\n }\n });\n }\n\n /**\n * @function\n * @description The per global scene event update call. Handles updating all text items including updates for style and cleaning of content for special characters.\n */\n eventUpdate = () => {\n for (const entity of this.registry) {\n this.checkIfTextContentChanged(entity);\n this.handleTextContentUpdate(entity);\n }\n };\n\n /**\n * @function\n * @description The per-frame system update call for all text items including updates for style and cleaning of content for special characters.\n * @param {number} deltaTime - given timestep to be used for any feature changes\n * @param {object} frame - given frame information to be used for any feature changes\n */\n update(deltaTime, frame) {\n // For this system, since we have the 'per entity' and 'per scene event' update calls,\n // we dont need a main update call here.\n }\n\n /**\n * @function\n * @description Updates the style for the text's information based on compStyle and inputted css elements.\n * @param {MRTextEntity} entity - the text entity whose style is being updated\n */\n updateStyle = (entity) => {\n const { textObj } = entity;\n\n // Font\n textObj.font = textObj.text.trim().length != 0 ? this.preloadedFonts[entity.compStyle.fontFamily] : null;\n textObj.fontSize = this.parseFontSize(entity.compStyle.fontSize, entity);\n textObj.fontWeight = this.parseFontWeight(entity.compStyle.fontWeight);\n textObj.fontStyle = entity.compStyle.fontStyle;\n\n // Alignment\n textObj.anchorY = this.getVerticalAlign(entity.compStyle.verticalAlign, entity);\n textObj.textAlign = this.getTextAlign(entity.compStyle.textAlign);\n textObj.lineHeight = this.getLineHeight(entity.compStyle.lineHeight, entity);\n\n // Color and opacity\n mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.color.setTEXTObject3DColor(textObj, entity.compStyle.color, entity.compStyle.opacity ?? 1);\n\n // Whitespace and Wrapping\n textObj.whiteSpace = entity.compStyle.whiteSpace ?? textObj.whiteSpace;\n textObj.maxWidth = entity.width * 1.001;\n\n // Offset position for visibility on top of background plane\n textObj.position.z = 0.0001;\n };\n\n /**\n * @function\n * @description Handles when text is added as an entity updating content and style for the internal textObj appropriately.\n * @param {MRTextEntity} entity - the text entity being updated\n */\n addText = (entity) => {\n const text = entity.textContent.trim();\n entity.textObj.text = text.length > 0 ? text : ' ';\n\n this.updateStyle(entity);\n };\n\n /**\n * @function\n * @description parses the font weight as 'bold', 'normal', etc based on the given weight value\n * @param {number} weight - the numerical representation of the font-weight\n * @returns {string} - the enum of 'bold', 'normal', etc\n */\n parseFontWeight(weight) {\n if (weight >= 500) {\n return 'bold';\n }\n return 'normal';\n }\n\n /**\n * @function\n * @description parses the font size based on its `XXpx` value and converts it to a usable result based on the virtual display resolution\n * @param {number} val - the value being adjusted\n * @param {object} el - the css element handler\n * @returns {number} - the font size adjusted for the display as expected\n */\n parseFontSize(val, el) {\n const result = parseFloat(val.split('px')[0]) / mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.display.VIRTUAL_DISPLAY_RESOLUTION;\n return result;\n }\n\n /**\n * @function\n * @description Gets the vertical align\n * @param {number} verticalAlign - the numerical representation in pixel space of the vertical Align\n * @param {MREntity} entity - the entity whose comp style (css) is relevant\n * @returns {string} - the string representation of the the verticalAlign\n */\n getVerticalAlign(verticalAlign, entity) {\n let result = verticalAlign;\n\n if (typeof result === 'number') {\n result /= mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(entity.compStyle.fontSize);\n }\n\n switch (result) {\n case 'baseline':\n case 'sub':\n case 'super':\n return 0;\n case 'text-top':\n return 'top-ex';\n case 'text-bottom':\n return 'bottom';\n case 'middle':\n default:\n return result;\n }\n }\n\n /**\n * @function\n * @description Gets the line height\n * @param {number} lineHeight - the numerical representation in pixel space of the line height\n * @param {MREntity} entity - the entity whose comp style (css) is relevant\n * @returns {number} - the numerical representation of the the lineHeight\n */\n getLineHeight(lineHeight, entity) {\n let result = mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(lineHeight);\n\n if (typeof result === 'number') {\n result /= mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(entity.compStyle.fontSize);\n }\n\n return result;\n }\n\n /**\n * @function\n * @description Gets the text alignment string\n * @param {string} textAlign - handles values for `start`, `end`, `left`, and `right`; otherwise, defaults to the same input as `textAlign`.\n * @returns {string} - the resolved `textAlign`.\n */\n getTextAlign(textAlign) {\n if (textAlign == 'start') {\n return 'left';\n } else if (textAlign == 'end') {\n return 'right';\n }\n return textAlign;\n }\n\n /**\n * @function\n * @description Based on the given font-face value in the passed cssString, tries to either use by default or download the requested font-face\n * for use by the text object.\n * @param {string} cssString - the css string to be parsed for the font-face css value.\n * @returns {object} - json object respresenting the preloaded font-face\n */\n parseFontFace(cssString) {\n const obj = {};\n const match = cssString.match(/@font-face\\s*{\\s*([^}]*)\\s*}/);\n\n if (match) {\n const fontFaceAttributes = match[1];\n const attributes = fontFaceAttributes.split(';');\n\n attributes.forEach((attribute) => {\n const [key, value] = attribute.split(':').map((item) => item.trim());\n if (key === 'src') {\n const urlMatch = value.match(/url\\(\"([^\"]+)\"\\)/);\n if (urlMatch) {\n obj[key] = urlMatch[1];\n }\n } else {\n obj[key] = value;\n }\n });\n }\n\n return obj;\n }\n}\n\n\n//# sourceURL=webpack://mrjs/./src/core/componentSystems/TextSystem.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ TextSystem: () => (/* binding */ TextSystem)\n/* harmony export */ });\n/* harmony import */ var troika_three_text__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! troika-three-text */ \"./node_modules/troika-three-text/dist/troika-three-text.esm.js\");\n/* harmony import */ var mrjs_core_entities_MRButtonEntity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! mrjs/core/entities/MRButtonEntity */ \"./src/core/entities/MRButtonEntity.js\");\n/* harmony import */ var mrjs_core_MREntity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! mrjs/core/MREntity */ \"./src/core/MREntity.js\");\n/* harmony import */ var mrjs_core_MRSystem__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! mrjs/core/MRSystem */ \"./src/core/MRSystem.js\");\n/* harmony import */ var mrjs_core_entities_MRTextEntity__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! mrjs/core/entities/MRTextEntity */ \"./src/core/entities/MRTextEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! mrjs/core/entities/MRTextInputEntity */ \"./src/core/entities/MRTextInputEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextFieldEntity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! mrjs/core/entities/MRTextFieldEntity */ \"./src/core/entities/MRTextFieldEntity.js\");\n/* harmony import */ var mrjs_core_entities_MRTextAreaEntity__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! mrjs/core/entities/MRTextAreaEntity */ \"./src/core/entities/MRTextAreaEntity.js\");\n/* harmony import */ var mrjs__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! mrjs */ \"./src/index.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * @class TextSystem\n * @classdesc Handles text creation and font rendering for `mr-text`, `mr-textfield`, and `mr-textarea` with a starting framerate of 1/30.\n * @augments MRSystem\n */\nclass TextSystem extends mrjs_core_MRSystem__WEBPACK_IMPORTED_MODULE_2__.MRSystem {\n /**\n * @class\n * @description TextSystem's default constructor\n */\n constructor() {\n super(false);\n\n // Setup all the preloaded fonts\n this.preloadedFonts = {};\n const styleSheets = Array.from(document.styleSheets);\n styleSheets.forEach((styleSheet) => {\n const cssRules = Array.from(styleSheet.cssRules);\n // all the font-faces rules\n const rulesFontFace = cssRules.filter((rule) => rule.cssText.startsWith('@font-face'));\n\n rulesFontFace.forEach((fontFace) => {\n const fontData = this.parseFontFace(fontFace.cssText);\n\n (0,troika_three_text__WEBPACK_IMPORTED_MODULE_8__.preloadFont)(\n {\n font: fontData.src,\n },\n () => {\n this.preloadedFonts[fontFace.style.getPropertyValue('font-family')] = fontData.src;\n document.dispatchEvent(new CustomEvent('font-loaded'));\n }\n );\n });\n });\n\n // Handle text style needs update\n this.app.addEventListener('trigger-text-style-update', (e) => {\n // The event has the entity stored as its detail.\n if (e.detail !== undefined) {\n this._updateSpecificEntity(e.detail);\n }\n });\n }\n\n /**\n * @function\n * @description When a new entity is created, adds it to the physics registry and initializes the physics aspects of the entity.\n * @param {MREntity} entity - the entity being set up\n */\n onNewEntity(entity) {\n if (!(entity instanceof mrjs_core_entities_MRTextEntity__WEBPACK_IMPORTED_MODULE_3__.MRTextEntity)) {\n return;\n }\n this.registry.add(entity);\n this._updateSpecificEntity(entity);\n }\n\n /**\n * @function\n * @param {object} entity - the entity that needs to be updated.\n * @description The per entity triggered update call. Handles updating all text items including updates for style and cleaning of content for special characters.\n */\n _updateSpecificEntity(entity) {\n this.checkIfTextContentChanged(entity);\n this.handleTextContentUpdate(entity);\n }\n\n /**\n *\n * @param {object} entity - checks if the content changed and if so, updates it to match.\n * @returns {boolean} true if the content needed to be updated, false otherwise.\n */\n checkIfTextContentChanged(entity) {\n // Add a check in case a user manually updates the text value\n let text =\n entity instanceof mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__.MRTextInputEntity\n ? entity.hiddenInput?.value ?? false\n : // troika honors newlines/white space\n // we want to mimic h1, p, etc which do not honor these values\n // so we have to clean these from the text\n // ref: https://github.com/protectwise/troika/issues/289#issuecomment-1841916850\n entity.textContent\n .replace(/(\\n)\\s+/g, '$1')\n .replace(/(\\r\\n|\\n|\\r)/gm, ' ')\n .trim();\n\n if (entity.textObj.text != text) {\n entity.textObj.text = text;\n return true;\n }\n return false;\n }\n\n /**\n *\n * @param {object} entity - the entity whose content updated.\n */\n handleTextContentUpdate(entity) {\n this.updateStyle(entity);\n\n // The sync step ensures troika's text render info and geometry is up to date\n // with any text content changes.\n entity.textObj.sync(() => {\n if (entity instanceof mrjs_core_entities_MRButtonEntity__WEBPACK_IMPORTED_MODULE_0__.MRButtonEntity) {\n // MRButtonEntity\n\n entity.textObj.anchorX = 'center';\n } else if (entity instanceof mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_4__.MRTextInputEntity) {\n // MRTextAreaEntity, MRTextFieldEntity, etc\n\n // textObj positioning and dimensions\n entity.textObj.maxWidth = entity.width;\n entity.textObj.maxHeight = entity.height;\n entity.textObj.position.setX(-entity.width / 2);\n entity.textObj.position.setY(entity.height / 2);\n // background positioning and dimensions\n // Always want background to be slightly bigger for input field 'niceness', but\n // we dont want this to be specifically based on the margin since the user might\n // have other purposes for the margin css attribute.\n entity.background.scale.x = entity.textObj.scale.x + mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(30);\n entity.background.scale.y = entity.textObj.scale.y + mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(30);\n // cursor positioning and dimensions\n entity.cursorStartingPosition.x = entity.textObj.position.x;\n entity.cursorStartingPosition.y = entity.textObj.position.y - entity.cursorHeight / 2;\n // handle activity\n if (entity == document.activeElement) {\n entity.updateCursorPosition();\n } else {\n entity.blur();\n }\n } else {\n // MRTextEntity\n\n entity.textObj.position.setX(-entity.width / 2);\n entity.textObj.position.setY(entity.height / 2);\n }\n });\n }\n\n /**\n * @function\n * @description The per global scene event update call. Handles updating all text items including updates for style and cleaning of content for special characters.\n */\n eventUpdate = () => {\n for (const entity of this.registry) {\n this.checkIfTextContentChanged(entity);\n this.handleTextContentUpdate(entity);\n }\n };\n\n /**\n * @function\n * @description Updates the style for the text's information based on compStyle and inputted css elements.\n * @param {MRTextEntity} entity - the text entity whose style is being updated\n */\n updateStyle = (entity) => {\n const { textObj } = entity;\n\n // Font\n textObj.font = textObj.text.trim().length != 0 ? this.preloadedFonts[entity.compStyle.fontFamily] : null;\n textObj.fontSize = this.parseFontSize(entity.compStyle.fontSize, entity);\n textObj.fontWeight = this.parseFontWeight(entity.compStyle.fontWeight);\n textObj.fontStyle = entity.compStyle.fontStyle;\n\n // Alignment\n textObj.anchorY = this.getVerticalAlign(entity.compStyle.verticalAlign, entity);\n textObj.textAlign = this.getTextAlign(entity.compStyle.textAlign);\n textObj.lineHeight = this.getLineHeight(entity.compStyle.lineHeight, entity);\n\n // Color and opacity\n // TODO - swap this to use the mrjsUtils.color.setObject3DColor function in future.\n // For now since that creates a weird affect for styling (white edges), leaving as the\n // current implementation. This probably just means there's a default style css thing\n // we need to change before we swap.\n this.setTextObject3DColor(textObj, entity.compStyle.color);\n\n // Whitespace and Wrapping\n textObj.whiteSpace = entity.compStyle.whiteSpace ?? textObj.whiteSpace;\n textObj.maxWidth = entity.width * 1.001;\n\n // Offset position for visibility on top of background plane\n textObj.position.z = 0.0001;\n };\n\n /**\n * @function\n * @description Handles when text is added as an entity updating content and style for the internal textObj appropriately.\n * @param {MRTextEntity} entity - the text entity being updated\n */\n addText = (entity) => {\n const text = entity.textContent.trim();\n entity.textObj.text = text.length > 0 ? text : ' ';\n\n this.updateStyle(entity);\n };\n\n /**\n * @function\n * @description parses the font weight as 'bold', 'normal', etc based on the given weight value\n * @param {number} weight - the numerical representation of the font-weight\n * @returns {string} - the enum of 'bold', 'normal', etc\n */\n parseFontWeight(weight) {\n if (weight >= 500) {\n return 'bold';\n }\n return 'normal';\n }\n\n /**\n * @function\n * @description parses the font size based on its `XXpx` value and converts it to a usable result based on the virtual display resolution\n * @param {number} val - the value being adjusted\n * @param {object} el - the css element handler\n * @returns {number} - the font size adjusted for the display as expected\n */\n parseFontSize(val, el) {\n const result = parseFloat(val.split('px')[0]) / mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.display.VIRTUAL_DISPLAY_RESOLUTION;\n return result;\n }\n\n /**\n * @function\n * @description Gets the vertical align\n * @param {number} verticalAlign - the numerical representation in pixel space of the vertical Align\n * @param {MREntity} entity - the entity whose comp style (css) is relevant\n * @returns {string} - the string representation of the the verticalAlign\n */\n getVerticalAlign(verticalAlign, entity) {\n let result = verticalAlign;\n\n if (typeof result === 'number') {\n result /= mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(entity.compStyle.fontSize);\n }\n\n switch (result) {\n case 'baseline':\n case 'sub':\n case 'super':\n return 0;\n case 'text-top':\n return 'top-ex';\n case 'text-bottom':\n return 'bottom';\n case 'middle':\n default:\n return result;\n }\n }\n\n /**\n * @function\n * @description Gets the line height\n * @param {number} lineHeight - the numerical representation in pixel space of the line height\n * @param {MREntity} entity - the entity whose comp style (css) is relevant\n * @returns {number} - the numerical representation of the the lineHeight\n */\n getLineHeight(lineHeight, entity) {\n let result = mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(lineHeight);\n\n if (typeof result === 'number') {\n result /= mrjs__WEBPACK_IMPORTED_MODULE_7__.mrjsUtils.css.pxToThree(entity.compStyle.fontSize);\n }\n\n return result;\n }\n\n /**\n * @function\n * @description Gets the text alignment string\n * @param {string} textAlign - handles values for `start`, `end`, `left`, and `right`; otherwise, defaults to the same input as `textAlign`.\n * @returns {string} - the resolved `textAlign`.\n */\n getTextAlign(textAlign) {\n if (textAlign == 'start') {\n return 'left';\n } else if (textAlign == 'end') {\n return 'right';\n }\n return textAlign;\n }\n\n /**\n * @function\n * @description Based on the given font-face value in the passed cssString, tries to either use by default or download the requested font-face\n * for use by the text object.\n * @param {string} cssString - the css string to be parsed for the font-face css value.\n * @returns {object} - json object respresenting the preloaded font-face\n */\n parseFontFace(cssString) {\n const obj = {};\n const match = cssString.match(/@font-face\\s*{\\s*([^}]*)\\s*}/);\n\n if (match) {\n const fontFaceAttributes = match[1];\n const attributes = fontFaceAttributes.split(';');\n\n attributes.forEach((attribute) => {\n const [key, value] = attribute.split(':').map((item) => item.trim());\n if (key === 'src') {\n const urlMatch = value.match(/url\\(\"([^\"]+)\"\\)/);\n if (urlMatch) {\n obj[key] = urlMatch[1];\n }\n } else {\n obj[key] = value;\n }\n });\n }\n\n return obj;\n }\n\n /**\n * @function\n * @description Sets the text object3D color.\n * @param {object} object3D - the threejs object representation of the troika textt to be colored\n * @param {string} color - the string representation of the color in rgba, hex, or name ('red') form\n * @param {string} default_color - fallback color used if the system does not understand the color parameter. Defaults to black.\n */\n setTextObject3DColor = function (object3D, color, default_color = '#000') {\n if (color.includes('rgba')) {\n const rgba = color\n .substring(5, color.length - 1)\n .split(',')\n .map((part) => parseFloat(part.trim()));\n object3D.material.color.setStyle(`rgb(${rgba[0]}, ${rgba[1]}, ${rgba[2]})`);\n\n object3D.material.opacity = rgba[3];\n } else {\n object3D.material.color.setStyle(color ?? '#000');\n }\n object3D.material.needsUpdate = true;\n };\n}\n\n\n//# sourceURL=webpack://mrjs/./src/core/componentSystems/TextSystem.js?"); /***/ }), @@ -773,7 +773,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ MRTextAreaEntity: () => (/* binding */ MRTextAreaEntity)\n/* harmony export */ });\n/* harmony import */ var mrjs_core_entities_MRTextInputEntity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! mrjs/core/entities/MRTextInputEntity */ \"./src/core/entities/MRTextInputEntity.js\");\n\n\n\n\n/**\n * @class MRTextAreaEntity\n * @classdesc The text area element that simulates the behavior of an HTML