tgui paper bandaid fix (#91098)

## About The Pull Request
For the record, this is a bandaid fix. The inputs are losing focus with
each update (aka EVERY backend update). This sets them to only update
when their editability changes, letting you type again. Caveat: The UI,
or at least how it's done with dangerous HTML, doesn't seem to be smart
enough to detect when the UI turns back to being non editable, nor am I
smart enough to fix this behavior after several attempts. The result:
When you drop the pen, the fields look editable but lose focus when
clicked just like the bug originally. It's not great UX. Paperwork needs
to just be wholesale redone.
 
Bumped marked.js versions a HUGE amount which should come with better
performance, typings, etc. This seemingly resolves a few issues with
markdown otherwise (like typing _a)
This commit is contained in:
Jeremiah
2025-05-16 13:23:51 -05:00
committed by GitHub
parent e3b17a5a31
commit b216f05b6f
6 changed files with 114 additions and 67 deletions
@@ -29,7 +29,7 @@ type MarkdownRendererProps = {
export const MarkdownRenderer = (props: MarkdownRendererProps) => {
let { content, sanitize } = props;
content = marked(content);
content = marked(content, { async: false });
if (sanitize) {
content = sanitizeText(content, /* advHtml = */ false);
}
@@ -1,10 +1,12 @@
import { marked } from 'marked';
import { baseUrl } from 'marked-base-url';
import { markedSmartypants } from 'marked-smartypants';
import { Component, RefObject } from 'react';
import { Box, Section } from 'tgui-core/components';
import { useBackend, useLocalState } from '../../backend';
import { sanitizeText } from '../../sanitize';
import { canEdit, tokenizer, walkTokens } from './helpers';
import { tokenizer, walkTokens } from './helpers';
import { StampView } from './StampView';
import { FieldInput, InteractionType, PaperContext } from './types';
@@ -12,6 +14,7 @@ type PreviewViewProps = {
scrollableRef: RefObject<HTMLDivElement | null>;
handleOnScroll: (this: GlobalEventHandlers, ev: Event) => any;
textArea: string;
canEdit: boolean;
};
type FieldCreationReturn = {
@@ -78,15 +81,17 @@ export class PreviewView extends Component<PreviewViewProps> {
walkTokens,
};
marked.use({
extensions: [inputField],
breaks: true,
gfm: true,
smartypants: true,
walkTokens: walkTokens,
marked.use(
{
extensions: [inputField],
breaks: true,
gfm: true,
walkTokens: walkTokens,
},
markedSmartypants(),
// Once assets are fixed might need to change this for them
baseUrl: 'thisshouldbreakhttp',
});
baseUrl('thisshouldbreakhttp'),
);
};
// Extracts the paper field "counter" from a full ID.
@@ -145,6 +150,12 @@ export class PreviewView extends Component<PreviewViewProps> {
document.removeEventListener('input', this.onInputHandler);
}
shouldComponentUpdate(nextProps: Readonly<PreviewViewProps>): boolean {
if (!this.props.canEdit) return true;
return this.props.canEdit !== nextProps.canEdit;
}
// Creates the partial inline HTML for previewing or reading the paper from
// only static_ui_data from DM.
createPreviewFromDM = (): { text: string; newFieldCount: number } => {
@@ -155,13 +166,12 @@ export class PreviewView extends Component<PreviewViewProps> {
default_pen_font,
default_pen_color,
paper_color,
held_item_details,
} = data;
let output = '';
let fieldCount = 0;
const readOnly = !canEdit(held_item_details);
const readOnly = !this.props.canEdit;
// If readonly is the same (input field writiability state hasn't changed)
// And the input stats are the same (no new text inputs since last time)
@@ -254,7 +264,7 @@ export class PreviewView extends Component<PreviewViewProps> {
text: string,
font: string,
color: string,
bold: boolean = false,
bold = false,
): string => {
return `<span style="color:${color};font-family:${font};${
bold ? 'font-weight: bold;' : ''
@@ -289,7 +299,7 @@ export class PreviewView extends Component<PreviewViewProps> {
},
};
return marked.parse(rawText);
return marked.parse(rawText, { async: false });
};
// Fully formats, sanitises and parses the provided raw text and wraps it
@@ -300,12 +310,11 @@ export class PreviewView extends Component<PreviewViewProps> {
color: string,
paperColor: string,
bold: boolean,
fieldCounter: number = 0,
fieldCounter = 0,
forceReadonlyFields: boolean = false,
advanced_html: boolean = false,
advanced_html = false,
): FieldCreationReturn => {
// First lets make sure it ends in a new line
const { data } = useBackend<PaperContext>();
rawText += rawText[rawText.length] === '\n' ? '\n' : '\n\n';
// Second, parse the text using markup
@@ -358,41 +367,38 @@ export class PreviewView extends Component<PreviewViewProps> {
color: string,
paperColor: string,
forceReadonlyFields: boolean,
counter: number = 0,
counter = 0,
): FieldCreationReturn => {
const { data } = useBackend<PaperContext>();
const { raw_field_input } = data;
const ret_text = rawText.replace(
fieldRegex,
(match, p1, offset, string) => {
const width = this.textWidth(match, font, fontSize);
const matchingData = raw_field_input?.find(
(e) => e.field_index === `${counter}`,
);
if (matchingData) {
return this.createFilledInputField(
matchingData,
p1.length,
width,
font,
fontSize,
color,
paperColor,
this.createIDHeader(counter++),
);
}
return this.createInputField(
const ret_text = rawText.replace(fieldRegex, (match, p1) => {
const width = this.textWidth(match, font, fontSize);
const matchingData = raw_field_input?.find(
(e) => e.field_index === `${counter}`,
);
if (matchingData) {
return this.createFilledInputField(
matchingData,
p1.length,
width,
font,
fontSize,
color,
paperColor,
this.createIDHeader(counter++),
forceReadonlyFields,
);
},
);
}
return this.createInputField(
p1.length,
width,
font,
fontSize,
color,
this.createIDHeader(counter++),
forceReadonlyFields,
);
});
return {
nextCounter: counter,
@@ -74,6 +74,8 @@ export class PrimaryView extends Component {
const tooManyCharacters = usedCharacters > max_length;
const canEdit = interactMode === InteractionType.writing;
return (
<>
<PaperSheetStamper scrollableRef={this.scrollableRef} />
@@ -86,9 +88,10 @@ export class PrimaryView extends Component {
scrollableRef={this.scrollableRef}
handleOnScroll={this.onScrollHandler}
textArea={textAreaText}
canEdit={canEdit}
/>
</Flex.Item>
{interactMode === InteractionType.writing && (
{canEdit && (
<Flex.Item shrink={1} height={TEXTAREA_INPUT_HEIGHT + 'px'}>
<Section
title="Insert Text"
+3 -2
View File
@@ -10,14 +10,15 @@
"highlight.js": "^11.11.1",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"marked": "^4.3.0",
"marked": "^15.0.11",
"marked-base-url": "^1.1.6",
"marked-smartypants": "^1.1.9",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tgui-core": "^3.1.4",
"tgui-dev-server": "workspace:*"
},
"devDependencies": {
"@types/marked": "4.3.2",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.2",
"vitest": "^3.1.1"
+23 -10
View File
@@ -1,17 +1,30 @@
import { marked } from 'marked';
import { baseUrl } from 'marked-base-url';
import { markedSmartypants } from 'marked-smartypants';
import { sanitizeText } from './sanitize';
export const processedText = (value) => {
const textHtml = {
__html: sanitizeText(
marked(value, {
type ProcessedText = {
__html: string;
};
export function processedText(value: string | null): ProcessedText {
if (!value) {
return { __html: '' };
}
const parsed = marked
.use(
{
breaks: true,
smartypants: true,
smartLists: true,
baseUrl: 'thisshouldbreakhttp',
}),
),
},
markedSmartypants(),
baseUrl('thisshouldbreakhttp'),
)
.parse(value, { async: false });
const textHtml = {
__html: sanitizeText(parsed),
};
return textHtml;
};
}
+37 -13
View File
@@ -3918,13 +3918,6 @@ __metadata:
languageName: node
linkType: hard
"@types/marked@npm:4.3.2":
version: 4.3.2
resolution: "@types/marked@npm:4.3.2"
checksum: 10c0/6f44d28da5c940a719d6c6aca41e33f49c5fe957f7972939cc1cbb47d045951f4d969f382d655345d0463e47db1994635d2862018716fcef2fa85ceeceb116e7
languageName: node
linkType: hard
"@types/mime@npm:^1":
version: 1.3.5
resolution: "@types/mime@npm:1.3.5"
@@ -13824,12 +13817,32 @@ __metadata:
languageName: node
linkType: hard
"marked@npm:^4.3.0":
version: 4.3.0
resolution: "marked@npm:4.3.0"
"marked-base-url@npm:^1.1.6":
version: 1.1.6
resolution: "marked-base-url@npm:1.1.6"
peerDependencies:
marked: ">= 4 < 16"
checksum: 10c0/346b8f3a912ab5192caec896d8e923b83113f81312eb4838ec9773aca620088cd3ed4d9fce773f3251d71c72f271c3df8554f025c1146cea3e76845b29e5be42
languageName: node
linkType: hard
"marked-smartypants@npm:^1.1.9":
version: 1.1.9
resolution: "marked-smartypants@npm:1.1.9"
dependencies:
smartypants: "npm:^0.2.2"
peerDependencies:
marked: ">=4 <16"
checksum: 10c0/50a9f9ddeb67045933c26abdd8acf997368d96920990048b6d432c4d2374a513152c639b46c16c74934483b7a1df6093f8f26461f2409b9bf1e58f8ac759391f
languageName: node
linkType: hard
"marked@npm:^15.0.11":
version: 15.0.11
resolution: "marked@npm:15.0.11"
bin:
marked: bin/marked.js
checksum: 10c0/0013463855e31b9c88d8bb2891a611d10ef1dc79f2e3cbff1bf71ba389e04c5971298c886af0be799d7fa9aa4593b086a136062d59f1210b0480b026a8c5dc47
checksum: 10c0/d532db4955c1f2ac6efc65a644725e9e12e7944cb6af40c7148baecfd3b3c2f3564229b3daf12d2125635466448fb9b367ce52357be3aea0273e3d152efdbdcf
languageName: node
linkType: hard
@@ -18699,6 +18712,16 @@ __metadata:
languageName: node
linkType: hard
"smartypants@npm:^0.2.2":
version: 0.2.2
resolution: "smartypants@npm:0.2.2"
bin:
smartypants: bin/smartypants.js
smartypantsu: bin/smartypantsu.js
checksum: 10c0/29b46df6acaed9ad6271624e26e055d6360c9da7f79433b7a83ebd40b50bd6b554d8f3793932f42be07e5badce889f547c89378fda77cb1f935b09a902c4527b
languageName: node
linkType: hard
"snake-case@npm:^3.0.4":
version: 3.0.4
resolution: "snake-case@npm:3.0.4"
@@ -19885,7 +19908,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "tgui@workspace:packages/tgui"
dependencies:
"@types/marked": "npm:4.3.2"
"@types/react": "npm:^19.1.0"
"@types/react-dom": "npm:^19.1.2"
common: "workspace:*"
@@ -19895,7 +19917,9 @@ __metadata:
highlight.js: "npm:^11.11.1"
jest: "npm:^29.7.0"
js-yaml: "npm:^4.1.0"
marked: "npm:^4.3.0"
marked: "npm:^15.0.11"
marked-base-url: "npm:^1.1.6"
marked-smartypants: "npm:^1.1.9"
react: "npm:^19.1.0"
react-dom: "npm:^19.1.0"
tgui-core: "npm:^3.1.4"