sjdemartini / mui-tiptap

A Material UI (MUI) styled WYSIWYG rich text editor, using Tiptap
MIT License
319 stars 43 forks source link

Unable to use half of the mui-tiptap controls #174

Closed sgober closed 1 year ago

sgober commented 1 year ago

Describe the bug

I am unable to use like half of the mui-tiptap components as they fail with errors like setColor or toggleUnderline are not functions (see screenshot below)

To Reproduce

Steps to reproduce the behavior:

This is what I am trying to use with json forms:

        <RichTextEditorProvider editor={editor}>
          <RichTextField
            controls={
              <MenuControlsContainer>
                {/* <MenuSelectFontFamily /> */}
                <MenuSelectHeading />
                {/* <MenuSelectFontSize /> */}
                <MenuDivider />
                <MenuButtonBold />
                <MenuButtonItalic />
                <MenuButtonUnderline />
                <MenuButtonStrikethrough />
                {/* <MenuButtonSubscript /> */}
                {/* <MenuButtonSuperscript /> */}
                <MenuDivider />
                {/* <MenuButtonTextColor
                  defaultTextColor="fff"
                  swatchColors={[
                    { value: '#000000', label: 'Black' },
                    { value: '#ffffff', label: 'White' },
                    { value: '#888888', label: 'Grey' },
                    { value: '#ff0000', label: 'Red' },
                    { value: '#ff9900', label: 'Orange' },
                    { value: '#ffff00', label: 'Yellow' },
                    { value: '#00d000', label: 'Green' },
                    { value: '#0000ff', label: 'Blue' }
                  ]}
                /> */}
                {/* <MenuButtonHighlightColor /> */}
                {/* <MenuDivider /> */}
                <MenuButtonEditLink />

                <MenuDivider />

                {/* <MenuSelectTextAlign /> */}
                {/* <MenuButtonAlignLeft />
                <MenuButtonAlignCenter />
                <MenuButtonAlignRight />
                <MenuButtonAlignJustify />
                <MenuDivider /> */}
                <MenuButtonOrderedList />
                <MenuButtonBulletedList />
                {/* <MenuButtonTaskList /> */}
                <MenuDivider />
                {/* TODO: why are these disabled */}
                {/* <MenuButtonIndent />
                <MenuButtonUnindent />
                <MenuDivider /> */}

                <MenuButtonBlockquote />
                <MenuDivider />
                <MenuButtonCode />
                <MenuButtonCodeBlock />
                <MenuDivider />
                <MenuButtonUndo />
                <MenuButtonRedo />
              </MenuControlsContainer>
            }
          />
        </RichTextEditorProvider>

With editor defined as follows:

  const editor = useEditor({
    extensions: [StarterKit],
    content: data,
    onUpdate({ editor }) {
      handleChange(path, editor.getHTML());
    }
  });

Expected behavior

Every mui-tiptap component above that is commented out results in an error similar to the one I described above. It is odd that some of them work and some of them don't

Screenshots

What is able to work:

Screenshot 2023-10-27 at 11 22 25 AM

Error when I try to use MenuButtonUnderline:

Screenshot 2023-10-27 at 10 46 49 AM

All other error are similar, not sure if this is a tiptap error or something specific to mui-tiptap

System (please complete the following information):

Additional context

Add any other context about the problem here.

sjdemartini commented 1 year ago

@sgober In order to use extensions with Tiptap, you have to (1) install the packages you need (e.g. @tiptap/extension-color) and (2) include the extension in your extensions array when instantiating your editor (e.g., extensions: [StarterKit, Color]).

So rather than just including StarterKit (which bundles many common Tiptap extensions, but not all), you'd need to also pass in explicitly the other extensions you want to utilize as well.

  const editor = useEditor({
    extensions: [StarterKit, Color, Underline], // And all others you want to include!
    content: data,
    onUpdate({ editor }) {
      handleChange(path, editor.getHTML());
    }
  });

For instance, in the demo example in mui-tiptap (essentially the same as what's in the CodeSandbox linked from the README), you can check out the full list of extensions it uses here:

https://github.com/sjdemartini/mui-tiptap/blob/02e47069009e292a70bb7793a0e45ceb6798d3cc/src/demo/Editor.tsx#L33-L35

https://github.com/sjdemartini/mui-tiptap/blob/02e47069009e292a70bb7793a0e45ceb6798d3cc/src/demo/Editor.tsx#L143

https://github.com/sjdemartini/mui-tiptap/blob/02e47069009e292a70bb7793a0e45ceb6798d3cc/src/demo/useExtensions.ts#L84-L188

Apologies that this wasn't clearer in the README. I've updated the Choosing your extensions section to hopefully help folks avoid these sorts of confusing errors in the future.

sgober commented 1 year ago

This was SUPER helpful, THANK YOU!

I pretty much got everything working except for the TextAlign stuff.

This is in my extensions declarations:

TextAlign.configure({
  types: ['heading', 'paragraph', 'image']
})

With this imported: import { TextAlign } from '@tiptap/extension-text-align';

All I get is a disabled dropdown (see screenshot). Is there something I'm missing?

Screenshot 2023-10-30 at 2 58 31 PM
sgober commented 1 year ago

Also, would it be possible to add a aria-label to the inputs like font family and font size? There currently is one, but it's not on the input so there is a missing label error from an accessibility standpoint

sjdemartini commented 1 year ago

@sgober Glad to hear that! The MenuSelectTextAlign component will show as disabled if the editor itself is not editable (likely not the case for you) or if the Tiptap editor can't set text-alignment for the current selected content in the editor. You can see the logic here (note that enabledAlignments will be left, center, right, and justify by default per Tiptap TextAlign default options):

https://github.com/sjdemartini/mui-tiptap/blob/ddb43ab46a6d63d2ec0a0117ca913ae35ecda79b/src/controls/MenuSelectTextAlign.tsx#L190-L195

So for instance, if your cursor/caret is currently inside a code block (not one of the types you configured in TextAlign.configure({ types: [...] }), it will show the text align select as disabled, since you cannot align a code block. But if you put your cursor in a regular text paragraph or click to select an image, the text-align select controls should be enabled. This is the behavior in the CodeSandbox demo linked from the README for instance: https://codesandbox.io/p/sandbox/mui-tiptap-demo-3zl2l6

Try adding some text and highlighting it in your example.

sgober commented 1 year ago

Unfortunately still no luck...

https://github.com/sjdemartini/mui-tiptap/assets/35042815/b9751347-05f6-4641-b0bb-5343b33b71fe

sjdemartini commented 1 year ago

Also, would it be possible to add a aria-label to the inputs like font family and font size? There currently is one, but it's not on the input so there is a missing label error from an accessibility standpoint

I've added a new issue to track that here https://github.com/sjdemartini/mui-tiptap/issues/175. The input itself has aria-hidden="true", so I don't think it needs the aria-label (though you could provide one via inputProps similar to https://github.com/mui/material-ui/issues/25697#issuecomment-1121041190 if you like, since inputProps provided to MenuSelect* components get passed onto <Select />). If you know more about how the a11y should be handled, please follow up on that issue!

sjdemartini commented 1 year ago

Unfortunately still no luck...

Hm, I'm not sure what's going on. Can you create a CodeSandbox that reproduces the problem? I can definitely take a look that way. Please create a new issue for that if so!

Or can you tell what you're doing differently compared to the CodeSandbox linked from the mui-tiptap README?

sgober commented 1 year ago

I'm having a ton of issues with code sandbox at the moment where I can't even load yours to be able to for it. This is essentially what I have and matches what you're doing in the code sandbox, but this is essentially what I have. It's used within json forms you, you might have to ignore some

import React from 'react';
import PropTypes from 'prop-types';
import { and, isDescriptionHidden, isStringControl, optionIs, rankWith, showAsRequired } from '@jsonforms/core';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { FormHelperText, Hidden, InputLabel, useTheme } from '@mui/material';
import { useFocus } from '@jsonforms/material-renderers';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Color } from '@tiptap/extension-color';
import { FontFamily } from '@tiptap/extension-font-family';
import { Highlight } from '@tiptap/extension-highlight';
import { Link } from '@tiptap/extension-link';
import { Subscript } from '@tiptap/extension-subscript';
import { Superscript } from '@tiptap/extension-superscript';
import { TableCell } from '@tiptap/extension-table-cell';
import { TableHeader } from '@tiptap/extension-table-header';
import { TableRow } from '@tiptap/extension-table-row';
import { TaskItem } from '@tiptap/extension-task-item';
import { TaskList } from '@tiptap/extension-task-list';
import { TextAlign } from '@tiptap/extension-text-align';
import { TextStyle } from '@tiptap/extension-text-style';
import { Underline } from '@tiptap/extension-underline';
import {
  FontSize,
  MenuButtonAddTable,
  MenuButtonBlockquote,
  MenuButtonBold,
  MenuButtonBulletedList,
  MenuButtonCode,
  MenuButtonCodeBlock,
  MenuButtonEditLink,
  MenuButtonHighlightColor,
  MenuButtonHorizontalRule,
  MenuButtonItalic,
  MenuButtonOrderedList,
  MenuButtonRedo,
  MenuButtonRemoveFormatting,
  MenuButtonSubscript,
  MenuButtonSuperscript,
  MenuButtonStrikethrough,
  MenuButtonTaskList,
  MenuButtonTextColor,
  MenuButtonUnderline,
  MenuButtonUndo,
  MenuControlsContainer,
  MenuDivider,
  MenuSelectFontFamily,
  MenuSelectFontSize,
  MenuSelectHeading,
  MenuSelectTextAlign,
  LinkBubbleMenu,
  LinkBubbleMenuHandler,
  RichTextField,
  RichTextEditorProvider,
  TableImproved
} from 'mui-tiptap';
import _ from 'lodash';

export const RichTextRenderer = props => {
  const theme = useTheme();
  const [focused] = useFocus();
  const { config, data, description, errors, handleChange, label, path, required, uischema, visible } = props;

  const isValid = errors.length === 0;
  const appliedUiSchemaOptions = _.merge({}, config, uischema.options);
  const showDescription = !isDescriptionHidden(
    visible,
    description,
    focused,
    appliedUiSchemaOptions.showUnfocusedDescription
  );
  const firstFormHelperText = showDescription ? description : !isValid ? errors : null;
  const secondFormHelperText = showDescription && !isValid ? errors : null;

  const editor = useEditor({
    extensions: [
      StarterKit,
      Color,
      FontFamily,
      FontSize,
      Highlight,
      Link,
      LinkBubbleMenuHandler,
      Subscript.extend({
        excludes: 'superscript'
      }),
      Superscript.extend({
        excludes: 'subscript'
      }),
      TableCell,
      TableHeader,
      TableImproved.configure({
        resizable: true
      }),
      TableRow,
      TaskItem.configure({
        nested: true
      }),
      TaskList,
      TextAlign.configure({
        types: ['heading', 'paragraph', 'image']
      }),
      TextStyle,
      Underline
    ],
    content: data,
    onUpdate({ editor }) {
      handleChange(path, editor.getHTML());
    }
  });

  const fontOptions = [
    { label: 'Roboto', value: 'Roboto' },
    { label: 'Roboto Condensed', value: 'Roboto Condensed' },
    { label: 'Barlow', value: 'Barlow' },
    { label: 'Barlow Condensed', value: 'Barlow Condensed' }
  ];

  const textColorOptions = [
    { value: '#000000', label: 'Black' },
    { value: '#ffffff', label: 'White' },
    { value: '#888888', label: 'Grey' },
    { value: theme.palette.error.main, label: 'Red' },
    { value: theme.palette.warning.main, label: 'Orange' },
    { value: '#ffff00', label: 'Yellow' },
    { value: theme.palette.success.main, label: 'Green' },
    { value: theme.palette.primary.main, label: 'Blue' }
  ];

  const highlightColorOptions = [
    { value: '#d6d6d6', label: 'Light gray' },
    { value: theme.palette.error.background, label: 'Light red' },
    { value: theme.palette.warning.background, label: 'Light orange' },
    { value: '#ffffb1', label: 'Light yellow' },
    { value: theme.palette.success.background, label: 'Light green' },
    { value: theme.palette.primary.background, label: 'Light blue' }, // need to be updated in theme
    { value: theme.palette.secondary.background, label: 'Light blue' }, // need to be updated in theme
    { value: '#dcd7f3', label: 'Light purple' }
  ];

  return (
    <Hidden xsUp={!visible}>
      <InputLabel
        component="legend"
        error={!isValid}
        required={showAsRequired(required, appliedUiSchemaOptions.hideRequiredAsterisk)}>
        {label}
      </InputLabel>
      <div className="rich-text">
        <RichTextEditorProvider editor={editor}>
          <RichTextField
            controls={
              <MenuControlsContainer>
                <MenuSelectFontFamily options={fontOptions} />
                <MenuDivider />
                <MenuSelectHeading />
                <MenuDivider />
                <MenuSelectFontSize />
                <MenuDivider />
                <MenuButtonBold aria-label="Rich Text Input Bold" />
                <MenuButtonItalic aria-label="Rich Text Input Italic" />
                <MenuButtonUnderline aria-label="Rich Text Input Underline" />
                <MenuButtonStrikethrough aria-label="Rich Text Input Strikethrough" />
                <MenuButtonSubscript aria-label="Rich Text Input Subscript" />
                <MenuButtonSuperscript aria-label="Rich Text Input Superscript" />
                <MenuDivider />
                <MenuButtonTextColor
                  aria-label="Rich Text Input Text Color"
                  defaultTextColor="#000"
                  swatchColors={textColorOptions}
                />
                <MenuButtonHighlightColor
                  aria-label="Rich Text Input Highlight Color"
                  swatchColors={highlightColorOptions}
                />
                <MenuDivider />
                <MenuButtonEditLink aria-label="Rich Text Input Link" />
                <MenuDivider />
                {/* TODO: figure out how this works */}
                <MenuSelectTextAlign />
                <MenuDivider />
                <MenuButtonOrderedList aria-label="Rich Text Input Ordered List" />
                <MenuButtonBulletedList aria-label="Rich Text Input Bullet List" />
                <MenuButtonTaskList aria-label="Rich Text Input Task List" />
                <MenuDivider />
                <MenuButtonBlockquote aria-label="Rich Text Input Block Quote" />
                <MenuDivider />
                <MenuButtonCode aria-label="Rich Text Input Code" />
                <MenuButtonCodeBlock aria-label="Rich Text Input Code Block" />
                <MenuDivider />
                <MenuButtonHorizontalRule aria-label="Rich Text Input Horizontal Rule" />
                <MenuButtonAddTable aria-label="Rich Text Input Add Table" />
                <MenuDivider />
                <MenuButtonRemoveFormatting aria-label="Rich Text Input Remove Formatting" />
                <MenuDivider />
                <MenuButtonUndo aria-label="Rich Text Input Undo" />
                <MenuButtonRedo aria-label="Rich Text Input Redo" />
              </MenuControlsContainer>
            }
          />
          <LinkBubbleMenu />
        </RichTextEditorProvider>
      </div>
      <FormHelperText error={!isValid && !showDescription} sx={{ margin: '3px 0 0' }}>
        {firstFormHelperText}
      </FormHelperText>
      <FormHelperText error={!isValid} sx={{ margin: '3px 0 0' }}>
        {secondFormHelperText}
      </FormHelperText>
    </Hidden>
  );
};

It very much seems like I could be missing something small in the setup. All of the tiptap extensions are version 2.1.12

sjdemartini commented 1 year ago

@sgober Thanks, I was able to copy that locally (and remove the jsonforms stuff) and reproduce the above issue with MenuSelectTextAlign showing as disabled.

tl;dr: It seems this can be fixed by changing types: ['heading', 'paragraph', 'image'] to types: ['heading', 'paragraph'], since that array should only contain extension types that are part of the included Tiptap editor extensions, and your example is not including Image/ResizableImage.

I didn't realize this was the behavior until testing your example. But if there are any "unknown" types in that list, the editor.can().setTextAlign() method (used by MenuSelectTextAlign under the hood) will always return false, since it checks to see if "every" type can receive the update of text align under the hood here. Without the Image (or ResizableImage) extension being passed into extensions for useEditor, Tiptap doesn't recognize the "image" type so returns false for commands.updateAttributes("image", {textAlign}).

Let me know if that resolves the problem on your end!

sgober commented 1 year ago

@sjdemartini this did the trick, THANK YOU! I appreciate you taking the time to figure out what was wrong.

One other quick question for you, I'm seeing the indent/unindent buttons disabled (if you comment the the isTouchDevice conditional out in the code sandbox you can recreate). Is there something missing for this to work? I'm also not able to get shift+tab to work

sjdemartini commented 1 year ago

@sgober Great! The indent and unindent actions/buttons are specifically for lists (bullet, number, task). Also note that you can only indent something if there is a list item above it that is part of the same list and is at the same indentation level as it. You could also look at Tiptap's demo here to see the behavior: https://tiptap.dev/api/nodes/bullet-list. Hope that helps.

sgober commented 1 year ago

Got it - doesn't fully serve my purposes so will probably leave it out for now. Thanks again for all your help!