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

Still not possible to create a new paragraph on Enter #680

Closed
JosephHalter opened this issue Sep 10, 2019 · 25 comments
Closed

Still not possible to create a new paragraph on Enter #680

JosephHalter opened this issue Sep 10, 2019 · 25 comments
Labels

Comments

@JosephHalter
Copy link

There's been a few related issues:

#75
#117
#123
#202

And a few pull requests:

#187
#240

It's now possible to configure Trix to use a <p> tag instead of <div> with:

Trix.config.blockAttributes.default.tagName = 'p'

However, it's still not possible to configure Trix so that when I press Enter it creates a new paragraph instead of inserting a <br> tag. I'm aware of the issue with the <figure> tag which can't be put inside a paragraph and stay a valid HTML, however I'm not using images (and if I was I would just do it without using the <figure> tag). I don't say that creating a new paragraph on Enter should be the default but there should at least be an option. For example, the breakOnReturn option like proposed in pull request 187.

iTunes only supports the following tags: <p>, <ol>, <ul>, <li> and <a>. No other tag is supported, and this includes the <br> tag as far as I know. It would be really great if someone could have a fresh new look at that issue. Especially as it has been asked by numerous people already, and code was provided multiple times but no solution ever gets merged.

@wassim
Copy link

wassim commented Oct 9, 2019

We need this.

@lazaronixon
Copy link

#202 (comment)

@natevw
Copy link

natevw commented Feb 2, 2020

Yep, this is frustrating. Just got far enough along integrating Trix into an app only to find out that it turns a clean semantic document of <p> elements into a dog's dinner of <div> and <br> mess as soon as we hand it the HTML.

My understanding is that the Trix developers don't see any way forward since they can't use paragraph elements lest someone inserts an attachment inside of one: #202 (comment). I haven't seen any explanation for why they can't simply break a p element into two and put the figure between them.

There are several other WYSIWYG editor options listed in this discussion from another project that hit the issue before migrating to Quill.js: vapid/vapid#53 (comment)

@JoshInLisbon
Copy link

JoshInLisbon commented Feb 20, 2020

Hi all, I came across this and it annoyed me. I didn't want to switch editor so I build a hacky JS based solution but thought I'd share it as it does work (and handles images - probably other attachments too, but I'm only using images). Right now I'm separating my new lines with <div class="tlk-bubble">...content...</div>, but you can change it to <p></p> or whatever you want. I also can have multiple Trix text inputs on a page, so this code deals with that.

The code works by updating what I call the msgTrixStore (which is a hidden input field in the form that stores the value which is submitted to the backend).

<input type="hidden" name="msg[content]" id="msg_content_trix_input_msg" value="This value will be stored in the db."> 

Trix naturally updates this input when a change is made to the main form, and I highjack that change on keyup (and input on mobile) and set the value of the msgTrixStore with what I want (div separated blocks of text). I handle images by generating a keyup event when there is a Trix image upload and then start listening for changes in case the image is removed and a user never activates another keyup event (with my imageRemovalChecker function, if an image is removed, another keyup event is triggered which runs the 'reformatter').

Handling everything (text, pre [code blocks], blockquotes, h1, ol, ul, copy and paste) except images:

app/javascript/components/trix_change_br_to_div.js
(runs on pages where Trix editors that need to output <div class="tlk-bubble">...</div> are present)
(Requires jQuery to be present to handle binding of keyup and input events for mobile browsers.)

const changeBrToDiv = () => {
  // Finds all my 'areas' with Trix editors in my page
  let msgTrixInputDivs = document.querySelectorAll(".spkr-msg-form");
  msgTrixInputDivs.forEach(div => {
    let msgTrixInput = div.querySelector("trix-editor");
    let msgTrixStore = div.querySelector('[name="msg[content]"]');
    // set your own value here (e.g. if you want to use <p></p> etc...)
    let tlkBubbleStart = '<div class="tlk-bubble">';
    let tlkBubbleEnd = '</div>';

    let msgTrixPreview = div.querySelector(".msg-preview");

    // using a manually inserted `target` selector to convert the msgTrixInput into something I can access with jQuery
    let elemTarget = msgTrixInput.getAttribute('target');
    $(`[target="${elemTarget}"]`).bind('input keyup', function() {
      // setTimeout handles auto complete on mobile. Trix sets the value of msgTrixStore after 'input' is triggered
      // so while 'input' is triggered on mobile autocomplete, it is overriden by Trix editors 'natural processes'
      // the setTimeout delay means our function wins the battle.
      setTimeout(function() {
        let skipDivs = false;
        let textBlock = [];
        // handle copy and paste (where multiple divs are pasted into the editor)
        let msgTrixInputDivs = msgTrixInput.querySelectorAll("div, pre, h1, blockquote, ul, ol");

        // handles first word on mobile where no divs are in the Trix editor (reason unknown)
        if(!msgTrixInputDivs[0]) {
          textBlock.push(tlkBubbleStart, msgTrixInput.innerHTML, tlkBubbleEnd);
          skipDivs = true;
          let tlkBubbleText = textBlock.flat(Infinity).join("").replace(/<div class="tlk-bubble"><\/div>/g, "");
          // joins the textBlock array and sets it to the value of the msgTrixStore
          msgTrixStore.value = tlkBubbleText;
          // msgTrixStore.value = textBlock.flat(Infinity).join("");
        }

        if(!skipDivs) {
          msgTrixInputDivs.forEach(mtiDiv => {
            // handles code
            let pre = mtiDiv.outerHTML.match(/<pre>[\s\S]*?<\/pre>/)
            // handles blockquote
            let blockquote = mtiDiv.outerHTML.match(/<blockquote>[\s\S]*?<\/blockquote>/);
            // handles ul
            let ul = mtiDiv.outerHTML.match(/<ul>[\s\S]*?<\/ul>/)
            // handles ol
            let ol = mtiDiv.outerHTML.match(/<ol>[\s\S]*?<\/ol>/)
            // handles h1
            let h1 = mtiDiv.outerHTML.match(/<h1>[\s\S]*?<\/h1>/)
            // handles n > 3 line situations in the Trix input
            let brMatchesBr = mtiDiv.outerHTML.match(/(?<=<br>)([\s\S]*?)(?=<br>)/g);
            // handles the last line if there are n > 1 lines
            let brMatchDiv = mtiDiv.outerHTML.match(/<br>(?![\s\S]*<br>[\s\S]*$)([\s\S]*?)(?:<\/div>)/);
            // handles the first line if there are n > 1 lines
            let blockMatchBr = mtiDiv.outerHTML.match(/(?:<!--block-->)([\s\S]*?)(?=<br>)/);
            // handles the first line if there is one line
            let blockMatchDiv = mtiDiv.outerHTML.match(/(?:<!--block-->)([\s\S]*?)(?=<\/div>)/);

            // iterates through the different possibilities (i.e. if pre, if ul, n > 3, if n > 1, then if n = 1) and updates the textBlock array accordingly
            if(pre) {
              textBlock.push(tlkBubbleStart, pre[0], tlkBubbleEnd);
            } else if(blockquote) {
              textBlock.push(tlkBubbleStart, blockquote[0], tlkBubbleEnd);
            } else if(ul) {
              textBlock.push(tlkBubbleStart, ul[0], tlkBubbleEnd);
            } else if(ol) {
              textBlock.push(tlkBubbleStart, ol[0], tlkBubbleEnd);
            } else if(h1) {
              textBlock.push(tlkBubbleStart, h1[0], tlkBubbleEnd);
            } else if(brMatchesBr) {
              textBlock.push(tlkBubbleStart, blockMatchBr[1], tlkBubbleEnd);
              brMatchesBr.forEach(match => {
                if(match) {
                  textBlock.push(tlkBubbleStart, match, tlkBubbleEnd);
                }
              });
              if(brMatchDiv) {
                textBlock.push(tlkBubbleStart, brMatchDiv[1], tlkBubbleEnd);
              }
            } else if(blockMatchBr) {
              textBlock.push(tlkBubbleStart, blockMatchBr[1], tlkBubbleEnd);
              if(brMatchDiv) {
                textBlock.push(tlkBubbleStart, brMatchDiv[1], tlkBubbleEnd);
              }
            } else if (blockMatchDiv) {
              textBlock.push(tlkBubbleStart, blockMatchDiv[1], tlkBubbleEnd);
            }
            // see your result in the console
            // console.log(textBlock.flat(Infinity).join(""));
            let tlkBubbleText = textBlock.flat(Infinity).join("").replace(/<div class="tlk-bubble"><\/div>/g, "");
            // joins the textBlock array and sets it to the value of the msgTrixStore
            msgTrixStore.value = tlkBubbleText;
          });
        }
      }, 250);
    });
  });
}

export { changeBrToDiv }

Handling images:

Trix has an inbuilt 'trix-file-accept' event which you can listen to to do js validations on file types / sizes, which I was already using. So I added to that by making sure I got an alert if the upload conditions were satisfied (saveAlert). This also activates an imageRemovalChecker function which runs every second after an image has been uploaded to check if it has been subsequently removed without a further keystroke being made by the user.

app/javascript/components/trix-editor-overrides.js
(runs on pages where Trix editors that need to output <div class="tlk-bubble">...</div> are present)

const trixEditorOverrides = () => {
  window.addEventListener("trix-file-accept", function(event) {
    const maxFileSize = 2048 * 2048 // 4 MB
    let saveAlert = true
    if (event.file.size > maxFileSize) {
      event.preventDefault();
      alert("Only support attachment files upto size 4MB!");
      saveAlert = false
    }
    const acceptedTypes = ['image/jpeg', 'image/png']
    if (!acceptedTypes.includes(event.file.type)) {
      event.preventDefault()
      alert("Only support attachment of jpeg or png files")
      saveAlert = false
    }
    // saveAlert stops the subsequent function from running if the upload has failed
    if (saveAlert) {
      console.log("uploaded :)");
      triggerTrixKeyupEvent();
    }
  });
}

export { trixEditorOverrides }

const msgTrixInputDivs = document.querySelectorAll(".spkr-msg-form");
const keyupEvent = new Event('keyup');

const triggerTrixKeyupEvent = () => {
  msgTrixInputDivs.forEach(div => {
    let msgTrixInput = div.querySelector("trix-editor");
    let msgTrixStore = div.querySelector('[name="msg[content]"]');
    setTimeout(function(){
      msgTrixInput.dispatchEvent(keyupEvent);
      let startTrixValue = msgTrixStore.value;
      // interval required to allow time for msgTrixStore to update from the trix_change_br_to_div file
      setInterval(function(x){ imageRemovalChecker(msgTrixStore, msgTrixInput, startTrixValue); startTrixValue = msgTrixStore.value; }, 1000)
    }, 200);
  });
}

const imageRemovalChecker = (msgTrixStore, msgTrixInput, startTrixValue) => {
  if (msgTrixStore.value !== startTrixValue) {
    msgTrixInput.dispatchEvent(keyupEvent);
  }
}

The only other edge case that I had to resolve for was if someone uploads an image, begins to alter the image 'attachment_caption' and submits before pressing enter/starting a new line. I removed this edge case by simply setting:

.attachment__caption {
  display: none !important;
}

So, it is a big hack - but it works (as far as I can tell). I'm a junior junior developer, so this might be very bad practice to solve problems like this - if so please to tell me. And if not, then please contact me to hire me :). ✌️

@JosephHalter
Copy link
Author

Very hacky indeed. We already had multiple proposed pull requests for it anyway, it's just that they never get merged.

@mrigdon-zz
Copy link

did anyone figure out the best solution for this yet?

@dzunk
Copy link

dzunk commented Apr 29, 2020

+1. I was trying to migrate from TinyMCE to Trix, but this is a dealbreaker. Trix isn't able to handle existing HTML values with <p> tags and mangles them with <br> tags when editing.

We need to maintain the same paragraph and line-break behavior as the other editor to avoid messing up or reformatting existing markup.

@rreynier
Copy link

Same boat, was pretty excited about this editor until I found out it does not generate html with paragraph tags.

@moll
Copy link

moll commented May 12, 2020

It's not actually too difficult to convert the document structure to HTML manually, converting textual paragraphs to HTML <p>s and the rest for whichever semantic tags you prefer. I'd say it's actually a far more secure approach to do so on the server side rather than try to sanitize what Trix outputs once its already submitted to the server.

That what I did for a public initiative's site in Estonia — Rahvaalgatus. Serialization example is in https://github.com/rahvaalgatus/rahvaalgatus/blob/master/lib/trix.js and its tests at https://github.com/rahvaalgatus/rahvaalgatus/blob/master/test/lib/trix_test.jsx. The Trix editor itself is utilized on https://github.com/rahvaalgatus/rahvaalgatus/blob/master/views/initiatives/update_page.jsx, which, at the time of submit, grabs the structured document via el.editor.getDocument() and puts its JSON serialization back in the form element.

@JosephHalter
Copy link
Author

@moll we can do hacks to bypass the shortcomings of this library, but the default behaviour remains contrary to everyone's expectation. And it's not for a lack of proposed pull requests but a general unwillingness to merge them into the main repo.

@lazaronixon
Copy link

https://github.com/lazaronixon/trix/tree/contentify
It will generate a output like:

<h1>title<h1>
<div class="p">paragraph1</div>
<div class="p">paragraph1</div>

You can configure paragraph class with:

Trix.config.css.paragraph = "trix-p"

package.json

"trix": "lazaronixon/trix#contentify"

@stale
Copy link

stale bot commented Aug 13, 2020

This issue has been automatically marked as stale after 90 days of inactivity. It will be closed if no further activity occurs.

@stale stale bot added the stale label Aug 13, 2020
@JosephHalter
Copy link
Author

JosephHalter commented Aug 13, 2020

Still no hope? The <div class="p"> solution doesn’t really help when you need to edit HTML content which doesn’t come from trix, or if you don’t have control on the CSS from where the HTML will be displayed.

@stale stale bot removed the stale label Aug 13, 2020
@stale
Copy link

stale bot commented Nov 21, 2020

This issue has been automatically marked as stale after 90 days of inactivity. It will be closed if no further activity occurs.

@stale stale bot added the stale label Nov 21, 2020
@stale stale bot closed this as completed Nov 29, 2020
@MaksimBurnin
Copy link

MaksimBurnin commented Nov 30, 2020

https://github.com/lazaronixon/trix/tree/contentify

Monkey-patch version of this solution, for these who want latest version of Trix and need this feature

Trix.config.blockAttributes.default.tagName = "p"
Trix.config.blockAttributes.default.breakOnReturn = true;

Trix.Block.prototype.breaksOnReturn = function(){
  const attr = this.getLastAttribute();
  const config = Trix.getBlockConfig(attr ? attr : "default");
  return config ? config.breakOnReturn : false;
};
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function(){
  if(this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
    return this.startLocation.offset > 0
  } else {
    return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false;
  }
};

@JosephHalter
Copy link
Author

JosephHalter commented Nov 30, 2020

Small mistake, replace the following line:

return startLocation.offset > 0

with:

return this.startLocation.offset > 0

But apart from this, I can confirm that @MaksimBurnin solution works for new content. You can try it yourself with the usual caveat about images which you've to do manually because the default behavior uses the <figure> tag which is a block level element and therefore not allowed inside a paragraph.

If you want to use Trix to edit existing content however, it doesn't seem to be enough - here I give Trix the starting value <p>Hello</p><p>Test</p> but it's immediately converted to <div><br>Hello<br><br></div><div><br>Test<br><br></div>

@MaksimBurnin
Copy link

MaksimBurnin commented Nov 30, 2020

Good catch. Fixed in my snippet. Thank you @JosephHalter
In our use-case we have attachments disabled anyways, we are trying to focus on semantic markup and typogrphics, while limiting editor's options, so they can't mess up anything. Turns out Trix is really hard to extend / modify without ugly stuff.

If you want to use Trix to edit existing content however, it doesn't seem to be enough - here I give Trix the starting value <p>Hello</p><p>Test</p> but it's immediately converted to <div><br>Hello<br><br></div><div><br>Test<br><br></div>

This one is weird, because i can see it in your gist, but can not replicate in my project. <p> tags are persisted and parsed by trix flawlessly. Same version, same structure, same monkey-patches, nothing extra

Edit: this looks like a race condition. See modified example: https://jsfiddle.net/xcpr8qv5/
You need to modify config and apply patches before editor element appears in DOM

@JosephHalter
Copy link
Author

Edit: this looks like a race condition. See modified example: https://jsfiddle.net/xcpr8qv5/
You need to modify config and apply patches before editor element appears in DOM

Thank you, it indeed works better this way. There're plenty of monkey patches to force the square peg that is Trix into the round hole that I need for many client projects, each one more uglier that the next. I quite like your version, it's quite nice and limited in scope compared to many alternatives that I've seen and used before.

@MaksimBurnin
Copy link

My version is basically a direct translation from https://github.com/lazaronixon/trix/tree/contentify so credit goes there :)

@DanielJackson-Oslo
Copy link

DanielJackson-Oslo commented Oct 1, 2021

https://github.com/lazaronixon/trix/tree/contentify

Monkey-patch version of this solution, for these who want latest version of Trix and need this feature

@MaksimBurnin Any idea where to do this monkey patching when using trix in a Rails project? I can't seem to get it to work.

EDIT: Found a solution!

For those looking to implement in Rails 6, I ended up doing it like this in a custom trix.js that I then imported into application,js instead of the default module. If you try to do the default import too, then you will probably have the race condition problem described above.

See the PR here: lnu-norge/lokaler.lnu.no#26

@Tampa
Copy link

Tampa commented Apr 30, 2022

So for those who don't stuff trix into "your next best framework" what's the solution here? I tried appending some config options to trix in a script tag, but that messed up form submission to where the visible tags were not the ones submitted.

For countless reasons using div everywhere is troublesome when div behavior is not consistent with many theme frameworks such as bootstrap. It would be nice to not have div placed at all and just br on line breaks, but nothing in the documentation mentions on how to accomplish this, could this be explained somewhere easily accessible without having to dig through various issue tickets and PRs?

@migu0
Copy link

migu0 commented Aug 30, 2022

@MaksimBurnin Thanks a lot for your solution. It works really well but sometimes when saving, it adds empty

's between the paragraphs in my app (Rails 7). Have you experienced similar issues and is there a fix to this? Thank you

@fiszmon
Copy link

fiszmon commented Jun 14, 2023

@MaksimBurnin Thanks!
It works with Angular13 and Trix 2.0.5 version. (Migration from other editor which base on

tags). One method is missing but it can be fixed with line:
Trix.getBlockConfig = (attr) => Trix.config.blockAttributes[attr];

@jmas
Copy link

jmas commented Aug 3, 2023

Thanks, @MaksimBurnin , your solution works with small change:

Trix.config.blockAttributes.default.tagName = "p";
Trix.config.blockAttributes.default.breakOnReturn = true;

Trix.Block.prototype.breaksOnReturn = function() {
    const attr = this.getLastAttribute();
    // Function `Trix.getBlockConfig()` was not found so I replaced it with analog:
    const config = Trix.config.blockAttributes[attr ? attr : "default"];
    return config ? config.breakOnReturn : false;
};

Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function() {
    if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
        return this.startLocation.offset > 0;
    } else {
        return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false;
    }
};

@jakswa
Copy link

jakswa commented Oct 14, 2023

Selfishly this frustrates me greatly when I try to do things like "take the first few elements out as a preview." You'll get a big <div> or a list of <br> elements since these traversal libraries ignore text nodes, and trix didn't wrap paragraphs in <p>. I'm over here recursively traversing all children to get some text nodes. Feels insane but maybe this is the intended way.

In case my personal anecdote doesn't resonate, I'll tack on that accessibility is supposedly troublesome if you don't use <p>:

Creating separate paragraphs of text using <br> is not only bad practice, it is problematic for people who navigate with the aid of screen reading technology. Screen readers may announce the presence of the element, but not any content contained within <br>s. This can be a confusing and frustrating experience for the person using the screen reader.

Use <p> elements, and use CSS properties like margin to control their spacing.

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