fixed cursor jumping on contenteditable component

This commit is contained in:
Aminejvm 2021-04-01 15:05:51 +01:00
parent 1df2ce7450
commit be6bc0db2f
7 changed files with 129 additions and 81 deletions

View File

@ -19,6 +19,7 @@ const STYLES_CONTROLLER_WRAPPER = (theme) =>
export const Controls = ({ export const Controls = ({
view, view,
customView,
settings, settings,
defaultOptions, defaultOptions,
updateView, updateView,
@ -31,6 +32,8 @@ export const Controls = ({
getRandomLayout, getRandomLayout,
resetLayout, resetLayout,
}) => { }) => {
const isCustomView = (value) => view === "custom" && customView === value;
const arrayToSelectOptions = (arr) => const arrayToSelectOptions = (arr) =>
arr.reduce((acc, option) => [...acc, { value: option, name: Strings.capitalize(option) }], []); arr.reduce((acc, option) => [...acc, { value: option, name: Strings.capitalize(option) }], []);
@ -120,7 +123,7 @@ export const Controls = ({
value={settings.column} value={settings.column}
onChange={(e) => updateColumn(e.target.value)} onChange={(e) => updateColumn(e.target.value)}
selectMinWidth="none" selectMinWidth="none"
disabled={view !== "paragraph"} disabled={!(view === "paragraph" || isCustomView("paragraph"))}
/> />
</div> </div>
); );

View File

@ -0,0 +1,51 @@
import * as React from "react";
function normalizeHtml(str) {
return str && str.replace(/&nbsp;|\u202F|\u00A0/g, " ");
}
export default class ContentEditable extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
shouldComponentUpdate(nextProps, nextState) {
const { html } = nextProps;
const el = this.myRef.current;
if (normalizeHtml(el.innerHTML) !== normalizeHtml(html)) return true;
return false;
}
componentDidUpdate() {
if (!this.myRef) return;
const { html } = this.props;
const el = this.myRef.current;
/** NOTE(Amine): because we often prevent rerendering,
* React doesn't update the Dom, so we do it manually
*/
if (normalizeHtml(el.innerHTML) !== normalizeHtml(html)) {
el.innerHTML = this.props.html;
}
}
handleChange = () => {
const el = this.myRef.current;
if (!el) return;
this.props.onChange(el.innerHTML);
};
render() {
const { onChange, html, ...props } = this.props;
return (
<div
{...props}
contentEditable={true}
onInput={this.handleChange}
ref={this.myRef}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
}

View File

@ -1,8 +1,13 @@
import * as React from "react"; import * as React from "react";
import ContentEditable from "./ContentEditable";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
const STYLES_PARAGRAPH = (theme) => css` const STYLES_PARAGRAPH_WRAPPER = (theme) => css`
display: flex;
height: 100%;
.font_frame_paragraph {
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack}; color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
@ -13,24 +18,17 @@ const STYLES_PARAGRAPH = (theme) => css`
&:focus { &:focus {
outline: none; outline: none;
} }
}
`; `;
const STYLES_TYPE_TO_EDIT = (isFocused) => (theme) => css` const STYLES_TYPE_TO_EDIT = (isFocused) => (theme) => css`
::after { .font_frame_paragraph::after {
content: " type to edit"; content: " type to edit";
color: ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.textGrayLight}; color: ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.textGrayLight};
opacity: ${isFocused ? 0 : 1}; opacity: ${isFocused ? 0 : 1};
} }
`; `;
const MemoizedChild = React.memo(
({ children }) => {
return <div>{children}</div>;
},
(prevProps, nextProps) => !nextProps.shouldUpdateView
);
export default function Paragraph({ export default function Paragraph({
shouldUpdateView,
content, content,
valign, valign,
textAlign, textAlign,
@ -51,35 +49,33 @@ export default function Paragraph({
}; };
return ( return (
<div style={{ display: "flex", height: "100%" }}>
<div <div
contentEditable="true" css={[
suppressContentEditableWarning={true} STYLES_PARAGRAPH_WRAPPER,
style={{ STYLES_TYPE_TO_EDIT(isFocused),
css({
".font_frame_paragraph": {
fontSize: `${fontSize}px`, fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}%`, lineHeight: `${lineHeight}%`,
letterSpacing: `${tracking}em`, letterSpacing: `${tracking}em`,
textAlign, textAlign,
columnCount: column,
columnGap: "24px",
...mapAlignToFlex[valign], ...mapAlignToFlex[valign],
}} },
css={[ }),
STYLES_PARAGRAPH,
STYLES_TYPE_TO_EDIT(isFocused),
css`
width: 100%;
column-count: ${column};
column-gap: 24px;
`,
]} ]}
>
<ContentEditable
className="font_frame_paragraph"
contentEditable="true"
suppressContentEditableWarning={true}
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
onInput={(e) => { html={content}
onChange(e.currentTarget.innerText); onChange={onChange}
}}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
> />
<MemoizedChild shouldUpdateView={shouldUpdateView}>{content}</MemoizedChild>
</div>
</div> </div>
); );
} }

View File

@ -2,7 +2,10 @@ import * as React from "react";
import { css } from "@emotion/react"; import { css } from "@emotion/react";
const STYLES_SENTENCE = (theme) => css` import ContentEditable from "./ContentEditable";
const STYLES_SENTENCE_WRAPPER = (theme) => css`
.font_frame_sentence {
width: 100%; width: 100%;
margin-top: 12px; margin-top: 12px;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack}; color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
@ -11,22 +14,17 @@ const STYLES_SENTENCE = (theme) => css`
&:focus { &:focus {
outline: none; outline: none;
} }
}
`; `;
const STYLES_TYPE_TO_EDIT = (isFocused) => (theme) => css` const STYLES_TYPE_TO_EDIT = (isFocused) => (theme) => css`
::after { .font_frame_sentence::after {
content: " type to edit"; content: " type to edit";
color: ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.textGrayLight}; color: ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.textGrayLight};
opacity: ${isFocused ? 0 : 1}; opacity: ${isFocused ? 0 : 1};
} }
`; `;
const MemoizedChild = React.memo(
({ children }) => children,
(prevProps, nextProps) => !nextProps.shouldUpdateView
);
export default function Sentence({ export default function Sentence({
shouldUpdateView,
content, content,
valign, valign,
textAlign, textAlign,
@ -45,29 +43,34 @@ export default function Sentence({
bottom: { marginTop: "auto" }, bottom: { marginTop: "auto" },
}; };
return ( return (
<div style={{ display: "flex", height: "100%" }}>
<div <div
contentEditable="true" style={{ display: "flex", height: "100%" }}
suppressContentEditableWarning={true} css={[
style={{ STYLES_SENTENCE_WRAPPER,
STYLES_TYPE_TO_EDIT(isFocused),
css({
".font_frame_sentence": {
fontSize: `${fontSize}px`, fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}%`, lineHeight: `${lineHeight}%`,
letterSpacing: `${tracking}em`, letterSpacing: `${tracking}em`,
textAlign, textAlign,
...mapAlignToFlex[valign], ...mapAlignToFlex[valign],
}} },
css={[STYLES_SENTENCE, STYLES_TYPE_TO_EDIT(isFocused)]} }),
]}
>
<ContentEditable
className="font_frame_sentence"
contentEditable="true"
suppressContentEditableWarning={true}
onKeyDown={(e) => { onKeyDown={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
onInput={(e) => { onChange={onChange}
onChange(e.currentTarget.innerHTML);
}}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
> html={content}
<MemoizedChild shouldUpdateView={shouldUpdateView}>{content}</MemoizedChild> />
</div>
</div> </div>
); );
} }

View File

@ -7,7 +7,6 @@ export default function FontView({
view, view,
customView, customView,
content: { sentence, paragraph, custom }, content: { sentence, paragraph, custom },
shouldUpdateView,
updateCustomView, updateCustomView,
}) { }) {
const isCustomView = (value) => view === "custom" && customView === value; const isCustomView = (value) => view === "custom" && customView === value;
@ -19,7 +18,6 @@ export default function FontView({
if (view === "paragraph" || isCustomView("paragraph")) { if (view === "paragraph" || isCustomView("paragraph")) {
return ( return (
<Paragraph <Paragraph
shouldUpdateView={shouldUpdateView}
content={view === "custom" ? custom : paragraph} content={view === "custom" ? custom : paragraph}
valign={settings.valign} valign={settings.valign}
textAlign={settings.textAlign} textAlign={settings.textAlign}
@ -34,7 +32,6 @@ export default function FontView({
return ( return (
<Sentence <Sentence
shouldUpdateView={shouldUpdateView}
content={view === "custom" ? custom : sentence} content={view === "custom" ? custom : sentence}
valign={settings.valign} valign={settings.valign}
textAlign={settings.textAlign} textAlign={settings.textAlign}

View File

@ -39,7 +39,6 @@ const initialState = {
sentence: Content.sentences[0], sentence: Content.sentences[0],
paragraph: Content.paragraphs[0], paragraph: Content.paragraphs[0],
customViewContent: Content.sentences[1], customViewContent: Content.sentences[1],
shouldUpdateView: false,
settings: { settings: {
valign: "center", valign: "center",
textAlign: "left", textAlign: "left",
@ -103,7 +102,7 @@ const reducer = (state, action) => {
return { return {
...state, ...state,
view: action.value, view: action.value,
context: { ...state.context, shouldUpdateView: true }, context: { ...state.context },
}; };
case "UPDATE_CUSTOM_VIEW": case "UPDATE_CUSTOM_VIEW":
return { return {
@ -113,7 +112,6 @@ const reducer = (state, action) => {
context: { context: {
...state.context, ...state.context,
customViewContent: action.payload.customViewContent, customViewContent: action.payload.customViewContent,
shouldUpdateView: false,
}, },
}; };
case "RESET": case "RESET":

View File

@ -86,7 +86,6 @@ export default function FontFrame({ cid, url, ...props }) {
custom: currentState.context.customViewContent, custom: currentState.context.customViewContent,
}} }}
customView={currentState.customView} customView={currentState.customView}
shouldUpdateView={currentState.context.shouldUpdateView}
settings={currentState.context.settings} settings={currentState.context.settings}
updateCustomView={updateCustomView} updateCustomView={updateCustomView}
/> />
@ -95,6 +94,7 @@ export default function FontFrame({ cid, url, ...props }) {
{currentState.context.showSettings && ( {currentState.context.showSettings && (
<Controls <Controls
view={currentState.view} view={currentState.view}
customView={currentState.customView}
defaultOptions={currentState.defaultOptions} defaultOptions={currentState.defaultOptions}
resetLayout={resetLayout} resetLayout={resetLayout}
updateView={updateView} updateView={updateView}