GrapesJS / grapesjs

Free and Open source Web Builder Framework. Next generation tool for building templates without coding
https://grapesjs.com
BSD 3-Clause "New" or "Revised" License
22.36k stars 4.05k forks source link

Text component problem #761

Closed gabrigcl closed 6 years ago

gabrigcl commented 6 years ago

Hi. I wish a text component that do not create new components every time I press "Enter" key to create new paragraphs (this is a bad usability). In addition, the actual text component from the core of grapes has a problem (described in the screencast below). Thanks in advance for your attention! gif-screen-grapes

ryandeba commented 6 years ago

I looked into this myself a few days ago...apparently there's an insertBrOnReturn option for execCommand, but the browser support is terrible. A solution like this is probably better.

@artf How would you want this to change? Should hitting enter insert <br> tags as shown in the top answer on the that SO discussion? Would you want to expose this as a configuration option?

gabrigcl commented 6 years ago

I've found a solution to my needs using CKEditor and overriding the native "text" component creating my own. My text component now is a div with a css class that identifies the component:

comps.addType('text', {
      model: textModel.extend({
        defaults: Object.assign({}, textModel.prototype.defaults, {
          tagName: 'div',
          name: 'Texto',
          draggable: '*',
          droppable: false
        }),
      },
      {
        isComponent: function(el) {
          if(el.tagName == 'DIV' && el.classList.contains('txt')){
            return {type: 'text'};
          }
        },
      })
/*...*/

Now, typing ENTER does not create a new component. Nevertheless, this solution do not solve this problem, for those who don't want to use third party text editors.

artf commented 6 years ago

@gabrigcl I agree and think that Ryan's suggestion could be a solution (apart the fact insertHTML is not supported by IE)

gabrigcl commented 6 years ago

Yeah, I agree. Will you put this into roadmap?

artf commented 6 years ago

@gabrigcl I leave this issue as an opened bug so no need to put it into roadmap

richieteh94 commented 6 years ago

when use SHIFT + "Enter" to break line in text it will create <br/> tag instead of creating a new component maybe is rich text editor behavior (using default rte not using CKEditor).

artf commented 6 years ago

when use SHIFT + "Enter" to break line in text it will create
tag instead of creating a new component maybe is rich text editor behavior (using default rte not using CKEditor).

WebKit's behavior

drasill commented 6 years ago

Hi,

There should be a solution by listening on component:add and then convert these <div> to <br/>.

I need help on how to convert these in the event callback, please could you have a look ?

editor.on('component:add', (child) => {
   // Check if new component is a 'text'
   if (child.attributes.tagName === 'div' && child.attributes.type === 'text') {
      // Check if parent of this new component is also a "text"
      const parent = child.parent()
      if (parent.attributes.tagName === 'div' && parent.attributes.type === 'text') {
         // Now here, replace '<div>content</div>' to '<br/>content'
         // FIXME this doesn't work
         const t = child.getEl().innerHTML
         child.destroy()
         parent.getEl().innerHTML += '<br/>' + t
      }
   }
})
drasill commented 6 years ago

I still struggle a lot with this issue.

Here is an idea, which is still not perfect, as it requires to rename the text component, and it removes empty lines :

var originalText = comps.getType('text')
comps.addType('text', {
   model: originalText.model.extend({
      defaults: Object.assign({}, originalText.model.prototype.defaults, {
         tagName: 'div',
         name: 'MyText',
         draggable: '*',
         droppable: false,
         attributes: { 'data-mytext': 'MyText' },
      }),
   }, {
      isComponent: function (el) {
         if (el.tagName === 'DIV' && el.getAttribute('data-mytext') === 'MyText') {
            const contentTexts = []
            Array.from(el.childNodes).forEach((node) => {
               if (node.textContent !== '') {
                  contentTexts.push(node.textContent)
               }
            })
            const content = contentTexts.join('<br/>')
            return { type: 'text', name: 'MyText', content: content, components: [] }
         }
      },
   }),
   view: originalText.view,
})
drasill commented 6 years ago

Ok, this is simpler and seems to work, but I don't even know how / why.

var originalText = comps.getType('text')
comps.addType('text', {
   model: originalText.model.extend({
      defaults: Object.assign({}, originalText.model.prototype.defaults, {
         tagName: 'div',
         name: 'MyText',
         draggable: '*',
         droppable: false,
         attributes: { 'data-mytext': 'MyText' },
      }),
   }, {
      isComponent: function (el) {
         if (el.tagName === 'DIV' && el.getAttribute('data-mytext') === 'MyText') {
            return { type: 'text', name: 'MyText', content: el.innerHTML, components: [] }
         }
      },
   }),
   view: originalText.view,
})
gabrigcl commented 6 years ago

Hi drasil. Congratulations on your finding a solution, I'll test It. I've found a solution too, the same way, extending the text component

gabrigcl commented 6 years ago

I'll share it soon

v8jupiter commented 6 years ago

Any update for this?

artf commented 6 years ago

The new release https://github.com/artf/grapesjs/releases/tag/v0.14.33 improved a bit the TextComponent. It still creates new paragraphs but the editor will hide them from the selection. @gabrigcl from your original problem, one big issue that I guess you're doing there is, when you change the page, relying on HTML/CSS instead of the JSON and this is why are not editable after the change

gabrigcl commented 6 years ago

one big issue that I guess you're doing there is, when you change the page, relying on HTML/CSS instead of the JSON and this is why are not editable after the change

Yes, I was doing like this. However I'm currently using a custom text component, made by me, that doesn't have this issue.

artf commented 6 years ago

Great, for now, I close this

frasza commented 5 years ago

one big issue that I guess you're doing there is, when you change the page, relying on HTML/CSS instead of the JSON and this is why are not editable after the change

Yes, I was doing like this. However I'm currently using a custom text component, made by me, that doesn't have this issue.

Any news on this? We are still having issues when reloading.

artf commented 5 years ago

@frasza your issue is not related https://github.com/artf/grapesjs/issues/1635#issuecomment-445242563

frasza commented 5 years ago

@artf Not exactly, but it could be. I would much rather prefer my Text block to insert <br> instead of creating new <div>.

Moikapy commented 5 years ago

Not sure if I found this code in an Issue on here or on Stack Overflow, but this code here replaces the Divs with a
when you press the enter key. I placed this code in the same file as my grapes-config

grapesExample

var iframeBody = editor.Canvas.getBody();
    $(iframeBody).on("keydown", "[contenteditable]", e => {
        // trap the return key being pressed
        if (e.keyCode === 13) {
                e.preventDefault();
                // insert 2 br tags (if only one br tag is inserted the cursor won't go to the next line)
                e.target.ownerDocument.execCommand("insertHTML", false, "<br><br>");
                // prevent the default behaviour of return key pressed
                return false;
        }
    });
inaLar commented 5 years ago

The new release https://github.com/artf/grapesjs/releases/tag/v0.14.33 improved a bit the TextComponent. It still creates new paragraphs but the editor will hide them from the selection. @gabrigcl from your original problem, one big issue that I guess you're doing there is, when you change the page, relying on HTML/CSS instead of the JSON and this is why are not editable after the change

I'd appreciate more information on how to use the JSON in cases when the data (HTML/CSS) will be stored in DB and used again for editing.

inaLar commented 5 years ago

Also I have a custom component when dropped inside a Text component (I am using a custom text component that extends the original text component) after saving and reloading the HTML/CSS I am experiencing the same problem - the text component converts to a Box. I use a custom attribute, but no matter the attribute the type of the component converts from Text to Default.

Before saving:

<div data-gjs-type="text" data-highlightable="1" data-text-extended="Text">Insert <span data-gjs-type="custom-type" contenteditable="false" data-gjs-textable="true" >Custom comp test</span>your text here</div>

On loading already saved content:

<div data-gjs-type="default" data-highlightable="1" data-text-extended="Text">Insert <span data-gjs-type="custom-type" contenteditable="false" data-gjs-textable="true" >Custom comp test</span>your text here</div>

artf commented 5 years ago

I'd appreciate more information on how to use the JSON in cases when the data (HTML/CSS) will be stored in DB and used again for editing.

The point @inaLar is: you shouldn't use the HTML/CSS if your purpose is to store and edit the template, you can use it only for the initial import...

The JSON is like a "file type" for the editor, so it would be the same like asking to have a Photoshop file with all the groups and layers by starting from a JPEG image... you can't, the image doesn't have that information, the same is for the HTML and CSS created by GrapesJS

inaLar commented 5 years ago

@artf , I understand well what a JSON is, but I haven't seen any particular example or use case of the JSON storage for GrapesJS. It may be that I have missed some part of the documentation. Forgive me, but I am new to this and also I started with 0 experience in Backbone. If there is such an example I will appreciate any links. Speaking about examples - it will be very useful to have in the documentation information like what are the dependencies, etc. It may be very clear to you, as a creator what is the idea and the related technologies, but for a newbe with GrapesJS it takes not small effort to understand the basics without knowing these. I will risk to sound stupid, but it took me like a week to understand that it is based on Backbone :)

As for the HTML and CSS, for some reason I sore them and still managed to recognise which are the components on reload.

artf commented 5 years ago

I understand well what a JSON is, but I haven't seen any particular example or use case of the JSON storage for GrapesJS

All you need is here: https://grapesjs.com/docs/modules/Storage.html

but it took me like a week to understand that it is based on Backbone

Well, that means that more or less I'm doing a good job 😂 but it would be better not to see it at all. Backbone is just an internal tiny layer for managing some basic structure stuff (maybe even too much tiny) but being a dependency it shouldn't be exposed, so doing this have in the documentation information like what are the dependencies would be totally wrong, from a framework point of view. Indeed, my goal is to cover the Backbone's API as much as possible, so if one day it'll be removed (which is might happen soon) the API will not break.

abulka commented 3 years ago

@drasill Your second last solution worked OK re converting a html div e.g. <div data-mytext="MyText">HI THERE <div>sub-div</div> </div> with nested divs into a single div with </br> for carriage returns instead, however your later solution solution which simply returns content: el.innerHTML doesn't work for me since it just returns the html unchanged. If you had returned el.innerText then that at least that would have removed the nested divs and replaced them with spaces. Going back to your second last solution, it seems it can be simplified to:

const compType = 'text3'

const text3Plugin = editor => {

  editor.DomComponents.addType(compType, {
    extend: "text",
    isComponent: function (el) {
      if (el.tagName === 'DIV' && el.getAttribute('data-mytext') === 'MyText') {
        const contentTexts = []
        Array.from(el.childNodes).forEach((node) => {
          if (node.textContent !== '') {
            contentTexts.push(node.textContent)
          }
        })
        const content = contentTexts.join('<br/>')
        return { type: compType, name: 'MyText', content: content, components: [] }
      }
    },
    model: {
      defaults: {
        tagName: 'div',
      },
    },
  })

  editor.BlockManager.add(compType, {
    label: compType,
    category: 'Extra',
    content: '<div data-mytext="MyText">HI THERE <div>sub-div</div> </div>',
  });

}

Even so, when you actually edit the component and hit ENTER divs will be inserted again. The solution by @Moikapy is the best so far, here is a non jquery version:

// Prevent DIVs whilst editing.
var iframeBody = editor.Canvas.getBody();
iframeBody.addEventListener("keydown", (e) => { 
  if (e.keyCode === 13 && e.target.getAttribute('contenteditable') && e.target.getAttribute('data-gjs-type') == compType) {
    e.preventDefault();
    // insert 2 br tags (if only one br tag is inserted the cursor won't go to the next line)
    e.target.ownerDocument.execCommand("insertHTML", false, "<br><br>");
    // prevent the default behaviour of return key pressed
    return false;
  }
});

however often it inserts too many BR's and puts the cursor in the wrong place - perhaps I re-implemented it incorrectly? https://jsfiddle.net/tcab/Lcor654e/