Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC for implementing "bold" rich text functionality into document producer #3329

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"lodash": "^4.17.21",
"luxon": "^3.3.0",
"mapbox-gl": "^3.0.0",
"prismjs": "^1.29.0",
"re-reselect": "^4.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down Expand Up @@ -161,6 +162,7 @@
"@types/luxon": "^3.2.0",
"@types/mapbox__mapbox-gl-draw": "^1.4.0",
"@types/node": "^18.15.11",
"@types/prismjs": "^1.26.5",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^7.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const SectionValue = ({
}: SectionValueProps): React.ReactElement | null => {
switch (value.type) {
case 'SectionText':
// Need to implement markdown view here
return <span style={{ fontSize: '16px', whiteSpace: 'pre-wrap' }}>{value.textValue}</span>;
case 'SectionVariable':
const reference = value.usageType === 'Reference';
Expand Down
203 changes: 146 additions & 57 deletions src/components/DocumentProducer/EditableSection/Edit.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';

import { Box, Typography, useTheme } from '@mui/material';
import { Box, Typography, styled, useTheme } from '@mui/material';
import { Autocomplete, Button, DropdownItem } from '@terraware/web-components';
import { useDeviceInfo } from '@terraware/web-components/utils';
import { isKeyHotkey } from 'is-hotkey';
import { BaseEditor, Descendant, Element as SlateElement, Transforms, createEditor } from 'slate';
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
import { Descendant, Editor, Transforms, createEditor } from 'slate';
import { Editable, RenderElementProps, RenderLeafProps, Slate, withReact } from 'slate-react';

import strings from 'src/strings';
import { Section, VariableWithValues } from 'src/types/documentProducer/Variable';
Expand All @@ -14,24 +14,8 @@ import { VariableValueValue } from 'src/types/documentProducer/VariableValue';
import InsertOptionsDropdown from './InsertOptionsDropdown';
import TextChunk from './TextChunk';
import TextVariable from './TextVariable';
import { editorValueFromVariableValue, variableValueFromEditorValue } from './helpers';

// Required type definitions for slatejs (https://docs.slatejs.org/concepts/12-typescript):
export type CustomElement = {
type?: 'text' | 'variable';
children: CustomText[];
variableId?: number;
docId: number;
reference?: boolean;
};
export type CustomText = { text: string };
declare module 'slate' {
interface CustomTypes {
Editor: BaseEditor & ReactEditor;
Element: CustomElement;
Text: CustomText;
}
}
import { decorateMarkdown, editorValueFromVariableValue, scrubMarkdown, variableValueFromEditorValue } from './helpers';
import { isEmptyDescendant, isSectionElement, isTextElement, isVariableElement } from './types';

type EditableSectionEditProps = {
section: Section;
Expand Down Expand Up @@ -59,37 +43,46 @@ const SectionEdit = ({
const [variableToBeInserted, setVariableToBeInserted] = useState<VariableWithValues>();

const initialValue: Descendant[] = useMemo(() => {
const editorValue =
sectionValues !== undefined && sectionValues.length > 0
? [
{
children: [
{ text: '' },
...sectionValues.map((value) => editorValueFromVariableValue(value, allVariables)),
{ text: '' },
],
},
]
: [{ children: [{ text: '' }] }];
return editorValue as Descendant[];
const editorValue: Descendant = {
type: 'section',
children: [],
};

if (sectionValues !== undefined && sectionValues.length > 0) {
editorValue.children = sectionValues.map((value) => editorValueFromVariableValue(value, allVariables));
}

return [editorValue];
}, [sectionValues, allVariables]);

const onChange = useCallback(
(value: Descendant[]) => {
(values: Descendant[]) => {
const newVariableValues: VariableValueValue[] = [];
value.forEach((v) => {
const children = (v as SlateElement).children;
if (children.length === 1 && children[0].text === '') {

values.forEach((value: Descendant) => {
if (!isSectionElement(value)) {
// We shouldn't ever hit here, for now we are only using SectionElement as top level elements
return;
}

const children = value.children;

// This is an "empty" value
if (isEmptyDescendant(value)) {
newVariableValues.push({
id: -1,
listPosition: newVariableValues.length,
type: 'SectionText',
textValue: '\n',
});
} else {
children.forEach((c) => {
if (c.text === undefined || c.text !== '') {
newVariableValues.push(variableValueFromEditorValue(c, allVariables, newVariableValues.length));
children.forEach((child) => {
if (isEmptyDescendant(child)) {
return;
}

if (isVariableElement(child) || isTextElement(child)) {
newVariableValues.push(variableValueFromEditorValue(child, newVariableValues.length));
}
});
}
Expand All @@ -105,27 +98,29 @@ const SectionEdit = ({
);

const renderElement = useCallback(
(props: any) => {
switch (props.element.type) {
case 'variable':
const variable = allVariables.find((v) => v.id === props.element.variableId);
return (
<TextVariable
isEditing
icon='iconVariable'
onClick={() => onEditVariableValue(variable)}
reference={props.element.reference}
variable={variable}
{...props}
/>
);
default:
return <TextChunk {...props} />;
(props: RenderElementProps) => {
const { element } = props;
if (isVariableElement(element)) {
const variable = allVariables.find((v) => v.id === element.variableId);
return (
<TextVariable
isEditing
icon='iconVariable'
onClick={() => onEditVariableValue(variable)}
reference={element.reference}
variable={variable}
{...props}
/>
);
}

return <TextChunk {...props} />;
},
[allVariables]
);

const renderLeaf = useCallback((props: RenderLeafProps) => <Leaf {...props} />, []);

const insertVariable = useCallback(
(variable: VariableWithValues, reference?: boolean) => {
Transforms.insertNodes(editor, {
Expand Down Expand Up @@ -157,16 +152,40 @@ const SectionEdit = ({
Transforms.move(editor, { unit: 'offset', reverse: true });
return;
}

if (isKeyHotkey('right', nativeEvent)) {
event.preventDefault();
Transforms.move(editor, { unit: 'offset' });
return;
}

if (isKeyHotkey('enter', nativeEvent)) {
event.preventDefault();
Transforms.insertNodes(editor, { text: '\n' });
return;
}

if (isKeyHotkey('cmd+b', nativeEvent)) {
const range = Editor.unhangRange(editor, editor.selection, { voids: true });
event.preventDefault();
Transforms.setNodes(
editor,
// This property must be considered in rendering or this does nothing
{ bold: !Editor.marks(editor)?.bold },
{
at: range,
match: () => true,
split: true,
}
);
return;
}

if (isKeyHotkey('cmd+i', nativeEvent)) {
event.preventDefault();
// TODO implement italics?
return;
}
}
},
[editor]
Expand Down Expand Up @@ -254,7 +273,9 @@ const SectionEdit = ({
<Box marginTop={theme.spacing(2)}>
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
<Editable
decorate={decorateMarkdown}
renderElement={renderElement}
renderLeaf={renderLeaf}
onKeyDown={onKeyDown}
placeholder={strings.PLACEHOLDER}
renderPlaceholder={({ children, attributes }) => (
Expand Down Expand Up @@ -291,4 +312,72 @@ const withInlines = (editor: any) => {
return editor;
};

const Leaf = styled(({ attributes, children, className, leaf }: RenderLeafProps & { className?: string }) => {
// Scrub the markdown for the editor view
if (leaf.bold) {
leaf.text = scrubMarkdown(leaf.text);
}

return (
<span {...attributes} className={className}>
{children}
</span>
);
})(({ leaf }) => ({
...(leaf.bold
? {
fontWeight: 'bold',
}
: {}),
// TODO need to implement these as CustomText properties if we want to use them
// ...(leaf.italic
// ? {
// fontStyle: 'italic',
// }
// : {}),
// ...(leaf.underlined
// ? {
// textDecoration: 'underline',
// }
// : {}),
// ...(leaf.title
// ? {
// display: 'inline-block',
// fontWeight: 'bold',
// fontSize: '20px',
// margin: '20px 0 10px 0',
// }
// : {}),
// ...(leaf.list
// ? {
// paddingLeft: '10px',
// fontSize: '20px',
// lineHeight: '10px',
// }
// : {}),
// ...(leaf.hr
// ? {
// display: 'block',
// textAlign: 'center',
// borderBottom: '2px solid #ddd',
// }
// : {}),
// ...(leaf.blockquote
// ? {
// display: 'inline-block',
// borderLeft: '2px solid #ddd',
// paddingLeft: '10px',
// color: '#aaa',
// fontStyle: 'italic',
// }
// : {}),
// ...(leaf.code
// ? {
// fontFamily: 'monospace',
// backgroundColor: '#eee',
// padding: '3px',
// }
// : {}),
}));

export default SectionEdit;
Loading
Loading