portive / wysimark

The Wysiwyg Editor for Markdown: 100% CommonMark + GFM with Uploads and Image Resizing
Other
231 stars 41 forks source link

How to add a custom button on toolbar? #23

Open cevio opened 1 year ago

cevio commented 1 year ago

It's a very nice markdown editor!

I have some questions: React

  1. How to add a custom button and resolve its logic? I can't find in docs.
  2. Images uploader post api https://api.portive.com/api/v1/upload? How to change api by myself?
  3. How to use i18n

Thanks!

thesunny commented 1 year ago

@cevio

  1. There is currently no built-in way to do this but I'd like to add it. Can you let me know what your use case is? It could help me come up with the right design.
  2. Wysimark uploads are currently designed to work with our Portive service but we will (coming soon) enable inserting your own Markdown programmatically which will allow you to insert images any way you want. You will be responsible for resizing your images though in this case as with the Portive API, Portive takes care of the image resizing for you.
  3. Is there a specific language you're thinking of? Wysimark is built on Slate which supports i18n, but we haven't specifically built Wysimark to support i18n yet though we plan to. Some things should just work, but other things like right-to-left probably require us to be more intentional about the design to be compatible with it.
cevio commented 1 year ago

@thesunny

My project is PJBlog which since 2004-present. We decided to restart the project. So, Markdown editor is a very important function for us. The core of the blog is the editor, we hope that the basic functions and extended functions can be realized through your editor. But we're running into some difficulties now.

Can you consider the following implementation?

import { Editable, useEditor, Plugin } from '@wysimark/react';
import type { IPlugin } from '@wysimark/react';
import type { PropsWithoutRef } from 'react';

class CustomPlugin extends Plugin implements IPlugin {
  static create(key: string) {
    return new CustomPlugin(key);
  }
}

const plugins = [
  Plugin.Headings('headings', 2, 3),
  Plugin.Source('source', ...),
  Plugin.Image('image', {
    api: '...'
  }),
  CustomPlugin.create('custom'),
  // ... and more plugins
]

export default function Editor(props: PropsWithoutRef<{ value: string }>) {
  const editor = useEditor({
    initialMarkdown: props.value,
    // ... more options
  })
  return <Editable.Context plugins={plugins} language="en">
    <Editable.Toolbar format="source | headings| aligns font fontSize | custom history ..." />
    <Editable.Editor editor={editor} />
  </Editable.Context>
}

The plug-in provides the content on the toolbar and the function realization of the main part of the editor for the editor, and can achieve the desired effect through its own configuration. format can control the sorting order on the toolbar.

The separation of toolbar and editor can make the editor more customizable.

image
albjeremias commented 1 year ago

well, here we go. my project is helpbuttons.org and it would be awesome to be able to customize the toolbar so that some of the buttons could be hidden. also in the near future i will like to add a possibility to mention someone (but this is another issue)

a possible implementation could be:

export default function TextEditor() {
  const editor = useEditor({
    initialMarkdown: 'Type your post text here...',
  });
  return (
          <Editable editor={editor}>
               <EditableDefaultToolbar/>
        </Editable>);
}

custom toolbar:

export default function TextEditor() {
  const editor = useEditor({
    initialMarkdown: 'Type your post text here...',
  });
  return (
          <Editable editor={editor}>
               <CustomEditableToolbar/>
        </Editable>);
}

function CustomEditableToolar() {
return (<>
               <EditableButtonParagraph/>
               <EditableButtonBold/>
          </>
)
}
CodyCodes95 commented 12 months ago

+1 for this! I need to remove some toolbar options for my implementation and right now just have some janky CSS which targets the buttons and hides them. Would love to have this configurable.

seangwright commented 11 months ago

Adding and removing buttons (e.g. customizing the toolbar) would definitely be a nice feature enhancement.

juniormayhe commented 5 months ago

Customizing the buttons is tricky, since the class names are dynamic, without changing the source code we can perform a hack.

The first is to hide the buttons by position in css:

.mdeditor > div > div > div > div:nth-child(5),
.mdeditor > div > div > div > div:nth-child(7),
.mdeditor > div > div > div > div:nth-child(9),
.mdeditor > div > div > div > div:nth-child(10),
.mdeditor > div > div > div > div:nth-child(11),
.mdeditor > div > div > div > div:nth-child(12) {
  display: none;
}

//if you want to customize the editor you can do it here or use className
.mdeditor > div > div[role="textbox"] {
  padding: 1.2rem 1.2rem 0.2rem;
  color: red;
}

Next to remove the dropdown options of menus, we can add an observer to listen to changes in UI. In the following example, the code will hide "Checklist", "Heading 3", "Heading 4", "Heading 5", "Heading 6" options.

"use client";
import { Editable, useEditor } from "@wysimark/react";
import { useEffect, useRef, useState } from "react";
import "./hideToolbar.css";
const Test = () => {
  const [markdown, setMarkdown] = useState(
    "Favorite recipes:\n\n- Lasagna\n\n- Parmegianna Bistec\n\n- Mashed Potatoes\n\nYummy!"
  );
  const editor = useEditor({});

  useEffect(() => {
    const observer = new MutationObserver((mutationsList) => {
      for (const mutation of mutationsList) {
        if (
          mutation.type === "childList" ||
          mutation.type === "characterData"
        ) {
          const titles = document.querySelectorAll("div.--title");
          titles.forEach((title: any) => {
            const textContent = title.textContent.trim();
            const optionsToRemove = [
              "Checklist",
              "Heading 3",
              "Heading 4",
              "Heading 5",
              "Heading 6",
            ];
            if (optionsToRemove.includes(textContent)) {
              console.log('Found element with text "Numbered List"');
              const parentElement = title.parentElement;
              if (parentElement) {
                parentElement.style.display = "none";
              }
            }
          });
        }
      }
    });

    // Start observing the entire document for changes
    observer.observe(document.documentElement, {
      subtree: true,
      childList: true,
      characterData: true,
    });

    // Clean up the observer when the component is unmounted
    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <div className="mdeditor">
      <Editable editor={editor} value={markdown} onChange={setMarkdown} />
      <br></br>
      <div>What you will get:</div>
      <code style={{ backgroundColor: "#dadada" }}>
        {JSON.stringify(markdown)}
      </code>
    </div>
  );
};

export default Test;