Span tag and content not rendering inside quill editor - quill

I have recently started exploring quill to implement a rich text editor. I want to render following html content in quill editor:
Second span
However when rendered, span tag is removed and its content is wrapped inside P tag and rendered. I learnt that as part of optimization quill removed span tags. But I really want span tag to render so I added a SPAN blot extending BlockEmbed. However after adding SPAN blot, nothing rendered in the editor. I don't understand what I am doing wrong; Here is the SPAN Blot:
let quill = require("quill");
let BlockEmbed = quill.import("blots/block/embed");
class SpanBlot extends BlockEmbed {
public static create(value: any): any {
let node = super.create(value);
node.setAttribute("data-embed-id", value);
return node;
}
public static value(node: any): any {
return node;
}
public static formats(node: any): any {
let format: any = {};
if (node.hasAttribute("data-embed-id")) {
format.height = node.getAttribute("data-embed-id");
}
return format;
}
public formats(): any {
let formats = super.formats();
formats["span"] = SpanBlot.formats(this.domNode);
return formats;
}
}
SpanBlot.blotName = "Span";
SpanBlot.tagName = "SPAN";
SpanBlot.class = "social";
export { BlockEmbed, SpanBlot };

Related

Implement custom editor for Quill blot

I'm trying to customize the Quill editor for my needs. I managed to implement and insert custom blots, as described in https://quilljs.com/guides/cloning-medium-with-parchment/ But I need to edit data, which is attached to my blots, like the URL of a link for example. The default implementation of Quill displays a small "inline" edit box for links. I want to implement something like that myself, but just don't get it. I did not find any hints in the docs and guides. Reading the source code of Quill, I was not able to figure out where the editing dialog for links is implemented. Any starting point would be very appreciated.
I've tried something similar. Proper way of doing it should be creating a module. Unfortunately as you already know it is not as easy as it seems.
Let me point you to some useful resources that helped me a lot with understanding how to create extensions for quill.
Quills maintainer is curating Awesome quill list.
I recommend looking especially into
quill-emoji it contains code to display tooltip emoji while writing
quill-form maybe some form extension has some code that will point you in the right direction
Here is my try on to it using custom quill module.
const InlineBlot = Quill.import('blots/inline');
class NamedLinkBlot extends InlineBlot {
static create(value) {
const node = super.create(value);
node.setAttribute('href', value);
node.setAttribute('target', '_blank');
return node;
}
}
NamedLinkBlot.blotName = 'namedlink';
NamedLinkBlot.tagName = 'A';
Quill.register('formats/namedlink', NamedLinkBlot);
const Tooltip = Quill.import('ui/tooltip');
class NamedLinkTooltip extends Tooltip {
show() {
super.show();
this.root.classList.add('ql-editing');
}
}
NamedLinkTooltip.TEMPLATE = [
'<a class="ql-preview" target="_blank" href="about:blank"></a>',
'<input type="text" data-link="https://quilljs.com">',
'Url displayed',
'<input type="text" data-name="Link name">',
'<a class="ql-action"></a>',
'<a class="ql-remove"></a>',
].join('');
const QuillModule = Quill.import('core/module');
class NamedLinkModule extends QuillModule {
constructor(quill, options) {
super(quill, options);
this.tooltip = new NamedLinkTooltip(this.quill, options.bounds);
this.quill.getModule('toolbar').addHandler('namedlink', this.namedLinkHandler.bind(this));
}
namedLinkHandler(value) {
if (value) {
var range = this.quill.getSelection();
if (range == null || range.length === 0) return;
var preview = this.quill.getText(range);
this.tooltip.show();
}
}
}
Quill.register('modules/namedlink', NamedLinkModule);
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
namedlink: {},
toolbar: {
container: [
'bold',
'link',
'namedlink'
]
}
}
});
CodePen Demo
To see the tooltip:
Select any word
Click invisible button on the right of link button, your cursor will turn to hand.
Main issues that need to be addressed:
in order to customize the tooltip you will need to copy majority of the code from SnowTooltip Main pain point is that you cannot easily extend That tooltip.
you need to also adapt the code of event listeners but it should be straightforward
Here's a partial answer. Please see lines 41-64 in file "https://github.com/quilljs/quill/blob/08107187eb039eababf925c8216ee2b7d5166d41/themes/snow.js" (Please note, that the authors have since moved to TypeScript, lines 45-70?, and possibly made other changes.)
I haven't tried implementing something similar but it looks like Quill is watching a "selection-change" event and checks if the selection is on a LinkBlot with a defined link.
The SnowTooltip class includes references to the selectors, 'a.ql-preview', 'ql-editing', 'a.ql-action', and 'a.ql-remove', which we find in the link-editing tooltip.
this.quill.on(
Emitter.events.SELECTION_CHANGE,
(range, oldRange, source) => {
if (range == null) return;
if (range.length === 0 && source === Emitter.sources.USER) {
const [link, offset] = this.quill.scroll.descendant(
LinkBlot,
range.index,
);
if (link != null) {
this.linkRange = new Range(range.index - offset, link.length());
const preview = LinkBlot.formats(link.domNode);
this.preview.textContent = preview;
this.preview.setAttribute('href', preview);
this.show();
this.position(this.quill.getBounds(this.linkRange));
return;
}
} else {
delete this.linkRange;
}
this.hide();
},
);

How to make successful calls to execCommand('copy') from React mouse event handler?

Summary
I wrote a small method to copy text to the clipboard, which works (in Chrome, Edge and IE11) if I trigger the code from a simple HTML button event handler like this:
class MyController
{
constructor()
{
...
testButton_.onclick = (event: MouseEvent) => { this.onTestButtonClicked(event); }
...
}
onTestButtonClicked(event: MouseEvent)
{
Utilities.copyTextToClipboard("Testing the clipboard functionality!");
}
...
}
where testButton_ is an HTMLButtonElement.
Now, when I call this same method from the event handler of a React component, the method fails in Edge and Chrome, but succeeds in IE11.
It appears that the document.execCommand("copy") call returns false.
Can someone please explain me how I can make a successful call to document.execCommand("copy") from within a React mouse event handler?
Note: I did read various other posts on here and on https://w3c.github.io/editing/execCommand.html#dfn-the-copy-command but since this is all called from within a user (right-)click event, I would expect this to work (as it does with the plain HTML event handler) also in React.
Details
(I'm using React and TypeScript by the way)
This is what's in the React component:
class DashboardItemComponent extends React.Component<DashboardItemComponentProps, DashboardItemComponentState>
{
static defaultProps: DashboardItemComponentProps = {
projectName: ""
};
constructor(props: DashboardItemComponentProps)
{
super(props);
this.onContextMenu = this.onContextMenu.bind(this);
}
render(): JSX.Element
{
return (
<div onContextMenu={this.onContextMenu}>
<div className="inner">
<header>
<h2>{this.props.projectName}</h2>
</header>
<div>more text</div>
</div>
</div>
);
}
protected onContextMenu(e: React.MouseEvent)
{
e.preventDefault();
Utilities.copyTextToClipboard("testing 1 2 3");
}
}
And this is what's in the copyTextToClipboard method of my Utilities class (parts gathered from other posts on SO and this code does work fine):
static copyTextToClipboard(text: string): boolean
{
// Find the dummy text area or create it if it doesn't exist
const dummyTextAreaID = "utilities-copyTextToClipboard-hidden-TextArea-ID";
let dummyTextArea: HTMLTextAreaElement = document.getElementById(dummyTextAreaID) as HTMLTextAreaElement;
if (!dummyTextArea)
{
console.log("Creating dummy textarea for clipboard copy.");
let textArea = document.createElement("textarea");
textArea.id = dummyTextAreaID;
// Place in top-left corner of screen regardless of scroll position.
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
// Ensure it has a small width and height. Setting to 1px / 1em
// doesn't work as this gives a negative w/h on some browsers.
textArea.style.width = "1px";
textArea.style.height = "1px";
// We don't need padding, reducing the size if it does flash render.
textArea.style.padding = "0";
// Clean up any borders.
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
// Avoid flash of white box if rendered for any reason.
textArea.style.background = "transparent";
document.querySelector("body").appendChild(textArea);
dummyTextArea = document.getElementById(dummyTextAreaID) as HTMLTextAreaElement;
console.log("The dummy textarea for clipboard copy now exists.");
}
else
{
console.log("The dummy textarea for clipboard copy already existed.")
}
// Set the text in the text area to what we want to copy and select it
dummyTextArea.value = text;
dummyTextArea.select();
// Now execute the copy command
try
{
let status = document.execCommand("copy");
if (!status)
{
console.error("Copying text to clipboard failed.");
return false;
}
else
{
console.log("Text copied to clipboard.");
return true;
}
}
catch (error)
{
console.log("Unable to copy text to clipboard in this browser.");
return false;
}
}

Custom attributes are removed when using custom blots

I created a custom blot for links that requires to be able to set rel and target manually. However when loading content that has those attributes, quill strips them. I'm not sure why.
I created a codepen to illustrate the issue.
This is my custom blot:
const Inline = Quill.import('blots/inline')
class CustomLink extends Inline {
static create(options) {
const node = super.create()
node.setAttribute('href', options.url)
if (options.target) { node.setAttribute('target', '_blank') }
if (options.follow === 'nofollow') { node.setAttribute('rel', 'nofollow') }
return node
}
static formats(node) {
return node.getAttribute('href')
}
}
CustomLink.blotName = 'custom_link'
CustomLink.tagName = 'A'
Quill.register({'formats/custom_link': CustomLink})
Do I have to tell Quill to allow certain atttributes?
Upon initialization from existing HTML, Quill will try to construct the data model from it, which is the symmetry between create(), value() for leaf blots, and formats() for inline blots. Given how create() is implemented, you would need formats() to be something like this:
static formats(node) {
let ret = {
url: node.getAttribute('href'),
};
if (node.getAttribute('target') == '_blank') {
ret.target = true;
}
if (node.getAttribute('rel') == 'nofollow') {
ret.follow = 'nofollow';
}
return ret;
}
Working fork with this change: https://codepen.io/quill/pen/xPxGgw
I would recommend overwriting the default link as well though instead of creating another one, unless there's some reason you need both types.

Insert raw html code to quill

Is there option to insert raw html code to quill?
quill.insertText();
quill.clipboard.dangerouslyPasteHTML()
both are parsed by matcher but I need to paste exactly formatted html code for an email footer.
If the footer content is meant to be static and un-editable, you can do this by extending the BlockEmbed blot then adding a button for your new format in the toolbar. There are 2 different ways to handle what HTML get's entered into the new format.
1. Let the user enter the HTML to embed:
// Import the BlockEmbed blot.
var BlockEmbed = Quill.import('blots/block/embed');
// Create a new format based off the BlockEmbed.
class Footer extends BlockEmbed {
// Handle the creation of the new Footer format.
// The value will be the HTML that is embedded.
// By default, the toolbar will show a prompt window to get the value.
static create(value) {
// Create the node using the BlockEmbed's create method.
var node = super.create(value);
// Set the srcdoc attribute to equal the value which will be your html.
node.setAttribute('srcdoc', value);
// Add a few other iframe fixes.
node.setAttribute('frameborder', '0');
node.setAttribute('allowfullscreen', true);
node.setAttribute('width', '100%');
return node;
}
// return the srcdoc attribute to represent the Footer's value in quill.
static value(node) {
return node.getAttribute('srcdoc');
}
}
// Give our new Footer format a name to use in the toolbar.
Footer.blotName = 'footer';
// Give it a class name to edit the css.
Footer.className = 'ql-footer';
// Give it a tagName of iframe to tell quill what kind of element to create.
Footer.tagName = 'iframe';
// Lastly, register the new Footer format so we can use it in our editor.
Quill.register(Footer, true);
var quill = new Quill('#editor-container', {
modules: {
toolbar: {
container: ['footer'] // Toolbar with just our footer tool (of course you can add all you want).
}
},
theme: 'snow'
});
.ql-toolbar .ql-footer:before {
content: 'footer';
}
.ql-editor .ql-footer {
background: #f7f7f7;
}
<link href="//cdn.quilljs.com/1.3.4/quill.core.css" rel="stylesheet"/>
<link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet"/>
<div id="editor-container">
<h1>Test Content</h1>
<p>Enter a footer</p>
</div>
<script src="//cdn.quilljs.com/1.3.4/quill.min.js"></script>
2. Use specific HTML
// Import the BlockEmbed blot.
var BlockEmbed = Quill.import('blots/block/embed');
// Create a new format based off the BlockEmbed.
class Footer extends BlockEmbed {
// Handle the creation of the new Footer format.
// The value will be the HTML that is embedded.
// This time the value is passed from our custom handler.
static create(value) {
// Create the node using the BlockEmbed's create method.
var node = super.create(value);
// Set the srcdoc attribute to equal the value which will be your html.
node.setAttribute('srcdoc', value);
// Add a few other iframe fixes.
node.setAttribute('frameborder', '0');
node.setAttribute('allowfullscreen', true);
node.setAttribute('width', '100%');
return node;
}
// return the srcdoc attribute to represent the Footer's value in quill.
static value(node) {
return node.getAttribute('srcdoc');
}
}
// Give our new Footer format a name to use in the toolbar.
Footer.blotName = 'footer';
// Give it a class name to edit the css.
Footer.className = 'ql-footer';
// Give it a tagName of iframe to tell quill what kind of element to create.
Footer.tagName = 'iframe';
// Register the new Footer format so we can use it in our editor.
Quill.register(Footer, true);
// Specify the HTML that will be embedded.
var footerHTML = '<h1>Footer</h1>'
+ '<p>This is our new footer</p>';
// Create the footer handler.
var footerHandler = function() {
// Get the cursor location to know where footer will be added.
var index = this.quill.getSelection(true).index;
// Insert the footer with the footerHTML.
this.quill.insertEmbed(index, 'footer', footerHTML);
};
// Import the Toolbar module so we can add a custom handler to our footer button.
var Toolbar = Quill.import('modules/toolbar');
// Add our custom footer handler to the footer button.
Toolbar.DEFAULTS.handlers['footer'] = footerHandler;
var quill = new Quill('#editor-container', {
modules: {
toolbar: {
container: ['footer'] // Toolbar with just our footer tool (of course you can add all you want).
}
},
theme: 'snow'
});
.ql-toolbar .ql-footer:before {
content: 'footer';
}
.ql-editor .ql-footer {
background: #f7f7f7;
}
<link href="//cdn.quilljs.com/1.3.4/quill.core.css" rel="stylesheet"/>
<link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet"/>
<div id="editor-container">
<h1>Test Content</h1>
<p>Enter a footer</p>
</div>
<script src="//cdn.quilljs.com/1.3.4/quill.min.js"></script>

How to add a non editable tag to content in Quill editor

I want to add a couple of pre-built labels like
<div class="label"> Label Text <span>x</span><div>
to the html content in the quill editor. Add such a tag should not be a problem in itself. However I don't want the user to be able to edit the text inside the label. The cursor should not even be allowed to be placed inside the label. On delete the whole div should be deleted. The whole label should act like an image in some sense.
Is it possible ?
You should be able to achieve this by writing your own Blot:
class Label extends Parchment.Embed {
static create(value) {
const node = super.create(value);
node.innerText = value;
// Remember to set this so users can't edit
// the label's content
node.contentEditable = 'false';
this._addRemovalButton(node);
return node;
}
static value(node) {
// Only return the text of the first child
// node (ie the text node), otherwise the
// value includes the contents of the button
return node.childNodes[0].textContent;
}
static _addRemovalButton(node) {
const button = document.createElement('button');
button.innerText = 'x';
button.onclick = () => node.remove();
button.contentEditable = 'false';
node.appendChild(button);
// Extra span forces the cursor to the end of
// the label, otherwise it appears inside the
// removal button
const span = document.createElement('span');
span.innerText = ' ';
node.appendChild(span);
}
}
Label.blotName = 'label';
Label.tagName = 'SPAN';
Label.className = 'ql-label';
You register it with Quill:
Quill.register(Label);
And finally, you can use it in a similar way to an image or other embeds:
quill.updateContents(
new Delta().insert({ label: 'foo' })
);
NB: Any styling you need can be applied with the .ql-label class:
.ql-label {
background-color: lightgrey;
padding: 0.3em;
margin: 0 0.2em;
}
.ql-label button {
margin-left: 0.3em;
}
Finally finally: a working example.

Resources