tinymce / tinymce-react

Offical TinyMCE React component
MIT License
956 stars 156 forks source link

Form elements like checkbox,select with actions on it. #346

Closed X0rD3v1L closed 2 years ago

X0rD3v1L commented 2 years ago

I am not able to add a custom plugin with checkbox having it's own actions.


  function handleChange(){
    alert("Ok")
  }
  function checkBox() {
     return '<input type="checkbox" onclick='+ handleChange() + '> Hello </input>';
  }
  editor.ui.registry.addMenuItem("checkbox", {
    text: 'checkBox',
    onAction: function () {
      let html = checkBox();
      editor.insertContent(html);
    }
  })
})```

In this code the action is perfoming at first even before checking the checkbox.

How to handle such cases?
exalate-issue-sync[bot] commented 2 years ago

Ref: INT-2916

tiny-james commented 2 years ago

Well I went down a rabbit hole trying to get this to work, however I ran into a Firefox bug - so to summarize you're going to have to use fake checkboxes inside the editor and transform them on output.

The editor will remove any event handler attributes when it parses content. This is to prevent cross site scripting. You could override this however generally that is a bad idea. The normal way to handle clicks on things like this is to put a handler on a surrounding element and then find out what the target is. You can do this inside TinyMCE and in normal HTML so it will work for the output content too.

If you only cared about Chrome then this might work for you: https://codesandbox.io/s/happy-cookies-eckfcu?file=/src/index.js


    <Editor
      apiKey="qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc"
      init={{
        menu: {
          custom: {
            title: "Custom",
            items: "myCustomMenuItem"
          }
        },
        menubar: "custom",
        setup: (editor) => {
          editor.on("click", (evt) => {
            if (evt.target.matches('input[type="checkbox"]')) {
              alert("clicked!");
            }
          });
          editor.ui.registry.addMenuItem("myCustomMenuItem", {
            text: "My Custom Menu Item",
            onAction: function () {
              editor.insertContent('<input type="checkbox">Hello</input>');
            }
          });
        }
      }}
    />

The problem is that in Firefox the click event never propagates inside a contenteditable=true region so you can't catch it. You can stick the checkbox inside a span with <span contenteditable="false"> and you'll be able to detect the click but then you'll not be able to display the checkbox as checked on either Firefox or Chrome so that seems to be a net loss...

The only real solution is to fake the checkbox while it is inside the contenteditable region used by TinyMCE. You have two options for that

  1. Use CSS to render the checkbox. If you want inspiration for that have a look at what the checklist plugin does, specifically the CSS.
  2. Use Unicode characters to render the checkbox. Just create a span with a specific class on it and put the characters ☐ or ☑ inside the span. When you click on the span then you switch the character displayed.

Then you can use a nodeFilter to rewrite the HTML as you prefer it.

For example: https://codesandbox.io/s/awesome-germain-56x49o?file=/src/index.js

    <Editor
      apiKey="qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc"
      init={{
        menu: {
          custom: {
            title: "Custom",
            items: "myCustomMenuItem"
          }
        },
        menubar: "custom",
        setup: (editor) => {
          console.log("setup runs");
          editor.on("click", (evt) => {
            if (evt.target.matches("span.fakebox")) {
              const container = evt.target.closest("div.fakecheckbox");
              const checked = container.classList.toggle("checked");
              evt.target.innerHTML = checked ? "☑" : "☐";
            }
          });
          editor.ui.registry.addMenuItem("myCustomMenuItem", {
            text: "My Custom Menu Item",
            onAction: function () {
              editor.insertContent(
                '<div class="fakecheckbox"><span contenteditable="false" style="display: cursor;" class="fakebox">☐</span> Hello</div>'
              );
            }
          });
          editor.on("PreInit", () => {
            editor.serializer.addNodeFilter("div", (nodes, name, args) => {
              const chks = nodes.filter((node) =>
                /\bfakecheckbox\b/.test(node.attr("class"))
              );
              chks.forEach((chk) => {
                const checked = /\bchecked\b/.test(chk.attr("class"));
                const children = chk
                  .children()
                  .filter((child) => !/\bfakebox\b/.test(child.attr("class")));
                const input = new tinymce.html.Node("input", 1);
                input.attr("type", "checkbox");
                if (checked) input.attr("checked", "checked");
                const label = new tinymce.html.Node("label", 1);
                label.append(input);
                children.forEach((child) => label.append(child));
                chk.replace(label);
              });
            });
          });
        }
      }}
    />

This kind of question isn't well suited to issues. This actually had nothing to do with the react integration. In future please post these questions to stackoverflow or subscribe to our paid support and ask there.