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 = ({
view,
customView,
settings,
defaultOptions,
updateView,
@ -31,6 +32,8 @@ export const Controls = ({
getRandomLayout,
resetLayout,
}) => {
const isCustomView = (value) => view === "custom" && customView === value;
const arrayToSelectOptions = (arr) =>
arr.reduce((acc, option) => [...acc, { value: option, name: Strings.capitalize(option) }], []);
@ -120,7 +123,7 @@ export const Controls = ({
value={settings.column}
onChange={(e) => updateColumn(e.target.value)}
selectMinWidth="none"
disabled={view !== "paragraph"}
disabled={!(view === "paragraph" || isCustomView("paragraph"))}
/>
</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,36 +1,34 @@
import * as React from "react";
import ContentEditable from "./ContentEditable";
import { css } from "@emotion/react";
const STYLES_PARAGRAPH = (theme) => css`
width: 100%;
margin-top: 12px;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
padding: 0px 32px 28px;
word-break: break-word;
white-space: pre-wrap;
const STYLES_PARAGRAPH_WRAPPER = (theme) => css`
display: flex;
height: 100%;
.font_frame_paragraph {
width: 100%;
margin-top: 12px;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
padding: 0px 32px 28px;
word-break: break-word;
white-space: pre-wrap;
&:focus {
outline: none;
&:focus {
outline: none;
}
}
`;
const STYLES_TYPE_TO_EDIT = (isFocused) => (theme) => css`
::after {
.font_frame_paragraph::after {
content: " type to edit";
color: ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.textGrayLight};
opacity: ${isFocused ? 0 : 1};
}
`;
const MemoizedChild = React.memo(
({ children }) => {
return <div>{children}</div>;
},
(prevProps, nextProps) => !nextProps.shouldUpdateView
);
export default function Paragraph({
shouldUpdateView,
content,
valign,
textAlign,
@ -51,35 +49,33 @@ export default function Paragraph({
};
return (
<div style={{ display: "flex", height: "100%" }}>
<div
<div
css={[
STYLES_PARAGRAPH_WRAPPER,
STYLES_TYPE_TO_EDIT(isFocused),
css({
".font_frame_paragraph": {
fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}%`,
letterSpacing: `${tracking}em`,
textAlign,
columnCount: column,
columnGap: "24px",
...mapAlignToFlex[valign],
},
}),
]}
>
<ContentEditable
className="font_frame_paragraph"
contentEditable="true"
suppressContentEditableWarning={true}
style={{
fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}%`,
letterSpacing: `${tracking}em`,
textAlign,
...mapAlignToFlex[valign],
}}
css={[
STYLES_PARAGRAPH,
STYLES_TYPE_TO_EDIT(isFocused),
css`
width: 100%;
column-count: ${column};
column-gap: 24px;
`,
]}
onKeyDown={(e) => e.stopPropagation()}
onInput={(e) => {
onChange(e.currentTarget.innerText);
}}
html={content}
onChange={onChange}
onFocus={handleFocus}
onBlur={handleBlur}
>
<MemoizedChild shouldUpdateView={shouldUpdateView}>{content}</MemoizedChild>
</div>
/>
</div>
);
}

View File

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

View File

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

View File

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

View File

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