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

Insert inline LateX within Slate #1913

Closed
kevinguard opened this issue Jun 19, 2018 · 9 comments
Closed

Insert inline LateX within Slate #1913

kevinguard opened this issue Jun 19, 2018 · 9 comments
Labels

Comments

@kevinguard
Copy link

kevinguard commented Jun 19, 2018

I am trying to insert an inline LateX formula and am having some difficulties getting the equation rendered properly. To do this, I have created the following data model and fed it to the editor component. The extra blub is marked by latex, whose text attribute is set to \\lambda.

{
  "document": {
    "nodes": [
      {
        "object": "block",
        "type": "paragraph",
        "nodes": [
          {
            "object": "text",
            "leaves": [
              {
                "text": "This is editable "
              },
              {
                "text": "rich",
                "marks": [
                  {
                    "type": "bold"
                  }
                ]
              },
              {
                "text": " \\lambda ",
                "marks": [
                  {
                    "type": "latex"
                  }
                ]
              },
              {
                "text": " text, "
              },
              {
                "text": "much",
                "marks": [
                  {
                    "type": "italic"
                  }
                ]
              },
              {
                "text": " better than a "
              },
              {
                "text": "<textarea>",
                "marks": [
                  {
                    "type": "code"
                  }
                ]
              },
              {
                "text": "!"
              }
            ]
          }
        ]
      }
   }
}

Following is the method where I analyze the node types:

_renderMark = props => {
        const { children, mark, attributes } = props;

        switch (mark.type) {
            case "bold":
                return <strong {...attributes}>{children}</strong>;
            case "code":
                return <code {...attributes}>{children}</code>;
            case "italic":
                return <em {...attributes}>{children}</em>;
            case "underlined":
                return <u {...attributes}>{children}</u>;
            case "latex":
                return <InlineMath {...attributes} math={children}/>;
        }
    };

Following is the method that renders the editor:

_renderEditor = () => {
        return (
            <div className="editor">
                <Editor placeholder="Enter some rich text..."
                        value={this.props.value}
                        onChange={this._onChange}
                        onKeyDown={this._onKeyDown}
                        renderNode={this._renderNode}
                        renderMark={this._renderMark}
                        spellCheck
                        autoFocus/>
            </div>
        );
    };

As it can be seen in the following image, the letter lambda is both rendered as \lambda and λ, the latter of which is the expected behavior. Can someone please advise what I might be doing wrong?

Note: InlineMath comes from react-katex.

image

Thanks!

@isubasti
Copy link
Contributor

isubasti commented Jun 19, 2018

might want to use inline instead of mark and put the latex(e.g \lambda) into data instead of text

@ianstormtaylor
Copy link
Owner

Hey, thanks for using Slate! Unfortunately, we can't offer support for usage questions in the issues here because it becomes overwhelming to maintain the project if the issues are filled with questions.

However, we do have a Slack channel and people are constantly asking and answering questions in there. So I'm going to close this issue, but I definitely recommend joining the Slack channel if you want to find people that might be able to help.

Thanks for understanding!

@Anaizing
Copy link

What is the Slack channel name?

@isubasti
Copy link
Contributor

@Anaizing it's https://slate-js.slack.com/

@williamstein
Copy link

I just wanted to comment here that I did implement math typesetting/editing support in CoCalc using Slate. Math is rendered using katex and editing is done using a CodeMirror editor (all using void elements). To try it out, use https://cocalc.com and create a new project and then a new file in it with extension .md. The editor you see on the right is slate (and the left is markdown).

@onzag
Copy link

onzag commented Feb 3, 2023

I used mathquill and it worked much better, as in, the editor was working in flow with mathquill so they worked seamlessly.

Sadly I have issues, with accessibility, limited functionality and arabic, which makes me believe I may need to fork mathquill, but I haven't found a better combo so far.

@onzag
Copy link

onzag commented Feb 3, 2023

I am using a custom Wrapper on top of slate which offers multiple editor compatibility, but this may give you an idea on how it works.

export class MathQuillDynamicUIHandler extends React.Component<ISlateTemplateUIHandlerProps> {
  constructor(props: ISlateTemplateUIHandlerProps) {
    super(props);

    this.onUpdate = this.onUpdate.bind(this);
    this.onFocus = this.onFocus.bind(this);
    this.forceIdChange = this.forceIdChange.bind(this);
    this.onEscape = this.onEscape.bind(this);
    this.getPath = this.getPath.bind(this);
  }
  public getPath() {
    const path = this.props.helpers.ReactEditor.findPath(
      this.props.helpers.ReactEditor as any,
      this.props.element as any,
    );
    return path;
  }
  public onUpdate(newValue: string) {
    const path = this.props.helpers.ReactEditor.findPath(
      this.props.helpers.ReactEditor as any,
      this.props.element as any,
    );
    this.props.helpers.setUIHandlerArg("value", newValue, path);
  }
  public onFocus() {
    const mqfield = (window as any).MQREGISTRY[this.props.element.uiHandlerArgs.mathId];
    mqfield && mqfield.focus();
    const path = this.props.helpers.ReactEditor.findPath(
      this.props.helpers.ReactEditor as any,
      this.props.element as any,
    );
    this.props.helpers.selectPath(path);
  }
  public forceIdChange(newId: string) {
    this.props.helpers.setUIHandlerArg("mathId", newId, this.getPath());
  }
  public componentDidUpdate(prevProps: ISlateTemplateUIHandlerProps) {
    if (!prevProps.selected && this.props.selected) {
      const path = this.props.helpers.ReactEditor.findPath(
        this.props.helpers.ReactEditor as any,
        this.props.element as any,
      );

      const previousTextAnchor = this.props.helpers.getPreviousTextAnchor();
      const beforeAnchor = [...path];
      beforeAnchor[path.length - 1]--;
      const afterAnchor = [...path];
      afterAnchor[path.length + 1]++;

      const isBefore = previousTextAnchor && this.props.helpers.Path.equals(previousTextAnchor, afterAnchor);
      const isAfter = previousTextAnchor && this.props.helpers.Path.equals(previousTextAnchor, beforeAnchor);

      const mqfield = (window as any).MQREGISTRY[this.props.element.uiHandlerArgs.mathId];
      mqfield && mqfield.focus();
      this.props.helpers.selectPath(path);

      if (isBefore) {
        mqfield && mqfield.moveToRightEnd();
      } else if (isAfter) {
        mqfield && mqfield.moveToLeftEnd();
      }
    }
  }
  public onEscape(dir: "left" | "right") {
    const addedPathValue = dir === "left" ? -1 : 1;
    const path = this.getPath();

    const last = path.length - 1;
    path[last] += addedPathValue;
    if (path[last] < 0) {
      path[last] = 0;
    }

    const mqfield = (window as any).MQREGISTRY[this.props.element.uiHandlerArgs.mathId];
    mqfield && mqfield.blur();
  
    let offset = 0;
    if (dir === "left") {
      const element = this.props.helpers.Node.get(this.props.helpers.editor, path);
      offset = measureTextInSlate(element);
    }

    this.props.helpers.focusAt({
      anchor: {
        offset,
        path,
      },
      focus: {
        offset,
        path,
      },
    });
  }
  public render() {
    return (
      <span {...this.props.attributes} onFocus={this.onFocus}>
        <MathQuillField
          value={this.props.args.value || ""}
          onChange={this.onUpdate}
          id={this.props.args.mathId}
          forceIdChange={this.forceIdChange}
          onEscape={this.onEscape}
        />
        {this.props.children}
      </span>
    );
  }
}

So this is given to the "renderElement" and the result is a seamless formula.

@onzag
Copy link

onzag commented Feb 3, 2023

export class MathQuillField extends React.Component<IMathQuillFieldProps, IMathQuillFieldState> {
  private rootRef: React.RefObject<HTMLDivElement | HTMLSpanElement>;
  private internalMathField: any = null;
  private isUnmounted: boolean = false;
  constructor(props: IMathQuillFieldProps) {
    super(props);

    this.rootRef = React.createRef();
    this.processMath = this.processMath.bind(this);

    this.state = {
      isReady: isLibReady("script-mathquill") && isLibReady("script-jquery"),
    };
  }
  public async componentDidMount() {
    if (this.state.isReady) {
      MQ = MQ || (window as any).MathQuill.getInterface(2);
      this.processMath();
    } else {
      loadCSS("css-mathquill", "/rest/resource/mathquill.css");
      await loadLib("script-jquery", "/rest/resource/jquery.min.js", () => {
        return (window as any).jQuery;
      });
      await loadLib("script-mathquill", "/rest/resource/mathquill.min.js", () => {
        return (window as any).MathQuill;
      });
      MQ = MQ || (window as any).MathQuill.getInterface(2);
      !this.isUnmounted && this.setState({
        isReady: true,
      });
      !this.isUnmounted && this.processMath();
    }
  }
  public componentWillUnmount() {
    this.isUnmounted = true;
    if (this.props.static) {
      delete (window as any).MQSTATICREGISTRY[this.props.id];
    } else {
      delete (window as any).MQREGISTRY[this.props.id];
    }
  }
  public processMath(props: IMathQuillFieldProps = this.props) {
    if (this.isUnmounted) {
      return;
    }

    if (this.internalMathField) {
      this.internalMathField.latex(props.value);
    } else {
      if (props.static) {
        this.internalMathField = MQ.StaticMath(this.rootRef.current);
        (window as any).MQSTATICREGISTRY[props.id] = this.internalMathField;
      } else {
        // during copy events two fields might share the same uuid
        // which is a huge issue
        let idToUse = props.id;
        if ((window as any).MQREGISTRY[props.id]) {
          idToUse = uuid.v4().replace(/-/g, "");
          props.forceIdChange(idToUse);
        }

        this.internalMathField = MQ.MathField(this.rootRef.current, {
          handlers: {
            edit: () => {
              const latex = this.internalMathField.latex();
              props.onChange(latex);
            },
            moveOutOf: (dir: string) => {
              if (dir === (window as any).MathQuill.R) {
                props.onEscape && props.onEscape("right");
              } else {
                props.onEscape && props.onEscape("left");
              }
            }
          },
        });

        (window as any).MQREGISTRY[idToUse] = this.internalMathField;
      }
      if (this.internalMathField.latex() !== props.value) {
        this.internalMathField.latex(props.value);
      }
    }
  }
  public render() {
    const Component = this.props.component || "span";
    return (
      <Component className={this.props.className} ref={this.rootRef as any} children={this.props.value}/>
    );
  }
  public shouldComponentUpdate(nextProps: IMathQuillFieldProps, nextState: IMathQuillFieldState) {
    const internalValue = this.internalMathField && this.internalMathField.latex();
    if (nextProps.value !== internalValue && nextState.isReady) {
      this.processMath(nextProps);
    }
    return false;
  }
}

@onzag
Copy link

onzag commented Feb 3, 2023

Demonstration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants