Convert Quill Delta to HTML - quill

How do I convert Deltas to pure HTML? I'm using Quill as a rich text editor, but I'm not sure how I would display the existing Deltas in a HTML context. Creating multiple Quill instances wouldn't be reasonable, but I couldn't come up with anything better yet.
I did my research, and I didn't find any way to do this.

Not very elegant, but this is how I had to do it.
function quillGetHTML(inputDelta) {
var tempCont = document.createElement("div");
(new Quill(tempCont)).setContents(inputDelta);
return tempCont.getElementsByClassName("ql-editor")[0].innerHTML;
}
Obviously this needs quill.js.

I guess you want the HTML inside it. Its fairly simple.
quill.root.innerHTML

If I've understood you correctly, there's a quill thread of discussion here, with the key information you're after.
I've quoted what should be of most value to you below:
Quill has always used Deltas as a more consistent and easier to use (no parsing)
data structure. There's no reason for Quill to reimplement DOM APIs in
addition to this. quill.root.innerHTML or document.querySelector(".ql-editor").innerHTML works just fine (quill.container.firstChild.innerHTML is a bit more brittle as it depends on child ordering) and the previous getHTML implementation did little more than this.

Simple, solution is here:
https://www.scalablepath.com/blog/using-quill-js-build-wysiwyg-editor-website/
The main code is:
console.log(quill.root.innerHTML);

This is a very common confusion when it comes to Quilljs. The thing is you should NOT retrieve your html just to display it. You should render and display your Quill container just the same way you do when it is an editor. This is one of the major advantages to Quilljs and the ONLY thing you need to do is:
$conf.readOnly = true;
This will remove the toolbar and make the content not editable.

I have accomplished it in the backend using php.
My input is json encoded delta and my output is the html string.
here is the code , if it is of any help to you.This function is still to handle lists though and some other formats but you can always extend those in operate function.
function formatAnswer($answer){
$formattedAnswer = '';
$answer = json_decode($answer,true);
foreach($answer['ops'] as $key=>$element){
if(empty($element['insert']['image'])){
$result = $element['insert'];
if(!empty($element['attributes'])){
foreach($element['attributes'] as $key=>$attribute){
$result = operate($result,$key,$attribute);
}
}
}else{
$image = $element['insert']['image'];
// if you are getting the image as url
if(strpos($image,'http://') !== false || strpos($image,'https://') !== false){
$result = "<img src='".$image."' />";
}else{
//if the image is uploaded
//saving the image somewhere and replacing it with its url
$imageUrl = getImageUrl($image);
$result = "<img src='".$imageUrl."' />";
}
}
$formattedAnswer = $formattedAnswer.$result;
}
return nl2br($formattedAnswer);
}
function operate($text,$ops,$attribute){
$operatedText = null;
switch($ops){
case 'bold':
$operatedText = '<strong>'.$text.'</strong>';
break;
case 'italic':
$operatedText = '<i>'.$text.'</i>';
break;
case 'strike':
$operatedText = '<s>'.$text.'</s>';
break;
case 'underline':
$operatedText = '<u>'.$text.'</u>';
break;
case 'link':
$operatedText = ''.$text.'';
break;
default:
$operatedText = $text;
}
return $operatedText;
}

Here's a full function using quill.root.innerHTML, as the others didn't quite cover the complete usage of it:
function quillGetHTML(inputDelta) {
var tempQuill=new Quill(document.createElement("div"));
tempQuill.setContents(inputDelta);
return tempQuill.root.innerHTML;
}
This is just a slight different variation of km6 's answer.

For Quill version 1.3.6, just use:
quill.root.innerHTML;
Try it online: https://jsfiddle.net/Imabot/86dtuhap/
Detailed explaination on my blog
This link if you have to post the Quill HTML content in a form

quill.root.innerHTML on the quill object works perfectly.
$scope.setTerm = function (form) {
var contents = JSON.stringify(quill.root.innerHTML)
$("#note").val(contents)
$scope.main.submitFrm(form)
}

I put together a node package to convert html or plain text to and from a Quill Delta.
My team used it to update our data model to include both Quill's Delta and HTML. This allows us to render on the client without an instance of Quill.
See node-quill-converter.
It features the following functions:
- convertTextToDelta
- convertHtmlToDelta
- convertDeltaToHtml
Behind the scenes it uses an instance of JSDOM. This may make it best suited for migration scripts as performance has not been tested in a typical app request lifecycle.

Try
console.log ( $('.ql-editor').html() );

Here is how I did it, for you Express folks. It seems to have worked very well in conjunction with express-sanitizer.
app.js
import expressSanitizer from 'express-sanitizer'
app.use(expressSanitizer())
app.post('/route', async (req, res) => {
const title = req.body.article.title
const content = req.sanitize(req.body.article.content)
// Do stuff with content
})
new.ejs
<head>
<link href="https://cdn.quilljs.com/1.3.2/quill.snow.css" rel="stylesheet">
</head>
...
<form action="/route" method="POST">
<input type="text" name="article[title]" placeholder="Enter Title">
<div id="editor"></div>
<input type="submit" onclick="return quillContents()" />
</form>
...
<script src="https://cdn.quilljs.com/1.3.2/quill.js"></script>
<script>
const quill = new Quill('#editor', {
theme: 'snow'
})
const quillContents = () => {
const form = document.forms[0]
const editor = document.createElement('input')
editor.type = 'hidden'
editor.name = 'article[content]'
editor.value = document.querySelector('.ql-editor').innerHTML
form.appendChild(editor)
return form.submit()
}
</script>
express-sanitizer (https://www.npmjs.com/package/express-sanitizer)
document.forms (https://developer.mozilla.org/en-US/docs/Web/API/Document/forms)
My view only has one form, so I used document.forms[0], but if you have multiple or may extend your view in the future to have multiple forms, check out the MDN reference.
What we are doing here is creating a hidden form input that we assign the contents of the Quill Div, and then we bootleg the form submit and pass it through our function to finish it off.
Now, to test it, make a post with <script>alert()</script> in it, and you won't have to worry about injection exploits.
That's all there is to it.

Here is a proper way to do it.
var QuillDeltaToHtmlConverter = require('quill-delta-to-html').QuillDeltaToHtmlConverter;
// TypeScript / ES6:
// import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';
var deltaOps = [
{insert: "Hello\n"},
{insert: "This is colorful", attributes: {color: '#f00'}}
];
var cfg = {};
var converter = new QuillDeltaToHtmlConverter(deltaOps, cfg);
var html = converter.convert();
Refer https://github.com/nozer/quill-delta-to-html

For a jQuery-style solution that allows getting and setting the Quill value I am doing the following:
Quill.prototype.val = function(newVal) {
if (newVal) {
this.container.querySelector('.ql-editor').innerHTML = newVal;
} else {
return this.container.querySelector('.ql-editor').innerHTML;
}
};
let editor = new Quill( ... );
//set the value
editor.val('<h3>My new editor value</h3>');
//get the value
let theValue = editor.val();

quill-render looks like it's what you want. From the docs:
var render = require('quill-render');
render([
{
"attributes": {
"bold": true
},
"insert": "Hi mom"
}
]);
// => '<b>Hi mom</b>'

If you want to render quill using nodejs, there is a package quite simple based on jsdom, usefull to render backside (only one file & last update 18 days from now) render quill delta to html string on server

Just use this clean library to convert from delta from/to text/html
node-quill-converter
example:
const { convertDeltaToHtml } = require('node-quill-converter');
let html = convertDeltaToHtml(delta);
console.log(html) ; // '<p>hello, <strong>world</strong></p>'

Related

How to query and update the DOM with yew?

Is there any way to make DOM action via use_node_ref? or alternatively, how to do document.query_selector() in Rust using yew?
use web_sys::HtmlInputElement;
use yew::{
function_component, functional::*, html,
NodeRef, Html
};
#[function_component(UseRef)]
pub fn ref_hook() -> Html {
let input_ref = use_node_ref();
let all_editables = input_ref.query_selector("[contenteditable]")
web_sys::console::(all_editables)
html! {
<div>
<input ref={input_ref} type="number" />
</div>
}
}
Goal: I have a rich text editor app. And I have a minified html in form of string like this <h1> this is title</h1><p>hello world</> and I need to get the DOM and change the innerHTML to set it to this value.
Goal2: I will also need to update the innerHtml as the user write things in the contenteditable elements. Fore example when the user type #john smith I will make a create an <a> element with href the have the link to John smith's profile.
Many things to tackle with your question.
#1 Do not set inner html
More to read about this in Alternative for innerHTML?
But instead create text nodes.
So using web-sys you would do something like:
let txtNode: Node = window()
.unwrap_throw()
.document()
.unwrap_throw()
.create_text_node("Hello")
.dyn_into()
.unwrap_throw();
myDomElement.append_hild(&txtNode).unwrap_throw();
#2 How to query data from an input
There are many ways to do this so ill just show you one of them -
controlled input
core idea is keep your input value in use_state and sync it with the input element using value and oninput attributes.
#[function_component(ControlledInputComponent)]
pub fn controlled_input_component() -> Html {
let my_text_handle = use_state(|| "".to_string());
let my_text = (*my_text_handle).clone();
let handle_input = Callback::from(move |input_event: InputEvent| {
let event: Event = input_event.dyn_into().unwrap_throw();
let input_elem: HTMLInputElement = event.target().unwrap_throw().dyn_into().unwrap_throw();
let value = input_elem.value();
my_text_handle.set(value); // update as user types
});
html! {
<div>
<input type="text" value={my_text} oninput={handle_input} />
</div>
}
}
#3 Update DOM
**External to yew as you should generally avoid updating DOM that is controlled by yew
You can then use use_effec_with_deps to react to your input changing and update your external preview there
let my_text_handle = use_state(|| "".to_string());
let my_text = (*my_text_handle).clone();
use_effect_with_deps(move |my_text| {
// run all the code from my tip #1 like:
// myDomElement.append_hild(&txtNode).unwrap_throw();
||{}
}, my_text);

ReactJs: How to replace html and string template with a component?

I want to manage the content of the page from a content editor where I am getting page content from the API.
Check this screenshot.
I used two different react modules for this react-html-parser and react-string-replace but it is still not working.
Here is my code.
let pageData = '';
pageData = ReactHtmlParser(page.content);
// replacing contact us form with a contact us form component
pageData = reactStringReplace(pageData, '{CONTACT_US_FORM}', (match, i) => (
<ContactUsForm />
));
return <div>{pageData}</div>;
react-html-parser -> It is used to parse HTML tags which are in string format into tree of elements.
react-string-replace -> It is used to replace a string into react a component.
Note: If I use react-html-parser or react-string-replace individually then it works fine but it does not work together.
Any suggestion?
Depends on the expected structure of page.content. If it contains HTML you are right in using react-html-parser, which has a replace option.
import parse from 'html-react-parser';
const macro = '{CONTACT_US_FORM}';
const replaceContactUsForm = (domhandlerNode) => {
if (domhandlerNode.type === 'text' && domhandlerNode.data.includes(macro))
return <ContactUsForm />;
};
// ...
const elements = parse(page.content, { replace: replaceContactUsForm });
return <div>{elements}</div>;
Additionally, If the string {CONTACT_US_FORM} is embedded in text you could use react-string-replace to keep the rest of the text intact:
const replaceContactUsForm = (domhandlerNode) => {
if (domhandlerNode.type === 'text' && domhandlerNode.data.includes(macro))
return <>{reactStringReplace(domhandlerNode.data, macro, () => (<ContactUsForm />))}</>;
};
If page.content does not contain HTML you do not need react-html-parser. But judging from your screenshot some markup is probably contained.

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();
},
);

Removing inline formats in Quill

I'm having some trouble getting removeFormat to work as it should. Funnily, I thought I had this working a couple days ago; but now when I check it it's not right. It is removing the block-level formatting regardless of where the selection is. A simple example, using the Quill Quickstart with a few modifications:
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
let Block = Quill.import('blots/block');
let Parchment = Quill.import('parchment');
class BlockquoteBlot extends Block { }
BlockquoteBlot.scope = Parchment.Scope.BLOCK;
BlockquoteBlot.blotName = 'blockquote';
BlockquoteBlot.tagName = 'blockquote';
Quill.register(BlockquoteBlot);
let quill = new Quill('#editor');
$('#italic-button').click(function() {
quill.format('italic', true);
});
$('#bold-button').click(function() {
quill.format('bold', true);
});
$('#blockquote-button').click(function() {
quill.format('blockquote', true);
});
$('.cust-clear').click(function(ev) {
var range = quill.getSelection();
quill.removeFormat(range.index, range.length);
});
<link href="https://cdn.quilljs.com/1.0.3/quill.snow.css" rel="stylesheet"/>
<script src="https://cdn.quilljs.com/1.0.3/quill.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!-- Create the toolbar container -->
<div id="toolbar">
<button id="bold-button" class="ql-bold">Bold</button>
<button id="italic-button" class="ql-italic">Italic</button>
<button id="blockquote-button" class="ql-blockquote">Blockquote</button>
<button class="cust-clear" title="Clear Formatting">X</button>
</div>
<!-- Create the editor container -->
<div id="editor">
<p>Hello World!</p>
</div>
In this example, if I apply bold to "Hello" and make the entire line a blockquote, all looks as it should. If I then select "Hello" and click my X button to remove the formatting, it removes the blockquote formatting even though I'm nowhere near a "\n" character. Am I misunderstanding removeFormat, or is there an error in how I've created my BlockquoteBlot? I took that code mostly from the Medium demo on the Quill site, but I found in some cases I needed to specifically define the scope so that the tag would be recognized as block (that may not be necessary for this demo, but I'm including it in case it poses an issue).
The way removeFormat is supposed to work currently does remove all block formats a user highlights, even if it is not across the "\n" character. This makes more sense when the user is selecting multiple lines Issue #649 but perhaps it should not work this way when there is only one line partially selected. I've created a Github Issue to discuss this. Please feel free to chime in.
I am aware that this is an old thread - as an additional to your code in case someone hasn't selected anything - works on Quilljs 1.2.6
$('.cust-clear').click(function(ev) {
var range = quill.getSelection();
if (range.length ===0) {
let leaf, offset = quill.getLeaf(range.index);
quill.removeFormat(range.index - offset, range.index + leaf.domNode.length);
} else {
quill.removeFormat(range.index, range.length);
}
});
This should work
$('.cust-clear').click(function()
{
var range = editor.getSelection();
if (range){
if (range.length > 0) {
editor.removeFormat(range, Quill.sources.USER);
}
}
});

bootstrap selectpicker and cloning

I have been using bootstrap selectpicker where I've added an Add Button for user to replicate the button as much as he wants. Problem is, selectpicker is not working on the second / cloned element and values of dropdown are just showing and not changing on click.
Main Select:
<div id="main_product">
<select name="product[]" class="selectpicker" >
<option value="Tube Lights" >Tube Lights</option>
<option value="Downlights" >Downlights</option>
</select>
</div>
Clone Function:
function clone()
{
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
$cloned.appendTo('#new_products');
// $cloned.find('.bootstrap-select').remove();
// $cloned.find('select').selectpicker();
}
Note that I tried to reassign the selectpicker to the cloned object which is in comments atm, because it dint work also.
Any help would be really appreciated.
I came across this problem but if using the latest version now the select is placed inside the .bootstrap-select element for html5 error handling purposes. The remove() also removes the select, a work around is:
Instead of:
$cloned.find('.bootstrap-select').remove();
Use:
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); });
This will replace the .bootstrap-select element with the select element that it contains inside.
function clone()
{
//you can use :
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
//Or
var $cloned = $('#main_product').clone();
//then use this to solve duplication problem
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); })
$cloned .find('.selectpicker').selectpicker('render');
//Then Append
$cloned.appendTo('#new_products');
}
if we replace the bootstrap-select with the select then the problem is in HTML structure.
so, original element is actual bootstrap-select and cloned are normal select.
take look at following for more clarification.
instead of this
$cloned.find('.bootstrap-select').replaceWith(function() { return $('select', this); });
use
$cloned.selectpicker('refresh');
$cloned.find('.bootstrap-select:odd').remove();
You can please change clone function
function clone() {
var $orginal = $('#main_product');
var $cloned = $orginal.clone();
var $selectPicker = $cloned.find('select');
$cloned
.find('.bootstrap-select').remove().end()
.append($selectPicker).end();
$cloned.find('select').selectpicker();
$cloned.appendTo('#new_products');
}
When using in Form Sets along with other form fields you need to replace and render all the selectpicker mentioned by #cm_mehdi. First find the new form html element and apply the below code:
//Clone new form
let newForm = purchaseForm[0].cloneNode(true);
// Replace and render the cloned html selectpicker
$(newForm).find('.bootstrap-select').replaceWith(function() { return $('select', this); })
$(newForm).find('.selectpicker').selectpicker('render');
// Re-Initialize search in dropdown
$(`#id_form_formfield-${formNum}`).selectpicker({liveSearch: true});

Resources