mirror of
https://github.com/filecoin-project/slate.git
synced 2024-09-20 10:47:18 +03:00
fixed cursor jumping on contenteditable component
This commit is contained in:
parent
1df2ce7450
commit
be6bc0db2f
@ -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>
|
||||
);
|
||||
|
51
components/core/FontFrame/Views/ContentEditable.js
Normal file
51
components/core/FontFrame/Views/ContentEditable.js
Normal file
@ -0,0 +1,51 @@
|
||||
import * as React from "react";
|
||||
|
||||
function normalizeHtml(str) {
|
||||
return str && str.replace(/ |\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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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":
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user