Skip to content

Commit

Permalink
Support non-breaking spaces in RTE (#1310)
Browse files Browse the repository at this point in the history
Adds a new `"non-breaking-space"` "supported thing" to the RTE options.
  • Loading branch information
johnnyomair authored Nov 6, 2023
1 parent 2b41033 commit dbdc0f5
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 2 deletions.
32 changes: 32 additions & 0 deletions .changeset/breezy-parents-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@comet/admin-icons": minor
"@comet/admin-rte": minor
"@comet/cms-admin": minor
---

Add support for non-breaking spaces to RTE

Add `"non-breaking-space"` to `supports` when creating an RTE:

```tsx
const [useRteApi] = makeRteApi();

export default function MyRte() {
const { editorState, setEditorState } = useRteApi();
return (
<Rte
value={editorState}
onChange={setEditorState}
options={{
supports: [
// Non-breaking space
"non-breaking-space",
// Other options you may wish to support
"bold",
"italic",
],
}}
/>
);
}
```
2 changes: 1 addition & 1 deletion demo/admin/src/pages/blocks/HeadlineBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const RichTextBlock = createRichTextBlock({
link: LinkBlock,
rte: {
maxBlocks: 1,
supports: ["bold", "italic"],
supports: ["bold", "italic", "non-breaking-space"],
blocktypeMap: {},
},
minHeight: 0,
Expand Down
2 changes: 2 additions & 0 deletions packages/admin/admin-rte/src/core/Controls/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import InlineStyleTypeControls from "./InlineStyleTypeControls";
import LinkControls from "./LinkControls";
import ListsControls from "./ListsControls";
import ListsIndentControls from "./ListsIndentControls";
import SpecialCharactersControls from "./SpecialCharactersControls";
import Toolbar from "./Toolbar";

export default function Controls(p: IControlProps) {
Expand All @@ -25,6 +26,7 @@ export default function Controls(p: IControlProps) {
ListsControls,
ListsIndentControls,
LinkControls,
SpecialCharactersControls,
...(hasCustomButtons ? [CustomControls] : []),
]}
</Toolbar>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ButtonGroup } from "@mui/material";
import * as React from "react";

import NonBreakingSpaceToolbarButton from "../extension/NonBreakingSpace/ToolbarButton";
import { IControlProps } from "../types";

function SpecialCharactersControls(props: IControlProps) {
const {
options: { supports: supportedThings },
} = props;

if (!supportedThings.includes("non-breaking-space")) {
return null;
}

return <ButtonGroup>{supportedThings.includes("non-breaking-space") && <NonBreakingSpaceToolbarButton {...props} />}</ButtonGroup>;
}

export default SpecialCharactersControls;
3 changes: 2 additions & 1 deletion packages/admin/admin-rte/src/core/Rte.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export type SupportedThings =
| "blockquote"
| "history"
| "link"
| "links-remove";
| "links-remove"
| "non-breaking-space";

export interface IRteOptions {
supports: SupportedThings[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DraftDecorator } from "draft-js";

import EditorComponent from "./EditorComponent";

const NO_BREAK_SPACE_UNICODE_CHAR = /\u00a0/g;

const Decorator: DraftDecorator = {
strategy: (contentBlock, callback) => {
findWithRegex(NO_BREAK_SPACE_UNICODE_CHAR, contentBlock, callback);
},
component: EditorComponent,
};

function findWithRegex(regex: RegExp, contentBlock: Draft.ContentBlock, callback: (start: number, end: number) => void) {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
callback(start, start + matchArr[0].length);
}
}

export default Decorator;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RteNonBreakingSpace } from "@comet/admin-icons";
import { styled } from "@mui/material/styles";
import { ContentState } from "draft-js";
import * as React from "react";

interface Props {
contentState: ContentState;
entityKey: string;
children?: React.ReactNode;
}

function EditorComponent({ children }: Props): React.ReactElement {
return (
<Root>
<Icon />
{children}
</Root>
);
}

const Root = styled("span")`
position: relative;
// Arbitrary value to make the non-breaking space the same width as the icon
letter-spacing: 0.9em;
`;

const Icon = styled(RteNonBreakingSpace)`
position: absolute;
// Arbitrary values to make the icon look centered
top: 0.12em;
left: 0.08em;
font-size: inherit;
color: currentcolor;
opacity: 0.5;
`;

export default EditorComponent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RteNonBreakingSpace } from "@comet/admin-icons";
import Tooltip from "@mui/material/Tooltip";
import { EditorState, Modifier } from "draft-js";
import * as React from "react";
import { FormattedMessage } from "react-intl";

import ControlButton from "../../Controls/ControlButton";
import { IControlProps } from "../../types";

const NO_BREAK_SPACE_UNICODE_CHAR = 0x00a0;

function ToolbarButton({ editorState, setEditorState }: IControlProps): React.ReactElement {
function handleClick(event: React.MouseEvent) {
event.preventDefault(); // Preserve focus in editor

const currentContent = editorState.getCurrentContent();
const selection = editorState.getSelection();
// TODO insert \u00a0 in a way that link-entities dont break when inserted in the middle of a link text
// right now the link is split into 2 link-entities
// works as expected when \u00ad is copied and pasted: https://unicode.flopp.net/c/00A0
let textWithEntity;

if (selection.isCollapsed()) {
textWithEntity = Modifier.insertText(currentContent, selection, String.fromCharCode(NO_BREAK_SPACE_UNICODE_CHAR));
} else {
textWithEntity = Modifier.replaceText(currentContent, selection, String.fromCharCode(NO_BREAK_SPACE_UNICODE_CHAR));
}
setEditorState(EditorState.push(editorState, textWithEntity, "insert-characters"));
}

return (
<Tooltip
title={<FormattedMessage id="comet.rte.extensions.nonBreakingSpace.buttonTooltip" defaultMessage="Insert a non-breaking space" />}
placement="top"
>
<span>
<ControlButton icon={RteNonBreakingSpace} onButtonClick={handleClick} />
</span>
</Tooltip>
);
}

export default ToolbarButton;
5 changes: 5 additions & 0 deletions packages/admin/admin-rte/src/core/makeRteApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react";
import useDebounce from "../useDebounce";
import usePrevious from "../usePrevious";
import LinkDecorator from "./extension/Link/Decorator";
import NonBreakingSpaceDecorator from "./extension/NonBreakingSpace/Decorator";
export interface IMakeRteApiProps<T = any> {
decorators?: DraftDecorator[];
parse?: (v: T) => ContentState;
Expand Down Expand Up @@ -35,6 +36,10 @@ function defaultFormatContent(v: ContentState): any {

function makeRteApi<T = any>(o?: IMakeRteApiProps<T>) {
const { decorators = [LinkDecorator], parse = defaultParseContent, format = defaultFormatContent }: IMakeRteApiProps<T> = o || {};

// Add default decorators
decorators.push(NonBreakingSpaceDecorator);

const decorator = new CompositeDecorator(decorators);

function createEmptyState() {
Expand Down
1 change: 1 addition & 0 deletions packages/admin/admin-rte/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as FeaturesButtonGroup, RteFeaturesButtonGroupClassKey } from "
export { default as LinkControls, RteLinkControlsClassKey } from "./core/Controls/LinkControls";
export { RteToolbarClassKey, default as Toolbar } from "./core/Controls/Toolbar";
export { default as LinkDecorator } from "./core/extension/Link/Decorator";
export { default as NonBreakingSpaceDecorator } from "./core/extension/NonBreakingSpace/Decorator";
export { default as filterEditorStateDefault } from "./core/filterEditor/default";
export { default as filterEditorStateRemoveUnsupportedBlockTypes } from "./core/filterEditor/removeUnsupportedBlockTypes";
export { default as filterEditorStateRemoveUnsupportedEntities } from "./core/filterEditor/removeUnsupportedEntities";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const rteOptions: IRteOptions = {
"history",
"link",
"links-remove",
"non-breaking-space",
],
listLevelMax: 2,
blocktypeMap: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const createRichTextBlock = (
"history",
"link",
"links-remove",
"non-breaking-space",
],
draftJsProps: {
spellCheck: true,
Expand Down

0 comments on commit dbdc0f5

Please sign in to comment.