So here is what I am trying to do.
I would like to embed a form in the editor. The use case in my app is that users are able to build a page by adding text and images, and in addition, they are also able to create a form (in a sort of modal, which is separate) and then add that to the page as well.
The route I have gone so far is to create a FormBlot by subclassing BlockEmbed. This almost works, but there are two issues I am facing:
label or input tag) and pressing keys changes the form, when it should not.So, has anyone figured out a way to accomplish both of these things? I have tried rooting around in the Quill and Parchment codebases and have not had much luck so far.
Just a followup: It is technically possible to do this, but it gets _really_ complicated, and this is mainly due to contenteditable. I'll document some of the stuff I did though in case anyone finds this useful:
index that always returns 0 (this will come in handy later).select and unselect. These methods will be called when the blot is selected (either by clicking on it or using the cursor) or unselected (only by using the cursor).select, I dispatched a custom event on this.domNode (the element that represents the blot) called select-block-embed. I made it bubble upward, and set a blot property on the Event object to this (the FormBlot instance). In this method I also set an active class so that I could draw a box around the form.unselect, I took away the active class.mousedown so that select is called.core/selection.js so that update is no longer fired on keyup, mouseup, or touchend. Instead of this, I added another handler to call update when selectionchange occurred on the editor. Since you can't technically listen for selectionchange on anything other than the entire document, inside of the handler I had to check that document.activeElement (the node where the selection was created or changed) was inside of, or was itself, this.root (which is the editor). After this, I then modified update so that at the top, after it calls this.getRange() to get the current selection state, but before setting this.lastRange and then comparing it to the previous selection state, it fires a new event called selection-before-change.selection-before-change. In the handler, I took the range that was passed in and then used Quill#getLine to get the current blot.select on the FormBlot and then called quill.selection.setRange(null, "silent") to hide the cursor (otherwise it appears inside of the form). (The "silent" is to prevent update from getting called again, which is going to cause issues since we're still in the middle of update.)range.index in a variable that is defined outside of the selection-before-change handler.keydown, because at this point we need to tell the editor that if the user presses up, down, left, or right the blot needs to get unselected and the cursor needs to reappear either above or below the form. I accomplished this by either calling quill.setSelection(index - 1, 0, "silent") or quill.setSelection(index + 1, 0, "silent") depending on the direction. (Here, index is the range.index that we cached before in the variable I mentioned before.)unselect on the FormBlot that I cached before, cleared out this cache, and removed the keydown handler from the editor.select-block-embed to occur on the editor element. (As mentioned before, FormBlot will emit this event when the blot is selected, and the blot can be selected two ways: when the selection is changed via the cursor and when the element is clicked on.) In the handler I made sure that we were caching the selected blot in the variable I mentioned before and that the selection was cleared (using quill.selection.setRange(null, "silent")). So this logic got me pretty far: I could click on the form to select it, and I could use the arrow keys to cursor around, and depending on where it was the form might get selected and unselected automatically.
The problem arose when I wanted to implement dragging and dropping for the form. It just does not seem that contenteditable was not built for embedding HTML and treating that HTML as an indivisible, immutable unit. I started going down the route of implementing my own drag and drop logic, and I quickly realized that it was completely buggy. And I think it's all because we are working within a contenteditable.
I think if you wanted to do this sort of thing right, you'd have to get rid of contenteditable completely and implement it from scratch using plain JavaScript just like Google Docs does (cursor, selections, dragging and dropping, everything). It's a real shame and I wish there were better support for this in the browser.
Having tried something like this a few times, ultimately it's easier to create a meta-layout that contains a series of different node types (rich text/form/gallery) than to try to hook into the editor directly. You can still make it appear as one document canvas visually.
@alexkrolick I'm making something similar. How do I make that 'meta-layout' thing? Using an iframe?
Can I get a demo on this?
No iframe, just something like this:
toolbar
----------------
some rich text
editor
----------------
picture
----------------
some rich text
editor
...
And manually manage the navigation/focus of the different types of nodes.
@mcmire I麓m trying to embed a form in the quill editor. Seems like you麓ve done similar maybe you can help me
After reading over the documentation I created an formBlot by also subclassing BlockEmbed.
// formBlock.js file:
import * as Quill from 'quill';
const formEmbed = Quill.import('blots/block/embed');
export class CustomBlock extends BlockEmbed {
/* stuck here creating the form fields, just looking for few radio buttons that would set an pre-define template on the editor based on the choose, */
}
CustomBlock.blotName = 'custom';
CustomBlock.tagName = 'form';
Would like to know how you set the fields for your form, in case you also have done it.
Most helpful comment
Just a followup: It is technically possible to do this, but it gets _really_ complicated, and this is mainly due to contenteditable. I'll document some of the stuff I did though in case anyone finds this useful:
indexthat always returns 0 (this will come in handy later).selectandunselect. These methods will be called when the blot is selected (either by clicking on it or using the cursor) or unselected (only by using the cursor).select, I dispatched a custom event onthis.domNode(the element that represents the blot) calledselect-block-embed. I made it bubble upward, and set ablotproperty on the Event object tothis(the FormBlot instance). In this method I also set anactiveclass so that I could draw a box around the form.unselect, I took away theactiveclass.mousedownso thatselectis called.core/selection.jsso thatupdateis no longer fired onkeyup,mouseup, ortouchend. Instead of this, I added another handler to callupdatewhenselectionchangeoccurred on the editor. Since you can't technically listen forselectionchangeon anything other than the entire document, inside of the handler I had to check thatdocument.activeElement(the node where the selection was created or changed) was inside of, or was itself,this.root(which is the editor). After this, I then modifiedupdateso that at the top, after it callsthis.getRange()to get the current selection state, but before settingthis.lastRangeand then comparing it to the previous selection state, it fires a new event calledselection-before-change.selection-before-change. In the handler, I took therangethat was passed in and then usedQuill#getLineto get the current blot.selecton the FormBlot and then calledquill.selection.setRange(null, "silent")to hide the cursor (otherwise it appears inside of the form). (The"silent"is to preventupdatefrom getting called again, which is going to cause issues since we're still in the middle ofupdate.)range.indexin a variable that is defined outside of theselection-before-changehandler.keydown, because at this point we need to tell the editor that if the user presses up, down, left, or right the blot needs to get unselected and the cursor needs to reappear either above or below the form. I accomplished this by either callingquill.setSelection(index - 1, 0, "silent")orquill.setSelection(index + 1, 0, "silent")depending on the direction. (Here,indexis therange.indexthat we cached before in the variable I mentioned before.)unselecton the FormBlot that I cached before, cleared out this cache, and removed thekeydownhandler from the editor.select-block-embedto occur on the editor element. (As mentioned before, FormBlot will emit this event when the blot is selected, and the blot can be selected two ways: when the selection is changed via the cursor and when the element is clicked on.) In the handler I made sure that we were caching the selected blot in the variable I mentioned before and that the selection was cleared (usingquill.selection.setRange(null, "silent")).So this logic got me pretty far: I could click on the form to select it, and I could use the arrow keys to cursor around, and depending on where it was the form might get selected and unselected automatically.
The problem arose when I wanted to implement dragging and dropping for the form. It just does not seem that contenteditable was not built for embedding HTML and treating that HTML as an indivisible, immutable unit. I started going down the route of implementing my own drag and drop logic, and I quickly realized that it was completely buggy. And I think it's all because we are working within a contenteditable.
I think if you wanted to do this sort of thing right, you'd have to get rid of contenteditable completely and implement it from scratch using plain JavaScript just like Google Docs does (cursor, selections, dragging and dropping, everything). It's a real shame and I wish there were better support for this in the browser.