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 = ({
|
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>
|
||||||
);
|
);
|
||||||
|
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 * 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`
|
||||||
width: 100%;
|
display: flex;
|
||||||
margin-top: 12px;
|
height: 100%;
|
||||||
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
|
.font_frame_paragraph {
|
||||||
padding: 0px 32px 28px;
|
width: 100%;
|
||||||
word-break: break-word;
|
margin-top: 12px;
|
||||||
white-space: pre-wrap;
|
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
|
||||||
|
padding: 0px 32px 28px;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&: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
|
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"
|
contentEditable="true"
|
||||||
suppressContentEditableWarning={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()}
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,31 +2,29 @@ import * as React from "react";
|
|||||||
|
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
|
||||||
const STYLES_SENTENCE = (theme) => css`
|
import ContentEditable from "./ContentEditable";
|
||||||
width: 100%;
|
|
||||||
margin-top: 12px;
|
const STYLES_SENTENCE_WRAPPER = (theme) => css`
|
||||||
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
|
.font_frame_sentence {
|
||||||
padding: 0px 32px 28px;
|
width: 100%;
|
||||||
word-break: break-word;
|
margin-top: 12px;
|
||||||
&:focus {
|
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
|
||||||
outline: none;
|
padding: 0px 32px 28px;
|
||||||
|
word-break: break-word;
|
||||||
|
&:focus {
|
||||||
|
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
|
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"
|
contentEditable="true"
|
||||||
suppressContentEditableWarning={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) => {
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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":
|
||||||
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user