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

Question: Dragging blocks into a Text block? #481

Open
mathieuk opened this Issue Nov 3, 2017 · 20 comments

Comments

Projects
None yet
7 participants
@mathieuk

mathieuk commented Nov 3, 2017

This is not a bug but an implementation question. If this is not the right place to ask these questions, please let me know.

We're working on an implementation where we want to use GrapesJS to allow users to create an e-mail template. As part of this implementation we are working to create mail-merge functionality: we've introduced the concept op 'merge-fields' or 'placeholders' which we will replace with the proper values on the server side. This means we send over the components JSON structure and turn it into HTML server-side, replacing values as we go.

So, as an example, one of our users might enter the text: Hello <<username>> and we'll replace that merge-field <<username>> with the proper field.

But, we haven't been able to implement this this way quite yet as we're not able to drag these blocks into a Text block. We can only drag it around it. So, for now we're extending the RTE with a merge-field 'inline block' ( <input type=text readonly class=mergeField data-isMergefield=1 />) and creating a merge-field block with the same HTML in the block manager. Implementing a DomComponent type to recognize it offers a method to configure it. But it feels suboptimal, we'd really like to be able to drag that mergefield block onto the right place in the textfield.

To allow this, I imagine GrapesJS would have to be able to grab the textNode and split it in to (atleast) two textnodes and a tag for the merge-field but I'm not sure where to start with this. Could you advise as to how we might implement this?

@mathieuk

This comment has been minimized.

Show comment
Hide comment
@mathieuk

mathieuk Nov 3, 2017

I suspect #287 is actually the same question.

mathieuk commented Nov 3, 2017

I suspect #287 is actually the same question.

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Nov 6, 2017

Owner

Have you checked API-Rich-Text-Editor? You can add a custom action like this

editor.RichTextEditor.add('custom-vars', {
  icon: `<select class="gjs-field">
		<option value="">- Select -</option>
        <option value="[[firstname]]">FirstName</option>
        <option value="[[lastname]]">LastName</option>
        <option value="[[age]]">Age</option>
      </select>`,
    // Bind the 'result' on 'change' listener
  event: 'change',
  result: (rte, action) => rte.insertHTML(action.btn.firstChild.value),
  // Reset the select on change
  update: (rte, action) => { action.btn.firstChild.value = "";}
})

rte-action

Owner

artf commented Nov 6, 2017

Have you checked API-Rich-Text-Editor? You can add a custom action like this

editor.RichTextEditor.add('custom-vars', {
  icon: `<select class="gjs-field">
		<option value="">- Select -</option>
        <option value="[[firstname]]">FirstName</option>
        <option value="[[lastname]]">LastName</option>
        <option value="[[age]]">Age</option>
      </select>`,
    // Bind the 'result' on 'change' listener
  event: 'change',
  result: (rte, action) => rte.insertHTML(action.btn.firstChild.value),
  // Reset the select on change
  update: (rte, action) => { action.btn.firstChild.value = "";}
})

rte-action

@mathieuk

This comment has been minimized.

Show comment
Hide comment
@mathieuk

mathieuk Nov 8, 2017

Yes, I've used that and I've pretty much got that working. The difference is that I am not using a text placeholder like you are. I'm actually inserting a block (with a corresponding 'type') so that I can further configure these placeholders (for instance, a field might be a Datetime field and my user may want to configure the exact output format for that datetime). I used HTML5 drag'n'drop to implement this and that works pretty nice, bút...

It feels like departing from the expected user interface. I feel I should be able to drag a mergefield from the 'blocks' and onto the right position. I've been toying around with the Sorter to allow this and I'm up to this:

image

I now have the problem that the Sorter very much wants actual blocks to align to so I have some more tweaking to do. For now implementing this has required changed in the ComponentTextView (mostly: dont clear out the toolbar for 'mergefields') and the Sorter (if I'm hovering a mergefield over a textblock, insert HTML into the activeRTE instead of appending a block).

I'm currently working on making the dragging work more reliably (for some reason, whenever I drag right the field gets appended to the textnode instead of at the cursor position, works fine when dragging left :) ), making the sorter ignore the idea of 'blocks' when within a textfield, showing the proper placeholder in that case (I'd want to see the actual cursor) and cleaning things up.

Not sure on how to approach the Sorter issue at this point, short of specialcasing textblocks so if you have any ideas in that area I'd love to hear them. You can see some of the hacky code I've made so far over at mathieuk@d58c5ee .

mathieuk commented Nov 8, 2017

Yes, I've used that and I've pretty much got that working. The difference is that I am not using a text placeholder like you are. I'm actually inserting a block (with a corresponding 'type') so that I can further configure these placeholders (for instance, a field might be a Datetime field and my user may want to configure the exact output format for that datetime). I used HTML5 drag'n'drop to implement this and that works pretty nice, bút...

It feels like departing from the expected user interface. I feel I should be able to drag a mergefield from the 'blocks' and onto the right position. I've been toying around with the Sorter to allow this and I'm up to this:

image

I now have the problem that the Sorter very much wants actual blocks to align to so I have some more tweaking to do. For now implementing this has required changed in the ComponentTextView (mostly: dont clear out the toolbar for 'mergefields') and the Sorter (if I'm hovering a mergefield over a textblock, insert HTML into the activeRTE instead of appending a block).

I'm currently working on making the dragging work more reliably (for some reason, whenever I drag right the field gets appended to the textnode instead of at the cursor position, works fine when dragging left :) ), making the sorter ignore the idea of 'blocks' when within a textfield, showing the proper placeholder in that case (I'd want to see the actual cursor) and cleaning things up.

Not sure on how to approach the Sorter issue at this point, short of specialcasing textblocks so if you have any ideas in that area I'd love to hear them. You can see some of the hacky code I've made so far over at mathieuk@d58c5ee .

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Nov 8, 2017

Owner

Well @mathieuk I've never taken into account the possibility to add stuff inside a text component, mainly because it might lead to strange behaviors (probably even from UX), but it is definitely an interesting proposal. About your Sorter question, I'd suggest creating a new property for blocks, eg.

blockManager.add('my-block', {
     label: 'Block',
     textable: 1, // allow the block to be inserted inside text components
     content: `...`,
})

and differentiate the sorter's behavior by this property

Owner

artf commented Nov 8, 2017

Well @mathieuk I've never taken into account the possibility to add stuff inside a text component, mainly because it might lead to strange behaviors (probably even from UX), but it is definitely an interesting proposal. About your Sorter question, I'd suggest creating a new property for blocks, eg.

blockManager.add('my-block', {
     label: 'Block',
     textable: 1, // allow the block to be inserted inside text components
     content: `...`,
})

and differentiate the sorter's behavior by this property

@mathieuk

This comment has been minimized.

Show comment
Hide comment
@mathieuk

mathieuk Nov 18, 2017

@artf so, I've taken your advice and went that route and i now have a fairly functional situation where it lets you drag a mergefield onto a textview, lets you move it around and lets you move it between textviews and other blocks.

image

You can see my implementation over at https://github.com/mathieuk/grapesjs/tree/mergefields/src .
Would you be interested in having this in core? Would you need any additional changes for that?

mathieuk commented Nov 18, 2017

@artf so, I've taken your advice and went that route and i now have a fairly functional situation where it lets you drag a mergefield onto a textview, lets you move it around and lets you move it between textviews and other blocks.

image

You can see my implementation over at https://github.com/mathieuk/grapesjs/tree/mergefields/src .
Would you be interested in having this in core? Would you need any additional changes for that?

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Nov 21, 2017

Owner

Wow @mathieuk, this is amazing 😍 it'd awesome to have it in core

Owner

artf commented Nov 21, 2017

Wow @mathieuk, this is amazing 😍 it'd awesome to have it in core

@mathieuk

This comment has been minimized.

Show comment
Hide comment
@mathieuk

mathieuk Nov 21, 2017

@artf Great! Any ideas on what form that should take? The merge fields approach that I'm using is kinda specific to our solution where we process the JSON generated by GrapesJS server-side. Any suggestions as to what type of sample block we should add to be draggable onto the textfield by default?

mathieuk commented Nov 21, 2017

@artf Great! Any ideas on what form that should take? The merge fields approach that I'm using is kinda specific to our solution where we process the JSON generated by GrapesJS server-side. Any suggestions as to what type of sample block we should add to be draggable onto the textfield by default?

@NorthstarTech

This comment has been minimized.

Show comment
Hide comment
@NorthstarTech

NorthstarTech Nov 22, 2017

I go through with your code @Matthieuk , but its not working properly. It doesnot allow to add inside the text. It always drag on a separate textfield. I also added the textable:1 . here is my code.
Need your help asap. Thanks.

bm.add('mergefield', {
label: 'Merge Field',
textable: 1,
attributes: {class:'fa fa-image'},
content: {
type: 'mergefield',
attributes: {
'data-mergefield': 1,
'data-highlightable': 1,
readonly: 'true',
value: 'mergefield'
},
style: {
height: '100px',
width: '200px',
display: 'inline-block',
border: '1px dashed #455699',
'text-align': 'center',
'background-color': '#000000',
'padding': '3px',
'border-radius': '5px',
'color': 'black'
}
}
});

domc.addType('mergefield', {
model: defaultModel.extend({
toolbar: [
{
attributes: {class: 'fa fa-arrows'},
command: 'tlb-move',
}
],
defaults: Object.assign({}, defaultModel.prototype.defaults, {
'custom-name': "Merge field",
tagName: 'input',
class: 'merge-field',
badgable: true,
highlightable: true,
editable: true,
droppable: false,
draggable: true,
removable: true,

    	         traits: [
    	         {
    				 label: "mergefield",
    	             type: 'select',
    	             name: 'value',
    	             options: [
    	               {value: 'invoice.invoicenumber', name: "invoice.invoicenumber"},
    	               {value: 'invoice.invoicedate', name: "invoice.invoicedate"},
    	               {value: 'company.name', name: "company.name"},
    	               {value: 'company.business_regnr', name: "company.business_regnr1"},
    	             ]
   	         }
    	         ]
    	       }),
    	     }, {
               toolbar: [
                   {
                       attributes: {class: 'fa fa-arrows'},
                       command: 'tlb-move',
                   }
               ],
    	       isComponent(el) {
    	         if(el.tagName == 'INPUT' && el.dataset.mergefield == "1") {
    	           return {type: 'mergefield'};
             }
    	       },
    	     }),
    	     view: defaultView.extend({
               
    				 events: {
                       'click': function(e) { 
                    	   console.log("click"); 
                    	   },
                      //  'dragstart': function(e) {
                      //     e.target.id = 'mergefield-' + (new Date()).getTime();
                      //     console.log('dragstart', e.target.id);
                      //     e.dataTransfer.setData('mergefield', e.target.id);
                      // },
                      'keydown': function(e) {
                          if (e.key == 'Backspace') {
                              e.target.parentNode.removeChild(e.target);
                          }
                          // console.log("KEYDOWN:", e);
                      }
    				 }
    	     }),
    	   }); 

NorthstarTech commented Nov 22, 2017

I go through with your code @Matthieuk , but its not working properly. It doesnot allow to add inside the text. It always drag on a separate textfield. I also added the textable:1 . here is my code.
Need your help asap. Thanks.

bm.add('mergefield', {
label: 'Merge Field',
textable: 1,
attributes: {class:'fa fa-image'},
content: {
type: 'mergefield',
attributes: {
'data-mergefield': 1,
'data-highlightable': 1,
readonly: 'true',
value: 'mergefield'
},
style: {
height: '100px',
width: '200px',
display: 'inline-block',
border: '1px dashed #455699',
'text-align': 'center',
'background-color': '#000000',
'padding': '3px',
'border-radius': '5px',
'color': 'black'
}
}
});

domc.addType('mergefield', {
model: defaultModel.extend({
toolbar: [
{
attributes: {class: 'fa fa-arrows'},
command: 'tlb-move',
}
],
defaults: Object.assign({}, defaultModel.prototype.defaults, {
'custom-name': "Merge field",
tagName: 'input',
class: 'merge-field',
badgable: true,
highlightable: true,
editable: true,
droppable: false,
draggable: true,
removable: true,

    	         traits: [
    	         {
    				 label: "mergefield",
    	             type: 'select',
    	             name: 'value',
    	             options: [
    	               {value: 'invoice.invoicenumber', name: "invoice.invoicenumber"},
    	               {value: 'invoice.invoicedate', name: "invoice.invoicedate"},
    	               {value: 'company.name', name: "company.name"},
    	               {value: 'company.business_regnr', name: "company.business_regnr1"},
    	             ]
   	         }
    	         ]
    	       }),
    	     }, {
               toolbar: [
                   {
                       attributes: {class: 'fa fa-arrows'},
                       command: 'tlb-move',
                   }
               ],
    	       isComponent(el) {
    	         if(el.tagName == 'INPUT' && el.dataset.mergefield == "1") {
    	           return {type: 'mergefield'};
             }
    	       },
    	     }),
    	     view: defaultView.extend({
               
    				 events: {
                       'click': function(e) { 
                    	   console.log("click"); 
                    	   },
                      //  'dragstart': function(e) {
                      //     e.target.id = 'mergefield-' + (new Date()).getTime();
                      //     console.log('dragstart', e.target.id);
                      //     e.dataTransfer.setData('mergefield', e.target.id);
                      // },
                      'keydown': function(e) {
                          if (e.key == 'Backspace') {
                              e.target.parentNode.removeChild(e.target);
                          }
                          // console.log("KEYDOWN:", e);
                      }
    				 }
    	     }),
    	   }); 
@kickbk

This comment has been minimized.

Show comment
Hide comment
@kickbk

kickbk Nov 25, 2017

Hmm, exactly what we need as well. Would love to see such functionality merged into the core.

kickbk commented Nov 25, 2017

Hmm, exactly what we need as well. Would love to see such functionality merged into the core.

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Nov 28, 2017

Owner

@mathieuk can I just use any block content? I mean something like this:

blockManager.add('my-block', {
     label: 'Block',
     textable: 1, 
     content: `<div style="...">custom stuff / custom components</div>`,
})

I don't care about thecontent, I know that with textable I'm able to drag it inside text components.
Would be awesome if you set up a demo just to test it online :) (with something like codepan/codesandbox)

Owner

artf commented Nov 28, 2017

@mathieuk can I just use any block content? I mean something like this:

blockManager.add('my-block', {
     label: 'Block',
     textable: 1, 
     content: `<div style="...">custom stuff / custom components</div>`,
})

I don't care about thecontent, I know that with textable I'm able to drag it inside text components.
Would be awesome if you set up a demo just to test it online :) (with something like codepan/codesandbox)

@gordon-matt

This comment has been minimized.

Show comment
Hide comment
@gordon-matt

gordon-matt Jan 19, 2018

@mathieuk / @artf I'm desperate for this functionality right now as well. Any idea when this will be merged to core?

gordon-matt commented Jan 19, 2018

@mathieuk / @artf I'm desperate for this functionality right now as well. Any idea when this will be merged to core?

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Jan 19, 2018

Owner

I hope to hear more from @mathieuk about this :)

Owner

artf commented Jan 19, 2018

I hope to hear more from @mathieuk about this :)

@kickbk

This comment has been minimized.

Show comment
Hide comment
@kickbk

kickbk Jan 25, 2018

For those looking for a ckeditor 4 merge fields plugin, this works: https://github.com/57u
To activate:

'gjs-plugin-ckeditor': {
	options: {
		...
		extraPlugins: '...,strinsert',
		toolbar: [
			...
			{name: 'Merge Fields', items: [ 'strinsert' ]},
		],
		strinsert_strings: [
			{'value': '*|FIRSTNAME|*', 'name': 'First name'},
			{'value': '*|LASTNAME|*', 'name': 'Last name'},
			{'value': '*|INVITEURL|*', 'name': 'Activate invite URL'},
		],
		// Optionally add the below settings
		strinsert_button_label: 'Merge Fields',
		strinsert_button_title: 'Insert Merge Field',
		...
	}
},

kickbk commented Jan 25, 2018

For those looking for a ckeditor 4 merge fields plugin, this works: https://github.com/57u
To activate:

'gjs-plugin-ckeditor': {
	options: {
		...
		extraPlugins: '...,strinsert',
		toolbar: [
			...
			{name: 'Merge Fields', items: [ 'strinsert' ]},
		],
		strinsert_strings: [
			{'value': '*|FIRSTNAME|*', 'name': 'First name'},
			{'value': '*|LASTNAME|*', 'name': 'Last name'},
			{'value': '*|INVITEURL|*', 'name': 'Activate invite URL'},
		],
		// Optionally add the below settings
		strinsert_button_label: 'Merge Fields',
		strinsert_button_title: 'Insert Merge Field',
		...
	}
},
@tvkit

This comment has been minimized.

Show comment
Hide comment
@tvkit

tvkit Feb 27, 2018

Any progress on this cool capability, @mathieuk ?

tvkit commented Feb 27, 2018

Any progress on this cool capability, @mathieuk ?

@LKozakewycz

This comment has been minimized.

Show comment
Hide comment
@LKozakewycz

LKozakewycz Jul 20, 2018

@mathieuk @artf

I'm looking at using the textable method and have built grapesjs with your implementation but I'm still struggling to get the component to drop into the middle of the RTE. Was there something specific you did to get this to work?

LKozakewycz commented Jul 20, 2018

@mathieuk @artf

I'm looking at using the textable method and have built grapesjs with your implementation but I'm still struggling to get the component to drop into the middle of the RTE. Was there something specific you did to get this to work?

@mathieuk

This comment has been minimized.

Show comment
Hide comment
@mathieuk

mathieuk Jul 24, 2018

I haven't had the time to work on this any further yet, sorry @artf. I might later this year. @LKozakewycz I'm a bit rusty on the details, but I recall that having to set ContentEditable was crucial to getting accurate ranges from the textselection APIs in the various browsers. That is: the textblock must be in content-editable mode during the whole time you're dragging the mergefield over it. That was one of the challenges of getting it right..

Does that help?

mathieuk commented Jul 24, 2018

I haven't had the time to work on this any further yet, sorry @artf. I might later this year. @LKozakewycz I'm a bit rusty on the details, but I recall that having to set ContentEditable was crucial to getting accurate ranges from the textselection APIs in the various browsers. That is: the textblock must be in content-editable mode during the whole time you're dragging the mergefield over it. That was one of the challenges of getting it right..

Does that help?

@LKozakewycz

This comment has been minimized.

Show comment
Hide comment
@LKozakewycz

LKozakewycz Jul 24, 2018

@mathieuk - I've been working on it lately and have so far come up with this, very similar to yours:

ezgif-1-307632a1f2

I know for a fact that setting contenteditable="false" on the merge field component itself will let the RTE know it is to be handled as a whole block, because unfortunatly at this moment the RTE will see it as just more editable text (i.e, you can edit the text inside the component). By doing this, you can use backspace to delete the merge field as a whole.

The trick is... trying to set contenteditable as a permanent attribute. I don't know if it's the RTE activation, but I'm working out how to get contenteditable to stick.

LKozakewycz commented Jul 24, 2018

@mathieuk - I've been working on it lately and have so far come up with this, very similar to yours:

ezgif-1-307632a1f2

I know for a fact that setting contenteditable="false" on the merge field component itself will let the RTE know it is to be handled as a whole block, because unfortunatly at this moment the RTE will see it as just more editable text (i.e, you can edit the text inside the component). By doing this, you can use backspace to delete the merge field as a whole.

The trick is... trying to set contenteditable as a permanent attribute. I don't know if it's the RTE activation, but I'm working out how to get contenteditable to stick.

@LKozakewycz

This comment has been minimized.

Show comment
Hide comment
@LKozakewycz

LKozakewycz Jul 24, 2018

Found it. Set contenteditable attribute on the component to false and ensure the attribute is not skipped over if explicitly set.

//... ParserHtml.js
          } else if (nodeName == 'contenteditable') {
            // Explicitly set contenteditable attributes should not be ignored
            if ( nodeValue === 'false' && !model.editable ) {
              model.attributes[nodeName] = 'false';
            }
            continue;
          }
//...

I'll be doing some more work on this and hopefully share in the future.

LKozakewycz commented Jul 24, 2018

Found it. Set contenteditable attribute on the component to false and ensure the attribute is not skipped over if explicitly set.

//... ParserHtml.js
          } else if (nodeName == 'contenteditable') {
            // Explicitly set contenteditable attributes should not be ignored
            if ( nodeValue === 'false' && !model.editable ) {
              model.attributes[nodeName] = 'false';
            }
            continue;
          }
//...

I'll be doing some more work on this and hopefully share in the future.

@artf

This comment has been minimized.

Show comment
Hide comment
@artf

artf Jul 25, 2018

Owner

Thanks for the help @LKozakewycz looking forward to your updates. BTW, before applying your changes I'd like to understand why contenteditable is removed when parsed, honestly, I'd expect to see it kept

Owner

artf commented Jul 25, 2018

Thanks for the help @LKozakewycz looking forward to your updates. BTW, before applying your changes I'd like to understand why contenteditable is removed when parsed, honestly, I'd expect to see it kept

@LKozakewycz

This comment has been minimized.

Show comment
Hide comment
@LKozakewycz

LKozakewycz Jul 26, 2018

@artf In this case, contenteditable is set to false on the merge field only because it let's the merge field show as a single block and not a bunch of characters. It stops the cursor from accessing the contents of the merge field block and allows for deleting it entirely using backspace.

In my version, I intend to use modals to display an advanced merge field selector which can be populated by meta data in my back-end. One of the challenges with this is trying to understand why the modal doesn't appear automatically when I drop it in the canvas (just like the image block).

LKozakewycz commented Jul 26, 2018

@artf In this case, contenteditable is set to false on the merge field only because it let's the merge field show as a single block and not a bunch of characters. It stops the cursor from accessing the contents of the merge field block and allows for deleting it entirely using backspace.

In my version, I intend to use modals to display an advanced merge field selector which can be populated by meta data in my back-end. One of the challenges with this is trying to understand why the modal doesn't appear automatically when I drop it in the canvas (just like the image block).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment