Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): support direction and text align in toolbar #6817

Closed
20 changes: 20 additions & 0 deletions app/components/Icons/LtrIcon.tsx
@@ -0,0 +1,20 @@
import * as React from "react";

function LtrIcon() {
return (
<svg
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,0)">
<path
xmlns="http://www.w3.org/2000/svg"
d="M8 12V8q-1.25 0-2.125-.875T5 5q0-1.25.875-2.125T8 2h6v1.5h-1.5V12H11V3.5H9.5V12Zm6 6-1.062-1.062 1.187-1.188H3v-1.5h11.125l-1.187-1.188L14 12l3 3ZM8 6.5v-3q-.625 0-1.062.438Q6.5 4.375 6.5 5t.438 1.062Q7.375 6.5 8 6.5ZM8 5Z"
/>
</g>
</svg>
);
}

export default LtrIcon;
17 changes: 17 additions & 0 deletions app/components/Icons/RtlIcon.tsx
@@ -0,0 +1,17 @@
import * as React from "react";

function RtlIcon() {
return (
<svg
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<g xmlns="http://www.w3.org/2000/svg" transform="translate(0,0)">
<path d="m6 18-3-3 3-3 1.062 1.062-1.187 1.188H17v1.5H5.875l1.187 1.188Zm2-6V8q-1.25 0-2.125-.875T5 5q0-1.25.875-2.125T8 2h6v1.5h-1.5V12H11V3.5H9.5V12Zm0-5.5v-3q-.625 0-1.062.438Q6.5 4.375 6.5 5t.438 1.062Q7.375 6.5 8 6.5ZM8 5Z" />
</g>
</svg>
);
}

export default RtlIcon;
7 changes: 6 additions & 1 deletion app/editor/components/FloatingToolbar.tsx
Expand Up @@ -5,7 +5,7 @@ import { Portal as ReactPortal } from "react-portal";
import styled, { css } from "styled-components";
import { isCode } from "@shared/editor/lib/isCode";
import { findParentNode } from "@shared/editor/queries/findParentNode";
import { depths, s } from "@shared/styles";
import { depths, hideScrollbars, s } from "@shared/styles";
import { Portal } from "~/components/Portal";
import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener";
Expand Down Expand Up @@ -294,6 +294,9 @@ const MobileWrapper = styled.div`
box-sizing: border-box;
z-index: ${depths.editorToolbar};

overflow-x: auto;
overflow-y: hidden;

&:after {
content: "";
position: absolute;
Expand Down Expand Up @@ -322,6 +325,8 @@ const Wrapper = styled.div<WrapperProps>`
box-sizing: border-box;
pointer-events: none;
white-space: nowrap;
overflow: auto hidden;
${hideScrollbars()}

${arrow}

Expand Down
10 changes: 9 additions & 1 deletion app/editor/components/SelectionToolbar.tsx
Expand Up @@ -33,6 +33,7 @@ type Props = {
rtl: boolean;
isTemplate: boolean;
readOnly?: boolean;
isCommentEditor?: boolean;
canComment?: boolean;
canUpdate?: boolean;
onOpen: () => void;
Expand Down Expand Up @@ -104,6 +105,7 @@ export default function SelectionToolbar(props: Props) {
const isDragging = useIsDragging();
const previousIsActive = usePrevious(isActive);
const isMobile = useMobile();
const { isCommentEditor } = props;

React.useEffect(() => {
// Trigger callbacks when the toolbar is opened or closed
Expand Down Expand Up @@ -244,7 +246,13 @@ export default function SelectionToolbar(props: Props) {
} else if (readOnly) {
items = getReadOnlyMenuItems(state, !!canUpdate, dictionary);
} else {
items = getFormattingMenuItems(state, isTemplate, isMobile, dictionary);
items = getFormattingMenuItems(
state,
isTemplate,
isMobile,
dictionary,
isCommentEditor
);
}

// Some extensions may be disabled, remove corresponding items
Expand Down
2 changes: 1 addition & 1 deletion app/editor/components/ToolbarMenu.tsx
Expand Up @@ -120,7 +120,7 @@ function ToolbarMenu(props: Props) {

const FlexibleWrapper = styled.div`
color: ${s("textSecondary")};
overflow: hidden;
overflow: visible;
display: flex;
gap: 6px;
`;
Expand Down
15 changes: 12 additions & 3 deletions app/editor/index.tsx
Expand Up @@ -132,6 +132,7 @@ export type Props = {
style?: React.CSSProperties;
/** Optional style overrides for the contenteeditable */
editorStyle?: React.CSSProperties;
isCommentEditor?: boolean;
};

type State = {
Expand Down Expand Up @@ -730,8 +731,16 @@ export class Editor extends React.PureComponent<
};

public render() {
const { dir, readOnly, canUpdate, grow, style, className, onKeyDown } =
this.props;
const {
dir,
readOnly,
canUpdate,
grow,
style,
className,
onKeyDown,
isCommentEditor,
} = this.props;
const { isRTL } = this.state;

return (
Expand All @@ -748,7 +757,6 @@ export class Editor extends React.PureComponent<
>
<EditorContainer
dir={dir}
rtl={isRTL}
grow={grow}
readOnly={readOnly}
readOnlyWriteCheckboxes={canUpdate}
Expand All @@ -761,6 +769,7 @@ export class Editor extends React.PureComponent<
{this.view && (
<SelectionToolbar
rtl={isRTL}
isCommentEditor={isCommentEditor}
readOnly={readOnly}
canUpdate={this.props.canUpdate}
canComment={this.props.canComment}
Expand Down
59 changes: 58 additions & 1 deletion app/editor/menus/formatting.tsx
Expand Up @@ -16,23 +16,31 @@ import {
OutdentIcon,
IndentIcon,
CopyIcon,
AlignLeftIcon,
AlignCenterIcon,
AlignRightIcon,
Heading3Icon,
} from "outline-icons";
import { EditorState } from "prosemirror-state";
import { isInTable } from "prosemirror-tables";
import * as React from "react";
import isAttrActiveOnSelection from "@shared/editor/queries/isAttrActiveOnSelection";
import isInCode from "@shared/editor/queries/isInCode";
import isInList from "@shared/editor/queries/isInList";
import isMarkActive from "@shared/editor/queries/isMarkActive";
import isNodeActive from "@shared/editor/queries/isNodeActive";
import { MenuItem } from "@shared/editor/types";
import { Direction, TextAlign } from "@shared/types";
import LtrIcon from "~/components/Icons/LtrIcon";
import RtlIcon from "~/components/Icons/RtlIcon";
import { Dictionary } from "~/hooks/useDictionary";

export default function formattingMenuItems(
state: EditorState,
isTemplate: boolean,
isMobile: boolean,
dictionary: Dictionary
dictionary: Dictionary,
isCommentEditor?: boolean
): MenuItem[] {
const { schema } = state;
const isTable = isInTable(state);
Expand Down Expand Up @@ -124,6 +132,55 @@ export default function formattingMenuItems(
attrs: { level: 2 },
visible: allowBlocks && !isCode,
},
{
name: "separator",
},
{
name: "left_to_right",
tooltip: dictionary.leftToRight,
icon: <LtrIcon />,
active: isAttrActiveOnSelection({ attr: Direction.LTR, attrKey: "dir" }),
},
{
name: "right_to_left",
tooltip: dictionary.rightToLeft,
icon: <RtlIcon />,
active: isAttrActiveOnSelection({ attr: Direction.RTL, attrKey: "dir" }),
},
{
name: "separator",
visible: !isCommentEditor,
},
{
name: "align_left",
tooltip: dictionary.alignLeft,
icon: <AlignLeftIcon />,
visible: !isCommentEditor,
active: isAttrActiveOnSelection({
attr: TextAlign.Left,
attrKey: "textAlign",
}),
},
{
name: "align_center",
tooltip: dictionary.alignCenter,
icon: <AlignCenterIcon />,
visible: !isCommentEditor,
active: isAttrActiveOnSelection({
attr: TextAlign.Center,
attrKey: "textAlign",
}),
},
{
name: "align_right",
tooltip: dictionary.alignRight,
icon: <AlignRightIcon />,
visible: !isCommentEditor,
active: isAttrActiveOnSelection({
attr: TextAlign.Right,
attrKey: "textAlign",
}),
},
{
name: "separator",
visible: (allowBlocks || isList) && !isCode,
Expand Down
3 changes: 3 additions & 0 deletions app/hooks/useDictionary.ts
Expand Up @@ -13,6 +13,9 @@ export default function useDictionary() {
alignCenter: t("Align center"),
alignLeft: t("Align left"),
alignRight: t("Align right"),
rightToLeft: t("Right to left"),
leftToRight: t("Left to right"),
Auto: t("Auto"),
alignFullWidth: t("Full width"),
bulletList: t("Bulleted list"),
checkboxList: t("Todo list"),
Expand Down
8 changes: 7 additions & 1 deletion app/scenes/Document/components/CommentEditor.tsx
Expand Up @@ -30,7 +30,13 @@ const CommentEditor = (
const user = useCurrentUser({ rejectOnEmpty: false });

return (
<Editor extensions={extensions} userId={user?.id} {...props} ref={ref} />
<Editor
extensions={extensions}
userId={user?.id}
{...props}
ref={ref}
isCommentEditor
/>
);
};

Expand Down
1 change: 0 additions & 1 deletion app/scenes/Document/components/RevisionViewer.tsx
Expand Up @@ -43,7 +43,6 @@ function RevisionViewer(props: Props) {
<EditorContainer
dangerouslySetInnerHTML={{ __html: revision.html }}
dir={revision.dir}
rtl={revision.rtl}
/>
{children}
</Flex>
Expand Down
10 changes: 9 additions & 1 deletion server/editor/index.test.ts
Expand Up @@ -4,7 +4,15 @@ test("renders an empty doc", () => {
const ast = parser.parse("");

expect(ast?.toJSON()).toEqual({
content: [{ type: "paragraph" }],
content: [
{
type: "paragraph",
attrs: {
dir: null,
textAlign: null,
},
},
],
type: "doc",
});
});
2 changes: 1 addition & 1 deletion server/models/helpers/ProsemirrorHelper.tsx
Expand Up @@ -165,7 +165,7 @@ export default class ProsemirrorHelper {
<>
{options?.title && <h1 dir={rtl ? "rtl" : "ltr"}>{options.title}</h1>}
{options?.includeStyles !== false ? (
<EditorContainer dir={rtl ? "rtl" : "ltr"} rtl={rtl} staticHTML>
<EditorContainer dir={rtl ? "rtl" : "ltr"} staticHTML>
{content}
</EditorContainer>
) : (
Expand Down
16 changes: 16 additions & 0 deletions server/utils/ProsemirrorHelper.test.ts
Expand Up @@ -26,6 +26,10 @@ describe("#ProsemirrorHelper", () => {
content: [
{
type: "paragraph",
attrs: {
dir: null,
textAlign: null,
},
content: [
{
type: "text",
Expand Down Expand Up @@ -67,6 +71,10 @@ describe("#ProsemirrorHelper", () => {
content: [
{
type: "paragraph",
attrs: {
dir: null,
textAlign: null,
},
content: [
{
type: "text",
Expand Down Expand Up @@ -108,6 +116,10 @@ describe("#ProsemirrorHelper", () => {
content: [
{
type: "paragraph",
attrs: {
dir: null,
textAlign: null,
},
content: [
{
type: "text",
Expand Down Expand Up @@ -170,6 +182,10 @@ describe("#ProsemirrorHelper", () => {
content: [
{
type: "paragraph",
attrs: {
dir: null,
textAlign: null,
},
content: [
{
type: "text",
Expand Down
16 changes: 16 additions & 0 deletions shared/constants.ts
Expand Up @@ -28,3 +28,19 @@ export const UserPreferenceDefaults: UserPreferences = {
[UserPreference.UseCursorPointer]: true,
[UserPreference.CodeBlockLineNumers]: true,
};

export const NodesWithTextDirAlignSupport = [
"container_notice",
"th",
"td",
"table",
"paragraph",
"ordered_list",
"list_item",
"heading",
"checkbox_item",
"checkbox_list",
"bullet_list",
"blockquote",
"attachment",
];